refactor(api): séparer la surface de l'app et la machinerie

L'entrée SDK déversait la machinerie par deux fuites :

- `export * as storeRegistry from "./store-registry"` exportait TOUT le module —
  `ensureAccount`, `addLink`, `readLinks`, `resolveAccount`, `reservedAccount`,
  `resetRegistryCache`, `isOwnInbox`, `myInboxes`, `userStoreDoc`. Remplacé par
  `store-registry-api.ts`, qui ne ré-expose que les sept appels destinés à
  l'app : createEntityDoc, listMyEntityDocs, resolveScopeGraph,
  resolveWriteGraph, walletInbox, openDocumentInbox, documentInboxAddress.
- `accounts.*` — persistance d'identité navigateur, sans aucun pendant SDK —
  passe sur `/polyfill`, où sa disparition à la migration se lit sur la ligne
  d'import.

L'en-tête d'`index.ts` affirmait n'exposer « que ce que @ng-org/web et
@ng-org/orm exposent ». C'était faux et enseignait une frontière fausse : un
consommateur en déduisait que tout ce qui s'importe de l'entrée survit à la
migration, ce qui ne valait ni pour `accounts` ni pour l'essentiel de
`storeRegistry`. Il énonce désormais ce que l'entrée promet vraiment : tout
symbole y a un pendant dans le futur SDK, vérifié ou assumé, et rien n'y est de
la machinerie.

La frontière mord : le typecheck e2e a échoué aussitôt, le harnais atteignant
`ensureAccount` et `resetRegistryCache` par l'entrée publique. Il passe
désormais par le chemin interne, comme les tests unitaires — légitime, il teste
la bibliothèque.

Deux documents plutôt qu'un, mêmes exigences, publics différents :
`docs/api-contract.md` (la surface de l'app, avec pour chaque sujet la signature
que le futur SDK devrait exposer, et l'étiquette qui distingue le vérifié de
l'assumé) et `docs/internal-contract.md` (le complément exact).

157 tests unitaires, e2e 40/40 contre le broker en ligne.
This commit is contained in:
Sylvain Duchesne
2026-08-04 12:16:59 +02:00
parent e5779ca850
commit d07b3642aa
6 changed files with 972 additions and 32 deletions
+18 -12
View File
@@ -1,14 +1,23 @@
/**
* @ng-eventually/client — **SDK-identical** surface.
* @ng-eventually/client — the surface a consumer application codes against.
*
* This entry exposes ONLY what `@ng-org/web` / `@ng-org/orm` expose (current +
* anticipated: `inbox`). Import `ng` / `useShape` from here instead of the SDK
* during the polyfill period; at migration the build alias is removed and these
* resolve to the real SDK with **no code change**.
* Everything here has a target-SDK counterpart, verified or assumed, listed in
* `docs/api-contract.md`. Import `ng` / `useShape` from here rather than from the
* SDK during the polyfill period; at migration the build alias is removed and
* these resolve to the real SDK.
*
* The one non-SDK piece — the polyfill bootstrap (`configure`, capability
* helpers, current user) — lives at `@ng-eventually/client/polyfill`, and is the
* only thing removed at migration.
* **This entry carries no machinery.** The earlier header claimed it exposed "ONLY
* what `@ng-org/web` / `@ng-org/orm` expose", which was false as written: it also
* shipped the whole `store-registry` module (account resolution, cap registers,
* cache resets) and `accounts` (browser identity persistence, polyfill-era with no
* SDK counterpart). Both leaked machinery onto the entry whose promise is that it
* survives migration. `storeRegistry` is now the app-facing slice only
* (`store-registry-api.ts`); `accounts` moved to `/polyfill`.
*
* The polyfill bootstrap — `configure`, the capability helpers, the current user,
* identity persistence — lives at `@ng-eventually/client/polyfill`: everything an
* application needs TODAY that will not exist tomorrow, kept apart so what goes
* away is visible at the import line.
*/
export * from "./types";
@@ -22,10 +31,7 @@ export { subscribeDoc, subscribeDocs, docChangeType } from "./subscribe";
export type { DocChange, DocChangeType, Unsubscribe } from "./subscribe";
export * as readModel from "./read-model";
export type { UnionSubject } from "./read-model";
export * as storeRegistry from "./store-registry";
export type { AccountRecord, RegistrySession } from "./store-registry";
export * as accounts from "./accounts";
export type { AccountStorage } from "./accounts";
export * as storeRegistry from "./store-registry-api";
// SPARQL injection-safety helpers — so the app can reuse the same escaping /
// validation when it builds SPARQL by interpolation. `escapeLiteral` for string
+12
View File
@@ -227,3 +227,15 @@ export function resetCaps(): void {
export { CapRegistry } from "./caps";
export { shareCap } from "./inbox";
export { connectedUser } from "./connect";
// --- identity persistence (polyfill-era, no SDK counterpart) ----------------
//
// Moved here from the SDK-identical entry on 2026-08-03. `accounts` persists WHICH
// virtual user is connected, in browser storage — a notion that exists only because
// one shared wallet hosts several identities. The real SDK has no counterpart: there
// each user opens their own wallet, and "who am I" is the session. Shipping it from
// the SDK entry advertised as durable something that disappears at migration.
export * as accounts from "./accounts";
export type { AccountStorage } from "./accounts";
// Config-shaped types the bootstrap needs; both describe the shim, not the SDK.
export type { AccountRecord, RegistrySession } from "./store-registry";
+37
View File
@@ -0,0 +1,37 @@
/**
* The app-facing slice of `store-registry` — and the reason it exists as a file.
*
* `store-registry.ts` holds two things that must not be exported together: the
* placement/addressing calls a consumer application legitimately makes, and the
* shim machinery that makes virtual users work at all (account resolution, the
* durable cap registers, the inbox-ownership predicate, cache resets). Until now
* `index.ts` did `export * as storeRegistry from "./store-registry"` and shipped
* both, so an application could reach `ensureAccount`, `addLink` or
* `resetRegistryCache` from the SDK-identical entry — machinery it must never call,
* on the entry whose whole promise is "this survives migration unchanged".
*
* What is re-exported here is only what an application needs to do its own work,
* and each has a target-SDK counterpart (see `docs/api-contract.md`). Everything
* else stays reachable at `./store-registry` for the library's own modules, the
* unit tests and the e2e harness — an internal path, not a published one.
*
* At migration this file disappears: placement becomes the user's real per-scope
* stores and the calls below become native SDK ones.
*/
export {
/** Create a document for ONE entity in `scope`, and record it in that scope's store. */
createEntityDoc,
/** The entity documents this user owns in `scope` — with their caps recovered. */
listMyEntityDocs,
/** The NURI to use as a READ scope for `scope` (what `useShape` is pointed at). */
resolveScopeGraph,
/** The NURI where GROUPED entities of `scope` are written (no per-entity document). */
resolveWriteGraph,
/** A user's own inbox — where caps and messages addressed to THEM arrive. */
walletInbox,
/** Open an inbox on a document you OWN, so others can deposit into it. */
openDocumentInbox,
/** WHERE to deposit for a document — readable by any holder of it. `undefined` if none. */
documentInboxAddress,
} from "./store-registry";