refactor: renommer client → sdk, et fusionner les deux portes en une
Deux mouvements de surface, aucun changement de comportement. **`packages/client` → `packages/sdk`, `@ng-eventually/client` → `@ng-eventually/sdk`.** « client » ne disait rien : ce paquet EST le SDK que l'application appelle, et c'est tout ce qu'elle appelle. L'ancien nom reste comme mot-clé de recherche dans `docs/source-layout-by-fate.md` et le tableau des paquets du README. **Une seule entrée.** L'entrée `./polyfill` disparaît ; ses symboles applicatifs — `configure`, `configureStoreRegistry`, `setCurrentUser`, `connectedUser` et leurs types — vivent dans un bloc `POLYFILL-ERA` de `src/index.ts`. Ce que la seconde porte portait mérite d'être nommé avant d'être retiré : *ce qu'on importe de ce chemin est exactement ce qu'on supprimera à la migration*. Une seule porte perd ce signal — rien à la ligne d'import ne distingue `configure`, qui part, de `docs`, que le vrai SDK remplace sur place. Trois choses le portent désormais : le bloc lui-même, l'inventaire d'exports de `docs/api-contract.md` (épinglé par `test/vocabulary.test.ts`, donc il ne peut pas rancir en silence), et le contrôle de vocabulaire sur les noms publiés. **Six symboles quittent la surface au passage**, et la fusion est ce qui a rendu le choix visible plutôt qu'hérité : - `getConfig` / `getStoreRegistryDeps` — câblage interne, atteint par `shared-wallet/bootstrap` ; - `resetConfig` / `resetStoreRegistry` / `resetCaps` — remises à zéro de test, atteintes par leur chemin interne, ce qui est leur raison d'être ; - le `share` direct — `inbox.share` a toujours été la même fonction, et la publier deux fois brouillait la frontière qu'elle servait à marquer. Corrections d'affirmations fausses trouvées en chemin : le contrat annonçait `isNuri` / `hasReadCap` sur la porte SDK alors qu'ils ne sont plus exportés depuis le passage au permissif en entrée (`NuriLike` validé à la porte) ; le README du paquet documentait `capFor`, `shareCap`, `getCaps` et `publishRepoLink`, dont aucun n'existe ; et le README de l'app d'exemple affirmait que la suite e2e la pilote, ce qui reste à faire. 179 tests unitaires, typecheck bibliothèque / exemple / harnais, e2e 42/42 contre le broker en ligne — mesuré une fois après le renommage, une fois après la fusion.
This commit is contained in:
+54
-46
@@ -1,10 +1,10 @@
|
||||
# API contract — what `@ng-eventually/client` exposes today, and what the future SDK should expose per subject
|
||||
# API contract — what `@ng-eventually/sdk` exposes today, and what the future SDK should expose per subject
|
||||
|
||||
> **Updated 2026-08-03, after the source layout was reorganised by migration fate** (`docs/source-layout-by-fate.md`). Paths, and three names, changed under this document: `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 real exported surface of `@ng-eventually/client` (verified against the `export` statements in `packages/client/src/index.ts` and `packages/client/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/sdk` (verified against the `export` statements in `packages/sdk/src/index.ts` and `packages/sdk/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`).
|
||||
|
||||
**How to read the epistemic labels.** Every target-side claim carries one of:
|
||||
|
||||
@@ -19,10 +19,10 @@ Per the design principle (`README.md` § *Design principle*): an absent implemen
|
||||
|
||||
## 1. Bootstrap and configuration
|
||||
|
||||
### Today — `@ng-eventually/client/polyfill` (everything here is removed at migration)
|
||||
### Today — `@ng-eventually/sdk` (the POLYFILL-ERA block of `src/index.ts`; everything here is removed at migration)
|
||||
|
||||
```ts
|
||||
// polyfill.ts:44
|
||||
// shared-wallet/bootstrap.ts
|
||||
export interface EventuallyConfig {
|
||||
ng: NgLike;
|
||||
useShape: UseShapeLike;
|
||||
@@ -32,23 +32,23 @@ export interface EventuallyConfig {
|
||||
init?: (...args: any[]) => any;
|
||||
initNg?: (...args: any[]) => any;
|
||||
}
|
||||
// polyfill.ts:99
|
||||
// shared-wallet/bootstrap.ts
|
||||
export function configure(c: EventuallyConfig): void;
|
||||
// polyfill.ts:113 — tests only
|
||||
// shared-wallet/bootstrap.ts — tests only
|
||||
export function resetConfig(): void;
|
||||
|
||||
// polyfill.ts:24
|
||||
// shared-wallet/bootstrap.ts
|
||||
export interface StoreRegistryDeps {
|
||||
getSession: () => Promise<RegistrySession>;
|
||||
normalizeId?: (id: string) => string;
|
||||
pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number };
|
||||
}
|
||||
// polyfill.ts:124
|
||||
// shared-wallet/bootstrap.ts
|
||||
export function configureStoreRegistry(deps: StoreRegistryDeps): void;
|
||||
// polyfill.ts:161 — tests only
|
||||
// shared-wallet/bootstrap.ts — tests only
|
||||
export function resetStoreRegistry(): void;
|
||||
|
||||
// polyfill.ts:106 / :153 — both tagged @internal, exported so the SDK-shaped wrappers can reach the injected SDK
|
||||
// shared-wallet/bootstrap.ts — both tagged @internal, exported so the SDK-shaped wrappers can reach the injected SDK
|
||||
export function getConfig(): EventuallyConfig;
|
||||
export function getStoreRegistryDeps(): ResolvedRegistryDeps;
|
||||
```
|
||||
@@ -61,7 +61,7 @@ export function getStoreRegistryDeps(): ResolvedRegistryDeps;
|
||||
|
||||
## 2. Lifecycle
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// lifecycle.ts:11 — forwards to the real @ng-org/web init injected at configure()
|
||||
@@ -98,7 +98,7 @@ Divergence: none in behaviour (pure forwarding), but the wrapper erases the para
|
||||
|
||||
## 3. The `ng` object
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// index.ts:55
|
||||
@@ -122,7 +122,7 @@ The two overrides:
|
||||
|
||||
## 4. Reactive typed reads — `useShape`
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// use-shape.ts:12
|
||||
@@ -160,7 +160,7 @@ Divergence to note: the wrapper types everything `unknown`, losing the generic `
|
||||
|
||||
## 5. Reactive typed reads with load state — `watchShape`
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// watch-shape.ts:73
|
||||
@@ -201,7 +201,7 @@ So the constraint on the bet: the target can already answer "synced?" (`readyPro
|
||||
|
||||
## 6. One-shot listing — the read-model
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// read-model.ts:59
|
||||
@@ -237,7 +237,7 @@ The anchored-read mechanics are level-1 VERIFIED: an anchor restricts the query
|
||||
|
||||
## 7. Raw document / SPARQL primitives — `docs.*`
|
||||
|
||||
### Today — `@ng-eventually/client` (namespace `docs`)
|
||||
### Today — `@ng-eventually/sdk` (namespace `docs`)
|
||||
|
||||
```ts
|
||||
// docs.ts:46
|
||||
@@ -298,7 +298,7 @@ So the target's direction for scope placement is **already visible in the source
|
||||
|
||||
## 8. Per-document subscription — `subscribeDoc`
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// subscribe.ts:47,60,79
|
||||
@@ -336,7 +336,7 @@ declare function doc_subscribe(repo_o: string, session_id: any, callback: Functi
|
||||
|
||||
## 9. Inbox — deposits, and cap delivery
|
||||
|
||||
### Today — `@ng-eventually/client` (namespace `inbox`; `share` also re-exported from `/polyfill`)
|
||||
### Today — `@ng-eventually/sdk` (namespace `inbox`)
|
||||
|
||||
```ts
|
||||
// inbox.ts:48,58
|
||||
@@ -397,21 +397,21 @@ Consequences per function:
|
||||
### Today
|
||||
|
||||
```ts
|
||||
// @ng-eventually/client — nuri.ts:50,60 (type guards; the only doors from string to typed)
|
||||
export function isNuri(s: string): s is Nuri;
|
||||
export function hasReadCap(s: string): s is ReadCap;
|
||||
|
||||
// @ng-eventually/client — types.ts:11,34
|
||||
// @ng-eventually/sdk — model/types.ts. The types are the whole published cap surface.
|
||||
export type Nuri = `did:ng:${string}`;
|
||||
export type ReadCap = `did:ng:${string}:r:${string}`;
|
||||
export type NuriLike = Nuri | string;
|
||||
|
||||
// @ng-eventually/client/polyfill
|
||||
// tests / fresh wallet only — the registry itself is NOT published, and neither is any
|
||||
// "do I hold this?" predicate (`hasCap`, removed 2026-08-06: it read like "may I read
|
||||
// this?", and a document in a public store answers `false` until something asks for it).
|
||||
export function resetCaps(): void;
|
||||
// NOT published, each deliberately:
|
||||
// isNuri / hasReadCap — the type guards (`model/nuri.ts`). Unpublished since the
|
||||
// permissive-in change: every entry takes `NuriLike` and validates at the door, so
|
||||
// a consumer holding a plain string narrows nothing. Publishing a guard would
|
||||
// invite the cast it exists to prevent.
|
||||
// hasCap(doc) — removed 2026-08-06. It read like "may I read this?", and a
|
||||
// document in a public store answers `false` until something asks for its cap.
|
||||
// getCaps / CapRegistry / resetCaps — the emulation's engine room and its test reset.
|
||||
|
||||
// @ng-eventually/client/polyfill — caps.ts:59 (class CapRegistry)
|
||||
// INTERNAL — `emulated-verifier/caps.ts` (class CapRegistry). Never published; listed for the maintainer.
|
||||
constructor(holder?: () => PrincipalId | null);
|
||||
mint(nuri: Nuri): ReadCap;
|
||||
learn(cap: ReadCap): void;
|
||||
@@ -446,7 +446,7 @@ The `CapRegistry` class itself is machinery (the in-memory record of what the co
|
||||
|
||||
## 11. NURI and SPARQL string utilities
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// sparql.ts:32,66,95
|
||||
@@ -455,7 +455,7 @@ export function escapeIri(value: string): string;
|
||||
export function assertNuri<T extends string>(nuri: T): T;
|
||||
```
|
||||
|
||||
(`isNuri` / `hasReadCap` are in § 10; `targetOf`, `parseNuri`, `mintCap` exist in `nuri.ts` but are **not** exported from either entry point — deliberately: nothing on the surface turns a bare reference into a cap.)
|
||||
(`isNuri`, `hasReadCap`, `targetOf`, `parseNuri`, `toNuri` and `mintCap` all exist in `model/nuri.ts` and **none** is exported — deliberately: nothing on the surface turns a bare reference into a cap, and validation happens at the door rather than in the caller's hands.)
|
||||
|
||||
### Target
|
||||
|
||||
@@ -465,9 +465,9 @@ export function assertNuri<T extends string>(nuri: T): T;
|
||||
|
||||
## 12. Scope resolution, per-entity documents, and the store registry
|
||||
|
||||
### Today — `@ng-eventually/client` (namespace `storeRegistry`) — plus `Scope` from `types.ts`
|
||||
### Today — `@ng-eventually/sdk` (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`, `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.
|
||||
> **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/sdk`** 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
|
||||
@@ -530,7 +530,7 @@ export function resetRegistryCache(): void;
|
||||
### Today
|
||||
|
||||
```ts
|
||||
// @ng-eventually/client — virtualUsers.ts (namespace accounts)
|
||||
// @ng-eventually/sdk — virtualUsers.ts (namespace accounts)
|
||||
export const ACCOUNT_STORAGE_KEY = "ng-eventually.account.id"; // :18
|
||||
export interface VirtualUserStorage { // :26
|
||||
getItem(key: string): string | null;
|
||||
@@ -545,9 +545,9 @@ export class IdentityStore { // :37
|
||||
}
|
||||
export function browserIdentityStore(key?: string): IdentityStore; // :89
|
||||
|
||||
// @ng-eventually/client/polyfill
|
||||
export function setCurrentUser(id: PrincipalId | null): void; // polyfill.ts:171
|
||||
export function getCurrentUser(): PrincipalId | null; // polyfill.ts:187
|
||||
// @ng-eventually/sdk — the POLYFILL-ERA block
|
||||
export function setCurrentUser(id: PrincipalId | null): void; // shared-wallet/bootstrap.ts
|
||||
export function getCurrentUser(): PrincipalId | null; // shared-wallet/bootstrap.ts
|
||||
export async function connectedUser(): Promise<void>; // connect.ts:52
|
||||
```
|
||||
|
||||
@@ -571,7 +571,7 @@ declare function user_disconnect(user_id: string): Promise<void>;
|
||||
|
||||
## 14. Type re-exports
|
||||
|
||||
`@ng-eventually/client` re-exports, type-only (erased at build, `index.ts:48-50`):
|
||||
`@ng-eventually/sdk` re-exports, type-only (erased at build, `index.ts:48-50`):
|
||||
|
||||
```ts
|
||||
export type { ShapeType, BaseType, Schema } from "@ng-org/shex-orm";
|
||||
@@ -600,7 +600,8 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
||||
### Places the current surface teaches something to unlearn
|
||||
|
||||
- ~~**The SDK entry is not as pure as its header claims.**~~ **FIXED 2026-08-03.** The header claimed the entry "exposes ONLY what `@ng-org/web` / `@ng-org/orm` expose" while also shipping `virtualUsers` and the whole `store-registry` module. Both are gone from it, and the header now states what the entry actually promises: *every symbol here has a target-SDK counterpart, verified or assumed, listed in this document*. It still exports `docs`, `readUnion`, `watchShape`, `subscribeDoc(s)`, the SPARQL helpers and the NURI guards — justified inventions, documented per subject above — so the promise is no longer "@ng-org surface only", which was never true, but "nothing here is machinery".
|
||||
- **`share` is importable from both entries** (`inbox.share` on the SDK entry via `export * as inbox`, and a named re-export on `/polyfill`). The polyfill re-export exists "so the cap vocabulary stays on the polyfill side" — but the namespace export undoes that. Harmless functionally; blurs the same boundary.
|
||||
- ~~**`share` is importable from both entries.**~~ **FIXED 2026-08-07** with the entry merge: there is one entry and one `share`, under `inbox`.
|
||||
- **One entry means the import line no longer says what disappears.** Until 2026-08-07 a second import path (`/polyfill`) WAS the deletion list. It is now the `POLYFILL-ERA` block in `src/index.ts`, this appendix's note above, and the per-subject rulings in this document. That is a documentation-carried signal where it used to be a mechanical one — the appendix is pinned by a test, the grouping is not.
|
||||
- **`inbox.read`/`materialize` as a mailbox** — enumerating raw deposits is emulation detail (§ 9); the durable contract is deposit-and-it-gets-applied. An app building UI on the deposit list should expect that surface to change shape entirely.
|
||||
- **`watchShape`'s "planned `useShape` upgrade"** — stated in the module header with no provenance in this repo or the clone (§ 5). The load-state *distinction* is safe; the claim that NextGraph plans this exact hook shape is an assumption and must not be cited as an announced API.
|
||||
- **`UnionSubject` property bags** — polyfill read-model shape, not a target type; map them into app types at the boundary (which `watchShape`'s design already assumes).
|
||||
@@ -610,19 +611,26 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
||||
|
||||
## Appendix — full export inventory (for diffing)
|
||||
|
||||
*Generated from the `export` statements, and pinned by `packages/client/test/vocabulary.test.ts` — if this list and the code disagree, that test fails. It went stale once, still listing `storeRegistry`'s shim internals after the entry had been narrowed, which is what a hand-maintained inventory does.*
|
||||
*Generated from the `export` statements, and pinned by `packages/sdk/test/vocabulary.test.ts` — if this list and the code disagree, that test fails. It went stale once, still listing `storeRegistry`'s shim internals after the entry had been narrowed, which is what a hand-maintained inventory does.*
|
||||
|
||||
### `@ng-eventually/client` — `src/index.ts`
|
||||
### `@ng-eventually/sdk` — `src/index.ts` (the only entry since 2026-08-07)
|
||||
|
||||
```text
|
||||
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, InboxScope, NG, NgLike, Nuri, NuriLike, PrincipalId, ReadCap, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, docChangeType, ensureIdentity, init, initNg, ng, readUnion, subscribeDoc, subscribeDocs, useShape, watchShape
|
||||
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, InboxScope, NG, NgLike, Nuri, NuriLike, PrincipalId, ReadCap, RegistrySession, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, StoreRegistryDeps, UnionSubject, Unsubscribe, UseShapeLike, configure, configureStoreRegistry, connectedUser, docChangeType, ensureIdentity, init, initNg, ng, readUnion, setCurrentUser, subscribeDoc, subscribeDocs, useShape, watchShape
|
||||
docs: depositInto, docCreate, sparqlQuery, sparqlUpdate
|
||||
inbox: Deposit, PostOptions, materialize, post, postToDocument, processInbox, read, readForDocument, readSynced, share, watch
|
||||
storeRegistry: createEntityDoc, listMyEntityDocs, openDocumentInbox, resolveScopeGraph, resolveWriteGraph
|
||||
```
|
||||
|
||||
### `@ng-eventually/client/polyfill` — `src/polyfill.ts`
|
||||
**Of these, four are POLYFILL-ERA and have no target counterpart** — `configure`,
|
||||
`configureStoreRegistry`, `setCurrentUser`, `connectedUser` (plus the types
|
||||
`EventuallyConfig`, `StoreRegistryDeps`, `RegistrySession`). They are the deletion list,
|
||||
and `src/index.ts` groups them under a heading that says so. `ensureIdentity` is a fifth
|
||||
in substance — the shared-wallet gate — but the *call site* survives (§ 2bis).
|
||||
|
||||
```text
|
||||
direct: EventuallyConfig, RegistrySession, StoreRegistryDeps, VirtualUserRecord, configure, configureStoreRegistry, connectedUser, getConfig, getStoreRegistryDeps, resetCaps, resetConfig, resetStoreRegistry, setCurrentUser, share
|
||||
```
|
||||
Six symbols the previous `/polyfill` entry published are gone from the surface entirely:
|
||||
`getConfig` and `getStoreRegistryDeps` (internal wiring, reached through
|
||||
`shared-wallet/bootstrap`), `resetConfig` / `resetStoreRegistry` / `resetCaps` (test
|
||||
resets, reached by their internal path), and the direct `share` re-export — `inbox.share`
|
||||
was always the same function, and publishing it twice blurred the boundary it was meant
|
||||
to mark.
|
||||
|
||||
@@ -142,4 +142,4 @@ An adversary refuted the brief (7 findings — the 7th marked *(Plausible)*). **
|
||||
|
||||
**Consequence**: add **P0 (keyless-fetch spike)** up front and a **distinct WriteCap track**; requalify P2 (the real content = durability + cap-less + re-sharing, not "inverting the ACL"); record that **without crypto, read privacy is not applicable** (choose: real crypto vs masked projection).
|
||||
|
||||
Links: `readcap-and-nuri-model.md`, `packages/client/src/emulated-verifier/caps.ts`. On the consumer side, the Festipod brief "realign the sign-ups" depends on this effort.
|
||||
Links: `readcap-and-nuri-model.md`, `packages/sdk/src/emulated-verifier/caps.ts`. On the consumer side, the Festipod brief "realign the sign-ups" depends on this effort.
|
||||
|
||||
@@ -22,7 +22,7 @@ The spec below is unchanged — read it first. Everything from here to *Why this
|
||||
|
||||
| Spec | Where |
|
||||
|---|---|
|
||||
| `Nuri` / `ReadCap` (plain strings, `:r:` discriminant) | `packages/client/src/model/types.ts`, `src/model/nuri.ts` (internal parse/mint/derive) |
|
||||
| `Nuri` / `ReadCap` (plain strings, `:r:` discriminant) | `packages/sdk/src/model/types.ts`, `src/model/nuri.ts` (internal parse/mint/derive) |
|
||||
| Keyring, one per identity — `capFor` | `src/emulated-verifier/caps.ts` (`CapRegistry`), surfaced as `capFor` in `src/polyfill.ts` |
|
||||
| Caps of my OWN documents (the emulated `AddRepo { read_cap }`) | `src/shared-wallet/account-registry.ts` `fileOwnCaps`, called from `createEntityDoc` and `listMyEntityDocs` |
|
||||
| `shareCap(cap, toInbox)` + reception with no dedicated operation | `src/surface/inbox.ts` (`shareCap`, and the inline absorption in `read`) |
|
||||
@@ -45,7 +45,7 @@ One property this makes explicit and that is worth confirming: **the bare NURI o
|
||||
|
||||
## The exact surface a consumer codes against
|
||||
|
||||
From `@ng-eventually/client/polyfill`:
|
||||
From `@ng-eventually/sdk/polyfill`:
|
||||
|
||||
```ts
|
||||
capFor(nuri: Nuri): ReadCap | undefined // the keyring lookup
|
||||
@@ -192,13 +192,13 @@ 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` + `userInbox`, which is what a consumer must do too.
|
||||
- **The e2e ran against the live broker (`nextgraph.eu`) on 2026-08-03 — 39 passed, 0 failed.** The first run was 22/8, and the eight refusals were not test noise: they exposed a **real hole in the surface**. `docs.docCreate` filed no cap for the creator, so a consumer could create a document through the public primitive and then be refused reading or writing it. Upstream that cannot happen — `doc_create` commits `AddRepo { read_cap }` to the store's Store branch, so the creator holds it from the first instant. Fixed at `packages/sdk/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
|
||||
|
||||
The permanent documentation was updated in the same pass (root `README.md`, `packages/client/README.md`, `docs/simulation.md`, `docs/migration-guide.md` §1 + the assumed `declareConnections` break, `docs/read-model.md`, `docs/readcap-and-nuri-model.md` §5, `docs/nextgraph-current-state.md`, `packages/client/docs/sdk-reference.md`). **If the review changes the surface, those are the files to re-align** — they describe the code as it stands now, not a validated state.
|
||||
The permanent documentation was updated in the same pass (root `README.md`, `packages/sdk/README.md`, `docs/simulation.md`, `docs/migration-guide.md` §1 + the assumed `declareConnections` break, `docs/read-model.md`, `docs/readcap-and-nuri-model.md` §5, `docs/nextgraph-current-state.md`, `packages/sdk/docs/sdk-reference.md`). **If the review changes the surface, those are the files to re-align** — they describe the code as it stands now, not a validated state.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
>
|
||||
> And four more, all confirmed:
|
||||
>
|
||||
> 4. **D4 would delete a working recovery path.** Inbox deposits are never removed (`packages/client/src/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".
|
||||
> 4. **D4 would delete a working recovery path.** Inbox deposits are never removed (`packages/sdk/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(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.**
|
||||
|
||||
@@ -47,7 +47,7 @@ breaks writes with `RepoNotFound`). See the scope rule in
|
||||
|
||||
*The decision stands; the mechanism named in it has been replaced.* Opening was
|
||||
`orm_start_graph` when this was written. It is now `ensureRepoOpen` — `doc_subscribe`
|
||||
plus a wait for the first `State` (`packages/client/src/emulated-verifier/open-repo.ts:167`) — after
|
||||
plus a wait for the first `State` (`packages/sdk/src/emulated-verifier/open-repo.ts:167`) — after
|
||||
`orm_start_graph` was found to hang on a fan-out (`subscribe.ts:28,181`). What must be
|
||||
read here is the invariant *"open the repo, by its store NURI, before writing"*, not the
|
||||
call that used to implement it.
|
||||
|
||||
@@ -70,11 +70,11 @@ The surface consequence: the *act* — obtain a link, circulate it — is the sa
|
||||
|
||||
## 5. Recommendation for the polyfill
|
||||
|
||||
The surface already exists: `linkTo(doc: NuriLike): ReadCap` (`packages/client/src/surface/placement.ts:65-77`) for the traveling value, `inbox.share(doc, toUser)` (`packages/client/src/surface/inbox.ts:285`) for directed delivery. **Keep `linkTo` — the act is the right one** — with four adjustments:
|
||||
The surface already exists: `linkTo(doc: NuriLike): ReadCap` (`packages/sdk/src/surface/placement.ts:65-77`) for the traveling value, `inbox.share(doc, toUser)` (`packages/sdk/src/surface/inbox.ts:285`) for directed delivery. **Keep `linkTo` — the act is the right one** — with four adjustments:
|
||||
|
||||
1. **Label it LEVEL-1 SHAPE in `docs/api-contract.md`.** What supports it: the `NgLink` family and its stated sharing flow, the `PermaShare` permission, the exercised object-URL and profile-QR precedents, and the PO doctrine that circulation is the only distribution. The model's own stated flows are unusable without *some* produce-a-link affordance, which is as much confidence as an unbuilt feature allows. What cannot be promised: the SDK's name for it, sync vs async (upstream link-building needs overlay + peers from the session, so async is plausible — same adapter-sized delta class as `subscribeDoc`'s sync unsubscribe), and whether the value is a NURI string or a structured link. Therefore: **the returned value is opaque**; a consumer that stores it, transmits it, and hands it back unmodified learns nothing to unlearn; a consumer that parses it does.
|
||||
2. **Fix the comment-vs-code mismatch in `linkTo`.** The docstring claims *"A protected document's key never comes out this way — it goes through `share`"*; the code returns any held cap, with no scope check. The **code** is the model-true side: `RepoLinkV0`-with-key IS the protected-document link, and sharing it out-of-band is the documented normal case (`:5059`). Align the comment: a protected link carries the key and is legitimate to circulate — with the §4 durability caveat, not a prohibition.
|
||||
3. **The recipient verb is missing.** Nothing exported ingests an out-of-band link: `learn` is reached only by inbox processing and the connection drain (`packages/client/src/surface/inbox.ts:410`, `packages/client/src/emulated-verifier/connect.ts:68`), and `getCaps()` is documented machinery (api-contract §15). The model names the recipient act precisely — open the link: load the repo from its read cap, file `AddLink` durably on the User branch, subscribe (`:5059`; `engine/repo/src/types.rs:1934-1950`; `verifier.rs:2237`). Suggested surface, same epistemic label as `linkTo`: `openLink(link: string): Promise<Nuri>` — files the cap in the emulated registers and returns the cap-less target for use in reads. Without it, path 2 has a producer and no consumer, and the multi-actor test where Bob *obtains* the document through calls (never through a shared variable) cannot be written — the exact failure mode `rules/engineering/multi-actor-tests-obtain-not-receive.md` records.
|
||||
3. **The recipient verb is missing.** Nothing exported ingests an out-of-band link: `learn` is reached only by inbox processing and the connection drain (`packages/sdk/src/surface/inbox.ts:410`, `packages/sdk/src/emulated-verifier/connect.ts:68`), and `getCaps()` is documented machinery (api-contract §15). The model names the recipient act precisely — open the link: load the repo from its read cap, file `AddLink` durably on the User branch, subscribe (`:5059`; `engine/repo/src/types.rs:1934-1950`; `verifier.rs:2237`). Suggested surface, same epistemic label as `linkTo`: `openLink(link: string): Promise<Nuri>` — files the cap in the emulated registers and returns the cap-less target for use in reads. Without it, path 2 has a producer and no consumer, and the multi-actor test where Bob *obtains* the document through calls (never through a shared variable) cannot be written — the exact failure mode `rules/engineering/multi-actor-tests-obtain-not-receive.md` records.
|
||||
4. **Do not add**: link options (expiry, audience, revoke-this-link), per-reader introspection for public documents, or any API that parses or inspects a link's insides — nothing upstream supports any of them, and each teaches a lever the model does not have.
|
||||
|
||||
## 6. The question for the NextGraph developer
|
||||
|
||||
@@ -37,7 +37,7 @@ Interpretation (**plausible mechanism, not settled**): the write was pushed into
|
||||
|
||||
## What the SDK exposes but does not consume
|
||||
|
||||
`disconnections_subscribe` **does fire** on this failure — but neither the polyfill (`@ng-eventually/client`) nor the consumer app subscribes to it. The signal exists, nobody listens to it; on the app side, no mechanism retries or warns the user.
|
||||
`disconnections_subscribe` **does fire** on this failure — but neither the polyfill (`@ng-eventually/sdk`) nor the consumer app subscribes to it. The signal exists, nobody listens to it; on the app side, no mechanism retries or warns the user.
|
||||
|
||||
## Scope & not reproduced
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Internal contract — what `@ng-eventually/client` keeps off its surface, and what NextGraph does or would do about each subject
|
||||
# Internal contract — what `@ng-eventually/sdk` keeps off its surface, and what NextGraph does or would do about each subject
|
||||
|
||||
> **Updated 2026-08-03, after the source layout was reorganised by migration fate** (`docs/source-layout-by-fate.md`). Paths, and three names, changed under this document: `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/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`).
|
||||
**Scope.** The complement of [`docs/api-contract.md`](./api-contract.md): every module export under `packages/sdk/src/` that is NOT reachable from the two published entry points (`package.json` maps exactly `.` → `src/index.ts` and `./polyfill` → `src/polyfill.ts`). A consumer never reads this document; a maintainer does. The internal code is held to the same standard as the surface — as close as possible to what NextGraph does or plans — so every subject below carries the same target-side analysis. Written 2026-08-04, verified against the `nextgraph-rs` clone (HEAD `213338f6`) and the installed `@ng-org/web@0.1.2-alpha.13` declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
|
||||
|
||||
**How the boundary was computed — mechanically, from the `export` statements.** `index.ts` re-exports wholesale (`export *` / `export * as ns`) from `types.ts`, `inbox.ts`, `docs.ts`, `surface/read-model.ts`, and by name everything `surface/use-shape.ts`, `surface/watch-shape.ts`, `lifecycle.ts`, `sparql.ts` export, 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 `share` 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`.
|
||||
**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; its polyfill-era symbols — `configure`, `configureStoreRegistry`, `setCurrentUser`, `connectedUser` and their types — now sit in a marked block of the single entry, and the machinery accessors and test resets it used to publish are no longer published at all.)* 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.
|
||||
|
||||
@@ -146,7 +146,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 `VirtualUserRecord` (`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 type `RegistrySession` is published by the entry (a consumer types its injected `getSession` with it) and covered by the surface contract; `VirtualUserRecord` is internal.
|
||||
|
||||
### 9a. Account shim — provision, resolve, reserved names, cache
|
||||
|
||||
@@ -164,7 +164,7 @@ export async function ensureAccount(id: string): Promise<VirtualUserRecord>;
|
||||
`resolveAccount` — barrier-authoritative O(1) lookup of one account's record in the doc-shim; `ensureAccount` — resolve-or-provision (creates the three scope docs on first sight, concurrency-deduped); `reservedAccount` — NUL-prefixed sentinel namespace for lib-internal accounts; `resetRegistryCache` — test/wallet-switch reset.
|
||||
|
||||
- **NO COUNTERPART, shared-wallet machinery — the whole group.** The target has no directory of identities to resolve or provision: a user's site (three stores + their inboxes) is created once at wallet creation (`engine/verifier/src/site.rs` — the site-creation flow committing the stores and the two store-inbox `AddInboxCap`s at `:128,149`), and "which user" is the session. `ensureAccount`'s provision-on-first-sight has no target analogue and is exactly what `connectedUser` refuses to trigger (`connect.ts:60-65`). All of it disappears with the shim (`docs/migration-guide.md` § 3).
|
||||
- `reservedAccount`'s collision-safety rests on an **ASSUMPTION about a consumer-injected function**: the comment (`store-registry.ts:200-206`) asserts the injected `normalizeId` can never produce a U+0000-prefixed key, but `normalizeId` is injected by the consumer and the lib's own default is a bare `trim()` (`polyfill.ts:145`), which does not strip U+0000. Bound: a consumer id would have to begin with a literal NUL to collide — implausible from any UI, but the property is the consumer's to keep, not the lib's (see Findings F5).
|
||||
- `reservedAccount`'s collision-safety rests on an **ASSUMPTION about a consumer-injected function**: the comment (`store-registry.ts:200-206`) asserts the injected `normalizeId` can never produce a U+0000-prefixed key, but `normalizeId` is injected by the consumer and the lib's own default is a bare `trim()` (`shared-wallet/bootstrap.ts`), which does not strip U+0000. Bound: a consumer id would have to begin with a literal NUL to collide — implausible from any UI, but the property is the consumer's to keep, not the lib's (see Findings F5).
|
||||
|
||||
### 9b. Scope-index resolution
|
||||
|
||||
@@ -249,9 +249,9 @@ 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 `virtualUsers.*` on the SDK entry — since the split (`index.ts:34` routes through `surface/placement.ts`; `polyfill.ts:238` carries `virtualUsers`) 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` routes through `surface/placement.ts`) those are internal. That file is being edited concurrently; noted here, deliberately not fixed by this document.
|
||||
|
||||
**F5 — `reservedAccount`'s collision guarantee is asserted about code the lib does not own.** `store-registry.ts:200-206` states the injected `normalizeId` "strips a leading `@`, trims, and lowercases, so a NUL prefix is unreachable" — that describes ONE consumer's normalizer, not a contract; the lib's own default is `id.trim()` (`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`.
|
||||
**F5 — `reservedAccount`'s collision guarantee is asserted about code the lib does not own.** `store-registry.ts:200-206` states the injected `normalizeId` "strips a leading `@`, trims, and lowercases, so a NUL prefix is unreachable" — that describes ONE consumer's normalizer, not a contract; the lib's own default is `id.trim()` (`shared-wallet/bootstrap.ts`), which passes U+0000 through. The reserved namespace is disjoint only if every consumer's normalizer keeps it so. Either document the requirement on `StoreRegistryDeps.normalizeId`, or reject NUL-prefixed raw ids at `accountKey`.
|
||||
|
||||
**Migration-risk flags (shapes that will not travel):**
|
||||
|
||||
@@ -268,4 +268,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/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.
|
||||
Modules with no internal exports (everything they export is published): `types.ts`, `docs.ts`, `inbox.ts`, `surface/read-model.ts`, `shared-wallet/virtualUsers.ts`, `emulated-verifier/caps.ts`, `sparql.ts`, `lifecycle.ts`, `surface/use-shape.ts`, `surface/watch-shape.ts`, `surface/placement.ts`, and the entry point.
|
||||
|
||||
@@ -96,7 +96,7 @@ The consumer application imports `@ng-org/web` / `@ng-org/orm` resolved to this
|
||||
via a build alias during the polyfill period. Removing the alias makes those imports
|
||||
resolve to the real SDK — the `ng`/`useShape`/`inbox` surface is SDK-identical, so
|
||||
no consumer code changes. The one non-SDK call — `configure(...)` /
|
||||
`@ng-eventually/client/polyfill` — is deleted. The lib itself disappears.
|
||||
the POLYFILL-ERA block of `@ng-eventually/sdk` — is deleted. The lib itself disappears.
|
||||
|
||||
## The one break already taken: `declareConnections`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Current-state NextGraph — what the SDK/broker do and do NOT expose
|
||||
|
||||
**Owner:** this library. `@ng-eventually/client` exists because the *current*
|
||||
**Owner:** this library. `@ng-eventually/sdk` exists because the *current*
|
||||
NextGraph JS SDK is immature. This file is the authoritative reference on what
|
||||
today's SDK/broker actually give us — the ground truth every polyfill in this
|
||||
lib compensates for. Read [`simulation.md`](./simulation.md) for how we emulate
|
||||
@@ -323,7 +323,7 @@ has both**:
|
||||
- **Subscribable with a sync BARRIER.** `doc_subscribe(nuri)` delivers `TabInfo` then
|
||||
an initial **`State`** (`verifier.rs:470`/`:476`); that first `State` is the sync
|
||||
barrier — **after it, presence is guaranteed and absence is definitive** (pinned
|
||||
empirically by CONTRACT 3 in `packages/client/e2e/`). But this barrier exists only
|
||||
empirically by CONTRACT 3 in `packages/sdk/e2e/`). But this barrier exists only
|
||||
for a repo `doc_subscribe` can open, i.e. a `did:ng:o:<RepoID>` repo. A **store-root
|
||||
has no first-`State` barrier**: an anchored read on it can return 0 rows during
|
||||
sync-lag with no signal distinguishing "still syncing" from "genuinely empty".
|
||||
@@ -747,7 +747,7 @@ chain never runs and consumers keep a stale value until the next connection
|
||||
delivers a fresh initial `State`. REMOTE commits DO push correctly (verified:
|
||||
cross-browser reactive update works). Verdict pending a live instrumented run.
|
||||
Full write-up (suspect link, instrumentation, planned polyfill-side fix):
|
||||
[`../packages/client/docs/sdk-reference.md`](../packages/client/docs/sdk-reference.md)
|
||||
[`../packages/sdk/docs/sdk-reference.md`](../packages/sdk/docs/sdk-reference.md)
|
||||
§ *Current emulation status*.
|
||||
|
||||
### Cold-start anchored read returns 0 rows instead of an error — symptom VERIFIED, mechanism INFERRED, healed polyfill-side
|
||||
@@ -756,7 +756,7 @@ On a FRESH session over the SAME persistent wallet (reconnect, new page, re-logi
|
||||
anchored `sparql_query` against a document written in an earlier session comes back with
|
||||
**0 rows and no error** — persisted documents read as empty. Observed on every anchored
|
||||
reader of the polyfill and healed identically in each (`ensureRepoOpen` before the read,
|
||||
`packages/client/src/emulated-verifier/open-repo.ts`): the user's own documents,
|
||||
`packages/sdk/src/emulated-verifier/open-repo.ts`): the user's own documents,
|
||||
the user's store (`shared-wallet/account-registry.ts` `readUserStore`), the by-need doc batch
|
||||
(`surface/read-model.ts` `readUnion`), and the store-root pointer read (`shared-wallet/account-registry.ts`
|
||||
`resolvePointer`). The heal is `doc_subscribe(nuri)` → await the first `State` (the sync
|
||||
@@ -792,7 +792,7 @@ is a single account subject carrying MULTIPLE values for one scope predicate (ob
|
||||
five `shim:docPublic`), after which a writer and a later reader can resolve DIFFERENT
|
||||
scope docs and the reader's anchored read returns 0.
|
||||
|
||||
Two polyfill-side guards, both in `packages/client/src/shared-wallet/account-registry.ts`: `ensureInFlight`
|
||||
Two polyfill-side guards, both in `packages/sdk/src/shared-wallet/account-registry.ts`: `ensureInFlight`
|
||||
(a bounded promise map keyed by account, so concurrent `ensureAccount` calls share ONE
|
||||
resolve-or-provision) prevents new forks; `canonicalDoc` (pick the lexicographically
|
||||
smallest NURI among all distinct values for a scope predicate — NURIs are
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ shared wallet. This is a design decision, grounded entirely in the query
|
||||
capability documented in
|
||||
[`nextgraph-current-state.md`](./nextgraph-current-state.md) § *The query
|
||||
capability*. The consumer application never sees any of this: it asks
|
||||
`@ng-eventually/client` for its lists by need and trusts the answer — the whole
|
||||
`@ng-eventually/sdk` for its lists by need and trusts the answer — the whole
|
||||
read mechanism lives here, in the polyfill.
|
||||
|
||||
> The rule in one line: read each by-need doc with its own anchored
|
||||
@@ -172,7 +172,7 @@ and never iterates the other named graphs. (A repo absent from `self.repos` thro
|
||||
`RepoNotFound` and is skipped per-doc, see the VERIFIED note above — the read cannot
|
||||
sync an unknown repo.)
|
||||
|
||||
> **Re-confirmed by the standing e2e harness (`packages/client/e2e/`, broker
|
||||
> **Re-confirmed by the standing e2e harness (`packages/sdk/e2e/`, broker
|
||||
> `@ng-org/web 0.1.2-alpha.13`).** The `docRoundTrip` check measures all three shapes
|
||||
> anchored to a doc D: (a) a no-GRAPH default-graph write round-trips; (b) an explicit
|
||||
> `INSERT DATA { GRAPH <D> {…} }` — a **constant** plain NURI — **also** round-trips
|
||||
|
||||
@@ -374,7 +374,7 @@ 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
|
||||
|
||||
**Realigned 2026-07-28 (batch P1a).** `packages/client/src/emulated-verifier/caps.ts` used to model `readers: Map<Nuri, Set<PrincipalId>>` + `grantRead(doc, grantee)` — a per-document **ACL of principals**, the exact INVERSION of the real model. It now records, **per identity**, the caps that identity holds (`Map<Nuri, ReadCap>`) — whose only question is `capFor(nuri)` — and `nuri.ts` carries the cap-less / cap-bearing distinction on the `r:` segment. The durable registers are emulated in `shared-wallet/account-registry.ts` (`readCap` on the Store branch, `link` on the User branch); this in-memory record is their cache.
|
||||
**Realigned 2026-07-28 (batch P1a).** `packages/sdk/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) |
|
||||
|---|---|---|
|
||||
|
||||
+4
-4
@@ -11,7 +11,7 @@
|
||||
> file for *how* each emulation works; read those two for *what is fake* and *what
|
||||
> replaces it*.
|
||||
|
||||
The consumer application writes against `@ng-eventually/client` as if NextGraph
|
||||
The consumer application writes against `@ng-eventually/sdk` as if NextGraph
|
||||
already shipped per-entity documents in public/protected/private stores, capabilities
|
||||
and inboxes. It hasn't (see [`nextgraph-current-state.md`](./nextgraph-current-state.md)).
|
||||
This file is the lib's own engineering doctrine on how it fabricates that mature
|
||||
@@ -143,7 +143,7 @@ public/protected/private stores — on top of one shared wallet.
|
||||
- **Generic by construction.** The registry knows only the three native scopes,
|
||||
zero application entity kind. The consumer application maps its entities to a scope
|
||||
and injects the session + identity-id normalization via `configureStoreRegistry({
|
||||
getSession, normalizeId })` (`polyfill.ts`).
|
||||
getSession, normalizeId })` (`shared-wallet/bootstrap.ts`, published by the entry).
|
||||
|
||||
The `store≠document` two axes materialize here directly: the registry moves along
|
||||
axis B (more documents = more isolation), never axis A (it always writes into the
|
||||
@@ -361,7 +361,7 @@ another name.
|
||||
|
||||
### Sharing, publication, and the recipient
|
||||
|
||||
- **`setCurrentUser(id)` (`polyfill.ts`)** — the SDK's "current identity" call.
|
||||
- **`setCurrentUser(id)` (`shared-wallet/bootstrap.ts`)** — the SDK's "current identity" call.
|
||||
It selects *whose* caps are consulted, lazily, so the delivered subset always
|
||||
reflects the identity in effect at read time.
|
||||
- **`inbox.share(doc, toUser)`** — the one sharing act the lib exposes. Recipients
|
||||
@@ -639,5 +639,5 @@ document trust root):
|
||||
(`did:ng:...`): validates and throws on IRI-breaking chars rather than emitting
|
||||
a malformed/injected query.
|
||||
|
||||
These are re-exported from `@ng-eventually/client` so the consumer application
|
||||
These are re-exported from `@ng-eventually/sdk` so the consumer application
|
||||
reuses the same escaping when it builds SPARQL.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Source layout by migration fate — analysis and recommendation
|
||||
|
||||
**Status: analysis only.** Nothing has been moved; no source file was modified. Written 2026-08-04 from the source of `packages/client/src/` (25 modules), the two contracts (`docs/api-contract.md`, `docs/internal-contract.md`) used as the export-level inventory, and the read-only `nextgraph-rs` clone (HEAD `213338f6`); the upstream facts this analysis leans on were re-verified at the source and are cited with layer numbers per `README.md` § *The three references* (1 = engine, 2 = wasm binding, 3 = JS ORM).
|
||||
**Status: ADOPTED.** The layout this document recommends is the one in the tree — `model/`, `surface/`, `emulated-verifier/`, `shared-wallet/`. Read the rest as the reasoning that produced it, not as a proposal. *(Two things have moved under it since: the package became `packages/sdk` / `@ng-eventually/sdk` on 2026-08-07 — it was `packages/client` / `@ng-eventually/client`, kept here as a search keyword — and `emulated-verifier/public-store.ts` was added on 2026-08-06. Paths below point at where the files are now.)*
|
||||
|
||||
Written 2026-08-04, when nothing had yet been moved, from the source of `src/` (25 modules), the two contracts (`docs/api-contract.md`, `docs/internal-contract.md`) used as the export-level inventory, and the read-only `nextgraph-rs` clone (HEAD `213338f6`); the upstream facts this analysis leans on were re-verified at the source and are cited with layer numbers per `README.md` § *The three references* (1 = engine, 2 = wasm binding, 3 = JS ORM).
|
||||
|
||||
**The question.** Today all 25 modules sit flat in `src/`, named mechanically. Three different fates coexist undistinguished: modules whose *shape* the consumer keeps (the surface the real SDK replaces), modules standing in for what the engine/verifier will do natively, and modules that exist only because the emulation runs on one shared wallet. The bet under evaluation: if the folder structure mirrors the target's own structure, divergence gets harder to commit and easier to spot.
|
||||
|
||||
@@ -44,11 +46,11 @@ And a fourth group the three-fate framing misses: the **target's model vocabular
|
||||
|
||||
## 2. The recommended layout
|
||||
|
||||
Both entry files stay at `src/` root, so `package.json`'s `exports` map (exactly `.` and `./polyfill`) is untouched and the consumer application sees no change.
|
||||
Both entry files stay at `src/` root, so `package.json`'s `exports` map is untouched and the consumer application sees no change. *(The two doors were merged into one on 2026-08-07 — the `exports` map is now just `.`, and the polyfill-era symbols sit in a marked block of `index.ts`.)*
|
||||
|
||||
| Folder | What the name asserts | Alignment reference | Fate at migration |
|
||||
|---|---|---|---|
|
||||
| `src/` root (`index.ts`, `polyfill.ts`) | The two published doors, nothing else. `index.ts` may re-export only from `surface/` and `model/`; `polyfill.ts` may re-export by name from anywhere — it is the polyfill-era door and its imports *are* the list of what dies. | — | `index.ts` is replaced by the real SDK via the build alias; `polyfill.ts` is deleted. |
|
||||
| `src/` root (`index.ts`, `polyfill.ts`) | The two published doors, nothing else. `index.ts` may re-export only from `surface/` and `model/`; `polyfill.ts` may re-export by name from anywhere — it is the polyfill-era door and its imports *are* the list of what dies. *(Merged 2026-08-07: one door, `index.ts`, with the polyfill-era symbols in a marked block. The rule survives as a block boundary instead of a file boundary.)* | — | The SDK-shaped half is replaced by the real SDK via the build alias; the polyfill-era block is deleted. |
|
||||
| `model/` | The target's addressing model, transcribed: pure vocabulary (types, NURI grammar, guards). No I/O, no state, no minting. Importable by every layer. | Level 1, verified (`NuriV0`, `readcap_nuri` — `engine/repo/src/types.rs:518-521`) | Survives as knowledge; the guards stay useful against the real SDK (which takes plain strings). |
|
||||
| `surface/` | App-facing, and every symbol has a target counterpart — verified or a documented bet — in `docs/api-contract.md`. A consumer coding against this folder learns nothing to unlearn. | Levels 3/2 where they answer, level-1 shape where they do not (per subject, in the contract) | Deleted when the alias flips; the consumer's code is unchanged. |
|
||||
| `emulated-verifier/` | Stand-ins for what the engine/verifier/broker do natively: possession, filing, boundary, non-delivery, inbox processing, branch registers, repo opening. Aligned on the level-1 model; each module names its native counterpart mechanism. **This is the folder where divergence from the model is possible, and its main risk.** | Level 1 (the model is the specification) | Deleted — the native side takes over. |
|
||||
@@ -128,7 +130,7 @@ Full mixed list: `docs.ts`, `surface/read-model.ts`, `inbox.ts`, `subscribe.ts`,
|
||||
|
||||
## 5. Cost and risk
|
||||
|
||||
**What does not change: the published surface.** Both entries keep their `src/` paths; `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)*; `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.
|
||||
|
||||
**Import churn — the inventory:**
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Vision & principles of the `@ng-eventually/client` polyfill
|
||||
# Vision & principles of the `@ng-eventually/sdk` polyfill
|
||||
|
||||
## Purpose
|
||||
|
||||
|
||||
Reference in New Issue
Block a user