diff --git a/scripts/check-doc-authoring.mjs b/scripts/check-doc-authoring.mjs index 54d5ae4dfd..0dff9bbcfa 100644 --- a/scripts/check-doc-authoring.mjs +++ b/scripts/check-doc-authoring.mjs @@ -85,7 +85,8 @@ // It lives in `collectFiles`, not in `main`, so the self-test drives the // invariant itself rather than a proxy for it. import { - mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, + existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, statSync, + writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { createRequire } from 'node:module'; @@ -226,7 +227,13 @@ const SKIP_FILES = new Set(['content/docs/ai/skills-reference.mdx']); * not exist — since #4916 a hard refusal rather than a silent skip, but one * that fails naming the wrong problem. The self-test pins both halves. */ -const ROOT_WATCH_HINTS = ['.claude/**', 'docs/**', 'skills/**', 'content/**']; +// `packages/**` joined on #13297: the cross-package prose-id leg walks every +// sibling package, so the gate genuinely reads any package edit. The same +// precedent as `check:slot-lookup-ratchet` declaring the whole of +// `packages/**`; the over-claim (test files, `packages/spec` — which the SPEC +// leg of this same gate reads anyway) is carve-outs inside a walked root, the +// tolerated case argued above. +const ROOT_WATCH_HINTS = ['.claude/**', 'docs/**', 'skills/**', 'content/**', 'packages/**']; const DOMAINS = [ 'Datasource', 'Connector', 'Policy', 'SharingRule', 'Position', 'PermissionSet', @@ -585,9 +592,12 @@ const POSITIONAL_MESSAGE_CALLS = new Set([ */ function scanBoundaryLines() { return [ - `ℹ Rule 3 scan boundary — root: ${SPEC_SOURCE_ROOT}/ only (*.ts|*.mts, test/spec/bench` - + ' files excluded; customer-facing refusal prose in SIBLING PACKAGES is NOT scanned —' - + ' root extension deferred on #13179, revival condition codified there).', + `ℹ Rule 3 scan boundary — root: ${SPEC_SOURCE_ROOT}/ (position-based, no exemption;` + + ' *.ts|*.mts, test/spec/bench files excluded). SIBLING PACKAGES are scanned by the' + + ` LEDGERED total-string leg — root: ${PACKAGES_PROSE_ROOT}/ (${PACKAGES_PROSE_EXCLUDED}/` + + ' excluded there, it belongs to the position-based rule; baseline:' + + ` ${PACKAGES_PROSE_LEDGER}) — the #13179 root-extension deferral revived by its own` + + ' codified condition (#13297).', ' recognised sink shapes: `message:` / `error:` properties · positional messages on' + ` ${POSITIONAL_MESSAGE_CALLS.size} zod validators · strictObject option keys` + ` (${[...STRICT_OPTION_KEYS].join(', ')}) · ${[...TOMBSTONE_CALLS].join('/')}()` @@ -597,6 +607,210 @@ function scanBoundaryLines() { ]; } +// ── Rule 3b: the CROSS-PACKAGE leg — every sibling package, ledgered ──────── +// +// The #13179 adjudication deferred extending Rule 3's root beyond +// `packages/spec/src` and codified its own revival condition: a later census +// finding same-audience tracker ids outside the root reopens the question as an +// instrument card. #13297 is that card, and this leg is the answer, sized by +// the AST re-census the card demanded (2026-08-30, this file's `--census`): +// +// grep-shaped estimate on the card: ~11 candidate sites +// AST census, strings only, spec out: 831 sites · 974 id occurrences · +// 231 files · 632 (file,id) pairs +// +// The grep census was ~75× under — this tree's prose is concatenation-split, +// exactly the shape a line-oriented pattern cannot see (proven in --self-test +// for the spec leg, re-proven here for a package file). +// +// ## Why ROOT WIDENING and not per-package guards +// +// One criterion (INTERNAL_ID_SOURCE), one walker, one ledger — a new package +// is covered the day it appears, and the audience rule cannot drift into N +// spellings. Per-package guards would copy the id regex and the string walk +// into ~20 packages and each copy would rot on its own schedule; the family +// lesson ("a second source of truth while the first is still reachable") is +// the whole argument, and it is why this leg lives HERE rather than in a new +// script. +// +// ## Why TOTAL string coverage here, when the spec leg is position-based +// +// The spec leg's closed position list exists to keep VALUES from being +// reported as prose (`.default('draft')` is not a message). An INTERNAL_ID +// match has no such ambiguity: a tracker-shaped token is one wherever it +// sits, and the audience question that positions cannot answer — the +// 2026-08-29 triage boundary is "can the reader change what they authored", +// NOT throw-vs-logger call shape — is not mechanizable from the AST at all. +// So this leg does not try: it counts every id in every non-test string and +// holds the count against a pinned baseline. The audience judgment lives in +// the BASELINE DIFF, where a reviewer can see it, instead of in a heuristic +// that would silently misfile the next warn-at-authors site. Known residual +// false-positive family, measured: 3-digit all-numeric CSS colours (`#111`) — +// 3 strings in one file at census time, absorbed by the baseline; a NEW one +// reds and the remedy is the 6-digit colour spelling. +// +// ## The ratchet +// +// growth (actual > pinned) → red. Strip the id or move it to a `//` comment +// (comments are the sanctioned home for internal +// anchors — the audience that can resolve #NNNN +// reads the source, not the string at runtime). +// shrink (actual < pinned) → red, stale baseline: regenerate with +// `--census-ledger` in the same PR, so burn-down +// is recorded where review can see it. +// +// While the baseline is non-empty, the ratchet IS this leg's blindness floor: +// a walker or prefilter that goes blind reads 0 sites against 632 pinned +// pairs and reds as stale. The id regex itself is shared with the spec leg, +// whose per-bucket floors guard it independently. If the baseline is ever +// burned to empty, add an explicit seen-floor here in the same PR — at that +// point the stale arm can no longer catch a dormant walker. +// +// ## What this leg deliberately does NOT do +// +// It does not judge packages/spec (the position-based rule owns it, with the +// stricter no-exemption regime), test/spec/bench files (internal audience, +// and two deliberate test twins pin id-bearing text as range evidence), or +// comments (the prescribed destination for the ids this rule strips). +const PACKAGES_PROSE_ROOT = 'packages'; +const PACKAGES_PROSE_EXCLUDED = 'packages/spec'; +const PACKAGES_PROSE_LEDGER = 'scripts/doc-authoring-prose-id.baseline.json'; + +/** + * Cheap byte-level prefilter: a SUPERSET of {@link INTERNAL_ID} (no + * lookarounds), deliberately non-global so `.test()` is stateless. A file this + * does not match cannot contain a string the real regex matches, so it is + * skipped unparsed — the difference is ~4s vs the whole tree parsed for + * nothing. The superset property is pinned in --self-test. + */ +const PACKAGES_PROSE_PREFILTER = /#[0-9]{3,5}/; + +/** Non-test TS sources of every sibling package, spec's subtree excluded. */ +function collectPackageProseFiles(root = PACKAGES_PROSE_ROOT) { + assertRootsResolvable([root]); + const files = []; + (function descend(dir) { + for (const e of readdirSync(dir)) { + if (e === 'node_modules' || e === '.git' || e === 'dist' || e === '.turbo') continue; + const p = join(dir, e); + if (posix(p) === PACKAGES_PROSE_EXCLUDED) continue; + if (statSync(p).isDirectory()) descend(p); + else if (/\.m?tsx?$/.test(e) && !/\.(test|spec|bench)\.m?tsx?$/.test(e)) files.push(posix(p)); + } + })(root); + if (files.length === 0) throw new EmptyRootError([root], 0); + return files.sort(); +} + +/** + * A rough consumer classification for ONE literal, census output only — never + * enforcement. The climb records what consumes the string (a throw, a callee, + * a property key) so a triage reading the census can bucket sites by audience + * family; the LEDGER stays classification-free on purpose (a stored verdict + * would drift, a recomputed shape cannot). + */ +function packageProseShape(node, ts) { + let thrown = false; + let consumer = ''; + const props = []; + let cur = node; + for (let hops = 0; cur.parent && hops < 120; hops++) { + const p = cur.parent; + if (ts.isThrowStatement(p)) { thrown = true; break; } + if (ts.isPropertyAssignment(p) && p.initializer === cur) { props.push(p.name.getText()); cur = p; continue; } + if (ts.isCallExpression(p)) { + if (!consumer && p.arguments.includes(cur)) consumer = `call:${calleeName(p, ts)}`; + cur = p; continue; + } + if (ts.isNewExpression(p)) { if (!consumer) consumer = `new:${p.expression.getText().slice(0, 40)}`; cur = p; continue; } + if (ts.isVariableDeclaration(p)) { if (!consumer) consumer = `const:${p.name.getText()}`; cur = p; continue; } + if (ts.isSourceFile(p) || isFunctionLike(p, ts)) break; + cur = p; + } + return { thrown, consumer, prop: props.length ? props[props.length - 1] : '' }; +} + +/** + * Every INTERNAL_ID occurrence in every string literal of the given sources. + * + * Template expressions are matched on their FULL source text and never + * descended into — an id inside an embedded expression is covered by the + * outer `getText()`, and visiting the inner literal too would double-count + * it, which a ratchet compared run-to-run cannot afford. Deterministic by + * construction: same tree, same counts. + * + * @returns {{sites: Array, counts: Map>, + * stringsSeen: number, filesParsed: number}} + */ +function scanPackageProseIds(files, ts) { + const sites = []; + const counts = new Map(); + let stringsSeen = 0; + let filesParsed = 0; + for (const file of files) { + const source = readFileSync(file, 'utf8'); + if (!PACKAGES_PROSE_PREFILTER.test(source)) continue; + const sf = parseSourceFile(file, source); + filesParsed += 1; + const visit = (node) => { + if ( + ts.isStringLiteral(node) + || ts.isNoSubstitutionTemplateLiteral(node) + || ts.isTemplateExpression(node) + ) { + stringsSeen += 1; + const text = node.getText(sf); + const ids = text.match(INTERNAL_ID); + if (ids) { + const shape = packageProseShape(node, ts); + sites.push({ + file, + line: sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1, + ids, + ...shape, + text: text.replace(/\s+/g, ' ').slice(0, 160), + }); + const m = counts.get(file) ?? new Map(); + counts.set(file, m); + for (const id of ids) m.set(id, (m.get(id) ?? 0) + 1); + } + return; // never descend: see the docblock. + } + ts.forEachChild(node, visit); + }; + ts.forEachChild(sf, visit); + } + return { sites, counts, stringsSeen, filesParsed }; +} + +/** `counts` as the baseline's JSON shape: {file: {id: n}}, both levels sorted. */ +function packageProseLedgerShape(counts) { + const out = {}; + for (const file of [...counts.keys()].sort()) { + out[file] = Object.fromEntries([...counts.get(file)].sort(([a], [b]) => (a < b ? -1 : 1))); + } + return out; +} + +/** Growth and shrink of the measured counts against the pinned baseline. */ +function comparePackageProseLedger(counts, ledger) { + const growth = []; + const stale = []; + const files = new Set([...Object.keys(ledger), ...counts.keys()]); + for (const file of [...files].sort()) { + const actual = counts.get(file) ?? new Map(); + const pinned = ledger[file] ?? {}; + const ids = new Set([...Object.keys(pinned), ...actual.keys()]); + for (const id of [...ids].sort()) { + const a = actual.get(id) ?? 0; + const p = pinned[id] ?? 0; + if (a > p) growth.push({ file, id, actual: a, pinned: p }); + else if (a < p) stale.push({ file, id, actual: a, pinned: p }); + } + } + return { growth, stale }; +} + const posix = (p) => p.split(sep).join('/'); function walk(dir, out) { @@ -1503,6 +1717,7 @@ function selfTest() { } selfTestRule3(expect); + selfTestPackagesProse(expect); } finally { process.chdir(cwd); rmSync(dir, { recursive: true, force: true }); @@ -1516,13 +1731,16 @@ function selfTest() { // missing from the brief — which is exactly how it stood before this block. // Both sides are derived from ROOTS rather than re-spelled, so renaming or // widening a root cannot leave the declaration describing the old population. - const separatorless = ROOTS.filter((r) => !r.includes('/')); - expect('the declaration exists for every ROOT the hint extractor cannot see (a root with no ' - + 'path separator is refused as too generic, so it needs the subtree spelling)', + // The walked roots are the four Markdown ROOTS plus the cross-package leg's + // — both sides of the derivation below read the same constants the scans do. + const walkedRoots = [...ROOTS, PACKAGES_PROSE_ROOT]; + const separatorless = walkedRoots.filter((r) => !r.includes('/')); + expect('the declaration exists for every walked root the hint extractor cannot see (a root with ' + + 'no path separator is refused as too generic, so it needs the subtree spelling)', separatorless.every((r) => ROOT_WATCH_HINTS.includes(`${r}/**`)), true); expect('and it declares no root this gate does not walk (a declaration that can drift from the ' + 'scan is worse than none — it replaces a silent gate with a lying one)', - ROOT_WATCH_HINTS.every((h) => ROOTS.includes(h.replace(/\/\*+$/, ''))), true); + ROOT_WATCH_HINTS.every((h) => walkedRoots.includes(h.replace(/\/\*+$/, ''))), true); // Provenance, never a lookup key: the glob form appearing in ROOTS would send // `walk()` at a directory that does not exist. Since #4916 that is a hard // refusal rather than a silent skip, but it fails naming the wrong problem. @@ -1561,7 +1779,7 @@ function selfTest() { console.error(`\n✗ check-doc-authoring self-test failed:\n${failures.join('\n')}\n`); process.exit(1); } - console.log('✓ check-doc-authoring self-test: scope wiring (.claude and the live docs/ corpus in, .claude/worktrees and docs/{audits,handoff,plans} out), detection, the dead-root hard error (red when a ROOT is renamed, green when restored), the empty-scan hard error (red when a root yields nothing and when the whole scan does, green when restored), the published-catalog internal-id rule (red on a planted id in prose, in a fenced comment and in the repo#NNNN spelling, green when removed; hex colours, version numbers, HTTP codes, array indices and the "#1" ordinal all pass; references/ reached, generated artifacts and the internal roots out; the `#` placeholder passes while the concrete ids it replaced stay red, with no exemption to reach for), the spec customer-facing-text internal-id rule (red on an id planted on a LATER line of a concatenated message — the shape a line-oriented census cannot see, proven here — and in a template chain, a positional validator message, the repo#NNNN spelling, a nested strictObject `guidance` prescription, a HOISTED guidance const, a `KeySetGuidance` const consumed only CROSS-MODULE in both the annotated and the `as const satisfies` spelling, a HOISTED refusal message, a `retiredKey()` tombstone, `new Map` and `Object.freeze` guidance tables, `.describe()` prose, and the nested `guidance` of a whole options table written `satisfies StrictObjectOptions`; green when removed; an ADR id on a tombstone, a `.default()` VALUE, `history`/`guidance` outside a strictObject options position, `extraKeys` key names and an inferred local that merely MENTIONS `KeySetGuidance` all pass; test bodies out; the seen floor is PER BUCKET so one matcher rotting while the others carry the total still reds; and the two TYPE ANCHORS are pinned on the predicate itself — the annotation, `satisfies` and `as const satisfies` spellings all read as a strictObject options position while some other satisfied type does not, and the `*_STRICT_OPTIONS` NAME branch still fires where no type is written at all — which is the only place they can be told apart, since end to end they are redundant), the fourth population — customer-facing text BUILT INSIDE A FUNCTION (red on an id in an inline `error: () =>` callback, in a const the callback only dispatches to, inside a `message:` builder function, RETURNED from a tombstone-prescription builder, in a `: StrictObjectOptions` options factory, and in a plain `error:` string; ⛔ the body of an ordinary helper and a local inside a recognised factory stay unswept, because the climb crosses a function only when the FUNCTION sits in a recognised position; and `functionBuilt` carries its own blindness floor, since an unrecognised spelling produces no flag SILENTLY), the FIFTH population — prose built inside plain `function` DECLARATIONS (#13156: red on an id in a declaration consumed by `message:`, RETURNED to a `retiredKey()` argument, and in a const the declaration only dispatches to; its own `functionDeclared` bucket with its own floor, so the declaration clause rotting cannot hide behind the arrows; ⛔ an unconsumed declaration and one consumed only by an unrecognised call stay unswept — the clause is the fourth population\'s, one declaration form over, never an unconditional crawl), the Rule 3 boundary OUTPUT (names the scanned root, says sibling packages are not scanned, and lists every floored bucket — derived from the same constants the scan reads) and the dispatch-gates declaration (every separator-less ROOT declared as a subtree, nothing declared this gate does not walk, the over-claim bounded to SKIP_PATHS) all hold.'); + console.log('✓ check-doc-authoring self-test: scope wiring (.claude and the live docs/ corpus in, .claude/worktrees and docs/{audits,handoff,plans} out), detection, the dead-root hard error (red when a ROOT is renamed, green when restored), the empty-scan hard error (red when a root yields nothing and when the whole scan does, green when restored), the published-catalog internal-id rule (red on a planted id in prose, in a fenced comment and in the repo#NNNN spelling, green when removed; hex colours, version numbers, HTTP codes, array indices and the "#1" ordinal all pass; references/ reached, generated artifacts and the internal roots out; the `#` placeholder passes while the concrete ids it replaced stay red, with no exemption to reach for), the spec customer-facing-text internal-id rule (red on an id planted on a LATER line of a concatenated message — the shape a line-oriented census cannot see, proven here — and in a template chain, a positional validator message, the repo#NNNN spelling, a nested strictObject `guidance` prescription, a HOISTED guidance const, a `KeySetGuidance` const consumed only CROSS-MODULE in both the annotated and the `as const satisfies` spelling, a HOISTED refusal message, a `retiredKey()` tombstone, `new Map` and `Object.freeze` guidance tables, `.describe()` prose, and the nested `guidance` of a whole options table written `satisfies StrictObjectOptions`; green when removed; an ADR id on a tombstone, a `.default()` VALUE, `history`/`guidance` outside a strictObject options position, `extraKeys` key names and an inferred local that merely MENTIONS `KeySetGuidance` all pass; test bodies out; the seen floor is PER BUCKET so one matcher rotting while the others carry the total still reds; and the two TYPE ANCHORS are pinned on the predicate itself — the annotation, `satisfies` and `as const satisfies` spellings all read as a strictObject options position while some other satisfied type does not, and the `*_STRICT_OPTIONS` NAME branch still fires where no type is written at all — which is the only place they can be told apart, since end to end they are redundant), the fourth population — customer-facing text BUILT INSIDE A FUNCTION (red on an id in an inline `error: () =>` callback, in a const the callback only dispatches to, inside a `message:` builder function, RETURNED from a tombstone-prescription builder, in a `: StrictObjectOptions` options factory, and in a plain `error:` string; ⛔ the body of an ordinary helper and a local inside a recognised factory stay unswept, because the climb crosses a function only when the FUNCTION sits in a recognised position; and `functionBuilt` carries its own blindness floor, since an unrecognised spelling produces no flag SILENTLY), the FIFTH population — prose built inside plain `function` DECLARATIONS (#13156: red on an id in a declaration consumed by `message:`, RETURNED to a `retiredKey()` argument, and in a const the declaration only dispatches to; its own `functionDeclared` bucket with its own floor, so the declaration clause rotting cannot hide behind the arrows; ⛔ an unconsumed declaration and one consumed only by an unrecognised call stay unswept — the clause is the fourth population\'s, one declaration form over, never an unconditional crawl), the Rule 3 boundary OUTPUT (names the position-based root AND the ledgered cross-package leg\'s root, exclusion and baseline, no longer claims siblings are unscanned, and lists every floored bucket — derived from the same constants the scans read), the CROSS-PACKAGE prose-id leg (#13297: a concatenation-split id in a plain helper is counted — total-string coverage, no position climb to rot; a `//` comment, a test body and the spec subtree are out; an id inside a template\'s embedded expression counts exactly once; a 6-digit colour never matches while the cross-repo spelling\'s id half does; the prefilter is a superset of the id regex on every counted site; and the ledger arithmetic answers all three verdicts from one measurement — exact baseline green, empty baseline all-growth, over-pinned baseline stale without invented growth) and the dispatch-gates declaration (every separator-less walked root declared as a subtree — `packages/**` included since #13297 — nothing declared this gate does not walk, the over-claim bounded to SKIP_PATHS) all hold.'); } /** @@ -2374,8 +2592,15 @@ function selfTestRule3(expect) { const boundary = scanBoundaryLines().join('\n'); expect('the boundary output names Rule 3\'s scanned root', boundary.includes(SPEC_SOURCE_ROOT), true); - expect('the boundary output says sibling packages are NOT scanned', - boundary.includes('SIBLING PACKAGES is NOT scanned'), true); + // #13297 flipped the deferral: the boundary now STATES the sibling + // packages are covered by the ledgered leg, and names root and baseline + // derived from the same constants that leg reads — never re-spelled. + expect('the boundary output no longer claims sibling packages are unscanned', + boundary.includes('is NOT scanned'), false); + expect('the boundary output names the cross-package leg\'s root', + boundary.includes(`${PACKAGES_PROSE_ROOT}/`), true); + expect('...its exclusion', boundary.includes(`${PACKAGES_PROSE_EXCLUDED}/`), true); + expect('...and its baseline', boundary.includes(PACKAGES_PROSE_LEDGER), true); expect('the boundary output names every bucket the per-bucket floor guards', Object.keys(r.seen).every((b) => boundary.includes(b)), true); expect('...and the strictObject option keys, derived from the same set the scan reads', @@ -2395,8 +2620,152 @@ function selfTestRule3(expect) { } } +// The #8435 authority token, verbatim — check-ratchet-remedy-authority.mjs +// sweeps author-facing text for an offer that expands a registry and requires +// this marker (or an outright refusal) beside it. The prose-id baseline below +// is exactly such a registry: shrink is the landing author's to take, growth +// is not. +const RATCHET_AUTHORITY_MARKER = '⛔ MAINTAINER-ONLY'; + +/** + * `--census`: the #13297 instrument. Prints every id-bearing string site of + * the cross-package leg as JSON — file, line, ids, consumer shape — for + * triage to bucket by audience. `--census-ledger` prints the baseline shape + * instead, and REFUSES to print one that grows any (file,id) pair beyond the + * checked-in baseline: regeneration exists for burn-down, and a grown pair + * arriving through a blind regen is the one thing diff review reliably + * misses. (Bootstrap — no baseline on disk yet — prints freely.) + */ +function census(ledgerOnly) { + const ts = requireFromHere('typescript'); + const { sites, counts, stringsSeen, filesParsed } = scanPackageProseIds(collectPackageProseFiles(), ts); + if (!ledgerOnly) { + console.log(JSON.stringify({ + root: PACKAGES_PROSE_ROOT, + excluded: PACKAGES_PROSE_EXCLUDED, + filesParsed, + stringsSeen, + sites, + }, null, 1)); + return; + } + const shape = packageProseLedgerShape(counts); + // A BLANK file is the bootstrap/regeneration path itself: `--census-ledger > + // baseline` truncates the target before node runs, so "exists but empty" + // means this very command's own redirection, not a baseline to defend. + const onDisk = existsSync(PACKAGES_PROSE_LEDGER) ? readFileSync(PACKAGES_PROSE_LEDGER, 'utf8') : ''; + if (onDisk.trim() !== '') { + const pinned = JSON.parse(onDisk); + const { growth } = comparePackageProseLedger(counts, pinned); + if (growth.length > 0) { + console.error( + `\n✗ --census-ledger refused: the regenerated baseline would GROW ${growth.length} ` + + `(file,id) pair(s) beyond ${PACKAGES_PROSE_LEDGER}:\n`, + ); + for (const g of growth) console.error(` ${g.file} ${g.id} ${g.pinned} -> ${g.actual}`); + console.error( + `\nRegeneration exists for BURN-DOWN. Strip the new id(s) first — or, for prose a` + + `\nmaintainer has adjudicated ops-facing, ${RATCHET_AUTHORITY_MARKER}: hand-edit the` + + `\nbaseline entry. That file is otherwise shrink-only.\n`, + ); + process.exit(1); + return; + } + } + console.log(JSON.stringify(shape, null, 1)); +} + +/** + * Rule 3b's red/green battery, over a real temporary `packages/` tree — the + * walker, the prefilter, the count determinism and the ledger arithmetic are + * each asserted from the real entry points, never re-derived. Runs with cwd + * already inside {@link selfTest}'s temp dir, after the Rule 3 battery (which + * leaves `packages/spec/src` behind — deliberately reused here as the + * exclusion case). + */ +function selfTestPackagesProse(expect) { + const ts = requireFromHere('typescript'); + const write = (rel, body) => { + const full = join(...rel.split('/')); + mkdirSync(dirname(full), { recursive: true }); + writeFileSync(full, body); + return full; + }; + const scan = () => scanPackageProseIds(collectPackageProseFiles(), ts); + + // RED — the census's founding shape: a concatenation-split message whose id + // sits on a LATER line than any `message:`/callee anchor, inside a plain + // helper the spec leg's position climb would never recognise. Total-string + // coverage is the whole point of this leg: the site is counted anyway. + write('packages/widgets/src/refuse.ts', [ + "export function refuseWidget(kind: string): string {", + " return 'The widget kind \"' + kind + '\" was retired ' +", + " 'and is refused (#4242).';", + "}", + ].join('\n')); + // A comment is the SANCTIONED home for an internal anchor — never counted. + write('packages/widgets/src/ops.ts', [ + '// #4242 explains this workaround, and belongs exactly here.', + 'export const N = 1;', + ].join('\n')); + // Test bodies are out (two deliberate test twins pin id-bearing text as + // range evidence, and their audience is internal). + write('packages/widgets/src/pin.test.ts', "export const T = 'pinned (#4242)';\n"); + // The spec subtree belongs to the position-based rule, not this leg. + write('packages/spec/src/data/excluded.ts', "export const S = 'spec-only (#4242)';\n"); + // Determinism: an id inside a template's EMBEDDED expression is covered by + // the outer getText() and must be counted exactly ONCE. + write('packages/widgets/src/tmpl.ts', + "export const T = (flag: boolean) => `outer ${flag ? 'inner (#5151)' : ''} tail`;\n"); + // A 6-digit colour never matches; the cross-repo spelling matches its #NNNN. + write('packages/widgets/src/edge.ts', [ + "export const COLOUR = '#123456';", + "export const CROSS = 'tracked in objectui#7777';", + "export const THROWN = () => { throw new Error('nope (#6161)'); };", + ].join('\n')); + + const r = scan(); + const byFile = (f) => r.sites.filter((s) => s.file === f); + expect('a concatenation-split id in a plain helper is counted', byFile('packages/widgets/src/refuse.ts').length, 1); + expect('...and the recorded id is the one planted', byFile('packages/widgets/src/refuse.ts')[0]?.ids.join(','), '#4242'); + expect('a `//` comment is never counted — it is the remedy, not a violation', + byFile('packages/widgets/src/ops.ts').length, 0); + expect('test bodies are not scanned', byFile('packages/widgets/src/pin.test.ts').length, 0); + expect('the spec subtree is excluded from this leg', byFile('packages/spec/src/data/excluded.ts').length, 0); + expect('an id inside a template\'s embedded expression is counted exactly once', + r.counts.get('packages/widgets/src/tmpl.ts')?.get('#5151'), 1); + expect('a 6-digit colour does not match', r.counts.get('packages/widgets/src/edge.ts')?.has('#12345'), false); + expect('the cross-repo spelling matches its id half', r.counts.get('packages/widgets/src/edge.ts')?.get('#7777'), 1); + expect('a thrown literal records the throw in its census shape', + byFile('packages/widgets/src/edge.ts').find((s) => s.ids.includes('#6161'))?.thrown, true); + expect('the prefilter is a SUPERSET of the id regex on every counted site', + r.sites.every((s) => PACKAGES_PROSE_PREFILTER.test(s.text)), true); + expect('strings were seen at all (the walker is not dormant)', r.stringsSeen > 0, true); + + // ── The ledger arithmetic, all three verdicts from one measurement ──────── + const exact = packageProseLedgerShape(r.counts); + const green = comparePackageProseLedger(r.counts, exact); + expect('an exact baseline is green — no growth', green.growth.length, 0); + expect('an exact baseline is green — no staleness', green.stale.length, 0); + + const empty = comparePackageProseLedger(r.counts, {}); + expect('every measured (file,id) pair is GROWTH against an empty baseline', + empty.growth.length, [...r.counts.values()].reduce((a, m) => a + m.size, 0)); + expect('...and none of it reads as staleness', empty.stale.length, 0); + + const overPinned = JSON.parse(JSON.stringify(exact)); + overPinned['packages/widgets/src/gone.ts'] = { '#9999': 2 }; + const stale = comparePackageProseLedger(r.counts, overPinned); + expect('a pinned pair the tree no longer carries is STALE (the burn-down/blindness arm)', + stale.stale.length, 1); + expect('...named precisely', stale.stale[0]?.file, 'packages/widgets/src/gone.ts'); + expect('...without inventing growth', stale.growth.length, 0); +} + function main() { if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--census')) return census(false); + if (process.argv.includes('--census-ledger')) return census(true); let files; try { @@ -2474,6 +2843,31 @@ function main() { const blindBuckets = Object.keys(seenByBucket).filter((b) => seenByBucket[b] === 0); const totalTextSeen = Object.values(seenByBucket).reduce((a, b) => a + b, 0); + // ── Rule 3b: the cross-package ledgered leg ─────────────────────────────── + if (!existsSync(PACKAGES_PROSE_LEDGER)) { + console.error( + `\n✗ doc authoring guard: the prose-id baseline (${PACKAGES_PROSE_LEDGER}) is missing,` + + `\nso the cross-package leg has nothing to hold its measurement against. Restore it` + + `\nfrom git — deleting the baseline is not a remedy this gate offers.\n`, + ); + process.exit(1); + return; + } + let packageProse; + try { + packageProse = scanPackageProseIds(collectPackageProseFiles(), ts); + } catch (err) { + console.error( + `\n✗ doc authoring guard: the sibling-package root (${PACKAGES_PROSE_ROOT}/) could not be` + + `\nscanned for internal issue-id references in string prose, so this run cannot vouch` + + `\nfor it:\n\n ${err.message}\n`, + ); + process.exit(1); + return; + } + const prosePinned = JSON.parse(readFileSync(PACKAGES_PROSE_LEDGER, 'utf8')); + const { growth: proseGrowth, stale: proseStale } = comparePackageProseLedger(packageProse.counts, prosePinned); + // The boundary is stated on EVERY verdict, red or green — a reader of the // green line must be able to see what the clean bill covers, and a reader of // a red must be able to see what a fix inside the root cannot have swept. @@ -2578,6 +2972,46 @@ function main() { ); } + if (proseGrowth.length > 0) { + failed = true; + console.error(`\n✗ NEW internal issue-id reference(s) in sibling-package string prose:\n`); + for (const g of proseGrowth) { + console.error(` ${g.file} ${g.id} (${g.pinned} pinned, ${g.actual} measured)`); + for (const s of packageProse.sites.filter((s2) => s2.file === g.file && s2.ids.includes(g.id))) { + console.error(` :${s.line} ${s.text}`); + } + } + console.error( + `\n${proseGrowth.length} (file,id) pair(s) above ${PACKAGES_PROSE_LEDGER}. A runtime string` + + `\nreaches authors, operators and generated surfaces — none of whom can resolve \`#NNNN\`` + + `\n(no tracker, no git log, no ADRs). Strip the id from the string, or move it to an` + + `\nadjacent \`//\` comment — the reader who CAN resolve it reads the source, and git` + + `\nhistory keeps the anchor either way. A 3-digit all-numeric CSS colour tripped this?` + + `\nSpell it 6-digit. Keep customer-resolvable references (an ADR id, a protocol version,` + + `\nan error code) — only the tracker id goes.` + + `\n\nAdding a baseline entry instead is ${RATCHET_AUTHORITY_MARKER}, NOT a co-equal option:` + + `\nthat file pins the adjudicated pre-#13297 population and is otherwise shrink-only, so` + + `\nan entry weakens a ratchet and needs a maintainer to agree first — do not take this` + + `\npath to get CI green.` + + `\n\nMaintainer ruling 2026-08-12, verbatim: 「处理 issue 时犯的错应该总结成经验,保留 issue id没有意义」\n`, + ); + } + + if (proseStale.length > 0) { + failed = true; + console.error(`\n✗ doc authoring guard: the prose-id baseline is STALE — pinned entries exceed the tree:\n`); + for (const s of proseStale) console.error(` ${s.file} ${s.id} (${s.pinned} pinned, ${s.actual} measured)`); + console.error( + `\n${proseStale.length} (file,id) pair(s) in ${PACKAGES_PROSE_LEDGER} now over-pin the tree.` + + `\nThis is the ratchet recording burn-down — regenerate the baseline in this same PR:` + + `\n\n node scripts/check-doc-authoring.mjs --census-ledger > ${PACKAGES_PROSE_LEDGER}` + + `\n\n(That command refuses to GROW the baseline, so it is safe to run as the shrink` + + `\nremedy. While the baseline is non-empty this staleness check doubles as the leg's` + + `\nblindness floor: a walker that goes blind measures 0 against every pinned pair and` + + `\nlands here rather than printing green.)\n`, + ); + } + if (failed) process.exit(1); console.log(`✓ doc authoring guard: ${files.length} files clean — no bare metadata literals.`); @@ -2589,6 +3023,12 @@ function main() { + `tombstone ${seenByBucket.tombstone} · describe ${seenByBucket.describe} · ` + `functionBuilt ${seenByBucket.functionBuilt} · functionDeclared ${seenByBucket.functionDeclared}).`, ); + console.log( + `✓ doc authoring guard: sibling-package prose ids hold the baseline — ` + + `${packageProse.sites.length} pinned site(s) across ${packageProse.counts.size} file(s), ` + + `${packageProse.stringsSeen} string(s) read in ${packageProse.filesParsed} parsed source(s), ` + + `no growth, no burn-down unrecorded.`, + ); } main(); diff --git a/scripts/check-ratchet-remedy-authority.mjs b/scripts/check-ratchet-remedy-authority.mjs index a41147efe9..ba1c6a49a4 100644 --- a/scripts/check-ratchet-remedy-authority.mjs +++ b/scripts/check-ratchet-remedy-authority.mjs @@ -711,8 +711,8 @@ const CONTROL = { why: 'CROSS_PACKAGE_TEST_INPUTS declares which globs a package tests read. A naive-prototype false positive.', }, 'check-doc-authoring.mjs': { - expect: 'excluded', - why: 'Carries no ratchet at all. ROOTS, SKIP_PATHS and SKIP_FILES are declarations of what the two corpus rules read and what is generated rather than authored; each is path-scoped, and none is offered to the author as something to widen. Its published-catalog id rule reached this corpus by first shipping a per-passage allowlist and being turned red here for offering it — maintainer ruling 2026-08-25 replaced that passage with a placeholder and removed the list. If this flips, that list came back.', + expect: 'marked', + why: 'Joined the convention on #13297: its cross-package prose-id leg holds measured counts against scripts/doc-authoring-prose-id.baseline.json, and the growth red offers the baseline path marked in the same breath — shrink (burn-down regeneration) stays the landing author\'s, growth is a maintainer\'s. Its published-catalog and spec rules still refuse exemptions outright, unchanged since the 2026-08-25 placeholder ruling replaced the one per-passage allowlist this rule ever shipped.', }, 'check-error-code-casing.mjs': { expect: 'excluded', diff --git a/scripts/doc-authoring-prose-id.baseline.json b/scripts/doc-authoring-prose-id.baseline.json new file mode 100644 index 0000000000..0b9441cf44 --- /dev/null +++ b/scripts/doc-authoring-prose-id.baseline.json @@ -0,0 +1,1096 @@ +{ + "packages/cli/src/commands/compile.ts": { + "#3366": 1, + "#3786": 1 + }, + "packages/cli/src/commands/db/clean.ts": { + "#6469": 1 + }, + "packages/cli/src/commands/dev.ts": { + "#5148": 1 + }, + "packages/cli/src/commands/doctor.ts": { + "#2377": 1, + "#5397": 1, + "#5673": 1 + }, + "packages/cli/src/commands/i18n/extract.ts": { + "#11671": 1 + }, + "packages/cli/src/commands/meta/resync.ts": { + "#2705": 1, + "#8692": 1 + }, + "packages/cli/src/commands/migrate/duplicates.ts": { + "#8686": 1, + "#8725": 1, + "#8928": 1 + }, + "packages/cli/src/commands/migrate/multi-value-columns.ts": { + "#11535": 1 + }, + "packages/cli/src/commands/migrate/recorded-by.ts": { + "#4556": 1 + }, + "packages/cli/src/commands/migrate/summary-nulls.ts": { + "#5749": 1 + }, + "packages/cli/src/commands/serve.ts": { + "#111": 1, + "#3963": 1, + "#444": 1, + "#4719": 1, + "#4818": 1, + "#666": 1 + }, + "packages/cli/src/commands/storage/orphans.ts": { + "#10950": 1 + }, + "packages/cli/src/commands/validate.ts": { + "#3366": 1 + }, + "packages/cli/src/utils/i18n-extract.ts": { + "#11671": 1, + "#12069": 1, + "#8765": 1 + }, + "packages/cli/src/utils/storage-driver.ts": { + "#3276": 2 + }, + "packages/cli/test/helpers/serve-process.ts": { + "#12525": 1, + "#13062": 1 + }, + "packages/cloud-connection/src/cloud-connection-route-ledger.ts": { + "#11863": 1, + "#8976": 4, + "#9011": 1 + }, + "packages/connectors/connector-mcp/src/mcp-provider.ts": { + "#3055": 2 + }, + "packages/core/src/health-monitor.ts": { + "#12032": 3 + }, + "packages/core/src/metadata-service-contract.ts": { + "#7378": 2 + }, + "packages/core/src/plugin-order.ts": { + "#4131": 3 + }, + "packages/core/src/plugin-registration.ts": { + "#9864": 1 + }, + "packages/core/src/security/admin-standing-surface.ts": { + "#11663": 1, + "#11970": 1, + "#8613": 1 + }, + "packages/core/src/security/authz-cache-posture.ts": { + "#4785": 1 + }, + "packages/drivers/driver-memory/src/filter-refusal.ts": { + "#10413": 1, + "#3948": 2, + "#4286": 1, + "#5158": 1, + "#5240": 1, + "#5324": 2, + "#5328": 1, + "#5345": 2, + "#5347": 1 + }, + "packages/drivers/driver-mongodb/src/mongodb-aggregation.ts": { + "#10413": 1, + "#12818": 1, + "#5907": 3, + "#6188": 1, + "#6212": 1 + }, + "packages/drivers/driver-mongodb/src/mongodb-filter.ts": { + "#5158": 1, + "#5240": 1, + "#5346": 2, + "#5347": 1 + }, + "packages/drivers/driver-mongodb/src/test-mongod.ts": { + "#5517": 3 + }, + "packages/drivers/driver-sql/src/cross-field-conformance-cases.ts": { + "#1051": 1, + "#5041": 1, + "#5146": 2, + "#7398": 1, + "#7597": 1 + }, + "packages/drivers/driver-sql/src/live-dialect-matrix.testkit.ts": { + "#9350": 1 + }, + "packages/drivers/driver-sql/src/schema-drift.ts": { + "#11535": 1, + "#12121": 1, + "#3696": 1, + "#4560": 1 + }, + "packages/drivers/driver-sql/src/sql-driver.ts": { + "#10413": 1, + "#11067": 1, + "#11374": 2, + "#11541": 1, + "#11565": 1, + "#11567": 1, + "#11627": 7, + "#12380": 1, + "#12999": 1, + "#13015": 3, + "#13056": 3, + "#3896": 1, + "#3912": 1, + "#3942": 1, + "#3994": 2, + "#4363": 1, + "#5030": 5, + "#5158": 1, + "#5240": 2, + "#5347": 2, + "#5348": 1, + "#5369": 1, + "#5907": 2, + "#6050": 1, + "#6212": 1, + "#6409": 1, + "#7398": 2, + "#7929": 7, + "#8197": 5, + "#8220": 1, + "#8790": 1, + "#8931": 1 + }, + "packages/drivers/driver-turso/src/remote-canonical-backfill.ts": { + "#1005": 1, + "#5770": 1 + }, + "packages/drivers/driver-turso/src/remote-transport.ts": { + "#1003": 1, + "#1004": 6, + "#10413": 1, + "#1051": 1, + "#1058": 6, + "#1066": 3, + "#1073": 2, + "#1075": 1, + "#1076": 1, + "#1116": 1, + "#5347": 2, + "#5348": 2, + "#5368": 1, + "#5369": 1, + "#5769": 3, + "#5903": 1, + "#5907": 2, + "#6050": 1, + "#6212": 1, + "#6409": 1, + "#7929": 1, + "#8197": 1 + }, + "packages/drivers/driver-turso/src/turso-driver.ts": { + "#6944": 1, + "#7099": 1 + }, + "packages/formula/src/matches-filter.ts": { + "#5240": 1 + }, + "packages/lint/src/authoring-rules.ts": { + "#4463": 4, + "#4716": 2, + "#4717": 1, + "#7220": 2, + "#8309": 2, + "#8310": 2, + "#9698": 1 + }, + "packages/lint/src/data-model-rules.ts": { + "#5030": 1, + "#5082": 1 + }, + "packages/lint/src/lint-flow-patterns.ts": { + "#1315": 2, + "#1870": 1, + "#1874": 2, + "#1888": 1, + "#3760": 1, + "#3801": 1, + "#3810": 1, + "#3823": 1, + "#3863": 1, + "#4414": 6, + "#5393": 1, + "#5482": 1 + }, + "packages/lint/src/lint-liveness-properties.ts": { + "#11384": 1 + }, + "packages/lint/src/lint-startup-registry-verdict.ts": { + "#4769": 1, + "#4771": 7, + "#4772": 3 + }, + "packages/lint/src/validate-action-body-writes.ts": { + "#4271": 1, + "#4345": 1 + }, + "packages/lint/src/validate-approval-approvers.ts": { + "#3424": 1, + "#3508": 1 + }, + "packages/lint/src/validate-component-props.ts": { + "#5068": 1 + }, + "packages/lint/src/validate-dashboard-action-refs.ts": { + "#6739": 1 + }, + "packages/lint/src/validate-empty-combinators.ts": { + "#5134": 1, + "#5297": 1, + "#5322": 3, + "#5330": 1 + }, + "packages/lint/src/validate-expressions.ts": { + "#4343": 1, + "#4889": 2, + "#4977": 1, + "#6010": 2, + "#6146": 1 + }, + "packages/lint/src/validate-flow-template-paths.ts": { + "#3475": 2, + "#3810": 3 + }, + "packages/lint/src/validate-flow-trigger-readiness.ts": { + "#3427": 3, + "#3457": 1 + }, + "packages/lint/src/validate-hook-body-writes.ts": { + "#4271": 3 + }, + "packages/lint/src/validate-nav-object-servability.ts": { + "#7912": 1 + }, + "packages/lint/src/validate-null-guards.ts": { + "#4649": 2, + "#4763": 1, + "#4811": 1 + }, + "packages/lint/src/validate-predicate-path-refs.ts": { + "#4049": 2, + "#5149": 2, + "#6254": 1, + "#7696": 1 + }, + "packages/lint/src/validate-react-page-props.ts": { + "#11284": 1, + "#5583": 1 + }, + "packages/lint/src/validate-readonly-flow-writes.ts": { + "#2948": 1, + "#3042": 1 + }, + "packages/lint/src/validate-rule-schema-formats.ts": { + "#5029": 1 + }, + "packages/lint/src/validate-searchable-fields.ts": { + "#4254": 4, + "#6674": 1 + }, + "packages/lint/src/validate-security-posture.ts": { + "#2348": 1, + "#7474": 1 + }, + "packages/lint/src/validate-seed-state-machine.ts": { + "#3433": 1 + }, + "packages/lint/src/validate-sortable-fields.ts": { + "#6994": 3, + "#7095": 2 + }, + "packages/lint/src/validate-view-containers.ts": { + "#5320": 1 + }, + "packages/lint/src/validate-visibility-predicates.ts": { + "#5149": 3 + }, + "packages/lint/src/validate-widget-bindings.ts": { + "#2501": 1 + }, + "packages/metadata-core/src/contract-suite.ts": { + "#7856": 1 + }, + "packages/metadata-core/src/engine-delete-dispatch.ts": { + "#11009": 4 + }, + "packages/metadata-core/src/engine-dispatch-unhonoured-predicate.ts": { + "#11009": 1 + }, + "packages/metadata-core/src/engine-findone-predicate.ts": { + "#11767": 1, + "#3896": 1, + "#4346": 1, + "#4419": 1 + }, + "packages/metadata-core/src/engine-update-dispatch.ts": { + "#11009": 6, + "#11142": 5, + "#11230": 6, + "#5748": 4 + }, + "packages/metadata-core/src/object-schema-fls-contract.ts": { + "#3545": 1, + "#6603": 1, + "#7020": 1 + }, + "packages/metadata-protocol/src/migrations/live-mysql-database.testkit.ts": { + "#10382": 1 + }, + "packages/metadata-protocol/src/migrations/overlay-index.ts": { + "#6418": 3, + "#8725": 1 + }, + "packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts": { + "#8686": 9, + "#9261": 1, + "#9451": 4 + }, + "packages/metadata-protocol/src/migrations/sys-setting-identity-index.ts": { + "#8629": 4 + }, + "packages/metadata-protocol/src/migrations/view-definition-active-index.ts": { + "#5839": 3, + "#6417": 3, + "#8725": 1 + }, + "packages/metadata-protocol/src/plugin.ts": { + "#5839": 1, + "#8629": 1, + "#8686": 1 + }, + "packages/metadata-protocol/src/protocol.ts": { + "#10377": 1, + "#3043": 1, + "#3172": 1, + "#3493": 1, + "#3770": 1, + "#4196": 1, + "#4432": 1, + "#6190": 3, + "#6640": 1, + "#6992": 1, + "#7894": 2, + "#8502": 1, + "#8586": 1, + "#8957": 2 + }, + "packages/metadata-protocol/src/runtime-authoring-gate.ts": { + "#6153": 1 + }, + "packages/metadata-protocol/src/sys-metadata-repository.ts": { + "#4867": 1 + }, + "packages/metadata/src/endpoint-matcher.ts": { + "#5040": 1 + }, + "packages/metadata/src/loaders/database-loader.ts": { + "#4825": 1 + }, + "packages/metadata/src/metadata-manager.ts": { + "#5184": 1 + }, + "packages/metadata/src/metadata-route-ledger.ts": { + "#12140": 2 + }, + "packages/metadata/src/plugin.ts": { + "#4246": 1 + }, + "packages/objectql/src/engine.ts": { + "#2982": 2, + "#3171": 1, + "#3438": 2, + "#3617": 2, + "#4371": 1, + "#4419": 1, + "#4626": 1, + "#4639": 2, + "#4769": 2, + "#4797": 2, + "#5158": 2, + "#5351": 1, + "#7413": 1, + "#7922": 1 + }, + "packages/objectql/src/having-filter.ts": { + "#4286": 1 + }, + "packages/objectql/src/hook-binder.ts": { + "#4001": 1 + }, + "packages/objectql/src/hook-target-rebind-errors.ts": { + "#5574": 1, + "#5846": 1, + "#6752": 1, + "#9719": 1, + "#9974": 1 + }, + "packages/objectql/src/integrity/dangling-reference-audit.ts": { + "#4551": 1 + }, + "packages/objectql/src/lifecycle/lifecycle-settings.ts": { + "#5195": 1 + }, + "packages/objectql/src/plugin.ts": { + "#2462": 1 + }, + "packages/objectql/src/readonly-strict-errors.ts": { + "#3407": 1, + "#3493": 1 + }, + "packages/objectql/src/registry.ts": { + "#3543": 1 + }, + "packages/objectql/src/search-companion.ts": { + "#2486": 1 + }, + "packages/objectql/src/tenancy/system-write-organization.ts": { + "#8844": 1 + }, + "packages/objectql/src/validation/rule-validator.ts": { + "#2948": 1, + "#3407": 2, + "#3493": 2, + "#4649": 2, + "#5126": 2, + "#5503": 1 + }, + "packages/platform-objects/src/apps/translations/en.objects.generated.ts": { + "#7987": 3, + "#8149": 1 + }, + "packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts": { + "#7987": 3 + }, + "packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts": { + "#7987": 3 + }, + "packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts": { + "#7987": 3 + }, + "packages/platform-objects/src/audit/sys-email.object.ts": { + "#8149": 1 + }, + "packages/platform-objects/src/identity/sys-account.object.ts": { + "#7987": 3 + }, + "packages/platform-objects/src/identity/sys-scim-connection-credential.object.ts": { + "#3653": 1 + }, + "packages/platform-objects/src/plugin.ts": { + "#4795": 2 + }, + "packages/plugins/plugin-approvals/src/approval-service.ts": { + "#10153": 1, + "#10230": 1, + "#10547": 2, + "#3424": 1, + "#3447": 2, + "#3508": 1, + "#3807": 1, + "#3823": 1 + }, + "packages/plugins/plugin-approvals/src/sys-approval-action.object.ts": { + "#3266": 1, + "#3424": 1 + }, + "packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts": { + "#1322": 1 + }, + "packages/plugins/plugin-approvals/src/translations/en.objects.generated.ts": { + "#1322": 1, + "#3266": 1, + "#3424": 1 + }, + "packages/plugins/plugin-approvals/src/translations/es-ES.objects.generated.ts": { + "#1322": 1, + "#3266": 1, + "#3424": 1 + }, + "packages/plugins/plugin-approvals/src/translations/ja-JP.objects.generated.ts": { + "#1322": 1, + "#3266": 1, + "#3424": 1 + }, + "packages/plugins/plugin-approvals/src/translations/zh-CN.objects.generated.ts": { + "#1322": 1, + "#3266": 1, + "#3424": 1 + }, + "packages/plugins/plugin-audit/src/audit-writers.ts": { + "#5226": 1 + }, + "packages/plugins/plugin-audit/src/objects/sys-activity.object.ts": { + "#11507": 1 + }, + "packages/plugins/plugin-audit/src/translations/en.objects.generated.ts": { + "#11507": 1 + }, + "packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts": { + "#11507": 1 + }, + "packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts": { + "#11507": 1 + }, + "packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts": { + "#11507": 1 + }, + "packages/plugins/plugin-auth/src/adopt-membership.ts": { + "#7725": 1 + }, + "packages/plugins/plugin-auth/src/audience-posture.ts": { + "#5205": 1 + }, + "packages/plugins/plugin-auth/src/auth-manager.ts": { + "#3585": 1 + }, + "packages/plugins/plugin-auth/src/auth-plugin.ts": { + "#2780": 1, + "#4790": 1 + }, + "packages/plugins/plugin-auth/src/auth-route-ledger.ts": { + "#2766": 1, + "#7735": 2, + "#8019": 1, + "#9653": 2, + "#9941": 1 + }, + "packages/plugins/plugin-auth/src/internal-field-readback.ts": { + "#8118": 1 + }, + "packages/plugins/plugin-auth/src/last-admin-guard.ts": { + "#10982": 1, + "#11663": 1, + "#6084": 1 + }, + "packages/plugins/plugin-auth/src/member-role-canonical.ts": { + "#8317": 4 + }, + "packages/plugins/plugin-auth/src/objectql-adapter.ts": { + "#5813": 1, + "#5814": 1 + }, + "packages/plugins/plugin-auth/src/otp-send-guard.ts": { + "#4808": 1 + }, + "packages/plugins/plugin-auth/src/sso-client-secret.ts": { + "#8009": 1 + }, + "packages/plugins/plugin-auth/src/two-factor-reenrollment-verified-reset.ts": { + "#10700": 3 + }, + "packages/plugins/plugin-auth/src/walled-owner-verification-path.ts": { + "#12751": 1 + }, + "packages/plugins/plugin-dev/src/dev-plugin.ts": { + "#3963": 1, + "#4818": 2 + }, + "packages/plugins/plugin-email/src/email-service.ts": { + "#5172": 1 + }, + "packages/plugins/plugin-email/src/internal-header-readback.ts": { + "#8149": 1 + }, + "packages/plugins/plugin-email/src/templates/auth-templates.ts": { + "#8019": 4 + }, + "packages/plugins/plugin-hono-server/src/hono-plugin.ts": { + "#4073": 1 + }, + "packages/plugins/plugin-reports/src/report-service.ts": { + "#2849": 2, + "#2980": 2 + }, + "packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts": { + "#5876": 1, + "#8552": 2 + }, + "packages/plugins/plugin-security/src/cleanup-package-permissions.ts": { + "#2747": 1 + }, + "packages/plugins/plugin-security/src/delegated-admin-gate.ts": { + "#830": 2 + }, + "packages/plugins/plugin-security/src/explain-engine.ts": { + "#4647": 3 + }, + "packages/plugins/plugin-security/src/normalize-managed-by.ts": { + "#2920": 1 + }, + "packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts": { + "#2920": 1 + }, + "packages/plugins/plugin-security/src/objects/sys-position.object.ts": { + "#2920": 1 + }, + "packages/plugins/plugin-security/src/permission-evaluator.ts": { + "#2565": 2 + }, + "packages/plugins/plugin-security/src/security-plugin.ts": { + "#11082": 2, + "#12699": 3, + "#12974": 1, + "#2852": 1, + "#3022": 1, + "#3544": 1, + "#3547": 1, + "#4467": 1, + "#5386": 2, + "#5492": 1, + "#5493": 2, + "#7616": 1, + "#8552": 1, + "#8993": 1 + }, + "packages/plugins/plugin-security/src/translations/en.objects.generated.ts": { + "#2920": 2 + }, + "packages/plugins/plugin-security/src/unresolved-posture.ts": { + "#10401": 1, + "#10424": 1, + "#3545": 6 + }, + "packages/plugins/plugin-sharing/src/share-link-service.ts": { + "#5190": 1 + }, + "packages/plugins/plugin-sharing/src/sharing-rule-service.ts": { + "#7795": 1, + "#8158": 1 + }, + "packages/plugins/plugin-sharing/src/sharing-service.ts": { + "#5103": 1, + "#5493": 1, + "#5973": 1, + "#6428": 1 + }, + "packages/plugins/plugin-webhooks/src/auto-enqueuer.ts": { + "#3196": 1, + "#7799": 3, + "#7986": 3, + "#8022": 4, + "#8069": 3 + }, + "packages/plugins/plugin-webhooks/src/bootstrap-declared-webhooks.ts": { + "#7799": 1 + }, + "packages/plugins/plugin-webhooks/src/migrate-webhook-secrets.ts": { + "#7799": 2, + "#7986": 2 + }, + "packages/plugins/plugin-webhooks/src/redeliver-guard.ts": { + "#7799": 1, + "#8069": 2 + }, + "packages/plugins/plugin-webhooks/src/webhook-headers-gate.ts": { + "#7986": 1, + "#8558": 1, + "#8566": 1 + }, + "packages/plugins/plugin-webhooks/src/webhook-headers.ts": { + "#7986": 3, + "#8558": 2 + }, + "packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts": { + "#7799": 1, + "#8069": 1 + }, + "packages/plugins/plugin-webhooks/src/webhook-secret.ts": { + "#7799": 2, + "#8542": 1 + }, + "packages/qa/dogfood/test/armed.ts": { + "#8074": 2 + }, + "packages/qa/dogfood/test/authz-conformance.matrix.ts": { + "#10145": 1, + "#10243": 1, + "#10982": 3, + "#11089": 1, + "#11373": 1, + "#11757": 1, + "#12176": 1, + "#12497": 1, + "#1883": 2, + "#1884": 1, + "#1888": 2, + "#1901": 2, + "#1994": 1, + "#2077": 1, + "#2567": 3, + "#2698": 1, + "#2937": 6, + "#2948": 2, + "#2956": 1, + "#2982": 1, + "#2992": 1, + "#3003": 3, + "#3004": 3, + "#3022": 1, + "#3043": 2, + "#3167": 1, + "#3801": 1, + "#3963": 1, + "#4983": 1, + "#5519": 4, + "#5561": 1, + "#5591": 1, + "#7023": 1, + "#7033": 1, + "#7665": 4, + "#7685": 3, + "#7792": 1, + "#7807": 1, + "#7900": 1, + "#7976": 2, + "#8613": 3, + "#8710": 1, + "#8711": 1, + "#8811": 1, + "#9083": 2, + "#9377": 1 + }, + "packages/qa/dogfood/test/build-shaped-artifact.ts": { + "#6293": 3 + }, + "packages/qa/dogfood/test/enterprise-organizations.ts": { + "#4719": 1 + }, + "packages/qa/dogfood/test/expression-conformance.ledger.ts": { + "#1887": 1, + "#2614": 2, + "#3067": 1, + "#5016": 1, + "#7310": 1, + "#7327": 1 + }, + "packages/qa/dogfood/test/fixtures/attachments-fixture.ts": { + "#2755": 1 + }, + "packages/qa/dogfood/test/fixtures/comments-fixture.ts": { + "#4630": 1 + }, + "packages/qa/dogfood/test/fixtures/email-template-materialization-fixture.ts": { + "#4509": 1 + }, + "packages/qa/dogfood/test/fixtures/flow-durable-suspend-fixture.ts": { + "#4470": 1 + }, + "packages/qa/dogfood/test/fixtures/flow-function-effect-fixture.ts": { + "#4396": 1 + }, + "packages/qa/dogfood/test/fixtures/flow-runas-fixture.ts": { + "#1888": 1 + }, + "packages/qa/dogfood/test/fixtures/label-scope-fixture.ts": { + "#3602": 1 + }, + "packages/qa/dogfood/test/fixtures/rls-owner-fixture.ts": { + "#1994": 2 + }, + "packages/qa/dogfood/test/fixtures/webhook-materialization-fixture.ts": { + "#3461": 1 + }, + "packages/qa/dogfood/test/search-conformance.ledger.ts": { + "#4254": 1 + }, + "packages/qa/dogfood/test/showcase-security.ts": { + "#5491": 1 + }, + "packages/qa/downstream-contract/src/stack.ts": { + "#2035": 1 + }, + "packages/rest/src/import-mapping.ts": { + "#2611": 1 + }, + "packages/rest/src/openapi-builtin-paths.ts": { + "#5588": 2 + }, + "packages/rest/src/rest-api-plugin.ts": { + "#3963": 1 + }, + "packages/rest/src/rest-route-ledger.ts": { + "#10179": 1, + "#11678": 2, + "#11924": 2, + "#12038": 9, + "#12702": 4, + "#2747": 1, + "#3610": 1, + "#3611": 1, + "#4327": 1, + "#5682": 1, + "#5950": 1, + "#6603": 1, + "#7019": 1, + "#7526": 1, + "#7563": 1, + "#8140": 1, + "#9180": 1 + }, + "packages/rest/src/rest-server.ts": { + "#1604": 1, + "#7912": 1 + }, + "packages/runtime/src/action-execution.ts": { + "#11519": 1, + "#5933": 1 + }, + "packages/runtime/src/api-endpoint-step.ts": { + "#5040": 3 + }, + "packages/runtime/src/api-mapping.ts": { + "#5040": 2 + }, + "packages/runtime/src/app-plugin.ts": { + "#8686": 1 + }, + "packages/runtime/src/dispatcher-error-vocabulary.ts": { + "#12772": 1, + "#3724": 1, + "#5085": 2, + "#8035": 6, + "#8885": 1, + "#9106": 1, + "#9223": 1, + "#9232": 1, + "#9460": 2, + "#9968": 1 + }, + "packages/runtime/src/domains/activation-gate.ts": { + "#10243": 1 + }, + "packages/runtime/src/domains/auth.ts": { + "#5085": 1 + }, + "packages/runtime/src/endpoint-executor.ts": { + "#5040": 2 + }, + "packages/runtime/src/endpoint-policy.ts": { + "#5040": 1 + }, + "packages/runtime/src/resolve-project-database.ts": { + "#6469": 1 + }, + "packages/runtime/src/route-ledger.ts": { + "#10145": 4, + "#10179": 1, + "#10243": 2, + "#12038": 11, + "#12160": 1, + "#12702": 1, + "#3584": 2, + "#3656": 1, + "#3718": 2, + "#3801": 1, + "#3913": 1, + "#4327": 1, + "#5040": 2, + "#5519": 2, + "#5561": 1, + "#5682": 1, + "#6593": 1, + "#7019": 1, + "#7526": 2, + "#9180": 2, + "#9406": 2 + }, + "packages/runtime/src/sandbox/body-runner.ts": { + "#4345": 1, + "#4352": 1, + "#7448": 1 + }, + "packages/runtime/src/standalone-stack.ts": { + "#3276": 3 + }, + "packages/services/service-analytics/src/analytics-service.ts": { + "#3867": 1, + "#5222": 1, + "#5918": 1, + "#7598": 1 + }, + "packages/services/service-analytics/src/comparand-shape.ts": { + "#5222": 3, + "#7596": 1, + "#7598": 3 + }, + "packages/services/service-analytics/src/read-scope-sql.ts": { + "#5347": 1, + "#5369": 1, + "#6050": 1, + "#6125": 1, + "#6387": 1 + }, + "packages/services/service-analytics/src/strategies/filter-normalizer.ts": { + "#3650": 2, + "#5158": 2, + "#5240": 1, + "#5334": 2, + "#6050": 1, + "#6386": 1, + "#6444": 1 + }, + "packages/services/service-analytics/src/strategies/native-sql-strategy.ts": { + "#5222": 1, + "#7598": 1 + }, + "packages/services/service-analytics/src/strategies/objectql-strategy.ts": { + "#3601": 1, + "#3602": 1, + "#3650": 1, + "#5222": 1, + "#5333": 1, + "#7598": 1 + }, + "packages/services/service-automation/src/builtin/connector-nodes.ts": { + "#3017": 1 + }, + "packages/services/service-automation/src/builtin/parse-config.ts": { + "#4277": 1 + }, + "packages/services/service-automation/src/builtin/screen-nodes.ts": { + "#1870": 1 + }, + "packages/services/service-automation/src/builtin/template.ts": { + "#11060": 1 + }, + "packages/services/service-automation/src/engine.ts": { + "#1888": 1, + "#2419": 2, + "#3017": 1, + "#3528": 1, + "#3760": 1, + "#3801": 1, + "#3823": 1, + "#4045": 1, + "#4277": 1, + "#4414": 1, + "#5393": 4, + "#5561": 4 + }, + "packages/services/service-automation/src/plugin.ts": { + "#1928": 1, + "#2612": 1, + "#2977": 1, + "#3017": 4, + "#3356": 1, + "#3475": 1 + }, + "packages/services/service-automation/src/runtime-identity.ts": { + "#1888": 1, + "#3760": 1 + }, + "packages/services/service-automation/src/sys-automation-run.object.ts": { + "#2585": 1, + "#3801": 1, + "#7533": 2 + }, + "packages/services/service-automation/src/sys-flow-dispatch.object.ts": { + "#10220": 1 + }, + "packages/services/service-datasource/src/datasource-credential-migration.ts": { + "#8078": 1, + "#8081": 1 + }, + "packages/services/service-datasource/src/datasource-route-ledger.ts": { + "#3827": 1, + "#4249": 1, + "#7744": 1, + "#7955": 1, + "#8155": 1 + }, + "packages/services/service-datasource/src/external-datasource-service.ts": { + "#10997": 1, + "#11000": 1 + }, + "packages/services/service-i18n/src/i18n-route-ledger.ts": { + "#3636": 2 + }, + "packages/services/service-knowledge/src/knowledge-service-plugin.ts": { + "#4639": 1, + "#4672": 1 + }, + "packages/services/service-knowledge/src/knowledge-service.ts": { + "#2981": 1 + }, + "packages/services/service-messaging/src/http-outbox.ts": { + "#7799": 1, + "#8069": 3 + }, + "packages/services/service-messaging/src/objects/http-delivery.object.ts": { + "#8069": 2, + "#8118": 1 + }, + "packages/services/service-messaging/src/outbox.ts": { + "#11453": 2, + "#11859": 1 + }, + "packages/services/service-messaging/src/sql-http-outbox.ts": { + "#8118": 1 + }, + "packages/services/service-messaging/src/translations/en.objects.generated.ts": { + "#8069": 2, + "#8118": 1 + }, + "packages/services/service-messaging/src/translations/es-ES.objects.generated.ts": { + "#8069": 2, + "#8118": 1 + }, + "packages/services/service-messaging/src/translations/ja-JP.objects.generated.ts": { + "#8069": 2, + "#8118": 1 + }, + "packages/services/service-messaging/src/translations/zh-CN.objects.generated.ts": { + "#8069": 2, + "#8118": 1 + }, + "packages/services/service-queue/src/db-queue-adapter.ts": { + "#5179": 1 + }, + "packages/services/service-queue/src/queue-service-plugin.ts": { + "#5195": 1 + }, + "packages/services/service-sms/src/sms-daily-quota.ts": { + "#2814": 1 + }, + "packages/services/service-sms/src/sms-plugin.ts": { + "#2814": 1 + }, + "packages/services/service-storage/src/attachment-lifecycle.ts": { + "#4797": 1 + }, + "packages/services/service-storage/src/storage-route-ledger.ts": { + "#3584": 1, + "#3689": 2 + }, + "packages/triggers/trigger-record-change/src/record-change-trigger.ts": { + "#3457": 1 + }, + "packages/types/src/node.ts": { + "#10943": 1, + "#4719": 1 + }, + "packages/verify/src/harness.ts": { + "#4719": 1 + }, + "packages/verify/src/rls.ts": { + "#1994": 3, + "#7685": 3, + "#7978": 3 + } +} diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index 1494998c97..6532068dc1 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -7782,14 +7782,25 @@ function selfTest() { // — so the gate's own self-test (which derives its declaration from ROOTS) // cannot pin it and this case is the only place that does. t('and the spec refusal-message population Rule 3 walks', docAuthoringHints.some((h) => hintCovers(h, 'packages/spec/src/ui/action.zod.ts'))); - // The negative half, load-bearing for a declaration spanning five roots: a - // gate named on EVERY card is the louder version of naming none. `packages/` - // is now claimed in ONE place and must stay that narrow — the whole tree is - // 78 packages and none of the other 77 is corpus, nor is spec's own build - // output, which the walk skips. - t('and claims nothing elsewhere under packages/', !docAuthoringHints.some((h) => hintCovers(h, 'packages/runtime/src/index.ts'))); - t('nor spec outside its source tree', !docAuthoringHints.some((h) => hintCovers(h, 'packages/spec/package.json'))); - t('nor under apps/', !docAuthoringHints.some((h) => hintCovers(h, 'apps/console/src/main.tsx'))); + // #13297 widened Rule 3's root: the cross-package prose-id leg walks every + // sibling package's non-test sources against a pinned baseline, so the gate + // now genuinely reads any package edit and declares `packages/**`. The + // narrow claim these cases used to pin ("spec/src and nothing else under + // packages/") is the boundary the #13179 deferral drew, and the deferral's + // own codified revival condition retired it — a sibling package source is + // now POSITIVE coverage, not an over-claim. + t('and the sibling-package prose population the ledgered leg walks', docAuthoringHints.some((h) => hintCovers(h, 'packages/runtime/src/index.ts'))); + // The residual over-claim is bounded and known: `packages/**` subsumes + // spec's non-src files and every test file, which the leg's own walk skips + // (spec belongs to the position-based rule; test bodies are out). That is + // the tolerated carve-out-inside-a-walked-root case — the same shape as + // check:slot-lookup-ratchet declaring the whole of `packages/**` — pinned + // here so it stays a recorded residual rather than an accident. + t('spec outside its source tree rides the bounded packages/** over-claim', docAuthoringHints.some((h) => hintCovers(h, 'packages/spec/package.json'))); + // The negative half that SURVIVES the widening, still load-bearing: a gate + // named on EVERY card is the louder version of naming none, and the leg + // walks packages/ only — never apps/ or examples/. + t('and claims nothing under apps/', !docAuthoringHints.some((h) => hintCovers(h, 'apps/console/src/main.tsx'))); t('nor under examples/', !docAuthoringHints.some((h) => hintCovers(h, 'examples/crm/objects/account.object.ts'))); // The second gate of that class (#9700): a whole-tree ESLint ratchet whose