docs: ce que NextGraph offre vraiment autour des inbox, et ce qui n'est qu'un nom réservé

This commit is contained in:
Sylvain Duchesne
2026-08-20 10:30:31 +02:00
parent fafac38907
commit c507e79f8a
3 changed files with 54 additions and 1 deletions
+2 -1
View File
@@ -2,7 +2,7 @@
type: overview
summary: What an application may rely on from @ng-eventually/polyfill, and what it will have to delete
triggers:
keywords: [polyfill, sdk, surface, contract, publish, published, entry, export, migration, unlearn, consumer, app-facing]
keywords: [polyfill, sdk, surface, contract, publish, published, entry, export, migration, unlearn, consumer, app-facing, inbox, deposit]
paths:
- "packages/polyfill/src/index.ts"
- "packages/polyfill/src/surface/**"
@@ -36,3 +36,4 @@ This repo is the **provider** of `polyfill-surface`; consuming applications live
- `polyfill-surface/contract_polyfill-surface` — the engagement itself, written from the caller's point of view.
- `rule_no-divergence-from-nextgraph` — the test that decides what may be built at all.
- `knowledge_what-an-app-deletes-at-migration` — the two fates a published symbol can have.
- `knowledge_what-an-inbox-is-upstream` — what the target actually offers around inboxes, and where a choice of ours begins.
@@ -0,0 +1,47 @@
---
type: knowledge
summary: What NextGraph actually offers around inboxes — a document may be given one, nothing above the engine exposes them, and most message variants are reserved names carrying nothing
last_checked: 2026-08-17
---
# What an inbox is upstream
Read at the source in `nextgraph-rs`. Every claim here points at a symbol rather than a line, so it stays verifiable by `git grep` after the code moves.
## A document may have an inbox, and must be given one
`Repo` upstream **is** a document — not a store. Its `inbox` field is an `Option<PrivKey>`: at most one inbox per document, and a document has none until something creates it. The creating act is an **`AddInboxCap` commit**, verified and applied like any other — see `update_inbox_cap_v0` in the verifier and the `CommitVerifier for AddInboxCap` impl.
The automatic creation inside `complete_site_store` is narrower than it looks: it gives an inbox to the site's **non-private stores** only. Reading that as *"only stores can have inboxes"* is a mistake this project has now made twice — the site path is one caller of a general capability, not the rule.
So an inbox on an ordinary document is **aligned with the target**, not an emulation of something absent.
## Nothing above the engine exposes any of it
Run the cascade for *"post, observe, or process an inbox message"* and every level above the engine is silent. The ORM has no occurrence of `inbox` at all; neither the web API nor the wasm binding exposes one — the binding's single mention is an unused import — and there is no generic `app_request` escape hatch, since every command carries its own dedicated wrapper.
Anything built here therefore aligns on the **engine's model**, because levels 2 and 3 answer nothing.
## Most message variants are reserved names, not shapes
`InboxMsgContent` declares eight variants. `ContactDetails` and `SocialQuery` carry data and have processors. **`Link`, `Patch`, `ServiceRequest`, `ExtRequest`, `RemoteQuery` and `DialogRequest` carry nothing at all** — they are bare unit variants, so they cannot hold a reference, name a servicer, or describe an operation.
That distinction decides how much they bind us, and it is why the provenance taxonomy in [[rule_no-divergence-from-nextgraph]] separates a reserved name from a declared shape.
## Processing is closed, and so is posting
`process_inbox` is `pub(crate)`, its match ends in `NotImplemented`, and its only caller is `Verifier::inbox`. There is no trait, registry or hook: **nothing outside the engine can contribute a processor** for a message type.
Posting is no better wired. `AppRequestCommandV0::InboxPost` is declared with its constructor and has **no handler arm** — the identifier appears nowhere else in the tree.
## A message waits for its recipient, and there is no delegate
An inbox's readers are `UserId`s, and messages queue until that user comes online. The only *"someone else runs it"* notion upstream is `Headless(Credentials)` — a server-side verifier holding a **user's own** credentials, which is that user running elsewhere, not a service delegated over someone's inbox.
## What follows for us
The **container** is settled by the target: a document may be given an inbox, one at most. Build on that freely.
The **content** of a deposit and **who processes it, when** have no upstream answer — the variants that would carry them are empty, and the processor set admits nothing from outside. So these are ours to choose, and a choice here is a bet that must be named as one rather than presented as alignment.
The standing choice, deliberate and provisional: **a deposit is a reference to a document**. It is enough for what is built on it today, and it is expected to evolve — most plausibly toward whatever shape the target eventually gives its own variants.
@@ -39,9 +39,14 @@ Answering *does the target do this?* once, in conversation, does not survive the
| `passthrough` | upstream's own symbol, same name and shape |
| `aligned` | our name or ergonomics, upstream's semantics and model |
| `declared-not-wired` | upstream **defines** the shape; nothing constructs it yet; we implement it |
| `reserved-name` | upstream has declared a NAME with no shape behind it — it binds nothing, so building on it is a bet |
| `invention` | nothing at any level answers — a deliberate, documented bet |
| `divergent` | upstream does this, and we do it differently |
A test requires the annotation on every published symbol; it cannot check that the citation is *true*, and that is fine — a citation is refutable by a reader in seconds, prose is not. The classification also applies to unpublished code that **claims to resemble the target**: the mistake this section exists for was made on an internal function, and being unexported protected nobody.
**A reserved name is not a declared shape, and the gap between them is the whole point.** `declared-not-wired` is strong *because the shape constrains us*: implementing it is realising something the target wrote down. A name with nothing behind it constrains nothing, so what we build under it is our own invention wearing the target's vocabulary — the most dangerous kind, because the borrowed name makes it read as alignment.
The tell is mechanical: **open the declaration and look for a payload.** Several of `InboxMsgContent`'s variants — `Link`, `Patch`, `ServiceRequest`, `ExtRequest`, `RemoteQuery`, `DialogRequest` — are bare unit variants carrying nothing at all, while `ContactDetails` and `SocialQuery` carry data and have processors. Presenting the first group as *"the strongest thing to align on"* was an error made here, in the same breath as citing this rule; see [[knowledge_what-an-inbox-is-upstream]] for what those variants do and do not offer.
**Why this exists.** A maintainer read that `AddLinkV0` is defined upstream but constructed nowhere, and reported our implementation of it as our own invention — reasoning from the absence while quoting, in the same session, the rule against exactly that. The correct reading was `declared-not-wired`, which is the best alignment available. Prose said otherwise elsewhere too: `docs/api-contract.md` claimed `docs.*` was a 1:1 passthrough while two of its signatures diverged, and stated a member count that was simply wrong. Both errors are the same one — a claim about the target that nothing forced anyone to check.