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

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.mjs — inspectDeclarationStamp and the new inspectBuildStamp now share one inspectStamp(root, pkgDir, basename), so the two stamps cannot drift into two comparisons.
  • scripts/check-regen-pending.mjs — buildStamp() 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 consequence — json-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 1ebde49536b431dc891b4bbe1a144b638c6205ae → packageMentionDocs.

@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

@baozhoutao
baozhoutao marked this pull request as ready for review September 6, 2026 08:13
@baozhoutao
baozhoutao enabled auto-merge September 6, 2026 08:13
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit 6971170 Sep 6, 2026
56 of 57 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-16175-regen-sibling-stale-rules branch September 6, 2026 08:45
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…neration stamp acquits a tree whose sources were re-checked-out unchanged (objectstack-ai#16175) (objectstack-ai#17789)

Closes objectstack-ai#16175

Clause-②: no

`scripts/check-regen-pending.mjs` exports three freshness predicates
over the same
"the newest artifact mtime is older than the newest source mtime"
comparison, and all three share one blind spot: `git merge`,
`git checkout` and `git worktree add` re-check-out a source file with
**identical bytes** and bump its
mtime, the build that follows correctly does not run (turbo's cache
hashes content), and the rule then
refuses an artifact that is exactly current.

## What each predicate could actually read — measured first, on a built
tree at the merge base

The card says the two unfixed siblings "need different evidence, and one
of them has no evidence to read
at all". Measured before writing anything, by calling each predicate and
each stamp reader against the
real `packages/spec` after a clean build and then after `touch
packages/spec/src/data/query.zod.ts` with
`git status` empty:

| predicate | evidence file on disk | reader verdict | after the touch |
|:--|:--|:--|:--|
| `distIsStale` | `dist/.build-input-hash-dts` PRESENT | `match` |
`false` — acquitted |
| `bundlesAreStale` | `dist/.build-input-hash` PRESENT | `match` |
`false` — acquitted |
| `schemaTreeIsStale` | none | no reader exists | **`true` — the false
refusal** |

⚠️ **One premise of the card body is stale, and triage had already
recorded it.** The card's table marks
`bundlesAreStale` "not fixed". It was fixed by PR objectstack-ai#16240 (`6971170791`),
and triage's `pm:retriage`
answer says so in as many words: 「已落地的一半(`bundlesAreStale`,PR
objectstack-ai#16240,`697117079`)⛔ 不在范围内;
本卡剩下的**只有 `schemaTreeIsStale` 这一支**。」 Measured on `origin/main` at the
merge base — the
predicate ends `return buildStamp(specDir).state !== 'match';` — so this
PR carries only the schema half,
which is exactly the file surface triage declared. `bundlesAreStale` is
still exercised here as a control.

## The evidence given to the one that could read nothing, and where it
comes from

⛔ Neither `dist/` stamp could stand in, and reaching for one would be
objectstack-ai#7122's rejected direction
relocated rather than relaxed: both are written at the **END** of the
build, whereas `gen:schema` is its
**FIRST** step and is also run standalone (this rule's own refusal
message says so) and again by
`check:authorable-surface`. A `dist/` stamp is evidence about `dist/`,
and in the standalone case there
would be none at all.

So the evidence is new. `packages/spec/scripts/build-schemas.ts` writes
`json-schema/.build-input-hash-schema` as **the last thing it does**,
over the digest of the inputs that
generation consumed — computed by the same `buildInputHash` the readers
use, so the two cannot drift.

Why that write point is sound, and why one is enough:

- the script rebuilds the **whole tree unconditionally**, before the
`--check` / `--update-base` fork, so
one write point covers `gen:schema`, `check:authorable-surface` and
`gen:authorable-surface-base`;
- it sits after every ratchet that can `exit 1`, and `clearOwnedOutputs`
removes the previous stamp at
the top of every run — so a stamp exists only for a run that emitted the
tree beside it **and** reached
  the end. A generation that died halfway leaves none;
- `json-schema/` is a turbo build output, gitignored, cleared by this
generator and no other, so the
stamp lives and dies with the tree it speaks for. That is the same
argument `--stamp` makes for writing
  inside `dist/`.

⭐ **It may only ACQUIT.** Absent, unreadable, or not 64 hex characters
is `unstamped` — no evidence — and
leaves the mtime refusal exactly where it stood (objectstack-ai#4690). Nothing that
passes today can start failing, and
mtimes remain the only instrument that convicts.

**The digest's input set widens in the same commit**, which is why this
is one PR and not two: the
generators live under `packages/spec/scripts/`, which was in none of the
previous input sets (`src/**`,
`PACKAGE_BUILD_CONFIG`, turbo's `globalDependencies`). An edited
generator kept a digest that had not
moved, so a stamp written by the OLD generator would acquit a tree the
new one emits differently. A
superset can only ever WITHHOLD an acquittal, never grant one, so the
two `dist/` stamps become strictly
more honest too.

## Acceptance, measured

**1 — the false refusal is gone.** After a clean build, `touch` on three
sources with `git status` empty:
all three predicates `false`, all three stamps `match`. End to end, the
gate the card names:

```
BEFORE   pnpm --filter @objectstack/spec check:docs   exit 1
           packages/spec/json-schema is older than packages/spec/src.
AFTER    pnpm --filter @objectstack/spec check:docs   exit 0
           222 generated files in sync with packages/spec        (no rebuild)
```

⭐ **2 — positive control.** A *genuine* byte-level source change
(appended line; blob
`341bb947` → `42c300d1`, `git status` shows ` M`) still convicts all
three:
`distIsStale`/`schemaTreeIsStale`/`bundlesAreStale` all `true`, all
three stamps `mismatch`. Without this
row, a predicate that stopped refusing and a predicate that went blind
read identically. Restored by
`git checkout HEAD --`, verified by blob hash equality.

**3 — ablation.** The acquittal line removed from `schemaTreeIsStale`
(on-disk landing proved by an
occurrence count 1 → 0 and a changed blob hash; restored under a trap
and verified back to the HEAD blob):
acceptance 1 reverts to `schemaTreeIsStale = true` while its two
siblings stay `false` — the differential
isolates the change to this predicate alone. No build/`dist` leg
applies: `check-regen-pending.mjs` is a
root script the probe imports by path, resolved through no package
`exports`.

## Declared file surface — and where I went past it

Triage declared three implementation sites. All three, and nothing else
on that axis:

- `scripts/build-input-hash.mjs` — widened input set,
`SCHEMA_STAMP_BASENAME`, `inspectSchemaStamp`, `writeSchemaStamp`
- `scripts/check-regen-pending.mjs` — `schemaTreeIsStale` wired;
`schemaStamp` / `recordSchemaStamp` wrappers ·
`scripts/check-regen-pending.d.mts` follows (`check:declaration-mirrors`
green)
- `packages/spec/scripts/build-schemas.ts` — **one** write point at the
end of generation

⚠️ Touching `packages/spec` is the cross-domain exception triage
authorized (precedent PR objectstack-ai#16240); the
`domain:spec` seat carries the notification duty from this declaration.

⚠️ **Beyond it, and why** — reported rather than assumed. Five further
files, none of them a second
implementation site:

1. `packages/spec/scripts/schema-tree-freshness.test.ts` and
`packages/spec/scripts/build-schemas-check-mode.test.ts` — **tests**,
required by triage in its own
words (「照已落地那一半的做法用消融证明单向性」). The second is the only place that can
notice the write
point going missing: without it the rule degrades to `unstamped`, which
is the conservative verdict, so
every gate stays green and the only symptom is the false refusal quietly
returning.
2. `packages/spec/scripts/def-key-collisions.test.ts` — **mechanically
forced.** Both suites spawn the real
generator out of a copied `scripts/` in a flat tmpdir; its new repo-root
import walks off the top of the
filesystem from there, and the spawn dies with `MODULE_NOT_FOUND` before
any assertion runs. Both
fixtures now sit at the repo's own depth with the root scripts symlinked
beside them, so they keep
   running the production import graph instead of a reduced one.
3. `packages/spec/vitest.repo-tests.json` — **a gate named it.** Both
suites now genuinely read outside
their package; `check:cross-package-test-inputs` refused until they were
declared.
4. `scripts/check-dev-prereqs.mjs` and
`packages/spec/scripts/lib/json-schema-out-dir.ts` — **comment-only**,
   and both are reverse-read hits (below).

## Reverse-read — which existing sentence does this make false

Scanned before pushing; both hits fixed in place, zeros reported as
zeros.

**Made false, fixed:**

- `scripts/check-dev-prereqs.mjs` — "THE INPUT SET, and why each part is
in it" enumerates the digest's
inputs and is that gate's stated authority on them. The widening makes
the list incomplete. One bullet added.
- `packages/spec/scripts/lib/json-schema-out-dir.ts` — "gen:schema emits
`(category)/(Name).json` plus the
bundled `objectstack.json`" reads as exhaustive and no longer is.
Corrected, with the one thing a future
reader could get actively wrong stated: ⛔ the stamp may never get a
`FOREIGN_JSON_SCHEMA_ARTIFACTS`
entry — exempting it from the sweep would leave a stamp acquitting a
tree nobody emitted.
- `scripts/check-regen-pending.d.mts` header ("five functions"),
`scripts/build-input-hash.mjs` header
("the two stamps") and `inspectStamp`'s docblock ("ONE of the two
stamps") — all updated with the edit.

**Zeros — read and unaffected:**

- `packages/spec/scripts/lib/dist-freshness.ts` — cites
`schemaTreeIsStale` as precedent for guarding in
the generator rather than its callers. Still true; that is exactly what
this write point does.
- `packages/spec/scripts/lib/json-schema-out-dir.ts` module title — "the
one output directory **two
generators** write into". Still two: the stamp is `gen:schema`'s own
output, not a third writer.
- `check-generated.ts`'s `readsSchemaTree` ordering rules,
`schema-closure.ts`'s empty-category rule,
`regen-artifacts.mjs`'s `packages/spec/json-schema/**` row — none says
anything this changes. (That last
row's `.gitignore:61` line number is drift that predates this branch —
the pattern is at `:63`; noted,
  not filed, no PR or reader is routed through it.)

## Tests re-judged in place, never deleted

All six original cases in `schema-tree-freshness.test.ts` still assert
exactly what they were written to
assert, because **none of those sandboxes carries a stamp** — and that
is now the property being pinned:
no stamp is `unstamped`, `unstamped` is no evidence, and no evidence
leaves the mtime verdict standing. The
re-judgement is recorded in the file header and on the false-green case
itself. Six new cases supply the
evidence and pin what it may and may not do: acquittal on `match`;
refusal held on `mismatch`; refusal held
on a non-digest stamp; a missing tree stays stale however good the
stamp; a mismatched stamp may not
overturn a verdict of fresh; and the stamp is invisible to both sides of
the mtime rule, so it cannot vouch
for itself.

## Changeset — measured, not defaulted

`skip-changeset` would have been wrong. `json-schema` is in
`@objectstack/spec`'s published `files[]`, so
the stamp travels in the tarball — `npm pack --dry-run --json`, on the
built tree:

```
STAMP FILES IN TARBALL: dist/.build-input-hash, dist/.build-input-hash-dts, json-schema/.build-input-hash-schema
positive control (json-schema/ ships): json-schema/.build-input-hash-schema, json-schema/ai/Agent.json, …
```

One 65-byte file joins the published package; no export, schema key,
registry entry or runtime behaviour
moves. `@objectstack/spec: patch`.

## Verification

`dispatch-gates --ran` reconciliation on the final commit (`git
rev-parse --short HEAD` = `7d1292f393`):
**87 derived, 84 run, 3 NOT MEASURED, 0 unrun.**

- `pnpm --filter @objectstack/spec build` — green (all three stamps
written, identical digest, which also
  cross-checks the new writer against `--stamp`'s)
- `pnpm --filter @objectstack/spec typecheck` — green (`tsc`,
`check:scripts-typecheck`, `check:test-typecheck`)
- `pnpm --filter @objectstack/spec test` — 470 passed, 1 skipped (471
files) · `test:repo` — 29 files, 503 passed
- spec gates green: `check:authorable-surface`, `check:docs`,
`check:generated` (15/15 artifacts current),
  `check:browser-reachable-entries`
- root gates green: `check:declaration-mirrors` (+self-test),
`check:merge-driver`, `check:nul-bytes`,
`check:cross-package-test-inputs`, `check:published-files`,
`check-dev-prereqs --self-test`,
`check:self-test-wired`, `check:scripts-symbol-anchors`,
`check:type-check-coverage`,
`check:test-source-alias`, `check:tier-file-adoption`, changeset gates,
and the rest of the derived 84
- ⊘ **NOT MEASURED (3+1), left to CI:** `check:dual-build-cjs-loads`,
`check:lean-entry-closure` and
`check:type-check-debt` each exit **3** — PREREQUISITE NOT MET, they
read a whole-repo `dist/` this
container has not built (`Build Core` owns that run).
`check:pm-dispatch-gates` exceeded the container's
foreground cap at 560s (exit 124) — a checker-health self-test for a
file this diff does not touch.
  ⛔ None of the four is a pass or a failure.

**A measured cost, reported rather than buried:** because the digest now
covers `packages/spec/scripts/**`,
editing anything there — including a script test — moves the digest and
makes `check:browser-reachable-entries`,
`check:generated` and the boot gate demand a rebuild first. Observed
twice during this run. It is the same
ergonomics that `src/**/*.test.ts` already carries (those are in the
digest today), and the alternative — a
rule deciding which files under `scripts/` are "really" inputs — decides
wrong the day someone extracts a
helper, in the acquitting direction. Triage's stop condition was 大量无关包的
boot gate 失效; measured,
`AMPLIFIERS = ['packages/spec']`, so exactly one package stamps and the
blast radius is that one package.

Draft, not armed.

---
_Generated by [Claude
Code](https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU)_

Co-authored-by: Claude <noreply@anthropic.com>
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