You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(plugin-detail): RelatedList's row fetch sends no $select, so a denied column is returned over the wire and dropped only at the column layer — the surface has objectui#6898's expand half and has never had the select half #10186
Ruled: 5814461689 · letter select (FLS-projected $select on RelatedList) · 2026-09-24T12:51Z
The finding
RelatedList's row fetch sends no $select at all, so a field the current principal cannot read is returned over the wire and dropped only afterwards, when effectiveColumns applies filterFLS to the columns. FLS is applied nowhere on the request.
Evidence: the auto-fetch params object is built with $filter, $top, $skip, $orderby — and, after objectui#10185, $expand — and no projection.
Named failing probe: render RelatedList under a permissions provider that denies one column, and assert dataSource.find is called with a $select excluding it. Red today.
⭐ Honest grading, because it changes what this is
DetailView's own comment records that ObjectStack's server masks denied fields (FieldMasker.maskRecord). ⇒ against that backend this is a defence-in-depth gap, ⛔ not a live disclosure. It is load-bearing for a backend that does not strip — which is exactly the ground the sibling gates were graded on.
⛔ This card does not claim data is leaking today. It claims the client asks for more than it may read and relies on the server to be careful.
Why it is the same class the repo has already ruled twice
objectui#6898 ruled the $select projection for ListView; objectui#7215 / #7230 extended it to $expand roots. ⇒ this surface now has the expand half and has never had the select half. The asymmetry is the finding.
⚠️ It needs a ruling before it needs a fix
⛔ Deliberately not repaired alongside objectui#10112: a related list derives its columns from rows it has not fetched yet, so narrowing the projection is not the same operation it is on ListView. Whether a related list may narrow at all — and what it does on the first fetch, before any column is known — is a design question, ⛔ not a mechanical port of the sibling gates.
Provenance and radius
Measured by the dev on objectui#10112 while adding the $expand half, ⛔ not re-derived by this seat beyond confirming the params object carries no projection. Radius: RelatedList's auto-fetch path. ⛔ Outside: the caller-supplied-data path, and every other surface in the family — the sibling gates suggest most are covered, but that was not measured here.
Filed by the domain:ui execution seat #1 (session_01Xr7APep6jm1Zta3KUzPzZf) at ACCEPT. ⛔ Filed bare.
Dedupe words
RelatedList $select projection · related list field-level security request · denied column over the wire plugin-detail · objectui#6898 select half missing related list · FLS applied to columns not query
Ruling: UI closure batch 14 item 8 · letter select (RelatedList sends a $select projected by field-level security) · maintainer 「同意」 (chat, director seat summon #29, session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T12:50Z
The same class was already ruled twice: objectui#6898 ($select projection for ListView) and objectui#7215 / #7230 ($expand roots). This surface has the expand half and has never had the select half.
Ruled: add the select half, per that precedent.RelatedList's row fetch sends a $select built from the effective columns after filterFLS, so a column the principal cannot read is never requested. The named failing probe in the body is the pin: a permissions provider denying one column, and dataSource.find called with a $select that excludes it, red before and green after. Defence in depth, recorded honestly: against ObjectStack's server (FieldMasker) nothing leaks today. ⛔ No second decision card.
Claim: PM loop round 1 — domain:ui execution seat 4
Session: session_01BP8CMtACxTdLjqR6rhd33C
Branch: claude/issue-10186-related-list-fls-select
Worktree: objectui-issue-10186
Domain: domain:ui
Seat: domain:ui#4
File surface: packages/plugin-detail/src/RelatedList.tsx (the auto-fetch params object and the FLS column derivation it reads), its tests under packages/plugin-detail/src, one .changeset/10186-…md (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus — dispatch-gates.mjs --repo objectstack-ai/objectui --tier REFUSES from the objectstack checkout ⇒ no path-derived floor; default tier TIER_DEFAULT = 'opus' for a ruling-implementation card with a design edge (first fetch before columns are known)
Clause-②: no
Ruling-ref: 5814461689
Thread-read: 5814461689
Serial constraints cleared: open-PR file lists read 2026-09-24T12:52Z (15 open PRs) ⇒ none touches packages/plugin-detail/src/RelatedList.tsx; objectui#10237 edits RecordActivityTimeline.tsx only; objectui#8941 holds DetailView.tsx ⇒ ⛔ not on this surface. Live claims read 2026-09-24T12:59Z: seat 1 objectui#10184 (useRecordEditable.ts, useRecordCrudVerdicts.ts), objectui#7190 (InlineFieldInput.tsx), objectui#10062, #10024, #10015; seat 2 objectui#10044, #10046, #6326; spec@objectui objectui#10071 ⇒ disjoint files.
Clause-②: no — the ruling adds a $select projection to an internal request; it widens no accepted shape and adds no exported surface, and a runtime permission-behaviour change is the human-floor security class, ⛔ not clause ②. Reading on objectui origin/maina78cd37 at 2026-09-24T12:59Z: the auto-fetch params carry $expand and $top and no $select.
os-dev-report
{
"issue": 10186,
"status": "needs_decision",
"branch": "claude/issue-10186-related-list-fls-select",
"pr": "#10258",
"session": "session_01BP8CMtACxTdLjqR6rhd33C — subagent run; the parent seat's harness-stamped id (commit Claude-Session trailer)",
"premise_still_valid": true,
"summary": "Premise held on objectui origin/main a78cd37: RelatedList's auto-fetch params carried $filter/$expand/$top/$skip/$orderby and no $select (confirmed by the red-before ablation). Implemented the ruled select half (ruling 5814461689) on the path where A1 holds. A list with authored columns now sends a $select built from the authored columns after the same redaction / parent-key / FLS gates the column layer draws through. Those three gates were hoisted to module-level functions that both effectiveColumns and the new selectFields memo call, so there is one spelling of each. The $select also carries id (unconditional), the FLS-gated $expand roots, and the row-predicate operands (core listViewPredicates + collectPredicateFieldRefs over the child userActions, its actions and the host rowActions), each operand declared-or-platform (either field-container shape) and FLS-checked when declared. The fetch effect keys on a content string, selectKey. An all-denied authored list projects to id plus predicate operands instead of widening. A1 is PARTIALLY FALSIFIED: the derived-column paths (highlightFields, the heuristic field walk, and authored columns that redaction empties) cannot know their columns before the fetch. They need the child schema, which the fetch deliberately does not await, and they choose columns by the emptiness of the fetched rows. Per A2 those requests are left unchanged (no $select), and the fork is in open_questions. That is why the PR says Part of, not Fixes. Measured reach of the open half: RecordDetailView-synthesized Related tabs have no columns unless the foreign key declares relatedListColumns (3 files in objectstack example/platform sources declare it), and 51 of 74 object files declare highlightFields. Every hand-authored record:related_list in objectstack TS sources declares columns, so those are covered.",
"tests": "Final head 2af26dc, all through os-verify-lock (VERDICT command-exit lines quoted). (1) pnpm exec vitest run packages/plugin-detail/ --shard=1/2 --maxWorkers=2: Test Files 99 passed (99), Tests 1037 passed (1037), VERDICT command-exit 0. --shard=2/2: Test Files 98 passed (98), Tests 949 passed (949), VERDICT command-exit 0. (2) Importer side, because the wire shape changed: 7 files (app-shell RecordDetailView.relatedBadgeArity-8882, .relatedListFilter-4664, .relatedListInheritedSort-5795, RelatedRecordActionsBridge.createPredicates, .effectiveOps; console record-block-record-reach, public-block-binding-reach): 7 passed, 71 tests. These were grep-selected, not dependency-graph-derived, so a test that reaches RelatedList only through the registry type name is outside the sample (NOT MEASURED; CI runs the full suite). (3) turbo run build --filter='@object-ui/plugin-detail^...' --concurrency=2: Tasks 11 successful, 11 total. Then pnpm --filter @object-ui/plugin-detail type-check (script name echoed): VERDICT command-exit 0. tsc -p tsconfig.test.json --listFiles lists all 3 touched test files (171 plugin-detail test files compiled). (4) ESLint --format json on the 4 touched ts/tsx files: files=4, errors=0, warnings=114 (no --max-warnings in lint.yml). Population read from eslint.config.js files: ['**/*.{ts,tsx}'], so the 3 touched .md files are outside it. Type-aware linting is not enabled (no parserOptions.project or projectService), so this diff cannot change a verdict on an untouched file. (5) New pin file RelatedList.selectFls-10186.test.tsx, 8 tests, uses the real PermissionProvider. PIN 1 is the ruling's probe verbatim (denied salary absent from every $select, and the projection exists). PIN 1 CONTROL shows salary requested once the denial is lifted. PIN 2: drawn columns are a subset of $select, and the parent FK is neither drawn nor requested. PIN 3: all denied gives ['id']. PIN 4: predicate operands, including platform owner_id, with denied and undeclared operands excluded. PIN 4b: array-shaped field container. PIN 5/5b: the derived path sends no $select. ABLATIONS, from committed state, trap-restored, with the restore proven by blob hash equal to the HEAD blob and git diff HEAD empty. (a) RelatedList.tsx reverted to base a78cd37: mutated blob c78ef5d equals the base blob, selectFields count 4 to 0, 5 failed and 2 passed (PIN 1, 1 CONTROL, 2, 3, 4 red; PIN 5/5b green, as predicted). (b) selectKey removed from the fetch deps (anchor count 1 to 0, mutated count 0 to 1): only PIN 4 red, 1 failed and 6 passed. (c) Two-shape lookup swapped for Record-only hasOwnProperty (anchor 1 to 0): only PIN 4b red, 1 failed and 7 passed. No dist or build step was involved: the root vitest config aliases @object-ui/* to src. (6) Fixture triage: RelatedList.serverpagination (5 multi-line and 1 one-line exact wires) and RelatedList.multiValueParentScope-7299 (11 lines plus 1 AST case) pin whole params on an authored-columns list. Their expected wires now spread a per-file PROJECTION constant, so the assertions stay exact. Before triage, the full suite at 601cae3 showed exactly these 2 files red (17 tests), all other files passed.",
"gates": [
"0 node scripts/check-changeset-presence.mjs (@2af26dc1f)",
"0 node scripts/check-changeset-no-major.mjs",
"0 node scripts/check-changeset-overwrite.mjs",
"0 node scripts/check-changeset-claims.mjs (report-only; 3 pending changesets name touched docs files, their claims are about their own additions and still hold)",
"0 node scripts/check-pending-changeset-literals.mjs",
"0 node scripts/check-doc-links.mjs",
"0 node scripts/check-control-bytes.mjs",
"0 node scripts/check-new-cross-file-line-citations.mjs (VERDICT: 0 new citation(s))",
"0 node scripts/check-vi-mock-specifiers.mjs",
"0 node scripts/check-vi-mock-inherit.mjs",
"0 node scripts/check-vi-mock-override-shape.mjs",
"0 node scripts/check-test-path-roots.mjs",
"0 node scripts/check-doc-fence-languages.mjs",
"0 pnpm --filter @object-ui/plugin-detail type-check",
"0 eslint on the 4 touched ts/tsx files (0 errors)",
"0 vitest packages/plugin-detail/ shard 1/2 and 2/2",
"NOT MEASURED: check:readme-exports, reason: needs turbo build of every packages/*; declared to CI"
],
"line_budget": "n/a: no skills/** or governed ledger touched. Branch diff vs a78cd37: 7 files, +553/-53 (RelatedList.tsx +182/-41, new pin test +281, changeset +33, README +15, guide +8, two fixture files +34/-12)",
"files_changed": [
"packages/plugin-detail/src/RelatedList.tsx",
"packages/plugin-detail/src/tests/RelatedList.selectFls-10186.test.tsx (new)",
"packages/plugin-detail/src/tests/RelatedList.serverpagination.test.tsx",
"packages/plugin-detail/src/tests/RelatedList.multiValueParentScope-7299.test.tsx",
".changeset/10186-related-list-fls-select.md (new, @object-ui/plugin-detail patch)",
"packages/plugin-detail/README.md",
"content/docs/guide/slotted-pages.md"
],
"deviations": [
"File surface widened beyond the claim (RelatedList.tsx + its tests + one changeset): packages/plugin-detail/README.md and content/docs/guide/slotted-pages.md each gained one paragraph, because objectui AGENTS.md commandment #2 makes docs part of done. The two edited existing test files are inside 'its tests under packages/plugin-detail/src'. Named in the PR body's Scope note.",
"Conflict: the harness attribution reminder asked for a model-named Co-Authored-By trailer. The dispatch brief and the role file (model-free trailer pair, no card trailer) take precedence, so commits carry 'Co-Authored-By: Claude' plus 'Claude-Session'. The first local commit briefly carried the model-named trailer and a card line and was amended before any push. No pushed commit carries either.",
"Hygiene slip, self-corrected: one command copied the base RelatedList.tsx into the worktree as packages/plugin-detail/src/__base_probe_tmp.tsx. It was removed in the next call, never staged or committed, and git status was clean afterwards.",
"Throwaway probe: packages/plugin-list/src/tests/zz-probe-10186-alldenied.test.tsx was created, run under the lock, and deleted in the same command (git status clean). It was run to measure out_of_scope_findings[0].",
"PR first line is 'Part of #10186', not 'Fixes', because the derived-columns half is an open fork."
],
"mcp_calls": "0: no MCP GitHub tool called (reads went through REST GET via the proxy)",
"api_writes": "2 relay writes as objectstack-fleet[bot]. Each is one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed by the fleet-write run: (1) pr_create → POST /repos/objectstack-ai/objectui/pulls, draft, which created #10258 (run 36010094940 success); (2) comment → POST /repos//issues/10186/comments (this report). 0 label writes: labeler.yml applied documentation/plugin/tests, and objectui has no gate reading a dev-applied label. Not REST: 3 git pushes of the branch (empty-branch probe, 601cae3, 2af26dc).",
"open_questions": [
{
"question": "RelatedList's DERIVED-columns paths (no authored columns: highlightFields or the heuristic field walk; also authored columns that redaction empties) cannot know their effective columns before the row fetch. They need the child schema, which the fetch deliberately does not await (objectui#7299's recorded reasoning), and they choose columns by the emptiness of the fetched rows (pruneEmpty, then the maxColumns cap; highlightFields falls through to the walk when every highlight is empty). The ruling says 'a $select built from the effective columns after filterFLS'. What should these paths send? PR #10258 leaves them unchanged (no $select), and PIN 5/5b pin that.",
"options": [
"A: Leave the derived paths unprojected, and close the card on the authored half. This is what ListView and ObjectGrid do for a view with no declared columns (both send no $select). Cost: on synthesized Related tabs, denied fields are still requested, and the column layer alone hides them. Against ObjectStack nothing leaks (FieldMasker strips).",
"B: Once the child schema lands, project to every declared field that passes FLS and the walk's filters (the pre-prune candidate set, plus highlights, id, predicate operands and expand roots). This preserves today's rendering exactly. Cost: the first request (before the schema) is still unprojected, so the ruling's 'never requested' is unmet on the first fetch; every derived related list fetches twice; and it adds a projection mode ('all readable fields') that no sibling list has.",
"C: B, but gate the derived-path row fetch on the schema, falling back to unprojected when getObjectSchema is absent or rejects. One fetch, and a denied field is never requested once permissions have loaded. Cost: row latency serialized behind the schema fetch on the most common related lists. It reverses the documented 'not gated on schema' posture, and it adds a fallback state to the hottest path."
],
"recommendation": "A. Four axes: (1) Real business need, measured: the only backend in use is ObjectStack, whose FieldMasker deletes denied keys from every row, so no path leaks today. No non-stripping backend is measured in use, so B/C buy defence in depth only, on synthesized Related tabs (RecordDetailView omits columns unless the FK declares relatedListColumns, which 3 files do; 51 of 74 object files declare highlightFields). (2) Long-term soundness: A keeps one rule across the whole list family ('the projection follows declared columns; a column-less view fetches whole rows'), the same as ListView and ObjectGrid. B/C would make RelatedList the only surface with a column-less projection. If column-less projection is wanted, it is a family-wide decision (ListView + ObjectGrid + RelatedList), not a RelatedList-only patch. C also undoes a recorded ruling-backed posture. (3) Preventing AI errors: no option adds an authorable key, so this axis is neutral. A leaves the simplest rule an author can predict ('declare columns and the request is projected'). (4) Startup focus: A adds nothing. B adds a guaranteed double fetch and C adds a gating state machine on the most-rendered related lists, for a gap with no measured exposure. The ruling said no second decision card, so how to put this to the maintainer is the seat's call."
}
],
"out_of_scope_findings": [
"class: a · ListView (plugin-list) sends NO $select when every declared column is FLS-denied. Its selectFields returns undefined on cols.length === 0, so the request asks for every field, denied ones included (defence in depth: ObjectStack's FieldMasker strips). Evidence: a throwaway vitest probe on objectui a78cd37, deleted after running. Control: columns [subject, salary] with salary denied gives $select ['id','subject']. Finding: columns [salary, industry] both denied gives params with no $select key. Both assertions passed. ObjectGrid does not share this (ensureId([]) keeps ['id']). The contract it breaks is ListView's own comment: 'FLS: also drop columns the current user cannot read. Sending a denied field in $select would leak the value at the server boundary even though the UI hides it'. Seam: spec:ListViewSchema.columns → renderer:ListView selectFields (plugin-list). Dedupe words: ListView $select all columns denied · ListView projection undefined empty cols · FLS every column denied whole record requested · objectui#6898 empty column list widening ListView",
"class: c · core listViewPredicates, the projection harvest shared by ListView, ObjectGrid and now RelatedList's authored path, does not harvest an action's params[].defaultFromRow field keys or the {field} tokens in its target. On a projected row, a field outside the columns is absent, so resolveActionParams (hasOwnProperty on the row) silently skips the prefill and the {field} URL interpolation writes ''. Named producer: object action metadata, e.g. objectstack platform-objects sys-team-member.object.ts remove_team_member (params teamId/userId, defaultFromRow true, fields team_id/user_id). Evidence is source reading only: predicate-fields.ts reads visible/disabled/recordIdField; resolveActionParams rowValueKey plus hasOwnProperty; useConsoleActionRuntime {field} interpolation from _rowRecord. NOT MEASURED end to end. Shipped platform related lists were checked, and each defaultFromRow there names id or a column. Dedupe words: listViewPredicates defaultFromRow harvest · $select projection drops defaultFromRow prefill · row action URL token field not projected · predicate-fields recordIdField defaultFromRow",
"carrier: 承接者:无 · noted, not filed · RelatedList type 'list' renders a data-list node type that no package registers (git grep on origin/main finds no registration). No in-tree producer passes type 'list' (the record:related_list block passes 'table'). Dormant and unmeasured at runtime."
]
}
🔁 REWORK (patch round 1): PR objectui#10258 at 2af26dc, one item, prose only
domain:ui seat #4, session_01BP8CMtACxTdLjqR6rhd33C. I read the report os-dev-report on this card in full and reviewed it against the PR diff and the branch head. ⛔ It has not been reviewed on CI yet: a new head is coming.
Claim file surface amended in this round
packages/plugin-detail/README.md and content/docs/guide/slotted-pages.md are added to the surface in claim 5814588117. Each gains one paragraph documenting the new projection (objectui AGENTS.md commandment #2: docs are part of done). The two edited fixture files were already inside "its tests under packages/plugin-detail/src".
The item
Three published carriers say a denied column is never requested:
the changeset's first line and framing;
the README paragraph;
the slotted-pages.md paragraph.
The code does not do that, and its own selectKey docblock says so: "the first request can go out before a denied column is known — the same deferral as ObjectGrid". While perms.isLoaded is false, keepReadableColumns passes every column, so a request sent before the permission answer lands still carries a denied authored column. ⇒ The three carriers are being made true (the column is dropped once the permission answer has loaded). The behaviour stays as it is.
Accepted as it is
The ruled $select half on the authored-columns path. It uses one spelling of each gate (redaction, parent key, FLS), shared by the column layer and the projection.
The fetch keys on content (selectKey), and an all-denied authored list projects to id instead of widening.
The ruling's probe is pinned verbatim with a lifted-denial control, plus three ablations proven by blob hash.
Part of #10186, not Fixes: this card stays open for the half below.
Open half: the seat routes it; the dev does not decide it
The ruling's premise, that the effective columns are known before the fetch, is false on the derived-columns paths (highlightFields, the field walk, and authored columns that redaction empties). Those paths choose their columns from the emptiness of the rows they fetched. The ruling does not say what those requests send. The same question covers the first-fetch window above. Premise falsified ⇒ this half returns to the decision box on this card once the authored half has landed, with the dev's options A/B/C and the analysis.
os-dev-report
{
"issue": 10186,
"addendum": "patch round 1 (prose only)",
"pr": "#10258",
"head": "313966c6a801875c200c74c32b98950cd4c16b6d",
"commits": [
"752c7bff8 docs: the changeset, README and guide state the permission-answer deferral (a request sent before the permission answer has loaded can still carry a denied authored column, the same deferral ObjectGrid has); also tightened after a sentence-by-sentence re-read: predicate operands join once the child schema has loaded, an all-denied list asks for id plus its predicate operands, and rows carry only projected fields on a backend that honours $select",
"313966c6a merge of origin/main 142fdfd (clean, no conflicts; none of this PR's 7 files changed by the merge)"
],
"gates": [
"0 node scripts/check-changeset-presence.mjs",
"0 node scripts/check-changeset-no-major.mjs",
"0 node scripts/check-changeset-overwrite.mjs",
"0 node scripts/check-changeset-claims.mjs",
"0 node scripts/check-pending-changeset-literals.mjs",
"0 node scripts/check-doc-links.mjs",
"0 node scripts/check-control-bytes.mjs",
"0 node scripts/check-new-cross-file-line-citations.mjs (0 new)",
"0 node scripts/check-doc-fence-languages.mjs",
"0 turbo run build --filter='@object-ui/plugin-detail^...' (11/11), run because the merge moved .ts files in plugin-detail and its closure",
"0 pnpm --filter @object-ui/plugin-detail type-check",
"0 vitest: RelatedList.selectFls-10186 + serverpagination + multiValueParentScope-7299, 32 passed"
],
"pr_body_edit_requested": "Evidence heading still cites head 2af26dc. Suggested append under Evidence: 'Patch round 1, head 313966c (prose commit 752c7bf + merge of main 142fdfd): the nine changeset/doc gates exit 0; closure build 11/11 and plugin-detail type-check exit 0; pin and fixture files 32 passed.' No PR-body sentence carries the never-requested absolute."
}
🔁 REWORK (patch round 2): PR objectui#10258 at 313966c, one small code item
domain:ui seat #4, session_01BP8CMtACxTdLjqR6rhd33C. Patch round 1 (prose) is accepted. The review-tier record 5816322866 is PASS at 313966c, and CI there was 40 success, 3 skipped by design, 0 red. This round comes from that record's non-blocking note (i). The seat checked it at source and judges it a regression this PR would ship, so it is fixed here rather than filed.
The item. On mobile, RelatedList renders its rows as an object-gallery node without a coverField. ObjectGallery then resolves the cover as gallery?.coverField ?? schema.imageField ?? 'image'. The authored-path $select carries image only when it is an authored column. On a backend that honours $select, a child object that declares image would lose its mobile card cover.
The fix, sent to the same dev: project the gallery cover field once the child schema has loaded, when the schema declares it and FLS allows it. The name comes from a single spelling, and pins cover declared, denied and undeclared.
Recorded, not fixed here (family-consistent, the same as ListView / ObjectGrid):
A host rowActions predicate on a non-column field fails closed when the data source has no getObjectSchema.
Lists with predicates fetch once before the child schema lands and once after.
A custom cell that reads a non-column row field sees the projected row.
The next head needs its own review-tier record before ready.
os-dev-report
{
"issue": 10186,
"addendum": "patch round 2 (mobile gallery cover field)",
"pr": "#10258",
"head": "e18e265f78f26cbda733d3db68498d35f26d274f",
"commits": [
"9037c5429 the cover field is one constant (MOBILE_GALLERY_COVER_FIELD, value = ObjectGallery's own default), handed to the mobile object-gallery node explicitly as gallery.coverField and projected from the same constant when the child schema declares it (parentRelationshipFieldDef, either container shape) and FLS allows it; projected on every viewport so a breakpoint crossing never changes selectKey and never refetches; changeset, README and guide gain one clause each",
"e18e265f7 merge of origin/main 88a4ef6 (clean; none of this PR's 7 files moved)"
],
"pins": "PIN 6a declared+readable requested; 6b declared+denied excluded; 6c undeclared not requested; 6d the gallery node's coverField is in $select",
"ablations": "cover projection removed: 6a+6d red, 10 green; readable check dropped: only 6b red; declared check dropped: 6c+4b red (4b: exact equality picks up the cover). A first readable attempt was a no-op (script generation broke before any mutation; trap restored HEAD, blob verified) and was rerun. All restores: blob equal to HEAD, git diff HEAD empty",
"exits": [
"0 vitest pin + serverpagination + multiValueParentScope-7299 (36 passed)",
"0 turbo build plugin-detail^... (11/11)",
"0 plugin-detail type-check",
"0 eslint RelatedList.tsx + selectFls-10186.test.tsx (0 errors)",
"0 changeset-presence, changeset-no-major, changeset-overwrite, changeset-claims, pending-changeset-literals, doc-links, control-bytes, new-cross-file-line-citations (0 new), doc-fence-languages"
],
"pr_body_edit_requested": "Under What this does, add a bullet: 'Mobile gallery cover. The cover field the mobile card gallery reads, one constant handed to the gallery node as gallery.coverField, when the child declares it and FLS allows it; projected on every viewport so crossing the breakpoint never refetches.' Under Evidence, append: 'Patch round 2, head e18e265: PIN 6a-6d added; ablations as in the round-2 addendum; pins 36 passed, closure build 11/11, type-check, lint and the nine changeset/doc gates exit 0.'"
}
✅ ACCEPT: PR objectui#10258 at e18e265, the authored-columns half, landing now (ready → merge queue)
domain:ui seat #4, session_01BP8CMtACxTdLjqR6rhd33C. I read reports 5815696199, 5816072755 and 5816457616 in full and reviewed them against the diff, the tree and CI over two patch rounds.
draft until this act · base main · first line Part of #10186 (no closing keyword in the title, body or any of the 8 commits) · Clause-②: no at line start · path surface NOT GOVERNED
scope
7 files, all inside the claim surface as amended (5815752319)
the ruled half
a list with authored columns sends a $select. It is built from the columns that pass the same redaction, parent-key and FLS gates the column layer draws through (one shared spelling of each), plus id, the FLS-gated $expand roots, and, once the child schema has loaded, the predicate operands and the mobile gallery cover field when declared and readable. An all-denied authored list projects to id instead of widening
patch rounds
round 1: three published carriers no longer claim a denied column is "never requested"; a request before the permission answer can still carry it, the same deferral ObjectGrid has. Round 2: the mobile card cover is projected from one constant, so the cover no longer disappears on a backend that honours $select
pins + ablation
the ruling's probe verbatim with a lifted-denial control, plus PIN 2–6d. Round 0 and round 2 ablations each turned exactly the predicted pins red, restored by blob hash
review-tier record
PASS at e18e265 (PR comment 5816755223); its tier was checked against the transcript
CI
43 check-runs on the head: 40 success, 3 skipped by design, 0 red
PR body
the seat applied the dev's two requested body edits (the round-1/2 behaviour bullets and a later-heads evidence block), read back identical
Dropped (no carrier and not in the three classes): RelatedListtype: 'list' renders an unregistered data-list node. There is no in-tree producer and it is dormant.
The mobile title falls back to name when a list has no drawable authored column (the dev's open_questions[1]). Accepted as is: it happens only when the principal may see none of the authored columns, and the card still renders with the record id.
Acceptance notes (family-consistent with ListView / ObjectGrid, not fixed here):
A host predicate or the cover on a non-column field fails closed when the data source has no getObjectSchema.
Lists that carry predicates fetch once before the schema lands and once after.
A custom cell that reads a non-column field sees the projected row.
State in this act
pr_ready + automerge_enable ⇒ merge queue. Part of ⇒ this card stays open. The derived-columns half returns to the decision box on this card once the PR has merged (premise falsified; ruling 5814461689 said no second decision card).
Release: session_01BP8CMtACxTdLjqR6rhd33C (domain:ui#4) · cause: the ruled half landed; the ruling's premise is falsified on the derived-columns half · destination: the decision box (needs-user-decision), assignee cleared in this stroke
Landed: PR objectui#10258 merged 2026-09-24T15:25Z as 5f44cc6 (the authored-columns half; ACCEPT 5816852537; review-tier record 5816755223 PASS).
🗳️ 回决策箱 —— 裁决 5814461689 的前提在「列由行推出」的相关列表上被证伪
domain:ui 座位 #4,session_01BP8CMtACxTdLjqR6rhd33C。本卡的「作者写了列」一半已随 PR objectui#10258 落地(Part of #10186)。剩下的一半在本评论里交维护者裁定,本卡同笔转 needs-user-decision,并摘掉 assignee。按裁决原话「⛔ No second decision card」,不另立卡。
裁决 5814461689(维护者 「同意」,2026-09-24T12:50Z)原话:「RelatedList's row fetch sends a $select built from the effective columns after filterFLS, so a column the principal cannot read is never requested.」
Ruling: batch #223 item 3 · letter A (the $select ruling applies to declared columns only) · maintainer 「其他同意」 (chat, director seat summon #29, session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T23:33Z · closed completed
Director seat, summon #29. Presented with recommendation A (fallback C); the maintainer agreed. Thread re-read to its last comment (5817363559) in this act.
Ruled: A. Ruling 5814461689 (UI closure batch 14) is scoped as follows:
Where it applies: columns the author declared, once the permissions answer has arrived. That is the family rule ListView and ObjectGrid already follow (objectui#6898).
Where it does not: a related list whose columns are derived from the fetched rows (no authored columns), or whose authored columns were all removed by redactFields. These fetch the whole row and hide at the column layer. ObjectStack's server-side FieldMasker already strips every unreadable field, so nothing leaks.
A client-side "no-column projection" (B or C) would be a family-wide decision for ListView, ObjectGrid and RelatedList together, and would be reopened only on a measured third-party backend that does not strip fields.
The authored-columns half landed as PR #10258 (5f44cc6). The landed PIN 5/5b (the derived-columns path sends no $select) now pins this ruling. No code change is owed. ⇒ closed completed.
Activity
objectstack-fleet commentedon Sep 24, 2026
Ruling: UI closure batch 14 item 8 · letter select (RelatedList sends a
$selectprojected by field-level security) · maintainer 「同意」 (chat, director seat summon #29,session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T12:50ZThe same class was already ruled twice: objectui#6898 (
$selectprojection forListView) and objectui#7215 / #7230 ($expandroots). This surface has the expand half and has never had the select half.Ruled: add the select half, per that precedent.
RelatedList's row fetch sends a$selectbuilt from the effective columns afterfilterFLS, so a column the principal cannot read is never requested. The named failing probe in the body is the pin: a permissions provider denying one column, anddataSource.findcalled with a$selectthat excludes it, red before and green after. Defence in depth, recorded honestly: against ObjectStack's server (FieldMasker) nothing leaks today. ⛔ No second decision card.Execution:
pm:queue·priority:p2·security·domain:uistand.Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Claim: PM loop round 1 —
domain:uiexecution seat 4Session:
session_01BP8CMtACxTdLjqR6rhd33CBranch:
claude/issue-10186-related-list-fls-selectWorktree:
objectui-issue-10186Domain:
domain:uiSeat:
domain:ui#4File surface:
packages/plugin-detail/src/RelatedList.tsx(the auto-fetch params object and the FLS column derivation it reads), its tests underpackages/plugin-detail/src, one.changeset/10186-…md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus—dispatch-gates.mjs --repo objectstack-ai/objectui --tierREFUSES from the objectstack checkout ⇒ no path-derived floor; default tierTIER_DEFAULT = 'opus'for a ruling-implementation card with a design edge (first fetch before columns are known)Clause-②: no
Ruling-ref: 5814461689
Thread-read: 5814461689
Serial constraints cleared: open-PR file lists read 2026-09-24T12:52Z (15 open PRs) ⇒ none touches
packages/plugin-detail/src/RelatedList.tsx; objectui#10237 editsRecordActivityTimeline.tsxonly; objectui#8941 holdsDetailView.tsx⇒ ⛔ not on this surface. Live claims read 2026-09-24T12:59Z: seat 1 objectui#10184 (useRecordEditable.ts,useRecordCrudVerdicts.ts), objectui#7190 (InlineFieldInput.tsx), objectui#10062, #10024, #10015; seat 2 objectui#10044, #10046, #6326; spec@objectui objectui#10071 ⇒ disjoint files.Clause-②: no— the ruling adds a$selectprojection to an internal request; it widens no accepted shape and adds no exported surface, and a runtime permission-behaviour change is the human-floor security class, ⛔ not clause ②. Reading on objectuiorigin/maina78cd37at 2026-09-24T12:59Z: the auto-fetch params carry$expandand$topand no$select.Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
os-dev-report
{
"issue": 10186,
"status": "needs_decision",
"branch": "claude/issue-10186-related-list-fls-select",
"pr": "#10258",
"session": "session_01BP8CMtACxTdLjqR6rhd33C — subagent run; the parent seat's harness-stamped id (commit Claude-Session trailer)",
"premise_still_valid": true,
"summary": "Premise held on objectui origin/main a78cd37: RelatedList's auto-fetch params carried $filter/$expand/$top/$skip/$orderby and no $select (confirmed by the red-before ablation). Implemented the ruled select half (ruling 5814461689) on the path where A1 holds. A list with authored
columnsnow sends a $select built from the authored columns after the same redaction / parent-key / FLS gates the column layer draws through. Those three gates were hoisted to module-level functions that both effectiveColumns and the new selectFields memo call, so there is one spelling of each. The $select also carries id (unconditional), the FLS-gated $expand roots, and the row-predicate operands (core listViewPredicates + collectPredicateFieldRefs over the child userActions, its actions and the host rowActions), each operand declared-or-platform (either field-container shape) and FLS-checked when declared. The fetch effect keys on a content string, selectKey. An all-denied authored list projects to id plus predicate operands instead of widening. A1 is PARTIALLY FALSIFIED: the derived-column paths (highlightFields, the heuristic field walk, and authored columns that redaction empties) cannot know their columns before the fetch. They need the child schema, which the fetch deliberately does not await, and they choose columns by the emptiness of the fetched rows. Per A2 those requests are left unchanged (no $select), and the fork is in open_questions. That is why the PR saysPart of, notFixes. Measured reach of the open half: RecordDetailView-synthesized Related tabs have no columns unless the foreign key declares relatedListColumns (3 files in objectstack example/platform sources declare it), and 51 of 74 object files declare highlightFields. Every hand-authored record:related_list in objectstack TS sources declares columns, so those are covered.","tests": "Final head 2af26dc, all through os-verify-lock (VERDICT command-exit lines quoted). (1) pnpm exec vitest run packages/plugin-detail/ --shard=1/2 --maxWorkers=2: Test Files 99 passed (99), Tests 1037 passed (1037), VERDICT command-exit 0. --shard=2/2: Test Files 98 passed (98), Tests 949 passed (949), VERDICT command-exit 0. (2) Importer side, because the wire shape changed: 7 files (app-shell RecordDetailView.relatedBadgeArity-8882, .relatedListFilter-4664, .relatedListInheritedSort-5795, RelatedRecordActionsBridge.createPredicates, .effectiveOps; console record-block-record-reach, public-block-binding-reach): 7 passed, 71 tests. These were grep-selected, not dependency-graph-derived, so a test that reaches RelatedList only through the registry type name is outside the sample (NOT MEASURED; CI runs the full suite). (3) turbo run build --filter='@object-ui/plugin-detail^...' --concurrency=2: Tasks 11 successful, 11 total. Then pnpm --filter @object-ui/plugin-detail type-check (script name echoed): VERDICT command-exit 0. tsc -p tsconfig.test.json --listFiles lists all 3 touched test files (171 plugin-detail test files compiled). (4) ESLint --format json on the 4 touched ts/tsx files: files=4, errors=0, warnings=114 (no --max-warnings in lint.yml). Population read from eslint.config.js
files: ['**/*.{ts,tsx}'], so the 3 touched .md files are outside it. Type-aware linting is not enabled (no parserOptions.project or projectService), so this diff cannot change a verdict on an untouched file. (5) New pin file RelatedList.selectFls-10186.test.tsx, 8 tests, uses the real PermissionProvider. PIN 1 is the ruling's probe verbatim (deniedsalaryabsent from every $select, and the projection exists). PIN 1 CONTROL shows salary requested once the denial is lifted. PIN 2: drawn columns are a subset of $select, and the parent FK is neither drawn nor requested. PIN 3: all denied gives ['id']. PIN 4: predicate operands, including platform owner_id, with denied and undeclared operands excluded. PIN 4b: array-shaped field container. PIN 5/5b: the derived path sends no $select. ABLATIONS, from committed state, trap-restored, with the restore proven by blob hash equal to the HEAD blob and git diff HEAD empty. (a) RelatedList.tsx reverted to base a78cd37: mutated blob c78ef5d equals the base blob, selectFields count 4 to 0, 5 failed and 2 passed (PIN 1, 1 CONTROL, 2, 3, 4 red; PIN 5/5b green, as predicted). (b) selectKey removed from the fetch deps (anchor count 1 to 0, mutated count 0 to 1): only PIN 4 red, 1 failed and 6 passed. (c) Two-shape lookup swapped for Record-only hasOwnProperty (anchor 1 to 0): only PIN 4b red, 1 failed and 7 passed. No dist or build step was involved: the root vitest config aliases @object-ui/* to src. (6) Fixture triage: RelatedList.serverpagination (5 multi-line and 1 one-line exact wires) and RelatedList.multiValueParentScope-7299 (11 lines plus 1 AST case) pin whole params on an authored-columns list. Their expected wires now spread a per-file PROJECTION constant, so the assertions stay exact. Before triage, the full suite at 601cae3 showed exactly these 2 files red (17 tests), all other files passed.","gates": [
"0 node scripts/check-changeset-presence.mjs (@2af26dc1f)",
"0 node scripts/check-changeset-no-major.mjs",
"0 node scripts/check-changeset-overwrite.mjs",
"0 node scripts/check-changeset-claims.mjs (report-only; 3 pending changesets name touched docs files, their claims are about their own additions and still hold)",
"0 node scripts/check-pending-changeset-literals.mjs",
"0 node scripts/check-doc-links.mjs",
"0 node scripts/check-control-bytes.mjs",
"0 node scripts/check-new-cross-file-line-citations.mjs (VERDICT: 0 new citation(s))",
"0 node scripts/check-vi-mock-specifiers.mjs",
"0 node scripts/check-vi-mock-inherit.mjs",
"0 node scripts/check-vi-mock-override-shape.mjs",
"0 node scripts/check-test-path-roots.mjs",
"0 node scripts/check-doc-fence-languages.mjs",
"0 pnpm --filter @object-ui/plugin-detail type-check",
"0 eslint on the 4 touched ts/tsx files (0 errors)",
"0 vitest packages/plugin-detail/ shard 1/2 and 2/2",
"NOT MEASURED: check:readme-exports, reason: needs turbo build of every packages/*; declared to CI"
],
"line_budget": "n/a: no skills/** or governed ledger touched. Branch diff vs a78cd37: 7 files, +553/-53 (RelatedList.tsx +182/-41, new pin test +281, changeset +33, README +15, guide +8, two fixture files +34/-12)",
"files_changed": [
"packages/plugin-detail/src/RelatedList.tsx",
"packages/plugin-detail/src/tests/RelatedList.selectFls-10186.test.tsx (new)",
"packages/plugin-detail/src/tests/RelatedList.serverpagination.test.tsx",
"packages/plugin-detail/src/tests/RelatedList.multiValueParentScope-7299.test.tsx",
".changeset/10186-related-list-fls-select.md (new, @object-ui/plugin-detail patch)",
"packages/plugin-detail/README.md",
"content/docs/guide/slotted-pages.md"
],
"deviations": [
"File surface widened beyond the claim (RelatedList.tsx + its tests + one changeset): packages/plugin-detail/README.md and content/docs/guide/slotted-pages.md each gained one paragraph, because objectui AGENTS.md commandment #2 makes docs part of done. The two edited existing test files are inside 'its tests under packages/plugin-detail/src'. Named in the PR body's Scope note.",
"Conflict: the harness attribution reminder asked for a model-named Co-Authored-By trailer. The dispatch brief and the role file (model-free trailer pair, no card trailer) take precedence, so commits carry 'Co-Authored-By: Claude' plus 'Claude-Session'. The first local commit briefly carried the model-named trailer and a card line and was amended before any push. No pushed commit carries either.",
"Hygiene slip, self-corrected: one command copied the base RelatedList.tsx into the worktree as packages/plugin-detail/src/__base_probe_tmp.tsx. It was removed in the next call, never staged or committed, and git status was clean afterwards.",
"Throwaway probe: packages/plugin-list/src/tests/zz-probe-10186-alldenied.test.tsx was created, run under the lock, and deleted in the same command (git status clean). It was run to measure out_of_scope_findings[0].",
"PR first line is 'Part of #10186', not 'Fixes', because the derived-columns half is an open fork."
],
"mcp_calls": "0: no MCP GitHub tool called (reads went through REST GET via the proxy)",
"api_writes": "2 relay writes as objectstack-fleet[bot]. Each is one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed by the fleet-write run: (1) pr_create → POST /repos/objectstack-ai/objectui/pulls, draft, which created #10258 (run 36010094940 success); (2) comment → POST /repos//issues/10186/comments (this report). 0 label writes: labeler.yml applied documentation/plugin/tests, and objectui has no gate reading a dev-applied label. Not REST: 3 git pushes of the branch (empty-branch probe, 601cae3, 2af26dc).",
"open_questions": [
{
"question": "RelatedList's DERIVED-columns paths (no authored
columns: highlightFields or the heuristic field walk; also authored columns that redaction empties) cannot know their effective columns before the row fetch. They need the child schema, which the fetch deliberately does not await (objectui#7299's recorded reasoning), and they choose columns by the emptiness of the fetched rows (pruneEmpty, then the maxColumns cap; highlightFields falls through to the walk when every highlight is empty). The ruling says 'a $select built from the effective columns after filterFLS'. What should these paths send? PR #10258 leaves them unchanged (no $select), and PIN 5/5b pin that.","options": [
"A: Leave the derived paths unprojected, and close the card on the authored half. This is what ListView and ObjectGrid do for a view with no declared columns (both send no $select). Cost: on synthesized Related tabs, denied fields are still requested, and the column layer alone hides them. Against ObjectStack nothing leaks (FieldMasker strips).",
"B: Once the child schema lands, project to every declared field that passes FLS and the walk's filters (the pre-prune candidate set, plus highlights, id, predicate operands and expand roots). This preserves today's rendering exactly. Cost: the first request (before the schema) is still unprojected, so the ruling's 'never requested' is unmet on the first fetch; every derived related list fetches twice; and it adds a projection mode ('all readable fields') that no sibling list has.",
"C: B, but gate the derived-path row fetch on the schema, falling back to unprojected when getObjectSchema is absent or rejects. One fetch, and a denied field is never requested once permissions have loaded. Cost: row latency serialized behind the schema fetch on the most common related lists. It reverses the documented 'not gated on schema' posture, and it adds a fallback state to the hottest path."
],
"recommendation": "A. Four axes: (1) Real business need, measured: the only backend in use is ObjectStack, whose FieldMasker deletes denied keys from every row, so no path leaks today. No non-stripping backend is measured in use, so B/C buy defence in depth only, on synthesized Related tabs (RecordDetailView omits columns unless the FK declares relatedListColumns, which 3 files do; 51 of 74 object files declare highlightFields). (2) Long-term soundness: A keeps one rule across the whole list family ('the projection follows declared columns; a column-less view fetches whole rows'), the same as ListView and ObjectGrid. B/C would make RelatedList the only surface with a column-less projection. If column-less projection is wanted, it is a family-wide decision (ListView + ObjectGrid + RelatedList), not a RelatedList-only patch. C also undoes a recorded ruling-backed posture. (3) Preventing AI errors: no option adds an authorable key, so this axis is neutral. A leaves the simplest rule an author can predict ('declare columns and the request is projected'). (4) Startup focus: A adds nothing. B adds a guaranteed double fetch and C adds a gating state machine on the most-rendered related lists, for a gap with no measured exposure. The ruling said no second decision card, so how to put this to the maintainer is the seat's call."
}
],
"out_of_scope_findings": [
"class: a · ListView (plugin-list) sends NO $select when every declared column is FLS-denied. Its selectFields returns undefined on
cols.length === 0, so the request asks for every field, denied ones included (defence in depth: ObjectStack's FieldMasker strips). Evidence: a throwaway vitest probe on objectui a78cd37, deleted after running. Control: columns [subject, salary] with salary denied gives $select ['id','subject']. Finding: columns [salary, industry] both denied gives params with no $select key. Both assertions passed. ObjectGrid does not share this (ensureId([]) keeps ['id']). The contract it breaks is ListView's own comment: 'FLS: also drop columns the current user cannot read. Sending a denied field in $select would leak the value at the server boundary even though the UI hides it'. Seam: spec:ListViewSchema.columns → renderer:ListView selectFields (plugin-list). Dedupe words:ListView $select all columns denied·ListView projection undefined empty cols·FLS every column denied whole record requested·objectui#6898 empty column list widening ListView","class: c · core listViewPredicates, the projection harvest shared by ListView, ObjectGrid and now RelatedList's authored path, does not harvest an action's params[].defaultFromRow field keys or the {field} tokens in its target. On a projected row, a field outside the columns is absent, so resolveActionParams (hasOwnProperty on the row) silently skips the prefill and the {field} URL interpolation writes ''. Named producer: object action metadata, e.g. objectstack platform-objects sys-team-member.object.ts remove_team_member (params teamId/userId, defaultFromRow true, fields team_id/user_id). Evidence is source reading only: predicate-fields.ts reads visible/disabled/recordIdField; resolveActionParams rowValueKey plus hasOwnProperty; useConsoleActionRuntime {field} interpolation from _rowRecord. NOT MEASURED end to end. Shipped platform related lists were checked, and each defaultFromRow there names id or a column. Dedupe words:
listViewPredicates defaultFromRow harvest·$select projection drops defaultFromRow prefill·row action URL token field not projected·predicate-fields recordIdField defaultFromRow","carrier: 承接者:无 · noted, not filed · RelatedList type 'list' renders a
data-listnode type that no package registers (git grep on origin/main finds no registration). No in-tree producer passes type 'list' (the record:related_list block passes 'table'). Dormant and unmeasured at runtime."]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
🔁 REWORK (patch round 1): PR objectui#10258 at
2af26dc, one item, prose onlydomain:uiseat #4,session_01BP8CMtACxTdLjqR6rhd33C. I read the reportos-dev-reporton this card in full and reviewed it against the PR diff and the branch head. ⛔ It has not been reviewed on CI yet: a new head is coming.Claim file surface amended in this round
packages/plugin-detail/README.mdandcontent/docs/guide/slotted-pages.mdare added to the surface in claim5814588117. Each gains one paragraph documenting the new projection (objectui AGENTS.md commandment #2: docs are part of done). The two edited fixture files were already inside "its tests underpackages/plugin-detail/src".The item
Three published carriers say a denied column is never requested:
slotted-pages.mdparagraph.The code does not do that, and its own
selectKeydocblock says so: "the first request can go out before a denied column is known — the same deferral asObjectGrid". Whileperms.isLoadedis false,keepReadableColumnspasses every column, so a request sent before the permission answer lands still carries a denied authored column. ⇒ The three carriers are being made true (the column is dropped once the permission answer has loaded). The behaviour stays as it is.Accepted as it is
$selecthalf on the authored-columns path. It uses one spelling of each gate (redaction, parent key, FLS), shared by the column layer and the projection.selectKey), and an all-denied authored list projects toidinstead of widening.Part of #10186, notFixes: this card stays open for the half below.Open half: the seat routes it; the dev does not decide it
The ruling's premise, that the effective columns are known before the fetch, is false on the derived-columns paths (
highlightFields, the field walk, and authored columns that redaction empties). Those paths choose their columns from the emptiness of the rows they fetched. The ruling does not say what those requests send. The same question covers the first-fetch window above. Premise falsified ⇒ this half returns to the decision box on this card once the authored half has landed, with the dev's options A/B/C and the analysis.Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
os-dev-report
{
"issue": 10186,
"addendum": "patch round 1 (prose only)",
"pr": "#10258",
"head": "313966c6a801875c200c74c32b98950cd4c16b6d",
"commits": [
"752c7bff8 docs: the changeset, README and guide state the permission-answer deferral (a request sent before the permission answer has loaded can still carry a denied authored column, the same deferral ObjectGrid has); also tightened after a sentence-by-sentence re-read: predicate operands join once the child schema has loaded, an all-denied list asks for id plus its predicate operands, and rows carry only projected fields on a backend that honours $select",
"313966c6a merge of origin/main 142fdfd (clean, no conflicts; none of this PR's 7 files changed by the merge)"
],
"gates": [
"0 node scripts/check-changeset-presence.mjs",
"0 node scripts/check-changeset-no-major.mjs",
"0 node scripts/check-changeset-overwrite.mjs",
"0 node scripts/check-changeset-claims.mjs",
"0 node scripts/check-pending-changeset-literals.mjs",
"0 node scripts/check-doc-links.mjs",
"0 node scripts/check-control-bytes.mjs",
"0 node scripts/check-new-cross-file-line-citations.mjs (0 new)",
"0 node scripts/check-doc-fence-languages.mjs",
"0 turbo run build --filter='@object-ui/plugin-detail^...' (11/11), run because the merge moved .ts files in plugin-detail and its closure",
"0 pnpm --filter @object-ui/plugin-detail type-check",
"0 vitest: RelatedList.selectFls-10186 + serverpagination + multiValueParentScope-7299, 32 passed"
],
"pr_body_edit_requested": "Evidence heading still cites head 2af26dc. Suggested append under Evidence: 'Patch round 1, head 313966c (prose commit 752c7bf + merge of main 142fdfd): the nine changeset/doc gates exit 0; closure build 11/11 and plugin-detail type-check exit 0; pin and fixture files 32 passed.' No PR-body sentence carries the never-requested absolute."
}
Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
🔁 REWORK (patch round 2): PR objectui#10258 at
313966c, one small code itemdomain:uiseat #4,session_01BP8CMtACxTdLjqR6rhd33C. Patch round 1 (prose) is accepted. The review-tier record5816322866is PASS at313966c, and CI there was 40 success, 3 skipped by design, 0 red. This round comes from that record's non-blocking note (i). The seat checked it at source and judges it a regression this PR would ship, so it is fixed here rather than filed.The item. On mobile,
RelatedListrenders its rows as anobject-gallerynode without acoverField.ObjectGallerythen resolves the cover asgallery?.coverField ?? schema.imageField ?? 'image'. The authored-path$selectcarriesimageonly when it is an authored column. On a backend that honours$select, a child object that declaresimagewould lose its mobile card cover.The fix, sent to the same dev: project the gallery cover field once the child schema has loaded, when the schema declares it and FLS allows it. The name comes from a single spelling, and pins cover declared, denied and undeclared.
Recorded, not fixed here (family-consistent, the same as
ListView/ObjectGrid):rowActionspredicate on a non-column field fails closed when the data source has nogetObjectSchema.The next head needs its own review-tier record before ready.
Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
os-dev-report
{
"issue": 10186,
"addendum": "patch round 2 (mobile gallery cover field)",
"pr": "#10258",
"head": "e18e265f78f26cbda733d3db68498d35f26d274f",
"commits": [
"9037c5429 the cover field is one constant (MOBILE_GALLERY_COVER_FIELD, value = ObjectGallery's own default), handed to the mobile object-gallery node explicitly as gallery.coverField and projected from the same constant when the child schema declares it (parentRelationshipFieldDef, either container shape) and FLS allows it; projected on every viewport so a breakpoint crossing never changes selectKey and never refetches; changeset, README and guide gain one clause each",
"e18e265f7 merge of origin/main 88a4ef6 (clean; none of this PR's 7 files moved)"
],
"pins": "PIN 6a declared+readable requested; 6b declared+denied excluded; 6c undeclared not requested; 6d the gallery node's coverField is in $select",
"ablations": "cover projection removed: 6a+6d red, 10 green; readable check dropped: only 6b red; declared check dropped: 6c+4b red (4b: exact equality picks up the cover). A first readable attempt was a no-op (script generation broke before any mutation; trap restored HEAD, blob verified) and was rerun. All restores: blob equal to HEAD, git diff HEAD empty",
"exits": [
"0 vitest pin + serverpagination + multiValueParentScope-7299 (36 passed)",
"0 turbo build plugin-detail^... (11/11)",
"0 plugin-detail type-check",
"0 eslint RelatedList.tsx + selectFls-10186.test.tsx (0 errors)",
"0 changeset-presence, changeset-no-major, changeset-overwrite, changeset-claims, pending-changeset-literals, doc-links, control-bytes, new-cross-file-line-citations (0 new), doc-fence-languages"
],
"pr_body_edit_requested": "Under What this does, add a bullet: 'Mobile gallery cover. The cover field the mobile card gallery reads, one constant handed to the gallery node as gallery.coverField, when the child declares it and FLS allows it; projected on every viewport so crossing the breakpoint never refetches.' Under Evidence, append: 'Patch round 2, head e18e265: PIN 6a-6d added; ablations as in the round-2 addendum; pins 36 passed, closure build 11/11, type-check, lint and the nine changeset/doc gates exit 0.'"
}
Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
✅ ACCEPT: PR objectui#10258 at
e18e265, the authored-columns half, landing now (ready → merge queue)domain:uiseat #4,session_01BP8CMtACxTdLjqR6rhd33C. I read reports5815696199,5816072755and5816457616in full and reviewed them against the diff, the tree and CI over two patch rounds.main· first linePart of #10186(no closing keyword in the title, body or any of the 8 commits) ·Clause-②: noat line start · path surface NOT GOVERNED5815752319)columnssends a$select. It is built from the columns that pass the same redaction, parent-key and FLS gates the column layer draws through (one shared spelling of each), plusid, the FLS-gated$expandroots, and, once the child schema has loaded, the predicate operands and the mobile gallery cover field when declared and readable. An all-denied authored list projects toidinstead of wideningObjectGridhas. Round 2: the mobile card cover is projected from one constant, so the cover no longer disappears on a backend that honours$selecte18e265(PR comment5816755223); its tier was checked against the transcriptOut of scope
ListViewsends no$selectwhen every declared column is denied ⇒ filed finding(plugin-list):ListViewsends no$selectat all when every declared column is FLS-denied, so the request widens to the whole record #10275.$selectharvest missesdefaultFromRowparams and{field}URL tokens ⇒ filed finding(core): the$selectharvestlistViewPredicatesreadsvisible/disabled/recordIdFieldbut not a row action'sdefaultFromRowparams or{field}URL tokens, so on a projected row those prefills come up empty without a word #10277 (source reading only; flagged NOT MEASURED end to end).RelatedListtype: 'list'renders an unregistereddata-listnode. There is no in-tree producer and it is dormant.namewhen a list has no drawable authored column (the dev'sopen_questions[1]). Accepted as is: it happens only when the principal may see none of the authored columns, and the card still renders with the record id.ListView/ObjectGrid, not fixed here):getObjectSchema.State in this act
pr_ready+automerge_enable⇒ merge queue.Part of⇒ this card stays open. The derived-columns half returns to the decision box on this card once the PR has merged (premise falsified; ruling5814461689said no second decision card).Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Release:
session_01BP8CMtACxTdLjqR6rhd33C(domain:ui#4) · cause: the ruled half landed; the ruling's premise is falsified on the derived-columns half · destination: the decision box (needs-user-decision), assignee cleared in this strokeLanded: PR objectui#10258 merged 2026-09-24T15:25Z as
5f44cc6(the authored-columns half; ACCEPT5816852537; review-tier record5816755223PASS).🗳️ 回决策箱 —— 裁决
5814461689的前提在「列由行推出」的相关列表上被证伪domain:ui座位 #4,session_01BP8CMtACxTdLjqR6rhd33C。本卡的「作者写了列」一半已随 PR objectui#10258 落地(Part of #10186)。剩下的一半在本评论里交维护者裁定,本卡同笔转needs-user-decision,并摘掉 assignee。按裁决原话「⛔ No second decision card」,不另立卡。一句话问题:如果一个相关列表的列要等数据拉回来才定(作者没写
columns),它在拉数据那一刻不知道该只要哪些字段。裁决要求「无权读的列永不被请求」,但这类列表在技术上做不到这一点,除非改变它的加载方式。背景
5814461689(维护者 「同意」,2026-09-24T12:50Z)原话:「RelatedList's row fetch sends a$selectbuilt from the effective columns afterfilterFLS, so a column the principal cannot read is never requested.」5815696199,座位在origin/main上复读):有两类路径的前提不成立。columns的列表。它的列来自highlightFields或字段遍历,两者都按拉回来的行里哪些字段有值来挑列。redactFields全部去掉的列表。/me/permissions)是异步到达的。它到达之前发出的第一次请求,仍可能带着一个无权读的列。这和ObjectGrid已有的延迟完全一样。Governing text
5814461689(上引)。ListView的$select投影。packages/plugin-list/src/ListView.tsx在origin/mainfa5fbd9dc上:没有声明列时selectFields返回undefined(if (cols.length === 0) return undefined;);权限未加载时用未过滤的列。前提(每条带复查命令)
FieldMasker会从每一行里删掉无权读的字段。复查:git grep -n "FieldMasker" origin/main -- packages/plugin-detail/src/DetailView.tsx(注释记录了这一点)。ListView与ObjectGrid对没有声明列的视图都不发$select。复查:git grep -n "cols.length === 0" origin/main -- packages/plugin-list/src/ListView.tsx。RecordDetailView合成的「相关」标签页不带列,除非外键声明了relatedListColumns。复查:git grep -l "relatedListColumns" origin/main -- 'packages/*/src/**'。选项 × 真实代价
ObjectGrid一致业务含义直译:
四轴(从业务立场)
FieldMasker)。客户端投影是纵深防御。主流平台(Salesforce 的 related list 由布局声明列、服务端执行 FLS)的建模也是「列表带声明列,服务端强制」。A 保持全族一条规则。B、C 会让RelatedList成为全族唯一一个做「无列投影」的面。若要做,应当是ListView+ObjectGrid+RelatedList一起做的全族决定,而不是在一处打补丁。columns就一定会投影。这条规则可预测,AI 也最不容易写错。os-decision-facets
RelatedList独有的无列投影模式)。columns就投影)。Prior rulings read:
$select·FLS·RelatedList·projection→ 4 hits (objectui#10186 open; objectui#7215, ListView's speculative view-binding fields (kanban / gantt / timeline / calendar / gallery) reach$selectwithout an FLS check #7216, FLS-gate$expandat the remaining five build sites — calendar, gantt and record-detail expand EVERY declared relation with no column list at all #7230 closed completed); ADR none; thread:5814461689推荐:A(回退 C)。只看①选 A;②③④ 是否翻转:否。
置信缺口:座位看不到的是,是否有客户正在把 objectui 接到一个不剥字段的第三方后端。如果有,②翻为「有拉动」,推荐改为「全族一起做无列投影」,而不是只动
RelatedList。裁后执行
5814461689的适用面记为「作者声明的列,权限答复到达之后」。$select)转为钉住这一裁定。pm:queue,同一分支派发,派发令逐字引裁定字母。Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Ruling: batch #223 item 3 · letter A (the
$selectruling applies to declared columns only) · maintainer 「其他同意」 (chat, director seat summon #29,session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T23:33Z · closedcompletedDirector seat, summon #29. Presented with recommendation A (fallback C); the maintainer agreed. Thread re-read to its last comment (5817363559) in this act.
Ruled: A. Ruling 5814461689 (UI closure batch 14) is scoped as follows:
ListViewandObjectGridalready follow (objectui#6898).columns), or whose authored columns were all removed byredactFields. These fetch the whole row and hide at the column layer. ObjectStack's server-sideFieldMaskeralready strips every unreadable field, so nothing leaks.ListView,ObjectGridandRelatedListtogether, and would be reopened only on a measured third-party backend that does not strip fields.The authored-columns half landed as PR #10258 (
5f44cc6). The landed PIN 5/5b (the derived-columns path sends no$select) now pins this ruling. No code change is owed. ⇒ closedcompleted.Generated by Claude Code
fix(plugin-detail): a related list's row fetch sends an FLS-projected…