Repository navigation
Commit adce192
feat(cli): os migrate organization-ownership reports the declared sharing-rule mirrors as advisory while this release's boot still seeds them (#23988)
Fixes #23948
Clause-②: yes (widening — the apply report file, the plan file and both
`--json` payloads gain an `advisory` field on a departure entry, and the
run's `advisory` count includes that departure's rows)
## What changes
On a v17 database whose packages declare sharing rules, `os migrate
organization-ownership --apply` ended with `complete: true, blocking:
11` and never certified. The `adr-0131-organization-ownership` marker
kept `verified_at` null. The post-check waited for the
`declared-rule-mirrors` departure on `sys_sharing_rule` to leave. That
departure is the uncustomized `managed_by in (package, platform)` rows
with no organization. A served boot of this release writes exactly those
rows, so on this release they never leave.
**Reach.** Measured on cloud staging by the cloud#2868 PM (card comment
6218950893, 15:32Z): environment `9930e6cf` runs framework
`f7e836da1ed3` with HotCRM 5.0.0. Its tenant upgrade ran the ceremony to
`complete: true` and stopped on `sys_sharing_rule: 11 row(s) remain
(uncustomized declared sharing rules — …: 11)`, the environment's only
blocker. All 10 staging environments with HotCRM and the 5 production
demos are expected to land there.
The maintainer's order (quoted on the card) is 「框架先放行这一项」:
"在启动流程还会种声明共享规则的版本上,仪式不把这些镜像当成阻断项,只列为提示;等 D2 随 #15204 落地后再恢复阻断". The
director's ordering ruling for C3 stage 9 (#23952, "The order (ruled)")
scopes it: the zero-NULL gate "is read as satisfied by a D10 report
whose only remaining NULL rows are the `declared-rule-mirrors` the
ceremony itself charts for this stage; any other NULL row keeps
blocking". This PR implements that, and nothing wider:
- **The plan** (`organization-ownership-plan.ts`) marks a departure
`advisory` when no step of this release can carry its rows out
(`PlannedDeparture.advisory`: why, and what removes them). Their NULL
rows keep the table's NOT NULL reading `willReceive: false` with `N
row(s) stay NULL on this release, reported as advisory: LABEL`. Before
this change the plan read `willReceive: true` for `sys_sharing_rule` on
such a database, which was not true on this release. One warning per
such departure tells the operator before the first write, in the plan
and again in `--apply`: `sys_sharing_rule holds N organization-less
row(s) of LABEL, which stay: REASON ...`.
- **The apply's post-check** (`organization-ownership-apply.ts`) leaves
advisory rows out of `remaining`, so they are not discrepancies and not
`blocking`. It counts them in the run's `advisory`, beside the
unattributable rows, so the ledger verdict `{ complete, blocking,
advisory }` carries them. Nothing new is deleted or attributed: the rows
keep `organization_id` NULL.
- **The command's human summary** (`organization-ownership.ts`) prints
`N row(s) advisory, not held against certification — LABEL: REASON`
under the table. Its closing warnings now name the unattributable rows
and the advisory rows separately. Before, it said "N row(s) whose owner
cannot be derived" for the whole `advisory` count.
- **What still blocks:** `global-rung-move`, `template-promotion`, every
host fate supplement departure (even one named `declared-rule-mirrors`:
the rule is keyed on the table AND the id), and every other remaining
row.
### The `--json` / report-file / plan-file shape change (additive)
- `report.tables[].departures[]` (`--json` and the `--out` report file):
`{ id, label, as, rows }` gains `advisory?: string`. Present means these
rows stay on this release and do not hold certification. Absent means
the departure blocks, exactly as before.
- `report.tables[].remaining`: no longer counts an advisory departure's
rows.
- `report.advisory` and the `sys_migration` row's `advisory`:
unattributable rows plus advisory departure rows. The field's doc
already said "and rows held for another stage", and now the count
includes them.
- `plan.tables[].departures[]` (plan file and plan `--json`): the same
`advisory?: string`. `plan.tables[].notNull` for the table reads as
above, and `plan.warnings` and `report.warnings` gain the sentence
above.
- The inventory digest is unchanged: `05ba6e8512e2…` before and after
this change. The verdict rule is not inventory data. So a plan reviewed
on the parent commit still executes, and an interrupted ceremony still
resumes.
## The restore condition (the hand-off to #23952)
`DECLARED_SHARING_RULE_SEEDER_RUNS`
(`packages/cli/src/utils/organization-ownership-inventory.ts`, `true`)
states that a served boot of this release still seeds declared sharing
rules. `advisoryDepartureReason(object, departureId)` beside it is the
one rule: it answers only for `sys_sharing_rule` +
`declared-rule-mirrors`, and only while the constant is `true`. **The
flip point for #23952:** set the constant to `false` in the change that
retires `bootstrapDeclaredSharingRules`. From then the mirrors block
again until that stage's ceremony step deletes them.
What makes that flip mechanical rather than remembered is a behavioural
pin, not a source-text pin.
`organization-ownership.declared-rule-mirrors.integration.test.ts`, case
1, boots a served stack of this release over a SQLite file:
`@objectstack/verify`'s `bootStack`, which is `os serve`'s composition
with plugin starts. The package declares one sharing rule. The case
asserts the rows that boot wrote equal what the constant says: one
`managed_by 'package'`, uncustomized, organization-less row while
`true`, and none once `false`. When the seeder retires, the boot writes
nothing, and the case fails with a message naming the constant until it
flips.
Alternatives weighed:
- **A source pin over `sharing-plugin.ts:775`.** A CLI test reading
plugin-sharing's source is a cross-package read. It must be declared in
`scripts/cross-package-test-inputs.mjs` and `turbo.json`, both outside
this claim's surface. It also pins a call site rather than the
behaviour: a retirement that kept the call but emptied the seeder would
leave it green.
- **A capability that plugin-sharing exports.** That needs an edit under
`packages/plugins/plugin-sharing/**` (#23952's, held off) and a new
export on its published entry (refused for this card).
- **Reading the ADR-0087 registry entry #23952 will write for the
seeder's retirement.** That entry does not exist yet. Keying on a
guessed id is a version string in disguise.
- **A version string.** The card prefers otherwise.
## Measurements
All on this branch unless stated. Base `161aee1047`, head `98f87644e1`.
Not merged with `main`: the head merge-trees clean against it, including
#23874's `sys_scim_provider` row in the same inventory file (disjoint
hunks).
- **Positive control on unchanged code, at the door** (CLI dist built at
`161aee1047`). The served boot plus 17.x leftovers fixture is HotCRM's
shape: ten declared rules a 17.x seeder left, plus one the served boot
of this release seeded, plus an organization's rule, a customized
declared rule and a `sys_job` still carrying `organization_id`. Plan and
`--apply` (`--dev`) gave `complete: true, blocking: 11, advisory: 0` and
exit 1. The marker read `verified_at` NULL, `applied_at` set, `blocking`
11. The unit-level positive control: the old test at
`organization-ownership-apply.test.ts:885` passed on unchanged code with
`blocking: 1`.
- **The fix, at the door**
(`organization-ownership.declared-rule-mirrors.integration.test.ts`,
case 2). `--apply --json` exits 0 with `complete: true, blocking: 0,
advisory: 11`. The `sys_sharing_rule` departure carries `rows: 11` with
`advisory`, and `remaining` is 0. `sys_migration` reads `verified_at`
set, `blocking` 0, `advisory` 11. All 13 rule rows are kept, 11 still
NULL. Then a served boot of the certified file starts, with
`OrganizationOwnershipBootGuardPlugin` composed and `sys_job`'s
`organization_id` put back so the guard has a v17-shaped table and
decides on the marker. `findV17ShapeTables` answers `['sys_job']`, and
`readOrganizationOwnershipMarker`, `os migrate apply`'s drop gate, reads
`verified: true`.
- **A database that already ran `--apply` and stopped at `blocking:
11`**, both upgrade paths:
- *Same inventory digest (resume).* The pre-fix fixture above, re-run
with this branch's CLI and the same reviewed plan file: every step
`checkpointed`, `resumedFrom` all nine, post-check only. Result
`complete: true, blocking: 0, advisory: 11`, the marker verified, 13
rules kept.
- *Another digest (restart).* This is staging's case: `f7e836da1ed3`'s
inventory differs from this base's (`git diff --stat` 38+/1- on the
inventory file), and `main` has since added #23874's row. Same fixture
with the checkpoint's digest set to another value and the marker set
back to `blocking 11` / `verified_at` NULL. Plan again, then `--apply`,
with this change built over `main` `2e0e4517a6`: `restartedFrom` names
all nine steps, each runs again as `done` (idempotent), and the result
is `complete: true, blocking: 0, advisory: 11`, verified, 13 rules kept.
- **Remote face (sqld).** `@sqld/linux-x64` 0.24.1-pre.42 (`sqld
0.24.32`), reached as `libsql://127.0.0.1:PORT?tls=0`.
`organization-ownership.remote-face.integration.test.ts` now carries 11
organization-less declared rules. Plan, then `--apply`: `blocking 0`,
the departure advisory with `rows: 11`, the marker `applied 1, verified
1`, 11 NULL rules kept. Run locally with `OS_TEST_SQLD_BIN` set; the
test starts and stops its own `sqld`. CI skips it without the binary.
- **Ablation** (the card's negative control). On committed `5c0aa5c752`,
`scripts/ablation-replace.mjs` replaced `(d.advisory ? 0 : d.rows)` with
`(d.rows)` in `departingRows` (anchor 1 to 0, blob `e3237c0dc629` to
`a5f7d694d01e`).
`organization-ownership-apply.declared-rule-mirrors.test.ts` then went
red as predicted: `expected [ true, 11, 11 ] to deeply equal [ true, 0,
11 ]` (`[complete, blocking, advisory]`, so the apply's `blocking` was
back at 11). The keyed case went red as `[ true, 13, 11 ]`, and the
restore case stayed green. The restore was proven: blob equal to HEAD
(`e3237c0dc629`) and `git diff HEAD` empty. The subject resolves by
relative import to `src/`, so no rebuild was involved.
- **Every reader of the marker** (dispatch item 3). The boot guard
(`organization-ownership-boot-guard.ts:260`), the drop gate
(`organization-ownership-drop-gate.ts:68`), `platform-objects`'
`isDataMigrationVerified`, and objectql's two reads (`engine.ts:11525`,
`:11685`) all go through `isDataMigrationFlagVerified`: `verified_at`
set and `blocking === 0`. None reads `advisory`. The command writes
`passed = complete && blocking === 0` (`organization-ownership.ts:772`).
So `blocking 0` certifies everywhere the marker is read.
- **What the ceremony's own boot does to those rows** (dispatch item 2).
Nothing. The apply boot composes `SharingServicePlugin` through
`composeProviderForDeclarations`, with `start()` suppressed
(`schema-migration-plugins.ts`). The plan and apply runs left
`sys_sharing_rule` byte-identical, apart from the customized rule's
attribution. A served boot does seed: measured in case 1 and in the
fixture's eleventh row. That boot is `os serve` / `os dev`, the one the
v18 refusal stops at `kernel:ready`, after plugin starts.
- **The NOT NULL reading for `sys_sharing_rule`** (dispatch item 4).
`willReceive: false` with `11 row(s) stay NULL on this release, reported
as advisory: uncustomized declared sharing rules — …`, in both the plan
and the report. The ceremony has no NOT NULL DDL step; the reading is
the report's statement, and it stays true.
## Tests
- `src/utils/organization-ownership-apply.test.ts`: the test formerly
titled "does not certify a ceremony whose departures have not left: the
declared sharing-rule mirrors keep verified_at null" now reads
"certifies a ceremony whose only remaining NULL rows are the declared
sharing-rule mirrors this release still seeds: advisory, named, kept".
It asserts `blocking 0`, the verdict, the departure's `advisory`,
`remaining 0`, the NOT NULL reason, the warning and the rows kept.
- `src/utils/organization-ownership-apply.declared-rule-mirrors.test.ts`
(new, integration tier: it value-imports `better-sqlite3`), HotCRM's
eleven:
- certifies: `[true, 0, 11]`, nothing deleted, the customized rule
attributed;
- **the restore pin**: with the seeder retired
(`advisoryDepartureReason` answered with `seederRuns: false` through a
`vi.mock` wrapper; the other cases go through the shipped constant), the
same fixture gives `blocking 11`, the verdict not passing, and no
warning;
- the negative leg: a host fate supplement departure named
`declared-rule-mirrors` on its own table still blocks (`blocking 2`)
beside the advisory mirrors.
-
`src/commands/migrate/organization-ownership.declared-rule-mirrors.integration.test.ts`
(new, integration tier): the served-boot tie (case 1) and the door (case
2) above.
-
`src/commands/migrate/organization-ownership.remote-face.integration.test.ts`:
11 mirrors in its 17.x leftovers, and their advisory departure asserted.
## Local runs
All on `5c0aa5c752` unless stated. Every build and test went through
`os-verify-lock.sh`, with the exit read from its `VERDICT` line.
- CLI unit tier: `pnpm --filter @objectstack/cli exec vitest run
--project unit --maxWorkers=2` gave 305 files and 4558 tests passed,
`VERDICT command-exit 0`.
- Every test file reaching the ceremony's modules, both tiers (26 files:
`organization-ownership*`, `sqlite-wasm`,
`schema-migrate.one-shot-family`, `schema-migrate.sql-driver-lookup`),
with `OS_TEST_SQLD_BIN` set: 26 files and 342 tests passed, `VERDICT
command-exit 0`.
- `pnpm --filter @objectstack/cli typecheck` (`tsc --noEmit` plus
`check:test-typecheck`, the new test files included) on `98f87644e1`:
exit 0, test layer unchanged at its pinned 3 files / 28 errors. The
workspace `dist/` it read was built from `main` `2e0e4517a6` plus this
change.
- `pnpm exec eslint --no-inline-config` over the eight changed `.ts`
files: exit 0.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 66 families, and all 66 ran with
exit 0 (`--ran` with an exit code per line: 66 derived, 66 run, 0
NOT-MEASURED). `check:dual-build-cjs-loads` and `check:i18n-coverage`
first answered exit 3, PREREQUISITE NOT MET: packages outside the CLI
closure had no `dist/`. After building those packages, both answered 0.
After the last commit (`98f87644e1`, which changes only the changeset
text), the 20 families the derivation names for the changeset file ran
again on that head, all exit 0. The other 46 read none of the changed
bytes, and their readings stand from `5c0aa5c752`. The families CI runs
are cited from this PR's check-runs in the report.
## Acceptance notes
- **#23952's conflict rule stands and is not implemented here.** "The
ceremony deletes the mirrors" (its item 4) is that stage's. This PR
deletes nothing. #23952 remains open, and its executor flips
`DECLARED_SHARING_RULE_SEEDER_RUNS` in the change that retires the
seeder. The served-boot case fails until then.
- **The ruling's "any other NULL row keeps blocking" against today's
unattributable rows.** On `main` before this change, unattributable rows
(fate 4 reported rows, and fate 3 rows no anchor names under a wall)
were already `advisory`, never `blocking`: `advisory =
after.summary.unattributableRows`. So the dispatch's suggested negative
leg, "an unattributable non-mirror row still keeps blocking > 0", does
not hold on `main`. The negative leg here is another departure that has
not left. Whether the ruling's sentence means to make unattributable
rows block the marker is the ruling owner's reading; this PR changes
neither.
- **After certification the served boot keeps seeding.** Every `os
serve` re-runs `defineRule` (an upsert by name and organization), and a
rule a package adds later lands as a new organization-less row. The
marker is not re-read at boot, so nothing re-blocks; #23952's step
deletes them all.
- **The boot guard was not what stopped these databases.** `complete:
true` meant the column drops had run, so `findV17ShapeTables` found
nothing and the guard never read the marker. The stuck `verified_at`
mattered to the marker's other readers: `os migrate apply`'s drop gate
here, and the certification reader cloud's upgrade uses
(objectstack-ai/cloud#2868, outside this repository).
- **Wall time.** The new door file ran in about 18 s of tests plus about
37 s of import on this shared box: four boots, plan, apply and two
served boots. The remote-face file is similar when `sqld` is present.
Neither declares a test budget, and no class fits.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d050a8f commit adce192
9 files changed
Lines changed: 728 additions & 31 deletions
File tree
- .changeset
- packages/cli/src
- commands/migrate
- utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
31 | 33 | | |
32 | 34 | | |
33 | 35 | | |
| |||
77 | 79 | | |
78 | 80 | | |
79 | 81 | | |
| 82 | + | |
| 83 | + | |
80 | 84 | | |
81 | 85 | | |
82 | 86 | | |
| |||
204 | 208 | | |
205 | 209 | | |
206 | 210 | | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
207 | 220 | | |
208 | 221 | | |
209 | 222 | | |
| |||
233 | 246 | | |
234 | 247 | | |
235 | 248 | | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
236 | 253 | | |
237 | 254 | | |
238 | 255 | | |
| |||
244 | 261 | | |
245 | 262 | | |
246 | 263 | | |
| 264 | + | |
247 | 265 | | |
248 | | - | |
| 266 | + | |
249 | 267 | | |
250 | 268 | | |
0 commit comments