Repository navigation
[Decision] security(objectql): a validation rule that reads one hop through a master-detail FK still reads the header elevated, so an unreadable header's answer varies with its stored state after ruling A #22892
Description
Ruled: 6115662038 · letter A · 2026-10-12T00:35Z
Filing gate: ② a decision only the maintainer can make (two rulings meet), carrying a ① class (b) defect under the possible-data-disclosure exception. Raised by #22519's dev (os-dev-report 6113287460, out-of-scope finding 1, PR #22891) and filed by domain:engine seat 2 (#20966) · session_01ADzJtzYTLUfgrRZHxkagkX. ⛔ Not a claim. ⛔ Classes, positions and functions only.
Reader who acts: the maintainer (through the director's batch) picks a letter; then the domain:engine lane claims the fix. Duplicate check: the instrument is the same 5,122-issue local grep used for #22868 (closed included, #11835–#22865): resolvePredicateRelated 0, predicate traversal + master-detail 0, related read + elevated 0, accepted cost 0; control #18682 6.
维护者速读
- 一句话问题: 你在 objectql: a detail write reads its master-detail header elevated (tenant kept), so a parent-scoped
requiredWhenmay disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519 裁了「调用方读不到的主记录,写明细时按不存在答复」。PR fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891 已把主记录读取和引用检查改成先走调用方读权限。但还有一条路没改:明细对象上写了「跨一跳读主记录字段」的校验规则时,这条规则仍然越权读主记录。结果是同一个读不到的主记录,答复仍随它存的状态而变(一个比特泄露)。 - 为什么要你定: 这条越权读是 formula/objectql: relationship traversal in predicates — a validation rule or visibility predicate reads one hop through a lookup (
record.crm_account.type); replaces the removedos.lookupdeclaration (#18318, batch #148) #18682 能力(你批的 batch 🔗 Broken links detected in documentation #148 B)实现时的设计,理由写着「像parent一样按系统权限读」。现在parent已按裁决 A 改了,两条裁决在这条路上冲突,席位不能替你选哪条为准。 - 席位意见: 荐 A(把裁决 A 延伸到这条路:明细的校验规则读主记录也先走调用方读权限,读不到即按不存在)。回退 C(调换校验顺序,让引用检查先于校验规则)。
- 你要做的: 回一个字母,例如「本卡号 A」。PR fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891 本身三个选项下都要落地,我已验收但暂不挂自动合并,留你一个异议窗口;你回字母即视为放行。
Background (measured)
- PR fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891 (objectql: a detail write reads its master-detail header elevated (tenant kept), so a parent-scoped
requiredWhenmay disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519, headfef935481d) implements ruling A at three seams: the write's master-detail header read (resolveMasterDetailParent(s)) and themaster_detailhalf of the reference check now ask the caller's read door first (ObjectQL.masterDetailHeadersCallerReads). ObjectQL.resolvePredicateRelated(formula/objectql: relationship traversal in predicates — a validation rule or visibility predicate reads one hop through a lookup (record.crm_account.type); replaces the removedos.lookupdeclaration (#18318, batch #148) #18682) resolves the related rows a predicate rule reads one hop through a reference field. Its docblock: "Read under SYSTEM authority, likeparentand for a related reason" — theparentread it leans on is the one ruling A changed. "On a related object no organization wall scopes, the own read of any caller that is not SYSTEM also bounds the ROWS." So a tenant-walled related object (a tenanted master) is read elevated with no caller row bound. The system-context page records it as row 29b.- The dev's probe (on
fef935481d, synthetic read scope, not committed), one detail object with a rule reading its master through the master-detail FK: a header that exists nowhere →_recordrule_violation; an unreadable header in an open state →_recordrule_violation; an unreadable header in a locked state →invoicereference_not_found. Three answers for "unreadable or nonexistent", one bit of the header's stored state.
Governing text
- Ruling A on objectql: a detail write reads its master-detail header elevated (tenant kept), so a parent-scoped
requiredWhenmay disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519 (6107211425): "A: a write of a master-detail detail whose header the caller cannot read answers exactly what a write naming a header that exists nowhere answers. This holds for every detail,controlled_by_parentor not: insert, update (a repoint included), and the bulk paths." Scope: "themaster_detailheader only. The plainlookupreference probe is not ruled here." - security(data): a by-id write answers 403 for a row the caller cannot read and 404 for an id that does not exist, for principals the write pre-image check does not bind: an existence signal the read door withholds #21771 ruling A (5985885287), which ruling A cites: an unreadable row is a nonexistent row for the caller.
- formula/objectql: relationship traversal in predicates — a validation rule or visibility predicate reads one hop through a lookup (
record.crm_account.type); replaces the removedos.lookupdeclaration (#18318, batch #148) #18682 (ruling 5716041368, batch 🔗 Broken links detected in documentation #148 B): the capability "a validation rule reads one hop through a lookup"; the elevated read is the implementation's docblock rationale, not a ruled clause (formula/objectql: relationship traversal in predicates — a validation rule or visibility predicate reads one hop through a lookup (record.crm_account.type); replaces the removedos.lookupdeclaration (#18318, batch #148) #18682's thread read, 5 comments with a ruling or PM verdict; none rules the read's authority). - ADR-0055 §3 (the master-edit gate covers
controlled_by_parentonly). - Retrieval:
git grep -n "Read under SYSTEM authority, like" origin/main -- packages/objectql/src/engine.ts.
Premises (each with a re-check)
- P1 (holds): the predicate related read names its elevation by analogy to
parent. Re-check:git grep -n "Read under SYSTEM authority, like" origin/main -- packages/objectql/src/engine.ts(control:git grep -n "private async resolveMasterDetailParents" origin/main -- packages/objectql/src/engine.ts). - P2 (holds): a tenant-walled related object gets no caller row bound. Re-check:
git grep -n "On a related object no organization wall" origin/main -- packages/objectql/src/engine.ts. - P3 (holds on the PR head):
git grep -c "masterDetailHeadersCallerReads" origin/claude/issue-22519-unreadable-header-write -- packages/objectql/src/engine.tsanswers 8. - P4 (the dev's measurement, not re-runnable as committed): the three answers above.
Options
| Option | What it does | What a customer sees | Workload |
|---|---|---|---|
| A | A predicate rule's one-hop read through a master-detail FK takes ruling A's two-read shape: the caller's read door first; a header it does not return is unavailable, as a nonexistent one is. A plain lookup traversal is unchanged (not ruled). |
An unreadable header answers exactly as a nonexistent one, whatever rules the detail carries. A rule reading a header the writer can read is judged on stored values, as today. | domain:engine · 1 PR (resolvePredicateRelated reuses masterDetailHeadersCallerReads) · precedent PR #22891 (+754 / −58) |
| B | Keep #18682's elevated read; scope ruling A's outcome to the reference check and requiredWhen / readonlyWhen; record the residual on the system-context page (row 29b). |
A detail carrying a rule that reads its master still answers differently by the unreadable header's state. | docs only, ~10 lines · no precedent |
| C | Reorder the write so the reference check runs before predicate rules. | An unreadable or nonexistent header answers reference_not_found before any rule runs; every write with both a bad reference and a failing rule now reports the reference first. |
domain:engine · 1 PR in the write bodies · no precedent; order-dependent |
业务含义直译: A = 「看不见的主单,对谁都是不存在」,和 Salesforce 主从关系上「读不到主记录就不能写明细」一个口径;B = 留一条已知的旁路,写进文档;C = 先查门牌号再查规矩,碰巧也关上旁路,但靠的是顺序。
四轴(业务立场):
- 长远合理性: A 让「读不到 = 不存在」在写明细的每条路上只有一个答案;C 靠校验顺序成立,以后谁调一次顺序旁路就回来;B 是有意留的特例。
- 实际业务拉动: 实测拉动为零(没有具名客户撞上);但这是安全边界,零拉动不作为保留旁路的理由。
- 防 AI 犯错: A 下作者(含 AI)写的跨关系校验规则不会意外变成泄露通道;B 下泄露是静默的,作者和管理员都看不见。
- 创业阶段不扩散: A 复用 fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891 已有的读法,不新增概念;B 最省事但多一条要永久记住的例外。
推荐 A,回退 C;置信缺口: 生产部署里有多少明细对象带跨主记录的校验规则未实测;A 对「规则作者本意就是读写入者看不见的主记录状态」的场景会改变规则判定(此场景在裁决 A 下本就不成立)。自检:只看① 选 A;②③④ 是否翻转:否。
裁后执行
- A: the lane files and claims one card for
resolvePredicateRelatedover amaster_detailFK, two-read shape, pins mirroring fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891's; PR fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891 lands as is. - B: the lane updates row 29b's prose and the resolver docblock (its "like
parent" rationale is false after fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891); PR fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891 lands as is. - C: the lane files one card for the reorder, with every write door's refusal precedence pinned; PR fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891 lands as is.
- Under every letter, PR fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891 (ACCEPT on objectql: a detail write reads its master-detail header elevated (tenant kept), so a parent-scoped
requiredWhenmay disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519) is armed for the queue once the maintainer answers.
os-decision-facets
- ① 项目长远合理性:A 缩小特例(一条规则覆盖写明细的所有读法);B 扩大特例;C 用顺序约定代替规则。
- ② 实际业务拉动:今天无具名客户撞上;安全边界不以零拉动保留旁路。
- ③ 防 AI 犯错:A 结构上关闭旁路;B 静默保留;C 依赖顺序不被改动。
- ④ 创业阶段不扩散:A 复用既有读法,不新增概念;B 增加一条永久例外。
Prior rulings read: unreadable header, predicate traversal, SYSTEM authority → 3 hits (6107211425, 5985885287, 5716041368); ADR-0055 §3; thread: #18682 (5 comments read).
Activity
objectstack-fleet commentedon Oct 11, 2026
Triage: decision-box entry check passes. Labels added:
priority:p2·domain:engine·area:accessTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-11T20:53Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ No letter is chosen here.os-decision-facetsline counts, perdecision-analysis.md:40), the workload column and thePrior rulings read:line are all present.origin/main: P1's docblock line (engine.ts:9017, with its control at count 1) and P2's line (:9029).claude/issue-22519-unreadable-header-write: P3 answers 8.requiredWhenmay disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519, whose ruling A it extends. It does not gate the release.resolvePredicateRelatedinpackages/objectqlisdomain:engine.requiredWhenmay disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519 istarget:v18.objectstack-fleet commentedon Oct 12, 2026
Ruling: batch #318 item 3 · letter A · maintainer 「其他同意?」 2026-10-12T00:34Z
Director seat, summon #37,
session_01LqNgdPZB2Jnr5HpGbaqLeB(GitHubos-zhuang; written asobjectstack-fleet[bot]via the relay). Batch #318 (five cards, this one third) was presented in chat with this seat's own readings and the recommendation "A, fallback C"; the maintainer deferred one card (#22877) and answered 「其他同意?」 for the rest, which takes the recommendation here. Freshness gate: the body and the one comment (triage's entry check 6113638846) were re-read before this record. Thread-read: 6113638846.Premises re-run on
origin/main(3f694715e) in this act: P1engine.ts:9032("Read under SYSTEM authority, likeparent"); P2engine.ts:9044; P3 on PR #22891's head (pull/22891/head):masterDetailHeadersCallerReads8 occurrences.The ruling
A: ruling A on #22519 extends to a predicate rule's one-hop read through a master-detail FK.
resolvePredicateRelated, when the traversed reference field ismaster_detail, takes the two-read shape PR #22891 gave the header read: the caller's read door first, under the caller's own context; a header the door does not return is unavailable to the rule exactly as a header that exists nowhere is; the elevated, tenant-kept read follows only for headers the first read returned. For a header the caller can read, the rule keeps judging stored values (Salesforce validation rules run on true data). System callers are unchanged. The docblock's "likeparent" rationale is rewritten to state this rule. Scope: themaster_detailtraversal only; a plainlookuptraversal is not ruled here, as #22519's ruling did not rule the plain lookup probe.fef935481dbecomes one answer). CONTROLS: a readable header is judged on stored values; a plainlookuptraversal is unchanged; a system caller is unchanged; fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891's own pins stay green.Why on the first axis
"Unreadable equals nonexistent" must have one answer on every path a detail write takes, or the header's stored state leaks through whichever path was forgotten. A gives the predicate path the rule the header read and the reference check already follow; C makes the answer depend on evaluation order; B writes the exception down. On the platforms this one models itself on, a validation rule cannot see a parent record the writer cannot read. 自检:只看①选 A;②③④ 是否翻转:否(② 无具名客户撞上,安全边界不以零拉动保留旁路;③ A 结构性关闭旁路;④ 复用 #22891 的读法)。
Workload (#22597's rule):
domain:engine· 1 PR,resolvePredicateRelatedreusingmasterDetailHeadersCallerReads, plus pins · precedent PR #22891 (+754 / −58). Confidence gap: how many deployed detail objects carry a rule reading their master is unmeasured; a rule whose author meant to read a header the writer cannot see changes verdict, which ruling A already forbids.Prior rulings read: #22519 ruling A 6107211425 (scope: the
master_detailheader only); #21771 A 5985885287; #18682 ruling 5716041368 (batch #148 B: the capability, not the read's authority); ADR-0055 §3;check-prior-rulings.mjs --card 22892in this act: 519 hits, ADR-0139 D3 / D4 (the six activity consumers; D6 leaves whether a master id set becomes a seventh consumer unruled), ADR-0055 §3, ADR-0071 §1, ADR-0076 D10, ADR-0077 §1, ADR-0103 D2, ADR-0111 D2, ADR-0029 D9.5, ADR-0035 §3 named, none answering this question; thread: 6113638846.State and execution
needs-user-decision→pm:queuein this act, keepingpriority:p2,security,domain:engineandarea:access; no assignee. This card is the execution card (the lane claims it; no second card is needed). TheRuled:line is added to the body and the filing-time ⛔ line struck; the disclosure discipline (classes, positions and functions only) stands.requiredWhenmay disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519, ACCEPT 6113521641 atfef935481d, head now5ef9690d2d): the objection window this card held it for is closed; thedomain:engineseat arms it for the queue. A pointer is posted on objectql: a detail write reads its master-detail header elevated (tenant kept), so a parent-scopedrequiredWhenmay disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519 in this act.objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 1 · 2026-10-12T02:38Z
Session:
session_01ADzJtzYTLUfgrRZHxkagkXAccount:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-22892-predicate-related-master-detailWorktree:
objectstack-issue-22892Domain:
domain:engineSeat:
domain:engine#2(seat post #20966); the lane's only seated PMRuling-ref: 6115662038 (letter A, batch #318 item 3)
File surface (read on
origin/main018b077fbf; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.packages/objectql/src/engine.ts:resolvePredicateRelated, when the traversed reference field ismaster_detail, takes the two-read shape PR fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891 gave the header read: the caller's read door first (masterDetailHeadersCallerReads), the elevated tenant-kept read only for headers it returned. The docblock's "likeparent" rationale is rewritten to state this rule.content/docs/permissions/system-context.mdx: row 29b's prose follows the new rule (regenerate the census withpnpm gen:system-context-censusif a count moves).lookuptraversal (not ruled), not system callers, not fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891's three seams.Container & model:
M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate)Clause-②: no (narrowing)
requiredWhenmay disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519's did.Responsibility:
objectql'sresolvePredicateRelated, which reads a master-detail header elevated for a rule even after ruling A | PR fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891'smasterDetailHeadersCallerReads(the header read and the reference check) | a non-system caller writing a detail whose rule reads its master through the master-detail FK; measured by objectql: a detail write reads its master-detail header elevated (tenant kept), so a parent-scopedrequiredWhenmay disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519's dev (three answers for unreadable or nonexistent)Thread-read: 6115662038
Serial constraints cleared: objectql: a detail write reads its master-detail header elevated (tenant kept), so a parent-scoped
requiredWhenmay disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519 landed as PR fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891 (018b077fbf), which this card reuses. Open PRs: PR fix(security)!: under a wall the security catalog is the platform operator's — tenants are refused catalog writes and may only assign (ADR-0131 D3) #22895 (refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write undersingleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204) touchessystem-context.mdx(census), and the later lander mergesmain; PR feat(spec,metadata-protocol)!: rename the type registry's allowOrgOverride to allowEnvironmentOverlay, on the registry and on GET /meta/types #22874 (spec: renameallowOrgOverrideto an environment-overlay key with an ADR-0087 load-time conversion (ADR-0131 C5's spec half, split from #15206 per #22007 ruling C) #22340) touches oneengine.tsline inMETADATA_ARRAY_KEYS, which is disjoint. This seat's finding: a multi-valued reference field (lookup/userwithmultiple: true) admits an element of the wrong shape: REST stores an embedded record[{ id }]with 201, where the single-valued field refuses itinvalid_type(ADR-0104 D1) #22908, claimed in parallel, sits inrecord-validator.tsand is disjoint.objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{
"issue": 22892,
"status": "done",
"branch": "claude/issue-22892-predicate-related-master-detail",
"pr": "#22956",
"session": "session_01ADzJtzYTLUfgrRZHxkagkX — shared with the PM (mode:subagent); this run is identified by the branch",
"premise_still_valid": true,
"summary": "This implements ruling A (6115662038). ObjectQL.resolvePredicateRelated has a new step for a reference field whose type is master_detail when the caller is not SYSTEM. It asks the caller's read door first, reusing PR #22891's masterDetailHeadersCallerReads (no second helper). The existing elevated, tenant-kept read of the named columns then runs only for the headers the door returned. A header the door does not return gets no row and no own-read set, so its binding is 'unresolved': exactly what a header that exists nowhere gets, whatever it holds. A failed door read returns no header, so every header is missing. Unchanged: SYSTEM callers, a plain lookup traversal, a related object no organization wall scopes (its own-read branch runs first and already answers 'unreadable' for every miss), and #22891's three seams. The docblock's 'like parent' rationale is rewritten to state the rule, and the inline 'accepted cost' comment is narrowed. system-context.mdx row 29b states the new bound; no isSystem read was added, so the census counts do not move. The changeset is minor and BREAKING, with Clause-② no (narrowing) and adr-0087 not-required (no-migration-prescription). The dev's three-answer probe, re-run on main 018b077 before the fix, gave three answers: nowhere -> _record rule_violation (related record not found); unreadable open -> invoice reference_not_found; unreadable locked -> _record rule_violation with the rule's message. After the fix it gives one answer, the first, byte for byte.",
"tests": "New packages/objectql/src/engine-predicate-related-master-detail-header.test.ts: 22 tests on #22891's rig (store-backed driver, synthetic read scope). Refusals assert the ADR-0112 envelope (status 400, code VALIDATION_FAILED), the field codes, and the message with the header id removed. Pins: a header that exists nowhere, an unreadable open header and an unreadable locked header answer identically on insert, batch insert, update by id, repoint by id and bulk update; a refused read of the header object answers as a nonexistent header on the same 5 doors; validate() preview parity; the partial insertMany; the mechanism (door first, no elevated read for a header it did not return). Controls: a readable header judged on stored values on all 5 doors; a hidden or masked state column still judged on the stored value; a plain lookup traversal unchanged with no door asked; a system caller unchanged with no door asked. | Ablation, run from committed 0931f18 with node scripts/ablation-replace.mjs (trap-restored). The anchor 'master_detail' in the new branch's type test became 'master_detail_ablated' (anchor x1 -> x0, blob 2a14a9f089a9 -> 0d452143b11f). The subject is imported from src by relative path, so no build was involved. Mutated run over the new file, #22891's file and engine-predicate-relationship.test.ts: 13 failed / 81 passed. All 13 non-control pins of the new file went red; its 9 controls, #22891's 30 pins and the #18682 file stayed green. Restore: blob after 2a14a9f089a9 == HEAD, git diff HEAD empty. | Full objectql suite at fa29574 (os-verify-lock VERDICT command-exit 0): vitest run --project local gave Test Files 403 passed (403), Tests 7875 passed (7875). | After merging origin/main 1bd612b (HEAD 6586f41): full turbo build 73/73 (VERDICT 0); pnpm --filter @objectstack/objectql typecheck passed (tsc --noEmit plus check:test-typecheck OK; the new test is in the tsconfig.test.json program, --listFilesOnly count 1); 10 targeted objectql files passed (Test Files 10 passed, Tests 233 passed); 4 plugin-security files passed under the real SecurityPlugin with @objectstack/objectql aliased to src (predicate-related-read-tenant-scope, delete-reference-cleanup-system-identity, write-preview-field-gate-parity, security-plugin: Test Files 4 passed, Tests 318 passed). The merge brought no packages/objectql change, so the full objectql suite was not re-run on the merge commit (declared). | Lint, narrowed and measured: eslint --no-inline-config --format json over the 2 changed .ts files. Population: eslint.config.mjs's base block covers **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} outside packages/spec. JSON count: 2 files, 0 errors, 0 warnings. Invariance: no type-aware linting (no parserOptions.project, stated in the config). The repo-wide pnpm lint is left to CI. | Gates: union at final head 6586f41. All 98 derived commands exited 0. node scripts/pm/dispatch-gates.mjs --ran reconciled 98 derived, 98 run, 0 NOT-MEASURED, 0 UNRUN. The re-derived list on the real diff equals the dispatch list byte for byte. An earlier union at fa29574 had check:skill-examples and check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET, unbuilt packages); both went green after a full build.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 REST writes, all through the fleet-write relay as objectstack-fleet[bot], in 2 repository_dispatch strokes: (1) POST /repos/objectstack-ai/objectstack/pulls (draft PR #22956; read-back 8631 bytes sent, 8631 stored, identical); (2) POST /repos//issues/22956/assignees [os-steve] (the same pr_create action); (3) POST /repos//issues/22892/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). Not REST: 5 git pushes of the feature branch (the empty-branch probe, 3 commits, 1 merge of origin/main). Zero label writes (the dispatch named none). The PR body was written once and never PATCHed. REST reads only via gh api GET (card, comments, PR read-back).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted in the PR's Acceptance notes, not filed · packages/objectql/src/validation/rule-validator.ts (outside the claim's file surface). The RelatedFieldBinding docblock lists the related read's bounds (projection; own read on an unwalled object) but not the master-detail read door. RelatedUnavailableReason 'unresolved' reads 'a reference is stored but the related record was not found', which now also covers an unreadable header. Both are incomplete, not false, so they are not class a/b/c."
],
"gates": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
"node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
"node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
"node scripts/check-docs-section-name.mjs :: exit 0",
"node scripts/check-docs-section-name.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
"node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-section-landing-index.mjs :: exit 0",
"node scripts/check-section-landing-index.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:corpus-claim-drift :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-anchors :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-audit-scope :: exit 0",
"pnpm check:docs-redirects :: exit 0",
"pnpm check:docs-single-h1 :: exit 0",
"pnpm check:docs-spec-enumerations :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:durability-log-level :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:error-status-conformance :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:merge-driver :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:published-readme-links :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:react-page-adapter-contract :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:skill-identifier-liveness :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:stack-collection-maps :: exit 0",
"pnpm check:swallow-census-controls :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:vendor-version-stamps :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"line_budget": "501 changed lines (+478 / -23) over 4 files, generated included. No generated file changed: the census counts did not move. Split: test file +391, engine.ts +54 / -22, changeset +32, system-context.mdx +1 / -1. Precedent PR #22891 was +754 / -58. Under the 3,000-line human-merge threshold.",
"deviations": [
"Attribution follows AGENTS.md: commits carry the model-free pair (Claude-Session plus Co-authored-by: Claude), and the PR footer is the session-URL form. The harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer; AGENTS.md and the pre-push hook take precedence. Reported, not declared a deviation.",
"Merged origin/main (1bd612b) into the branch before opening the PR, as the claim's serial note asks of the later lander (PR #22895 touched system-context.mdx). The os-regen driver deferred system-context.mdx. I verified the merged page against origin/main: it differs by row 29b only, so main's edits were kept. check-system-context-census reported OK (130 sites), and scripts/check-regen-pending.mjs cleared the marker, so the pre-push hook passed. main has moved again since (51b43a8). None of its commits touches packages/objectql, system-context.mdx or this changeset, so I did not merge it.",
"The full objectql suite exceeded the tool's 600 s foreground budget (it waited 490 s for the lock) and the harness moved it to the background. I waited for it in the same turn with a foreground tail --pid on its recorded PID and read its VERDICT line from the log. I relied on no wake-up.",
"Lock contention: two of my lock calls ended queue-timeout (exit 99) behind sibling seats' holders. My slot was kept and both calls were retried. One wrapped targeted run (objectql plus plugin-security) was cut by my own timeout after its objectql half passed, and the plugin-security half was re-run alone. No holder was stalled: each held and released.",
"A scratch probe file (packages/objectql/src/zz-probe-22892.test.ts) reproduced the three answers on main and the one answer after the fix. It was deleted before the first test commit and was never committed or pushed.",
"File surface: every edit is inside the claim's surface (engine.ts resolvePredicateRelated, system-context.mdx row 29b, tests beside the code, one changeset). The rule-validator.ts docblock gap is noted, not edited.",
"Cleanup done: node_modules removed and git worktree remove succeeded for ../objectstack-issue-22892. Nothing was left uncommitted: HEAD equalled origin/claude/issue-22892-predicate-related-master-detail at 6586f41."
],
"files_changed": [
"packages/objectql/src/engine.ts",
"packages/objectql/src/engine-predicate-related-master-detail-header.test.ts",
"content/docs/permissions/system-context.mdx",
".changeset/22892-predicate-related-master-detail.md"
]
}
objectstack-fleet commentedon Oct 12, 2026
✅ ACCEPT: PR #22956 at
6586f417fbdomain:engineseat 2 (#20966) ·os-steve·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T05:44Z. Ruling 6115662038 (A) binds. Claim 6116757077, report 6118217501. Checked on GitHub rather than taken from the report. Classes, positions and functions only.main, assigneeos-steve;Fixes #22892is the body's only closing keyword;Clause-②: no (narrowing), which matches the claim and the changeset.check-governed-merges --pr 22956: not governed, 501 lines);content/docs/releases/;rule-validator.tsuntouched, as the claim's surface says;mainis clean (git merge-treeagainst51b43a8dc4).resolvePredicateRelatedgains one branch for a non-system caller whose predicate rule reads through amaster_detailFK. It asks the caller's read door through PR fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891'smasterDetailHeadersCallerReads(no second helper), and the elevated, tenant-kept value read then runs only over the headers the door returned. A withheld header bindsunresolved, so unreadable-open, unreadable-locked and nowhere give one answer on all four call sites (preview, insert, update by id, predicate/bulk update). A refused door read fails closed to the same answer. Unchanged: system callers, plainlookuptraversal, an unwalled related object, and fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891's three seams.parent" rationale is replaced by the rule. Row 29b ofsystem-context.mdxstates the new bound and its anchor is unchanged, so the census does not move.@objectstack/objectqlminor, BREAKING narrowing, ADR-0087not-required (no-migration-prescription), with FROM / TO and the one-line fix (grant read on the master, or declare the detailcontrolled_by_parent).insertMany, and the mechanism pin. Controls cover a readable header, a hidden or masked state column, a plain lookup and a system caller.main, one after the fix.6586f417fb: 35 runs, 33success, 2skipped, 0 failed. All seven required contexts aresuccess.CONTRACT_REVIEW_TIER, isolated reviewer, read-only, same head).rule-validator.tsdocblocks (RelatedFieldBinding, theunresolvedreason, and therelatedoption's "likeparent" note, which the reviewer added) are incomplete after this PR but not false. Acceptance notes, for the next PR in this lane that opensrule-validator.ts. Not one of the three filing classes.system-context.mdxdiffers frommainby row 29b only), a background suite waited on in-turn, lock retries, and a scratch probe deleted before the first commit.Landing: ready and auto-merge through the queue in this act. If the queue build reds only on the suite-duration ceiling step (#22949), the PR is parked as PR #22947 was.
objectstack-fleet commentedon Oct 12, 2026
Landed: PR #22956 →
8d8f965374·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T06:51Z8d8f965374d68819626c698960783dd6af9c6245, with one parent,39cca271c7. ACCEPT 6118507109. Contract review PASS 6118500886. The PR was in the queue from 05:47Z until the merge.mainat that commit: the squash carries the PR's exact change: 4 files, +478 / −23. The pin file and the changeset are byte-identical to the accepted head6586f417fb.engine.tsandsystem-context.mdxdiffer from that head only by commits that landed onmainin between (feat(spec,metadata-protocol)!: rename the type registry's allowOrgOverride to allowEnvironmentOverlay, on the registry and on GET /meta/types #22874, chore(docs,scripts): system-context page states no census totals; the census gate keeps the register #22950). This PR's hunks in both files are identical to the accepted diff.completedby the PR'sFixes #22892.pm:dispatchedcomes off in this act.rule-validator.tsdocblocks (RelatedFieldBinding, theunresolvedreason, therelatedoption's note) are incomplete after this change but not false. They go to the next PR in this lane that opensrule-validator.ts.ReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861 (ADR-0139 E2) waited on this card for the shared functional axis and file. It is the lane's next pick.