6 Commits
Author SHA1 Message Date
Will PflegerandGitHub 33bf7caa6e docs(nips): specify kind:30621 multi-repo projects (NIP-MP) (#3163)
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>
2026-07-30 11:28:49 -04:00
2ecdcce7bd Multi-tenant relay: spec + mechanized formal proof (S1–S8) (#1285)
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co>
2026-06-26 11:15:59 -04:00
1bb8b8d547 docs: clean up remaining Buzz references (#977)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
2026-06-11 10:22:14 -07:00
tlongwell-blockandGitHub 86f4c5d9a9 docs: fix stale claims, remove counts, purge LiveKit references (#742)
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-05-24 13:09:34 -04:00
WesandGitHub e0fdeb2ff5 [codex] clean up root docs and assets (#445) 2026-05-01 15:58:06 +00:00
tlongwell-blockandGitHub 0df883c490 Sovereign Sprout (#403) 2026-04-28 11:09:39 -04:00