feat: ancre la version que les citations de provenance decrivent

Une citation nomme un chemin et un symbole, jamais une version. Or le
checkout contre lequel toutes les citations `engine/` et `sdk/` se
resolvent declare des versions inferieures a celles que ce depot
installe, donc une citation prouve "c'etait vrai a ces versions-la" la
ou un lecteur lit "c'est vrai de ce qu'on construit".

La version est une propriete du CHECKOUT, pas d'une citation : aucune
des annotations n'est touchee. Elle est enregistree une seule fois, dans
l'`ANCHOR` de provenance.test.ts, a cote du resolveur qui s'en sert.

Ce que le mecanisme verifie maintenant, aux DEUX bouts de l'ecart, parce
qu'un enonce d'ecart peut perimer par l'un ou l'autre et a deja perime
par le bout eloigne : les versions que le checkout declare (a vide quand
le checkout est absent, comme la verification de resolution), celles que
les manifestes de l'espace de travail epinglent, et celle qui est
reellement installee sous packages/polyfill.

Pas un nouveau `kind` : les kinds classent la preuve d'UN symbole,
l'anciennete de toute la preuve appartient a l'arbre.
This commit is contained in:
Sylvain Duchesne
2026-09-24 14:55:10 +02:00
parent 50a77e5a95
commit 4e5b703949
3 changed files with 146 additions and 1 deletions
+135
View File
@@ -236,6 +236,141 @@ function upstreamCheckout(): string | null {
return fs.existsSync(root) ? root : null;
}
/**
* -- WHICH VERSION a citation describes ------------------------------------
* A `ref` names a path and a symbol. It does not say WHEN, and the difference is not
* academic: the checkout every `engine/` and `sdk/` citation resolves against declares
* package versions BELOW the ones this repo builds against. So a citation proves "this
* was true at the versions on the left below", while a reader takes it for "this is true
* of what we build against".
*
* The version is a property of the CHECKOUT, not of a citation. Every `engine/` and
* `sdk/` citation resolves against ONE tree, so carrying the version on each of them
* would state one fact many times over and get it wrong everywhere at once the day the
* tree moves. It is recorded ONCE, here, beside the resolver that uses it.
*
* What a reader needs is not a version but the GAP, and a gap has two ends. Both are
* checked below, because this statement went stale from the FAR end once already: the
* packages moved and the checkout, which cannot follow, was left describing releases
* nothing here installs any more.
*
* The commit that declares the left column is `213338f697df15afc086c042f4bab1bffd01aa04`
* - a pointer for `git show`, not the enforced identity. What is enforced is the
* VERSIONS, because they are what a citation's strength depends on: a tree at another
* commit declaring the same releases describes the same code.
*
* `sdk/js/lib-wasm` carries no `package.json` - it is the Rust crate that BUILDS the
* binding, and what ships is `@ng-org/web`, so it is anchored by that row. The gap is
* not cosmetic there: the checkout declares `orm_start_graph` with FIVE parameters,
* while `@ng-org/web@0.1.2-alpha.14` ships it with SIX, `config` at index 4. A citation
* into `sdk/js/lib-wasm` therefore cannot settle what the binding in this build does.
* `engine/` has no published version at all and is anchored by the commit alone, which
* is exactly why reading it cannot refute a claim about a shipped release.
*
* Deliberately NOT a new `kind`. The kinds classify what evidence ONE symbol has; how
* old ALL the evidence is belongs to the tree, not to any symbol standing in it.
*/
const ANCHOR = [
{ pkg: "@ng-org/orm", dir: "sdk/js/orm", cited: "0.1.2-alpha.18", built: "0.1.2-alpha.21" },
{ pkg: "@ng-org/web", dir: "sdk/js/web", cited: "0.1.2-alpha.13", built: "0.1.2-alpha.14" },
{ pkg: "@ng-org/shex-orm", dir: "sdk/js/shex-orm", cited: "0.1.2-alpha.8", built: "0.1.2-alpha.9" },
{
pkg: "@ng-org/alien-deepsignals",
dir: "sdk/js/alien-deepsignals",
cited: "0.1.2-alpha.11",
built: "0.1.2-alpha.13",
},
] as const;
/** The `version` a manifest declares, or `null` when there is no manifest there. */
function declaredVersion(manifest: string): string | null {
if (!fs.existsSync(manifest)) return null;
const parsed: unknown = JSON.parse(fs.readFileSync(manifest, "utf8"));
const version = (parsed as { version?: unknown }).version;
return typeof version === "string" ? version : null;
}
/** Every workspace manifest, read from disk so a NEW package cannot escape the check. */
function workspaceManifests(): string[] {
const out: string[] = [];
for (const group of ["packages", "examples"]) {
const dir = path.join(REPO, group);
if (!fs.existsSync(dir)) continue;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const manifest = path.join(dir, entry.name, "package.json");
if (entry.isDirectory() && fs.existsSync(manifest)) out.push(manifest);
}
}
return out;
}
/** An EXACT pin - a range says which versions are admissible, a pin says which one runs. */
function exactPins(manifest: string): Map<string, string> {
const parsed: unknown = JSON.parse(fs.readFileSync(manifest, "utf8"));
const out = new Map<string, string>();
for (const field of ["dependencies", "devDependencies"]) {
const deps = (parsed as Record<string, unknown>)[field];
if (typeof deps !== "object" || deps === null) continue;
for (const [name, spec] of Object.entries(deps as Record<string, unknown>)) {
if (typeof spec === "string" && /^\d/.test(spec)) out.set(name, spec);
}
}
return out;
}
test("the anchor still names the versions the cited checkout declares", () => {
const root = upstreamCheckout();
// Absent, this passes empty-handed for the same reason `deadCitations` does: a red that
// only means "the sibling is not here" teaches nothing and trains people to skip.
const moved =
root === null
? []
: ANCHOR.flatMap((a) => {
const found = declaredVersion(path.join(root, a.dir, "package.json"));
// Red here means the citations have STOPPED describing what the anchor says
// they describe. The repair is to re-read the gap, not to edit a citation.
return found === a.cited
? []
: [`${a.pkg} - the checkout declares ${found ?? "no version"}, the anchor says ${a.cited}`];
});
expect(moved).toEqual([]);
});
test("the anchor still names the versions this repo builds against", () => {
// Checked unconditionally: the far end of the gap is in this working copy, never the
// absent sibling - and it is the end that went stale last time. An exact pin that
// disagrees with the anchor makes the anchor's right-hand column a false statement.
const built = new Map<string, string>(ANCHOR.map((a) => [a.pkg, a.built] as const));
const wrong: string[] = [];
const pinned = new Set<string>();
for (const manifest of workspaceManifests()) {
const where = path.relative(REPO, manifest);
for (const [name, spec] of exactPins(manifest)) {
const expected = built.get(name);
if (expected === undefined) continue;
pinned.add(name);
if (spec !== expected) wrong.push(`${where} pins ${name}@${spec}, the anchor says ${expected}`);
}
}
// A gap statement nothing pins is vacuous: it would stay green while the fact it
// describes disappeared from the tree entirely.
for (const a of ANCHOR) if (!pinned.has(a.pkg)) wrong.push(`${a.pkg} - the anchor names it, no workspace manifest pins it`);
expect(wrong).toEqual([]);
});
test("what the polyfill really resolves is what the anchor calls the build", () => {
// "Builds against" has to mean the installed tree, not the manifest. The two have
// parted company here before: every `@ng-org` symlink pointed into a `bun` store that
// still held the previous releases, while the manifests read as current.
const wrong = ANCHOR.flatMap((a) => {
const manifest = path.join(REPO, "packages", "polyfill", "node_modules", a.pkg, "package.json");
const found = declaredVersion(manifest);
if (found === null) return [`${a.pkg} - not installed under packages/polyfill`];
return found === a.built ? [] : [`${a.pkg} - ${found} is installed, the anchor says ${a.built}`];
});
expect(wrong).toEqual([]);
});
/** `path:symbol`, split on the LAST colon — a path may carry one, a symbol never does. */
function splitRef(ref: string): { file: string; symbol: string } {
const at = ref.lastIndexOf(":");