From a85119ac781331f3afdbc08b98c8898b5363c0fe Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 18:08:27 +0000 Subject: [PATCH 1/2] fix(devx): mark ratchet-editing remedies as maintainer-only in three more gates (#8519) #8435 established the convention: a gate whose second remedy is editing a shrink-only ratchet/ledger/baseline must mark that path as maintainer-only in its own output. PR #8517 covered two gates; the census behind #8519 found the convention stops short of the rest. This covers the three uncovered gates the card names. - check-durability-degradation-log-level.mjs: the read-seam `OR :` line offered a durability-read-invention.baseline.json entry with no authority marker. - check-role-word.mjs: the NEW-use remedy offered `--update`, which expands the baseline. The marker labels the ACT, not the file -- `--update` rewrites the whole baseline from the current tree rather than appending a line. - check-driver-conformance.mjs: CONSUMED offered a DEBT/EXEMPT ledger entry as a co-equal option. The authority rule already existed in that file's LEDGER comment ("a MEASURED, tracked exception the maintainer has agreed to, never the cheaper half of 'enroll the driver'") but only where a maintainer reading the script would see it. Those words are mirrored into the author-facing text rather than reworded. Each gate gets the greppable token, a detector regex, a pure ratchetRemedyCarriesAuthority() predicate and non-overlapping self-test assertions, copying the shape PR #8517 landed. check-role-word.mjs had no self-test at all, so one is added and wired into `check:role-word` -- an assertion no script runs is a phantom check. No gate is weakened and no verdict changes. Detection logic is untouched: the three gates' green-run output is byte-identical before and after, and on a planted violation the flagged-subject sets and exit codes are identical too, with the remedy prose as the only difference. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Jqe56GnYFddggeAyfkZFVz --- package.json | 2 +- scripts/check-driver-conformance.mjs | 122 +++++++++++++++- ...check-durability-degradation-log-level.mjs | 135 +++++++++++++++++- scripts/check-role-word.mjs | 135 +++++++++++++++++- 4 files changed, 378 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 5f61e8d8a1..391f3fd9d9 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "check:doc-authoring": "node scripts/check-doc-authoring.mjs --self-test && node scripts/check-doc-authoring.mjs", "check:doc-anchors": "node scripts/check-doc-anchors.mjs --self-test && node scripts/check-doc-anchors.mjs", "check:docs-audit-scope": "node scripts/docs-audit/affected-docs.mjs --self-test && node scripts/docs-audit/check-audit-scope.mjs --self-test && node scripts/docs-audit/check-audit-scope.mjs", - "check:role-word": "node scripts/check-role-word.mjs", + "check:role-word": "node scripts/check-role-word.mjs --self-test && node scripts/check-role-word.mjs", "check:quick-reference-counts": "node scripts/check-quick-reference-counts.mjs --self-test && node scripts/check-quick-reference-counts.mjs", "check:skill-frame-sync": "node scripts/check-skill-frame-sync.mjs --self-test && node scripts/check-skill-frame-sync.mjs", "check:skill-frame-freshness": "node scripts/check-skill-frame-freshness.mjs --self-test && node scripts/check-skill-frame-freshness.mjs", diff --git a/scripts/check-driver-conformance.mjs b/scripts/check-driver-conformance.mjs index d121c79c08..d9c539191b 100644 --- a/scripts/check-driver-conformance.mjs +++ b/scripts/check-driver-conformance.mjs @@ -440,6 +440,76 @@ const CASE_SETS = [ const LEDGER = []; +// ── The ratchet-remedy authority convention (#8435) ───────────────────────── +// +// The rule above is the authority rule, and until now it was written only here +// — where a maintainer reading the script sees it and the author who trips the +// gate never does. CONSUMED offered "write the suite, or add a ledger entry" as +// two co-equal options, which is precisely the reading the LEDGER comment says +// is wrong. The convention landed for check-engine-double-contract.mjs and +// check-type-check-coverage.mjs; the twin blocks there are the reference. +// +// The words below are lifted from the LEDGER comment on purpose rather than +// invented: one rule stated twice in two voices is two rules by the next +// reading. +// +// ⛔ This STRENGTHENS ledger governance and weakens nothing. No cell's verdict +// moves, no entry is added, and the matrix this gate reports is byte-for-byte +// the one it reported before — only the diagnostic text changes. + +/** Kept identical to the other gates' token so the convention is greppable. */ +const RATCHET_AUTHORITY_MARKER = '⛔ MAINTAINER-ONLY'; + +/** The ledger as the message spells it — this script IS the ledger's home. */ +const LEDGER_REL = 'scripts/check-driver-conformance.mjs'; + +/** + * How this gate OFFERS the privileged path, as a detector rather than a string + * compare, so the self-test can prove it still reaches its subject: a reworded + * offer that stopped matching would make the convention check pass vacuously on + * every message. + * + * RECONCILED is deliberately out of its reach — that message tells the author to + * DELETE an entry, which is the ledger tightening and squarely the author's job. + */ +const RATCHET_EXPANSION_OFFER = new RegExp( + `add a measured DEBT/EXEMPT entry to the ledger in\\s+${LEDGER_REL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, +); + +/** + * The convention: a message that hands the author the ledger-expanding path must + * say in the same breath that the path is not theirs. A message offering no such + * path is unaffected — this is an authority label, not a vocabulary ban. + * + * @param {string} message + * @returns {boolean} + */ +function ratchetRemedyCarriesAuthority(message) { + if (!RATCHET_EXPANSION_OFFER.test(message)) return true; + return message.includes(RATCHET_AUTHORITY_MARKER); +} + +/** + * CONSUMED's text, named and pure so the self-test can assert on the exact + * string the author reads. Extracted from `audit()` for that reason — a message + * built inline is a message no assertion can reach. + * + * @param {string} driver + * @param {{marker: string, what: string}} caseSet + * @returns {string} + */ +function consumedMessage(driver, caseSet) { + return ( + `CONSUMED: ${driver} does not run ${caseSet.marker} (${caseSet.what}). Add a suite that ` + + 'drives the shared cases. That is the fix, and the only one of the two you can take on ' + + `your own. ${RATCHET_AUTHORITY_MARKER}, NOT a co-equal option: add a measured DEBT/EXEMPT ` + + `entry to the ledger in ${LEDGER_REL} saying why not. A ledger entry is a MEASURED, ` + + 'tracked exception the maintainer has agreed to, never the cheaper half of "enroll the ' + + 'driver" — do not take this path to get CI green.' + ); +} + + // ── Discovery ─────────────────────────────────────────────────────────────── /** A declared scan root that could not be resolved to a directory. Carries the names. */ @@ -710,11 +780,7 @@ function audit() { } else if (entry) { rows.push({ driver, marker: c.marker, state: entry.kind.toLowerCase() }); } else { - errors.push( - `CONSUMED: ${driver} does not run ${c.marker} (${c.what}). Add a suite that drives ` - + 'the shared cases, or add a measured DEBT/EXEMPT entry to the ledger in ' - + 'scripts/check-driver-conformance.mjs saying why not.', - ); + errors.push(consumedMessage(driver, c)); rows.push({ driver, marker: c.marker, state: 'MISSING' }); } } @@ -960,6 +1026,47 @@ function selfTest() { rmSync(tmpRoots, { recursive: true, force: true }); } + // ── The ratchet-remedy authority convention (#8435) ──────────────────────── + // + // Three assertions, deliberately non-overlapping, so each way this can rot is + // caught by exactly one NAMED failure: + // + // (1) the detector still reaches its subject — the only one that fails if + // the offer is reworded out from under `RATCHET_EXPANSION_OFFER`, which + // would make (3) pass vacuously forever after; + // (2) the real emitted message carries the marker — the only one that fails + // if the label is dropped from CONSUMED's text; + // (3) an offer WITHOUT the marker is REJECTED — the only one that fails if + // the predicate stops discriminating (e.g. is reduced to `return true`). + // + // (3) is what makes (2) worth having: without it, a predicate that approves + // everything would keep this block green while the convention is gone. + const consumed = consumedMessage('driver-example', { + marker: 'PAGINATION_CASES', + what: 'a sorted paged read is a partition', + }); + expect('#8435 — the ratchet-offer DETECTOR still matches CONSUMED (else the check below is ' + + 'vacuous)', + RATCHET_EXPANSION_OFFER.test(consumed)); + expect(`#8435 — CONSUMED marks the ledger path ${RATCHET_AUTHORITY_MARKER} (the LEDGER comment ` + + 'calls an entry a MEASURED exception the maintainer has agreed to, so the author must be ' + + 'told that where they read it, not only where a maintainer does)', + ratchetRemedyCarriesAuthority(consumed)); + + { + // (3)'s fixture is SYNTHETIC rather than the real message with the marker + // stripped out: derived, it also fires on a rewording — two named failures + // for one rot, the second misdescribing the cause. + const unmarkedOffer = + `CONSUMED: driver-example does not run PAGINATION_CASES. Add a suite that drives the shared ` + + `cases, or add a measured DEBT/EXEMPT entry to the ledger in ${LEDGER_REL} saying why not.`; + expect('#8435 — the synthetic unmarked-offer fixture is still recognised as an offer', + RATCHET_EXPANSION_OFFER.test(unmarkedOffer)); + expect('#8435 — ratchetRemedyCarriesAuthority() REJECTS an offer carrying no marker (proves ' + + 'the predicate discriminates rather than approving everything)', + !ratchetRemedyCarriesAuthority(unmarkedOffer)); + } + if (failures.length) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\ncheck-driver-conformance --self-test: ${failures.length} failure(s).\n`); @@ -967,8 +1074,9 @@ function selfTest() { } console.log( 'OK self-test: detects driven / unused / re-declared fixtures, discovers both axes, accounts for ' - + 'every entry under DRIVERS_DIR (a dropped or manifestless row is red, not a smaller matrix), and ' - + 'holds the dead-root hard error (red when a scan root is renamed, green when restored).', + + 'every entry under DRIVERS_DIR (a dropped or manifestless row is red, not a smaller matrix), ' + + 'holds the dead-root hard error (red when a scan root is renamed, green when restored), and ' + + 'keeps CONSUMED\'s ledger offer marked maintainer-only (#8435).', ); } diff --git a/scripts/check-durability-degradation-log-level.mjs b/scripts/check-durability-degradation-log-level.mjs index 89c97de118..dba0f488d8 100644 --- a/scripts/check-durability-degradation-log-level.mjs +++ b/scripts/check-durability-degradation-log-level.mjs @@ -1597,6 +1597,73 @@ function readInventionKey(f) { return `${f.file}::${f.fn ?? ''}`; } +// ── The ratchet-remedy authority convention (#8435) ────────────────────────── +// +// The read-seam report's `OR :` line hands the author the baseline-expanding +// path. That baseline is shrink-only and hand-edited — the message said so, and +// still presented the path as the second of two things the author may do. The +// convention landed for check-engine-double-contract.mjs and +// check-type-check-coverage.mjs; the twin blocks there are the reference. +// +// Deliberately NOT extended to this file's other two ledgers, both of which are +// declaration registries rather than debt ratchets: FAILURE_PROPAGATION_CALLEES +// / FAILURE_PROPAGATION_SITES record HOW a failure is delivered (declaring one +// is the correct fix, not a weakening), and the two baselines' stale-entry +// messages tell the author to DELETE an entry, which is the ratchet tightening +// and squarely their job. +// +// ⛔ This STRENGTHENS ratchet governance and weakens nothing. No seam's verdict +// moves, no baseline entry is added, and the findings this rule reports are +// byte-for-byte the ones it reported before — only the diagnostic text changes. + +/** Kept identical to the other gates' token so the convention is greppable. */ +const RATCHET_AUTHORITY_MARKER = '⛔ MAINTAINER-ONLY'; + +/** The baseline as the message spells it (the PATH constant is absolute). */ +const READ_INVENTION_BASELINE_REL = 'scripts/durability-read-invention.baseline.json'; + +/** + * How this rule OFFERS the privileged path, as a detector rather than a string + * compare, so the self-test can prove it still reaches its subject: a reworded + * offer that stopped matching would make the convention check pass vacuously on + * every message. `\s+` rather than a space because the offer is wrapped across + * lines with a hanging indent. + */ +const RATCHET_EXPANSION_OFFER = new RegExp( + `add an entry naming why to\\s+${READ_INVENTION_BASELINE_REL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, +); + +/** + * The convention: a message that hands the author the baseline-expanding path + * must say in the same breath that the path is not theirs. A message offering no + * such path is unaffected — this is an authority label, not a vocabulary ban. + * + * @param {string} message + * @returns {boolean} + */ +function ratchetRemedyCarriesAuthority(message) { + if (!RATCHET_EXPANSION_OFFER.test(message)) return true; + return message.includes(RATCHET_AUTHORITY_MARKER); +} + +/** + * The `OR :` line's text, named and pure so the self-test can assert on the + * exact string the author reads. Extracted from the report loop for that reason + * — a message built inline is a message no assertion can reach. + * + * @returns {string} + */ +function readInventionBaselineOffer() { + return ( + ` OR : ${RATCHET_AUTHORITY_MARKER}, NOT a co-equal option — the fix above is the one\n` + + ' you can take on your own. If the seam is a REVIEWED, legitimate degradation,\n' + + ` add an entry naming why to ${READ_INVENTION_BASELINE_REL}.\n` + + ' That baseline is shrink-only and hand-edited, so an entry weakens a ratchet\n' + + ' and needs a maintainer to agree the degradation is legitimate first — do not\n' + + ' take this path to get CI green.\n' + ); +} + /** Run the read-seam invention rule (#5186) over its narrowed scan scope. */ function runReadSeamRule({ list = false } = {}) { const findings = []; @@ -1698,10 +1765,7 @@ function runReadSeamRule({ list = false } = {}) { ' that the failure be distinguishable, never that the value change.', ); } - console.error( - ' OR : if the seam is a REVIEWED, legitimate degradation, add an entry naming why to\n' + - ' scripts/durability-read-invention.baseline.json (shrink-only, hand-edited).\n', - ); + console.error(readInventionBaselineOffer()); } } @@ -3015,11 +3079,72 @@ function selfTestReadSeams() { console.log(` ✓ ${c.name}`); } } + // ── The ratchet-remedy authority convention (#8435) ──────────────────────── + // + // Three assertions, deliberately non-overlapping, so each way this can rot is + // caught by exactly one NAMED failure: + // + // (1) the detector still reaches its subject — the only one that fails if + // the offer is reworded out from under `RATCHET_EXPANSION_OFFER`, which + // would make (3) pass vacuously forever after; + // (2) the real emitted line carries the marker — the only one that fails if + // the label is dropped from the `OR :` text; + // (3) an offer WITHOUT the marker is REJECTED — the only one that fails if + // the predicate stops discriminating (e.g. is reduced to `return true`). + // + // (3) is what makes (2) worth having: without it, a predicate that approves + // everything would keep this block green while the convention is gone. + const offer = readInventionBaselineOffer(); + if (!RATCHET_EXPANSION_OFFER.test(offer)) { + failures++; + console.error( + ' ✗ #8435 convention — the ratchet-offer DETECTOR no longer matches the `OR :` line it is ' + + 'written against. Either the offer was reworded (re-point RATCHET_EXPANSION_OFFER at the ' + + 'new wording) or the baseline path was removed (delete the convention block). Until then ' + + 'the convention check passes vacuously on every message.', + ); + } + if (!ratchetRemedyCarriesAuthority(offer)) { + failures++; + console.error( + ` ✗ #8435 convention — the \`OR :\` line offers ${READ_INVENTION_BASELINE_REL} without the ` + + `${RATCHET_AUTHORITY_MARKER} marker. That baseline is shrink-only, so the path is a ` + + 'maintainer action; presenting it unmarked next to the real fix is what let "add a ' + + 'baseline entry" read as the author\'s second option.', + ); + } + { + // (3)'s fixture is SYNTHETIC rather than the real line with the marker + // stripped out: derived, it also fires on a rewording — two named failures + // for one rot, and the second one misdescribes the cause. + const unmarkedOffer = + ' OR : if the seam is a REVIEWED, legitimate degradation, add an entry naming why to\n' + + ` ${READ_INVENTION_BASELINE_REL} (shrink-only, hand-edited).\n`; + if (!RATCHET_EXPANSION_OFFER.test(unmarkedOffer)) { + failures++; + console.error( + ' ✗ #8435 convention — the synthetic unmarked-offer fixture is no longer recognised as an ' + + 'offer, so it cannot test discrimination at all. Re-spell it to match ' + + 'RATCHET_EXPANSION_OFFER.', + ); + } else if (ratchetRemedyCarriesAuthority(unmarkedOffer)) { + failures++; + console.error( + ' ✗ #8435 convention — ratchetRemedyCarriesAuthority() ACCEPTED a message that offers the ' + + 'baseline-expanding path with no marker at all. The predicate is not discriminating, so ' + + 'the assertion above proves nothing.', + ); + } + } + if (failures > 0) { console.error(`\n✗ self-test (read-seam invention rule): ${failures} case(s) failed\n`); return 1; } - console.log(`\n✓ self-test (read-seam invention rule): ${cases.length} case(s) passed\n`); + console.log( + `\n✓ self-test (read-seam invention rule): ${cases.length} case(s) passed, and the baseline ` + + 'offer stays marked maintainer-only (#8435)\n', + ); return 0; } diff --git a/scripts/check-role-word.mjs b/scripts/check-role-word.mjs index 78277d6860..82f8d14854 100644 --- a/scripts/check-role-word.mjs +++ b/scripts/check-role-word.mjs @@ -19,6 +19,11 @@ // run with --update to ratchet the baseline down and commit it. // // node scripts/check-role-word.mjs [--update] +// node scripts/check-role-word.mjs --self-test # verify the checker's own rules +// +// `--update` expands the baseline, which is the shrink-only direction of this +// ratchet — the NEW-use message marks that path `⛔ MAINTAINER-ONLY` per the +// #8435 convention, and the self-test holds the marker in place. // // Scope: content/docs (hand-written; references/ is generated from spec and // excluded — the spec source is the fix site there) and skills/. File and @@ -49,6 +54,132 @@ function countMatches(text) { return m ? m.length : 0; } +// ── The ratchet-remedy authority convention (#8435) ────────────────────────── +// +// This gate's second remedy is `--update`, which expands the baseline. That is +// a shrink-only ratchet, so taking that path WEAKENS the gate — and the message +// used to offer it in the same breath as the real fix, with nothing saying whose +// path it is. The convention landed for check-engine-double-contract.mjs and +// check-type-check-coverage.mjs; the twin blocks there are the reference. +// +// The marker here has to label the ACT, not the file. `--update` does not append +// a line: it rewrites the entire baseline from the current tree (see the +// `update` branch below), so an author reaching for it to admit one new +// occurrence also re-baselines every other file in the same stroke. +// +// ⛔ This STRENGTHENS ratchet governance and weakens nothing. No threshold +// moves, no baseline entry is added, and the verdicts this gate reaches are +// byte-for-byte the ones it reached before — only the diagnostic text changes. + +/** Kept identical to the other gates' token so the convention is greppable. */ +const RATCHET_AUTHORITY_MARKER = '⛔ MAINTAINER-ONLY'; + +/** + * How this gate OFFERS the privileged path, as a detector rather than a string + * compare, so the self-test can prove it still reaches its subject: a reworded + * offer that stopped matching would make the convention check pass vacuously on + * every message. + * + * Deliberately keyed on the baseline-EXPANDING phrasing ("add it to ") and + * not on `--update` alone. The two ratchet-DOWN messages below also name + * `--update`, and ratcheting down is squarely the author's job — a detector that + * caught those would force the maintainer-only marker onto a message where it is + * actively wrong. + */ +const RATCHET_EXPANSION_OFFER = new RegExp( + `add it to\\s+${BASELINE_PATH.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, +); + +/** + * The convention: a message that hands the author the baseline-expanding path + * must say in the same breath that the path is not theirs. A message offering no + * such path is unaffected — this is an authority label, not a vocabulary ban. + * + * @param {string} message + * @returns {boolean} + */ +function ratchetRemedyCarriesAuthority(message) { + if (!RATCHET_EXPANSION_OFFER.test(message)) return true; + return message.includes(RATCHET_AUTHORITY_MARKER); +} + +/** + * The NEW-use verdict's text, named and pure so the self-test can assert on the + * exact string the author reads. A message built inline is a message no + * assertion can reach. + * + * @param {string} file + * @param {number} count + * @returns {string} + */ +function newUseMessage(file, count) { + return ( + `${file}: NEW use of the reserved word "role" (${count} occurrence(s)). ` + + 'ADR-0090 D3: use permission_set / position / business_unit. That is the fix, and the ' + + `only one of the two you can take on your own. ${RATCHET_AUTHORITY_MARKER}, NOT a co-equal ` + + 'option: for a genuine boundary (better-auth, ARIA, quoted history), add it to ' + + `${BASELINE_PATH} by running \`node scripts/check-role-word.mjs --update\`. The gated thing ` + + 'is that ACT, not the file — `--update` rewrites the whole baseline from the current tree, ' + + 'so it admits your occurrence and re-baselines every other file in one stroke. The baseline ' + + 'is shrink-only, so this weakens a ratchet and needs a maintainer to agree the boundary is ' + + 'genuine first — do not take this path to get CI green.' + ); +} + +function selfTest() { + const failures = []; + const expect = (label, cond) => { + if (!cond) failures.push(label); + }; + + // ── The ratchet-remedy authority convention (#8435) ──────────────────────── + // + // Four assertions, deliberately non-overlapping, so each way this can rot is + // caught by exactly one NAMED failure: (1) the detector still reaches its + // subject, (2) the real message carries the marker, (3) an unmarked offer is + // REJECTED, (4) the detector does NOT reach the ratchet-DOWN messages. (3) is + // what makes (2) worth having — a predicate that approved everything would + // keep (2) green with the convention gone. (4) is this gate's own hazard: + // both directions of its ratchet are spelled `--update`, and marking the + // improvement path maintainer-only would teach the opposite of the rule. + const real = newUseMessage('content/docs/example.mdx', 2); + expect('#8435 — the ratchet-offer DETECTOR still matches the NEW-use message (else the check ' + + 'below is vacuous)', + RATCHET_EXPANSION_OFFER.test(real)); + expect(`#8435 — the NEW-use message marks the baseline path ${RATCHET_AUTHORITY_MARKER} (the ` + + 'baseline is shrink-only, so running --update is a maintainer action, not the author\'s ' + + 'second option)', + ratchetRemedyCarriesAuthority(real)); + + // (3)'s fixture is SYNTHETIC rather than the real message with the marker + // stripped: derived, it also fires on a rewording and misdescribes the cause. + const unmarkedOffer = `example.mdx: NEW use. add it to ${BASELINE_PATH} with --update.`; + expect('#8435 — the synthetic unmarked-offer fixture is still recognised as an offer', + RATCHET_EXPANSION_OFFER.test(unmarkedOffer)); + expect('#8435 — ratchetRemedyCarriesAuthority() REJECTS an offer carrying no marker (proves the ' + + 'predicate discriminates rather than approving everything)', + !ratchetRemedyCarriesAuthority(unmarkedOffer)); + + const ratchetDown = `content/docs/example.mdx: role-word count improved 4 → 2 — ratchet DOWN: ` + + 'run `node scripts/check-role-word.mjs --update` and commit the baseline.'; + expect('#8435 — the detector does NOT match the ratchet-DOWN message, which also names --update ' + + '(marking the improvement path maintainer-only would teach the opposite of the rule)', + !RATCHET_EXPANSION_OFFER.test(ratchetDown) && ratchetRemedyCarriesAuthority(ratchetDown)); + + if (failures.length) { + for (const f of failures) console.error(` x self-test: ${f}`); + console.error(`\ncheck-role-word --self-test: ${failures.length} failure(s).\n`); + process.exit(1); + } + console.log( + 'OK self-test: the NEW-use remedy marks baseline expansion as maintainer-only, the predicate ' + + 'rejects an unmarked offer, and the ratchet-DOWN remedy stays the author\'s own.', + ); + process.exit(0); +} + +if (process.argv.includes('--self-test')) selfTest(); + const files = []; for (const root of ROOTS) if (existsSync(root)) walk(root, files); @@ -76,9 +207,7 @@ const errors = []; for (const [file, count] of Object.entries(current)) { const allowed = baseline[file]; if (allowed === undefined) { - errors.push(`${file}: NEW use of the reserved word "role" (${count} occurrence(s)). ` + - `ADR-0090 D3: use permission_set / position / business_unit — or, for a genuine ` + - `boundary (better-auth, ARIA, quoted history), add it to ${BASELINE_PATH} with --update.`); + errors.push(newUseMessage(file, count)); } else if (count > allowed) { errors.push(`${file}: role-word count grew ${allowed} → ${count}. New occurrences are banned (ADR-0090 D3).`); } From 6d0df9ddd696c755be3f37fc2d517b6308f3d3f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 18:09:10 +0000 Subject: [PATCH 2/2] refactor(devx): make the fixture-sanity and discrimination assertions mutually exclusive (#8519) A fixture that stopped being an offer failed BOTH the fixture check and the discrimination check, and the second failure misdescribed the cause. if/else so exactly one can fire -- the shape check-type-check-coverage.mjs already uses. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Jqe56GnYFddggeAyfkZFVz --- scripts/check-driver-conformance.mjs | 17 ++++++++++++----- scripts/check-role-word.mjs | 17 ++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/scripts/check-driver-conformance.mjs b/scripts/check-driver-conformance.mjs index d9c539191b..55a733eb22 100644 --- a/scripts/check-driver-conformance.mjs +++ b/scripts/check-driver-conformance.mjs @@ -1060,11 +1060,18 @@ function selfTest() { const unmarkedOffer = `CONSUMED: driver-example does not run PAGINATION_CASES. Add a suite that drives the shared ` + `cases, or add a measured DEBT/EXEMPT entry to the ledger in ${LEDGER_REL} saying why not.`; - expect('#8435 — the synthetic unmarked-offer fixture is still recognised as an offer', - RATCHET_EXPANSION_OFFER.test(unmarkedOffer)); - expect('#8435 — ratchetRemedyCarriesAuthority() REJECTS an offer carrying no marker (proves ' - + 'the predicate discriminates rather than approving everything)', - !ratchetRemedyCarriesAuthority(unmarkedOffer)); + // if/else, not two flat asserts: a fixture that stopped being an offer would + // ALSO fail the discrimination check, and that second failure would + // misdescribe the cause. Exactly one of these two can fire. + if (!RATCHET_EXPANSION_OFFER.test(unmarkedOffer)) { + expect('#8435 — the synthetic unmarked-offer fixture is no longer recognised as an offer, so ' + + 'it cannot test discrimination at all. Re-spell it to match RATCHET_EXPANSION_OFFER', + false); + } else { + expect('#8435 — ratchetRemedyCarriesAuthority() REJECTS an offer carrying no marker (proves ' + + 'the predicate discriminates rather than approving everything)', + !ratchetRemedyCarriesAuthority(unmarkedOffer)); + } } if (failures.length) { diff --git a/scripts/check-role-word.mjs b/scripts/check-role-word.mjs index 82f8d14854..df83f5bf90 100644 --- a/scripts/check-role-word.mjs +++ b/scripts/check-role-word.mjs @@ -153,12 +153,19 @@ function selfTest() { // (3)'s fixture is SYNTHETIC rather than the real message with the marker // stripped: derived, it also fires on a rewording and misdescribes the cause. + // if/else, not two flat asserts: a fixture that stopped being an offer would + // ALSO fail the discrimination check, and the second failure would misdescribe + // the cause ("the predicate is not discriminating" when the fixture is what + // broke). Exactly one of these two can fire. const unmarkedOffer = `example.mdx: NEW use. add it to ${BASELINE_PATH} with --update.`; - expect('#8435 — the synthetic unmarked-offer fixture is still recognised as an offer', - RATCHET_EXPANSION_OFFER.test(unmarkedOffer)); - expect('#8435 — ratchetRemedyCarriesAuthority() REJECTS an offer carrying no marker (proves the ' - + 'predicate discriminates rather than approving everything)', - !ratchetRemedyCarriesAuthority(unmarkedOffer)); + if (!RATCHET_EXPANSION_OFFER.test(unmarkedOffer)) { + expect('#8435 — the synthetic unmarked-offer fixture is no longer recognised as an offer, so ' + + 'it cannot test discrimination at all. Re-spell it to match RATCHET_EXPANSION_OFFER', false); + } else { + expect('#8435 — ratchetRemedyCarriesAuthority() REJECTS an offer carrying no marker (proves ' + + 'the predicate discriminates rather than approving everything)', + !ratchetRemedyCarriesAuthority(unmarkedOffer)); + } const ratchetDown = `content/docs/example.mdx: role-word count improved 4 → 2 — ratchet DOWN: ` + 'run `node scripts/check-role-word.mjs --update` and commit the baseline.';