Skip to content

fix(scripts): the console spec-injection probes are chosen with the bundle in view (#20646) - #20743

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20646-console-pin-probe
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20646-console-pin-probe

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #20646

Clause-②: no

Console Pin Gate is red on main since fbec216e2d (PR #20695, the @objectstack/spec/migrations entry split). Its build step exits 2: "Neither spec appears in the built console — no @objectstack/spec content matched." The injection works. The check was reading text the console never bundles. This PR makes the build-time probe derivation choose with the bundle in view. The fresh leg stops reading entries the console never imports, and the stale leg becomes strictly stronger.

Root cause, measured

scripts/console-spec-probes.mjs builds each spec's blob from every JS file the package's exports map resolves to. The assertion then took the alphabetically first unique .describe() text on each side. A console bundles only the entries it imports, so either probe could come from an entry the bundle can never carry.

The readings below compare the published @objectstack/spec 17.4.0 that objectui locks at pin dd3f7e1be356 with the framework spec.

leg framework fresh witness (carried by) in bundle stale detector (carried by) in bundle old check
dark f927864ea0 (main before the split) "A public export added or removed by one release." (.) yes "Accepted developer agreement version" (./cloud) no exit 0
lit fbec216e2d (origin/main) the same text, now (./migrations) no the same (./cloud) no exit 2
  • The witness moved with the split from the root to ./migrations, which the console never imports. Meanwhile 102 of the 142 injected-only descriptions ARE in that bundle; the first sorts from ./ui.
  • The stale leg was already blind. Its detector sits in the published ./cloud entry, which objectui's shipped source never imports: 116 /ui, 62 /data, 17 /kernel and so on, and no /cloud. A console built WITHOUT the injection carries 56 of the 160 published-only descriptions, and not that one.
  • The spec side is not the cause. The console never imported the change manifest, and the injected spec is in the bundle. packages/spec is untouched here.

The fix (scripts/console-spec-probes.mjs, chooseProbes)

  • Fresh witness: the first injected-only description the bundle DOES carry. With none carried, "neither spec appears" is still exit 2.

  • Stale leg: judged over EVERY published-only description. It fails when ANY of them is in the bundle, and reports the first one found with the count. A bundle the old single pick flagged is still flagged, so this is strictly stronger.

  • Stamp: keeps its shape and version. With nothing published in the bundle, the stamped detector is the same one pickProbe chose, so check:console-injection's cache-hit replay is unchanged.

  • Assertion script: scripts/assert-console-spec-injection.mjs calls chooseProbes and prints the counts. The exit ladder is unchanged: 0 verified, 1 published spec bundled, 2 unverified.

  • Self-test: scripts/check-console-injection.mjs gains battery 13 (roster floor 10 to 11). It runs the real assertion script on fixtures whose first unique candidate is not in the bundle:

    • a working injection verifies, and its stamp replays green;
    • a published-only description the old pick skipped fails the build and writes no stamp;
    • neither side present stays exit 2.

    I checked that the battery can fail: reverting chooseProbes to first-pick semantics made it red with 5 failures, and restoring the fix turned it green again.

Proof

All legs ran against real vite builds of the console, with the old and the new assertion on the same bundle.

console build objectui pin new check old check
injected spec from fbec216e2d dd3f7e1be356 exit 0 (witness from ./ui; 102/142 present; 0/160 published) exit 2
NOT injected (published spec) dd3f7e1be356 exit 1 (56/160 published-only descriptions present) exit 2
a bundle with no spec text — exit 2 ("Neither spec appears") —
injected spec from f927864ea0 (dark) dd3f7e1be356 exit 0 (111/142 present) exit 0
injected spec from fbec216e2d db11afd49670 (PR #20706's pin) exit 0 (102/142; 0/160) exit 2
NOT injected (published spec) db11afd49670 exit 1 (56/160) exit 2

On this branch's head, the Console Pin Gate's steps run locally all pass: bash scripts/build-console.sh with the injected spec exits 0; the dist presence assert passes; pnpm check:console-sha exits 0; and pnpm check:console-injection --require-stamp exits 0 and replays the new stamp.

Verification record

  • dispatch-gates --commands for the 3 changed paths derives 31 families. --ran reconciles them as 30 run, all exit 0, and 1 NOT MEASURED: check:pm-dispatch-gates, whose self-test alone outruns the 590-second foreground cap here. It does not read these files' behaviour.
  • node scripts/check-console-injection.mjs --self-test passes: 44 assertions, battery floor met.
  • Lint, narrowed and proven: eslint --no-inline-config on the 3 changed files reports 0 errors and 0 warnings. The config enables no type-aware linting, so untouched files' verdicts cannot move.
  • No changeset: the diff touches only root scripts/, which ship in no published package (@objectstack/spec-monorepo is private).

Acceptance notes

  • The cache-hit replay's stale leg is still a single stamped detector. At this pin that detector is ./cloud text no console bundles, so on a dist-cache HIT the replay cannot catch a published spec. The build-time leg now can. Fixing the replay needs a multi-detector stamp (a stampVersion bump that invalidates every cached dist under the unchanged cache key), which is outside this claim. Carrier: the domain:spec seat. Noted, not filed.
  • CI on this head: Console Pin Gate (job 109696257740) concluded success. The dist cache missed, so step 11 built the console and ran the changed assertion (105 of 145 injected-only descriptions present, all 160 published-only absent), and steps 12 to 14 passed on that fresh dist.

Generated by Claude Code

…undle in view

The build-time assertion took the alphabetically first unique description from
every exports-map entry, so both probes could come from entries the console never
imports: after the migrations entry split the fresh witness was
@objectstack/spec/migrations text and a working injection read as 'neither spec
appears', and the stale detector was published ./cloud text, absent by
construction. The witness is now the first injected-only description the bundle
carries, and the stale leg fails on ANY published-only description in the bundle.
The stamp keeps its shape.

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5e32522c10e4f90b2ef4ff42f4e8878cf39608e7
Local-runs: none

Inputs: card #20646 (body and all 16 comments — the landing-with-a-red-check record 5901417945, the follow-up claim 5901457286, the dev's report 5902247352), PR #20695's comments (the seat's Console Pin Gate note 5901327082 and the queue-triage comment), PR #20743 (body, zero comments, 3-file list, the net diff against main at the head, whose merge base is fbec216e2d; main has since advanced 5 commits, none touching the three files), ci.yml's filter and console-pin jobs and scripts/build-console.sh at the head, @objectstack/spec's exports map and sources at fbec216e2d, f927864ea0 and the tag @objectstack/spec@17.4.0 plus the npm packument for 17.4.0, objectui at pin dd3f7e1be356 (git objects) and the compare to db11afd49670 (API), and the head's 34 check-runs with job 109696257740's log, read once. Nothing built, run or re-run.

① Derived judgments

  • Root cause, read from the code. At f927864ea0 packages/spec/src/index.ts:227 re-exports ./migrations/index.js, so "A public export added or removed by one release." (src/migrations/spec-changes.ts:141) sat in the root entry, which the console imports (13 root imports at the pin). At fbec216e2d that line is gone, the exports map gains ./migrations (root plus 18 subpaths), and the text is reachable only through dist/migrations/index.mjs; objectui at the pin imports @objectstack/spec/migrations nowhere. The old fresh leg keyed on the alphabetically first injected-only candidate, so a working injection read as exit 2. The stale side: "Accepted developer agreement version" is src/cloud/developer-portal.zod.ts:69 at the tag 17.4.0, whose exports map (tag and npm packument agree) carries ./cloud; the tree dropped ./cloud at 776d64cd3d, and objectui at dd3f7e1be356 has zero source imports of @objectstack/spec/cloud (the 11 files naming it do so in docblocks and tests). Both pins lock @objectstack/spec@17.4.0 (the lockfile is unchanged between them). So the detector was unbundled by construction before the split and the witness became so with it: the fault is the derivation's assumption that the first unique candidate sits in an entry the console bundles, not the spec. The seat's hypothesis in 5901327082 holds. Right.
  • The accept-set change, enumerated. Old: fresh = first unique injected candidate in the bundle, stale = first unique vendored candidate in the bundle. New (chooseProbes): fresh = ANY injected-only candidate in the bundle, stale = ANY published-only candidate in the bundle; the exit ladder and the tri-state (null when a side has no unique candidate) are unchanged. Per verdict: the exit-1 set is a superset of the old one (the old detector is an element of the new pool); the exit-2 set shrinks to exactly "no unique text of either spec in the bundle"; the exit-0 set gains bundles that carry some injected-only text but not the first-sorted one and no published-only text, and loses bundles that carry the first witness, lack the first detector, and carry another published-only text. Right, and the direction the order asked for.
  • Bundles the old script rejected that the new one accepts, constructed. Every such bundle is an old exit 2 (never an old exit 1). Case (i), the fbec216e2d bundle itself: the injection worked, 102 (CI: 105) injected-only descriptions were in it, and the old verdict "Neither spec appears" was false — an accident of sort order and entry layout, since the same bundle passed the old script the day before the split with the same text one entry over. Case (ii), the only defective shape in that set: a failed injection whose console-imported entries carry no published-only description while the client's transitive copy of the tree's spec carries injected-only text. There the old exit 2 was luck, not design — its message was still false, and it would have been exit 0 had the first candidate sat in a client-imported entry — and the assert header already assigns that one-sided blindness to the stale leg, which is stronger here, not weaker. At these pins case (ii) is unreachable: a non-injected build carries 56 of the 160 published-only descriptions (the dev's positive control at both pins), so it exits 1 under the new script where the old one exited 2. No bundle the old script rejected by design is accepted. Right.
  • The order's three lines. (1) Yes: a bundle with no unique text from either spec exits 2 under both scripts (freshPresent and stalePresent both non-true), battery 13's third fixture pins it against the real assert script, and the verdict text is unchanged; bundles that carried unique text of one spec and were exit 2 only because the first pick was elsewhere now get a verdict, which is the fix and not a loosening. (2) Yes: every bundle the old stale leg failed is failed, as above; the fixture whose first published-only text is unbundled and second is bundled goes from old exit 0 to exit 1. (3) Yes: three files, no workflow step, no allowlist, no skip; SELF_TEST_BATTERY_FLOOR 10 to 11 pins the roster size of the existing self-test (the file's own comment: a deleted battery must red), and raising it when a battery is added is that gate's internal parameter, not a new gate. Right.
  • False positives of the stronger stale leg. The head's own gate run confirms the dev's reading at dd3f7e1be356 independently: "all 160 published-only descriptions are absent" on a real injected build. At db11afd49670 the reading is the dev's build alone; the compare between the pins adds no @objectstack/spec/cloud or /migrations import in the 300 files the API lists (through packages/app-shell; the remainder is unlisted) and leaves the lockfile untouched, and the pin-bump PR's own head runs this gate. Enough for the two pins in play. If a later pin or lockfile bump brings published-spec text into the assets through objectui's own dependencies, the leg reds with a true statement about the bundle; the message names the count and the first text found, not the asset file it sits in — a nicety, not owed. Right.
  • The stamp and its replay. writeStamp is called with the same five fields, STAMP_VERSION stays 1, and readStamp is untouched. On a passing build staleInBundle is empty, so the stamped detector is stalePool[0], exactly pickProbe's choice; the stamped witness is by construction in the bundle. evaluate() in check-console-injection.mjs is byte-unchanged (the diff touches only the roster, the floor and battery 13), so the replay still asserts the witness present, the detector absent, and the detector not expired against this tree's spec; the head's step 14 replayed the new stamp green. Right.
  • Battery 13. Three fixtures drive scripts/assert-console-spec-injection.mjs through spawnSync: fresh (first injected-only candidate unbundled, second bundled: exit 0, stamp records the bundled witness, replay green), stale (first published-only candidate unbundled, second bundled: exit 1, message names PUBLISHED and the text, no stamp written), neither (shared text only: exit 2, message unchanged). Six registered cases, floor 6; the two unregistered checked += 1 message checks follow the file's idiom. Read against a revert of chooseProbes to first-pick semantics: UNBUNDLED_FRESH sorts before FRESH so the fresh fixture exits 2 and writes no stamp; UNBUNDLED_STALE sorts before STALE so the stale fixture exits 0 and writes a stamp; that is five failures (fresh status, stamp witness, stale status, stale message, stale stamp) and the dev's ablation count. The third fresh expectation (replay green) does not discriminate under that revert, because an unstamped dist is advisory exit 0; the two before it do. Fixture strings are within the 32-to-160 length window. Right.
  • The dist cache on this head. Job 109696257740's log: CACHE_HIT is empty at step 12, the presence assert prints "via scripts/build-console.sh", step 11 ran the vite build and the changed assertion ("105 of 145 injected-only descriptions are in the bundle", "all 160 published-only descriptions are absent"), step 13 matched the pin, step 14 ran the self-test (44 assertions) and the replay, and step 15 saved the dist under Linux-console-dist-b59b7e9a…. The dev's correction is the true reading; the body's second Acceptance note is wrong for this head, and right only as a statement about a future hit. The seat applies pr_body_lines.
  • Gate coverage at the single read of the head's 34 check-runs: Console Pin Gate success (steps 1 to 15 all success; this is the family the diff derives — check:console-injection and check:console-sha ran against a fresh build); Type Check source gates, consumer gates, workspace, debt ledger and TypeScript Type Check success; Test Core 2 to 6, Dogfood Regression Gate, filter, Governed Surface Queue Guard, the three claim guards, Check Changeset, Check PR Size, Auto Label and Check Documentation Links success; Build Core, Build Docs, Dogfood matrix, Dogfood Verify CLI, Temporal Conformance and Packed-tarball smoke skipped by filter or opt-in, plus a second trigger's Auto Label, Check PR Size, Check Changeset and Packed-tarball rows skipped; 0 red; NOT concluded at my read: Lint & Repo Gates (lint and the scripts/ gates the dev's 31 families name) and Test Core (1/6). Nothing red is this diff's; the landing waits for every check, which is the seat's read.

② Semver level

  • skip-changeset matches what the diff publishes: three files under root scripts/, owned by @objectstack/spec-monorepo (private: true) and inside no package's files; nothing from any released package moves. Check Changeset concluded success. Right.
  • Clause-②: no, judged from scripts/pm/clause2-line.mjs's reading (value first after the colon, no arm): a repo gate's internals; no contract accepts or rejects anything new, and no public surface widens or narrows. Right. No changeset owed.

③ Boundary flags

  • Dev deviation, the refused worktree removals (objectstack-issue-20646 at 6ce9c2b349, objectstack-issue-20646-dark at f927864ea0): not a PR fact; both are on the dev machine, clean, for the seat or the maintainer to remove. Answered.
  • Dev deviation, the objectui build tree moved into scratch and reached through a gitignored .cache symlink: local hygiene under the read-only rule for the pin; nothing in the diff. Answered.
  • Dev deviation, the positive-control and second-pin builds run as build-console.sh's own commands by hand: the right method, since the script always injects and reads the pin the claim forbids touching; those readings are the dev's, and the head's gate run is the record's. Answered.
  • Dev deviation, check-console-injection.mjs changes only its self-test: verified by the diff. Answered.
  • Dev deviation, the body's second Acceptance note: verified against the log above; the seat patches the body with the dev's line. Answered.
  • Dev out-of-scope finding, the replay's single stamped detector: on a cache hit the replay re-reads immutable bytes the build-time leg judged against the whole pool before step 15 saved them (release.yml's combined save can store a failed build, but the stamp is written last, so --require-stamp refuses it), and no pre-fix cache under this key can hold an unproven dist at this pin, because the old script exited 2 on a non-injected build and saved nothing. Not a wrong result anyone can reach today; the replay's live value is the witness-presence and expiry checks, both intact. An Acceptance note is the right carrier; a card only if the seat wants the multi-detector stamp on its own merits. Not escalated.
  • Dev NOT MEASURED, check:pm-dispatch-gates: the diff does not touch dispatch-gates.mjs; CI runs it under Lint & Repo Gates, unconcluded at my read. Gate-covered.
  • Reviewer's flag, the counts: the code comment in chooseProbes and the PR body record 102 of 142 injected-only descriptions at fbec216e2d; the head's CI build reads 105 of 145 for the same tree, a difference consistent with a locally reused spec dist (build-console.sh skips the spec build when dist/index.mjs exists). The verdict path reads no count, so nothing turns on it; the comment states its measurement context. Not a defect.
  • Reviewer's flag, cost: the assertion now runs one substring search per unique candidate (about 305) over the concatenated assets instead of two; about thirteen seconds in the head's run, inside a 45-minute job. Noted, not a defect.
  • Dev open_questions: none.

Implemented-by: claude/issue-20646-console-pin-probe
Reviewed-by: session_014EJ1ED8X4MMrT18BhVx4tx

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 01:47
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 9c8f113 Sep 30, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20646-console-pin-probe branch September 30, 2026 02:21
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

Projects

None yet

2 participants