feat: une cible annoncee mais pas encore ecrite a desormais sa categorie
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
*
|
||||
* ── The format ────────────────────────────────────────────────────────────
|
||||
*
|
||||
* // @provenance <name> kind=<kind> level=<1|2|3|none> ref=<path:symbol|none> — <note>
|
||||
* // @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
|
||||
@@ -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** — `<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 |
|
||||
* | `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 '<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.
|
||||
@@ -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<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/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", () => {
|
||||
|
||||
Reference in New Issue
Block a user