Skip to content

finding(build): an overlapping dependency-closure build can leave an INCOMPLETE @object-ui/types dist (4 of 40 .d.ts) and still exit 0 — the cascade then reads as real TS errors in unrelated packages #6703

Description

@os-sales

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:

  1. It exits 0. Nothing in the chain says "this artifact is incomplete." The build is not the thing that goes red.
  2. 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.
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopm:queuepriority:p1tooling

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions