Repository navigation
fix(objectql)!: expand and the master-detail parent root never hydrate a stored-metadata-body table - #24382
Conversation
… a stored-metadata-body table Claude-Session: https://claude.ai/code/session_01MHGpe6k6Yrmt3TsGu3wgYP Co-authored-by: Claude <noreply@anthropic.com>
…ngine hydrations, and the changeset Claude-Session: https://claude.ai/code/session_01MHGpe6k6Yrmt3TsGu3wgYP Co-authored-by: Claude <noreply@anthropic.com>
…mily-hydration-refusal
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 17 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b2b7b210300e2b73c98bac8b7f16988a86a36bf6 && git checkout b2b7b210300e2b73c98bac8b7f16988a86a36bf6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5da2e88818a006145330e3f323759ebef4d6ca87 b3af029a091d7728d2d51f9c27426f82f94415b4 && git checkout -B drift-repro 5da2e88818a006145330e3f323759ebef4d6ca87 && git merge --no-ff b3af029a091d7728d2d51f9c27426f82f94415b4
node scripts/docs-audit/affected-docs.mjs --json 5da2e88818a006145330e3f323759ebef4d6ca87 |
Contract reviewServed-tier: Security card: this record keeps to category and position. It names symbols, files, reader classes, releases and dispositions only; no request string, no path into a stored body, no value. Read-only shape held: the net diff against the merge-base ① Derived judgmentsExit 1, What the field answers, and that it discloses nothing — right. Exit 2, the The family reads a detail write still makes — id-only, right. The write's reference check ( Every other place a reference target is read by id — searched, none unguarded. Non-family references unchanged on both exits — right. Both guards key on The family pin — right. ② Semver levelRight. The changeset bumps ③ Boundary flagsDisclosure line — kept. The PR body, the changeset, the card's four comments, every code comment, every test name and both pin rows describe category and position: which reader classes were served before the fix (an administrator and a member granted read on the family table, by session and by API key; a default member not), with counts, and which disposition each surface now has. No request string, no value. The engine pin's fixture is a synthetic placeholder body plus a control, in the shape the family pin already publishes on Deviations — each judged.
Open questions — answered.
Out-of-scope findings — judged. Not filed by this review (no GitHub writes).
Exposure — the dev's claim, checked against this repository's tagged trees at category level. The clone's commit history is shallow (horizon 2026-09-10), so no windowed count is answered here; the release tags are present, and their trees were read. Exit 1: One observation, not a defect of this PR. The plain- Neither exit named by the card remains open at this head: both hydrations refuse a family target with the one predicate, no second projection, nothing in Implemented-by: VERDICT: PASS |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24358. 4 files, +286 / −3. Not governed (check-governed-merges.mjs --pr 24382: 0 of 4 paths). |
| Scope | The claim's file surface: packages/objectql/src/engine.ts (expandRelatedRecords, resolveMasterDetailParent, resolveMasterDetailParents), a new engine pin, the family enumeration pin (26 → 28 rows), and one changeset for @objectstack/objectql. The one function outside the claim's named list, resolvePreviewParents, is the third binder of the same parent root in the same file; it is reported as a deviation and accepted (below). There is no second projection, no packages/spec edit and no new error code, and the doors' projection, the reader-context seam and the epic:#15194 regions of engine.ts are untouched. |
| Seat's own read | At the head, the family predicate guards four places. In expandRelatedRecords it sits after the reference-target checks and before any id is collected or any find is issued. In resolveMasterDetailParent, resolveMasterDetailParents and resolvePreviewParents it sits before any header read. expandRelatedRecords has exactly three callers: its own recursion, find and findOne. So every depth and every entry point that reaches the loop is covered. |
| Reach (measured before the fix) | Per the report: on both exits, the reader classes #24316 named were served (an administrator, and a member granted read on the family table, by session and by API key). A default member was not. Both save doors and os validate / os build accepted the carrier. After the fix, no reader class is served on either exit, and the parent root gives every caller the same refusal. |
| Review face | The narrowing. CONTRACT_REVIEW_TIER record 6254034266 on this head is PASS. The seat verified its central claims at the head (the row above). |
| Declaration | Clause-②: no (narrowing), on the claim, the PR body and the changeset alike. The changeset is minor, BREAKING (a served-set narrowing), with the adr-0087 not-required (no-migration-prescription) marker; the record ② finds both right. |
| Pins and ablation | The engine pin has 22 cases, including no family read at all on the expand cases and no body read on the parent cases, under a member and under SYSTEM, with ordinary-target controls. Four ablations, one per guard, each turned its own cases red. Each restore read back as the HEAD blob. |
| Gates | Dev at b3af029a09: 70 derived, 70 run, 0 NOT MEASURED. Typecheck exit 0 for objectql and metadata-protocol. |
| CI | At this read: 40 check-runs on b3af029a09, 34 success, 6 skipped, 0 failed. The skips are the rostered path and opt-in skips. Test Core, every Type Check lane, Lint & Repo Gates, Check Changeset and Governed Surface Queue Guard are green. |
| Disclosure line | The seat checked the PR body, the changeset, the dev report and the record: each keeps to category and position. Fixtures use synthetic placeholders only. |
Disposition:
open_questions:- (b) Withhold the whole expansion, keeping the stored id: A, as record 6254034266 answers it. A projected expansion would be the second projection the card forbids, at the one layer the family's projection never runs.
- (a) The author-time route (also refusing a
lookupormaster_detailinto a family table at save) is not blocking. The engine guards are owed under either answer, because an installed artifact runs no authoring rule. Whether to add that authoring rule is put to the maintainer by this seat; any follow-up is its own card.
deviations: all accepted, as record ③ judges them:resolvePreviewParentsgets the guard. Its first header read was a caller-context engine read that would otherwise have fetched the family row (4 red before the guard).- No
@objectstack/metadata-protocolchangeset: its change is test-only. - Process notes only: the newer cut point (both points are on
main), the eight packages built for the live backend, and the detached checkout.
out_of_scope_findings: the package install door is settled in code by record 6251578116 (①). The engine guards hold whatever door admits the carrier. The cloud pin reading is the seat's and triage's.- Exposure: the report and record ③ place both exits in published releases. That is a release question for the maintainer, and this seat has put it to them.
- Noted, not a defect of this PR (record ③): a plain
lookup's write-time existence probe is a pre-existing id-only existence check on any reference target, not specific to the family.
Landing: ordinary queue (pr_ready + automerge_enable through the relay). Every check on this head is green or a rostered skip. Fixes #24358 then closes the card.
Generated by Claude Code
Fixes #24358
Clause-②: no (narrowing) (owes one
CONTRACT_REVIEW_TIERreview before the queue)What changes
The stored-metadata-body family (
STORED_METADATA_BODY_OBJECTS) is projected at its doors, but only for the object a request names. Two relationship hydrations inpackages/objectqlread a family row as an ordinary row. Each now refuses a family target with the one family predicate (isStoredMetadataBodyObject), as PR #24344 does at the one-hop loader.expandRelatedRecords. A reference into the family is never expanded, at any depth, for any caller. The field keeps its stored id: the answer the door already gives for a reference the caller may not expand (security(data, analytics): a lookup target's exposure declaration is not judged when the data door's$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661's precedent, and the refused sub-read).parentroot.resolveMasterDetailParentandresolveMasterDetailParentsnever read a family header.parentstays unbound, which is the fail-closed signal (Parent-scopedreadonlyWhenis unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889) an unreadable header already gives, so a field rule that reads it has no verdict, whatever the row holds. Thevalidate()preview's own first header read (resolvePreviewParents) gets the same guard, so the preview reads no family row either.No second projection. Nothing in
packages/spec, no new error code, and no edit to the doors' projection or the reader-context seam.resolveMasterDetailRelationis unchanged: the refusal sits at the reads, so the relation's other readers keep their answers.Reach, measured before the fix
Live over HTTP, on
maina2a3777bf(app-todo, SQLite,--fresh), with a synthetic stored datasource-type row carrying a placeholder credential, and a control row without one. "Served" means a value the family's own door withholds from that same reader: the credential material or the unkeyed hash. For every reader below, the family's own door served 0 such values.Exit 1 (
expand). Rows of the carrier that served a family value. Each cell holds for the list read with both expand spellings, the single-record read and the nested query form.ctx.apiread reaches the same loop throughfind. It is pinned at the engine, not measured over HTTP.Exit 2 (the
parentroot). The verdict for the same write under the header holding the placeholder and under the control header:requiredWhen2/2 writes differ;readonlyWhenover the stored hash 2/2 differrequiredWhen2/2 differWhy #24316's dev saw "refused as unbound": the write's first header read is the caller's own read door (#22519,
masterDetailHeadersCallerReads). For a caller whose door does not return the family header,parentstays unbound. For a caller whose door does return it, the second, elevated read binds the whole row.Producers. Each door, for a lookup and for a master_detail into a family table:
os validateandos build: ACCEPTED, exit 0, for both;Exposure.
expandloop was first published in@objectstack/objectql3.1.0 (bisected over the npm tarballs). The family's door projection was first published in@objectstack/metadata-protocol17.6.0; from that release on,expandbypasses it. The loop has no family check in 17.6.0 and 17.7.0 (latest), or in18.0.0-next.3(next).@objectstack/objectql17.0.0-rc.4 (a symbol bisect; an earlier spelling was not measured). It is unguarded in the same three releases.be076dad28a0) is onmain, and its tree carries both exits unguarded beside the door projection.After the fix
Live, after rebuilding objectql, on a fresh backend:
Route, by the four axes
The engine guards were chosen. The author-time refusal (a lint rule refusing a reference or master_detail into a family table at save) was not added here.
Verification (head
b3af029a0, after mergingorigin/main)@objectstack/objectqltest: 443 files, 8337 passed.@objectstack/metadata-protocoltest (before the merge, at61df76467): 244 passed and 3 skipped files, 20641 passed and 19 skipped. The family pin was re-run after the merge: 9 passed.engine-hydration-stored-metadata-body.test.ts: 22 passed, with PR fix(objectql)!: the one-hop read through a reference never reads a stored-metadata-body table; the lint refuses it #24344's pin beside it (32 together).check:test-typecheck) and metadata-protocol exit 0 (the pin is in its tsc program).dispatch-gates --commands: 70 derived, 70 run, each exit captured before any pipe, all 0.--ranprinted "70 derived, 70 run, 0 NOT-MEASURED (a DERIVED zero)".scripts/ablation-replace.mjs; each mutation landed and was restored to the HEAD blob withgit diff HEADempty):Acceptance notes
resolvePreviewParentsis outside the claim's named functions. It is the third binder of the sameparentroot, in the same file.parentnow refuses every write. That is the fail-closed answer a default member already got before this change.🤖 Generated with Claude Code
https://claude.ai/code/session_01MHGpe6k6Yrmt3TsGu3wgYP
Generated by Claude Code