Skip to content

feat(spec,metadata-core)!: every retired ADR-0087 conversion carries retiredAfter; the artifact door opens its window per entry (#20390) - #20435

Merged
objectstack-fleet[bot] merged 16 commits into
mainfrom
claude/issue-20390-forward-conversion-window
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 16 commits into
mainfrom
claude/issue-20390-forward-conversion-window

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #20390

Clause-②: yes

Implements ruling 5865890672 (batch #235 item 1, letter A, maintainer 「同意 A」; maintainer record 5865873150, route 5866178043): every retired entry in the ADR-0087 conversion registry carries a REQUIRED retiredAfter, and the artifact forward-conversion window decides per entry. It is one vertical PR across packages/spec, packages/metadata-core and the artifact door in packages/metadata.

Spec half

  • MetadataConversion is a live-or-retired union (packages/spec/src/conversions/types.ts). An entry with retiredFromLoadPath: true must also carry retiredAfter, typed as a stable x.y.z template-literal string; a live entry carries neither. tsc refuses an unstamped retirement (the reverse verification is below). The type moves from an interface to a type alias, so gen:api-surface and gen:export-origins each rewrite one row: MetadataConversion (interface) becomes MetadataConversion (type).
  • Backfill, from the published tarballs. Each published entry's value is the stable release just before the first tarball that carries it retired. Each entry in no published tarball carries the current package.json label, 17.4.0.
  • Census test. src/conversions/retired-after.census.json holds raw facts per stable release since the registry first shipped (14.8.0 through 17.4.0): the tarball integrity and the ids its ALL_CONVERSIONS marks retired. src/conversions/retired-after.census.test.ts pins every entry's value against it, offline, in the local tier. It pins that every entry absent from the last published tarball carries the label, and that no value is malformed or above the label. scripts/build-retired-after-census.ts re-derives the census from registry.npmjs.org. It checks each tarball's integrity, imports each release's dist/index.mjs, and writes the census, or compares it with --check.

metadata-core half

applyArtifactForwardConversions replays entry E when the artifact's floor is below the runtime label OR at or below E.retiredAfter. DEFAULT_FLIPS_NOT_REPLAYED_HERE is still read first. Its membership is unchanged; flow-decision-mode-inclusive-explicit came in with the merge of #20344. When the floor is at or above the label, only the entries the floor predates are replayed. The rest reach the strict parse and their tombstones through the existing excludeConversionIds seam, computed per entry from the registry. There is no second table. ArtifactForwardConversionVerdict gains 'converted-retired-after' for that case. ArtifactForwardConversionResult gains replayedRetirements (element type ArtifactReplayedRetirement): under that verdict, each retirement this runtime enforces past the artifact's floor, with its retiredAfter; it is empty for every other verdict. The module docblock's two policy sentences still hold: "a key retired at version V stays a loud refusal for anything authored at ≥ V" (the floor-at-or-above-label bullet), and "Not a second conversion table".

The door's consumer arm (packages/metadata/src/plugin.ts)

The verdict has one in-tree consumer that branches on it, and the new arm is added there.

The four pins

Pin Where Asserts
(1) a 17.4.0-CLI-built artifact with dashboard charts and page assignedProfiles boots on main and logs the notices packages/metadata/src/plugin-artifact-forward-conversion-retired-after.test.ts, on a REAL fixture: dist/objectstack.json built verbatim by the published @objectstack/cli 17.4.0 the dashboard and page register with chartConfig.type/xAxis/yAxis and assignedProfiles converted away; one warn line each for dashboard-widget-chart-config-structure-removed (3 sites) and page-assigned-profiles-removed (1 site)
(2) newly authored sources using the retired keys are still refused loudly same file defineStack refuses with code: 'STACK_SCHEMA_INVALID', status: 422, and one issue per retired site (4 paths)
(3) floor exactly 17.5.0 on a 17.5.0-labelled runtime is refused, not converted packages/metadata-core/src/artifact-forward-conversion.test.ts verdict authored-current, zero notices, and the strict parse refuses the same 4 paths
(4) unreleased main (label 17.4.0), artifact at the last release (^17.4.0) same file verdict converted-retired-after, notices by id and path, and the strict parse passes

Beside pin (1), the #12915 pin (plugin-artifact-forward-conversion-retired-after.test.ts, "announces a bare-root form predicate once"): the ^17.4.0 fixture with one bare-root form predicate (stage == "won") on the 17.4.0 runtime logs the unbound-root line exactly once, including across a second ingestion. It is red under the old guard and green now (below).

Three companions sit beside the pins. After the release (label 17.5.0) the same artifact converts through the label half, with replayedRetirements empty. A 17.2.0 retirement still meets its tombstone inside the open per-entry window. flow-decision-mode-inclusive-explicit stays refused inside its own per-entry window. Pin (4) also asserts replayedRetirements: both retirements at 17.4.0, and never the default flip.

Census (re-derived on this tree, npm latest = 17.4.0, label = 17.4.0)

94 retired entries: 73 published and 21 unpublished. The ruling counted 91 retired with 18 unpublished at df3ba164. Three unpublished entries landed since then: action-aria-removed, connector-resilience-keys-removed (#20350) and flow-decision-mode-inclusive-explicit (#20344, merged into this branch).

first published retirement entries retiredAfter
15.1.0 5 15.0.0
17.0.0 45 16.1.0
17.1.0 5 17.0.0
17.2.0 2 17.1.0
17.3.0 8 17.2.0
17.4.0 8 17.3.0
none (unpublished) 21 17.4.0

The ruling's census bucket of 50 entries "first retired in 17.0.0" is 45 + 5. The engine seat's census started at the 17.0.0 tarball. Those 5 entries (object-compactLayout-to-highlightFields, stack-roles-to-positions, owd-legacy-read-aliases, sharing-recipient-role-to-position, book-audience-profile-to-permission-set) are already retired in the 15.1.0, 15.1.1, 16.0.0 and 16.1.0 tarballs, so the ruling's own principle gives them 15.0.0.

Verification (final HEAD 2c537b7e)

  • Derived gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack gave 90 commands at 2c537b7e (16 files, +1667/−72), and all 90 exit 0 on that head. The --ran reconciliation (each line carrying its exit code) reads: "90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN". The full package closure was rebuilt first (turbo 71/71).
  • @objectstack/spec test (--project local): Test Files 565 passed (565), Tests 16645 passed, 1 todo. test:repo (--project repo, run in two halves of 18 files each to fit the foreground cap): 18 files / 460 tests and 18 files / 195 tests, together Test Files 36 passed (36), Tests 655 passed.
  • @objectstack/metadata-core test: Test Files 16 passed (16), Tests 295 passed (295). typecheck exit 0.
  • @objectstack/metadata test: Test Files 55 passed (55), Tests 826 passed (826). typecheck exit 0.
  • eslint --no-inline-config --format json on the 10 changed source files: 10 files linted, 0 errors, 0 warnings. eslint.config.mjs never enables type-aware linting, so this diff cannot move the verdict on any untouched file.
  • Main was merged three times, all through scripts/pm/os-regen-merge.sh. None of this round's incoming commits touch packages/spec/src/conversions, packages/metadata-core or packages/metadata, and none adds a retired entry: every one of the 94 carries retiredAfter.

Ablation and reverse verification (from committed state, through scripts/ablation-replace.mjs)

  • Guard ablation (this round). In plugin.ts, if (!FORWARD_WINDOW_OPENED[result.verdict]) return; was put back to the old guard, if (result.verdict !== 'converted-forward' && result.verdict !== 'converted-undeclared') return;, with a marker comment. On-disk count: marker 1, new guard 0. Across the three door suites (21 tests), exactly one went red, the 17.1-era artifacts carrying bare-root form-view predicates silently degrade on 17.2: unbound root faults open, conditionally-hidden fields render and their required: true dead-ends console record creation — no ADR-0087 conversion covers the predicate-root move #12915 pin ("announces a bare-root form predicate once"). The rest stayed green, including the updated current-surface silence pin. Restore: blob 8f43972c equals HEAD, git diff HEAD is empty, git status --porcelain has 0 lines, and all 21 tests pass again. The suites import plugin.ts from source, so no build sits between the mutation and the run.
  • tsc forces the next verdict decision. With the 'converted-retired-after': true row removed from FORWARD_WINDOW_OPENED, tsc --noEmit in packages/metadata exits 2 with error TS2741: Property '"converted-retired-after"' is missing. Restored to the HEAD blob.
  • Window ablation (round 0, at 87da6b88). The per-entry branch was replaced with the old label-only verdict, and metadata-core was rebuilt, with the marker present in 2 built files. Pin (4), pin (1) boot and pin (1) notices went red, along with both per-entry companions. Pins (2) and (3) stayed green. The restore was proven (blob equals HEAD, 0 porcelain lines, and the marker absent from the rebuilt dist).
  • tsc refuses an unstamped retirement. With retiredAfter removed from page-assigned-profiles-removed, spec tsc --noEmit exits 2 with exactly one error TS2322.
  • The census test fails when it should. A published entry stamped low reds the PUBLISHED test, and an unpublished entry stamped low reds the UNPUBLISHED test. build-retired-after-census.ts --check passes against npm (11 releases), and exits 1 on a tampered census.

Deviations from the ruling text, and why

  1. The rule for unpublished entries has one tolerance. While the label is AHEAD of the census's last release, an unpublished entry may carry any version from that release up to the label. Taken literally ("carries the current label"), the rule turns the Version Packages PR red. That PR bumps the label to 17.5.0 before 17.5.0 is published, while the 17.5.0 entries correctly carry 17.4.0. The tolerance closes again once the census records the new tarball. The seat confirmed this reading (5869635456). The refresh is now a written step of the GA release flow: docs/releases-maintenance.md, under "Cutting a GA release — the Version Packages PR flow", says to run scripts/build-retired-after-census.ts after a stable @objectstack/spec publish and commit the refreshed census. The seat answered the refresh question with A; no workflow and no gate are added.
  2. The network half is a script, not a repo-tier test (accepted by the seat, 5869635456). vitest.repo-tests.json is held equal to the set of tests that read outside the package (check:cross-package-test-inputs), so a network-only test cannot be listed there. Reading the tarballs means downloading every stable release since 14.8.0 (about 11 tarballs, over 250 MB), so no per-run suite does it. So CI pins the committed census offline, and scripts/build-retired-after-census.ts re-derives it. The script refuses loudly when offline; it never skips. It is not a package.json script and not wired into CI, so no gate is added.
  3. Stable releases only. The census and the rule skip -rc versions: a caret floor never names a prerelease, and the door compares x.y.z triples.
  4. Counts. See the Census section: 73 published, 21 unpublished, and a 15.1.0 bucket the ruling's counts did not have.

Acceptance notes


Generated by Claude Code

…he artifact window decides per entry

MetadataConversion is now live-or-retired: a retired entry must carry
retiredAfter, the last published spec version whose authoring surface still
accepted its old shape. The 73 published retired entries are backfilled from
the published tarballs (census committed beside the registry, re-derivable by
scripts/build-retired-after-census.ts); the 20 unreleased ones carry 17.4.0.

applyArtifactForwardConversions replays entry E when the artifact floor is
below the runtime label OR at or below E.retiredAfter, so an artifact built by
the last release boots on a main that enforces the next release's retirements.

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
….0-built artifact

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
…ersion is now a type alias

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
…nterface-only disposition

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
…rward-conversion-window

# Conflicts:
#	packages/metadata-core/src/artifact-forward-conversion.test.ts
…17.4.0; pin the refusal list read before it

The entry landed with #15429 after this branch forked; it is in no published
tarball, so it carries the package label. The door still refuses it inside
its own per-entry window, because DEFAULT_FLIPS_NOT_REPLAYED_HERE is read first.

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-core, @objectstack/metadata, @objectstack/spec, touching 19 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json, packages/spec/src/conversions/retired-after.census.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via MetadataPlugin (symbol, a top-level class))
  • content/docs/api/index.mdx (via MetadataPlugin (symbol, a top-level class))
  • content/docs/automation/flows.mdx (via MetadataPlugin (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via MetadataPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/metadata-service.mdx (via MetadataPlugin (symbol, a top-level class))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via MetadataPlugin (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json, packages/spec/src/conversions/retired-after.census.json) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 140 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 3cf64493899458632f87e661fff1b130bd3a8273 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 705d94ad0b0f455f92817bcc5fc0430702546d14 — the merge of head 59687e3f2fbd435cbbab9b2df698cc3f88c6fc59 into base 3cf64493899458632f87e661fff1b130bd3a8273, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 705d94ad0b0f455f92817bcc5fc0430702546d14 && git checkout 705d94ad0b0f455f92817bcc5fc0430702546d14
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3cf64493899458632f87e661fff1b130bd3a8273 59687e3f2fbd435cbbab9b2df698cc3f88c6fc59 && git checkout -B drift-repro 3cf64493899458632f87e661fff1b130bd3a8273 && git merge --no-ff 59687e3f2fbd435cbbab9b2df698cc3f88c6fc59

node scripts/docs-audit/affected-docs.mjs --json 3cf64493899458632f87e661fff1b130bd3a8273

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3cf64493899458632f87e661fff1b130bd3a8273 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 87da6b88af4f9da13c78162a0ba910c161b4442c
Local-runs: none

Inputs, and nothing else: card #20390 body and all nine comments (5865033067 triage, 5865235478 claim, 5865469618 dev report, 5865523485 decision card, 5865873150 maintainer record, 5865890672 ruling, 5866178043 route, 5867115315 claim, 5869372253 dev report); PR #20435 body, its 13-file list and the net diff against main (merge-base dbddf02c, 13 files, +1480/−49, the GitHub diff and git diff origin/main...FETCH_HEAD agree file for file); the check-runs on the head. Read-only throughout: git fetch / show / diff / grep / merge-tree / log against refs, plus one scratch computation over git show output (the census cross-check below) — nothing built, run or re-run.

① Derived judgments

The ruling's seven execution parameters (5865890672, letter A, maintainer 「同意 A」; maintainer record 5865873150):

  • (i) right. packages/spec/src/conversions/types.ts: MetadataConversion is MetadataConversionBody & (LiveConversionState | RetiredConversionState); the retired state is retiredFromLoadPath: true plus retiredAfter: \${number}.${number}.${number}`, the live state pins both absent. Type Check · source gateson the head is success, so no in-repo entry is unstamped. The template literal is loose at the type level (it accepts numeric spellings that are not stable triples); the census test's/^\d+.\d+.\d+$/pin closes that at runtime and the door'sparseVersion` closes an unreadable value in the loud direction (entry stays with the strict parse).
  • (ii) right. The registry at the head carries 100 textual retiredFromLoadPath: true hits, of which 6 are prose and 94 are entries; all 94 carry retiredAfter, no live entry does. Cross-checked against the committed census by reading the two files off git show: 73 published entries match the release before their first retiring tarball — first retired in 15.1.0: 5 at 15.0.0; 17.0.0: 45 at 16.1.0; 17.1.0: 5 at 17.0.0; 17.2.0: 2 at 17.1.0; 17.3.0: 8 at 17.2.0; 17.4.0: 8 at 17.3.0 — and 21 unpublished entries carry 17.4.0 (the label on the head and, per the card, npm latest). Zero mismatches. The 45 + 5 split of the ruling's 50 is the ruling's own principle applied to a census that starts where the registry first shipped (14.8.0, with 14.7.0 proven to export no ALL_CONVERSIONS); the engine seat's 50 came from a census that began at 17.0.0. Right.
  • (iii) right. retired-after.census.test.ts pins well-formedness (ascending stable releases, census never ahead of the label, sorted ids, sha512 integrity strings), every retired value a stable triple at or below the label, no live value, published values with an anti-vacuity floor, unpublished values (rule below). It lives in the local tier (src/**, not in vitest.repo-tests.json) and reads only package-internal files.
  • (iv) right. api-surface/root.json and export-origins/root.json each move one row, MetadataConversion (interface) → (type); no generated file lists members, so nothing else is owed. check:generated sits in Lint & Repo Gates (state below).
  • (v) right. idsTheFloorPostdates seeds closed with DEFAULT_FLIPS_NOT_REPLAYED_HERE and skips those ids before any version is read — read first, as ruled; the floor below runtime branch is unchanged (full chain, default flips excluded); the list is byte-identical on origin/main and the head (app-hidden-to-unpublished, flow-decision-mode-inclusive-explicit). The per-entry half reads ALL_CONVERSIONS (a value import, keeping the module header's declaration-surface rule) — no second table.
  • (vi) right. Pins (1) and (2) on the real door in packages/metadata/src/plugin-artifact-forward-conversion-retired-after.test.ts; (3) and (4) in packages/metadata-core/src/artifact-forward-conversion.test.ts. Traced: (3) floor 17.5.0 on a 17.5.0 label — every retiredAfter on this tree is at most 17.4.0, so nothing opens, null returns authored-current with the definition by identity and the strict parse refuses the four sites; (4) floor 17.4.0 on a 17.4.0 label — the 21 entries at 17.4.0 minus the one default flip open, verdict converted-retired-after, four notices by id and path, strict parse passes. The three companions (post-release reduction to converted-forward, older retirement still meeting its tombstone inside the open window, default flip refused inside its own window with an anti-vacuity assertion) are right.
  • (vii) right. In the floor-at-or-above-label branch an entry replays only when the floor is at or below its retiredAfter, so anything authored at or above the retiring release meets its tombstone — the sentence holds where it is stated; "Not a second conversion table" holds. The added paragraph (below the label the label still stands in per artifact) records what was already true on main and the ruling's own refusal to take the strict narrowing.

Accept-set and public-surface changes the diff implies:

  1. @objectstack/spec root — MetadataConversion interface → type alias, retired members gain a REQUIRED retiredAfter. Breaking for an out-of-repo implementer (the ruling presumes zero); the compiler names the member. Right.
  2. @objectstack/metadata-core — ArtifactForwardConversionVerdict gains 'converted-retired-after'; applyArtifactForwardConversions gains the per-entry branch through the existing excludeConversionIds seam. Right.
  3. Artifact-door accept set — an artifact whose floor is at or above the label but at or below some retired entry's retiredAfter now boots with those entries replayed; nothing else widens; the authoring funnel is untouched (normalizeStackInput never opens includeRetired, pin (2)). Live entries are closed in the per-entry branch — the ruling's rule names retired entries only, and the door never replayed live entries under authored-current before; all 17 live entries shipped in the 17.4.0 tarball, so no last-release-built artifact carries a live old shape today. Right; observation only.
  4. Wrong — one in-tree consumer of the verdict is left behind. packages/metadata/src/plugin.ts:830, MetadataPlugin._warnUnboundFormPredicateRoots, keeps if (result.verdict !== 'converted-forward' && result.verdict !== 'converted-undeclared') return;, so the 17.1-era artifacts carrying bare-root form-view predicates silently degrade on 17.2: unbound root faults open, conditionally-hidden fields render and their required: true dead-ends console record creation — no ADR-0087 conversion covers the predicate-root move #12915 scope-C operator notice (unbound form-predicate roots, one warn line per artifact, maintainer ruling 「同意C」) is withheld for exactly the artifacts the new arm admits, while its own docblock says it is "read off that pass's own verdict rather than recomputed, so the two can never disagree about which artifacts are 'old'". They now disagree, and the same artifact receives the notice the day the label moves to 17.5.0 (converted-forward) — the notice becomes label-dependent, which is the dependence this PR removes for the conversions. The changeset's own sentence "A consumer that switches exhaustively over the verdict adds that arm" names the duty; the claim's file surface allowed it ("the artifact door is test side unless the door itself must change"). The runtime door (packages/runtime/src/app-plugin.ts:917) only logs the verdict as data and is transparent; no other consumer branches on the verdict anywhere in the tree. Detection-only, no behaviour change, and not a regression against main (these artifacts did not boot at all) — but a declared invariant in the door's contract is made false silently, the quiet direction. Owed before landing (③).
  5. Fixture provenance (dist/objectstack.json built verbatim by the published 17.4.0 CLI) is the dev's attestation; the premise guard pins the retired-key shape, and the fixture's objectstack.json-shaped keys are consistent with a CLI build. Accepted.
  6. origin/main at e4d3f2ca: merge-base dbddf02c; docs(spec): generated reference pages follow the docs title rule, sidebar labels kept via navTitle #20401, feat(spec): a metadata-form row each for sixteen field and action keys (#19332, flight G1b) #20428 and fix(driver-sql, driver-turso): reclaimSpace() returns the whole SQLite freelist, not one page per call (#20106) #20425 touch none of the 13 files; git merge-tree --write-tree origin/main FETCH_HEAD is clean; registry.ts is untouched on main since the base, so no unstamped entry arrives with the merge. No conflict, no dependency. After landing, any open PR that adds a retired entry (the claim names feat(spec)!: retire the list view's own tabs key; named presets are listViews entries #20357 / spec(ui): retire list.tabs and the view container's body name (2 keys); listViews + ViewTabBar and the row name already deliver both #20301) reds in typecheck until it stamps retiredAfter — the designed loud direction; worth one cross-seat line.
  7. field-required-notnull-explicit is retired in the 17.0.0–17.3.0 tarballs and absent from 17.4.0 and main; the census test ignores ids off main. Right.

Check-runs on the head, read at 2026-09-28T12:11Z: 27 success — Type Check · source gates, Type Check · consumer gates, Type Check · workspace, Type Check · debt ledger, TypeScript Type Check, Build Core, Check Changeset (check-empty-changeset, check-adr-0087-registration and check-changeset-no-major against the merge base), Governed Surface Queue Guard, Spec property liveness, Dogfood Regression Gate (1/3, 2/3, 3/3, rollup), Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), Test Core (2/6, 3/6, 4/6), Check Documentation Links, Check PR Size, Flag docs affected by code changes, Auto Label, filter, and the four claim/card guards; 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)); 4 still in progress — Lint & Repo Gates, Test Core (1/6), Test Core (5/6), Test Core (6/6) — read as unconcluded, not green; no failure. The census test, check:generated and the repo gates the dev reports 88/88 locally are answered by those four when they conclude.

② Semver level

  • @objectstack/spec: minor with a BREAKING banner — right. The launch-window rule (check-changeset-no-major refuses major; breaking-ness is carried by the banner plus the ADR-0087 disposition) applies, and the body states the migration (stamp retiredAfter; the compiler names the member).
  • @objectstack/metadata-core: minor — right. A published union gaining a member is an additive widening of a published surface, at least minor.
  • packages/metadata: tests and a fixture only; no changeset owed — right.
  • Clause-②: yes — right: two published-surface widenings; no arm from the closed pair is required ("at most one"); the level axis is satisfied (both moved packages at minor).
  • ADR-0087 disposition not-required (runtime-interface-only packages/spec/src/conversions/types.ts#MetadataConversion) — right: the symbol resolves to an exported type at the head, is declared in a plain .ts (not *.zod.ts, not contracts/), is not a Zod projection, and the only *.zod.ts textual hit is a comment (masked by the gate's step 4); no metadata shape changes. Check Changeset is success on the head.

③ Boundary flags

  • Deviation (a), release-pending tolerance — keeps the ruling's intent. Read literally, the rule reds the Version Packages PR (label 17.5.0 on that branch, census last release 17.4.0, the 17.5.0 cohort correctly 17.4.0) and would block the maintainer's release act. The tolerance is exactly [last censused release, label], open only while the label is ahead; today the two are equal and the exact-label rule is live. Residual: between a publish and the next census refresh, an entry landing after the publish and stamped at the previous release passes, then reds after the refresh (unpublished → must equal the label). That residual is the open question, not a departure. Escalation: the seat confirms the tolerance reading; no change owed.
  • Deviation (b), network half — keeps the intent. The pin is the committed census (11 stable releases with tarball integrity, preceding release proven registry-less) plus the offline test; scripts/build-retired-after-census.ts --check re-derives from npm and is not in CI. The reason holds on the tree: vitest.repo-tests.json is held equal to the cross-package-input scan, and local is the cacheable offline tier — a network suite fits neither. Residual: the census's completeness against npm is attested by the dev's --check run, not by a check-run, and the offline test cannot see a missing stable release. Escalation: the seat accepts the script as the network half; the refresh trigger is where the residual closes.
  • open_questions — census refresh after a stable publish. The PR is sound without an answer: every stamp on this tree is pinned (73 from the census, 21 at the label that is also npm latest), so the door is exact for every entry today. Option A (one line under docs/releases-maintenance.md § "Cutting a GA release — the Version Packages PR flow") fits the fact that a release is the maintainer's own act; it leaves the refresh dependent on the checklist being followed, and a missed refresh leaves the tolerance open until noticed. Option B leaves the tolerance open indefinitely — a low stamp after the next publish recreates this card's defect for that entry, silently, against the ruling's AI-proofing axis. Option C closes it mechanically at the cost of a network gate (11 tarballs, hundreds of MB) the ruling's scope axis defaults to no. Recommend A; the changeset already says "Run it after each stable publish". Seat's call; not blocking.
  • Out-of-scope notes. Door warn wording — not owed by itself, but plugin.ts must be touched for the item below, so amend the line in the same touch (name the retirement the runtime enforces rather than "predates this runtime's spec … runtime spec 17.4.0"). ADR-0087 D2 — owed as a filed follow-up, not carrier-none: the Artifacts built by released 17.x tooling are REFUSED by the 17.2 runtime: retired-key tombstones fire at artifact parse, and no artifact-ingestion door runs the ADR-0087 conversion that exists for exactly this #12772 addendum ("The window, and what keys it") states verbatim that floor at or above runtime "replays nothing", which is false the moment this lands; Tier H (docs/adr/**, the maintainer's hand), fenced for this PR, so file the card and link it from the PR, not blocking. .claude/skills/spec-property-retirement/SKILL.md retirement kit — owed as a Tier S follow-up, not carrier-none; tsc makes the omission loud, so not blocking.
  • Owed before landing (the FAIL): (1) packages/metadata/src/plugin.ts _warnUnboundFormPredicateRoots admits 'converted-retired-after' — or the guard is inverted to return only on 'authored-current' and 'runtime-version-unknown' — so its "same versioned window" sentence stays true; (2) one pin beside pin (1) in plugin-artifact-forward-conversion-retired-after.test.ts: the ^17.4.0 fixture carrying a bare-root form predicate, on the 17.4.0 runtime, logs the 17.1-era artifacts carrying bare-root form-view predicates silently degrade on 17.2: unbound root faults open, conditionally-hidden fields render and their required: true dead-ends console record creation — no ADR-0087 conversion covers the predicate-root move #12915 line once; (3) the changeset and PR body drop "The door's operator log lines are unchanged" and name the arm added. Everything else in this record stands; a re-spin limited to those three re-renders PASS on the parameters above.

Implemented-by: claude/issue-20390-forward-conversion-window
Reviewed-by: session_01ARcDurZ5j34RdqsGgc4jgH

VERDICT: FAIL

…e per-entry window

MetadataPlugin reads which forward-conversion verdicts open the window from
one total table over ArtifactForwardConversionVerdict, with
'converted-retired-after' on the open side, so the unbound form-predicate
notice no longer waits for the package label to move. Under that verdict the
conversion summary names the retirement this runtime enforces past the
artifact's floor, carried on the result as replayedRetirements. The census
refresh joins the GA release flow in docs/releases-maintenance.md.

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
…lib predates ES2022

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
The entry landed with #20357 after this branch's last merge. It is in no
published tarball, so it carries the package label (17.4.0, also npm latest).

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 59687e3f2fbd435cbbab9b2df698cc3f88c6fc59
Local-runs: none

Inputs, and nothing else: card #20390 body and all twelve comments (the nine the previous record read, plus 5869635456 rework order, 5870851980 round-1 report, 5872531662 stamp-lap report); PR #20435 body (as stored at 2026-09-28T14:27Z), its 16-file list and the net diff against main (merge-base 3cf64493, the PR's base sha; 16 files, +1668/−72; the GitHub diff and git diff origin/main...59687e3f agree file for file); the previous record 5869622844 on the PR; the 35 check-runs on the head. Read-only throughout: git fetch / show / diff / grep / merge-tree / log against refs, plus one scratch computation over git show output (the 95-stamp census cross-check below); nothing built, run or re-run.

What moved since the FAIL head 87da6b88 (the bytes every carried judgment is checked against): 8 of the 16 files are byte-identical — the fixture, api-surface/root.json, export-origins/root.json, build-retired-after-census.ts, apply.ts, retired-after.census.json, retired-after.census.test.ts, types.ts. The other 8 moved by exactly the round-1 commits 874a4e74 + a1dc7d33 (changeset, docs, both metadata-core files, both plugin tests, plugin.ts), the merge 9cfc3122 (registry: +109, #20357's entry) and the stamp 59687e3f (registry: +1). Three os-regen-merge.sh merges since the FAIL head (df46d5b8, 2c537b7e, 9cfc3122), all clean; both root.json files unchanged through all three.

① Derived judgments

Rework order 5869635456, the five owed items, on this head:

  • (1) right — the arm, as a total table. plugin.ts:46 declares FORWARD_WINDOW_OPENED as a readonly Record keyed by every ArtifactForwardConversionVerdict member and valued boolean, six rows: converted-forward, converted-undeclared, converted-retired-after true; authored-current, runtime-version-unknown, not-an-object false. _warnUnboundFormPredicateRoots (:889) returns on !FORWARD_WINDOW_OPENED[result.verdict]. The three true rows are exactly the three verdicts under which applyArtifactForwardConversions reaches applyConversions; the three false rows are exactly its three early returns — so the docblock's "read off that pass's own verdict rather than recomputed, so the two can never disagree" is true by construction, and the notice depends on the label only the way the replay does (through the verdict), never on its own. No other verdict moved: the old guard admitted converted-forward and converted-undeclared and returned for everything else, not-an-object included; the table keeps each of those five where it was. A seventh verdict is refused at the const's declared type (TS2741) until placed — the dev's reverse verification; Type Check · consumer gates and Type Check · workspace are success on the head. The declaration is module-private, so no public surface in @objectstack/metadata.
  • (2) right — the pin. plugin-artifact-forward-conversion-retired-after.test.ts "announces a bare-root form predicate once — the 17.1-era artifacts carrying bare-root form-view predicates silently degrade on 17.2: unbound root faults open, conditionally-hidden fields render and their required: true dead-ends console record creation — no ADR-0087 conversion covers the predicate-root move #12915 notice follows the per-entry window": the ^17.4.0 fixture with one form view carrying visibleWhen: stage == "won" (bare root), ingested twice under one label, exactly one "root identifier is NOT bound" line naming 'stage' and 1 view(s): fwd_deal. Traced: floor 17.4.0 is not below the 17.4.0 runtime, the 21 entries at retiredAfter 17.4.0 (the default flip skipped) open, verdict converted-retired-after, table true, notice; the dedupe key unbound-form-predicate-root|label (plugin.ts:894) gives once across the second ingestion. Under the old guard that verdict was neither admitted member, so zero lines — red, as the dev reports (1 of 21 red under the guard ablation). The summary test also gains not.toContain("predates this runtime's spec") over every conversion line. Both live in packages/metadata and are answered by the Test Core shards, all six success.
  • (3) right, with one prose drift. "The door's operator log lines are unchanged" is gone from both the changeset and the PR body (zero hits). The changeset names the arm in its own @objectstack/metadata paragraph and its front matter adds '@objectstack/metadata': patch; the PR body has the "door's consumer arm" section (FORWARD_WINDOW_OPENED named four times). Drift: the changeset's Census paragraph and the PR body's Census section still read "94 retired … 21 unpublished" from the 2c537b7e tree, and the body's Verification section names 2c537b7e as final head; this head carries 95 / 22 after the feat(spec)!: retire the list view's own tabs key; named presets are listViews entries #20357 stamp (below). Prose only — no level line, no accept set and no Clause-② rests on it — but the changeset text becomes the CHANGELOG entry; ③ says what to do with it.
  • (4) right — the wording, and the new surface it needed. artifactWindowClause (plugin.ts:63) returns, under converted-retired-after, "was built on a surface that still accepted shapes this runtime has since retired (authored engines.protocol floor F, at or below the last release that accepted them; runtime spec R, whose label has not moved past that release)", and for every other verdict the old fragment verbatim, "predates this runtime's spec (authored engines.protocol floor F, runtime spec R)". The conversion summary (:823–841) looks each conversion up in result.replayedRetirements; found, it says the conversion "is a retirement this runtime enforces past the artifact's floor (the shape was last accepted by @objectstack/spec V): converted N site(s) forward (first at P) … converts again on every boot until it is rebuilt ('os build') with tooling from a release that ships this retirement"; not found, it is the old string character for character. Under the new verdict every notice's id is in replayedRetirements (opened is every id not closed, and only closed is excluded from the replay), and under every other verdict the field is [] (the three early returns and the two label-half branches all leave it empty; pin (3) and the post-release companion assert it), so the old wording is kept for every other verdict. The 17.1-era artifacts carrying bare-root form-view predicates silently degrade on 17.2: unbound root faults open, conditionally-hidden fields render and their required: true dead-ends console record creation — no ADR-0087 conversion covers the predicate-root move #12915 line changes only by the same clause swap. The test's ADR-0087 conversion '…' regex still matches the new line. New public surface in @objectstack/metadata-core: ArtifactReplayedRetirement (exported interface, reaching the root through export * at index.ts:21) and replayedRetirements, a REQUIRED member of ArtifactForwardConversionResult. Additive for every reader; a construct-side break only for out-of-repo code that builds the result literal — the same class as the verdict union gaining a member, which Clause-②: yes and the metadata-core minor already cover, and the changeset names both. In-tree the only constructor is the function itself; the only other caller, packages/runtime/src/app-plugin.ts:917, reads verdict / authoredFloor / runtimeSpecVersion into a debug log and branches on none of them, so it is transparent to the new field and the new verdict. No metadata-core api-surface baseline exists to regenerate. Right.
  • (5) right — the docs line. docs/releases-maintenance.md:450, inside "### Cutting a GA release — the Version Packages PR flow" (:423) and before "## Drift guard" (:452): after a stable @objectstack/spec publish, run build-retired-after-census.ts and commit the census in an ordinary PR, with an accurate statement of the tolerance that stays open until then. No workflow, no gate, no governed file; Check Documentation Links and Flag docs affected by code changes are success.

The changed silence pin ("says NOTHING about an artifact authored against the current surface") — intent kept, guard not weakened. The old spelling, a caret on the installed label, is ^17.4.0 on this tree: under the per-entry window that is an artifact built BY the last release, the exact case this PR opens, so keeping it would have pinned the defect (silence for an artifact the door now converts and announces). currentSurfaceFloor() takes the first x.y.z past both the label and every retiredAfter — 17.4.1 today — and asserts silence there. Traced: 17.4.1 is not below the 17.4.0 runtime and exceeds every retiredAfter (max 17.4.0), so idsTheFloorPostdates returns null, verdict authored-current, table false, nothing logged. That is the tightest floor the new rule is silent for (17.4.0 itself is open), so the boundary is pinned at its edge; a regression that emitted under authored-current reds it; its anti-vacuity toBeTruthy stays. It was never a discriminator for the guard (green under both, per the dev's ablation) — the new #12915 pin is. Observation only: after a label bump it names label-patch+1 rather than the label itself (both silent), a patch looser than necessary and never inside a window.

The stamp lap (9cfc3122 + 59687e3f) — right.

  • The stamp follows the census rule: view-list-tabs-removed appears in no censused tarball (retired-after.census.json is byte-identical to 2c537b7e and names it nowhere), and the census's last release (17.4.0) equals the label (packages/spec/package.json 17.4.0 on the head), so the release-pending tolerance is closed and the UNPUBLISHED test accepts exactly the label; '17.4.0' is the only value that passes.
  • No other retired entry on the merged tree is unstamped. Cross-checked by reading the registry and the census off git show: 112 conversions; 95 retired = 73 published + 22 unpublished; 17 live, none carrying retiredAfter; every retired entry stamped; zero mismatches — published values are the release before the first retiring tarball (5 at 15.0.0, 45 at 16.1.0, 5 at 17.0.0, 2 at 17.1.0, 8 at 17.2.0, 8 at 17.3.0) and all 22 unpublished carry 17.4.0. Structural count (code lines, not text): 95 retiredFromLoadPath: true and 95 retiredAfter, 0 unpaired, on both the head and the merged tree ea2c5e98. The one census id absent from the registry is field-required-notnull-explicit (withdrawn by ADR-0087's field-required-notnull-explicit conversion asserts an implication ADR-0113 abolished — the boot calls it a forward conversion, but its output and the source it prescribes disagree at the storage layer #16693); the test iterates the registry, so it ignores it by construction. Test Core on the head runs the census test in spec's local project (src/**/*.test.ts; vitest.repo-tests.json on the merged tree does not list it).
  • The merge moved nothing else of this PR's: on the PR's own paths 2c537b7e..9cfc3122 touches only registry.ts, and its +/− lines are identical to git diff 87c37aec 3cf64493 -- registry.ts (main's feat(spec)!: retire the list view's own tabs key; named presets are listViews entries #20357 hunk, 6e3e5462), with zero retiredAfter lines; both root.json files are byte-identical since the FAIL head; 7fa3e3e0, 8cdbe0c6, aeb0557f, 3cf64493 touch none of the 16 files.

origin/main now. b2855081 (#20414), one commit past the merge-base; it touches none of the 16 files and adds no retired entry (the merged tree holds the same 95/95). git merge-tree --write-tree origin/main 59687e3f → ea2c5e98c91364c6e8ed9545f06e37c99a53ea2c, exit 0, no conflict.

Carried from 5869622844 on unchanged bytes (each named file byte-identical since 87da6b88): (i) the live-or-retired union with retiredAfter required on the retired state (types.ts); (iii) the census test's rules and its local tier (retired-after.census.test.ts); (iv) the one regenerated row in each of api-surface/root.json and export-origins/root.json; deviation (b)'s script (build-retired-after-census.ts) and the census facts (retired-after.census.json); the fixture's provenance (forward-probe-17.4-built.artifact.json); the apply.ts comment. Also (vii): the module docblock of artifact-forward-conversion.ts (lines 31–110, both policy sentences) has no hunk since the FAIL head; and (v): idsTheFloorPostdates still seeds closed with DEFAULT_FLIPS_NOT_REPLAYED_HERE and skips those ids before any version is read — the loop's order is unchanged, only its bookkeeping moved from a counter to the opened array. Re-rendered on this head: (ii) the stamps (95, above); (vi) the pins — (1)/(2) on the real door unchanged in substance, (3) and (4) now also assert replayedRetirements (empty under authored-current and converted-forward; both retirements at 17.4.0 and never the default flip under converted-retired-after).

Accept-set and public-surface changes the diff implies, each named: (a) @objectstack/spec root — MetadataConversion interface → type alias, retiredAfter required on retired members: right, carried. (b) @objectstack/metadata-core — verdict union gains 'converted-retired-after'; result gains replayedRetirements; new ArtifactReplayedRetirement: right. (c) Artifact-door accept set — an artifact whose floor is at or above the label but at or below some retired entry's retiredAfter now boots with exactly those entries replayed; the authoring funnel is untouched (pin (2): defineStack refuses the four sites with STACK_SCHEMA_INVALID / 422): right, unchanged since the FAIL head. (d) Operator notices — the #12915 notice now fires for the artifacts (c) admits, and the conversion summary under that verdict names the retirement and its release; every other verdict's lines are byte-identical: right — the consumer the FAIL named is no longer left behind.

Check-runs on 59687e3f, read at 2026-09-28T14:57Z: 35, all completed — 32 success (Lint & Repo Gates; Test Core 1/6–6/6 and rollup; TypeScript Type Check, Type Check · source gates, · consumer gates, · workspace, · debt ledger; Build Core; Check Changeset; Spec property liveness; Governed Surface Queue Guard; Dogfood Regression Gate 1/3–3/3 and rollup; Dogfood Verify CLI; Temporal Conformance (live PG + MySQL); Check Documentation Links; Check PR Size; Flag docs affected by code changes; Auto Label; filter; the four claim/card guards), 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failure, 0 in progress. The four that were unconcluded at the FAIL record are all success here.

② Semver level

  • @objectstack/spec: minor with the BREAKING banner — right; types.ts and the banner text are unchanged since the FAIL head.
  • @objectstack/metadata-core: minor — right; two additive widenings of a published surface (a union member, a result member plus its element type), both named in the changeset.
  • @objectstack/metadata: patch, new this round — right; plugin.ts changes no export (the table is module-private), only which artifacts receive an existing notice and the wording under one verdict.
  • Clause-②: yes — right, in the changeset and the PR body; the level axis is satisfied for every moved package. The ADR-0087 disposition comment (not-required (runtime-interface-only …#MetadataConversion)) is unchanged since the FAIL head; Check Changeset is success.
  • Not a level matter, recorded here because it is the changeset's text: "94 retired entries when this landed … 21 unpublished" will be false by one at landing (95 / 22).

③ Boundary flags

  • Round-1 deviations (5870851980), six: (1) total Record rather than a disjunction — right, ① (1). (2) replayedRetirements as a metadata-core surface so the door does not recompute — right, ① (4); inside the declared minor. (3) @objectstack/metadata: patch — right. (4) the silence pin re-derived — intent kept, ① above. (5) test:repo in two halves — a local-run matter; the head's check-runs are the gate verdicts and are green. (6) two os-regen-merge.sh merges — verified clean, root.json bytes kept.
  • Stamp-lap deviation (5872531662): b2855081 not merged — acceptable: it touches none of the 16 files, adds no retired entry, and merge-tree on this head is clean (above). Nothing owed.
  • open_questions: [] in both reports. The round-0 question (census refresh after a publish) is answered by the seat's A and is now ① (5). Nothing to escalate.
  • Round-0 deviations (a) release-pending tolerance and (b) the network half as --check plus the committed census: carried — the bytes they rest on are unchanged and the seat confirmed both readings on the card (5869635456); the refresh step in ① (5) is where (a)'s residual closes.
  • Governed-surface follow-ups (ADR-0087's Artifacts built by released 17.x tooling are REFUSED by the 17.2 runtime: retired-key tombstones fire at artifact parse, and no artifact-ingestion door runs the ADR-0087 conversion that exists for exactly this #12772 addendum sentence, Tier H; the spec-property-retirement kit, Tier S): neither must block this PR — the ADR is fenced for this lane and the seat files both at landing per 5869635456; the kit's omission is loud (tsc refuses an unstamped retirement).
  • Not owed for the verdict, recommended before landing (seat's call): a one-line refresh of the census counts in the changeset (94 to 95, 21 to 22) — the dev's own round-0 practice after the A decision node with no declared config.conditions takes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429 merge (b9a07ea3) — and the same counts plus the final-head line in the PR body. Without it the CHANGELOG entry misstates the count by one; no accept set, level or pin depends on it.
  • Cross-seat line, carried: any open PR that adds a retired entry reds in typecheck until it stamps retiredAfter — the designed loud direction; feat(spec)!: retire the list view's own tabs key; named presets are listViews entries #20357 was exactly that case and this branch absorbed it.

Implemented-by: claude/issue-20390-forward-conversion-window
Reviewed-by: session_01ARcDurZ5j34RdqsGgc4jgH

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 15:12
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants