4e5b703949
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.
512 lines
27 KiB
TypeScript
512 lines
27 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> [announced-by=<who>@<date>] — <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** — 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`.
|
|
* - **announced-by** — `<who>@<YYYY-MM-DD>`: 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 |
|
|
* | `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 |
|
|
*
|
|
* `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";
|
|
import * as path from "node:path";
|
|
|
|
const SRC = path.join(import.meta.dir, "..", "src");
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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+)(?:\s+announced-by=(\S+))?/,
|
|
);
|
|
if (m && !m[1]!.startsWith("<")) {
|
|
out.push({
|
|
name: m[1]!,
|
|
kind: m[2]!,
|
|
level: m[3]!,
|
|
ref: m[4]!,
|
|
announcedBy: m[5] ?? null,
|
|
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({});
|
|
});
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* -- 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(":");
|
|
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 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 '<doc>.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=<who>@<YYYY-MM-DD>`);
|
|
else if (!ANNOUNCED_BY_SHAPE.test(a.announcedBy)) malformed.push(`${where} — announced-by '${a.announcedBy}' is not '<who>@<YYYY-MM-DD>'`);
|
|
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.
|
|
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`);
|
|
}
|
|
}
|
|
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. The form below is the one
|
|
// `useShapeQuery` carries in `surface/use-shape-query.ts` since `1.0.0-dev.5`, restated
|
|
// here so the REFUSALS beneath it have something well-formed to depart from — and
|
|
// checked against the document that holds the statement.
|
|
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<Provenance>, 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 '<who>@<YYYY-MM-DD>'");
|
|
// `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/use-shape-query.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", () => {
|
|
// 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);
|
|
});
|