Skip to content

Commit 0a2f63a

Browse files
claude[bot]claude
andauthored
fix(devx): branch the TEST_DEBT graduation remedy on the ledger it graduates from (#11848)
`check-type-check-coverage.mjs` printed one graduation note for both of its ledgers, offering both remedies joined by an "or": add a `typecheck` script, or drop the test exclusion. Each remedy belongs to exactly one ledger, and the gate's own header states the distinction the message dropped -- DEBT is "src does not check", TEST_DEBT is "src checks, tests are hidden". Measured on the ledgers at e47d5ef, both halves misfire on TEST_DEBT: * "add a `typecheck` script" is a no-op for 19 of 19 TEST_DEBT entries. Every one already declares one -- that is what makes them TEST_DEBT. * "drop the test exclusion" turns `check:type-source-resolution` from exit 0 to exit 1 on 14 of the 18 entries that have an exclusion to drop, measured by doing it per package and restoring. The re-admitted tests import workspace packages the src program never held, and that gate's registry is shrink-only, so the author arrives at a second gate with no remedy at all. The 19th entry, `@objectstack/cli`, has no exclusion to drop -- its tests are hidden by an `include` that never reaches them. On DEBT the sentence misfires once more, on the entry easiest to get wrong: the workspace root's `typecheck` slot is the aggregator (`turbo run typecheck`) and its own TypeScript is read through `typecheck:root`. A taker following the old note verbatim would overwrite every other package's typecheck. So `graduationRemedy()` keys the remedy on `m.ledger`, which was already on every measurement. Neither remedy is dropped -- each prints in the branch that owns it. TEST_DEBT keeps both of its real routes and names the precondition plus `pnpm check:type-source-resolution`, the command that decides which one is available: a message that needs a second gate's source read to act on has not fixed what this card names. No verdict and no number moves. Graduation candidates remain a note, the structural run's output is byte-identical, and the `--lower` clause is pinned across all branches as the half that must not move. Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx Co-authored-by: Claude <noreply@anthropic.com>
1 parent 99150ec commit 0a2f63a

1 file changed

Lines changed: 213 additions & 5 deletions

File tree

scripts/check-type-check-coverage.mjs

Lines changed: 213 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -712,8 +712,8 @@ const EXEMPT = {
712712
//
713713
// `@objectstack/trigger-record-change` GRADUATED from this ledger (entry: 9 raw
714714
// TS2353, re-measured 0). It is worth a line here because BOTH remedies the
715-
// graduation message above offers were wrong for it, and that message is what
716-
// the next taker will read:
715+
// graduation message offered at the time were wrong for it -- that message has
716+
// since been split per ledger (#11491), and this is the case it was split on:
717717
// - "add a `typecheck` script" -- it already had one, and always had. The
718718
// package was never in DEBT's hole ("src does not check"); it was in this
719719
// ledger's ("src checks, tests are hidden"), which is why the message's
@@ -731,6 +731,17 @@ const EXEMPT = {
731731
// general lesson, which is this ledger's to carry: the two remedies are
732732
// interchangeable only where the excluded tests import nothing the src layer
733733
// does not, and that is a property to MEASURE per package, never to assume.
734+
//
735+
// SINCE MEASURED, across this whole ledger rather than on that one package
736+
// (#11491, at e47d5ef61, by dropping each entry's `"**/*.test.ts"` exclusion
737+
// and reading `check:type-source-resolution`): 14 of the 18 entries that HAVE
738+
// an exclusion go red the way trigger-record-change did, 4 stay green
739+
// (`objectql`, `lint`, `formula`, `verify`), and the 19th (`cli`) has no
740+
// exclusion to drop at all -- its tests are hidden by an `include` that never
741+
// reaches them. So that package was the majority case, not the exception, and
742+
// the graduation message no longer offers the exclusion route without its
743+
// precondition. Re-measure before relying on the split: it moves with every
744+
// import a test file gains.
734745
const TEST_DEBT = {
735746
'@objectstack/plugin-approvals': {
736747
errors: 348,
@@ -3077,6 +3088,9 @@ function measureLedgers(packages, rootName, state) {
30773088
ledger: 'DEBT',
30783089
name,
30793090
dir,
3091+
// The root is a ledger member like any other and its remedy is NOT the
3092+
// same edit -- its `typecheck` slot is the workspace aggregator (#11491).
3093+
isRoot: name === rootName,
30803094
recorded: entry.errors ?? 0,
30813095
note: entry.note,
30823096
compositionAt: entry.compositionAt,
@@ -3090,6 +3104,7 @@ function measureLedgers(packages, rootName, state) {
30903104
ledger: 'TEST_DEBT',
30913105
name,
30923106
dir,
3107+
isRoot: name === rootName,
30933108
recorded: entry.errors ?? 0,
30943109
note: entry.note,
30953110
compositionAt: entry.compositionAt,
@@ -3161,6 +3176,104 @@ function ratchetRemedyCarriesAuthority(message) {
31613176
return message.includes(RATCHET_AUTHORITY_MARKER);
31623177
}
31633178

3179+
// ── The graduation remedy is a function of the LEDGER (#11491) ──────────────
3180+
//
3181+
// This message used to offer both ledgers' remedies in one breath -- `add a
3182+
// "typecheck" script, OR drop the test exclusion` -- joined by an "or" that
3183+
// says they are alternatives a reader may pick between. They are not
3184+
// alternatives. Each one belongs to exactly one ledger, and the header three
3185+
// screens above already states the distinction the message dropped: DEBT is
3186+
// "src does not check", TEST_DEBT is "src checks, tests are hidden".
3187+
//
3188+
// The reader is, by construction, someone who does NOT already know which
3189+
// applies -- a graduation is a once-per-package event and this note is what
3190+
// tells them what to do about it. Measured on the ledgers as they stand at
3191+
// e47d5ef61, both halves of the old sentence misfire, and not marginally:
3192+
//
3193+
// * "add a `typecheck` script" is a NO-OP for 19 of the 19 TEST_DEBT
3194+
// entries. Every one of them already declares one -- that is what makes
3195+
// them TEST_DEBT rather than DEBT. A taker adds a script that is already
3196+
// there, or concludes the reading is wrong.
3197+
// * "drop the test exclusion" is a RED `main` for 14 of the 18 TEST_DEBT
3198+
// entries that have an exclusion to drop, measured by doing it: remove
3199+
// `"**/*.test.ts"` from the package's `tsconfig.json` and
3200+
// `check:type-source-resolution` goes exit 0 -> exit 1, naming the
3201+
// workspace packages the re-admitted tests import and the src program
3202+
// never held (plugin-approvals surfaces 6, runtime 9, http-conformance 5).
3203+
// That gate's registry is shrink-only and its own message rules that
3204+
// widening the entry is not the fix, so the author who followed this note
3205+
// arrives at a second gate with no remedy at all. The 19th entry,
3206+
// `@objectstack/cli`, has no exclusion to drop -- its tests are hidden by
3207+
// an `include` that never reaches them -- so the remedy names an edit that
3208+
// does not exist for it.
3209+
// * on DEBT the sentence misfires once more, on the entry it is easiest to
3210+
// get wrong: the workspace root's `typecheck` slot is the aggregator
3211+
// (`turbo run typecheck`), and its own TypeScript is read through
3212+
// `typecheck:root`. A taker following this note verbatim overwrites every
3213+
// other package's typecheck with a bare `tsc --noEmit`.
3214+
//
3215+
// `@objectstack/trigger-record-change` is the worked case behind #11491 -- it
3216+
// graduated by the #5286 sibling route after both offered remedies proved
3217+
// wrong for it, and the paragraph above TEST_DEBT records it. That package is
3218+
// gone from the ledger; the shape it demonstrated is what the 19 still there
3219+
// would each hit.
3220+
//
3221+
// So the fix is not more words. It is the branch: `m.ledger` is already on
3222+
// every measurement, and each ledger's remedy prints only where it is the
3223+
// remedy. Neither is dropped -- both are still offered, in the branch that
3224+
// owns them. Within TEST_DEBT there IS a real choice of route, so that one
3225+
// keeps both and names the PRECONDITION plus the command that decides it: a
3226+
// message the reader has to open a gate's source to act on has not fixed
3227+
// anything.
3228+
//
3229+
// ⛔ This changes no verdict and no number. Graduation candidates were, and
3230+
// remain, a NOTE -- never a failure.
3231+
3232+
/**
3233+
* The remedy for one graduation candidate, keyed on the ledger it is
3234+
* graduating from. Pure, and separate from the note that frames it, so the
3235+
* self-test can pin each branch's content AND its ANTI-content -- the DEBT
3236+
* branch must not carry TEST_DEBT's remedy, which is the exact defect this
3237+
* replaces and the one a well-meaning re-merge would reintroduce.
3238+
*
3239+
* An unrecognised ledger gets no remedy text rather than an inherited one: a
3240+
* third ledger silently receiving DEBT's advice is how this message became
3241+
* wrong in the first place.
3242+
*
3243+
* @param {{ledger: string, isRoot?: boolean}} measurement
3244+
* @returns {string}
3245+
*/
3246+
function graduationRemedy({ ledger, isRoot = false }) {
3247+
if (ledger === 'TEST_DEBT') {
3248+
return (
3249+
`Onboard it: put the hidden test files in front of tsc, and delete the TEST_DEBT entry in the same ` +
3250+
`PR. ⛔ Adding a \`typecheck\` script is NOT the remedy here -- this ledger is "src checks, tests ` +
3251+
`are hidden", so the package already has one.\n` +
3252+
` (a) The #5286 sibling route: add a \`tsconfig.test.json\` that reaches the ` +
3253+
`tests and NAME it in the \`typecheck\` script. Always available -- it leaves \`tsconfig.json\` alone.\n` +
3254+
` (b) Drop the \`**/*.test.ts\` entry from \`exclude\` in \`tsconfig.json\` (or widen \`include\` to ` +
3255+
`reach the test tree). Available ONLY while \`pnpm check:type-source-resolution\` still passes with ` +
3256+
`the tests re-admitted: that gate reads \`tsconfig.json\` and nothing else, the re-admitted tests ` +
3257+
`import workspace packages this package's src program never held, and its registry is ⛔ SHRINK-ONLY ` +
3258+
`-- registering the new ones is not the way out. Measured red on 14 of the 18 entries that have an ` +
3259+
`exclusion to drop, so assume (b) is unavailable until that gate says otherwise. Run it before you ` +
3260+
`commit; nothing in this gate's own verdict will tell you.`
3261+
);
3262+
}
3263+
if (ledger === 'DEBT') {
3264+
return isRoot
3265+
? `Onboard it: add a \`typecheck:root\` script that invokes tsc AND the step in ` +
3266+
`.github/workflows/lint.yml that runs it -- this gate requires both -- then delete the DEBT ` +
3267+
`entry in the same PR. ⛔ NOT \`typecheck\`: the root's \`typecheck\` slot is the workspace ` +
3268+
`aggregator (\`turbo run typecheck\`), and overwriting it with \`tsc --noEmit\` would stop every ` +
3269+
`other package's typecheck from running while this gate went green.`
3270+
: `Onboard it: add \`"typecheck": "tsc --noEmit"\` to its package.json, and delete the DEBT entry ` +
3271+
`in the same PR. This ledger is "src does not check", so the package has no \`typecheck\` script ` +
3272+
`to begin with -- COVERED would already be red if it did.`;
3273+
}
3274+
return `Onboard it and delete the ${ledger} entry in the same PR.`;
3275+
}
3276+
31643277
/**
31653278
* MEASURED's verdict, pure over already-taken measurements so the self-test
31663279
* pins the semantics without running a compiler.
@@ -3201,9 +3314,9 @@ function evaluateMeasurements(measurements) {
32013314
);
32023315
} else if (m.actual === 0 && m.recorded > 0) {
32033316
notes.push(
3204-
`${m.name}: ${m.ledger} records ${m.recorded}, and tsc now reports 0 -- graduation candidate. ` +
3205-
`Onboard it (add \`"typecheck": "tsc --noEmit"\`, or drop the test exclusion, and delete the ` +
3206-
`ledger entry in the same PR). \`--lower\` deliberately leaves this one alone: 0 is not a lower ` +
3317+
`${m.name}: ${m.ledger} records ${m.recorded}, and tsc now reports 0 -- graduation candidate.\n` +
3318+
` ${graduationRemedy(m)}\n` +
3319+
` \`--lower\` deliberately leaves this one alone: 0 is not a lower ` +
32073320
`ceiling, it is a graduation, and an entry recording 0 fails the structural half of this gate.`,
32083321
);
32093322
} else if (m.actual < m.recorded) {
@@ -4323,6 +4436,101 @@ function selfTest() {
43234436
}
43244437
}
43254438

4439+
// ── The graduation remedy is a function of the LEDGER (#11491) ─────────────
4440+
//
4441+
// The defect this replaces was a message that read CORRECTLY on the ledger it
4442+
// happened to be written for and wrongly on the other one, so every assertion
4443+
// here comes in a pair: the remedy that must be present, and the remedy that
4444+
// must be ABSENT. A regex that only checks presence would stay green if the
4445+
// two branches were re-merged into one sentence -- which is precisely the
4446+
// state this replaces, and the state a well-meaning "simplification" returns
4447+
// to.
4448+
//
4449+
// The graduation note is a NOTE in every case below. None of these fixtures
4450+
// produces a problem, and an implementation that made a graduation red would
4451+
// fail the `problems.length` half of every one of them.
4452+
const gradNote = (m) => evaluateMeasurements([{ recorded: 7, actual: 0, name: 'a', ...m }]).notes[0];
4453+
const debtGrad = gradNote({ ledger: 'DEBT' });
4454+
const testDebtGrad = gradNote({ ledger: 'TEST_DEBT' });
4455+
const rootGrad = gradNote({ ledger: 'DEBT', isRoot: true });
4456+
4457+
const ADD_SCRIPT = '`"typecheck": "tsc --noEmit"`';
4458+
const gradCases = [
4459+
{
4460+
label: 'DEBT graduation offers the script remedy -- that ledger IS "src does not check"',
4461+
message: debtGrad,
4462+
present: [ADD_SCRIPT],
4463+
absent: ['drop the test exclusion'],
4464+
why: 'a DEBT graduate has no `typecheck` script at all; naming the test exclusion here sends the '
4465+
+ 'author to edit a tsconfig that is not the hole.',
4466+
},
4467+
{
4468+
label: 'TEST_DEBT graduation does NOT tell the author to add a script it already has',
4469+
message: testDebtGrad,
4470+
present: ['put the hidden test files in front of tsc'],
4471+
absent: [ADD_SCRIPT],
4472+
why: 'measured at e47d5ef61: 19 of 19 TEST_DEBT entries already declare a `typecheck` script, so '
4473+
+ 'that remedy is a no-op on every one of them -- the misfire #11491 was filed on.',
4474+
},
4475+
{
4476+
label: 'TEST_DEBT graduation names the gate that DECIDES whether the exclusion route is available',
4477+
message: testDebtGrad,
4478+
present: ['check:type-source-resolution', 'SHRINK-ONLY', 'tsconfig.test.json'],
4479+
absent: [],
4480+
why: 'the exclusion route reds that gate on 14 of the 18 entries that have an exclusion, and this '
4481+
+ 'gate never runs it. A message the author has to read a second gate\'s SOURCE to act on is the '
4482+
+ 'half of #11491 that a correct-but-terse rewrite would leave unfixed.',
4483+
},
4484+
{
4485+
label: 'the workspace root graduates through `typecheck:root`, never through `typecheck`',
4486+
message: rootGrad,
4487+
present: ['`typecheck:root`', 'aggregator'],
4488+
absent: [ADD_SCRIPT],
4489+
why: 'the root\'s `typecheck` slot is `turbo run typecheck`; an author who overwrote it with '
4490+
+ '`tsc --noEmit` would stop every other package from being type-checked and this gate would '
4491+
+ 'still go green.',
4492+
},
4493+
{
4494+
label: 'an unrecognised ledger inherits NEITHER remedy',
4495+
message: gradNote({ ledger: 'FUTURE_DEBT' }),
4496+
present: ['FUTURE_DEBT'],
4497+
absent: [ADD_SCRIPT, 'drop the test exclusion', 'check:type-source-resolution'],
4498+
why: 'a third ledger silently receiving DEBT\'s advice is how this message was wrong for TEST_DEBT '
4499+
+ 'for its whole life. Saying less is the only safe default.',
4500+
},
4501+
];
4502+
for (const c of gradCases) {
4503+
for (const needle of c.present) {
4504+
if (!c.message.includes(needle))
4505+
failures.push(`#11491 graduation remedy — ${c.label}: message does not contain ${needle}. ${c.why}`);
4506+
}
4507+
for (const needle of c.absent) {
4508+
if (c.message.includes(needle))
4509+
failures.push(
4510+
`#11491 graduation remedy — ${c.label}: message STILL contains ${needle}, which is the other `
4511+
+ `ledger's remedy. ${c.why}`,
4512+
);
4513+
}
4514+
}
4515+
4516+
// THE CONTROL. Splitting the remedy must not move the half of this note that
4517+
// is about `--lower` rather than about either ledger: it is the same sentence
4518+
// for a graduation from anywhere, and it is the one that stops a graduation
4519+
// being auto-written into the ledger as a 0. Pinned across ALL branches, so a
4520+
// future branch that forgets to carry it is named here rather than noticed
4521+
// when someone runs `--lower` on a graduate.
4522+
const LOWER_CLAUSE =
4523+
'`--lower` deliberately leaves this one alone: 0 is not a lower ceiling, it is a graduation, and an '
4524+
+ 'entry recording 0 fails the structural half of this gate.';
4525+
for (const [what, message] of [['DEBT', debtGrad], ['TEST_DEBT', testDebtGrad], ['the root', rootGrad]]) {
4526+
if (!message.includes(LOWER_CLAUSE))
4527+
failures.push(
4528+
`#11491 graduation remedy — the \`--lower\` clause is missing from the ${what} branch. That `
4529+
+ 'sentence is ledger-independent and must survive the split; without it a graduate reads as '
4530+
+ 'something `--lower` could write back as 0, which fails the structural half of this gate.',
4531+
);
4532+
}
4533+
43264534
// The counter is the other half that can be silently wrong: over-count and
43274535
// main goes red for nothing, under-count and the ratchet hands out free
43284536
// headroom. Multi-line elaborations are the trap -- one TS2322 can print five

0 commit comments

Comments
 (0)