docs+test: le contrat avait dérivé — et le mécanisme ne voyait pas les règles
En vérifiant l'alignement de la surface, cinq sections du contrat s'étaient désynchronisées du code sans que rien ne rougisse : - § 1 montrait `getConfig`, `getStoreRegistryDeps`, `resetConfig` et `resetStoreRegistry` comme exportés — retirés à la fusion des portes ; - § 11 documentait `escapeLiteral` / `escapeIri` / `assertNuri` comme publiés — ils ne le sont plus, et l'absence de garde de type est désormais expliquée par sa raison : les portes valident elles-mêmes (`NuriLike`), publier une garde inviterait le cast que les types servent à empêcher ; - § 12 listait sept fonctions `storeRegistry` — il y en a cinq depuis que les deux fonctions d'ADRESSE d'inbox sont parties (une app nomme un document ou une personne, jamais une adresse) ; - § 13 listait `IdentityStore`, `browserIdentityStore` et `getCurrentUser` comme publiés — retirés le 2026-08-05 ; - `ensureIdentity` était publié **sans aucune règle**, et l'annexe renvoyait à un « § 2bis » qui n'existait pas. **§ 2bis est écrit** : le portail d'accès n'a aucune contrepartie en substance — en amont un utilisateur ouvre SON portefeuille et il n'y a rien à nommer — mais son SITE D'APPEL survit, et c'est pourquoi sa signature ne prend pas d'identifiant : nommer son identité est précisément la partie qui disparaît, donc elle ne doit pas figurer dans les paramètres. **Le mécanisme est étendu.** `test/vocabulary.test.ts` épinglait l'annexe — les NOMS — et ne voyait pas les sections, là où vivent les règles. Une règle périmée est pire qu'une règle absente : elle se lit comme vérifiée. Désormais tout `export` montré dans un bloc « ### Today » doit être réellement exporté ; ce qu'on garde pour mémoire passe en commentaire, que le contrôle ignore par construction. Les cinq dérives ci-dessus auraient été rouges le jour même. Nettoyé aussi : deux commentaires de doc orphelins dans `surface/placement.ts`, restés au-dessus de l'accolade fermante après le retrait des fonctions qu'ils décrivaient. 180 tests unitaires, typecheck bibliothèque / exemple / harnais.
This commit is contained in:
+67
-58
@@ -22,35 +22,29 @@ Per the design principle (`README.md` § *Design principle*): an absent implemen
|
||||
### Today — `@ng-eventually/sdk` (the POLYFILL-ERA block of `src/index.ts`; everything here is removed at migration)
|
||||
|
||||
```ts
|
||||
// shared-wallet/bootstrap.ts
|
||||
// all from shared-wallet/bootstrap.ts
|
||||
export interface EventuallyConfig {
|
||||
ng: NgLike;
|
||||
useShape: UseShapeLike;
|
||||
sharedWallet?: { name: string; secret: string };
|
||||
sharedWallet?: SharedWalletConfig; // the gate's, § 2bis
|
||||
currentUser?: PrincipalId;
|
||||
debugAccessLog?: boolean;
|
||||
init?: (...args: any[]) => any;
|
||||
initNg?: (...args: any[]) => any;
|
||||
}
|
||||
// shared-wallet/bootstrap.ts
|
||||
export function configure(c: EventuallyConfig): void;
|
||||
// shared-wallet/bootstrap.ts — tests only
|
||||
export function resetConfig(): void;
|
||||
|
||||
// shared-wallet/bootstrap.ts
|
||||
export interface StoreRegistryDeps {
|
||||
getSession: () => Promise<RegistrySession>;
|
||||
normalizeId?: (id: string) => string;
|
||||
pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number };
|
||||
}
|
||||
// shared-wallet/bootstrap.ts
|
||||
export function configureStoreRegistry(deps: StoreRegistryDeps): void;
|
||||
// shared-wallet/bootstrap.ts — tests only
|
||||
export function resetStoreRegistry(): void;
|
||||
|
||||
// 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;
|
||||
// NOT published (2026-08-07, with the entry merge) — merging the two doors made
|
||||
// publishing these a visible choice rather than an inherited one, and the choice is no:
|
||||
// getConfig, getStoreRegistryDeps internal wiring; the surface reaches them by import
|
||||
// resetConfig, resetStoreRegistry test resets; the suite reaches them the same way
|
||||
```
|
||||
|
||||
### Target
|
||||
@@ -96,6 +90,35 @@ Divergence: none in behaviour (pure forwarding), but the wrapper erases the para
|
||||
|
||||
---
|
||||
|
||||
## 2bis. The access gate — `ensureIdentity`
|
||||
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
export async function ensureIdentity(): Promise<void>; // shared-wallet/access-gate.ts
|
||||
export interface SharedWalletConfig { fileUrl: string; password: string; importUrl?: string }
|
||||
```
|
||||
|
||||
One call, before the application renders. It resolves the identity from the URL (`?ng-id=`), failing that from browser storage, and only if neither answers does it show a barrier: download the shared wallet, here is its password, import it once, and name your space.
|
||||
|
||||
### Target
|
||||
|
||||
**NO COUNTERPART in substance, and a surviving CALL SITE — this section exists because those two are not the same thing.**
|
||||
|
||||
The substance is pure scaffolding. Every step it performs exists only because one wallet hosts several identities: upstream a user opens THEIR wallet, it contains THEIR site (`SensitiveWalletV0.personal_identity()`, `engine/wallet/src/types.rs:576-579`), and `session_start(wallet_name, user_id)` takes an id that came FROM the wallet. There is nothing to name and nothing to choose. The step that takes an identifier is the one that inverts the model, and it is the reason the whole gate is scaffolding.
|
||||
|
||||
The call site is a different matter. An application still has to wait for a session before it renders, and that will still be one awaited call at the same place. So the signature was designed to survive: **it takes no identifier**, deliberately — naming one is the part that disappears, so it must not appear in the parameters. The day the wallet supplies the identity, `ensureIdentity` resolves without showing anything and the caller's line is unchanged.
|
||||
|
||||
What a consumer must NOT conclude:
|
||||
|
||||
- that it may pass an identity in (it cannot — that is the point);
|
||||
- that the barrier is a product screen. It is a technical gate, rendered in plain DOM inside a shadow root so no application stylesheet reshapes it and its own leaks nowhere. It is deliberately not bound to a UI framework: a screen that is going away must not make every consumer adopt one.
|
||||
- that `SharedWalletConfig` describes a user setting. It describes what a DEPLOYMENT hands out, and it disappears with the gate. The library reads no environment variable, ever — the application resolves these values at its own build and passes them.
|
||||
|
||||
Why it lives here and not in the consumer application: the first consumer had ~300 lines of it — a gate component, a screen, a wallet module, an identity context, three BDD features. That is code an application would have to delete, and worse, code that teaches its authors a model NextGraph does not have (*"I name my identity"*).
|
||||
|
||||
---
|
||||
|
||||
## 3. The `ng` object
|
||||
|
||||
### Today — `@ng-eventually/sdk`
|
||||
@@ -446,20 +469,25 @@ The `CapRegistry` class itself is machinery (the in-memory record of what the co
|
||||
|
||||
## 11. NURI and SPARQL string utilities
|
||||
|
||||
### Today — `@ng-eventually/sdk`
|
||||
### Today — nothing. The entry publishes **no** string utility and **no** type guard.
|
||||
|
||||
```ts
|
||||
// sparql.ts:32,66,95
|
||||
export function escapeLiteral(value: string): string;
|
||||
export function escapeIri(value: string): string;
|
||||
export function assertNuri<T extends string>(nuri: T): T;
|
||||
// NOT published — internal, and each for a stated reason:
|
||||
// surface/sparql.ts escapeLiteral, escapeIri, assertNuri
|
||||
// model/nuri.ts isNuri, hasReadCap, targetOf, parseNuri, toNuri, mintCap
|
||||
```
|
||||
|
||||
(`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.)
|
||||
Two decisions meet here, and both point the same way.
|
||||
|
||||
**No guard, because the doors validate.** Every public entry takes `NuriLike` (`Nuri | string`) and runs `toNuri` itself — permissive in, precise out. A consumer holding a string from storage, a URL or a form passes it straight in; publishing a guard would invite the cast the types exist to prevent, and would put validation in the caller's hands where the door already does it.
|
||||
|
||||
**No `mintCap`, ever.** Nothing on the surface may turn a bare reference into a cap — that is the model's central invariant (§ 0 of `readcap-and-nuri-model.md`), so the function that could is unreachable from outside.
|
||||
|
||||
The escaping helpers were published until the surface was narrowed. Their removal costs a consumer nothing it will miss: they are generic injection-safety utilities, and neither `@ng-org/web` nor the ORM exposes an equivalent (re-verified against `index.d.ts` and `sdk/js/orm/src` — the engine escapes ad hoc where it builds SPARQL, e.g. `update_header`, `engine/verifier/src/request_processor.rs:196-208`). An application that interpolates SPARQL writes its own two-line escaper, against this lib or the real SDK alike.
|
||||
|
||||
### Target
|
||||
|
||||
**NO COUNTERPART at any level, and none expected.** Neither `@ng-org/web` nor the ORM exposes SPARQL escaping helpers (re-verified against `index.d.ts` and `sdk/js/orm/src`); the engine does its own ad-hoc literal escaping internally where it builds SPARQL (e.g. `update_header`, `engine/verifier/src/request_processor.rs:196-208`). These are generic injection-safety utilities, not SDK anticipation: they stay useful to any app that builds SPARQL by interpolation, against this lib or the real SDK. Nothing to unlearn; also nothing that migration replaces.
|
||||
**NO COUNTERPART at any level, and none expected** — which is precisely why none of it is published: a symbol with no successor, on a surface that promises one, is the thing this document exists to catch.
|
||||
|
||||
---
|
||||
|
||||
@@ -467,14 +495,15 @@ export function assertNuri<T extends string>(nuri: T): T;
|
||||
|
||||
### 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/sdk`** and is covered by `docs/internal-contract.md`. The signatures below are kept for the record, marked accordingly.
|
||||
> **Narrowed twice.** 2026-08-03 the entry stopped re-exporting the whole `store-registry` module and kept an app-facing slice (`src/surface/placement.ts`). 2026-08-05 that slice lost its two inbox-ADDRESS functions as well: an application deposits with `inbox.postToDocument(doc, …)` and shares with `inbox.share(doc, toUser)` — always naming a document or a person, never an address, because upstream an address is resolved from a profile and never handled by a caller. **Five functions remain published**, listed first below; everything after them is kept for the record and is covered by `docs/internal-contract.md`.
|
||||
|
||||
```ts
|
||||
// types.ts:38 — NB: NOT the ORM's Scope (a graphs/subjects filter); this is the store scope
|
||||
export type Scope = "public" | "protected" | "private";
|
||||
|
||||
// store-registry.ts:90,234
|
||||
export interface VirtualUserRecord {
|
||||
// VirtualUserRecord is INTERNAL (shape kept here for the ruling below).
|
||||
interface VirtualUserRecord {
|
||||
id: string;
|
||||
docPublic: Nuri;
|
||||
docProtected: Nuri;
|
||||
@@ -487,29 +516,18 @@ export interface RegistrySession {
|
||||
publicStoreId?: string;
|
||||
}
|
||||
|
||||
// consumer-facing, designed to survive migration (store-registry.ts:917, 1358, 1079, 735, 698)
|
||||
// PUBLISHED — the whole `storeRegistry` namespace, and nothing else.
|
||||
export async function createEntityDoc(id: string, scope: Scope): Promise<Nuri>;
|
||||
export async function listMyEntityDocs(id: string, scope: Scope): Promise<Nuri[]>;
|
||||
export async function userStoreDoc(id: string, scope: Scope): Promise<Nuri>;
|
||||
export async function resolveScopeGraph(scope: Scope): Promise<Nuri>;
|
||||
export async function resolveWriteGraph(id: string, scope: Scope): Promise<Nuri>;
|
||||
|
||||
// inbox-side (store-registry.ts:772, 837, 1133, 1203, 1286)
|
||||
export async function userInbox(id: string): Promise<Nuri>;
|
||||
export async function isOwnInbox(nuri: Nuri): Promise<boolean>;
|
||||
export async function openDocumentInbox(doc: Nuri): Promise<Nuri>;
|
||||
export async function documentInboxAddress(doc: Nuri): Promise<Nuri | undefined>;
|
||||
export async function myInboxes(): Promise<Nuri[]>;
|
||||
|
||||
// User-branch registers (store-registry.ts:1307, 1331)
|
||||
export async function addLink(cap: ReadCap): Promise<void>;
|
||||
export async function readLinks(): Promise<ReadCap[]>;
|
||||
|
||||
// shim machinery (store-registry.ts:542, 631, 213, 278)
|
||||
export async function resolveAccount(id: string): Promise<VirtualUserRecord | null>;
|
||||
export async function ensureAccount(id: string): Promise<VirtualUserRecord>;
|
||||
export function reservedAccount(name: string): string;
|
||||
export function resetRegistryCache(): void;
|
||||
// NOT published — internal, kept here because the target rulings below still cover them.
|
||||
// userStoreDoc, userInbox, documentInboxAddress, isOwnInbox, myInboxes,
|
||||
// addLink, readLinks, resolveAccount, ensureAccount, reservedAccount,
|
||||
// resetRegistryCache, and the VirtualUserRecord type.
|
||||
// `RegistrySession` IS published: a consumer types its injected `getSession` with it.
|
||||
```
|
||||
|
||||
### Target — split by what each piece maps to
|
||||
@@ -530,25 +548,16 @@ export function resetRegistryCache(): void;
|
||||
### Today
|
||||
|
||||
```ts
|
||||
// @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;
|
||||
setItem(key: string, value: string): void;
|
||||
removeItem(key: string): void;
|
||||
}
|
||||
export class IdentityStore { // :37
|
||||
constructor(storage: VirtualUserStorage | null, key?: string);
|
||||
get(): string | null;
|
||||
set(id: string): string | null;
|
||||
clear(): void;
|
||||
}
|
||||
export function browserIdentityStore(key?: string): IdentityStore; // :89
|
||||
|
||||
// @ng-eventually/sdk — the POLYFILL-ERA block
|
||||
// PUBLISHED — the POLYFILL-ERA block of `src/index.ts`, both with no counterpart.
|
||||
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
|
||||
export async function connectedUser(): Promise<void>; // emulated-verifier/connect.ts
|
||||
|
||||
// NOT published (removed 2026-08-05) — identity persistence is the application's job
|
||||
// upstream too, and asking the library who you signed in is a shared-wallet convenience:
|
||||
// shared-wallet/virtual-users.ts IdentityStore, browserIdentityStore,
|
||||
// VirtualUserStorage, ACCOUNT_STORAGE_KEY
|
||||
// shared-wallet/bootstrap.ts getCurrentUser
|
||||
// The access gate persists what IT needs (§ 2bis); nothing else has to be exposed.
|
||||
```
|
||||
|
||||
### Target
|
||||
@@ -563,8 +572,8 @@ declare function user_connect(client_info: any, user_id: string, location?: stri
|
||||
declare function user_disconnect(user_id: string): Promise<void>;
|
||||
```
|
||||
|
||||
- `virtualUsers.*` (the persisted identity id) — **NO COUNTERPART**; removed at migration (`docs/migration-guide.md` § 5). It exists only because every virtual user shares one wallet. It is exported from the SDK entry, which is a placement wart (§ 15).
|
||||
- `setCurrentUser` / `getCurrentUser` — **NO COUNTERPART**; the relay of an identity the broker cannot see. Disappears with the shared wallet.
|
||||
- `IdentityStore` / `browserIdentityStore` (the persisted identity id) — **NO COUNTERPART**; they exist only because every virtual user shares one wallet, and they are no longer published at all. Removed at migration (`docs/migration-guide.md` § 5).
|
||||
- `setCurrentUser` — **NO COUNTERPART**; the relay of an identity the broker cannot see. Disappears with the shared wallet. `getCurrentUser` was its read side and is gone from the surface: an application knows who it signed in.
|
||||
- `connectedUser()` — the awaitable form of what the target does **automatically**: the recipient's verifier processes its inbox as messages arrive/at connection (`Verifier::inbox`, `engine/verifier/src/verifier.rs:1674`). VERIFIED at level 1 that no consumer call is needed upstream; the polyfill fires it from `setCurrentUser` for the same reason. A consumer should treat it as "await a deterministic start" (tests), not as an operation the future SDK will name.
|
||||
|
||||
---
|
||||
|
||||
@@ -36,10 +36,9 @@ export {
|
||||
resolveScopeGraph,
|
||||
/** The NURI where GROUPED entities of `scope` are written (no per-entity document). */
|
||||
resolveWriteGraph,
|
||||
/** Open an inbox on a document you OWN, so others can deposit into it. */
|
||||
/** WHERE to deposit for a document — readable by any holder of it. `undefined` if none. */
|
||||
} from "../shared-wallet/account-registry";
|
||||
|
||||
/** Open an inbox on a document you OWN, so others can deposit into it. */
|
||||
export { openDocumentInbox } from "../emulated-verifier/branch-registers";
|
||||
|
||||
// No `linkTo` here, and its absence is deliberate (it existed 2026-08-06, one day).
|
||||
|
||||
@@ -172,6 +172,48 @@ test("a reserved-namespace key cannot be produced by a consumer's normalizeId",
|
||||
|
||||
// --- the contract's inventory must match the code ------------------------
|
||||
|
||||
/**
|
||||
* Every `export` the contract SHOWS in a "### Today" block must actually be exported.
|
||||
*
|
||||
* ── Why this exists beside the appendix check ─────────────────────────────
|
||||
* The appendix check pins the NAMES. It cannot see the rulings — the per-subject
|
||||
* sections where each symbol gets its epistemic label — and on 2026-08-07 three of them
|
||||
* had drifted without anything going red: § 11 documented `escapeLiteral` / `assertNuri`
|
||||
* as published (they are not), § 12 listed seven `storeRegistry` functions (there are
|
||||
* five), § 13 listed `IdentityStore` / `getCurrentUser` (removed two days earlier). A
|
||||
* reader trusting the sections was reading the surface of a fortnight ago.
|
||||
*
|
||||
* A stale ruling is worse than a missing one: it reads as verified. So the sections are
|
||||
* held to the same standard as the appendix — if a block shows `export function X`, X is
|
||||
* exported. Anything kept for the record goes in a comment, which this check ignores by
|
||||
* construction (it only looks at lines beginning with `export`).
|
||||
*/
|
||||
test("every `export` shown in a contract '### Today' block is actually exported", () => {
|
||||
const md = fs.readFileSync(
|
||||
path.join(import.meta.dir, "..", "..", "..", "docs", "api-contract.md"),
|
||||
"utf8",
|
||||
);
|
||||
const published = new Set(publishedNames());
|
||||
const stale: string[] = [];
|
||||
// Sections run from a "### Today" heading to the next heading of any level.
|
||||
for (const m of md.matchAll(/### Today[^\n]*\n([\s\S]*?)(?=\n#{2,3} )/g)) {
|
||||
for (const block of m[1]!.matchAll(/```ts\n([\s\S]*?)```/g)) {
|
||||
for (const line of block[1]!.split("\n")) {
|
||||
const decl = line.match(
|
||||
/^export (?:declare )?(?:async )?(?:const|function|class|interface|type) (\w+)/,
|
||||
);
|
||||
// Namespace members (`inbox.post`, `storeRegistry.createEntityDoc`) are exported
|
||||
// from their module, not from the entry — the same allowance the appendix makes.
|
||||
const name = decl?.[1];
|
||||
if (name && !published.has(name) && !isNamespaceMember(name)) stale.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
// A failure is a question, not a rename: has the symbol been removed (then say so in a
|
||||
// comment and rule on why), or has the entry lost something it should still publish?
|
||||
expect([...new Set(stale)]).toEqual([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* Read the appendix of `docs/api-contract.md` back into `{ group: names }`.
|
||||
* The appendix is a generated block; this parses the same shape.
|
||||
|
||||
Reference in New Issue
Block a user