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(":");