From 4e5b70394926a18cb1d9e1e4236e1d2f7a9f051b Mon Sep 17 00:00:00 2001 From: Sylvain Duchesne Date: Thu, 24 Sep 2026 14:55:10 +0200 Subject: [PATCH] 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. --- .../nextgraph/no-divergence-from-nextgraph.md | 4 + packages/polyfill/src/index.ts | 8 +- packages/polyfill/test/provenance.test.ts | 135 ++++++++++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) diff --git a/.project/rules/nextgraph/no-divergence-from-nextgraph.md b/.project/rules/nextgraph/no-divergence-from-nextgraph.md index 4596213..06ecdab 100644 --- a/.project/rules/nextgraph/no-divergence-from-nextgraph.md +++ b/.project/rules/nextgraph/no-divergence-from-nextgraph.md @@ -47,6 +47,10 @@ Answering *does the target do this?* once, in conversation, does not survive the | `invention` | nothing at any level answers — a deliberate, documented bet | | `divergent` | upstream does this, and we do it differently | +**A citation names a symbol, not a version - and the checkout is behind.** Every citation into `engine/` and `sdk/` resolves against one sibling checkout, which declares older releases than the packages this repo installs. So a citation proves *"this was true at the cited releases"*, never *"this is true of what we build against"*. The version is a property of that checkout, not of a citation, so it is recorded **once** - the `ANCHOR` in `packages/polyfill/test/provenance.test.ts` - and checked at **both** ends: the releases the checkout declares, and the ones the workspace pins and installs. Both, because a gap statement goes stale from either end, and it went stale from the far one already: the packages moved and the checkout, which cannot follow, was left describing releases nothing here installs. + +The consequence is a rule about evidence, and it has already been broken here: **a claim about a shipped release cannot be refuted by reading the checkout.** Doing so refutes it against the older releases instead, which is a different statement. The tell is a sentence of the form *"the published package declares X, but upstream `foo()` still does Y"* - `foo()` at the cited release, and that says nothing about the release in the build. Worse, the usual shape of Y is an ABSENCE, and inferring from an absent implementation is what the top of this rule forbids: the pinned engine building its ORM config from `json!({})` behind a `// TODO` was read as *"the engine discards the config"* and written into two leaves, when the declaration it feeds is complete, every field of it is consumed downstream, and the only missing hop is a binding parameter the shipped binding already has. Settle a claim about a release against that release's own shipped artifact, and say plainly when the artifact cannot settle it. + A test requires the annotation on every published symbol; it cannot check that the citation is *true*, and that is fine — a citation is refutable by a reader in seconds, prose is not. It does check that the citation still **resolves**, which a reader cannot: into `nextgraph-rs` when that sibling checkout is present — absent, it asks nothing rather than going red for a reason that would only teach people to skip the suite — and into this working copy always, for the one kind whose record lives here. The classification also applies to unpublished code that **claims to resemble the target**: the mistake this section exists for was made on an internal function, and being unexported protected nobody. **An announced target has no file to cite, and that is not a reason to call it ours.** `announced-not-written` covers the case the other kinds leave out: the developers have said what the direction is, and the tree carries nothing yet — no definition, not even a name. It is the **strongest** justification available there, because deviating *toward* what the target has announced is alignment, and it is the rest of the surface that will have to catch up. The two moves it replaces are both bad: citing an approximate symbol, which is the `readUnion` failure — a true citation attached to a false claim — or declaring `invention`, which understates the alignment and tells a later reader the shape is ours to change when it is not. diff --git a/packages/polyfill/src/index.ts b/packages/polyfill/src/index.ts index fb71027..94c693b 100644 --- a/packages/polyfill/src/index.ts +++ b/packages/polyfill/src/index.ts @@ -62,8 +62,14 @@ // it differently). Pinned by `test/provenance.test.ts`; the reasoning behind each ruling // stays in `docs/api-contract.md`, and where the two disagree the annotation wins. // +// A `ref` names a path and a symbol, never a VERSION - the version is a property of the +// checkout every citation resolves against, so it is recorded once, in the `ANCHOR` of +// `test/provenance.test.ts`, and checked at both ends there. Read it before letting a +// citation settle anything about a release: the tree is BEHIND the packages this repo +// builds against, so a claim about a shipped version cannot be refuted by opening it. +// // `announced-not-written` is the STRONGEST kind available to a symbol with no counterpart -// in the tree — deviating TOWARD what the target has announced is alignment, and calling +// in the tree - deviating TOWARD what the target has announced is alignment, and calling // it `invention` tells a later reader the shape is ours to change when it is not. Being // the strongest, it is the hardest to assert: it carries `announced-by=@` and // its `ref` is the `.md` in this working copy where the statement is on record. An diff --git a/packages/polyfill/test/provenance.test.ts b/packages/polyfill/test/provenance.test.ts index 865d973..9db0f18 100644 --- a/packages/polyfill/test/provenance.test.ts +++ b/packages/polyfill/test/provenance.test.ts @@ -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 { + const parsed: unknown = JSON.parse(fs.readFileSync(manifest, "utf8")); + const out = new Map(); + for (const field of ["dependencies", "devDependencies"]) { + const deps = (parsed as Record)[field]; + if (typeof deps !== "object" || deps === null) continue; + for (const [name, spec] of Object.entries(deps as Record)) { + 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(ANCHOR.map((a) => [a.pkg, a.built] as const)); + const wrong: string[] = []; + const pinned = new Set(); + 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(":");