From 30c99ce399273b9a5be59982de5e1d92bad6eec2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 23:49:14 +0000 Subject: [PATCH] fix(scripts): make the three refusal-shape gates state their registry's nature in author-facing text (#8576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three refuse the ratchet-weakening remedy outright — the stronger shape under the #8435 convention — but each stated WHY only in a comment. An author who trips the gate was told "no" without being told "because this only ever shrinks". Mirrors one clause of each gate's own comment into its existing refusal string; no new message, no verdict changed, no ratchet touched. Two of the three consequently flip in the #8540 detector (PR #8575) from `excluded` to `refused`, which is the intended effect: the refusal limb's sample grows from one gate to three. The detector's hand-classified control corpus is re-read and updated to match, per its own C2/C3 audit cases. check-driver-memory-census.mjs does NOT flip, and is left exactly as it is: its refusal sentence carries no target inside the detector's 200-char offer window, and its governance wording sits outside the GOVERN vocabulary. That is a finding about the detector's grammar, not a defect in a correct gate. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Jqe56GnYFddggeAyfkZFVz --- scripts/check-driver-memory-census.mjs | 12 ++++++- scripts/check-ratchet-remedy-authority.mjs | 38 ++++++++++++++-------- scripts/check-test-source-alias.mjs | 13 ++++++++ scripts/check-type-source-resolution.mjs | 15 ++++++++- 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/scripts/check-driver-memory-census.mjs b/scripts/check-driver-memory-census.mjs index 8c1b93f0f5..a41504223e 100644 --- a/scripts/check-driver-memory-census.mjs +++ b/scripts/check-driver-memory-census.mjs @@ -376,7 +376,12 @@ export function reconcile(scan, ledger, read = (f) => readFileSync(join(ROOT, f) : '') + ' A new arrival is not a bookkeeping chore: it is the #6664 defect itself. Do NOT add an entry to make ' + 'this green if the answer is "this should have been migrated" — take the disposition through the ' - + 'process #5704 / #6664 record (rule it, migrate it, or file it), then write down what was decided.', + + 'process #5704 / #6664 record (rule it, migrate it, or file it), then write down what was decided.' + // The REASON for that refusal, in the text the author actually reads (#8576). + // Mirrors this file's own header verbatim rather than restating it: one rule in + // two voices becomes two rules by the next reading. + + ' "Is this consumer legitimate?" is a maintainer ruling (#5704 Q2, #6664 A) and this gate is the ' + + 'bookkeeping under it, not a second opinion on it.', ); } // LIVE — bindings @@ -580,6 +585,11 @@ function selfTest() { arrival.bindings.push({ file: 'p/new.test.ts', kind: 'import', line: 2, specifier: SPECIFIER }); let ps = reconcile(arrival, okLedger(), readFrom({ [ruledFile]: textOk(), 'p/new.test.ts': '' })); expect('an unledgered arrival is a finding', ps.some((p) => p.startsWith('LEDGERED:') && p.includes('p/new.test.ts'))); + // #8576. The refusal above turns the bookkeeping remedy down; this pins that it + // also says WHOSE call the disposition is, in the text the author reads. + // Asserted on the planted arrival — the string only ever prints on failure. + expect('the LEDGERED refusal states whose call the disposition is, not just that it is refused', + ps.some((p) => p.startsWith('LEDGERED:') && p.includes('is a maintainer ruling'))); // The mirror: a ledger entry whose consumer was migrated away. ps = reconcile({ bindings: [], mentions: [], manifests: [] }, okLedger(), readFrom({ [ruledFile]: textOk() })); diff --git a/scripts/check-ratchet-remedy-authority.mjs b/scripts/check-ratchet-remedy-authority.mjs index dc83d10a5b..e311b96a1b 100644 --- a/scripts/check-ratchet-remedy-authority.mjs +++ b/scripts/check-ratchet-remedy-authority.mjs @@ -606,16 +606,24 @@ const CONTROL = { expect: 'refused', why: 'Refuses by binding a negation to the verb, over a shrink-only registry.', }, - // Refuses by predication (the widening act named as subject and denied), and - // the refusal predicate does fire on that exact sentence — self-test (12) pins - // it. It still lands in `excluded` because stage 2 declines it FIRST: the - // sentence names its target as a bare noun and carries no shrink testimony in - // that message, so the target is never established as a ratchet. Recorded as - // `excluded` rather than `refused` because that is what is measured, not what - // the shape deserves — a control that flattered the detector would be worthless. + // The two gates below refuse by PREDICATION (the act named as subject and + // denied) — self-test (12) pins that predicate on their exact sentence. + // + // Until #8576 both were recorded here as `excluded`, and that was the honest + // reading: stage 2 declined them FIRST, because each named its target in a + // message carrying no testimony about the registry's nature — the testimony + // sat in a comment, where no author and no detector reads it. #8576 mirrored + // one clause of each gate's own shrink-only comment into that same message, so + // the target is now established as a ratchet and the refusal limb is reached. + // Growing the refusal limb's sample from one gate to three was the point: a + // regression in that limb used to be measured against a sample of one. 'check-test-source-alias.mjs': { - expect: 'excluded', - why: 'A refusal-shaped gate that stage 2 declines before refusal is consulted. Not a violation by either route.', + expect: 'refused', + why: 'Refuses by predication. Its registry states its own nature in the same message since #8576, so stage 2 reaches it and the refusal limb is consulted.', + }, + 'check-type-source-resolution.mjs': { + expect: 'refused', + why: 'The other refusal precedent, refusing by the same predication shape. Its registry states its own nature in the same message since #8576, so stage 2 reaches it rather than declining on a path target.', }, // ── Declaration registries and near-misses: recording the fact IS the fix ── @@ -643,13 +651,15 @@ const CONTROL = { expect: 'excluded', why: 'Carries shrink-only registries, but its author-facing remedy names a declaration registry instead. Reached only when two unrelated diagnostics are allowed to merge, which is why messages are bounded.', }, + // The third gate #8576 made reader-visible, and the one that did NOT move. Its + // testimony is governance, not shrink, and it now states that governance in + // author-facing text — yet the verdict is unchanged, for two reasons that are + // each about THIS DETECTOR rather than about the gate. Recorded rather than + // engineered around: the gate's wording is correct, and bending it to satisfy a + // grammar would be the control flattering the detector. 'check-driver-memory-census.mjs': { expect: 'excluded', - why: 'Named in the #8540 ruling as a refusal precedent, and it does refuse. It lands here rather than in refused because its ledger carries no shrink or governance testimony, so stage 2 declines it first. Not a violation either way; recorded so the distinction stays measured rather than assumed.', - }, - 'check-type-source-resolution.mjs': { - expect: 'excluded', - why: 'The other refusal precedent. Its remedy points at a package tsconfig, which is not a ratchet, so stage 2 declines it. Reached only when a path target is allowed file-wide scope, which is why it is not.', + why: 'Named in the #8540 ruling as a refusal precedent, and it does refuse. Since #8576 it states its ledger governance in author-facing text too, yet it still lands here for two independent reasons: stage 1 finds no target inside the offer window of its refusal sentence, and its wording — a maintainer ruling — sits outside the governance vocabulary. Not a violation by any route; recorded so the distinction stays measured rather than assumed.', }, 'regen-artifacts.mjs': { expect: 'excluded', diff --git a/scripts/check-test-source-alias.mjs b/scripts/check-test-source-alias.mjs index 189dcaaec8..04e7e532ed 100644 --- a/scripts/check-test-source-alias.mjs +++ b/scripts/check-test-source-alias.mjs @@ -1370,6 +1370,11 @@ function check(root, registry) { failures.push( `${name}: NEW unaliased artifact import(s) since this entry was measured: ${added.join(', ')}.\n` + " Alias them in the package's vitest.config.* — widening the registry entry is not the fix.\n" + + // The REASON for that refusal, in the text the author actually reads (#8576). + // Mirrors `KNOWN_UNALIASED_TEST_IMPORTS`'s own words verbatim rather than + // restating them: one rule in two voices becomes two rules by the next reading. + ' That registry is ⛔ SHRINK-ONLY: entries are audited in both directions, so one that is no\n' + + ' longer needed fails the gate and names itself for deletion.\n' + // Same defect, same fix: this branch also named bare packages and left // the reader to guess the specifier shape (#8256). remediationHint( @@ -1824,6 +1829,14 @@ function selfTest() { }); const grown = check(root, { '@fx/violator': ['@fx/core'] }); expect(has(grown.failures, 'NEW unaliased artifact import'), 'a new unaliased import under an existing entry did not fail'); + // #8576. The refusal above turns the registry remedy down; this pins that it + // also says WHY, in the text the author reads. Asserted on the planted + // violation, never on a green run — the string only ever prints on failure. + expect( + has(grown.failures, '⛔ SHRINK-ONLY'), + 'the refusal no longer states WHY it refuses — the registry\'s shrink-only nature is back to being ' + + 'comment-only, which tells the maintainer reading the script and not the author tripping the gate', + ); // Shrink: an entry wider than the measurement must fail too — no headroom. const wide = check(root, { '@fx/violator': ['@fx/core', '@fx/other', '@fx/gone'] }); diff --git a/scripts/check-type-source-resolution.mjs b/scripts/check-type-source-resolution.mjs index a0cc84e332..830b1c9bac 100644 --- a/scripts/check-type-source-resolution.mjs +++ b/scripts/check-type-source-resolution.mjs @@ -878,7 +878,12 @@ function check(root, registry) { if (added.length > 0) failures.push( `${name}: NEW dist-resolved type import(s) since this entry was measured: ${added.join(', ')}.\n` + - " Add the `paths` rules to the package's tsconfig.json — widening the registry entry is not the fix.", + " Add the `paths` rules to the package's tsconfig.json — widening the registry entry is not the fix.\n" + + // The REASON for that refusal, in the text the author actually reads (#8576). + // Mirrors `KNOWN_DIST_RESOLVED_TYPE_IMPORTS`'s own words verbatim rather than + // restating them: one rule in two voices becomes two rules by the next reading. + ' That registry is ⛔ SHRINK-ONLY: entries are audited in both directions, so one that is no\n' + + ' longer needed fails the gate and names itself for deletion.', ); if (gone.length > 0) failures.push( @@ -1283,6 +1288,14 @@ function selfTest() { }); const grown = check(root, measuredNames); expect(has(grown.failures, 'NEW dist-resolved type import'), 'a new dist-resolved import under an entry did not fail'); + // #8576. The refusal above turns the registry remedy down; this pins that it + // also says WHY, in the text the author reads. Asserted on the planted + // violation, never on a green run — the string only ever prints on failure. + expect( + has(grown.failures, '⛔ SHRINK-ONLY'), + 'the refusal no longer states WHY it refuses — the registry\'s shrink-only nature is back to being ' + + 'comment-only, which tells the maintainer reading the script and not the author tripping the gate', + ); const wide = check(root, { ...measuredNames, '@fx/violator': ['@fx/spec', '@fx/other', '@fx/gone'] }); expect(has(wide.failures, 'STALE'), 'a registry entry listing a dep that is no longer dist-resolved did not fail');