feat: knowledge receipt event kinds — a machine-readable record that room events were derived into an external store
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
**Motivation**
Rooms produce durable knowledge, and that output increasingly lands in stores *outside* the relay — a knowledge graph, an archive, a wiki, a git repo. Today the only way a service can tell the room "an artifact derived from these messages now exists over there" is a kind:9 chat message with structured text inside. That works — we run it in production — but it is invisible to the protocol: clients can't render or filter it, nothing can subscribe to "knowledge receipts in this channel", search can't treat it as a distinct thing, and third-party NIP-29 clients see opaque bot text.
This is the situation Buzz's own architecture principle addresses — *"Event kinds are the only switch. … Adding a new feature means defining a new kind."* (CONTRIBUTING.md, Architecture Overview.) And because the relay deliberately rejects unregistered kinds, an integration cannot solve it from the outside: the registry is the only door.
Motivating consumer: a shipping integration that derives pinned Buzz threads into an external knowledge store and posts receipts back as kind:9 (https://github.com/OriginTrail/buzz-dkg-integration). The kinds proposed here are provider-neutral — `store` is a free string, and the same two kinds serve an IPFS pinner, a git archiver, or a wiki exporter identically.
**Proposed solution**
Two regular (immutable) channel-scoped kinds. Numbers are a concrete suggestion; final placement is maintainers' call (same posture as #2451).
*Suggested numbers:* the registry allocates thousand-blocks per domain (43xxx jobs, 45xxx forum, 46xxx workflow, 47xxx reserved user-groups, 48xxx system, 49xxx media — `crates/buzz-core/src/kind.rs`); the next free block is 50000–50999 (nothing ≥50000 in `ALL_KINDS`; fits the `u16` the nostr crate requires). Regular semantics on purpose: receipts are append-only facts.
***Kind 50001 — knowledge receipt*** — "an artifact derived from these room events now exists in an external store."
Tags: `h` = channel (required — receipts inherit channel membership visibility and the fan-out scoping invariant); `e` = source event id(s), ≥1.
Content (JSON):
| field | required | meaning |
|---|---|---|
| `artifact_uri` | yes | resolvable identifier of the artifact in the external store |
| `store` | yes | free-form store discriminator (e.g. `ipfs`, `git`, `wiki`, a knowledge-graph name) |
| `artifact_kind` | no | what the artifact is (e.g. `decision-summary`) |
| `content_hash` | no | `sha256:` over the canonical source set, for independent re-derivation |
| `title` | no | one human-readable line for rendering/search |
***Kind 50002 — knowledge promotion receipt*** — "that artifact was finalized/anchored." Same tags, plus one `e` referencing the kind:50001 it promotes. Content adds `anchor` (free-form object, e.g. `{"system":"…","tx":"…"}`) and `permanence` (`finalized` | `anchored`).
*Trust semantics (explicit):* a receipt is a **signed claim by the posting member**, not a relay-verified fact. The relay validates shape and channel scope only; it never fetches `artifact_uri` and cannot verify that the external artifact exists or matches. Verification is the consumer's job, via `artifact_uri` and `content_hash`. That is the honest ceiling for external stores — and precisely the line that separates this from #2451, whose in-relay artifacts the relay *can* validate.
*Privacy:* payloads carry pointers and hashes only — never the artifact body or credentials. A receipt necessarily reveals to channel members that content was exported to an external store; posting one is the service's declaration of that fact, which is a feature (today the same export happens with no protocol-visible trace). Whether receipts should be text-searchable is an operator/maintainer policy call — see search note below.
*Relay wiring (per CONTRIBUTING § How to Add a New Event Kind):* scope `Scope::MessagesWrite` (same class as stream messages/pins); no side effects beyond storage and fan-out in v1; standard events table, no side table. *Search:* FTS inclusion is **not** a uniform one-line change across installations — fresh databases get the positive allowlist from migration `0008` (empty-DB conditional), populated databases retain their earlier expression until an operator runs `scripts/maintenance/nip_rs_search_allowlist.sql`, and later migrations wrap whichever expression is installed (see `0014`). Simplest v1: leave receipts out of FTS entirely and let `title` rendering carry discoverability; adding them to search later is an ordinary follow-up in that same framework.
**Alternatives considered**
- *Structured kind:9 content* (status quo): zero changes, but invisible to kind-level filtering/rendering/search; every consumer writes a bespoke parser. We've run this to its ceiling.
- *Tags on kind:9*: lighter, but still not subscribable or renderable as a type, and pollutes chat semantics.
- *Unregistered custom kinds*: not possible — the relay rejects unknown kinds at ingest, by design.
- *In-relay knowledge artifacts (#2451)*: closest prior work and **complementary, not competing** — #2451's proposal/review artifacts live inside Buzz where the relay can validate them; this covers artifacts living in external stores, where a signed pointer + hash is the honest primitive. They compose: a crystallized #2451 artifact exported anywhere could emit a 50001, and its approved export a 50002.
**Additional context**
- Closest existing issues (searched 2026-07-29; none a duplicate): #2451 (complementary, above), #1702 (thread sealing), #242 (Human Signal Layer), #2433 (knowledge agent), #3261 (precedent for a small structured kind family). Room-knowledge is a live gap; this is its narrow external-store slice.
- Offer: prepared to implement the full nine-step path (constants, payload types, scope arm, tests in `buzz-core` + `buzz-test-client`, docs) with DCO sign-off and Conventional Commits once maintainers indicate direction and placement. Opened as an issue first per CONTRIBUTING; happy to reshape as a `docs/nips/` draft alongside the existing in-repo NIP drafts if preferred.
**Questions for maintainers**
1. Does an external-artifact *receipt* primitive belong in the event registry, alongside (not instead of) the in-relay direction #2451 sketches?
2. One kind with a `status` field, or two kinds (receipt / promotion) as proposed?
3. Should receipts be in FTS at all, given the mixed allowlist/exclusion install base?
4. Issue-first is per CONTRIBUTING — would you rather see this as a `docs/nips/` draft before any implementation?
Contributor guide
Research direction
Wait for maintainer decisions on kind placement, one versus two kinds, search behavior, and whether to draft docs first. If approved, start with crates/buzz-core/src/kind.rs and CONTRIBUTING.md's event-kind instructions, then trace the scope and validation paths and inspect existing tests in buzz-core and buzz-test-client. Done means the agreed kinds are registered, scoped, validated, documented, and covered by tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100