Repository navigation
feat(objectql): a referenceNameVisible lookup or user field names a target the reader is refused, on $expand (#29460) - #29576
Conversation
…et by name and id on $expand
A lookup or user field declaring referenceNameVisible answers
{ id, name, readRefused: true } on $expand for a target the reader is
refused (object gate or row scope); readable targets, dangling ids and
fields without the key keep today's answers.
Claude-Session: https://claude.ai/code/session_01ADHkmRXbaXBLCJeJXfoh21
Co-authored-by: Claude <noreply@anthropic.com>
… record the key live The describe states the served value and the field-level floors; the liveness ledger row cites the engine module and its real-driver test. Reference docs and liveness counts regenerated. Changeset added. Claude-Session: https://claude.ai/code/session_01ADHkmRXbaXBLCJeJXfoh21 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmRXbaXBLCJeJXfoh21 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 170 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 c5495da519080862348f13a8ab4a7eb19f9ad429 && git checkout c5495da519080862348f13a8ab4a7eb19f9ad429
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b16ee8ba069d00bc00d01295869c1036fc9ea2e2 36a4f3f758992e8d830376947ae4f1b734ff281d && git checkout -B drift-repro b16ee8ba069d00bc00d01295869c1036fc9ea2e2 && git merge --no-ff 36a4f3f758992e8d830376947ae4f1b734ff281d
node scripts/docs-audit/affected-docs.mjs --json b16ee8ba069d00bc00d01295869c1036fc9ea2e2
|
|
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67 Contract-review-claim: session_01ADHkmQJdgarUjPLNgFBY67 · head 7e8a7b3 · 2026-11-02T19:39Z |
|
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67 Contract reviewServed-tier: Inputs: card #29460 (body, triage 6581850357, claim 6583606476, dev report 6586656992), PR #29576 body and file list, ① Derived judgments
② Semver levelThe changeset Clause-②: yes (widening) Both as the claim declared. ③ Boundary flags
④ Design judgment
Findings, none must-fix:
Implemented-by: VERDICT: PASS |
… row-serving door pin The new `engine-reference-name-visible.ts` asks `canReadObject` to tell which ids the caller's own `$expand` left bare it is refused. It runs only inside `expandRelatedRecords`, after the expansion of a read its door already admitted through the exposure decision, and serves no row of its own, so it is classified not-a-door, beside the file-field hydration precedent. Claude-Session: https://claude.ai/code/session_01ADHkmRXbaXBLCJeJXfoh21 Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67 席位核对 · ACCEPT · PR #29576 · 2026-11-02T20:39ZHead-sha: 维护者速读(席位终稿)
核对六项
|
Requested by Steve · project thread
Fixes #29460
维护者速读
referenceNameVisible: true的 lookup / user 字段,在$expand时,读者被拒读的目标记录现在答{ id, name, readRefused: true };此前它只答裸 id。referenceNameVisiblelookup's name as plain text, and add its Studio form row (#29171 ruling B) #29462 照这个形状把名字渲染成不带链接的纯文本。Record #<id>底名。不动存储形态,回滚即 revert 本 PR。packages/objectql/src/engine-reference-name-visible.ts的两次读各自拿到什么,再看packages/rest/src/data-reference-name-visible.test.ts里的负向钉子与对照组。pnpm --filter @objectstack/objectql build,再在packages/rest下pnpm exec vitest run src/data-reference-name-visible.test.ts,23 个全绿。Record #<id>,还是照给名字),其余无。What changes
FieldSchema.referenceNameVisiblewas declared ahead of its runtime. It is now served by objectql's$expand:{ id, name, readRefused: true }in place of the bare id.idis the id the referencing row holds.nameis the target's display name as ADR-0079 resolves it (nameField, thendisplayNameField, then the derived title, with the floorRecord #<id>).readRefused: truesays in the value that the reader was refused the target (themetadataRefusedprecedent of a refused file value), so a renderer shows the name as text and links nothing. Amultiple: truefield is answered per element.$expandnesting is forwarded.idand the resolved name field, under the tenant-kept elevation ({ ...caller, isSystem: true }with operation-private keys stripped, so the driver still scopes it to the caller's organization). The$expandrequest's projection, nested expand, filter and order never reach it, and the field's owndisplayFielddoes not choose the column it reads.getReadableFields) does not hold the name field, the field declares amaskingRule, it is a formula reading through another reference, it is not a title-typed column, or no field answer can be had, the name is theRecord #<id>floor. See Open questions 1.sys_usernames through auserfield or a lookup tosys_usertake the same path; there is no separate carve-out..describe()drops its[PLANNED — not yet served]marker and states what is served;packages/spec/liveness/field.jsonrecords the keylive, citing the server test; generated reference docs and the liveness counts are regenerated.Code:
packages/objectql/src/engine-reference-name-visible.ts(new; the module-split host pattern, engine imported as a type only), one call site and one private method inpackages/objectql/src/engine.ts#expandRelatedRecords, andisReadRefusalexported fromengine-file-reference-hydration.tsso the two sub-reads that serve a refusal in the value read one predicate.Response shape for the ui card (#29462)
After
$expandof areferenceNameVisiblefield, each value is one of:{ id, name, readRefused: true };namemay be the floorRecord #<id>$expandWhy
Ruling 6574398310 (letter B) on #29171: a principal who can read the record carrying the field may see the referenced record's name and id even when it cannot read that record, and nothing else of it. The ui lane renders it (#29462).
Readings this card starts from (
mainat a922779, real ObjectQL over SQLite, REST server)GETby id and REST list (no$expand): the lookup holds the bare FK id for every reader (readable, row-scope-hidden, gate-refused, dangling).$expand: a readable target is the full record; a row-scope-hidden or gate-refused target, or a dangling id, keeps the bare id. Asys_userthe reader's row scope admits is expanded; one it does not keeps the bare id.sys_username carve-out exists in the engine or the plugins.Verification
Clause-②: yes (widening)
Metadata-protocol: none
Keep one value on each of the two lines above, each line starting with its key.
Human Minimum
item 5 says what they mean.
Tests —
packages/rest/src/data-reference-name-visible.test.ts(new, 23 tests): real ObjectQL, the SQLite driver, the security plugin and the REST server; the record, list and query doors; a single lookup, amultiple: truelookup, auserfield and a lookup tosys_user; the row-scope, gate and cross-organization cases; the floors; and a control for every negative pin.packages/specfield and liveness suites: the field, field-reference-name-visible and liveness suites 3 files, 181 passed; on this head the package suites read objectql 534/534 files, 10391 tests passed; rest 374/374 files, 7080 passed, 332 skipped; spec 792/792 files, 23522 passed, 1 todo.Forced test edit:
packages/spec/src/data/field-reference-name-visible.test.tspinned the[PLANNEDmarker in the describe. It now pins the marker's absence and the served shape (the designed flip of a declared-ahead key); the bounds pins are unchanged.Ablations (each planted with
scripts/ablation-replace.mjs, objectql rebuilt, presence proven indist/withscripts/ablation-dist-preflight.mjs, the suite run, the source restored and proven equal to HEAD; after the last leg every marker proven absent fromdist/, the tree clean, the suite 23/23):$expandprojection, nested expand and nested filter and the field'sdisplayFieldforwarded into the name read, and its row spread into the valuedisplayFieldpins among them)maskingRuleignoredLocal gates on
7e8a7b30:dispatch-gates --commandsderived 109 commands after the last commit; 109 run, every one exit 0;--ranreconcile 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN.pnpm lintexit 0;typecheckof objectql, rest and spec exit 0.Dogfood battery on
7e8a7b30:os-verify-lockVERDICT command-exit 0 (held 29m15s, shared box) · Test Files 397 passed, 3 skipped (400) · Tests 3277 passed, 28 skipped (3305) · no failuresContract review: the paths put this on the schema face (
record-recognisers.mjs --paths), atCONTRACT_REVIEW_TIER, scopepackages/spec/src/data/field.zod.ts.Open questions
Record #<id>with the id, and the describe says so; B serve the name regardless, and amend the describe to say the key lifts field-level rules on the name field too (drop three checks inservableNameField). Recommendation A: it serves the ruling's "name and id" without widening any field grant, and B stays a contained follow-up if ruled. A reader who can read the target row but not its name field is served exactly what it is served without the key (the ruling's "can read the target ⇒ as today").$expandonly; a ui that wants the name expands the field.{ id, name, readRefused: true }besideFileRefusedValueSchemainfield-value.zod.ts) is spec-lane follow-up; this PR exports the TypeScript shapeReferenceNameOnlyValuefrom the engine module only.Human Minimum
Each item is one rule from
CONTRIBUTING.md › Human Minimum,
which links the AGENTS.md text that states it.
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code