@@ -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.
734745const 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