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:
Sylvain Duchesne
2026-08-07 11:05:19 +02:00
parent 0832338201
commit 0eb25286c8
85 changed files with 423 additions and 413 deletions
+54 -46
View File
@@ -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.