docs: reprendre les citations après le rangement, et retirer le membre login fabriqué

Les deux contrats et la doctrine portent des centaines de citations `file:line`
vers `src/`. Le rangement par destin les périmait en bloc — 208 reprises (45
chemins `src/x.ts`, 163 mentions nues). Sans ça la réorganisation pourrissait
l'instrument même qui tient la discipline qu'elle sert.

Et un défaut relevé par le contrat interne, vérifié : `ng-proxy` fabriquait un
membre `login`. `@ng-org/web` n'expose aucune méthode de ce nom — zéro
occurrence dans les déclarations installées comme dans `sdk/js/lib-wasm/src/lib.rs`
— mais le proxy répondait une fonction au lieu d'`undefined`, laquelle plantait
à l'appel. C'était le seul endroit où ce wrapper ajoutait à la surface du SDK,
contre son propre en-tête.

157 tests unitaires, typecheck src/test/e2e vert.
This commit is contained in:
Sylvain Duchesne
2026-08-04 12:56:02 +02:00
parent cd096de2b0
commit 0b37d17c2f
19 changed files with 151 additions and 145 deletions
+10 -10
View File
@@ -51,17 +51,17 @@ is needed), and how this lib emulates it today.
| Capability | What the consumer application does | Real NextGraph target | Current NextGraph status (why a workaround) | Current emulation |
|---|---|---|---|---|
| Multi-identity / per-identity wallet | Treats each identity id as its own wallet with its own documents | Each identity opens its own real wallet; native cross-wallet reads | Not-yet-implemented: the JS SDK exposes no cross-wallet read, so one session cannot read another identity's wallet | One shared wallet everyone opens; "identities" are virtual users — shim accounts keyed by an id, each mapped to its documents in `store-registry.ts` |
| Multi-identity / per-identity wallet | Treats each identity id as its own wallet with its own documents | Each identity opens its own real wallet; native cross-wallet reads | Not-yet-implemented: the JS SDK exposes no cross-wallet read, so one session cannot read another identity's wallet | One shared wallet everyone opens; "identities" are virtual users — shim accounts keyed by an id, each mapped to its documents in `shared-wallet/account-registry.ts` |
| Three native stores per identity | Places entities by scope `public` / `protected` / `private` | The identity's three real native stores hold the entity documents | Not-yet-implemented: `doc_create`/ORM can target only the private (and protected) native store today; a `public`/arbitrary `StoreRepo` is not JS-constructible | Three emulated scope-index documents per account — each "store" is an index doc listing its entity-doc NURIs; all physically live in the one shared private store, and scope is a logical label |
| Per-document read isolation | Nothing to declare: creating a document records its cap on its store, and its creator holds it. Reading is `capFor(doc)` — you hold the key or you do not read | The broker/verifier delivers only documents the wallet holds a ReadCap for; accessing a document without the cap yields an empty result in a union read (a targeted read of an unheld repo errors with `RepoNotFound`) | The model itself is the point: reading is key possession, and there is no read-ACL to introspect — a client cannot ask "may this identity read this doc?" because that question does not exist upstream | Caps recorded per identity: `AddRepo` on the store's emulated Store branch for documents it creates, `AddLink` on its User branch for caps received; `caps.ts` caches them for the session. A read filter (`read-filter.ts`) plus the boundary (`reach.ts`) keep only documents whose cap is held. The cap value is the stand-in `OK` — enforcement is P1b |
| Per-document read isolation | Nothing to declare: creating a document records its cap on its store, and its creator holds it. Reading is `capFor(doc)` — you hold the key or you do not read | The broker/verifier delivers only documents the wallet holds a ReadCap for; accessing a document without the cap yields an empty result in a union read (a targeted read of an unheld repo errors with `RepoNotFound`) | The model itself is the point: reading is key possession, and there is no read-ACL to introspect — a client cannot ask "may this identity read this doc?" because that question does not exist upstream | Caps recorded per identity: `AddRepo` on the store's emulated Store branch for documents it creates, `AddLink` on its User branch for caps received; `emulated-verifier/caps.ts` caches them for the session. A read filter (`emulated-verifier/read-filter.ts`) plus the boundary (`emulated-verifier/reach.ts`) keep only documents whose cap is held. The cap value is the stand-in `OK` — enforcement is P1b |
| Directed read sharing | Owns the relationship concept ("who is connected to whom") itself, and on acceptance shares one document's cap to the other's inbox (`shareCap(cap, theirInbox)`) | The cap sealed to the recipient's inbox key (`ContactDetails.read_cap`), opened by their own verifier while processing the inbox | Not-yet-implemented — a **gap, not a disagreement**: the field exists but the message construction is `unimplemented!()`, its only caller passes "without read_cap", and the receiver discards the cap. The shape is right; the implementation is absent | `shareCap` deposits the cap into the recipient's inbox document; the recipient's existing `inbox.watch` absorbs it into what they hold. No "receive" operation, and no principal is ever named to the registry |
| Inbox (registration notifications) | `inbox.post` / `read` / `watch` | A message is sealed to the recipient's key and queued in their inbox; the recipient's own verifier unseals and applies each queued message inline while processing the inbox | Not reachable from JS: the verifier has no `InboxPost` arm, and no `inbox` method exists in `@ng-org/web`. (`inbox_post_link` is OUR proposed name from [`docs/fork-inbox-fallback.md`](docs/fork-inbox-fallback.md), not an announced NextGraph API — no such symbol exists in `nextgraph-rs`.) | Deposits written as RDF into an inbox document via SPARQL; `read`/`watch` read the deposits back — an in-lib stand-in for the recipient's own inbox processing |
| ~~Discovery of all public events~~ **REMOVED 2026-07-30** | Circulates the link itself — into inboxes, or into a document the reader already holds | **There is no discovery.** You cannot discover, you can only follow links: publishing = place the data in your public store **and** circulate the link, seen only by those who received it (a foundation of local-first) | Not a gap to be filled — a global index is not a NextGraph shape, and it would pool data across wallets | Nothing. `discovery.ts` and its global index were removed: they emulated a capability the target will never have. See [`docs/readcap-and-nuri-model.md`](docs/readcap-and-nuri-model.md) §4ter-bis |
| Reads / listing | Lists the documents it needs, by scope, and reads them | Native per-wallet reads over the real per-identity stores | Bug/perf: an anchorless union query spans every named graph in the session store, which on a shared / accumulating wallet is O(wallet size) and stalls | A bounded, by-need set of per-doc anchored `sparql_query`s (each anchored to one repo's default graph), independent of wallet size |
| Reactivity | Lists update on change | Native reactive reads | Not-yet-implemented: there is no reactive union query across graphs | Re-query the bounded per-doc anchored set on a lightweight change signal (`doc_subscribe` / ORM on an already-opened single store) |
| Writes | Writes an entity to its scope | Writes land in the entity's real store via native primitives | Not-yet-implemented: `doc_create` can target only the private/protected store today (`StoreRepo` not JS-constructible) | Per-entity documents via direct SPARQL (`docs.sparqlUpdate` on the real injected `ng`) |
| Current identity | Sets the current identity id (established at wallet import) via the SDK's current-identity call | Opening one's own wallet at the broker gate establishes the session identity | Not-yet-implemented for the shared-wallet case: everyone shares one wallet, so the broker cannot distinguish identities | A relayed id (`accounts.ts` `IdentityStore` persists it); the read filter and inbox `from` read it |
| Write-guard | Writes refused without the write cap | The broker/verifier enforces the write cap natively | Partial: the guard fires only on the public proxy, but the real write paths call the injected `ng` directly (the `DataCloneError` constraint), so it is best-effort today | A `sparql_update` override (`ng-proxy.ts`) checking the emulated write cap |
| Current identity | Sets the current identity id (established at wallet import) via the SDK's current-identity call | Opening one's own wallet at the broker gate establishes the session identity | Not-yet-implemented for the shared-wallet case: everyone shares one wallet, so the broker cannot distinguish identities | A relayed id (`shared-wallet/accounts.ts` `IdentityStore` persists it); the read filter and inbox `from` read it |
| Write-guard | Writes refused without the write cap | The broker/verifier enforces the write cap natively | Partial: the guard fires only on the public proxy, but the real write paths call the injected `ng` directly (the `DataCloneError` constraint), so it is best-effort today | A `sparql_update` override (`surface/ng-proxy.ts`) checking the emulated write cap |
## Packages
@@ -175,17 +175,17 @@ the unused list" is not a reason to investigate it.
Implemented. The polyfill mechanisms are wired against a real broker, not stubbed:
- Shared-wallet shim — `store-registry.ts` (`(account, scope) → document NURI`,
- Shared-wallet shim — `shared-wallet/account-registry.ts` (`(account, scope) → document NURI`,
`createEntityDoc` / `listMyEntityDocs` + per-user stores, cross-device via the RDF
shim anchored in the private store).
- Document / SPARQL primitive — `docs.ts`, calling the real injected `ng` directly
(avoids the `@ng-org` double-proxy `DataCloneError`).
- Emulated ReadCaps — `caps.ts` (`CapRegistry`, per-document, directed grants) +
read filter `read-filter.ts` (reactive-set `Proxy` view), applied by
`use-shape.ts` only once a cap exists (`caps.isEnforcing()`).
- Write guard — `ng-proxy.ts` (`sparql_update` override, emulated write cap).
- Emulated ReadCaps — `emulated-verifier/caps.ts` (`CapRegistry`, per-document, directed grants) +
read filter `emulated-verifier/read-filter.ts` (reactive-set `Proxy` view), applied by
`surface/use-shape.ts` only once a cap exists (`caps.isEnforcing()`).
- Write guard — `surface/ng-proxy.ts` (`sparql_update` override, emulated write cap).
- Inbox — `inbox.ts` (`post` / `read` / `materialize` / `watch`).
- Identity — `accounts.ts` (`IdentityStore`, injected storage).
- Identity — `shared-wallet/accounts.ts` (`IdentityStore`, injected storage).
- SPARQL hardening — `sparql.ts` (`escapeLiteral` / `escapeIri` / `assertNuri`).
The remaining `TODO` markers are narrow: the shared-wallet credential passthrough
+3 -3
View File
@@ -143,7 +143,7 @@ const useShape = <T extends BaseType>(
scope: Scope | string | undefined
) => DeepSignalSet<T>;
// its Scope — sdk/js/orm/src/types.ts:25-38 (NOT this lib's Scope, see § 12)
// its Scope — sdk/js/orm/src/model/types.ts:25-38 (NOT this lib's Scope, see § 12)
export type Scope = {
graphs?: string[] | string;
subjects?: string[];
@@ -184,7 +184,7 @@ export function watchShape<T = UnionSubject>(
### Target
**Partly ASSUMPTION — flagged deliberately.** `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:
**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
@@ -462,7 +462,7 @@ export function assertNuri<T extends string>(nuri: T): T;
### Today — `@ng-eventually/client` (namespace `storeRegistry`) — plus `Scope` from `types.ts`
> **Narrowed 2026-08-03.** The entry used to re-export the WHOLE `store-registry` module. It now re-exports an app-facing slice (`src/store-registry-api.ts`): `createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `walletInbox`, `openDocumentInbox`, `documentInboxAddress`. The rest — `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `resolveAccount`, `ensureAccount`, `reservedAccount`, `resetRegistryCache`, and the `AccountRecord` / `RegistrySession` types — is **no longer importable from `@ng-eventually/client`** and is covered by `docs/internal-contract.md`. The signatures below are kept for the record, marked accordingly.
> **Narrowed 2026-08-03.** The entry used to re-export the WHOLE `store-registry` module. It now re-exports an app-facing slice (`src/surface/placement.ts`): `createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `walletInbox`, `openDocumentInbox`, `documentInboxAddress`. The rest — `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `resolveAccount`, `ensureAccount`, `reservedAccount`, `resetRegistryCache`, and the `AccountRecord` / `RegistrySession` types — is **no longer importable from `@ng-eventually/client`** and is covered by `docs/internal-contract.md`. The signatures below are kept for the record, marked accordingly.
```ts
// types.ts:38 — NB: NOT the ORM's Scope (a graphs/subjects filter); this is the store scope
@@ -4,7 +4,7 @@
## Problem
`caps.ts` emulates read rights as an **ACL** (`Map<Nuri, Set<PrincipalId>>`, `grantRead(doc, grantee)`) — **the inversion** of the real NextGraph model (key possession). Consequences: no notion of a **cap-less reference**, grant/revocation **instantaneous and total** (instead of durable sealing + re-key), and an API (`declareConnections`) that consumers have to **re-declare every session**. This divergence makes it impossible to properly build models that rest on the real semantics — in particular **anonymous presence** (naming/counting without reading).
`emulated-verifier/caps.ts` emulates read rights as an **ACL** (`Map<Nuri, Set<PrincipalId>>`, `grantRead(doc, grantee)`) — **the inversion** of the real NextGraph model (key possession). Consequences: no notion of a **cap-less reference**, grant/revocation **instantaneous and total** (instead of durable sealing + re-key), and an API (`declareConnections`) that consumers have to **re-declare every session**. This divergence makes it impossible to properly build models that rest on the real semantics — in particular **anonymous presence** (naming/counting without reading).
## Objective: shape-fidelity, NOT security
@@ -113,7 +113,7 @@ Erroneous content kept below as a record:
**Extracted into its own brief: [`2026-07-27-p1a-cap-surface.md`](2026-07-27-p1a-cap-surface.md), which records what landed where.**
The ACL inversion — *the central defect this whole chantier exists to fix* — is gone: `caps.ts` is a keyring, sharing is a per-document delivery to an inbox, and a bare reference reads nothing. **P1b is now the blocker for any privacy claim**: the emulated key is derived (hence reproducible) and the bypass inventory below is untouched.
The ACL inversion — *the central defect this whole chantier exists to fix* — is gone: `emulated-verifier/caps.ts` is a keyring, sharing is a per-document delivery to an inbox, and a bare reference reads nothing. **P1b is now the blocker for any privacy claim**: the emulated key is derived (hence reproducible) and the bypass inventory below is untouched.
In two lines: a single new type (`ReadCap`), a keyring (`capFor`), a per-document share to an inbox (`shareCap`) — and nothing else. The branded types, `resolveCapLess`, `receivedCaps`, `refOf`, `parseNuri` and `PrincipalId` were **discarded** after a double adversarial review; the reasons are in that note.
@@ -142,4 +142,4 @@ An adversary refuted the brief (7 findings — the 7th marked *(Plausible)*). **
**Consequence**: add **P0 (keyless-fetch spike)** up front and a **distinct WriteCap track**; requalify P2 (the real content = durability + cap-less + re-sharing, not "inverting the ACL"); record that **without crypto, read privacy is not applicable** (choose: real crypto vs masked projection).
Links: `readcap-and-nuri-model.md`, `packages/client/src/caps.ts`. On the consumer side, the Festipod brief "realign the sign-ups" depends on this effort.
Links: `readcap-and-nuri-model.md`, `packages/client/src/emulated-verifier/caps.ts`. On the consumer side, the Festipod brief "realign the sign-ups" depends on this effort.
+10 -10
View File
@@ -11,7 +11,7 @@ The spec below is unchanged — read it first. Everything from here to *Why this
> **Superseded in places by later lots — read with [`2026-07-30-virtual-wallet-boundary.md`](2026-07-30-virtual-wallet-boundary.md).** This report was accurate on 2026-07-28 and is kept as written; four of its statements have since been overtaken, and a fifth was wrong when written:
>
> - *"ReadCaps are NOT persisted as caps anywhere. There is no key store"* — **no longer true.** Both durable registers are now emulated: `shim:readCap` on the store's Store branch (`AddRepo`) and `shim:link` on its User branch (`AddLink`). Caps are read back, not recomputed.
> - *"Processing inboxes … Not started"* — **done** (`src/connect.ts`), at both levels, including per-document inboxes.
> - *"Processing inboxes … Not started"* — **done** (`src/emulated-verifier/connect.ts`), at both levels, including per-document inboxes.
> - *"`Nuri` and `ReadCap` are plain strings"* — **superseded the same week**: they are template literal types, so the confusion the runtime guard catches is now also a compile error. The *Typing* section below records the change; the earlier sentences were not rewritten.
> - The `:k:` segment throughout — **a ReadCap is `r:`** (`BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`), reported by NextGraph's developer and verified. `:k:` belongs to objects, files and commits.
> - *"That branch lists the store's documents… It is the owner's keyring. Upstream, the keyring is the wallet"* — **wrong when written**, and it is the sentence that produced a global in-memory "keyring". There is no keyring object; the wallet holds one root key per user. See [`../readcap-and-nuri-model.md`](../readcap-and-nuri-model.md) §4quater. The word *keyring* is left standing everywhere below because this report is kept as written; read it as *"what the holder holds"*, which is what the code now calls it.
@@ -22,13 +22,13 @@ The spec below is unchanged — read it first. Everything from here to *Why this
| Spec | Where |
|---|---|
| `Nuri` / `ReadCap` (plain strings, `:r:` discriminant) | `packages/client/src/types.ts`, `src/nuri.ts` (internal parse/mint/derive) |
| Keyring, one per identity — `capFor` | `src/caps.ts` (`CapRegistry`), surfaced as `capFor` in `src/polyfill.ts` |
| Caps of my OWN documents (the emulated `AddRepo { read_cap }`) | `src/store-registry.ts` `fileOwnCaps`, called from `createEntityDoc` and `listMyEntityDocs` |
| `shareCap(cap, toInbox)` + reception with no dedicated operation | `src/inbox.ts` (`shareCap`, and the inline absorption in `read`) |
| `publishRepoLink` | `src/caps.ts`. *(The published-only guard it fed lived in `src/discovery.ts`, removed 2026-07-30 — see the boundary brief.)* |
| Possession gate on reads | `src/read-model.ts` (`readUnion`), `src/read-filter.ts`, `src/use-shape.ts` |
| Cap-mutation signal (a delivered cap re-triggers reads) | `CapRegistry.onChange``src/watch-shape.ts` |
| `Nuri` / `ReadCap` (plain strings, `:r:` discriminant) | `packages/client/src/model/types.ts`, `src/model/nuri.ts` (internal parse/mint/derive) |
| Keyring, one per identity — `capFor` | `src/emulated-verifier/caps.ts` (`CapRegistry`), surfaced as `capFor` in `src/polyfill.ts` |
| Caps of my OWN documents (the emulated `AddRepo { read_cap }`) | `src/shared-wallet/account-registry.ts` `fileOwnCaps`, called from `createEntityDoc` and `listMyEntityDocs` |
| `shareCap(cap, toInbox)` + reception with no dedicated operation | `src/surface/inbox.ts` (`shareCap`, and the inline absorption in `read`) |
| `publishRepoLink` | `src/emulated-verifier/caps.ts`. *(The published-only guard it fed lived in `src/discovery.ts`, removed 2026-07-30 — see the boundary brief.)* |
| Possession gate on reads | `src/surface/read-model.ts` (`readUnion`), `src/emulated-verifier/read-filter.ts`, `src/surface/use-shape.ts` |
| Cap-mutation signal (a delivered cap re-triggers reads) | `CapRegistry.onChange``src/surface/watch-shape.ts` |
| Acceptance test (§8) | `test/cross-user-access.test.ts` (see below); isolation end-to-end in `test/isolation-active.test.ts`. *Originally `test/watch-shape.test.ts` (e), on the discovery fold — dropped 2026-07-30 with `discovery.ts`; the property it proved is covered on the model's own terms by the cross-user scenario.* |
| Cross-user scenario (§5 non-recursiveness) | `test/cross-user-access.test.ts` — see below |
@@ -192,7 +192,7 @@ Not started. It changes the consumer contract in the right direction (one less o
- **Unit suite green — 146 tests**, typecheck clean on `src`, `test` and the e2e harness.
- The typing was verified from a **consumer's** point of view, not just the library's: a synthetic app compiled against the entry points shows the two real mistakes (`shareCap(bareNuri, …)` and passing a raw `string` from storage) as compile errors, while every correct path — `capFor(doc)``shareCap(cap, inbox)`, and narrowing with the exported guards — needs no cast.
- The acceptance test was **mutation-checked**: reverting both gardes (the discovery fold and the `readUnion` possession gate) makes `watch-shape.test.ts` (e) fail with the bare-referenced document reappearing. The test has teeth.
- **The e2e ran against the live broker (`nextgraph.eu`) on 2026-08-03 — 39 passed, 0 failed.** The first run was 22/8, and the eight refusals were not test noise: they exposed a **real hole in the surface**. `docs.docCreate` filed no cap for the creator, so a consumer could create a document through the public primitive and then be refused reading or writing it. Upstream that cannot happen — `doc_create` commits `AddRepo { read_cap }` to the store's Store branch, so the creator holds it from the first instant. Fixed at `packages/client/src/docs.ts:73`, and deliberately NOT replicated in `physical.ts`: the shim's own documents belong to no user, and `store-registry` files their caps where it knows whose they are. The remaining failures were the harness acting as a second identity without establishing it (`createEntityDoc(id, …)` with someone else connected) or reading an arbitrary document as an inbox; both are now `setCurrentUser` + `walletInbox`, which is what a consumer must do too.
- **The e2e ran against the live broker (`nextgraph.eu`) on 2026-08-03 — 39 passed, 0 failed.** The first run was 22/8, and the eight refusals were not test noise: they exposed a **real hole in the surface**. `docs.docCreate` filed no cap for the creator, so a consumer could create a document through the public primitive and then be refused reading or writing it. Upstream that cannot happen — `doc_create` commits `AddRepo { read_cap }` to the store's Store branch, so the creator holds it from the first instant. Fixed at `packages/client/src/surface/docs.ts:73`, and deliberately NOT replicated in `shared-wallet/physical.ts`: the shim's own documents belong to no user, and `store-registry` files their caps where it knows whose they are. The remaining failures were the harness acting as a second identity without establishing it (`createEntityDoc(id, …)` with someone else connected) or reading an arbitrary document as an inbox; both are now `setCurrentUser` + `walletInbox`, which is what a consumer must do too.
- **An e2e run against a persistent wallet must use a FRESH identity per run.** The second run was green and the third was not, on unchanged code: moving the inbox tests onto `walletInbox(id)` made the inbox *stable for its owner* — which is the point of an inbox — so a fixed id accumulates every past run's deposits and `deposits.length === 2` drifts to 4. Green-then-red on identical code is the tell. The disposable thing is the **user**, not the inbox: `run.ts` now stamps `@inbox-user-`/`@watcher-`/`@friend-` with `Date.now()`, as it already did for `@alice-`. Any future step that resolves a durable per-user document (inbox, stores, Links) inherits this constraint.
- **The cap registry is process-wide and `bun test` shares modules across files**, so suites that read without declaring caps now reset explicitly (`read-model.test.ts`, `watch-shape.test.ts`). Worth knowing before adding a suite.
@@ -206,7 +206,7 @@ Written 2026-07-27, after two adversarial reviews and three corrections from the
## Why this lot exists
`caps.ts` currently models read rights as an **ACL** — a `Map<doc, Set<principal>>` plus `grantRead(doc, grantee)`. That is the **exact inversion** of the real model, where reading is **key possession**: whoever holds the key reads, and there is no authorization list anywhere.
`emulated-verifier/caps.ts` currently models read rights as an **ACL** — a `Map<doc, Set<principal>>` plus `grantRead(doc, grantee)`. That is the **exact inversion** of the real model, where reading is **key possession**: whoever holds the key reads, and there is no authorization list anywhere.
This is not a security problem — the library is deliberately insecure and that is accepted (see `../vision.md`). It is a **shape** problem, and shape is the only thing this library exists to get right. A consumer coded against an ACL is coded against a model that will never exist, and will have to be rewritten.
@@ -10,7 +10,7 @@
>
> And four more, all confirmed:
>
> 4. **D4 would delete a working recovery path.** Inbox deposits are never removed (`packages/client/src/inbox.ts`), so a second device/tab recovers its caps by re-reading. localStorage-without-re-reading loses them permanently, and contradicts P1a's delivered doctrine that per-process rebuild "is correct".
> 4. **D4 would delete a working recovery path.** Inbox deposits are never removed (`packages/client/src/surface/inbox.ts`), so a second device/tab recovers its caps by re-reading. localStorage-without-re-reading loses them permanently, and contradicts P1a's delivered doctrine that per-process rebuild "is correct".
> 5. **D3 is false outside entity documents.** `capFor(scopeIndexDoc)` and `capFor(walletInbox)` are undefined before *and after* `listMyEntityDocs` — their caps can only ever be derived. Yet the boundary brief requires them reachable. Upstream that root comes from the wallet plus `AddSignerCap` on the User branch — a level the fact table omitted entirely.
> 6. **`doc_create` writes four times, not two** (+ the class quad on the Header branch, + `AddSignerCap` on the User branch).
> 7. **Ordering defect: D2 before the boundary guard opens cap harvesting.** Once caps are triples in `scopeIndexDoc(bob,…)`, and both `scopeIndexDoc` and `docs.sparqlQuery` are exported, `setCurrentUser("mallory")` reads Bob's caps. Today `mintCap` is unexported, so a NURI yields nothing. **The guard must land before the caps become triples.**
@@ -93,7 +93,7 @@ The in-memory `CapRegistry` then stops being "the keyring" and becomes what it a
Verified: there is no received-caps register upstream, and inventing one would expose a shape the target does not have. What upstream does is persist the `read_cap` of every **opened** repo in local user storage.
So the emulation is a **local, per-virtual-user store** — the same nature as `accounts.ts`'s existing `IdentityStore` (localStorage). This ends "re-read the inbox every session to recover caps", which the PO identified as the wrong model: an inbox is a queue you consume, not a store you re-read.
So the emulation is a **local, per-virtual-user store** — the same nature as `shared-wallet/accounts.ts`'s existing `IdentityStore` (localStorage). This ends "re-read the inbox every session to recover caps", which the PO identified as the wrong model: an inbox is a queue you consume, not a store you re-read.
*Open*: whether to do D4 in this lot or after the boundary lot. It is the piece with the most design risk, and it is not needed for D1D3 to be correct.
@@ -129,7 +129,7 @@ So this is not a P1b concern, it is a **shape** concern — the one thing this l
- **My own documents** → the cap goes **beside the NURI in the scope container**, which is the emulation of `AddRepo { read_cap }` on a branch of the store. Today the container stores only `shim:contains <nuri>` and the cap is **re-derived** from that NURI; it gains a `shim:readCap` beside it. Deriving then stops being how a cap is *recovered* and becomes merely how the stand-in value is *minted* — the single function P1b replaces.
- **Caps I received** → into the emulation of `AddLink { read_cap }` on the **User branch** of the private store. Verified 2026-07-30 (see [`../readcap-and-nuri-model.md`](../readcap-and-nuri-model.md) §4quinquies): that register exists, it is explicitly for **external repos**, and its stated purpose is to *"share with all its device a new Link they received"* — wallet-resident and cross-device. So a received cap belongs **inside the virtual user**, like everything else. *(Two earlier versions of this note were wrong and are recorded in [`2026-07-30-users-stores-branches.md`](2026-07-30-users-stores-branches.md): the first blamed key derivation and proposed a "keyring document"; the second concluded no register existed at all and proposed browser-local storage — which would have put library data OUTSIDE even the physical user. Both were refuted; `AddLink` is the answer.)*
3. ~~**The guard at the four passage points**~~**DONE 2026-07-30.** `src/reach.ts` holds the boundary as **two rules on one criterion — possession — implemented in two places** (PO directive):
3. ~~**The guard at the four passage points**~~**DONE 2026-07-30.** `src/emulated-verifier/reach.ts` holds the boundary as **two rules on one criterion — possession — implemented in two places** (PO directive):
- **Rule 1, authorization**, at the passage points (`assertMayReach`, called from `docs.sparqlQuery` / `sparqlUpdate`): nothing reaches `ng` unless the connected user possesses the document's cap.
- **Rule 2, do not even attempt**, at the callers (`mustNotAttempt`, applied in `read-model.readUnion`, which now filters BEFORE opening or reading): a reader that holds no cap does not issue the operation at all. Upstream you cannot even address a repo you have no cap for, so asking about one is not "a read that will be refused" — it is a read with no meaning.
@@ -149,7 +149,7 @@ The PO's framing, which replaced the exemption list entirely:
> Clearly distinguish what is polyfill machinery (and therefore the PHYSICAL user) from what is a virtual user's operation. Use different functions, probably grouped in different namespaces — because one API is exposed to the app and the other must never be.
`src/physical.ts` now holds `physicalCreate` / `physicalQuery` / `physicalUpdate`, with `ensurePhysicalRepoOpen` and `subscribePhysicalDoc` as their open/subscribe counterparts. They are unguarded, and **never exported from the package** — a test asserts it, because a regression there is silent and total.
`src/shared-wallet/physical.ts` now holds `physicalCreate` / `physicalQuery` / `physicalUpdate`, with `ensurePhysicalRepoOpen` and `subscribePhysicalDoc` as their open/subscribe counterparts. They are unguarded, and **never exported from the package** — a test asserts it, because a regression there is silent and total.
The dividing line:
@@ -14,7 +14,7 @@
>
> **`inbox.postToDocument(doc, { payload })`** is the one call an app makes: it names the DOCUMENT, never an inbox, and **throws** when the document has no inbox rather than returning quietly — a deposit that vanishes without an error is the bug this whole path exists to close. There is deliberately **no target-document field on a deposit**, for the reason above.
>
> **Where the address lives.** On the document's emulated **Header branch** (`urn:ng-eventually:shim:headerBranch`), beside the content rather than in it — the same subject-as-compartment shape already used for the Store and User branches. `read-model` now drops every subject under the reserved `urn:ng-eventually:` namespace (`src/machinery.ts`), so the address cannot surface as one of the entity's properties. That filter is by SUBJECT, so it covers every emulated compartment present and future.
> **Where the address lives.** On the document's emulated **Header branch** (`urn:ng-eventually:shim:headerBranch`), beside the content rather than in it — the same subject-as-compartment shape already used for the Store and User branches. `read-model` now drops every subject under the reserved `urn:ng-eventually:` namespace (`src/emulated-verifier/machinery.ts`), so the address cannot surface as one of the entity's properties. That filter is by SUBJECT, so it covers every emulated compartment present and future.
>
> **The open question, answered.** *"Is 'a document has an inbox addressable by any holder' upstream, or this library's invention?"* — **upstream, as a capability**; ours only as a JS surface. The engine lets any repo have an inbox: `inbox: Option<PrivKey>` is a field of every `Repo` (`engine/repo/src/repo.rs:126`), `AddInboxCapV0` is keyed by `repo_id` (`engine/repo/src/types.rs:1973`), and `update_inbox_cap_v0` applies it with **no `is_store` check** (`engine/verifier/src/verifier.rs:1920`), at any time (User-branch commit, `commit.rs:1043-1050`, whose type documents the update case). What no code path does is CREATE one for a document: `new_store_default` attaches one only `if !private` (`verifier.rs:2994`), `doc_create` leaves `inbox: None` (`repo.rs:574`), and the engine's only two `AddInboxCap` commits are for the public and protected STORE repos (`site.rs:128,149`). "Does not" is not "cannot" — an earlier version of this note called the feature an anticipation on exactly that confusion. The half-split is upstream's too — a depositor seals with the inbox PUBLIC key (`engine/net/src/types.rs:4299`) and only the owner holds the private half — which is why an address is public by nature and belongs on the document, not on the owner's User branch.
>
+2 -2
View File
@@ -23,7 +23,7 @@
**Date:** 2026-06-16 · **Status:** SUPERSEDED 2026-07-30 (see the block above). *Originally: mechanism accepted; target owner undecided.*
Ported here for the discovery mechanism it defines — the piece this lib
realizes (`inbox.ts` post/materialize/watch; `store-registry.ts` fan-out). The
realizes (`inbox.ts` post/materialize/watch; `shared-wallet/account-registry.ts` fan-out). The
product intent (what a consumer application *should* surface) is the consumer
application's concern, not this lib's; only the mechanism is recorded here.
@@ -84,7 +84,7 @@ later. This is why a global-index package is a deferred separate package in this
## Polyfill reality — the fan-out drift is now RESOLVED (special-account index)
The shared-wallet polyfill originally shipped a cross-account fan-out over
every account's public documents (`store-registry.ts` `listEntityDocs('public')`
every account's public documents (`shared-wallet/account-registry.ts` `listEntityDocs('public')`
/ `resolveReadGraphs`) — one account saw another's public entity without any
relationship to its creator. This ADR classified that per-account fan-out as a drift
to be replaced by the single global index.
+1 -1
View File
@@ -47,7 +47,7 @@ breaks writes with `RepoNotFound`). See the scope rule in
*The decision stands; the mechanism named in it has been replaced.* Opening was
`orm_start_graph` when this was written. It is now `ensureRepoOpen``doc_subscribe`
plus a wait for the first `State` (`packages/client/src/open-repo.ts:167`) — after
plus a wait for the first `State` (`packages/client/src/emulated-verifier/open-repo.ts:167`) — after
`orm_start_graph` was found to hang on a fan-out (`subscribe.ts:28,181`). What must be
read here is the invariant *"open the repo, by its store NURI, before writing"*, not the
call that used to implement it.
+2 -2
View File
@@ -2,7 +2,7 @@
**Date:** 2026-06-15 · **Status:** Accepted (frozen). The rationale behind how
the consumer application presents identity selection as a perceived login, and why
the lib's identity store (`accounts.ts`) must never touch NextGraph. The lib itself
the lib's identity store (`shared-wallet/accounts.ts`) must never touch NextGraph. The lib itself
no longer frames this as a login: it receives an identity id, set at wallet-import
time; the perceived-login UX lives entirely in the consumer application.
@@ -63,7 +63,7 @@ barrier becomes the real per-user login — the flow shape does not change.
## How this lib realizes it
`accounts.ts` is an `IdentityStore`: `set(id)` / `clear()` / `get()` only read/write
`shared-wallet/accounts.ts` is an `IdentityStore`: `set(id)` / `clear()` / `get()` only read/write
the identity id in an injected `AccountStorage`; they never call NG. The id is set at
wallet-import time and relayed via the lib's current-identity call; the perceived
login is the consumer application's. See the identity store in
+30 -30
View File
@@ -2,13 +2,13 @@
**Scope.** The complement of [`docs/api-contract.md`](./api-contract.md): every module export under `packages/client/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`, `read-model.ts`, and by name everything `use-shape.ts`, `watch-shape.ts`, `lifecycle.ts`, `sparql.ts` export, plus `isNuri`/`hasReadCap` from `nuri.ts` and `subscribeDoc`/`subscribeDocs`/`docChangeType` (+ types) from `subscribe.ts`; its `storeRegistry` namespace is the **`store-registry-api.ts` slice only** (7 functions: `createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `walletInbox`, `openDocumentInbox`, `documentInboxAddress`). `polyfill.ts` re-exports `CapRegistry` from `caps.ts`, `shareCap` from `inbox.ts`, `connectedUser` from `connect.ts`, `* as accounts` from `accounts.ts`, and the types `AccountStorage`, `AccountRecord`, `RegistrySession`. Everything else that carries `export` in a `src/` module is internal and inventoried here. Eight modules are internal in their entirety: `access-log.ts`, `machinery.ts`, `ng-proxy.ts`, `open-repo.ts`, `outbox-log.ts`, `physical.ts`, `reach.ts`, `read-filter.ts`. Four are internal in part: `nuri.ts`, `connect.ts`, `subscribe.ts`, `store-registry.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, plus `isNuri`/`hasReadCap` from `nuri.ts` and `subscribeDoc`/`subscribeDocs`/`docChangeType` (+ types) from `subscribe.ts`; its `storeRegistry` namespace is the **`surface/placement.ts` slice only** (7 functions: `createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `walletInbox`, `openDocumentInbox`, `documentInboxAddress`). `polyfill.ts` re-exports `CapRegistry` from `emulated-verifier/caps.ts`, `shareCap` from `inbox.ts`, `connectedUser` from `emulated-verifier/connect.ts`, `* as accounts` from `shared-wallet/accounts.ts`, and the types `AccountStorage`, `AccountRecord`, `RegistrySession`. 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 — `ng-proxy.ts`
## 1. The wrapped `ng` factory — `surface/ng-proxy.ts`
```ts
// ng-proxy.ts:10
@@ -36,9 +36,9 @@ 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 is `r:{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. `mintCap` exists solely because the emulation needs a cap VALUE at creation time and P1b has not yet supplied real key material; the constant `OK` pretends nothing (`nuri.ts:87-103`). It has exactly two call sites (`store-registry.ts` `createEntityDoc`; `caps.ts` internals) — the minting points of the emulation. At P1b the constant becomes a real key; at migration the function is deleted (the engine mints).
- `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. `mintCap` exists solely because the emulation needs a cap VALUE at creation time and P1b has not yet supplied real key material; the constant `OK` pretends nothing (`nuri.ts:87-103`). It has exactly two call sites (`shared-wallet/account-registry.ts` `createEntityDoc`; `emulated-verifier/caps.ts` internals) — 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 — `reach.ts`
## 3. The reach boundary — `emulated-verifier/reach.ts`
```ts
// reach.ts:66
@@ -58,10 +58,10 @@ 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 is `pub(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 by `store-registry.ts` for 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.
- The infrastructure exemption (`declareInfrastructure`, registered by `shared-wallet/account-registry.ts` for 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 — `physical.ts`
## 4. The physical user's primitives — `shared-wallet/physical.ts`
```ts
// physical.ts:54
@@ -84,11 +84,11 @@ The unguarded counterparts of `docs.docCreate` / `sparqlQuery` / `sparqlUpdate`,
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 `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`).
`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 `physical.ts`.
- **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 — `open-repo.ts`
## 6. Bootstrap repo opening — `emulated-verifier/open-repo.ts`
```ts
// open-repo.ts:75
@@ -111,11 +111,11 @@ Heals the cold-start defect of the anchored read path: on a fresh session a not-
- The opening mechanism is **level 2, VERIFIED as a composition**: `doc_subscribe` exists (`sdk/js/lib-wasm/src/lib.rs:1908`), and the push variants `TabInfo`/`State`/`Patch` are the engine's `AppResponseV0` (`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-12` says an anchored `sparql_query` on a repo absent from `self.repos` "silently returns 0 rows (never a `RepoNotFound`)". Verified upstream: absence from `self.repos` yields `Err(NgError::RepoNotFound)` (`request_processor.rs:264,269`), the ReadQuery arm converts it into `AppResponse::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 into `self.repos` at `Verifier::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`, `read-model.ts` per-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.
- **Defect — the header's mechanism claim is contradicted at the source (see Findings F2).** `open-repo.ts:10-12` says an anchored `sparql_query` on a repo absent from `self.repos` "silently returns 0 rows (never a `RepoNotFound`)". Verified upstream: absence from `self.repos` yields `Err(NgError::RepoNotFound)` (`request_processor.rs:264,269`), the ReadQuery arm converts it into `AppResponse::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 into `self.repos` at `Verifier::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.ts` per-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.
- `SyncState` and `getSyncState` are 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 — `read-filter.ts`
## 7. The read filter — `emulated-verifier/read-filter.ts`
```ts
// read-filter.ts:47
@@ -124,13 +124,13 @@ export function filterReadable<T>(items: Iterable<T>, caps: CapRegistry): T[];
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 `use-shape.ts` once `caps.isEnforcing()`.
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 of `docs/api-contract.md`). There is no post-hoc filter to migrate to; the module is deleted.
- The `@graph` key 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/types.ts:19`). Items with no `@graph` are kept — a lib policy choice (they name no document), not an upstream rule.
- The `@graph` key 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 `@graph` are 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 `connect.ts`
## 8. Connection trigger — the unexported slice of `emulated-verifier/connect.ts`
```ts
// connect.ts:91
@@ -142,9 +142,9 @@ Fire-and-forget wrapper over the published `connectedUser()` (restore Links, the
- **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).
- `startConnect` itself disappears at migration; the automatic-processing behaviour it fabricates is native.
## 9. The shim registry — the unexported slice of `store-registry.ts`
## 9. The shim registry — the unexported slice of `shared-wallet/account-registry.ts`
The sharpest boundary case: `store-registry-api.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 types `AccountRecord` (`store-registry.ts:90`) and `RegistrySession` (`:234`) are published via `/polyfill` and covered by the surface contract.
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 types `AccountRecord` (`store-registry.ts:90`) and `RegistrySession` (`:234`) are published via `/polyfill` and covered by the surface contract.
### 9a. Account shim — provision, resolve, reserved names, cache
@@ -199,10 +199,10 @@ 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 is `connect.ts` / `inbox.ts`, which is exactly where upstream's verifier sits).
- **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 is `emulated-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 — `machinery.ts`
## 10. The machinery namespace — `emulated-verifier/machinery.ts`
```ts
// machinery.ts:32
@@ -215,7 +215,7 @@ The URN prefix every triple the lib writes for itself lives under, and the one p
- **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 four `shim:*` compartment subjects it protects all disappear at migration.
## 11. Diagnostics — `access-log.ts` and `outbox-log.ts`
## 11. Diagnostics — `shared-wallet/access-log.ts` and `shared-wallet/outbox-log.ts`
```ts
// access-log.ts:25,45,50,63,76,87,99,116
@@ -232,22 +232,22 @@ export function logAccess(op: AccessOp, nuri: string, label: string, extra?: str
export function inspectOutbox(): void;
```
`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`). `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` — 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.
- `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 with `setCurrentUser`. Deleted at migration.
- `outbox-log.ts`**NO COUNTERPART as API, but every fact it relies on is level-2 VERIFIED** in the clone: the outbox is persisted through `JsStorageConfig` (`sdk/rust/src/local_broker.rs:89-100`), keyed `ng_peer_last_seq@<peerId>` (`:119,141`) and `ng_outboxes@<peerId>@start` / `@<idx>` with zero-padded `{:05}` indexes (`:163-213`, pad at `:183,210`); the real `outbox_read_function` DRAINS on read (`session_del` per key plus the start key, `:218-224`) — which is why the probe only counts and never touches; and the storage callbacks land in browser `sessionStorage` (`sdk/js/api-web/main.ts:47,57,66`), whose access-denied error string is the one `convert_error` handles (`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.
- `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 with `setCurrentUser`. 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 through `JsStorageConfig` (`sdk/rust/src/local_broker.rs:89-100`), keyed `ng_peer_last_seq@<peerId>` (`:119,141`) and `ng_outboxes@<peerId>@start` / `@<idx>` with zero-padded `{:05}` indexes (`:163-213`, pad at `:183,210`); the real `outbox_read_function` DRAINS on read (`session_del` per key plus the start key, `:218-224`) — which is why the probe only counts and never touches; and the storage callbacks land in browser `sessionStorage` (`sdk/js/api-web/main.ts:47,57,66`), whose access-denied error string is the one `convert_error` handles (`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 — `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"`.
**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 — `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.
**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 `store-registry-api.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 `accounts.*` on the SDK entry — since the split (`index.ts:34` routes through `store-registry-api.ts`; `polyfill.ts:238` carries `accounts`) those are internal or `/polyfill`. That file is being edited concurrently; noted here, deliberately not fixed by this document.
**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 `accounts.*` on the SDK entry — since the split (`index.ts:34` routes through `surface/placement.ts`; `polyfill.ts:238` carries `accounts`) those are internal or `/polyfill`. 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()` (`polyfill.ts:145`), 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`.
@@ -255,15 +255,15 @@ export function inspectOutbox(): void;
- **The `(document, inbox)` pair is persisted as a space-joined string literal** (`"${doc} ${inbox}"`, written `store-registry.ts:1174`, parsed by `split(" ")` at `:1268`). Upstream the record is the typed `AddInboxCapV0 { 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` / `myInboxes`** encode 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, `open-repo.ts` is the module that breaks first; the probe is the tripwire.
- **`ensureAccount`'s provision-on-first-sight** (§ 9a) is a behaviour with no target image; `connect.ts` already 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`).
- **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.ts` is 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.ts` already 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: `access-log.ts` (`AccessOp`, `setAccessLog`, `enabled`, `activeIdentity`, `accessLogPrefix`, `logStage`, `shortNuri`, `logAccess`); `machinery.ts` (`MACHINERY_NS`, `isMachinerySubject`); `ng-proxy.ts` (`makeNg`); `open-repo.ts` (`SyncState`, `setOpenTimeoutForTests`, `resetOpenedRepos`, `getSyncState`, `ensureRepoOpen`, `ensurePhysicalRepoOpen`, `ensureReposOpen`); `outbox-log.ts` (`inspectOutbox`); `physical.ts` (`physicalCreate`, `physicalQuery`, `physicalUpdate`); `reach.ts` (`declareInfrastructure`, `isInfrastructure`, `resetInfrastructure`, `mayReach`, `assertMayReach`, `mustNotAttempt`); `read-filter.ts` (`filterReadable`, `makeReadFilteredView`).
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`); `connect.ts` (`startConnect`); `subscribe.ts` (`subscribePhysicalDoc`); `store-registry.ts` (`reservedAccount`, `resetRegistryCache`, `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`).
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`, `read-model.ts`, `accounts.ts`, `caps.ts`, `sparql.ts`, `lifecycle.ts`, `use-shape.ts`, `watch-shape.ts`, `store-registry-api.ts`, and the two entry points.
Modules with no internal exports (everything they export is published): `types.ts`, `docs.ts`, `inbox.ts`, `surface/read-model.ts`, `shared-wallet/accounts.ts`, `emulated-verifier/caps.ts`, `sparql.ts`, `lifecycle.ts`, `surface/use-shape.ts`, `surface/watch-shape.ts`, `surface/placement.ts`, and the two entry points.
+6 -6
View File
@@ -18,7 +18,7 @@ The shape is already the target's (P1a): a `ReadCap` is the document's key, a
each identity holds a set of caps, and there is no read-ACL anywhere. So
this step swaps the *emulated* key for the real one, not the model:
- `caps.ts`'s per-identity record becomes the verifier's own local user storage —
- `emulated-verifier/caps.ts`'s per-identity record becomes the verifier's own local user storage —
it was always the cache, not the register. The two durable registers we emulate
(`readCap` on the store's Store branch, `link` on its User branch) become the real
`AddRepo` / `AddLink` commits. Remove the emulation; the wallet and the branches
@@ -32,10 +32,10 @@ this step swaps the *emulated* key for the real one, not the model:
recipient's own verifier applying queued messages. **The consumer's call does not
change.**
- `publishRepoLink` becomes `RepoLinkV0`.
- The read filter (`read-filter.ts`) and the possession gate in
- The read filter (`emulated-verifier/read-filter.ts`) and the possession gate in
`read-model.readUnion` are then dead code — the broker only delivers documents
whose cap the wallet holds. Remove them.
- The write guard (`ng-proxy.ts` `sparql_update` override) is a separate axis and
- The write guard (`surface/ng-proxy.ts` `sparql_update` override) is a separate axis and
is decorative today (every internal writer bypasses the proxy); it belongs to the
P1b batch, not here.
@@ -56,7 +56,7 @@ in the shim (see the two-axes section in [`simulation.md`](./simulation.md)).
`getNativeStore(scope)`-style resolver returning the real store to pass as the
`docCreate` destination, so the logical scope label becomes a real store
placement. (No such helper exists yet — it is blocked on the SDK gap above.)
- At that point `store-registry.ts` maps `(account, scope)` to the user's real
- At that point `shared-wallet/account-registry.ts` maps `(account, scope)` to the user's real
store NURI instead of a document in the shared wallet; the per-scope index
document (the store-container emulation) is replaced by the store itself. The
surface facing the consumer application (`createEntityDoc`, `listMyEntityDocs`,
@@ -67,7 +67,7 @@ The `sharedWalletShim` (account → 3 scope-document NURIs, held in a subscribab
doc-shim reached via a write-once pointer in the store-root — see
[`nextgraph-current-state.md`](./nextgraph-current-state.md) § *The pointer → doc-shim
indirection*) has no target equivalent — the target has no central directory. Remove
it entirely: `store-registry.ts`, `configureStoreRegistry`, the pointer + doc-shim
it entirely: `shared-wallet/account-registry.ts`, `configureStoreRegistry`, the pointer + doc-shim
resolution, and the `pointerGuard` dep. Cross-wallet reads replace the fan-out;
per-user wallets replace the shared one.
@@ -79,7 +79,7 @@ when it processes its inbox — there is no separate curator to build; the in-li
emulation simply goes away. *(There is no global index to replace the cross-account fan-out: both were removed on 2026-07-30 — you cannot discover in NextGraph, you follow links.)*
### 5. Retire the identity store → real per-user login
Remove `accounts.ts` (the `IdentityStore` that persists the identity id in
Remove `shared-wallet/accounts.ts` (the `IdentityStore` that persists the identity id in
`localStorage`) and the app-level "Connexion" screen. The technical broker gate
becomes the real per-user login
(see [`decisions/shared-wallet-login-flow.md`](./decisions/shared-wallet-login-flow.md)).
+8 -8
View File
@@ -86,7 +86,7 @@ users/quorum (write/permissions), **not** read-cap possession. (Repos of a
> Consequence for this lib's emulation (see [`simulation.md`](./simulation.md)):
> the read access unit is the repo = each item's `@graph` — a per-document
> filter, never per-store and never per-item. This is exactly what
> `caps.ts` (`CapRegistry`) and `read-filter.ts` model: no store-level
> `emulated-verifier/caps.ts` (`CapRegistry`) and `emulated-verifier/read-filter.ts` model: no store-level
> inheritance, purely per-document caps. In a mono-store layout (all items in one
> repo) the filter is therefore all-or-nothing on that document — which *is* the
> native behaviour, and why fine-grained isolation requires one document per
@@ -285,7 +285,7 @@ from JS today a repo becomes queryable ONLY by being `doc_create`d in this sessi
**Consequence for this lib's mono-wallet polyfill:** every account's documents are
`doc_create`d in the one shared wallet within the same session, so they are all
already in `self.repos`. `read-model.ts` reads the bounded, by-need set of docs
already in `self.repos`. `surface/read-model.ts` reads the bounded, by-need set of docs
with one anchored `sparql_query` per doc (`SELECT ?s ?p ?o WHERE { ?s ?p ?o }`,
anchor = the doc NURI): the anchor resolves that same-session repo directly (no
separate open needed) and restricts the query to its graph, so it is O(1) per doc,
@@ -346,7 +346,7 @@ account map fell into (see next section).
### The pointer → doc-shim indirection (how the polyfill shim resolves accounts)
`store-registry.ts` keeps a map `identifier → {docPublic, docProtected, docPrivate}`
`shared-wallet/account-registry.ts` keeps a map `identifier → {docPublic, docProtected, docPrivate}`
(the "shim", the account→document trust root). It must be reachable by a fresh
reconnecting session (findable) AND authoritative on a cold read (so a fresh page
does not mistake sync-lag for "account absent" and PROVISION a fork). Since no single
@@ -750,9 +750,9 @@ On a FRESH session over the SAME persistent wallet (reconnect, new page, re-logi
anchored `sparql_query` against a document written in an earlier session comes back with
**0 rows and no error** — persisted documents read as empty. Observed on every anchored
reader of the polyfill and healed identically in each (`ensureRepoOpen` before the read,
`packages/client/src/open-repo.ts`): the user's own documents,
the user's store (`store-registry.ts` `readUserStore`), the by-need doc batch
(`read-model.ts` `readUnion`), and the store-root pointer read (`store-registry.ts`
`packages/client/src/emulated-verifier/open-repo.ts`): the user's own documents,
the user's store (`shared-wallet/account-registry.ts` `readUserStore`), the by-need doc batch
(`surface/read-model.ts` `readUnion`), and the store-root pointer read (`shared-wallet/account-registry.ts`
`resolvePointer`). The heal is `doc_subscribe(nuri)` → await the first `State` (the sync
barrier) → THEN the anchored read, and it is verified to return the data.
@@ -786,7 +786,7 @@ is a single account subject carrying MULTIPLE values for one scope predicate (ob
five `shim:docPublic`), after which a writer and a later reader can resolve DIFFERENT
scope docs and the reader's anchored read returns 0.
Two polyfill-side guards, both in `packages/client/src/store-registry.ts`: `ensureInFlight`
Two polyfill-side guards, both in `packages/client/src/shared-wallet/account-registry.ts`: `ensureInFlight`
(a bounded promise map keyed by account, so concurrent `ensureAccount` calls share ONE
resolve-or-provision) prevents new forks; `canonicalDoc` (pick the lexicographically
smallest NURI among all distinct values for a scope predicate — NURIs are
@@ -821,7 +821,7 @@ semantics of the replay path itself (VERIFIED by reading `send_outbox`).
**Consequence for this lib:** a queued write can be dropped without any observable error,
and one unknown topic can take the rest of the queue with it. The polyfill's own
`outbox-log.ts` does not record anything: it exports a single `inspectOutbox()` that
`shared-wallet/outbox-log.ts` does not record anything: it exports a single `inspectOutbox()` that
READS the SDK's own `sessionStorage` outbox and logs how many peers still have queued
writes. It observes the symptom; it holds nothing it could replay, and no
write-durability confirmation exists to await — so "the write returned" is not "the write
+2 -2
View File
@@ -165,7 +165,7 @@ Step (4) has a nuance worth recording, and it is exactly why the read path uses
graph — so an explicit `GRAPH ?g` body spans every opened graph independently of
the anchor. The anchor's "one repo" restriction is observable only for a body that
reads the **default graph** (no `GRAPH` wrapper). That is precisely why the per-doc
read in `read-model.ts` uses the anchored default-graph body
read in `surface/read-model.ts` uses the anchored default-graph body
`SELECT ?s ?p ?o WHERE { ?s ?p ?o }`: the anchor makes that one repo the default
graph, so the read is bounded to it — O(1) per doc, independent of wallet size —
and never iterates the other named graphs. (A repo absent from `self.repos` throws
@@ -185,7 +185,7 @@ sync an unknown repo.)
> is per-doc anchored, preserved above), **not** a **constant** `GRAPH <D>` write,
> which lands in D's repo. Re-run that harness to re-verify against a newer broker.
## Implementation — `read-model.ts`
## Implementation — `surface/read-model.ts`
`readModel.readUnion(docs)` implements this: for each requested doc NURI (the
bounded by-need set), run — in parallel, tolerant per-doc (a doc that fails is
+3 -3
View File
@@ -2,7 +2,7 @@
**Established 2026-07-20**, VERIFIED by direct reading of the `nextgraph-rs` Rust core (except for points marked INFERRED). The `file:line` references are dated — line numbers are volatile, navigate by symbol/regex.
Purpose: to give the ground truth of NextGraph's access-rights model, in order to align the polyfill's `caps.ts` emulation (an ACL until P1a — the inverse of the real model; realigned 2026-07-28, see §5). This is the basis for the item "align ReadCap/WriteCap with NextGraph".
Purpose: to give the ground truth of NextGraph's access-rights model, in order to align the polyfill's `emulated-verifier/caps.ts` emulation (an ACL until P1a — the inverse of the real model; realigned 2026-07-28, see §5). This is the basis for the item "align ReadCap/WriteCap with NextGraph".
> ## How to use this document — verify, never infer
>
@@ -271,7 +271,7 @@ What remains true, and is a separate matter — the *delivery* path is unimpleme
## 5. What the polyfill emulates (caps.ts) — and where it still diverges
**Realigned 2026-07-28 (batch P1a).** `packages/client/src/caps.ts` used to model `readers: Map<Nuri, Set<PrincipalId>>` + `grantRead(doc, grantee)` — a per-document **ACL of principals**, the exact INVERSION of the real model. It now records, **per identity**, the caps that identity holds (`Map<Nuri, ReadCap>`) — whose only question is `capFor(nuri)` — and `nuri.ts` carries the cap-less / cap-bearing distinction on the `r:` segment. The durable registers are emulated in `store-registry.ts` (`readCap` on the Store branch, `link` on the User branch); this in-memory record is their cache.
**Realigned 2026-07-28 (batch P1a).** `packages/client/src/emulated-verifier/caps.ts` used to model `readers: Map<Nuri, Set<PrincipalId>>` + `grantRead(doc, grantee)` — a per-document **ACL of principals**, the exact INVERSION of the real model. It now records, **per identity**, the caps that identity holds (`Map<Nuri, ReadCap>`) — whose only question is `capFor(nuri)` — and `nuri.ts` carries the cap-less / cap-bearing distinction on the `r:` segment. The durable registers are emulated in `shared-wallet/account-registry.ts` (`readCap` on the Store branch, `link` on the User branch); this in-memory record is their cache.
| | Real NextGraph | caps.ts emulation (post-P1a) |
|---|---|---|
@@ -282,7 +282,7 @@ What remains true, and is a separate matter — the *delivery* path is unimpleme
| Granularity | repo / branch / commit / object | **one cap per doc-NURI** |
| Ref. without rights | **cap-less NURI** (no `r:` segment) | same — `Nuri` names, `ReadCap` names and reads |
**The divergence that REMAINS**: the stand-in cap value is the constant `OK` rather than a secret. The read paths that once consulted no cap at all are now confined to the connected virtual user (`reach.ts`, 2026-07-30) — `docs.sparqlQuery`/`sparqlUpdate` and `subscribeDoc` are guarded, the inbox is read only by its owner, and the shim's own machinery moved to unguarded primitives that are never exported. So what is left for **P1b** is per-document encryption: replacing one constant with a real key. Until then, nothing may be claimed "anonymous" or "private".
**The divergence that REMAINS**: the stand-in cap value is the constant `OK` rather than a secret. The read paths that once consulted no cap at all are now confined to the connected virtual user (`emulated-verifier/reach.ts`, 2026-07-30) — `docs.sparqlQuery`/`sparqlUpdate` and `subscribeDoc` are guarded, the inbox is read only by its owner, and the shim's own machinery moved to unguarded primitives that are never exported. So what is left for **P1b** is per-document encryption: replacing one constant with a real key. Until then, nothing may be claimed "anonymous" or "private".
**App-facing**: `declareConnections` (on the consumer side), which re-declared "my connections read my protected entities" **every session**, was an artifact of the ephemeral ACL — **it disappears**. The grant moves to the moment a connection is accepted (`shareCap` once, per document), which is a consumer **re-architecture**, not an API swap.
+19 -19
View File
@@ -52,10 +52,10 @@ Because the emulation runs on ONE shared wallet, distinguish two levels:
- **A virtual user** — the library's emulation of one identity: the documents the
shim attributes to that account (its three store documents in
`store-registry.ts`). This is what "the user owns", and over it "list my
`shared-wallet/account-registry.ts`). This is what "the user owns", and over it "list my
documents" is meaningful and bounded.
**Which API you use follows that line, and it is enforced** (see `physical.ts`):
**Which API you use follows that line, and it is enforced** (see `shared-wallet/physical.ts`):
machinery operating on the *index of virtual users* — the store-root pointer, the
doc-shim, the account records — goes through unguarded primitives that are never
exported from the package. Everything touching a virtual user's own content goes
@@ -70,7 +70,7 @@ ones whose cap you were given.
At migration each virtual user becomes a real user with its own wallet; the
physical/virtual distinction, the "never enumerate the physical one" rule, and
`physical.ts` all dissolve into native per-user reads.
`shared-wallet/physical.ts` all dissolve into native per-user reads.
## Two axes, never conflate them (store ≠ document)
@@ -86,7 +86,7 @@ terminology historically fused:
`docCreate(sessionId, "Graph", "data:graph", "store", undefined)` targets the shared
wallet's private store. The trailing `store` arg left `undefined` targets the
private store (this is what `store-registry.ts`'s `createDoc()` does). So every
private store (this is what `shared-wallet/account-registry.ts`'s `createDoc()` does). So every
document the shim creates physically lives in one store (private), and the
`public|protected|private` scope is a logical label tracked in RDF by the
shim — not a NextGraph store. Therefore what a consumer application's "multi-store"
@@ -100,7 +100,7 @@ logical label the registry attaches.
> [`migration-guide.md`](./migration-guide.md)). The private store is reachable
> because it opens without `RepoNotFound`.
## The shared-wallet shim (`store-registry.ts`)
## The shared-wallet shim (`shared-wallet/account-registry.ts`)
Emulates the target infrastructure — where each user owns their own
public/protected/private stores — on top of one shared wallet.
@@ -207,7 +207,7 @@ store-id:
redirect the owner's deposits to itself). Both are dedicated documents (real repo
NURIs from `docCreate`), never the private-store root: routing deposits into the shim
graph would bloat the account→document trust root without bound. `myInboxes()`
enumerates both levels — what `connect.ts` drains at connection — and `isOwnInbox`
enumerates both levels — what `emulated-verifier/connect.ts` drains at connection — and `isOwnInbox`
answers from the same record. *(The former `resolveInboxAnchor`, a single inbox COMMON
to every user, was removed on 2026-07-30: nothing may be common but the mechanisms
that make the virtual users work.)*
@@ -220,7 +220,7 @@ store-id:
A document that has an inbox carries its address on its emulated **Header branch**
a reserved subject inside the document, so any holder of the document reads it, and
`read-model` filters the whole `urn:ng-eventually:` namespace out of consumer data
(`src/machinery.ts`). This mirrors upstream's split: a depositor seals with the inbox
(`src/emulated-verifier/machinery.ts`). This mirrors upstream's split: a depositor seals with the inbox
PUBLIC key and needs nothing else, only the owner holds the private half.
**One inbox belongs to one document** — never several documents behind one inbox, a
relation upstream cannot express (the verifier routes by `inboxes: PubKey → RepoId`
@@ -265,7 +265,7 @@ is preserved in [`decisions/private-store-nuri-scope.md`](./decisions/private-st
A validated hard constraint, not a style choice: `docs.ts` calls the real
injected `ng` (`getConfig().ng`) directly, never the public `ng` proxy
(`makeNg` in `ng-proxy.ts`).
(`makeNg` in `surface/ng-proxy.ts`).
`@ng-org/web`'s `ng` is already an iframe-RPC proxy (postMessage marshaling,
see [`nextgraph-current-state.md`](./nextgraph-current-state.md) § integration).
@@ -283,13 +283,13 @@ verified: routing the shim's `doc_create`/SPARQL through the public proxy turned
`docs.ts` therefore imports **no** `@ng-org` package and must **not** import from
`./ng-proxy`.
## Emulated ReadCap — per document (`caps.ts` + `read-filter.ts`)
## Emulated ReadCap — per document (`emulated-verifier/caps.ts` + `emulated-verifier/read-filter.ts`)
In the target the broker only delivers documents the wallet holds a ReadCap
for, so `useShape` already returns an authorized subset. Here (single shared
wallet, everything readable) the lib reproduces that with a read-filtered view:
- **`CapRegistry` (`caps.ts`)** models a ReadCap as what it is: **the document's
- **`CapRegistry` (`emulated-verifier/caps.ts`)** models a ReadCap as what it is: **the document's
key**. The access unit is the document = repo NURI (an item's `@graph`), never
the item — because in `nextgraph-rs` a store is just a container repo and holding
its cap does not grant the repos it references (no store-level read inheritance;
@@ -305,13 +305,13 @@ wallet, everything readable) the lib reproduces that with a read-filtered view:
constant `OK` (see the module header): the only question the emulation answers is
*do I hold this cap or not*, so the value says exactly that and pretends nothing
more. P1b, not P1a, is the batch that turns the shape into a protection.
- **`read-filter.ts`** — `makeReadFilteredView` wraps the reactive set in a
- **`emulated-verifier/read-filter.ts`** — `makeReadFilteredView` wraps the reactive set in a
`Proxy`: iteration / `size` / `forEach` keep only items whose `@graph` the
current holder holds; everything else (`add`, `delete`, `has`, `getById`…) forwards to
the target, preserving writes and reactivity. An item with no `@graph` is kept (it
names no document, so there is no cap to hold). `filterReadable` is the pure
variant. Note the absence of a `user` parameter — that absence *is* the model.
- **`useShape` (`use-shape.ts`)** applies the view only once a cap exists at all
- **`useShape` (`surface/use-shape.ts`)** applies the view only once a cap exists at all
(`caps.isEnforcing()`) — before that it passes the real set through unchanged (no
regression for a consumer that never touches caps). Once ANY cap is issued the
regime is possession for **every** holder, including one who holds nothing:
@@ -428,7 +428,7 @@ cosmetic. Proven in `test/cross-user-access.test.ts`.
### Write-guard coverage (honest scope)
The emulated write guard (`ng-proxy.ts`, `sparql_update` override) enforces the
The emulated write guard (`surface/ng-proxy.ts`, `sparql_update` override) enforces the
per-document write cap on the public `ng` proxy only. In practice the
consumer application's write paths (`docs.sparqlUpdate`, ORM `ngSet`) call the real
injected `ng` directly — never the public proxy — for the validated `DataCloneError`
@@ -440,7 +440,7 @@ natively at migration); the read side is what makes isolation observably active.
### The per-document ReadCap is the isolation path (item-level filter retired)
Isolation is enforced by the per-document ReadCap (`caps.ts` + `read-filter.ts`)
Isolation is enforced by the per-document ReadCap (`emulated-verifier/caps.ts` + `emulated-verifier/read-filter.ts`)
alone: the access unit is the document (`@graph` = repo), and the only acts are
possession-shaped (`createEntityDoc` files a cap, `shareCap` delivers one,
`publishRepoLink` emits an openable link). Because the consumer application writes
@@ -515,7 +515,7 @@ The inbox + watcher is the one deposit/read mechanism a consumer reuses for its
purposes — a registration/deposit, a cap delivery (`shareCap`), a link handed to
someone — same `post` API, same watcher.
## The virtual user boundary (`reach.ts` + `physical.ts`)
## The virtual user boundary (`emulated-verifier/reach.ts` + `shared-wallet/physical.ts`)
Every access function is confined to the user currently connected: no cross-user
access, so the consumer is coded against a reach that will actually exist.
@@ -541,13 +541,13 @@ named primitive rather than a flag, because it is a different act — you hold n
you cannot read back, and you get nothing in return. It is the only channel by which
a link crosses between users, hence the bootstrap of the whole reachability graph.
The machinery lives in `physical.ts` (see *Physical user vs virtual user* above):
The machinery lives in `shared-wallet/physical.ts` (see *Physical user vs virtual user* above):
unguarded primitives, never exported from the package, used only for the index of
virtual users. Separating the FUNCTIONS is what replaced an earlier exemption list —
the machinery does not get waved through the guard, it calls something the guard
never saw.
## Connecting a user (`connect.ts`)
## Connecting a user (`emulated-verifier/connect.ts`)
Processing inboxes is the **library's** job, not the app's: a consumer must never
have to remember to drain a queue for documents shared with it to become readable —
@@ -587,7 +587,7 @@ first.
The ADR that specified it ([`decisions/discovery-model.md`](./decisions/discovery-model.md)) is marked superseded, and keeps the part that survives: the `discovery → synchronization → query` frame still holds, with stage 1 re-read as *"a link reached you"* rather than *"you consulted an index"*. Which makes the **inbox** the bootstrap of the whole reachability graph — see [`briefs/2026-07-30-virtual-wallet-boundary.md`](./briefs/2026-07-30-virtual-wallet-boundary.md).
## Emulated write guard (`ng-proxy.ts`)
## Emulated write guard (`surface/ng-proxy.ts`)
The public `ng` proxy overrides `sparql_update` to enforce an emulated write
cap: a write is refused unless the current user holds the target document's
@@ -596,7 +596,7 @@ specific document (the `anchor` arg) is governed by it — ungoverned docs (the
mono-store default, no cap declared) flow through unchanged. This mirrors the target
broker/verifier, which refuses a write without the document's write cap.
## Identity store (`accounts.ts`)
## Identity store (`shared-wallet/accounts.ts`)
The real NextGraph login (redirect to the broker, opening the single shared
wallet) is perceived as a technical access barrier (see the login
+29 -29
View File
@@ -26,17 +26,17 @@ And a fourth group the three-fate framing misses: the **target's model vocabular
### 1c. Alternatives, honestly
- **By feature** (`inbox/`, `caps/`, `read/`…): optimises "find everything about X", which the api-contract's by-subject sections already do better — and it actively hides the fate axis: `inbox.ts` would stay one folder while its sender half is target-shaped surface and its reader half is emulation detail a consumer must not code against (`docs/api-contract.md` § 9). Rejected: it organises along the axis that is already served and flattens the one that is not.
- **By dependency direction** (layered, low → high): the import graph refuses it. `docs.ts` (surface) calls `getCaps().mint` (emulation) because upstream `doc_create` itself commits `AddRepo` (level 1, `engine/verifier/src/request_processor.rs:698`, re-verified); `connect.ts` (verifier stand-in) calls `resolveAccount` (shim) because the emulation runs on the shim. These cross-fate imports are *target-faithful*, not accidents — a layout that forbids them would force artificial inversions or be violated on day one. Rejected: it encodes a property the semantics do not have.
- **By dependency direction** (layered, low → high): the import graph refuses it. `docs.ts` (surface) calls `getCaps().mint` (emulation) because upstream `doc_create` itself commits `AddRepo` (level 1, `engine/verifier/src/request_processor.rs:698`, re-verified); `emulated-verifier/connect.ts` (verifier stand-in) calls `resolveAccount` (shim) because the emulation runs on the shim. These cross-fate imports are *target-faithful*, not accidents — a layout that forbids them would force artificial inversions or be violated on day one. Rejected: it encodes a property the semantics do not have.
- **Flat with naming conventions only** (`surface-docs.ts`, `shim-physical.ts`…): carries the same information at the same churn — a rename churns every importer exactly as a move does — with weaker affordances: no per-folder contract note, no one-glance grouping, and no folder-granular entry rule to grep or lint. Prefixes also rot silently in a way a misplaced file in a four-folder tree does not. Rejected as strictly dominated: same cost, less structure.
- **Do nothing — rely on the contracts:** the serious alternative. The contracts are finer-grained than any layout (per-claim epistemic labels, not per-module), and they were verified at the source. But they are read *after* the fact; the incident that motivates this analysis happened in a module whose own header states the right doctrine. A layout is confronted *during* the edit: a new module must be placed, and placing it forces the "which fate?" question at the moment the docs.ts-style mistake is made. Layout and contracts are complementary instruments — the layout is the cheap always-on prompt, the contracts remain the enforcement.
### 1d. What the layout actually buys — bounded claims
1. **The placement question fires at creation time.** A new module cannot be added without answering "surface, native stand-in, shared-wallet, or model?" — the question whose non-asking is the root of the flat layout's failure.
2. **The entry rule becomes folder-granular and mechanical.** Today `index.ts`'s purity is maintained per-symbol (the hand-built `store-registry-api.ts` slice). With folders the rule is "`index.ts` re-exports only from `surface/` and `model/`" — one grep, lintable in CI, reviewable at a glance.
2. **The entry rule becomes folder-granular and mechanical.** Today `index.ts`'s purity is maintained per-symbol (the hand-built `surface/placement.ts` slice). With folders the rule is "`index.ts` re-exports only from `surface/` and `model/`" — one grep, lintable in CI, reviewable at a glance.
3. **Cross-fate imports become visible seams.** `import { … } from "../emulated-verifier/…"` inside `surface/docs.ts` is a reviewable event in a diff; the same call inside a flat sibling import is invisible. The seam does not *prevent* the docs.ts incident class — it makes it show up in review.
4. **The contracts map onto the tree.** `docs/api-contract.md``surface/` + `model/`; `docs/internal-contract.md``emulated-verifier/` + `shared-wallet/`. Drift between doc and code becomes a folder-membership diff instead of an inventory audit.
5. **It generalises a pattern this repo already proved.** `store-registry-api.ts` (a hand-maintained surface slice), `physical.ts` (privilege as *separate functions*, not exemption flags), and the 2026-08-03 entry-header fix are all the same idea implemented piecemeal at module granularity. The layout is the same discipline promoted to the tree.
5. **It generalises a pattern this repo already proved.** `surface/placement.ts` (a hand-maintained surface slice), `shared-wallet/physical.ts` (privilege as *separate functions*, not exemption flags), and the 2026-08-03 entry-header fix are all the same idea implemented piecemeal at module granularity. The layout is the same discipline promoted to the tree.
**Verdict on the bet:** "easier to spot" — substantially yes (points 24). "Harder to commit" — only mildly: nothing in a folder tree stops a determined or oblivious edit, and the decisive question (*is this behaviour the target's?*) is answered by reading `nextgraph-rs`, not by any layout (§ 6). Worth doing, with the expectations of § 1d and the costs of § 5.
@@ -71,24 +71,24 @@ The 25 current modules, with the two splits' offspring shown where a module divi
| `sparql.ts` | `surface/sparql.ts` | Published, generic injection-safety utilities with NO COUNTERPART and none expected (`docs/api-contract.md` § 11) — the one surface family that survives *any* migration unchanged. Placed with the surface because it is published and documented there; the folder note must carry this exception. |
| `docs.ts` | `surface/docs.ts` — stays whole | Level-2 passthroughs whose in-body cap filing and reach guard *mirror the target's own composition* (§ 4 ruling). The mint and the guard become named imports from `emulated-verifier/` — the visible seam. |
| `lifecycle.ts` | `surface/lifecycle.ts` | Pure forwarding to the injected level-2/3 calls (`docs/api-contract.md` § 2). |
| `ng-proxy.ts` | `surface/ng-proxy.ts` — stays whole | Builds the published `ng`; its two overrides compose `emulated-verifier/` predicates (the write guard stands in for `verify_perm` inside `Commit::verify`, level 1, `engine/repo/src/commit.rs:892-899`, re-verified — noting `verify` has no runtime caller today, which says nothing about the target). The `login` arm is finding F1 of the internal contract: an unprovenanced fabricated member — its fix is deletion, not relocation. |
| `use-shape.ts` | `surface/use-shape.ts` | Level-3 passthrough + the read-filter view imported from `emulated-verifier/` — mixture-by-import, already in the right shape. |
| `watch-shape.ts` | `surface/watch-shape.ts` | Surface composition over `emulated-verifier/` and the placement calls; its "planned `useShape` upgrade" header claim remains an ASSUMPTION with no provenance (`docs/api-contract.md` § 5) — a layout cannot fix that (§ 6). |
| `surface/ng-proxy.ts` | `surface/ng-proxy.ts` — stays whole | Builds the published `ng`; its two overrides compose `emulated-verifier/` predicates (the write guard stands in for `verify_perm` inside `Commit::verify`, level 1, `engine/repo/src/commit.rs:892-899`, re-verified — noting `verify` has no runtime caller today, which says nothing about the target). The `login` arm is finding F1 of the internal contract: an unprovenanced fabricated member — its fix is deletion, not relocation. |
| `surface/use-shape.ts` | `surface/use-shape.ts` | Level-3 passthrough + the read-filter view imported from `emulated-verifier/` — mixture-by-import, already in the right shape. |
| `surface/watch-shape.ts` | `surface/watch-shape.ts` | Surface composition over `emulated-verifier/` and the placement calls; its "planned `useShape` upgrade" header claim remains an ASSUMPTION with no provenance (`docs/api-contract.md` § 5) — a layout cannot fix that (§ 6). |
| `subscribe.ts` | `surface/subscribe.ts`, minus `subscribePhysicalDoc``shared-wallet/physical.ts` | The guarded `subscribeDoc`/`subscribeDocs` and `docChangeType` are surface (level 2, `doc_subscribe`, `sdk/js/lib-wasm/src/lib.rs:1908`). The physical door moves to the machinery module (§ 4); the unguarded core is exported under its `Unguarded` name for that one importer. |
| `read-model.ts` | `surface/read-model.ts` — stays whole | The anchored-read mechanics are level-1-verified and survive as composition (`docs/api-contract.md` § 6). Its possession gate and machinery filter mirror native behaviour (§ 4 ruling) and arrive via named `emulated-verifier/` imports. |
| `surface/read-model.ts` | `surface/read-model.ts` — stays whole | The anchored-read mechanics are level-1-verified and survive as composition (`docs/api-contract.md` § 6). Its possession gate and machinery filter mirror native behaviour (§ 4 ruling) and arrive via named `emulated-verifier/` imports. |
| `inbox.ts` | **split**: `surface/inbox.ts` (post, `postToDocument`, `shareCap`) + `emulated-verifier/inbox-processing.ts` (`read`/`materialize`/`readSynced`/`processInbox`/`watch`, `assertOwnInbox`, the deposit RDF vocabulary) | § 4 ruling. `surface/inbox.ts` re-exports the processing functions with a header saying exactly what `docs/api-contract.md` § 9 says — that enumerating deposits is emulation detail — so the published `inbox.*` namespace is unchanged and the warning sits at the one place the two halves meet. |
| `store-registry-api.ts` | dissolved into `surface/placement.ts` | The hand-built slice becomes a real module: the app-facing placement/addressing calls (`createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `walletInbox`, `openDocumentInbox`, `documentInboxAddress`), composed from the two lower layers. Its existence today is the strongest in-repo evidence for the whole reorganisation: it does by hand what the folder boundary does structurally. |
| `store-registry.ts` | **split three ways**: `surface/placement.ts` + `emulated-verifier/branch-registers.ts` + `shared-wallet/account-registry.ts` | The sharpest case — 1377 lines spanning all three fates. `branch-registers.ts` takes the durable compartments: the Store-branch cap register (`holdOwnCap`/`readStoreCaps` — emulating `AddRepo { read_cap }`, level 1, `engine/repo/src/types.rs:1890-1899`), the User-branch Link register (`addLink`/`readLinks``AddLink { read_cap }`, `types.rs:1939-1948`), the inbox-cap records (`AddInboxCapV0`, `types.rs:1969-1981`) and the Header-branch address records. `account-registry.ts` takes the pointer→doc-shim indirection, `resolveAccount`/`ensureAccount`, `reservedAccount`, the cache, `AccountRecord`/`RegistrySession` — NO COUNTERPART, the shim proper. `placement.ts` (surface) keeps the app-facing calls listed above plus `userStoreDoc`/`isOwnInbox`/`myInboxes` staying internal on the register side per their contract entries. |
| `caps.ts` | `emulated-verifier/caps.ts` — stays whole (+ receives `mintCap`) | The in-memory record of what the connected holder holds — the verifier's cap state. The per-holder `heldByHolder` keying is its one shared-wallet dimension (one wallet, many holders); it stays, isolated behind the injected `holder()` and noted: at migration there is exactly one holder and the map collapses. |
| `reach.ts` | `emulated-verifier/reach.ts` | The emulated wallet boundary: stands in for "a repo you hold no cap for cannot even be addressed" (level 1, `resolve_target_for_sparql``RepoNotFound`, `request_processor.rs:264,269`, re-verified). The two-rules redundancy is a lib choice, documented. |
| `read-filter.ts` | `emulated-verifier/read-filter.ts` | Stands in for cryptographic non-delivery (same level-1 citations); deleted at migration with nothing to migrate to. |
| `connect.ts` | `emulated-verifier/connect.ts` — stays whole | The recipient-verifier moment (`Verifier::inbox``process_inbox`, level 1, `verifier.rs:1674-1690`). Its import of `resolveAccount` (shared-wallet) is a deliberate cross-fate *check* — connecting must not provision — and stays visible at the import line. |
| `open-repo.ts` | `emulated-verifier/open-repo.ts`, minus `ensurePhysicalRepoOpen``shared-wallet/physical.ts` | Stands in for the verifier bringing repos into `self.repos` (native at migration: open by cap at bootstrap). Flag kept from the internal contract: this module is a *current-state heal*, not a model emulation — its barrier ("TabInfo then first State") is empirical, pinned only by the e2e CONTRACT-3 probe, and its header's mechanism claim is finding F2 (contradicted at the source). The folder cannot fix either (§ 6). |
| `machinery.ts` | `emulated-verifier/machinery.ts` | The subject filter fabricates what is structurally impossible upstream: a content read cannot see Store/User/Header compartments because they are separate branches with no triples (level 1, `BranchCrdt::None`, `engine/repo/src/types.rs:1420`, re-verified). It sits beside `branch-registers.ts`, whose compartments it hides; note that `MACHINERY_NS` also covers the account-shim's vocabulary. |
| `physical.ts` | `shared-wallet/physical.ts` — grows into the complete privileged door | The quintessential shared-wallet module. It absorbs `subscribePhysicalDoc` and `ensurePhysicalRepoOpen`, so **one module is the machinery's entire unguarded API** — completing its own doctrine ("separate functions, never exemptions", `physical.ts:19-27`) at the tree level. Cost: the two unguarded cores get exported (under `Unguarded` names) from their mechanism modules; acceptable because neither entry ever re-exports them. |
| `accounts.ts` | `shared-wallet/accounts.ts` | Identity persistence for the shared wallet; NO COUNTERPART (`docs/api-contract.md` § 13); already correctly published via `/polyfill` only. |
| `access-log.ts` | `shared-wallet/access-log.ts` | Diagnoses the shared-wallet isolation leak; the identity it prefixes is the relayed virtual id. Deleted at migration. |
| `outbox-log.ts` | `shared-wallet/outbox-log.ts` | Polyfill-era trace probe over the injected SDK's private persistence (level-2 facts verified in the internal contract § 11). Deleted at migration. |
| `surface/placement.ts` | dissolved into `surface/placement.ts` | The hand-built slice becomes a real module: the app-facing placement/addressing calls (`createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `walletInbox`, `openDocumentInbox`, `documentInboxAddress`), composed from the two lower layers. Its existence today is the strongest in-repo evidence for the whole reorganisation: it does by hand what the folder boundary does structurally. |
| `shared-wallet/account-registry.ts` | **split three ways**: `surface/placement.ts` + `emulated-verifier/branch-registers.ts` + `shared-wallet/account-registry.ts` | The sharpest case — 1377 lines spanning all three fates. `branch-registers.ts` takes the durable compartments: the Store-branch cap register (`holdOwnCap`/`readStoreCaps` — emulating `AddRepo { read_cap }`, level 1, `engine/repo/src/types.rs:1890-1899`), the User-branch Link register (`addLink`/`readLinks``AddLink { read_cap }`, `types.rs:1939-1948`), the inbox-cap records (`AddInboxCapV0`, `types.rs:1969-1981`) and the Header-branch address records. `account-registry.ts` takes the pointer→doc-shim indirection, `resolveAccount`/`ensureAccount`, `reservedAccount`, the cache, `AccountRecord`/`RegistrySession` — NO COUNTERPART, the shim proper. `placement.ts` (surface) keeps the app-facing calls listed above plus `userStoreDoc`/`isOwnInbox`/`myInboxes` staying internal on the register side per their contract entries. |
| `emulated-verifier/caps.ts` | `emulated-verifier/caps.ts` — stays whole (+ receives `mintCap`) | The in-memory record of what the connected holder holds — the verifier's cap state. The per-holder `heldByHolder` keying is its one shared-wallet dimension (one wallet, many holders); it stays, isolated behind the injected `holder()` and noted: at migration there is exactly one holder and the map collapses. |
| `emulated-verifier/reach.ts` | `emulated-verifier/reach.ts` | The emulated wallet boundary: stands in for "a repo you hold no cap for cannot even be addressed" (level 1, `resolve_target_for_sparql``RepoNotFound`, `request_processor.rs:264,269`, re-verified). The two-rules redundancy is a lib choice, documented. |
| `emulated-verifier/read-filter.ts` | `emulated-verifier/read-filter.ts` | Stands in for cryptographic non-delivery (same level-1 citations); deleted at migration with nothing to migrate to. |
| `emulated-verifier/connect.ts` | `emulated-verifier/connect.ts` — stays whole | The recipient-verifier moment (`Verifier::inbox``process_inbox`, level 1, `verifier.rs:1674-1690`). Its import of `resolveAccount` (shared-wallet) is a deliberate cross-fate *check* — connecting must not provision — and stays visible at the import line. |
| `emulated-verifier/open-repo.ts` | `emulated-verifier/open-repo.ts`, minus `ensurePhysicalRepoOpen``shared-wallet/physical.ts` | Stands in for the verifier bringing repos into `self.repos` (native at migration: open by cap at bootstrap). Flag kept from the internal contract: this module is a *current-state heal*, not a model emulation — its barrier ("TabInfo then first State") is empirical, pinned only by the e2e CONTRACT-3 probe, and its header's mechanism claim is finding F2 (contradicted at the source). The folder cannot fix either (§ 6). |
| `emulated-verifier/machinery.ts` | `emulated-verifier/machinery.ts` | The subject filter fabricates what is structurally impossible upstream: a content read cannot see Store/User/Header compartments because they are separate branches with no triples (level 1, `BranchCrdt::None`, `engine/repo/src/types.rs:1420`, re-verified). It sits beside `branch-registers.ts`, whose compartments it hides; note that `MACHINERY_NS` also covers the account-shim's vocabulary. |
| `shared-wallet/physical.ts` | `shared-wallet/physical.ts` — grows into the complete privileged door | The quintessential shared-wallet module. It absorbs `subscribePhysicalDoc` and `ensurePhysicalRepoOpen`, so **one module is the machinery's entire unguarded API** — completing its own doctrine ("separate functions, never exemptions", `physical.ts:19-27`) at the tree level. Cost: the two unguarded cores get exported (under `Unguarded` names) from their mechanism modules; acceptable because neither entry ever re-exports them. |
| `shared-wallet/accounts.ts` | `shared-wallet/accounts.ts` | Identity persistence for the shared wallet; NO COUNTERPART (`docs/api-contract.md` § 13); already correctly published via `/polyfill` only. |
| `shared-wallet/access-log.ts` | `shared-wallet/access-log.ts` | Diagnoses the shared-wallet isolation leak; the identity it prefixes is the relayed virtual id. Deleted at migration. |
| `shared-wallet/outbox-log.ts` | `shared-wallet/outbox-log.ts` | Polyfill-era trace probe over the injected SDK's private persistence (level-2 facts verified in the internal contract § 11). Deleted at migration. |
Resulting tree: 2 entry files + `model/` (2) + `surface/` (10) + `emulated-verifier/` (8) + `shared-wallet/` (6).
@@ -102,9 +102,9 @@ Fate labels exist per *export* in the two contracts; a module is mixed when its
- **Signal A — wire call × emulation-state write:** the module calls the injected `ng` (`getConfig().ng`) *and* mutates emulation state (`getCaps().mint/learn/open`, `addLink`, `declareInfrastructure`). Grep hits: `docs.ts:73` (mint), `inbox.ts:385` (learn), `store-registry.ts:875,893-895,903,1169,1371` (learn/open), `connect.ts:67` (learn).
- **Signal B — guarded/unguarded twin exports:** the `*Physical*` / `*Unguarded` pairs. Hits: `subscribe.ts:104/118`, `open-repo.ts:167/184`.
- **Signal C — act vs stand-in-processing under one namespace:** exports of the same module carrying different fate labels in the contracts. Hits: `inbox.ts` (§ 9: sender acts are target-shaped; deposit enumeration is emulation detail), `store-registry.ts` (§ 12: labels range from level-2 VERIFIED to NO COUNTERPART), `types.ts` (model types vs injection types, § 1 vs § 10), `nuri.ts` (guards vs `mintCap`, internal contract § 2), `polyfill.ts` (entry vs config store).
- **Signal C — act vs stand-in-processing under one namespace:** exports of the same module carrying different fate labels in the contracts. Hits: `inbox.ts` (§ 9: sender acts are target-shaped; deposit enumeration is emulation detail), `shared-wallet/account-registry.ts` (§ 12: labels range from level-2 VERIFIED to NO COUNTERPART), `types.ts` (model types vs injection types, § 1 vs § 10), `nuri.ts` (guards vs `mintCap`, internal contract § 2), `polyfill.ts` (entry vs config store).
Full mixed list: `docs.ts`, `read-model.ts`, `inbox.ts`, `subscribe.ts`, `open-repo.ts`, `store-registry.ts`, `nuri.ts`, `types.ts`, `polyfill.ts`, `ng-proxy.ts`, `caps.ts`, `connect.ts`. (`use-shape.ts` and `watch-shape.ts` cross fates only through imports — already the desired end state.)
Full mixed list: `docs.ts`, `surface/read-model.ts`, `inbox.ts`, `subscribe.ts`, `emulated-verifier/open-repo.ts`, `shared-wallet/account-registry.ts`, `nuri.ts`, `types.ts`, `polyfill.ts`, `surface/ng-proxy.ts`, `emulated-verifier/caps.ts`, `emulated-verifier/connect.ts`. (`surface/use-shape.ts` and `surface/watch-shape.ts` cross fates only through imports — already the desired end state.)
### The criterion
@@ -113,16 +113,16 @@ Full mixed list: `docs.ts`, `read-model.ts`, `inbox.ts`, `subscribe.ts`, `open-r
### Rulings
- **`docs.ts` — KEEP WHOLE.** The cap filing inside `docCreate` mirrors the target's own `doc_create`, which commits `AddRepo` to the Store branch and `ldp:contains` to the Main branch *in the same native call* (level 1, `engine/verifier/src/request_processor.rs:697-710`, re-verified). Splitting the mint into a separate caller-visible step would create a two-step creation surface the target does not have — the split itself would be the divergence. Same for the reach guard: the refusal is native (`RepoNotFound`). The fix is visibility, not surgery: both effects become named imports from `emulated-verifier/`.
- **`read-model.ts` — KEEP WHOLE.** Its possession gate mirrors native non-delivery, and its machinery-subject drop mirrors the structural invisibility of non-content branches (`BranchCrdt::None`, `types.rs:1420`, level 1). Both are the target's own composition of "read a document".
- **`inbox.ts` — SPLIT.** Different fates (api-contract § 9: the acts are target-shaped level-1 inventions; the deposit-list surface "may never have this shape") *and* different callers (apps post/share/watch; `connect.ts` processes). The deposit RDF vocabulary — pure emulation transport (upstream a deposit is a sealed message, `InboxMsgBody`, `engine/net/src/types.rs:4265`, level 1, carrying no target document) — lives once, on the emulated side.
- **`subscribe.ts` / `open-repo.ts` — SPLIT the physical doors out** (criterion's corollary): different caller (machinery only), different fate (the guarded/unguarded pair collapses to one call when the wallet is the boundary). They regroup in `shared-wallet/physical.ts`.
- **`store-registry.ts` — SPLIT three ways** (§ 3). It is the module the flat layout hides most: signal C fires on nearly every export group, and the repo already voted for the split by hand-building `store-registry-api.ts`.
- **`surface/read-model.ts` — KEEP WHOLE.** Its possession gate mirrors native non-delivery, and its machinery-subject drop mirrors the structural invisibility of non-content branches (`BranchCrdt::None`, `types.rs:1420`, level 1). Both are the target's own composition of "read a document".
- **`inbox.ts` — SPLIT.** Different fates (api-contract § 9: the acts are target-shaped level-1 inventions; the deposit-list surface "may never have this shape") *and* different callers (apps post/share/watch; `emulated-verifier/connect.ts` processes). The deposit RDF vocabulary — pure emulation transport (upstream a deposit is a sealed message, `InboxMsgBody`, `engine/net/src/types.rs:4265`, level 1, carrying no target document) — lives once, on the emulated side.
- **`subscribe.ts` / `emulated-verifier/open-repo.ts` — SPLIT the physical doors out** (criterion's corollary): different caller (machinery only), different fate (the guarded/unguarded pair collapses to one call when the wallet is the boundary). They regroup in `shared-wallet/physical.ts`.
- **`shared-wallet/account-registry.ts` — SPLIT three ways** (§ 3). It is the module the flat layout hides most: signal C fires on nearly every export group, and the repo already voted for the split by hand-building `surface/placement.ts`.
- **`nuri.ts` — SPLIT `mintCap` out.** Different fate (model vocabulary survives; the minting point is deleted when the engine mints) and the module's own stated invariant argues for it.
- **`types.ts` — SPLIT the injection types out** (small; see § 5 for the published-type consequence, which must be decided, not slipped).
- **`polyfill.ts` — SPLIT entry from config store.** Different fates (a published door vs internal state) and it removes real import cycles.
- **`ng-proxy.ts` — KEEP WHOLE.** 59 lines; the overrides *are* "what the native side takes over" and already compose `emulated-verifier/` predicates; the proxy artifact itself is the published surface. F1 (the fabricated `login` member) is fixed by deletion wherever the file lives.
- **`caps.ts` — KEEP WHOLE.** The per-holder keying is shared-wallet-flavoured, but splitting holder-resolution from the possession model would fragment one coherent level-1 model for no boundary gain; the injected `holder()` already isolates the dimension that collapses at migration.
- **`connect.ts` — KEEP WHOLE.** Its shim import is a deliberate cross-fate check (must-not-provision), which is exactly what the seam should show.
- **`surface/ng-proxy.ts` — KEEP WHOLE.** 59 lines; the overrides *are* "what the native side takes over" and already compose `emulated-verifier/` predicates; the proxy artifact itself is the published surface. F1 (the fabricated `login` member) is fixed by deletion wherever the file lives.
- **`emulated-verifier/caps.ts` — KEEP WHOLE.** The per-holder keying is shared-wallet-flavoured, but splitting holder-resolution from the possession model would fragment one coherent level-1 model for no boundary gain; the injected `holder()` already isolates the dimension that collapses at migration.
- **`emulated-verifier/connect.ts` — KEEP WHOLE.** Its shim import is a deliberate cross-fate check (must-not-provision), which is exactly what the seam should show.
---
@@ -153,7 +153,7 @@ Full mixed list: `docs.ts`, `read-model.ts`, `inbox.ts`, `subscribe.ts`, `open-r
Being specific, because overclaiming here would recreate the false-certainty problem the layout is meant to reduce:
- **It cannot decide whether a behaviour is the target's.** The decisive act remains reading `nextgraph-rs`. Both README § *Design principle* incidents would have type-checked and folder-checked: "every document has a native inbox" was a *belief* error, and the owner-inbox pointer was a *model* error inside code that belongs exactly where it was. A correctly named folder holds wrong code without complaint.
- **It cannot fix wrong claims inside correctly placed modules.** Finding F2 (`open-repo.ts`'s header asserts a silent-0-rows mechanism the source contradicts — upstream errors `RepoNotFound`, `request_processor.rs:264,269`, level 1) and `watch-shape.ts`'s unprovenanced "planned upgrade" survive any tree untouched. Header claims are policed by source-verification passes, not placement.
- **It cannot fix wrong claims inside correctly placed modules.** Finding F2 (`emulated-verifier/open-repo.ts`'s header asserts a silent-0-rows mechanism the source contradicts — upstream errors `RepoNotFound`, `request_processor.rs:264,269`, level 1) and `surface/watch-shape.ts`'s unprovenanced "planned upgrade" survive any tree untouched. Header claims are policed by source-verification passes, not placement.
- **It is coarser than the epistemic labels.** PASSTHROUGH vs LEVEL-1 SHAPE vs ASSUMPTION vary per *claim* within one module (`inbox.post`'s act is level-1-shaped, its transport is pure emulation, arbitrary payloads are an ASSUMPTION — all in one function's orbit). A folder carries one label; the contracts remain the finer instrument and the layout must not be read as replacing them.
- **It cannot stop in-module shape drift.** `Deposit` growing a target-document field — the exact divergence class of the reverted owner-inbox episode, since upstream a message carries no document (`InboxMsgBody`, `engine/net/src/types.rs:4265`, level 1) — is one line in a correctly placed file.
- **It does not police the empirical bets.** The sync barrier's push ordering and "a held subscription keeps the repo open" are pinned by the e2e CONTRACT-3 probe alone; no layout substitutes for that tripwire.
+10 -10
View File
@@ -109,7 +109,7 @@ and every subsequent patch to a `DeepSignalSet`
alongside the React one (`sdk/js/orm/src/frontendAdapters/{vue,svelte}/`).
`@ng-eventually/client` re-exports `useShape` from
[`../src/use-shape.ts`](../src/use-shape.ts); import it from the SDK
[`../src/surface/use-shape.ts`](../src/surface/use-shape.ts); import it from the SDK
(`@ng-eventually/client`), never from `@ng-org/orm` directly.
### What you get, in order
@@ -164,11 +164,11 @@ computes a result and returns once (`sparql_query`,
In `@ng-eventually/client` the one-shot read is exposed as:
- **`docs.sparqlQuery(sid, query, base?, anchor?)`** — a raw anchored SPARQL query
([`../src/docs.ts`](../src/docs.ts)). `anchor` = the document NURI to read; the
([`../src/surface/docs.ts`](../src/surface/docs.ts)). `anchor` = the document NURI to read; the
anchor restricts the query to that one repo's graph.
- **`readModel.readUnion(docs)`** — read a **bounded, by-need set** of document NURIs,
each with its own anchored query, grouped per subject
([`../src/read-model.ts`](../src/read-model.ts)). This is the polyfill's listing
([`../src/surface/read-model.ts`](../src/surface/read-model.ts)). This is the polyfill's listing
primitive (see [§ Current emulation status](#current-emulation-status) and
[`read-model.md`](../../../docs/read-model.md)).
@@ -185,7 +185,7 @@ You do not need the write internals to read, but reads and writes share the same
document model, so briefly:
- **Create a document:** `docs.docCreate(sid, crdt, cls, dest, store?)`
([`../src/docs.ts`](../src/docs.ts)) — mirrors `ng.doc_create`. **One document = one
([`../src/surface/docs.ts`](../src/surface/docs.ts)) — mirrors `ng.doc_create`. **One document = one
repo** (`did:ng:o:<RepoID>`); there is no separate `Document` type.
- **Write into it:** `docs.sparqlUpdate(sid, query, anchor)` — a SPARQL
`INSERT/DELETE` scoped to the anchor document's graph. Or, at the ORM layer, the ORM
@@ -294,7 +294,7 @@ from the reactive contract:
2. **The inbox uses a polling watcher.** The inbox is emulated
(`AppRequestCommandV0::InboxPost` has no verifier arm today; no wasm helper seals a
deposit), so `inbox.watch` ([`../src/inbox.ts`](../src/inbox.ts)) **polls** via
deposit), so `inbox.watch` ([`../src/surface/inbox.ts`](../src/surface/inbox.ts)) **polls** via
`setInterval` (default 1s) instead of subscribing. The finished contract is push
(the broker already routes the inbox natively); these become subscriptions when the
sealed-inbox path is exposed to JS (no such method exists today).
@@ -314,15 +314,15 @@ from the reactive contract:
`sparqlUpdate` on a doc it is itself subscribed to (`subscribeDoc` /
`ng.doc_subscribe`), the subscription callback appears NOT to fire for its OWN
local commit in the same session — so the reactive re-read chain
([`../src/watch-shape.ts`](../src/watch-shape.ts) `watchShape``reread`
[`../src/read-model.ts`](../src/read-model.ts) `readUnion`) never runs, and
([`../src/surface/watch-shape.ts`](../src/surface/watch-shape.ts) `watchShape``reread`
[`../src/surface/read-model.ts`](../src/surface/read-model.ts) `readUnion`) never runs, and
consumers keep the STALE value until the next connection delivers a fresh
initial `State`. **Remote** commits DO push correctly (verified: cross-browser
reactive update works). A code review verified the consumer wiring is correct,
the doc IS in the subscribed set, and a triggered re-read WOULD return the new
value — leaving the self-commit echo as the only suspect link. That link is
**INFERRED**, not observed: the real `ng.doc_subscribe` runtime is not readable
from source, and [`../src/subscribe.ts`](../src/subscribe.ts)'s own doc-comment
from source, and [`../src/surface/subscribe.ts`](../src/surface/subscribe.ts)'s own doc-comment
CLAIMS local writes push a `Patch` — contradicted by the observation. (This
also sits in tension with § *The reactivity model* above, which documents the
target contract — one commit, every subscriber pushed, local or remote.) The
@@ -330,9 +330,9 @@ from the reactive contract:
must propagate reactively to ALL viewers — other viewers (remote push, which
works) AND the writer's own view (this suspect link). **Treatment (PLANNED,
not done):** confirm first via the temporary instrumentation just added
([`../src/subscribe.ts`](../src/subscribe.ts) ≈`:119` logs
([`../src/surface/subscribe.ts`](../src/surface/subscribe.ts) ≈`:119` logs
`doc_subscribe FIRE <nuri> (State|Patch)`;
[`../src/watch-shape.ts`](../src/watch-shape.ts) ≈`:341` logs
[`../src/surface/watch-shape.ts`](../src/surface/watch-shape.ts) ≈`:341` logs
`reread TRIGGER by <nuri>` — line numbers volatile, grep the log strings);
then, IF confirmed, fix **polyfill-side** — a
local commit should notify the doc's active `subscribeDoc` callbacks.
+8 -2
View File
@@ -12,8 +12,14 @@ export function makeNg(): Record<string, any> {
get(_target, prop: string) {
const { ng } = getConfig();
// login / session_start → open the SHARED wallet invisibly.
if (prop === "login" || prop === "session_start") {
// session_start → open the SHARED wallet invisibly.
//
// `login` used to be listed here too. `@ng-org/web` exposes no such method —
// zero occurrences in the installed declarations and in `sdk/js/lib-wasm/src/lib.rs`
// — so the proxy FABRICATED a member: `ng.login` answered a function instead of
// `undefined`, and calling it threw. The one place this wrapper added to the SDK
// surface, against its own header. Removed 2026-08-03.
if (prop === "session_start") {
return (...args: any[]) => {
// TODO(polyfill): supply shared-wallet credentials so no wallet UI
// is shown. For now, passthrough.