Observation found while measuring #9710 (which is the reason it is filed rather than fixed there — #9710's answer is a ruling, not a diff).
What I measured
scripts/check-console-injection.mjs makes five assertions that are pure functions of the restored dist + its stamp, and exactly one that reads this tree: the probe-expiry re-check
if(staleDetector&&treeBlob&&treeBlob.includes(staleDetector)){…EXPIRED…}treeBlob comes from readSpecBlob(specDir), which resolves packages/spec's exports map — i.e. it needs packages/spec/dist/**. When the spec is not built, readSpecBlob throws ProbeError, the script catches it, and the expiry branch is skipped because treeBlob is null. The run still exits 0, printing only:
ℹ Probe expiry not re-checked: this tree's spec at `…/packages/spec` has no built JavaScript to compare
(build the spec — `pnpm --filter @objectstack/spec build` — to enable it)
Driving the real exported evaluate() against a fixed dist + stamp, varying only the tree:
| tree state | verdict |
|---|
| spec not built | PASS — expiry check skipped |
| spec built, unchanged | PASS |
| spec built, moved forward | PASS |
| spec built, caught up to the published text | FAIL(1) — ✗ The stamped staleness probe … has EXPIRED. |
Row 1 and row 4 are the same tree question; only row 4 can ever be reached, and only with a built spec.
Why this is worth a card
--require-stamp exists precisely so the gate refuses to pass vacuously on a missing or unstamped dist. There is no equivalent for the spec side: no --require-spec-blob, and --require-stamp does not imply one. So the gate has a second vacuity path that its own design philosophy already rejects — the frozen-probe failure #8134 was filed about, one step further out.
It is not live today: ci.yml's Console Pin Gate runs pnpm exec turbo run build --filter=@objectstack/client... before the check, and @objectstack/spec is in that closure (verified: turbo … --dry=json lists 32 packages, spec among them). But nothing enforces that ordering — it is step adjacency, not a contract. Reordering the steps, or invoking the script from any job that does not build the closure, turns the expiry check off silently and still reports green.
Suggested shape (not prescriptive)
Either make --require-stamp imply a readable spec blob, or add an explicit flag and pass it in ci.yml, so a run that cannot perform the expiry re-check says so as a failure rather than an ℹ. A cheaper variant worth pricing: derive the expiry probe from packages/spec/src/** source text instead of built JS — .describe() arguments are plain string literals present in both — which would make the assertion buildless and remove the dependency entirely.
Refs: #9710 (where this was measured) · #9706 (the gate) · #9667 · #8134
Observation found while measuring #9710 (which is the reason it is filed rather than fixed there — #9710's answer is a ruling, not a diff).
What I measured
scripts/check-console-injection.mjsmakes five assertions that are pure functions of the restored dist + its stamp, and exactly one that reads this tree: the probe-expiry re-checktreeBlobcomes fromreadSpecBlob(specDir), which resolvespackages/spec'sexportsmap — i.e. it needspackages/spec/dist/**. When the spec is not built,readSpecBlobthrowsProbeError, the script catches it, and the expiry branch is skipped becausetreeBlobisnull. The run still exits 0, printing only:Driving the real exported
evaluate()against a fixed dist + stamp, varying only the tree:PASS— expiry check skippedPASSPASSFAIL(1)—✗ The stamped staleness probe … has EXPIRED.Row 1 and row 4 are the same tree question; only row 4 can ever be reached, and only with a built spec.
Why this is worth a card
--require-stampexists precisely so the gate refuses to pass vacuously on a missing or unstamped dist. There is no equivalent for the spec side: no--require-spec-blob, and--require-stampdoes not imply one. So the gate has a second vacuity path that its own design philosophy already rejects — the frozen-probe failure #8134 was filed about, one step further out.It is not live today:
ci.yml's Console Pin Gate runspnpm exec turbo run build --filter=@objectstack/client...before the check, and@objectstack/specis in that closure (verified:turbo … --dry=jsonlists 32 packages, spec among them). But nothing enforces that ordering — it is step adjacency, not a contract. Reordering the steps, or invoking the script from any job that does not build the closure, turns the expiry check off silently and still reports green.Suggested shape (not prescriptive)
Either make
--require-stampimply a readable spec blob, or add an explicit flag and pass it inci.yml, so a run that cannot perform the expiry re-check says so as a failure rather than anℹ. A cheaper variant worth pricing: derive the expiry probe frompackages/spec/src/**source text instead of built JS —.describe()arguments are plain string literals present in both — which would make the assertion buildless and remove the dependency entirely.Refs: #9710 (where this was measured) · #9706 (the gate) · #9667 · #8134