Skip to content

refactor(spec): major 18's conversions as identifier-sorted entries with an explicit application order, so two retirements merge clean (#20574) - #20685

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-20574-conversions-merge-clean
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-20574-conversions-merge-clean

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20574
Clause-②: no

Every major-18 retirement that carries a D2 conversion touched two tails of packages/spec/src/conversions/registry.ts: it appended its conversion to the end of CONVERSIONS_BY_MAJOR[18], and most of them defined the conversion at the end of the definitions, directly above that table. So any two such retirement PRs in flight conflicted in GitHub's driver-free merge. This PR reshapes both spots so that two retirements no longer insert into the same gap. Nothing a consumer reads changes: the list the loader replays is value-identical.

This is the sibling of #20535 (PR #20572), which reshaped step18.rationale in migrations/registry.ts. It uses the same instrument.

What changed

  • CONVERSIONS_BY_MAJOR[18] is inApplicationOrder(MAJOR_18_CONVERSIONS). MAJOR_18_CONVERSIONS holds one { conversion, order } entry per conversion. The list is kept sorted by the conversion's identifier (code-unit order). order is the application order: inApplicationOrder sorts by ascending order, ties broken by the conversion's id, and returns the plain readonly MetadataConversion[] it always was. Orders 1 to 46 are the 46 entries' old positions, so the replayed sequence is unchanged.
  • The definitions' insertion point, the other shared tail. I measured it. On origin/main 9a4b2bb38f, 57 of the last 80 first-parent commits that touched this file added a conversion definition. Together they added 66 definitions, and 42 of them were added after every other definition, in that one gap. The list's doc comment now says where a new definition goes: directly above the definition of the entry that follows it in MAJOR_18_CONVERSIONS. A conversion that sorts last goes after every other conversion. Two retirements in different list gaps therefore also define their conversions in different gaps. No existing definition moved.
  • OrderedConversion and inApplicationOrder are module-private, and CONVERSIONS_BY_MAJOR keeps its explicit type annotation. api-surface/ and export-origins/ are unchanged, and check:api-surface is green. The released majors (11 to 17) stay plain arrays. The next major takes the same shape when it opens.
  • Consumers are untouched. ALL_CONVERSIONS, step 18's conversionIds (CONVERSIONS_BY_MAJOR[18]!.map((c) => c.id), pinned verbatim by the refactor(spec): step 18 rationale as key-sorted fragments, conversionIds derived, so two retirements merge clean #20572 test), spec-changes.ts and build-upgrade-guide.ts all read CONVERSIONS_BY_MAJOR[18] as before. No generated projection moved: check:generated reports 15 of 15 up to date and regenerated nothing.

Why sorted by identifier, with an explicit order

Git reports a conflict whenever two branches insert into the same gap between unchanged lines, whatever they insert. A list appended at its end is one gap. An explicit order alone would not change that: the end-append lit control below conflicts with the order key present. Kept sorted by identifier, two retirements insert into different gaps, and one existing entry between them is enough. The application order cannot depend on where an insertion lands, so it lives in order. Two PRs in flight may both take the next number. They then apply in id order, which is deterministic whatever the textual positions are.

Verification record

Lit controls on the parent (c6b37cd08d)

This was a one-shot run with git 2.43.0 in a scratch repo that held the real file, with no attributes and no driver. Each side made the edit a major-18 retirement makes:

pair git merge-tree --write-tree
two list-tail appends (a new identifier after viewListTabsRemoved,) exit 1, CONFLICT (content) in registry.ts
two definitions added directly above export const CONVERSIONS_BY_MAJOR exit 1, CONFLICT (content)
both edits on each side exit 1, CONFLICT (content)

Dark control and the permanent pin

The pin is packages/spec/scripts/conversions-major18-merge.test.ts. It is in the repo project beside step18-rationale-merge.test.ts, and it works against the REAL file. It is a test, not a new gate. It asserts:

  • The premise. CONVERSIONS_BY_MAJOR wires 18: inApplicationOrder(MAJOR_18_CONVERSIONS), with no 18: [ array. The entries are strictly sorted by identifier. Each entry names a major-18 conversion defined in the file, and the list is all of them. order is positive, and the application order parsed from the entries (by order, ties by id) equals CONVERSIONS_BY_MAJOR[18], the tail of ALL_CONVERSIONS, and MIGRATIONS_BY_MAJOR[18].conversionIds. The anti-vacuity check: that order differs from key order. A conversion added after this change must be defined directly above its list successor. The 46 entries that predate the rule are exempt, and that set is closed: its size is pinned.

  • The card's reproduction, now clean. Two retirement-shaped edits, each adding a definition above its successor's and an entry where its identifier sorts, one existing entry apart, both taking the same next order: exit 0. The merged bytes equal both edits applied together. The merged file still satisfies both rules, and its application order is the old 46 followed by the two new ids in id order.

  • Lit controls, all exit 1 with conflicted path registry.ts:

    • two identifiers in the same gap, the residue a key sort cannot remove;
    • the same two entries appended at the list's END;
    • the same two conversions defined after every other conversion;
    • a synthetic model of the old array-plus-tail shape.

    The pin also proves that its two rule checks fire. The end-appended side breaks the sort rule, and the tail-defined side breaks the placement rule with the expected message.

Byte-identical replay

value parent c6b37cd08d after the restructure
CONVERSIONS_BY_MAJOR[18] ids 46, sha256 9e9666c56a5e6314… identical
majors 11, 13, 14, 15, 17 ids 4 / 3 / 1 / 2 / 57 identical, each
ALL_CONVERSIONS ids 113, c0dee67fbecf705e… identical
every conversion's body (JSON with function source) fbba8d0066fb121c… identical
MIGRATIONS_BY_MAJOR[18].conversionIds 9e9666c56a5e6314… identical
the whole MIGRATIONS_BY_MAJOR value as JSON 72f0c698a5bfcc09… identical

The two fingerprint files, from tsx over src at c6b37cd08d and at ed1c54db8d (the restructure commit), are byte-identical.

After merging origin/main 9a4b2bb38f, which carries #20305's change to one conversion's body, I compared head 63af2cb5f6 with main's plain arrays, read from main's source text. Every major's id sequence, ALL_CONVERSIONS (113) and step 18's conversionIds are identical. The built CJS and ESM bundles (dist/index.{js,mjs} and dist/browser/index.{js,mjs}) all load with the same id hashes. This branch's delta to registry.ts against main has the same git patch-id --stable as the restructure commit's own diff.

Ablations (one-shot, at d316f458a6, registry blob f9d797be1e80)

Each ablation went through scripts/ablation-replace.mjs, with the anchor proven to hit (1 → 0). Each was restored to the HEAD blob with git diff HEAD empty.

  1. Replacing the order sort in inApplicationOrder with .slice() (apply in list position) turned the pin red: 2 failed, 10 passed. The two failures were the application-order assertion and the merged-order assertion.
  2. Swapping the first two entries turned the pin red: 3 failed, 9 passed. The failures were the sortedness assertion and the two assertions that need a sorted base.

The observed direction was the usual one: the pin turns red. No build or dist leg was needed, because the pin imports ../src directly and reads the source text.

Tests and gates (final commit 63af2cb5f6)

  • @objectstack/spec local project: 575 files, 16,946 passed, 1 todo. repo project: 44 files, 773 passed. pnpm --filter @objectstack/spec typecheck: exit 0. That covers tsc, check:scripts-typecheck (the pin is in that program) and check:test-typecheck. All ran through os-verify-lock with --maxWorkers=2 on a shared box.
  • check:generated: 15 of 15 up to date, against the dist built at this head. check:migration-registry: exit 0.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands was reconciled with --ran: 84 derived, 80 exit 0, 4 NOT MEASURED, 0 unrun. The 4 exited 3 with PREREQUISITE NOT MET because they need the whole-repo build closure, which CI builds: check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt.
  • The first gate round, at b4f0179259, caught check:comment-mask-adoption red. It flagged the pin's own line-comment regex as a private comment stripper. Commit 0000e4ab13 fixed it: the pin now reads the list and the definitions through scripts/js-comment-mask.mjs's maskComments.
  • Declared to CI:
    • the CLI integration tier (this diff touches no CLI file);
    • the tests in other packages that read ALL_CONVERSIONS (metadata-core, metadata-protocol, metadata, service-automation). The public surface is byte-unchanged, and the values are proven identical above.
    • Repo-level pnpm lint is CI-owned.
  • Changeset: @objectstack/spec patch. I measured this rather than assumed it. After the build, inApplicationOrder appears in 6 files under dist/ and MAJOR_18_CONVERSIONS in 8, with the positive control view-list-tabs-removed also present. So the published bundle moves, while no value it exports does.

How a retirement adds its conversion once this lands

  1. Define the conversion directly above the definition of the entry that will follow it in MAJOR_18_CONVERSIONS. If it sorts last, define it after every other conversion, directly above OrderedConversion.
  2. Add ONE line, { conversion: IDENT, order: N }, where IDENT sorts. Never add it at the end.
  3. N is one more than the highest present. A conversion that must apply before an existing one takes a number between its neighbours' (for example 23.5) instead of renumbering them.

A branch cut before this lands meets the change once, on its next base merge: its appended 18: [ line becomes one entry at its sorted position.

Acceptance notes


Generated by Claude Code

…ith an explicit application order

CONVERSIONS_BY_MAJOR[18] was an end-appended array: every major-18
retirement with a D2 conversion inserted into its one tail gap, so any
two in flight conflicted in GitHub's driver-free merge. It is now
inApplicationOrder(MAJOR_18_CONVERSIONS): one { conversion, order }
entry per conversion, kept sorted by identifier, applied by ascending
order (ties by conversion id). Orders 1..46 are the old positions, so
the replayed sequence is unchanged.

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

A repo-project test that parses MAJOR_18_CONVERSIONS from the real
registry, holds it sorted by identifier, holds a new conversion's
definition directly above its list successor's, checks the replayed
order against CONVERSIONS_BY_MAJOR[18], ALL_CONVERSIONS and step 18's
conversionIds, and merges two retirement-shaped edits of the real file
with git merge-tree (exit 0), beside four lit controls that must still
conflict: the same gap, the list's end, the definitions' end, and the
old array shape.

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
check:comment-mask-adoption flagged the pin's own line-comment regex as
a private comment stripper. The list and the definitions are now read
through scripts/js-comment-mask.mjs maskComments, which keeps offsets,
so a comment between entries is a blank line to the parser.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/vitest.repo-tests.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/vitest.repo-tests.json) — pages documenting those are invisible to this run
  • 2 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 — 137 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 6981abfd26cd518174db432766580e37b29a6566 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 6981abfd26cd518174db432766580e37b29a6566

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 63af2cb5f62051dd433904eeff8aac01bf947620
Local-runs: none

Inputs: card #20574 (body, triage 5884668731, claim 5891509188, dev report 5895025812), #20535 and PR #20572 (the worked example), PR #20685 (body, its one comment, the four-file list, the diff against main at the merge base 9a4b2bb38f), and the head's check-runs, read once at 2026-09-29T17:20:26Z. Everything below is judged from the diff text and the pin's source, not from the dev's prose. The replay comparison is the parent's 18: [ array read from origin/main against the head's entries ordered by order, both read from git objects; nothing was built, run or re-run.

① Derived judgments

  • Application order: right. The parent's 18: [ array (46 identifiers) and MAJOR_18_CONVERSIONS sorted by ascending order are the same 46 identifiers in the same sequence. The orders are exactly the integers 1 to 46, each used once, so the id tie-break is never exercised by the existing entries and the order did not move. The identifiers are strictly sorted in code-unit order, as the doc comment says and the pin checks. The diff has two hunks in registry.ts: one inserts OrderedConversion, inApplicationOrder, the entries list and a doc comment on CONVERSIONS_BY_MAJOR; the other replaces the 18: [ array with 18: inApplicationOrder(MAJOR_18_CONVERSIONS). No conversion id, body or definition moved: all 113 definitions are untouched, and the last one, flowDecisionModeInclusiveExplicit, still precedes the new declarations.
  • Consumers: right. CONVERSIONS_BY_MAJOR[18] keeps its explicit readonly MetadataConversion[] annotation and is a plain array computed at module load, so every reader gets the old shape and value: ALL_CONVERSIONS (flatMap by ascending major), the loader (apply.ts iterates ALL_CONVERSIONS), step 18's conversionIds (the ids mapped off CONVERSIONS_BY_MAJOR[18], an expression untouched in migrations/registry.ts and pinned verbatim by the refactor(spec): step 18 rationale as key-sorted fragments, conversionIds derived, so two retirements merge clean #20572 test), spec-changes.ts, build-upgrade-guide.ts (prints majors up to 17, so no projection moves), and every test that iterates or .finds over the record. MAJOR_18_CONVERSIONS is declared after every definition and before CONVERSIONS_BY_MAJOR, so no TDZ. Nothing consumes order outside the module.
  • Public surface: right. OrderedConversion and inApplicationOrder are module-private; api-surface/root.json and export-origins/root.json are unchanged; the only declaration delta is the new JSDoc on CONVERSIONS_BY_MAJOR. Type Check · consumer gates, which hosts check:api-surface, concluded green on this head.
  • Non-integer order: right. The comparator (order difference, then id) is a total order over finite numbers with a deterministic tie-break; the pin admits any finite positive number and its entry regex accepts N or N.N only. Determinism does not depend on where an entry lands textually. The deviation from refactor(spec): step 18 rationale as key-sorted fragments, conversionIds derived, so two retirements merge clean #20572's integers-only lets an entry apply before an existing one without renumbering its neighbours, which is a smaller diff for a sibling to merge against. Sound.
  • The definitions' insertion point: right, and inside the claim. The card's body names both spots a retirement edits ("appends its conversion there, and adds its definition at the insertion point above"), and the claim's proof is "two synthetic appends exit 0 after the change". The dev measured that a retirement's whole edit still exits 1 when only the list is reshaped, so answering the second spot is what makes the card's proof true of a real retirement, in the same file and the same CONVERSIONS_BY_MAJOR neighbourhood. The answer is a placement RULE (define a new conversion directly above its list successor's definition), stated in the list's doc comment and enforced by the pin for every entry outside a closed exemption set; no existing code moved. Soundness from the code: two retirements with different list successors define above two different definitions, and a definition spans several lines, so git sees two separate insertions; adjacent list gaps are likewise separated by the one existing entry line, and the pin's pair is exactly the two closest different gaps. Two retirements with the SAME successor are already the same-gap pair in the list, so the rule adds no residue beyond the one pinned as a lit control. The rule stays consistent as entries accumulate: an entry inserted between a non-exempt predecessor and its successor lands directly below the predecessor's definition, so the predecessor's own rule still holds. The pin's placementFindings reports a non-exempt entry whose next definition is not its list successor, the lit control asserts the exact message for a tail-defined side, and the exemption set is the 46 pre-existing identifiers exactly (checked name by name against the entries), with its size pinned at 46 and a never-add-a-name note.
  • The pin: right, a test and not a gate. packages/spec/scripts/conversions-major18-merge.test.ts reads the REAL file through maskComments, parses every byte of the entries list (residue fails), and in a hermetic scratch repo with no attributes and no driver runs git merge-tree --write-tree: the sorted pair exits 0, the merged bytes equal both edits applied together, both rules hold on the merged text, and the merged application order is the old 46 followed by the two new ids in id order (both took the same next order, as two PRs cut from one base do). Four lit controls must exit 1 with registry.ts as the conflicted path: same gap, list end, definitions end, and a synthetic old array-plus-tail shape. A regression to the old shape fails the wiring check (18: inApplicationOrder(...) present and no 18: [), an end-append fails the sortedness check, and a tail definition fails the placement check. It is listed in vitest.repo-tests.json (the repo project), which is also where check:cross-package-test-inputs requires it, since it imports scripts/git-env.mjs and scripts/js-comment-mask.mjs from the repo root in the same spelling as the refactor(spec): step 18 rationale as key-sorted fragments, conversionIds derived, so two retirements merge clean #20572 pin. The file list is four paths: no workflow, no scripts/pm or check:* registry, no package.json changed.
  • Gate coverage on the head, read once at 17:20:26Z: 23 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke: roster skips), 7 in progress, 0 failed. The dev's four NOT MEASURED are all answered green by concluded runs: check:doc-formula-expressions and check:api-surface (Type Check · consumer gates), check:dual-build-cjs-loads and check:lean-entry-closure (Build Core), check:type-check-debt (Type Check · debt ledger). Also green: Check Changeset, Governed Surface Queue Guard, Spec property liveness, Type Check · source gates (hosts check:generated --reconcile-only), Test Core 2/6, the dogfood and temporal gates. Not concluded at read time, and not presumed green: Lint & Repo Gates (pnpm lint, check:migration-registry, check:cross-package-test-inputs, check:comment-mask-adoption, check:issue-citations, check:doc-authoring), Test Core 1/6, 3/6, 4/6, 5/6 and 6/6 (spec's test and test:repo, so the pin's own CI run), and Type Check · workspace (spec typecheck, including check:scripts-typecheck over the pin). The dev reports each of these green locally (spec local 575 files / 16,946 passed; repo 44 files / 773 passed with the pin 12 of 12; typecheck exit 0; 80 of 84 dispatch gates exit 0). Landing waits for every check on this head to conclude green, as it always does.

② Semver level

  • .changeset/20574-major18-conversions-sorted-entries.md: '@objectstack/spec': patch. Right. The diff publishes: the source is bundled, so the released tarball's JS moves (the dev measured inApplicationOrder and MAJOR_18_CONVERSIONS in the built dist), while no exported value, type or name changes. skip-changeset is for a diff that publishes nothing from a released package, which this is not; minor would claim a feature that does not exist. The wording states the invariant a consumer cares about (46 conversions, ALL_CONVERSIONS 113, conversionIds value-identical) and then the authoring rule, the same form as the accepted [finding] migrations/registry.ts: every major-18 retirement PR rewrites the closing line of step18.rationale, so any two in flight conflict in GitHub's merge #20535 changeset. No model identifier in it.
  • Clause-②: no. Right, on the PR body's second line, with no arm. No accept set and no reject set moves: the same conversions apply in the same sequence, and the spec's schemas are untouched. Check Changeset concluded green on this head.

③ Boundary flags

Dev deviations, each answered:

  1. Two spots changed (list entries plus the definition placement rule): answered in ①, inside the claim and the card's stated cost, sound, no existing code moved.
  2. order admits non-integers, unlike refactor(spec): step 18 rationale as key-sorted fragments, conversionIds derived, so two retirements merge clean #20572: answered in ①, sound and deterministic, module-private.
  3. The PR body writes plain Clause-②: no although the claim's line carried a parenthetical: right, the parenthetical was the claim's reasoning and not an arm, and clause2-line.mjs would read one as an arm.
  4. Container restart, readings re-run rather than recalled: process note, no diff consequence; CI answers the same gates.
  5. origin/main merged three times, and main has since moved to 6981abfd26 (six commits): checked, none of the six touches conversions/registry.ts, the pin, the repo-tests list or the changeset; GitHub reports the PR mergeable; the three-dot diff is what was reviewed.
  6. Ablations at d316f458a6, not repeated at the head: acceptable, the last merge changed only spec: retire the inline-row decline in page-component-filter-record-to-rule-array once the objectui pin carries objectui#10767 #20305's conversion-body region, which the ablations do not touch, and the pin imports ../src and reads source text, so no dist leg.
  7. Lock hold and background commands: process note.
  8. Push 503 retried: process note.
  9. Commit trailers are the model-free pair AGENTS.md requires: right, the pre-push hook refuses a model identifier there.

open_questions: none.

Out-of-scope findings, dispositions:

  • SKILL.md registration checklist wording (governed, .claude/skills/spec-property-retirement): routed to [finding] spec-property-retirement SKILL.md tells a step-18 author to append to conversionIds and extend the rationale string: both change shape when PR #20572 lands #20575, which already asks to fold this sibling's wording in. Right not to edit it here.
  • The import block of conversions/registry.ts as a third shared spot (8 of 80 commits): leaving it is within the claim, whose surface is CONVERSIONS_BY_MAJOR and its consumers, and the card names the list tail and the definition spot. ESCALATED to the seat as a filing decision, not a defect of this diff: it has a named landing site and a measured reach, so it meets the finding gate's shape if the seat judges the residue worth a card.
  • The pure-schema-construction tsup plugin rewriting strictObject( inside a string literal of one step-18 D3 entry (dashboard-widget-stage-order-non-funnel-refused), so dist text differs from src: unrelated to this diff. ESCALATED to the seat for filing (class a, reach: release text).

No new gate: nothing in the diff adds a workflow step or a check:* script; the two rule checks are assertions in a repo-project test.

Implemented-by: claude/issue-20574-conversions-merge-clean
Reviewed-by: session_014EJ1ED8X4MMrT18BhVx4tx

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 17:38
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit f379f57 Sep 29, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20574-conversions-merge-clean branch September 29, 2026 18:13
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/l tests tooling

Projects

None yet

2 participants