Repository navigation
feat(cli): os migrate organization-ownership reports the declared sharing-rule mirrors as advisory while this release's boot still seeds them - #23988
Conversation
…haring-rule mirrors as advisory while this release's boot still seeds them A served boot of this release still writes the package-declared sharing rules into sys_sharing_rule (plugin-sharing's seeder), organization-less under single -- the population the declared-rule-mirrors departure waits for. On this release that departure cannot happen, so the ceremony never certified a database whose packages declare sharing rules. DECLARED_SHARING_RULE_SEEDER_RUNS (the inventory) states that the seeder still runs; advisoryDepartureReason keys the rule on the table and the departure id. The plan marks those departures advisory, keeps the table's NOT NULL reading false with the reason, and warns before the first write; the apply's post-check counts their rows in advisory, never in remaining or blocking. Every other departure and remaining row still blocks. Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp Co-authored-by: Claude <noreply@anthropic.com>
…estore and its keying The existing "departures have not left" case reads the new rule: the one rule mirror is advisory, named and counted, the verdict is blocking 0 and nothing is deleted. A HotCRM-shaped fixture (eleven mirrors) pins blocking 0, the restore once the seeder retires (blocking 11 again), and that a host departure under the same id on its own table still blocks. Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp Co-authored-by: Claude <noreply@anthropic.com>
…haring rules, at the door and on the remote face A served boot of this release (verify's bootStack) of a package declaring one rule writes it organization-less as package-managed, which is what DECLARED_SHARING_RULE_SEEDER_RUNS states; that case fails until the constant flips when the seeder retires. Plan and --apply --json then certify HotCRM's shape (eleven mirrors): blocking 0, advisory 11, the sys_migration marker verified, every rule kept, and a served boot with the v18 boot guard composed starts while a v17-shaped table stands. The sqld remote-face case now carries eleven mirrors too. Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp Co-authored-by: Claude <noreply@anthropic.com>
… (cli minor) Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp Co-authored-by: Claude <noreply@anthropic.com>
…plans again; resume or restart both certify Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 29 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 37a78b1282b1a3d535221e4095f33ee05577d80d && git checkout 37a78b1282b1a3d535221e4095f33ee05577d80d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 022c6e799e88ae192e124c80e1cc343bda54d7bf 98f87644e16e6bd5894cfa3ae956ac640dc5eee9 && git checkout -B drift-repro 022c6e799e88ae192e124c80e1cc343bda54d7bf && git merge --no-ff 98f87644e16e6bd5894cfa3ae956ac640dc5eee9
node scripts/docs-audit/affected-docs.mjs --json 022c6e799e88ae192e124c80e1cc343bda54d7bf
|
Contract reviewServed-tier: Scope: owed by the ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
✅ ACCEPT: PR #23988 at
|
Fixes #23948
Clause-②: yes (widening — the apply report file, the plan file and both
--jsonpayloads gain anadvisoryfield on a departure entry, and the run'sadvisorycount includes that departure's rows)What changes
On a v17 database whose packages declare sharing rules,
os migrate organization-ownership --applyended withcomplete: true, blocking: 11and never certified. Theadr-0131-organization-ownershipmarker keptverified_atnull. The post-check waited for thedeclared-rule-mirrorsdeparture onsys_sharing_ruleto leave. That departure is the uncustomizedmanaged_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
9930e6cfruns frameworkf7e836da1ed3with HotCRM 5.0.0. Its tenant upgrade ran the ceremony tocomplete: trueand stopped onsys_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-mirrorsthe ceremony itself charts for this stage; any other NULL row keeps blocking". This PR implements that, and nothing wider:organization-ownership-plan.ts) marks a departureadvisorywhen 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 readingwillReceive: falsewithN row(s) stay NULL on this release, reported as advisory: LABEL. Before this change the plan readwillReceive: trueforsys_sharing_ruleon 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 ....organization-ownership-apply.ts) leaves advisory rows out ofremaining, so they are not discrepancies and notblocking. It counts them in the run'sadvisory, beside the unattributable rows, so the ledger verdict{ complete, blocking, advisory }carries them. Nothing new is deleted or attributed: the rows keeporganization_idNULL.organization-ownership.ts) printsN row(s) advisory, not held against certification — LABEL: REASONunder 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 wholeadvisorycount.global-rung-move,template-promotion, every host fate supplement departure (even one nameddeclared-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[](--jsonand the--outreport file):{ id, label, as, rows }gainsadvisory?: 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.advisoryand thesys_migrationrow'sadvisory: 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 sameadvisory?: string.plan.tables[].notNullfor the table reads as above, andplan.warningsandreport.warningsgain the sentence above.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 forsys_sharing_rule+declared-rule-mirrors, and only while the constant istrue. The flip point for #23952: set the constant tofalsein the change that retiresbootstrapDeclaredSharingRules. 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'sbootStack, which isos 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: onemanaged_by 'package', uncustomized, organization-less row whiletrue, and none oncefalse. When the seeder retires, the boot writes nothing, and the case fails with a message naming the constant until it flips.Alternatives weighed:
sharing-plugin.ts:775. A CLI test reading plugin-sharing's source is a cross-package read. It must be declared inscripts/cross-package-test-inputs.mjsandturbo.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.packages/plugins/plugin-sharing/**(C3 stage 9 (#15204): declared sharing rules read from the registry, bootstrapDeclaredSharingRules retires, and the ceremony deletes the declared-rule mirrors; the verified marker follows (director ordering ruling) #23952's, held off) and a new export on its published entry (refused for this card).Measurements
All on this branch unless stated. Base
161aee1047, head98f87644e1. Not merged withmain: the head merge-trees clean against it, including #23874'ssys_scim_providerrow in the same inventory file (disjoint hunks).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 asys_jobstill carryingorganization_id. Plan and--apply(--dev) gavecomplete: true, blocking: 11, advisory: 0and exit 1. The marker readverified_atNULL,applied_atset,blocking11. The unit-level positive control: the old test atorganization-ownership-apply.test.ts:885passed on unchanged code withblocking: 1.organization-ownership.declared-rule-mirrors.integration.test.ts, case 2).--apply --jsonexits 0 withcomplete: true, blocking: 0, advisory: 11. Thesys_sharing_ruledeparture carriesrows: 11withadvisory, andremainingis 0.sys_migrationreadsverified_atset,blocking0,advisory11. All 13 rule rows are kept, 11 still NULL. Then a served boot of the certified file starts, withOrganizationOwnershipBootGuardPlugincomposed andsys_job'sorganization_idput back so the guard has a v17-shaped table and decides on the marker.findV17ShapeTablesanswers['sys_job'], andreadOrganizationOwnershipMarker,os migrate apply's drop gate, readsverified: true.--applyand stopped atblocking: 11, both upgrade paths:checkpointed,resumedFromall nine, post-check only. Resultcomplete: true, blocking: 0, advisory: 11, the marker verified, 13 rules kept.f7e836da1ed3's inventory differs from this base's (git diff --stat38+/1- on the inventory file), andmainhas since added fix(cli): os migrate organization-ownership plans a v17 database still holding the retired sys_scim_provider, as fate 4 (ADR-0131 D10) #23874's row. Same fixture with the checkpoint's digest set to another value and the marker set back toblocking 11/verified_atNULL. Plan again, then--apply, with this change built overmain2e0e4517a6:restartedFromnames all nine steps, each runs again asdone(idempotent), and the result iscomplete: true, blocking: 0, advisory: 11, verified, 13 rules kept.@sqld/linux-x640.24.1-pre.42 (sqld 0.24.32), reached aslibsql://127.0.0.1:PORT?tls=0.organization-ownership.remote-face.integration.test.tsnow carries 11 organization-less declared rules. Plan, then--apply:blocking 0, the departure advisory withrows: 11, the markerapplied 1, verified 1, 11 NULL rules kept. Run locally withOS_TEST_SQLD_BINset; the test starts and stops its ownsqld. CI skips it without the binary.5c0aa5c752,scripts/ablation-replace.mjsreplaced(d.advisory ? 0 : d.rows)with(d.rows)indepartingRows(anchor 1 to 0, blobe3237c0dc629toa5f7d694d01e).organization-ownership-apply.declared-rule-mirrors.test.tsthen went red as predicted:expected [ true, 11, 11 ] to deeply equal [ true, 0, 11 ]([complete, blocking, advisory], so the apply'sblockingwas 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) andgit diff HEADempty. The subject resolves by relative import tosrc/, so no rebuild was involved.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 throughisDataMigrationFlagVerified:verified_atset andblocking === 0. None readsadvisory. The command writespassed = complete && blocking === 0(organization-ownership.ts:772). Soblocking 0certifies everywhere the marker is read.SharingServicePluginthroughcomposeProviderForDeclarations, withstart()suppressed (schema-migration-plugins.ts). The plan and apply runs leftsys_sharing_rulebyte-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 isos serve/os dev, the one the v18 refusal stops atkernel:ready, after plugin starts.sys_sharing_rule(dispatch item 4).willReceive: falsewith11 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 assertsblocking 0, the verdict, the departure'sadvisory,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-importsbetter-sqlite3), HotCRM's eleven:[true, 0, 11], nothing deleted, the customized rule attributed;advisoryDepartureReasonanswered withseederRuns: falsethrough avi.mockwrapper; the other cases go through the shipped constant), the same fixture givesblocking 11, the verdict not passing, and no warning;declared-rule-mirrorson 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
5c0aa5c752unless stated. Every build and test went throughos-verify-lock.sh, with the exit read from itsVERDICTline.pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2gave 305 files and 4558 tests passed,VERDICT command-exit 0.organization-ownership*,sqlite-wasm,schema-migrate.one-shot-family,schema-migrate.sql-driver-lookup), withOS_TEST_SQLD_BINset: 26 files and 342 tests passed,VERDICT command-exit 0.pnpm --filter @objectstack/cli typecheck(tsc --noEmitpluscheck:test-typecheck, the new test files included) on98f87644e1: exit 0, test layer unchanged at its pinned 3 files / 28 errors. The workspacedist/it read was built frommain2e0e4517a6plus this change.pnpm exec eslint --no-inline-configover the eight changed.tsfiles: exit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 66 families, and all 66 ran with exit 0 (--ranwith an exit code per line: 66 derived, 66 run, 0 NOT-MEASURED).check:dual-build-cjs-loadsandcheck:i18n-coveragefirst answered exit 3, PREREQUISITE NOT MET: packages outside the CLI closure had nodist/. 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 from5c0aa5c752. The families CI runs are cited from this PR's check-runs in the report.Acceptance notes
DECLARED_SHARING_RULE_SEEDER_RUNSin the change that retires the seeder. The served-boot case fails until then.mainbefore this change, unattributable rows (fate 4 reported rows, and fate 3 rows no anchor names under a wall) were alreadyadvisory, neverblocking:advisory = after.summary.unattributableRows. So the dispatch's suggested negative leg, "an unattributable non-mirror row still keeps blocking > 0", does not hold onmain. 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.os servere-runsdefineRule(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; C3 stage 9 (#15204): declared sharing rules read from the registry, bootstrapDeclaredSharingRules retires, and the ceremony deletes the declared-rule mirrors; the verified marker follows (director ordering ruling) #23952's step deletes them all.complete: truemeant the column drops had run, sofindV17ShapeTablesfound nothing and the guard never read the marker. The stuckverified_atmattered 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).sqldis present. Neither declares a test budget, and no class fits.Generated by Claude Code