You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
record page: the Edit action still opens on a row the server answers writable:false (the dialog then explains and offers no Save) #27328
Follow-up from #27138 (PR #27313, seat ACCEPT 6475438098).
What happens
On a record the caller may read but not write (public_read sharing, ownership, row-level security), the record page's Edit action is still enabled. Clicking it opens the edit dialog. Since #27313 the dialog draws every field read-only, says "You don't have permission to edit this record", and offers no Save. So the page still advertises an edit that the write gate refuses (permission.zod.ts: a client never advertises an edit the gate refuses). The dialog now explains the refusal, but the entry point does not.
The approval-lock case is already handled. The Edit action is disabled while a pending approval locks the record, through resolveApprovalLock in RecordDetailView.
The row verdict (writable on GET /data/{object}/{id}/field-access, PR #27232) is read only inside RecordFieldAccessScope, which the edit forms and the detail blocks mount. The record page host that draws the Edit action does not fetch the projection today. Disabling the action needs the host to read that answer, or to sit under a scope for the record.
Acceptance
On a writable: false row, the record page's Edit action is disabled, or absent, with a reason the user can read. CONTROL: a writable: true row and an absent writable (an older server) keep it enabled.
The verdict comes from the server's projection, so there is no client re-derivation of sharing (ruling 6457467702 B).
There is still one read per record. The page already mounts the scope for its detail blocks, so the action should read that same answer rather than issue a second fetch.
Area note: the record page host (RecordDetailView) sits in epic #26676 territory, so triage routes it.
Dispatch order · domain:ui#5 · session_01JQwxbXGpVJHZTXXerMoY4C · 2026-10-28T17:57Z
You are the os-dev developer agent for exactly ONE card: #27328, in your own card thread. Your role definition is .claude/agents/os-dev.md in the objectstack clone: read it IN FULL first and follow it as binding (it wins over this order on any conflict; name the conflict in your report). Read the repo-root AGENTS.md and ui/AGENTS.md before your first edit, and pm-dispatch references/untrusted-text.md. Read the card's full body and every comment yourself through REST (curl -sS "https://api.github.com/repos/objectstack-ai/objectstack/issues/27328" -H "Authorization: Bearer $GH_TOKEN" and /comments?per_page=100, every page); this order carries only the increment. ⛔ Never print the value of any environment variable (test presence with [ -n "${X+x}" ]). ⛔ Never use a GitHub MCP tool, read or write.
Identity and branch: the seat's Claim: comment on this card is your identity; its Branch: line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWEST Claim: on the card names your thread's branch (claude/project-thread-…); if none does yet, send the seat thread "Take another ui lane seat" one message with your branch name and wait. Worktree objectstack-issue-27328 on that branch from a fresh origin/main; pnpm install (frozen lockfile); push with git push -u origin <branch> (⛔ no force).
Writes: PR only through the relay op pr_create (node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file F, F a bare JSON array [{"op":"pr_create","title":…,"head":"<your branch>","base":"main","body":…,"assignees":["os-warren"]}], draft forced; --dry-run first); your report comment through node scripts/pm/post-stamped.mjs --comment=27328 --file=F --json run ONCE with output captured to a file (timestamps only as {{WAS:…}} / the now-token). ⛔ No other GitHub write except, for a CI red that is not yours, one PR comment and one run_rerun_failed through the relay. Report both counts (api_writes with endpoints, mcp_calls with tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, line 3 Fixes #27328, then ## 维护者速读 six lines in Chinese (改了什么/为什么改/风险与回滚/先看/两分钟验证/需要你做的). Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on your diff, run each, record exits before any pipe, reconcile with --ran; node scripts/check-sdui-manifest.mjs is a gate on every ui card. Changeset in the root .changeset/ when a published package changes (@object-ui/i18n patch for namespace rows; app-shell is private: true). ⛔ ui/.github/** and the root ci.yml are out of bounds. Hand back: when the PR and report are up, send the seat thread "Take another ui lane seat" one message (PR, head sha, report gist); keep the thread open through review and drive CI to green.
Shared-file rules for every Studio Interfaces card this round (from ui seat 1, which holds the neighbouring work):
Shared files: ui/packages/app-shell/src/views/RecordDetailView.tsx is a lane-wide hot file. The record page v3 epic's draft PR #27310 (#27055, branch claude/issue-27055-record-page-v3-layout) edits it at roughly L10–80, L245, L700–965, L2271–2301, L3326–3528 and L3876–4334 of origin/main: ⛔ keep every hunk of yours outside those ranges, and if #27310 lands first, merge origin/main (no rebase/force) before hand-back. ui seat 1 and seat 2 hold nothing on this file, useRecordApprovals.ts or @object-ui/permissions. Seat 2 handed this card over (it is the follow-up of its #27138 / PR #27313, seat ACCEPT 6475438098: read it first).
Ruling (cannot be re-ruled)
Ruling 6457467702, letter B: the row verdict is the server's (writable on GET /data/{object}/{id}/field-access, PR #27232). ⛔ No client re-derivation of sharing, ownership or row-level security.
The record page's Edit action is gated at ≈L3742, disabled: approvalLocked || editDisabledByPredicate, and approvalLocked comes from resolveApprovalLock ≈L1861 (openApprovalOf from useRecordApprovals.ts is its shared input). That line is outside feat(plugin-detail,app-shell,components): record page v3 layout #27310's ranges. The fix adds one more input there: the row's writable: false, with a reason the user can read (the dialog's existing "You don't have permission to edit this record" string, or its key, reused, not a new phrasing).
@object-ui/permissions' RecordFieldAccessScope turns writable: false into check(object, 'update') refused with reason record-not-writable (≈L169). But the record page HOST sits above the scopes: record:details / record:highlights each mount their own (PR fix(plugin-detail): the record page's inline edit reads the stored row's field locks #27226), so the host cannot read their answer through context. Measure, then choose and state: mount the published scope over the region that computes the Edit action and read check(object,'update') there; or read the projection once in the host through the same data source method. Each costs one more ask per page. ⛔ No second resolver of the verdict, and no new export from a published package unless declared (Clause-②).
Absent writable (an older server) and writable: true keep the action enabled. While the answer is in flight, keep today's behaviour (enabled), so a page never flickers to disabled on a writable row.
Scope
File surface: RecordDetailView.tsx (the Edit action's gate, outside #27310's ranges), possibly a small hook or helper beside it in app-shell, tests. Tests (acceptance): a writable: false row draws Edit disabled with the readable reason; writable: true and absent writable keep it enabled; an approval-locked row behaves as before. Ablation: drop the new input and the writable: false arm turns red.
Tier
No path-derived mandate expected (measure). TIER_DEFAULT, effort medium (S–M). Clause-②: no, Metadata-protocol: none. @object-ui/app-shell is private: true: skip-changeset unless a published package changes.
Claim: PM loop round 12
Session: session_01JQwxbXGpVJHZTXXerMoY4C
Account: os-warren
Branch: claude/project-thread-1yyls6
Worktree: objectstack-issue-27328
Domain: domain:ui
Seat: domain:ui#5
File surface: ui/packages/app-shell/src/views/RecordDetailView.tsx (the Edit action's gate only, outside draft PR #27310's ranges), a small hook or helper beside it in app-shell if needed, tests (stop on breach; explain in the report)
Container & model: S–M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate expected), effort: medium
Clause-②: no
Metadata-protocol: none
Responsibility: ui/packages/app-shell record page host produces it (this lane) | the Edit action stays enabled on a row the server answers writable: false | every record a caller may read but not write (sharing, ownership, row-level security)
Thread-read: 6476615311
Premise: holds (Edit gated ≈L3742 by approvalLocked || editDisabledByPredicate only; the host sits above the per-block RecordFieldAccessScopes; on origin/main1c5fe3cb; readings in dispatch order 6476615311)
Serial constraints cleared: seat 2 handed the card over (follow-up of #27138 / PR #27313); ui seat 1 holds nothing on these files; the epic's draft PR #27310 edits other ranges of RecordDetailView.tsx, and the dev merges main if it lands first.
Dispatch order: comment 6476615311. Card thread claude/project-thread-1yyls6 may start editing.
{
"issue": 27328,
"status": "done",
"branch": "claude/project-thread-1yyls6",
"pr": null,
"session": "session_01JQwxbW8JErLZtqFGN1kiAK",
"premise_still_valid": false,
"summary": "Premise disproved on origin/main 1c5fe3cb (objectstack-ai/objectstack). The record page header does not offer Edit on a row the server answers writable:false. Besides the disabled gate the order names (approvalLocked || editDisabledByPredicate), RecordDetailView already drops the Edit affordance through recordWriteAllowed, the useRecordEditable verdict (plugin-detail): the explain engine's record-grained update verdict, POST /api/v1/security/explain with a recordId, which the server keeps equal to enforcement (explain-enforce-parity.test.ts in plugin-security). Measured on a real booted stack (the record-field-access dogfood fixture, a public_read rfa_notice row another user owns, read as a signed-up member): explain update answered 200 with record visible:false decidedBy rls, field-access answered writable:false, and the PATCH answered 403. CONTROL, a public_read_write rfa_contract draft: explain visible:true, writable:true, PATCH 200. The header reads decision.record.visible, so it gets false and affordances.edit is false: Edit is absent, not enabled. The existing pin RecordDetailView.afterActionExplain-13341.test.tsx ('a save that revokes the edit hides Edit') mounts the real page and shows a visible:false verdict removes the button. The Edit-opens-a-read-only-dialog symptom quoted from PR #27313's acceptance notes was read from the code path without the explain gate, and no other door was measured giving writable:false beside explain visible:true. So no code change was made and no PR was opened. Adding a host read of the projection would add one GET per page and give the same answer twice, and it would not give a reason: page:header gives no disabledReason to host-computed Edit/Delete by design (disabledReasonFor in components containers.tsx only reasons AUTHORED actions)."
,
"tests": "Throwaway probe (deleted, never committed): packages/qa/dogfood/test/zz-probe-27328.dogfood.test.ts booted recordFieldAccessFixtureStack with its security, signed up a member, and read POST /security/explain (update, recordId), GET /data/OBJ/ID/field-access and PATCH for two rows. Run: pnpm exec vitest run --maxWorkers=2 test/zz-probe-27328.dogfood.test.ts, exit 0, Test Files 1 passed, Tests 1 passed. Readings: rfa_notice (public_read, owned by another user): explainStatus 200, record visible false decidedBy rls, field-access 200 writable false, PATCH 403 (server log: Access denied, not permitted to update this rfa_notice record, row-level security). rfa_contract draft (public_read_write): explain 200 visible true decidedBy object_crud, writable true, PATCH 200. Closure built first under the lock: pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build, VERDICT exit 0. Tree after deleting the probe: git status --porcelain empty, HEAD 1c5fe3cb. No ablation: no behaviour changed.",
"gates": "None run: no diff. dispatch-gates --commands derives nothing on an empty change set, and there is no PR, so there are no check-runs to cite.",
"dogfood_battery": "n/a — no PR, no diff",
"line_budget": "n/a — no diff",
"files_changed": [],
"deviations": [
"The order's mechanism assumption 1 (Edit gated only by approvalLocked || editDisabledByPredicate) missed the second gate: affordances.edit already ANDs recordWriteAllowed (useRecordEditable, the explain update verdict). Measured, reported, not re-routed: the ruling's intent (server verdict, no client re-derivation) is already met by that gate.",
"The order's scope expected a draft PR; os-dev rule 6 makes a premise_still_valid false report without a PR the deliverable."
],
"mcp_calls": "0",
"api_writes": "1 — this report comment (POST /repos/objectstack-ai/objectstack/issues/27328/comments through the fleet-write relay via post-stamped). No push, no pr_create.",
"open_questions": [
{
"question": "Should the record page say WHY Edit is missing on a read-only row?",
"options": [
"A: close the card as premise false; the absent Edit matches the existing explain gate and the dialog already explains when reached from elsewhere",
"B: a new card in the components lane to let page:header carry a host-supplied disabledReason for sys_edit, and switch the explain refusal from hidden to disabled with the reason; touches containers.tsx disabledReasonFor and its authored-only rule (objectui#11811)"
],
"recommendation": "A, because the card's acceptance allows absent and the server-verdict ruling is already met; B is a UX change with its own ruling question (11811 kept host booleans reason-less on purpose)."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed: the hidden Edit on a read-only row gives the user no reason. Observation only (no repro of a wrong answer: absent is an accepted outcome in the card's own acceptance). Dedupe words: record header Edit hidden reason, sys_edit disabledReason, useRecordEditable explain hidden"
]
}
席位意见:卡片描述的现象在 main 上不存在。记录页表头已经用 recordWriteAllowed(即 useRecordEditable,explain 引擎对这条记录给出的 update 判定)把不可写行上的"编辑"按钮去掉了。"按钮可点、点开是只读对话框"这一说法是从代码路径推出来的,没有在真实栈上实测过。所以不改代码,建议关闭此卡,等维护者确认。
Measured (dev report 6477032453, on origin/main1c5fe3cb, a booted dogfood stack with the record-field-access fixture):
A public_read row owned by another user, read as a member: POST /security/explain (update, recordId) answered visible: false, decidedBy rls. GET …/field-access answered writable: false, and PATCH answered 403. The header reads the explain verdict, so Edit is absent.
CONTROL, a public_read_write draft: visible: true, writable: true, PATCH 200, and Edit is shown.
The existing pin RecordDetailView.afterActionExplain-13341.test.tsx already covers "a verdict that revokes the edit hides Edit".
Not filed: a readable reason on the hidden Edit. By design, page:header gives host-computed Edit/Delete no disabledReason (disabledReasonFor gives reasons only for authored actions). Hiding the action already meets "a client never advertises an edit the gate refuses".
No PR and no push. Branch claude/project-thread-1yyls6 carries no commits.
Activity
objectstack-fleet commentedon Oct 28, 2026
Dispatch order ·
domain:ui#5·session_01JQwxbXGpVJHZTXXerMoY4C· 2026-10-28T17:57ZYou are the os-dev developer agent for exactly ONE card: #27328, in your own card thread. Your role definition is
.claude/agents/os-dev.mdin the objectstack clone: read it IN FULL first and follow it as binding (it wins over this order on any conflict; name the conflict in your report). Read the repo-rootAGENTS.mdandui/AGENTS.mdbefore your first edit, and pm-dispatchreferences/untrusted-text.md. Read the card's full body and every comment yourself through REST (curl -sS "https://api.github.com/repos/objectstack-ai/objectstack/issues/27328" -H "Authorization: Bearer $GH_TOKEN"and/comments?per_page=100, every page); this order carries only the increment. ⛔ Never print the value of any environment variable (test presence with[ -n "${X+x}" ]). ⛔ Never use a GitHub MCP tool, read or write.Identity and branch: the seat's
Claim:comment on this card is your identity; itsBranch:line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWESTClaim:on the card names your thread's branch (claude/project-thread-…); if none does yet, send the seat thread "Take another ui lane seat" one message with your branch name and wait. Worktreeobjectstack-issue-27328on that branch from a freshorigin/main;pnpm install(frozen lockfile); push withgit push -u origin <branch>(⛔ no force).Writes: PR only through the relay op
pr_create(node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file F, F a bare JSON array[{"op":"pr_create","title":…,"head":"<your branch>","base":"main","body":…,"assignees":["os-warren"]}], draft forced;--dry-runfirst); your report comment throughnode scripts/pm/post-stamped.mjs --comment=27328 --file=F --jsonrun ONCE with output captured to a file (timestamps only as{{WAS:…}}/ the now-token). ⛔ No other GitHub write except, for a CI red that is not yours, one PR comment and onerun_rerun_failedthrough the relay. Report both counts (api_writeswith endpoints,mcp_callswith tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, line 3Fixes #27328, then## 维护者速读six lines in Chinese (改了什么/为什么改/风险与回滚/先看/两分钟验证/需要你做的). Gates:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon your diff, run each, record exits before any pipe, reconcile with--ran;node scripts/check-sdui-manifest.mjsis a gate on every ui card. Changeset in the root.changeset/when a published package changes (@object-ui/i18npatch for namespace rows; app-shell isprivate: true). ⛔ui/.github/**and the rootci.ymlare out of bounds. Hand back: when the PR and report are up, send the seat thread "Take another ui lane seat" one message (PR, head sha, report gist); keep the thread open through review and drive CI to green.Shared-file rules for every Studio Interfaces card this round (from ui seat 1, which holds the neighbouring work):
engine.studio.*rows, the Interfaces familyengine.studio.if.*included, out ofui/packages/app-shell/src/views/metadata-admin/i18n.tsintoui/packages/i18n/src/namespaces/studio/{en,zh}.ts. ⛔ Add or edit no row inmetadata-admin/i18n.ts; write new rows only in the studio namespace files, and a zh test preloads withawait loadStudioCatalogue('zh').ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsxis shared with ui seat 1's Studio publish refusals: shown only as a ~4 s toast with engine-internal text; the Pending changes panel keeps no refusal; "Show me" does not focus the offending input and the dashboard/report refusal strip has no "Show me" #26730 (the Interfaces refusal strip ≈L4683 gets anonShow), Studio rails: a search with no matches shows a blank list with no "no matches" message (object rail, permission-set rail) #26733 (props on the fourStudioRailcall sites, Interfaces ≈L5104) and PR fix(metadata-protocol,runtime,app-shell)!: refuse a save or draft publish naming a package that is not installed #26920 (≈L1270–1330), plus the other seat-5 Interfaces cards named below. Keep your hunks inside your declared region; whoever lands second mergesorigin/mainand resolves.Shared files:
ui/packages/app-shell/src/views/RecordDetailView.tsxis a lane-wide hot file. The record page v3 epic's draft PR #27310 (#27055, branchclaude/issue-27055-record-page-v3-layout) edits it at roughly L10–80, L245, L700–965, L2271–2301, L3326–3528 and L3876–4334 oforigin/main: ⛔ keep every hunk of yours outside those ranges, and if #27310 lands first, mergeorigin/main(no rebase/force) before hand-back. ui seat 1 and seat 2 hold nothing on this file,useRecordApprovals.tsor@object-ui/permissions. Seat 2 handed this card over (it is the follow-up of its #27138 / PR #27313, seat ACCEPT 6475438098: read it first).Ruling (cannot be re-ruled)
Ruling 6457467702, letter B: the row verdict is the server's (
writableonGET /data/{object}/{id}/field-access, PR #27232). ⛔ No client re-derivation of sharing, ownership or row-level security.PM mechanism assumptions (measure; disproof welcome)
On
origin/main1c5fe3cb:disabled: approvalLocked || editDisabledByPredicate, andapprovalLockedcomes fromresolveApprovalLock≈L1861 (openApprovalOffromuseRecordApprovals.tsis its shared input). That line is outside feat(plugin-detail,app-shell,components): record page v3 layout #27310's ranges. The fix adds one more input there: the row'swritable: false, with a reason the user can read (the dialog's existing "You don't have permission to edit this record" string, or its key, reused, not a new phrasing).@object-ui/permissions'RecordFieldAccessScopeturnswritable: falseintocheck(object, 'update')refused with reasonrecord-not-writable(≈L169). But the record page HOST sits above the scopes:record:details/record:highlightseach mount their own (PR fix(plugin-detail): the record page's inline edit reads the stored row's field locks #27226), so the host cannot read their answer through context. Measure, then choose and state: mount the published scope over the region that computes the Edit action and readcheck(object,'update')there; or read the projection once in the host through the same data source method. Each costs one more ask per page. ⛔ No second resolver of the verdict, and no new export from a published package unless declared (Clause-②).writable(an older server) andwritable: truekeep the action enabled. While the answer is in flight, keep today's behaviour (enabled), so a page never flickers to disabled on a writable row.Scope
File surface:
RecordDetailView.tsx(the Edit action's gate, outside #27310's ranges), possibly a small hook or helper beside it inapp-shell, tests. Tests (acceptance): awritable: falserow draws Edit disabled with the readable reason;writable: trueand absentwritablekeep it enabled; an approval-locked row behaves as before. Ablation: drop the new input and thewritable: falsearm turns red.Tier
No path-derived mandate expected (measure).
TIER_DEFAULT, effortmedium(S–M).Clause-②: no,Metadata-protocol: none.@object-ui/app-shellisprivate: true:skip-changesetunless a published package changes.objectstack-fleet commentedon Oct 28, 2026
Claim: PM loop round 12
Session:
session_01JQwxbXGpVJHZTXXerMoY4CAccount:
os-warrenBranch:
claude/project-thread-1yyls6Worktree:
objectstack-issue-27328Domain:
domain:uiSeat:
domain:ui#5File surface:
ui/packages/app-shell/src/views/RecordDetailView.tsx(the Edit action's gate only, outside draft PR #27310's ranges), a small hook or helper beside it inapp-shellif needed, tests (stop on breach; explain in the report)Container & model:
S–M,mode:thread,model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate expected),effort: mediumClause-②: no
Metadata-protocol: none
Responsibility:
ui/packages/app-shellrecord page host produces it (this lane) | the Edit action stays enabled on a row the server answerswritable: false| every record a caller may read but not write (sharing, ownership, row-level security)Thread-read: 6476615311
Premise: holds (Edit gated ≈L3742 by
approvalLocked || editDisabledByPredicateonly; the host sits above the per-blockRecordFieldAccessScopes; onorigin/main1c5fe3cb; readings in dispatch order 6476615311)Serial constraints cleared: seat 2 handed the card over (follow-up of #27138 / PR #27313); ui seat 1 holds nothing on these files; the epic's draft PR #27310 edits other ranges of
RecordDetailView.tsx, and the dev merges main if it lands first.Dispatch order: comment 6476615311. Card thread
claude/project-thread-1yyls6may start editing.objectstack-fleet commentedon Oct 28, 2026
domain:ui#5 · os-warren · session_01JQwxbW8JErLZtqFGN1kiAK
os-dev-report
{ "issue": 27328, "status": "done", "branch": "claude/project-thread-1yyls6", "pr": null, "session": "session_01JQwxbW8JErLZtqFGN1kiAK", "premise_still_valid": false, "summary": "Premise disproved on origin/main 1c5fe3cb (objectstack-ai/objectstack). The record page header does not offer Edit on a row the server answers writable:false. Besides the disabled gate the order names (approvalLocked || editDisabledByPredicate), RecordDetailView already drops the Edit affordance through recordWriteAllowed, the useRecordEditable verdict (plugin-detail): the explain engine's record-grained update verdict, POST /api/v1/security/explain with a recordId, which the server keeps equal to enforcement (explain-enforce-parity.test.ts in plugin-security). Measured on a real booted stack (the record-field-access dogfood fixture, a public_read rfa_notice row another user owns, read as a signed-up member): explain update answered 200 with record visible:false decidedBy rls, field-access answered writable:false, and the PATCH answered 403. CONTROL, a public_read_write rfa_contract draft: explain visible:true, writable:true, PATCH 200. The header reads decision.record.visible, so it gets false and affordances.edit is false: Edit is absent, not enabled. The existing pin RecordDetailView.afterActionExplain-13341.test.tsx ('a save that revokes the edit hides Edit') mounts the real page and shows a visible:false verdict removes the button. The Edit-opens-a-read-only-dialog symptom quoted from PR #27313's acceptance notes was read from the code path without the explain gate, and no other door was measured giving writable:false beside explain visible:true. So no code change was made and no PR was opened. Adding a host read of the projection would add one GET per page and give the same answer twice, and it would not give a reason: page:header gives no disabledReason to host-computed Edit/Delete by design (disabledReasonFor in components containers.tsx only reasons AUTHORED actions)." , "tests": "Throwaway probe (deleted, never committed): packages/qa/dogfood/test/zz-probe-27328.dogfood.test.ts booted recordFieldAccessFixtureStack with its security, signed up a member, and read POST /security/explain (update, recordId), GET /data/OBJ/ID/field-access and PATCH for two rows. Run: pnpm exec vitest run --maxWorkers=2 test/zz-probe-27328.dogfood.test.ts, exit 0, Test Files 1 passed, Tests 1 passed. Readings: rfa_notice (public_read, owned by another user): explainStatus 200, record visible false decidedBy rls, field-access 200 writable false, PATCH 403 (server log: Access denied, not permitted to update this rfa_notice record, row-level security). rfa_contract draft (public_read_write): explain 200 visible true decidedBy object_crud, writable true, PATCH 200. Closure built first under the lock: pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build, VERDICT exit 0. Tree after deleting the probe: git status --porcelain empty, HEAD 1c5fe3cb. No ablation: no behaviour changed.", "gates": "None run: no diff. dispatch-gates --commands derives nothing on an empty change set, and there is no PR, so there are no check-runs to cite.", "dogfood_battery": "n/a — no PR, no diff", "line_budget": "n/a — no diff", "files_changed": [], "deviations": [ "The order's mechanism assumption 1 (Edit gated only by approvalLocked || editDisabledByPredicate) missed the second gate: affordances.edit already ANDs recordWriteAllowed (useRecordEditable, the explain update verdict). Measured, reported, not re-routed: the ruling's intent (server verdict, no client re-derivation) is already met by that gate.", "The order's scope expected a draft PR; os-dev rule 6 makes a premise_still_valid false report without a PR the deliverable." ], "mcp_calls": "0", "api_writes": "1 — this report comment (POST /repos/objectstack-ai/objectstack/issues/27328/comments through the fleet-write relay via post-stamped). No push, no pr_create.", "open_questions": [ { "question": "Should the record page say WHY Edit is missing on a read-only row?", "options": [ "A: close the card as premise false; the absent Edit matches the existing explain gate and the dialog already explains when reached from elsewhere", "B: a new card in the components lane to let page:header carry a host-supplied disabledReason for sys_edit, and switch the explain refusal from hidden to disabled with the reason; touches containers.tsx disabledReasonFor and its authored-only rule (objectui#11811)" ], "recommendation": "A, because the card's acceptance allows absent and the server-verdict ruling is already met; B is a UX change with its own ruling question (11811 kept host booleans reason-less on purpose)." } ], "out_of_scope_findings": [ "carrier: none · noted, not filed: the hidden Edit on a read-only row gives the user no reason. Observation only (no repro of a wrong answer: absent is an accepted outcome in the card's own acceptance). Dedupe words: record header Edit hidden reason, sys_edit disabledReason, useRecordEditable explain hidden" ] }objectstack-fleet commentedon Oct 28, 2026
席位核对
Premise disproved; close recommended · seat
domain:ui#5·session_01JQwxbXGpVJHZTXXerMoY4C· 2026-10-28T18:20Z席位意见:卡片描述的现象在
main上不存在。记录页表头已经用recordWriteAllowed(即useRecordEditable,explain 引擎对这条记录给出的 update 判定)把不可写行上的"编辑"按钮去掉了。"按钮可点、点开是只读对话框"这一说法是从代码路径推出来的,没有在真实栈上实测过。所以不改代码,建议关闭此卡,等维护者确认。origin/main1c5fe3cb, a booted dogfood stack with the record-field-access fixture):public_readrow owned by another user, read as a member:POST /security/explain(update, recordId) answeredvisible: false, decidedByrls.GET …/field-accessansweredwritable: false, andPATCHanswered 403. The header reads the explain verdict, so Edit is absent.public_read_writedraft:visible: true,writable: true,PATCH200, and Edit is shown.RecordDetailView.afterActionExplain-13341.test.tsxalready covers "a verdict that revokes the edit hides Edit".writable: falsesits beside explainvisible: true. If one turns up, reopen this card with that row's readings.page:headergives host-computed Edit/Delete nodisabledReason(disabledReasonForgives reasons only for authored actions). Hiding the action already meets "a client never advertises an edit the gate refuses".claude/project-thread-1yyls6carries no commits.Generated by Claude Code