refactor(vocabulary): les noms publiés parlent la langue de la cible, et un test le tient
La correction de nomenclature du 2026-07-30 — en amont un *wallet* n'est qu'un trousseau, ce qui possède des stores est un **user** (un *site*) — s'était faite à la main. `walletInbox` y a échappé et a vécu des semaines, en faisant des dégâts : le nom rendait « une inbox par wallet » évident, masquant qu'un user en a **deux** en amont (repos de store public et protected, les deux seuls `AddInboxCap` du moteur). Une discipline appliquée à la main en oublie un ; un test non. D'où `test/vocabulary.test.ts` : tout nom publié est bâti sur des mots que la CIBLE emploie — vérifiés dans `nextgraph-rs` — ou porte un marqueur disant POURQUOI il n'existe qu'ici (`virtual`, `physical`, `shim`, `emulated`, `polyfill`), ce qui dit aussi quand il disparaît. Un échec n'est pas « renommer pour faire passer le test », c'est une question : la cible a-t-elle un mot pour ça ? la chose n'existe-t-elle qu'ici ? le mot est-il vraiment de la glue ? Ce que le test a trouvé, et les réponses : - `walletInbox` → `userInbox`, avec l'écart de cardinalité écrit noir sur blanc plutôt que caché par le nom. - `accounts` / `AccountRecord` / `AccountStorage` → `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage`, module `accounts.ts` → `virtual-users.ts`. « account » n'est pas de la cible : c'est notre mot pour l'utilisateur virtuel, et le marqueur le dit désormais. - `readModel` → la fonction `readUnion`, exposée directement. « model » n'était ni de la cible ni de la glue, et le namespace ne tenait qu'une fonction. - Le reste était du vocabulaire légitime à déclarer (`subject`, `base`, `schema`, `connected`, le modèle réactif de l'ORM). Corrigé au passage, sur signalement du contrat interne : l'en-tête d'`open-repo` justifiait son correctif par un mécanisme que le source contredit. Un repo absent de `self.repos` lève bien `RepoNotFound` (`engine/verifier/src/request_processor.rs:264,269`). Les 0 lignes observées viennent d'ailleurs — `Verifier::load` repeuple `self.repos` depuis le stockage sur un profil persistant (`verifier.rs:535-560`), et notre propre `readDoc` attrape toute erreur et rend `[]`. Le correctif est bon, le diagnostic écrit à côté ne l'était pas. 159 tests unitaires, typecheck src/test/e2e vert, e2e 40/40 contre le broker.
This commit is contained in:
+14
-14
@@ -462,14 +462,14 @@ 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/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.
|
||||
> **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`, `userInbox`, `openDocumentInbox`, `documentInboxAddress`. The rest — `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `resolveAccount`, `ensureAccount`, `reservedAccount`, `resetRegistryCache`, and the `VirtualUserRecord` / `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
|
||||
export type Scope = "public" | "protected" | "private";
|
||||
|
||||
// store-registry.ts:90,234
|
||||
export interface AccountRecord {
|
||||
export interface VirtualUserRecord {
|
||||
id: string;
|
||||
docPublic: Nuri;
|
||||
docProtected: Nuri;
|
||||
@@ -490,7 +490,7 @@ export async function resolveScopeGraph(scope: Scope): Promise<Nuri>;
|
||||
export async function resolveWriteGraph(id: string, scope: Scope): Promise<Nuri>;
|
||||
|
||||
// inbox-side (store-registry.ts:772, 837, 1133, 1203, 1286)
|
||||
export async function walletInbox(id: string): Promise<Nuri>;
|
||||
export async function userInbox(id: string): Promise<Nuri>;
|
||||
export async function isOwnInbox(nuri: Nuri): Promise<boolean>;
|
||||
export async function openDocumentInbox(doc: Nuri): Promise<Nuri>;
|
||||
export async function documentInboxAddress(doc: Nuri): Promise<Nuri | undefined>;
|
||||
@@ -501,8 +501,8 @@ export async function addLink(cap: ReadCap): Promise<void>;
|
||||
export async function readLinks(): Promise<ReadCap[]>;
|
||||
|
||||
// shim machinery (store-registry.ts:542, 631, 213, 278)
|
||||
export async function resolveAccount(id: string): Promise<AccountRecord | null>;
|
||||
export async function ensureAccount(id: string): Promise<AccountRecord>;
|
||||
export async function resolveAccount(id: string): Promise<VirtualUserRecord | null>;
|
||||
export async function ensureAccount(id: string): Promise<VirtualUserRecord>;
|
||||
export function reservedAccount(name: string): string;
|
||||
export function resetRegistryCache(): void;
|
||||
```
|
||||
@@ -512,10 +512,10 @@ export function resetRegistryCache(): void;
|
||||
- **`createEntityDoc(id, scope)` → level 2, VERIFIED direction.** Target: `doc_create(session_id, crdt, class_name, destination, store_repo)` aimed at the identity's real per-scope store (see § 7 for the store-targeting nuance — the nodejs SDK already takes `store_type`/`store_repo` strings). The two writes the lib performs by hand are **native side effects** of `doc_create` upstream: the `ldp:contains` listing on the store's Main branch and the `AddRepo { read_cap }` on its Store branch (`engine/verifier/src/request_processor.rs:697-710`). The `id` parameter disappears (the session IS the identity); expect `createEntityDoc(id, scope)` to become `doc_create(sid, …, storeOf(scope))` with no listing/cap bookkeeping.
|
||||
- **`listMyEntityDocs(id, scope)` → level 1/2, VERIFIED mechanism.** Upstream the listing is the store's `ldp:contains` graph (written at `request_processor.rs:706-708`), readable with an anchored `sparql_query` on the store; the caps come back by replaying the Store branch (`AddRepo::verify` → `load_repo_from_read_cap`). The function's shape (give me my per-scope doc NURIs) survives; its implementation becomes one native read.
|
||||
- **`userStoreDoc(id, scope)` / `resolveScopeGraph(scope)` / `resolveWriteGraph(id, scope)` → level 2, VERIFIED.** The target answers these from the session: `did:ng:` + `session.private_store_id | protected_store_id | public_store_id` (`Session`, `index.d.ts:264-272`). The store IS the container; the per-scope index document disappears.
|
||||
- **`walletInbox(id)` → level 1, VERIFIED counterpart with a different granularity.** Upstream a user's inboxes are their public and protected STORE repos' inboxes — the only two `AddInboxCap` commits in the engine (`engine/verifier/src/site.rs:128,149`). An identity-level "my inbox" therefore maps to a store inbox; the resolution moves into the lib/SDK and the consumer's act (deposit to an address, process my own) is unchanged.
|
||||
- **`userInbox(id)` → level 1, VERIFIED counterpart with a different granularity.** Upstream a user's inboxes are their public and protected STORE repos' inboxes — the only two `AddInboxCap` commits in the engine (`engine/verifier/src/site.rs:128,149`). An identity-level "my inbox" therefore maps to a store inbox; the resolution moves into the lib/SDK and the consumer's act (deposit to an address, process my own) is unchanged.
|
||||
- **`openDocumentInbox(doc)` / `documentInboxAddress(doc)` → level 1, VERIFIED support, no exerciser.** Every `Repo` carries `inbox: Option<PrivKey>` (`engine/repo/src/repo.rs:126`); `AddInboxCapV0` is keyed by `repo_id` with no is-store restriction (`engine/repo/src/types.rs:1973`; applied at `engine/verifier/src/verifier.rs:1920-1928`); but no code path creates one for a plain document (`doc_create` leaves `inbox: None`, `repo.rs:574`) and no level-2/3 API exposes any of it. So: the *capability* is engine-verified; the *functions* are invented surface; and the **address publication is a real, deliberate divergence** (upstream transmits addresses, never publishes them — § 9), with the ownership guard compensating our design, not mirroring an upstream rule.
|
||||
- **`addLink(cap)` / `readLinks()` → level 1, VERIFIED model, no JS surface.** The emulated `AddLink { read_cap }` register (`engine/repo/src/types.rs:1939-1948` — *"so that a user can share with all its device a new Link they received"*, external repos only). Upstream this filing happens inside the verifier when it processes the inbox; the future SDK most likely never exposes these as calls, so consumers should not code against them (§ 15).
|
||||
- **`resolveAccount` / `ensureAccount` / `AccountRecord` / `RegistrySession` / `reservedAccount` / `resetRegistryCache` → NO COUNTERPART.** The shared-wallet shim (accounts directory, pointer → doc-shim indirection) has no image in the target — the target has no central directory of identities (`docs/migration-guide.md` § 3). The whole group disappears with the shim.
|
||||
- **`resolveAccount` / `ensureAccount` / `VirtualUserRecord` / `RegistrySession` / `reservedAccount` / `resetRegistryCache` → NO COUNTERPART.** The shared-wallet shim (accounts directory, pointer → doc-shim indirection) has no image in the target — the target has no central directory of identities (`docs/migration-guide.md` § 3). The whole group disappears with the shim.
|
||||
- **`isOwnInbox` / `myInboxes` → NO COUNTERPART as API.** Upstream the question "which inboxes may I read" is answered inside the verifier by the User branch's `AddInboxCap` records; nothing suggests a JS API for it. These exist for the emulated read guard and the connection drain.
|
||||
|
||||
---
|
||||
@@ -525,15 +525,15 @@ export function resetRegistryCache(): void;
|
||||
### Today
|
||||
|
||||
```ts
|
||||
// @ng-eventually/client — accounts.ts (namespace accounts)
|
||||
// @ng-eventually/client — virtualUsers.ts (namespace accounts)
|
||||
export const ACCOUNT_STORAGE_KEY = "ng-eventually.account.id"; // :18
|
||||
export interface AccountStorage { // :26
|
||||
export interface VirtualUserStorage { // :26
|
||||
getItem(key: string): string | null;
|
||||
setItem(key: string, value: string): void;
|
||||
removeItem(key: string): void;
|
||||
}
|
||||
export class IdentityStore { // :37
|
||||
constructor(storage: AccountStorage | null, key?: string);
|
||||
constructor(storage: VirtualUserStorage | null, key?: string);
|
||||
get(): string | null;
|
||||
set(id: string): string | null;
|
||||
clear(): void;
|
||||
@@ -558,7 +558,7 @@ declare function user_connect(client_info: any, user_id: string, location?: stri
|
||||
declare function user_disconnect(user_id: string): Promise<void>;
|
||||
```
|
||||
|
||||
- `accounts.*` (the persisted identity id) — **NO COUNTERPART**; removed at migration (`docs/migration-guide.md` § 5). It exists only because every virtual user shares one wallet. It is exported from the SDK entry, which is a placement wart (§ 15).
|
||||
- `virtualUsers.*` (the persisted identity id) — **NO COUNTERPART**; removed at migration (`docs/migration-guide.md` § 5). It exists only because every virtual user shares one wallet. It is exported from the SDK entry, which is a placement wart (§ 15).
|
||||
- `setCurrentUser` / `getCurrentUser` — **NO COUNTERPART**; the relay of an identity the broker cannot see. Disappears with the shared wallet.
|
||||
- `connectedUser()` — the awaitable form of what the target does **automatically**: the recipient's verifier processes its inbox as messages arrive/at connection (`Verifier::inbox`, `engine/verifier/src/verifier.rs:1674`). VERIFIED at level 1 that no consumer call is needed upstream; the polyfill fires it from `setCurrentUser` for the same reason. A consumer should treat it as "await a deterministic start" (tests), not as an operation the future SDK will name.
|
||||
|
||||
@@ -586,9 +586,9 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
||||
- **`getConfig` / `getStoreRegistryDeps`** — tagged `@internal` in source, exported for the lib's own wrappers.
|
||||
- **`resetConfig` / `resetStoreRegistry` / `resetCaps` / `storeRegistry.resetRegistryCache`** — test/reset machinery. In particular `resetCaps` wipes EVERY holder's caps, which no product flow should ever do.
|
||||
- **`getCaps()` and the `CapRegistry` class** — the registry is the emulation's engine room. The consumer surface is `capFor` (possession lookup), `inbox.shareCap` (grant), and the acts that file caps implicitly (creating a document, processing one's inbox). `CapRegistry.grantWrite` / `governsWrite` / `canWrite` / `hasWritePolicy` are explicitly decorative until P1b — the guard they feed is bypassed by every internal writer.
|
||||
- ~~**`storeRegistry.reservedAccount`, `resolveAccount`, `ensureAccount`, `AccountRecord`, `RegistrySession`**~~ — **RESOLVED 2026-08-03**: no longer exported. Shim internals, now in `docs/internal-contract.md`. The consumer's legitimate touchpoint is `configureStoreRegistry` (bootstrap) plus the scope/entity resolvers.
|
||||
- ~~**`storeRegistry.reservedAccount`, `resolveAccount`, `ensureAccount`, `VirtualUserRecord`, `RegistrySession`**~~ — **RESOLVED 2026-08-03**: no longer exported. Shim internals, now in `docs/internal-contract.md`. The consumer's legitimate touchpoint is `configureStoreRegistry` (bootstrap) plus the scope/entity resolvers.
|
||||
- ~~**`storeRegistry.addLink` / `readLinks`**~~ — **RESOLVED 2026-08-03**: no longer exported. Consumers receive caps by processing their inbox (automated at connection); calling these directly baked in a register the verifier owns upstream.
|
||||
- ~~**`accounts.*` on the SDK entry**~~ — **RESOLVED 2026-08-03**: moved to `/polyfill`, where its disappearance at migration is visible at the import line.
|
||||
- ~~**`virtualUsers.*` on the SDK entry**~~ — **RESOLVED 2026-08-03**: moved to `/polyfill`, where its disappearance at migration is visible at the import line.
|
||||
- **`inbox.watch`'s `_opts?: { intervalMs?: number }`** — accepted and ignored (no polling exists). Dead compatibility surface; do not pass it.
|
||||
- **The `label` parameters** on `docs.sparqlUpdate` / `docs.sparqlQuery` / `docs.depositInto` — lib-internal access-log tags, never forwarded to `ng`. The real signatures have no such parameter.
|
||||
|
||||
@@ -605,7 +605,7 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
||||
|
||||
## Appendix — full export inventory (for diffing)
|
||||
|
||||
`@ng-eventually/client` (from `index.ts`): types `Nuri`, `ReadCap`, `Scope`, `PrincipalId`, `NgLike`, `UseShapeLike`, `ShapeQuery`, `ShapeObservable`, `DocChange`, `DocChangeType`, `Unsubscribe`, `UnionSubject`, `AccountRecord`, `RegistrySession`, `AccountStorage`, `Deposit`, `PostOptions` (via namespaces), re-exported `ShapeType`, `BaseType`, `Schema`, `DeepSignalSet`, `NG`; values `ng`, `useShape`, `watchShape`, `init`, `initNg`, `subscribeDoc`, `subscribeDocs`, `docChangeType`, `escapeLiteral`, `escapeIri`, `assertNuri`, `isNuri`, `hasReadCap`; namespaces `inbox` (`post`, `postToDocument`, `shareCap`, `read`, `materialize`, `readSynced`, `processInbox`, `watch`), `docs` (`docCreate`, `sparqlUpdate`, `sparqlQuery`, `depositInto`), `readModel` (`readUnion`), `storeRegistry` (`reservedAccount`, `resetRegistryCache`, `resolveAccount`, `ensureAccount`, `resolveWriteGraph`, `resolveScopeGraph`, `walletInbox`, `isOwnInbox`, `createEntityDoc`, `userStoreDoc`, `openDocumentInbox`, `documentInboxAddress`, `myInboxes`, `addLink`, `readLinks`, `listMyEntityDocs`), `accounts` (`ACCOUNT_STORAGE_KEY`, `IdentityStore`, `browserIdentityStore`).
|
||||
`@ng-eventually/client` (from `index.ts`): types `Nuri`, `ReadCap`, `Scope`, `PrincipalId`, `NgLike`, `UseShapeLike`, `ShapeQuery`, `ShapeObservable`, `DocChange`, `DocChangeType`, `Unsubscribe`, `UnionSubject`, `VirtualUserRecord`, `RegistrySession`, `VirtualUserStorage`, `Deposit`, `PostOptions` (via namespaces), re-exported `ShapeType`, `BaseType`, `Schema`, `DeepSignalSet`, `NG`; values `ng`, `useShape`, `watchShape`, `init`, `initNg`, `subscribeDoc`, `subscribeDocs`, `docChangeType`, `escapeLiteral`, `escapeIri`, `assertNuri`, `isNuri`, `hasReadCap`; namespaces `inbox` (`post`, `postToDocument`, `shareCap`, `read`, `materialize`, `readSynced`, `processInbox`, `watch`), `docs` (`docCreate`, `sparqlUpdate`, `sparqlQuery`, `depositInto`), `readModel` (`readUnion`), `storeRegistry` (`reservedAccount`, `resetRegistryCache`, `resolveAccount`, `ensureAccount`, `resolveWriteGraph`, `resolveScopeGraph`, `userInbox`, `isOwnInbox`, `createEntityDoc`, `userStoreDoc`, `openDocumentInbox`, `documentInboxAddress`, `myInboxes`, `addLink`, `readLinks`, `listMyEntityDocs`), `accounts` (`ACCOUNT_STORAGE_KEY`, `IdentityStore`, `browserIdentityStore`).
|
||||
|
||||
`@ng-eventually/client/polyfill` (from `polyfill.ts`): types `StoreRegistryDeps`, `EventuallyConfig`; values `configure`, `getConfig`, `resetConfig`, `configureStoreRegistry`, `getStoreRegistryDeps`, `resetStoreRegistry`, `setCurrentUser`, `getCurrentUser`, `getCaps`, `capFor`, `resetCaps`, `CapRegistry`, `shareCap`, `connectedUser`.
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ This is the point where the emulation is furthest from the eventual target, wher
|
||||
|
||||
`inbox.read` had no guard and **absorbs caps into the reader's keyring**, so `inbox.read(someoneElsesInbox)` pocketed the caps addressed to them and directed sharing was defeatable by anyone who knew an inbox NURI. The inbox was never guarded before either, but before P1a it carried nothing that granted access.
|
||||
|
||||
Fixed in step 2 of [`2026-07-30-virtual-wallet-boundary.md`](2026-07-30-virtual-wallet-boundary.md): an inbox now BELONGS to a virtual user (`storeRegistry.walletInbox`), and only its owner may read it. Depositing into anyone's inbox stays open — that is the one legitimate cross-wallet act, and the only way a link crosses between wallets at all.
|
||||
Fixed in step 2 of [`2026-07-30-virtual-wallet-boundary.md`](2026-07-30-virtual-wallet-boundary.md): an inbox now BELONGS to a virtual user (`storeRegistry.userInbox`), and only its owner may read it. Depositing into anyone's inbox stays open — that is the one legitimate cross-wallet act, and the only way a link crosses between wallets at all.
|
||||
|
||||
## Follow-up decided by the PO — to plan, NOT in this lot
|
||||
|
||||
@@ -192,8 +192,8 @@ 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/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 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` + `userInbox`, 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 `userInbox(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.
|
||||
|
||||
## Documentation state
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
> And four more, all confirmed:
|
||||
>
|
||||
> 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.
|
||||
> 5. **D3 is false outside entity documents.** `capFor(scopeIndexDoc)` and `capFor(userInbox)` 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.**
|
||||
>
|
||||
@@ -69,7 +69,7 @@ All read in `nextgraph-rs` (`git 213338f6`) on 2026-07-30, recorded in full in [
|
||||
| scope index / scope container (`scopeIndexDoc`, `readScopeIndex`, `indexDocOf`, `INDEX_SUBJECT`) | **store** (`storeDoc`, `readStore`, …) | the thing that lists a user's documents IS a store |
|
||||
| `shim:contains` | `ldp:contains` | NextGraph's own predicate for exactly this |
|
||||
|
||||
`docPublic` / `docProtected` / `docPrivate` on `AccountRecord` already read as stores; keep them, or rename to `publicStore` / `protectedStore` / `privateStore` for symmetry.
|
||||
`docPublic` / `docProtected` / `docPrivate` on `VirtualUserRecord` already read as stores; keep them, or rename to `publicStore` / `protectedStore` / `privateStore` for symmetry.
|
||||
|
||||
### D2 — Emulate the Store branch as a distinct SUBJECT, not a distinct graph or document
|
||||
|
||||
@@ -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 `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.
|
||||
So the emulation is a **local, per-virtual-user store** — the same nature as `shared-wallet/virtualUsers.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 D1–D3 to be correct.
|
||||
|
||||
@@ -106,7 +106,7 @@ So the emulation is a **local, per-virtual-user store** — the same nature as `
|
||||
|
||||
## What this breaks
|
||||
|
||||
`storeRegistry`'s exported names change (`scopeIndexDoc`, `listEntityDocs`, `AccountRecord` fields). `shim:contains` becomes `ldp:contains`, so **existing dev wallets stop resolving their documents** — acceptable for dev data, and consistent with how the pointer/doc-shim migration was handled before, but it must be stated rather than discovered.
|
||||
`storeRegistry`'s exported names change (`scopeIndexDoc`, `listEntityDocs`, `VirtualUserRecord` fields). `shim:contains` becomes `ldp:contains`, so **existing dev wallets stop resolving their documents** — acceptable for dev data, and consistent with how the pointer/doc-shim migration was handled before, but it must be stated rather than discovered.
|
||||
|
||||
## Risks I want challenged
|
||||
|
||||
|
||||
@@ -113,13 +113,13 @@ Applies to every exported surface, including ones added later: **if it is expose
|
||||
## Order of work
|
||||
|
||||
1. ~~**Remove `discovery.***~~ — **DONE 2026-07-30.** `src/discovery.ts` and `test/discovery.test.ts` deleted; `INDEX_ACCOUNT`, `watchShape`'s public-scope fold and its discovery-index container subscription, `nurisFromRef`, the `submitToIndex` guard, and the e2e discovery block all removed. P1a's acceptance test did not need re-basing: `test/cross-user-access.test.ts` already proves the same property (a bare reference reads nothing, the link reads the document) on the model's own terms — following a link — so `watch-shape.test.ts` (e), which proved it on the discovery fold, was dropped. Docs realigned: the ADR is marked superseded, `read-model.md` now describes ONE regime (follow, never enumerate), and the root README's capability row records the removal.
|
||||
2. ~~**"My inbox" + the inbox read guard**~~ — **DONE 2026-07-30.** `storeRegistry.walletInbox(id)` gives every virtual user its own inbox document, created on first sight and recorded in the doc-shim under its own predicate (`shim:docInbox`), read by its OWN query so an account record written before this existed still resolves — the fixed account SELECT did not grow a fourth required field. `isOwnInbox(nuri)` is the predicate; `inbox.read` / `readSynced` (hence `watch`, which reads through it) refuse an inbox that is not the connected wallet's, and refuse outright when no identity is set. **Depositing stays open** — `post` / `shareCap` are untouched, because that is the one legitimate cross-wallet act. The shared `resolveInboxAnchor` (a reserved account's document, an inbox COMMON to every wallet) was removed: it was unused by the library and violated *nothing common*. Locked by `test/isolation-active.test.ts` *an inbox may be DEPOSITED into by anyone, and READ only by its owner*, which walks the exact breach — Alice deposits, cannot read back; Mallory knowing the NURI absorbs nothing; anonymous is refused; Bob reads his own and only then does the cap land.
|
||||
2. ~~**"My inbox" + the inbox read guard**~~ — **DONE 2026-07-30.** `storeRegistry.userInbox(id)` gives every virtual user its own inbox document, created on first sight and recorded in the doc-shim under its own predicate (`shim:docInbox`), read by its OWN query so an account record written before this existed still resolves — the fixed account SELECT did not grow a fourth required field. `isOwnInbox(nuri)` is the predicate; `inbox.read` / `readSynced` (hence `watch`, which reads through it) refuse an inbox that is not the connected wallet's, and refuse outright when no identity is set. **Depositing stays open** — `post` / `shareCap` are untouched, because that is the one legitimate cross-wallet act. The shared `resolveInboxAnchor` (a reserved account's document, an inbox COMMON to every wallet) was removed: it was unused by the library and violated *nothing common*. Locked by `test/isolation-active.test.ts` *an inbox may be DEPOSITED into by anyone, and READ only by its owner*, which walks the exact breach — Alice deposits, cannot read back; Mallory knowing the NURI absorbs nothing; anonymous is refused; Bob reads his own and only then does the cap land.
|
||||
|
||||
*Not done, and deliberately*: per-DOCUMENT inboxes. ~~Upstream every document has one~~ — **false, corrected 2026-08-03**: no document has an inbox upstream, and neither does the private store (see step 7's correction). Here only the wallet does. **The PO has ruled they must come** (2026-07-30) — *"it can come in a second step, but it must come"* — so this is a commitment, not an option. The guard predicate (`isOwnInbox`) is where they plug in: it answers "is this inbox mine?", which extends to "…one of my documents' inboxes" without changing a single caller.
|
||||
|
||||
### Two defects this step surfaced — the first still open, the second closed by steps 5–6
|
||||
|
||||
**`walletInbox(id)` is a directory, and directories do not exist.** It resolves ANY wallet's inbox from its identity id, and it is exported (`storeRegistry.*` is re-exported from the SDK entry). But you cannot look someone up in NextGraph — you cannot discover, you can only follow links. Their inbox NURI reaches you because *they gave it to you*, not because you resolved it from a name. Resolving **my own** inbox is legitimate plumbing; resolving **anyone's** is the same shape as the discovery index just removed. Fix: the public surface becomes "my inbox" (no argument), and reaching someone else's requires a NURI you were given. Resolution-by-id stays internal, for the shim and the tests.
|
||||
**`userInbox(id)` is a directory, and directories do not exist.** It resolves ANY wallet's inbox from its identity id, and it is exported (`storeRegistry.*` is re-exported from the SDK entry). But you cannot look someone up in NextGraph — you cannot discover, you can only follow links. Their inbox NURI reaches you because *they gave it to you*, not because you resolved it from a name. Resolving **my own** inbox is legitimate plumbing; resolving **anyone's** is the same shape as the discovery index just removed. Fix: the public surface becomes "my inbox" (no argument), and reaching someone else's requires a NURI you were given. Resolution-by-id stays internal, for the shim and the tests.
|
||||
|
||||
**The keyring is not stored anywhere, and the shape is wrong — fix it now, not at P1b.** It is an in-memory `Map<accountKey, Map<Nuri, ReadCap>>`, rebuilt from scratch each session. Nothing persists a cap *as a cap*. PO directive, 2026-07-30:
|
||||
|
||||
|
||||
@@ -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 (`shared-wallet/accounts.ts`) must never touch NextGraph. The lib itself
|
||||
the lib's identity store (`shared-wallet/virtualUsers.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,8 +63,8 @@ barrier becomes the real per-user login — the flow shape does not change.
|
||||
|
||||
## How this lib realizes it
|
||||
|
||||
`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
|
||||
`shared-wallet/virtualUsers.ts` is an `IdentityStore`: `set(id)` / `clear()` / `get()` only read/write
|
||||
the identity id in an injected `VirtualUserStorage`; 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
|
||||
[`../simulation.md`](../simulation.md).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**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`, `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`.
|
||||
**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`, `userInbox`, `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/virtualUsers.ts`, and the types `VirtualUserStorage`, `VirtualUserRecord`, `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.
|
||||
|
||||
@@ -144,7 +144,7 @@ Fire-and-forget wrapper over the published `connectedUser()` (restore Links, the
|
||||
|
||||
## 9. The shim registry — the unexported slice of `shared-wallet/account-registry.ts`
|
||||
|
||||
The sharpest boundary case: `surface/placement.ts` publishes the 7 app-facing calls; the 9 exports below stay internal (importable by the lib's modules, unit tests and the e2e harness, not by an application through the package entries). The 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 `VirtualUserRecord` (`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
|
||||
|
||||
@@ -154,9 +154,9 @@ export function reservedAccount(name: string): string;
|
||||
// store-registry.ts:278
|
||||
export function resetRegistryCache(): void;
|
||||
// store-registry.ts:542
|
||||
export async function resolveAccount(id: string): Promise<AccountRecord | null>;
|
||||
export async function resolveAccount(id: string): Promise<VirtualUserRecord | null>;
|
||||
// store-registry.ts:631
|
||||
export async function ensureAccount(id: string): Promise<AccountRecord>;
|
||||
export async function ensureAccount(id: string): Promise<VirtualUserRecord>;
|
||||
```
|
||||
|
||||
`resolveAccount` — barrier-authoritative O(1) lookup of one account's record in the doc-shim; `ensureAccount` — resolve-or-provision (creates the three scope docs on first sight, concurrency-deduped); `reservedAccount` — NUL-prefixed sentinel namespace for lib-internal accounts; `resetRegistryCache` — test/wallet-switch reset.
|
||||
@@ -247,7 +247,7 @@ export function inspectOutbox(): void;
|
||||
|
||||
**F3 — incomplete citation in `subscribe.ts`.** `subscribe.ts:31` cites the ORM fan-out abort as "`initialize.rs:125-128`" with no path. The file is `engine/verifier/src/orm/graph/initialize.rs`; lines 125-128 are the graph loop calling `self.open_for_target(&nuri.target, true).await?` — verified, the `?` propagates `RepoNotFound` and aborts the whole subscription. Substance correct; the bare filename is unfindable without this note.
|
||||
|
||||
**F4 — `docs/api-contract.md` lags the `surface/placement.ts` split.** Its § 12 and appendix still list `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `reservedAccount`, `resetRegistryCache` as the SDK entry's `storeRegistry` namespace, and § 13/§ 15 place `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.
|
||||
**F4 — `docs/api-contract.md` lags the `surface/placement.ts` split.** Its § 12 and appendix still list `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `reservedAccount`, `resetRegistryCache` as the SDK entry's `storeRegistry` namespace, and § 13/§ 15 place `virtualUsers.*` on the SDK entry — since the split (`index.ts: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`.
|
||||
|
||||
@@ -266,4 +266,4 @@ Fully internal modules: `shared-wallet/access-log.ts` (`AccessOp`, `setAccessLog
|
||||
|
||||
Internal slices of partially-published modules: `nuri.ts` (`targetOf`, `parseNuri`, `mintCap`); `emulated-verifier/connect.ts` (`startConnect`); `subscribe.ts` (`subscribePhysicalDoc`); `shared-wallet/account-registry.ts` (`reservedAccount`, `resetRegistryCache`, `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`).
|
||||
|
||||
Modules with no internal exports (everything they export is published): `types.ts`, `docs.ts`, `inbox.ts`, `surface/read-model.ts`, `shared-wallet/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.
|
||||
Modules with no internal exports (everything they export is published): `types.ts`, `docs.ts`, `inbox.ts`, `surface/read-model.ts`, `shared-wallet/virtualUsers.ts`, `emulated-verifier/caps.ts`, `sparql.ts`, `lifecycle.ts`, `surface/use-shape.ts`, `surface/watch-shape.ts`, `surface/placement.ts`, and the two entry points.
|
||||
|
||||
@@ -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 `shared-wallet/accounts.ts` (the `IdentityStore` that persists the identity id in
|
||||
Remove `shared-wallet/virtualUsers.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)).
|
||||
|
||||
@@ -353,7 +353,7 @@ does not mistake sync-lag for "account absent" and PROVISION a fork). Since no s
|
||||
document is both (previous section), the shim uses an **indirection**:
|
||||
|
||||
1. **doc-shim** — a `doc_create`d graph document (`did:ng:o:...`, hence a first-`State`
|
||||
barrier). **All `AccountRecord`s live inside it.** Because it is subscribable, an
|
||||
barrier). **All `VirtualUserRecord`s live inside it.** Because it is subscribable, an
|
||||
anchored read behind its `ensureRepoOpen` barrier is **authoritative**: a cold 0
|
||||
means the account is genuinely absent.
|
||||
2. **pointer** — a single well-known, **write-once** triple in the store-root graph,
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ globally enumerable, and nothing is meant to be.
|
||||
### Everything = follow a graph, never enumerate across accounts
|
||||
|
||||
My participations / my profile, protected data an owner has granted me, my
|
||||
notifications — none of these is enumerated across accounts. Each is reached by
|
||||
notifications — none of these is enumerated across virtualUsers. Each is reached by
|
||||
what is already reachable to me:
|
||||
|
||||
- my own docs (always in `self.repos`, and whose caps I hold);
|
||||
|
||||
+6
-6
@@ -125,7 +125,7 @@ public/protected/private stores — on top of one shared wallet.
|
||||
account→document trust root, which is why every untrusted value that reaches its
|
||||
SPARQL is escaped (see SPARQL hardening below). It makes identity resolution
|
||||
cross-device: another device opening the same wallet reads the same pointer → the
|
||||
same doc-shim → the same accounts.
|
||||
same doc-shim → the same virtualUsers.
|
||||
- **Per-entity documents + per-scope index.** `createEntityDoc(id, scope)`
|
||||
makes a dedicated document for one entity (mirrors the target, where each entity
|
||||
is its own document/repo with a future inbox) and appends its NURI to the
|
||||
@@ -166,7 +166,7 @@ Virtual user (id)
|
||||
|
||||
So the 3 native stores (public/protected/private) are present, but emulated: each
|
||||
"store" is an index document
|
||||
(`AccountRecord.{docPublic,docProtected,docPrivate}`) that lists the NURIs of the
|
||||
(`VirtualUserRecord.{docPublic,docProtected,docPrivate}`) that lists the NURIs of the
|
||||
per-entity documents in that scope. It is not a physical native store.
|
||||
|
||||
Everything is physical in one place: the 3 index documents, every per-entity
|
||||
@@ -200,7 +200,7 @@ store-id:
|
||||
blocker, [`migration-guide.md`](./migration-guide.md)). At migration each scope
|
||||
resolves to the user's real per-scope store — the change is in this function,
|
||||
and the consumer application is unchanged.
|
||||
- **`walletInbox(id)` / `openDocumentInbox(doc)`** — an inbox BELONGS to someone. The
|
||||
- **`userInbox(id)` / `openDocumentInbox(doc)`** — an inbox BELONGS to someone. The
|
||||
first is a user's own inbox (where Links arrive), the second a DEDICATED inbox for
|
||||
one of its documents, opened on demand by its **owner only** (ownership read from the
|
||||
Store branches — a received cap is not ownership, and a recipient must not be able to
|
||||
@@ -489,7 +489,7 @@ emulates the inbox on the shared wallet:
|
||||
|
||||
### An inbox BELONGS to a virtual user (2026-07-30)
|
||||
|
||||
`storeRegistry.walletInbox(id)` resolves — creating on first sight — the inbox
|
||||
`storeRegistry.userInbox(id)` resolves — creating on first sight — the inbox
|
||||
document of one virtual user, recorded in the doc-shim under `shim:docInbox` and
|
||||
read by its own query (so an account written before this existed still resolves).
|
||||
The asymmetry that matters:
|
||||
@@ -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 (`shared-wallet/accounts.ts`)
|
||||
## Identity store (`shared-wallet/virtualUsers.ts`)
|
||||
|
||||
The real NextGraph login (redirect to the broker, opening the single shared
|
||||
wallet) is perceived as a technical access barrier (see the login
|
||||
@@ -614,7 +614,7 @@ identity id the consumer application relays to it:
|
||||
open underneath. The real logout lives elsewhere (hidden in the consumer
|
||||
application's settings/debug), because it forces a new redirect.
|
||||
- Framework-agnostic: no React, no DOM beyond an optional injected
|
||||
`AccountStorage` (a `window.localStorage`, a test fake, or `null` for SSR). The
|
||||
`VirtualUserStorage` (a `window.localStorage`, a test fake, or `null` for SSR). The
|
||||
React `Context`/`Provider` stays in the consumer application. `normalizeId`
|
||||
(case-insensitive, optional leading `@` stripped, trimmed) is the pure
|
||||
normalizer, reusable as the shim key normalizer.
|
||||
|
||||
@@ -77,8 +77,8 @@ The 25 current modules, with the two splits' offspring shown where a module divi
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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`, `userInbox`, `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, `VirtualUserRecord`/`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. |
|
||||
@@ -86,7 +86,7 @@ The 25 current modules, with the two splits' offspring shown where a module divi
|
||||
| `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/virtualUsers.ts` | `shared-wallet/virtualUsers.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. |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user