Trois reponses coexistaient et pouvaient se contredire : un ensemble de souscriptions bootstrap et une carte d'etat de barriere dans `open-repo`, et les auditeurs du fan-out dans `surface/subscribe`. Les deux premieres survivaient a la disparition de la troisieme, parce que `resetDocSubscriptions()` - ce que fait le `configure()` publie - largue ses auditeurs sans rien dire a personne. Il n'en reste qu'une, l'etat de l'unique souscription reelle du document, exposee par `docChannel(nuri)`. `open-repo` n'enregistre plus que l'auditeur qu'il a pose - « est-ce que j'ecoute encore », rien de plus - et derive `getSyncState` a chaque lecture, si bien qu'aucune reponse ne peut survivre au canal qu'elle decrit. Disparaissent : `held`, `syncState`, `inFlight`, `settleInFlight` et le compteur de generation, remplace par l'identite de l'enregistrement. Les deux echecs prouves deviennent des tests qui echouent sans le changement : un second `configure()` apres une synchro reussie (`"synced"` au lieu de `"timed-out"`, zero re-souscription, et `isSuccess` cote application), et un etablissement encore en cours quand la couche de souscription est demontee - cas que le canal `onSetupFailed` ne peut pas rattraper, puisqu'il ne rapporte que la tentative encore attendue. La ligne qui ne casse pas : un document synchronise qui ne contient rien repond toujours un succes a zero objet. `1.0.0-dev.10`, les deux non-garanties nommees sont retirees et la garantie livree porte le test qui l'epingle.
36 KiB
Internal contract — what @ng-eventually/polyfill 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(itself removed in1.0.0-dev.8);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/polyfill/src/ that is NOT reachable from the published entry point (package.json maps exactly . → src/index.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 (removed in 1.0.0-dev.8, see below), 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. Nine 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, and — since 1.0.0-dev.8, when readUnion and UnionSubject left the entry — surface/read-document.ts (the former surface/read-model.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
export function makeNg(): NG;
export function wrappedNg(): NG; // the memoised one - THE instance, since 1.0.0-dev.7
Builds the ng Proxy: forwards every property to the injected real ng, overriding session_start (passthrough with a shared-wallet-credentials TODO), sparql_update (the emulated write-cap guard) and, since 1.0.0-dev.7, graph_orm_update (the ORM's own write door - ownership, assertMayWrite).
- 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. wrappedNg()exists because the proxy now has two holders:index.tspublishes it, andsurface/lifecycle.tshands it to the ORM throughinitNg, so the two must be the same object. It keeps no state (every property is answered fromgetConfig()at access time), so it survives a re-configure()and needs no reset.- 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. It is deliberately left on the decorative write-cap list rather than onassertMayWrite: the library's own REGISTERS are written through this member (emulated-verifier/register-write.ts), and a store document is not owned in the authorship sense, so arming it there would refuse every register write.graph_orm_updatehas no such traffic, which is why the live predicate sits on it. Defect - theFIXED 2026-08-03, and this bullet had outlived it:loginarm fabricates a member (Findings F1).@ng-org/webhas nologin- none among the exports ofindex.d.ts, nofn logininsdk/js/lib-wasm/src/lib.rs- and the arm that returned a function forprop === "login"was the one place the proxy added a member, against its own header and the surface contract's "adds no member and removes none" (§ 3). It is gone fromng-proxy.ts; the note stays as the record of what the proxy must never do again.- Defect - the passthrough could not survive the real
ng(FIXED1.0.0-dev.7). It wasReflect.get(ng, prop).bind(ng), and@ng-org/web'sngis itself a Proxy whosegettrap answers every key -bindincluded - with a path-carrying callable returning a Promise. So the publishednghanded back a promise for all 88 members and a caller got<member> is not a function. Invisible because the library's own internals bypass this proxy and every fake in the suite is a plain object. NowReflect.apply(real, ng, args); the stand-in intest/ng-proxy.test.tsis shaped like the real proxy, which is what makes the test one. - Disappears at migration (the whole module).
2. NURI internals — the unexported slice of nuri.ts, plus the minting point
// model/nuri.ts
export function targetOf(nuri: Nuri): Nuri;
export function parseNuri(nuri: Nuri): { target: Nuri; readCap?: ReadCap };
// emulated-verifier/caps.ts — NOT `model/nuri.ts`; it moved when the source layout was
// reorganised by migration fate, and this block said `nuri.ts` until 2026-08-10.
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 (STAND_IN_CAP). Kept off the surface deliberately: nothing published turns a bare reference into a cap.
targetOf/parseNuri— LEVEL-1 SHAPE, model VERIFIED: they transcribe upstream's one-type-with-optional-access NURI, on the two fields this library uses. Not a "1:1 mirror" ofNuriV0, as this line claimed until 2026-08-10: that type has TEN fields —identity, target, entire_store, objects, signature, branch, overlay, access, topic, locator(engine/net/src/app_protocol.rs:181-194) — of whichparseNuricarriestargetand the cap half ofaccess. The other eight have no counterpart here (the missinglocatoris a stated gap,docs/readcap-and-nuri-model.md§ 4sexies). 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 cap-enforcement has not yet supplied real key material; the constantOKpretends nothing (STAND_IN_CAP,emulated-verifier/caps.ts). It has exactly two call sites (shared-wallet/account-registry.tscreateEntityDoc;emulated-verifier/caps.tsinternals) — the minting points of the emulation. With cap-enforcement 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:98
export type SyncState = "syncing" | "synced" | "timed-out";
// open-repo.ts:197 - TEST-ONLY
export function setOpenTimeoutForTests(ms: number): void;
// open-repo.ts:204
export function resetOpenedRepos(): void;
// open-repo.ts:252 - DERIVED, nothing stores it
export function getSyncState(nuri: Nuri): SyncState | "unknown";
// open-repo.ts:315
export async function ensureRepoOpen(nuri: Nuri, holderKey?: string): Promise<void>;
// open-repo.ts:339 - the unguarded core, for ONE importer (`shared-wallet/physical.ts`)
export async function openRepoUnguarded(nuri: Nuri): Promise<void>;
Corrected 2026-09-21 (
1.0.0-dev.9), for this module only. The listing above used to name anensurePhysicalRepoOpenexport that this module has never had - it lives inshared-wallet/physical.ts:134, the machinery's door - and anensureReposOpen(nuris)batch that was deleted as dead code. It also missedopenRepoUnguardedandensureRepoOpen's second parameter. The rest of this document carries drift of the same age and was deliberately left alone.
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; SyncState keeps synced and timed-out apart.
Corrected 2026-09-24 (
1.0.0-dev.10), for this module only: the line numbers above, and the fact that nothing here STORES openness or the barrier any more. This module kept a set of live bootstrap subscriptions and a per-nuri sync-state map, and both outlived the subscription they described -surface/subscribe.tsdrops its listeners without a word, whichconfigure()makes it do. There is one answer now,surface/subscribe.ts'sdocChannel(nuri), andgetSyncStatederives from it on every call; what this module records is the bootstrap listener it took out, which answers "am I still listening" and nothing more.
- 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. ensureRepoOpenTHROWS when the public store could not be asked (1.0.0-dev.9). It callsemulated-verifier/public-store.ts'sfetchReadCapfirst - a document in a public store hands its key to whoever asks, so the question has to be put before the boundary decides - and that call used to answerfalsefor a download that had REJECTED, which is the same value a document exposing no key answers with. The open then declined silently and the anchored read that followed refused the reader for holding no key: a true statement, made out of an outage, and the wrong one to act on. The rejection now reaches the caller. The open itself still never throws (adoc_subscribethat never pushes is bounded by the fallback timeout), andholderKeyskips the fetch entirely, so the processor's door is unaffected.- Defect, SINCE REPAIRED in the code — the header's mechanism claim was contradicted at the source (see Findings F2). The header was corrected on 2026-08-03 and now states the mechanism below itself; what follows is kept as the record of what was wrong and how it was established.
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, and thesurface/read-model.tsper-doc catch until1.0.0-dev.8removed it) 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
// REMOVED in `1.0.0-dev.6`
// export async function userStoreDoc(id: string, scope: Scope): Promise<Nuri>;
The scope-INDEX document of one account, which the reactive shape read subscribed to for container changes. That read goes through the SDK's own shape query since 1.0.0-dev.6, which follows the data rather than a container this package maintains, so the function lost its only caller and went with it. The ruling below is kept for the record, because the question it answers is the one the target closes.
- 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, 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, wrappedNg); 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: model/nuri.ts (targetOf, parseNuri) and emulated-verifier/caps.ts (mintCap); emulated-verifier/connect.ts (startConnect); subscribe.ts (subscribePhysicalDoc); shared-wallet/account-registry.ts (reservedAccount, resetRegistryCache, resolveAccount, ensureAccount, isOwnInbox, myInboxes, addLink, readLinks).
Modules with no internal exports (everything they export is published): types.ts, docs.ts, inbox.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.