Files
ng-eventually/docs/api-contract.md
T
Sylvain Duchesne 43aadbeb45 docs: chaque symbole dit d'où il vient
98 annotations posées à côté des déclarations, et un test qui les exige sur la
surface publiée. Elles portent trois choses : le niveau qui répond, la référence
amont, et la catégorie parmi les cinq.

La cinquième est celle qui manquait : declared-not-wired, quand la cible DÉFINIT
la forme et ne la câble pas. Neuf symboles en relèvent, dont readLinks — que
j'avais classé « notre invention » en raisonnant depuis l'absence, alors que
c'est le meilleur alignement disponible.

Les références citent un SYMBOLE, jamais une ligne : trois citations du document
avaient déjà pourri. Cinq corrections au passage, toutes vérifiées à la source —
un chemin ORM qui n'existe pas, deux plages de lignes fausses, et surtout
docs.* et subscribeDoc étiquetés PASSTHROUGH alors qu'ils sont alignés : nos
noms, plus un argument jamais transmis. La sémantique survit à la migration,
les sites d'appel non, et la nuance disparaissait sous une étiquette trop
flatteuse.

Le test échoue à l'annotation retirée, à la catégorie mal orthographiée, et à
une invention qui prétendrait citer une référence — vérifié en cassant les
trois. Il a aussi attrapé un défaut en lui-même : le gabarit de format placé
dans index.ts se faisait analyser comme une annotation.

La classification couvre l'interne qui prétend ressembler à la cible — tout
emulated-verifier — et exclut ce qui ne le prétend pas. La faute d'origine
portait sur une fonction non exportée ; n'être pas publié n'a protégé personne.

Quatre symboles ont résisté et sont annotés avec leur catégorie dominante, la
seconde nommée dans la note plutôt que lissée.
2026-08-16 22:53:50 +02:00

729 lines
64 KiB
Markdown

# API contract — what `@ng-eventually/polyfill` exposes today, and what the future SDK should expose per subject
> **Updated 2026-08-03, after the source layout was reorganised by migration fate** (`docs/source-layout-by-fate.md`). Paths, and three names, changed under this document: `readModel` became the directly-exported `readUnion`; `accounts` / `AccountRecord` / `AccountStorage` became `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage` (module `shared-wallet/virtual-users.ts`); `store-registry-api.ts` became `surface/placement.ts`. Two modules were created and are covered here: `emulated-verifier/branch-registers.ts` (the four durable registers, split out of the shim) and `shared-wallet/bootstrap.ts` (the injection store, split out of the `/polyfill` entry). The subject-by-subject rulings below are unaffected — what moved is where the code lives, not what it promises.
**Scope: the APP-FACING contract only.** Everything reachable from the published entry point, and nothing else. The library's internal modules — the shim machinery, the read paths, the boundary guards — are held to the same standard (as close as possible to what NextGraph does or plans) but have their own document, `docs/internal-contract.md`: a consumer never reads that one, a maintainer does. This split was made on 2026-08-03, together with the export change described in § 15.
**Scope.** The real exported surface of `@ng-eventually/polyfill` (verified against the `export` statements in `packages/polyfill/src/index.ts``package.json` maps exactly one entry point, `.`), and, for each subject, the target signature the future NextGraph JS SDK is expected to expose. Written 2026-08-03, verified against the `nextgraph-rs` clone (HEAD `213338f6`, 2026-05-16) and the installed `@ng-org/web@0.1.2-alpha.13` type declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
**How to read the epistemic labels.** Every target-side claim carries one of:
- **PASSTHROUGH (level 3 / level 2, VERIFIED)** — the target function exists today; the lib forwards to it. Citation into `nextgraph-rs` or the installed `.d.ts`. Level numbers per `README.md` § *The three references*: 3 = JS ORM (`sdk/js/orm`), 2 = wasm binding / `@ng-org/web` (`sdk/js/lib-wasm`, `sdk/js/web`), 1 = Rust engine (`engine/`).
- **LEVEL-1 SHAPE (model VERIFIED, JS surface ASSUMED)** — the engine's model constrains the shape and is cited, but **no JS surface exists at any level**, so the signature offered here is this library's invention. The future SDK's name and parameter order for it are unknown.
- **ASSUMPTION** — nothing at any layer constrains this; the bet and what bounds it are stated.
- **NO COUNTERPART** — the subject has no image in the target at any layer, usually because it is shared-wallet machinery that disappears at migration. That is a finding about the emulation, not a gap in the target.
Per the design principle (`README.md` § *Design principle*): an absent implementation is never treated as evidence about the future — "the engine does not do X" and "the SDK will not offer X" are kept apart throughout.
### The per-symbol annotations are the authority; this document is the reasoning
Since 2026-08-16 every published symbol — and every internal one that claims to mirror the target — carries its provenance **in the source, beside the declaration**, pinned by `packages/polyfill/test/provenance.test.ts`:
```text
// @provenance <name> kind=<kind> level=<1|2|3|none> ref=<path:symbol|none> — <note>
```
It exists because this document could not stop drifting from the code: it claimed a "1:1 passthrough" for `docs.*` that was false in two ways, and `emulated-verifier/branch-registers.readLinks` was classified as this library's own invention when it implements `AddLinkV0` — a type the engine **defines** and constructs nowhere. That second mistake is the one the annotations are designed against: *nothing constructs this upstream* is an **absence**, and an absence is not evidence that a thing is ours.
The `ref` cites a **symbol**, never a line number — three citations in this document had already rotted (§ 2, § 4, § 12 below).
The prose labels above map onto the annotation's closed `kind` set, and where they disagree **the annotation wins**:
| label here | annotation `kind` | |
|---|---|---|
| PASSTHROUGH | `passthrough` | only when the name and shape are upstream's OWN, so the call site survives migration untouched |
| PASSTHROUGH | `aligned` | when the semantics are upstream's but the name or the ergonomics are ours (`docCreate` for `doc_create`, a `label` parameter that is not forwarded) — § 7, § 8 |
| LEVEL-1 SHAPE | `aligned` / `declared-not-wired` | the two are NOT the same claim: `aligned` means the engine does this and we follow it; `declared-not-wired` means the engine **defines** the type and constructs it nowhere |
| ASSUMPTION / NO COUNTERPART | `invention` | nothing at any level answers; `ref` is `none`, deliberately |
| *(no prose label)* | `divergent` | upstream does this and we do it differently — the inbox-address publication, the re-readable deposit list |
---
## 1. Bootstrap and configuration
### Today — `@ng-eventually/polyfill`: **one call**
```ts
// shared-wallet/bootstrap.ts
export interface EventuallyConfig {
ng: NgLike; // the REAL @ng-org/web ng
useShape: UseShapeLike; // the REAL @ng-org/orm useShape
pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number };
sharedWallet?: SharedWalletConfig; // the gate's, § 2bis
debugAccessLog?: boolean;
init?: (...args: any[]) => any;
initNg?: (...args: any[]) => any;
}
export function configure(c: EventuallyConfig): void;
```
**The count is the contract here.** The agreed target was two polyfill-era calls, or one; it had drifted to four, and each extra one was a reason the LIBRARY has rather than a need an application has. Four became one on 2026-08-07:
| Was published | Where it went |
|---|---|
| `configureStoreRegistry` + `StoreRegistryDeps` | folded into `configure` — two bootstrap calls existed because the library has two internals, which is not a reason a caller should pay |
| `setCurrentUser` | the access gate sets the identity (§ 2bis). An application naming its own identity is the gesture that INVERTS the model; it must not have a published call to reach for |
| `connectedUser` | `ensureIdentity` awaits it. Upstream, opening the session IS the connection — no application awaits a second call |
| `getConfig`, `getStoreRegistryDeps`, `resetConfig`, `resetStoreRegistry` | internal wiring and test resets, reached by their internal path (2026-08-07, with the entry merge) |
**And two FIELDS of that one call, on 2026-08-12.** The count was already one; what was left inside it still made an application build things the target never asks anyone to build:
| Was published | Where it went |
|---|---|
| `getSession` (and the `RegistrySession` type with it, § 12) | the package's. Upstream a session is **returned**`init()`'s callback delivers `{ status: "loggedin", session }` (`@ng-org/web` `dist/ngweb.js:124`, VERIFIED) and `session_start` hands one back; nowhere does an application ASSEMBLE one out of `session_id` / `private_store_id` / …. Every consumer wrapped `init()` in a promise and wrote the same unwrapping thunk, with nothing to migrate it to. The lib's `init` wrapper captures the event on its way through (§ 2) and holds the session (`shared-wallet/session.ts`) |
| `normalizeId` | the package's, as `normalizeIdentityId` — trim, strip a leading `@`, lowercase. The identities it keys are the shared wallet's own virtual users, so there was never a decision here for a consumer to make; and one rule in one place is what stops the barrier, the URL and storage keying onto three different spaces |
Both remain substitutable through `configureStoreRegistry` (`shared-wallet/bootstrap.ts`), which the published entry does not re-export: the unit suites have no browser and the e2e harness holds a session the broker handed it directly, and neither is an application.
So an application's whole bootstrap is `configure({ ng, useShape, init, sharedWallet })` plus `await ensureIdentity()` — and the second of those keeps its call site after migration.
### Target
**NO COUNTERPART, by design.** The whole subject is the polyfill bootstrap: it exists to inject the real SDK without a hard import (build-alias safety). At migration the consumer initializes the real SDK directly, with the two calls in § 2, and `configure` / `configureStoreRegistry` are deleted (`docs/migration-guide.md` § 7). Nothing in the target takes an "injected `ng`".
---
## 2. Lifecycle
### Today — `@ng-eventually/polyfill`
```ts
// lifecycle.ts:11 — settles the identity, wraps the callback, then forwards to the real
// @ng-org/web init injected at configure()
export function init(...args: any[]): any;
// lifecycle.ts:18 — forwards to the real @ng-org/orm initNg injected at configure()
export function initNg(...args: any[]): any;
```
### Target
**PASSTHROUGH, VERIFIED at both levels — with one argument touched, deliberately.** `init`'s callback in position 0 is wrapped since 2026-08-12: the wrapper reads the event, keeps the session it carries (§ 1), and calls the caller's callback with that same event, unchanged. Everything else — the remaining arguments, the return value, what the callback observes — passes straight through, so an application's call site is what it would write against the real SDK. This is the only place the capture can sit: it is the only one that knows both what the caller asked and what the SDK will answer, and the alternative was every consumer re-implementing it (which is what it replaces). The real signatures forwarded to are:
```ts
// level 2 — @ng-org/web: index.d.ts:108, source sdk/js/web/src/index.ts:51
export declare const init: (callback: Function | null, singleton: boolean, access_requests: any) => Promise<void>;
// level 3 — @ng-org/orm: sdk/js/orm/src/connector/initNg.ts:51 (exported as initNg from core.ts)
export function initNgSignals(ngImpl: NG, session: Session): void;
// level 2 — the Session initNg consumes: index.d.ts:265-273 (source sdk/js/web/src/index.ts:15-22)
export declare type Session = {
session_id: string | number;
protected_store_id: string;
private_store_id: string;
public_store_id: string;
ng: typeof NGModule;
[key: string]: unknown;
};
```
Divergence: none in behaviour (pure forwarding), but the wrapper erases the parameter types. A consumer typing calls against the wrapper learns nothing it must unlearn — it just gets no compile-time help the real SDK would give.
---
## 2bis. The access gate — `ensureIdentity`
### Today — `@ng-eventually/polyfill`
```ts
export async function ensureIdentity(): Promise<PrincipalId>; // shared-wallet/access-gate.ts
export interface SharedWalletConfig { fileUrl: string; password: string; importUrl?: string }
```
One call, before the application renders. It resolves the identity from the URL (`?ng-id=`), failing that from browser storage — and top-level it shows the barrier anyway, with whatever it found already in the field: download the shared wallet, here is its password, import it once, and name your space. Knowing who someone is says nothing about whether their browser still holds the wallet, and the barrier is the only place it is handed out. Past the broker round-trip, inside the iframe, a known identifier stands it down.
It takes no timeout and needs none: the session it waits for arrives through this package's `init` and nowhere else, so awaited before that call it throws, naming the call to make first (`shared-wallet/access-gate.ts`, `refuseAWaitNothingCanEnd`).
### Target
**NO COUNTERPART in substance, and a surviving CALL SITE — this section exists because those two are not the same thing.**
The substance is pure scaffolding. Every step it performs exists only because one wallet hosts several identities: upstream a user opens THEIR wallet, it contains THEIR site (`SensitiveWalletV0.personal_identity()`, `engine/wallet/src/types.rs:576-579`), and `session_start(wallet_name, user_id)` takes an id that came FROM the wallet. There is nothing to name and nothing to choose. The step that takes an identifier is the one that inverts the model, and it is the reason the whole gate is scaffolding.
The call site is a different matter. An application still has to wait for a session before it renders, and that will still be one awaited call at the same place. So the signature was designed to survive: **it takes no identifier and RETURNS one**, deliberately. Naming an identity is the part that disappears, so it must not be a parameter; but knowing which identity you are is something an application legitimately has upstream — it passes `user_id` to `session_start(wallet_name, user_id)` (`index.d.ts:276`), having got it from the wallet it opened. Here the gate chooses it, so the gate hands it back. Without that, the example application had to read the gate's own private storage key.
What a consumer must NOT conclude:
- that it may pass an identity in (it cannot — that is the point);
- that the barrier is a product screen. It is a technical gate, rendered in plain DOM inside a shadow root so no application stylesheet reshapes it and its own leaks nowhere. It is deliberately not bound to a UI framework: a screen that is going away must not make every consumer adopt one.
- that `SharedWalletConfig` describes a user setting. It describes what a DEPLOYMENT hands out, and it disappears with the gate. The library reads no environment variable, ever — the application resolves these values at its own build and passes them.
Why it lives here and not in the consumer application: the first consumer had ~300 lines of it — a gate component, a screen, a wallet module, an identity context, three BDD features. That is code an application would have to delete, and worse, code that teaches its authors a model NextGraph does not have (*"I name my identity"*).
---
## 3. The `ng` object
### Today — `@ng-eventually/polyfill`
```ts
// index.ts:152
export const ng: NG;
// type re-export, index.ts:86
export type { NG } from "@ng-org/web";
```
`ng` is a `Proxy` (`ng-proxy.ts:26`) forwarding every property to the injected real `ng`, overriding exactly one thing: `sparql_update` (the emulated write-cap guard, rejecting a write when a write policy governs the anchored document and the current user lacks the cap). `session_start` is intercepted but is a pure passthrough today, with a TODO for shared-wallet credentials. (`login` was named here until 2026-08-14 but has not existed since 2026-08-03: `@ng-org/web` exposes no such method, and the proxy was FABRICATING it.)
### Target
**PASSTHROUGH (level 2, VERIFIED).** `export declare const ng: NG` with `NG = typeof NGModule`, **88** exported members (`index.d.ts:136-231`; the namespace body is `:140-231`). The surface is identical by construction — the proxy adds no member and removes none — **and identical at the type level too since 2026-08-14**, when the published `ng` stopped being declared `Record<string, any>` and took `NG`, upstream's own type. That escape hatch silently dropped all 88 signatures: an application got no completion, and a misspelt member typechecked.
The overrides:
- `session_start(wallet_name: string, user_id: any): Promise<any>` (`index.d.ts:276`) — target signature unchanged; only the emulated credential injection disappears.
- `sparql_update(session_id: any, sparql: string, nuri: any): Promise<any>` (`index.d.ts:297`) — target signature unchanged. The native enforcement the guard stands in for is the engine's permission model (`verify_perm`, `engine/repo/src/commit.rs:897`), which today is **called only from tests** (its enclosing `Commit::verify` has no runtime caller — see `docs/nextgraph-current-state.md` § *Author-signature verification*). That absence says nothing about the target: write permissions are the engine's declared model, so the guard's *behaviour* (a refused write) is target-shaped even though its *mechanism* (a JS-side check) is emulation. Known limit, documented in `README.md`: the guard fires only on this proxy, and the lib's own writers call the injected `ng` directly, so it is best-effort until cap-enforcement.
---
## 4. Reactive typed reads — `useShape`
### Today — `@ng-eventually/polyfill`
```ts
// use-shape.ts:12
export function useShape(shapeType: unknown, scope: unknown): unknown;
// type re-exports, index.ts:48-49
export type { ShapeType, BaseType, Schema } from "@ng-org/shex-orm";
export type { DeepSignalSet } from "@ng-org/alien-deepsignals";
```
Behaviour: forwards to the injected real `useShape`; once any emulated cap exists (`caps.isEnforcing()`), the returned set is wrapped in a read-filtered view keeping only items whose document cap the current holder has.
### Target
**PASSTHROUGH (level 3, VERIFIED), with a signature the wrapper widens.** The real hook:
```ts
// level 3 — @ng-org/orm/react: sdk/js/orm/src/frontendAdapters/react/useShape.ts:86-124
const useShape = <T extends BaseType>(
shape: ShapeType<T>,
scope: Scope | string | undefined
) => DeepSignalSet<T>;
// its Scope — sdk/js/orm/src/types.ts:25-38 (NOT this lib's Scope, see § 12)
// *(Corrected 2026-08-16: this said `sdk/js/orm/src/model/types.ts`, a path that does not
// exist in the clone. The type is at `sdk/js/orm/src/types.ts:25`. A citation nobody can
// follow reads as verified and is not — the reason the per-symbol annotations below cite a
// SYMBOL rather than a line.)*
export type Scope = {
graphs?: string[] | string;
subjects?: string[];
};
```
The read filter disappears at migration: in the target, isolation is cryptographic — a repo whose cap the wallet does not hold is never decrypted, a union read over it yields nothing, and a targeted read errors `RepoNotFound` (`engine/verifier/src/request_processor.rs:155,163` via `resolve_target`). VERIFIED at level 1; the *consumer-visible* result (you only see what you hold) is the same, which is the point of the emulation.
Divergence to note: the wrapper types everything `unknown`, losing the generic `T`. A consumer wanting typed sets today must cast; at migration the real generic signature gives it back. Nothing to unlearn, only ergonomics deferred.
---
## 5. Reactive typed reads with load state — `watchShape`
### Today — `@ng-eventually/polyfill`
```ts
// watch-shape.ts:73
export interface ShapeQuery<T = UnionSubject> {
data: T[];
isPending: boolean;
isSuccess: boolean;
isError: boolean;
error: unknown;
}
// watch-shape.ts:90
export interface ShapeObservable<T = UnionSubject> {
getSnapshot(): ShapeQuery<T>;
subscribe(onChange: () => void): () => void;
refetch(): void;
}
// watch-shape.ts:166
export function watchShape<T = UnionSubject>(
shapeType: unknown,
scope: Scope,
): ShapeObservable<T>;
```
### Target
**Partly ASSUMPTION — flagged deliberately.** `surface/watch-shape.ts`'s header says it "anticipates NextGraph's planned `useShape(shape, scope)` upgrade, which will natively distinguish 'sync in progress' from 'synced but empty'". **No provenance for that plan exists in this repo's docs or in the `nextgraph-rs` clone** — treat the "planned upgrade" as an assumption, not a stated NextGraph direction. What IS verified at level 3 is that the distinction is *expressible* today, just not through the hook:
```ts
// level 3, VERIFIED — sdk/js/orm/src/connector/GraphOrmSubscription.ts:228,260,274
OrmSubscription.getOrCreate<T extends BaseType>(shape: ShapeType<T>, scope: NormalizedScope): OrmSubscription<T>;
get readyPromise(): Promise<void>; // resolves when the subscription is synced — the native "no longer pending" signal
public close(): void;
```
So the constraint on the bet: the target can already answer "synced?" (`readyPromise`), and `useShape` today returns "an empty set, if still loading" (its own doc comment, `useShape.ts:29-31`) — indistinguishable from synced-empty. `watchShape` surfaces the distinction with a TanStack-`useQuery`-minimal vocabulary (`isPending`/`isSuccess`/`isError`), which is a **shape of this library's choosing**. If the future hook exposes load state under different names, the consumer's binding code changes; the underlying distinction it teaches (pending ≠ empty) is target-expressible and safe to learn.
---
## 6. One-shot listing — the read-model
### Today — `@ng-eventually/polyfill`
```ts
// read-model.ts:60
export interface UnionSubject {
subject: string; // the subject IRI as the document carries it — any IRI, not a Nuri
graph: Nuri; // the document reference the caller passed, unchanged
props: Record<string, string[]>;
}
// read-model.ts:166
export async function readUnion(docs: Nuri[]): Promise<UnionSubject[]>;
```
Behaviour: one anchored `sparql_query` per doc (default-graph body, no `GRAPH` wrapper), parallel, per-doc failure tolerance, cap filter applied inside, machinery subjects dropped.
**Grouping is per (document, subject), and subjects come back as written.** A document holding several subjects yields several entries — one each, properties never merged across subjects; the same subject IRI seen in two documents stays two entries, told apart by `graph`. This matches level 3, where an object carries `@id` and `@graph` as two distinct read-only properties and the ORM fabricates an `@id` when the writer leaves it empty (`sdk/js/orm/src/connector/GraphOrmSubscription.ts`, `":q:"`) — several objects per graph is the provided case, and `@id` is what distinguishes them inside a `@graph`. Only `graph` is a `Nuri`; `subject` is typed `string` because an RDF subject may be any IRI. One document per business entity remains the recommended placement (a key is per repo, so isolating an entity needs a repo of its own), but that is a recommendation about writing — the read reports what is there rather than making the other arrangement invisible.
### Target
Two verified counterparts, one per level; neither returns `UnionSubject` — that grouping is lib-invented:
```ts
// level 2, VERIFIED — the primitive readUnion composes: index.d.ts:295, source sdk/js/lib-wasm/src/lib.rs:352 (nodejs) / :555 (web)
declare function sparql_query(session_id: any, sparql: string, base: any, nuri: any): Promise<any>;
// level 3, VERIFIED — the one-shot typed read: sdk/js/orm/src/connector/getObjects.ts:23
export async function getObjects<T extends BaseType>(
shapeType: ShapeType<T>,
scope: Scope | string
); // returns a deep-cloned Set of matching objects
```
The anchored-read mechanics are level-1 VERIFIED: an anchor restricts the query to that repo's graph as default graph (`resolve_target_for_sparql`, `engine/verifier/src/request_processor.rs:256-285`), an anchorless query unions every named graph in the session store (same function, `UserSite → None``set_default_graph_as_union`). At migration `readUnion` survives as composition (the anchored per-doc read is native); a consumer that wants typed results should be on `useShape`/`getObjects`, not on `UnionSubject` — the property-bag shape is a polyfill artifact, kept generic precisely so the consumer maps it into its own types and can drop it later.
---
## 7. Raw document / SPARQL primitives — `docs.*`
### Today — `@ng-eventually/polyfill` (namespace `docs`)
```ts
// docs.ts:53
export async function docCreate(
sessionId: string | number,
crdt: string,
cls: string,
dest: string,
store?: unknown,
): Promise<Nuri>;
// docs.ts:101
export async function sparqlUpdate(
sessionId: string | number,
query: string,
anchor?: Nuri,
label = "sparqlUpdate",
): Promise<unknown>;
// docs.ts:146
export async function sparqlQuery(
sessionId: string | number,
query: string,
base?: string,
anchor?: Nuri,
label = "sparqlQuery",
): Promise<unknown>;
// docs.ts:113 — machinery, see § 15
// NOT published since 2026-08-07 — moved to `emulated-verifier/register-write.ts`.
// It skips the boundary by design ("the one write that legitimately crosses"), and a door
// that skips a guard must not be one an application can open: holding nothing but a public
// document's bare reference, one could rewrite the inbox address posted on it and divert
// every deposit meant for its owner. Go through `inbox.post` / `inbox.share`.
```
### Target
**Annotated `aligned` (level 2, VERIFIED) — not `passthrough`, and the distinction is the point.** These forward to the real methods with the same arguments in the same order, but the names are ours (`docCreate` for `doc_create`) and they take a trailing `label` (a lib-internal access-log tag) the real signatures have no room for. So the *semantics* survive migration and the *call site* does not — which is exactly what `aligned` says and what a blanket "PASSTHROUGH" hid. *(This entry said PASSTHROUGH until 2026-08-16; it had already been caught claiming a 1:1 mirror, false in two ways, both fixed rather than documented as deltas:)*
- **The session id is `string | number`, upstream's own declared type for it** (`Session.session_id`, `sdk/js/web/src/index.ts:16` and the installed `index.d.ts:266`) — it used to be narrowed to `string` here, which made the value the SDK hands an application impossible to pass back into this library. It is RELAYED, never converted: the wasm side deserializes a `u64` (`sdk/js/lib-wasm/src/lib.rs:352-358` `sparql_query`, `:452-457` `sparql_update`, `:1575` `doc_create`), and stringifying it fails that deserialization for real — observed as `Deserialization error of session_id JsValue("1")`.
- **`sparqlUpdate` returns what the real method returns** — the commits the update produced (`lib.rs:481-483` serialises `AppResponseV0::Commits`; the installed `index.d.ts:297` types it `Promise<any>`). It was declared `Promise<void>` while already relaying the value at runtime, so the answer was thrown away for every caller. Typed `unknown` rather than `any`, exactly as `sparqlQuery` already renders the same upstream `Promise<any>`. A caller that ignores it is unaffected.
The real signatures forwarded to:
```ts
// index.d.ts:60 — the installed web SDK's doc_create
declare function doc_create(session_id: any, crdt: string, class_name: string, destination: string, store_repo: any): Promise<any>;
// index.d.ts:297
declare function sparql_update(session_id: any, sparql: string, nuri: any): Promise<any>;
// index.d.ts:295
declare function sparql_query(session_id: any, sparql: string, base: any, nuri: any): Promise<any>;
```
`depositInto` has **NO COUNTERPART as a SPARQL write**: upstream a deposit is a sealed message, not an update into the recipient's graph (§ 9). It exists only because the emulated inbox is an RDF document — and it is no longer published (see the block above).
**Store targeting — finer than "not JS-constructible".** *(The other docs were corrected on 2026-08-03 to match this entry; they used to state the blanket form.)* Verified in the clone:
- The **web** wasm variant (`sdk/js/lib-wasm/src/lib.rs:1575`, `#[cfg(not(wasmpack_target = "nodejs"))]`) deserializes its 5th argument as `Option<StoreRepo>` via serde — so a value CAN be passed, but no JS helper exists to build the serde form, which keeps it out of practical reach. The published `.d.ts` documents this 5-arg form.
- The **nodejs** variant (`lib.rs:1618`, 6 args) takes `store_type: Option<String>` + `store_repo: Option<String>` and builds the store via `StoreRepo::from_type_and_repo(store_type, repo_id_str)` with `store_type ∈ "public" | "protected" | "private" | "group"` (`sdk/rust/src/local_broker.rs:2969-2987`, `engine/repo/src/types.rs:819-828`).
So the target's direction for scope placement is **already visible in the source** (level 2, VERIFIED, nodejs SDK): name the store by type + repo id strings. The migration-guide's anticipated `getNativeStore(scope)`-style resolver should expect to produce exactly that pair (or the serde `StoreRepo` once a web helper lands) — not a new concept.
---
## 8. Per-document subscription — `subscribeDoc`
### Today — `@ng-eventually/polyfill`
```ts
// subscribe.ts:47,60,79
export type DocChange = unknown;
export type DocChangeType = string | undefined;
export type Unsubscribe = () => void;
// subscribe.ts:69
export function docChangeType(resp: DocChange): DocChangeType;
// subscribe.ts:104
export function subscribeDoc(
nuri: Nuri,
onChange: (r: DocChange, type: DocChangeType) => void,
): Unsubscribe;
// subscribe.ts:184
export function subscribeDocs(
nuris: Nuri[],
onChange: (nuri: Nuri, r: DocChange, type: DocChangeType) => void,
): Unsubscribe;
```
### Target
**Annotated `aligned` (level 2, VERIFIED) — the deltas below are what makes it `aligned` rather than `passthrough`:**
```ts
// index.d.ts:66, source sdk/js/lib-wasm/src/lib.rs:1908
declare function doc_subscribe(repo_o: string, session_id: any, callback: Function): Promise<any>;
```
- The real call is `async` and resolves to an unsubscribe function; the wrapper returns the unsubscribe **synchronously** and honours an early cancel when the promise settles. A consumer coding against the sync return will keep working against the real SDK only through an adapter — a small, known unlearn, traded for not forcing `await` on every subscription site.
- The real callback receives one argument, the serialized `AppResponse` (`{ V0: { State | Patch | TabInfo | … } }`); the wrapper adds a second, pre-extracted `type`. `docChangeType` is a convenience over the verified payload shape (pinned by the e2e CONTRACT-3 probe), not an upstream API.
- `subscribeDocs` has **NO COUNTERPART and needs none**: it is client-side composition (a set of `doc_subscribe` with per-doc error isolation). The upstream fan-out primitive that looks like it (`orm_start_graph(graph_scope, …)`, `index.d.ts:243`) aborts wholesale on one `RepoNotFound` (`sdk/js/orm``engine/verifier/src/request_processor.rs:53-66`) — the reason this composition exists.
---
## 9. Inbox — deposits, and cap delivery
### Today — `@ng-eventually/polyfill` (namespace `inbox`)
```ts
export interface Deposit {
from: PrincipalId | null;
payload: unknown;
ts: number;
}
export interface PostOptions {
from?: PrincipalId | null;
payload: unknown;
ts?: number;
}
export async function post(targetInbox: NuriLike, opts: PostOptions): Promise<void>;
export async function postToDocument(doc: NuriLike, opts: PostOptions): Promise<void>;
export async function share(doc: NuriLike, toUser: string): Promise<void>;
export async function read(targetInbox: NuriLike): Promise<Deposit[]>;
export async function readForDocument(doc: NuriLike): Promise<Deposit[]>;
// `materialize` — a second published name for `read` — was REMOVED on 2026-08-14. It was
// an alias and nothing else: no call site, and upstream has no such member, so it was a
// symbol an application could learn and would then have to unlearn. Use `read`.
export async function readSynced(targetInbox: NuriLike): Promise<Deposit[]>;
export async function processInbox(targetInbox: NuriLike): Promise<Deposit[]>;
export function watch(
targetInbox: NuriLike,
onDeposits: (deposits: Deposit[]) => void,
_opts?: { intervalMs?: number },
): () => void;
```
### Target
**LEVEL-1 SHAPE throughout — the model is VERIFIED, every JS signature here is this library's invention.** There is no inbox method in `@ng-org/web` (none in the 77 `index.d.ts` exports, re-verified), and the verifier's dispatch has no `InboxPost` arm (arms actually handled listed at `engine/verifier/src/request_processor.rs:53-1444`, re-verified). The engine model that constrains the shape:
- An inbox is a keypair on **exactly one repo**: `pub inbox: Option<PrivKey>` (`engine/repo/src/repo.rs:126`); routing is `inboxes: HashMap<PubKey, RepoId>` on the verifier (`engine/verifier/src/verifier.rs:105`, looked up at `:1677`, inserted at `:1928`).
- A message is sealed to the inbox pubkey and carries **no target document**`InboxMsgBody { to_overlay, to_inbox: PubKey, from_overlay: Option<OverlayId>, from_inbox: Option<PubKey>, … }` (`engine/net/src/types.rs:4265`). The address identifies the recipient repo; nothing else is needed. This is why `Deposit` has no document field and why `post` takes only the inbox NURI.
- `from` optional upstream (`from_inbox: Option<PubKey>`) — the "identified if known, anonymous otherwise" behaviour `PostOptions.from` mirrors, including the `null`-means-anonymous case.
- The recipient's own verifier unseals and **applies** queued messages when it processes its inbox (`engine/verifier/src/verifier.rs:1674-1690``process_inbox`); an inbox is a consumed queue, not a store you re-read.
Consequences per function:
- `post` / `postToDocument` — the sender-side act exists in the model (the broker routes `InboxPost` natively, `engine/net/src/server_broker.rs`); its JS surface does not. **The future SDK's name and signature are unknown**`docs/nextgraph-current-state.md:187` records that nothing is announced. `postToDocument`'s resolution step (find the document's inbox address) rides on a **deliberate divergence**: this lib PUBLISHES the address on the document (Header-branch emulation), whereas upstream an address is only ever TRANSMITTED (`ContactDetails` carries `ng:site_inbox`/`ng:protected_inbox`, `engine/verifier/src/inbox_processor.rs:778-830`; the address→repo association lives in `inboxes: PubKey → RepoId`, a table of the **verifier** — one per user, `verifier.rs:105`). *(Corrected 2026-08-10: this said the table was "session-local, rebuilt empty". It is initialized empty (`verifier.rs:520,2820`) and then repopulated at every load — `Verifier::load` → `add_repo_without_saving` → `add_repo_`, `verifier.rs:534-566,2871,2887` — with the inbox private key persisted per repo, `user_storage/repo.rs:61,171,207,362`. The property that matters is that it is **per verifier**, not that it is ephemeral.)* Documented in `docs/briefs/2026-08-03-document-inbox-addressing.md`.
- `share` — a **gap upstream, not a disagreement**, verified at both ends: `ContactDetails.read_cap: Option<ReadCap>` exists (`engine/net/src/types.rs:4233`) but building a message with it is `unimplemented!()` (`types.rs:3786`), its only caller passes `with_readcap: false`, and the receiving arm never reads the field (`inbox_processor.rs:778-830`). `InboxMsgContent::Link` is a **unit variant carrying nothing** (`types.rs:4252`) — do not read it as the delivery channel. The recipient-side filing the lib emulates is real: `AddLink { read_cap }` on the User branch (`engine/repo/src/types.rs:1939-1948`). The consumer's *act* (share one document's cap to one inbox) is target-shaped; only the transport is emulated.
- `read` / `readSynced` / `processInbox` / `watch`**stand-ins for the recipient's own verifier processing**, which has no consumer-facing JS surface upstream and may never have this list-of-deposits shape. A consumer should treat "my inbox gets processed when I connect, and applied caps just appear in what I hold" as the durable contract (that is what `connectedUser` automates, § 13); code that leans on enumerating raw deposits as a mailbox UI is coding against emulation detail it may have to unlearn. The consumer-payload case (`Deposit.payload` as app data) maps to `InboxMsgContent` variants upstream (`types.rs:4249-4260`), of which only `ContactDetails` and `SocialQuery` are more than unit variants today — arbitrary app payloads through the inbox are an **ASSUMPTION**, constrained by the model only in that messages are sealed, per-recipient, and applied by the recipient.
- `readForDocument(doc)` — the owner's side of a document's inbox, named by the DOCUMENT. Same LEVEL-1 SHAPE ruling as `read`: it is the recipient's own processing, which has no consumer-facing JS surface upstream, and enumerating its deposits is emulation detail. It exists so an application never handles an inbox address.
- `share(doc, toUser)` **refuses an unknown recipient** since 2026-08-10. It used to provision one: a mistyped name minted that name's stores and an inbox, and the cap landed where nobody looks. Upstream a deposit is sealed to an inbox pubkey that reached you through an inbound contact, so you cannot address a name you invented.
- `watch`'s `_opts?: { intervalMs?: number }` is accepted and **ignored** (kept for signature compatibility with a removed polling watcher) — dead surface, see § 15.
---
> **Known divergence, low impact today — `inbox.share` always deposits on the recipient's PROTECTED inbox.** Upstream the choice follows the profile through which the person was reached (`a_or_b = if details.profile.is_public() { "site" } else { "protected" }`, `engine/verifier/src/inbox_processor.rs:787`). This library has no notion of "which profile I know this person by", so it picks one. It flattens a distinction the model makes; it will be wrong the day an application shares with someone met through a public profile. Recorded rather than fixed, because the fix needs a notion nothing here has established — note that the `Identity` enum that would name it is entirely commented out upstream (`engine/repo/src/types.rs:586-595`), so there is no profile model to read yet.
## 10. Capabilities — possession, not ACL
### Today
```ts
// @ng-eventually/polyfill — model/types.ts. The published cap surface is now ONE type.
export type Nuri = `did:ng:${string}`;
export type NuriLike = Nuri | string;
// NOT published, each deliberately:
// ReadCap — `did:ng:${string}:r:${string}`. Unpublished 2026-08-10, when
// `export * from "./model/types"` became a named list. It remains the library's
// internal type for a cap-bearing reference, but NO published signature takes or
// returns one: within `surface/inbox.ts` only two private helpers use it
// (`capsSeenIn`, `capOfPayload`), plus the emulated registers. Publishing it named
// the one value the model says must never be handed over on request (§ 0 of
// `readcap-and-nuri-model.md`) — while leaving no published call able to produce
// one, since `linkTo` was removed and `mintCap` is unreachable (§ 11). A type whose
// only possible use by a consumer is a cast is worse than no type. See § 14.
// InboxScope — unpublished the same day, same rule: its only user is
// `account-registry.userInbox(id, scope)`, which is not published (§ 12).
// isNuri / hasReadCap — the type guards (`model/nuri.ts`). Unpublished since the
// permissive-in change: every entry takes `NuriLike` and validates at the door, so
// a consumer holding a plain string narrows nothing. Publishing a guard would
// invite the cast it exists to prevent.
// hasCap(doc) — removed 2026-08-06. It read like "may I read this?", and a
// document in a public store answers `false` until something asks for its cap.
// getCaps / CapRegistry / resetCaps — the emulation's engine room and its test reset.
// INTERNAL — `emulated-verifier/caps.ts` (class CapRegistry). Never published; listed for the maintainer.
constructor(holder?: () => PrincipalId | null);
mint(nuri: Nuri): ReadCap;
learn(cap: ReadCap): void;
capFor(nuri: Nuri): ReadCap | undefined;
learnFromPublicStore(cap: ReadCap): void; // a cap the public store SERVED — read only
isReadOnlyPublicCap(nuri: Nuri): boolean;
markInPublicStore(nuri: Nuri): void;
isInPublicStore(nuri: Nuri): boolean;
open(nuri: Nuri, scope: Scope): ReadCap;
isEnforcing(): boolean;
onChange(listener: () => void): () => void;
grantWrite(doc: Nuri, principal: PrincipalId): void; // decorative until cap-enforcement
governsWrite(doc: Nuri): boolean; // decorative until cap-enforcement
canWrite(doc: Nuri, principal: PrincipalId | null): boolean; // decorative until cap-enforcement
hasWritePolicy(): boolean; // decorative until cap-enforcement
clear(): void;
```
### Target
**LEVEL-1 SHAPE.** There is no capability API at level 2 or 3 (no cap method in `index.d.ts`, none in the ORM), and there is **nothing to introspect upstream**: reading is key possession. The model, VERIFIED:
- A ReadCap is the serialized `ObjectRef``format!("r:{}", base64_url::encode(&ser))` (`BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`). **The `r:` segment and its encoding are upstream's**, reported by NextGraph's developer and verified in that function: id and key are serialized together into ONE opaque segment, unlike the `:k:` object/file/commit forms where they are two. The lib's `ReadCap` template-literal type uses that segment, with the stand-in constant `OK` in place of the key material.
- **"cap-enforcement swaps the value, not the shape" is a BET, and this section stated it as a fact until 2026-08-10.** What the source establishes is narrower, in three readings: (a) `readcap_nuri()` is produced as a **field value**, never concatenated onto a NURI — every call site fills `AppTabBranchInfo.readcap: Option<String>` (`engine/net/src/app_protocol.rs:1334`; `engine/verifier/src/verifier.rs:278,320`; `rocksdb_user_storage.rs:162,172`); (b) **no upstream parser accepts a repo NURI carrying `:r:`**`NuriV0::new_from` (`app_protocol.rs:643-737`) tries `did:ng:i`, `RE_REPO_O`, `RE_FILE_READ_CAP`, `RE_REPO` and `RE_BRANCH`, and none of the regexes at `engine/net/src/types.rs:48-80` has an `r:` form; (c) the slot the type *declares* for a repo read cap is a **field**`NuriV0.access: Vec<NgAccessV0>` with `NgAccessV0::ReadCap(ReadCap)` (`app_protocol.rs:54-62,192`) — itself constructed nowhere today (only `NgAccessV0::Key`, `:622`). Per the design principle none of that says the target will *not* parse a cap-bearing repo NURI; it says nothing parses one yet, so "the shape survives, only the value changes" is an assumption and not a passthrough. If the cap turns out to belong in a field, cap-enforcement moves it there instead of swapping a substring — a change the surface absorbs, because the value is opaque and nothing published parses it (§ 11).
- Caps live in two durable registers by origin: created documents → `AddRepo { read_cap }` on the store's Store branch (`engine/repo/src/types.rs:1890-1899`, committed by `doc_create` via `send_add_repo_to_store`, `engine/verifier/src/request_processor.rs:698`); received caps → `AddLink { read_cap }` on the private store's User branch (`types.rs:1939-1948`).
- The one path that loads a repo from a cap is `pub(crate)``Verifier::load_repo_from_read_cap` (`engine/verifier/src/verifier.rs:2237`) — unexposed to JS.
`capFor(nuri)` asks the only question the model admits — "do I hold this document's key?" — and returning `undefined` is the whole possible answer. There is no "may principal P read D?" anywhere, and the future SDK cannot offer one without inventing an ACL the engine does not have. That absence is a **finding about the target's model**, not a missing feature: a consumer should never expect a cap-introspection API.
The `CapRegistry` class itself is machinery (the in-memory record of what the connected holder holds — upstream's local user storage). It is not published at all: the consumer surface is the ACTS (creating a document, `inbox.share`, processing one's inbox — and, for a document in a public store, simply reading it), never a lookup; see § 15.
---
## 11. NURI and SPARQL string utilities
### Today — nothing. The entry publishes **no** string utility and **no** type guard.
```ts
// NOT published — internal, and each for a stated reason:
// surface/sparql.ts escapeLiteral, escapeIri, assertNuri
// model/nuri.ts isNuri, hasReadCap, targetOf, parseNuri, toNuri
// emulated-verifier/caps.ts mintCap (it lived in `model/nuri.ts` until the source
// layout was reorganised by migration fate; this list
// still said so until 2026-08-10)
```
Two decisions meet here, and both point the same way.
**No guard, because the doors validate.** Every public entry takes `NuriLike` (`Nuri | string`) and runs `toNuri` itself — permissive in, precise out. A consumer holding a string from storage, a URL or a form passes it straight in; publishing a guard would invite the cast the types exist to prevent, and would put validation in the caller's hands where the door already does it.
**No `mintCap`, ever.** Nothing on the surface may turn a bare reference into a cap — that is the model's central invariant (§ 0 of `readcap-and-nuri-model.md`), so the function that could is unreachable from outside.
The escaping helpers were published until the surface was narrowed. Their removal costs a consumer nothing it will miss: they are generic injection-safety utilities, and neither `@ng-org/web` nor the ORM exposes an equivalent (re-verified against `index.d.ts` and `sdk/js/orm/src` — the engine escapes ad hoc where it builds SPARQL, e.g. `update_header`, `engine/verifier/src/request_processor.rs:196-208`). An application that interpolates SPARQL writes its own two-line escaper, against this lib or the real SDK alike.
### Target
**NO COUNTERPART at any level, and none expected** — which is precisely why none of it is published: a symbol with no successor, on a surface that promises one, is the thing this document exists to catch.
---
## 12. Scope resolution, per-entity documents, and the store registry
### Today — `@ng-eventually/polyfill` (namespace `storeRegistry`) — plus `Scope` from `types.ts`
> **Narrowed twice.** 2026-08-03 the entry stopped re-exporting the whole `store-registry` module and kept an app-facing slice (`src/surface/placement.ts`). 2026-08-05 that slice lost its two inbox-ADDRESS functions as well: an application deposits with `inbox.postToDocument(doc, …)` and shares with `inbox.share(doc, toUser)` — always naming a document or a person, never an address, because upstream an address is resolved from a profile and never handled by a caller. **Five functions remain published**, listed first below; everything after them is kept for the record and is covered by `docs/internal-contract.md`.
```ts
// types.ts:38 — NB: NOT the ORM's Scope (a graphs/subjects filter); this is the store scope
export type Scope = "public" | "protected" | "private";
// store-registry.ts:90,234
// VirtualUserRecord is INTERNAL (shape kept here for the ruling below).
interface VirtualUserRecord {
id: string;
docPublic: Nuri;
docProtected: Nuri;
docPrivate: Nuri;
}
// RegistrySession is INTERNAL since 2026-08-12 (shape kept here for the ruling below).
interface RegistrySession {
sessionId: string | number; // relayed untouched — upstream's own type (§ 7)
privateStoreId: string;
protectedStoreId?: string;
publicStoreId?: string;
}
// PUBLISHED — the whole `storeRegistry` namespace, and nothing else.
// NO identity parameter, since 2026-08-10: a session belongs to one user, and the
// target's own `doc_create(session_id, …)` carries no user at all. Passing one's own
// identity to every placement call was a gesture with no successor — and it forced an
// application to KNOW its identity, which it could only do by reading the access gate's
// private storage key. `ensureIdentity()` returns it now; these take it from the session.
export async function createEntityDoc(scope: Scope): Promise<Nuri>;
export async function listMyEntityDocs(scope: Scope): Promise<Nuri[]>;
export async function resolveScopeGraph(scope: Scope): Promise<Nuri>;
export async function resolveWriteGraph(scope: Scope): Promise<Nuri>;
export async function openDocumentInbox(doc: NuriLike): Promise<Nuri>;
// NOT published — internal, kept here because the target rulings below still cover them.
// userStoreDoc, userInbox, documentInboxAddress, isOwnInbox, myInboxes,
// addLink, readLinks, resolveAccount, ensureAccount, reservedAccount,
// resetRegistryCache, and the VirtualUserRecord type.
// `RegistrySession` joined them on 2026-08-12: it was published for ONE reason — a consumer
// typed the session thunk it injected with it — and that thunk is gone (§ 1). Upstream a
// session is RETURNED, never assembled, so no application has a session shape to declare.
```
### Target — split by what each piece maps to
- **`createEntityDoc(id, scope)` → level 2, VERIFIED direction.** Target: `doc_create(session_id, crdt, class_name, destination, store_repo)` aimed at the identity's real per-scope store (see § 7 for the store-targeting nuance — the nodejs SDK already takes `store_type`/`store_repo` strings). The two writes the lib performs by hand are **native side effects** of `doc_create` upstream: the `ldp:contains` listing on the store's Main branch and the `AddRepo { read_cap }` on its Store branch (`engine/verifier/src/request_processor.rs:697-710`). The `id` parameter is already gone from the published call (2026-08-10); expect `createEntityDoc(scope)` to become `doc_create(sid, …, storeOf(scope))` with no listing/cap bookkeeping.
- **`listMyEntityDocs(id, scope)` → level 1/2, VERIFIED mechanism.** Upstream the listing is the store's `ldp:contains` graph (written at `request_processor.rs:706-708`), readable with an anchored `sparql_query` on the store; the caps come back by replaying the Store branch (`AddRepo::verify``load_repo_from_read_cap`). The function's shape (give me my per-scope doc NURIs) survives; its implementation becomes one native read.
- **`userStoreDoc(id, scope)` / `resolveScopeGraph(scope)` / `resolveWriteGraph(id, scope)` → level 2, VERIFIED.** The target answers these from the session: `did:ng:` + `session.private_store_id | protected_store_id | public_store_id` (`Session`, `sdk/js/web/src/index.ts:Session`, installed `index.d.ts:265-273`). The store IS the container; the per-scope index document disappears.
- **`userInbox(id)` → level 1, VERIFIED counterpart with a different granularity.** Upstream a user's inboxes are their public and protected STORE repos' inboxes — the only two `AddInboxCap` commits in the engine (`engine/verifier/src/site.rs:128,149`). An identity-level "my inbox" therefore maps to a store inbox; the resolution moves into the lib/SDK and the consumer's act (deposit to an address, process my own) is unchanged.
- **`openDocumentInbox(doc)` / `documentInboxAddress(doc)` → level 1, VERIFIED support, no exerciser.** Every `Repo` carries `inbox: Option<PrivKey>` (`engine/repo/src/repo.rs:126`); `AddInboxCapV0` is keyed by `repo_id` with no is-store restriction (`engine/repo/src/types.rs:1973`; applied at `engine/verifier/src/verifier.rs:1920-1928`); but no code path creates one for a plain document (`doc_create``new_repo_default``Store::create_repo_default``create_repo_with_keys`, which builds the `Repo` with `inbox: None``engine/verifier/src/verifier.rs:3004`, `engine/repo/src/store.rs:264,284,691`) and no level-2/3 API exposes any of it. So: the *capability* is engine-verified; the *functions* are invented surface; and the **address publication is a real, deliberate divergence** (upstream transmits addresses, never publishes them — § 9), with the ownership guard compensating our design, not mirroring an upstream rule.
- **`addLink(cap)` / `readLinks()` → annotated `declared-not-wired` (level 1).** The emulated `AddLink { read_cap }` register (`engine/repo/src/types.rs:AddLinkV0`*"so that a user can share with all its device a new Link they received"*, external repos only). **Verified 2026-08-16, and this is the load-bearing fact:** the type is DEFINED, its `CommitVerifier::verify` is a no-op `Ok(())` (`engine/verifier/src/commits/mod.rs`), and **nothing in the engine constructs one** — the only other occurrences are the `CommitBodyV0::AddLink` enum variant and two match arms classifying body kinds. Its sibling `AddRepoV0` *is* constructed (`Verifier::send_add_repo_to_store`), which is why the Store-branch register next door is annotated `aligned` and this one is not. A maintainer read that absence and concluded the function was this library's own invention; it is not — an absent implementation says nothing about the target. Upstream this filing happens inside the verifier when it processes the inbox; the future SDK most likely never exposes these as calls, so consumers should not code against them (§ 15).
- **`resolveAccount` / `ensureAccount` / `VirtualUserRecord` / `RegistrySession` / `reservedAccount` / `resetRegistryCache` → NO COUNTERPART.** The shared-wallet shim (accounts directory, pointer → doc-shim indirection) has no image in the target — the target has no central directory of identities (`docs/migration-guide.md` § 3). The whole group disappears with the shim.
- **`isOwnInbox` / `myInboxes` → NO COUNTERPART as API.** Upstream the question "which inboxes may I read" is answered inside the verifier by the User branch's `AddInboxCap` records; nothing suggests a JS API for it. These exist for the emulated read guard and the connection drain.
---
## 13. Identity and connection
### Today
```ts
// PUBLISHED: nothing. Identity is established by `ensureIdentity()` (§ 2bis) and the
// connection is awaited inside it.
//
// NOT published, and each removal is a gesture an application no longer performs:
// setCurrentUser (2026-08-07) naming one's own identity — the step that inverts the
// model. The gate does it; the e2e harness, which plays
// several identities on one page, reaches it internally.
// connectedUser (2026-08-07) awaited inside `ensureIdentity`; upstream, opening the
// session IS the connection.
// getCurrentUser (2026-08-05) an application knows who it signed in.
// IdentityStore, browserIdentityStore, VirtualUserStorage, ACCOUNT_STORAGE_KEY
// (2026-08-05) persisting an identity is the application's job
// upstream too; the gate persists what IT needs.
```
### Target
**PASSTHROUGH-to-be at level 2, VERIFIED signatures.** In the target the identity is established by opening one's own wallet and starting a per-user session — there is no "set the current user" call because the session IS the user:
```ts
// index.d.ts:276, 280, 313, 315
declare function session_start(wallet_name: string, user_id: any): Promise<any>;
declare function session_stop(user_id: string): Promise<void>;
declare function user_connect(client_info: any, user_id: string, location?: string | null): Promise<any>;
declare function user_disconnect(user_id: string): Promise<void>;
```
- `IdentityStore` / `browserIdentityStore` (the persisted identity id) — **NO COUNTERPART**; they exist only because every virtual user shares one wallet, and they are no longer published at all. Removed at migration (`docs/migration-guide.md` § 5).
- `setCurrentUser`**NO COUNTERPART**; the relay of an identity the broker cannot see. Disappears with the shared wallet, and is no longer published: the gate is the only caller an application needs.
- `connectedUser()` (internal since 2026-08-07) — the awaitable form of what the target does **automatically**: the recipient's verifier processes its inbox as messages arrive/at connection (`Verifier::inbox`, `engine/verifier/src/verifier.rs:1674`). VERIFIED at level 1 that no consumer call is needed upstream; the polyfill fires it from `setCurrentUser` for the same reason. A consumer should treat it as "await a deterministic start" (tests), not as an operation the future SDK will name.
---
## 14. Type re-exports
`@ng-eventually/polyfill` re-exports, type-only (erased at build, `src/index.ts`):
```ts
export type { ShapeType, BaseType, Schema } from "@ng-org/shex-orm";
export type { DeepSignalSet } from "@ng-org/alien-deepsignals";
export type { NG } from "@ng-org/web";
```
**PASSTHROUGH (levels 2/3, VERIFIED)**`ShapeType`/`BaseType` at `@ng-org/shex-orm` `dist/types.d.ts:5,12` (installed 0.1.2-alpha.8); `NG` at `index.d.ts:136`. At migration these imports point at the same packages directly; nothing changes for the consumer.
### The library's own model types — published by NAME since 2026-08-10
The entry used to say `export * from "./model/types"`, a blanket re-export publishing eight types in one gesture. It now names them, under one rule:
> **A type is published only if a PUBLISHED SIGNATURE uses it.**
```ts
export type { Nuri, NuriLike, Scope, PrincipalId, NgLike, UseShapeLike } from "./model/types";
```
Each one's warrant: `Nuri` is what every reference-returning call returns and `NuriLike` what every entry accepts (§ 10, § 11); `Scope` types `storeRegistry.*` and `watchShape` (§ 12, § 5); `PrincipalId` is `ensureIdentity`'s return and a field of `Deposit`, `PostOptions` and `EventuallyConfig` (§ 2bis, § 9, § 1); `NgLike` and `UseShapeLike` type the two injected objects in `EventuallyConfig` (§ 1).
Two types the blanket export published are now internal, each because **nothing published names it**:
- **`ReadCap`** — no published signature takes or returns one. Its users are two private helpers of `surface/inbox.ts` (`capsSeenIn`, `capOfPayload`) and the emulated registers. Publishing it advertised a value a consumer has no published call to obtain, and deliberately so: `linkTo` was removed precisely for handing one out (§ 0 of `readcap-and-nuri-model.md`), and `mintCap` is unreachable from outside (§ 11). The only use a consumer could make of it is a cast — which is what the surface's permissive-in / precise-out design exists to make unnecessary.
- **`InboxScope`** — used only by `account-registry.userInbox(id, scope)`, unpublished since 2026-08-05 (§ 12). An application never handles an inbox address, so it never names an inbox scope.
Both remain **defined** in `model/types.ts` and are used throughout the library; only their publication changed. Nothing about the target motivates either removal — this is a statement about *this* surface, and the same test that pins the appendix pins it.
---
## 15. Machinery on the surface — what a consumer should NOT code against
Exported, but not SDK surface. Coding against these builds knowledge that migration deletes:
- ~~**`docs.depositInto`**~~ **FIXED 2026-08-07.** It was published "only because `inbox.ts` lives in another module", with the note that a consumer must always go through `inbox.post`. That note is not a mechanism: an adversarial review drove through it — bare reference to a public document, rewrite its posted inbox address, divert its owner's deposits. It now lives in `emulated-verifier/register-write.ts`, which nothing exports.
- **`getConfig` / `getStoreRegistryDeps`** — tagged `@internal` in source, exported for the lib's own wrappers.
- **`resetConfig` / `resetStoreRegistry` / `resetCaps` / `storeRegistry.resetRegistryCache`** — test/reset machinery. In particular `resetCaps` wipes EVERY holder's caps, which no product flow should ever do.
- **`getCaps()` and the `CapRegistry` class** — the registry is the emulation's engine room. The consumer surface is the acts that file caps: creating a document, `inbox.share` (grant), processing one's inbox, and reading a document a public store serves. `CapRegistry.grantWrite` / `governsWrite` / `canWrite` / `hasWritePolicy` are explicitly decorative until cap-enforcement — the guard they feed is bypassed by every internal writer.
- ~~**`storeRegistry.reservedAccount`, `resolveAccount`, `ensureAccount`, `VirtualUserRecord`, `RegistrySession`**~~ — **RESOLVED 2026-08-03**: no longer exported. Shim internals, now in `docs/internal-contract.md`. The consumer's legitimate touchpoint is `configureStoreRegistry` (bootstrap) plus the scope/entity resolvers.
- ~~**`storeRegistry.addLink` / `readLinks`**~~ — **RESOLVED 2026-08-03**: no longer exported. Consumers receive caps by processing their inbox (automated at connection); calling these directly baked in a register the verifier owns upstream.
- ~~**`virtualUsers.*` on the SDK entry**~~ — **RESOLVED 2026-08-03**: moved to `/polyfill`, where its disappearance at migration is visible at the import line.
- **`inbox.watch`'s `_opts?: { intervalMs?: number }`** — accepted and ignored (no polling exists). Dead compatibility surface; do not pass it.
- **The `label` parameters** on `docs.sparqlUpdate` / `docs.sparqlQuery` — lib-internal access-log tags, never forwarded to `ng`. The real signatures have no such parameter.
### Places the current surface teaches something to unlearn
- ~~**The SDK entry is not as pure as its header claims.**~~ **FIXED 2026-08-03.** The header claimed the entry "exposes ONLY what `@ng-org/web` / `@ng-org/orm` expose" while also shipping `virtualUsers` and the whole `store-registry` module. Both are gone from it, and the header now states what the entry actually promises: *every symbol here has a target-SDK counterpart, verified or assumed, listed in this document*. It still exports `docs`, `readUnion`, `watchShape`, `subscribeDoc(s)`, the SPARQL helpers and the NURI guards — justified inventions, documented per subject above — so the promise is no longer "@ng-org surface only", which was never true, but "nothing here is machinery".
- ~~**`share` is importable from both entries.**~~ **FIXED 2026-08-07** with the entry merge: there is one entry and one `share`, under `inbox`.
- **One entry means the import line no longer says what disappears.** Until 2026-08-07 a second import path (`/polyfill`) WAS the deletion list. It is now the `POLYFILL-ERA` block in `src/index.ts`, this appendix's note above, and the per-subject rulings in this document. That is a documentation-carried signal where it used to be a mechanical one — the appendix is pinned by a test, the grouping is not.
- **`inbox.read` as a mailbox** — enumerating raw deposits is emulation detail (§ 9); the durable contract is deposit-and-it-gets-applied. An app building UI on the deposit list should expect that surface to change shape entirely.
- **`watchShape`'s "planned `useShape` upgrade"** — stated in the module header with no provenance in this repo or the clone (§ 5). The load-state *distinction* is safe; the claim that NextGraph plans this exact hook shape is an assumption and must not be cited as an announced API.
- **`UnionSubject` property bags** — polyfill read-model shape, not a target type; map them into app types at the boundary (which `watchShape`'s design already assumes).
- **The sync-returning `subscribeDoc` unsubscribe** vs the target's promise-resolved one (§ 8) — a deliberate, documented ergonomic delta; an adapter is one line at migration, but it is a delta.
---
## Appendix — full export inventory (for diffing)
*Generated from the `export` statements, and pinned by `packages/polyfill/test/vocabulary.test.ts` — if this list and the code disagree, that test fails. It went stale once, still listing `storeRegistry`'s shim internals after the entry had been narrowed, which is what a hand-maintained inventory does.*
### `@ng-eventually/polyfill` — `src/index.ts` (the only entry since 2026-08-07)
```text
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, NG, NgLike, Nuri, NuriLike, PrincipalId, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, configure, docChangeType, ensureIdentity, init, initNg, ng, readUnion, subscribeDoc, subscribeDocs, useShape, watchShape
docs: docCreate, sparqlQuery, sparqlUpdate
inbox: Deposit, PostOptions, post, postToDocument, processInbox, read, readForDocument, readSynced, share, watch
storeRegistry: createEntityDoc, listMyEntityDocs, openDocumentInbox, resolveScopeGraph, resolveWriteGraph
```
**Of these, exactly ONE is polyfill-era with no target counterpart**`configure` (plus
the type `EventuallyConfig`; `RegistrySession` left the surface on 2026-08-12 with the
session thunk that was its only reason to be there). It is the deletion list, and
`src/index.ts` groups it under a heading that says so. `ensureIdentity` is a second in
substance — the shared-wallet gate — but its *call site* survives (§ 2bis).
Nine symbols published before 2026-08-07 are gone from the surface: `configureStoreRegistry`
and `StoreRegistryDeps` (folded into `configure`), `setCurrentUser` and `connectedUser`
(§ 1), `getConfig` / `getStoreRegistryDeps` (internal wiring), `resetConfig` /
`resetStoreRegistry` / `resetCaps` (test resets), and the direct `share` re-export —
`inbox.share` was always the same function, and publishing it twice blurred the boundary
it was meant to mark.