diff --git a/.project/concepts/app-contract/_overview.md b/.project/concepts/app-contract/_overview.md index 378c7b1..bb2f697 100644 --- a/.project/concepts/app-contract/_overview.md +++ b/.project/concepts/app-contract/_overview.md @@ -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. diff --git a/.project/concepts/app-contract/knowledge_what-an-inbox-is-upstream.md b/.project/concepts/app-contract/knowledge_what-an-inbox-is-upstream.md new file mode 100644 index 0000000..5291324 --- /dev/null +++ b/.project/concepts/app-contract/knowledge_what-an-inbox-is-upstream.md @@ -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`: 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. diff --git a/.project/concepts/app-contract/rule_no-divergence-from-nextgraph.md b/.project/concepts/app-contract/rule_no-divergence-from-nextgraph.md index 382afde..411af5c 100644 --- a/.project/concepts/app-contract/rule_no-divergence-from-nextgraph.md +++ b/.project/concepts/app-contract/rule_no-divergence-from-nextgraph.md @@ -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.