Skip to content

feat(cli): os migrate organization-ownership --apply executes only the reviewed plan file (ADR-0131 D10, C7b S4) - #23437

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-15211-c7b-plan-digest
Oct 14, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-15211-c7b-plan-digest

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Part of #15211 (stage S4 of C7b; the other stages are this card's S1 and the split cards #23429 and #23430)

Clause-②: yes (widening)

What

os migrate organization-ownership --apply executes only the plan the operator reviewed (ADR-0131 D10). It compares the reviewed plan's inventory digest, tenancy posture and database. Which rows a --template-choice (or S1's --metadata-choice) deletes is not yet bound to the plan file: #23452 carries that binding, on top of this PR once it is on main.

BREAKING: --apply shipped in @objectstack/cli@18.0.0-next.1, and its bare invocation --apply --backup-taken (with or without --fate-supplement) is now refused, plan-file-required. The replacement is a plan run, then --apply --backup-taken --plan-file plan.json. The changeset carries the banner, the FROM → TO table, and an ADR-0087 ledger entry (cli-organization-ownership-apply-plan-file-required).

  • --apply requires --plan-file FILE, the plan file a plan run wrote. Before it writes anything, it refuses:
    • plan-digest-mismatch when the inventory digest it computes differs from the file's, naming both digests and the remedy (plan again with the same flags, review, pass the new file);
    • plan-posture-mismatch when the run resolves another tenancy posture (OS_TENANCY_POSTURE / OS_MULTI_ORG_ENABLED) than the one the plan was computed under, naming both. The posture decides which rows attribution gives an owner and which it reports, and whether authored sets and positions are converted;
    • plan-database-mismatch when the file names another database.
  • Two refusals come before the backup question and before the database is opened: plan-file-required (no flag) and plan-file-unusable (the file is not a plan, lacks its digest, posture or database, or cannot be read).
  • A checkpoint recorded under another digest is no longer discarded silently. The reviewed plan supersedes it: every step runs again (each is idempotent), and the apply report names the discarded checkpoint in restartedFrom, beside reviewedPlan.
  • No flag skips the comparison, and none is needed.

Why

Measured by spec seat 3 (6158876765). A plan made with fate supplement A could be applied with supplement B, which ran B's fates unreviewed, including a fate-2 deletion A never listed. The plan run writes no row (it is read-only), so the only place a plan can record its digest is the plan file. The apply never read that file.

Verification (head d7ef4bd6d7, base: main cb9841aa25, merged)

Delta round (review 6167948246, seat rulings):

  • F5: main is merged; it did not conflict, and GitHub reported the PR dirty. The two tests main added get a reviewed plan.
    • organization-ownership-not-declared.integration.test.ts runs the plan first and passes its file.
    • organization-ownership-apply.served-catalog.integration.test.ts passes the reviewedPlan its run computes.
  • F6: the changeset is minor with Clause-②: yes (widening), a BREAKING banner, a FROM → TO table and the ADR-0087 disposition.
    • The check refused not-required (no-migration-prescription) beside a FROM → TO prescription, so the bare invocation's retirement is registered as a D3 semantic entry, 18.cli-organization-ownership-apply-plan-file-required; @objectstack/spec gets patch.
    • The WHICH LEVEL rule reads the same as the ruling: the widening takes minor, and the narrowing is carried by the banner and the disposition, not the level.
  • F4 revisited: both corrections of the consumed notes are reverted, and the diff against main no longer touches .changeset/pre/.

Earlier rounds (review 6165431301; the code is unchanged since): F1 (fate-supplement plan files), F2 (plan-posture-mismatch), F3 (the one-shot family reaches the apply's boot), and the pinned post-boot refusal mapping.

Checks, on the merged head:

  • The whole cli integration tier, vitest run --project integration in 4 shards under the verify lock: 1,111 passed, 0 failed, 2 skipped. Both skips are in files this PR does not touch: preview-read-only.integration.test.ts (a cell that needs OS_TEST_POSTGRES_URL) and test/migrate-meta-default-range.test.ts (a skip in the source).

  • The two new tests and the ceremony suites, run after the cli's dependency closure was rebuilt on the merged tree: 8 files, 89/89. Before that rebuild, served-catalog failed 3 cases on dists older than fix(cli,plugin-security): os migrate organization-ownership --apply resolves the built-in positions and the everyone baseline #23433, which the rebuild fixed.

  • cli typecheck: exit 0. spec src/migrations: 209/209, with the registry regenerated (it is generated at install and untracked). check:upgrade-guide and check:spec-changes: exit 0.

  • dispatch-gates --ran: 91 derived, 91 run, all exit 0, 0 NOT MEASURED. That includes check-adr-0087-registration and check-empty-changeset, which is green again now that the consumed notes are untouched.

  • Narrowed lint of the 9 changed .ts files: 0 errors, 0 warnings.

  • Reverse verification (ablation-replace, each restore verified blob == HEAD; earlier rounds, code unchanged since):

    Mutation Pins that went red
    posture comparison removed the posture pin
    the command's post-boot ReviewedPlanRefusal mapping removed the mapping pin
    refuseUnreviewedPlan a no-op the digest and database pins
  • Dogfood (first round, bc3525a9cf), with the real os binary on a seeded showcase SQLite database: no plan file is refused; a tampered digest is refused, naming both, with no ceremony row written; the reviewed file runs.

Acceptance notes


Generated by Claude Code

…, C7b S4)

--apply requires --plan-file and refuses, before any write, when the
inventory digest it computes or the database differs from the reviewed
plan's; a checkpoint under another digest is superseded and reported.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
…n is the seat's call (C7b S4)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 14, 2026
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/spec, touching 15 documentable anchor(s).

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

  • content/docs/permissions/capabilities.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts))
  • content/docs/permissions/permission-sets.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts))
What this run could not see
  • 8 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 — 148 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 394f65e891f0ac6c6be708fb807740d5cb446520 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 394f65e891f0ac6c6be708fb807740d5cb446520

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

…e (seat answer Q-S4 A, C7b S4)

`--apply` now refuses without the reviewed plan file, so the example the
compiled CHANGELOG prints names it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 66b826f9d9b159bfaf180fc8abde3cde44ec0f16
Local-runs: none

Read: the net diff origin/main...66b826f9d9 (6 files, +357/−10; merge base 5656c5638a, and main has not touched these files since), the PR body, card comments 6162365516, 6163669810, 6163356782, 6164220398, 6164948359, ruling 6163114098, pointer 6158876765, ADR-0131 D10 on origin/main, and the head's check-runs, last read at 2026-10-14T09:37Z. Nothing was built, run or re-run. Every test verdict below comes from reading the code, except the CI conclusions quoted.

① Derived judgments

F1 (must fix): a test on main that this PR leaves untouched now fails. packages/cli/src/commands/migrate/organization-ownership.fate-supplement.integration.test.ts came with #23348 and is in the CI-mandatory integration tier. Two of its cases run --apply --backup-taken without --plan-file:

  • with --fate-supplement, the apply step receives the host row (line 147);
  • control: without it, the apply step is handed no inventory (line 155).

At this head, runApply refuses plan-file-required before the boot and before applyOrganizationOwnership. So toMatchObject({ report: … }) and expect(apply).toHaveBeenCalledTimes(1) both fail. The dev's 33/33 covered two files only, not this one.

CI agrees. Test Core (3/6) failed at step 12 ("Run this shard's tests"), and its annotation reads packages/cli … pnpm run test exited (1). The required Test Core context failed with it. The failing case's name is not readable here: the proxy refused the job-log download. The failure is consistent with these two cases.

Remedy: give both cases a plan-shaped --plan-file. The apply is mocked there, so only the parse runs.

F2 (must fix, or the seat rules the narrower claim): the posture changes what the apply does, and it is neither in the digest nor compared.

  • The plan file records posture. reviewedPlanFromFile reads only generatedAt, database and inventoryDigest.
  • The posture comes from OS_TENANCY_POSTURE / OS_MULTI_ORG_ENABLED and is resolved separately on each run.
  • Under single with a Default Organization, attribution gives every anchorless NULL row to that organization (organization-ownership-plan.ts, about line 811). That includes every application table, because APPLICATION_OBJECT_FATE is attribution with no anchors. Under isolated/group those rows are reported (fate 4, "never guessed").
  • authored-conversion converts organization-authored sets and positions only under single; under a wall it reports them (apply.ts line 572).
  • So a plan reviewed under isolated, which lists rows as reported, can be applied under single, which writes owners for them. Plan A runs as plan B through one environment variable, and the changeset's "executes only the plan the operator reviewed" does not hold.
  • Remedy: compare posture (for example plan-posture-mismatch), with a pin and its ablation.

Other inputs outside the digest:

  • --template-choice: apply-only by design. The plan lists each conflict and the operator chooses at apply, under 6020178017.
  • The database's state between plan and apply (new rows, a new application table, a Default Organization created in between): D10 does not bind it, so it is not this stage's to bind.
  • The ceremony's code: the digest covers the inventory, not the steps. A later release's CLI with an unchanged inventory accepts an earlier plan file. ceremonyVersion is in the file but not compared, so "another release" is caught only when the inventory moved. Not blocking.

Unreviewed plans: what is refused, and when.

  • Refused before the backup question and before the boot, pinned with boot not called: no flag (plan-file-required), a missing file, not JSON, an apply report, an empty digest, a missing database (plan-file-unusable). --plan-file without --apply is refused as flag-without-apply.
  • Digest and database mismatch are refused after the backup question and after the served boot, as the PR says. They are refused before the ledger read, before validateTemplateChoices, before every step, and so before mirror-deletion's gate re-read and before any UPDATE, DELETE or DDL. planFor issues SELECT only.
  • Weakness (not blocking): inventoryDigest(inventory) is a pure function of the merged inventory, which runApply already holds. The digest refusal could come before the backup question and the boot. As built, the operator confirms a backup for an apply that will be refused, and a boot failure hides the mismatch. The claim that nothing is written before the refusal rests on the one-shot boot's no-write design. The dogfood measured sys_migration ceremony rows, not whole-database identity.
  • Weakness (not blocking): plan-database-mismatch compares stack.dbLabel, a display string (describeDb: a DSN with credentials and query string stripped, a SQLite filename as written, or the client name when the config carries no address). It can wrongly accept two databases that differ only in the query string, two configs without an address, or a relative SQLite path resolved from another directory. The module's own describeDriverSqliteFile warns against reading identity off this string.

Checkpoint under another digest. A checkpoint is reused only when its digest equals the current digest, and after the refusal the current digest is the reviewed one. A checkpoint under another digest yields completed: {}, so no step the reviewed plan names is skipped. It is reported in restartedFrom (the superseded list) and announced by onProgress.

The refusal. It names the file and both digests (or both database labels), says that nothing was written, and gives the remedy (plan again with the same flags, review, pass the new file). Reason codes are stable, the --json error is apply_refused, and the exit code is 1. Gap: the command-level mapping of a ReviewedPlanRefusal raised after the boot (the catch branch in runApply) is pinned by no test. If that branch were removed, the refusal would surface as apply_failed with "re-run --apply to resume". Only the dogfood observed the mapping.

Do the tests fail when the comparison is removed?

  • The ablation turned refuseUnreviewedPlan into a no-op, and both mismatch pins failed. That covers the database path too, since both checks live in that one function.
  • The checkpoint-supersede path was not ablated. By reading, its pin fails if restartedFrom is dropped (toEqual), or if a checkpoint under a foreign digest is reused (resumedFrom would be non-empty and attribution would read checkpointed).

What on main invokes --apply without --plan-file:

② Semver level

  • patch with Clause-②: no is correct by the WHICH LEVEL rule. The command's released surface (18.0.0-next.0, CHANGELOG cc305a3) is the plan mode only. 22937-* and 23280-* are still pending in .changeset/, not in .changeset/pre/.
  • --plan-file is refused outside the unreleased --apply, so no released invocation accepts a new key or loses one.
  • reviewedPlan and restartedFrom sit on the unreleased apply report, and the plan payload is unchanged.
  • The narrowing lands inside C7b build-ahead: os migrate organization-ownership --apply and E5's pure row→definition mappers in core (drafts from main; the --apply PR lands after #15204 stage 8) #22937's pending minor release (Clause-②: yes (widening)), so the combined level is unchanged.
  • Q-S4 → A, as applied: the corrected example (--apply --backup-taken --plan-file plan.json) is accurate, and it runs as written at this head. Minor: the note's "The backup comes first" bullet now trails the plan-file refusal.
  • Check Changeset fails only for the correction. In both runs only step 12 (Reject an empty-frontmatter changeset added by this PR) failed. Its one failure annotation names .changeset/22937-cli-organization-ownership-apply.md as a deliberate correction. ADR-0087 (step 13) and the major/level guard (step 15) are green.
  • F4 (seat decision): the same defect in another note. .changeset/23280-host-fate-supplement.md line 20 is pending and minor: os migrate organization-ownership --fate-supplement host-fates.json --apply --backup-taken. At this head that invocation is refused as plan-file-required, so the compiled CHANGELOG would print it, which is the reason given for Q-S4 A. The seat's answer named C7b build-ahead: os migrate organization-ownership --apply and E5's pure row→definition mappers in core (drafts from main; the --apply PR lands after #15204 stage 8) #22937's note only. Either extend A to this note, with the confirmation on the PR, or rule otherwise.

③ Boundary flags

Seat adoption (2026-10-14T09:39Z). Adopted after an audit of the reviewer transcript: one Write, to this record only; 70 Bash calls, all reads (git show, git diff, git ls-tree, REST GETs); no MCP call, no GitHub write, no worktree, build or test run. The seat re-read F1 on origin/main (the two cases at lines 147 and 155 of the fate-supplement integration test call --apply --backup-taken with no plan file) and F4 (.changeset/23280-host-fate-supplement.md:20). The seat rules:

Implemented-by: claude/issue-15211-c7b-plan-digest
Reviewed-by: session_01ADzJtzYTLUfgrRZHxkagkX

VERDICT: FAIL

…ture; main's --apply callers hand in a plan file (C7b S4 review)

- F2: the plan file's recorded posture is read and compared;
  plan-posture-mismatch names both postures and the remedy.
- F1: the fate-supplement integration cases pass a plan file.
- F3: the one-shot family's --apply write mode runs the plan first and
  hands its file in, so the seed-write and key-material pins reach the
  apply's boot again; a mode declaring preceding runs must reach its
  own write.
- F4: #23280's pending note's example carries --plan-file (seat answer
  Q-S4 A, extended); #22937's note and the module doc say the plan file
  comes before the backup question.
- The command's mapping of a post-boot plan refusal is pinned.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0802a12756f042750737f571072111c396702f24
Local-runs: none

Delta review against the FAIL record 6165431301 (head 66b826f9d9). The head has not moved since the brief. Read: the delta 66b826f9d9..0802a12756 (one commit, 9 files, +130/−30), the net diff from merge base 5656c5638a (9 files, +474/−27), the PR body, the dev report 6167752569, the seat answer 6164948359, and the head's check-runs, last read at 2026-10-14T12:04Z. To judge "any other test on main" and the semver premise, I also read current main (cb9841aa25) and a local git merge-tree of it with the head. Nothing was built, run or re-run. Every test verdict below comes from reading the code, except where a dev measurement is quoted.

① Derived judgments

F1, as ruled: fixed. In organization-ownership.fate-supplement.integration.test.ts, both --apply cases now pass --plan-file (writePlan(), a plan-shaped file that includes posture). Each keeps its pin: the host row reaches the mocked apply, and the control case is handed no inventory. The supplement-refusal case at line 205 still runs --apply without a plan file, which is correct: the supplement refusal fires in run() before runApply.

F5 (must fix): main moved, and two newer tests on main run the apply without a reviewed plan. The merge with main has no textual conflicts, so both tests land in the merged tree, and the merged tree breaks them:

Nothing else invokes this command's --apply without a plan file:

  • In packages/ and examples/ of the merged tree, the only other cases are the deliberate plan-file-required pin and the supplement-refusal case.
  • content/docs/** mentions the command only in prose (capabilities.mdx, permission-sets.mdx), and that prose stays true.

Remedy: merge main, then give the first test a plan run and its file, and give the second test a reviewedPlan.

F2: fixed.

  • refuseUnreviewedPlan compares reviewed.posture with plan.posture after the SELECT-only planFor. That is before validateTemplateChoices, the ledger read, and every step.
  • The refusal is plan-posture-mismatch. It names both values, says that nothing was written, and gives both remedies: apply under the plan's posture, or plan again.
  • The pin refuses, before any write, a plan computed under another tenancy posture asserts no writes, no checkpoint, and an unchanged database hash. By reading, it goes red without the comparison, because the digest and database match, so the apply would proceed. The dev measured that ablation red.
  • Same source: both modes call resolveTenancyPosture() once in run(), before branching:
    • the plan run writes that value as plan.posture;
    • the apply passes the same value as opts.posture, which both planFor and the authored-conversion step read;
    • so the env vars are the same (OS_TENANCY_POSTURE, else OS_MULTI_ORG_ENABLED true means isolated, else single), and both sides compare the normalized value, with multi normalized to isolated.
  • A plan file without posture is refused as plan-file-unusable, before the backup question and the boot. It is pinned.
  • Still not compared: ceremonyVersion. It is the literal 1 on both sides, so comparing it would catch nothing until it is bumped. Non-blocking, as the baseline found. client follows the database label. I found no other recorded input that adds actions. Composition and catalog differences can only block a step, as fix(cli): the ceremony's column drops answer not-declared for a table the booted composition declares no object for #23446's not-declared shows.
  • Like the digest refusal, the posture refusal still comes after the backup question and the boot (the baseline's non-blocking weakness, which the dev did not change).

F3: fixed.

F4 and the riders, as worded: correct. As a remedy: overtaken (see ②).

Carried to S1 (#23452), not a FAIL here: --template-choice deletions come from groups recomputed at apply time.

② Semver level

F6 (must fix, seat ruling): the premise behind patch / Clause-②: no has failed on main. --apply has shipped.

  • The version commit 0c52e43075 (chore: version packages (next) #23382, 2026-10-14T10:19Z) consumed 22937-cli-organization-ownership-apply.md and 23280-host-fate-supplement.md into .changeset/pre/.
  • packages/cli is now 18.0.0-next.1, and its CHANGELOG prints both bare invocations at lines 44 and 60: --apply --backup-taken and --fate-supplement host-fates.json --apply --backup-taken.
  • github-actions pushed the tag @objectstack/cli@18.0.0-next.1 at 2026-10-14T11:58Z. Release run 38551831636 was in its "Publish to npm + push version tags" step at 2026-10-14T12:00Z.

What follows:

  • The changeset's "--apply has not shipped in a release yet (its changeset is still pending), so no released invocation changes" is false, and so is the PR body's matching acceptance note.
  • On the released surface, this PR adds a new accepted flag (--plan-file), new keys on the apply report (reviewedPlan, restartedFrom), and new reason values. The delta's plan-posture-mismatch and its posture requirement add to these. Together they are a widening. By WHICH LEVEL that means at least minor with Clause-②: yes (widening).
  • The PR also refuses the released invocation --apply --backup-taken. That is a narrowing. In the launch window, that kind of break is carried by the BREAKING banner plus the ADR-0087 disposition, not by the level.
  • The baseline's "the narrowing lands inside C7b build-ahead: os migrate organization-ownership --apply and E5's pure row→definition mappers in core (drafts from main; the --apply PR lands after #15204 stage 8) #22937's pending minor" no longer holds.
  • The seat rules the level, the declaration and the banner. The PR's own note must then say that the released bare invocation is now refused.

F4 re-opened by F6.

  • Q-S4 A's class, "a wording correction of an unreleased note", no longer applies.
  • In the merged tree the two edits land on .changeset/pre/22937-… and .changeset/pre/23280-…, which are consumed notes. They change nothing in the published 18.0.0-next.1 CHANGELOG.
  • Seat decision: keep or revert those edits, and carry the correction in this PR's own note instead.

③ Boundary flags

  • Gate verdicts on the head: none exist.
    • At 2026-10-14T12:04Z, 0802a12756 had 0 check-runs and 0 workflow runs. Five check suites (vercel, fly-io, claude, cloudflare-workers-and-pages, objectstack-fleet) were queued, with no runs.
    • GitHub reports the PR mergeable: false and mergeable_state: dirty.
    • refs/pull/23437/merge is still 9aa1106aa7, the merge of the previous head 66b826f9d9.
    • So nothing failed and nothing is running, but nothing is measured. That includes Test Core, which was red at the baseline over F1, and Check Changeset.
  • Dev deviation, "Main was not merged (the push did not conflict)": that was true locally, for the text only. GitHub says the PR is dirty, and the merged tree breaks the two tests (F5).
  • Dev claims not measured by CI: 1,105/0/2 for the integration tier, 83/83, 88/88, typecheck 0, the two new ablations, dispatch-gates at 65 of 65, and check:query-options-erasure red once (attributed to a race) and then exit 0 when run alone.
  • Out-of-scope 1 (--template-choice binding): carried to S1 (feat(cli): the organization-ownership ceremony carries the sys_metadata organization layer into the environment, and a choice deletes only the rows the reviewed plan listed (ADR-0131 D10, C7b S1) #23452) by the seat (①).
  • Out-of-scope 2 (the digest and posture refusals could come before the boot; the database comparison reads the display label): left undone, as the baseline's non-blocking grading allows.
  • The showcase rewrite stop on admin_full_access and organization_admin_no_bypass is reported on the card (6163356782). Since then, fix(cli,plugin-security): os migrate organization-ownership --apply resolves the built-in positions and the everyone baseline #23433 on main touches how the ceremony resolves built-in positions. Whether that bears on the stop is not measured.
  • Not measured by anyone:
    • every CI gate on this head;
    • the posture route through the real binary;
    • the merged tree's four failing cases and the tsc failure, which come from reading only;
    • how Check Changeset treats edits to .changeset/pre/ notes;
    • whether the npm publish of 18.0.0-next.1 completed (the tag is pushed and the step was in progress).
  • Note on my reads: git fetch auto-followed release tags into the shared checkout's local refs and advanced origin/main there. Nothing else was written outside this record.

Seat adoption (2026-10-14T12:06Z). Adopted after an audit of the reviewer transcript: one Write, to this record only; 60 Bash calls, all reads (git reads with fetches into local refs, REST GETs); no MCP call, no GitHub write, no worktree, build or test run. The seat confirmed F6 on origin/main: packages/cli/CHANGELOG.md (version commit 0c52e43075, #23382) prints both bare invocations at lines 44 and 60. GitHub reports this PR mergeable: false. The seat rules:

Implemented-by: claude/issue-15211-c7b-plan-digest
Reviewed-by: session_01ADzJtzYTLUfgrRZHxkagkX

VERDICT: FAIL

… are (seat ruling, C7b S4 delta review)

Both notes shipped in 18.0.0-next.1; this PR's own note states the refused
invocation and its replacement instead. Q-S4 A is withdrawn.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
…e note is minor with its BREAKING banner (C7b S4 delta review)

- F5: organization-ownership-not-declared runs the plan first and passes
  its file; organization-ownership-apply.served-catalog passes the
  reviewedPlan its run computes.
- F6: --apply shipped in 18.0.0-next.1, so refusing its bare invocation
  is a narrowing and --plan-file a widening: minor, Clause-②: yes
  (widening), a BREAKING banner with the refused invocations and their
  replacement, and the ADR-0087 disposition.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
…edger (C7b S4 delta review)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

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

Third-round delta review against the FAIL record 6167948246 (head 0802a12756). The head has not moved since the brief. Read: the four commits after 0802a12756 (the revert 91db38b7c0, the merge 15ebc0208f of main at cb9841aa25, then c07af11aa8 and d7ef4bd6d7); the net diff cb9841aa25...d7ef4bd6d7 (10 files, +526/−26) against the old net diff 5656c5638a...0802a12756 (9 files, +474/−27); the PR body; the dev report 6170134203; the WHICH LEVEL rule in .github/workflows/pr-automation.yml; scripts/check-adr-0087-registration.mjs, the semantic registry and its precedents; and the head's check-runs, last read at 2026-10-14T14:34Z. Nothing was built, run or re-run.

Unchanged since the baseline, not re-judged. Of the old net diff's code and tests, six files carry the same added and removed lines against their new base: the command, organization-ownership-apply.ts, its unit test, the apply-flags test, the fate-supplement integration test and the one-shot family. What changed: the two main tests (+3, +13/−2), the changeset (rewritten), one new registry file, and the two consumed-note edits (gone).

① Derived judgments

F5: fixed.

  • main is merged. The merge 15ebc0208f carries no hand resolution (its combined diff is empty), GitHub now reports mergeable: true, with mergeable_state: clean at the last read (it was dirty at the baseline), and a git merge-tree of the head with today's main (394f65e891) is clean.
  • organization-ownership-not-declared.integration.test.ts: applyIn now runs the real plan (--out plan.json, same --database-url, same process env, so the same posture and database label, no supplement on either side), clears stdout, then runs --apply --backup-taken --plan-file plan.json. The pins are untouched: summary.notDeclared, the named blocker, the kept column, the in-run control, the dropped control column, and exit 1 in case (i). No vacuous pass: the plan run's own outcome is not asserted, but a plan run that fails writes no file, and the apply then answers plan-file-unusable, which the existing payload.error toBeUndefined assertion turns red.
  • organization-ownership-apply.served-catalog.integration.test.ts: ceremony() now passes a reviewedPlan with every ReviewedPlan field: inventoryDigest() over the default inventory (the run passes none), the case's own posture (single / isolated), and database v17, matching opts.database. By reading, refuseUnreviewedPlan passes, and the cases still pin the catalog's built-ins and the zz_undeclared blockers. The plan is self-computed, which is right for a test that pins the catalog, not the refusal.
  • No other caller without a plan, on the head and on its merge with today's main (the same file set):
    • applyOrganizationOwnership has three callers: the command (passes reviewedPlan), the served-catalog test, and the unit-test apply helper (defaults reviewedPlan; main's added column-drop case goes through it).
    • In packages/ and examples/, the other --apply mentions are prose: schema-migrate.ts, security-plugin.ts, builtin-positions.declaration-phase.test.ts, and json-stdout-purity.e2e.test.ts, which drives only the bare plan form.
    • content/docs/**: capabilities.mdx and permission-sets.mdx name --apply in prose that stays true. environment-variables.mdx matches only an ADR link.

F6: fixed.

  • The changeset is '@objectstack/cli': minor plus '@objectstack/spec': patch. It says Clause-②: yes (widening), as does the PR body.
  • The BREAKING banner names the narrowing and the release (18.0.0-next.1), and it lists the widening: the flag, the reasons, reviewedPlan and restartedFrom.
  • The FROM → TO is true.
    • origin/main's packages/cli/CHANGELOG.md prints both bare invocations under ## 18.0.0-next.1 (lines 44 and 60), and the GitHub Release @objectstack/cli@18.0.0-next.1 was published at 2026-10-14T12:08Z.
    • Both rows' TO is what runApply does: plan-file-required before the backup question and the boot.
    • Row 3's "(no such flag)" is right for next.1.
    • The "has not shipped" sentence is gone.
  • The D3 entry is right in shape and placement.
    • The file is packages/spec/src/migrations/entries/semantic/18.cli-organization-ownership-apply-plan-file-required.ts, named major.id.ts per entries/README.md. registry.ts is generated and git-ignored, so the one file is the whole registration.
    • Its id matches the file name and the changeset's adr-0087: registered marker. It sets surface, replacement, reason and acceptanceCriteria, which is the SemanticMigration shape, with no conversionIds (none applies).
    • It has no relevantWhen, which the type requires here: the surface lives in no stack key.
    • It matches the precedents for a non-spec breaking change registered in the ledger: a package bumped minor plus @objectstack/spec: patch, with the same marker (cab6396715, plugin-security; 1f0469655f, trigger-record-change).
    • It is the first entry whose surface is a CLI invocation. Its peers name REST, client or session surfaces outside the schema in the same way.
  • Published spec surface beyond the row: none. No schema, export or type changes. The row reaches the generated, uncommitted projections (the built registry, os migrate meta's protocol-18 TODOs, the generated upgrade guide), which is what registration is for.
  • Check Changeset, the job that runs check-adr-0087-registration and the level axis, is green on the head (both runs).

F4: fixed. The rider: met.

The merge breaks no earlier "right" finding. main's side touches the PR's code only through #23446 (a not-declared column-drop outcome, its blocker and summary.notDeclared) and #23433 (the security plugin's init() and the catalog). Neither moves an input of refuseUnreviewedPlan:

  • inventoryDigest reads the inventory alone;
  • posture is resolved once in run();
  • database is stack.dbLabel on both sides.

The refusal order is still: planFor, then refuseUnreviewedPlan, then validateTemplateChoices, then the ledger and the steps. plan-file-required and plan-file-unusable still come before the backup question and the boot.

② Semver level

minor is right under WHICH LEVEL.

  • --apply is released in 18.0.0-next.1. This PR adds an accepted flag, --plan-file, new reason values and two report keys. That is a widening, so at least minor.
  • It refuses the released bare invocation. That is a narrowing, and in the launch window it is carried by the BREAKING banner plus the ADR-0087 disposition, not by the level. Both are present.
  • Clause-②: yes (widening) with the moved cli package at minor satisfies the level axis in check-changeset-no-major.
  • @objectstack/spec: patch releases one more registry row and nothing else, which fits its precedents.

③ Boundary flags

Seat adoption (2026-10-14T14:35Z). Adopted after an audit of the reviewer transcript: its writes are this record and scratch files under the brief's directory; 71 Bash calls, all reads (git show, git diff, git merge-tree, REST GETs); no MCP call, no GitHub write, no worktree, build or test run. The seat re-ran the landing prechecks on d7ef4bd6d7:

  • ci-failure.mjs exits 0;
  • check-governed-merges.mjs reports no governed path and 552 lines;
  • git merge-tree against origin/main exits 0;
  • the body carries no closing keyword (Part of #15211).

The three ③ nits do not block; the PR title's missing ! matches no rule.

Implemented-by: claude/issue-15211-c7b-plan-digest
Reviewed-by: session_01ADzJtzYTLUfgrRZHxkagkX

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 14, 2026 14:37
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 14, 2026 14:37
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit f9b4217 Oct 14, 2026
49 checks passed
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

Development

Successfully merging this pull request may close these issues.

2 participants