Files
ng-eventually/packages/polyfill/test/provenance.test.ts
Sylvain Duchesne 43aadbeb45 docs: chaque symbole dit d'où il vient
98 annotations posées à côté des déclarations, et un test qui les exige sur la
surface publiée. Elles portent trois choses : le niveau qui répond, la référence
amont, et la catégorie parmi les cinq.

La cinquième est celle qui manquait : declared-not-wired, quand la cible DÉFINIT
la forme et ne la câble pas. Neuf symboles en relèvent, dont readLinks — que
j'avais classé « notre invention » en raisonnant depuis l'absence, alors que
c'est le meilleur alignement disponible.

Les références citent un SYMBOLE, jamais une ligne : trois citations du document
avaient déjà pourri. Cinq corrections au passage, toutes vérifiées à la source —
un chemin ORM qui n'existe pas, deux plages de lignes fausses, et surtout
docs.* et subscribeDoc étiquetés PASSTHROUGH alors qu'ils sont alignés : nos
noms, plus un argument jamais transmis. La sémantique survit à la migration,
les sites d'appel non, et la nuance disparaissait sous une étiquette trop
flatteuse.

Le test échoue à l'annotation retirée, à la catégorie mal orthographiée, et à
une invention qui prétendrait citer une référence — vérifié en cassant les
trois. Il a aussi attrapé un défaut en lui-même : le gabarit de format placé
dans index.ts se faisait analyser comme une annotation.

La classification couvre l'interne qui prétend ressembler à la cible — tout
emulated-verifier — et exclut ce qui ne le prétend pas. La faute d'origine
portait sur une fonction non exportée ; n'être pas publié n'a protégé personne.

Quatre symboles ont résisté et sont annotés avec leur catégorie dominante, la
seconde nommée dans la note plutôt que lissée.
2026-08-16 22:53:50 +02:00

203 lines
10 KiB
TypeScript

/**
* Every published symbol must SAY what it is aligned on — in a machine-checkable line
* beside the code, not in a document that drifts away from it.
*
* ── Why this is a test, and why the line lives in the source ───────────────
* The library's governing rule is that nothing may diverge from the target. Nothing in
* the code recorded *what each symbol is aligned on*, and it cost twice in one week:
* `docs/api-contract.md` claimed a "1:1 passthrough" that was false in two ways, and a
* maintainer classified `branch-registers.readLinks` as this library's own invention when
* it implements `AddLinkV0` — a type NextGraph **defines** (`engine/repo/src/types.rs`)
* and constructs nowhere. Reading "nothing constructs this upstream" and concluding "so
* it is ours" is inference from an ABSENT IMPLEMENTATION, which the project's own rule
* forbids: a definition is a fact, its being unwired is an absence, and the absence says
* nothing about the target.
*
* So the provenance is recorded per symbol, next to it, and this test pins it.
*
* ── The format ────────────────────────────────────────────────────────────
*
* // @provenance <name> kind=<kind> level=<1|2|3|none> ref=<path:symbol|none> — <note>
*
* - **name** — the PUBLISHED name, qualified for a namespace member (`inbox.post`,
* `storeRegistry.createEntityDoc`). Internal symbols carry their own name. It is in
* the line so this file can map an annotation to a symbol without a TypeScript parser.
* - **level** — which layer of the target answers, numbered from the bottom like the
* stack: `1` the engine (`engine/`), `2` the wasm binding / `@ng-org/web`
* (`sdk/js/lib-wasm`, `sdk/js/web`), `3` the JS ORM (`sdk/js/orm`). `none` only when
* nothing answers.
* - **ref** — `path:symbol` into `nextgraph-rs` (a SYMBOL, never a line number: line
* numbers rot and three citations in `docs/api-contract.md` already had). `none` only
* for `invention`.
* - **kind** — from a closed set:
*
* | kind | meaning |
* |----------------------|----------------------------------------------------------------|
* | `passthrough` | upstream's own symbol, same name and shape |
* | `aligned` | our name or ergonomics, upstream's semantics and model |
* | `declared-not-wired` | upstream DEFINES the type or shape; nothing constructs it yet |
* | `invention` | nothing at any level answers — a deliberate, documented bet |
* | `divergent` | upstream does this, and we do it differently |
*
* ── What this test deliberately does NOT check ────────────────────────────
* That the `ref` path exists on disk. `nextgraph-rs` is a sibling checkout that may be
* absent on another machine, and a test that goes red for that reason teaches nothing —
* it just trains people to skip it. The claim is verified by a human reading the source;
* this file pins that a claim was MADE, is well-formed, and covers the whole surface.
*/
import { test, expect } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
const SRC = path.join(import.meta.dir, "..", "src");
const KINDS = new Set(["passthrough", "aligned", "declared-not-wired", "invention", "divergent"]);
const LEVELS = new Set(["1", "2", "3", "none"]);
/** `path/to/file.ext:Symbol` — a path and a symbol, never a line number. */
const REF_SHAPE = /^[A-Za-z0-9_.@/-]+\.[a-z]+:[A-Za-z0-9_]+$/;
interface Provenance {
name: string;
kind: string;
level: string;
ref: string;
file: string;
}
/**
* The names a module really EXPORTS — declarations and named re-exports, comments
* stripped first so a name that only appears in prose (an explained removal, a
* `{@link}`) is not mistaken for a live export. Same rule as `vocabulary.test.ts`,
* for the same reason: a gate that says yes to a name that is not there reads as
* verified while proving nothing.
*/
function moduleExports(file: string): Set<string> {
const out = new Set<string>();
if (!fs.existsSync(file)) return out;
const text = fs
.readFileSync(file, "utf8")
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/^\s*\/\/.*$/gm, "");
for (const m of text.matchAll(
/^export (?:declare )?(?:async )?(?:const|function|class|interface|type) (\w+)/gm,
)) {
out.add(m[1]!);
}
for (const m of text.matchAll(/export (?:type )?\{([^}]*)\}/g)) {
for (const raw of m[1]!.split(",")) {
const name = raw.trim().replace(/^type /, "").split(/\s+as\s+/).pop()?.trim();
if (name) out.add(name);
}
}
return out;
}
/** namespace name → the names it really carries (`export * as ns from "./x"`). */
function namespaces(): Map<string, Set<string>> {
const text = fs.readFileSync(path.join(SRC, "index.ts"), "utf8");
const out = new Map<string, Set<string>>();
for (const m of text.matchAll(/export \* as (\w+) from "\.\/([^"]+)"/g)) {
out.set(m[1]!, moduleExports(path.join(SRC, m[2]! + ".ts")));
}
return out;
}
/**
* Every symbol the entry publishes, as a consumer names it: bare for a direct export,
* `ns.member` for a namespace member. The namespace NAMES themselves are groupings, not
* symbols — a consumer never holds `docs`, it calls `docs.docCreate` — so they carry no
* provenance of their own and are excluded.
*/
function publishedSymbols(): string[] {
const ns = namespaces();
const out = new Set<string>();
for (const name of moduleExports(path.join(SRC, "index.ts"))) {
if (!ns.has(name)) out.add(name);
}
for (const [name, members] of ns) for (const m of members) out.add(`${name}.${m}`);
return [...out].sort();
}
/** Every `@provenance` line in the source tree, parsed. */
function annotations(): Provenance[] {
const out: Provenance[] = [];
const walk = (dir: string): void => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walk(full);
else if (entry.name.endsWith(".ts")) {
for (const line of fs.readFileSync(full, "utf8").split("\n")) {
// Anchored to a REAL annotation line — a comment that IS the annotation, not
// one that quotes the format. `src/index.ts` documents the shape in its header
// (a `// @provenance <name> kind=<kind> …` template nested inside a `//` block);
// an unanchored match read that template as a symbol called `<name>` with a kind
// called `<kind>`, and the well-formedness check went red on the documentation
// of its own format. A `<…>` name is skipped for the same reason, belt and
// braces: a placeholder is never a symbol.
const m = line.match(
/^\s*\/\/ @provenance\s+(\S+)\s+kind=(\S+)\s+level=(\S+)\s+ref=(\S+)/,
);
if (m && !m[1]!.startsWith("<")) {
out.push({
name: m[1]!,
kind: m[2]!,
level: m[3]!,
ref: m[4]!,
file: path.relative(SRC, full),
});
}
}
}
}
};
walk(SRC);
return out;
}
test("every published symbol carries a provenance annotation", () => {
const annotated = new Set(annotations().map((a) => a.name));
const missing = publishedSymbols().filter((s) => !annotated.has(s));
// A failure is a question, not a formality: on what does this symbol align? Read the
// nextgraph-rs source and say so — including "nothing answers", which is `invention`.
expect(missing).toEqual([]);
});
test("no symbol is annotated twice", () => {
const seen = new Map<string, string[]>();
for (const a of annotations()) seen.set(a.name, [...(seen.get(a.name) ?? []), a.file]);
const duplicated = Object.fromEntries([...seen].filter(([, files]) => files.length > 1));
// Two annotations for one name is two answers to one question, and nothing says which
// is current — the drift this file exists to prevent, reintroduced inside it.
expect(duplicated).toEqual({});
});
test("every provenance annotation is well-formed", () => {
const malformed: string[] = [];
for (const a of annotations()) {
const where = `${a.file} :: ${a.name}`;
if (!KINDS.has(a.kind)) malformed.push(`${where} — kind '${a.kind}' is not one of ${[...KINDS].join(" | ")}`);
if (!LEVELS.has(a.level)) malformed.push(`${where} — level '${a.level}' is not 1 | 2 | 3 | none`);
if (a.kind === "invention") {
// Nothing answers, at any level. A ref here would name a counterpart the kind
// denies exists — the two halves have to agree or the label means nothing.
if (a.ref !== "none") malformed.push(`${where} — kind=invention must carry ref=none, not '${a.ref}'`);
if (a.level !== "none") malformed.push(`${where} — kind=invention must carry level=none, not '${a.level}'`);
} else {
// Every other kind CLAIMS a counterpart, so it must cite one.
if (a.ref === "none") malformed.push(`${where} — kind=${a.kind} claims a counterpart, so ref may not be 'none'`);
else if (!REF_SHAPE.test(a.ref)) malformed.push(`${where} — ref '${a.ref}' is not 'path:symbol' (a symbol, never a line number)`);
if (a.level === "none") malformed.push(`${where} — kind=${a.kind} claims a counterpart, so level must be 1, 2 or 3`);
}
}
expect(malformed).toEqual([]);
});
test("the published surface is annotated at every kind that claims a counterpart", () => {
// Not a quota — a smoke check that the closed set is actually being USED. If every
// published symbol ever lands on one kind, the vocabulary has stopped discriminating
// and the annotations have become decoration.
const published = new Set(publishedSymbols());
const kinds = new Set(annotations().filter((a) => published.has(a.name)).map((a) => a.kind));
expect(kinds.size).toBeGreaterThan(1);
});