L'objectif acté était deux appels spécifiques au polyfill, voire un. Il en publiait
quatre. Chacun des trois de trop était une raison que la BIBLIOTHÈQUE a, pas un besoin
qu'une application a :
- **`configureStoreRegistry`** existait parce qu'il y a deux internes à câbler — le SDK
injecté d'un côté, la session de l'autre. Vu de l'appelant, les deux disent « voici ce
qu'il te faut pour tourner ». Replié dans `configure`, qui prend désormais
`getSession` / `normalizeId` / `pointerGuard`.
- **`setCurrentUser`** n'a plus lieu d'être publié depuis que le portail d'accès est
passé dans le polyfill : c'est lui qui pose l'identité. Et une application qui nomme
sa propre identité est exactement le geste qui inverse le modèle — il ne doit pas
exister d'appel publié vers lequel se tourner. Le harnais e2e, lui, joue plusieurs
identités sur une même page ; il y accède par le chemin interne, ce qu'un harnais a
le droit de faire et une application non.
- **`connectedUser`** est maintenant attendu DANS `ensureIdentity`. Ce n'était pas une
commodité : la suite applicative avait montré qu'une app devait l'attendre elle-même,
sinon une note qu'on venait de lui partager se lisait comme illisible. J'avais traité
le symptôme dans l'app d'exemple ; le défaut était côté bibliothèque. En amont, ouvrir
la session EST la connexion — aucune application n'attend un second appel.
Reste donc `configure({ … })`, plus `await ensureIdentity()` dont le site d'appel
survit à la migration : une application attendra toujours une session avant de rendre.
Le test étendu hier a fait son travail : les deux contrôles de contrat sont passés au
rouge sur `configureStoreRegistry`, `connectedUser` et `StoreRegistryDeps` dès que la
surface a bougé.
180 tests unitaires, e2e 40/40 (3,4 min) et applicatif 10/10 (0,8 min).
31 KiB
Internal contract — what @ng-eventually/sdk keeps off its surface, and what NextGraph does or would do about each 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:readModelbecame the directly-exportedreadUnion;accounts/AccountRecord/AccountStoragebecamevirtualUsers/VirtualUserRecord/VirtualUserStorage(moduleshared-wallet/virtual-users.ts);store-registry-api.tsbecamesurface/placement.ts. Two modules were created and are covered here:emulated-verifier/branch-registers.ts(the four durable registers, split out of the shim) andshared-wallet/bootstrap.ts(the injection store, split out of the/polyfillentry). The subject-by-subject rulings below are unaffected — what moved is where the code lives, not what it promises.
Scope. The complement of docs/api-contract.md: every module export under packages/sdk/src/ that is NOT reachable from the two published entry points (package.json maps exactly . → src/index.ts and ./polyfill → src/polyfill.ts). A consumer never reads this document; a maintainer does. The internal code is held to the same standard as the surface — as close as possible to what NextGraph does or plans — so every subject below carries the same target-side analysis. Written 2026-08-04, verified against the nextgraph-rs clone (HEAD 213338f6) and the installed @ng-org/web@0.1.2-alpha.13 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 the boundary was computed — mechanically, from the export statements. index.ts re-exports wholesale (export * / export * as ns) from types.ts, inbox.ts, docs.ts, surface/read-model.ts, and by name everything surface/use-shape.ts, surface/watch-shape.ts, lifecycle.ts, sparql.ts export, and subscribeDoc/subscribeDocs/docChangeType (+ types) from subscribe.ts; its storeRegistry namespace is the surface/placement.ts slice only (createEntityDoc, listMyEntityDocs, resolveScopeGraph, resolveWriteGraph, openDocumentInbox). model/nuri.ts is now internal in full — the guards isNuri/hasReadCap stopped being published when the entries became permissive-in (NuriLike validated at the door). (The second entry, polyfill.ts, was merged into index.ts on 2026-08-07, and the polyfill-era block was then cut to ONE published call, configure. configureStoreRegistry, setCurrentUser and connectedUser became internal the same day — folded, replaced by the gate, and awaited inside it respectively; the machinery accessors and test resets are internal too. All four are inventoried below.) Everything else that carries export in a src/ module is internal and inventoried here. Eight modules are internal in their entirety: shared-wallet/access-log.ts, emulated-verifier/machinery.ts, surface/ng-proxy.ts, emulated-verifier/open-repo.ts, shared-wallet/outbox-log.ts, shared-wallet/physical.ts, emulated-verifier/reach.ts, emulated-verifier/read-filter.ts. Four are internal in part: nuri.ts, emulated-verifier/connect.ts, subscribe.ts, shared-wallet/account-registry.ts.
Labels are those of docs/api-contract.md: PASSTHROUGH (level 3/2, VERIFIED), LEVEL-1 SHAPE (model VERIFIED, JS surface ASSUMED), ASSUMPTION, NO COUNTERPART. Level numbers per README.md § The three references: 3 = JS ORM, 2 = wasm binding (@ng-org/web), 1 = Rust engine. One label recurs here that the surface contract rarely needs: NO COUNTERPART, shared-wallet machinery — the code below the emulation's floor, which the target has no image of because the target has no shared wallet. Per the design principle, an absent implementation is never treated as evidence about the future.
1. The wrapped ng factory — surface/ng-proxy.ts
// ng-proxy.ts:10
export function makeNg(): Record<string, any>;
Builds the published ng Proxy (consumed once, index.ts:61): forwards every property to the injected real ng, overriding login/session_start (passthrough with a shared-wallet-credentials TODO) and sparql_update (the emulated write-cap guard).
- The factory itself is NO COUNTERPART, by design — the target has no "wrap the SDK" step; at migration
ngIS@ng-org/web's andmakeNgis deleted. - The
sparql_updateguard stands in for the engine's write-permission model (verify_perminsideCommit::verify,engine/repo/src/commit.rs:892-899) — same analysis asdocs/api-contract.md§ 3. - Defect — the
loginarm fabricates a member (see Findings F1).@ng-org/webhas nologin: none among the exports ofindex.d.ts(re-verified), and nofn logininsdk/js/lib-wasm/src/lib.rs. The proxy nevertheless returns a function forprop === "login"(ng-proxy.ts:16-22), sotypeof ng.login === "function"on the wrapper while the real SDK yieldsundefined— the one place the proxy adds a member, contradicting its own header and the surface contract's "adds no member and removes none" (§ 3). Calling it throws at runtime (ng[prop]is undefined). ASSUMPTION with no provenance — no target layer names alogin. - Disappears at migration (the whole module).
2. NURI internals — the unexported slice of nuri.ts
// nuri.ts:73
export function targetOf(nuri: Nuri): Nuri;
// nuri.ts:82
export function parseNuri(nuri: Nuri): { target: Nuri; readCap?: ReadCap };
// nuri.ts:116
export function mintCap(nuri: Nuri): ReadCap;
targetOf strips a :r: cap segment to the naming form; parseNuri is the parsed pair; mintCap builds the cap-bearing form with the stand-in value OK. Kept off the surface deliberately: nothing published turns a bare reference into a cap.
targetOf/parseNuri— LEVEL-1 SHAPE, model VERIFIED: a 1:1 mirror of upstream's one-type-with-optional-access NURI. The ReadCap encoding they discriminate on isr:{base64url(serde_bare(ObjectRef))}(BlockRef::readcap_nuri,engine/repo/src/types.rs:518-521), distinct from the:k:object/commit forms (object_nuri/commit_nuri,types.rs:510-514). No JS surface parses NURIs at level 2 or 3 — the real SDK takes plain strings — so these helpers never surface in signatures and survive only as internals.mintCap— NO COUNTERPART as an operation, and that is the point: upstream a ReadCap is produced by the engine when a repo is created, never derived from a bare reference by a caller.mintCapexists solely because the emulation needs a cap VALUE at creation time and P1b has not yet supplied real key material; the constantOKpretends nothing (nuri.ts:87-103). It has exactly two call sites (shared-wallet/account-registry.tscreateEntityDoc;emulated-verifier/caps.tsinternals) — the minting points of the emulation. At P1b the constant becomes a real key; at migration the function is deleted (the engine mints).
3. The reach boundary — emulated-verifier/reach.ts
// reach.ts:66
export function declareInfrastructure(nuri: Nuri): void;
// reach.ts:71
export function isInfrastructure(nuri: Nuri): boolean;
// reach.ts:76
export function resetInfrastructure(): void;
// reach.ts:93
export function mayReach(nuri: Nuri): boolean;
// reach.ts:110
export function assertMayReach(nuri: Nuri, op: string): void;
// reach.ts:131
export function mustNotAttempt(nuri: Nuri): boolean;
The single predicate deciding whether the CONNECTED virtual user may touch a document at all: cap possession, or explicitly-declared infrastructure (the store-root and doc-shim). assertMayReach guards the passage points (rule 1, throw on refusal); mustNotAttempt guards the callers (rule 2, do not even issue the operation). Inert until the first cap exists (caps.isEnforcing()).
- NO COUNTERPART, shared-wallet machinery — the emulated stand-in for the wallet boundary itself. In the target the boundary is cryptographic, not a predicate: a repo whose cap the wallet does not hold is never decrypted, a targeted read of it errors
RepoNotFound(resolve_target_for_sparql,engine/verifier/src/request_processor.rs:264,269), and the only path that loads a repo from a cap ispub(crate)(Verifier::load_repo_from_read_cap,engine/verifier/src/verifier.rs:2237). The two-rules split (refuse at the gate AND do not attempt) is redundancy this lib chose; upstream only "cannot" exists — there is nothing to refuse because the request cannot be formed. - The infrastructure exemption (
declareInfrastructure, registered byshared-wallet/account-registry.tsfor the store-root and doc-shim only) has no image in the target: there is no shim to exempt. Registration-not-pattern-matching is a lib-internal safety choice. - Everything here disappears at migration; the durable lesson it protects (naming a document does not grant access) is the target's own model.
4. The physical user's primitives — shared-wallet/physical.ts
// physical.ts:54
export async function physicalCreate(sessionId: string, crdt = "Graph", cls = "data:graph", dest = "store", store?: unknown): Promise<Nuri>;
// physical.ts:80
export async function physicalQuery(sessionId: string, query: string, base: string | undefined, anchor: Nuri, label = "physicalQuery"): Promise<unknown>;
// physical.ts:94
export async function physicalUpdate(sessionId: string, query: string, anchor: Nuri, label = "physicalUpdate"): Promise<void>;
The unguarded counterparts of docs.docCreate / sparqlQuery / sparqlUpdate, callable only by the library's own machinery on the shim's documents (store-root pointer, doc-shim, provisioning). Separated as FUNCTIONS rather than as an exemption list so machinery never gets "waved through" a guard (module header, physical.ts:19-27).
- As wire calls: PASSTHROUGH (level 2, VERIFIED) — the same
doc_create/sparql_query/sparql_updatethe publisheddocs.*forwards to (index.d.ts:60,295,297; sourcessdk/js/lib-wasm/src/lib.rs:1575web /:1618nodejs,:352/:555), minus the lib-internallabel. - As a CONCEPT: NO COUNTERPART, shared-wallet machinery. The physical/virtual user split exists only because one wallet hosts many identities; the target has exactly one user per wallet and no privileged "machinery caller". The module disappears with the shim.
5. Physical subscription — the unexported slice of subscribe.ts
// subscribe.ts:118
export function subscribePhysicalDoc(nuri: Nuri, onChange: (r: DocChange, type: DocChangeType) => void): Unsubscribe;
subscribeDoc minus the reach guard — the machinery's door to doc_subscribe, used by emulated-verifier/open-repo.ts to hold shim repos open. Same wire behaviour as the published subscribeDoc (analysed in docs/api-contract.md § 8, target doc_subscribe, index.d.ts:66, sdk/js/lib-wasm/src/lib.rs:1908).
- NO COUNTERPART, shared-wallet machinery — the guarded/unguarded pair collapses to one call when the wallet is the boundary. Disappears with
shared-wallet/physical.ts.
6. Bootstrap repo opening — emulated-verifier/open-repo.ts
// open-repo.ts:75
export type SyncState = "syncing" | "synced" | "timed-out";
// open-repo.ts:104 — TEST-ONLY
export function setOpenTimeoutForTests(ms: number): void;
// open-repo.ts:110
export function resetOpenedRepos(): void;
// open-repo.ts:135
export function getSyncState(nuri: Nuri): SyncState | "unknown";
// open-repo.ts:167
export async function ensureRepoOpen(nuri: Nuri): Promise<void>;
// open-repo.ts:184
export async function ensurePhysicalRepoOpen(nuri: Nuri): Promise<void>;
// open-repo.ts:259
export async function ensureReposOpen(nuris: Nuri[]): Promise<void>;
Heals the cold-start defect of the anchored read path: on a fresh session a not-yet-open repo reads empty, so before an anchored read the repo is opened by subscribing (subscribePhysicalDoc) and awaiting the first State push — the sync barrier — with a bounded timeout. The subscription is held for the session; per-nuri SyncState keeps synced and timed-out apart.
- The opening mechanism is level 2, VERIFIED as a composition:
doc_subscribeexists (sdk/js/lib-wasm/src/lib.rs:1908), and the push variantsTabInfo/State/Patchare the engine'sAppResponseV0(engine/net/src/app_protocol.rs:1354-1358). The ORDER (TabInfo first, then the initial State) and "first State = presence guaranteed, absence definitive" are empirical, pinned by the in-repo e2e CONTRACT-3 probe — an ASSUMPTION about ordering as far as upstream is concerned: no upstream statement fixes the push order, so a future reordering upstream would silently break the barrier. Bound: the e2e probe fails loudly if the order changes. - "Hold a live subscription to keep the repo open" — ASSUMPTION (nothing upstream documents subscription lifetime as what retains a repo in
self.repos); observed to work, bounded by the same probe. - Defect — the header's mechanism claim is contradicted at the source (see Findings F2).
open-repo.ts:10-12says an anchoredsparql_queryon a repo absent fromself.repos"silently returns 0 rows (never aRepoNotFound)". Verified upstream: absence fromself.reposyieldsErr(NgError::RepoNotFound)(request_processor.rs:264,269), the ReadQuery arm converts it intoAppResponse::error(:1293-1296), and the web binding REJECTS the JS promise with it (sdk/js/lib-wasm/src/lib.rs:606). The observed 0-rows-no-error behaviour has two candidate explanations that the source does support: a persistent verifier reloads every known repo intoself.reposatVerifier::load(engine/verifier/src/verifier.rs:535-560) so the repo is present-but-unsynced (a genuine 0-row read), and/or the lib's own per-doc tolerance (readUserStore,surface/read-model.tsper-doc catch) converts a rejection into an empty result. The healed symptom is real and the fix correct; the stated mechanism is not established, and a maintainer reasoning from it would mispredict behaviour whenever the verifier is not persistent. SyncStateandgetSyncStateare lib-invented vocabulary — NO COUNTERPART (upstream has no consumer-facing "sync state of a repo" API at any level;OrmSubscription.readyPromise,sdk/js/orm/src/connector/GraphOrmSubscription.ts:260, is the closest level-3 signal, per-subscription not per-repo).- At migration the whole module becomes "open the store by cap at bootstrap" (native) and is removed with the shim.
7. The read filter — emulated-verifier/read-filter.ts
// read-filter.ts:47
export function filterReadable<T>(items: Iterable<T>, caps: CapRegistry): T[];
// read-filter.ts:60
export function makeReadFilteredView<S extends object>(set: S, caps: CapRegistry): S;
The polyfill of capability-based read access: a Proxy view over the reactive set keeping only items whose @graph document the current holder holds; applied by surface/use-shape.ts once caps.isEnforcing().
- NO COUNTERPART, by design — it stands in for cryptographic non-delivery. In the target the broker/verifier simply never yields what the wallet holds no cap for (targeted read errors,
request_processor.rs:264,269; union read yields nothing for undecrypted repos — the § 4 analysis ofdocs/api-contract.md). There is no post-hoc filter to migrate to; the module is deleted. - The
@graphkey it filters on is level 3, VERIFIED: the ORM annotates every object with its graph NURI (sdk/js/orm/src/frontendAdapters/react/useShape.ts:41,sdk/js/orm/src/model/types.ts:19). Items with no@graphare kept — a lib policy choice (they name no document), not an upstream rule. - Access unit = the DOCUMENT, not the item — faithful to the model (a ReadCap opens a repo,
types.rs:518-521), and the reason the filter is all-or-nothing per document.
8. Connection trigger — the unexported slice of emulated-verifier/connect.ts
// connect.ts:91
export function startConnect(): void;
Fire-and-forget wrapper over connectedUser() (internal since 2026-08-07, awaited inside ensureIdentity) (restore Links, then drain every inbox), called by setCurrentUser so inbox processing is the library's job, not the app's.
- LEVEL-1 SHAPE for the timing, VERIFIED: upstream the recipient's verifier processes inbox messages as they arrive, with no consumer call (
Verifier::inbox→process_inbox,engine/verifier/src/verifier.rs:1674-1690); firing on connection is the emulation's equivalent moment. The restore-before-drain order is a lib choice; upstream "restore" does not exist as a step (applied caps are already in the User branch replay). startConnectitself disappears at migration; the automatic-processing behaviour it fabricates is native.
9. The shim registry — the unexported slice of shared-wallet/account-registry.ts
The sharpest boundary case: surface/placement.ts publishes the 7 app-facing calls; the 9 exports below stay internal (importable by the lib's modules, unit tests and the e2e harness, not by an application through the package entries). The type RegistrySession is published by the entry (a consumer types its injected getSession with it) and covered by the surface contract; VirtualUserRecord is internal.
9a. Account shim — provision, resolve, reserved names, cache
// store-registry.ts:213
export function reservedAccount(name: string): string;
// store-registry.ts:278
export function resetRegistryCache(): void;
// store-registry.ts:542
export async function resolveAccount(id: string): Promise<VirtualUserRecord | null>;
// store-registry.ts:631
export async function ensureAccount(id: string): Promise<VirtualUserRecord>;
resolveAccount — barrier-authoritative O(1) lookup of one account's record in the doc-shim; ensureAccount — resolve-or-provision (creates the three scope docs on first sight, concurrency-deduped); reservedAccount — NUL-prefixed sentinel namespace for lib-internal accounts; resetRegistryCache — test/wallet-switch reset.
- NO COUNTERPART, shared-wallet machinery — the whole group. The target has no directory of identities to resolve or provision: a user's site (three stores + their inboxes) is created once at wallet creation (
engine/verifier/src/site.rs— the site-creation flow committing the stores and the two store-inboxAddInboxCaps at:128,149), and "which user" is the session.ensureAccount's provision-on-first-sight has no target analogue and is exactly whatconnectedUserrefuses to trigger (connect.ts:60-65). All of it disappears with the shim (docs/migration-guide.md§ 3). reservedAccount's collision-safety rests on an ASSUMPTION about a consumer-injected function: the comment (store-registry.ts:200-206) asserts the injectednormalizeIdcan never produce a U+0000-prefixed key, butnormalizeIdis injected by the consumer and the lib's own default is a baretrim()(shared-wallet/bootstrap.ts), which does not strip U+0000. Bound: a consumer id would have to begin with a literal NUL to collide — implausible from any UI, but the property is the consumer's to keep, not the lib's (see Findings F5).
9b. Scope-index resolution
// store-registry.ts:1079
export async function userStoreDoc(id: string, scope: Scope): Promise<Nuri>;
The scope-INDEX document of one account (what watchShape subscribes to for container changes). Same resolution as the published resolveWriteGraph, without doubling as a write target.
- Level 2, VERIFIED counterpart with the indirection removed: in the target the container IS the store, and its id is on the session (
Session.private_store_id | protected_store_id | public_store_id,index.d.ts:264-272); the listing a subscriber watches is the store'sldp:containsgraph, written natively bydoc_create(request_processor.rs:706-708). The function's question survives; the per-account parameter and the index-document indirection disappear.
9c. Inbox ownership predicates
// store-registry.ts:837
export async function isOwnInbox(nuri: Nuri): Promise<boolean>;
// store-registry.ts:1286
export async function myInboxes(): Promise<Nuri[]>;
isOwnInbox — the read guard's question (may the current identity READ this inbox); myInboxes — the drain list for connectedUser (own wallet inbox + one per document this user opened an inbox on).
- LEVEL-1 SHAPE for the underlying record, VERIFIED; NO COUNTERPART as API. Upstream "which inboxes may I read" is answered inside the verifier by the User branch's
AddInboxCaprecords (AddInboxCapV0 { repo_id, overlay, priv_key },engine/repo/src/types.rs:1969-1981, applied atverifier.rs:1916-1932) and consulted implicitly when a message arrives (verifier.rs:1674-1690); nothing suggests a JS API for the question at any level. Both functions exist only because the emulation must ANSWER it in JS (the read guard, the drain); they disappear with the emulated inbox read side.
9d. The durable Link register
// store-registry.ts:1307
export async function addLink(cap: ReadCap): Promise<void>;
// store-registry.ts:1331
export async function readLinks(): Promise<ReadCap[]>;
File / read back a received cap on the emulated User branch of the private store — what makes a received cap durable across sessions.
- LEVEL-1 SHAPE, model VERIFIED; no JS surface anywhere. The record emulated is
AddLink { read_cap }on the User branch — "so that a user can share with all its device a new Link they received", external repos only (engine/repo/src/types.rs:1934-1950). Upstream the filing happens inside the verifier while processing the inbox; the future SDK most likely never exposes these as calls (the surface contract's § 12 finding, restated here because these are now internal-only: the one caller isemulated-verifier/connect.ts/inbox.ts, which is exactly where upstream's verifier sits). - At migration both are deleted; the verifier files and replays.
10. The machinery namespace — emulated-verifier/machinery.ts
// machinery.ts:32
export const MACHINERY_NS = "urn:ng-eventually:";
// machinery.ts:40
export function isMachinerySubject(subject: string | undefined): boolean;
The URN prefix every triple the lib writes for itself lives under, and the one predicate read paths use to keep machinery out of consumer data.
- NO COUNTERPART, by design — the seam where the emulation pays for having no branches. Upstream the separation is structural: a compartment is a different BRANCH with its own CRDT and topic (
BranchType,engine/repo/src/types.rs:1536-1551; the Store/User/Overlay branches carry no triples at all,BranchCrdt::None,types.rs:1420), so machinery cannot appear in a content read and no subject filter exists to write. The namespace, the filter, and the fourshim:*compartment subjects it protects all disappear at migration.
11. Diagnostics — shared-wallet/access-log.ts and shared-wallet/outbox-log.ts
// access-log.ts:25,45,50,63,76,87,99,116
export type AccessOp = "READ" | "WRITE";
export function setAccessLog(on: boolean): void;
export function enabled(): boolean;
export function activeIdentity(): string;
export function accessLogPrefix(): string;
export function logStage(line: string): void;
export function shortNuri(nuri: string): string;
export function logAccess(op: AccessOp, nuri: string, label: string, extra?: string): void;
// outbox-log.ts:62
export function inspectOutbox(): void;
shared-wallet/access-log.ts — the off-by-default per-identity access trace for the shared-wallet isolation leak (toggled by configure({ debugAccessLog }) or NG_EVENTUALLY_ACCESS_LOG=1). shared-wallet/outbox-log.ts — a read-only count of the real SDK's offline write outbox at session bootstrap, warning when non-empty.
shared-wallet/access-log.ts— NO COUNTERPART, shared-wallet machinery: the leak it makes visible cannot exist in the target (isolation is per-wallet), and the "active identity" it prefixes is the relayed virtual id that disappears withsetCurrentUser. Deleted at migration.shared-wallet/outbox-log.ts— NO COUNTERPART as API, but every fact it relies on is level-2 VERIFIED in the clone: the outbox is persisted throughJsStorageConfig(sdk/rust/src/local_broker.rs:89-100), keyedng_peer_last_seq@<peerId>(:119,141) andng_outboxes@<peerId>@start/@<idx>with zero-padded{:05}indexes (:163-213, pad at:183,210); the realoutbox_read_functionDRAINS on read (session_delper key plus the start key,:218-224) — which is why the probe only counts and never touches; and the storage callbacks land in browsersessionStorage(sdk/js/api-web/main.ts:47,57,66), whose access-denied error string is the oneconvert_errorhandles (main.ts:18-22). The probe reads a private persistence format of the injected SDK — acknowledged in its header as out-of-contract, hence count-only. Deleted with the rest of the trace instrumentation at migration.
Findings — defects and migration risks
F1 — surface/ng-proxy.ts fabricates a login member the real SDK does not have. ng-proxy.ts:16-22 intercepts prop === "login", but @ng-org/web exports no login (none in index.d.ts, re-verified against the full declare function list; no fn login in sdk/js/lib-wasm/src/lib.rs). On the wrapper ng.login is a function; on the real SDK it is undefined; calling it throws. This contradicts the module's own "surface stays identical" header and docs/api-contract.md § 3's "the proxy adds no member and removes none". No target layer names a login — the arm is an unprovenanced assumption. Cheap fix: drop the login case (keep session_start), or gate it on typeof ng.login === "function".
F2 — emulated-verifier/open-repo.ts's stated mechanism is contradicted at the source. The header (open-repo.ts:10-12) asserts an anchored sparql_query on a repo absent from self.repos "silently returns 0 rows (never a RepoNotFound)". Upstream, absence from self.repos errors RepoNotFound (engine/verifier/src/request_processor.rs:264,269), the ReadQuery arm wraps it as AppResponse::error (:1293-1296), and the web binding rejects the JS promise (sdk/js/lib-wasm/src/lib.rs:606). The observed behaviour is real but its cause is one (or both) of: the repo WAS in self.repos (a persistent verifier reloads all known repos at Verifier::load, engine/verifier/src/verifier.rs:535-560) and read 0 rows because unsynced; or the lib's own catch-and-continue layers absorbed the rejection. The fix (open before reading) is correct either way; the diagnosis in the header should not be relied on, and mispredicts non-persistent-verifier behaviour.
F3 — incomplete citation in subscribe.ts. subscribe.ts:31 cites the ORM fan-out abort as "initialize.rs:125-128" with no path. The file is engine/verifier/src/orm/graph/initialize.rs; lines 125-128 are the graph loop calling self.open_for_target(&nuri.target, true).await? — verified, the ? propagates RepoNotFound and aborts the whole subscription. Substance correct; the bare filename is unfindable without this note.
F4 — docs/api-contract.md lags the surface/placement.ts split. Its § 12 and appendix still list resolveAccount, ensureAccount, userStoreDoc, isOwnInbox, myInboxes, addLink, readLinks, reservedAccount, resetRegistryCache as the SDK entry's storeRegistry namespace, and § 13/§ 15 place virtualUsers.* on the SDK entry — since the split (index.ts routes through surface/placement.ts) those are internal. That file is being edited concurrently; noted here, deliberately not fixed by this document.
F5 — reservedAccount's collision guarantee is asserted about code the lib does not own. store-registry.ts:200-206 states the injected normalizeId "strips a leading @, trims, and lowercases, so a NUL prefix is unreachable" — that describes ONE consumer's normalizer, not a contract; the lib's own default is id.trim() (shared-wallet/bootstrap.ts), which passes U+0000 through. The reserved namespace is disjoint only if every consumer's normalizer keeps it so. Either document the requirement on StoreRegistryDeps.normalizeId, or reject NUL-prefixed raw ids at accountKey.
Migration-risk flags (shapes that will not travel):
- The
(document, inbox)pair is persisted as a space-joined string literal ("${doc} ${inbox}", writtenstore-registry.ts:1174, parsed bysplit(" ")at:1268). Upstream the record is the typedAddInboxCapV0 { repo_id, overlay, priv_key }(engine/repo/src/types.rs:1969-1981). Internal-only and replaced wholesale at migration, but it is the one shim record with an ad hoc micro-format a future reader must know to parse. isOwnInbox/myInboxesencode questions the target answers only inside the verifier (§ 9c) — any new internal caller added to them deepens a dependency that has no successor API; keep callers to the read guard and the connection drain.- The sync barrier is empirical (§ 6): "TabInfo before the first State" and "held subscription keeps the repo open" are pinned by the in-repo e2e probe, not by any upstream statement. If upstream changes push ordering or repo retention,
emulated-verifier/open-repo.tsis the module that breaks first; the probe is the tripwire. ensureAccount's provision-on-first-sight (§ 9a) is a behaviour with no target image;emulated-verifier/connect.tsalready refuses to trigger it. Any future internal path that provisions as a side effect of resolving would be teaching the emulation something the target contradicts (creation is an explicit act at wallet/site creation,engine/verifier/src/site.rs).
Appendix — full internal export inventory (for diffing)
Fully internal modules: shared-wallet/access-log.ts (AccessOp, setAccessLog, enabled, activeIdentity, accessLogPrefix, logStage, shortNuri, logAccess); emulated-verifier/machinery.ts (MACHINERY_NS, isMachinerySubject); surface/ng-proxy.ts (makeNg); emulated-verifier/open-repo.ts (SyncState, setOpenTimeoutForTests, resetOpenedRepos, getSyncState, ensureRepoOpen, ensurePhysicalRepoOpen, ensureReposOpen); shared-wallet/outbox-log.ts (inspectOutbox); shared-wallet/physical.ts (physicalCreate, physicalQuery, physicalUpdate); emulated-verifier/reach.ts (declareInfrastructure, isInfrastructure, resetInfrastructure, mayReach, assertMayReach, mustNotAttempt); emulated-verifier/read-filter.ts (filterReadable, makeReadFilteredView).
Internal slices of partially-published modules: nuri.ts (targetOf, parseNuri, mintCap); emulated-verifier/connect.ts (startConnect); subscribe.ts (subscribePhysicalDoc); shared-wallet/account-registry.ts (reservedAccount, resetRegistryCache, resolveAccount, ensureAccount, userStoreDoc, isOwnInbox, myInboxes, addLink, readLinks).
Modules with no internal exports (everything they export is published): types.ts, docs.ts, inbox.ts, surface/read-model.ts, shared-wallet/virtualUsers.ts, emulated-verifier/caps.ts, sparql.ts, lifecycle.ts, surface/use-shape.ts, surface/watch-shape.ts, surface/placement.ts, and the entry point.