Repository navigation
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
Description
Filing gate ③: a task the maintainer set by ruling. Batch #382 item 1 on #24932, sub-question Q1, letter C: the maintainer answered the director's recommendation (Q1 C, Q2 A) with 「同意」. Filed by the director seat (seat post #12708, session_01TQvbE2ENCniJr1ffCzbPc6, os-bill). The decision this executes is 6318800595 on #24932; the measurement it rests on is the dev report 6318648840 (P3). ⛔ Not a claim.
Reader (who acts): the domain:engine seat (seat post #6367) claims and dispatches. #24932 (domain:services, p1) is blocked on this card: its plugin-security switch to the ADR-0139 leaf lands only after this refusal has merged, so that at no point does a declaration exist that boots today and answers 500 at read tomorrow. The two may be developed in parallel; the switch merges second.
Dedupe (REST list, both states, every objectstack issue updated since 2026-09-20: 3,314 scanned; 39 match controlled_by_parent and one of datasource|middleware|D5.2|D5.3|registration|boot|refuse). None is this refusal. The nearest are #23270 and #23387 (stored-shape refusals on the master count, closed), #22788 (the ADR-0139 execution epic, closed) and #25092 (a write door, a different rule). New.
What is measured (origin/main abc25a3536; P3 of 6318648840 at f2b6cf230)
- The engine composes no ReadableIds leaf over a master in two classes, by name, at read time: a master whose rows an object-scoped read middleware narrows (
packages/objectql/src/engine.ts:6553,hasObjectMiddleware), and a master bound to another datasource than the detail (:6557,resolveDatasourceBinding). ADR-0139 D5.2 / D5.3 record both as limits. - The declaration path admits both:
ObjectSchema.parseadmits acontrolled_by_parentchild whose master is declared on another datasource, or whose master issys_attachment;engine.registerAppadmits a child whose master is bound explicitly to a second registered datasource, or carries an object-scoped read middleware (resolveDatasourceBinding→{ datasource: 'other', via: 'explicit' };hasObjectMiddlewaretrue). - Today the id-list pre-scan serves both shapes rows. Under the leaf (the 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 switch) both answer
INTERNAL_ERROR/ 500 for every caller, by name. - Producers: zero measured. HotCRM
e6d5d3edeclares sixcontrolled_by_parentobjects, every master acrm_*object on the one datasource with no object-scoped middleware; in-repo declarations outside test fixtures: none. Deployed tenants were not read. - Objects that carry an object-scoped read middleware on main, enumerated by
registerMiddleware(fn, { object })(every one a platform system object): comment and comment reaction (plugin-audit/src/comment-access-hooks.ts:1215-1216), activity (activity-read-visibility.ts,activity-field-redaction.ts), the audit ledger (audit-log-read-visibility.ts,audit-log-field-redaction.ts), mail message and mail association (service-mailbox/src/mail-read-visibility.ts:316-320), attachment (service-storage/src/attachment-access-hooks.ts:972), position assignment (plugin-security/src/security-plugin.ts:4610), approval request (plugin-approvals×2). Global registrations ('*'or no object) are not counted, byhasObjectMiddleware's own reading.
Governing text
- ADR-0139 D5.2 / D5.3 (the engine refuses to compose a leaf for these classes, loud as D3's envelope; recorded as a limit, not solved).
- ADR-0049 (enforce-or-remove: a declaration the runtime cannot honour is not admitted).
- ADR-0055 §1 (a detail's access derives from exactly one required master).
- The ruling on 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, batch Implement server-side batch operations, metadata caching, and view storage APIs #382 item 1 (Q1 C): "refuse the declaration where it is made"; both facts are deployment facts (datasource mapping, package default datasource, lifecycle routing, middleware registrations), so the refusal lives in the engine/runtime boot, not in the spec parse.
Wanted
- At the point where both facts are known — every plugin has registered its middleware and every object has its datasource binding — the boot is refused when a
controlled_by_parentdetail's master (a) resolves to a datasource other than the detail's, or (b) carries an object-scoped read middleware. The refusal names the detail, the master, the class, and the remedy (bind both to one datasource; choose a master without an object-scoped read middleware; or declare the detail withoutcontrolled_by_parent). Loud, as D3's envelope; ⛔ no silent skip, no fallback to an id list. - Placement is the dev's first measurement (the ruled confidence gap). Candidates: a
kernel:readyhook in the engine or the runtime (the hook exists:dispatchHookPropagating('kernel:ready', …)in@objectstack/core), orregisterAppif both facts are already resolvable there. The dev measures which point sees the middleware registrations made during plugin start, and reports a fork if neither point sees them; ⛔ a check that cannot see a registration made later than it runs is not the refusal this card asks for. - Not in the spec parse (
ObjectSchema.parsestays as it is);os validateonly if it already resolves bindings, otherwise out of scope. - Pins: one per class, plus the control (HotCRM's six
controlled_by_parentobjects and every in-repo example boot unchanged). - Changeset:
Clause-②: yes (narrowing)— a composition that booted yesterday is refused today;Metadata-protocol: noneunless the dev finds a spec shape moving, which would be a fork.
Acceptance
- A stack declaring a
controlled_by_parentdetail whose master is bound to another datasource is refused at boot with the named remedy; so is one whose master carries an object-scoped read middleware (e.g. a detail mastered bysys_attachment). - Every in-repo example, the showcase and the HotCRM fixture boot unchanged (control).
- After this merges, 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 may land with the leaf answered for every
controlled_by_parentobject, with no up-front routing to an id list.
Related
#24932 (blocked on this card; the switch), ADR-0139 (#25009 / PR #25032, #25010 / PR #25072), #25092 (the write-door rule, same family), #25106 (serial after the switch).
Activity
objectstack-fleet commentedon Oct 22, 2026
Claim: PM loop round 29 (the lane's p1 and p2 too while seat 2 is vacant, by the maintainer's 「engine 车道有任务为什么不派发」) · 2026-10-22T02:20Z · p1,
domain:engine. A task the maintainer set by ruling: batch #382 item 1 on #24932, Q1 C (decision 6318800595, 「同意」), filed by the director seat. First in the pick order: a maintainer-ruled card outranks the label order. #24932 (domain:services, p1) merges after this card.Session:
session_01JtSQQ3EWsbNPzyYMug1tXy(the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-25144-controlled-by-parent-unservable-master-refusedWorktree:
objectstack-issue-25144Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
start()(registerMiddleware(fn, { object })). The card enumerates comment, activity, the audit ledger, mail, attachment, position assignment and approval request.resolveDatasourceBinding).kernel:readyhook in the engine (packages/objectql), orregisterAppif both facts are already resolvable there.packages/runtime(domain:cli) does, reportneeds_decisioninstead of building there.packages/objectqlat the measured point: acontrolled_by_parentdetail whose master (a) resolves to a datasource other than the detail's, or (b) carries an object-scoped read middleware, refuses the boot.controlled_by_parent.hasObjectMiddleware,resolveDatasourceBinding,engine.tsnear:6546–:6557), so the boot rule and the read rule cannot drift.sys_attachmentcase. Controls: HotCRM's sixcontrolled_by_parentobjects (the card'se6d5d3ereading), the showcase and every in-repo example boot unchanged. Ablation: remove the refusal, and both class pins go red while the controls stay green.Clause-②: yes (narrowing),Metadata-protocol: none(the spec parse does not move; a spec shape moving would be a fork), the BREAKING banner, the migration (the three remedies) and its ADR-0087 disposition, at the levelcheck-changeset-no-majortakes for the packages moved.⛔ Not in scope:
ObjectSchema.parseandpackages/spec(the ruling: deployment facts, not a spec parse);os validate, unless it already resolves bindings;domain:services'), 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;Container & model:
M,mode:cloud(a boot-time refusal at a measured point, two classes, controls and a dogfood battery),model: TIER_DEFAULTto build (this run'sdispatch-gates.mjs --tier: "no path-derived mandate"). The review is owed atCONTRACT_REVIEW_TIER, since a narrowing declaration is a contract face.Clause-②: yes (narrowing)
Responsibility: an author who declares a
controlled_by_parentdetail over a master on another datasource, or over a master an object-scoped read middleware narrows | the declaration boots today and, once #24932's leaf lands, answers 500 for every caller at read | every stack carrying such a declaration (none measured in HotCRM or in-repo; deployed tenants unread)Thread-read: none
Serial constraints cleared: the 12 open PRs' file lists were read at this claim, on
origin/main23540582f4. None touchespackages/objectql/src. #24932 develops in parallel and merges second, by the ruling.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-22T02:22Z:mode:cloud. The dev session issession_01Dw4pQznF7eB7VtCsFQXMAH. It builds at the default tier on branchclaude/issue-25144-controlled-by-parent-unservable-master-refused, under claim 6331129246.packages/objectqlsees both every datasource binding and every object-scoped read middleware a plugin registers instart(), proved with a late-registered middleware. If onlypackages/runtimedoes, the dev reportsneeds_decision.controlled_by_parentdetail whose master is bound to another datasource, or carries an object-scoped read middleware, refuses the boot. The refusal is loud as ADR-0139 D3's envelope, names the detail, master and class, gives three remedies, and reuses the read path's own predicates;sys_attachment), with HotCRM's six shapes, the showcase and every example as controls, plus an ablation;Clause-②: yes (narrowing),Metadata-protocol: none, the migration and an ADR-0087 disposition. The PR title carries!, and the dogfood battery runs in full.ObjectSchema.parse/packages/spec; 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 (it merges after this); 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; 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.CONTRACT_REVIEW_TIERreview of record before the queue, since this is a narrowing.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{ "issue": 25144, "status": "done", "branch": "claude/issue-25144-controlled-by-parent-unservable-master-refused", "pr": "https://github.com/objectstack-ai/objectstack/pull/25166", "session": "session_01Dw4pQznF7eB7VtCsFQXMAH", "premise_still_valid": true, "summary": "A controlled_by_parent detail whose one required master is in ADR-0139 class D5.3 (master bound to another datasource) or D5.2 (master narrowed by an object-scoped read middleware) now refuses the boot from ObjectQLPlugin's kernel:bootstrapped handler. It answers INVALID_METADATA/422, naming each detail, master field, master, class and the three remedies. The read path's two checks were folded into one private predicate, uncomposableReadableIdsParent, which both the unchanged read-time refusal and the new ObjectQL#findUnservableControlledByParentMasters() call, so the boot rule and the read rule cannot drift. Placement was measured first: the card says the middlewares register in start(), but mailbox, audit and attachment storage register them inside their own kernel:ready handlers, which run after the engine's. So kernel:ready (and registerApp) cannot see them, while kernel:bootstrapped sees both facts and still precedes kernel:listening. A one-off probe confirmed this. Card assignee untouched (os-project-manager); the claim names this branch.", "placement_measurement": "origin/main 23540582f4. The kernel order is start() for every plugin, then kernel:ready (handlers in registration order; the engine's runs first), then kernel:bootstrapped, then kernel:listening. Object-scoped read middleware registered inside a plugin's kernel:ready handler: service-mailbox installMailReadVisibility; plugin-audit comment/activity/activity-redaction/audit-log-redaction/audit-log-visibility; service-storage installAttachmentReadVisibility. Registered in start(): plugin-approvals bindRequestReadGate/bindRequestChildReadGates; plugin-security position-catalog refusal. None is registered in any kernel:bootstrapped or kernel:listening handler (all of them grepped). Bindings (drivers via AppPlugin.start, federated reconcile at engine kernel:ready) are final by kernel:bootstrapped. Verdict: registerApp sees neither fact; engine kernel:ready sees the bindings but not the late middleware; kernel:bootstrapped in packages/objectql sees both. No needs_decision. Probe: with the check moved to the engine's kernel:ready, the three late-middleware pins went unrefused (3 failed | 5 passed of 8) and class (a) still refused. The first probe attempt was a no-op, which ablation-replace refused because the replacement contained the anchor; it was redone with another anchor. Restored blob == HEAD.", "tests": "New packages/objectql/src/plugin-controlled-by-parent-unservable-master.test.ts runs on a real ObjectKernel + ObjectQLPlugin, with middleware registered from a plugin's kernel:ready. Pins: class (a); class (b); class (b) over sys_attachment; several offenders in one refusal. Controls: HotCRM e6d5d3e six CBP shapes; global middleware; both on one second datasource; non-CBP across datasources. Result 8/8. Ablation (scripts/ablation-replace.mjs, anchor `if (unservable) throw unservable;` 1->0): 4 failed | 4 passed, the class pins red and the controls green; restored blob 2eccc2620e81 == HEAD, git diff HEAD empty. @objectstack/objectql typecheck exit 0; full test 461/461 files, 8645/8645 tests (e0ca492fd). @objectstack/runtime full test 433/433 files, 6805 passed, 19 skipped (e0ca492fd). pnpm --filter @objectstack/core test:repo 6/6 files, 82/82 tests. After merging origin/main 06900c9c2 (no objectql file in it), head 0f5d9a6bc: turbo run build 77/77, objectql typecheck and the new pins exit 0 (VERDICT command-exit 0).", "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 71 families on head 0f5d9a6bc. All 71 were run with the exit code captured before any pipe, and all exit 0. --ran reconciliation: '71 derived, 71 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 71 recorded an exit code and none of them is 3)'. On the first pass (e0ca492fd), check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET, no dist/); after a full pnpm exec turbo run build --concurrency=2 it exited 0, and it exits 0 on the final pass. The clone was deepened with git fetch --shallow-since=2026-07-13; check-engine-split-ratio --days 90 and check-plugin-teardown-shape --self-test both exit 0. check:adr-0087-registration: 1 declared-breaking changeset, not-required (no-migration-prescription). CI check-runs on PR #25166: in_progress at report time, not awaited (os-dev contract).", "dogfood_battery": "e0ca492fd (the pre-merge head; the merge of origin/main 06900c9c2 brings in no objectql/runtime file) · os-verify-lock: VERDICT command-exit 0, held the lock 967s · Test Files 287 passed | 3 skipped (290) · Tests 2395 passed | 18 skipped (2413) · no failures. The first run was killed by a worker restart before it finished (no verdict) and was re-run in full.", "line_budget": "none set; diff vs origin/main: .changeset/25144-cbp-unservable-master-boot-refusal.md +34/-0; packages/objectql/src/engine.ts +75/-7; packages/objectql/src/plugin.ts +47/-1; packages/objectql/src/plugin-controlled-by-parent-unservable-master.test.ts +187/-0", "files_changed": [ ".changeset/25144-cbp-unservable-master-boot-refusal.md", "packages/objectql/src/engine.ts", "packages/objectql/src/plugin.ts", "packages/objectql/src/plugin-controlled-by-parent-unservable-master.test.ts" ], "deviations": [ "Premise correction, not a scope change: the card says plugins register their object-scoped middleware in start(); measured, mailbox/audit/storage register it in their own kernel:ready handlers. So the refusal is placed at kernel:bootstrapped (still packages/objectql/src/plugin.ts, inside the claim's fence), not at kernel:ready as the card's candidates named.", "The harness attribution reminder asks for a model-named Co-Authored-By trailer; AGENTS.md (which takes precedence) requires the model-free pair, so the commits carry Claude-Session + Co-authored-by: Claude.", "I accidentally started an unlocked background `pnpm typecheck` once; it was stopped by its recorded PID (19492, cwd verified as this worktree) before it finished. No reading was taken from it.", "The dogfood battery ran on e0ca492fd, not on the final merge head 0f5d9a6bc. The merge brings in only cli/plugin-auth/console-pin files, none of them in the battery's engine path, and the post-merge build + objectql typecheck + pins were re-run green. If the seat wants it on 0f5d9a6bc, CI's battery covers it." ], "mcp_calls": "2 — mcp__github__issue_read (get #25144, get_comments #25144); no write tool", "api_writes": "3 — git push to claude/issue-25144-controlled-by-parent-unservable-master-refused (5 pushes of the one branch; git, not REST); fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches -> relay POST /pulls draft + POST /issues/25166/assignees), read back byte-identical; fleet-write relay comment (POST /issues/25144/comments, this report). REST reads: gh api GET /issues/comments/6318800595 and /6318648840.", "open_questions": [ { "question": "Which envelope does the boot refusal carry?", "options": [ "A INVALID_METADATA / 422, like the sibling boot refusals (picklist, bound-action target)", "B INTERNAL_ERROR / 500, D3's read-time composition envelope" ], "recommendation": "A (taken). Business need: the author must see a declaration error with its remedy, not a server fault. Long term: the boot refusals share one code. AI-error-proofing: a 422 at boot reads as 'fix your metadata'. Startup focus: no new code. D3's 500 stays where it belongs, at read time." }, { "question": "Should a manifest installed after the boot (marketplace install, metadata:reloaded) also be judged on arrival?", "options": [ "A no: the boot only, as the card asks; a late detail is still refused by name at read time", "B judge on arrival, like the picklist seal" ], "recommendation": "A (taken). Business need: zero measured producers and no late-install CBP pull. Startup focus: no extra surface. Long term: B would mean resolving bindings for objects not yet registered. Left as an acceptance note; a card only if a producer appears." }, { "question": "Which master-reference spellings does the boot rule read?", "options": [ "A canonical `reference` only (taken)", "B also the rejected `reference_to` / `referenceTo` aliases plugin-security still tolerates on raw-registered objects" ], "recommendation": "A. Contract-first: no new consumer-side alias. A raw-registered alias-spelled detail still fails loudly at read." } ], "out_of_scope_findings": [ "carrier: none · noted, not filed: late-installed manifests are not judged by the boot rule (they are still refused at read time); in PR #25166 Acceptance notes.", "carrier: none · noted, not filed: the boot rule reads the canonical `reference`, while plugin-security's CBP resolver tolerates two rejected aliases on raw-registered objects; in PR #25166 Acceptance notes." ], "other_lane_files": "none — every changed file is in packages/objectql or .changeset; no content/docs edit" }Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Late-install measurement
Relayed by
domain:engineseat 1 (session_01JtSQQ3EWsbNPzyYMug1tXy) at 2026-10-22T04:23Z, verbatim from devsession_01Dw4pQznF7eB7VtCsFQXMAH. The dev's dispatch allowed three writes, and all three were spent, so the seat posts it. Nothing was pushed and PR #25166 is unchanged.Seat's reading: no card today.
reach:measured once through a public entry, or a named real producer. Here the HTTP install door cannot carry an object definition at all. The doors that admit a late detail are in-process: themanifestservice andmetadata:reloaded.ReadableIdsleaf, and no production read writes one today.plugin.ts: judge the detail on arrival once the boot has sealed.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Landed ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-22T05:01ZCONTRACT_REVIEW_TIER. Record 6332548600 PASS on0f5d9a6bce. ACCEPT 6332714745. Not governed.gh-readonly-queue/main/pr-25166-*branch remains.origin/main504d8b3acdis onorigin/main, one squash commit carrying PR #25166's 4 files, +343 / −8.completedthroughFixes #25144;pm:dispatchedis cleared.What
mainnow holds:controlled_by_parentdetail. A detail whose one required master is bound to another datasource (ADR-0139 D5.3), or whose master an object-scoped read middleware narrows (D5.2), now stops the boot. The refusal isINVALID_METADATA/ 422. It names each detail, master field, master and class, and the three remedies:controlled_by_parent.Before, such a deployment booted and answered 500 on the read that composed the leaf.
ObjectQLPlugin'skernel:bootstrappedhandler, after every plugin'skernel:readyhas registered its middleware and every binding is final, and beforekernel:listeningopens the socket.ObjectQL#findUnservableControlledByParentMasters().ObjectQLPluginwith the boot-phase hooks. The in-repocontrolled_by_parentdetails (app-crm and app-showcase) master on same-package, ungated objects and boot unchanged.patchof@objectstack/objectql,Clause-②: yes (narrowing),Metadata-protocol: none, with the BREAKING banner, the Remedy section and ADR-0087not-required (no-migration-prescription).Beside it:
domain:services, the leaf switch) is now unblocked per ruling 6329027469 ②. Its seat re-claims.manifestservice ormetadata:reloaded, and refused at read with the engine's D5 500 (measurement 6332654314). The HTTP install door cannot carry an object definition. No card today, because no public entry was measured and no production read writes the leaf 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. The seat re-measures when 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.UnservableControlledByParentMasteris not re-exported from the package root (the record's ① item 2 nit).The dev session
session_01Dw4pQznF7eB7VtCsFQXMAHis archived.Generated by Claude Code