L'appendice « inventaire des exports pour diff » d'`api-contract.md` était
périmé : il listait encore les internes du shim dans le namespace
`storeRegistry` alors que l'entrée avait été réduite à sept fonctions. Or c'est
précisément l'instrument qu'on diffe quand la surface bouge — et un inventaire
périmé est pire qu'aucun, il se lit comme vérifié.
Régénéré depuis les `export`, et désormais tenu par `vocabulary.test.ts` : si la
liste et le code divergent, le test échoue. Le document suit le code au lieu de
dériver.
Au passage, le contrôle de vocabulaire suit maintenant `export * from`, ce qui
lui a fait voir trois types qu'il ignorait — d'où le suffixe structurel `…Like`
(`NgLike` = « ce qui a la forme de ng ») déclaré comme de la glue de typage et
non un mot de domaine.
162 tests unitaires, typecheck src/test/e2e vert.
Tranché par la cascade plutôt qu'en attendant une réponse : le niveau 3 (ORM) ne
dit rien des inbox, le niveau 2 non plus — `@ng-org/web` n'expose aucune méthode
contenant « inbox » et la session n'en publie aucune. C'est donc le modèle du
moteur qui décide, et il dit DEUX : un site porte une inbox sur son repo de store
public et une autre sur son protégé (`engine/verifier/src/site.rs:127-152`), les
seuls `AddInboxCap` du moteur, `new_store_default` n'en posant une que
`if !private`. Elles sont adressées séparément jusque dans les enregistrements de
contact, qui choisissent leur prédicat selon le profil visé — `ng:site_inbox` pour
un profil public, `ng:protected_inbox` sinon
(`engine/verifier/src/inbox_processor.rs:787,823-824`).
`userInbox(id)` en exposait une : une cardinalité que cette bibliothèque avait
inventée, et que le nom `walletInbox` avait contribué à masquer. Elle prend
désormais le scope, et le store PRIVÉ n'en a pas — d'où `InboxScope` plutôt que
`Scope` : demander l'inbox privée n'est pas une recherche qui ne rend rien, c'est
une question sans référent dans le modèle, et le type l'interdit.
`myInboxes` énumère les deux, `isOwnInbox` reconnaît les deux. Le shim garde un
triple par (user, scope).
160 tests unitaires, typecheck src/test/e2e vert, e2e 40/40 contre le broker.
La correction de nomenclature du 2026-07-30 — en amont un *wallet* n'est qu'un
trousseau, ce qui possède des stores est un **user** (un *site*) — s'était faite
à la main. `walletInbox` y a échappé et a vécu des semaines, en faisant des
dégâts : le nom rendait « une inbox par wallet » évident, masquant qu'un user en
a **deux** en amont (repos de store public et protected, les deux seuls
`AddInboxCap` du moteur). Une discipline appliquée à la main en oublie un ; un
test non.
D'où `test/vocabulary.test.ts` : tout nom publié est bâti sur des mots que la
CIBLE emploie — vérifiés dans `nextgraph-rs` — ou porte un marqueur disant
POURQUOI il n'existe qu'ici (`virtual`, `physical`, `shim`, `emulated`,
`polyfill`), ce qui dit aussi quand il disparaît. Un échec n'est pas « renommer
pour faire passer le test », c'est une question : la cible a-t-elle un mot pour
ça ? la chose n'existe-t-elle qu'ici ? le mot est-il vraiment de la glue ?
Ce que le test a trouvé, et les réponses :
- `walletInbox` → `userInbox`, avec l'écart de cardinalité écrit noir sur blanc
plutôt que caché par le nom.
- `accounts` / `AccountRecord` / `AccountStorage` → `virtualUsers` /
`VirtualUserRecord` / `VirtualUserStorage`, module `accounts.ts` →
`virtual-users.ts`. « account » n'est pas de la cible : c'est notre mot pour
l'utilisateur virtuel, et le marqueur le dit désormais.
- `readModel` → la fonction `readUnion`, exposée directement. « model » n'était
ni de la cible ni de la glue, et le namespace ne tenait qu'une fonction.
- Le reste était du vocabulaire légitime à déclarer (`subject`, `base`,
`schema`, `connected`, le modèle réactif de l'ORM).
Corrigé au passage, sur signalement du contrat interne : l'en-tête d'`open-repo`
justifiait son correctif par un mécanisme que le source contredit. Un repo absent
de `self.repos` lève bien `RepoNotFound`
(`engine/verifier/src/request_processor.rs:264,269`). Les 0 lignes observées
viennent d'ailleurs — `Verifier::load` repeuple `self.repos` depuis le stockage
sur un profil persistant (`verifier.rs:535-560`), et notre propre `readDoc`
attrape toute erreur et rend `[]`. Le correctif est bon, le diagnostic écrit à
côté ne l'était pas.
159 tests unitaires, typecheck src/test/e2e vert, e2e 40/40 contre le broker.
Les deux contrats et la doctrine portent des centaines de citations `file:line`
vers `src/`. Le rangement par destin les périmait en bloc — 208 reprises (45
chemins `src/x.ts`, 163 mentions nues). Sans ça la réorganisation pourrissait
l'instrument même qui tient la discipline qu'elle sert.
Et un défaut relevé par le contrat interne, vérifié : `ng-proxy` fabriquait un
membre `login`. `@ng-org/web` n'expose aucune méthode de ce nom — zéro
occurrence dans les déclarations installées comme dans `sdk/js/lib-wasm/src/lib.rs`
— mais le proxy répondait une fonction au lieu d'`undefined`, laquelle plantait
à l'appel. C'était le seul endroit où ce wrapper ajoutait à la surface du SDK,
contre son propre en-tête.
157 tests unitaires, typecheck src/test/e2e vert.
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.