Repository navigation
fix(objectql)!: refuse at boot a controlled_by_parent detail whose master the engine cannot compose a read over - #25166
Conversation
…e master at boot Claude-Session: https://claude.ai/code/session_01Dw4pQznF7eB7VtCsFQXMAH Co-authored-by: Claude <noreply@anthropic.com>
… over an unservable master Claude-Session: https://claude.ai/code/session_01Dw4pQznF7eB7VtCsFQXMAH Co-authored-by: Claude <noreply@anthropic.com>
…boot refusal BREAKING Claude-Session: https://claude.ai/code/session_01Dw4pQznF7eB7VtCsFQXMAH Co-authored-by: Claude <noreply@anthropic.com>
…ntrolled-by-parent-unservable-master-refused
📓 Docs Drift CheckThis PR changes 1 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 11 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 18 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 63415434f143c07588b907206beccfb618c04622 && git checkout 63415434f143c07588b907206beccfb618c04622
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 425c743fd9b80005d83e528dfdc8a5f8c68c8b30 0f5d9a6bce5d740326bb7ba06291ddcff428d9de && git checkout -B drift-repro 425c743fd9b80005d83e528dfdc8a5f8c68c8b30 && git merge --no-ff 0f5d9a6bce5d740326bb7ba06291ddcff428d9de
node scripts/docs-audit/affected-docs.mjs --json 425c743fd9b80005d83e528dfdc8a5f8c68c8b30
|
Contract reviewServed-tier: Scope: Inputs read as the contract: card #25144 (Wanted 1-5, Acceptance), ruling ① Derived judgments1.
2.
3.
4.
5.
6. In-repo controls outside the test. The three in-repo 7. ② Semver level
③ Boundary flagsEach of the dev's open questions and deviations, answered; then what the seat decides.
Nothing must be fixed before landing. Items 2 and the ① item 2 nit are follow-ups, not conditions. Implemented-by: VERDICT: PASS |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #25144. 4 files, +343/−8. Not governed: check-governed-merges matched 0 of 4 paths. |
| Scope | The boot refuses a controlled_by_parent detail whose one required master is bound to another datasource (ADR-0139 D5.3) or narrowed by an object-scoped read middleware (D5.2). The refusal is thrown from ObjectQLPlugin's kernel:bootstrapped handler, before kernel:listening opens the socket. It names each detail, master field, master and class, and the three remedies. The read path's two checks and the boot rule share one private predicate, so they cannot drift. |
| Seat's own read | The placement follows the dev's measurement: mailbox, audit and storage register their object-scoped middleware inside their own kernel:ready, after the engine's, so kernel:bootstrapped is the first point that sees both facts. It sits in packages/objectql, inside the claim's fence, so no fork was owed. The read-time refusal is unchanged and still covers what the boot rule does not see. |
| Review | CONTRACT_REVIEW_TIER (narrowing face). Record 6332548600 is PASS on this head, with no blocking findings. |
| Declaration | One changeset, @objectstack/objectql patch, with Clause-②: yes (narrowing) and Metadata-protocol: none at line starts, as on the PR body. It keeps the BREAKING banner, the Remedy section and the one ADR-0087 marker, not-required (no-migration-prescription). |
| Cross-lane | None edited. The middleware owners (service-mailbox, plugin-audit, service-storage, plugin-approvals, plugin-security) were read, not changed. #24932 (domain:services) merges after this. |
| Docs drift | The bot lists 14 hand-written pages. None states that a controlled_by_parent detail over a master on another datasource or behind an object-scoped read middleware boots, and none restates D5's read-time refusal, so none is falsified. The 11 release-owned pages are read-only and are not edited here. |
| Gates | Dev at 0f5d9a6bc: 71 derived, 71 run, all exit 0. objectql passed 8645/8645 tests and the new pins 8/8. Ablation (the throw removed): the 4 class pins red and the 4 controls green, restored to HEAD. runtime passed 6805 tests (19 skipped). Dogfood battery on e0ca492fd, the pre-merge head (the merge brings no objectql or runtime file): 2395 passed, 18 skipped, no failures. |
| CI | At this read on 0f5d9a6bce: ci-failure GREEN, all 41 check-runs completed, none failed. git merge-tree against today's origin/main (425c743fd9) is clean. |
Disposition:
open_questions: all three answered A by the seat.- Envelope.
INVALID_METADATA/ 422 stays. At boot this is a declaration the deployment cannot honour, and every sibling boot refusal in the package uses 422. D3's 500 is a read-time driver fault. The ruling asks for a loud refusal with the remedy and names no code. - Late installs. The rule runs at boot only, as the card asks. A late artifact is still refused by name at read time.
- Spellings. Only the canonical
referenceand the top-levelsharingModelare read. The spec admits nothing else.
- Envelope.
deviations: all accepted:- the
kernel:bootstrappedplacement, a measured premise correction; - the dogfood battery on the pre-merge head;
- the AGENTS.md commit trailers.
- the
- Follow-ups, not conditions:
- Late installs. The record's ③ item 2: a late artifact is not judged at arrival, while the same file already judges late picklists and bound actions. The measurement is 6332654314 on objectql: a controlled_by_parent detail whose master is bound to another datasource, or carries an object-scoped read middleware, is admitted at registration and refused only at read time — refuse the boot instead (ADR-0139 D5.2 / D5.3) #25144. The HTTP install door cannot carry an object definition. The in-process
manifestservice andmetadata:reloadedadmit a late detail, which answers 500 only on aReadableIdsleaf read, and no production read writes one before plugin-security: a controlled_by_parent child's read loads every readable master id with no limit (twice per request), so its CPU grows with the master table and the read answers 500 "too many SQL variables" past ~32.8k masters #24932 lands. No card today; the seat re-measures once plugin-security: a controlled_by_parent child's read loads every readable master id with no limit (twice per request), so its CPU grows with the master table and the read answers 500 "too many SQL variables" past ~32.8k masters #24932 lands. - Return type. The record's ① item 2 nit:
UnservableControlledByParentMasteris not re-exported from the package root. It is not filed.
- Late installs. The record's ③ item 2: a late artifact is not judged at arrival, while the same file already judges late picklists and bound actions. The measurement is 6332654314 on objectql: a controlled_by_parent detail whose master is bound to another datasource, or carries an object-scoped read middleware, is admitted at registration and refused only at read time — refuse the boot instead (ADR-0139 D5.2 / D5.3) #25144. The HTTP install door cannot carry an object definition. The in-process
Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #25144 then closes the card.
Generated by Claude Code
Fixes #25144
Clause-②: yes (narrowing)
Metadata-protocol: none
What
A
controlled_by_parentdetail whose master is in one of ADR-0139's two uncomposable classes now refuses the boot. Before this change the stack booted, and the id-list pre-scan served it. Once #24932's leaf switch lands, every read of such a detail would answerINTERNAL_ERROR/ 500. The two classes:registerMiddleware(fn, { object })), for examplesys_attachment.The refusal is
INVALID_METADATA/ 422. It names each detail, its master field, the master and the class, plus the three remedies: bind both to one datasource; or choose a master without an object-scoped read middleware; or declare the detail withoutcontrolled_by_parent. It is one error listing every offender. There is no warn-and-continue, no silent skip and no id-list fallback. The read-time refusal (refuseUncomposableReadableIdsClasses) is unchanged, and its messages are byte-identical.packages/objectql/src/engine.ts: the read path's two checks move into one private predicate,uncomposableReadableIdsParent(object, parent). It ishasObjectMiddlewareand thenresolveDatasourceBindingon both sides, in the read path's own order. The read refusal and the new publicObjectQL#findUnservableControlledByParentMasters()both call it, so the boot rule and the read rule cannot drift. The master is the detail's one requiredmaster_detail(isControlledByParentMasterField, ADR-0055 §1).packages/objectql/src/plugin.ts:describeUnservableControlledByParentMasters(), thrown fromObjectQLPlugin'skernel:bootstrappedhandler..changeset/25144-cbp-unservable-master-boot-refusal.md: BREAKING,patch(Metadata-protocol: none), with the remedy and the ADR-0087 dispositionnot-required (no-migration-prescription).Placement (measured first)
On
origin/main23540582f4, the kernel runs everystart(), thenkernel:ready(handlers in registration order), thenkernel:bootstrapped, thenkernel:listening, which opens the socket.kernel:readyhandler in its ownstart(), so that handler runs before its dependents' handlers.kernel:readyhandler, not instart():installMailReadVisibility;installAttachmentReadVisibility.start().kernel:bootstrappedorkernel:listeninghandler. I grepped every one of them: sharing, security, auth, automation, mcp, pinyin, storage, organizations.So
registerAppsees neither final fact, and the engine'skernel:readysees the bindings but not the later middlewares.kernel:bootstrappedsees both and is still a boot refusal, because no socket is open yet. The same precedent exists inorganizations-plugin.ts#runMembershipPolicyGate. The placement stays inpackages/objectql, so noneeds_decisionis raised.Probe (one-off, not committed). I moved the check into the engine's
kernel:readyand removed it fromkernel:bootstrapped. Class (a) still went red as expected. The three pins whose middleware is registered at a plugin'skernel:readypassed through unrefused:3 failed | 5 passed (8). The restored blob equalsHEAD. A first attempt at this probe was a no-op:ablation-replacerefused it because the replacement contained its anchor. It was redone with a different anchor.Verification (head
0f5d9a6bcafter mergingorigin/main06900c9c2; the merge brings in no objectql file)plugin-controlled-by-parent-unservable-master.test.ts, run on a realObjectKernel+ObjectQLPlugin, with the middleware registered from a plugin'skernel:readythe way the platform's own gates are:sys_attachment;controlled_by_parentshapes (e6d5d3e), with a gated system object nobody masters on; global middleware (*/ no object); both bound to one second datasource; a non-CBP detail across datasources. 8/8.if (unservable) throw unservable;was replaced viascripts/ablation-replace.mjs(anchor 1 → 0). Result:4 failed | 4 passed (8): the four class pins went red and the four controls stayed green. Restored, blob2eccc2620e81equalsHEAD, andgit diff HEADis empty.@objectstack/objectqltypecheck: exit 0. Full suite: 461/461 files, 8645/8645 tests (one0ca492fd).@objectstack/runtimefull suite: 433/433 files, 6805 passed, 19 skipped (one0ca492fd).pnpm --filter @objectstack/core test:repo: 6/6 files, 82/82 tests.pnpm --filter @objectstack/dogfood test, full, under the lock, one0ca492fd):VERDICT command-exit 0, Test Files 287 passed + 3 skipped (290), Tests 2395 passed + 18 skipped (2413). It boots the showcase, whose twocontrolled_by_parentobjects are controls; the CRM example'scrm_opportunity_line_itemis covered by its suites.turbo run build(77/77), objectql typecheck and the new pins, all exit 0 on0f5d9a6bc.dispatch-gates.mjs --commandslists 71 families. Every one was run with its exit code captured before any pipe, and--ranreconciles 71 derived, 71 run, 0 NOT-MEASURED.check:dual-build-cjs-loadsexited 3 (nodist/) until the full build, then exited 0.check:adr-0087-registrationpasses: 1 declared-breaking changeset,not-required (no-migration-prescription).Acceptance notes
metadata:reloaded) is not judged by this rule. Its detail is still refused at read time by name (D5.2 / D5.3, unchanged). Judging it on arrival would mean resolving bindings for objects not yet registered. This card asked for the boot.referencekey. plugin-security's CBP resolver also tolerates the rejectedreference_to/referenceToaliases on raw-registered objects. A raw-registered alias-spelled detail would boot here and still be refused at read.INVALID_METADATA/ 422, like the sibling boot refusals (picklists, bound actions), not D3'sINTERNAL_ERROR/ 500. That envelope is a composition fault at read time; this is a declaration refused before anything is served.ObjectSchema.parse/packages/spec,os validate(it resolves no deployment bindings or middleware registrations), and plugin-security: a controlled_by_parent child's read loads every readable master id with no limit (twice per request), so its CPU grows with the master table and the read answers 500 "too many SQL variables" past ~32.8k masters #24932's switch. controlled_by_parent: under one authorable master-reference declaration, one write door outside the detail write gate is not held to ADR-0055 §1 (reach measured; detail withheld under the disclosure ruling) #25092 and plugin-security: the raw-SQL read-scope provider (getReadFilter) does not apply the delegated organization-membership rule PR #25101 enforces at the operation prologue; measure reach first (detail withheld) #25106 remain open.Generated by Claude Code