docs: nommer par la fonction, et n'annoncer qu'un point d'entrée

Deux corrections indépendantes dans la doc vivante, les briefs et décisions
datés restant tels qu'écrits.

P1a et P1b ne disaient rien à personne. Six mois plus tard il aurait fallu lire
le code pour savoir de quoi on parle, et le coût de la recherche se repaie à
chaque lecture. Ils deviennent cap-surface — la forme des capacités, livrée le
2026-07-28 — et cap-enforcement — ce qui reste : le chiffrement par document et
les gardes d'écriture aujourd'hui décoratives. 28 occurrences.

Et api-contract.md se contredisait à quatre lignes d'intervalle : il annonçait
deux points d'entrée en tête, et en bas qu'il n'y en a qu'un depuis la fusion du
2026-08-07. Vérifié dans package.json avant d'écrire — exports mappe exactement
{".": "./src/index.ts"} et src/polyfill.ts n'existe pas.

Ce qui identifie un symbole polyfill-era ne change pas : le bloc marqué dans
src/index.ts et le test de vocabulaire, plus aucun chemin d'import.
This commit is contained in:
Sylvain Duchesne
2026-08-11 19:10:34 +02:00
parent 3be8da2178
commit 2726f4a26f
8 changed files with 28 additions and 28 deletions
@@ -15,7 +15,7 @@ Every published symbol has exactly one of two fates, and knowing which is the wh
## Why the deletion list is not an import path any more ## Why the deletion list is not an import path any more
There were two entry points until 2026-08-07, `.` and `./polyfill`, and the second one carried a signal worth naming: *what you import from that path is exactly what you will delete*. That was a mechanical guarantee — the compiler produced the list. There were two entries until 2026-08-07, `.` and `./polyfill`, and the second one carried a signal worth naming: *what you import from that path is exactly what you will delete*. That was a mechanical guarantee — the compiler produced the list.
Merging them lost it. Nothing at an import line now distinguishes `configure`, which goes away, from `docs`, which is replaced. Three things carry it instead, and it is worth knowing that only the last two are enforced: Merging them lost it. Nothing at an import line now distinguishes `configure`, which goes away, from `docs`, which is replaced. Three things carry it instead, and it is worth knowing that only the last two are enforced:
+9 -9
View File
@@ -2,9 +2,9 @@
> **Updated 2026-08-03, after the source layout was reorganised by migration fate** (`docs/source-layout-by-fate.md`). Paths, and three names, changed under this document: `readModel` became the directly-exported `readUnion`; `accounts` / `AccountRecord` / `AccountStorage` became `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage` (module `shared-wallet/virtual-users.ts`); `store-registry-api.ts` became `surface/placement.ts`. Two modules were created and are covered here: `emulated-verifier/branch-registers.ts` (the four durable registers, split out of the shim) and `shared-wallet/bootstrap.ts` (the injection store, split out of the `/polyfill` entry). The subject-by-subject rulings below are unaffected — what moved is where the code lives, not what it promises. > **Updated 2026-08-03, after the source layout was reorganised by migration fate** (`docs/source-layout-by-fate.md`). Paths, and three names, changed under this document: `readModel` became the directly-exported `readUnion`; `accounts` / `AccountRecord` / `AccountStorage` became `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage` (module `shared-wallet/virtual-users.ts`); `store-registry-api.ts` became `surface/placement.ts`. Two modules were created and are covered here: `emulated-verifier/branch-registers.ts` (the four durable registers, split out of the shim) and `shared-wallet/bootstrap.ts` (the injection store, split out of the `/polyfill` entry). The subject-by-subject rulings below are unaffected — what moved is where the code lives, not what it promises.
**Scope: the APP-FACING contract only.** Everything reachable from the two published entry points, and nothing else. The library's internal modules — the shim machinery, the read paths, the boundary guards — are held to the same standard (as close as possible to what NextGraph does or plans) but have their own document, `docs/internal-contract.md`: a consumer never reads that one, a maintainer does. This split was made on 2026-08-03, together with the export change described in § 15. **Scope: the APP-FACING contract only.** Everything reachable from the published entry point, and nothing else. The library's internal modules — the shim machinery, the read paths, the boundary guards — are held to the same standard (as close as possible to what NextGraph does or plans) but have their own document, `docs/internal-contract.md`: a consumer never reads that one, a maintainer does. This split was made on 2026-08-03, together with the export change described in § 15.
**Scope.** The real exported surface of `@ng-eventually/polyfill` (verified against the `export` statements in `packages/polyfill/src/index.ts` and `packages/polyfill/src/polyfill.ts``package.json` maps exactly two entry points, `.` and `./polyfill`), and, for each subject, the target signature the future NextGraph JS SDK is expected to expose. Written 2026-08-03, verified against the `nextgraph-rs` clone (HEAD `213338f6`, 2026-05-16) and the installed `@ng-org/web@0.1.2-alpha.13` type declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`). **Scope.** The real exported surface of `@ng-eventually/polyfill` (verified against the `export` statements in `packages/polyfill/src/index.ts``package.json` maps exactly one entry point, `.`), and, for each subject, the target signature the future NextGraph JS SDK is expected to expose. Written 2026-08-03, verified against the `nextgraph-rs` clone (HEAD `213338f6`, 2026-05-16) and the installed `@ng-org/web@0.1.2-alpha.13` type declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
**How to read the epistemic labels.** Every target-side claim carries one of: **How to read the epistemic labels.** Every target-side claim carries one of:
@@ -141,7 +141,7 @@ export type { NG } from "@ng-org/web";
The two overrides: The two overrides:
- `session_start(wallet_name: string, user_id: any): Promise<any>` (`index.d.ts:276`) — target signature unchanged; only the emulated credential injection disappears. - `session_start(wallet_name: string, user_id: any): Promise<any>` (`index.d.ts:276`) — target signature unchanged; only the emulated credential injection disappears.
- `sparql_update(session_id: any, sparql: string, nuri: any): Promise<any>` (`index.d.ts:297`) — target signature unchanged. The native enforcement the guard stands in for is the engine's permission model (`verify_perm`, `engine/repo/src/commit.rs:897`), which today is **called only from tests** (its enclosing `Commit::verify` has no runtime caller — see `docs/nextgraph-current-state.md` § *Author-signature verification*). That absence says nothing about the target: write permissions are the engine's declared model, so the guard's *behaviour* (a refused write) is target-shaped even though its *mechanism* (a JS-side check) is emulation. Known limit, documented in `README.md`: the guard fires only on this proxy, and the lib's own writers call the injected `ng` directly, so it is best-effort until P1b. - `sparql_update(session_id: any, sparql: string, nuri: any): Promise<any>` (`index.d.ts:297`) — target signature unchanged. The native enforcement the guard stands in for is the engine's permission model (`verify_perm`, `engine/repo/src/commit.rs:897`), which today is **called only from tests** (its enclosing `Commit::verify` has no runtime caller — see `docs/nextgraph-current-state.md` § *Author-signature verification*). That absence says nothing about the target: write permissions are the engine's declared model, so the guard's *behaviour* (a refused write) is target-shaped even though its *mechanism* (a JS-side check) is emulation. Known limit, documented in `README.md`: the guard fires only on this proxy, and the lib's own writers call the injected `ng` directly, so it is best-effort until cap-enforcement.
--- ---
@@ -453,10 +453,10 @@ isInPublicStore(nuri: Nuri): boolean;
open(nuri: Nuri, scope: Scope): ReadCap; open(nuri: Nuri, scope: Scope): ReadCap;
isEnforcing(): boolean; isEnforcing(): boolean;
onChange(listener: () => void): () => void; onChange(listener: () => void): () => void;
grantWrite(doc: Nuri, principal: PrincipalId): void; // decorative until P1b grantWrite(doc: Nuri, principal: PrincipalId): void; // decorative until cap-enforcement
governsWrite(doc: Nuri): boolean; // decorative until P1b governsWrite(doc: Nuri): boolean; // decorative until cap-enforcement
canWrite(doc: Nuri, principal: PrincipalId | null): boolean; // decorative until P1b canWrite(doc: Nuri, principal: PrincipalId | null): boolean; // decorative until cap-enforcement
hasWritePolicy(): boolean; // decorative until P1b hasWritePolicy(): boolean; // decorative until cap-enforcement
clear(): void; clear(): void;
``` ```
@@ -465,7 +465,7 @@ clear(): void;
**LEVEL-1 SHAPE.** There is no capability API at level 2 or 3 (no cap method in `index.d.ts`, none in the ORM), and there is **nothing to introspect upstream**: reading is key possession. The model, VERIFIED: **LEVEL-1 SHAPE.** There is no capability API at level 2 or 3 (no cap method in `index.d.ts`, none in the ORM), and there is **nothing to introspect upstream**: reading is key possession. The model, VERIFIED:
- A ReadCap is the serialized `ObjectRef``format!("r:{}", base64_url::encode(&ser))` (`BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`). **The `r:` segment and its encoding are upstream's**, reported by NextGraph's developer and verified in that function: id and key are serialized together into ONE opaque segment, unlike the `:k:` object/file/commit forms where they are two. The lib's `ReadCap` template-literal type uses that segment, with the stand-in constant `OK` in place of the key material. - A ReadCap is the serialized `ObjectRef``format!("r:{}", base64_url::encode(&ser))` (`BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`). **The `r:` segment and its encoding are upstream's**, reported by NextGraph's developer and verified in that function: id and key are serialized together into ONE opaque segment, unlike the `:k:` object/file/commit forms where they are two. The lib's `ReadCap` template-literal type uses that segment, with the stand-in constant `OK` in place of the key material.
- **"P1b swaps the value, not the shape" is a BET, and this section stated it as a fact until 2026-08-10.** What the source establishes is narrower, in three readings: (a) `readcap_nuri()` is produced as a **field value**, never concatenated onto a NURI — every call site fills `AppTabBranchInfo.readcap: Option<String>` (`engine/net/src/app_protocol.rs:1334`; `engine/verifier/src/verifier.rs:278,320`; `rocksdb_user_storage.rs:162,172`); (b) **no upstream parser accepts a repo NURI carrying `:r:`**`NuriV0::new_from` (`app_protocol.rs:643-737`) tries `did:ng:i`, `RE_REPO_O`, `RE_FILE_READ_CAP`, `RE_REPO` and `RE_BRANCH`, and none of the regexes at `engine/net/src/types.rs:48-80` has an `r:` form; (c) the slot the type *declares* for a repo read cap is a **field**`NuriV0.access: Vec<NgAccessV0>` with `NgAccessV0::ReadCap(ReadCap)` (`app_protocol.rs:54-62,192`) — itself constructed nowhere today (only `NgAccessV0::Key`, `:622`). Per the design principle none of that says the target will *not* parse a cap-bearing repo NURI; it says nothing parses one yet, so "the shape survives, only the value changes" is an assumption and not a passthrough. If the cap turns out to belong in a field, P1b moves it there instead of swapping a substring — a change the surface absorbs, because the value is opaque and nothing published parses it (§ 11). - **"cap-enforcement swaps the value, not the shape" is a BET, and this section stated it as a fact until 2026-08-10.** What the source establishes is narrower, in three readings: (a) `readcap_nuri()` is produced as a **field value**, never concatenated onto a NURI — every call site fills `AppTabBranchInfo.readcap: Option<String>` (`engine/net/src/app_protocol.rs:1334`; `engine/verifier/src/verifier.rs:278,320`; `rocksdb_user_storage.rs:162,172`); (b) **no upstream parser accepts a repo NURI carrying `:r:`**`NuriV0::new_from` (`app_protocol.rs:643-737`) tries `did:ng:i`, `RE_REPO_O`, `RE_FILE_READ_CAP`, `RE_REPO` and `RE_BRANCH`, and none of the regexes at `engine/net/src/types.rs:48-80` has an `r:` form; (c) the slot the type *declares* for a repo read cap is a **field**`NuriV0.access: Vec<NgAccessV0>` with `NgAccessV0::ReadCap(ReadCap)` (`app_protocol.rs:54-62,192`) — itself constructed nowhere today (only `NgAccessV0::Key`, `:622`). Per the design principle none of that says the target will *not* parse a cap-bearing repo NURI; it says nothing parses one yet, so "the shape survives, only the value changes" is an assumption and not a passthrough. If the cap turns out to belong in a field, cap-enforcement moves it there instead of swapping a substring — a change the surface absorbs, because the value is opaque and nothing published parses it (§ 11).
- Caps live in two durable registers by origin: created documents → `AddRepo { read_cap }` on the store's Store branch (`engine/repo/src/types.rs:1890-1899`, committed by `doc_create` via `send_add_repo_to_store`, `engine/verifier/src/request_processor.rs:698`); received caps → `AddLink { read_cap }` on the private store's User branch (`types.rs:1939-1948`). - Caps live in two durable registers by origin: created documents → `AddRepo { read_cap }` on the store's Store branch (`engine/repo/src/types.rs:1890-1899`, committed by `doc_create` via `send_add_repo_to_store`, `engine/verifier/src/request_processor.rs:698`); received caps → `AddLink { read_cap }` on the private store's User branch (`types.rs:1939-1948`).
- The one path that loads a repo from a cap is `pub(crate)``Verifier::load_repo_from_read_cap` (`engine/verifier/src/verifier.rs:2237`) — unexposed to JS. - The one path that loads a repo from a cap is `pub(crate)``Verifier::load_repo_from_read_cap` (`engine/verifier/src/verifier.rs:2237`) — unexposed to JS.
@@ -637,7 +637,7 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
- ~~**`docs.depositInto`**~~ **FIXED 2026-08-07.** It was published "only because `inbox.ts` lives in another module", with the note that a consumer must always go through `inbox.post`. That note is not a mechanism: an adversarial review drove through it — bare reference to a public document, rewrite its posted inbox address, divert its owner's deposits. It now lives in `emulated-verifier/register-write.ts`, which nothing exports. - ~~**`docs.depositInto`**~~ **FIXED 2026-08-07.** It was published "only because `inbox.ts` lives in another module", with the note that a consumer must always go through `inbox.post`. That note is not a mechanism: an adversarial review drove through it — bare reference to a public document, rewrite its posted inbox address, divert its owner's deposits. It now lives in `emulated-verifier/register-write.ts`, which nothing exports.
- **`getConfig` / `getStoreRegistryDeps`** — tagged `@internal` in source, exported for the lib's own wrappers. - **`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. - **`resetConfig` / `resetStoreRegistry` / `resetCaps` / `storeRegistry.resetRegistryCache`** — test/reset machinery. In particular `resetCaps` wipes EVERY holder's caps, which no product flow should ever do.
- **`getCaps()` and the `CapRegistry` class** — the registry is the emulation's engine room. The consumer surface is the acts that file caps: creating a document, `inbox.share` (grant), processing one's inbox, and reading a document a public store serves. `CapRegistry.grantWrite` / `governsWrite` / `canWrite` / `hasWritePolicy` are explicitly decorative until P1b — the guard they feed is bypassed by every internal writer. - **`getCaps()` and the `CapRegistry` class** — the registry is the emulation's engine room. The consumer surface is the acts that file caps: creating a document, `inbox.share` (grant), processing one's inbox, and reading a document a public store serves. `CapRegistry.grantWrite` / `governsWrite` / `canWrite` / `hasWritePolicy` are explicitly decorative until cap-enforcement — the guard they feed is bypassed by every internal writer.
- ~~**`storeRegistry.reservedAccount`, `resolveAccount`, `ensureAccount`, `VirtualUserRecord`, `RegistrySession`**~~ — **RESOLVED 2026-08-03**: no longer exported. Shim internals, now in `docs/internal-contract.md`. The consumer's legitimate touchpoint is `configureStoreRegistry` (bootstrap) plus the scope/entity resolvers. - ~~**`storeRegistry.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. - ~~**`storeRegistry.addLink` / `readLinks`**~~ — **RESOLVED 2026-08-03**: no longer exported. Consumers receive caps by processing their inbox (automated at connection); calling these directly baked in a register the verifier owns upstream.
- ~~**`virtualUsers.*` on the SDK entry**~~ — **RESOLVED 2026-08-03**: moved to `/polyfill`, where its disappearance at migration is visible at the import line. - ~~**`virtualUsers.*` on the SDK entry**~~ — **RESOLVED 2026-08-03**: moved to `/polyfill`, where its disappearance at migration is visible at the import line.
+2 -2
View File
@@ -2,7 +2,7 @@
> **Updated 2026-08-03, after the source layout was reorganised by migration fate** (`docs/source-layout-by-fate.md`). Paths, and three names, changed under this document: `readModel` became the directly-exported `readUnion`; `accounts` / `AccountRecord` / `AccountStorage` became `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage` (module `shared-wallet/virtual-users.ts`); `store-registry-api.ts` became `surface/placement.ts`. Two modules were created and are covered here: `emulated-verifier/branch-registers.ts` (the four durable registers, split out of the shim) and `shared-wallet/bootstrap.ts` (the injection store, split out of the `/polyfill` entry). The subject-by-subject rulings below are unaffected — what moved is where the code lives, not what it promises. > **Updated 2026-08-03, after the source layout was reorganised by migration fate** (`docs/source-layout-by-fate.md`). Paths, and three names, changed under this document: `readModel` became the directly-exported `readUnion`; `accounts` / `AccountRecord` / `AccountStorage` became `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage` (module `shared-wallet/virtual-users.ts`); `store-registry-api.ts` became `surface/placement.ts`. Two modules were created and are covered here: `emulated-verifier/branch-registers.ts` (the four durable registers, split out of the shim) and `shared-wallet/bootstrap.ts` (the injection store, split out of the `/polyfill` entry). The subject-by-subject rulings below are unaffected — what moved is where the code lives, not what it promises.
**Scope.** The complement of [`docs/api-contract.md`](./api-contract.md): every module export under `packages/polyfill/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`). **Scope.** The complement of [`docs/api-contract.md`](./api-contract.md): every module export under `packages/polyfill/src/` that is NOT reachable from the published entry point (`package.json` maps exactly `.``src/index.ts`). A consumer never reads this document; a maintainer does. The internal code is held to the same standard as the surface — as close as possible to what NextGraph does or plans — so every subject below carries the same target-side analysis. Written 2026-08-04, verified against the `nextgraph-rs` clone (HEAD `213338f6`) and the installed `@ng-org/web@0.1.2-alpha.13` declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
**How the boundary was computed — mechanically, from the `export` statements.** `index.ts` re-exports wholesale (`export *` / `export * as ns`) from `types.ts`, `inbox.ts`, `docs.ts`, `surface/read-model.ts`, and by name everything `surface/use-shape.ts`, `surface/watch-shape.ts`, `lifecycle.ts`, `sparql.ts` export, and `subscribeDoc`/`subscribeDocs`/`docChangeType` (+ types) from `subscribe.ts`; its `storeRegistry` namespace is the **`surface/placement.ts` slice only** (`createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `openDocumentInbox`). `model/nuri.ts` is now internal in full — the guards `isNuri`/`hasReadCap` stopped being published when the entries became permissive-in (`NuriLike` validated at the door). *(The second entry, `polyfill.ts`, was merged into `index.ts` on 2026-08-07, and the polyfill-era block was then cut to ONE published call, `configure`. `configureStoreRegistry`, `setCurrentUser` and `connectedUser` became internal the same day — folded, replaced by the gate, and awaited inside it respectively; the machinery accessors and test resets are internal too. All four are inventoried below.)* Everything else that carries `export` in a `src/` module is internal and inventoried here. Eight modules are internal in their entirety: `shared-wallet/access-log.ts`, `emulated-verifier/machinery.ts`, `surface/ng-proxy.ts`, `emulated-verifier/open-repo.ts`, `shared-wallet/outbox-log.ts`, `shared-wallet/physical.ts`, `emulated-verifier/reach.ts`, `emulated-verifier/read-filter.ts`. Four are internal in part: `nuri.ts`, `emulated-verifier/connect.ts`, `subscribe.ts`, `shared-wallet/account-registry.ts`. **How the boundary was computed — mechanically, from the `export` statements.** `index.ts` re-exports wholesale (`export *` / `export * as ns`) from `types.ts`, `inbox.ts`, `docs.ts`, `surface/read-model.ts`, and by name everything `surface/use-shape.ts`, `surface/watch-shape.ts`, `lifecycle.ts`, `sparql.ts` export, and `subscribeDoc`/`subscribeDocs`/`docChangeType` (+ types) from `subscribe.ts`; its `storeRegistry` namespace is the **`surface/placement.ts` slice only** (`createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `openDocumentInbox`). `model/nuri.ts` is now internal in full — the guards `isNuri`/`hasReadCap` stopped being published when the entries became permissive-in (`NuriLike` validated at the door). *(The second entry, `polyfill.ts`, was merged into `index.ts` on 2026-08-07, and the polyfill-era block was then cut to ONE published call, `configure`. `configureStoreRegistry`, `setCurrentUser` and `connectedUser` became internal the same day — folded, replaced by the gate, and awaited inside it respectively; the machinery accessors and test resets are internal too. All four are inventoried below.)* Everything else that carries `export` in a `src/` module is internal and inventoried here. Eight modules are internal in their entirety: `shared-wallet/access-log.ts`, `emulated-verifier/machinery.ts`, `surface/ng-proxy.ts`, `emulated-verifier/open-repo.ts`, `shared-wallet/outbox-log.ts`, `shared-wallet/physical.ts`, `emulated-verifier/reach.ts`, `emulated-verifier/read-filter.ts`. Four are internal in part: `nuri.ts`, `emulated-verifier/connect.ts`, `subscribe.ts`, `shared-wallet/account-registry.ts`.
@@ -38,7 +38,7 @@ export function mintCap(nuri: Nuri): ReadCap;
`targetOf` strips a `:r:` cap segment to the naming form; `parseNuri` is the parsed pair; `mintCap` builds the cap-bearing form with the stand-in value `OK` (`STAND_IN_CAP`). Kept off the surface deliberately: nothing published turns a bare reference into a cap. `targetOf` strips a `:r:` cap segment to the naming form; `parseNuri` is the parsed pair; `mintCap` builds the cap-bearing form with the stand-in value `OK` (`STAND_IN_CAP`). Kept off the surface deliberately: nothing published turns a bare reference into a cap.
- `targetOf` / `parseNuri`**LEVEL-1 SHAPE, model VERIFIED**: they transcribe upstream's one-type-with-optional-access NURI, on the **two** fields this library uses. Not a "1:1 mirror" of `NuriV0`, as this line claimed until 2026-08-10: that type has TEN fields — `identity, target, entire_store, objects, signature, branch, overlay, access, topic, locator` (`engine/net/src/app_protocol.rs:181-194`) — of which `parseNuri` carries `target` and the cap half of `access`. The other eight have no counterpart here (the missing `locator` is a stated gap, `docs/readcap-and-nuri-model.md` § 4sexies). 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. - `targetOf` / `parseNuri`**LEVEL-1 SHAPE, model VERIFIED**: they transcribe upstream's one-type-with-optional-access NURI, on the **two** fields this library uses. Not a "1:1 mirror" of `NuriV0`, as this line claimed until 2026-08-10: that type has TEN fields — `identity, target, entire_store, objects, signature, branch, overlay, access, topic, locator` (`engine/net/src/app_protocol.rs:181-194`) — of which `parseNuri` carries `target` and the cap half of `access`. The other eight have no counterpart here (the missing `locator` is a stated gap, `docs/readcap-and-nuri-model.md` § 4sexies). 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 (`STAND_IN_CAP`, `emulated-verifier/caps.ts`). 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). - `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 cap-enforcement has not yet supplied real key material; the constant `OK` pretends nothing (`STAND_IN_CAP`, `emulated-verifier/caps.ts`). It has exactly two call sites (`shared-wallet/account-registry.ts` `createEntityDoc`; `emulated-verifier/caps.ts` internals) — the minting points of the emulation. With cap-enforcement the constant becomes a real key; at migration the function is deleted (the engine mints).
## 3. The reach boundary — `emulated-verifier/reach.ts` ## 3. The reach boundary — `emulated-verifier/reach.ts`
+3 -3
View File
@@ -14,7 +14,7 @@ has no clear target image, that is a drift signal (see
## Checklist ## Checklist
### 1. Emulated ReadCaps → real capabilities ### 1. Emulated ReadCaps → real capabilities
The shape is already the target's (P1a): a `ReadCap` is the document's key, a The shape is already the target's (the cap-surface work): a `ReadCap` is the document's key, a
each identity holds a set of caps, and there is no read-ACL anywhere. So 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: this step swaps the *emulated* key for the real one, not the model:
@@ -41,7 +41,7 @@ this step swaps the *emulated* key for the real one, not the model:
whose cap the wallet holds. Remove them. whose cap the wallet holds. Remove them.
- The write guard (`surface/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 is decorative today (every internal writer bypasses the proxy); it belongs to the
P1b batch, not here. cap-enforcement batch, not here.
The access unit is already the document (`@graph`), matching the native per-repo cap The access unit is already the document (`@graph`), matching the native per-repo cap
model, so this is a key-material step, not a reshape. model, so this is a key-material step, not a reshape.
@@ -103,7 +103,7 @@ the POLYFILL-ERA block of `@ng-eventually/polyfill` — is deleted. The lib itse
## The one break already taken: `declareConnections` ## The one break already taken: `declareConnections`
P1a broke the consumer once, deliberately and early, so that migration would not. The cap-surface work broke the consumer once, deliberately and early, so that migration would not.
The old surface was an ACL held in memory, which forced the consumer to re-declare The old surface was an ACL held in memory, which forced the consumer to re-declare
every grant on every session (`declareConnections`). That call **disappears**: with every grant on every session (`declareConnections`). That call **disappears**: with
delivered caps the grant moves to the moment a connection is *accepted* — one delivered caps the grant moves to the moment a connection is *accepted* — one
+5 -5
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. **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 `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". 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 the cap-surface work — 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 > ## How to use this document — verify, never infer
> >
@@ -378,9 +378,9 @@ One thing a consumer must not conclude from the emulation: that a public documen
## 5. What the polyfill emulates (caps.ts) — and where it still diverges ## 5. What the polyfill emulates (caps.ts) — and where it still diverges
**Realigned 2026-07-28 (batch P1a).** `packages/polyfill/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. **Realigned 2026-07-28 (the cap-surface batch).** `packages/polyfill/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) | | | Real NextGraph | caps.ts emulation (post-cap-surface) |
|---|---|---| |---|---|---|
| Nature | possession of a **key** | possession of a **key** — recorded per identity, indexed by the cap-less NURI | | Nature | possession of a **key** | possession of a **key** — recorded per identity, indexed by the cap-less NURI |
| Grant | seal the key (crypto_box) to the inbox | `inbox.share(doc, toUser)` → an inbox deposit, absorbed inline on read | | Grant | seal the key (crypto_box) to the inbox | `inbox.share(doc, toUser)` → an inbox deposit, absorbed inline on read |
@@ -390,7 +390,7 @@ One thing a consumer must not conclude from the emulation: that a public documen
| Ref. without rights | **cap-less NURI** (no `r:` segment) | same — `Nuri` names, `ReadCap` names and reads | | Ref. without rights | **cap-less NURI** (no `r:` segment) | same — `Nuri` names, `ReadCap` names and reads |
| Public store | the broker serves the outer overlay; the ReadCap is **downloaded** from it | `public-store.ts` — the cap is exposed on the document and fetched through the machinery's door, then held like any other. Filed apart (`learnFromPublicStore`) so it grants reading and **not** writing | | Public store | the broker serves the outer overlay; the ReadCap is **downloaded** from it | `public-store.ts` — the cap is exposed on the document and fetched through the machinery's door, then held like any other. Filed apart (`learnFromPublicStore`) so it grants reading and **not** writing |
**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". **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 **cap-enforcement** 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 (`inbox.share` once, per document), which is a consumer **re-architecture**, not an API swap. **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 (`inbox.share` once, per document), which is a consumer **re-architecture**, not an API swap.
@@ -398,7 +398,7 @@ One thing a consumer must not conclude from the emulation: that a public documen
- "**protected scope = my network can read**" is **not** an ACL checked by the broker: it is "I have **sealed my read key** to each of my connections". The "scope = ACL" mental model is wrong at the NextGraph level. - "**protected scope = my network can read**" is **not** an ACL checked by the broker: it is "I have **sealed my read key** to each of my connections". The "scope = ACL" mental model is wrong at the NextGraph level.
- **Anonymous references are possible**: putting a **cap-less NURI** in a third party's collection lets that third party **name/count** without **reading the identity**; the cap-bearing one is sealed separately to the authorized parties only. (Basis for a presence model of the form "self-owned participation + curated cap-less Set + cap sealed to the connections".) - **Anonymous references are possible**: putting a **cap-less NURI** in a third party's collection lets that third party **name/count** without **reading the identity**; the cap-bearing one is sealed separately to the authorized parties only. (Basis for a presence model of the form "self-owned participation + curated cap-less Set + cap sealed to the connections".)
- **Alignment DONE for the surface (P1a, 2026-07-28)**: the emulated ACL is gone, replaced by per-identity cap possession + per-document delivery to an inbox; `declareConnections`-as-a-re-declared-ACL has disappeared. What remains for the real cap operations is swapping the stand-in key value (`OK`) for the real one and closing the bypasses (P1b) — a key-material step, not a reshape. See `migration-guide.md` §1. - **Alignment DONE for the surface (cap-surface, 2026-07-28)**: the emulated ACL is gone, replaced by per-identity cap possession + per-document delivery to an inbox; `declareConnections`-as-a-re-declared-ACL has disappeared. What remains for the real cap operations is swapping the stand-in key value (`OK`) for the real one and closing the bypasses (cap-enforcement) — a key-material step, not a reshape. See `migration-guide.md` §1.
## Caveats / gaps ## Caveats / gaps
+5 -5
View File
@@ -304,7 +304,7 @@ wallet, everything readable) the lib reproduces that with a read-filtered view:
type would be a concept NextGraph does not have. The stand-in key value is the type would be a concept NextGraph does not have. The stand-in key value is the
constant `OK` (see the module header): the only question the emulation answers is 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 *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. more. Cap-enforcement, not cap-surface, is the batch that turns the shape into a protection.
- **`emulated-verifier/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 `Proxy`: iteration / `size` / `forEach` keep only items whose `@graph` the
current holder holds; everything else (`add`, `delete`, `has`, `getById`…) forwards to current holder holds; everything else (`add`, `delete`, `has`, `getById`…) forwards to
@@ -340,7 +340,7 @@ Three ways a cap arrives, and there are no others:
- **Creation.** `createEntityDoc(id, scope)` writes the cap on the store's emulated - **Creation.** `createEntityDoc(id, scope)` writes the cap on the store's emulated
Store branch (`shim:readCap`) and the creator holds it. The consumer declares Store branch (`shim:readCap`) and the creator holds it. The consumer declares
nothing, and the cap is minted exactly ONCE — the stored value is the held value, nothing, and the cap is minted exactly ONCE — the stored value is the held value,
which is what keeps this correct when P1b makes the key real. which is what keeps this correct when cap-enforcement makes the key real.
- **Re-listing.** `listMyEntityDocs(id, scope)` READS those records back. It does not - **Re-listing.** `listMyEntityDocs(id, scope)` READS those records back. It does not
recompute anything: that is the whole reason for storing them, and it is what lets recompute anything: that is the whole reason for storing them, and it is what lets
a **fresh session** read its own documents again with nothing re-declared — the a **fresh session** read its own documents again with nothing re-declared — the
@@ -418,10 +418,10 @@ registry. And dynamically: the cap lands in Bob's inbox, his client processes it
the read that was empty yields the content — the held-caps signal re-running it. the read that was empty yields the content — the held-caps signal re-running it.
That is what real NextGraph does, and it holds **without a line of encryption** That is what real NextGraph does, and it holds **without a line of encryption**
which is what makes the P1a (shape) / P1b (enforcement) split honest rather than which is what makes the cap-surface (shape) / cap-enforcement (enforcement) split honest rather than
cosmetic. Proven in `test/cross-user-access.test.ts`. cosmetic. Proven in `test/cross-user-access.test.ts`.
> **After P1a the shape is right and the isolation is still fake.** The stand-in key > **After the cap-surface batch the shape is right and the isolation is still fake.** The stand-in key
> is a constant, and several read paths (`docs.sparqlQuery`/`sparqlUpdate`, the whole > is a constant, and several read paths (`docs.sparqlQuery`/`sparqlUpdate`, the whole
> inbox, `store-registry`, `subscribe`, `open-repo`) consult no cap at all — worse, > inbox, `store-registry`, `subscribe`, `open-repo`) consult no cap at all — worse,
> any wallet can reach any document. That is the subject of > any wallet can reach any document. That is the subject of
@@ -501,7 +501,7 @@ The asymmetry that matters:
the whole reachability graph. A deposit grants the depositor nothing in return — the whole reachability graph. A deposit grants the depositor nothing in return —
upstream it is an anonymous sealed box. upstream it is an anonymous sealed box.
- **Reading an inbox is confined to its owner** (`isOwnInbox`, enforced in `read` / - **Reading an inbox is confined to its owner** (`isOwnInbox`, enforced in `read` /
`readSynced`, hence in `watch`). Since P1a routes ReadCaps through deposits, an `readSynced`, hence in `watch`). Since the cap-surface batch routes ReadCaps through deposits, an
unguarded read let anyone who knew an inbox NURI collect the caps addressed to its unguarded read let anyone who knew an inbox NURI collect the caps addressed to its
owner — defeating directed sharing. Anonymous owns no inbox and reads none. owner — defeating directed sharing. Anonymous owns no inbox and reads none.
+2 -2
View File
@@ -69,7 +69,7 @@ The 25 current modules, with the two splits' offspring shown where a module divi
| `index.ts` | `src/index.ts` (unchanged path) | The SDK-entry manifest. New rule made checkable: imports only from `surface/` and `model/`. | | `index.ts` | `src/index.ts` (unchanged path) | The SDK-entry manifest. New rule made checkable: imports only from `surface/` and `model/`. |
| `polyfill.ts` | `src/polyfill.ts` (path unchanged) + **new** `shared-wallet/bootstrap.ts` | Split: the entry keeps the re-exports; the config store (`configure`, `getConfig`, `registryDeps`, the current-user relay, the `CapRegistry` singleton wiring) moves to `shared-wallet/bootstrap.ts` — it is the injection machinery with NO COUNTERPART by design (`docs/api-contract.md` § 1). Side effect: removes the current entry↔internal import cycles (`polyfill.ts:16``connect.ts:37`; `polyfill.ts:228``inbox.ts:32`). | | `polyfill.ts` | `src/polyfill.ts` (path unchanged) + **new** `shared-wallet/bootstrap.ts` | Split: the entry keeps the re-exports; the config store (`configure`, `getConfig`, `registryDeps`, the current-user relay, the `CapRegistry` singleton wiring) moves to `shared-wallet/bootstrap.ts` — it is the injection machinery with NO COUNTERPART by design (`docs/api-contract.md` § 1). Side effect: removes the current entry↔internal import cycles (`polyfill.ts:16``connect.ts:37`; `polyfill.ts:228``inbox.ts:32`). |
| `types.ts` | `model/types.ts`, minus `NgLike` / `UseShapeLike``shared-wallet/bootstrap.ts` | `Nuri`/`ReadCap`/`Scope` are level-1-verified vocabulary; `PrincipalId` stays with a note (target: the wallet user; polyfill: a relayed id). `NgLike`/`UseShapeLike` describe the *injection*, not the target — they belong to the bootstrap. See § 5 for the published-type wrinkle this creates. | | `types.ts` | `model/types.ts`, minus `NgLike` / `UseShapeLike``shared-wallet/bootstrap.ts` | `Nuri`/`ReadCap`/`Scope` are level-1-verified vocabulary; `PrincipalId` stays with a note (target: the wallet user; polyfill: a relayed id). `NgLike`/`UseShapeLike` describe the *injection*, not the target — they belong to the bootstrap. See § 5 for the published-type wrinkle this creates. |
| `nuri.ts` | `model/nuri.ts`, minus `mintCap``emulated-verifier/` | The guards and `targetOf`/`parseNuri` are the model transcription. `mintCap` is the emulation's minting point — upstream only the engine mints, at repo creation (level 1, `BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`) — and its presence in the model module contradicts the module's own header ("nothing on the surface turns a bare reference into a cap"). P1b swaps its constant; migration deletes it. | | `nuri.ts` | `model/nuri.ts`, minus `mintCap``emulated-verifier/` | The guards and `targetOf`/`parseNuri` are the model transcription. `mintCap` is the emulation's minting point — upstream only the engine mints, at repo creation (level 1, `BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`) — and its presence in the model module contradicts the module's own header ("nothing on the surface turns a bare reference into a cap"). Cap-enforcement swaps its constant; migration deletes it. |
| `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. | | `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. | | `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). | | `lifecycle.ts` | `surface/lifecycle.ts` | Pure forwarding to the injected level-2/3 calls (`docs/api-contract.md` § 2). |
@@ -130,7 +130,7 @@ Full mixed list: `docs.ts`, `surface/read-model.ts`, `inbox.ts`, `subscribe.ts`,
## 5. Cost and risk ## 5. Cost and risk
**What does not change: the published surface.** Both entries keep their `src/` paths *(they were merged into one on 2026-08-07)*; `package.json`'s `exports` map is untouched; the `inbox.*` and `storeRegistry.*` namespaces are re-assembled at the entries with identical contents. A consumer application importing the two entries sees nothing — with one deliberate exception below. **What does not change: the published surface.** Both entries keep their `src/` paths *(they were merged into one on 2026-08-07, and `package.json`'s `exports` map now maps only `.`)*; the `inbox.*` and `storeRegistry.*` namespaces are re-assembled at the entry with identical contents. A consumer application importing the entry sees nothing — with one deliberate exception below.
**Import churn — the inventory:** **Import churn — the inventory:**
+1 -1
View File
@@ -20,7 +20,7 @@ Without a minimum of crypto simulation, damaging shortcuts get taken (reading th
Concretely, **in the target**: a document's data is **stored encrypted** (per-doc symmetric encryption, however lightweight); the **ReadCap = the key**; without it, **decrypting/reading is impossible**. No ACL, no plaintext accessible "on the side". Obtaining read access = **holding the key**, exactly as in the target model. Concretely, **in the target**: a document's data is **stored encrypted** (per-doc symmetric encryption, however lightweight); the **ReadCap = the key**; without it, **decrypting/reading is impossible**. No ACL, no plaintext accessible "on the side". Obtaining read access = **holding the key**, exactly as in the target model.
> **Not yet true here, and saying so matters.** The shape is in place — possession decides, every access is confined to the connected virtual user, caps are stored and read back — but the cap value is the constant `OK` and nothing is encrypted. Per-document encryption is **P1b**, and it is one function (`mintCap`, in `emulated-verifier/caps.ts` — this said `nuri.ts` until 2026-08-10). Until it lands, nothing this library does may be described as anonymous or private. > **Not yet true here, and saying so matters.** The shape is in place — possession decides, every access is confined to the connected virtual user, caps are stored and read back — but the cap value is the constant `OK` and nothing is encrypted. Per-document encryption is the **cap-enforcement** work, and it is one function (`mintCap`, in `emulated-verifier/caps.ts` — this said `nuri.ts` until 2026-08-10). Until it lands, nothing this library does may be described as anonymous or private.
## Shape consequences (to respect everywhere) ## Shape consequences (to respect everywhere)