Skip to content

[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

@objectstack-fleet

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:ui execution seat that claims this card once it is ruled; domain:ui seat 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:ui seat 1 (session_01BA3nKVUwKQJf8DBxrSVtNC) from the measurement objectui#10501's claim ordered (dev report 5831003160, PR objectui#10592).

维护者速读

  • 要裁的: 「把一条记录按当前用户可读的字段过滤一遍」这条规则,现在手抄了 6 份,分在 4 个包里(main 上 5 份,PR objectui#10592 再加 1 份)。队列里 objectui#10535、objectui#10499 还会各抄一份。问题是要不要把它收成一个公开导出。
  • 为什么现在问: 6 份逻辑逐行一致。每开一张 FLS 卡就多一份手抄,将来改规则要逐份去改。
  • 风险与代价: A 在 @object-ui/core 加一个纯函数导出(不新增包依赖),6 处改成调用它,行为不变,回滚是一个 revert。C 什么都不动,零成本,但副本随卡增长。
  • 席位意见: 荐 A。
  • 你要做的: 回一个字母: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 FieldMasker strips 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

  • What the rule does: objectui#7215 / objectui#7230, 「FLS gates the OUTPUT」. All six copies implement it identically; no option changes behaviour.
  • Where the rule lives: none found. Searched objectui AGENTS.md for duplicat|single source|shared helper|one helper|copy of|reimplement and got 0 relevant hits.

Protocol

No @objectstack/spec declaration is involved. ⛔ No option changes the protocol.

Premises, each with its re-check

  1. Five copies are on objectui main 50e41f738, 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.
  2. @object-ui/core is already a runtime dependency of fields, plugin-detail, app-shell and react. @object-ui/react does not depend on @object-ui/permissions. @object-ui/permissions depends only on @object-ui/types.
    Re-check: grep -c '"@object-ui/permissions"' packages/react/package.json ⇒ 0.
  3. At least two queued cards need the same rule: objectui#10535 (PeoplePicker name and the read-only user cell) and objectui#10499 (the page:header H1).

The census (dev report 5831003160, re-measured by this seat on 50e41f738)

# file symbol scope keeps gate source
1 packages/fields/src/widgets/LookupField.tsx fieldReadGate + withoutDeniedFields(record, readable) module-private (the file is export *-ed by the entry) id, _id, the declared idField usePermissions()
2 packages/fields/src/widgets/RecordPickerDialog.tsx withoutDeniedFields(record, perms, objectName, idField) module-private id, _id, idField usePermissions()
3 packages/fields/src/index.tsx (PR objectui#10592) withoutDeniedFields(record, policy, objectName) module-private id, _id usePermissions()
4 packages/plugin-detail/src/withoutDeniedFields.ts withoutDeniedFields(record, perms, objectName) module export, not a package export id, _id caller
5 packages/app-shell/src/views/RecordDetailView.tsx withoutDeniedFields(record, perms, objectName) module-private id, _id usePermissions()
6 packages/react/src/hooks/useRecordSearch.ts readableRow(record, objectName, policy) module-private id, _id a structural policy passed by the caller

All six share one body: pass the row through unless a policy is loaded and an object is named; keep id, _id and any extra identity key; drop each key checkField(object, key, 'read') denies; return the SAME object when nothing is withheld. The field-LIST filters (PeoplePicker's expand, subtitle and avatar lists; LookupField's readablePreviewColumns; the keepReadableColumns family) are a different shape and ⛔ not in scope.

Options

option what it does what a customer notices
A One pure export 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. Nothing today. The next FLS fix calls one function instead of writing a seventh copy.
B The same export from @object-ui/permissions. @object-ui/react adds a dependency on it (no cycle). Nothing today. Same as A, plus one new dependency edge.
C Keep the module-private copies. Each new FLS card writes its own. Nothing today. If one copy is ever written wrong, that screen shows a denied field (a silent leak) or a blank, and no other screen does.

What each option means in business terms:

  • A is one rule book for "what may this viewer see", which every screen consults.
  • B is the same rule book filed with the permissions team, with one more team subscribing to it.
  • C lets each screen keep its own photocopy, and a rule change must find every photocopy.

四维分析

  • 项目长远合理性: 两年后,这条规则应该只有一个定义,每个显示面都调用它。主流做法也是这样:Salesforce 的 WITH SECURITY_ENFORCED / stripInaccessible、Django 的 permission-aware serializer,都把「按权限剥字段」放在一个共享入口,而不是每个视图各写一遍。A 与 B 都到达这个终态;A 不改依赖图。
  • 实际业务拉动: 实测拉动存在。今天有 6 个真实调用点,队列里还有 2 张卡(objectui#10535、objectui#10499)要用它,全都是真实界面:查找单元格、查找下拉、记录选择器、详情页标题、全局搜索。
  • 防 AI 犯错: A/B 让下一张 FLS 卡的 dev 调用一个现成函数,结构上很难写错。C 让每个 dev 再抄一次。抄错时要么静默泄露(policy 未加载就放行被拒字段),要么静默空白(误删 id / _id 导致链接失效),而且只错在那一个界面,没有人会从别的界面看出来。
  • 创业阶段不扩散: A 新增一个已发布符号,是永久义务。但它替换的是 6 份私有副本,并阻止第 7、第 8 份出现,净维护面在缩小,不是能力扩张。

os-decision-facets

  • ① 项目长远合理性:A 把六份特例收成一份(公开面 +1 符号,不增依赖边);C 让特例随卡增生。
  • ② 实际业务拉动:今天 6 个真实调用点,另有 2 张排队卡要用;拉动已实测。
  • ③ 防 AI 犯错:A 让下一张卡调用现成函数;C 让每个 dev 再抄一遍,抄错即静默泄露或静默空白。
  • ④ 创业阶段不扩散:A 增 1 个已发布符号,同时删 6 份私有副本,净面缩小。
    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 counts main plus PR objectui#10592.

After the ruling

  • A or B: this card goes to pm:queue as the execution card. One PR adds the export and its pin (a denied field is dropped, id / _id kept, 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.
  • C: close 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:ui seat #1 · session_01BA3nKVUwKQJf8DBxrSVtNC · decision card · 2026-09-25

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    on Sep 25, 2026
  2. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Census update for this decision, from the domain:ui seat 1 (session_01BA3nKVUwKQJf8DBxrSVtNC). ⛔ No label, option or recommendation changes.

    • The body's census says six copies (five on main plus PR objectui#10592). On objectui main today 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 a readableRow in packages/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: UserCellRenderer reuses index.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:ui seat #1 · census note · 2026-09-25T12:45Z

  3. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: 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/main 5c61e52): git grep -n -E "function (withoutDeniedFields|readableRow|fieldReadGate)" origin/main -- 'packages/*/src/**' ':!**/__tests__/**' → 8 definitions in 5 files (packages/app-shell/src/views/RecordDetailView.tsx carries two, withoutDeniedFields and readableRow); packages/react/package.json has no @object-ui/permissions dependency (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 (Salesforce Security.stripInaccessible and 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 / _id and 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; changeset minor for @object-ui/core, patch for 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

  4. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01DuWo5bdP9SdVebamn99GGk
    Account: os-steve (the seat's linked user as GET /user answers 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 export withoutDeniedFields in @object-ui/core (a new module under packages/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.json dependency lines only if a caller lacks @object-ui/core; changesets (minor core, patch callers). ⛔ The field-LIST filters are out of scope (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (TIER_DEFAULT, objectstack scripts/pm/dispatch-gates.mjs:12534 at 0d3ec47137; --tier refuses objectui paths)
    Clause-②: yes
    Thread-read: 5852005636
    Serial constraints cleared: read 2026-09-27T16:07Z at objectui origin/main c58b8e4a0. 8 definitions in 7 files (git grep -n -E "function (withoutDeniedFields|readableRow|fieldReadGate)" over non-test packages/*/src). None of the 12 open PRs touches those files; no live Claim: on the 19 in-flight lane cards names them. objectui#10657 (seat #4) holds RelatedList.tsx / ObjectDataTable.tsx, which import the plugin-detail copy but are not edited here ⇒ region-disjoint; merge main if it lands first.

    Ruling-ref: 5852005636 (director, batch #225 item 5, letter A, maintainer 「其他同意」). Why Clause-②: yes: the ruling says so — a new published export from @object-ui/core.

    domain:ui seat #1 · claim · 2026-09-27T16:07Z


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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 a major bump.",
    "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

  6. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Seven of eight sites converted; the eighth is a permission-boundary fork ⇒ decision box after the partial landing · domain:ui seat #1 · 2026-09-27T17:19Z

    session_01DuWo5bdP9SdVebamn99GGk (os-steve). Dev report 5858024529; draft PR objectui#10820 (head bda451605). 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:56 returns early on !policy?.isLoaded || !objectName || … ⇒ an empty objectName passes 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-467 has no !objectName guard: with a loaded policy it asks checkField(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: LookupField renders it under referenceTo &&, AccessExplainPanel under objectName.trim() &&, and AssignedUsersSection passes '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 checkField turned 11 caller pin files red; the held copy's pin was the one green control). PR objectui#10820 lands them as Part 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 goes pm:dispatched → needs-user-decision with its assignee cleared and a Release: 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 referenceTo can 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.tsx imports the export and deletes its copy; the displayFls-10373 pin 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:ui seat #1 · box (state flips at the partial landing) · 2026-09-27T17:19Z


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR objectui#10820 (seven of eight sites; Part of) · domain:ui seat #1 · 2026-09-27T17:31Z

    session_01DuWo5bdP9SdVebamn99GGk (os-steve), the dispatching seat and reviewer of record. Checked against GitHub and origin/main, ⛔ not against the report.

    Checklist

    • Shape: draft → main; body opens Part of #10594 — the seat rewrote it from Fixes, because the eighth site is held (box 5858046810), and read it back identical. No closing keyword anywhere in the body. Clause-②: yes at line start; PR assignee os-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 in RecordDetailView.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 head bda451605: 43 check-runs, 40 success, 3 skipped, 0 failure. mergeable_state: clean.
    • Contract review (new published export): record 5858113393 — Served-tier: CONTRACT_REVIEW_TIER, head bda451605dbb7509c5661d57e1f15c77f42df88a (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 (minor core, patch callers).
    • The dev's ablation: replacing the export's checkField with true turned 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 RecordPickerDialog copy'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 checkField with 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_ready then automerge_enable through the relay, in this act. On merge, in the same stroke, this card moves pm:dispatched → needs-user-decision, with its assignee cleared and a Release: line (residual = the held site, box 5858046810).

    domain:ui seat #1 · ACCEPT · 2026-09-27T17:31Z


    Generated by Claude Code

  8. 3 remaining items

  9. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: 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, calls withoutDeniedFields from @object-ui/core and 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/main fab627ff: the export returns early on !policy?.isLoaded || !objectName || … (without-denied-fields.ts:56); the held copy has no !objectName guard (RecordPickerDialog.tsx:457) and, with a loaded policy, asks checkField(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 (LookupField under referenceTo &&, AccessExplainPanel under objectName.trim() &&, AssignedUsersSection with 'sys_user'), so the branch serves zero real scenarios today.

    Mainstream reading the ruling rests on: Salesforce's Security.stripInaccessible takes 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-10373 pin 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, patch for @object-ui/fields. The card closes by Fixes with 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:queue in this act (domain:ui · p3 kept); body first line set to this ruling.


    Generated by Claude Code

  10. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2
    Session: session_01DuWo5bdP9SdVebamn99GGk
    Account: os-steve (the seat's linked user as GET /user answers 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 ruling 5861445694 (batch #229 item 4, letter A), all in its execution parameters:

    • packages/fields/src/widgets/RecordPickerDialog.tsx calls withoutDeniedFields from @object-ui/core and deletes its local copy, together with the copy's docblock, which falsely says the helper is not public.
    • The LookupField.displayFls-10373 pin 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 patch changeset for @object-ui/fields.

    Stop on breach; explain in the report.
    Container & model: S, mode:subagent, model: opus (TIER_DEFAULT, objectstack scripts/pm/dispatch-gates.mjs:12692 at c5dcb3ba)
    Clause-②: no
    Thread-read: 5861445694
    Serial constraints cleared: read 2026-09-28T01:01Z at objectui origin/main fab627ff9. None of the 11 open PRs touches packages/fields or packages/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:ui seat #1 · claim · 2026-09-28T01:01Z


    Generated by Claude Code

  11. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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

  12. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR objectui#10876 · domain:ui seat #1 · 2026-09-28T02:21Z

    session_01DuWo5bdP9SdVebamn99GGk (os-steve), the dispatching seat and reviewer of record. Checked against GitHub and origin/main, ⛔ not against the reports.

    Checklist

    • Shape. Draft → main. The body opens Fixes #10594, the only closing keyword, and Clause-②: no sits at line start. PR assignee os-steve.

    • Scope. 3 files, +265 / −30. This is the eighth site under ruling 5861445694 (A):

      • RecordPickerDialog.tsx calls withoutDeniedFields from @object-ui/core with [idField], which reproduces the deleted copy's keep set exactly: id, _id and 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 patch changeset 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_state clean.

    • Contract review.

      • Round 1: record 5861954455 on 50cca18c0 FAILED 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 production MePermissionsProvider, 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, head 24824c7cbd6874823f205063405eeb4f02029acb (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.
    • 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) call checkField with no object-name guard. The ruling keeps the row subject to them, and there is no reach. No card.
    • useRecordQuery keeps 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_ready then automerge_enable through the relay, in this act.

    domain:ui seat #1 · ACCEPT · 2026-09-28T02:21Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions