Skip to content

Commit 7b20ae2

Browse files
claude[bot]claude
andauthored
tooling(devx): the role-word NEW-use message names the pure-relocation case (#15933)
The baseline is path-keyed, so splitting or moving a baselined file re-addresses its occurrences without adding one: the run reports them as NEW use at the new paths and a ratchet-DOWN at the old one, and the only remedy offered for the first half is the MAINTAINER-ONLY path whose warning is written for a genuine new boundary. Nothing in the message told those two cases apart, so the argument that a relocation is safe was re-derived by hand, by the author and again by the reviewer, every time. The message now names the case, states its recognition signal (a ratchet-DOWN row in the same run whose counts account for the NEW-use rows) and the evidence a reviewer checks (a baseline diff touching only those paths, the old row removed and the new rows summing back to it). Wording only. The verdicts, the accept set and the ratchet's strength are unchanged, and the clause says in writing that the gate will not net the two halves against each other and why: the ratchet is keyed to WHERE the word is, so a file that gains an occurrence while a sibling loses an unrelated one has to stay distinguishable from a move. The clause is generic for the same reason - naming the actual old path and its counts would mean pairing NEW-use rows with vanished baseline rows and summing across files, which is that refused verdict arriving as advice. The two ratchet-DOWN messages become named pure functions, the way grewMessage already is, so the coupling the clause creates is reachable from the self-test; their emitted text is byte-identical. A new self-test battery of seven pins the wording, including the refusal itself and the example diff's arithmetic. Claude-Session: https://claude.ai/code/session_016vtdMao3dQS9EfQfpyWixd Co-authored-by: Claude <noreply@anthropic.com>
1 parent d4c2cb1 commit 7b20ae2

1 file changed

Lines changed: 206 additions & 5 deletions

File tree

scripts/check-role-word.mjs

Lines changed: 206 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@
3939
// marker in place. Both pin the WORDING, not the act — the flag takes either
4040
// direction from whoever runs it.
4141
//
42+
// That message also NAMES the one shape that reads as a new boundary and is not
43+
// one — a pure RELOCATION, where a split or move carried already-baselined
44+
// occurrences to a new path — and states the evidence a reviewer checks for it.
45+
// Wording only: a relocation is still reported as NEW-use rows plus a
46+
// ratchet-DOWN row, and the gate still does not net those halves against each
47+
// other. See `relocationClause()` for why that netting is REFUSED rather than
48+
// merely unimplemented.
49+
//
4250
// Scope: content/docs (hand-written; its `references/` tree is generated from
4351
// spec and excluded BY PATH — the spec source is the fix site there) and
4452
// skills/, walked whole, `references/` included: that tree is published catalog
@@ -774,6 +782,98 @@ function ratchetRemedyCarriesAuthority(message) {
774782
return message.includes(RATCHET_AUTHORITY_MARKER);
775783
}
776784

785+
/**
786+
* The ratchet-DOWN verdict's distinctive phrase, shared by the messages below
787+
* rather than re-typed in each.
788+
*
789+
* `relocationClause()` sends a relocating author looking for THIS row in the
790+
* same run, so the two strings have to move together: a rewording on one side
791+
* that left the other naming a sentence the gate no longer prints would send
792+
* that author hunting for a signal that is not there, and the message would
793+
* still read as correct. The self-test asserts the clause quotes it and that the
794+
* emitted ratchet-DOWN message really carries it.
795+
*/
796+
const RATCHET_DOWN_PHRASE = 'baselined file is clean/gone';
797+
798+
/**
799+
* The relocation paragraph of the NEW-use verdict, named and pure for the same
800+
* reason the messages around it are: the wording IS the deliverable here, so the
801+
* self-test has to reach the exact sentences the author reads.
802+
*
803+
* ## The case it names
804+
*
805+
* The ledger is path-keyed, so a split or move re-addresses occurrences without
806+
* adding one, and the run reports the moved occurrences as NEW use at their new
807+
* paths AND a ratchet-DOWN at the old path. The only remedy offered for the
808+
* first half is the `⛔ MAINTAINER-ONLY` one — the loudest, most-gated path in
809+
* this gate's vocabulary, whose warning is written for a genuine new boundary.
810+
* Nothing in the message told a relocating author their case apart from that
811+
* one, so the argument that it is safe — the occurrences only changed address,
812+
* and the baseline diff touches nothing else — was re-derived by hand, by the
813+
* author and again by the reviewer, on every split.
814+
*
815+
* ## Why this is a MESSAGE and not a verdict
816+
*
817+
* Teaching the gate to net the two halves was considered and refused. A
818+
* total-preserving reading admits the case it cannot tell from a relocation: a
819+
* file that GAINS an occurrence while a sibling loses an unrelated one passes
820+
* it, so a real new use arrives green paired with an unrelated removal. The
821+
* ratchet's whole value is that it is keyed to WHERE, and this is a gate there
822+
* is structural reason to want quieter — a tolerance that makes a genuine
823+
* regression indistinguishable from a move must not be bought with "the total
824+
* did not move".
825+
*
826+
* ⛔ Which is also why this clause is GENERIC and interpolates nothing about the
827+
* run in hand. To name the actual old path and its counts the gate would have to
828+
* pair NEW-use rows with vanished baseline rows and sum across files — the
829+
* refused verdict arriving as advice, computed in the same place and believed
830+
* the same way. So the message states the TEST; the author states the answer, in
831+
* the diff, where a reviewer can check it and the ledger records what was
832+
* agreed.
833+
*
834+
* @returns {string}
835+
*/
836+
function relocationClause() {
837+
return (
838+
'One shape reads as a new boundary and is NOT one: a pure RELOCATION, where a split or move '
839+
+ 'carried already-baselined occurrences to a new path. You are in that case only if THIS '
840+
+ `run also reports \`${RATCHET_DOWN_PHRASE}\` (or an improved count) on the path they came `
841+
+ 'from, and those rows account for every occurrence reported here. The gate will not net '
842+
+ 'those two halves against each other, and is not being taught to: the ratchet is keyed to '
843+
+ 'WHERE the word is, so a file that GAINS an occurrence while a sibling loses an unrelated '
844+
+ 'one has to stay distinguishable from a move. So the path is still this one and still '
845+
+ `${RATCHET_AUTHORITY_MARKER} — what a relocation changes is only that the maintainer's `
846+
+ 'question is cheap to answer, so answer it in the diff: re-baselining a pure move touches '
847+
+ 'nothing but the paths involved, the old row removed and the new rows added summing back '
848+
+ 'to it — `- "OLD_PATH": 2` replaced by `+ "NEW_PATH_A": 1` and `+ "NEW_PATH_B": 1`. Any '
849+
+ 'OTHER row in that diff is `--update` re-baselining something else in the same stroke, '
850+
+ 'which is the act the marker is on, and what you are showing is then not a relocation.'
851+
);
852+
}
853+
854+
/**
855+
* The second half of the same convention: a message that hands the author the
856+
* baseline-expanding path must also name the one case that path is wrong for.
857+
*
858+
* A predicate rather than a substring compare at the assertion site, for the
859+
* reason `ratchetRemedyCarriesAuthority` is one — the self-test can then prove
860+
* it DISCRIMINATES, and a check that approved every long message would keep the
861+
* pin green with the clause gone.
862+
*
863+
* Keyed on the SAME offer detector as the authority marker, so the two rules
864+
* have one subject: a message offering no baseline-expanding path is unaffected,
865+
* and in particular the ratchet-DOWN messages — whose case this clause is about
866+
* — must not be forced to carry a paragraph addressed to someone reading a
867+
* NEW-use row.
868+
*
869+
* @param {string} message
870+
* @returns {boolean}
871+
*/
872+
function newUseOfferNamesRelocation(message) {
873+
if (!RATCHET_EXPANSION_OFFER.test(message)) return true;
874+
return message.includes(RATCHET_DOWN_PHRASE) && message.includes('pure RELOCATION');
875+
}
876+
777877
/**
778878
* The NEW-use verdict's text, named and pure so the self-test can assert on the
779879
* exact string the author reads. A message built inline is a message no
@@ -797,7 +897,8 @@ function newUseMessage(file, count) {
797897
+ 'is that ACT, not the file — `--update` rewrites the whole baseline from the current tree, '
798898
+ 'so it admits your occurrence and re-baselines every other file in one stroke. The baseline '
799899
+ 'is shrink-only, so this weakens a ratchet and needs a maintainer to agree the boundary is '
800-
+ 'genuine first — do not take this path to get CI green.'
900+
+ 'genuine first — do not take this path to get CI green. '
901+
+ relocationClause()
801902
);
802903
}
803904

@@ -828,6 +929,42 @@ function grewMessage(file, allowed, count) {
828929
);
829930
}
830931

932+
/**
933+
* The two ratchet-DOWN verdicts, named and pure for the reason `grewMessage` was
934+
* made so: a message built inline is a message no assertion can reach — and
935+
* these two are now QUOTED by `relocationClause()` as the signal a relocating
936+
* author is told to look for, which is a coupling only a reachable string can
937+
* pin. The text each emits is unchanged.
938+
*
939+
* ⛔ Neither offers the baseline-EXPANDING path, so neither carries the
940+
* maintainer-only marker and neither may acquire one: ratcheting down is
941+
* squarely the author's own remedy, and `RATCHET_EXPANSION_OFFER` is keyed so
942+
* that it does not reach these.
943+
*
944+
* @param {string} file
945+
* @param {number} allowed
946+
* @returns {string}
947+
*/
948+
function cleanOrGoneMessage(file, allowed) {
949+
return (
950+
`${file}: ${RATCHET_DOWN_PHRASE} (was ${allowed}) — ratchet DOWN: run `
951+
+ '`node scripts/check-role-word.mjs --update` and commit the baseline.'
952+
);
953+
}
954+
955+
/**
956+
* @param {string} file
957+
* @param {number} allowed
958+
* @param {number} now
959+
* @returns {string}
960+
*/
961+
function improvedMessage(file, allowed, now) {
962+
return (
963+
`${file}: role-word count improved ${allowed}${now} — ratchet DOWN: run `
964+
+ '`node scripts/check-role-word.mjs --update` and commit the baseline.'
965+
);
966+
}
967+
831968
// ── What a PASSING run tells the reader (#9910) ────────────────────────
832969
//
833970
// The green line used to be, in full:
@@ -1123,6 +1260,7 @@ let selfTestReachedVerdict = false;
11231260
const SELF_TEST_BATTERIES = Object.freeze({
11241261
'The scan population: which `references/` the skip means (#15061)': 5,
11251262
'The ratchet-remedy authority convention (#8435)': 4,
1263+
'The NEW-use message names the relocation case (#14659)': 7,
11261264
'The green body reports what was READ (#9910)': 5,
11271265
'A missing ROOT is REFUSED, per root (#9932)': 8,
11281266
'The dispatch-gates declaration (#9964\'s pattern)': 4,
@@ -1137,7 +1275,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
11371275

11381276
// DELETING an entry silences that battery's floor exactly as effectively as
11391277
// zeroing it, so the roster's own size is pinned too.
1140-
const SELF_TEST_BATTERY_FLOOR = 12;
1278+
const SELF_TEST_BATTERY_FLOOR = 13;
11411279

11421280
// The key an assertion is filed under when no battery is open. It is not a
11431281
// declared battery, so it reds by the same set difference rather than silently
@@ -1256,6 +1394,65 @@ function selfTest() {
12561394
+ '(marking the improvement path maintainer-only would teach the opposite of the rule)',
12571395
!RATCHET_EXPANSION_OFFER.test(ratchetDown) && ratchetRemedyCarriesAuthority(ratchetDown));
12581396

1397+
// ── The NEW-use message names the relocation case (#14659) ─────────────────
1398+
//
1399+
// Seven assertions over the clause the ledger's path-keying makes necessary. A
1400+
// split moves baselined occurrences to new paths, so the run reports them as
1401+
// NEW use and the old path as a ratchet-DOWN, and the author is routed down
1402+
// the maintainer-only path by a warning written for a different case.
1403+
//
1404+
// (3) is the load-bearing one, and it is not the obvious one. The risk this
1405+
// wording carries is not that it disappears — (1) and (2) cover that — but
1406+
// that a later edit SOFTENS it into an apology for the gate. The refused
1407+
// verdict (net the two halves out and call it a relocation) is exactly what a
1408+
// reader re-argues from a message that reads as tolerance, so the refusal and
1409+
// its reason have to be pinned as text, not left to the paragraph's tone.
1410+
battery('The NEW-use message names the relocation case (#14659)');
1411+
const clause = relocationClause();
1412+
expect('#14659 — the relocation clause is REACHED by the real NEW-use message (else every '
1413+
+ 'assertion below is about a string no author is ever shown)',
1414+
real.includes(clause));
1415+
expect('#14659 — the NEW-use message satisfies newUseOfferNamesRelocation(): a message offering '
1416+
+ 'the baseline-expanding path names the one case that path is wrong for',
1417+
newUseOfferNamesRelocation(real));
1418+
1419+
expect('#14659 — the clause REFUSES the netting in writing and gives the reason (keyed to '
1420+
+ 'WHERE), so the wording cannot be read as the tolerance that was refused — a gained '
1421+
+ 'occurrence paired with an unrelated loss must stay distinguishable from a move',
1422+
clause.includes('will not net') && clause.includes('not being taught to')
1423+
&& clause.includes('keyed to WHERE') && clause.includes('unrelated'));
1424+
1425+
// The signal the clause sends the author looking for has to be one the gate
1426+
// really prints. Read off the EMITTED messages on both sides, so a rewording
1427+
// of either fails here rather than in a reader's terminal.
1428+
const emittedGone = cleanOrGoneMessage('content/docs/example.mdx', 2);
1429+
const emittedImproved = improvedMessage('content/docs/example.mdx', 4, 2);
1430+
expect('#14659 — both ratchet-DOWN rows the clause tells the author to look for are rows the '
1431+
+ 'gate actually emits (the recognition signal has to exist to be looked for)',
1432+
clause.includes(RATCHET_DOWN_PHRASE) && emittedGone.includes(RATCHET_DOWN_PHRASE)
1433+
&& clause.includes('improved count') && emittedImproved.includes('count improved'));
1434+
1435+
// The evidence a reviewer checks, pinned as ARITHMETIC rather than as a
1436+
// quotation: an example edited into rows that do not sum would teach the wrong
1437+
// audit while every substring assertion above stayed green.
1438+
const removedRow = /`- "[A-Z_]+": (\d+)`/.exec(clause);
1439+
const addedRows = [...clause.matchAll(/`\+ "[A-Z_]+": (\d+)`/g)];
1440+
expect('#14659 — the clause carries the baseline-diff shape a pure relocation produces: ONE '
1441+
+ 'removed row, MORE THAN ONE added row, and the added rows summing back to the removed one',
1442+
removedRow !== null && addedRows.length > 1
1443+
&& addedRows.reduce((sum, m) => sum + Number(m[1]), 0) === Number(removedRow[1]));
1444+
1445+
// Discrimination, the same shape as #8435's own (3) and for the same reason:
1446+
// without it, a predicate that approved everything would keep assertion (2)
1447+
// green with the clause gone. The fixture is the synthetic offer already built
1448+
// for #8435 — an offer carrying no relocation wording at all.
1449+
expect('#14659 — newUseOfferNamesRelocation() REJECTS a baseline-expanding offer that does not '
1450+
+ 'name the case (proves the predicate discriminates rather than approving every message)',
1451+
!newUseOfferNamesRelocation(unmarkedOffer));
1452+
expect('#14659 — newUseOfferNamesRelocation() leaves the ratchet-DOWN message alone: it is an '
1453+
+ 'offer-scoped rule, not a vocabulary requirement on every message this gate prints',
1454+
newUseOfferNamesRelocation(ratchetDown) && newUseOfferNamesRelocation(emittedGone));
1455+
12591456
// ── The green body reports what was READ (#9910) ──────────────────────
12601457
//
12611458
// Interpolated counts again, so the source proves nothing about the rendered
@@ -2067,7 +2264,11 @@ function selfTest() {
20672264
+ 'directions pinned from the WALK, never from a typed count, so a skip that empties the '
20682265
+ 'published half and one that swallows the generated half each name themselves. '
20692266
+ 'The NEW-use remedy marks baseline expansion as maintainer-only, the predicate '
2070-
+ 'rejects an unmarked offer, the ratchet-DOWN remedy stays the author\'s own, and both '
2267+
+ 'rejects an unmarked offer, the ratchet-DOWN remedy stays the author\'s own, and that '
2268+
+ 'same remedy NAMES the pure-relocation case, quotes a ratchet-DOWN row the gate really '
2269+
+ 'prints as its recognition signal, carries a baseline diff whose rows are pinned to SUM, '
2270+
+ 'and refuses the netting in writing — so the wording tells a relocating author apart from '
2271+
+ 'a new boundary without the gate ever reading one row against another. Both '
20712272
+ 'success texts report what was READ \u2014 so a scanned tree and an unscanned one cannot print '
20722273
+ 'the same result once the ledger is empty. Every separator-less ROOT also declares the '
20732274
+ 'subtree spelling dispatch-gates derives from, and declares nothing this gate does not '
@@ -2205,9 +2406,9 @@ for (const [file, count] of Object.entries(current)) {
22052406
for (const [file, allowed] of Object.entries(baseline)) {
22062407
const now = current[file];
22072408
if (now === undefined) {
2208-
errors.push(`${file}: baselined file is clean/gone (was ${allowed}) — ratchet DOWN: run \`node scripts/check-role-word.mjs --update\` and commit the baseline.`);
2409+
errors.push(cleanOrGoneMessage(file, allowed));
22092410
} else if (now < allowed) {
2210-
errors.push(`${file}: role-word count improved ${allowed}${now} — ratchet DOWN: run \`node scripts/check-role-word.mjs --update\` and commit the baseline.`);
2411+
errors.push(improvedMessage(file, allowed, now));
22112412
}
22122413
}
22132414

0 commit comments

Comments
 (0)