refactor(vocabulary): les noms publiés parlent la langue de la cible, et un test le tient

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.
This commit is contained in:
Sylvain Duchesne
2026-08-04 14:35:01 +02:00
parent e01a8dbab1
commit 107f9d1633
28 changed files with 297 additions and 135 deletions
+3 -3
View File
@@ -163,7 +163,7 @@ This is the point where the emulation is furthest from the eventual target, wher
`inbox.read` had no guard and **absorbs caps into the reader's keyring**, so `inbox.read(someoneElsesInbox)` pocketed the caps addressed to them and directed sharing was defeatable by anyone who knew an inbox NURI. The inbox was never guarded before either, but before P1a it carried nothing that granted access.
Fixed in step 2 of [`2026-07-30-virtual-wallet-boundary.md`](2026-07-30-virtual-wallet-boundary.md): an inbox now BELONGS to a virtual user (`storeRegistry.walletInbox`), and only its owner may read it. Depositing into anyone's inbox stays open — that is the one legitimate cross-wallet act, and the only way a link crosses between wallets at all.
Fixed in step 2 of [`2026-07-30-virtual-wallet-boundary.md`](2026-07-30-virtual-wallet-boundary.md): an inbox now BELONGS to a virtual user (`storeRegistry.userInbox`), and only its owner may read it. Depositing into anyone's inbox stays open — that is the one legitimate cross-wallet act, and the only way a link crosses between wallets at all.
## Follow-up decided by the PO — to plan, NOT in this lot
@@ -192,8 +192,8 @@ 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` + `walletInbox`, 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 `walletInbox(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 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.
- **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