Skip to content

finding: lint.yml's "must stay immediately above the re-measure" ordering contract is prose-only, and violating it is currently SILENT #10572

Description

@os-zhuang

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions