diff --git a/.project/rules/nextgraph/no-divergence-from-nextgraph.md b/.project/rules/nextgraph/no-divergence-from-nextgraph.md index 7df36fe..4596213 100644 --- a/.project/rules/nextgraph/no-divergence-from-nextgraph.md +++ b/.project/rules/nextgraph/no-divergence-from-nextgraph.md @@ -35,18 +35,23 @@ When a divergence is genuinely unavoidable it must be **deliberate, documented a ## Every symbol says where it comes from -Answering *does the target do this?* once, in conversation, does not survive the week. So each symbol carries its own provenance, beside the code rather than in a document that drifts: the **level** that answers (1 the engine, 2 the binding, 3 the ORM), the upstream `path:symbol` it aligns on, and one of five kinds. +Answering *does the target do this?* once, in conversation, does not survive the week. So each symbol carries its own provenance, beside the code rather than in a document that drifts: the **level** that answers (1 the engine, 2 the binding, 3 the ORM), the `path:symbol` it cites — a symbol in `nextgraph-rs`, or, for the one kind that has nothing upstream to cite, the record in this working copy — and one 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 shape; nothing constructs it yet; we implement it | +| `announced-not-written` | upstream's developers have **stated** this is the direction; nothing in the tree carries it — not a definition, not even a name | | `reserved-name` | upstream has declared a NAME with no shape behind it — it binds nothing, so building on it is a bet | | `invention` | nothing at any level answers — a deliberate, documented bet | | `divergent` | upstream does this, and we do it differently | -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. 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. +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. + +Being the strongest, it is the **hardest to assert**. It names **who** stated it and **when** (`announced-by=@`), and its `ref` is not a symbol upstream — there is none — but the **document in this working copy where the statement is on record**, plus the announced name it is on record for. That record is prose and may never be one of our own source files: our code standing as the evidence for its own shape is a circle, and the circle would wear the strongest label in the set. An announcement nobody wrote down is hearsay, and hearsay is exactly how *"every document has a native inbox"* became an implementation before anyone checked it. The date is there to be weighed: a direction announced long ago and still unwritten is worth less than one stated last week, and without a date nobody can tell the two apart. **A reserved name is not a declared shape, and the gap between them is the whole point.** `declared-not-wired` is strong *because the shape constrains us*: implementing it is realising something the target wrote down. A name with nothing behind it constrains nothing, so what we build under it is our own invention wearing the target's vocabulary — the most dangerous kind, because the borrowed name makes it read as alignment. diff --git a/.project/rules/nextgraph/surface-mirrors-the-sdk.md b/.project/rules/nextgraph/surface-mirrors-the-sdk.md index 79a7f44..5448ddf 100644 --- a/.project/rules/nextgraph/surface-mirrors-the-sdk.md +++ b/.project/rules/nextgraph/surface-mirrors-the-sdk.md @@ -22,13 +22,19 @@ This is the surface-level sibling of [[no-divergence-from-nextgraph]], which ask **The legitimate deviation, so nobody concludes every difference is a fault.** `watchShape` returns a `ShapeQuery` shaped like TanStack Query's `useQuery` — `{ data, isPending, isSuccess, isError, error }` — where the SDK's `useShape` returns a bare `DeepSignalSet`. That is a real difference from what the SDK ships today, and it is **justified**: NextGraph's direction, as reported by its developers, is a `useShapeQuery` returning exactly that shape. Deviating *toward* the announced target is alignment, not divergence — it is the rest of the surface that will have to catch up. -**The gap that same example exposes — a known weakness of the annotation format.** A target **stated by its developers but not yet written** has no `file:symbol` to cite: `useShapeQuery` has zero occurrences in the tree. The taxonomy in [[no-divergence-from-nextgraph]] has no kind for it either — `declared-not-wired` needs a declaration, `reserved-name` needs a name present in the tree. So an author holding the strongest possible justification has only two bad moves left: cite an approximate symbol, which is precisely the `readUnion` failure, or declare `invention`, which understates the alignment and tells a later reader the shape is ours to change. Recorded here as a weakness to fix, deliberately not fixed here. +**The gap that same example exposed — closed by `announced-not-written`.** A target **stated by its developers but not yet written** has no `file:symbol` to cite: `useShapeQuery` has zero occurrences in the tree. The taxonomy in [[no-divergence-from-nextgraph]] had no kind for it either — `declared-not-wired` needs a declaration, `reserved-name` needs a name present in the tree — so an author holding the strongest possible justification had only two bad moves left: cite an approximate symbol, which is precisely the `readUnion` failure, or declare `invention`, which understates the alignment and tells a later reader the shape is ours to change. The kind `announced-not-written` is the third move: in place of a symbol upstream it cites the **record of the statement**, and it names who made it and when. + +``` +// @provenance useShapeQuery kind=announced-not-written level=3 ref=.project/rules/nextgraph/surface-mirrors-the-sdk.md:useShapeQuery announced-by=nextgraph-developers@2026-09-16 — the SDK ships `useShape` returning a bare `DeepSignalSet`; the announced direction is a `useShapeQuery` returning a TanStack-shaped snapshot +``` + +**This section is that record**, and the annotation above points back at it. The statement it holds: NextGraph's direction is a `useShapeQuery` returning `{ data, isPending, isSuccess, isError, error }`, reported from its developers and written down here on 2026-09-16 — the date the annotation carries, so a later reader can weigh how long the direction has been announced and still unwritten. An announcement nobody wrote down is hearsay: `packages/polyfill/test/provenance.test.ts` refuses an annotation of this kind whose record is missing, is a source file rather than prose, or no longer names the symbol. **How to apply.** - **Before naming a published symbol**, find the target's own name and use it. If the target has not named it, check the verb is *free* at the target: one it already spends elsewhere is taken, and borrowing it says something false. - **Prefer passing the target's type through to translating it.** When a parameter cannot be passed through, stop and ask what will have to be built beneath it — that is the real decision, and it is being made by the parameter. -- **Return what the SDK returns**, unless the difference is the target's announced direction — then say whose statement it is. +- **Return what the SDK returns**, unless the difference is the target's announced direction — then say whose statement it is, in the annotation's `announced-by` field and against a record the reader can open. "Whose statement" is a field the test checks, not a remark. - **"Duly justified" has a fixed cost**: one annotation line, beside the code, naming what the target does, what was chosen instead, and why. The test is refutability — a reader must be able to check it by opening one file. - **Check every citation against the CLAIM, not just against the tree.** Same granularity on both sides: a function may cite a function; a composition, a pipeline or a policy cites nothing at level 2 and must say so. "The reference resolves" is not "the annotation is true". - **Applies to what is published.** An internal helper that claims to resemble the target falls under [[no-divergence-from-nextgraph]]; this rule is about the symbols an application types. diff --git a/.tasks/todo/T01.b-what-the-orm-offers-for-writing-and-deleting.md b/.tasks/doing/T01.b-what-the-orm-offers-for-writing-and-deleting.md similarity index 86% rename from .tasks/todo/T01.b-what-the-orm-offers-for-writing-and-deleting.md rename to .tasks/doing/T01.b-what-the-orm-offers-for-writing-and-deleting.md index 4e0d0a3..4463dfd 100644 --- a/.tasks/todo/T01.b-what-the-orm-offers-for-writing-and-deleting.md +++ b/.tasks/doing/T01.b-what-the-orm-offers-for-writing-and-deleting.md @@ -4,8 +4,8 @@ title: What the ORM offers for writing and deleting, and what the three remainin parent: T01 depends_on: [] created: 2026-09-16 -claimed_by: null -claimed_at: null +claimed_by: claude-f93872b5 +claimed_at: 2026-09-16T15:25:00Z blocked: false --- @@ -35,4 +35,8 @@ blocked: false ## Progress +- 2026-09-16T15:25Z — claimed. Baselines taken BEFORE any edit: `bun test` 397 pass / 0 fail; `tsc -p packages/polyfill/tsconfig.json` exit 0; `tsc -p packages/polyfill/e2e/tsconfig.json` exit 0; `concept/lint.py` 0 ERROR (13 WARNING). `nextgraph-rs` read at `213338f697df15afc086c042f4bab1bffd01aa04` (clean but for one untracked engine test fixture). +- 2026-09-16T15:30Z — questions 1-3 settled by reading the target. Deletion answers at LEVEL 3 and is wired end to end (`DeepSignalSet.delete` -> `OrmPatch{op:remove}` -> `graph_orm_update` -> `Verifier::orm_frontend_update` -> `SparqlBuilder::remove_object`). Full chain recorded in the knowledge leaf. +- 2026-09-16T15:30Z — question 4: e2e baseline run started (`bun run test:e2e`) before adding anything. + ## Result diff --git a/.tasks/todo/T01.a-provenance-for-an-announced-target.md b/.tasks/done/T01.a-provenance-for-an-announced-target.md similarity index 56% rename from .tasks/todo/T01.a-provenance-for-an-announced-target.md rename to .tasks/done/T01.a-provenance-for-an-announced-target.md index 16240e6..61543d2 100644 --- a/.tasks/todo/T01.a-provenance-for-an-announced-target.md +++ b/.tasks/done/T01.a-provenance-for-an-announced-target.md @@ -32,4 +32,19 @@ Extend the `@provenance` mechanism so that a symbol aligned on a target **its de ## Progress +- 2026-09-16 — Baseline before any edit: `bun test` 394 pass / 0 fail, `lint.py` 0 ERROR. Proof set is `provenance.test.ts` plus every source file carrying an annotation. +- 2026-09-16 — Measured the "must resolve" question instead of guessing: all 92 existing `ref`s resolve in `../nextgraph-rs` today, path AND symbol. So the third way costs nothing to adopt and catches rot the reader cannot (the citation is the thing that moved). Implemented it: upstream refs checked only when the checkout is present (`../nextgraph-rs` or `$NEXTGRAPH_RS`), announced records checked ALWAYS because they point into this working copy. +- 2026-09-16 — Named the kind `announced-not-written`, parallel to `declared-not-wired`: it says what is there (a statement) and what is not (anything in the tree). Evidence is `ref` = the `.md` record here + the announced name, plus `announced-by=@`. The record must be prose, never one of our own source files — otherwise our code stands as the evidence for its own shape, wearing the strongest label in the set. +- 2026-09-16 — Found a pre-existing disagreement the pass condition forces to fix: the test's closed set had five kinds, the rule file's table six (`reserved-name` missing from the test), and the rule's prose said "five kinds" over a six-row table. Test, `src/index.ts` header and rule table now carry the same seven; the prose says "a closed set" instead of a number that rots. +- 2026-09-16 — Mutation-checked both new checks rather than trusting green: a renamed upstream symbol goes red; a real `announced-not-written` line in a `.ts` file parses end to end and goes red when `announced-by` is dropped. Both probes reverted, `git diff` clean on the probed files. +- 2026-09-16 — Gate: `bun test` 397 pass / 0 fail; both `tsc` exit 0; `lint.py` 0 ERROR. + ## Result + +**Done.** The provenance taxonomy gains `announced-not-written`: upstream's developers have stated the direction, and the tree carries nothing — not a definition, not even a name. Being the strongest kind available there, it is the hardest to assert: its `ref` is the `.md` in this working copy where the statement is on record (never a source file), and it carries `announced-by=@`, refused on every other kind. The third way on resolution is implemented: upstream citations are checked only when `../nextgraph-rs` (or `$NEXTGRAPH_RS`) is present; announced records are checked always: they point here. The kind is exercised by a fixture test over a pure `malformations()`, and verified through the parser. + +**Files.** `packages/polyfill/test/provenance.test.ts`, `packages/polyfill/src/index.ts` (header block), `.project/rules/nextgraph/no-divergence-from-nextgraph.md`, `.project/rules/nextgraph/surface-mirrors-the-sdk.md`. + +**Surprises.** The test's closed set was missing `reserved-name`, which the rule table already had, and the rule said "five kinds" over six rows — both fixed. All 92 existing refs already resolve, so the new check cost no fix-ups. + +**Follow-ups.** T01.c's annotation is written out verbatim in `surface-mirrors-the-sdk.md` § "The gap that same example exposed" — copy it. `announced-by` is `nextgraph-developers`; a named individual would be stronger evidence, and the record names none. diff --git a/.tasks/todo/T01-align-the-published-surface.md b/.tasks/todo/T01-align-the-published-surface.md index 97694c8..71bb5ab 100644 --- a/.tasks/todo/T01-align-the-published-surface.md +++ b/.tasks/todo/T01-align-the-published-surface.md @@ -42,3 +42,5 @@ The polyfill's published surface has drifted from the SDK's: invented names, inv - T01.g Adversarial review of the whole published surface (deps: all) ## Learnings digest + +- T01.a — the taxonomy gains `announced-not-written`: its `ref` points at the `.md` in THIS repo where the statement is on record, never a source file, plus `announced-by=@`. Upstream citations now resolve-check only when the sibling checkout is present; announced records always. Two latent defects fell out: the test's closed set was missing `reserved-name`, and the rule said "five kinds" over six rows. All 92 existing refs already resolved. diff --git a/packages/polyfill/src/index.ts b/packages/polyfill/src/index.ts index b4458a8..561519a 100644 --- a/packages/polyfill/src/index.ts +++ b/packages/polyfill/src/index.ts @@ -47,16 +47,27 @@ // // WHAT each symbol is aligned on is recorded BESIDE IT, not in a document that drifts: // -// // @provenance kind= level=<1|2|3|none> ref= — +// // @provenance kind= level=<1|2|3|none> ref= [announced-by=@] — // // `level` numbers the target's stack from the bottom (1 engine, 2 wasm binding / -// `@ng-org/web`, 3 JS ORM); `ref` cites a SYMBOL in `nextgraph-rs`, never a line number. -// `kind` is closed: `passthrough` (upstream's own symbol, name and shape), `aligned` (our -// name or ergonomics, upstream's semantics), `declared-not-wired` (upstream DEFINES the -// shape and constructs it nowhere — an absence, which is NOT evidence the thing is ours), +// `@ng-org/web`, 3 JS ORM); `ref` cites a SYMBOL, never a line number — in `nextgraph-rs` +// for every kind that cites the target, and in THIS working copy for the one kind that +// has no upstream file to cite. `kind` is closed: `passthrough` (upstream's own symbol, +// name and shape), `aligned` (our name or ergonomics, upstream's semantics), +// `declared-not-wired` (upstream DEFINES the shape and constructs it nowhere — an +// absence, which is NOT evidence the thing is ours), `announced-not-written` (upstream's +// developers have STATED this direction and nothing in the tree carries it yet), +// `reserved-name` (upstream has declared a NAME with no shape behind it), // `invention` (nothing answers, a documented bet), `divergent` (upstream does this, we do // 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. +// +// `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 +// 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 +// announcement nobody wrote down is hearsay, so the test refuses it. // A type is published only when a PUBLISHED SIGNATURE uses it. `export *` published // eight in one gesture (2026-08-10: it was a blanket re-export), of which two named diff --git a/packages/polyfill/test/provenance.test.ts b/packages/polyfill/test/provenance.test.ts index 5ee86ac..a58abb1 100644 --- a/packages/polyfill/test/provenance.test.ts +++ b/packages/polyfill/test/provenance.test.ts @@ -17,7 +17,7 @@ * * ── The format ──────────────────────────────────────────────────────────── * - * // @provenance kind= level=<1|2|3|none> ref= — + * // @provenance kind= level=<1|2|3|none> ref= [announced-by=@] — * * - **name** — the PUBLISHED name, qualified for a namespace member (`inbox.post`, * `storeRegistry.createEntityDoc`). Internal symbols carry their own name. It is in @@ -26,24 +26,57 @@ * 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 + * - **ref** — the ONE file a reader opens to check the claim, as `path:symbol` — a + * SYMBOL, never a line number (line numbers rot, and three citations in + * `docs/api-contract.md` already had). For every kind that cites the target it is a + * symbol in `nextgraph-rs`. For `announced-not-written` there is no such file — the + * target has not written the thing yet — so it is the `.md` in THIS working copy where + * the statement is on record, and the announced name it is on record for. `none` only * for `invention`. - * - **kind** — from a closed set: + * - **announced-by** — `@`: whose statement this is, and the date it is + * on record for. Required by `announced-not-written`, refused on every other kind, so + * it can never be attached to a weaker claim to make it read as a stronger one. + * - **kind** — from a closed set, the same one as the taxonomy in + * `.project/rules/nextgraph/no-divergence-from-nextgraph.md`; the two must agree: * - * | 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 | + * | 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 | + * | `announced-not-written` | upstream's developers have STATED this direction, and nothing in the tree carries it yet — not a definition, not even a name | + * | `reserved-name` | upstream has declared a NAME with no shape behind it | + * | `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. + * `announced-not-written` is the STRONGEST kind available to a symbol with no counterpart + * in the tree, and the two moves it replaces were both bad: citing an approximate symbol + * (the `readUnion` failure — a true citation attached to a false claim) or declaring + * `invention`, which tells a later reader the shape is ours to change when it is not. + * Being the strongest, it is also the hardest to assert: it carries a named author, a + * date, and the document where the statement is on record. An announcement nobody wrote + * down is hearsay — and hearsay is how *"every document has a native inbox"* became an + * implementation before anyone checked it. + * + * ── What this test checks, and what it deliberately does NOT ────────────── + * It does NOT check that a claim is TRUE. `readUnion` cites a symbol that exists and does + * what its annotation says it does, and the claim is still false: a composition cannot be + * "aligned" on one function. Truth is a human reading the source. This file pins that a + * claim was MADE, is well-formed, covers the whole surface — and does not point at a file + * that has gone away. + * + * It checks that a citation RESOLVES, in the only two ways that cannot end up training + * people to skip the suite: + * + * - a `nextgraph-rs` citation is checked ONLY when the checkout is there to ask + * (`../nextgraph-rs`, or `$NEXTGRAPH_RS`). It is a sibling that may be absent on + * another machine, and a test that goes red for THAT reason teaches nothing. Absent, + * the check passes empty-handed; present, a file or a symbol that has gone away is + * caught, which is the rot a human reader will not notice. + * - an `announced-not-written` record is checked ALWAYS, because it points into this + * working copy, which is never absent. That is precisely why the record has to live + * here rather than upstream: it makes the one kind with no upstream file to cite the + * one kind whose citation can be enforced unconditionally. */ import { test, expect } from "bun:test"; import * as fs from "node:fs"; @@ -51,16 +84,38 @@ 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 ANNOUNCED = "announced-not-written"; +const KINDS = new Set([ + "passthrough", + "aligned", + "declared-not-wired", + ANNOUNCED, + "reserved-name", + "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_]+$/; +/** + * The record of an announcement is PROSE in this working copy — a `.md`, never one of our + * own source files. A source file would let the code stand as the evidence for its own + * shape, which is a circle, and the circle would read as the strongest kind in the set. + */ +const RECORD_SHAPE = /^[A-Za-z0-9_.@/-]+\.md:[A-Za-z0-9_]+$/; +/** `who@YYYY-MM-DD` — a name one can go back to, and a date one can weigh. */ +const ANNOUNCED_BY_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._-]*@\d{4}-\d{2}-\d{2}$/; + +/** This working copy — where an `announced-not-written` record lives. */ +const REPO = path.join(import.meta.dir, "..", "..", ".."); interface Provenance { name: string; kind: string; level: string; ref: string; + /** `who@date` for `announced-not-written`; `null` on every other kind. */ + announcedBy: string | null; file: string; } @@ -135,7 +190,7 @@ function annotations(): Provenance[] { // 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+)/, + /^\s*\/\/ @provenance\s+(\S+)\s+kind=(\S+)\s+level=(\S+)\s+ref=(\S+)(?:\s+announced-by=(\S+))?/, ); if (m && !m[1]!.startsWith("<")) { out.push({ @@ -143,6 +198,7 @@ function annotations(): Provenance[] { kind: m[2]!, level: m[3]!, ref: m[4]!, + announcedBy: m[5] ?? null, file: path.relative(SRC, full), }); } @@ -171,12 +227,73 @@ test("no symbol is annotated twice", () => { expect(duplicated).toEqual({}); }); -test("every provenance annotation is well-formed", () => { +/** + * The `nextgraph-rs` checkout, or `null` when it is not there to ask. A sibling by + * convention; `$NEXTGRAPH_RS` names it when it is kept somewhere else. + */ +function upstreamCheckout(): string | null { + const root = process.env["NEXTGRAPH_RS"] ?? path.join(REPO, "..", "nextgraph-rs"); + return fs.existsSync(root) ? root : null; +} + +/** `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(":"); + return { file: ref.slice(0, at), symbol: ref.slice(at + 1) }; +} + +/** + * A citation is DEAD when the file it names is gone, or when that file no longer carries + * the symbol. Neither is a disagreement about the claim — both say the annotation points + * at nothing, which is the one failure a reader checking the citation cannot catch, + * because the citation is the thing that moved. + */ +function deadCitations(list: Provenance[], root: string): string[] { + const dead: string[] = []; + for (const a of list) { + const { file, symbol } = splitRef(a.ref); + const where = `${a.file} :: ${a.name}`; + const full = path.join(root, file); + if (!fs.existsSync(full)) { + dead.push(`${where} — ref '${a.ref}' names a file that is not there`); + continue; + } + // The symbol charset is pinned by REF_SHAPE, so it is safe inside a pattern. + if (!new RegExp(`\\b${symbol}\\b`).test(fs.readFileSync(full, "utf8"))) { + dead.push(`${where} — ref '${a.ref}' names a file that no longer carries '${symbol}'`); + } + } + return dead; +} + +/** + * Every way an annotation can be ill-formed, one line each. Pure over a list rather than + * over the tree, so a kind that no symbol carries YET can still be exercised — and + * `announced-not-written` is exactly that kind: its first real annotation cannot exist + * until the symbol it is for is renamed. A rule nothing has ever run is a rule nobody + * has checked. + */ +function malformations(list: Provenance[]): string[] { const malformed: string[] = []; - for (const a of annotations()) { + for (const a of list) { 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 === ANNOUNCED) { + // The strongest kind in the set, so the hardest one to assert. It cites the + // document that holds the statement, in this working copy, and names who made it: + // an announcement nobody wrote down is hearsay, and hearsay is how "every document + // has a native inbox" became an implementation before anyone checked it. + if (a.ref === "none") malformed.push(`${where} — kind=${ANNOUNCED} must cite the record of the statement, so ref may not be 'none'`); + else if (!RECORD_SHAPE.test(a.ref)) malformed.push(`${where} — ref '${a.ref}' is not '.md:Symbol' — the record of an announcement is prose in this working copy, never a source file`); + if (a.level === "none") malformed.push(`${where} — kind=${ANNOUNCED} names the layer that will carry it, so level must be 1, 2 or 3`); + if (a.announcedBy === null) malformed.push(`${where} — kind=${ANNOUNCED} must say whose statement it is: announced-by=@`); + else if (!ANNOUNCED_BY_SHAPE.test(a.announcedBy)) malformed.push(`${where} — announced-by '${a.announcedBy}' is not '@'`); + continue; + } + // The evidence of an announcement cannot be borrowed by a kind that has none — that + // is how a bet would come to read as the strongest alignment available. + if (a.announcedBy !== null) malformed.push(`${where} — announced-by belongs to kind=${ANNOUNCED} only, not to kind=${a.kind}`); 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. @@ -189,7 +306,64 @@ test("every provenance annotation is well-formed", () => { if (a.level === "none") malformed.push(`${where} — kind=${a.kind} claims a counterpart, so level must be 1, 2 or 3`); } } - expect(malformed).toEqual([]); + return malformed; +} + +test("every provenance annotation is well-formed", () => { + expect(malformations(annotations())).toEqual([]); +}); + +test("every citation into nextgraph-rs resolves, when the checkout is there to ask", () => { + const root = upstreamCheckout(); + const upstream = annotations().filter((a) => a.kind !== ANNOUNCED && a.ref !== "none"); + // Absent, there is nothing to ask and this passes empty-handed — deliberately. A suite + // that goes red because a sibling checkout is missing teaches nothing and trains people + // to skip it. Present, it catches the one thing a human re-reading the citation will + // not: upstream moved or renamed, and the annotation now points at nothing. + expect(root === null ? [] : deadCitations(upstream, root)).toEqual([]); +}); + +test("every announced-not-written annotation cites a record that is really here", () => { + // Checked unconditionally: the record is in THIS working copy, which is never the + // absent sibling. It is what makes the kind with no upstream file to cite the kind + // whose citation is enforced hardest. + expect(deadCitations(annotations().filter((a) => a.kind === ANNOUNCED), REPO)).toEqual([]); +}); + +test("announced-not-written accepts a statement on record and refuses every weaker form", () => { + // The kind exists for a target ANNOUNCED and not yet written, so its first real + // annotation cannot be in the tree to exercise it. This is that annotation written out + // — the form `useShapeQuery` will carry — checked against the document that holds the + // statement today. + const onRecord: Provenance = { + name: "useShapeQuery", + kind: ANNOUNCED, + level: "3", + ref: ".project/rules/nextgraph/surface-mirrors-the-sdk.md:useShapeQuery", + announcedBy: "nextgraph-developers@2026-09-16", + file: "(the form, not a file)", + }; + expect(malformations([onRecord])).toEqual([]); + expect(deadCitations([onRecord], REPO)).toEqual([]); + + const refused = (weaker: Partial, reason: string): void => { + const malformed = malformations([{ ...onRecord, ...weaker }]); + expect(malformed).toHaveLength(1); + expect(malformed.join("")).toContain(reason); + }; + // Hearsay: "the developers said so", nobody named, no date to weigh it by. + refused({ announcedBy: null }, "whose statement it is"); + refused({ announcedBy: "someone" }, "is not '@'"); + // `ref=none` is what `invention` carries, and this kind is the opposite of invention. + refused({ ref: "none" }, "must cite the record of the statement"); + // Our own source standing as the evidence for its own shape. + refused({ ref: "packages/polyfill/src/surface/watch-shape.ts:useShapeQuery" }, "never a source file"); + // An announcement names the layer that will carry the thing. + refused({ level: "none" }, "level must be 1, 2 or 3"); + // And the evidence cannot be lifted onto a weaker claim to dress it up. + expect(malformations([{ ...onRecord, kind: "invention", level: "none", ref: "none" }])).toEqual([ + "(the form, not a file) :: useShapeQuery — announced-by belongs to kind=announced-not-written only, not to kind=invention", + ]); }); test("the published surface is annotated at every kind that claims a counterpart", () => {