Skip to content

Commit 6673c80

Browse files
feat(cli)!: the v18 upgrade carries stored comment reactions into sys_comment_reaction records, and the column drop waits for it (#23318)
Fixes #23281 Clause-②: no (narrowing) — `os migrate apply --allow-destructive` now refuses the `sys_comment.reactions` drop until a verified conversion is recorded, and its two `--json` account-issuer refusals now exit 1, as their human face already did. Converting stored rows into an existing object widens no accept set (the gated-subcommand precedent is #21120). The semantic entry changes in prose only. Dispatched by the `domain:services` seat 2 PM (claim comment 6150488591 on #23281), session `session_01FzQy26KznSa5j2gnf3AzsE`, branch `claude/issue-23281-comment-reactions-upgrade-conversion`. ## What this does The v18 retirement of `sys_comment.reactions` (`9963322fe8`) left every reaction a 17.x deployment stored in that column unread, and the drift remedy deleted them. Under the maintainer's rule that an upgrade keeps existing data (objectstack-ai/cloud#2843 `6145481762`), the upgrade now carries them: - **`os migrate comment-reactions`** (new, `packages/cli`). Each (comment, emoji, user) in a stored map becomes one `sys_comment_reaction` row with the same comment, emoji and user, in the comment's `organization_id`, with `created_by` = the reactor (the key of the own-record delete floor, so the member can un-react it). A value that does not read is listed in the report with its comment id and converted into nothing: `not_json`, `not_a_map`, `not_a_user_list`, `not_a_user_id`, `emoji_not_storable` (bound read from the registered `sys_comment_reaction.emoji`), `unknown_user` (an id with no `sys_user` row). Readable entries of the same comment still convert. Dry run by default; `--apply` writes, re-reads the column, and records the run in `sys_migration` under `sys-comment-reactions-to-records`, verified only when everything readable is converted and nothing is unreadable. Idempotent on the natural key, so a re-run inserts nothing and finishes an interrupted run. - **`os migrate apply --allow-destructive` holds the drop.** A column-specific refusal beside the existing `sys_account.issuer` retirement pre-flight, placed by the same rule (below the report, above the prompt, above both writes): the drop of `sys_comment.reactions` is refused (exit 1, `--json` `message: "refused_comment_reactions_unconverted"`) until the verified run is recorded. Every other drift entry and flag is unchanged. The boot drift line names that command as the remedy, so following it before converting is now safe. - **The semantic entry** `18.sys-comment-reactions-column-retired.ts`: `replacement`, `reason` and `acceptanceCriteria` prose name the conversion and order it before the drop. `id` and `surface` unchanged; no schema, `*.zod.ts`, contract or register change. The upgrade guide was regenerated with `pnpm --filter @objectstack/spec gen:upgrade-guide` (it writes the gitignored `content/docs/protocol-upgrade/`, so nothing is committed; `18.md` carries the new prose) and `check:upgrade-guide` is green. - `plugin-audit`'s `sys-comment.object.ts` docblock, `content/docs/deployment/cli.mdx` (table row and a subsection), the two family tables that enumerate every `bootSchemaStack` caller, and one changeset (`@objectstack/cli` minor, `@objectstack/spec` patch) saying what the upgrade now carries. ## The PM's hypotheses, measured - **H1 — holds.** On a booted database the column stays with its values; `os migrate plan` lists `sys_comment.reactions [drop_column]` as destructive. The raw read reuses `os migrate account-issuer`'s: `engine.getDriverForObject(name).find(...)` below the data door (the engine's `find` projects declared fields only). `sys_user` is read the same way (the data-migration boot composes no auth plugin). - **H2 — holds.** The stored shape is objectui's `JSON.stringify({ [emoji]: userIds[] })` (`RecordDetailView.tsx` `storedReactions`). The six unreadable reasons above are reported, never guessed. - **H3 — holds.** Natural key `(comment_id, emoji, user_id)`; the declared `unique: 'organization'` index refuses a duplicate (`DUPLICATE_RECORD`, seen in ablation 1 below). Each row carries its comment's organization. - **H4 — measured.** No code in this repo fires a notification or automation on a reaction insert; the data-migration boot composes no automation runtime. plugin-audit's reaction create gate returns early for a system write, so `user_id` keeps the stored reactor (an unexempted gate would have replaced or refused it). The writes use the bookkeeping context the organization seed claim uses, `{ isSystem: true, skipAutomations: true }`; audit stamps and code hooks still run. Found on the way: `created_by` must be the reactor or the member cannot un-react a converted reaction; it is carried in the row (the stamp hook assigns nothing without a session user). - **H5 — partly falsified.** `audit-metadata-bodies` records no `sys_migration` row and is not journal-backed; `value-shapes` records the row; only `recorded-by` / `meta` use the ADR-0119 journal. The shape taken: `audit-metadata-bodies`'s boot, dry run and idempotent rewrite, plus `value-shapes`'s ledger row. No journal: every reaction is one atomic insert and the natural-key guard makes each re-run the resume. - **H6 — falsified as stated, landed through the existing mechanism that does it.** `files-to-references`' column-step refusal gates that command's own column move on its own gate; it cannot hold back `os migrate apply`. The mechanism that holds back one destructive drop inside `os migrate apply` is the `sys_account.issuer` retirement pre-flight; this adds its sibling for one column. It reads a ledger row rather than re-measuring live, deliberately: after conversion a member may un-react, and a live probe would read that deleted row as unconverted and either refuse the drop forever or, worse, re-create the reaction if the operator re-ran the conversion to satisfy it. No path can write the column after v18 (the data door refuses it), so a verified run stays true until the column is gone. ## Measured on a booted composition (before the pins) `pnpm dev:todo -- -p PORT --database file:SCRATCH/react.db` (`SqlDriver(better-sqlite3)`, Audit plugin composed, branch at `f5db79977e`), signed in as the seeded admin; a second user made with the admin create-user door; four comments posted through `POST /api/v1/data/sys_comment`. The server was stopped and the 17.x leftover planted by hand: `ALTER TABLE sys_comment ADD COLUMN reactions TEXT` (v17 declared it a textarea) and four v17-shaped values: `{👍:[admin,m2],🎉:[m2]}`, `{❤️:[admin],👀:[]}`, `"not json at all"`, `{👍:[m2,"usr_deleted_since"],🚀:3}`. | step | exit | `sys_comment_reaction` rows | `sys_migration` row | column | |:--|:--|:--|:--|:--| | before | | 0 | none | present | | `os migrate apply --allow-destructive --yes --json` | 1 `refused_comment_reactions_unconverted` | 0 | none | present | | `os migrate comment-reactions --json` (dry run): 4 scanned, 5 readable, 5 to convert, 3 unreadable (`not_json`, `not_a_user_list`, `unknown_user`) | 1 | 0 | none | present | | `… --apply --yes --json`: 5 converted, `created_by` and `organization_id` as the reactor and the comment's org | 1 | 5 | `verified_at null`, `blocking 3` | present | | `… --apply` again: 0 converted, 5 already, `failures []` | 1 | 5 | `blocking 3` | present | | `os migrate apply --allow-destructive` | 1 refused (`blocking 3`) | 5 | | present | | operator clears one value, keeps the readable part of the other; `… --apply` | 0, verified | 5 | `verified_at` set, `blocking 0` | present | | `os migrate apply --allow-destructive --yes --json` | 0, `applied: sys_comment drop_column reactions` | 5 | | gone | | `pnpm dev:todo` on the same file | no `[schema-drift]` line; `GET /api/v1/data/sys_comment_reaction` as admin answers the 5 rows; admin `DELETE`s its own converted reaction: `200` | 4 | | gone | ## Pins (`packages/cli/src/commands/migrate/comment-reactions.integration.test.ts`) One real SQLite file under the SQL driver the commands boot, schema made by `os migrate apply --yes` (the served composition with an auth secret), then the 17.x column and values planted by hand. Seven ordered cases through the commands' own `run()`: (1) drop refused before any conversion, `{ message: 'refused_comment_reactions_unconverted', detail, conversion: null, applied: [] }`, exit 1, column present; (2) dry run: exact counts, the four unreadable entries with their comment ids, nothing written; (3) `--apply`: rows equal the expected set (same comment, emoji, user; comment's organization; `created_by` = reactor), unreadable listed, nothing for the unreadable comment, ledger `{ verified_at: null, blocking: 4 }`; (4) a re-run inserts nothing, `{ alreadyConverted: 5, converted: 0, failures: [] }`; (5) drop still refused with the recorded run under `conversion`; (6) after the operator resolves the values, verified, exit 0; (7) the drop proceeds and every row survives. ### Negative-pin ablations (fix committed first; each run through `scripts/ablation-replace.mjs` under the verify lock, with a `trap` restore; both restored to the HEAD blob `1c075685b2e1`, `git diff HEAD` empty) 1. **Idempotency guard removed** (`if (held.has(reactionKey(…)))` → `if (false && held.has(…))`, anchor 1 → 0, blob `1c075685b2e1` → `dd02d298f4c0`): red, 5 failed / 2 passed. Case 4: `- "alreadyConverted": 5 / + "alreadyConverted": 0`, `- "failures": [] / + "failures": [ { "code": "DUPLICATE_RECORD", "commentId": "rx_c1", "emoji": "👍", … } × 5 ]` — the declared unique index refused every duplicate; the report is what the pin reads. 2. **Unreadable report entry removed** (the parse-level `report.unreadable.push(…)` deleted, anchor 1 → 0, blob → `75af39952334`): red, 3 failed / 4 passed. Cases 2 and 3: the expected `not_json` / `not_a_user_id` / `not_a_user_list` entries missing, only `unknown_user` received. ## Local verification (at HEAD `bf15e3062a`, after merging `origin/main` at `5c9bfb0d3f`) - Build: `turbo run build --filter=!@objectstack/docs --concurrency=2` green (74 tasks) after the merge. - `pnpm --filter @objectstack/cli typecheck`: `tsc --noEmit` clean; `check:test-typecheck: OK` (3 files / 28 errors / 6 pinned signatures held in the existing ledger; the new test adds none). - `@objectstack/cli` integration, the three files this diff touches or depends on (`comment-reactions.integration.test.ts`, `apply.account-issuer-retirement.integration.test.ts`, `schema-migrate.one-shot-family.integration.test.ts`): 3 files, 99 tests passed. The seven new cases take 0.1 to 0.65 s each. - `@objectstack/cli` unit tier (`vitest run --project unit`): 286 files, 4288 tests passed. - `@objectstack/spec` `pnpm test`: 669 files, 20276 passed, 1 todo. `@objectstack/plugin-audit` (`sys-comment-retired-fields.test.ts`, `objects.test.ts`, `sys-comment-reactions-door.integration.test.ts`): 3 files, 22 passed. - Derived gates: `node scripts/pm/dispatch-gates.mjs --commands` (no paths) derived 118 families from 10 changed paths; all 118 ran, each exit 0. `--ran` reconciliation: `✓ dispatch-gates --ran: 118 derived famil(ies) accounted for — 118 run, 0 NOT-MEASURED (a DERIVED zero — all 118 recorded an exit code and none of them is 3).` Among them `check:upgrade-guide` (`✓ protocol-upgrade-guide.md generates from the ADR-0087 registries`), `check:spec-changes`, `check:doc-authoring`, `check:tenant-audit-census` (`OK -- 225 write call sites certified`), `check:nul-bytes` (`OK … no raw ASCII control bytes`), `check:adr-0087-registration` (`this PR adds no declared-breaking changeset`). - Lint, narrowed: `eslint --no-inline-config --format json` over the 8 changed `.ts` files: 8 files, 0 errors, 0 warnings. The population is the config's own: every changed file matches `**/*.{ts,…}` and none sits in `NEVER_LINTED`; `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot move the verdict on an untouched file. The repo-wide `pnpm lint` is CI's. ## Patch round 1 (at `42b3638b94`; the seat's amendment `6152677078`, the dev's report `6153407686`) - **Grading:** the card is `Clause-②: no (narrowing)`: the destructive apply refuses a drop it accepted before. The changeset now carries a bang summary, a **BREAKING** (accept-set narrowing) banner and the ADR-0087 disposition `not-required (already-registered sys-comment-reactions-column-retired)`. `check:adr-0087-registration` passes: `[BREAKING+bang+clause-②-narrowing] not-required (already-registered)`. - **The `--json` account-issuer refusals now exit 1.** This was measured first at the real command (`os migrate apply --allow-destructive --yes` on legacy-shaped SQLite databases). Under `--json`, `refused_account_issuer_collisions` and `refused_account_issuer_preflight_unreadable` exited `0`, while the human face exited `1`. Both now exit `1`. The pins are in `apply.account-issuer-json-refusal.test.ts`: exit 1, the `message`, and nothing written, with a human-face control and a clean-pre-flight control. - **Ablation:** with both sites put back to `0`, the two new pins go red (`AssertionError: expected +0 to be 1`) and both controls stay green. Restored: blob == HEAD (`4fe54313fc9f`), `git diff HEAD` empty. - **QA checklist:** `docs/qa/platform-checklist/areas/cli.json`, item `cli.migrate-plan-apply-json`, now lists the 16 subcommands `os migrate --help` registers. Added: `organization-ownership`, `security-catalog-overlays` and `comment-reactions`. - **Gates at `42b3638b94`:** 119 derived, 119 run. 118 exit 0. `check:platform-checklist` exits 1 on 8 symbol anchors outside this diff (`access-security.json`, `search.json`, `attachments-storage.json`). The same 8 lines appear without this PR's edit, and on `origin/main` `404fc477e5`. Per `lint.yml`, that gate runs as a daily watchdog on `main`, not per PR. - **Tests:** cli typecheck clean; cli integration 3 files, 99 passed; cli unit tier 287 files, 4292 passed. ## Acceptance notes - `os migrate apply --json`'s two `sys_account.issuer` pre-flight refusals (`refused_account_issuer_collisions`, `refused_account_issuer_preflight_unreadable`) emit with exit code 0 (`emitJson(…, 0, …)` then `return`), while this PR's sibling refusal exits 1. Read from the code, not measured at the door; noted, not filed. - `docs/qa/platform-checklist/areas/cli.json` (the migrate item) enumerates 13 `os migrate` subcommands as variants; the tree now registers more (`organization-ownership`, `security-catalog-overlays`, and this `comment-reactions` among them). A checklist sweep owns it; noted, not filed. - The CLI e2e `test/json-stdout-purity.e2e.test.ts` (nightly tier) gains the new command in its derived family; it was not run locally. --- _Generated by [Claude Code](https://claude.ai/code/session_01FzQy26KznSa5j2gnf3AzsE)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 37510aa commit 6673c80

12 files changed

Lines changed: 1521 additions & 21 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/cli': minor
3+
'@objectstack/spec': patch
4+
---
5+
6+
feat(cli)!: the v18 upgrade carries stored comment reactions — `os migrate comment-reactions` turns each reaction stored in the retired `sys_comment.reactions` column into a `sys_comment_reaction` record, and `os migrate apply --allow-destructive` refuses to drop that column until the conversion is verified
7+
8+
Clause-②: no (narrowing) — `os migrate apply --allow-destructive` now refuses the `sys_comment.reactions` drop it accepted before, until a verified conversion is recorded; the conversion itself only writes records of an existing object, so nothing widens.
9+
10+
<!-- adr-0087: not-required (already-registered sys-comment-reactions-column-retired) The narrowed input is the drop of the column that semantic entry retired, and the entry already reaches every upgrading deployment through `os migrate meta` and the upgrade guide. This changeset's prescription (convert, verify, then drop) is carried by that entry: this PR rewrites its replacement, reason and acceptance criteria to name the conversion and order it before the drop, with its id and surface unchanged. No spec key, export or stored shape moves. -->
11+
12+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
13+
14+
**What the upgrade now carries.** A database upgraded from 17.x keeps `sys_comment.reactions` and every emoji reaction stored in it (schema sync is additive). Those reactions are no longer lost. `os migrate comment-reactions` converts them into records:
15+
16+
- **One record per stored reaction.** Each (comment, emoji, user) in a stored map becomes one `sys_comment_reaction` row with the same comment, emoji and user, in the comment's organization, owned by the user who reacted. That user can remove it like any reaction made on v18. A user id listed twice under one emoji is one reaction.
17+
- **Nothing is guessed.** A stored value that is not JSON or not a map of emoji to user-id lists, an entry that is not a user id, an emoji longer than a reaction record holds, and a user id that names no `sys_user` row are each listed in the report with their comment id. Nothing is written for them. The readable entries of the same comment still convert.
18+
- **Idempotent.** A reaction that already has its record is skipped, so a second run writes nothing, and re-running finishes an interrupted run.
19+
- **Dry run by default.** `--apply` writes the records, reads the column back, and records the run in `sys_migration`. The run is recorded as verified only when every readable reaction has its record and no stored value is left unreadable. `--json` gives the full report. A run that lists an unreadable value or fails a write exits `1`, and so does an `--apply` run whose read-back finds anything unconverted.
20+
21+
**The column drop waits for the conversion (the narrowing).** Until a verified run is recorded, `os migrate apply --allow-destructive` refuses to drop `sys_comment.reactions` and exits `1` (`--json`: `message: "refused_comment_reactions_unconverted"`, with the recorded run under `conversion`). Nothing else is applied on that run. This also covers the boot drift line, which names that command as the remedy for the orphaned column. What to do when it refuses: run the conversion below until it reports the run verified, then re-run the apply.
22+
23+
**`os migrate apply --json` exits `1` when the `sys_account.issuer` retirement pre-flight refuses.** Its two refusals, `refused_account_issuer_collisions` and `refused_account_issuer_preflight_unreadable`, emitted their document with exit code `0`, so a caller gating on the status read a refused apply as a successful one. They now exit `1`, as the same refusals do without `--json`. The document is unchanged.
24+
25+
**Upgrade order:** take a backup, run `os migrate comment-reactions`, then `os migrate comment-reactions --apply`. If it lists unreadable values, correct each one in the database so it reads as a map of emoji to user ids, or clear it if it holds nothing to keep, and run `--apply` again. Once the run is verified, run `os migrate apply --allow-destructive` to drop the column. Run these with the flags and environment the deployment boots with (an auth secret set), so the audit plugin's objects are composed.
26+
27+
This supersedes the "stored reactions are not migrated" sentence in the entry that retired the column. `@objectstack/spec`: the protocol-18 semantic entry `sys-comment-reactions-column-retired` now names the conversion in its replacement, reason and acceptance criteria, and orders it before the drop. `os migrate meta` and the upgrade guide carry that text. Its id and surface are unchanged.

‎content/docs/deployment/cli.mdx‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,7 @@ where the data lives.
11531153
| `os migrate files-to-references` | Convert legacy file-field values to `sys_file` references, verify the ownership ledger, and record the deployment's migration flag |
11541154
| `os migrate value-shapes` | Scan stored reference and structured-JSON field values against the platform's value contract, and record the deployment's migration flag when clean |
11551155
| `os migrate summary-nulls` | Backfill roll-up `count` / `sum` columns still stored as `NULL` on parent rows created before the insert-time seed. Repairs values; no flag, nothing depends on it having run |
1156+
| `os migrate comment-reactions` | Carry the emoji reactions a 17.x deployment stored in the retired `sys_comment.reactions` column into `sys_comment_reaction` records, list every stored value that does not read, and record the verified run that lets `os migrate apply --allow-destructive` drop the column |
11561157
| `os migrate meta --stored` | Replay the metadata conversion chain over this deployment's `sys_metadata` rows and rewrite the ones still carrying a pre-protocol shape. Hygiene, not a gate — nothing depends on it having run |
11571158
| `os migrate security-catalog-overlays` | List the environment-wide permission-set and position rows (the legacy plurals `permissions` / `positions` included) stored over a name a configured package holds — the rows a v18 cold boot refuses — each with the package that holds its name; `--apply` deletes them, one history tombstone per row. Run it before the first v18 boot, with the flags and environment the deployment boots with: the platform security plugin's sets are held only where `os serve` composes it (an auth secret set, and the `auth` tier on), and the step takes `serve`'s `--preset` and `--dev` with `serve`'s meaning. Adopts nothing; exits 1 while it lists a row |
11581159
| `os migrate duplicates` | Report business identifiers already minted twice across the organization partitions, and the rows blocking the boot-time NULL-safe index tightenings — a read-only inventory as JSON on stdout. Renumbers nothing and writes no row and no schema; run it before the boot-time tenancy repair, which overwrites part of the evidence |
@@ -1349,6 +1350,49 @@ A deployment whose database was seeded fresh on this version has nothing to do
13491350
here: its parents were created with the value already in place, and the run
13501351
reports zero.
13511352
1353+
#### `os migrate comment-reactions`
1354+
1355+
v18 retired `sys_comment.reactions`, the JSON column that held every emoji
1356+
reaction to a comment as one map of emoji to user ids. A reaction is now its
1357+
reactor's own `sys_comment_reaction` record. Schema sync is additive, so a
1358+
database upgraded from 17.x keeps the column and every reaction stored in it,
1359+
and nothing reads it any more. This command carries those reactions over.
1360+
1361+
```bash
1362+
os migrate comment-reactions # Dry run: full report, writes no rows
1363+
os migrate comment-reactions --apply # Write the records and record the run (prompts)
1364+
os migrate comment-reactions --apply --yes --json # CI / scripts
1365+
```
1366+
1367+
**Each stored reaction becomes one record**: the same comment, emoji and user,
1368+
in the comment's organization, and owned by the user who reacted, so that user
1369+
can remove it like any reaction made on v18. A user id listed twice under one
1370+
emoji is one reaction. **A value that does not read is listed, never guessed
1371+
at**: a value that is not JSON, one that is not a map of emoji to user-id lists,
1372+
an entry that is not a user id, an emoji longer than a reaction record holds, or
1373+
a user id that names no `sys_user` row. Each is reported with its comment id
1374+
and converted into nothing, while the readable entries of the same comment
1375+
still convert. The report quotes up to 200 characters of each value; the column
1376+
keeps the whole of it.
1377+
1378+
**Idempotent**: a reaction that already has its record is skipped, so a second
1379+
run writes nothing, and re-running is how an interrupted run is finished.
1380+
1381+
**Convert, verify, then drop.** An `--apply` run reads the column again after
1382+
writing, and records a verified run in `sys_migration` only when every readable
1383+
reaction has its record and no stored value is left unreadable. Until it has,
1384+
`os migrate apply --allow-destructive` refuses to drop `sys_comment.reactions`,
1385+
and says why, because the drop would delete the reactions. The boot's drift
1386+
line names that drop as the remedy for the orphaned column, so following it
1387+
first is safe. When a value is listed as unreadable, correct it in the database
1388+
so it reads as a map of emoji to user ids, or clear it if it holds nothing to
1389+
keep, then run `--apply` again.
1390+
1391+
A run that lists an unreadable value or fails a write exits `1`, and so does an
1392+
`--apply` run whose read-back finds anything unconverted.
1393+
A database created on v18 never had the column: the run finds nothing, and
1394+
there is no drop to wait for.
1395+
13521396
#### A database created by this version needs no migration
13531397
13541398
A deployment whose database the platform **creates from empty** records these

‎docs/qa/platform-checklist/areas/cli.json‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@
204204
"title": "os migrate: bare command is a read-only plan, apply is safe-by-default, re-runs are idempotent, and --json exits 0 on success (#4873)",
205205
"since": "v15",
206206
"status": "active",
207-
"revision": 3,
207+
"revision": 4,
208208
"priority": "P1",
209209
"surface": "cli",
210210
"personas": ["operator (local shell)"],
@@ -285,14 +285,17 @@
285285
"account-issuer",
286286
"audit-metadata-bodies",
287287
"multi-value-columns",
288-
"unmapped-columns"
288+
"unmapped-columns",
289+
"organization-ownership",
290+
"security-catalog-overlays",
291+
"comment-reactions"
289292
],
290293
"traps": ["stale-dist"],
291294
"automated": { "kind": "unit", "ref": "packages/cli/src/utils/format.exit-code.test.ts" },
292295
"source": [
293296
"packages/cli/src/commands/migrate/index.ts + plan.ts (#2186 bare-command-is-plan; #3917 enforced never-mutates; occupancy warning) + apply.ts (--allow-destructive / --force / --yes / in_sync)",
294297
"packages/cli/src/utils/format.ts#CliExitCode (CliExitCode narrows the emitJson exit slot — the #4873 fix, commit 83df2fd)",
295-
"packages/cli/src/commands/migrate/ (the 13 registered subcommands under `os migrate`, besides the bare command, enumerated as variants — oclif pattern strategy, one default-exported Command per non-test file)",
298+
"packages/cli/src/commands/migrate/ (the 16 registered subcommands under `os migrate`, besides the bare command, enumerated as variants — oclif pattern strategy, one default-exported Command per non-test file)",
296299
"sibling item cli.migrate-duplicates-inventory (the duplicates subcommand's own contract — cross-referenced, not duplicated)"
297300
],
298301
"history": [
@@ -303,6 +306,12 @@
303306
"date": "2026-10-04",
304307
"change": "A5 / step 4 re-pointed: an in-sync showcase answers in_sync_partial (its federated showcase_external objects have no managed table and are never examined), and plan prints the PARTIAL warning rather than the in-sync line; plain in_sync is the answer only when nothing is unexamined (apply.ts / plan.ts in-sync early returns, schema-migration-plugins.ts coverage). Also the subcommand count: 13 registered under `os migrate`, not nine — the four missing (account-issuer, audit-metadata-bodies, multi-value-columns, unmapped-columns) are added to variants. Stale clause. Source 6c6157af05 (#13057)",
305308
"ref": "#21735"
309+
},
310+
{
311+
"revision": 4,
312+
"date": "2026-10-13",
313+
"change": "variants re-measured against the tree: `os migrate --help` lists 16 registered subcommands and the item named 13. Added the three it lacked: organization-ownership, security-catalog-overlays and comment-reactions (the v18 conversion of stored sys_comment.reactions into sys_comment_reaction records, whose verified run `apply --allow-destructive` waits for before dropping the column). The source note's count moves from 13 to 16 with them",
314+
"ref": "#23281"
306315
}
307316
]
308317
},

0 commit comments

Comments
 (0)