Skip to content

ci(console-pin): a head that moves the spec's entry layout runs Console Pin Gate and misses its dist cache - #20945

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20765-console-filter-spec-entry
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20765-console-filter-spec-entry

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20765

Clause-②: no

A head that moves the spec's entry layout now runs Console Pin Gate on the PR, and misses the dist cache so the gate judges it. Before this, such a head skipped the job on every push, and the first run was the merge-queue build, where a check outside the required set cannot stop a merge (#20695: main red on this check for about 2 h 17 min).

What changes

  • ci.yml filter job, console: list gains packages/spec/package.json and packages/spec/tsup.config.ts. The comment above the list now says which entries are build inputs and which are guards, and why the rest of packages/spec stays out.
  • ci.yml console-pin job, dist-cache key (all three spellings: restore, the CONSOLE_DIST_CACHE_KEY env, save) now hashes every build input the list names: the pin, scripts/build-console.sh, the two probe scripts (scripts/assert-console-spec-injection.mjs, scripts/console-spec-probes.mjs), and the two spec files. Comments that described the old key are corrected. The restore step name now says what it is keyed on.
  • scripts/check-ci-filter-parity.mjs gains a second subject, judgeConsole, run by main() in both lint.yml legs. It holds five things of the checked-in ci.yml: every console entry is a literal path; the key is spelled one way everywhere the job uses it; every hashed path is a console entry; every console entry is hashed or a declared guard (CONSOLE_GUARDS: ci.yml and the two check scripts, which run on a hit and a miss alike); and every declared guard is still listed and not hashed. There is a new self-test battery (8) with 20 cases, and the battery floor goes from 7 to 8.
  • scripts/pm/check-expected-skips.mjs: the Console Pin Gate row's reason text names the two spec files. Only the prose changes. The row's gate, the job's if: and the roster shape are unchanged.
  • scripts/check-console-injection.mjs (comments and the remedy() fallback text only; seat ruling 5920200169, Q2): the header no longer restates the dist key. It names the two restore steps that each spell one, and says the keys now differ. Its section is renamed "Why the rest of packages/spec is NOT in ci.yml's console filter" and says the spec's entry layout is in both the filter and ci.yml's key. The remedy() fallback, used only when CONSOLE_DIST_CACHE_KEY is unset, prints gh cache delete "KEY" plus a line naming where each workflow spells its key, instead of a stale copy of the old key.
  • .github/workflows/release.yml (comment only; no key, step or env change; seat ruling 5920200169, Q1 = B): the comment above the dist restore step says its key hashes the pin and the build script, ci.yml's hashes a superset, the two workflows no longer share dist entries, and the narrower key is deliberate.

Why the key had to move too (H1, measured)

On a cache hit the job skips Build the Console SPA, and assert-console-spec-injection.mjs runs inside that step. What runs on a hit is check:console-injection, which replays the stamp the earlier build wrote. The only thing it reads from the tree is the probe-expiry check over the union of all exports entries, and moving text between entries does not change that union.

  • The queue build that went red was a cache MISS. Run 36645820047, job 109668493655: Restore vendored Console dist took 0 s, Build the Console SPA… ran 23:37:45 to 23:41:37 and failed, and the only failure annotation is "Process completed with exit code 2" (the assert script's inconclusive exit, "Neither spec appears in the built console"). The job log itself could not be read from this container because the log blob host is refused by the egress policy.
  • Fixture replay of the feat(spec)!: the ADR-0087 migration chain leaves the root entry for @objectstack/spec/migrations (#20646) #20695 shape (scratch fixture: a root entry whose first injected-only description moves to a new ./migrations entry the console does not import; the assert and probe scripts as they stood at 9c8f113c~1):
    • leg 0: the pre-move build stamps the dist. Exit 0; witness "A public export added or removed by one release."
    • leg 1, cache hit on the post-move head: node scripts/check-console-injection.mjs --dist dist-pre --spec tree-post --require-stamp prints "✓ @objectstack/spec: the dist carries this tree's copy, and not the published one." with exit 0. The hit is green without judging the change.
    • leg 2, cache miss on the same head: the rebuild prints "✗ Neither spec appears in the built console" with exit 2. This is the queue's red.
  • How often the cache is cold anyway. On merge_group, 32 of the 38 finished Console Pin Gate runs from 2026-09-30T13:46Z to 20:23Z rebuilt, and 6 hit. On the hourly schedule, 25 of 30 runs from 2026-09-24 to 09-27 rebuilt. So widening the filter alone would have judged this shape most of the time, but not every time. Moving the key makes the rebuild certain for such a head.

Which spec paths (H2, measured)

What the console build and the probe derivation read from packages/spec:

  • package.json exports: objectui's OBJECTSTACK_SPEC_DIST hook derives one alias per exports entry and refuses a missing target (apps/console/vite.config.ts, scripts/vite-objectstack-spec-dist.ts), and readSpecBlob in scripts/console-spec-probes.mjs resolves every entry.
  • tsup.config.ts: its entries list decides which built file each exports entry points at, and build-console.sh builds the spec through it.
  • dist/index.mjs and json-schema/openapi.json are build sentinels. They are generated, not authored.

Over the first-parent commits on main from 2026-08-31 to 9905e61ca2 (3219 commits, window proven by scripts/pm/git-history.mjs), 4 commits moved the entry layout: fbec216e2 (#20695), c23cfb346, 9165d5cd4, 776d64cd3. Every one of them touched both package.json and tsup.config.ts. src/index.ts changed in 5 other commits, all of them content (a new metadata kind, a cycle fix, schema edits, citation re-anchoring), so it is not in the list. browser-reachable-entries.json (touched by #20695) is read only by a lint gate, not by the console build, so it is not in the list either. The triage control holds: no console entry reaches packages/spec/src.

Where the pins live (H3)

Before this PR, nothing asserted the console list. The only readers were dispatch-gates.mjs fixtures, and check-expected-skips.mjs, which reads outputs and if: but never paths. The pins go in check-ci-filter-parity.mjs, the gate that already reads and pins the filter job's lists, through the same YAML reader. Its battery (8) pins the following:

  • A head touching only packages/spec/package.json selects the job and moves the key. The same holds for tsup.config.ts.
  • The control: a head touching only packages/spec/src/ui/view.zod.ts (a tracked file) does not select the job.
  • Each way the filter and key can drift turns red on a synthetic tree: a filter entry the key does not hash, a hashed path the filter does not name, a pattern entry, a split key, a stale guard and a hashed guard. There are four refusals, and the report path turns red over the checked-in ci.yml with one hashed path dropped from the filter.

A bounded in-place fix, declared

The two probe scripts were already console entries but not in the key, so a head that changed the derivation could also get a green hit without the new derivation ever running. The new rule, applied to the base ci.yml, turns red on exactly those two (leg B below). It is the same defect class and the same parameter, a mechanical edit to the same file, and it adds no new verification surface, so it is fixed here. Cost: 1 commit in the 30-day window touched either script.

Cost

  • Selection of the job on a pushed head: 37 → 53 of the 3219 commits. The key moves on 13 → 32 of them.
  • The Version Packages PR bumps packages/spec/package.json, so its runs now select the job (93 CI runs on that branch in the same 30 days). The first push of each version rebuilds; later pushes with the same file content hit that PR's own entry.
  • The queue already runs this job on every merge_group build and rebuilds most of the time (above), so the queue's cost barely moves.

Reverse verification (from the committed state, 1bd002357)

  • Leg A, the hazard itself: filter widened, key not moved. node scripts/ablation-replace.mjs --file .github/workflows/ci.yml --anchor NEW_HASHFILES_ARGS --replacement OLD_HASHFILES_ARGS --expect 3 -- node scripts/check-ci-filter-parity.mjs reported anchor 3 → 0 and blob 24aac400cf2d → 14c292d64529. The gate printed "FAIL: ci.yml's console filter and the console dist key are out of step", naming the four unhashed entries, and exited 1. The restore brought the blob back to 24aac400cf2d, equal to HEAD, and git diff HEAD was empty.
  • Leg B, the new rule over the base ci.yml (bee75cebe6): exit 1, naming exactly scripts/console-spec-probes.mjs and scripts/assert-console-spec-injection.mjs. That is the in-place fix above.
  • Direction observed: red, as expected.

Gates

Derived from this diff with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 1bd002357 (57 commands). Exit codes and verdict lines are in the report on #20765. On-point results:

  • node scripts/check-ci-filter-parity.mjs: exit 0, "OK: all 9 console entr(ies) are literal paths; 6 are build inputs the console dist key hashes and 3 are declared guards; the key is spelled one way in all 3 place(s)…"
  • node scripts/check-ci-filter-parity.mjs --self-test: exit 0, 68 assertions.
  • pnpm check:pm-expected-skips, pnpm check:console-injection, pnpm check:console-sha, pnpm check:workflow-status-functions, pnpm check:workflow-step-name-quoting, node scripts/check-self-test-workflow-commands.mjs (both legs) and pnpm check:nul-bytes: all exit 0.
  • NOT MEASURED, prerequisite not met: check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure and check:sourcemap-no-sources-content need built package dist/ trees. This diff touches no package source; CI builds before them.
  • ESLint, narrowed and proven: the population is eslint.config.mjs's **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} block, which includes both edited .mjs files. --format json reports 2 files, 0 errors and 0 warnings. The config enables no type-aware linting, so this diff cannot move an untouched file's verdict, and ci.yml is not an ESLint input.
  • Console Pin Gate runs on this PR's own head, because ci.yml and the key's inputs moved. Its verdict is read from CI.
  • Patch round at f57066dd5 (comment and fallback text in check-console-injection.mjs and release.yml, one ci.yml comment): dispatch-gates.mjs --commands names the same 57 commands; reconciled with --ran (53 run, 4 NOT MEASURED as above). node scripts/check-console-injection.mjs --self-test exit 0 (44 assertions); both remedy() branches exercised on a scratch dist.

release.yml's key (seat ruling 5920200169, Q1 = B)

release.yml keeps its narrower key, and only its comment changes. The two workflows no longer share dist entries. Following ci.yml would rebuild the console on every Version Packages merge, and publishing a cached console whose spec content lags is the existing cache design.

Acceptance notes

  • The prose this PR made stale in scripts/check-console-injection.mjs and release.yml is corrected in the patch round (f57066dd5). Neither the header nor the remedy fallback restates the key any more, so check-ci-filter-parity.mjs's hold on ci.yml's spellings is the only copy that matters.
  • Observation, not filed (a read-only inference with no measured reach): build-console.sh copies the tracked sdui.manifest.json into the dist, and that file is in neither the key nor the list. a093ce3e2 changed it without a pin move, so a cached dist would carry the older copy until the pin moves. Carrier: none.
  • [finding] pm-dispatch platform reading: in this repo a draft conversion alone did NOT remove a queued PR from the merge queue (PR #20695), contrary to platform-readings.md:42 #20764 (the seat protocol watching non-required queue checks) and making this job a required context are out of scope here, as triage ruled.

Body edited by the domain:spec seat 2 PM (session_017VaLJnYwhPsanVCe9dMCJU) after the patch round, from the dev's wording in its report on #20765.

…le Pin Gate and misses its dist cache (#20765)

The `console` filter gains packages/spec/package.json (the exports map the
OBJECTSTACK_SPEC_DIST hook and the probe derivation resolve) and
packages/spec/tsup.config.ts (the entry list), and the console-pin dist-cache
key now hashes every build input the filter names: those two, plus the two
scripts that derive and stamp the injection probes. A selection the key does
not follow restores the pre-change dist and skips the build step the
assertion lives in, so the run is green without judging the change.

check-ci-filter-parity gains a second subject that holds the filter and the
key in step: literal entries only, one key spelling, every hashed path
selected, every selected path hashed or a declared guard.

Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU
Co-authored-by: Claude <[email protected]>
…e.yml's key comment, say the two dist keys now differ (#20765)

ci.yml's console dist key hashes a superset of release.yml's since the
previous commit, so three pieces of prose went false: the header of
scripts/check-console-injection.mjs restated the old key and said
packages/spec was out of the console filter, its remedy() fallback printed
that key as the one to delete, and release.yml asked the two keys to stay
in step. The header and the fallback now point at each workflow's restore
step instead of carrying a third copy of the key, and release.yml's comment
says its narrower key is deliberate. No key, step or env changes.

Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU
Co-authored-by: Claude <[email protected]>
@github-actions github-actions Bot added size/l and removed size/m labels Sep 30, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 22:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 3ad65b0 Sep 30, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20765-console-filter-spec-entry branch September 30, 2026 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants