Observation-class finding, filed unassigned from #8331's implementation (PR #10568). Nothing is broken today. Recording it so it is not rediscovered.
What
.github/workflows/lint.yml's aggregator comment declares an ordering contract in prose:
Build the ledgered packages' dependencies must stay immediately above the re-measure — both are in typecheck-debt.
Nothing enforces it, and — this is the part worth recording — violating it would not fail loudly either.#8331's implementation measured why:
pnpm check:type-check-debt (--re-measure) calls unbuiltClosure()beforerefreshBuiltClosure(), and refuses rather than building from nothing. So the workflow step really is load-bearing in principle: the gate cannot rescue a closure member no earlier step built.- But of the 55 workspace packages in the ledgered dependency closure, the step above it (
Build workspace packages, --filter='./packages/*' --filter='./examples/*^...') already covers all 55 on the current tree. Measured with turbo's own --dry=json package sets against the gate's own closure enumeration.
⇒ Move the build step to another lane, or delete it, and the refusal does not fire; refreshBuiltClosure() quietly does the work instead and the lane stays green. The prose contract is unenforced and self-healing, which is the combination that lets it rot unnoticed until the day the coverage accident stops holding.
The margin is one package: @objectstack/service-knowledge (packages/services/, pulled into the closure by the ledgered @objectstack/knowledge-ragflow) is reached by that preceding step only through ./examples/*^..., i.e. because examples/app-todo happens to depend on it — the same accident lint.yml already records one step above for #3037. C \ (./packages/* alone) = exactly that one package.
Why it is filed rather than fixed
PR #10568 deliberately stayed comment-only. A mechanical pin is cheap and would not need a new gate or a new lint.yml step, which is why this is worth recording rather than dropping: scripts/check-type-check-coverage.mjs's observed()already reads .github/workflows/lint.yml and already derives CI-wiring facts from it (ciInvokesTask, ciInvokesRoot) that its evaluate() asserts. A third derived fact — "the step immediately before the one running pnpm check:type-check-debt runs the closure build with the same filters refreshBuiltClosure() uses" — would ride the existing --self-test, which two jobs already invoke, and would let the filters be single-sourced in the script instead of hand-copied.
Why it may not be worth doing
Post-#8330 the drift this would catch is already loud in the direction that matters: the gate refreshes with its own filters before measuring, so a narrower workflow filter ends in the unbuiltClosure() refusal and a wider one costs cache hits. Neither can make CI measure a different world than a local run, which is what #8271 was. What a pin buys is protection of the ordering claim itself — currently resting on a set inclusion that no test asserts and that one manifest edit in examples/app-todo can break.
Whoever picks this up should decide against that trade, not assume it.
Observation-class finding, filed unassigned from #8331's implementation (PR #10568). Nothing is broken today. Recording it so it is not rediscovered.
What
.github/workflows/lint.yml's aggregator comment declares an ordering contract in prose:Nothing enforces it, and — this is the part worth recording — violating it would not fail loudly either.#8331's implementation measured why:
pnpm check:type-check-debt(--re-measure) callsunbuiltClosure()beforerefreshBuiltClosure(), and refuses rather than building from nothing. So the workflow step really is load-bearing in principle: the gate cannot rescue a closure member no earlier step built.Build workspace packages,--filter='./packages/*' --filter='./examples/*^...') already covers all 55 on the current tree. Measured with turbo's own--dry=jsonpackage sets against the gate's own closure enumeration.⇒ Move the build step to another lane, or delete it, and the refusal does not fire;
refreshBuiltClosure()quietly does the work instead and the lane stays green. The prose contract is unenforced and self-healing, which is the combination that lets it rot unnoticed until the day the coverage accident stops holding.The margin is one package:
@objectstack/service-knowledge(packages/services/, pulled into the closure by the ledgered@objectstack/knowledge-ragflow) is reached by that preceding step only through./examples/*^..., i.e. becauseexamples/app-todohappens to depend on it — the same accidentlint.ymlalready records one step above for #3037.C \ (./packages/* alone)= exactly that one package.Why it is filed rather than fixed
PR #10568 deliberately stayed comment-only. A mechanical pin is cheap and would not need a new gate or a new
lint.ymlstep, which is why this is worth recording rather than dropping:scripts/check-type-check-coverage.mjs'sobserved()already reads.github/workflows/lint.ymland already derives CI-wiring facts from it (ciInvokesTask,ciInvokesRoot) that itsevaluate()asserts. A third derived fact — "the step immediately before the one runningpnpm check:type-check-debtruns the closure build with the same filtersrefreshBuiltClosure()uses" — would ride the existing--self-test, which two jobs already invoke, and would let the filters be single-sourced in the script instead of hand-copied.Why it may not be worth doing
Post-#8330 the drift this would catch is already loud in the direction that matters: the gate refreshes with its own filters before measuring, so a narrower workflow filter ends in the
unbuiltClosure()refusal and a wider one costs cache hits. Neither can make CI measure a different world than a local run, which is what #8271 was. What a pin buys is protection of the ordering claim itself — currently resting on a set inclusion that no test asserts and that one manifest edit inexamples/app-todocan break.Whoever picks this up should decide against that trade, not assume it.