From 56a504657ab1534da4cf0964dae7bf0b3ff9d5d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 21:56:39 +0000 Subject: [PATCH 1/3] wip(rest): discovered teaching-site population Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza --- .github/workflows/ci.yml | 12 + .../src/meta-state-route-doc-spelling.test.ts | 279 +++++++++++++++++- scripts/cross-package-test-inputs.mjs | 44 ++- turbo.json | 5 +- 4 files changed, 318 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f48d68078b..cf42afba32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -187,6 +187,18 @@ jobs: # the same suite declares are covered by entries the core filter # already carries). - 'sdui.manifest.json' + # @objectstack/rest's meta-state-route-doc-spelling test discovers + # its population instead of listing it (#14561): it asks git for + # the authored-prose corpus under content/, docs/ and skills/ and + # judges every file that mentions the route. Verbatim per that + # declaration. `content/**` and `skills/**` above already cover + # two of the three roots; this is the third, and the FIRST entry + # to open the `docs/` root that the WIDTH note above records as + # declined — declined then because no declaration reached past + # `docs/audits/...`, and reached now because one does. The + # narrower audits entry is left as the package that declared it + # spelled it, exactly as the two narrower `skills/` entries were. + - 'docs/**' test: # Sharded 6-way BY PACKAGE: a core-touching PR ran the affected suite diff --git a/packages/rest/src/meta-state-route-doc-spelling.test.ts b/packages/rest/src/meta-state-route-doc-spelling.test.ts index 23dd80c097..7937fabea5 100644 --- a/packages/rest/src/meta-state-route-doc-spelling.test.ts +++ b/packages/rest/src/meta-state-route-doc-spelling.test.ts @@ -2,7 +2,7 @@ /** * The published prose that teaches the ADR-0020 D3.3 legal-next-state - * introspection route spells it the way the REST ledger does (#10178). + * introspection route spells it the way the REST ledger does (#10178, #14561). * * WHY THIS EXISTS (measured, not argued). #9180 step ② retired the plural * `/api/v1/meta/objects/:name/state/:field` registration and moved the SDK to @@ -18,13 +18,40 @@ * become a second, hand-copied statement of the canonical spelling — exactly * the disease the two-site drift is an instance of. Change the ledger row and * this test asks the docs to follow; change a doc line to a non-canonical - * spelling and it reddens naming the file. + * spelling and it reddens naming the file. #14561 extended that derivation from + * the SPELLING to the POPULATION: both regexes below are built out of the + * ledger row's own segments, so neither the expected path nor the net that + * finds candidates is written down a second time. * - * PRESENCE, NOT ABSENCE. The assertion is that the canonical path IS THERE. - * "no doc contains the plural" would pass on a page that stopped mentioning - * the route at all, which is the same silence this guard exists to break — the - * doc-authoring gate under `scripts/` carries the repo's standing statement of - * why an evaporated corpus must not read as a clean one. + * PRESENCE, NOT ABSENCE. The two named sites below are asserted to CONTAIN the + * canonical path. "no doc contains the plural" would pass on a page that + * stopped mentioning the route at all, which is the same silence this guard + * exists to break — the doc-authoring gate under `scripts/` carries the repo's + * standing statement of why an evaporated corpus must not read as a clean one. + * + * ── CONDITIONAL PRESENCE over a DISCOVERED population (#14561) ────────────── + * + * A two-file `TEACHING_SITES` list is the same shape as the drift it catches: + * a hand-maintained stand-in for a fact already on disk, going stale in + * silence as the corpus grows. It had already gone stale. Measured on + * `origin/main` 4d0d9445: SIX files under the authored-prose roots mention this + * route's shape, two of them are the list below, and one more — the QA + * platform-checklist area file for the API/backend surface — carries the wire + * path three times over, with concrete values rather than placeholders. It + * spells it canonically. That was LUCK, not enforcement: nothing here would + * have reddened had it been written with the retired plural. + * + * So the population is discovered rather than declared. Every authored-prose + * file that mentions this route AT ALL is judged, and a page that stops + * mentioning it drops out of the population instead of passing vacuously — + * which is why the silence objection above does not apply to this half, while + * the two positive assertions keep doing their own job of proving the canonical + * spelling is taught SOMEWHERE. Both properties are needed and neither implies + * the other. + * + * ⛔ NOT a discovered-corpus NEGATIVE scan ("no page contains the plural"), + * which is the shape this deliberately is not; see the next block for why that + * one would gate against a spelling the platform still answers on purpose. * * ⛔ WHAT THIS DOES NOT SAY. The plural is NOT universally dead and this test * must never be read as saying it is: the legacy if-chain branch in the @@ -32,17 +59,58 @@ * `/meta/objects/:name/state/:field` is refused by a REST-fronted deployment * and still ANSWERED wherever `dispatch()` is the front door. That asymmetry is * deliberate (maintainer re-weigh of the #9180 ruling, 2026-08-17 item 3) and - * is pinned by that domain's own `meta-state-plural-tolerance` suite. This file - * is about the spelling the docs TEACH, nothing else. + * is pinned by that domain's own `meta-state-plural-tolerance` suite. + * + * That is not prose here, it is the POPULATION BOUNDARY, and it is the reason + * the discovery net is anchored on the WHOLE ledger route including its + * versioned API prefix rather than on the `/meta/...` tail. A mention carrying + * the prefix is a claim about the REST wire path, which is the door this + * package's ledger row governs and the door where the plural really is + * refused. A mention without it is the dispatcher spelling, which this file has + * no opinion about and must not acquire one about — so it is not discovered at + * all. `PREDICATE_CASES` pins both directions, because a boundary that lives + * only in a comment is a boundary the next regex tweak can move by accident. + * + * WHAT HOLDS THE DERIVATION HONEST. Deriving the population from the same + * regex that judges it costs the pin the independence a hand-written list gave + * it for free: a detector that matched nothing would empty the population and + * leave every assertion below green. Two answers, both mechanical. + * `PREDICATE_CASES` is a set of whole tiny inputs whose verdict is known by + * construction — one per spelling this net must catch, one per shape it must + * NOT claim. And the discovered population is required to contain every named + * site below, so a net that goes blind reddens on the two files this guard has + * always known about. + * + * ── The corpus, and the input radius it declares (#14561) ─────────────────── + * + * The corpus is git's answer, not a filesystem crawl: tracked plus authored- + * but-untracked files under the authored-prose roots, with ignored paths + * excluded. A `readdirSync` crawl over these roots would be both slower and + * NONDETERMINISTIC inside merge-queue builds, where generated artifacts land + * under the same trees mid-run — the reasoning `packages/core`'s + * operation-private-key pin records in full, reached there by the same route. + * + * That is a real widening of this package's cross-package input radius and it + * is declared deliberately rather than inherited by accident: `content/**`, + * `docs/**` and `skills/**` are the roots the scan reads, so all three are + * declared in `CROSS_PACKAGE_TEST_INPUTS` and hashed by `turbo.json`. The two + * per-file globs stay alongside them: the named sites below are read BY NAME + * and the gate's roster holds those two paths, while the discovered corpus is + * a `git ls-files` result whose members this package's detector cannot name — + * so the `docs/**` root is declared with a `heldBy` witness naming this test, + * which is the mechanism that table publishes for exactly this case. Price, + * stated because it is charged to every docs PR: a diff under any of those + * roots now schedules and re-runs this package's suite. * - * Those two modules are CITED, never read — spelling them as repo paths here - * would make `check-cross-package-test-inputs` demand a declared radius over - * `packages/runtime/**`, claiming an input dependency this test does not have - * and invalidating this package's test cache on every runtime change. + * Those two runtime modules are CITED, never read — spelling them as repo + * paths here would make `check-cross-package-test-inputs` demand a declared + * radius over the runtime package, claiming an input dependency this test does + * not have and invalidating this package's test cache on every runtime change. */ +import { execFileSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; @@ -62,6 +130,11 @@ const HERE = dirname(fileURLToPath(import.meta.url)); * or nested in an object literal — would hold a real input radius while naming * nothing its roster can check. Both files are declared for `@objectstack/rest` * in `CROSS_PACKAGE_TEST_INPUTS` and hashed by `turbo.json`. + * + * ⛔ This list is no longer the population — it is the floor. Adding a file + * here is NOT how a new teaching site gets covered (the discovered population + * covers it the moment it is written); a name belongs here only when the site + * must be asserted to keep teaching the route even if its text is rewritten. */ const STATE_MACHINE_DOC = resolve(HERE, '../../../content/docs/protocol/objectql/state-machine.mdx'); const AUTOMATION_SKILL = resolve(HERE, '../../../skills/objectstack-automation/SKILL.md'); @@ -71,6 +144,100 @@ const TEACHING_SITES = [ { label: 'skills/objectstack-automation/SKILL.md', path: AUTOMATION_SKILL }, ] as const; +/** + * The authored-prose roots the discovery scan reads. Bare directory names, and + * pathspecs for `git ls-files` rather than paths this file resolves — the + * radius they hold is declared in `CROSS_PACKAGE_TEST_INPUTS`, which is where a + * reader can check it against the code. + */ +const CORPUS_ROOTS = ['content', 'docs', 'skills'] as const; + +/** + * What a single path SEGMENT's value looks like where prose writes it: a + * placeholder (`:name`), a real id (`showcase_task`), or one carrying a query + * (`status?from=in_review`). Everything that ends a URL in running prose, + * markdown, or a JSON string is excluded, and BOTH regexes below share it — so + * a mention and its canonical judgement can never disagree about where the + * route stops. + */ +const SEGMENT_VALUE = '[^/\\s`\'"),;\\]]+'; + +const escapeForRegExp = (literal: string): string => literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +/** + * Every mention this file reports, with the line it sits on, so a failure names + * a place a reader can open rather than a file to go searching in. + */ +interface Mention { + readonly text: string; + readonly line: number; +} + +function mentionsIn(text: string, pattern: RegExp): Mention[] { + const found: Mention[] = []; + const lines = text.split('\n'); + for (let i = 0; i < lines.length; i++) { + for (const m of lines[i]!.matchAll(pattern)) found.push({ text: m[0], line: i + 1 }); + } + return found; +} + +/** + * Inputs whose verdict is known by construction — the independence a derived + * population costs (#14561). Each is a whole tiny "page": the first three are + * spellings the net MUST catch, the last four are shapes it must NOT claim, + * and the dispatcher case is the 2026-08-17 asymmetry made checkable. + */ +const PREDICATE_CASES: readonly { + readonly label: string; + readonly text: string; + readonly mentioned: boolean; + readonly canonical: boolean; +}[] = [ + { + label: 'the canonical wire path, placeholders, as the protocol page writes it', + text: 'Over HTTP, `GET /api/v1/meta/object/:name/state/:field?from=:state` returns the legal next list.', + mentioned: true, + canonical: true, + }, + { + label: 'the canonical wire path with concrete values, as a checklist step writes it', + text: '"fire the meta state route: GET /api/v1/meta/object/showcase_task/state/status?from=in_review",', + mentioned: true, + canonical: true, + }, + { + label: 'the retired PLURAL object segment on the REST wire path', + text: 'Call `GET /api/v1/meta/objects/showcase_task/state/status` to read the legal next states.', + mentioned: true, + canonical: false, + }, + { + label: 'the third spelling the original sweep found — `metadata` plus the plural', + text: 'Call `GET /api/v1/metadata/objects/:name/state/:field` to read the legal next states.', + mentioned: true, + canonical: false, + }, + { + label: 'the DISPATCHER spelling, no versioned prefix — deliberately out of the population', + text: 'Embedded, `dispatch()` answers `/meta/objects/:name/state/:field` as well as the singular form.', + mentioned: false, + canonical: false, + }, + { + label: 'a different meta route that shares the object segment but has no state tail', + text: '"server reject: PUT /api/v1/meta/object/qa_owd_probe with sharingModel private — capture the 4xx"', + mentioned: false, + canonical: false, + }, + { + label: 'prose about state machines that names no route at all', + text: 'A `state_machine` rule declares the legal transitions out of each state of a picklist field.', + mentioned: false, + canonical: false, + }, +]; + describe('meta state-introspection route — docs spell it the way the ledger does', () => { const rows = REST_ROUTE_LEDGER.filter((r) => r.client === 'meta.getLegalNextStates'); @@ -89,6 +256,33 @@ describe('meta state-introspection route — docs spell it the way the ledger do expect(canonicalPath).toMatch(/^\/api\/v1\/meta\/object\/:name\/state\/:field$/); }); + // Both nets, one derivation. A literal segment of the ledger row is exact in + // the canonical net and TOLERANT of a longer word in the discovery net — + // which is what turns `object` into `objects` and `meta` into `metadata` + // without either retired spelling being written down here. A placeholder + // segment is a free value in both. + const segments = canonicalPath.split('/').filter(Boolean); + const canonicalSource = `/${segments + .map((seg) => (seg.startsWith(':') ? SEGMENT_VALUE : escapeForRegExp(seg))) + .join('/')}`; + const mentionSource = `/${segments + .map((seg) => (seg.startsWith(':') ? SEGMENT_VALUE : `${escapeForRegExp(seg)}[A-Za-z0-9_]*`)) + .join('/')}`; + const MENTION = new RegExp(mentionSource, 'g'); + const CANONICAL = new RegExp(`^${canonicalSource}$`); + + it('the discovery net is derived from the ledger row and is wider than the canonical one', () => { + // A net narrower than the thing it is meant to contain would drop the + // canonical spelling itself out of the population, and every file would + // then pass by never being looked at. + expect(mentionsIn(canonicalPath, MENTION).map((m) => m.text)).toEqual([canonicalPath]); + expect(CANONICAL.test(canonicalPath)).toBe(true); + // One tolerance group per LITERAL segment: a derivation that stopped + // deriving would show up here as a count that no longer tracks the row. + const literals = segments.filter((seg) => !seg.startsWith(':')); + expect(mentionSource.split('[A-Za-z0-9_]*').length - 1).toBe(literals.length); + }); + for (const site of TEACHING_SITES) { it(`${site.label} teaches the canonical path`, () => { // readFileSync throws on a moved/renamed file rather than passing @@ -104,4 +298,61 @@ describe('meta state-introspection route — docs spell it the way the ledger do ).toBe(true); }); } + + it('the detector classifies each known spelling the way its construction says', () => { + const verdicts = PREDICATE_CASES.map((c) => { + const found = mentionsIn(c.text, MENTION); + return { + label: c.label, + mentioned: found.length > 0, + canonical: found.length > 0 && found.every((m) => CANONICAL.test(m.text)), + }; + }); + expect(verdicts).toEqual( + PREDICATE_CASES.map((c) => ({ label: c.label, mentioned: c.mentioned, canonical: c.canonical })), + ); + }); + + // The corpus, read once. git's answer rather than a crawl — see the header. + const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], { + cwd: HERE, + encoding: 'utf8', + }).trim(); + const corpus = execFileSync( + 'git', + ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', ...CORPUS_ROOTS], + { cwd: repoRoot, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }, + ) + .split('\0') + .filter(Boolean); + const population = corpus + .map((relPath) => ({ relPath, found: mentionsIn(readFileSync(join(repoRoot, relPath), 'utf8'), MENTION) })) + .filter((entry) => entry.found.length > 0); + + it('the corpus and the population it yields are both real', () => { + // Two ways this whole half can evaporate into a vacuous green: git + // hands back nothing, or the net matches nothing. Neither is silent. + expect(corpus.length, 'the authored-prose roots yielded no files at all').toBeGreaterThan(100); + expect( + population.map((p) => p.relPath), + 'the discovered population no longer contains the sites this guard has always known about, ' + + 'so the detector — not the corpus — is what changed', + ).toEqual(expect.arrayContaining(TEACHING_SITES.map((s) => s.label))); + }); + + it('every authored page that mentions this route spells it the way the ledger row does', () => { + const offenders = population.flatMap(({ relPath, found }) => + found.filter((m) => !CANONICAL.test(m.text)).map((m) => `${relPath}:${m.line} ${m.text}`), + ); + expect( + offenders, + `these pages mention the \`meta.getLegalNextStates\` route on the REST wire path and do not ` + + `spell it \`${canonicalPath}\`, the path this package's ledger row declares:\n` + + `${offenders.join('\n')}\n` + + 'Update the prose to the ledger spelling (or, if the route itself moved, update the ledger ' + + 'first and let this follow). ⛔ Do not "fix" this by narrowing the net: the plural is still ' + + 'answered wherever `dispatch()` is the front door, which is why only mentions carrying the ' + + 'versioned REST prefix are judged here at all.', + ).toEqual([]); + }); }); diff --git a/scripts/cross-package-test-inputs.mjs b/scripts/cross-package-test-inputs.mjs index 5cdd30c273..1a6c0ce3b9 100644 --- a/scripts/cross-package-test-inputs.mjs +++ b/scripts/cross-package-test-inputs.mjs @@ -758,17 +758,47 @@ export const CROSS_PACKAGE_TEST_INPUTS = { globs: ['packages/spec/src/security/rls.zod.ts', 'skills/objectstack-formula/**'], }, '@objectstack/rest': { - // src/meta-state-route-doc-spelling.test.ts reads the two published prose - // sites that teach the `meta.getLegalNextStates` route and asserts each - // spells it the way this package's REST_ROUTE_LEDGER row does, so the - // ledger row and the prose can no longer drift apart in silence (#10178). - // Per-file rather than `content/docs/**` or `skills/**` for the reason the - // @objectstack/spec entry gives: those roots are edited far more often than - // anything this radius really depends on. + // src/meta-state-route-doc-spelling.test.ts asserts the published prose that + // teaches the `meta.getLegalNextStates` route spells it the way this + // package's REST_ROUTE_LEDGER row does, so the ledger row and the prose can + // no longer drift apart in silence (#10178). + // + // The two per-file globs are the sites it reads BY NAME, and they are the + // half this gate's roster can check: each is a `const` bound to one + // relative literal, so a rename shows up here as an uncovered path rather + // than as a quiet green. + // + // The three ROOTS are #14561, and they are a deliberate widening rather than + // one inherited by accident. A hand-kept two-file list is the same defect + // the test exists to catch -- it had already gone stale, with a third site + // teaching the wire path outside it -- so the population is now DISCOVERED: + // the test asks `git ls-files` for the authored-prose corpus under these + // roots and judges every file that mentions the route. The radius is + // therefore the roots, not the files that happen to match today, and + // anything narrower would be a second hand-kept list standing in for the + // same fact. Price, charged to every docs PR: a diff under these roots + // schedules and re-runs this package's suite. + // + // ⛔ Do NOT re-narrow this to the matching files. That is the #7802 shape + // ("a list you must remember to update") wearing the radius' clothes: the + // next page to teach the route would land outside the declared globs, its + // edit would not re-run this suite, and the gate would go on reporting + // green over a corpus it no longer hashes. globs: [ 'content/docs/protocol/objectql/state-machine.mdx', 'skills/objectstack-automation/SKILL.md', + 'content/**', + 'docs/**', + 'skills/**', ], + heldBy: { + // The corpus is a `git ls-files` result read through a loop variable, so + // the roster gets no name from it -- the trade `pathExpression` + // documents. `content/**` and `skills/**` are held mechanically anyway by + // the two named sites above; `docs/**` has no such holder and is held by + // the scan itself. + 'docs/**': ['packages/rest/src/meta-state-route-doc-spelling.test.ts'], + }, }, '@objectstack/metadata-protocol': { // src/sys-metadata-repository.draft-drain.test.ts reads the durability diff --git a/turbo.json b/turbo.json index 5727026559..06cae481f2 100644 --- a/turbo.json +++ b/turbo.json @@ -356,7 +356,10 @@ "!coverage/**", "!.turbo/**", "$TURBO_ROOT$/content/docs/protocol/objectql/state-machine.mdx", - "$TURBO_ROOT$/skills/objectstack-automation/SKILL.md" + "$TURBO_ROOT$/skills/objectstack-automation/SKILL.md", + "$TURBO_ROOT$/content/**", + "$TURBO_ROOT$/docs/**", + "$TURBO_ROOT$/skills/**" ] }, "@objectstack/formula#test": { From c7e0e7a34d2c0698456d68c7419060d0de0d7fff Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 23:39:57 +0000 Subject: [PATCH 2/3] wip(scripts): ci-filter-parity self-test pin for the new docs/ root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserved from the previous agent's uncommitted working tree after a container restart. Kept or reverted together with the ci.yml change it pins — justification is the next step, not settled by this commit. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza --- scripts/check-ci-filter-parity.mjs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/check-ci-filter-parity.mjs b/scripts/check-ci-filter-parity.mjs index 4b43cad016..c7bf14d2cd 100644 --- a/scripts/check-ci-filter-parity.mjs +++ b/scripts/check-ci-filter-parity.mjs @@ -614,14 +614,20 @@ export async function selfTest() { // card added (the checked-in SDUI manifest artefact @objectstack/lint's // production-witness suite reads from the workspace root), covered only // through its own single-file `crosspkg` entry the way #10848's SKILL.md is. - // Ten plus one plus two plus one plus one: the rollback now uncovers - // fifteen. This pin is judged over the LIVE declaration table on + // Plus, since #14561, the one NEW root that card opened: @objectstack/rest's + // meta-state doc-spelling test discovers its population instead of listing + // it, so it declares the three authored-prose roots it reads, and `docs/**` + // is the one of the three no earlier declaration had reached. Its two + // siblings move nothing here -- `content/**` and `skills/**` were already + // unique members of this set from #10015 and #12201. + // Ten plus one plus two plus one plus one plus one: the rollback now uncovers + // sixteen. This pin is judged over the LIVE declaration table on // purpose: a declaration added under a root the rollback keeps leaves the // count alone, one under a new root moves it and is recorded here by name. const preFix = judge(fixtureWorkflow({ core: real.filters?.core, crosspkg: ['scripts/**'] }), CROSS_PACKAGE_TEST_INPUTS); assert( - new Set(uncoveredGlobs(preFix)).size === 15, - `rolling \`crosspkg\` back to its pre-#10015 list uncovers the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one -- got ${new Set(uncoveredGlobs(preFix)).size}`, + new Set(uncoveredGlobs(preFix)).size === 16, + `rolling \`crosspkg\` back to its pre-#10015 list uncovers the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one plus #14561's one -- got ${new Set(uncoveredGlobs(preFix)).size}`, ); assert( uncoveredGlobs(preFix).includes('skills/**'), @@ -643,6 +649,10 @@ export async function selfTest() { uncoveredGlobs(preFix).includes('sdui.manifest.json'), `-- and #12924 added the repo-root SDUI manifest artefact, by name`, ); + assert( + uncoveredGlobs(preFix).includes('docs/**'), + `-- and #14561 added the authored-prose root the discovered teaching-site population reads, by name`, + ); // ── (7) WIRING: the gate and its self-test really run in CI ────────────── const SELF = 'scripts/check-ci-filter-parity.mjs'; @@ -667,7 +677,7 @@ export async function selfTest() { `same-root-different-file case observed failing and then covered by naming the file, a glob covered by ` + `\`core\`, one covered only by \`crosspkg\` and one covered by neither judged separately in one table, the ` + `stale-entry direction, seven refusals over subjects that could not be read, the checked-in ci.yml, the ` + - `pre-#10015 rollback uncovering the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one, ` + + `pre-#10015 rollback uncovering the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one plus #14561's one, ` + `and the CI wiring read out of lint.yml.`, ); selfTestReachedVerdict = true; From 773f0d0d99f7895429782de201764d5c36ae4a25 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 23:57:40 +0000 Subject: [PATCH 3/3] test(rest): correct the header's population measurement to the merged base Two corrections, both re-measured on this tree rather than inherited: - The cited base was 4d0d9445 and the tree is now 7a17f3bf; the population is re-derived there (721-file corpus, three files on the REST wire path). - The header read the triaged "six files" as if the file's own net produced it. It does not: that count comes from a LINE-scoped sweep, and the three extra files name a DIFFERENT `/meta/object/...` route on a line that also contains the word "state", with no `/state/` tail. That is exactly the sixth PREDICATE_CASES entry, so the distinction is already pinned. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza --- .../src/meta-state-route-doc-spelling.test.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/rest/src/meta-state-route-doc-spelling.test.ts b/packages/rest/src/meta-state-route-doc-spelling.test.ts index 7937fabea5..b43f0ec83e 100644 --- a/packages/rest/src/meta-state-route-doc-spelling.test.ts +++ b/packages/rest/src/meta-state-route-doc-spelling.test.ts @@ -34,12 +34,21 @@ * A two-file `TEACHING_SITES` list is the same shape as the drift it catches: * a hand-maintained stand-in for a fact already on disk, going stale in * silence as the corpus grows. It had already gone stale. Measured on - * `origin/main` 4d0d9445: SIX files under the authored-prose roots mention this - * route's shape, two of them are the list below, and one more — the QA - * platform-checklist area file for the API/backend surface — carries the wire - * path three times over, with concrete values rather than placeholders. It + * `origin/main` 7a17f3bf, over a 721-file corpus: THREE files carry this + * route on the REST wire path, two of them are the list below, and the third + * — the QA platform-checklist area file for the API/backend surface — carries + * it three times over, with concrete values rather than placeholders. It * spells it canonically. That was LUCK, not enforcement: nothing here would - * have reddened had it been written with the retired plural. + * have reddened had it been written with the retired plural, and the rollback + * arm of this card's verification confirmed exactly that: the pre-#14561 file + * passes over a corpus in which that page teaches the retired plural. + * + * (A looser, LINE-scoped sweep reports SIX files — the number this card was + * triaged on. The other three are not mentions of this route at all: each + * names a DIFFERENT `/meta/object/...` route — an OWD probe, a field-order + * check — on a line that also happens to contain the word "state", with no + * `/state/` tail anywhere. That is the sixth `PREDICATE_CASES` entry, + * pinned so this distinction cannot quietly move.) * * So the population is discovered rather than declared. Every authored-prose * file that mentions this route AT ALL is judged, and a page that stops