Skip to content

ci(lint): record why the ledgered-closure build is NOT a duplicate of the gate's own refresh - #10568

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-8331-lint-ledgered-deps-duplicate
Aug 21, 2026
Merged

ci(lint): record why the ledgered-closure build is NOT a duplicate of the gate's own refresh#10568
os-zhuang merged 1 commit into
mainfrom
claude/issue-8331-lint-ledgered-deps-duplicate

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes: #8331

What the card asked, and what current main says

#8331 observed that lint.yml's "Build the ledgered packages' dependencies" step runs the same turbo command, with the same filters, that refreshBuiltClosure() runs inside pnpm check:type-check-debt — and asked whoever picked it up to decide between removing the step and commenting it.

Re-derived on origin/main@ceb33a9f12. The duplication is real and byte-identical, and it is not redundant. This PR is comment-only: no step, command, filter or ordering changed (git diff --stat: 1 file changed, 30 insertions(+)).

The duplication, as it stands today

ownertext
.github/workflows/lint.yml:2487 (lane typecheck-debt)pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'
scripts/check-type-check-coverage.mjs:1684 (refreshBuiltClosure())['run','build','--filter=./packages/*','--filter=./packages/*/*']

(The third instance, lint.yml:2639 in lane typecheck-consumers, is a different lane with no gate that refreshes anything — out of this card's scope and already documented in place.)

Why removing it would be wrong — the ordering the workflow never mentioned

--re-measure calls unbuiltClosure()before it calls refreshBuiltClosure(), and refuses rather than building from nothing. That order is deliberate and argued in the gate's own docblock (a cold 7-task closure measured 3m2s, on a gate people run before pushing). So the refresh owns the present-but-stale case only (#8271) — it cannot rescue a closure member that no earlier step built.

What the preceding step actually covers — measured, not assumed

The gate enumerates the ledgered dependency closure itself when nothing is built: 55 workspace packages. Against turbo's own --dry=json package sets:

filter setpackagesclosure members missing
./packages/* alone581@objectstack/service-knowledge
./packages/* + ./examples/*^... (the step above)680
./packages/* + ./packages/*/* (this step)740

The middle row is why removal is tempting and the top row is why it is wrong. @objectstack/service-knowledge lives at packages/services/, is pulled into the closure by the ledgered @objectstack/knowledge-ragflow, and is reached in this lane only through ./examples/*^... — because examples/app-todo happens to depend on it. That is the same accident lint.yml already records one step above for #3037.

⇒ Delete this step and the lane's green starts depending on one example app's manifest. Drop that dependency and check:type-check-debt refuses in CI, with a remedy message naming the command the workflow no longer runs.

And the drift argument is now loud, not silent

The card's case for removal was that "a precondition owned in two places drifts". Since #8330 the gate refreshes with its own filters before measuring, so divergence can only end in that refusal (narrower here) or extra cache hits (wider here) — never in CI measuring a different world than a local run, which is what #8271 was. The two owners are safe; what was missing is that nothing said so. This PR says so, at the step.

Verification

Gate union re-derived with node scripts/pm/dispatch-gates.mjs (no path arguments) on the committed diff — 10 families, all run at 7b2ce42081:

check-node-version: OK (32 setup-node step(s) across 26 workflow(s), all on Node 22).
✓ check-required-contexts: 6 required context name(s) pinned across 2 workflow(s); 5 instruction surface(s) scanned against 2 retired name(s) (#9491).
✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
check-workflow-status-functions: OK (scanned 26 workflow file(s), 49 job(s), 24 job-level if: expression(s); 10 read needs.*.outputs.*, all naming a status function).
✓ check-half-states self-test: 715 cases pass.
check-type-check-coverage: OK — 64/77 workspace packages type-checked (plus the root), 13 in the DEBT ledger, 1 exempt.
check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 254.2s, 1924 raw tsc error(s) total, none above its recorded number.
check-nul-bytes: OK (scanned 6178 text file(s) ... no raw ASCII control bytes).

check:type-check-debt ran through scripts/pm/os-verify-lock.sh on a fully built closure: VERDICT command-exit 0 · held the lock 255s (4m15s).

Ablations (both legs, on disk)

The claim under test is the ordering: the refusal precedes the refresh, so the refresh cannot rescue an unbuilt closure member.

  1. Input genuinely absent. With the closure fully built and turbo present, rm -rf packages/services/service-knowledge/dist (mutation confirmed on disk: 6 files / 1 .d.ts before, directory gone after) ⇒

    Error: --re-measure cannot run: 1 workspace dependenc(ies) of the ledgered packages have
    no built type entry point on disk -- @objectstack/service-knowledge.
    

    grep -c 'Tasks:|cached, ' over that run's output: 0refreshBuiltClosure() never got to run, though turbo was on PATH and would have rebuilt it in ~1s. Restored by rebuilding that one package; md5sum over the dist tree is byte-identical to the state the green run measured.

  2. Positive control, the other direction. With the closure fully built, node_modules/.bin/turbo moved aside ⇒ the run gets past the refusal and dies inside the refresh instead:

    Error: --re-measure needs the workspace's own turbo at .../node_modules/.bin/turbo; run `pnpm install` first.
    

    Restored immediately.

    Together: dist missing + turbo present ⇒ refusal; dist present + turbo missing ⇒ refresh. The refusal is first, and it is not decorative.

  3. Positive control for the set arithmetic.C \ (./packages/* alone) is non-empty (1 member), so the C \ A = 0 result above is a measurement, not a comparison that cannot see anything.

What I could not verify

  • A workflow file cannot be executed end-to-end locally. The step ordering and run: bodies were verified by parsing the edited lint.yml with the yaml package (typecheck-debt steps 9 and 10 remain Build the ledgered packages' dependenciesRe-measure the type-check DEBT / TEST_DEBT ledger, both run: strings unchanged); everything about how GitHub schedules the lane is CI's to confirm.
  • node scripts/pm/check-half-states.mjswithout--self-test (the live GitHub scan run by half-state-patrol.yml) was not run: it scans issues over the org API and is unaffected by a YAML comment. The --self-test half — which is what lint.yml invokes — is green above. Declared narrowing.
  • The 185ms / 9.5s / ~13s figures for the duplicate build are turbo cache-hit numbers from three different environments (this container warm, the card's measurement, and lint.yml's sibling comment). The comment quotes the card's 9.5s, which is the CI-side one.

Changeset

None: the diff is one YAML comment in .github/workflows/lint.yml, so the publish surface at diff time is empty. skip-changeset applied, which is this workflow's own prescription for a PR that "releases nothing".


Generated by Claude Code

… the gate's own refresh
`.github/workflows/lint.yml`'s "Build the ledgered packages' dependencies"
step and `refreshBuiltClosure()` in scripts/check-type-check-coverage.mjs run
the identical turbo command with identical filters, and #8331 was filed asking
whether the workflow step is now dead weight. Re-derived on current main: it is
not, and the reason is an ordering the gate documents but the workflow never
mentioned.
`--re-measure` checks `unbuiltClosure()` BEFORE it calls `refreshBuiltClosure()`
and refuses rather than building from nothing (deliberate: a cold 7-task closure
measured 3m2s on a gate people run before pushing). So the refresh owns the
present-but-stale case only; it cannot rescue a closure member no earlier step
built.
Measured what the preceding step actually covers: of the 55 workspace packages
in the ledgered dependency closure, `Build workspace packages` reaches 54 through
`./packages/*`, and the 55th -- @objectstack/service-knowledge, pulled in by the
ledgered knowledge-ragflow -- arrives only through `./examples/*^...`, because
examples/app-todo happens to depend on it. Deleting the step would tie the lane's
green to that one manifest.
Comment only; no step, command, filter or ordering changed.
Fixes: #8331
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@os-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 21, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 04:00
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 9616ffbAug 21, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8331-lint-ledgered-deps-duplicate branch August 21, 2026 04:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cdsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lint.yml's "Build the ledgered packages' dependencies" step duplicates the closure refresh once #8330 lands

2 participants

@os-zhuang@claude