Skip to content

fix(tooling): the bundle freshness rule can be answered — the build stamp acquits a tree whose bytes never moved - #16240

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-16175-regen-sibling-stale-rules
Sep 6, 2026
Merged

fix(tooling): the bundle freshness rule can be answered — the build stamp acquits a tree whose bytes never moved#16240
baozhoutao merged 2 commits into
mainfrom
claude/issue-16175-regen-sibling-stale-rules

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Part of #16175. The bundlesAreStale half only — the schemaTreeIsStale half is not addressed here and #16175 remains open for it; the open decision is stated at the bottom.

What was false, measured on this branch before the change

A built worktree, git status empty, one touch packages/spec/src/data/query.zod.ts with no content change:

pnpm --filter @objectstack/spec run check:browser-reachable-entries   exit 1
  packages/spec/dist's .mjs/.js bundles are OLDER than packages/spec/src
pnpm --filter @objectstack/spec run check:docs                        exit 1
  packages/spec/json-schema is older than packages/spec/src

Both on a tree whose bytes never moved. That is the blind spot #14985 measured for the declarations, and the remedy it prescribed here was a multi-minute rebuild under the shared verify lock of bundles that were already exactly current.

What lands

bundlesAreStale keeps its power to convict and gains one way to be answered: dist/.build-input-hash, the stamp every build of the package already writes. unstamped — absent, unreadable, not 64 hex characters, or a package whose build does not stamp — leaves the mtime verdict standing, so nothing that passes today can start failing.

  • scripts/build-input-hash.mjsinspectDeclarationStamp and the new inspectBuildStamp now share one inspectStamp(root, pkgDir, basename), so the two stamps cannot drift into two comparisons.
  • scripts/check-regen-pending.mjsbuildStamp() wrapper beside declarationStamp(), consulted by bundlesAreStale only after the mtime rule has already accused.
  • scripts/check-regen-pending.d.mts — the declaration mirror, in step.
  • packages/spec/scripts/check-browser-reachable-entries.ts — four cases in its existing bundle-freshness self-test.

Why this is not the direction #7122 had rejected

packages/spec/scripts/lib/dist-freshness.ts carries a section titled "Why the mtime rule CONVICTS, and what may acquit", and dist-freshness.test.ts pins the refusal of "the OS_SKIP_DTS=1 shape on an ALREADY-BUILT tree — the case dist/.build-input-hash cannot see". That ruling is about the declaration axis and is untouched: --stamp writes dist/.build-input-hash under OS_SKIP_DTS=1, which emits JS and leaves whatever .d.ts was there before, so it says fresh over stale declarations. Its pin still passes.

On the bundle axis the same fact points the other way, and three measured things make the evidence sound:

  1. OS_SKIP_DTS=1 emits exactly the artifact being vouched for. inspectBundleFreshness's own refusal text already told the reader so: "OS_SKIP_DTS=1 is fine for THIS gate — it still emits every bundle this reads."
  2. What makes it sound is the build script's order, not the flag. packages/spec's build is one && chain: gen:schema && gen:openapi && tsup && [conditional BUILD_DTS pass] && check-dts-emitted && check-dev-prereqs --stamp. The unconditional tsup (the JS pass) always precedes --stamp, so nothing writes this stamp without having emitted bundles first. Only the declaration pass is conditional, and skipping it cannot refresh a stamp written after both. There is no OS_SKIP_JS counterpart.
  3. The digest's input set is a strict superset of the source set this rule measures. buildInputHash hashes every file under src/ (.test.ts included), PACKAGE_BUILD_CONFIG (package.json, tsconfig.json, tsconfig.build.json, tsup.config.ts, tsdown.config.ts) and turbo's globalDependencies; the rule measures src/**/*.ts minus .test.ts, plus tsup.config.ts. So tsup.config.ts — the card's one caveat to confirm rather than assume — is in the digest, and a superset can only ever withhold an acquittal, never grant one it should not.

Evidence

After, same tree, same touch: check:browser-reachable-entries exit 0. check:docs still exit 1 — the untouched sibling axis, an in-tree control that this is not a blanket pass.

Negative control (real content change must still convict). Appending one line to packages/spec/src/data/query.zod.ts (blob 341bb947 becomes 1d578c93, one on-disk marker hit): check:browser-reachable-entries exit 1, "NOT MEASURED". Restored, blob back to 341bb947, git diff HEAD empty.

Ablation (the new pins can fail). Reverting bundlesAreStale to the plain mtime comparison — deleted text 1 hit becomes 0, injected text 0 becomes 1, blob c413dc0f becomes bcf0d494 — reddens exactly one case:

✓ the sandbox build-input digest can be computed at all (the fixture is not vacuous)
✗ ACQUITS an mtime-stale tree whose build stamp matches the sources (#16175)
✓ and CONVICTS the same tree the moment a source byte actually changes
✓ ignores a build stamp that is not a digest at all

exit 1. Restored byte-identical (blob back to c413dc0f, git diff HEAD empty). Direction observed: turns red, as expected. No build/dist leg: every module in this loop is resolved by relative source path through tsx, never through a package's exports into dist/.

Not addressed here: the json-schema axis

Stopping that half rather than guessing, per the direction the card itself sets. Two forks are resolvable and one is not:

  • Where the stamp would live — resolvable. json-schema/** is a turbo output of build, so a stamp inside it is cached, restored and cleaned with the tree it describes; json-schema/ is gitignored and carries no merge=os-regen attribute, and build-schemas.ts clears its own outputs by deny-list, so a stamp it writes is its own artifact and needs no FOREIGN_JSON_SCHEMA_ARTIFACTS registration. One write point covers all three modes: the tree is regenerated unconditionally at the top of the script, before the --check / --update-base branching.
  • The provenance — not resolvable inside this card's ruling. buildInputHash is the digest of the inputs the tsup build consumes; it does not hash packages/spec/scripts/build-schemas.ts or its lib/, which are real inputs to the json-schema tree. For the declaration stamp the analogous generator input (tsup.config.ts) is hashed, so reusing this digest would make the schema stamp measurably weaker than the one it mirrors: edit the generator, run nothing, then let a checkout bump a src/ mtime, and the rule would acquit a tree the old generator produced. The card's own bar is "the stamp records the digest of the inputs gen:schema actually consumed".
  • A second consequencejson-schema is in packages/spec's files, so any stamp there ships in the published tarball. fix(spec): the dist freshness rule can be answered — a declaration stamp acquits a tree whose sources were re-checked-out unchanged #16176 made that call deliberately for dist/ and said so in its changeset; the same call for json-schema/ is a maintainer's, and it flips this PR's skip-changeset answer.

Options and a recommendation are in the dev report on #16175.

Verification

All 70 derived gate commands ran in this worktree (node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack at acef174a). Green except four NOT MEASURED, all of them PREREQUISITE-NOT-MET on an unbuilt closure in this worktree and none reachable from this diff, which touches no package source: check:dts-closure and check:generated's check:api-surface leg (this worktree's packages/spec was built with OS_SKIP_DTS=1, so it holds no .d.ts), check:dual-build-cjs-loads (exit 3, "some package has no dist/"), check:type-check-debt (exit 3, "33 workspace dependencies have no built type entry point"). check:plugin-teardown-shape --self-test refuses on a shallow clone that cannot reach its pinned positive-control commit. CI builds fresh and runs all of them.

pnpm check:merge-driver (this file's own self-test) green, and pnpm check:pm-dispatch-gates green in 675 s (1511 self-test cases). Run in the foreground without the shared verify lock: the standing rule is that check:* gates do not take it, and wait plus run would have exceeded the container's foreground cap. pnpm check:declaration-mirrors, check:scripts-symbol-anchors, check:self-test-wired, check:nul-bytes, check:cross-package-test-inputs, check:published-files, check:test-source-alias, check:dev-prereqs --self-test all green.


Generated by Claude Code

…tamp acquits a tree whose sources were re-checked-out unchanged

`bundlesAreStale` compared `dist/**/*.mjs`/`*.js` mtimes against `src/**/*.ts`
and `tsup.config.ts` mtimes and had no way to be answered. A `git merge`,
`git checkout` or `git worktree add` re-checks-out a source file with identical
bytes and bumps its mtime; the build that follows correctly does not run
(turbo's cache hashes content, so it is a cache hit that rewrites nothing) and
every `dist/` mtime stays where the previous build left it. The rule then
refused `check:browser-reachable-entries` over bundles that were exactly
current, and the only remedy was a multi-minute rebuild under the shared verify
lock.

The mtime rule keeps its power to convict and gains one way to be answered:
`dist/.build-input-hash`, already written by every build of the package. That is
the file #7122 proposed for the DECLARATION rule, where it was measured wrong
and stays rejected — `OS_SKIP_DTS=1` writes it while skipping the declarations.
On the bundle axis that same flag emits exactly the artifact being vouched for,
and what makes the evidence sound is the build script's order rather than the
flag: the unconditional `tsup` pass runs before `--stamp` in one `&&` chain, so
nothing writes this stamp without having emitted bundles first.

The evidence may only ever ACQUIT: a missing, unreadable or mismatched stamp
leaves the mtime verdict standing, so nothing that passed before can start
failing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
…on-vacuous

Three cases in `check:browser-reachable-entries --self-test`, beside the mtime
cases that already drive `inspectBundleFreshness`: the acquittal itself, the
conviction that returns the moment a source byte changes (so the rule is keyed
on the digest and not on the stamp's presence), and a stamp that is not a digest
at all leaving the refusal standing. A fourth asserts the sandbox digest can be
computed, so a fixture that silently stopped producing one cannot pass the rest
by accident.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 6, 2026
@github-actions github-actions Bot added the size/m label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1ebde49536b431dc891b4bbe1a144b638c6205aepackageMentionDocs.

@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Out-of-scope finding filed while working this branch, not addressed here: #16241 — the bundle-freshness refusal names one cause for three distinguishable states, unlike the declaration-axis sibling above it in the same file. It is a message-quality gap, not a wrong verdict, and it sits outside this card's declared file surface (packages/spec/scripts/lib/dist-freshness.ts). #16241 remains open.


Generated by Claude Code

@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Not flipped — Test Core (1/6) on this head was CANCELLED at the job timeout and the Test Core rollup read SUCCESS over it. That green is not clearance. PM seat domain:devx @ objectstack, session session_01Vbw3RPgdtqesx4azk9SbW8, 2026-09-06T07:4xZ.

Readings on head acef174a7, run 34018298477, job 101446068788: "Run this shard's tests" cancelled 07:08:57→07:38:03 (29m06s of a 30-minute timeout-minutes job), "Attest this shard ran and passed" skipped, then the rollup job 101449815369 Test Core success at 07:38:08–07:38:21 — i.e. the #16157 short-circuit (a cancelled shard counts toward "expected attestations: 0") fired on a real PR, and the cause is #16173's stale timings (this diff touches root scripts/, which makes the whole workspace --affected, so shard 1/6 carried @objectstack/cli at its true ~20-minute duration).

Why it is not this PR's: the diff changes no package source (scripts/** and one spec self-test file); shard 1/6's wall-time imbalance is measured on main itself (merge_group run 34013842594: 1168 s vs siblings 462–793 s) and is the open card #16173, whose fix is in flight on PR #16220. Standing down once, with the one allowed re-run: the cancelled jobs of run 34018298477 are re-run now (the earlier successful attempt of the same shard took 23m12s, so a re-run is within the wall). If shard 1/6 is killed again, this PR parks in draft per landing-operations C — expected red: Test Core (1/6) cancelled at the job timeout; release condition: #16173's rebalance lands, then merge origin/main here — ⛔ never flipped on a rollup green that sits over a cancelled shard. #16157 remains open; #16173 remains open.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants