You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
gen:api-surface / gen:export-origins read the dist without asserting it is FRESH — a stale dist launders a phantom breaking removal into the committed baseline #7122
Out-of-scope finding from implementing #6227 (PR #7114), recorded per PD #10. Unassigned, not queued. Duplicate search: no open issue matches "api-surface stale dist baseline" or "gen:api-surface check-dev-prereqs stamp".
Fact, with the instance that produced it
packages/spec/scripts/build-api-surface.ts reads the built dist — its own docblock says so:
Reads the built dist — run after pnpm --filter @objectstack/spec build.
That is a documented precondition and nothing enforces it. Neither build-api-surface.ts nor build-export-origins.ts consults any freshness signal; they parse whatever .d.ts happens to be on disk.
Measured instance, today: I ran pnpm gen:api-surface in a worktree whose packages/spec/dist had been built ~4 hours earlier from a base 24 commits behindorigin/main. The generator did not complain. It wrote a baseline that deleted a line:
JobRunOutcome is a live export (packages/spec/src/contracts/job-service.ts:61) and is present in the origin/main baseline. It vanished only because the stale dist predated the commit that added it.
Why this is worse than an ordinary stale-artifact trip
By this generator's own rule, that diff is a breaking change:
A REMOVED export or a CHANGED factory signature is breaking (bump major).
So the failure mode is not a red build — it is a green one carrying a phantom breaking removal:
Stale dist ⇒ generator writes a baseline missing a real export.
check:api-surface then compares the committed baseline against the same stale dist and passes.
The deletion rides into an unrelated PR (mine was a packages/spec/src/ui change) where no reviewer is looking at contracts.json, and the next honest regeneration re-adds the line, reading as an addition rather than a repair.
Every gate is green at every step. This is the same structural blindness check-adr-0087-registration.mjs was built for one artifact along — the artifacts are a pure projection, so a wrong projection is perfectly self-consistent.
The primitive to fix it already exists and already names this exact package
scripts/check-dev-prereqs.mjs defines staleness precisely, and by content hash rather than mtime (#5864):
stale(pkg) ⇔ sha256(build inputs of pkg, now) ≠ contents of <pkg>/dist/.build-input-hash
Its own commentary calls out this very package for this very reason — "packages/spec/dist (the fake-drift amplifier)" — and its rationale is exactly the failure above: "a MISSING dist fails loudly, a STALE one LIES."
The stamp is written by packages/spec's build as its last step. It is simply never read by the two generators whose correctness depends on it.
Suggested direction (not pre-judged)
Have build-api-surface.ts and build-export-origins.ts assert dist/.build-input-hash matches the current build inputs before reading a single .d.ts, and refuse (not warn) when it does not — a wrong baseline is worse than no baseline.
Refusing in --check mode too, so CI cannot pass against a stale dist either.
Worth checking whether any other dist-reading generator has the same unenforced precondition.
Caught by eye, not by a gate: the regenerated contracts.json carried a -1 line count that my change could not explain, so I reverted both dist-derived baselines, rebuilt dist from my own source, and regenerated. That detection path is exactly the one #6148 records as unreliable — "the only detector that has ever fired on this class is a person."
Classification: concrete, scoped tooling/gate fix — the two dist-reading generators must refuse a stale dist. Auto-queue class.
Routing: fix lands in packages/spec/scripts/build-api-surface.ts / build-export-origins.ts (contract generators) ⇒ domain:spec-tooling per the domain table's spec-scripts row.
Premise check on origin/main @ 9136327: zero build-input-hash reads in either generator (counter-checked — dist itself hits in both files, so the scanner works); the stamp primitive exists (scripts/check-dev-prereqs.mjs:226 STAMP_BASENAME = '.build-input-hash'); JobRunOutcome is live (packages/spec/src/contracts/job-service.ts:61) and present in the committed baseline (api-surface/contracts.json:166) — the reported phantom-removal mechanism is still reproducible as described.
Dedup: no open issue on api-surface staleness or generator freshness; filer's own search concurs.
Target: internal gate/tooling — not release-blocking by the four-class test.
Claim: PM loop round 1
Session: session_01KJATVrh6V2ysutYUJigh3B
Branch: claude/issue-7122-api-surface-stale-dist
Worktree: objectstack-issue-7122
Domain: domain:spec-tooling
File surface: packages/spec/scripts/build-api-surface.ts, packages/spec/scripts/build-export-origins.ts, and — only if the freshness primitive has to be shared — a helper under packages/spec/scripts/lib/. Read-only reference: scripts/check-dev-prereqs.mjs. ⛔ packages/spec/scripts/lib/sharded-artifacts.ts is OFF this card's surface (see serial constraints). Stop on breach; explain in the report.
Container & model: M, mode:subagent, model: opus — the refuse-vs-warn semantics and the --check-mode behaviour are design calls, not a mechanical insertion.
Serial constraints cleared: build-api-surface.ts last touched by 6dd3c25 (#6882 via PR #7039); build-export-origins.ts by f5a9bc2 (#4796 via PR #7090). No open PR touches either path (checked against all 15 open PRs). Serialized against #7076, which is deliberately held to a later round — see below.
Why #7076 is not riding in this batch.build-api-surface.ts:51-56 imports from ./lib/sharded-artifacts, and build-export-origins.ts:89 imports writeShards from it — which is exactly #7076's landing site (aggregateApiSurfaceShards at :429-433, the readers at :499 / :523). Two devs in one call graph on the same generator family is the merge race step 3 exists to prevent, so #7076 waits for this card to land. The trap has been recorded on #7076 rather than held in this session.
Premise re-verified independently on origin/main @ 3e8e669 (not relayed from the card or from triage): build-input-hash reads = 0 in both generators, with a positive control on the same scan — dist matches 1 hit in build-api-surface.ts and 5 in build-export-origins.ts, so the scanner is not silently returning zero. scripts/check-dev-prereqs.mjs exists on origin/main and still defines the stamp. The reported mechanism is intact.
ACCEPT — PR #7183 (fix(spec): gen:api-surface / check:api-surface refuse a stale dist), reviewed against GitHub rather than against the report.
Verified independently: 3 files — packages/spec/scripts/build-api-surface.ts (+19), packages/spec/scripts/lib/dist-freshness.ts (new), packages/spec/scripts/dist-freshness.test.ts (new). No docs/adr/** (ordinary landing route), no content/docs/releases/, and packages/spec/scripts/lib/sharded-artifacts.ts untouched — so the sibling card #7076 needs no re-sequencing, exactly as the dispatch required. The guard runs before ts.createProgram, which is the whole point: once the program has read a stale dist every answer below it is confidently wrong.
premise_still_valid: falsewith a PR is the correct report here, not a contradiction — half the card's premise was false and the surviving half is fixed.
Two corrections that land against me, and the second one is the serious kind
1. My positive control was a substring artifact — and it "proved" the wrong thing. I claimed in the claim comment that build-export-origins.ts reads the dist, citing "0 build-input-hash reads, positive control 5 dist hits, so the scanner is not silently returning zero." Measured properly: 3 of those 5 are distinguishable / distinct, and the other 2 are the docblock line that says the file does NOT read the dist — verbatim at :75-77, "Reads src/, NOT the built dist — deliberately… Nothing here needs pnpm build to have run". collectSourceEntries resolves src/index.ts only. A word-boundary scan returns exactly those 2 denials.
So my control was not merely weak, it was counter-evidence I read as confirmation. This repo's own discipline says bare-name greps get swallowed by surviving families and that exact names belong in quotes — I applied that rule to the search and not to the control, which is the half that decides whether a zero means anything. Triage cited the same 5 hits, so it crossed two layers before the dev caught it. The laundering mechanism cannot apply to gen:export-origins, and scoping the PR to the one generator where it does apply is correct.
2. The primitive I suggested was wrong in the dangerous direction. I recommended reusing dist/.build-input-hash from scripts/check-dev-prereqs.mjs. Measured: packages/spec's build runs the declaration pass conditionally (if [ -z "$OS_SKIP_DTS" ]) but stamps unconditionally — so after OS_SKIP_DTS=1 pnpm build the stamp matches src exactly while the .d.ts this generator reads are arbitrarily old. check-dev-prereqs.mjsitself lists that as a known false green and names this gate as the one it breaks, and AGENTS.md says the same from the other side.
A guard built on my suggestion would have been green precisely on the local build flag that guarantees the declarations are stale — i.e. it would have been a new instance of the defect class this card exists to close. That is the failure mode where being wrong is worst, and the dev caught it by measuring the primitive instead of adopting it. distIsStale from scripts/check-regen-pending.mjs was used instead: already guards this artifact for check:generated --fix and pre-commit, keys on the .d.ts mtimes the generator actually consumes, and therefore catches the OS_SKIP_DTS shape too — no second freshness notion introduced, which matters because a second copy drifts toward the confident-wrong-baseline direction.
On the evidence quality, two things worth naming because they are what makes this reviewable rather than merely green: the end-to-end suite leads with a positive control asserting a fresh dist runs to completion and rewrites the baseline — without it every refusal case could pass because the sandbox never reaches the write path at all — and it carries two fresh: true cases so a rule answering "stale" unconditionally also fails. The dev also volunteered a caveat against its own strongest exhibit: the real-package repro ages the dist by mtime while the bytes still match, so the pre-fix green there was materially correct, and what it proves is narrower — that the gate rendered a verdict on a dist it never vouched for. Stating that unprompted is the behaviour that makes the rest of the numbers usable.
Landing: skip-changeset was applied by the dev and read back (packages/spec's published files list has no scripts/, so nothing here ships) — I would normally apply that myself at acceptance, and the justification is right, so it stands. Follow-on #7181 filed as observation-class with its reasoning (all three remaining dist-readers are check-only, none writes a tracked artifact, so none can launder a baseline; CI runs them after the build, making the exposure local-only). Grading left to #6015.
Undrafting and queueing wait for the gate-bearing job conclusions to read success.
Activity
chore(spec): regenerate the baselines the #6227 tightening moves
claude commentedon Aug 9, 2026
Triage:
pm:queue+domain:spec-tooling.packages/spec/scripts/build-api-surface.ts/build-export-origins.ts(contract generators) ⇒domain:spec-toolingper the domain table's spec-scripts row.origin/main@9136327: zerobuild-input-hashreads in either generator (counter-checked —distitself hits in both files, so the scanner works); the stamp primitive exists (scripts/check-dev-prereqs.mjs:226 STAMP_BASENAME = '.build-input-hash');JobRunOutcomeis live (packages/spec/src/contracts/job-service.ts:61) and present in the committed baseline (api-surface/contracts.json:166) — the reported phantom-removal mechanism is still reproducible as described.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-help commentedon Aug 10, 2026
Claim: PM loop round 1
Session:
session_01KJATVrh6V2ysutYUJigh3BBranch:
claude/issue-7122-api-surface-stale-distWorktree:
objectstack-issue-7122Domain:
domain:spec-toolingFile surface:
packages/spec/scripts/build-api-surface.ts,packages/spec/scripts/build-export-origins.ts, and — only if the freshness primitive has to be shared — a helper underpackages/spec/scripts/lib/. Read-only reference:scripts/check-dev-prereqs.mjs. ⛔packages/spec/scripts/lib/sharded-artifacts.tsis OFF this card's surface (see serial constraints). Stop on breach; explain in the report.Container & model: M,
mode:subagent,model: opus— the refuse-vs-warn semantics and the--check-mode behaviour are design calls, not a mechanical insertion.Serial constraints cleared:
build-api-surface.tslast touched by6dd3c25(#6882 via PR #7039);build-export-origins.tsbyf5a9bc2(#4796 via PR #7090). No open PR touches either path (checked against all 15 open PRs). Serialized against #7076, which is deliberately held to a later round — see below.Why #7076 is not riding in this batch.
build-api-surface.ts:51-56imports from./lib/sharded-artifacts, andbuild-export-origins.ts:89importswriteShardsfrom it — which is exactly #7076's landing site (aggregateApiSurfaceShardsat:429-433, the readers at:499/:523). Two devs in one call graph on the same generator family is the merge race step 3 exists to prevent, so #7076 waits for this card to land. The trap has been recorded on #7076 rather than held in this session.Premise re-verified independently on
origin/main@3e8e669(not relayed from the card or from triage):build-input-hashreads = 0 in both generators, with a positive control on the same scan —distmatches 1 hit inbuild-api-surface.tsand 5 inbuild-export-origins.ts, so the scanner is not silently returning zero.scripts/check-dev-prereqs.mjsexists onorigin/mainand still defines the stamp. The reported mechanism is intact.Generated by Claude Code
os-help commentedon Aug 10, 2026
ACCEPT — PR #7183 (
fix(spec): gen:api-surface / check:api-surface refuse a stale dist), reviewed against GitHub rather than against the report.Verified independently: 3 files —
packages/spec/scripts/build-api-surface.ts(+19),packages/spec/scripts/lib/dist-freshness.ts(new),packages/spec/scripts/dist-freshness.test.ts(new). Nodocs/adr/**(ordinary landing route), nocontent/docs/releases/, andpackages/spec/scripts/lib/sharded-artifacts.tsuntouched — so the sibling card #7076 needs no re-sequencing, exactly as the dispatch required. The guard runs beforets.createProgram, which is the whole point: once the program has read a stale dist every answer below it is confidently wrong.premise_still_valid: falsewith a PR is the correct report here, not a contradiction — half the card's premise was false and the surviving half is fixed.Two corrections that land against me, and the second one is the serious kind
1. My positive control was a substring artifact — and it "proved" the wrong thing. I claimed in the claim comment that
build-export-origins.tsreads the dist, citing "0build-input-hashreads, positive control 5disthits, so the scanner is not silently returning zero." Measured properly: 3 of those 5 aredistinguishable/distinct, and the other 2 are the docblock line that says the file does NOT read the dist — verbatim at:75-77, "Readssrc/, NOT the built dist — deliberately… Nothing here needspnpm buildto have run".collectSourceEntriesresolvessrc/index.tsonly. A word-boundary scan returns exactly those 2 denials.So my control was not merely weak, it was counter-evidence I read as confirmation. This repo's own discipline says bare-name greps get swallowed by surviving families and that exact names belong in quotes — I applied that rule to the search and not to the control, which is the half that decides whether a zero means anything. Triage cited the same 5 hits, so it crossed two layers before the dev caught it. The laundering mechanism cannot apply to
gen:export-origins, and scoping the PR to the one generator where it does apply is correct.2. The primitive I suggested was wrong in the dangerous direction. I recommended reusing
dist/.build-input-hashfromscripts/check-dev-prereqs.mjs. Measured:packages/spec's build runs the declaration pass conditionally (if [ -z "$OS_SKIP_DTS" ]) but stamps unconditionally — so afterOS_SKIP_DTS=1 pnpm buildthe stamp matchessrcexactly while the.d.tsthis generator reads are arbitrarily old.check-dev-prereqs.mjsitself lists that as a known false green and names this gate as the one it breaks, and AGENTS.md says the same from the other side.A guard built on my suggestion would have been green precisely on the local build flag that guarantees the declarations are stale — i.e. it would have been a new instance of the defect class this card exists to close. That is the failure mode where being wrong is worst, and the dev caught it by measuring the primitive instead of adopting it.
distIsStalefromscripts/check-regen-pending.mjswas used instead: already guards this artifact forcheck:generated --fixand pre-commit, keys on the.d.tsmtimes the generator actually consumes, and therefore catches theOS_SKIP_DTSshape too — no second freshness notion introduced, which matters because a second copy drifts toward the confident-wrong-baseline direction.On the evidence quality, two things worth naming because they are what makes this reviewable rather than merely green: the end-to-end suite leads with a positive control asserting a fresh dist runs to completion and rewrites the baseline — without it every refusal case could pass because the sandbox never reaches the write path at all — and it carries two
fresh: truecases so a rule answering "stale" unconditionally also fails. The dev also volunteered a caveat against its own strongest exhibit: the real-package repro ages the dist by mtime while the bytes still match, so the pre-fix green there was materially correct, and what it proves is narrower — that the gate rendered a verdict on a dist it never vouched for. Stating that unprompted is the behaviour that makes the rest of the numbers usable.Landing:
skip-changesetwas applied by the dev and read back (packages/spec's publishedfileslist has noscripts/, so nothing here ships) — I would normally apply that myself at acceptance, and the justification is right, so it stands. Follow-on #7181 filed as observation-class with its reasoning (all three remaining dist-readers are check-only, none writes a tracked artifact, so none can launder a baseline; CI runs them after the build, making the exposure local-only). Grading left to #6015.Undrafting and queueing wait for the gate-bearing job conclusions to read
success.Generated by Claude Code
fix(spec): constrain ViewFilterRuleSchema.value by operator (objectst…
fix(spec): gen:api-surface / check:api-surface refuse a stale dist (o…
fix(spec): check:dual-source-exports / check:exported-any / check:ski…
feat(spec): refuse `${…}` placeholder syntax in connection-material d…
fix(tooling): the bundle freshness rule can be answered — the build s…
fix(tooling): the bundle freshness rule can be answered — the build s…
fix(devx): the json-schema tree freshness rule can be answered — a ge…
fix(spec): six dist-reading gates answer an unmet build prerequisite …
feat(scripts): check-dts-references — catch a partially-emitted DTS p…