Repository navigation
[Decision] the "record as this viewer may read it" rule is hand-written in six places across four packages: one shared export, or keep private copies #10594
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guards
on Sep 25, 2026 objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsCensus update for this decision, from the
domain:uiseat 1 (session_01BA3nKVUwKQJf8DBxrSVtNC). ⛔ No label, option or recommendation changes.- The body's census says six copies (five on
mainplus PR objectui#10592). On objectuimaintoday the regex census (function (withoutDeniedFields|readableRow|fieldReadGate), tests excluded) reads 8. PR objectui#10592 landed copy 3, and PR objectui#10602 (objectui#10499, another seat) added areadableRowinpackages/app-shell/src/views/RecordDetailView.tsx, a second copy in that file beside copy 5. Whether it wraps copy 5 or duplicates it was not read. - PR objectui#10624 (objectui#10535) adds none:
UserCellRendererreusesindex.tsx's module-private copy, and PeoplePicker's name gate is a field-LIST filter, outside this card's scope.
The pull the four-axis block cites grows with each FLS card that lands before a ruling.
domain:uiseat #1 · census note · 2026-09-25T12:45Z- The body's census says six copies (five on
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsRuling: batch #225 item 5 · letter A · maintainer 「其他同意」 2026-09-27T02:35Z
Director seat, summon #30,
session_01AsCNgFBs8HCjwhyHQsFbx3. Batch #225 was presented in the seat chat; the maintainer named two other items for a further mainstream-platform analysis and ruled every remaining item as recommended: 「20149 19995 其他主流平台是怎么处理类似问题的,帮我分析给我建议;其他同意。」Ruled: A — one pure export from
@object-ui/core; the copies are deleted and call it.Readings this ruling rests on (re-run on objectui
origin/main5c61e52):git grep -n -E "function (withoutDeniedFields|readableRow|fieldReadGate)" origin/main -- 'packages/*/src/**' ':!**/__tests__/**'→ 8 definitions in 5 files (packages/app-shell/src/views/RecordDetailView.tsxcarries two,withoutDeniedFieldsandreadableRow);packages/react/package.jsonhas no@object-ui/permissionsdependency (0). Governing: objectui#7215 / objectui#7230 「FLS gates the OUTPUT」; no AGENTS.md rule on shared helpers. Direction by the long-term axis: one definition of 「the record as this viewer may read it」, and every display surface calls it (SalesforceSecurity.stripInaccessibleand Django's permission-aware serializers are the same shape). A reaches it with no new dependency edge, B with one; C keeps eight photocopies whose failure is silent and local to one screen.Execution parameters, ruled here so the dispatch needs no second decision card:
- One PR:
withoutDeniedFields(record, policy, objectName, extraKeep?)exported from@object-ui/core, typed on a structural policy (isLoaded,checkField), with pins: a denied field is dropped;id/_idand the extra identity keys are kept; the same object comes back when nothing is withheld; pass-through before a policy loads. The eight sites (per the census note 5832593814) are rewritten to call it; their existing pins are the regression net. ⛔ The field-LIST filters (readablePreviewColumns,keepReadableColumns, PeoplePicker's lists) are out of scope. Clause-②: yes(a new published export) ⇒ a contract review before enqueue; changesetminorfor@object-ui/core,patchfor the callers.- objectui#10535 and objectui#10499 are ⛔ not blocked: they call the export if it has landed, otherwise they write a module-private copy this PR sweeps.
- Confidence gap carried into the dispatch: whether PR objectui#10590 reshapes copy 2 was not read; the dev re-counts the copies on the head it branches from.
State after this ruling:
needs-user-decision→pm:queue(domain:ui·priority:p3·area:access); this card is the execution card.
Generated by Claude Code
- One PR:
- added and removed
on Sep 27, 2026 objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_01DuWo5bdP9SdVebamn99GGk
Account:os-steve(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-10594-field-read-export
Worktree:objectui-issue-10594
Domain:domain:ui
Seat:domain:ui#1
File surface: a new pure exportwithoutDeniedFieldsin@object-ui/core(a new module underpackages/core/src/plus the package entry's export line) with its pins; the copies rewritten to call it —packages/app-shell/src/views/RecordDetailView.tsx(withoutDeniedFields,readableRow),packages/fields/src/index.tsx,packages/fields/src/widgets/LookupField.tsx(fieldReadGate,withoutDeniedFields),packages/fields/src/widgets/RecordPickerDialog.tsx,packages/react/src/hooks/useRecordSearch.ts(readableRow),packages/plugin-detail/src/withoutDeniedFields.ts;package.jsondependency lines only if a caller lacks@object-ui/core; changesets (minorcore,patchcallers). ⛔ The field-LIST filters are out of scope (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus(TIER_DEFAULT, objectstackscripts/pm/dispatch-gates.mjs:12534at0d3ec47137;--tierrefuses objectui paths)
Clause-②: yes
Thread-read: 5852005636
Serial constraints cleared: read 2026-09-27T16:07Z at objectuiorigin/mainc58b8e4a0. 8 definitions in 7 files (git grep -n -E "function (withoutDeniedFields|readableRow|fieldReadGate)"over non-testpackages/*/src). None of the 12 open PRs touches those files; no liveClaim:on the 19 in-flight lane cards names them. objectui#10657 (seat #4) holdsRelatedList.tsx/ObjectDataTable.tsx, which import the plugin-detail copy but are not edited here ⇒ region-disjoint; mergemainif it lands first.Ruling-ref:
5852005636(director, batch #225 item 5, letter A, maintainer 「其他同意」). WhyClause-②: yes: the ruling says so — a new published export from@object-ui/core.domain:uiseat #1 · claim · 2026-09-27T16:07Z
Generated by Claude Code
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 10594,
"status": "needs_decision",
"branch": "claude/issue-10594-field-read-export",
"pr": "#10820",
"session": "session_01DuWo5bdP9SdVebamn99GGk (subagent; the parent's harness-stamped id, per the Claude-Session trailer)",
"premise_still_valid": true,
"summary": "Draft PR objectui#10820 (head bda4516) adds withoutDeniedFields(record, policy, objectName, extraKeep?) to @object-ui/core. The policy is structural ({ isLoaded, checkField }), core gains no dependency edge, and every caller already depends on core. The PR carries the four ruled pins. Seven of the eight definitions now call the export or are deleted: RecordDetailView's copy is deleted and its readableRow identity cache kept to call core; the fields entry's copy is deleted; LookupField's fieldReadGate and withoutDeniedFields are deleted, with recordToOption and the search chip avatar calling core with [idField]; plugin-detail's module is deleted and its two importers call core; useRecordSearch's readableRow is deleted. The eighth, RecordPickerDialog's copy, is HELD UNCHANGED because it is a behaviour fork. It has no object-name guard, so with objectName === '' and a loaded policy it asks checkField('', key, 'read') where every other copy and the export pass the row through. No in-repo producer passes an empty name. Per the dispatch I report the fork instead of picking a side; see open_questions. Census on the base 13220af: 8 definitions in 6 files. The ruling's 5 and the claim's 7 are both wrong; the 8 lines are the same. Status is needs_decision only for that one site; everything else is done and verified.",
"tests": "All at HEAD bda4516. (1) Build: turbo run build --filter=@object-ui/app-shell^... --concurrency=2 -> 'Tasks: 28 successful, 28 total'. The core build's dist-completeness printed '1 package(s) complete (210 emitted files verified)'. Lock verdict 'command-exit 0'. (2) Type-check: pnpm --workspace-concurrency=2 with --filter for core, fields, react, plugin-detail and app-shell, run type-check. Five echoed 'type-check$ tsc --noEmit && tsc -p tsconfig.test.json' lines, each 'Done', exit 0. The new pin is in core's test program (tsc -p tsconfig.test.json --listFilesOnly lists it). (3) Tests, from the repo root: pnpm exec vitest run --maxWorkers=2 packages/core/ packages/fields/ packages/react/ packages/plugin-detail/ packages/app-shell/ -> 'Test Files 1534 passed | 3 skipped (1537)', 'Tests 18847 passed | 24 skipped (18871)', exit 0. Lock verdict 'command-exit 0', held 40m09s on a shared box. (4) Ablation, with no permanent test left: objectstack scripts/ablation-replace.mjs replaced the anchor policy.checkField(objectName, key, 'read') with true in packages/core/src/utils/without-denied-fields.ts. Mutation landed: anchor 1 -> 0, blob a0c1f5936c94 -> 554a86086dc0. It ran the core pin plus 11 caller FLS pin files -> 'Test Files 11 failed | 1 passed (12)', 'Tests 52 failed | 42 passed (94)'. The only green file was RecordPickerDialog.displayFls-10373, the held copy and the control. Restore proven: blob after restore equals HEAD (a0c1f5936c94), git diff HEAD empty, git status clean. The caller pins resolve @object-ui/core to packages/core/src through the root vitest.config.mts alias, so the mutation needed no dist leg; the 11 reds are the proof that it reached them.",
"mcp_calls": "0",
"api_writes": "3 REST writes, all through the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches, each executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectui/pulls, making draft #10820; (2) label-write --assign os-steve -> POST /repos//issues/10820/assignees, read back 1 assignee os-steve; (3) this os-dev-report comment -> POST /repos//issues/10594/comments. Plus 2 git pushes (the empty-branch probe, then the six commits). Zero label writes.",
"gates": [
{
"command": "bash os-verify-lock.sh -c 'pnpm exec turbo run build --filter=@object-ui/app-shell^... --concurrency=2'",
"verdict": "Tasks: 28 successful, 28 total / os-verify-lock: VERDICT command-exit 0",
"exit": 0
},
{
"command": "pnpm --workspace-concurrency=2 --filter @object-ui/core --filter @object-ui/fields --filter @object-ui/react --filter @object-ui/plugin-detail --filter @object-ui/app-shell run type-check",
"verdict": "5 x 'type-check: Done'",
"exit": 0
},
{
"command": "pnpm exec vitest run --maxWorkers=2 packages/core/ packages/fields/ packages/react/ packages/plugin-detail/ packages/app-shell/",
"verdict": "Test Files 1534 passed | 3 skipped (1537); Tests 18847 passed | 24 skipped (18871)",
"exit": 0
},
{
"command": "node scripts/check-control-bytes.mjs",
"verdict": "check-control-bytes: OK (scanned 9000 tracked text file(s); skipped 85 binary)",
"exit": 0
},
{
"command": "node scripts/check-new-cross-file-line-citations.mjs",
"verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0",
"exit": 0
},
{
"command": "node scripts/check-changeset-presence.mjs",
"verdict": "10 source file(s) of 5 released package(s) changed, and this change declares 1 changeset(s)",
"exit": 0
},
{
"command": "node scripts/check-changeset-fixed.mjs",
"verdict": "All workspace packages are in the changeset fixed group.",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs",
"verdict": "No changeset declares amajorbump.",
"exit": 0
},
{
"command": "node scripts/check-changeset-claims.mjs",
"verdict": "report-only: 7 pending changesets name touched files; read, none describes the field-read rule",
"exit": 0
},
{
"command": "pnpm check:unreferenced-sources",
"verdict": "OK Every shipped source file in every covered package is reachable. (covers packages/components only)",
"exit": 0
},
{
"command": "pnpm check:phantom-deps",
"verdict": "Every in-scope import is declared by the package that publishes it.",
"exit": 0
},
{
"command": "pnpm check:esm-specifiers",
"verdict": "Specifier leg: no un-ledgered package emits an extensionless relative specifier. (load leg not run)",
"exit": 0
},
{
"command": "pnpm check:test-path-roots",
"verdict": "check-test-path-roots: OK",
"exit": 0
},
{
"command": "pnpm check:self-import",
"verdict": "No package names itself inside its own src/.",
"exit": 0
},
{
"command": "node scripts/check-readme-exports.mjs --list",
"verdict": "core README row 'withoutDeniedFields from @object-ui/core' judged real; exit 2 = unbuilt other packages; NOT MEASURED tree-wide, the README Export Check workflow builds every package",
"exit": 2
},
{
"command": "pnpm exec eslint --no-inline-config --format json (9 added or modified .ts/.tsx files)",
"verdict": "9 results, 0 ignored, 0 errors; warnings per file equal to the base version linted via --stdin; no type-aware linting in eslint.config.js",
"exit": 0
},
{
"command": "check:published-dist, check:node-esm-load, full pnpm lint",
"verdict": "NOT MEASURED: the first two workflows are path-filtered to their own scripts; full lint is CI-owned (narrowed lint above)",
"exit": null
}
],
"line_budget": "n/a: no skills/** or governed line-ratchet ledger touched (the diff is not governed-surface)",
"deviations": [
"Site 6 (RecordPickerDialog's withoutDeniedFields) is held unchanged. It is a behaviour fork on objectName === '' (no !objectName guard). I read the dispatch's 'stop and report' as scoped to that site: the other seven are behaviour-identical on every input their types admit, so converting them picks no side. Conversion is a follow-up of about three lines on this branch.",
"Claim mechanism assumption 3 was measured false: the named in-flight branch claude/issue-10657-masked-first-paint (e742e22) has zero occurrences of withoutDeniedFields in its diff against its merge base. So plugin-detail's withoutDeniedFields.ts is deleted (its two importers call core) and not kept as a re-export.",
"Census: 8 definitions in 6 files on 13220af. The claim's '7 files' and the ruling's '5 files' are both miscounts of the same 8 lines. Cross-check: 'withheld = true' over non-test sources gives 6 lines in the same 6 files.",
"Two of the eight are not row-filter copies. RecordDetailView's readableRow is an identity cache over the copy, and it is KEPT and now calls core. LookupField's fieldReadGate is a predicate factory; it is deleted, and its two uses (recordToOption and the search chip avatar) call core with [idField]. recordToOption's parameters changed. It is module-private and not exported by the fields entry.",
"Local commits were regrouped once before the first content push (a staged deletion had ridden into the core commit). No pushed history was rewritten and there was no force push.",
"The full 5-package test run held the shared verify lock 40m09s, and the lock printed a holder-side-starvation advisory. The run was not narrowed because the brief asks for whole-package tests of all five."
],
"files_changed": [
"A .changeset/10594-field-read-rule-core-export.md",
"A packages/core/src/utils/without-denied-fields.ts",
"A packages/core/src/utils/tests/without-denied-fields.test.ts",
"M packages/core/src/index.ts",
"M packages/core/README.md",
"M packages/fields/src/index.tsx",
"M packages/fields/src/widgets/LookupField.tsx",
"M packages/react/src/hooks/useRecordSearch.ts",
"M packages/app-shell/src/views/RecordDetailView.tsx",
"M packages/plugin-detail/src/DetailView.tsx",
"M packages/plugin-detail/src/renderers/record-details.tsx",
"D packages/plugin-detail/src/withoutDeniedFields.ts"
],
"open_questions": [
{
"question": "RecordPickerDialog's copy (site 6) has no object-name guard. With objectName === '' and a loaded policy it asks checkField('', key, 'read') for every key; for an authenticated MePermissionsProvider an unknown object answers false, so it strips the row. The export, like the other seven sites, passes the row through for ''. Convert site 6 to the export, keep its private copy, or tighten the export?",
"options": [
"A: convert. Import withoutDeniedFields from @object-ui/core, call withoutDeniedFields(toPredicateRecord(record, fieldsMeta), perms, objectName, [idField]) and delete the local copy, as one follow-up commit on this branch before enqueue. Drawn output changes only when objectName is '', the policy is loaded and denies on '', an objectSchema is given, rows come back for '', AND the display field is the id field (otherwise the same file's readableColumns gate has already dropped the display column). Every in-repo producer passes a non-empty name: LookupField renders the dialog under referenceTo &&, AccessExplainPanel under objectName.trim() &&, AssignedUsersSection passes 'sys_user'.",
"B: keep the copy, update its docblock to say why it differs, and close the card with one copy left.",
"C: make the export judge '' too (drop pass-through for an empty name only). This moves the other seven sites' behaviour for '', including the lookup cell, whose referenceTo can be empty, so it is a real behaviour change there."
],
"recommendation": "A. By the four axes. 实际业务需求: no producer in the repo passes an empty name, so the '' branch serves no measured scenario; on every real input A and B draw the same thing. 项目长远合理性: A reaches the ruled end state, one definition of the rule; B keeps a photocopy whose only difference is exercised by no caller. 防 AI 写错: A leaves one function to call; B leaves a copy the next FLS card can copy from (the census shows copies breed); C spreads a behaviour change to seven sites. 创业阶段不扩散: A deletes about 20 lines and adds nothing; no option adds a gate. No axis flips the answer."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed. RecordPickerDialog's field-LIST gates (the expand and readableColumns filters) also call checkField(objectName, ...) with no object-name guard. They are field-list filters, out of scope by the ruling, and have no reach (every producer passes a non-empty name). Dedupe words: RecordPickerDialog objectName guard, empty objectName checkField, field-list gate."
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsSeven of eight sites converted; the eighth is a permission-boundary fork ⇒ decision box after the partial landing ·
domain:uiseat #1 · 2026-09-27T17:19Zsession_01DuWo5bdP9SdVebamn99GGk(os-steve). Dev report5858024529; draft PR objectui#10820 (headbda451605). The dispatch fenced 「If you find two copies that differ in behaviour, that is a fork: stop and report it」. The dev stopped at exactly one site, and the seat re-read the fork on the PR head itself:- The new export
packages/core/src/utils/without-denied-fields.ts:56returns early on!policy?.isLoaded || !objectName || …⇒ an emptyobjectNamepasses the row through. This is what the other seven copies did, so converting them changes nothing. - The held copy
packages/fields/src/widgets/RecordPickerDialog.tsx:451-467has no!objectNameguard: with a loaded policy it askscheckField(objectName, key, 'read')for every key, and for''an authenticated provider answers false ⇒ the row is stripped to its id.
⇒ The one real difference is the empty-object-name branch, on a field-level-security surface. Changing which way it fails is a permission-boundary change, which is the maintainer's floor, ⛔ not an execution seat's call. The dev measured that no in-repo producer passes an empty name to the dialog:
LookupFieldrenders it underreferenceTo &&,AccessExplainPanelunderobjectName.trim() &&, andAssignedUsersSectionpasses'sys_user'.Landing plan, so the seat does not idle on the answer: the seven converted sites are behaviour-identical (the dev's ablation of the export's
checkFieldturned 11 caller pin files red; the held copy's pin was the one green control). PR objectui#10820 lands them asPart of #10594— the seat rewrites its first line, since the dev writes a body once — after its contract review (Clause-②: yes, a new published export). At that merge, in the same stroke, this card goespm:dispatched→needs-user-decisionwith its assignee cleared and aRelease:line. The residual is one site, about three lines, on a new branch after the ruling.Governing text: ruling
5852005636(「one pure export from@object-ui/core; the copies are deleted and call it」, which names eight sites and says their pins are the regression net); objectui#7215 / objectui#7230 「FLS gates the OUTPUT」; North Star 「优先级」 1 (security and data integrity rank above the road).One-line problem
In the record picker, if a caller ever opened it without naming an object, the rows would show only their ids, while every other surface would show the whole row. Today no caller does that.
Options × real cost
what is done what a customer sees A Convert the eighth site to the export (drop the local copy). Nothing changes for any caller in the repo. For an empty object name (no caller today), the picker would show rows unfiltered instead of id-only. B Keep the local copy and document why it differs. Nothing changes. One hand-written copy of the rule stays, so the ruling's end state (one definition) is not reached. C Make the export fail closed on an empty object name. The other seven surfaces change for an empty name. The lookup cell (whose referenceTocan be empty) would blank its display.业务含义直译
- A:八处统一成一个规则。那个「没给对象名」的角落今天没人走到;将来万一走到,会按「不设防」处理。
- B:什么都不变,但留一份和别处不一样的手抄件,下一张权限卡可能照抄它。
- C:按「宁可不显示」统一。代价是另外七处在没给对象名时也会把内容藏掉,查找字段的显示可能变空。
四轴论证(业务立场)
- ① 项目长远合理性:裁决要的终态是一条规则一个定义。A 一步到位;B 留下一份行为不同的副本;C 把一个今天没人走到的角落的行为,强加给另外七处。参照主流平台:Salesforce
Security.stripInaccessible从记录本身取得对象类型,不存在「不知道是哪个对象」的调用。所以更彻底的做法,是让「没给对象名」成为调用方的错误——那是另一张收紧卡,不是本卡。 - ② 实际业务拉动:仓内没有任何调用方传空对象名(三个生产者逐一实测),这个分叉今天服务零个真实场景。
- ③ 防 AI 犯错:A 只留一个函数可调;B 的副本会被下一张 FLS 卡照抄(普查显示副本就是这样繁殖的);C 把行为变化扩散到七处。
- ④ 创业阶段不扩散:A 删约 20 行、不增任何东西;三个选项都不新增门禁。
- 安全地板:A 在一个无人可达的分支上由「全拒」变「放行」。它是权限边界上的放宽,即使不可达也需要维护者点头,所以本席不自裁。
os-decision-facets ① 项目长远合理性:A 收成一条规则一个定义;B 留一份行为不同的副本;C 把不可达角落的行为扩散到七处。 ② 实际业务拉动:仓内零个调用方传空对象名(三个生产者实测)。 ③ 防 AI 犯错:A 只剩一个可调函数;B 的副本会被照抄;C 行为变化面最大。 ④ 创业阶段不扩散:A 净删约 20 行,三个选项都不加门禁。 Prior rulings read: withoutDeniedFields,empty objectName,field read → 1 hit; ruling 5852005636 on this card (eight sites, one export); thread: dev report 5858024529推荐:A(第八处改调导出,删本地副本)。回退:B。
自检:只看①选 A;②③④ 是否翻转:否。安全地板要求维护者确认放宽方向,推荐不变。
置信缺口:仓外的宿主是否以空对象名打开这个对话框,本席看不见;若有,A 会让它从只显示 id 变为显示整行(仍受其余字段级列门控约束)。After the ruling
- A ⇒ a small follow-up on a new branch:
RecordPickerDialog.tsximports the export and deletes its copy; thedisplayFls-10373pin is re-read, and a pin for the empty-name case is added. Card closes on that PR. - B ⇒ a docblock on the copy stating why it differs; the card closes with the partial landing.
- C ⇒ the export and its pins change, and all eight sites are re-pinned for the empty name. Re-dispatch with
Clause-②: yes.
domain:uiseat #1 · box (state flips at the partial landing) · 2026-09-27T17:19Z
Generated by Claude Code
- The new export
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsACCEPT — PR objectui#10820 (seven of eight sites;
Part of) ·domain:uiseat #1 · 2026-09-27T17:31Zsession_01DuWo5bdP9SdVebamn99GGk(os-steve), the dispatching seat and reviewer of record. Checked against GitHub andorigin/main, ⛔ not against the report.Checklist
- Shape: draft →
main; body opensPart of #10594— the seat rewrote it fromFixes, because the eighth site is held (box5858046810), and read it back identical. No closing keyword anywhere in the body.Clause-②: yesat line start; PR assigneeos-steve. - Scope: 12 files, +293 / −208 — the new
packages/core/src/utils/without-denied-fields.ts, its pin, the core entry export and README row; seven definitions converted or deleted inRecordDetailView.tsx,fields/src/index.tsx,LookupField.tsx,useRecordSearch.ts, and plugin-detail's module (deleted; its two importers call core).RecordPickerDialog.tsx: 0 lines changed. Inside the claimed surface. - Census corrected by the dev: 8 definitions in 6 files (the ruling said 5 files, the claim said 7 — same 8 lines).
- Governed-surface predicate on the 12 paths:
NOT governed. Checks on headbda451605: 43 check-runs, 40 success, 3 skipped, 0 failure.mergeable_state: clean. - Contract review (new published export): record
5858113393—Served-tier: CONTRACT_REVIEW_TIER, headbda451605dbb7509c5661d57e1f15c77f42df88a(the current head), independence pair present, PASS. It judged all seven sites behaviour-identical on every type-admissible input, found exactly one new exported name, no new dependency edge, the four ruled pins present, and changeset levels matching the ruling (minorcore,patchcallers). - The dev's ablation: replacing the export's
checkFieldwithtrueturned 11 caller FLS pin files red. The held site's pin was the one green control. Restore was proven by blob equality.
Flags carried
- The held
RecordPickerDialogcopy's docblock is now false (it says LookupField keeps a copy and the helper is not public). The file is frozen pending the box. The follow-up after the ruling edits that docblock under any letter. The PR body's sentence about it was corrected by the seat. - The dev's out-of-scope note (the picker's field-LIST gates call
checkFieldwith no object-name guard): Acceptance notes — out of the ruling's scope, no reach (every producer passes a name).
Landing: not governed, green, review PASS ⇒
pr_readythenautomerge_enablethrough the relay, in this act. On merge, in the same stroke, this card movespm:dispatched→needs-user-decision, with its assignee cleared and aRelease:line (residual = the held site, box5858046810).domain:uiseat #1 · ACCEPT · 2026-09-27T17:31Z
Generated by Claude Code
- Shape: draft →
3 remaining items
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsRuling: batch #229 item 4 · letter A · maintainer 「同意」 2026-09-28T00:53Z
Director seat, summon #30 续 2,
session_01AsCNgFBs8HCjwhyHQsFbx3. This card's ruling A (5852005636, batch #225 item 5) chose one published export; seven of eight sites landed on it (PR objectui#10820,9a5f99880) and the eighth was held on a fork the execution seat may not settle (box 5858046810). Batch #229 was presented in the seat chat with the full analysis (that box re-read, four axes not flipped); the maintainer answered 「同意」 (verbatim, recorded here per the charter).Ruled: A — the eighth site,
packages/fields/src/widgets/RecordPickerDialog.tsx, callswithoutDeniedFieldsfrom@object-ui/coreand deletes its local copy. On an empty object name the picker then behaves as the other seven surfaces do: the row passes through, still subject to the picker's field-list gates.Readings on objectui
origin/mainfab627ff: the export returns early on!policy?.isLoaded || !objectName || …(without-denied-fields.ts:56); the held copy has no!objectNameguard (RecordPickerDialog.tsx:457) and, with a loaded policy, askscheckField(objectName, key, 'read')per key, which answers false for''and strips the row to its id (:462). That is the only behavioural difference among the eight, and it sits on a field-level-security surface, which is why it came here. No in-repo producer opens the dialog without a name (LookupFieldunderreferenceTo &&,AccessExplainPanelunderobjectName.trim() &&,AssignedUsersSectionwith'sys_user'), so the branch serves zero real scenarios today.Mainstream reading the ruling rests on: Salesforce's
Security.stripInaccessibletakes the object type from the record itself, so 「which object is this」 is never an open question at the call; the cleaner end state is that an empty object name is the caller's error — a separate tightening card, not this one. Within this card, one rule with one definition (A) beats a documented divergent copy (B) or spreading an unreachable corner's behaviour to seven surfaces (C).Execution parameters: one small PR on a new branch — the import and the deleted copy (about twenty lines net removed), the
displayFls-10373pin re-read, one new pin for the empty-name case (pass-through, with a control that a denied field on a named object still strips), and the held copy's docblock (which now falsely says the helper is not public) goes with it;Clause-②: no,patchfor@object-ui/fields. The card closes byFixeswith that PR.Confidence gap carried: an out-of-repo host opening the dialog with an empty object name is invisible from the fleet; under A it would see whole rows instead of ids, still gated per field elsewhere.
State:
needs-user-decision→pm:queuein this act (domain:ui· p3 kept); body first line set to this ruling.
Generated by Claude Code
- added and removed
on Sep 28, 2026 objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsClaim: PM loop round 2
Session:session_01DuWo5bdP9SdVebamn99GGk
Account:os-steve(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-10594-record-picker-shared-rule
Worktree:objectui-issue-10594b
Domain:domain:ui
Seat:domain:ui#1
File surface, per ruling5861445694(batch #229 item 4, letter A), all in its execution parameters:packages/fields/src/widgets/RecordPickerDialog.tsxcallswithoutDeniedFieldsfrom@object-ui/coreand deletes its local copy, together with the copy's docblock, which falsely says the helper is not public.- The
LookupField.displayFls-10373pin is re-read. - One new pin for the empty-object-name case: the row passes through. Its control: a denied field on a named object still strips.
- A
patchchangeset for@object-ui/fields.
Stop on breach; explain in the report.
Container & model:S,mode:subagent,model: opus(TIER_DEFAULT, objectstackscripts/pm/dispatch-gates.mjs:12692atc5dcb3ba)
Clause-②: no
Thread-read: 5861445694
Serial constraints cleared: read 2026-09-28T01:01Z at objectuiorigin/mainfab627ff9. None of the 11 open PRs touchespackages/fieldsorpackages/core/src/utils/without-denied-fields.ts.Why
Clause-②: no: the ruling sets it. One rule with one definition replaces a divergent private copy. The only behavioural change is on an input no in-repo producer sends: an empty object name, where the row now passes through as on the other seven surfaces.domain:uiseat #1 · claim · 2026-09-28T01:01Z
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 10594,
"status": "done",
"branch": "claude/issue-10594-record-picker-shared-rule",
"pr": "#10876",
"session": "session_01DuWo5bdP9SdVebamn99GGk (subagent; the parent's harness-stamped id, per the Claude-Session trailer)",
"premise_still_valid": true,
"summary": "Draft PR objectui#10876 (head 50cca18, Fixes #10594, Clause-②: no) executes ruling 5861445694 letter A. RecordPickerDialog imports withoutDeniedFields from @object-ui/core on its existing core import line, calls withoutDeniedFields(toPredicateRecord(record, fieldsMeta), perms, objectName, [idField]) at its one call site (renderCellContent, display column), and deletes its local copy together with the docblock that said the helper was not public. A patch changeset for @object-ui/fields is added, and one new pin file: RecordPickerDialog.fieldReadRule-10594.test.tsx. Census on the branch: 'withheld = true' over non-test packages src returns only core's line; the name census returns 3 definitions on base fab627f and 2 on the branch (the export and RecordDetailView's readableRow identity cache that calls it). Zone 2 measured: (1) holds, with the copy and its signature where stated, one call site, and the export's guard adding only !objectName. (2) holds: the export always keeps id and _id and every extraKeep key, and [idField] reproduces the copy's keep set exactly; a custom idField stays kept, and the new pin's control leg (idField 'code', denied) proves it. (3) holds: fields lists @object-ui/core workspace:* and already imported from it. (5) holds: every RecordPickerDialog* test and LookupField.displayFls-10373 are green, unedited. (4) is FALSIFIED in part. Through the real dialog the empty-name leg cannot be red on base, because useRecordQuery gates every read on !!objectName (canQuery) and resets its records when it cannot query. So with objectName '' neither the copy nor the export ever sees a row, and the new pin's empty-name leg (no read, no row, no cell, against MePermissionsProvider with a backend that answers for any name) is green on base and on head. The only render that holds rows under an empty name is the single commit after a host empties the name of an OPEN picker, before the kernel's passive reset effect runs. A one-shot probe (a layout-effect snapshot, not committed) measured that commit. Under MePermissionsProvider (authenticated) base and head draw the same thing: the column gate denies every field on '', so only the id column is drawn. Only under the role-based PermissionProvider with a policy entry keyed on '' do they differ: base draws 'Account 0 - C-t-0', head draws 'Account 0 - C-t-0 - S-t-0'. After the reset both draw no row. I kept the probe out of the tree because it pins the kernel's reset timing and needs a policy keyed on the empty name. This also answers the ruling's carried confidence gap: an out-of-repo host that opens the dialog with an empty name sees an empty table under both the copy and the export. Producer correction: the seat and dev named three producers; there are five mounts in four files. The fourth file is plugin-detail RelatedList, which mounts the dialog under pickerObject &&. Only LookupField passes the objectSchema the title path needs.",
"tests": "Commands and verdicts. Source blob 6ce5768b4a63 is identical at ee254e1 and at head 50cca18; the second commit adds only the changeset. (1) Build: os-verify-lock, then turbo run build --filter=@object-ui/fields^... --concurrency=2: 'Tasks: 10 successful, 10 total', lock VERDICT command-exit 0. (2) Type-check: pnpm --filter @object-ui/fields run type-check echoed 'tsc --noEmit && tsc -p tsconfig.test.json', VERDICT command-exit 0. tsc -p tsconfig.test.json --listFilesOnly lists the new pin (count 1). (3) Tests from the repo root, at 50cca18: pnpm exec vitest run --maxWorkers=2 packages/fields/ gave 'Test Files 218 passed | 1 skipped (219)' and 'Tests 3493 passed | 7 skipped (3500)', VERDICT command-exit 0, lock held 6m54s on a shared box. (4) At 50cca18: pnpm exec vitest run --maxWorkers=2 scripts/tests/ gave 'Test Files 177 passed | 2 skipped (179)' and 'Tests 5317 passed | 2 skipped (5319)', VERDICT command-exit 0, after a 7m55s queue wait behind another agent's components run. (5) Targeted pins on head: the new pin, RecordPickerDialog.displayFls-10373 and LookupField.displayFls-10373 gave 'Test Files 3 passed (3)', 'Tests 19 passed (19)'. (6) Ablation, one-shot, with no permanent file. objectstack scripts/ablation-replace.mjs replaced the anchor 'perms, objectName, [idField])' with 'perms, objectName)' in RecordPickerDialog.tsx: anchor 1 to 0, blob 6ce5768b4a63 to f4f327bca2ff. The new pin gave 'Tests 1 failed | 1 passed (2)'; the control leg went red with "expected [ 'Account 0', 'Account 1', …(1) ] to deeply equal [ 'Account 0 - C-named-0', …(2) ]". The restore was proven: blob after restore equals HEAD (6ce5768b4a63), git diff HEAD empty, git status clean. The test imports ./RecordPickerDialog from src, so no dist leg applies. (7) Base leg, one-shot. A trap-guarded script checked out base fab627f's RecordPickerDialog.tsx over the committed tree; on disk the copy definition counted 1 and the core import 0. It ran the probe, the new pin and RecordPickerDialog.displayFls-10373: 'Test Files 3 passed (3)', 'Tests 13 passed (13)'. The new pin is green on base, both legs, as analysed. It restored with git checkout HEAD -- on the absolute path: blob equal to HEAD, git diff HEAD empty. The probe was then deleted and git status is clean. (8) Probe readings, transition commit: head with the role-based policy on '' drew 'Account 0 - C-t-0 - S-t-0'; base drew 'Account 0 - C-t-0'. Under MePermissionsProvider, head and base both drew only 'code=C-t-0' and 'code=C-t-1'. After the reset both drew 0 cells. (9) Gates, each exit 0: check-control-bytes (9113 files OK); check-new-cross-file-line-citations ('0 new citation(s)'); check-changeset-presence ('2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'); check-changeset-fixed; check-changeset-no-major; check-changeset-claims; check-changeset-overwrite ('No pre-existing changeset was modified or deleted'); check-pending-changeset-literals; check-phantom-dependencies; check-package-self-import; check-unused-dependencies; check-test-path-roots; check-vi-mock-specifiers; check-vi-mock-inherit; check-vi-mock-override-shape; check-i18n-call-site-keys; check-handler-key-read-sites; check-object-metadata-write-doors; check-shell-escape-residue; check-comment-mask-corpus (1 disagreeing file within its declared residue ceiling, not a touched file). check-governed-queue-guard --test on the 3 paths: NOT GOVERNED. A self-scan grep for control bytes over the 3 files exited 1 (no hit). (10) Narrowed lint: eslint --no-inline-config --format json over the 2 touched .ts/.tsx files. Population: both files were linted, not ignored, under eslint.config.js. Count: 2 results, 0 errors. Invariance: no parserOptions.project or projectService in eslint.config.js, and no rule under eslint-rules/ reads the filesystem (grep exit 1; control grep exit 0). The dialog's warnings equal base's via --stdin: 36, same rule counts. The full pnpm lint is CI-owned. NOT MEASURED: check:esm-specifiers and check:node-esm-load (they need the fields dist; the diff adds one name to an existing bare @object-ui/core import and no relative specifier), and check:readme-exports (no README or export changed). CI on head 50cca18 at report time: 42 check-runs, 20 success, 3 skipped, 19 in_progress, 0 failure. That is in_progress, not awaited.",
"mcp_calls": "0",
"api_writes": "3 REST writes, all through the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]). (1) pr_create: POST /repos/objectstack-ai/objectui/pulls, making draft #10876; the body was read back byte-identical (8585 bytes, first differing index none). (2) label-write --assign os-steve: POST /repos//issues/10876/assignees; the read-back matched (assignee os-steve; labels tests and 'package: fields' were set by the path labeler, not by me). (3) This os-dev-report comment: POST /repos//issues/10594/comments. Plus 2 git pushes: the empty-branch probe, then 2 commits. Zero label writes. REST reads with GH_TOKEN: the issue, its comments, PR 10820, the PR list, PR 10876 and its check-runs.",
"open_questions": [],
"deviations": [
"Zone 2 assumption 4 (the pin must fail on base, on the empty-name leg) was measured false for the real dialog in steady state, because an empty objectName reads nothing and draws nothing on base and on head. Rerouted by the ruling's intent. The permanent pin carries an empty-name leg that is green on both, recording the reach fact, plus a control leg that kills the [idField] mutation. The fork's red and green are shown by a one-shot probe on the one transition commit, cited in the PR body. If the PM wants a permanent red-on-base leg, the only available shape is that probe: a role-based policy keyed on '' plus a layout-effect snapshot of the transition commit. It would pin the query kernel's reset timing.",
"Both commits carry the harness reminder's trailer 'Co-Authored-By: Claude Opus 5.5', which names a model. The role file asks for model-free trailer pairs. I noticed this after the push, and history is not rewritten (no force push).",
"The seat's reachability census named three producers; there are five RecordPickerDialog mounts in four files. plugin-detail RelatedList mounts it under pickerObject && with no objectSchema, so it never reaches the title path. The conclusion (no in-repo producer passes an empty name) is unchanged.",
"The ruling says 'the displayFls-10373 pin re-read', which is ambiguous between the two files of that name. Both RecordPickerDialog.displayFls-10373 (9 tests) and LookupField.displayFls-10373 were run green and read; neither needed an edit, and the RecordPickerDialog one's docblock carries nothing about the deleted copy.",
"The objectui gate list was derived by hand from package.json and scripts/: objectstack's dispatch-gates.mjs does not apply to objectui paths. The scripts/tests corpus question was answered by running the whole directory (179 files) rather than selecting files, so no touched file's gate could be missed."
],
"files_changed": [
"M packages/fields/src/widgets/RecordPickerDialog.tsx (+5 / -30)",
"A packages/fields/src/widgets/RecordPickerDialog.fieldReadRule-10594.test.tsx",
"A .changeset/10594-record-picker-shared-field-read-rule.md (@object-ui/fields patch)"
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed. RecordPickerDialog's field-LIST gates (expand and readableColumns) call checkField(objectName, ...) with no object-name guard. They are out of scope by the ruling, which keeps the row subject to them, and have no reach (no producer passes an empty name). Already in PR objectui#10876 Acceptance notes and in the earlier dev report. Dedupe words: RecordPickerDialog objectName guard, empty objectName checkField, field-list gate.",
"carrier: 承接者:无 · noted, not filed (a read-only inference; not in the PR body, which is written once). Measured: useRecordQuery keeps the previous object's rows for the one commit after objectName changes on an open picker, and clears them in a passive effect; the probe shows rows in that commit and 0 cells after. Inferred, not measured: a host that switches an open picker between two NAMED objects would, for that one commit, draw object A's rows under object B's column gate and title rule. No in-repo producer changes the object name of an open picker. Dedupe words: useRecordQuery stale rows objectName change, canQuery reset commit, picker object switch."
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsACCEPT — PR objectui#10876 ·
domain:uiseat #1 · 2026-09-28T02:21Zsession_01DuWo5bdP9SdVebamn99GGk(os-steve), the dispatching seat and reviewer of record. Checked against GitHub andorigin/main, ⛔ not against the reports.Checklist
-
Shape. Draft →
main. The body opensFixes #10594, the only closing keyword, andClause-②: nosits at line start. PR assigneeos-steve. -
Scope. 3 files, +265 / −30. This is the eighth site under ruling
5861445694(A):RecordPickerDialog.tsxcallswithoutDeniedFieldsfrom@object-ui/corewith[idField], which reproduces the deleted copy's keep set exactly:id,_idand the declared id field;- the copy and its docblock (「the helper is not public」) are gone;
- one new pin,
RecordPickerDialog.fieldReadRule-10594.test.tsx; - a
patchchangeset for@object-ui/fields.
The export is now the rule's only definition, per a census of non-test
src. -
Governed-surface predicate. 3 paths:
NOT governed(AGENTS.md lit control: exit 3). -
Checks on head
24824c7cb. 43 check-runs: 40 success, 3 skipped, 0 failure.mergeable_stateclean. -
Contract review.
- Round 1: record
5861954455on50cca18c0FAILED on three defects.- The pin could not tell the PR from its revert: its empty-name leg observed the query kernel's gate, not the ruled pass-through. That also falsified the dev's claim that no red-on-base leg existed under the production provider.
- Two PR-body sentences were false.
- The changeset said the copy judged every field, where it exempted the identity keys.
- Patch round 2 (
24824c7cb) added leg 3. It uses the real dialog and the productionMePermissionsProvider, with the id field equal to the display field, and snapshots the one commit after a host empties the name of an open picker. The round also corrected the changeset. The seat applied the dev's corrected body through the relay, read back identical. - Delta record
5862141921:Served-tier: CONTRACT_REVIEW_TIER, head24824c7cbd6874823f205063405eeb4f02029acb(the current head), independence pair present, PASS. The reviewer re-measured:- leg 3 RED with the base dialog swapped in;
- leg 3 RED with core's empty-name guard ablated;
- the control leg RED with
[idField]dropped; - all three GREEN when the kernel clears rows in the same commit, so the pin does not depend on reset timing.
- Round 1: record
-
Premise corrections recorded. The seat's brief assumed a red-on-base leg was possible, and the dev first measured it impossible. The review showed the shape. The seat's census named three producers; there are five mounts in four files, and none passes an empty name.
Out-of-scope findings, one line each (Acceptance notes)
- The picker's field-LIST gates (
expand,readableColumns) callcheckFieldwith no object-name guard. The ruling keeps the row subject to them, and there is no reach. No card. useRecordQuerykeeps the previous object's rows for one commit after an object-name change on an open picker, then clears them in a passive effect. That is a kernel property with no in-repo producer. No card.- Leg 3's negative form would go vacuous, not red, if the display column ever stopped surviving the empty-name column gate by id identity. The optional hardening is in the review record. Not required.
- The two round-1 commits carry the harness-written trailer. It does not reach
main: the squash message is the PR title plus body. - This ruling's carried confidence gap is answered in the PR body: a host that opens the dialog with an empty object name sees an empty table under both the copy and the export.
Landing: not governed, green, review PASS ⇒
pr_readythenautomerge_enablethrough the relay, in this act.domain:uiseat #1 · ACCEPT · 2026-09-28T02:21Z
Generated by Claude Code
-
Ruled: 5861445694 · letter A · 2026-09-28T00:58Z
Filing-gate category: ② a decision only the maintainer can make (whether a new published export is added). Reader who acts: the
domain:uiexecution seat that claims this card once it is ruled;domain:uiseat 5 is holding its FLS siblings behind this measurement (seat post objectui#10488 §2). Dedupe: objectui issues and PRs updated since 2026-09-18 (1000 walked, open and closed) —withoutDeniedFields→ 5 (the fix PRs that each wrote a copy),readableRow→ 2,fieldReadGate→ 3,field-read rule→ 4 (source cards objectui#10500 / objectui#10501 and seat post objectui#10488),one export.{0,80}(read|FLS)→ 0. No card carries this question.Filed by the
domain:uiseat 1 (session_01BA3nKVUwKQJf8DBxrSVtNC) from the measurement objectui#10501's claim ordered (dev report5831003160, PR objectui#10592).维护者速读
@object-ui/core加一个纯函数导出(不新增包依赖),6 处改成调用它,行为不变,回滚是一个 revert。C 什么都不动,零成本,但副本随卡增长。回一个字母:A / B / C。已裁 A(裁决评论 5852005636),本卡转执行卡。Background
objectui#7215 / objectui#7230 ruled that 「FLS gates the OUTPUT」: the renderer does not draw a field value the loaded permission policy denies, even when a backend serves it. (ObjectStack's
FieldMaskerstrips server-side, so this is defence in depth.) Each card that closed one output site (objectui#10411, objectui#10434, objectui#10500, objectui#10501) wrote its own module-private copy of the same row filter. objectui#10501's triage asked for the count (5828865865): 「This is the natural place to measure whether the field-read rule, now spelled four times, earns one export.」 This card is that measurement put to a decision.Governing text
AGENTS.mdforduplicat|single source|shared helper|one helper|copy of|reimplementand got 0 relevant hits.Protocol
No
@objectstack/specdeclaration is involved. ⛔ No option changes the protocol.Premises, each with its re-check
main50e41f738, and a sixth is in PR objectui#10592.Re-check:
git grep -n -E "function (withoutDeniedFields|readableRow|fieldReadGate)" origin/main -- 'packages/*/src/**' ':!**/__tests__/**'⇒ 6 lines in 5 files.@object-ui/coreis already a runtime dependency offields,plugin-detail,app-shellandreact.@object-ui/reactdoes not depend on@object-ui/permissions.@object-ui/permissionsdepends only on@object-ui/types.Re-check:
grep -c '"@object-ui/permissions"' packages/react/package.json⇒ 0.page:headerH1).The census (dev report
5831003160, re-measured by this seat on50e41f738)packages/fields/src/widgets/LookupField.tsxfieldReadGate+withoutDeniedFields(record, readable)export *-ed by the entry)id,_id, the declaredidFieldusePermissions()packages/fields/src/widgets/RecordPickerDialog.tsxwithoutDeniedFields(record, perms, objectName, idField)id,_id,idFieldusePermissions()packages/fields/src/index.tsx(PR objectui#10592)withoutDeniedFields(record, policy, objectName)id,_idusePermissions()packages/plugin-detail/src/withoutDeniedFields.tswithoutDeniedFields(record, perms, objectName)id,_idpackages/app-shell/src/views/RecordDetailView.tsxwithoutDeniedFields(record, perms, objectName)id,_idusePermissions()packages/react/src/hooks/useRecordSearch.tsreadableRow(record, objectName, policy)id,_idAll six share one body: pass the row through unless a policy is loaded and an object is named; keep
id,_idand any extra identity key; drop each keycheckField(object, key, 'read')denies; return the SAME object when nothing is withheld. The field-LIST filters (PeoplePicker's expand, subtitle and avatar lists; LookupField'sreadablePreviewColumns; thekeepReadableColumnsfamily) are a different shape and ⛔ not in scope.Options
withoutDeniedFields(record, policy, objectName, extraKeep?)from@object-ui/core, typed on a structural policy (isLoaded,checkField). The six copies are deleted and call it. No new package edge.@object-ui/permissions.@object-ui/reactadds a dependency on it (no cycle).What each option means in business terms:
四维分析
WITH SECURITY_ENFORCED/stripInaccessible、Django 的 permission-aware serializer,都把「按权限剥字段」放在一个共享入口,而不是每个视图各写一遍。A 与 B 都到达这个终态;A 不改依赖图。id/_id导致链接失效),而且只错在那一个界面,没有人会从别的界面看出来。os-decision-facets
Prior rulings read:
withoutDeniedFields|readableRow|fieldReadGate|field-read rule→ 14 hits across 1000 objectui items (fix PRs and their source cards; none rules on placement); ADR none; thread: none.Recommendation: A. 只看①选 A;②③④ 是否翻转:否(④ 的新增符号被 6 份副本的删除抵消)。
Fallback: B, if the rule should live with the policy it reads.
Confidence gap: this seat did not read whether open PR objectui#10590 (seat 5,
RecordPickerDialog) reshapes copy 2; the census countsmainplus PR objectui#10592.After the ruling
pm:queueas the execution card. One PR adds the export and its pin (a denied field is dropped,id/_idkept, the same object back when nothing is withheld, a pass-through before a policy loads) and rewrites the six sites to call it. The sites' existing pins are the regression net.Clause-②: yes(a new export) ⇒ a contract review before enqueue. objectui#10535 and objectui#10499 are ⛔ not blocked: if the export has landed when they are claimed they call it, otherwise they write a module-private copy and this card's PR sweeps it.not_planned; this census is the record.Related: objectui#10501 / PR objectui#10592 (copy 3), objectui#10500 / PR objectui#10570 (copy 6), objectui#10434 / PR objectui#10491 (copies 4, 5), objectui#10411 (copies 1, 2), objectui#10535, objectui#10499, seat post objectui#10488.
domain:uiseat #1 ·session_01BA3nKVUwKQJf8DBxrSVtNC· decision card · 2026-09-25