Repository navigation
fix(cli): os db orphans sweeps details whose controlled-by-parent master is optional - #31226
Conversation
…ter is optional The sweep read only required master-detail fields, so a detail of a controlled_by_parent object whose one master is optional (required: false) was never reported when it named a master that no longer exists. Read that optional master through controlledByParentMasterOf from @objectstack/spec/data; a row that names no master stays valid and unlisted. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5madiyLr75cvc8cS31
…e top Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5madiyLr75cvc8cS31
📓 Docs Drift CheckThis PR changes 1 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 9 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 35 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 ad434ba7ca2f3016f9f74ef78a19ae642667dcd3 && git checkout ad434ba7ca2f3016f9f74ef78a19ae642667dcd3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fe97d85b060b8314880e8ee5f6f897ffbb300bcd ec53068182c0b15fd57552fb774176ab8253919c && git checkout -B drift-repro fe97d85b060b8314880e8ee5f6f897ffbb300bcd && git merge --no-ff ec53068182c0b15fd57552fb774176ab8253919c
node scripts/docs-audit/affected-docs.mjs --json fe97d85b060b8314880e8ee5f6f897ffbb300bcd
|
|
domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ 维护者速读
席位意见:接受。判定只多认「 席位核对 — ACCEPT ·
|
Requested by Leehom · project thread
Fixes #30976
Clause-②: no
Metadata-protocol: none
维护者速读
os db orphans现在也扫描「主表可选」的子对象(controlled_by_parent对象唯一的master_detail声明为required: false,controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only #30905 裁决 A 放行的写法):这类行若指向一条已不存在的主记录,会被列为孤儿(--delete时先导出再随级联删除);主表字段为空的行是合法的,不列出、不删除。required: true的主表字段(spec 的isControlledByParentMasterField),PR feat(spec,security): a controlled_by_parent object may declare an optional master #30940 引入可选主表后,这类悬空行既没有任何用户入口能读到(访问权从不存在的主记录派生),os db orphans也看不见。controlledByParentMasterOf,不改 spec。回滚即还原本 PR。packages/cli/src/utils/orphan-detail-sweep.ts的masterFieldsOf(新判定,含sharingModel限定);packages/cli/src/commands/db/orphans.integration.test.ts新夹具od_tip与改动的删除断言;orphan-detail-sweep.test.ts新 describe。pnpm --filter @objectstack/cli exec vitest run src/commands/db/orphans.integration.test.ts,预期 10 个用例全绿,其中[#30976]用例证明空主表行t_none不被列出、悬空行t_orphan被列为孤儿。What changes
packages/cli/src/utils/orphan-detail-sweep.ts— the sweep's "which field is a master" judgement moves into one local predicate,masterFieldsOf(object):master_detail(isControlledByParentMasterField, unchanged — the CONTROL);controlled_by_parentobject, its OPTIONAL master:controlledByParentMasterOf(fields)from@objectstack/spec/dataansweringoptional: true(the onemaster_detail, declaredrequired: false, with no required one beside it). The spec function reads any field map and leaves the sharing model to the caller, so the sweep askssharingModel === 'controlled_by_parent'itself: only such an object derives its rows' access from that field (plugin-security's cbp read filter,security-plugin-cbp-read-filter.ts, returns no filter for any other model).objectsWithRequiredMaster→objectsWithMaster,isRequiredMasterFinding→isMasterFinding(module-internal to the CLI; not on anyexportssubpath). Docblocks and the "other dangling" note no longer say "required master-detail".null/undefined/''as "no link" (packages/objectql/src/integrity/dangling-reference-audit.ts, "Empty is not a reference"), so the sweep adds no null check of its own.packages/cli/src/commands/db/orphans.ts— the command'sdescription, docblock and human heading said "required master-detail master"; that became false with this change, so they now say "master" (description: "Report detail rows whose master-detail reference names a master that does not exist, …").content/docs/deployment/cli.mdx— theos db orphanstable row and section say the same, and name the optional master and the empty-master row..changeset/30976-orphans-optional-master.md—@objectstack/clipatch.No
packages/specedit: the optional-master judgement already exists there (controlledByParentMasterOf, public since PR #30940); the order's stop-line (a new spec predicate) is not reached.Tests
packages/cli/src/utils/orphan-detail-sweep.test.ts(unit, fake read-only engine):sw_optional(cbp, onemaster_detailrequired: false, plus a lookup),sw_optional_private(the same lone optionalmaster_detailon aprivateobject) andsw_required_cbp(cbp with a required master and an optionalmaster_detailbeside it);sw_optionalandsw_required_cbpread,sw_optional_privatenot;[#30976]describe:masterFieldsOfper object (CONTROL:sw_detailstill answers its two required masters), and a dangling optional master is an orphan while a dangling lookup on the same object and a dangling optionalmaster_detailbeside a required master stay inotherDangling, never deletable.packages/cli/src/commands/db/orphans.integration.test.ts(integration, real boot + SQLite): fixture gainsod_tip, cbp with an optional master onod_campaign, seeded through a system write with three rows —t_none(no campaign),t_live(the live campaign),t_orphan(the missing campaign).[#30976] an optional master: …:t_orphanis an orphan;t_noneandt_liveappear in neitherorphansnorotherDangling;od_tip/t_orphan(countsorphans: 2),--deleteremoves it and keepst_noneandt_live(od_tip: ['t_live', 't_none']after the run), the export holds it, and in the restrict case the unrestrictedod_tiporphan is deleted whileod_memberis refused as before.@objectstack/service-datasource,driver-turso,runtime,platform-objects/plugin,plugin-security,rest) —check:test-source-aliasasks them of an edited test file whose source reaches those dynamic imports, copyingserve-host-crypto-provider.test.ts.Runs
orphan-detail-sweep.test.ts,orphans.integration.test.ts,orphans.no-auto-run.test.ts) on headec530681:Test Files 3 passed (3) · Tests 25 passed (25).pnpm --filter @objectstack/cli exec vitest run --project uniton0967087c(theorigin/mainmerge;ec530681only adds the six imports to the integration file):Test Files 2 failed | 396 passed (398) · Tests 5595 passed | 29 skipped. The two files weretest/published-subpath-console.pin.test.tsandtest/published-subpath-hook-body.pin.test.ts, refusing "packages/cli is not built (./dist/index.js is absent)"; afterpnpm --filter @objectstack/cli buildboth ran:Test Files 2 passed (2) · Tests 29 passed (29).pnpm --filter @objectstack/cli typecheck(tsc --noEmit && pnpm check:test-typecheck): exit 0.Ablations (both restored to the HEAD blob, `git diff HEAD` empty after each)
node scripts/ablation-replace.mjs --file packages/cli/src/utils/orphan-detail-sweep.ts --anchor 'if (master?.optional === true) out.push(master.field);' --delete(anchor 1 → 0, blob7e854c86→766c6ecb), running the unit file and the integration file: 8 failed | 10 passed (18). Red: the three unit cases above, e.g.AssertionError: expected [] to deeply equal [ { object: 'sw_optional', …(3) } ], and five integration cases, e.g.[#30976] an optional master: …AssertionError: expected [ 'od_member', 'od_member_note', …(1) ] to include 'od_tip'and the report caseexpected [ { object: 'od_member', …(3) } ] to deeply equal [ { object: 'od_member', …(3) }, …(1) ]. The CONTROL case ("an orphan is a dangling REQUIRED master-detail …") stayed green under the ablation.controlled_by_parentlimit (negative pin: a lone optionalmaster_detailon aprivateobject is not a master) — anchorif (def?.sharingModel === 'controlled_by_parent') {replaced byif (def !== undefined || def?.sharingModel === 'controlled_by_parent') {(blob7e854c86→9bc35e09), unit file: 2 failed | 6 passed (8) —expected [ 'master' ] to deeply equal []forsw_optional_private, and the objects list with one extra entry.The CLI suites resolve
orphan-detail-sweep.tsandorphans.tsfromsrc/(relative imports), so no build leg was needed.Acceptance notes
origin/main658d84f5,isControlledByParentMasterField(packages/spec/src/data/controlled-by-parent-master.ts) answerstype === 'master_detail' && required === trueonly; PR feat(spec,security): a controlled_by_parent object may declare an optional master #30940 left it a required-master predicate and addedcontrolledByParentMasterOfbeside it for the optional master. The sweep called only the former.master_detailcascades or restricts (deleteBehavior: 'set_null'on it is a parse-time refusal,field.zod.ts), so the master's own delete would have removed such a row or been refused while it stood. Nulling the reference instead would be a different remedy and is not offered.required: falsemaster_detailon a non-cbp object, and one beside a required master on a cbp object, stay "other dangling" — named, never deleted. A row with an empty optional master is never read as an orphan.orphan-detail-sweep.ts+ its test):orphans.tstext,content/docs/deployment/cli.mdx, andorphans.integration.test.ts. The first two said "required master-detail master", which this change makes false; the integration file is the only place the null-master half can fail for real (the unit file's audit is a fake, and the audit is what skips empty values). Declared in the report.Verification
Gate union derived by
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(92 families for this diff, the same list at0967087candec530681), run on headec530681, each exit code captured before any pipe, reconciled with--ran:--ran: "92 derived famil(ies) accounted for — 92 run, 0 NOT-MEASURED (a DERIVED zero — all 92 recorded an exit code and none of them is 3)".pnpm --filter '@objectstack/cli^...' build(VERDICT command-exit 0),pnpm --filter @objectstack/cli build, and the build closurecheck:i18n-coveragenames plus@objectstack/client/@objectstack/client-reactforcheck:skill-examples(turbo, VERDICT command-exit 0).check-plugin-teardown-shape.mjs --self-testneededgit fetch --depth=1 origin 621a4876for its pinned fixture on this shallow clone.0967087c, answeredcheck:test-source-aliasexit 1 — the edited integration file reached six dynamic imports with no module-top load — fixed inec530681(the six imports above), then exit 0; and four exit 3 PREREQUISITE NOT MET (the three above pluscheck:dual-build-cjs-loads, which on the second round found enough builtdist/to measure: "116 published require entry point(s) across 71 package(s) load").pnpm lintscoped:node --stack-size=4000 node_modules/eslint/bin/eslint.js packages/cli --no-inline-config, exit 0 (the package has nolintscript); re-run on the edited integration file atec530681, exit 0.node scripts/check-platform-checklist.mjs --changed-from origin/main(two module-internal renames): exit 0, "0 anchored files in the change set".Generated by Claude Code