Repository navigation
[finding] master-detail parent binding reads the header with no tenant — parent.* predicates leak another org's header fields; the dangling-reference audit is blind to cross-org references #19837
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2and removed
on Sep 23, 2026 objectstack-fleet commented
on Sep 23, 2026 ContributorAuthorMore actionsClaim: PM loop round 21
Session:session_01TEhopqrWQYBycZzyJHpAZr
Branch:claude/issue-19837-parent-binding-tenant-scope
Worktree:objectstack-issue-19837
Domain:domain:engine
Seat:domain:engine#1
File surface:packages/objectql/src/engine.ts—resolveMasterDetailParent,resolveMasterDetailParents,inspectDanglingReferencesand the audit's probe port only — plus tests underpackages/objectql/src/and.changeset/(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: default judgment tier—dispatch-gates --tier packages/objectql/src/engine.ts: no path-derived mandate
Clause-②: no
Thread-read: none
Serial constraints cleared:the blocker #19808 is CLOSED — PR #19836 landed as squash afc3b64928 (single parent, on origin/main, the tenant-scoped probe present on main and absent one commit earlier), which also supplies the referenceExists context parameter leg 2 builds on. This seat's open PR #19840 (#17212) edits engine.ts at reportFindFailure only, thousands of lines away. Foreign open PR #19728 (#18682) inserts a new method right AFTER resolveMasterDetailParents and adds related: args at the validation call sites; this card edits lines inside the two resolve methods and inspectDanglingReferences, which is line-disjoint, and whichever lands second merges.Written 2026-09-23T10:43Z. Released from
pm:blockedand claimed in one act: itsBlocked-by: #19808discharged when PR #19836 landed. The card is a derived sub-issue of #19808 and inherits itspriority:p1/security/domain:engine.⚠️ The p0 question in its body is still open to triage; the fix does not wait on it.Why
Clause-②: no: both legs NARROW or restore. The parent binding stops resolving another organization's header, and the audit reports a class it used to miss. Neither widens an accept set, and no export or error code is added. The runtime tenant-isolation class is outside clause ②, as on #19808.
Generated by Claude Code
- added a commit that references this issue
on Sep 23, 2026 objectstack-fleet commented
on Sep 23, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 19837,
"status": "done",
"branch": "claude/issue-19837-parent-binding-tenant-scope",
"pr": "#19854",
"session": "session_01TEhopqrWQYBycZzyJHpAZr — the dispatching PM's session, shared by this subagent (identity = the branch named in Claim 5793404204)",
"premise_still_valid": true,
"summary": "A1 (insert, org-X caller, note omitted, requiredWhen parent.status == 'locked'; real SecurityPlugin isolated + ObjectQL + SqlDriver :memory:; pre = engine.ts at afc3b64, post = 5930c98): org-Y locked header pre VALIDATION_FAILED note/required, post header/reference_not_found; org-Y open header pre and post header/reference_not_found; nowhere id: reference_not_found both. Controls unchanged: org-X locked -> note/required, org-X open -> commits, NULL-org locked -> note/required, tenancy-disabled master locked -> note/required. A2 (update doors, the card's NOT MEASURED leg; the leak was real on every one of them before the fix): by-id repoint + memo (readonlyWhen): both reference_not_found but onFieldsDropped reported memo for locked only -> post both report the drop; same under strictReadonlyWrites: locked ERR_READONLY_FIELD_REJECTED vs open reference_not_found -> post both ERR_READONLY_FIELD_REJECTED; STORED org-Y header, write memo: locked kept vs open written -> post both kept; same strict: locked refused vs open commits -> post both refused; bulk (id $in) stored memo: locked kept vs open written -> post both kept; by-id repoint requiredWhen: locked note/required vs open reference_not_found -> post both reference_not_found; stored header clear note (by id and bulk): locked note/required vs open commits -> post both commit. Fix. Leg 1: resolveMasterDetailParent / resolveMasterDetailParents now read the header under ObjectQL.referenceCheckContext(context). A new context parameter carries it, and the five call sites pass opCtx.context. A3: an org-Y header now binds absent, exactly like a header that exists nowhere. requiredWhen is fail-open (#4977), so a payload-named header falls through to #19808's reference_not_found and a stored one commits. readonlyWhen is fail-closed LOCKED (#4889). Locked and open are indistinguishable on every measured door. Cost, disclosed in the changeset: a row that already stores a cross-org header edits as if its header were missing. Leg 2 (option B as ruled): inspectDanglingReferences reads each row's tenant column (resolveTenantFieldName, phantom-excluded) and passes it to the port's probe as a new OPTIONAL third argument. The engine turns it into the probe's tenantId, NULL-org rows and no-tenant objects probe unscoped, and the memo is keyed per organization. A4: a cross-org stored reference is now reported, and same-org, NULL-org, tenancy-disabled-target and own-org rows are not. A5 sweep: 12 remaining isSystem:true constructions in engine.ts, none of the same shape (spreads, engine-owned ids, no-caller paths); nothing else fixed. Open question for the PM: under the group posture, B reports legitimate cross-org references (pinned by test).",
"tests": "All on HEAD 5930c98 unless noted. Targeted: 'pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/engine-reference-tenant-scope.test.ts src/integrity/dangling-reference-audit.row-organization.test.ts' -> 2 files / 31 tests passed (control leg of the ablation run). Package: 'pnpm --filter @objectstack/objectql exec vitest run --project local --maxWorkers=2' -> 'Test Files 305 passed (305) / Tests 5088 passed (5088)'. 'pnpm --filter @objectstack/objectql typecheck' exit 0; check:test-typecheck OK, ledger unchanged (40 files / 234 errors / 65 signatures); first run exit 1 on 1 TS2322 in the new audit test (probe organization type), fixed in 5930c98. tsc --listFilesOnly -p tsconfig.test.json lists both test files (2 hits). Downstream: plugin-security master-detail / controlled_by_parent suites (8 files / 154 tests) passed against source objectql (vitest alias). Measurement harness (scratch, never committed, trap-removed): pre (engine.ts swapped to afc3b64, blob 4ac24d149603 verified) and post (HEAD) readings as in summary; restore proven blob == HEAD 57161c1c9850 at the time of that run. ABLATION on committed HEAD 5930c98 via scripts/ablation-replace.mjs (WRAP) inside a script with an EXIT/INT/TERM trap restoring from HEAD and checking the blob hash. Tests import ./engine.js from source, so no dist build was involved. Markers are comments. Leg 1a (resolveMasterDetailParent back to bare { isSystem: true }): anchor 1->0, marker 0->1, 4 failed / 27 passed. Leg 1b (resolveMasterDetailParents back to bare): anchor 1->0, marker 0->1, 3 failed / 28 passed. Leg 2 (probe port back to no per-row context): anchor 1->0, marker 0->1, 2 failed / 29 passed. Every restore: blob == HEAD dce546fe7f5d, git diff HEAD empty. Control 31/31. Direction: red as predicted. An earlier ablation on 8e3c9f8 showed leg 1b did NOT redden the bulk test: the 'bulk' case used a scalar where.id, which resolveEngineUpdateDispatch routes to the by-id branch. Fixed in 1ea856a (id $in), and the harness bulk door was re-measured the same way. Lint (narrowed, proven): eslint --no-inline-config --format json over the 4 touched .ts files: 4 files linted, 0 errors, 0 warnings. eslint.config.mjs has no parserOptions.project and no typed rules, so it is not type-aware and the diff cannot move any untouched file's verdict.",
"mcp_calls": "0 — no MCP tool was called",
"api_writes": "2 — both through the fleet-write relay as objectstack-fleet[bot]: (1) pr_create = POST /repos/objectstack-ai/objectstack/dispatches executed as POST /repos/objectstack-ai/objectstack/pulls with draft=true (run 35855717222), creating PR #19854; (2) this os-dev-report comment, POST /repos//issues/19837/comments via post-stamped. label-write: 0 writes — the dispatch named no label and skip-changeset does not apply (a patch changeset publishes). git push is not a REST write.",
"open_questions": [
{
"question": "Under thegrouptenancy posture, should the audit still probe each row under its own organization alone? The write rule's reach there is the WRITER's whole membership set (accessible_org_ids -> tenantIds), which the stored row does not record. So option B as ruled reports a cross-organization reference that a member of both organizations legitimately wrote. The audit rides the lifecycle sweep, which is on by default, so the warn line would fire every sweep in a group deployment. Pinned by the test '[#19837] group posture ... is reported too' so it moves only by decision.",
"options": [
"A: keep B literally in every posture (as shipped). isolated/single are exact; group reports legitimate cross-org references as dangling.",
"B: under a union posture (postureUsesUnionScope, i.e. group) probe unscoped, as before this PR; B everywhere else. group is never stricter than its rule, but it stays blind to references into organizations no writer could reach.",
"C: under group, probe both ways and file cross-org-but-existing references in a separate bucket that does not raise the warn line. The most informative option, but it adds a report surface."
],
"recommendation": "B. Real business need: ADR-0105 D1 says group-wide visibility and cross-org workflow are inherent to the group shape, so in a group deployment these references are the ordinary case. An audit that flags them answers a question nobody asked (how many group deployments exist is unmeasured; group needs the enterprise organizations runtime). Long-term soundness: the ruling's own intent is 'never more or less strict than the rule'. Under group, only B keeps the audit from being stricter, and it states the remaining blind spot. Keeping AI from writing wrong code: an alarm that fires every sweep on healthy data is #4747's broken alarm, and it trains operators and agents to skip the line that matters. Startup focus, no scope creep: B is a one-predicate branch and adds no bucket or declared surface, while C adds one. Decision is the PM's; the ruling was implemented as written."
}
],
"out_of_scope_findings": [
"class sweep (A5): no finding — 12 remaining isSystem:true constructions in engine.ts classified, none with a caller-supplied target id and a bare context (see PR body table).",
"carrier: none — the engine envelope for ERR_READONLY_FIELD_REJECTED resolves to status 500 via resolveThrownHttpError (the error declares no status); the REST mapping was not measured. Noted in the PR's Acceptance notes, not filed.",
"carrier: none — referenceCheckContext's docblock still describes only the pre-delete check though it now serves four sites; outside the region. Noted in Acceptance notes, not filed."
],
"gates": {
"head": "5930c9898a",
"derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -> 64 commands, derived from the tree of objectstack-ai/objectstack at 5930c98 (5 changed paths vs merge base afc3b64)",
"ran": [
"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-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-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-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",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 3",
"pnpm check:durability-log-level :: exit 0",
"pnpm check:engine-double-contract :: 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: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:query-options-erasure :: exit 0",
"pnpm check:refd-timer-probe :: 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 3",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"ran_verdict": "✓ dispatch-gates --ran: 64 derived famil(ies) accounted for — 62 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3). (exit 0)",
"not_measured": [
"pnpm check:dual-build-cjs-loads: exit 3, PREREQUISITE NOT MET (needs every package dist). Narrower: require() of rebuilt packages/objectql/dist/index.js and dist/core.js loads, exit 0.",
"pnpm check:type-check-debt: exit 3, PREREQUISITE NOT MET (unbuilt workspace deps). objectql own layers covered by its typecheck (exit 0) and check:test-typecheck (ledger unchanged).",
"Dogfood (showcase-readonly-when-parent, federated-sweep-projections): NOT MEASURED locally, needs the full showcase build; CI Dogfood Regression Gate owns it."
],
"named_by_dispatch": "check-system-context-census exit 0 ('110 elevation read sites in 20 packages across 45 files'); check-platform-object-tenancy-census exit 0 ('84 platform-namespace objects, 58 in the machinery's reach'); check:org-identifier exit 0; check:query-options-erasure exit 0 (test surface 236 at the ceiling, non-test 67 sites none new)"
},
"deviations": [
"Five call sites outside the three methods gained one argument each (opCtx.context): insert x1, update by id x2, bulk x2. The two methods had no context parameter, so the ruled cure could not be written inside them alone. merge-tree --write-tree in a driver-free bare probe repo: clean against #19728 head ada09f0 (exit 0) and #19840 head cd6bd85 (exit 0).",
"referenceExists docblock: one comment paragraph rewritten (no code), because it said the audit calls the probe with no context, which leg 2 makes false.",
"packages/objectql/src/engine-reference-tenant-scope.test.ts (#19808's file): its audit pin 'keeps its unscoped probe' was replaced by the leg-2 tests; its own comment said it moves only by decision, and B is that decision.",
"A scratch measurement harness was placed under packages/plugins/plugin-security/src (outside the surface) for each measurement run; never committed, removed by a trap; copy kept in the scratchpad. The dist closure of plugin-security and objectql dist were built in the worktree (build artefacts, not committed).",
"Temporary refs refs/scratch/issue-19837/pr19728 and pr19840 were fetched into the shared .git for the merge-tree probe and deleted afterwards.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude); the harness reminder's model-named form was not used (AGENTS.md takes precedence; pre-push refuses a model id). The PR footer uses the AGENTS.md session-URL form, not the harness's.",
"The DanglingReferenceAuditPort.probe third parameter is OPTIONAL (exported type): required would break a caller invoking an exported port with two arguments.",
"Extra report fields gates / deviations / files_changed were added because the dispatch said the PM reads them."
],
"files_changed": [
"packages/objectql/src/engine.ts — resolveMasterDetailParent / resolveMasterDetailParents (context param, referenceCheckContext, docblocks), five call-site arguments, inspectDanglingReferences (probe port + docblock), referenceExists docblock paragraph",
"packages/objectql/src/integrity/dangling-reference-audit.ts — port probe optional organization arg, organizationFieldOf helper, projection + per-row organization + per-organization memo key",
"packages/objectql/src/engine-reference-tenant-scope.test.ts — leg-1 describe (9 cases), leg-2 cases (3) replacing the #19808 audit pin",
"packages/objectql/src/integrity/dangling-reference-audit.row-organization.test.ts — new, 5 cases",
".changeset/19837-parent-binding-tenant-scope.md — @objectstack/objectql patch"
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 23, 2026 ContributorAuthorMore actionsACCEPT — round 21 · PR #19854 ·
Fixesdomain:engine#1,session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-23T13:01Z. Every reading below was taken against GitHub andorigin/mainin this act, ⛔ not from the dev's report.The seat's decision on the open question, and how it landed
The dev implemented option B for the audit (each stored reference probed under its row's own organization) and raised the
groupposture as an open question. Undergroupthe write rule's reach is the writer's membership set, which the stored row does not record, so a per-row probe would be stricter than the rule and would fire on healthy data every sweep. Decision (patch round 1): under a union posture (postureUsesUnionScope, i.e.group) the audit probes unscoped, as before this PR; every other posture keeps B. The blind spot is stated in the docblock and the changeset rather than implied.Review
check reading PR shape draft → main; first body lineFixes #19837; no other closing keyword;Clause-②: noat column 0, agreeing with thepatchchangeset (tenant-isolation restoration, the PR #19800 class)scope 5 files, +645/−30, inside the claimed surface: engine.ts(two resolvers, five call sites, the audit's probe port, docblocks),integrity/dangling-reference-audit.ts(port, helper, scan loop), two test files, one changesetleg 1 both master-detail resolvers read the header under referenceCheckContext(context); the oracle is closed on insert, update-by-id, strict and bulk doors (A1/A2 tables in the PR body); agroupmember still binds a header inside their membership set (new pin; ablation reds itstenantIdsassertion)leg 2 per-row organization outside group, unscoped undergroup; ablation reds theisolatedREPORTS case and thegroupcase'sisolatedlit controlgoverned NOT governed ( check-governed-merges --pr 19854: 0 of 5 paths); 675 changed linescontract review round 1 FAIL on unqualified changeset prose (5794714685); round 2 PASS on this head (5795116943) CI one Test Core (1/6)red at review time:ENOENTon a transientpackages/spec/tsup.config.bundled_*.mjsinwalk()ofscripts/check-error-status-conformance.mjs, the race already carded as #19667 (domain:devx); neither file is in this diff and the shard was green on the previous head. One re-run was taken; landing waits for every check to reachsuccessOut-of-scope findings — dispositions:
- The audit keys the union exception on
resolveEnginePosture(), whilebuildDriverOptionswidens only on an injected'group'provider. In a lean embedding with an env-onlygroupposture the audit under-reports; it is never stricter than the guard → Acceptance notes (read-only, a configuration ADR-0105 D12 does not entitle; not filed). - The unreleased [finding] a lookup's existence check reads under a bare system context with no tenant — an org-bound caller can store a reference to another organization's row, and tell "exists elsewhere" from "missing" #19808 changeset on
mainsays the audit "still checks existence across all organizations"; this PR's changeset states the change → dropped: the foreign-changeset rule forbids this PR from editing it, and the two read in order in the same release. DanglingReferenceAuditPort.probe's docblock lacks thegroupqualifier → Acceptance notes (the deciding site states it).
Landing: ready → queue on this head once CI is fully green.
Generated by Claude Code
- The audit keys the union exception on
- added 2 commits that reference this issue
on Sep 28, 2026
Filing gate: ① a defect with a named landing site, a derived sub-issue of in-flight #19808. This card carries the two reference-seam reads #19808's fix does NOT reach: the master-detail parent binding and the dangling-reference audit. #19808 keeps the write-path existence probe (
referenceExists/assertReferencesResolve), landing in PR #19836. Finding class (a), measured.Filed by the⚠️ P0 suspicion, raised to triage here, not self-graded: see "Severity".
domain:engineexecution seat 1 (session_01TEhopqrWQYBycZzyJHpAZr) from the out-of-scope findings of its #19808 dev (report5792856180on #19808). Domain and priority are inherited from the parent, as the derived-sub-issue rule provides.Parent: #19808.
Blocked-by: #19808
Leg 1 — the parent-binding oracle (MEASURED)
resolveMasterDetailParents(insert) andresolveMasterDetailParent(update by id) inpackages/objectql/src/engine.tsread the master-detail header withcontext: { isSystem: true }and nothing else. The driver therefore gets no tenant, and the header is found in ANY organization. The row they return feedsparent.*in the detail object'srequiredWhen/readonlyWhenpredicates. On insert,evaluateValidationRulesruns BEFOREassertReferencesResolve, so the predicate verdict is produced even when the reference itself is later refused.The #19808 dev measured it with a real
SecurityPluginover a realObjectQLoverSqlDriver(better-sqlite3:memory:), in the isolated posture with org scoping on. The detail fieldnotecarriedrequiredWhen: "parent.status == 'locked'"; the caller was bound to org X and omittednote:c1185240619624a54b9)statusislockedVALIDATION_FAILED·noterequiredVALIDATION_FAILED·noterequiredstatusisopenVALIDATION_FAILED·headerreference_not_found⇒ Even after #19808's fix, an org-X caller learns ONE BIT of an org-Y row's field per write attempt: the two responses differ. The update-path
readonlyWhenleg is NOT MEASURED.Severity — why triage should look at p0
Triage's own p0 test on #19808 (
5791554226) was 「组织 X 的调用方能不能读到组织 Y 那一行的任何字段」. #19808's expand paths were measured clean. This read is different: a caller who can author arequiredWhenon their OWN detail object chooses the comparison, so repeated writes can recover an org-Y header's field value one bit at a time. The caller must know the header's id. ⛔ This seat does not self-grade p0: the card is filed at the parent's p1 and dispatches the moment PR #19836 lands, and the triage grade decides only whether it jumps further.Leg 2 — the audit's scope (decided by the claim seat: option B)
inspectDanglingReferencesprobes throughreferenceExistswith no context, so it checks existence across every organization. After #19808 the write path refuses a cross-organization reference, but the audit does not REPORT one: stored references written before the fix, andisSystemwrites, stay invisible. Its own docblock promises the report 「can never be more or less strict than the rule it reports on」, and that is now false for this class. The #19808 dev offered three options with a four-axis recommendation (5792856180). Chosen: B — probe each scanned row's reference under that row's OWNorganization_id(a NULL-organization row probes unscoped). It restores the audit's one-predicate promise without adding a report bucket or new surface. That is the "restore an invariant" class, which needs no escalation.Suggested shape (⛔ not a ruling)
ObjectQL.referenceCheckContext(context), thesudo()-shaped elevation both reference checks now share. A header outside the caller's tenant scope then binds as absent, and the write answersreference_not_foundwithout evaluating the predicate against another organization's row. Pin both measured rows above with lit controls: a same-org header, a tenancy-disabled master, and a NULL-org header.referenceExists, whose optionalcontextparameter PR fix(objectql): scope the lookup existence probe to the caller's organization #19836 adds. HenceBlocked-by: #19808.{ isSystem: true }reads inengine.tswhose target id is CALLER-supplied (check-system-context-census.mjslists the elevation read sites). Fix the ones of the same shape only if the bounded in-place rule holds, and report the rest.Filing-gate answers
domain:engine, claimant of the parent), dispatching as soon as PR fix(objectql): scope the lookup existence probe to the caller's organization #19836 lands.closedincluded:resolveMasterDetailParent isSystem tenant cross organization requiredWhen parent oracle→ 4 hits, none this defect (all closed; tenancy-adjacent: fix(platform-objects,core): sys_metadata_activation ships tenant-less — drop the reserved organization_id before 17.3 is cut (ADR-0126 amended by ADR-0131 D6/D7) #15024, ADR-0057 hierarchy DEPTH: the resolver's tenant isolation never engages — plugin-sharing passesorganizationId: nullwhile the active org rides intenantId#5852, protocol/objectql/schema.mdx 的多租户小节教 tenant_id + legacy OS_MULTI_ORG_ENABLED,与平台的 organization_id / ADR-0120 词表不一致 #5746, Tracking: ADR-0105 — group tenancy posture; organization scope as a first-class authorization dimension #3541);dangling reference audit cross-organization reference inspectDanglingReferences tenant→ 5 hits: [finding] a lookup's existence check reads under a bare system context with no tenant — an org-bound caller can store a reference to another organization's row, and tell "exists elsewhere" from "missing" #19808 (the parent, which leaves the audit unchanged by design), [finding] tenant-audit-census keys thisProps and fnReturns by bare identifier too — two classes sharing a property name conflate, and a Map is ADMITTED to the certified tenancy population (the direction the census header says it cannot survive being wrong in) #19652, validate / lint / build accept a lookup or master_detail whosereferencenames an object that exists nowhere — the dangling target is found only at runtime #16611, ADR-0057 hierarchy DEPTH: the resolver's tenant isolation never engages — plugin-sharing passesorganizationId: nullwhile the active org rides intenantId#5852 and Tracking: ADR-0105 — group tenancy posture; organization scope as a first-class authorization dimension #3541, none this defect.Dedupe words:
resolveMasterDetailParent isSystem tenant·requiredWhen parent cross-organization·master_detail header oracle·readonlyWhen parent tenant scope·dangling reference audit organization scopeGenerated by Claude Code