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:
Sylvain Duchesne
2026-08-07 11:51:24 +02:00
parent aa6bbc436e
commit b98fcaa77d
3 changed files with 110 additions and 60 deletions
+42
View File
@@ -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.