Filed unlabelled with suggested routing — domain:* and grading belong to triage. Suggested routing: domain:devx (build tooling). Observation of a real hazard, not a scoped fix.
Provenance: measured by the dev implementing #6527 (PR #6627) while verifying the option-B subpath move. Reported by that dev as a build-tooling hazard rather than a code defect, and it was correct to separate them — the card's own change was fine.
What was measured
Running a package build whose task graph pulled the dependency closure (a ^-prefixed turbo task) concurrently with another build in the same checkout produced a @object-ui/types dist/ containing 4 of 40 .d.ts files — and the build exited 0.
The incomplete artifact then cascaded: packages/permissions and packages/mobile reported TypeScript errors that look exactly like ordinary type errors in those packages. They are not. They are the downstream reading of a truncated dist/ that nothing reported as truncated.
Isolating the builds sequentially — types alone, then plugin-designer alone, then app-shell alone — reproduced a complete, correct dist/, and all three packages built and type-checked clean.
Why this is worth a card
The failure has all three properties that make a hazard expensive:
- It exits 0. Nothing in the chain says "this artifact is incomplete." The build is not the thing that goes red.
- It blames the wrong package. The errors surface in
permissions and mobile — packages the developer was not touching and has no reason to suspect. The natural next move is to "fix" a type error that does not exist.
- It is concurrency-dependent, so it does not reproduce on a retry that happens to run alone. That is the signature of a bug people learn to work around individually and never file.
This is the same family as the existing rule that a gate reading a build artefact is only as good as the artefact — but one level earlier: here the artefact producer reports success on a partial write.
Adjacent, and probably related — the check:self-import gate already knows this shape
scripts/check-package-self-import.mjs exists because a package self-import resolves through the package's own exports map to dist/, while turbo's type-check depends on ^build (the dependencies' builds) and not the package's own. Its message states the consequence directly:
A package self-import is green on every machine that has ever run a build and red on a cold CI cache only — which is why this is a gate and not a review note.
That gate covers the self-import route to depending on an unordered dist/. This card is about a different route to a bad dist/: one that is ordered, runs, and reports success while writing 10% of its output. Whoever picks this up should check whether the two share a root cause in how the dist/ write is sequenced (partial-write visibility, no atomic swap, no output manifest check) before treating them as independent.
Suggested first questions (not prescriptive — untried)
- Does the build write
dist/ in place rather than staging and atomically swapping? A concurrent reader would then legitimately observe a partial tree.
- Is there any post-build assertion that the emitted
.d.ts set matches the expected entry set? A 4-of-40 outcome suggests nothing counts them.
- Is the 40 figure stable enough to assert against, or does it need to be derived from the package's own entry points?
⚠️ Whoever takes this: the reproduction is concurrency-dependent. A single clean sequential build proving green is not evidence the hazard is absent — that is the exact reading that makes this class invisible. Establish a reproduction that fails before trusting one that passes.
Refs: #6527 / PR #6627 (where it was measured) · objectui#4801 and PR #4789 (the self-import gate's own history).
Filed unlabelled with suggested routing —
domain:*and grading belong to triage. Suggested routing:domain:devx(build tooling). Observation of a real hazard, not a scoped fix.Provenance: measured by the dev implementing #6527 (PR #6627) while verifying the option-B subpath move. Reported by that dev as a build-tooling hazard rather than a code defect, and it was correct to separate them — the card's own change was fine.
What was measured
Running a package build whose task graph pulled the dependency closure (a
^-prefixed turbo task) concurrently with another build in the same checkout produced a@object-ui/typesdist/containing 4 of 40.d.tsfiles — and the build exited 0.The incomplete artifact then cascaded:
packages/permissionsandpackages/mobilereported TypeScript errors that look exactly like ordinary type errors in those packages. They are not. They are the downstream reading of a truncateddist/that nothing reported as truncated.Isolating the builds sequentially —
typesalone, thenplugin-designeralone, thenapp-shellalone — reproduced a complete, correctdist/, and all three packages built and type-checked clean.Why this is worth a card
The failure has all three properties that make a hazard expensive:
permissionsandmobile— packages the developer was not touching and has no reason to suspect. The natural next move is to "fix" a type error that does not exist.This is the same family as the existing rule that a gate reading a build artefact is only as good as the artefact — but one level earlier: here the artefact producer reports success on a partial write.
Adjacent, and probably related — the
check:self-importgate already knows this shapescripts/check-package-self-import.mjsexists because a package self-import resolves through the package's ownexportsmap todist/, while turbo'stype-checkdepends on^build(the dependencies' builds) and not the package's own. Its message states the consequence directly:That gate covers the self-import route to depending on an unordered
dist/. This card is about a different route to a baddist/: one that is ordered, runs, and reports success while writing 10% of its output. Whoever picks this up should check whether the two share a root cause in how thedist/write is sequenced (partial-write visibility, no atomic swap, no output manifest check) before treating them as independent.Suggested first questions (not prescriptive — untried)
dist/in place rather than staging and atomically swapping? A concurrent reader would then legitimately observe a partial tree..d.tsset matches the expected entry set? A 4-of-40 outcome suggests nothing counts them.Refs: #6527 / PR #6627 (where it was measured) · objectui#4801 and PR #4789 (the self-import gate's own history).