Skip to content

feat(spec,cli): a structured relevance question takes a semantic notice off os migrate meta's default list (counted; --all lists it) - #22115

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-22072-migrate-meta-relevance-predicate
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-22072-migrate-meta-relevance-predicate

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #22072

Clause-②: yes

Patch round 1 (head 5104a5d74) answers the contract review FAIL 6044604409 with the seat's REWORK 6044626135: (1) four entries whose surface also names a code door lose their question, so the batch is 25; (2) disposition B: todos stays whole and absentTodos names the proven-absent subset; (3) a declared tiers answers unknown, like plugins / devPlugins; (4) --all is listed in content/docs/upgrading.mdx. This body was updated by the seat from the dev's report 6045718354.

Merge round (head 1d6efd229): the contract review 6045970836 PASSed 5104a5d74. Since then, the branch has merged origin/main db4c45b8c in merge commit e74764dbb (parents 5104a5d74, db4c45b8c). That brings in main's os migrate meta --write and eight new step-18 entries.

  • Conflicts: two, resolved by stacking both intents. In meta.ts, the --all and --write flags are now both declared. In upgrading.mdx, the flags table carries the --all row and main's --out and --write rows.
  • Catalogue: 391 entries (77 + 314).
  • Changeset: commit 1d6efd229 makes the --step sentence precise, as the review asked.

Merge round 2 (head 16cb55b8d): 1d6efd229 was red on CI once merged with main. Type Check · workspace failed in @objectstack/cli check:test-typecheck, on #22121's new test/migrate-meta-out-snapshot.test.ts.

  • The merge: origin/main 54ace18c6 is merged in merge commit 5fc3a5a2a (parents 1d6efd229, 54ace18c6). It brings fix(cli): os migrate meta --out writes its snapshot on a run with nothing to migrate #22121, which makes os migrate meta --out write its snapshot on a run with nothing to migrate. The merge was a clean auto-merge, with no conflict and no hand edit.
  • The fix: commit 16cb55b8d adds all: false to the MigrationReport literal in that test.
    • The measured error was TS2741: Property 'all' is missing … but required in type 'MigrationReport' at line 274. That is this PR's required --all field on the CLI-internal report type, not absentTodos.
    • No assertion changed.

os migrate meta now has the one exit that #20620's triage (5888087153) allows a notice: a structured proof, derived from the stack, that the notice's surface is absent. An entry that carries a question leaves the default list only when the chain answers absent for this stack. The run counts those entries on one line, and --all lists them in full. ⛔ Nothing matches the prose of surface.

The question: SemanticMigration.relevantWhen (packages/spec/src/migrations/types.ts)

Evaluation (chain.ts)

The answer has three values, and only absent moves an entry (H2, built in):

what the question reads answer
the key is missing, or holds [] or {} absent
a non-empty array or map (the authored map form included) present
any other value: a function, a promise, a scalar, null, or a getter that throws unknown
a stack that is not a plain object, or no stack at all unknown
any entry in plugins / devPlugins / tiers (unless the stack visibly declares the key itself) unknown
an assembled packages[].manifest body read like the top level; an unreadable body is unknown
  • The question is asked of the stack the chain was handed and of every hop's checkpoint. Present in any of them counts as present, and absent needs all of them, so a conversion that renames or removes a key cannot make the key read as absent on either side.
  • An entry whose conversionIds names a conversion that applied an edit in the same run is always listed. The applied edit proves the surface is there.
  • MigrationChainResult and MigrationHopResult gain a required member, absentTodos. todos keeps every semantic entry of every hop crossed, exactly as before; absentTodos names the proven-absent subset of it (the same objects, in chain order).

The output (packages/cli/src/commands/migrate/meta.ts; the cross-lane half, declared on the domain:cli seat post)

  • ③ lists todos minus absentTodos, and its header counts that difference. A new group ④ follows it. It is two lines. The first counts the entries proven absent and names --all. The second says the proof covers the stack this run loaded, and not metadata a deployment stores (Studio, the metadata API).
  • --all prints each of those entries in full, as the same block ③ prints, followed by absent: nothing is declared under and the keys.
  • --json always carries absentTodos, and hops[].absentTodos with --step. --step adds a not listed count to a hop's line when that count is not zero.
  • A run whose only notices are proven absent still counts them in ④ and writes --out.
  • With fix(cli): os migrate meta --out writes its snapshot on a run with nothing to migrate #22121's --out on both exits, the early return is guarded by the original condition applied.length === 0 && todos.length === 0.
    • Because todos is whole, a run whose only notices are proven absent never takes that branch. It goes down the main path: ④, then the one writeStackSnapshot, then --write.

    • Measured in-process on the merged code, with and without --all. Every arm writes the snapshot exactly once and names it on exactly one line:

      arm exit taken ④ printed
      the real hop-18 run (314 todos, 20 absent) main path yes
      only-absent (todos = absentTodos = the 20 questioned) main path yes
      nothing listed and nothing absent early return no
      an empty range early return no
  • With main's --write, its own group (⑤ in the report docstring) prints after ④ and before the data-migration advice.
    • --write reads only the chain's applied, stack and the normalized input. It never reads todos or absentTodos, and neither of them changes what it writes.
    • Measured on a probe project with one dashboard refreshInterval and no cubes (--from 17):
      • The dry run and --write --json report identical todos (314) and absentTodos (17).
      • The written sites equal applied (1 of 1).
      • The idempotent re-run applies 0 and still names the same 17.
      • --write --all lists ④ in full, then prints ⑤ (Wrote 1 of 1 …).
    • A run with no applied edit prints --write's "no mechanical change" line in either branch.

hotcrm's default report now ends like this:

  302 manual change(s) require your judgment:
    …
  12 more manual change(s) not listed: their surfaces are absent from this stack (run with --all to list them).
    Absent is proven over the stack this run loaded; metadata a deployment stores (Studio, the metadata API) is not read here.

The first batch: 25 of the 391 entries carry a question

keys the question names protocol 17 protocol 18 entries
analyticsCubes 0 8 cube-join-sql-and-relationship-retired, cube-member-inner-name-retired, cube-member-sql-expression-retired, cube-metric-expression-types-retired, cube-metric-filters-retired, cube-refresh-key-retired, analytics-cube-public-default-visible-enforced, analytics-cube-single-granularity-default-enforced
dashboards 1 2 dashboard-widget-compareto-offset, dashboard-refresh-interval-unit-in-key, dashboard-header-modal-target-page-only
dashboards / reports / pages 0 1 chart-config-aria-retired
permissions 0 1 permission-restore-purge-bits-retired
sharingRules 1 0 sharing-rule-recipient-reconcile
apis 1 1 declarative-apis-endpoints-live, api-endpoint-cache-ttl-unit-in-key
jobs 1 1 job-retry-policy-constraints-tightened, job-timeout-unit-in-key
agents 0 2 agent-memory-store-retired-and-limits-required, agent-structured-output-refused-members-retired
datasets 0 2 dataset-measure-aggregate-field-type-refused, dataset-measure-selecting-aggregate-field-type-refused
mappings 0 1 mapping-lookup-params-retired
hooks 0 1 hook-timeout-unit-in-key
tools 1 0 tool-requires-confirmation-retired
total 5 of 77 20 of 314 25 of 391

semantic-relevance.test.ts pins this table, so every addition is a reviewed edit. I read each entry against three rules:

  1. Only the named keys. The surface lives only under the named top-level key(s). I checked this against the schema: in a stack, the item schema is referenced only by those collections. ReportSchema is also carried inline in an object-metric drill-down, so the chart-config entry names pages too.
  2. No runtime door. The surface names no runtime request body, no client, engine or driver API, no kernel or plugin config, and no platform-shipped item.
  3. No stored row. The acceptance criteria send the author to no stored sys_metadata row and no runtime door.

Some families are left out on purpose:

  • Rule 3: flows (their entries send the author to stored flow rows), the joined-report entries, dashboard widget arity, stage order and chart structure, RLS tags, check on select/delete, the cross-class comparison, and cel-predicate-list-comparand-refused.
  • Rule 2: the analytics row wildcard and the dataset member expression (both are judged on inline datasets in query bodies too); and cel-predicate-one-value-comparand-refused, cel-predicate-variable-root-comparand-refused, rls-predicate-array-comparand-refused and rls-predicate-stored-list-ordering-refused: their surface also names a code door, a direct matchesFilterCondition / compiler caller (patch round 1).
  • Rule 1:
    • connectors: a connector document also reaches the runtime through engine.registerConnector from code;
    • datasources: driver configs are built in code;
    • the hook-body stored-metadata target: it applies to stored hook rows.
  • Main's eight new step-18 entries (merge round) carry no question:
    • flow-edge-unresolved-or-repeated-refused falls under rule 3: its acceptance criteria send the author to stored flow rows in sys_metadata.
    • The seven sys_flow_dispatch / sys_job / sys_job_queue / sys_job_run / sys_migration_journal / sys_migration / sys_presence organization_id retirements fall under rule 2: each surface is a platform-shipped table, not a stack key.

Measured: listed count before and after

"Before" is todos, which is whole again (disposition B). "After" is todos minus absentTodos, which is what ③ lists. I read both from --json of this branch's CLI at the merge head e74764dbb. absentTodos was a subset of todos on every row.

stack --from before after left the list
hotcrm c967803 17 314 302 12
hotcrm c967803 16 391 376 15
examples/app-crm 17 314 301 13
examples/app-todo 17 314 300 14
examples/app-multi-package (assembled packages[]) 17 314 294 20
examples/app-showcase (lists runtime plugins) 17 314 314 0 (H2: a listed plugin makes every answer unknown)
a minimal stack with one object 17 314 294 20
a minimal stack with one object 16 391 366 25
  • hotcrm is a read-only clone at c967803, the commit the card measured, loaded with this branch's CLI and spec.
    • At the card's time it reproduced the baseline exactly: 304 manual notices and 12 applied edits.
    • The base is 314 now because main has added ten step-18 entries since. The 12 entries that leave the list, and the 12 applied edits, are unchanged.
    • hotcrm's --step hop line reads 12 mechanical, 302 manual, 12 not listed (surface absent).
  • The ceiling: most of the remaining notices govern surfaces that are not stack keys at all (kernel and plugin configs, client, engine and driver APIs, exported types). No stack-declares question can reach them, and each needs a different kind of question.

For the contract review

  • todos membership — disposition B (patch round 1). applyMetaMigrations().todos, MigrationHopResult.todos and --json's todos are unchanged: every semantic entry, as their TSDoc promised. absentTodos is a new required member of MigrationChainResult and MigrationHopResult naming the proven-absent subset. migrations.test.ts's original equality pin is restored, with a subset pin beside it. Clause-②: yes, minor, no BREAKING line.
  • What the proof covers. The proof is about the definition this run loads. Rows a deployment stores (Studio, the metadata API, AI-authored rows) are a separate subject. os migrate meta --stored reports D2 conversion TODOs per row and never reports the D3 catalogue. The printed scope line states this boundary.
  • Internal exports: two functions and one type. chain.ts exports the functions semanticRelevanceVerdict and semanticTodoAbsent and the type SemanticRelevanceVerdict for its own tests only. None of them is re-exported from @objectstack/spec/migrations; the api-surface/migrations.json shard gains only the three published types.

Verification (head 16cb55b8d; the merge 5fc3a5a2a with 54ace18c6)

  • build: the turbo build of the CLI closure, the CLI, the showcase closure and client-react gave 61 tasks, all successful.
  • The --out / report pins against the merged code: test/migrate-meta-out-snapshot.test.ts (fix(cli): os migrate meta --out writes its snapshot on a run with nothing to migrate #22121, whole file), src/commands/migrate/meta.report-order.test.ts (ORDER, SET, PAIR, ABSENT, including 'a run whose only notices are proven absent still counts them and still writes --out') and test/migrate-meta-write.test.ts gave Test Files 3 passed (3), Tests 52 passed (52).
  • spec, the 58 test files that read the registry or the chain: Test Files 58 passed (58), Tests 2331 passed (2331). That covers the ENUMERATION (25: 5 + 20), SHAPE, EVALUATION and CHAIN pins, and migrations.test.ts's equality and subset pins.
  • typecheck:
    • pnpm --filter @objectstack/spec typecheck exit 0.
    • pnpm --filter @objectstack/cli typecheck exit 0, check:test-typecheck included: OK — … 3 file(s) / 28 error(s) / 6 pinned signature(s) held in test-typecheck-debt.json.
    • tsc -p tsconfig.test.json gave 29 errors before the fix (the 28 the ledger holds, plus the new TS2741) and 28 after. The ledger is not touched.
  • cli unit tier: Test Files 263 passed (263), Tests 3874 passed (3874).
  • cli integration tier: test/migrate-meta-engine-guidance.test.ts (it passes --all) and test/migrate-meta-default-range.test.ts gave Test Files 2 passed (2), Tests 10 passed | 1 skipped (11).
  • Generated artifacts: check:migration-registry reports 391 semantic; fix(cli): os migrate meta --out writes its snapshot on a run with nothing to migrate #22121 adds no entry. registry.ts carries 25 relevantWhen lines.
  • Gates: dispatch-gates --commands derived 117 families at 16cb55b8d. 116 exited 0. 1 is NOT MEASURED: check:dual-build-cjs-loads exited 3, PREREQUISITE NOT MET.
    • check:vendor-version-stamps first exited 1 on ENOENT for a temp file the concurrently running CLI unit tier created under packages/cli/tmp/. Re-run alone, it exited 0.
    • --ran accounted for all 117.
  • ESLint, narrowed to the 35 changed TS files against the merge base 54ace18c6: 35 files, 0 errors, 0 warnings. There is no type-aware linting, so no untouched file's verdict can move.

Mechanism hypotheses, measured

  • H1 held. SemanticMigration was at types.ts:38 and conversionIds at :73. The chain mapped every step.semantic entry into todos (chain.ts:102). Since the order and pair work, ③ prints from meta.ts around :439.
  • H2 held and is built in (see the evaluation table). app-showcase measures it: 314 before and 314 after.
  • H3: hotcrm was reachable read-only (it is a public repository), so its before/after is measured rather than NOT MEASURED.
  • H4: the closed form covers the first batch, so no callback form exists.

Acceptance notes

  • A pre-existing defect. os migrate meta --from 18 --to 18 --out FILE covers a range with no step: its human report writes no snapshot and says nothing about it, while --json writes the file. Unchanged here (an empty range has no absentTodos). Filed by the seat as finding(cli): os migrate meta --out FILE over a range with no step exits 0 and writes no snapshot, while the same run with --json writes FILE #22116.
  • Upgrade skill. skills/objectstack-upgrade/SKILL.md (Tier H) loops over --json's todos; under disposition B its reading is unchanged, so no governed edit is owed.
  • Merges. The branch carries three merges of origin/main:
    • de8ccbcfa (with a543e244f, clean);
    • e74764dbb (with db4c45b8c, two conflicts resolved by stacking both intents, see the merge-round note at the top);
    • 5fc3a5a2a (with 54ace18c6, clean, see merge round 2).
    • Every line the branch added to meta.ts and upgrading.mdx survives the merge, and so does every line main added, except two that were changed by hand. Both are comments.
    • The comment on main's --write group is relabelled from ④ to ⑤, because this PR's ④ is the absent group.
    • The report docstring's ④ item now ends with ;, and a ⑤ item follows it for --write.
    • None of main's ten new step-18 entries carries a question, so the enumeration pin is unaffected.
    • The measured before/after table was read at e74764dbb. The catalogue (391) and every relevantWhen are unchanged since, so it still holds.

Generated by Claude Code

claude added 4 commits October 7, 2026 16:32
…luated by the chain

`SemanticMigration.relevantWhen` is an optional, closed question over the
loaded stack (`{ kind: 'stack-declares', keys: [...] }`). `applyMetaMigrations`
evaluates it over the stack it is handed and every hop checkpoint, and an entry
whose surface it proves absent moves from `todos` to the new `absentTodos`
(chain and per hop). Unreadable values, opaque plugins and an applied edit the
entry judges all keep the entry listed. A first batch of 29 entries whose
surface lives only under named top-level stack keys carries the question.

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

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/spec, touching 29 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/migrations.json, packages/spec/export-origins/migrations.json, packages/spec/src/migrations/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 54ace18c669a776c7e849708039c7876ac534cee.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/migrations.json, packages/spec/export-origins/migrations.json, packages/spec/src/migrations/index.ts) — 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 — 145 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 54ace18c669a776c7e849708039c7876ac534cee → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 54ace18c669a776c7e849708039c7876ac534cee

⚠️ 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 54ace18c669a776c7e849708039c7876ac534cee → 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: 1904d535078bd530b4c32af812a01bf909ce7aac
Local-runs: none

Isolated at-tier reviewer, dispatched from the adopting seat; read-only. Inputs, and nothing else: card #22072 (body and its 4 comments: triage 6038524437, claim 6041997447, dev report 6044219249, the seat's ACCEPT 6044275269, the last treated as a claim to test), #20620's ruling 5888087153, PR #22115 (body, 41-file list, the net diff against main), and the check-runs on the head. Files were read from git objects at the head. Record written at 2026-10-07T18:49Z.

Check-runs on the head. First reading at 2026-10-07T18:34Z: 36 runs, 16 success, 6 skipped, 14 in_progress. Second reading at 2026-10-07T18:44Z: 38 runs, 30 success, 6 skipped, 2 in_progress (Test Core (1/6), Test Core (4/6)). No run concluded other than success or skipped at either reading; in_progress is recorded as what it is, not as a pass. Landing waits for every check green on the landing head whatever this record says.

① Derived judgments

The accept-set and public-surface changes the diff implies, each judged.

  1. Three new exported types — right. SemanticRelevanceKey is Exclude of StackDefinitionKey over the four carrier keys, so a typo or a retired key fails to compile; StackDeclaresRelevance is { kind: 'stack-declares', keys: non-empty readonly tuple }; SemanticRelevance is a one-member closed union. index.ts re-exports exactly the three as types; api-surface/migrations.json and export-origins/migrations.json gain exactly those three rows; semanticRelevanceVerdict / semanticTodoAbsent / SemanticRelevanceVerdict are exported from chain.ts only and reach no published entry (verified against the regenerated shard).
  2. SemanticMigration.relevantWhen (optional) — right, and it is closed and structured as the ruling requires. The shape pin holds Object.keys(relevantWhen) equal to ['keys', 'kind']; there is no callback member and no free-text member. chain.ts never reads surface: stackDeclaresVerdict reads stack[key], packages[].manifest[key], plugins and devPlugins, nothing else — so "never removed by prose-matching surface" holds by construction, not by discipline.
  3. "Unknown keeps the entry listed" — right for every case the evaluation can meet. A non-plain stack (class instance, array, undefined, no checkpoints at all) is unknown; a value that is a function, promise, scalar, null or a throwing getter is unknown (the whole read is wrapped in try/catch); packages that is not an array, an entry that is not a plain dict, or a manifest body that is not a plain dict is unknown; any plugins / devPlugins value other than undefined, [] or {} pushes unknown — so a plugin list that is present OR unreadable both block the proof, while a key the stack visibly declares still answers present. absent needs every verdict absent across the input stack and every hop checkpoint (current is reassigned per conversion, so the checkpoints are real successive stacks, and a renamed or removed key reads present on one side). An entry whose conversionIds intersects the chain-wide applied set is listed whatever the question answers (conservative: the set spans all hops). Package bodies are read in the right place: preservePackageEntries emits { manifest: assemblePackageBody(stack) } with the collections INSIDE manifest (AssembledPackageBodySchema = ManifestSchema.extend(collections)), which is where chain.ts looks; examples/app-multi-package's two packages declare only objects and apps, which is why its measured count equals the one-object stack's — consistent, not a misread. Two carriers the evaluation does NOT treat as carriers are recorded as observations, not defects: tiers (a platform plugin-tier preset serve reads; the CLI's --preset flag is the same door and is not in the stack at all) and onEnable (a code door with host context). Platform-shipped metadata is not the author's upgrade work and the printed scope line bounds the proof, so the plugin rule is a conservative heuristic rather than a completeness claim; the seat may add tiers to the carrier set for symmetry.
  4. Partition and order — right. todos and absentTodos are built after all hops run, in step order then step.semantic order, so each array keeps chain order and their union is every entry of every hop crossed, once; pinned at chain and hop level, and migrations.test.ts's old equality pin was rewritten to the union (relevant to ② below).
  5. The 29 first-batch entries — 25 right, 4 wrong. Each entry's surface, acceptanceCriteria and conversionIds were read at the head and the item schema's references were traced at the head:
    • analyticsCubes (8): CubeSchema is referenced only by analyticsCubes and the stored type analytics_cube; the six cube-* retirements name analyticsCubes[] paths. The two enforcement entries (public default, single-granularity default) are judged on authored cubes; the dataset compiler's minted cubes were already under the compiled-dataset rule per the entry's own text, and an os compile artifact of a stack with no cubes carries none. Right.
    • dashboards (3): dashboard.refreshInterval and header.actions[] are dashboard-only; the object-metric page tile's compareTo carries kind only (by reference to the widget's kind enum) and refuses every other key, so the { offset } arm never lived on a page. Right.
    • dashboards / reports / pages (1, chart-config-aria-retired): ReportSchema is carried inline at object-metric.drillDown.report, so naming pages is the correct widening. Right.
    • apis (2), jobs (2), hooks (1), mappings (1), agents (2), tools (1): each item schema is referenced only by its stack collection (and a stored metadata type, which the scope line bounds); the hook-timeout-to-timeout-ms and mapping-lookup-params-removed conversions walk hooks and mappings only; ToolSchema is not carried inline by AgentSchema; the engine's hook-registration contract is a different shape, "NOT mirrored onto the authorable HookSchema". The jobs/hooks acceptance line "no code reads or writes timeout on a definition" is a sweep of the author's own TypeScript, whose definitions still land in the collection or arrive through a plugin (unknown). Right.
    • sharingRules (1, sharing-rule-recipient-reconcile) and permissions (1, permission-restore-purge-bits-retired): rowLevelSecurity is declared only on PermissionSetSchema and condition only on SharingRuleSchema; the acceptance's share verification and governance-document review are conditional on a rule or a bit existing. Right.
    • datasets (2): DatasetMeasureSchema is referenced only by DatasetSchema.measures; the inline Studio draft the analytics service also compiles is Studio territory, which the scope line bounds, and neither entry's surface or acceptance names it. Right.
    • Wrong — cel-predicate-one-value-comparand-refused, cel-predicate-variable-root-comparand-refused, rls-predicate-array-comparand-refused, rls-predicate-stored-list-ordering-refused. Each of these four surface texts names a code door beside the metadata half: "In a filter passed to matchesFilterCondition, also $gt / $gte / $lt / $lte with an array, and $in / $nin with an array member"; "For a caller of the published compiler that binds its own variables, also a variable that resolves to an object"; "together with { field: { $eq: [...] } }, at any depth under $and / $or / $not" (a shape no CEL lowering produces, so it addresses a direct filter caller only); "In a filter passed to matchesFilterCondition, $gt / $gte / $lt / $lte and $between on a field whose value on the record is a list or a plain object, whatever the comparand". matchesFilterCondition is a public export of @objectstack/formula (packages/formula/src/index.ts). A stack with no permissions and no sharingRules can still carry such a filter in a hook body, in onEnable, or in plain app code, and the chain cannot see it. So absent over ['permissions', 'sharingRules'] proves the metadata half absent and says nothing about the rest of the surface; the entry then leaves the default list, and ④'s count line asserts "their surfaces are absent from this stack", which is false for it. That contradicts [finding][devx] os migrate meta --from 17 buries a project's real findings under 240 generic protocol-18 notices, and marks default-flip conversions as "Applied" when the right action is usually no edit #20620's ruling (5888087153: an entry leaves the list ONLY on a structured, stack-derived proof that ITS SURFACE is absent) and the PR's own rule 2 ("the surface names no ... engine or driver API"), which the PR applied to exclude the row wildcard and the member-expression entries for the same reason. Fix: delete relevantWhen from the four entry files, regenerate registry.ts, shrink the enumeration pin to 25 (protocol 18: 20), correct the PR table and the changeset's count, re-measure the before/after table. The remaining permissions entry (permission-restore-purge-bits-retired) and the sharing-rule entry stay.
  6. CLI printed output — right. ④ prints after ③ and only when absentTodos is non-empty; by default one count line naming --all plus the scope line; under --all a header and each entry as the same block ③ prints, followed by absent: nothing is declared under and the keys. ③'s lines are byte-identical (the printNotice refactor is a pure extraction). --json always carries absentTodos, and hops[].absentTodos under --step; --step's hop line gains "N not listed (surface absent)". The "Nothing to migrate" branch now also requires absentTodos empty, so a run whose only notices are proven absent counts them and still writes --out. --all is exclusive: ['stored'], and the MigrationReport.all field is on a CLI-internal type (packages/cli/src/index.ts re-exports no migrate command). All pinned in meta.report-order.test.ts; the integration test passes --all so every family block is still located verbatim. One consequence of item 5: the count line's sentence is true only once every entry carrying a question has a complete proof.
  7. Docs — a gap, non-blocking. content/docs/upgrading.mdx's "Useful flags" table does not list --all, a public flag this diff adds; the file is ungoverned and check:upgrade-guide passed. Owed as a one-row ride-along on the fix-up push or as a follow-up. skills/objectstack-upgrade/SKILL.md (Tier H, untouched) loops over --json's todos at line 318; its reading changes under the shipped partition (see ②) and is unchanged under disposition B there.
  8. No governed path in the 41-file list; head repo is the base repo; registry.ts on origin/main (a543e244f, one commit past the recorded base) has no change under packages/spec/src/migrations/, so the enumeration pin is unaffected at this reading.

② Semver level

What the diff publishes. Additive: three exported types; the optional SemanticMigration.relevantWhen; relevantWhen on the 29 MigrationTodo objects --json emits; the --all flag; absentTodos and hops[].absentTodos in --json. Behavioural: applyMetaMigrations().todos (and MigrationHopResult.todos, and --json's todos) now omits the entries proven absent. Type-level: absentTodos is a REQUIRED member added to the published interfaces MigrationChainResult and MigrationHopResult — additive for a reader, a compile break for any consumer that constructs the type; tolerated for a result type the library alone produces, but it belongs in the changeset's sentence about the partition.

The changeset as shipped (@objectstack/spec minor, @objectstack/cli minor, Clause-②: yes, no arm, no BREAKING carrier, no migration, no ADR-0087 disposition) describes the diff faithfully sentence by sentence; the question is the grade.

Ruling on the open question (todos membership). The pre-change contract of MigrationChainResult.todos was its TSDoc: "Every semantic TODO across all hops — the judgment delegated to the consumer." After the diff that sentence is false on any stack lacking a batch key: todos is a subset and the guarantee has moved to the union. A programmatic consumer that relied on the old guarantee feels it, and this PR carries the proof: migrations.test.ts's pin asserted todos equal to the hop's whole semantic list, broke, and was rewritten to [...todos, ...absentTodos]. That is exactly the migration an external consumer with the same reading would need, and today nothing ships it to them. The direction is therefore not a widening — nothing is accepted that was refused — and not an accept-set narrowing either; it is a narrowing of what an existing published output GUARANTEES, the class the ADR-0087 registration gate's own docblock calls "the change a consumer needs told about, and the change with the weakest carrier". The union-equals-old pin and --json always carrying absentTodos keep every entry reachable (the ruling's "never silence" is honoured), but reachability is a D3 property, not a semver one.

Disposition — either of two satisfies the contract; the state as shipped does not:

  • A′ — keep the partition, declare it. The changeset carries the breaking carrier for the todos change: the arm Clause-②: yes (narrowing) (the gate reads this closed token; the **BREAKING** banner is the other accepted carrier), the FROM → TO migration (result.todos → [...result.todos, ...result.absentTodos], and --json's todos → todos concatenated with absentTodos, for a reader that wants the pre-change set), and the ADR-0087 disposition marker pnpm check:adr-0087-registration asks for. The bump level may stay minor (the launch-window guard pushes breaking changes to minor outside pre-mode); the carrier is the arm and the migration text, not the level.
  • B — keep todos complete, add the partition under new names. todos and hops[].todos keep every entry; the proven-absent set lives in absentTodos (or a per-entry absent marker) and the CLI prints ③ from the difference. Then Clause-②: yes, minor, no BREAKING line is exactly right, --json's todos keeps its old meaning, and the Tier H upgrade skill's loop is unaffected.

A′ is the smaller edit from the shipped diff; B is the cleaner contract (triage scoped the DEFAULT LIST, not the function's array). Either passes ②; Clause-②: yes with no arm and no migration, over a function whose output array changed membership, does not.

③ Boundary flags

Every flag and open_questions entry in the dev report 6044219249, answered or escalated:

  1. open_questions[0] — todos membership. Ruled in ②: A as shipped under-declares; A′ or B. Not escalated.
  2. deviations[0] — model-free commit trailers. AGENTS.md outranks the harness reminder and the pre-push hook enforces it; accepted, not a review matter.
  3. deviations[1] — hotcrm measured with this branch's spec, not the 17.7.0 tarball. Accepted: the baseline reproduced the card's own numbers (304 manual, 12 applied), which is the control the measurement needed.
  4. deviations[2] — branch behind main. At this reading origin/main is a543e244f, one commit past the recorded base, touching nothing under packages/spec/src/migrations/; the enumeration pin is unaffected and the queue rebuilds on main. Accepted; the fix-up push for ① item 5 should re-check it.
  5. deviations[3] — no reverse-mutation run for the cross-package type. Accepted: the CLI typecheck reads absentTodos and relevantWhen, which exist only in the rebuilt .d.ts, and Type Check · consumer gates is success on the head.
  6. deviations[4] — worktree cleaned. No review content.
  7. out_of_scope[0] — --out not written on a range with no step. Pre-existing, measured at the CLI door; the seat filed finding(cli): os migrate meta --out FILE over a range with no step exits 0 and writes no snapshot, while the same run with --json writes FILE #22116 (open at this reading). Not this PR's.
  8. out_of_scope[1] — --all missing from the upgrading docs' flag table. Owed (① item 7); non-blocking; a one-row edit on an ungoverned file that can ride the fix-up push.
  9. out_of_scope[2] — --stored never reports the D3 catalogue. Observation; the printed scope line states the boundary. Note that ① item 5's four entries are the one place where "the proof covers the loaded stack" is not a sufficient bound, because part of their surface is outside metadata altogether.
  10. out_of_scope[3] — packages[] bodies get no mechanical conversion pass. Pre-existing, not reproduced, not this PR's; the relevance read of package bodies is conservative (present anywhere wins). The seat may file it once reproduced.
  11. gates — check:dual-build-cjs-loads NOT MEASURED locally. Lint & Repo Gates is success on the head at the second reading; covered by CI.
  12. mcp_calls / api_writes. One read-only add_repo, three relay writes through scripts/pm; within protocol.
  13. Escalated to the seat, non-blocking: tiers and --preset as plugin carriers the evaluation does not treat as carriers (① item 3); the required absentTodos member on two published interfaces, to be named in the changeset sentence under A′.

What a PASS on the next head needs: ① item 5 fixed (four relevantWhens removed, registry regenerated, pin at 25, PR table and changeset count corrected, measurements re-run) and ② satisfied by A′ or B; the docs row is recommended, not required. The rest of the diff — the types, the evaluation, the partition mechanics, the CLI output, the pins — is right as read.

Implemented-by: claude/issue-22072-migrate-meta-relevance-predicate
Reviewed-by: session_01RPo7FUd6bSnAfkWMAKi848

VERDICT: FAIL


Generated by Claude Code

claude added 2 commits October 7, 2026 18:52
… code-door entries lose their question; tiers is a carrier

Contract-review patch round: applyMetaMigrations().todos and each hop's
todos keep every semantic entry again; absentTodos names the proven-absent
entries as a subset of them (same objects). os migrate meta lists todos minus
absentTodos. The four CEL/RLS predicate entries whose surface also names a
direct evaluator or compiler caller lose relevantWhen (batch 25). A declared
tiers preset answers unknown like plugins. Docs: --all in the flags table.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5104a5d7458e93d770cf1efaf41913a6fa4c64c8
Local-runs: none

Isolated at-tier reviewer, dispatched from the adopting seat; read-only; patch round 1, answering the FAIL 6044604409 on 1904d5350. Inputs, and nothing else: card #22072 (body and its 7 comments: triage 6038524437, claim 6041997447, dev report 6044219249, ACCEPT 6044275269, REWORK 6044626135, patch-round dev report 6045718354, patch-round ACCEPT 6045764932 — the three seat verdicts read as claims to test), #20620's ruling 5888087153, PR #22115 (body as rewritten by the seat for this head, the 38-file list, both PR comments, the net diff against the merge base a543e244f), and the check-runs on the head. Files were read from git objects at the head and at the merge base; origin/main was fetched to compute the diff and its drift. Record written at 2026-10-07T20:11Z.

Check-runs on the head. Read at 2026-10-07T20:00:47Z: 35 runs, every one completed: 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failed, 0 in_progress. Among the successes: Test Core and its six shards, TypeScript Type Check, Type Check · consumer gates, Lint & Repo Gates, Check Changeset, Spec property liveness, Build Core, Build Docs, Governed Surface Queue Guard.

① Derived judgments

The accept-set and public-surface changes the diff (vs main at the merge base) implies, each judged.

  1. Three new exported types — right. SemanticRelevanceKey is Exclude of StackDefinitionKey over FIVE carrier keys (manifest, packages, plugins, devPlugins, tiers); StackDeclaresRelevance is { kind: 'stack-declares', keys: non-empty readonly tuple }; SemanticRelevance is a one-member closed union. index.ts re-exports exactly the three as types; api-surface/migrations.json and export-origins/migrations.json gain exactly those three rows. semanticRelevanceVerdict, semanticTodoAbsent and the type SemanticRelevanceVerdict are exported from chain.ts only — the barrel re-exports applyMetaMigrations, composeMigrationChain and MigrationFloorError from it and nothing else — so they reach no published entry.
  2. SemanticMigration.relevantWhen (optional) — right, closed and structured as the ruling requires: the SHAPE pin holds Object.keys(relevantWhen) equal to ['keys', 'kind'] and every key against the runtime STACK_DEFINITION_KEYS minus CARRIER_KEYS; chain.ts never reads surface (it reads stack[key], packages[].manifest[key], and the three carriers), so "never removed by prose-matching surface" holds by construction.
  3. tiers is now a carrier, and does not leak into SemanticRelevanceKey — fixed, right. The exclusion is live, not vacuous: tiers IS a StackDefinitionKey (COMPOSE_KEY_DISPOSITIONS carries tiers: 'concat' and satisfies a Record keyed by StackDefinitionKey; the schema declares tiers: z.array(z.string()).optional(), "overrides --preset"). chain.ts iterates ['plugins', 'devPlugins', 'tiers'] and pushes unknown for any value that is not undefined, [] or {} — a non-empty string array, a function, a scalar all block the proof — while a key the stack visibly declares still answers present. Pinned three ways: EVALUATION ({ tiers: ['ai'] } unknown, { tiers: [] } absent, tiers beside a declared analyticsCubes present), CHAIN ({ tiers: ['core'] } on the objects-only stack gives absentTodos equal to []), and the SHAPE pin's CARRIER_KEYS names it. The previous record's other two carriers stay observations, not defects: onEnable is z.function() ("register action handlers and drivers here"; a question naming it would read unknown, and none does), and --preset is a serve-time flag with no stack footprint; neither is reachable by a stack-derived question, and the printed scope line bounds the proof to the definition this run loaded.
  4. Partition mechanics under disposition B — right. todos is built exactly as before: every step.semantic entry of every hop crossed, step order then entry order, whatever the stack holds (replayed is mapped after all hops ran; hops[].stack is still each hop's post-conversion stack). absentTodos is hopTodos.filter(semanticTodoAbsent) over the SAME objects — a subset in chain order at chain and hop level. checkpoints is the input stack plus every hop's checkpoint; appliedConversionIds is chain-wide; semanticTodoAbsent is false without relevantWhen, false when any conversionIds member applied, otherwise absent-only. Pinned: migrations.test.ts's original equality pin is restored byte-for-byte (the diff against the merge base adds four lines and removes none) with the subset pin beside it; semantic-relevance.test.ts CHAIN pins ids(todos) equal to the crossed catalogue on {} and on the objects-only stack, absentTodos exactly the questioned entries there, object identity and monotone positions, and [] under a plugin or a tier.
  5. The first batch — the previous ① item 5 is fixed; 25 entries, each bounded by its keys — right. The four code-door entries (cel-predicate-one-value-comparand-refused, cel-predicate-variable-root-comparand-refused, rls-predicate-array-comparand-refused, rls-predicate-stored-list-ordering-refused) carry no relevantWhen: their blobs at the head are identical to the merge base and to the claim's base bafb58bb0 (a9ab9d36a…, 36b399a42…, 32054144a…, 36837d946…), and they are not in the 38-file list. git grep relevantWhen over entries/semantic/ at the head: 25 files, 5 under 17. and 20 under 18.; registry.ts carries exactly 25 relevantWhen lines (its diff is +25, nothing else); the enumeration pin EXPECTED_RELEVANCE has 25 rows with toHaveLength(25), 5 and 20 beside it, and an anti-vacuity pin that every row is a registered entry. The 25 carry exactly the keys the previous record judged right (analyticsCubes ×8, dashboards ×3, dashboards/reports/pages ×1, permissions ×1, sharingRules ×1, apis ×2, jobs ×2, agents ×2, datasets ×2, mappings ×1, hooks ×1, tools ×1); the registry at the head has 77 step-17 and 306 step-18 entries, so the PR body's "5 of 77 · 20 of 306 · 25 of 383" is the tree's own count. The applied-conversion pin (an entry judging a conversion is listed on that conversion's own fixture) covers the two agents entries that carry conversionIds.
  6. CLI printed output — right. listedTodos is todos minus absentTodos, matched by ${toMajor}:${id} — sound, because build-migration-registry.ts makes each shard's filename a pure function of its entry id and refuses a mismatch, so ids are unique per major. ③'s header counts listed; ③'s lines are byte-identical (printNotice is a pure extraction). ④ prints only when absentTodos is non-empty: by default the count line naming --all and the scope line; under --all a header, each entry as ③ prints it, then absent: nothing is declared under and its keys. judgesByConversion(listed) equals the same map over todos, because an entry judging an applied conversion is never absent, so ②'s review lines are unchanged. --json always carries absentTodos and hops[].absentTodos under --step; the hop line gains , N not listed (surface absent) when N is non-zero. The "Nothing to migrate" condition is back to the original bytes, and because todos is whole a run whose only notices are proven absent has todos.length above zero, skips that branch, prints no ③, prints ④ and writes --out — pinned (ABSENT). --all is exclusive: ['stored']; MigrationReport.all is CLI-internal. One observation, non-blocking: printEmptyRangeAnswer's wider-range hint counts widerListed, and says "nothing for this stack either" when the wider range has zero applied and zero listed even if it has proven-absent entries — those carry the proof and --to N --all reaches them, and the branch is reachable only when EVERY crossed entry is proven absent, which 25 of 383 cannot produce.
  7. Docs — the previous ① item 7 is answered. content/docs/upgrading.mdx's "Useful flags" table gains the --all row, and its sentence matches the CLI (--json reports them in todos and names them in absentTodos). Right.
  8. No governed path in the 38-file list; head repo is the base repo. skills/objectstack-upgrade/SKILL.md (Tier H) is untouched; its loop over --json's todos (line 318) reads exactly what it read before under B, so no governed edit is owed. Right.
  9. Mergeability — the one new fact, escalated in ③. At this reading origin/main is 1920cf3f8, four commits past the merge base a543e244f the PR body names: a959493cd (os migrate meta --write: meta.ts +229, content/docs/upgrading.mdx's flags table and callout, changeset 9591-migrate-meta-write.md), f2a45db2a, 1920cf3f8 and db4c45b8c (eight new step-18 semantic entries, registry.ts +420). A read-only git merge-tree --write-tree origin/main 5104a5d74 (no worktree, no checkout) reports CONFLICT (content) in packages/cli/src/commands/migrate/meta.ts and in content/docs/upgrading.mdx; registry.ts auto-merges. Nothing collides semantically: main's --write changeset states it never writes the semantic changes (todos, "which stay listed exactly as before") and prints its block after the semantic notices; the eight new entries carry no relevantWhen (the field does not exist on main) and are flow and stored-row families the PR's own rule 3 excludes, so the enumeration pin at 25 holds after a merge. But the catalogue becomes 77 + 314 = 391, so the PR body's "25 of 383" table and its 306-based measurements go stale, and the PR cannot be rebuilt on current main without a hand merge of two files. GitHub's mergeable field read unknown at this stamp.

② Semver level

What the diff publishes under disposition B. Additive only: three exported types; the optional SemanticMigration.relevantWhen (and so relevantWhen on the 25 MigrationTodo objects --json emits); the --all flag; absentTodos and hops[].absentTodos in --json; and absentTodos as a REQUIRED member of the published interfaces MigrationChainResult and MigrationHopResult. todos's guarantee — its TSDoc "Every semantic TODO across all hops" — is TRUE again on every stack, pinned by the restored equality pin and by the CHAIN pin on {}. The accept set is unchanged (applyMetaMigrations accepts the same stacks and still never throws on content); no published output guarantee narrows; --json's todos keeps its meaning. The required member is additive for every reader and a compile break only for code that constructs one of the two result interfaces — a result type the library alone produces — and the changeset names exactly that class and its remedy.

Ruling. Clause-②: yes, @objectstack/spec minor, @objectstack/cli minor, no arm, no BREAKING line, no ADR-0087 disposition marker — exactly right under B; Check Changeset and Lint & Repo Gates are success on the head. The changeset sentence about absentTodos is accurate: "a new required member of MigrationChainResult and MigrationHopResult. It names the subset of todos whose question answered absent in all of them [the given stack and every hop checkpoint]: the same objects, in chain order. Code that only reads a chain result needs no change. Code that builds one of these two interfaces itself must now supply absentTodos (an empty array when nothing is proven absent)." — each clause matches chain.ts and types.ts. The todos sentence ("unchanged … every semantic entry of every hop crossed, whatever the stack holds, as before") is accurate. The remaining bullets read against the code: the field and its three types; the three unknown cases with tiers named; the applied-conversion rule; the 25-entry batch and its key list; the CLI's count line, scope line, --all, --json, --step and --out. One imprecision, non-blocking: "--step … adds a not listed count to the hop line" is true only when the count is non-zero (the suffix is omitted at zero).

③ Boundary flags

Every flag and deviations entry in the patch-round report 6045718354, answered or escalated — and the REWORK's four dispositions checked on the tree:

  1. REWORK 1 — four entries off, registry regenerated, pin at 25, PR table and changeset count corrected. Implemented (① item 5). The measurements were re-run by the dev on the 306/383 bases; not re-run here (read-only), and the pins fix the enumeration, not the per-stack counts.
  2. REWORK 2 — disposition B. Implemented (① item 4, ②): todos whole on the chain, the hop and --json; absentTodos a subset of the same objects; the CLI lists the difference; the TSDoc on both interfaces and the changeset say so.
  3. REWORK 3 — tiers a carrier. Implemented and pinned, and excluded from SemanticRelevanceKey (① item 3).
  4. REWORK 4 — the --all docs row. Implemented (① item 7).
  5. deviations[0] — hook-timeout-override-refusal.test.ts timed out twice under box load, passes alone 4/4; untouched by the diff and the merge. Test Core and all six shards are success on the head; CI is the reading that counts. Answered.
  6. deviations[1] — hotcrm measured on a read-only public clone against this branch's spec; base 306 because main added two step-18 entries. Accepted as in round 0: the control reproduced the card's 12 applied edits, and the 12 left are unchanged.
  7. deviations[2] — the dev did not edit the PR body; the seat rewrote it from pr_body_changes (a)–(h). Read against the diff: (a) the batch table is the 25 entry files, row for row, and sums to 5/77, 20/306, 25/383; (b) the left-out list names the four with the code-door reason; (c) the evaluation table reads plugins / devPlugins / tiers; (d) todos whole, absentTodos the subset, ③ lists the difference, the only-absent run still counts and writes --out, and the "Nothing to migrate" clause is gone; (e) the measured table is on the 306/383 bases — the dev's numbers, not re-measured here; (f) the contract-review bullet is disposition B; (g) verification is at this head; (h) the docs-table and upgrade-skill notes are dropped. Three things to correct, none blocking: "--step adds a not listed count to each hop line" holds only for a non-zero count; "Two internal exports" are two functions and one type (SemanticRelevanceVerdict), none published; and the Mergeability note ("a merge of origin/main a543e244f (clean)") was true when written and is no longer the landing picture — escalated to the seat: main moved four commits and the head conflicts with it in meta.ts and upgrading.mdx (① item 9). The landing needs a hand merge that keeps ③/④ and listedTodos as read here, places main's --write block after ④'s lines (main prints it "after the semantic notices and before the data-migration advice"), and carries both the --all and --write rows in the flags table; the PR body's totals then move to 391. This record names head 5104a5d74 only; whether the merge head re-owes a record is the seat's call under its process, and a reviewer of it needs to re-read exactly those two files.
  8. deviations[3] — worktree cleaned. No review content.
  9. out_of_scope_findings[0] — finding(cli): os migrate meta --out FILE over a range with no step exits 0 and writes no snapshot, while the same run with --json writes FILE #22116 (--out not written on a range with no step). Filed by the seat per its ACCEPT; pre-existing; not this PR's; not re-read here (outside the brief's inputs).
  10. gates — 117 families, 116 exit 0; check:dual-build-cjs-loads NOT MEASURED locally (prerequisite); two reruns on prerequisites. ci.yml runs pnpm check:dual-build-cjs-loads under Build Core, which is success on the head; Lint & Repo Gates is success. Covered.
  11. mcp_calls 0; api_writes 1 (the report, via post-stamped). Within protocol.
  12. The previous record's ③13 escalations. tiers taken (REWORK 3); the required absentTodos member is named in the changeset sentence (②); --preset is not addressable by a stack-derived question and stays an observation, bounded by the scope line.
  13. The seat's patch-round ACCEPT 6045764932, read as a claim. Its four tree checks match what I read; its CI line (33 success, 2 skipped) matches this reading; its mergeability premise is the one that has since changed.

Landing: the contract holds on this head. The one open item is mechanical and the seat's: merge current main (two conflicting files), re-run the enumeration pin and the report-order pins on the merge head, refresh the PR body's totals, and land through the queue once every check is green on the landing head.

Implemented-by: claude/issue-22072-migrate-meta-relevance-predicate
Reviewed-by: session_01RPo7FUd6bSnAfkWMAKi848

VERDICT: PASS

…grate-meta-relevance-predicate

# Conflicts:
#	content/docs/upgrading.mdx
#	packages/cli/src/commands/migrate/meta.ts
claude added 3 commits October 7, 2026 20:47
`MigrationReport.all` (the --all flag) is a required member of the report;
the literal the early-return pin builds omitted it (TS2741 under
tsconfig.test.json). No assertion changes.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 16cb55b8d0acda74b4e740e56bf1b5cbe3871bbd
Local-runs: none

Isolated at-tier reviewer, dispatched from the adopting seat; read-only; merge rounds 1 and 2, after the PASS 6045970836 on 5104a5d74. Inputs, and nothing else: card #22072 (body and its 7 comments: triage 6038524437, claim 6041997447, dev report 6044219249, ACCEPT 6044275269, REWORK 6044626135, patch-round dev report 6045718354, patch-round ACCEPT 6045764932 — the seat's verdicts and the PR body's merge-round notes read as claims to test), PR #22115 (body, the 39-file list, its 3 comments: the Docs Drift advisory 6044185790 and the two earlier records 6044604409 and 6045970836, and the net diff against main at the base 54ace18c6), and the check-runs on the head. Files were read as git objects already present in the local clone (no fetch, no checkout, no worktree) and over REST; nothing was built, run or re-run. origin/main at this reading IS the PR's base 54ace18c669a776c7e849708039c7876ac534cee (compare 54ace18c6...main: identical, 0 ahead, 0 behind), so the PR diff is the whole net diff; GitHub reads mergeable: true, mergeable_state: clean. Record written at 2026-10-07T22:40Z.

Check-runs on the head. Read at 2026-10-07T22:28:03Z: 42 runs, every one completed: 38 success, 4 skipped, 0 failed, 0 in_progress. The 4 skipped are Console Pin Gate, Packed-tarball smoke (opt-in), and the re-fired Auto Label and Check PR Size of a second workflow run at 22:26Z (whose Check Changeset and the three card/claim guards are success). Among the successes: Test Core and its six shards, TypeScript Type Check, Type Check · workspace (red on 1d6efd229, green here), Type Check · consumer gates, Type Check · source gates, Type Check · debt ledger, Lint & Repo Gates, Check Changeset, Spec property liveness, Build Core, Build Docs, Check Documentation Links, Dogfood Regression Gate and its three shards, Dogfood Verify CLI, Temporal Conformance, Governed Surface Queue Guard, Flag docs affected by code changes.

The commit chain (10 commits from bafb58bb0): 8c9e806bd, 866c31ca8, 671038431, 1904d5350 (round 0), merge de8ccbcfa (with a543e244f), 5104a5d74 (patch round 1, PASSed), merge e74764dbb (with db4c45b8c), 1d6efd229 (changeset sentence), merge 5fc3a5a2a (with 54ace18c6), 16cb55b8d (test fix). The branch's own commits since the PASS are 1d6efd229 (one changeset line) and 16cb55b8d (one test line); everything else since is merge.

① Derived judgments

The accept-set and public-surface changes the net diff (39 files, +942 / −64) implies, each judged.

  1. Three new exported types — right, unchanged since the PASS. SemanticRelevanceKey is Exclude of StackDefinitionKey over the five carriers manifest, packages, plugins, devPlugins, tiers (tiers IS a stack key: stack.zod.ts:830 declares it, :1071 gives it a compose disposition); StackDeclaresRelevance is { kind: 'stack-declares', keys: non-empty readonly tuple }; SemanticRelevance is a one-member closed union. index.ts re-exports exactly the three as types; api-surface/migrations.json and export-origins/migrations.json gain exactly those three rows. semanticRelevanceVerdict, semanticTodoAbsent and the type SemanticRelevanceVerdict are exported from chain.ts only — a grep of both barrels and both generated shards at the head finds none of them — so they reach no published entry.
  2. SemanticMigration.relevantWhen (optional) — right. Closed and structured as [finding][devx] os migrate meta --from 17 buries a project's real findings under 240 generic protocol-18 notices, and marks default-flip conversions as "Applied" when the right action is usually no edit #20620's ruling requires: the SHAPE pin holds Object.keys(relevantWhen) equal to ['keys', 'kind'] and every key against the runtime STACK_DEFINITION_KEYS minus CARRIER_KEYS; chain.ts reads stack[key], packages[].manifest[key] and the three carriers, and never surface, so "never removed by prose-matching surface" holds by construction.
  3. Evaluation — right. collectionVerdict: undefined, [], {} are absent; a non-empty array or plain map is present; a function, promise, scalar, null or class instance is unknown; the whole read is in try/catch so a throwing getter is unknown. A non-plain stack, a packages that is not an array, an entry that is not a plain dict or a non-dict manifest body are unknown; any plugins / devPlugins / tiers value other than undefined, [], {} pushes unknown while a key the stack visibly declares still folds to present. foldVerdicts: any present wins, then any unknown, absent only when every verdict is. semanticRelevanceVerdict folds over every checkpoint ([stack, ...replayed stacks]), and [] checkpoints is unknown. semanticTodoAbsent is false without relevantWhen, false when any conversionIds member is in the chain-wide applied set, else absent-only. All pinned in semantic-relevance.test.ts (EVALUATION, 10 cases including tiers, package bodies and multi-checkpoint folding).
  4. Partition under disposition B — right, unchanged since the PASS. applyMetaMigrations runs every hop first, then maps replayed into hops: todos.push(...hopTodos) unconditionally (every step.semantic entry of every hop crossed, step order then entry order, whatever the stack holds), and absentTodos.push(...hopAbsent) where hopAbsent = hopTodos.filter(semanticTodoAbsent) — the same objects, so a subset in chain order at chain and hop level. migrations.test.ts's original equality pin is intact (the file's diff vs main is +4 lines, −0: the subset pin and the "every absent entry carries relevantWhen" pin). The CHAIN pins hold ids(todos) equal to the crossed catalogue on {} and on the objects-only stack, absentTodos exactly the questioned entries there, object identity and monotone positions, and [] under a listed plugin or a tiers preset.
  5. The first batch — 25 entries, right; the merges brought nothing into it. At the head the semantic catalogue is 77 step-17 and 314 step-18 files (391; ids unique per step, 77 and 314), git grep relevantWhen over entries/semantic/ finds 25 files (5 under 17., 20 under 18.), the generated registry.ts carries exactly 25 relevantWhen lines (its diff vs main is +25, nothing else), and EXPECTED_RELEVANCE has 25 rows with toHaveLength(25), 5 and 20, plus the anti-vacuity pin that every row is registered. The four code-door entries (cel-predicate-one-value-comparand-refused, cel-predicate-variable-root-comparand-refused, rls-predicate-array-comparand-refused, rls-predicate-stored-list-ordering-refused) are blob-equal to main (a9ab9d36a, 36b399a42, 32054144a, 36837d946) and carry no question. main has no relevantWhen anywhere under packages/, so none of its ten new step-18 entries (the eight from db4c45b8c / 1920cf3f8 and the two from the patch-round merge) carries one; the PR body's rule-3 / rule-2 reading of them (stored flow rows; seven platform-shipped organization_id tables) is consistent with their names and with the batch's own rules. The 25 carry the keys the earlier records judged right (analyticsCubes ×8, dashboards ×3, dashboards / reports / pages ×1, permissions ×1, sharingRules ×1, apis ×2, jobs ×2, agents ×2, datasets ×2, mappings ×1, hooks ×1, tools ×1); the PR body's "5 of 77 · 20 of 314 · 25 of 391" is the tree's own count.
  6. absentTodos as a REQUIRED member of MigrationChainResult and MigrationHopResult — right under B, and declared. Additive for every reader; a compile break only for code constructing one of the two result interfaces, a type the library alone produces; the changeset names exactly that class and its remedy (an empty array). The TSDoc on todos now says "absentTodos never removes anything from it", which is what the chain does.
  7. CLI printed output after the hand merge with --write and --out — right. Read at the head: ① verdict; the early return at meta.ts:609 is the ORIGINAL condition applied.length === 0 && todos.length === 0, and because todos is whole, a run whose only notices are proven absent never takes it — pinned (ABSENT: "still counts them and still writes --out"); the early return itself now writes --out, prints --write's outcome and the data-migration advice (fix(cli): os migrate meta --out writes its snapshot on a run with nothing to migrate #22121, main's). Main path: ② printAppliedEdits pairs review lines from judgesByConversion(listedTodos(…)) — equal to the map over todos, because an entry judging an applied conversion is never absent; --step hop lines read N mechanical, M manual plus , K not listed (surface absent) only when K is non-zero; ③ lists listedTodos(todos, absentTodos) (matched by ${toMajor}:${id}, sound since ids are unique per step) under a header counting it, each notice through printNotice (a pure extraction; ③'s bytes unchanged); ④ printAbsentNotices prints only when absentTodos is non-empty — by default the count line naming --all and the scope line, under --all a header and each entry as ③ prints it followed by absent: nothing is declared under and its keys; then writeStackSnapshot once; then ⑤ printWriteOutcome; then the data-migration advice. writeSources is handed result and reads result.applied and result.stack only — no todos, no absentTodos — so the PR body's claim that --write is independent of the partition holds. --json carries absentTodos beside the whole todos, and hops[].absentTodos under --step. --all is Flags.boolean, default false, exclusive: ['stored'], with an examples row. The ORDER, SET, PAIR and ABSENT pins in meta.report-order.test.ts account for every printed line (ABSENT_COUNT_RE / ABSENT_SCOPE_RE are added to both "every line accounted for" pins). One observation, non-blocking, carried from the previous record: printEmptyRangeAnswer's wider-range hint counts widerListed and says "nothing for this stack either" when the wider range has zero applied and zero LISTED even with proven-absent entries; reachable only when every crossed entry is proven absent, which 25 of 391 cannot produce.
  8. MigrationReport.all (required) — right, and CLI-internal. MigrationReport is exported from commands/migrate/meta.ts and not from packages/cli/src/index.ts (which re-exports the migrate commands, not this type). Every constructor at the head supplies it: the command's run() (all: flags.all), meta.report-order.test.ts (all: options.all ?? false) and fix(cli): os migrate meta --out writes its snapshot on a run with nothing to migrate #22121's migrate-meta-out-snapshot.test.ts (all: false, the one line 16cb55b8d adds — the file's whole diff vs main; no assertion changed). The PR body's TS2741 account matches: the field is required, and 1d6efd229 predates the merge that brought the test.
  9. The hand merge (e74764dbb) — right; nothing of main's was lost. Of the 242 lines main added to meta.ts between a543e244f and 54ace18c6, exactly one is absent at the head: the comment // ④ \--write`: …, relabelled ⑤; the report docstring's ④ item is this PR's own line, extended with a ;and a ⑤ item for--write. Every line mainadded tocontent/docs/upgrading.mdxsurvives; the flags table carries--step, --all, --out, --write, --to, --json. Every deletion in the net meta.tsdiff is this PR's intent (thewider.todosreads,judgesByConversion(result.todos), the hop line, the ③ block, three docstring paragraphs and the ④ comment). The second merge (5fc3a5a`) is a clean auto-merge: the net diff touches the three files the brief names only as described here.
  10. Docs — right. The --all row's sentence matches the CLI: listed in full under --all, counted by default, --json reports them in todos and names them in absentTodos. The Docs Drift Check 6044185790 is advisory (three files yielded no anchor; nine release-owned pages affected read-only — none is in the diff, which touches content/docs/upgrading.mdx only); Flag docs affected by code changes, Build Docs and Check Documentation Links are success.
  11. No governed path in the 39-file list (docs/adr/**, docs/NORTH-STAR.md, .claude/**, skills/**, AGENTS.md, CLAUDE.md: 0 hits); head repo is the base repo (objectstack-ai/objectstack). skills/objectstack-upgrade/SKILL.md (Tier H) is untouched and its loop over --json's todos reads exactly what it read before under B. Governed Surface Queue Guard is success.
  12. The measured table (hotcrm 314 → 302, app-crm 301, app-todo 300, app-multi-package 294, app-showcase 314 → 314, one-object 294 / 366) is the dev's and the seat's, read at e74764dbb; not re-measured here (read-only), and the pins fix the enumeration, not per-stack counts. It still holds at this head: git diff --stat e74764dbb 16cb55b8d -- packages/spec/src/migrations/ is empty, so the catalogue and every relevantWhen are unchanged since the reading.

② Semver level

What the diff publishes. @objectstack/spec: three exported types; the optional SemanticMigration.relevantWhen (and so relevantWhen on the 25 MigrationTodo objects a consumer reads); absentTodos as a REQUIRED member of the published MigrationChainResult and MigrationHopResult; applyMetaMigrations accepts the same stacks, still never throws on content, and todos keeps its guarantee ("every semantic TODO across all hops") on every stack — pinned by the restored equality pin and the CHAIN pin on {}. @objectstack/cli: the --all flag; absentTodos and hops[].absentTodos in --json; ③'s default list is todos minus absentTodos, ④ counts the difference; MigrationReport.all on a CLI-internal type. No accept set narrows, no published output guarantee narrows, nothing an author can write is removed or renamed, so no migration text and no ADR-0087 disposition marker is owed.

Ruling. Clause-②: yes with no arm, @objectstack/spec minor, @objectstack/cli minor, no BREAKING line — exactly right under disposition B; Check Changeset and Lint & Repo Gates are success on the head. The changeset read sentence by sentence against the diff: the summary line (counts instead of lists, --all lists in full); the field and its three types, the packages[].manifest reading, "never free text, never matches the prose of surface"; "todos is unchanged" and the absentTodos sentence (required member, subset, same objects, chain order, readers need no change, constructors supply an empty array); the three unknown cases with tiers named and the applied-conversion rule; the 25-entry batch, its key list and "none of them names a code door"; the CLI's listing, count line, scope line, --all, --json, and the --step sentence — now "when that count is not zero", the one imprecision the previous record named, fixed in 1d6efd229; "a run whose only notices are proven absent still writes --out". Each matches the code. skip-changeset does not apply: both released packages publish.

③ Boundary flags

Every flag and open_questions entry of both dev reports, every escalation of the two earlier records, and every claim of the PR body's merge-round notes, answered or escalated:

  1. Report 6044219249 open_questions[0] — todos membership. Ruled A′-or-B by 6044604409; the seat chose B (6044626135); implemented at 5104a5d74 and intact at this head (① items 4 and 6, ②). Closed.
  2. Report 6044219249 deviations[0] — model-free commit trailers. Process, not a review matter; the pre-push hook enforces AGENTS.md. Accepted.
  3. Report 6044219249 deviations[1] / report 6045718354 deviations[1] — hotcrm measured on a read-only public clone against this branch's spec; the base moved 304 → 306 → 314 as main added step-18 entries. Accepted: the control reproduced the card's 12 applied edits and the 12 entries that leave the list are unchanged across the three readings; the catalogue is unchanged since the last reading (① item 12).
  4. Report 6044219249 deviations[2] — branch behind main. Answered by the three merges; main IS the base at this reading.
  5. Report 6044219249 deviations[3] — no reverse-mutation run for the cross-package type. Type Check · consumer gates and Type Check · workspace are success on the head, and 1d6efd229's red Type Check · workspace (TS2741 on all) followed by 16cb55b8d's green is itself the mutation evidence that the new required field is read. Covered.
  6. Report 6044219249 deviations[4] / report 6045718354 deviations[3] — worktree cleaned. No review content.
  7. Report 6044219249 out_of_scope[0] / report 6045718354 out_of_scope[0] — --out not written on a range with no step (finding(cli): os migrate meta --out FILE over a range with no step exits 0 and writes no snapshot, while the same run with --json writes FILE #22116). Filed by the seat; main's fix(cli): os migrate meta --out writes its snapshot on a run with nothing to migrate #22121 (merged here as 54ace18c6) makes the early return write --out on both arms, and this head carries it (① item 7). Not this PR's work; nothing further owed here.
  8. Report 6044219249 out_of_scope[1] — --all missing from the docs flag table. Done (① item 10).
  9. Report 6044219249 out_of_scope[2] — --stored never reports the D3 catalogue. Observation; the printed scope line bounds the proof to the loaded definition.
  10. Report 6044219249 out_of_scope[3] — packages[] bodies get no mechanical conversion pass. Pre-existing, not reproduced, not this PR's; the relevance read of package bodies is conservative (present anywhere wins). The seat may file it once reproduced.
  11. Gates (93 then 117 families; check:dual-build-cjs-loads NOT MEASURED locally, prerequisite; one check:vendor-version-stamps rerun on a transient temp file). Build Core and Lint & Repo Gates are success on the head. Covered.
  12. Report 6045718354 deviations[0] — hook-timeout-override-refusal.test.ts timed out under box load, passes alone. Test Core and its six shards are success on the head. Answered.
  13. Report 6045718354 deviations[2] — the PR body is the seat's. Read against the diff for this head: the batch table (25 rows summing to 5/77, 20/314, 25/391), the left-out list with the four code-door entries and main's ten, the evaluation table with tiers, the --out / --write interaction table, the measured table (the dev's numbers), the "For the contract review" bullets (disposition B; the scope boundary; "two functions and one type" — the previous record's correction, taken), the verification at 16cb55b8d, and the Acceptance notes. Each claim I could test on the tree holds (① items 5, 7, 8, 9, 12).
  14. mcp_calls / api_writes (both reports). One read-only add_repo, relay writes through scripts/pm only. Within protocol.
  15. Record 6044604409 ③13 escalations. tiers as a carrier: taken and pinned (① items 1, 3). The required absentTodos member named in the changeset: done (②). --preset: a serve-time flag with no stack footprint, not addressable by a stack-derived question; stays an observation bounded by the scope line.
  16. Record 6045970836 ③7 escalation — merge current main (two conflicting files), re-run the pins on the merge head, refresh the PR body's totals, land once green. Answered: e74764dbb resolved meta.ts and upgrading.mdx by stacking both intents with no line of main's lost (① item 9); 5fc3a5a2a was clean; the enumeration pin is unaffected (① item 5) and the report-order pins pass on the head (Test Core green); the PR body's totals read 391; every check is green. Its --step imprecision: fixed (②). Its printEmptyRangeAnswer observation: unchanged, non-blocking (① item 7).
  17. PR body merge-round claims — "two conflicts, both intents stacked", "391 (77 + 314)", "clean auto-merge, no hand edit", "no assertion changed", "--write never reads todos or absentTodos", "every arm writes the snapshot exactly once" (pinned by fix(cli): os migrate meta --out writes its snapshot on a run with nothing to migrate #22121's test and the ABSENT pin): each verified on the tree (① items 5, 7, 8, 9).
  18. Docs Drift Check 6044185790. Advisory; non-blocking (① item 10).
  19. Escalated to the seat, non-blocking: nothing new. The two standing observations (printEmptyRangeAnswer's wider hint; --preset) are recorded above and change no verdict.

Landing: the contract holds on this head; no governed path, so the record is owed by the claim's Clause-②: yes and not by a tier. Every check is green on 16cb55b8d at this reading, main has not moved past the base, and GitHub reads the PR as cleanly mergeable. The landing is the owning seat's, through the queue.

Implemented-by: claude/issue-22072-migrate-meta-relevance-predicate
Reviewed-by: session_01RPo7FUd6bSnAfkWMAKi848

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 7, 2026 22:38
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 7, 2026 22:38
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit cdeabec Oct 7, 2026
47 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22072-migrate-meta-relevance-predicate branch October 7, 2026 23:17
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