fix(spec): check:dual-source-exports / check:exported-any / check:skill-examples refuse a stale dist (#7181) - #7421
Merged
Conversation
…ll-examples refuse a stale dist (#7181) All three read the built `dist/*.d.ts` and documented that as a precondition without enforcing it, so each could report a verdict about a build nobody made. They now adopt #7122's `inspectDistFreshness` before the first declaration is read. The primitive's refusal named `check:api-surface` by hand, which would have made three gates prescribe a fourth gate's command -- following it re-runs something that was never refused, so the refusal reads as cleared. The caller now passes its own re-run command; a third `mode` value was measured and rejected, because `mode` selects the damage sentence (writing a wrong baseline vs agreeing with one) and both existing values are semantically right for these callers. Two findings that change the card's framing, both pinned by tests: - `check-dual-source-exports.ts --update` REWRITES a tracked baseline, so the "all three are check-only, none can launder a bad baseline" premise does not hold for it. That path gets `mode: 'generate'`. - the anti-vacuity floors these gates carry are self-test and missing-dist floors. None of them fires on a present-but-stale dist, which is the state being refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 0 changed package(s). ✅ |
os-help
marked this pull request as ready for review
August 10, 2026 11:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7181
check:dual-source-exports,check:exported-anyandcheck:skill-exampleseach resolve their public entry points to the builtdist/**declarations, each documented "run afterpnpm --filter @objectstack/spec build" in prose, and none of them asserted it. All three now adopt #7122'sinspectDistFreshnessbefore the first declaration is read.The
modequestion, measuredThe card asked whether
dist-freshness.tsneeds a thirdmodevalue, since none of these three isgen-shaped and thecheckwording namedcheck:api-surfaceby hand. Read against the code, it does not —modeis consumed in exactly two places and only one is semantic:${gen|check}:api-surfaceby hand. That is not a semantic difference; it is the caller's own name, and hardcoding it is what made three adopted gates print a fourth gate's.A third value would therefore have to mean "check-shaped, but print a different command" — text wearing a semantic label, still wrong for the fifth caller. So the re-run command is a required third argument instead: the compiler makes every new caller state how to re-run itself, and there is no default to inherit the wrong gate's identity from. It is a full command string rather than an npm script name because one call site (
--updatebelow) is not reachable through one.Why this matters beyond tidiness: a developer told to re-run a gate they never ran runs something that was never refused, sees it pass, and reads the refusal as cleared.
Two premises of the card that did not survive verification
check-dual-source-exports.ts --updatewrites a tracked artifact. The card graded all three observation-class because they "are check-only — none writes a tracked artifact, so none can launder a wrong baseline into a commit the waygen:api-surfacecould". That holds for two of them.--updaterewritesdual-source-exports.baseline.json, and the docblock and the no-baseline error both point at it. On a stale dist it writes a partition computed from declarations that predate the edit, and the shrink-only ratchet makes it wrong in both directions at once: a name that only became dual-source after the last build is written out as clean, and the plain run then compares that baseline against the same stale dist and agrees. That is #7122's laundering shape one artifact over, so that path getsmode: 'generate'and is pinned by a test asserting the baseline is left byte-identical.The "anti-vacuity floors of their own" do not cover this state. They are real, but each is a floor against a broken detector or a missing dist: the self-test count assertions run against temp fixtures, and
scan'sIs the package built?throw fires only when a module symbol will not resolve at all. A dist that resolves fine and merely predates the edit passes every one of them and prints a green. Same forcheck:skill-examples, whose "is the spec built" guard answers absence only.check:skill-examplesreaches the dist by a different routeThe card's A1 assumed all three share
collectEntries+ts.createProgram. Two do.check-skill-examples.tsinstead builds a tsconfigpathstable from the exports map (specPaths()) and a spawnedtscfollows it. A copy-pasted guard placement would therefore have been wrong here, so the guard sits at that boundary — immediately beforespecPaths()— rather than at the top ofmain(). Everything above it (extraction, the orphan-marker guard, the zero-block guard, the bare-anyguard) is dist-independent and still speaks on a stale build; nothing below it is computed. A test pins that ordering from both sides.For the same reason the two sibling guards sit after
--self-test: that path compiles its own fixture in a temp dir and never readsdist/, so refusing it would refuse a run the stale dist cannot affect — and a gate that cannot be exercised without a build is a gate people stop exercising.Reverse verification
Both directions, on the real tree, per gate. Fires (real
packages/spec, dist aged bytouch src/index.ts):— identically for
check:exported-anyandcheck:skill-examples, each naming itself, exit 1.--updateprints the writing damage instead and leaves the baseline untouched.Stays quiet, same tree after
pnpm --filter @objectstack/spec build:scripts/dist-freshness-adoption.test.tsruns the same two directions per gate end to end in a repo-shaped sandbox, with a fresh-dist positive control for each so the refusal cases cannot pass merely because a script could not run there.Gates
@objectstack/spectest367 files / 9568 tests green (23 in the two freshness files);typecheckgreen;check:api-surface,check:generated,check:dual-source-exports,check:exported-any,check:skill-examplesgreen; ESLint clean on the changed files;check-nul-bytesOK.No changeset
Nothing in the published package changes —
packages/spec/scripts/**is developer-facing gate behaviour only. #7122's own PR (#7183) landed the same way, changeset-free. Needsskip-changeset; left for the PM per the dispatch ruling.Generated by Claude Code