Skip to content

fix(ci): the future-spec-major gate and the artifact-door pins stop moving with the version label - #23155

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23135-v18-version-pr-red
Oct 13, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23135-v18-version-pr-red

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23135
Clause-②: no

The generated Version Packages PR #21988 (head 63eb607ad1, spec label 18.0.0-next.0) is red on four checks. Two of them are failures in their own right, and the other two are aggregators that roll those two up. Both real failures are pins on main that are stated against the spec package's version label, so they flip when the version pass rolls the label to 18, even though none of their own bytes changed. This PR fixes both on main, at their producers. It changes nothing on changeset-release/main, on #21988, in any release workflow or on any governed surface.

Every red on #21988's current head

Read after the last of them completed at 2026-10-13T03:23Z: GET /repos/objectstack-ai/objectstack/pulls/21988 returned head 63eb607ad15b883fee70224f20afbf3c89a18e46, and /commits/{sha}/check-runs returned 44 runs: 32 success, 8 skipped, 4 failure, 0 still running. The five Test Core shards and Type Check · workspace that were running when the card was filed have all finished green, except shard 1 below.

  1. Type Check · source gates (job 115105083441). Failing line: ✗ check-future-spec-major: 3 problem(s). This is red 1 below.
  2. TypeScript Type Check (the aggregator, job 115108695957). Failing line: type-check lane `typecheck-source-gates` concluded `failure` -- expected `success`. It has no failure of its own; it is red 1 rolled up.
  3. Test Core (1/6) (job 115105339463). Failing line: @objectstack/metadata:test: Tests 2 failed, 878 passed (880), plus Failed: @objectstack/metadata#test in the turbo summary. This is red 2 below.
  4. Test Core (the aggregator, job 115113050333). Failing lines: Test Core: 1 of 6 declared shard(s) of test published no positive attestation (test-1-of-6). and Test Core: leg test reported result 'failure'. It has no failure of its own; it is red 2 rolled up.

Red 1: check-future-spec-major on the 18 tree

The three problems, verbatim, from the job and reproduced locally:

  • scripts/check-future-spec-major.mjs holds 10 citation(s) above the published major, below the floor of 12.
  • QUOTATION_EXEMPTIONS entry 0 (docs/adr/0087-metadata-protocol-upgrade-contract.md, major 18) matched NOTHING
  • QUOTATION_EXEMPTIONS entry 1 (docs/v17-docs-sweep.md, major 18) matched NOTHING

Does a prerelease of the next major count as published? Yes. The gate's own text decides it.

  • The gate's header says: "refuses prose that dates a change to an @objectstack/spec npm major ABOVE the one this tree publishes". It also says: "The rule is 'an @objectstack/spec version whose MAJOR is above the major in packages/spec/package.json', read at runtime. A gate hardcoding 18 goes dormant the day the tree reaches 18".
  • The gate's self-test has the row "the class is DERIVED — the same corpus is green once the tree reaches 18".
  • The gate cites ADR-0087's level ruling: "A tombstone names the npm release it ships in, ⛔ never the protocol major."

The generated tree publishes 18.0.0-next.0, which is an npm release of major 18 on the next dist-tag. On that tree, a citation of 18 names the release that carries the change, so it is legitimate there. Reading 18 as the published major is therefore correct, and this PR leaves the class itself (majors above the published one) exactly as it was.

What was wrong: two pins stated against the label instead of against what they pin

  • The self-fixture floor counted the gate's own citations above spec.major. The self-test fixtures cite 18 as "above 17", because they are written against a synthetic 17.4.0 manifest. At 18, those fixtures read as "at the major", so the floor saw a gutted instrument (10 of 12) although nobody had touched it. Fix: the floor now counts above SELF_FIXTURE_MAJOR = 17, the major the fixtures are written against. The two synthetic manifests now derive from that same constant.
  • The two major: 18 quotation entries. At 18, the sentences they excuse are legitimate citations. Such a citation is continued before the ledger is ever consulted, so the entries matched nothing and the self-invalidation rule reported each one as a hole. An entry at or below the published major can excuse nothing: it is neither a hole nor over-broad. Fix: such an entry is now DORMANT. It is listed on the pass line ("delete once main itself carries that major") and is never judged. Live entries keep both refusals unchanged. The version branch cannot be edited, and main still needs these entries until it carries 18 itself, so a red there could never be fixed.
  • docs/adr/0087-metadata-protocol-upgrade-contract.md is not edited. Entry 0 still points at it unchanged.

There are four new self-test rows (the roster floor moves from 29 to 33):

  • the dormant-entry row;
  • the floor at 18 is green;
  • the gutted source is still red at 18;
  • "the roll adds no problem — the REAL corpus one major up is never redder than at the published major". This row runs the real corpus, the real ledger and the real gate file a second time under a label shaped like the version branch's (major plus one, .0.0-next.0). It fails when that run reports more problems than the run at the published major. A tree only ever gains majors, and a citation that is legitimate at M is legitimate at M plus one, so the count can never rise unless some pin moves with the label. That is this card's class, and the row now catches it on main, before the roll reaches the version branch.

Proofs

  • (a) Fails on the generated head, unmodified. Fresh depth-1 clone of changeset-release/main at 63eb607ad15b883fee70224f20afbf3c89a18e46, label 18.0.0-next.0. node scripts/check-future-spec-major.mjs --self-test printed ✓ check-future-spec-major self-test: 29 cases pass. (exit 0). node scripts/check-future-spec-major.mjs printed ✗ check-future-spec-major: 3 problem(s), the three lines above (exit 1).
  • (b) Passes there with the fix applied, uncommitted. The same clone with this PR's file copied in (blob dbe4fc5e0f, identical to the committed one). The self-test printed ✓ check-future-spec-major self-test: 33 cases pass., with the roll row reading (0 at 18, 0 at 19.0.0-next.0) (exit 0). The gate printed ✓ check-future-spec-major: no @objectstack/spec citation names a major above 18, then 3 live witnessed ledger entr(y|ies), then 2 DORMANT ledger entr(y|ies) at or below major 18 … docs/adr/0087-metadata-protocol-upgrade-contract.md (major 18), docs/v17-docs-sweep.md (major 18) (exit 0).
  • (c) main stays green. Worktree at f7ec15171: the self-test printed 33 cases pass, with the roll row reading (0 at 17, 0 at 18.0.0-next.0), and the gate printed no … citation names a major above 17, 5 live witnessed ledger entr(y|ies). pnpm check:future-spec-major exited 0.
  • One-off mutations on main, restored. Each mutation went through node scripts/ablation-replace.mjs in WRAP mode, with my own EXIT/INT/TERM trap on absolute paths. Each restore was proven by blob equal to HEAD and git diff HEAD empty.
    • M1 put back the label-relative floor count (comparing f.major with spec.major again). The self-test went red on 3 rows; the roll row read 0 problem(s) at 17, 1 at 18.0.0-next.0 … holds 10 citation(s).
    • M2 deleted the dormancy line. The self-test went red on 2 rows; the roll row read 0 problem(s) at 17, 2 at 18.0.0-next.0.
    • Together the two mutations reproduce, on main's real corpus, exactly the three problems CI read on the version branch.

Red 2: the two plugin-artifact-forward-conversion.test.ts failures

Verbatim, from the job and reproduced locally:

  • "an artifact claiming the CURRENT spec version with a NON-default retired-key value still refuses with the tombstone": AssertionError: expected 'expected "the strict parse must refus…' to contain 'allowRestore'. The received message was expected "the strict parse must refuse" not to be reached.
  • "…whose retired keys carry only the emitted default parses clean…": AssertionError: expected [ Array(1) ] to have a length of +0 but got 1.

The producer is the test's fixture, not the conversion source

  • The test claimed "the current surface" as ^ followed by the installed label. On the 18 tree that is ^18.0.0-next.0. parseRangeFloor drops the prerelease tag, so the floor reads as 18.0.0.
  • A floor of M.0.0 at or above the label is exactly where the door's major-step half opens. This is documented in the module doc of packages/metadata-core/src/artifact-forward-conversion.ts (verdict converted-major-step): a prerelease build of M may predate any retirement in step M, so every retired entry of step M is replayed. It is pinned at the labels 17.7.0, 18.0.0-next.0 and 18.0.0 (AT_OR_BELOW_THE_FLOOR) in packages/metadata-core/src/artifact-forward-conversion.test.ts. permission-allow-restore-purge-removed has toMajor: 18, so at the roll the "current" artifact was converted on purpose, and both pins read that documented rescue as an amnesty.
  • I probed the generated tree's own build of @objectstack/metadata-core with applyArtifactForwardConversions:
    • {"installed":"18.0.0-next.0","range":"^18.0.0-next.0","verdict":"converted-major-step","authoredFloor":"18.0.0","notices":["permission-allow-restore-purge-removed","permission-allow-restore-purge-removed"]}
    • against {"range":"^18.0.1","verdict":"authored-current","notices":[]}.
  • A one-off mutation on main (label 17.7.0), restored and proven as above, forced '^18.0.0' into both pins. It reproduced both CI assertion texts verbatim (Tests 2 failed, 4 passed (6)). So the M.0.0 floor alone is the mechanism, and the pins still go red when the window opens.

The source therefore behaves as designed, and the fixture's own comment was the false premise: "^X.Y.Z floors at exactly the installed version, so this pin survives every future spec release without edits" does not hold at any major's first release. Fix: the test now uses the floor derivation that the sibling plugin-unbound-form-predicate-roots.test.ts already uses for "the current surface": the first x.y.z past both the installed label and every retired entry's retiredAfter. That floor has a non-zero patch, so it is never an M.0.0. No assertion changed. One premise guard was added: the derived floor must be resolvable and have a non-zero patch.

Proofs

  • (a) Fails on the generated head, unmodified. In the same clone, after pnpm turbo run build --filter='@objectstack/metadata^...' --filter='@objectstack/service-settings^...' (15 of 15 tasks), pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2 src/plugin-artifact-forward-conversion.test.ts printed Tests 2 failed, 4 passed (6), with the two assertion texts above.
  • (b) Passes there with the fix applied, uncommitted. Same command, this PR's file copied in (blob a436ff248b, identical to the committed one): Test Files 1 passed (1), Tests 6 passed (6).
  • (c) main stays green. Worktree at f7ec15171: the file alone gave Tests 6 passed (6); the whole package (pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2) gave Test Files 61 passed (61), Tests 880 passed (880); and pnpm --filter @objectstack/metadata run typecheck (tsc --noEmit) exited 0. --listFiles shows that the program does include this test file (61 test files in total).

The other shard-1 ELIFECYCLE lines are collateral

@objectstack/spec:test:repo, @objectstack/spec:test and @objectstack/service-settings:test all end ELIFECYCLE between 03:00:06.39Z and 03:00:06.45Z. That is within 0.42 s of metadata's own ELIFECYCLE, and none of the three printed a failing test. Turbo's summary names only Failed: @objectstack/metadata#test (Tasks: 37 successful, 42 total). Run to completion on the generated tree (the clone, with this PR's two files present, neither of which those suites read for their verdicts), the suites measured as follows:

  • @objectstack/service-settings: Test Files 42 passed (42), Tests 755 passed (755);
  • @objectstack/spec, --project repo: Test Files 57 passed (57), Tests 997 passed (997);
  • @objectstack/spec, --project local: Test Files 660 passed (660), Tests 20021 passed, 1 todo (20022).

The pin

after #21988 next refreshes from a main carrying this fix, every check on its head is green or an expected skip

The seat reads that run.

That refresh will also carry everything else that lands on main before it. The commits on main after this branch's merge base are outside what this PR measured.

Local gates (CI runs the full farm)

I derived the gates with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on this branch at f7ec15171, after merging origin/main d206d08ca. It listed 73 commands. I ran each one and recorded its exit code before any pipe. pnpm check:lean-entry-closure first refused with PREREQUISITE NOT MET (no packages/objectql/dist); I built @objectstack/objectql and re-ran it, and it exited 0 (2 published condition(s) measured from a real load). --ran then reconciled the record as 73 derived famil(ies) accounted for — 72 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3). All 72 that ran exited 0, including pnpm check:future-spec-major, pnpm check:nul-bytes, pnpm check:cross-package-test-inputs, pnpm check:test-source-alias, node scripts/check-position-name-fold-loaders.mjs, pnpm check:type-check-coverage and pnpm check:type-check-debt. pnpm check:pm-dispatch-gates ran detached and exited 0 (1833 cases pass, fast tier).

NOT MEASURED locally: pnpm check:dual-build-cjs-loads (PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/; it needs the whole workspace built, and 73 packages are unbuilt in this worktree). It reads built dist/, and this diff changes no built byte of any package, so it cannot move it. CI's lint jobs build and run it.

Changeset

None: the skip-changeset label. The diff is a root scripts/ file (the root package @objectstack/spec-monorepo is private) and a test file in @objectstack/metadata, whose files is dist, README.md and CHANGELOG.md. Grepping packages/metadata/dist for the new test helper currentSurfaceFloor finds 0 files, against a positive control _parseAndRegisterArtifact that finds 6.

Acceptance notes

  • The two dormant entries can be deleted once main itself carries 18.0.0-next.N or later, which happens after the maintainer lands the Version Packages PR. The gate's pass line names both from then on. Nobody currently owns that cleanup.
  • A related question, handed to the seat in the report and not acted on here: while main sits in Changesets pre mode next with 24 major changesets queued, a change landing on main now first ships in 18.0.0-next.N. The gate's prescription on main ("Not shipped yet ⇒ the bare published major, which ADR-0087 guarantees is the carrier (a pre-GA retirement ships minor)") still names 17. That is the same pre-mode family as spec(conversions): under a pre-mode label (18.0.0-next.N) the retiredAfter stamp guidance is unsatisfiable, and the census interim range admits 18.0.0, which its own PUBLISHED rule rejects after GA #23042, which remains open, and it is a decision, not a fix for this card.
  • The local --project repo run of @objectstack/spec took 20m24s under the shared lock on this 4-CPU container (--maxWorkers=2). This is a shared-box reading.

Generated by Claude Code

…oving with the version label

On the generated Version Packages tree the spec label rolls to
18.0.0-next.0 and two pins that hold on `main` broke, with no byte of
their own changed:

- check-future-spec-major counted its own self-test fixtures against the
  tree's major (they cite 18 as "above 17" by construction), so the roll
  read as a gutted instrument; and its two major-18 quotation entries,
  whose sentences became legitimate citations at 18, reported "matched
  NOTHING". The floor now counts above the fixtures' own major, an entry
  at or below the published major is dormant (it can excuse nothing), and
  a self-test row drives the real corpus one major up under a
  prerelease-shaped label so the class reds on `main` first.
- plugin-artifact-forward-conversion.test.ts claimed "the current
  surface" as `^<installed label>`, which at M.0.0 (and M.0.0-next.N)
  is the floor the door's major-step half opens on by design. It now
  derives the floor the sibling door tests use: the first x.y.z past the
  label and every retiredAfter, never an M.0.0.

Claude-Session: https://claude.ai/code/session_012zwsLMxQBwHUYJgPyrpEvx
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 13, 2026
@github-actions

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

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 02bec6ca86e9f1237a3f82a9efff9cb3a53562ef → packageMentionDocs.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 13, 2026 04:54
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 13, 2026 04:54
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 31a872d Oct 13, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23135-v18-version-pr-red branch October 13, 2026 05:23
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 tests

Projects

None yet

2 participants