Dawn's onset-attribution finding (RESEARCH/BB_PUBLIC_REDIS_ONSET_ATTRIBUTION.md):
onset moves 18:00Z -> 16:40Z (15-min bins blurred it); attributed to organic
community creation (58 -> 425 in 2h, same 5-min bin as the Redis breakout,
flat subs-per-connection ratio excludes a code change). Community creation is
now linear (~3,100/day) but Redis load stays exponential — intensification
within communities — so the watch metric is channels-per-community. Adds the
four-account cacheclusterid Datadog trap (must scope aws_account:433851229429).
No planning number changes: 3.45d doubling and 5.2-9.0d band stand.
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Dawn's residual on 5baf0a10a: the inline derivation used 1.33x, above
the doc's own cited ~25-30% single-thread range — a kickoff eyeball
rounded up past its ceiling and presented as derived. Restated as the
vendor range 1.25-1.30x -> 1.1-1.3d, explicitly unbenchmarked. Wall
sentence takes the optimistic edge (+1.3d) and restacks: M = 1.882 x
1.30 x 1.111 = 2.718 -> ~5.0d (interaction-aware 2.633 -> 4.8d).
Conclusion unchanged: still ~4x short of the 21d likely migration.
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Dawn's delta review at 3292b35db caught the doc carrying two runways:
the problem statement still held kickoff's eyeballed ~3.5-4d doubling /
8-10d saturation, while the summary row read the fitted ~7d-to-80% —
and the eyeball figure was fitted across exactly the Jul-21-18:00Z
regime boundary the adjacent fit-window bullet forbids.
Problem statement now leads with the fitted post-onset clock (~3.4-3.5d
doubling, ~7d to 80%, r2=0.92) and demotes the kickoff figures to a
declared pre-onset eyeball. Same treatment for the ~150x/~400x family
growth (fitted: x9.6-12.4 over 9 days). G0's gate description (line
~595) keeps its original tasking text deliberately.
Model and configs untouched; doc only.
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Dawn's P3 replica-subtraction measurement (2026-07-30, receipts in
RESEARCH/BB_PUBLIC_REDIS_P3_ENGINE_CPU_DECOMPOSITION.md) resolves the
open <25%-decomposition caveat and lands three spec updates:
- D1 gets its gated input: ~40% of primary engine CPU is write-apply +
publish propagation (the never-read replica carries 7.8pp of 19.5pp);
~60% is client-facing and divides only as connections/subscriptions
redistribute. Notes the A4 dependency (propagation confines to the
owning shard only after P2) plus two measurement cautions: the
pub/sub-vs-keyed regression split is unidentifiable (r=0.997), and
the apparent sublinearity is a ~1.1% fixed baseline.
- Fallback row corrected: the replica is not idle — 7.8% is its own
unsheddable load on a ~3.8d doubling, saturating alone in ~14 days.
Kickoff's 'idles at 7%' framing removed.
- Problem statement gains the measured fit window: sharp onset
2026-07-21 ~18:00Z; fits must start post-onset; curr_connections
moves in pool-config plateaus, not demand. Onset unattributed
pending deploy history.
Summary timeline row updated to the fitted ~7d-to-80% with the
degraded-fallback note. Model and configs untouched; doc only.
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Dawn's G0 profile and P2 measurements (2026-07-30, receipts in
RESEARCH/BB_PUBLIC_REDIS_G0_PROFILE.md and in-thread) landed three
corrections to spec claims:
- "per-topic slot spreading" overstated the spread unit: 87.8% of publish
volume targets buzz:<community>:global (presence 60.8% + observer frames
27.0%), so the effective unit is per-community. Simulated hottest-shard
imbalance ~1.5x fair share at 4 shards (p95) — D1 must not size by
current/N.
- P2 payoff is bounded by measured amplification (~5.3 pod-deliveries per
publish on 15 pods), not worst-case broadcast.
- D2 is now measured-closed (all pattern publishers lifecycle/admin-rate,
~10x margin), and D1 records its first inputs plus the honest caveat that
per-family rate x latency explains under a quarter of engine CPU.
Also replaces the stale ~400x figure with the fitted 9-day x12.4 and the
rising per-op cost (1.18x/9d) as the P2 motivation.
Model and configs untouched; doc only.
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
The drain-reachability probe (M10) guaranteed nothing mechanically: it was
a documented procedure a reviewer had to remember to run. Deleting
MigrateB leaves all five safety invariants green, so nothing in CI would
have caught the round-1 bug coming back.
scripts/check-tla-specs.sh runs both configs with opposite pass
conditions: the safety model must complete with no violation, and the
drain probe must report `Invariant Probe_NotC is violated` — a green drain
run means phase C is unreachable from the stall state and fails the build.
The inverted assertion greps the specific invariant name rather than
checking TLC's exit status: a parse error or missing module also exits
non-zero, and treating that as the expected violation would make the check
useless exactly when the spec is broken. tla2tools is pinned by release
tag and by sha256, since release assets can be re-uploaded.
Verified in four directions on the committed script: unmodified specs exit
0; MigrateB stripped from DrainNext exits 1 via the green-drain branch;
a spec that fails to parse exits 1 via the neither-verdict branch; a jar
with the wrong checksum exits 1 before TLC runs. Runtime is ~3s including
the jar download.
The module allow-list is deliberately just RedisClusterFencingMigration,
not a glob over docs/spec/*.cfg: MultiTenantRelay does not finish in a
reasonable CI budget and GitOnObjectStore explores ~12.4M states, so
attaching them to every PR would buy flakiness rather than safety.
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Review round 2 (Dawn) found the safety model could not distinguish the
B2 fix from its absence: unguarded ExpireOld lets the C-gate be
discharged by luck, so deleting MigrateB left all invariants green.
Added DrainSpec/Probe_NotC — TLC from the stall state (full-B fleet, live
old-key lease, no spontaneous expiry) with inverted verdict semantics:
violation proves C reachable (93/44); mutant M10 (MigrateB removed =
round-1 behavior) stays green, making the round-1 bug permanently
visible. Main safety model unchanged (12636/3637, M1-M9 all killed).
Also per Dawn N1: rollback matrix now names B->A as dropping every
migrated session (A renews the old key; nobody renews the migrated
lease; renewer treats Lost as fatal) — liveness cost, prefer forward-fix.
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Buzz renders one card per `kind:30617`, so a project spanning several
repositories has no representation — the relay, desktop app, and mobile
app look like three unrelated things. This adds the spec for the
container event that fixes that, plus the two shared fixture files that
make it machine-checkable. Docs only; no code changes.
Membership cannot live in the repository announcements themselves. A
project spanning Alice's and Bob's repositories would need *both* of
them to publish a tag naming the group, and Alice cannot sign for Bob's
key. A project's own name, description, and channel binding likewise
have no single writer when scattered across per-repository tags, and no
deletion story. That is why multi-repo grouping is the one forge concept
in Buzz that warrants a custom kind.
## `docs/nips/NIP-MP.md`
`kind:30621`, an addressable event per NIP-01, addressed by `(pubkey,
30621, d)`. Members are `a` tags holding canonical
`30617:<lowercase-64-hex-owner>:<repo-d>` coordinates, following
NIP-01's 2-or-3-element grammar where the optional third element is a
relay hint clients MAY use and whose content ingest does not parse.
Metadata is `name`, `description`, `buzz-channel`, `buzz-visibility`.
- **Authority stops at the container.** The signer can replace their own
project and nothing else — no edit, delete, push, or admin over any
member. Deletion additionally admits the signer's registered NIP-OA
owner, because `validate_standard_deletion_event`
(`crates/buzz-relay/src/handlers/side_effects.rs`) grants that
platform-wide so a human can clean up events published by an agent they
own; the spec documents it as a Buzz extension to NIP-09 rather than
carving `kind:30621` out of it. `buzz-channel` on a project is metadata
only; git push policy reads the repository's own `kind:30617`
(`crates/buzz-relay/src/api/git/policy.rs`) and a project never becomes
an input to it.
- **Ingest validation contract**, with named rules the fixtures
reference: `d-cardinality`, `d-empty`, `member-cap` (64, counting every
`a` tag), `member-tag-arity`, `member-coordinate-malformed`,
`member-duplicate`, `metadata-cardinality`, `metadata-length`. Arity is
its own rule rather than part of coordinate parsing, because a
four-element member tag can carry a valid coordinate — the tag's shape
is what is wrong, and ignoring elements past the relay hint would admit
unvalidated data no consumer reads. Duplicates are rejected rather than
normalized — a relay cannot rewrite tags inside a signed event without
invalidating its id and signature.
- **Metadata interpretation is normative, not left to the reader.**
Ingest bounds cardinality and length and interprets nothing; clients
resolve absent `name` to the `d` value, any unrecognized
`buzz-visibility` token to `listed` (a typo is not a privacy signal),
and an unresolvable `buzz-channel` to a project rendered without a
channel rather than dropped. `content` carries no meaning: writers
SHOULD emit `""`, and readers and relays MUST ignore any value rather
than reject it.
- **Claim authority.** A project suppresses a member's standalone card
only when it is listing eligible *and* its signer is that repository's
owner or appears in the repository's own `maintainers` tag. Without
this, anyone could publish a project naming your repository and pull it
out of the collection into a container you never consented to. An
unauthorized project still renders, and still renders its members — it
just cannot remove a repository from where its owner expects to find it.
- **Deterministic client fold**, seven steps, with a table of required
cases: exhaustive enumeration (a fixed `limit: 200` makes repository 201
vanish), multiple membership, fallback to a standalone card,
unresolvable members marked unavailable rather than dropped, and local
hide of a container never hiding repositories. On a relay that provides
no exhaustive mode, the conformant behavior is a persistently marked
possibly-incomplete collection — not a violation of the enumeration
requirement.
- **Pagination is specified in two modes**, because exhaustive
enumeration is not universally achievable. Both modes share an explicit
three-condition relay contract: a relay must (1) apply the complete
filter before enforcing any limit, (2) expose the exact effective page
limit it enforces, and (3) saturate pages — return `min(effective limit,
remaining matches)`, so a short page proves all remaining matches were
returned. A relay satisfying any proper subset does not provide the
guarantee, and absent it a client MUST mark the collection possibly
incomplete. On a relay exposing a composite `(created_at, event id)`
keyset cursor — Buzz does on its authenticated HTTP bridge endpoint, via
`until` + `before_id`; the NIP-01 websocket REQ path silently discards
`before_id`, so a websocket client against Buzz is in mode 2 — clients
MUST page by it; within the relay contract the cursor's uniqueness means
no skips or re-reads and a short page is an unambiguous end signal, but
cursor uniqueness alone does not substitute for the relay contract. A
vanilla NIP-01 filter has no id tiebreak, so `until` alone either skips
a second's unread events or never advances; there a client MUST drain
the boundary second explicitly. The spec also adds normative guidance on
query shapes: a client MUST use only query shapes the relay applies
completely before limiting, and where a needed constraint (such as `#a`)
is post-applied, MUST widen to a pushable shape and match the rest
client-side.
- **Kind allocation** recorded with the checks performed: `30621` is
unassigned in the upstream nostr NIPs kind table and has no
nostrbook.dev entry, and it is the one free number between `30620` and
`30622` locally.
## `docs/nips/NIP-MP.fixtures.json`
The ingest contract: 31 cases — 11 accept, 20 reject — as unsigned
templates consumers sign with their own test key. Coverage includes
minimal and full projects, zero members, the 64-member boundary from
both sides, cross-owner and same-`d`-different-owner members,
colon-bearing repository `d` values, relay hints, non-empty `content`,
and every rejection rule. Each of the two 256-byte `buzz-` bounds gets
its own reject case so neither can hide behind the other's rejection,
and duplicate detection is pinned to the coordinate alone by a case
whose two identical coordinates carry different relay hints. A
four-element member tag carrying an otherwise valid coordinate pins
arity separately from coordinate parsing. Every rejection case names the
rules that may fire, so an implementation cannot pass by rejecting a bad
event for an unrelated reason.
## `docs/nips/NIP-MP.fold-fixtures.json`
The fold oracle: 12 cases covering every row of the required-fold-cases
table, including the discriminating case where one authorized and one
unauthorized project list the same repository — an implementation that
requires every listing project to be authorized emits a spurious
implicit card, and one that lets any listing project suppress drops a
card it owes the owner.
Inputs are semantic rather than signed envelopes: a repository or
project is named by its coordinate plus only what the fold reads —
signer, members, `maintainers`, visibility, viewer-hidden, deletion.
Every collection in `expect` is compared as a set, including each
container's `members`, since the fold fixes placement and not order.
Signing would re-test the ingest contract and obscure what is under
test. The fold is where claim authority lives, so without a shared
oracle two clients could each satisfy the prose and still render
different collections from identical heads.
## `VISION_PROJECTS.md`
Line 41's "zero custom kinds" now reads "no custom kind for the repo
itself", with a new "One Project, Many Repos" section recording why the
one exception is warranted. `30621` rows added to the kind and status
tables.
Related: #3171 (the `KIND_PROJECT` constant, relay ingest validation of
this contract, and the inclusive `created_at <= tombstone` bound this
spec's coordinate-deletion rule cites). Independent — either can merge
first.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Specifies the bb-public relay's migration from single-shard cluster-mode-
disabled Valkey to a sharded cluster, in the house style of
docs/git-on-object-storage.md and docs/multi-tenant-relay.md.
The mechanized core is the fenced session directory key migration: the
tunnel lease/generation pair must be renamed to share a hash slot, which
is a live handoff of fencing authority under a rolling deploy. The TLA+
model (docs/spec/RedisClusterFencingMigration.tla) proves single-authority,
generation-monotonicity, and one-way-door safety across the O->A->B->
backfill->C phase protocol; all five invariants are mutation-tested
non-vacuous (5 mutants, each trips its intended invariant).
The rest is gated engineering: split client architecture (deadpool cluster
pool + separate RESP3 push_sender subscription client), sharded pub/sub as
a sizing prerequisite (classic PUBLISH broadcasts to every node), the
silently-partial SCAN in mesh discovery replaced by a scored-expiry index,
and the staged ElastiCache mode change with cluster-mode=enabled named as
irreversible.
Inputs: Wren's deployed-source inventory and Dawn's migration-mechanics
digs (channel buzz-redis-cluster-mode, thread 3440fed6).
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
## What
Adds `docs/linux-rendering-troubleshooting.md` — the user-facing
troubleshooting page for Linux rendering failures.
## What's in the doc
**Crash: `colrv1_configure_skpaint` assertion abort (AppImage, Fedora
40+)**
Root cause: the AppImage bundles WebKitGTK compiled against FreeType
2.11.1, but `libfreetype.so.6` is not bundled — WebKit loads the host's
FreeType at runtime. FreeType 2.13.0 added a field to
`FT_ColorStopIterator` (16 → 20 bytes); on hosts with FreeType ≥ 2.13
the struct-layout mismatch corrupts Skia's COLRv1 color-stop arithmetic,
causing the assertion abort. Fix: upgrade to v0.5.2+ (build container
bumped to `ubuntu:24.04` in
[#3602](https://github.com/block/buzz/pull/3602)). Includes the glibc
floor table (2.35 → 2.39) and `.deb`/`.rpm` guidance for Ubuntu 22.04 /
Debian 12 users. A manual fontconfig workaround is preserved for users
stuck on older AppImages.
**Blank window / dmabuf renderer (NVIDIA, AppImage)**
Covers the auto-fix shipped in v0.5.1
([#3271](https://github.com/block/buzz/pull/3271)) and the
`--safe-rendering` flag for cases where auto-detection misses.
**AMD RDNA4 / transparent window
([#2643](https://github.com/block/buzz/issues/2643))**
Documents the three-variable workaround verified by the reporter
(`GDK_BACKEND=x11`, `WEBKIT_DISABLE_DMABUF_RENDERER=1`,
`WEBKIT_SKIA_ENABLE_CPU_RENDERING=1`).
Also includes a crash-log capture recipe and issue-filing checklist.
Context: [#2548](https://github.com/block/buzz/issues/2548),
[#2982](https://github.com/block/buzz/issues/2982),
[#2643](https://github.com/block/buzz/issues/2643),
[#2338](https://github.com/block/buzz/issues/2338).
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Kind 30175 persona sync events carry plaintext `system_prompt` and
`respond_to_allowlist`. This PR adds **author-only-unless-shared read
semantics**: events without `["shared","true"]` are visible only to the
author; events with that tag are community-readable.
## What changed
### New read class (kind 30175)
Kind 30175 gets per-event gating at every relay read surface. The
`shared` marker is a **tag**, not a content field, so content bytes
(which double as the `source_version` drift basis) are not affected when
toggling share state.
### `event_visible_to_reader` helper (`handlers/req.rs`)
Centralizes the three per-event access predicates —
`is_author_only_event`, `is_unshared_persona_event`,
`reader_authorized_for_event` — into one `pub(crate)` fn callable from
both WS and HTTP adapters. All result-visibility sites now call this
single helper.
### NIP-98 HTTP bridge (`api/bridge.rs`)
- `POST /query` catchall: replaced the two-step author-only +
result-gated checks with `event_visible_to_reader` (now also covers the
persona shared-gate).
- `POST /count`: added `needs_persona_filtering` to the fast-path guard
(forces per-event fallback when filter can match `kind:30175`) and
replaced both fallback loops' individual checks with
`event_visible_to_reader`.
- FTS `/search` bridge helper: replaced `is_author_only_event` with
`event_visible_to_reader` as defense-in-depth (30175 is not in the FTS
allowlist today; comment at site explains the future-proofing intent).
### Ingest validation (`handlers/ingest.rs`)
`validate_persona_envelope` rejects malformed `shared` tags: wrong
value, missing value, duplicates. Accepts exactly `["shared","true"]`
and tag-absent.
### Kind helpers (`buzz-core/src/kind.rs`)
`is_persona_shared_kind`, `is_unshared_persona_event`,
`filter_can_match_persona_shared_kinds`.
### Tests (`e2e_persona.rs`)
8 unit tests in `kind.rs`, 6 in `ingest.rs`, 8 e2e tests total:
- AC-1–6 covering the gated surfaces
- `test_persona_live_fanout_shared_gate`: reworked with explicit
monotonic `created_at` timestamps (t0 < t1 < t2) and per-step head
assertions, eliminating the NIP-33 event-id tie-break race. Also asserts
foreign live subscription receives nothing on shared→unshared
transition.
- `test_persona_ingest_shared_tag_validation`: added `shared=x` and
missing-value wire-level rejection cases.
- `test_persona_mixed_kind_filter_does_not_leak`: publishes a kind-9
event and asserts it IS returned; absence-only assertion no longer
sufficient.
- `test_persona_http_query_cross_author_gate`: NIP-98 `/query`
cross-author gate (authors filter, kindless `ids` — both blocked; shared
`ids` — passes).
- `test_persona_http_count_cross_author_gate`: NIP-98 `/count`
cross-author gate (foreign sees 1/shared, author sees all, wildcard
checked).
### NIP-AP.md
Replaced aspirational "every relay read chokepoint" wording with an
enumerated list of gated surfaces including NIP-98 `/query`, `/count`,
and FTS/search with their enforcement mechanism named. Added
**Non-goal** note for side-band existence oracles
(reaction/report/deletion target resolution).
## Existing tests
All pre-existing `e2e_persona` tests use `{ids:[event_id]}` or
`{authors:[self]}` filters — author self-reads bypass the gate and are
unaffected.
## Gates
`just check` ✅ | `just test-unit` ✅ | `cargo test -p buzz-relay` ✅ (749
passed, 1 pre-existing failure in
`demo_join_forwarded_arm_round_trips_echo` — flaky on `main`, unrelated
to this PR, verified red at `origin/main` before this branch)
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary
- Replace all references to a specific corporate VPN product name with
generic "VPN" / "corporate VPN" / "VPN tunnel" / "VPN CLI" language
across 15 files (21 lines)
- Comment-only and doc-only changes — zero functional impact
- Keeps the OSS repo free of vendor-specific assumptions
`rg -i warp` returns zero hits after this change (excluding
`node_modules`, lockfiles).