Skip to content

fix(metadata-core): a ^18 artifact built during the v18 prerelease boots on a later runtime — the door replays step 18 for a floor of 18.0.0 - #23023

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-23001-prerelease-artifact-window
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-23001-prerelease-artifact-window

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23001
Clause-②: yes

What this changes

The artifact door (applyArtifactForwardConversions, packages/metadata-core/src/artifact-forward-conversion.ts) opened its retired window per entry by comparing the artifact's engines.protocol floor with each entry's retiredAfter. A ^18 artifact has floor 18.0.0. During the v18 prerelease the package label is still 17.7.0, and every retirement that lands then is stamped at or below that label. So 18.0.0 sat above every stamp, the door answered 'authored-current', nothing was replayed, and a ^18 artifact built before a prerelease retirement met the tombstone at the strict parse. The boot rolled back.

The window now opens on a third condition:

floor below runtime   OR   floor at or below E.retiredAfter   OR   (floor = M.0.0  AND  E.toMajor = M)

A floor of M.0.0 at or above the runtime label replays every retired entry of the conversion step into protocol M (CONVERSIONS_BY_MAJOR[M], the step objectstack migrate meta --from M-1 replays). The door's default flips are still refused by id, read first. The new verdict is 'converted-major-step'. replayedRetirements keeps listing only the entries the per-entry rule opened, each with the retiredAfter the floor is at or below, so that list's invariant holds.

Which reading: (ii), idempotence. (i) was measured absent.

The card asks the PR to say which reading holds, with a test for each direction.

  • (i) A build stamp: absent. os build writes finalBundle = { ...result.data } (packages/cli/src/commands/compile.ts:995), adds docs/packages (:1015-:1020) and an optional runtimeModule (:1076), and writes that (:1095). Nothing in it names the spec or CLI that built it. The environment envelope's optional builtAt/builtWith (packages/spec/src/system/environment-artifact.zod.ts:122, :125) are written by the control plane, and the door receives only the envelope's metadata block (packages/metadata/src/plugin.ts:1304, :1311). Even a version stamp could not order a prerelease build against a prerelease retirement: both carry the same 17.7.0 label. So reading (i) would need a new artifact field, which is a contract change, and this PR does not take it.
  • (ii) Idempotence: holds over every retired step-18 entry, both directions. Each entry rewrites only its own old shape. The door therefore replays the whole step for a ^18 floor. A prerelease build from before a retirement boots, and one built after it comes back by reference.

Measured on this tree, both directions, over all 66 retired step-18 entries rather than a sample, each with its own fixture pair carried in a ^18 manifest, on a runtime labelled 17.7.0. These are permanent pins in artifact-forward-conversion.test.ts:

  • Built before. For each entry, the door turns its before fixture into exactly its after, with exactly expectedNotices, all from that entry. The one default flip in step 18, flow-decision-mode-inclusive-explicit, is never replayed and comes back by reference.
  • Built after. For each entry's after fixture, the door replays all 65 non-flip entries over it. The result is the same reference with zero notices: the full 65 x 66 cross product.

One-off corroboration, not kept as tests. On HEAD the whole replayed step goes over the real authored ^18 stacks of examples/app-crm, examples/app-todo and examples/app-showcase (each loaded through defineStack). Each run replays 65 entries, emits 0 notices and returns the same reference. Across all 133 registry after fixtures, the only fixture the step fires on is field-mapping-transform-removed. That is a step-17 entry whose canonical shape still carries connectors[].fieldMappings, which step 18 retires: the supersession SUPERSEDED_FIXTURES declares in spec's conversions.test.ts.

The card's acceptance

  • A ^18 artifact built before a prerelease retirement boots, with the entry replayed and named in the boot log. The subject is declared-index-unique-scope, a step-18 retirement stamped at the prerelease label. The strict parse refuses its old shape, bare unique: true on a declared index, so without the replay the artifact does not boot. Pinned in metadata-core at the labels 17.7.0, 18.0.0-next.0 and 18.0.0, and on the real door in packages/metadata/src/plugin-artifact-forward-conversion-major-step.test.ts. There the object registers with unique: 'global', and one warning reads [MetadataPlugin] artifact 'prerelease-probe' declares the first release of protocol 18 (authored engines.protocol floor 18.0.0), which a build from any 18.0.0 prerelease declares too, so it may predate retirements in the conversion step into protocol 18 that this runtime enforces (runtime spec 17.7.0) — converted 1 site(s) forward via ADR-0087 conversion 'declared-index-unique-scope' (first at objects[0].indexes[0].unique) ….
  • A ^18 artifact built after the retirement is unchanged. The same artifact with unique: 'global' gets verdict 'converted-major-step' (anti-vacuity: the window is open), zero notices, the same reference and identical serialized bytes at all three labels. The real door logs nothing for it.
  • The email-template keys ADR-0087 forward conversion misses two artifact doors: a ^18 artifact built on main gets no D2 replay and fails to boot; AppPlugin registers raw non-security collections, so /meta lists and the email loader read the unconverted copy #22994 measured. feat(spec)!: retire an email template's category and isSystem — the grouping and the built-in flag nothing read (#22876) #22990's entry, once it lands in step 18 with its fixture pair, is covered by the whole-step pins without edits here.
  • After 18.0.0 GA, the case the card left unmeasured, is now measured. With the label at 18.0.0 the floor equals the label, the major-step half opens, and the prerelease-built artifact boots. This is pinned beside the prerelease labels. From 18.0.1 on, the label half replays the whole chain for it, as before (pinned).
  • CONTROL: the protocol-18 handshake is untouched. checkProtocolCompat still refuses ^17 with OS_PROTOCOL_INCOMPATIBLE and objectstack migrate meta --from 17, and admits ^18 (pinned). protocol-handshake.ts is not in the diff.
  • CONTROL: a 17.x floor keeps the per-entry verdict. ^17.7.0 on a 17.7.0 label answers 'converted-retired-after' with exactly the entries stamped at or above 17.7.0. The older step-18 retirement permission-allow-restore-purge-removed is not replayed for it. The existing per-entry block passes unchanged.
  • Boundary: a ^18.0.1 floor (not a first release) on a prerelease runtime still meets the tombstone.

Ablation (reverse verification, run once, not kept)

Run at commit 1de05c1aaf, after the implementation and its tests were committed. node scripts/ablation-replace.mjs replaced const firstReleaseOfMajor = floor[1] === 0 && floor[2] === 0; with false (anchor 1 -> 0, blob 3082b968 -> 62829437). The suite ran 137 failed / 43 passed of 180. The first pin went red: runtime labelled 17.7.0: the subject replays …. Both whole-step directions and the #15429 major-step leg went red as well. Restore: blob 3082b968 == HEAD, git diff HEAD empty, marker count 0. The subject is imported from src (relative import), so no dist/ leg applies. Direction observed: red, the expected one.

One existing pin changed, and why

REFUSES the amnesty for an artifact authored at a NEWER spec than the runtime modelled "newer" as ^18.0.0 on a 17.2.0 runtime and asserted 'authored-current'. When it was written, 18 was hypothetical. On this tree 18 is the runtime's own protocol, and a ^18 artifact on a runtime labelled below 18.0.0 is exactly #23001's case, so the pin was pinning the defect. It now models "newer" as a floor past the label and every retiredAfter that is not a first release (currentSurfaceFloor('17.2.0'), today 17.7.1), and asserts that. The assertion is unchanged.

Beyond the claim's file surface

The claim named artifact-forward-conversion.ts, its tests and one changeset. Two more files changed, both forced by the verdict:

  • packages/metadata/src/plugin.ts. FORWARD_WINDOW_OPENED is a total Record over ArtifactForwardConversionVerdict, so a new verdict does not compile until it is placed on one side. It is placed on the open side, so the bare-root form-predicate notice reads the same window as the replay, which is that table's stated invariant. artifactWindowClause gains the verdict's own clause. Under the existing clauses the line would print either "predates this runtime's spec (floor 18.0.0, runtime spec 17.7.0)" or "floor 18.0.0, at or below the last release that accepted them (17.7.0)", and both are false.
  • packages/metadata/src/plugin-artifact-forward-conversion-major-step.test.ts (new): the boot-log pin above.

Rollback: revert the PR. The window returns to two halves, and ^18 prerelease artifacts are refused again.

Tests (at 335aaf9ad7, the head after merging origin/main 4e6b270999)

  • pnpm --filter @objectstack/metadata-core test: 17 files, 537 passed. typecheck (tsc --noEmit && tsc --noEmit -p tsconfig.test.json): exit 0, and the test program lists artifact-forward-conversion.test.ts.
  • pnpm --filter @objectstack/metadata exec vitest run: 59 files, 874 passed. typecheck: exit 0, and --listFiles includes the new test.
  • Dependency closure built first: pnpm --filter '@objectstack/metadata^...' build, exit 0.
  • Narrowed lint, a measurement rather than the full pnpm lint (CI's): eslint --no-inline-config --format json over the 4 touched TS files reports 4 files, 0 errors, 0 warnings, each file inside the config's population (none reported as ignored). The config enables no type-aware linting, so the diff cannot move a verdict on an untouched file.
  • @objectstack/runtime (the door's other caller, which reads verdict only into a debug log object): vitest run src/app-plugin-artifact-forward-conversion.test.ts src/standalone-stack-seeder-declaration-copy.test.ts against the built metadata-core dist/ (which carries the new verdict): 2 files, 24 passed. The rest of runtime's suite is left to CI.

Gates (at 335aaf9ad7)

The lead list of 62 at 6a7eca6868 plus the re-derivation on this diff (node scripts/pm/dispatch-gates.mjs --commands) add check:durability-log-level and check:stack-collection-maps, for 64 derived. All 64 exited 0. check:dual-build-cjs-loads and check:lean-entry-closure first exited 3 (PREREQUISITE NOT MET: no dist/). Once check:type-check-debt's re-measure had built the closure, both were re-run and exited 0. --ran reconciliation: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.

Acceptance notes

  • ADR-0087's 2026-09-13 addendum (amended 2026-09-30) still states the window as two disjuncts. It names the module docblock as the rule's authority, and that docblock now carries the third, including the M2 caveat below. The ADR is a governed surface outside this card and is not edited here.
  • M2 caveat, recorded in the docblock. A retired key that later becomes legal again (the allowRestore/allowPurge return) cannot be protected for a bare-major floor by this half, nor by the label half, which strips it for the same artifact one release later. The change that re-legalizes a key decides that entry's fate at this door. The default-flip refusal is the existing mechanism.
  • Component-prop retirements are not refused by the door's strict parse. On this tree defineStack accepted and kept object-grid properties.defaultFilters. The carrier is z.record, and the per-type props gate lives in @objectstack/lint. For those step-18 entries, a pre-retirement artifact boots even without this change, but the retired prop reaches registration and no renderer reads it. The replay restores the canonical prop. os validate's component-props gate is NOT MEASURED here. This is noted, not filed (carrier: none).

Generated by Claude Code

… step M's retired conversions replayed (#23001)

The artifact door opened the retired window per entry by comparing the
artifact's engines.protocol floor with each entry's retiredAfter. A ^18
artifact (floor 18.0.0) built during the v18 prerelease sat above every
17.x stamp, so it got none of the step-18 retirements replayed and failed
to boot on any later runtime that enforced one.

A floor of M.0.0 now also opens every retired entry whose toMajor is M
(verdict 'converted-major-step'), safe because each entry rewrites only
its own old shape. The metadata plugin places the verdict on the open
side of its window table and names it in the boot-log clause.

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
…ired step-18 entry (#23001)

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
…nd names the conversion (#23001)

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
…ator sees when a ^18 prerelease artifact boots (#23001)

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-core, @objectstack/metadata, touching 6 documentable anchor(s).

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

  • content/docs/releases/v17/17-5.mdx (via ArtifactForwardConversionVerdict (symbol, a top-level type))

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
  • 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 — 17 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 9432bac077ecbfa703c09409f3fed416edd41d99 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 47a2be3ccc9e0aa683251d0f764f440476778b0e — the merge of head fa9ec1d75754ff456e83347ee018c27023b054cd into base 9432bac077ecbfa703c09409f3fed416edd41d99, 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 47a2be3ccc9e0aa683251d0f764f440476778b0e && git checkout 47a2be3ccc9e0aa683251d0f764f440476778b0e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9432bac077ecbfa703c09409f3fed416edd41d99 fa9ec1d75754ff456e83347ee018c27023b054cd && git checkout -B drift-repro 9432bac077ecbfa703c09409f3fed416edd41d99 && git merge --no-ff fa9ec1d75754ff456e83347ee018c27023b054cd

node scripts/docs-audit/affected-docs.mjs --json 9432bac077ecbfa703c09409f3fed416edd41d99

⚠️ 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 9432bac077ecbfa703c09409f3fed416edd41d99 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ported verdict union gains a member (#23001)

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fa9ec1d75754ff456e83347ee018c27023b054cd
Local-runs: none

① Derived judgments

Inputs read: card #23001's body and its four comments (claim 6121333983 as corrected, os-dev-report 6122615428, seat answer 6122658288, patch round 1 6122690936); PR #23023's body, its one comment (docs-drift bot 6122548820), its five-file list and the net diff of refs/review/pr-23023 against origin/main (merge base 4e6b270999; 5 files, +448/−26); the check-runs on the head; on origin/main: artifact-forward-conversion.ts, the door in packages/metadata/src/plugin.ts, packages/spec/src/conversions/ (registry, types, apply), protocol-handshake.ts, ADR-0087; the cited precedent #20390 / PR #20435 (e956924e17, its changeset read off the commit).

Accept-set and public-surface changes the diff implies, each judged:

  1. Accept set at the artifact door (widening, intended). applyArtifactForwardConversions now answers 'converted-major-step' for a floor of M.0.0 at or above the runtime label and replays every retiredFromLoadPath entry with toMajor === M, default flips refused by id first. On this tree that is every ^18 artifact on a runtime labelled 17.7.0 through 18.0.0, which previously got 'authored-current' and no replay. Judged right: it is the card's done-criterion, ADR-0087's at-rest supersession text covers it, and the per-entry comparison cannot reach it — measured on origin/main's registry: 66 retired step-18 entries, 0 live step-18 entries, every retiredAfter between 17.0.0 and 17.7.0, none at or above 18. Code shape: idsTheFloorPostdates reads firstReleaseOfMajor = floor[1] === 0 && floor[2] === 0 and classifies each non-flip retired entry as per-entry opened (floor at or below retiredAfter), else majorStep (first release and toMajor === floor[0]), else closed; live entries stay closed as before. The branch is reached only when the floor is not below the runtime label, so that half of the condition is positional, not re-tested — correct. A ^99.0.0 floor still answers 'authored-current' (no step-99 entries) and its existing pin stands. Per-entry is read before major-step, so an entry open under both would be listed once with its retiredAfter; on this tree no entry is open under both.

  2. Reading (ii), idempotence, is proven over the real entries in both directions. The whole-step block in artifact-forward-conversion.test.ts iterates ALL_CONVERSIONS filtered by toMajor === 18 && retiredFromLoadPath === true: each entry's fixture.before under a ^18 manifest converts to exactly its fixture.after with expectedNotices all from that entry (the single step-18 default flip, flow-decision-mode-inclusive-explicit, comes back by reference), and each fixture.after with the whole step replayed comes back by the same reference with zero notices. Because it iterates the registry, feat(spec)!: retire an email template's category and isSystem — the grouping and the built-in flag nothing read (#22876) #22990's entry and any later step-18 retirement are covered the day they land with no edit here. Judged right. Caveat, which the PR body states and which does not contradict the card: the pins prove "an artifact WITHOUT the retired shape is unchanged". For the component-prop family in step 18 (object-grid-default-filters-removed, object-grid-default-sort-removed, object-kanban-quick-add-removed, page-header-breadcrumb-removed, the element-* and record:* entries and their siblings) the door's strict parse does not refuse the old key, because PageComponent.properties is a z.record carrier — so a ^18 artifact built AFTER such a retirement can still carry the inert key, and the replay now rewrites it to the canonical spelling (for defaultFilters: moved into an empty filter, deleted beside a populated one). That is a change to a post-retirement artifact, but only of a key no renderer reads, in the rescue direction; the chain already does the same to that key for every ^17.x floor and every stored row, and DEFAULT_FLIPS_NOT_REPLAYED_HERE is unchanged, so no reinterpretation class enters. The card's "replaying an entry over an artifact without the retired key changes nothing" holds.

  3. The new verdict and replayedRetirements. 'converted-major-step' is chosen whenever majorStep is non-empty, else 'converted-retired-after'. replayedRetirements lists only the per-entry half's entries (empty under the new verdict on this tree, pinned toEqual(openedByRule('18.0.0'))), so its invariant — each row carries a retiredAfter the floor is at or below — holds. In _convertArtifactForward a major-step replay therefore takes the second log form, which prescribes rebuilding with current tooling — right for a prerelease build. Judged right.

  4. GA behaviour. At label 18.0.0 the floor equals the label, the else branch is reached, step 18 replays and the verdict is 'converted-major-step'; from 18.0.1 the floor is below the label and the verdict is 'converted-forward'. Pinned at labels 17.7.0, 18.0.0-next.0 and 18.0.0, and at 18.0.1 (parseVersion tolerates the prerelease tail). Judged right. Noted, not a defect: from this PR on every ^18 artifact runs the 65-entry replay at boot where 'authored-current' previously skipped applyConversions; the dev's corroboration over the three example stacks found 0 notices and the same reference.

  5. The protocol-18 handshake is untouched. protocol-handshake.ts is not in the file list; checkProtocolCompat still reads PROTOCOL_VERSION (18.0.0 on origin/main) through rangeAdmitsMajor, so ^17 is refused with OS_PROTOCOL_INCOMPATIBLE and objectstack migrate meta --from 17 before the door runs, and ^18 is admitted. The CONTROL pin passes '18.0.0' explicitly, so it pins the function rather than the constant — acceptable, since the diff touches nothing in packages/spec.

  6. Forward conversion never opens on unreleased main: spec still labelled 17.4.0 while main refuses 17.5.0 retirements, so artifacts built by the published 17.4.0 CLI are refused #20390's 17.x-floor path is unchanged. For a floor that is not M.0.0 the new else if never fires, so ^17.7.0 on a 17.7.0 label answers as before ('converted-retired-after' when openedByRule is non-empty, else 'authored-current'), and permission-allow-restore-purge-removed (stamped 17.2.0) is not replayed for it; pinned as CONTROL, and the existing per-entry block is untouched in the diff. Judged right.

  7. The rewritten existing pin. REFUSES the amnesty for an artifact authored at a NEWER spec than the runtime modelled "newer" as ^18.0.0 on a 17.2.0 label; on this tree that input is the card's defect, so the pin pinned the defect. It now uses currentSurfaceFloor('17.2.0') (the maximum of the label and every retiredAfter, patch plus one; 17.7.1 today), guards that it does not end in .0.0, and keeps the 'authored-current' and same-reference assertions. Forced by the design, not a weakening. Judged right.

  8. The packages/metadata surface breach. FORWARD_WINDOW_OPENED is a total Record over ArtifactForwardConversionVerdict, so the new member does not compile without a row; the open side keeps the table's stated invariant (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 bare-root form-predicate notice reads the same window as the replay). Consequence: that warn-only notice now also fires for a ^18 artifact carrying bare-root predicates, as it already does for every ^17.0.0-stamped app on a 17.7.0 runtime — no new class. artifactWindowClause gains a branch because both existing clauses print a false sentence for a floor above the label. The new test file pins the card's "named in the boot log" criterion on the real MetadataPlugin._parseAndRegisterArtifact. All three are in the domain:engine lane; the claim's "stop on a breach and explain it" was followed in the report and the seat accepted it. Judged right.

  9. Could a ^18 artifact that boots now carry anything the door used to refuse besides retired keys? No. The replay set is exactly the retired step-18 entries minus the refused flips, and each rewrites only the old shape it positively recognises — retired keys and retired value shapes (a bare unique: true, a string sort clause, a UTC-suffixed time default, a string-list manifest.permissions). Everything else still meets ObjectStackDefinitionSchema and EnvironmentArtifactSchema strictly. The runtime's second door (AppPlugin, the security bundle) consumes the same function and reads verdict only into a debug log; no exhaustive switch over the union exists in-tree outside FORWARD_WINDOW_OPENED.

② Semver level

  • @objectstack/metadata-core: minor, Clause-②: yes — the true value. ArtifactForwardConversionVerdict is exported from the package root (export * from './artifact-forward-conversion.js'), a consumer can now receive a value it could not before, and a TS exhaustive switch gains an arm: a public-surface enlargement, so yes, and yes takes at least minor. The precedent is the same shape: PR feat(spec,metadata-core)!: every retired ADR-0087 conversion carries retiredAfter; the artifact door opens its window per entry (#20390) #20435's changeset (e956924e17) reads '@objectstack/metadata-core': minor, '@objectstack/metadata': patch, Clause-②: yes, with the identical consumer sentence; its BREAKING banner and adr-0087: not-required marker belonged to the MetadataConversion type change in packages/spec, which this PR has no counterpart of.
  • @objectstack/metadata: patch — the true value. No exported surface changes; the table row and clause are internal, and the behaviour change is a boot-log clause plus a warn that now also reaches ^18 artifacts.
  • The PR body's second line, the changeset body and the corrected claim all read Clause-②: yes; Check Changeset is green on this head. Under .changeset/pre.json (mode: pre, tag next) the published label does not move with the level, which changes what ships, not what the declaration must say.
  • ADR-0087 markers. The changeset disposition marker is NOT owed: no (narrowing) arm, no BREAKING banner, and the dev's check-adr-0087-registration run reports no declared-breaking changeset. An ADR-0087 amendment note IS owed as a follow-up: the 2026-09-13 addendum's window bullet (as amended 2026-09-30) states the rule as two disjuncts and its closing paragraph says the window "is for within-line retirements"; both sentences are now narrower than the code. The ADR names the module docblock as the rule's authority, the docblock carries the third disjunct and the M2 caveat, and docs/adr/** is Tier H, so the note cannot ride this PR without making it Tier H — the precedent carried Forward conversion never opens on unreleased main: spec still labelled 17.4.0 while main refuses 17.5.0 retirements, so artifacts built by the published 17.4.0 CLI are refused #20390's note in a separate governed docs PR (4cc5bcd858, docs(governed): the retirement route is retiredKey() on any shape, with the retiredAfter stamp and the step-18 registry shape #20797) two days after the code landed. Escalated in ③; not a blocker.

③ Boundary flags

Round-0 report (6122615428):

Round-1 report (6122690936):

  • deviations[0] the PR body's second line read no beside a minor changeset until the seat's edit — resolved: the body reads Clause-②: yes at the final read and Check Changeset concluded success on this head.

Escalations for the seat (none blocks the verdict):

Check-runs on the head at the final read — green: Build Core, Check Changeset, Governed Surface Queue Guard, Dogfood Regression Gate (2/3), Dogfood Verify CLI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Check PR Size, Auto Label, Check Documentation Links, Flag docs affected by code changes, the claim, card and single-writer-path guards, filter; skipped: Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in); still pending: Lint & Repo Gates, Test Core (1/6) through Test Core (6/6), Dogfood Regression Gate (1/3) and (3/3), Temporal Conformance (live PG + MySQL), Type Check · workspace. Nothing red. The Tier S landing needs every check green beside this record, so the pending set is the seat's remaining wait; no monitor was started here. PR state at the final read: draft, auto-merge unarmed, head repo equals base repo, 474 changed lines, no governed path in the file list.

Implemented-by: claude/issue-23001-prerelease-artifact-window
Reviewed-by: session_01ADzJtzYTLUfgrRZHxkagkX

VERDICT: PASS

Adopted by domain:engine#2 (session_01ADzJtzYTLUfgrRZHxkagkX) at 2026-10-12T11:14Z. The record came from an isolated at-tier reviewer. Its transcript shows read-only tool use: REST reads, a fetch into a review ref and repository reads, and its only write was its own scratch record. The checks pending at the record's final read have since concluded: on this head, 34 runs, 31 success, 3 skipped, 0 failed. The ADR-0087 amendment note the record escalates is filed at landing, when the window paragraph becomes narrower than main. It is a Tier H docs edit for the maintainer's hand, like #20797 for #20390.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 12, 2026 11:15
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 12, 2026 11:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit 58f5c90 Oct 12, 2026
35 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23001-prerelease-artifact-window branch October 12, 2026 11:54
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/m tests tooling

Projects

None yet

2 participants