From 60f9b648a56767f4c2243a6485c57e59c4fe29f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 00:22:23 +0000 Subject: [PATCH] fix(devx): classify the tree-measurement claims under scripts/ instead of sweeping them Three kinds of sentence live under one phrase and take different repairs: a citation of a past measurement (anchored, stays true), a claim whose failure direction is named (loud when it moves), and a bare magnitude in the present tense (decays in silence). Only the third is a defect. Adds scripts/pm/measurement-claim-triage.mjs: it re-derives the population rather than recording its size, applies T1-T3 mechanically, records the T4 judgement (level or relation) per site, and carries controls in BOTH directions. Repairs the five sites judged kind 3 -- one by pointing at the gate that prints the value, four by pinning the reading to the commit that recorded it and saying out loud that the figure is deliberately not refreshed. Co-Authored-By: Claude --- scripts/check-pnpm-filter-targets.mjs | 14 +- scripts/check-published-files.mjs | 16 +- scripts/check-skill-compatibility-version.mjs | 12 +- scripts/check-slot-lookup-ratchet.mjs | 14 +- scripts/docs-audit/README.md | 9 +- scripts/pm/measurement-claim-triage.mjs | 657 ++++++++++++++++++ 6 files changed, 705 insertions(+), 17 deletions(-) create mode 100644 scripts/pm/measurement-claim-triage.mjs diff --git a/scripts/check-pnpm-filter-targets.mjs b/scripts/check-pnpm-filter-targets.mjs index d91ff6433a6..6ee15e13674 100644 --- a/scripts/check-pnpm-filter-targets.mjs +++ b/scripts/check-pnpm-filter-targets.mjs @@ -127,10 +127,16 @@ const SCANNED_EXTENSIONS = [...JS_EXTENSIONS, ...HASH_COMMENT_EXTENSIONS, '.json * ── Why `scripts/**` is honest here, with the measurement ─────────────────── * * This is the `subtree` case: the walk descends the whole of scripts/ and every - * file carrying a scanned extension is judged. Measured on this tree, the - * declaration names 235 tracked files under scripts/ and this gate reads 228 of - * them — 97.0%. The 7 it skips are the non-code files the extension filter - * drops, not a subtree it never opens. + * file carrying a scanned extension is judged. Measured at `52a41b72e` + * (2026-08-23): the declaration named 235 tracked files under scripts/ and this + * gate read 228 of them — 97.0%, the 7 it skipped being the non-code files the + * extension filter drops rather than a subtree it never opens. + * + * ⛔ That reading is deliberately pinned to its commit and NOT refreshed here. + * Both counts move with every file added under scripts/ and nothing reprints + * them, so a figure restored in the present tense would read as current and go + * false in silence. What this paragraph argues is the SHARE, and the share is + * what survives the churn; for today's counts, run the walk below. * * ── Why the workspace manifests stay UNDECLARED ───────────────────────────── * diff --git a/scripts/check-published-files.mjs b/scripts/check-published-files.mjs index 5946c161ba2..78ca0c1925d 100644 --- a/scripts/check-published-files.mjs +++ b/scripts/check-published-files.mjs @@ -388,11 +388,17 @@ function matcher(pattern) { * This is the `subtree` case, not the `filtered` one check-examples-live-imports * refuses. `walk()` below enumerates EVERY non-build file of every publishable * member and MINIMAL judges each of them against FORBIDDEN, so the declaration - * names files this gate really opens. Measured on this tree: the declaration - * names 5263 tracked files and the gate judges 4803 of them — 91.3%. The 460 it - * does not judge are the members whose OWN manifests this gate read in order to - * exclude them (`private`), which is itself a read of the declared subtree, so - * a manifest card there is a true lead rather than a fabricated one. + * names files this gate really opens. Measured at `52a41b72e` (2026-08-23): + * the declaration named 5263 tracked files and the gate judged 4803 of them — + * 91.3%. The 460 it did not judge are the members whose OWN manifests this gate + * reads in order to exclude them (`private`), which is itself a read of the + * declared subtree, so a manifest card there is a true lead rather than a + * fabricated one. + * + * ⛔ Those counts are deliberately pinned to that commit and NOT refreshed. They + * move with every package added to or removed from the workspace, no gate + * reprints them, and the claim this paragraph rests on is the SHARE — that the + * declaration names what the gate really opens — never the level. * * The contrast that sets the boundary is in check-published-readme-exports.mjs, * which enumerates the same members and scores 2.8% — its refusal docblock diff --git a/scripts/check-skill-compatibility-version.mjs b/scripts/check-skill-compatibility-version.mjs index 580021263f7..343d2c14a9a 100644 --- a/scripts/check-skill-compatibility-version.mjs +++ b/scripts/check-skill-compatibility-version.mjs @@ -97,14 +97,22 @@ const PACKAGE_ROOTS = ['packages', 'apps', 'examples']; * The instrument can only express a SUBTREE: `hintCovers` collapses globs, so a * declared hint names every tracked file beneath it and there is no way to spell * "the package manifests under this root". That makes the two sides of this gate - * completely different trades, and both were measured on this tree: + * completely different trades, and both were measured at `f29e89717` + * (2026-08-22): * - * skills/** 49 of 50 tracked files are skill directories this gate + * skills/** 49 of 50 tracked files were skill directories this gate * reads — 98% precision over a 50-file subtree. * packages/** 73 package.json files out of 4903 tracked files — 1.5%, * pasted into every packages/** dispatch prompt in the repo. * apps/** 1 of 35 (2.9%) · examples/** 4 of 238 (1.7%). * + * ⛔ Those four readings are deliberately pinned to that commit and NOT + * refreshed in place. Every term moves with the tree, nothing reprints them, + * and what decides the trade is the ORDER of magnitude between the two sides — + * which is why a figure restored in the present tense would be a new decaying + * claim rather than a better one. `node scripts/pm/bare-root-worklist.mjs` + * carries this gate's rows for the three package roots, with their own dates. + * * The three package roots are therefore the +139084 fabrication one level up — * the very measurement in `hintCovers`' docblock, which prices accepting bare * top-level directory words at that many fabricated (gate, file) pairs because diff --git a/scripts/check-slot-lookup-ratchet.mjs b/scripts/check-slot-lookup-ratchet.mjs index ce4ff50beb1..74eb9df5a30 100644 --- a/scripts/check-slot-lookup-ratchet.mjs +++ b/scripts/check-slot-lookup-ratchet.mjs @@ -147,9 +147,17 @@ const BASELINE_PATH = 'scripts/slot-lookup-baseline.json'; * a lead it does not need. That trade is the one `CHANGE_KIND_GATES` already * decided for the three structurally identical whole-tree ratchets in * `dispatch-gates.mjs`, and it is decided the same way here — by what the gate - * costs to run needlessly. Measured on this tree: 46s, no build required, and a - * failure names the offending file and line. A seat that runs it needlessly - * loses seconds; a seat that is never prompted loses a CI round. + * costs to run needlessly. Measured at 46s when this was recorded + * (`99ca6623f`, 2026-08-19), with no build required, and a failure names the + * offending file and line. A seat that runs it needlessly loses seconds; a seat + * that is never prompted loses a CI round. + * + * ⛔ The duration is deliberately left at that commit and NOT refreshed in + * place. A wall-clock reading is a reading of one BOX as much as of one tree — + * this repo's own agent containers read several times the figures recorded for + * them — so a fresh number written here would decay without the tree moving at + * all. Time it on the box you are deciding for; what the trade needs is the + * ORDER (seconds against a CI round), and that is what is stated. * * Two things a seat prompted by this declaration needs to know, and neither is * visible from a green `pnpm lint`: diff --git a/scripts/docs-audit/README.md b/scripts/docs-audit/README.md index 08be744a929..c5911531a04 100644 --- a/scripts/docs-audit/README.md +++ b/scripts/docs-audit/README.md @@ -552,9 +552,12 @@ How often it renders, re-derived over the 40 first-parent commits ending at `e43 3 of the 17 package-touching runs (18%) — `20a452e664`, `f213793ddb`, `dd4113ec0b` — so it is a rare notice rather than a per-PR banner, which is what keeps it readable. -**Cost** (the card's open question): the anchor derivation reads the same 178-page corpus -the old one did, plus the 18 route-source/ledger files (~875 KB) and one `git show` -per changed file per side. Measured end-to-end on the ten PRs above, `node affected-docs.mjs` +**Cost** (the card's open question): the anchor derivation reads the same hand-written +corpus the old one did, plus the 18 route-source/ledger files (~875 KB) and one `git show` +per changed file per side. ⛔ The corpus SIZE is deliberately absent from that sentence — +run `check-audit-scope.mjs` (`pnpm check:docs-audit-scope`) for today's page count. A size +written down in the present tense decays with nothing going red, which is exactly what the +`178` that used to stand here did. Measured end-to-end on the ten PRs above, `node affected-docs.mjs` went from 85-195 ms to 114-582 ms. The heaviest case is the widest diff; every case stays well under a second, against a job that already spends seconds checking out the repo and setting up Node. It is the right default for every PR. diff --git a/scripts/pm/measurement-claim-triage.mjs b/scripts/pm/measurement-claim-triage.mjs new file mode 100644 index 00000000000..a5c29bf0da8 --- /dev/null +++ b/scripts/pm/measurement-claim-triage.mjs @@ -0,0 +1,657 @@ +#!/usr/bin/env node +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Tree-measurement claims under `scripts/**` — the CLASSIFIER (#17797). + * + * node scripts/pm/measurement-claim-triage.mjs the population + every verdict + * node scripts/pm/measurement-claim-triage.mjs --review the kind-3 residue alone + * node scripts/pm/measurement-claim-triage.mjs --self-test the controls, both directions + * + * ## Why a tool and not a sentence + * + * The card that asked for this states its own population as a count. That count + * was taken once and then decayed — which is the very defect the card is about, + * reproduced by the card. ⛔ So no number describing this population is written + * down here. The scope line this tool prints IS the number, re-derived on the + * tree it is run against, and a reader who wants the population runs the tool. + * ⭐ The figures are deliberately absent from this docblock; ⛔ do not helpfully + * restore one. + * + * ## The three kinds, and why one phrase carries all of them + * + * A sentence that reports a measurement of this repository is one of three + * things, and they take DIFFERENT repairs: + * + * KIND 1 a citation of a past measurement. It is scoped to a moment — by a + * revision, a date, or a named event ("before this gate was written", + * "at the commit that took it") or a counterfactual condition ("with + * the reservation removed"). It reads as history, so it stays true + * forever. ⇒ NEEDS NOTHING. + * + * KIND 2 a claim whose failure direction is NAMED, and which something in + * the tree announces when it moves — a zero whose falsifier is + * spelled out, a figure a gate reprints, a figure a pin reds on. + * It does not decay SILENTLY, which is the whole harm. ⇒ NEEDS + * NOTHING. + * + * KIND 3 a bare magnitude in the present tense: no moment, no named + * direction, nothing that speaks when it moves. It reads as current + * state and goes false in silence. ⇒ THIS IS THE DEFECT. + * + * ⛔ The phrase alone separates none of them, which is why the population below + * is an UPPER BOUND on a population and NOT a count of defects. Treating the + * sweep's size as a finding count is the error this tool exists to prevent. + * + * ## What is mechanical here, and what is not + * + * Tests T0–T3 run over source text and are recall filters: each one can only + * move a hit OUT of the review residue, and each prints the cue that moved it, + * so a verdict is always auditable against the sentence. What no regex decides + * is T4 — whether a reader acts on the magnitude's LEVEL or on a relation the + * level's drift survives. T4 is a judgement, so it is recorded per site in + * `TRIAGE` below, with its reason, exactly as the bare-root worklist records + * its own. A review hit with no `TRIAGE` row prints as UNTRIAGED and reds the + * self-test: a new claim of this shape has to be judged by someone. + * + * ## Scope, and the two files deliberately outside it + * + * `scripts/pm/dispatch-gates.mjs` and `scripts/pm/check-widening-tells.mjs` are + * EXCLUDED, with their reasons carried as data in `EXCLUDED` and printed on + * every run rather than left in prose. The residue they hold is therefore + * un-swept, and the scope line says so instead of reading as complete. + * + * ## ⚠️ This tool is not wired into CI + * + * Wiring a `--self-test` needs a `package.json` script and a workflow step, and + * both sit outside the file surface this card was dispatched with. Until that + * lands, `--self-test` is run by hand and its controls are not watching + * anything on their own. ⛔ Do not read a green run here as CI coverage. + */ + +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join, relative, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from '../invoked-as.mjs'; + +const HERE = fileURLToPath(new URL('.', import.meta.url)); +const REPO_ROOT = join(HERE, '..', '..'); +const SCAN_ROOT = join(REPO_ROOT, 'scripts'); + +/** This file's own path, relative to the repo root, in the spelling verdicts use. */ +const SELF = 'scripts/pm/measurement-claim-triage.mjs'; + +/** + * FORM A — the phrase the card names, in every spelling it actually occurs in + * on this tree. ⚠️ The card's own probe was case-sensitive and lower-case only; + * on this tree that spelling reaches strictly fewer sites than the sentence + * does, and the gap is not a rounding error. The count either way is printed, + * never written down. + */ +const FORM_A = /(?:\bre-)?measured\s+(?:on|against)\s+this\s+tree/gi; + +/** + * FORM B — sites FORM A cannot reach, declared per file with the reason. + * + * ⭐ That this list is non-empty is the finding: a phrase probe is not a + * population. The card's own named instance carries no phrase at all — it is a + * bare corpus size in the present tense — so a sweep defined by the phrase + * would have reported the tree clean at the one site everybody agreed was + * broken. + */ +const FORM_B = [ + { + file: 'scripts/docs-audit/README.md', + // The hand-written docs corpus size. `pnpm check:docs-audit-scope` prints + // today's; every literal of the older figure left in this file is a claim + // about the corpus that the gate can contradict. + pattern: /\b178\b/g, + // `#11178` is a card number, not a corpus size. + reject: /#\d*178\b/, + why: 'the hand-written docs corpus size, which `check:docs-audit-scope` prints live', + }, +]; + +/** Excluded files, with the reason carried as data so every run prints it. */ +const EXCLUDED = [ + { + file: 'scripts/pm/dispatch-gates.mjs', + why: 'edited by open PRs #19162 and #19024, and its `--self-test` exceeds the agent ' + + 'container foreground cap (#17765), so a change here could not be verified from a seat', + }, + { + file: 'scripts/pm/check-widening-tells.mjs', + why: 'edited by open PRs #19153 and #19024', + }, +]; + +/* ─────────────────────────── the tests ─────────────────────────────────── */ + +/** + * T0 — is there a magnitude at all? Applied LAST, as the weakest escape hatch: + * a sentence that is already anchored or already loud is settled before the + * question is asked, so T0 only ever speaks about the residue. + * + * Nothing decays if nothing is counted. A ZERO is a magnitude — kind 2 is + * defined on one — so the word counts even where no digit does. Revisions, dates, card numbers, tool + * versions and exit codes are struck first: each is an identifier that happens + * to be spelled in digits, and counting them as magnitudes would classify every + * anchored citation as the thing an anchor exists to prevent. + */ +const strikeNonMagnitudes = (text) => text + .replace(/`[0-9a-f]{7,40}`/g, ' ') + .replace(/\b20\d{2}-\d{2}-\d{2}\b/g, ' ') + .replace(/#\d+/g, ' ') + .replace(/\bv?\d+\.\d+(?:\.\d+)?\b/g, ' ') + .replace(/\bexit \d+\b/gi, ' ') + .replace(/\bADR-\d+\b/g, ' ') + .replace(/\bbash \d\.\d\b/gi, ' '); + +const MAGNITUDE = /(? { + const flat = text.replace(/\s+/g, ' '); + for (const [re, name] of cues) if (re.test(flat)) return name; + return null; +}; + +/* ───────────────────── population derivation ───────────────────────────── */ + +const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.turbo']); + +function* walk(dir) { + for (const entry of readdirSync(dir).sort()) { + if (SKIP_DIRS.has(entry)) continue; + const full = join(dir, entry); + const st = statSync(full); + if (st.isDirectory()) yield* walk(full); + else if (st.isFile()) yield full; + } +} + +/** + * ## The window a verdict is read over, and why it is a SENTENCE + * + * The first build of this tool read a fixed ±6-line block. It cleared the one + * site everybody had already agreed was broken (`scripts/docs-audit/README.md`, + * the present-tense corpus size): six lines above it an UNRELATED sentence + * cites a revision, and a block window handed that anchor to a claim that has + * none. ⛔ An anchor belongs to the sentence that carries the figure and to no + * other, so T2 and T3 read the SENTENCE. + * + * T1 and T3 are the exceptions, and deliberately. A narrative frame is + * routinely set one sentence earlier — the dispatcher-vocabulary gate opens + * with "An earlier revision of this paragraph counted DIRECTORIES …" and only + * then reports the reading — so T1 also sees the sentence before. A named failure direction is routinely set one + * sentence LATER — the card's own kind-2 exemplar spells the zero in one + * sentence and names what would falsify it in the next — so T3 sees both + * neighbours. ⛔ T2 sees neither: an anchor belongs to its own sentence, and + * widening T2 is precisely the bug this window replaced. + * + * ⚠️ Neither test sees further than one sentence, and a frame or a direction + * set further back than that lands in the review residue — the safe direction, + * where a person judges it and records the judgement in `TRIAGE`. + */ +const SENTENCE_BREAK = /(?<=[.!?])[ \n]+(?=[A-Z⛔⭐⚠️`*#·•])/; + +/** Comment furniture, so a sentence reads as prose rather than as a comment. */ +function deComment(line, isMarkdown) { + let out = line.replace(/^\s*(?:\/\/+|\*)\s?/, ''); + if (!isMarkdown) out = out.replace(/^\s*#+\s?/, ''); + return out; +} + +/** + * Split a file into sentences, each carrying the 1-based line it starts on. + * The line map is built from the ORIGINAL line count, so a reported line is a + * line in the file a reader will open and not an offset into a derived string. + */ +function sentencesOf(text, isMarkdown) { + const lines = text.split('\n'); + const prose = lines.map((l) => deComment(l, isMarkdown)); + const out = []; + let buffer = []; + let bufferStart = 1; + const flush = () => { + if (!buffer.length) return; + const joined = buffer.join('\n'); + let offset = 0; + for (const piece of joined.split(SENTENCE_BREAK)) { + const before = joined.slice(0, offset); + out.push({ text: piece, line: bufferStart + (before.match(/\n/g) ?? []).length }); + offset += piece.length + 1; + } + buffer = []; + }; + prose.forEach((line, i) => { + if (line.trim() === '') { + // ⛔ A blank comment line does NOT end a sentence that was introducing a + // data block. A docblock here routinely ends its lead-in on a colon and + // then sets the table off with an empty ` *` line; flushing at that blank + // strands every figure in a fragment carrying no cue, and the sweep reads + // a docblock full of magnitudes as carrying none. + if (buffer.length && /:\s*$/.test(buffer[buffer.length - 1])) return; + flush(); + return; + } + if (!buffer.length) bufferStart = i + 1; + buffer.push(line); + }); + flush(); + return out; +} + +function hitsIn(relPath, text) { + const isMarkdown = relPath.endsWith('.md'); + const sentences = sentencesOf(text, isMarkdown); + const found = []; + sentences.forEach((s, i) => { + const lead = i > 0 ? sentences[i - 1].text : ''; + const next = i + 1 < sentences.length ? sentences[i + 1].text : ''; + FORM_A.lastIndex = 0; + if (FORM_A.test(s.text)) { + found.push({ file: relPath, line: s.line, form: 'A', sentence: s.text, lead, next }); + } + for (const form of FORM_B) { + if (form.file !== relPath) continue; + form.pattern.lastIndex = 0; + if (!form.pattern.test(s.text)) continue; + if (form.reject && form.reject.test(s.text)) continue; + found.push({ file: relPath, line: s.line, form: 'B', sentence: s.text, lead, next, why: form.why }); + } + }); + return found; +} + +export function collectPopulation() { + const excluded = new Set(EXCLUDED.map((e) => e.file)); + const hits = []; + for (const full of walk(SCAN_ROOT)) { + const rel = relative(REPO_ROOT, full).split(sep).join('/'); + if (excluded.has(rel) || rel === SELF) continue; + let text; + try { text = readFileSync(full, 'utf8'); } catch { continue; } + hits.push(...hitsIn(rel, text)); + } + return hits; +} + +/** + * The classifier proper: T1–T3 then T0, over one sentence (plus the sentence + * before it, for T1 only). T4 — level or relation — is a judgement and lives in + * `TRIAGE`. + */ +export function classify(sentence, lead = '', next = '') { + const frame = firstCue(FRAME_CUES, `${lead}\n${sentence}`); + if (frame) return { kind: 'KIND-1', cue: frame }; + const anchor = firstCue(ANCHOR_CUES, sentence); + if (anchor) return { kind: 'KIND-1', cue: anchor }; + const loud = firstCue(LOUD_CUES, `${lead}\n${sentence}\n${next}`); + if (loud) return { kind: 'KIND-2', cue: loud }; + if (!MAGNITUDE.test(strikeNonMagnitudes(sentence.replace(/\s+/g, ' ')))) { + return { kind: 'NO-FIGURE', cue: 'no magnitude survives T0' }; + } + return { kind: 'REVIEW', cue: 'present-tense magnitude, no frame, no anchor, nothing loud' }; +} + +/* ─────────────────────────── T4, recorded ───────────────────────────────── */ + +/** + * T4 — LEVEL or RELATION, the half no regex decides. + * + * With T1–T3 all declining, one question is left: does a reader act on the + * magnitude's **level**, or on a **relation** (a ratio, an ordering, an + * existence claim, a set equality) that the level's drift survives? A level is + * kind 3. A relation is an ILLUSTRATION — the figure sizes an argument that + * stays correct when the figure moves, and rewriting it buys nothing while + * costing a reviewer a diff. + * + * ⛔ Every row is keyed by an EXCERPT and never by a line number. The card that + * asked for this cited its own known instance by line, and the line had moved + * before anyone read the card — a ledger keyed on line numbers decays exactly + * the way its subject does. + * + * `verdict`: + * ILLUSTRATION — judged a relation; stays as written. + * REPAIRED — judged kind 3 and repaired in #17797. The self-test holds + * the excerpt ABSENT: if it comes back, so has the defect. + */ +const TRIAGE = [ + { file: 'scripts/check-bash32-floor.mjs', match: '8 findings, every one of them a false positive', + verdict: 'ILLUSTRATION', why: 'a counterfactual reading of the rule this gate replaced — the SIGN (all false positives) carries it, and the rule it measured is gone from this tree' }, + { file: 'scripts/check-cross-package-test-inputs.mjs', match: '6 exist only inside comments', + verdict: 'ILLUSTRATION', why: 'the load-bearing claim is the zero that follows it — none of the six escapes its package — and the corpus sizes only scale it' }, + { file: 'scripts/check-cross-package-test-inputs.mjs', match: '15 extensionless', + verdict: 'ILLUSTRATION', why: 'three spellings EXIST, which is why three branches exist; no branch is chosen by how many files carry it' }, + { file: 'scripts/check-cross-package-test-inputs.mjs', match: 'test files importing a workspace sibling by BARE specifier', + verdict: 'ILLUSTRATION', why: 'the ORDERING decides the design (bare specifiers dwarf relative ones), and no plausible drift reverses it' }, + { file: 'scripts/check-cross-package-test-inputs.mjs', match: '6 of the 60 declared globs are held by exactly such reads', + verdict: 'ILLUSTRATION', why: 'a non-emptiness claim — the blind spot is real — with a named specimen; nobody acts on how many' }, + { file: 'scripts/check-dispatcher-error-vocabulary.mjs', match: 'the naive `[{,]` anchor alone reports 19', + verdict: 'ILLUSTRATION', why: 'two recognisers compared at one moment; 19-against-13 is a relation, and the six extras are named individually' }, + { file: 'scripts/check-driver-conformance.mjs', match: "105 files under `driver-sql/src` carry a literal", + verdict: 'ILLUSTRATION', why: 'the claim is that those files are NOT defects — a class judgement; the count only says the class is large' }, + { file: 'scripts/check-examples-live-imports.mjs', match: '76 coupled files across 3 packages', + verdict: 'ILLUSTRATION', why: 'the refusal rests on the SHARE (a tiny coupled set inside a huge root), which no ordinary churn moves' }, + { file: 'scripts/check-logger-receiver-detach.mjs', match: 'over the property reads whose NAME is a log channel', + verdict: 'ILLUSTRATION', why: 'the section head one paragraph up names `--census` as reproducing every number in it — instrument-backed, one sentence further than T3 reaches' }, + { file: 'scripts/check-parse-guard.mjs', match: '9 of the 28 sites live in', + verdict: 'ILLUSTRATION', why: 'the finding is that the printed remedy is false for a subset that EXISTS; the subset is then described by kind, not by size' }, + { file: 'scripts/check-pnpm-filter-targets.mjs', match: 'Measured on this tree, the declaration names 235 tracked files', + verdict: 'REPAIRED', why: 'kind 3 — a declaration-honesty ratio over a root that grows with every script added, read as current state, reprinted by nothing' }, + { file: 'scripts/check-published-files.mjs', match: 'Measured on this tree: the declaration names 5263 tracked files', + verdict: 'REPAIRED', why: 'kind 3 — same shape over the whole publishable workspace, which moves on every package add or removal' }, + { file: 'scripts/check-release-section-coverage.mjs', match: 'assertion 1 fires on 24 minors', + verdict: 'ILLUSTRATION', why: 'the figures justify shipping WITH a baseline, and the ruling that fixes them is dated in the sentence before' }, + { file: 'scripts/check-sdui-lockstep.mjs', match: 'measured on this tree at 96 hint literals', + verdict: 'ILLUSTRATION', why: '"at 96 hint literals" is the CONDITION of the reading, not its subject: the claim is about which leading-dot literals the allowlist admits' }, + { file: 'scripts/check-skill-compatibility-version.mjs', match: 'both were measured on this tree', + verdict: 'REPAIRED', why: 'kind 3 — four precision ratios deciding which roots are declared, all moving with the tree and none reprinted' }, + { file: 'scripts/check-slot-lookup-ratchet.mjs', match: 'Measured on this tree: 46s', + verdict: 'REPAIRED', why: 'kind 3, and the worst-ageing kind: a duration is a reading of one BOX as much as of one tree, so it decays without the tree changing at all' }, + { file: 'scripts/check-test-source-alias.mjs', match: '1832 of the 4844 tracked files', + verdict: 'ILLUSTRATION', why: 'it records the state the fix REMOVED — the derivation no longer decides this gate by where a package sits — in a past-tense narrative T1 does not reach' }, + { file: 'scripts/check-where-matcher-conformance.mjs', match: '2889 of the 2889 files', + verdict: 'ILLUSTRATION', why: 'a set EQUALITY held in both directions by this gate own liveness-and-precision pin: it reds on drift rather than decaying' }, + { file: 'scripts/check-widget-option-census.mjs', match: 'inheritable literals: 0', + verdict: 'ILLUSTRATION', why: 'a table of zeros making an inheritance claim; the zeros are the claim' }, + { file: 'scripts/docs-audit/README.md', match: 'matched 82, 113, 43, 13 and 10 of 178 pages', + verdict: 'ILLUSTRATION', why: 'past-tense narrative of the first build guards — the words arrived as real declarations and matched; it describes a tree that is gone' }, + { file: 'scripts/docs-audit/README.md', match: 'named by 59 of 178 pages', + verdict: 'ILLUSTRATION', why: 'the argument is the 15% corpus SHARE rule stated two lines up; this is the share that condemned one term when the rule was written' }, + { file: 'scripts/docs-audit/README.md', match: 'reads the same 178-page corpus', + verdict: 'REPAIRED', why: 'kind 3, and the instance the card names: a present-tense corpus size that `check:docs-audit-scope` contradicts today' }, + { file: 'scripts/eslint-stack-headroom.mjs', match: 'the narrow scope is the worst case', + verdict: 'ILLUSTRATION', why: 'worst-case against intermittent — a comparison between two arms, and the gate is built on the comparison, not on either rate' }, + { file: 'scripts/git-merge-regen.mjs', match: 'measured on this tree: 18 rows', + verdict: 'ILLUSTRATION', why: 'the claim is that reverting #13585 loosening would turn the reconciliation RED; the row split only shows both branches are populated' }, + { file: 'scripts/pm/bare-root-worklist.mjs', match: 'reaches 2755 of ', + verdict: 'ILLUSTRATION', why: 'a set equality this file own --self-test re-derives and reds on, which is the whole point of a worklist that runs' }, + { file: 'scripts/pm/bare-root-worklist.mjs', match: 'numerator from the instrument own printed scan ', + verdict: 'ILLUSTRATION', why: 'the sentence names both instruments the two terms came from and refuses to carry either from the row above' }, + { file: 'scripts/pm/bare-root-worklist.mjs', match: 'the ratio does not change the verdict either direction', + verdict: 'ILLUSTRATION', why: 'the sentence says in its own words that the level is not acted on — a relation by declaration' }, +]; + +/* ──────────────────────────── the controls ─────────────────────────────── */ + +/** + * ⭐ BOTH DIRECTIONS, and neither one alone. A classifier only ever seen to + * fire proves nothing; one only ever seen to stay quiet proves less. + * + * Each control carries a `liveness` substring that must still be present in the + * file it was taken from. ⛔ A control quoting a sentence the tree no longer + * holds is a control that has stopped controlling anything, and it would pass + * in silence — the exact failure this whole file is about. + */ +const CONTROLS = [ + { + name: 'kind 1 — a citation carrying its revision, left alone', + file: 'scripts/check-tier-file-adoption.mjs', + liveness: 'Measured on this tree at d03c3c96d6', + sentence: 'Measured on this tree at d03c3c96d6, on the ONE package that owns tier files: ' + + '`packages/cli/vitest.config.ts` names `OS_TEST_TIERS` 5 times and `packages/cli/vitest-tiers.ts` 4 times.', + lead: '', next: '', + expect: 'KIND-1', + }, + { + name: 'kind 2 — a zero whose failure direction is named, left alone', + file: 'scripts/docs-audit/affected-docs.mjs', + liveness: 'zero commands declare `static topic`', + sentence: 'Measured on this tree: zero commands declare `static topic`, and the one `static id` ' + + "(`init.ts` -> `'init'`) agrees with its path, so nothing is mis-derived today.", + lead: '', + next: 'The failure direction if that ever changes is a phrase matching NO page — a recall miss the ' + + 'anchor list makes visible, not a false positive that pollutes the work list.', + expect: 'KIND-2', + }, + { + name: 'kind 3 — the card own named instance, CAUGHT (pre-repair text)', + file: 'scripts/docs-audit/README.md', + // ⛔ No liveness key: this is the sentence as it read BEFORE the repair, so + // the tree is required NOT to hold it. `absent` is checked instead. + absent: 'reads the same 178-page corpus', + sentence: '**Cost** (the card open question): the anchor derivation reads the same 178-page corpus ' + + 'the old one did, plus the 18 route-source/ledger files (~875 KB) and one `git show` per changed file per side.', + lead: 'How often it renders, re-derived over the 40 first-parent commits ending at `e43b18fd9`: ' + + '3 of the 17 package-touching runs (18%).', + next: 'Measured end-to-end on the ten PRs above, `node affected-docs.mjs` went from 85-195 ms to 114-582 ms.', + expect: 'REVIEW', + }, + { + name: 'kind 3 repaired — the SAME site now points at the gate that prints the value', + file: 'scripts/docs-audit/README.md', + liveness: 'check:docs-audit-scope', + sentence: '**Cost** (the card open question): the anchor derivation reads the same hand-written corpus ' + + 'the old one did — run `check-audit-scope.mjs` for today\'s page count rather than trusting one written ' + + 'down here — plus the 18 route-source/ledger files and one `git show` per changed file per side.', + lead: '', next: '', + expect: 'KIND-2', + }, + { + name: 'kind 2 — a refusal a live assertion holds, left alone', + file: 'scripts/check-skill-compatibility-version.mjs', + liveness: 'the refusal is pinned in the self-test', + sentence: 'So the manifest side stays undeclared, deliberately, and the refusal is pinned in the ' + + 'self-test rather than left in this paragraph — a later author who adds `packages/**` meets an assertion.', + lead: '', next: '', + expect: 'KIND-2', + }, + { + name: '⛔ the one-sentence anchor rule — a neighbour revision does NOT clear a bare magnitude', + file: 'scripts/pm/measurement-claim-triage.mjs', + sentence: 'The declaration names 235 tracked files under scripts/ and this gate reads 228 of them.', + lead: 'Re-derived over the 40 first-parent commits ending at `e43b18fd9`.', + next: '', + expect: 'REVIEW', + }, +]; + +/* ──────────────────────────────── report ───────────────────────────────── */ + +const KINDS = ['KIND-1', 'KIND-2', 'NO-FIGURE', 'REVIEW']; + +export function triageOf(hit) { + const flat = hit.sentence.replace(/\s+/g, ' '); + return TRIAGE.find((row) => row.file === hit.file && row.verdict !== 'REPAIRED' && flat.includes(row.match)) + ?? null; +} + +function verdicts() { + return collectPopulation().map((hit) => ({ hit, ...classify(hit.sentence, hit.lead, hit.next) })); +} + +export function report({ reviewOnly = false } = {}) { + const rows = verdicts(); + const counts = Object.fromEntries(KINDS.map((k) => [k, rows.filter((r) => r.kind === k).length])); + + console.log( + `measurement-claim triage: ${rows.length} claim(s) over ` + + `${new Set(rows.map((r) => r.hit.file)).size} file(s) under scripts/ — ` + + KINDS.map((k) => `${k} ${counts[k]}`).join(' · '), + ); + console.log( + ' ⛔ REVIEW is a CANDIDATE bucket, not a defect count — T4 (level or relation) is a judgement,\n' + + ' recorded per site in TRIAGE. ⛔ Reading any number above as a finding count is the error\n' + + ' this tool exists to prevent.', + ); + for (const e of EXCLUDED) console.log(` ⚠️ NOT SWEPT ${e.file} — ${e.why}`); + console.log(''); + + for (const row of rows) { + if (reviewOnly && row.kind !== 'REVIEW') continue; + const t = row.kind === 'REVIEW' ? triageOf(row.hit) : null; + const tag = row.kind === 'REVIEW' ? (t ? `T4 ${t.verdict}` : 'T4 UNTRIAGED') : row.kind; + console.log(` ${tag.padEnd(18)} ${row.hit.file}:${row.hit.line} [${row.cue}]`); + if (t) console.log(` ${' '.repeat(18)} ↳ ${t.why}`); + } + return rows; +} + +/* ─────────────────────────────── self-test ─────────────────────────────── */ + +const SELF_TEST_VERDICT = 'measurement-claim-triage self-test: controls hold in BOTH directions'; + +export function selfTest() { + const problems = []; + const rows = verdicts(); + + // #4690: an empty population refuses rather than printing a confident zero. + if (rows.length === 0) problems.push('PREREQUISITE NOT MET: the sweep found no claims at all under scripts/.'); + + for (const control of CONTROLS) { + const got = classify(control.sentence, control.lead, control.next).kind; + if (got !== control.expect) problems.push(`CONTROL "${control.name}": expected ${control.expect}, got ${got}.`); + let text = ''; + try { text = readFileSync(join(REPO_ROOT, control.file), 'utf8'); } catch { + problems.push(`CONTROL "${control.name}": ${control.file} is gone, so the control quotes nothing.`); + continue; + } + if (control.liveness && !text.includes(control.liveness)) { + problems.push( + `CONTROL "${control.name}": ${control.file} no longer carries \`${control.liveness}\`. ` + + 'A control quoting a sentence the tree has lost passes in silence — re-point it at a live one.', + ); + } + if (control.absent && text.includes(control.absent)) { + problems.push( + `CONTROL "${control.name}": ${control.file} carries \`${control.absent}\` again. ` + + 'That is the repaired kind-3 sentence restored — the defect is back.', + ); + } + } + + // Every REVIEW hit is judged by somebody. + for (const row of rows) { + if (row.kind !== 'REVIEW') continue; + if (!triageOf(row.hit)) { + problems.push( + `UNTRIAGED ${row.hit.file}:${row.hit.line} — a present-tense magnitude with no frame, no anchor ` + + 'and nothing loud, and no TRIAGE row judging it. Judge it (level or relation) and record why.', + ); + } + } + + // A TRIAGE row that matches nothing is a judgement about a tree that is gone. + for (const row of TRIAGE) { + const live = rows.some((r) => r.hit.file === row.file && r.hit.sentence.replace(/\s+/g, ' ').includes(row.match)); + if (row.verdict === 'ILLUSTRATION' && !live) { + problems.push(`STALE TRIAGE row ${row.file} "${row.match}" matches nothing in the population any more.`); + } + if (row.verdict === 'REPAIRED' && live) { + problems.push(`REGRESSED ${row.file} "${row.match}" is back in the population — the repair was undone.`); + } + } + + // ⛔ Every cue must still match something, or it is decoration. + for (const [cues, label] of [[FRAME_CUES, 'FRAME'], [ANCHOR_CUES, 'ANCHOR'], [LOUD_CUES, 'LOUD']]) { + for (const [re, name] of cues) { + const corpus = rows.map((r) => `${r.hit.lead}\n${r.hit.sentence}\n${r.hit.next}`.replace(/\s+/g, ' ')); + const controls = CONTROLS.map((c) => `${c.lead}\n${c.sentence}\n${c.next}`.replace(/\s+/g, ' ')); + if (![...corpus, ...controls].some((t) => re.test(t))) { + problems.push(`DEAD CUE ${label}/${name} ${re} matches nothing in the population or the controls.`); + } + } + } + + // This file's own prose must make no claim the sweep would have to judge. + const own = readFileSync(join(REPO_ROOT, SELF), 'utf8'); + const prose = own.slice(0, own.indexOf('const TRIAGE')); + FORM_A.lastIndex = 0; + if (FORM_A.test(prose)) { + problems.push('This file own prose now carries the swept phrase outside CONTROLS, and it excludes itself from the sweep.'); + } + + if (problems.length) { + console.error(`\n✗ measurement-claim-triage self-test: ${problems.length} problem(s)\n`); + for (const p of problems) console.error(` - ${p}`); + console.error(''); + process.exit(1); + } + console.log(`✓ ${SELF_TEST_VERDICT} (${CONTROLS.length} controls, ${TRIAGE.length} recorded judgements).`); + return SELF_TEST_VERDICT; +} + +if (isEntrypoint(import.meta.url)) { + if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error('\n✗ measurement-claim-triage: selfTest() returned without reaching its verdict.\n'); + process.exit(1); + } + } else report({ reviewOnly: process.argv.includes('--review') }); +}