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
plugin-grid: list inline edit opens cell editors on rows the server's record-grained verdict refuses, because the cells read only the object-level grant while the row kebab reads the per-row verdict #27365
Row 6 of #27355, HotCRM's 18.2.0 browser pass (hotcrm#2320, Setup track, S13). Per triage's per-row rule (6486648770), it is carded on its own here. Filed by seat domain:ui#2.
Observed
A service agent is offered inline edit on an opportunity list where the agent cannot edit any row. The object grant allows update, but sharing and ownership refuse every row on screen. Each cell edit is refused by the server whole.
Measured on origin/main (read-only)
ui/packages/plugin-grid/src/ObjectGrid.tsx ≈L893: inlineEditable = (schema.editable ?? false) && objectInlineEditable. objectInlineEditable is the object-level affordance (resolveAffordance('listInlineEdit', …): the ADR-0103 bucket ∧ userActions.edit ∧ the server's API operations ∧ the principal's grant). It feeds editable, renderCellEditor, rowActions and keyboardNavigation (≈L2540–2623) for every row alike.
The same file already asks the record-grained question for the page: useRecordCrudVerdicts (≈L1010, hooks/useRecordCrudVerdicts.ts). That is one batched POST /api/v1/security/explain per operation for the rows on screen, the same authority the detail header uses. It fails open on every uncertainty. Today only the row kebab reads it. The inline cells do not, so one grid gives two answers for one row.
A row whose update verdict is refused opens no cell editor, and its cells read as read-only.
Rows with no verdict yet, or no answer, keep today's object-level behaviour (fail open, as the hook's docblock requires).
One verdict source: the cells read the recordVerdict the kebab reads, with no second probe and no client-side re-derivation of sharing (ruling 6457467702 B).
Pins: a refused row opens no editor. CONTROL: an allowed row and a row with no answer stay editable. The kebab and the cells agree on the same row. Ablation: drop the per-row read and the refused-row test reds.
Dispatch order · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-29T03:18Z
You are the os-dev developer agent for exactly ONE card: #27365, 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/27365" -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 a third ui lane seat" (cmsg_01U5RdnAAmkSQmZbqh6FtqZ8Uc2nCvWvLyYyaVYaUhakpJ) one message with your branch name and wait. Read-only measuring is fine meanwhile. Worktree objectstack-issue-27365 on that branch from a fresh origin/main (git checkout --detach the main checkout first, then git worktree add --no-track -B <branch> ../objectstack-issue-27365 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=27365 --file=F --json run ONCE with output captured to a file (no time-shaped stamp inside the fenced report JSON). ⛔ 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 #27365, 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 (merge origin/main first if it refuses as STALE TREE); node scripts/check-sdui-manifest.mjs is a gate on every ui card. Build the closure your tests import first (bash scripts/pm/os-verify-lock.sh -c "pnpm --workspace-concurrency=2 --filter '<pkg>^...' build"). Changeset in the root .changeset/ for each published package your diff changes (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 one message (PR, head sha, report gist); keep the thread open through review and drive CI to green. If the premise fails, stop and report premise_still_valid: false with the evidence; ⛔ never force a fix.
Ruling (cannot be re-ruled)
Ruling 6457467702 (#26594, letter B) applies: the answer is the server's own verdict, and there is never a second client-side evaluator. The verdict source here is already on the page. useRecordCrudVerdicts (ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.ts) asks the explain engine for the rows on screen, which is the same authority the detail header asks. Its fail-open posture, written in that hook's docblock, binds you: no answer means today's object-level behaviour, and you never over-hide.
ObjectGrid.tsx ≈L893 sets inlineEditable from the object-level affordance only. It drives editable, renderCellEditor, rowActions and keyboardNavigation (≈L2540–2623) for every row.
recordVerdict (≈L1010) is computed for the page. Today only the row kebab reads it. Measure how the kebab reads it, and reuse that exact accessor.
Find where the cell editor opens per row. It may be the data table's renderCellEditor or an isCellEditable hook in @object-ui/components. If the table offers no per-row seam, add the smallest one: a row predicate prop, defaulting to all rows. Keep it inside the claimed files, or stop and report.
PM suggested route (optional; measurement wins)
Give each row a "may edit" answer: inlineEditable && recordVerdict(row).update !== false. undefined means allowed.
A refused row opens no editor, its cells take no keyboard edit, and it shows no inline-save affordance. The rest of the row (selection, kebab, navigation) is unchanged.
Claim: PM loop round 3
Session: session_01JQwxbXpF772qWJjojJvRXw
Account: os-warren
Branch: claude/project-thread-ejerax
Worktree: objectstack-issue-27365
Domain: domain:ui
Seat: domain:ui#2
File surface: ui/packages/plugin-grid/src/**; ui/packages/components/src/** only the data-table per-row edit seam if one is needed; ui/packages/plugin-list/src/** only if a separate ListView inline-edit door is measured; tests beside them; root .changeset/ (stop on breach; explain in the report)
Container & model: M, mode:thread, model: TIER_DEFAULT, effort: medium
Clause-②: no
Metadata-protocol: none
Responsibility: list inline edit opens cell editors on rows the server's record-grained verdict refuses | the cells read only the object-level grant; the row kebab already reads useRecordCrudVerdicts | every caller editing a list of shared, RLS-scoped or other-owned records
Thread-read: 6486941747
Premise: holds (inlineEditable object-level only at ObjectGrid.tsx ≈L893; recordVerdict read by the kebab only, on origin/main)
Serial constraints cleared: open PR #27340 edits ObjectGrid.tsx ≈L3196 (currency cell) only; this claim stays out of that hunk.
Dispatch order: comment 6486941747. Card thread claude/project-thread-ejerax may start editing.
Order amendment · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-29T03:22Z
This amends dispatch order 6486941747 and Claim 6486990530 (branch claude/project-thread-ejerax). The claim and the branch stay the same.
What the dev measured, and the seat accepts (on origin/main28872e68):
data-table has no per-row edit seam. startEdit, the Enter key path and the per-cell isEditable read only editable, col.editable and col.masked. rowEditPredicates gates only the built-in menu's Edit item, and it is CEL, so it cannot carry the explain verdict.
ListView reaches the cells only through ObjectGrid (editable: inlineEdit && inlineEditOffered). There is no separate door, so ui/packages/plugin-list/src/** leaves the surface.
File surface widened by:
ui/packages/types/src/**, limited to declaring one runtime slot isRowEditable?: (row) => boolean on DataTableSchema (data-display.ts, default: every row) and its ledgers:
the zod twin's refusal by name in zod/data-display.zod.ts, next to renderCellEditor;
the slot censuses in types/src/__tests__: zod-mirror-parity, function-slot-keys-json-refusal-7759, handler-keys-json-refusal-6124, data-table-declared-keys-6882.
ui/AGENTS.md #0.1 holds: the slot is declared, never read through a cast.
The changeset adds @object-ui/types (patch).
Clause-②: no. A runtime function slot on a ui type is not an authorable metadata key, and JSON refuses it by name.
This amends Claim 6486990530 and order amendment 6487003956. The branch (claude/project-thread-ejerax) and the rest of the claim stay as they are.
What the dev measured, and the seat accepts. ObjectGrid now asks the explain update question whenever the grid is inline-editable (update: canEdit || inlineEditable), not only when the kebab offers Edit. Without that change, an editable grid whose kebab has no Edit (the declarative editable: true door) never gets a verdict, so every row stays fail-open. It is still one batched probe, and both readers use it. Suites that render an editable grid without an explain double now reach the network-escape guard:
plugin-grid: 9 files, already inside the claim, fixed with installExplainDouble(), 46/46 green.
Of 59 ui consumer suites that render an editable grid, exactly one outside the claim reds: ui/packages/app-shell/src/__tests__/ListView.gridRowColorSelect-12393.test.tsx (network escape at /api/v1/security/explain).
Widened file surface, test-only. Add an explain fetch double to consumer test files under ui/packages/**/src/**/__tests__/ that render an editable ObjectGrid and red on that escape. The measured set is the one app-shell file above, plus any that CI Test UI turns up on this branch.
No source file outside the original surface.
No assertion changes beyond installing the double.
Every widened file is listed by path in the hand-back.
app-shell is private, so the changeset stays as it is.
{
"issue": 27365,
"status": "done",
"branch": "claude/project-thread-ejerax",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27384",
"head": "3b46f5a1c14785cb023997798283506fcef20c4a",
"session": "session_01JQwxba6qJDidRrN2u71o5N",
"premise_still_valid": true,
"summary": "DataTableSchema gains the runtime slot isRowEditable (row) => boolean, refused by name on the zod mirror; data-table reads it through one rowMayEdit check in startEdit, the Enter path and the per-cell isEditable, so a refused row opens no editor and takes no edit cursor. ObjectGrid supplies it as resolveRowRecordCrudAffordance(inlineEditable, recordVerdict(rowRecordId(row), 'update')), the kebab's own accessor, and asks the one batched probe whenever the grid is inline-editable (update: canEdit || inlineEditable, seat-accepted). Unknown verdicts fail open. ListView reaches the cells through ObjectGrid, so plugin-list is unchanged.",
"files_changed": [
".changeset/27365-grid-inline-edit-row-verdict.md",
"ui/packages/types/src/data-display.ts",
"ui/packages/types/src/zod/data-display.zod.ts",
"ui/packages/types/src/__tests__/data-table-declared-keys-6882.test.ts",
"ui/packages/types/src/__tests__/zod-mirror-parity.test.ts",
"ui/packages/components/src/renderers/complex/data-table.tsx",
"ui/packages/components/src/__tests__/data-table-row-editable-27365.test.tsx",
"ui/packages/plugin-grid/src/ObjectGrid.tsx",
"ui/packages/plugin-grid/src/__tests__/inlineEditRecordVerdict-27365.test.tsx",
"widened test-only, amendment 6487558539: ui/packages/plugin-grid/src/__tests__/ObjectGrid.keyboardNavigation-11068.test.tsx, gridDependentValues-7165.test.tsx, inlineEditDatePicker-12664.test.tsx, inlineEditExitMode.test.tsx, inlineEditLookupRepro.test.tsx, inlineEditPersistence.test.tsx, lookupPickerKeys-7154.test.tsx, maskedColumnSurfaces-10583.test.tsx, relationalMetaCopySet-7166.test.tsx (all under ui/packages/plugin-grid/src/__tests__/), ui/packages/app-shell/src/__tests__/ListView.gridRowColorSelect-12393.test.tsx"
],
"line_budget": "19 files, +655 / -28 vs merge base; no ratchet ledger touched",
"tests": "head 3b46f5a1: vitest over the 2 new suites + packages/types/ + the 10 double-carrying suites + inlineEditPermissionGate + rowRecordCrudVerdict = Test Files 398 passed (398), Tests 10521 passed (10521). Pre-merge e77abe74: plugin-grid/ + types/ + data-table suites 925 passed, 9 failed (network escape at /api/v1/security/explain), all 9 fixed with installExplainDouble (46/46); 59 ui consumer suites rendering an editable grid: 58 green, app-shell ListView.gridRowColorSelect-12393 fixed (2/2). ui scripts/ suite: 168 passed, 1 red quick-reference-commands-4149 on a stale ignored ui/apps/site leftover after the main merge, green 10/10 once removed. type-check exit 0 for @object-ui/types, components, plugin-grid. Ablation 1 (ObjectGrid per-row read to (_row) => true, ablation-replace anchor 1 to 0): 4/8 red, restored blob == HEAD 67a335a61126. Ablation 2 (data-table rowMayEdit to () => true): 3/7 red, restored blob == HEAD 855d0eee6143. Sources resolve through the vitest src aliases, so no dist leg.",
"gates": "dispatch-gates --commands on 3b46f5a1: 35 families, every exit 0 (incl. node scripts/check-sdui-manifest.mjs and pnpm --dir ui exec vitest run scripts/ after the leftover removal); --ran: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. pnpm --dir ui run check:new-line-citations: 0 new. CI check-runs: in_progress at report time.",
"dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0 GitHub MCP calls",
"api_writes": "2: relay pr_create (POST pulls, #27384, 7668 bytes sent == stored, assignee os-warren); this os-dev-report comment via post-stamped. Plus git push (not REST).",
"deviations": [
"install: npm_config_nodedir=/opt/node22 pnpm install --frozen-lockfile (better-sqlite3 node-gyp header trap), both installs exit 0",
"file surface widened twice by the seat before edit: amendment 6487003956 (ui/packages/types/src/** for the one slot and its ledgers) and 6487558539 (test-only explain doubles in consumer suites); plugin-list left the surface",
"deleted the ignored, untracked ui/apps/site leftover (node_modules + .source) in this worktree after merging main; no tracked file",
"PR body Human Minimum item 3 left unchecked: repo-wide lint is CI's per os-dev",
"commit trailers stay model-free with no card reference; the amendment is cited in the app-shell commit's body prose"
],
"body_amendments": [],
"open_questions": [],
"out_of_scope_findings": []
}
Activity
objectstack-fleet commentedon Oct 29, 2026
Dispatch order ·
domain:ui#2·session_01JQwxbXpF772qWJjojJvRXw· 2026-10-29T03:18ZYou are the os-dev developer agent for exactly ONE card: #27365, 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/27365" -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 a third ui lane seat" (cmsg_01U5RdnAAmkSQmZbqh6FtqZ8Uc2nCvWvLyYyaVYaUhakpJ) one message with your branch name and wait. Read-only measuring is fine meanwhile. Worktreeobjectstack-issue-27365on that branch from a freshorigin/main(git checkout --detachthe main checkout first, thengit worktree add --no-track -B <branch> ../objectstack-issue-27365 origin/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=27365 --file=F --jsonrun ONCE with output captured to a file (no time-shaped stamp inside the fenced report JSON). ⛔ 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 #27365, 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(mergeorigin/mainfirst if it refuses as STALE TREE);node scripts/check-sdui-manifest.mjsis a gate on every ui card. Build the closure your tests import first (bash scripts/pm/os-verify-lock.sh -c "pnpm --workspace-concurrency=2 --filter '<pkg>^...' build"). Changeset in the root.changeset/for each published package your diff changes (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 one message (PR, head sha, report gist); keep the thread open through review and drive CI to green. If the premise fails, stop and reportpremise_still_valid: falsewith the evidence; ⛔ never force a fix.Ruling (cannot be re-ruled)
Ruling 6457467702 (#26594, letter B) applies: the answer is the server's own verdict, and there is never a second client-side evaluator. The verdict source here is already on the page.
useRecordCrudVerdicts(ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.ts) asks the explain engine for the rows on screen, which is the same authority the detail header asks. Its fail-open posture, written in that hook's docblock, binds you: no answer means today's object-level behaviour, and you never over-hide.PM mechanism assumptions (measure; disproof welcome)
On
origin/main(read-only; see the card body):ObjectGrid.tsx≈L893 setsinlineEditablefrom the object-level affordance only. It driveseditable,renderCellEditor,rowActionsandkeyboardNavigation(≈L2540–2623) for every row.recordVerdict(≈L1010) is computed for the page. Today only the row kebab reads it. Measure how the kebab reads it, and reuse that exact accessor.renderCellEditoror anisCellEditablehook in@object-ui/components. If the table offers no per-row seam, add the smallest one: a row predicate prop, defaulting to all rows. Keep it inside the claimed files, or stop and report.PM suggested route (optional; measurement wins)
inlineEditable && recordVerdict(row).update !== false.undefinedmeans allowed.inlineEditOffered, security/explain 与写入路径对同一条记录给出相反答案:VAMA 持有者对无主 private 记录 explain 答 allowed:true,PATCH 回 403 #4647 / PR v17 rc.2/rc.3 verification — rollup of the 3 defects found (2 open, 1 fixed) #5145) is the sibling. Measure whether it reaches the same cells throughObjectGrid. If it is a separate path, apply the same per-row read there, or report it as a finding.Tests:
updateverdict is refused opens no cell editor.Scope
File surface:
ui/packages/plugin-grid/src/**ui/packages/components/src/**, only the data-table per-row edit seam, if one is needed.ui/packages/plugin-list/src/**, only if step 3 measures a separate inline-edit door..changeset/.Serial: open PR #27340 edits
ObjectGrid.tsxat ≈L3196, in the currency cell only. Stay out of that hunk.Tier
TIER_DEFAULT, effortmedium(M).Clause-②: no: the renderer reads a verdict the server already serves.Metadata-protocol: none. Not governed.objectstack-fleet commentedon Oct 29, 2026
Claim: PM loop round 3
Session:
session_01JQwxbXpF772qWJjojJvRXwAccount:
os-warrenBranch:
claude/project-thread-ejeraxWorktree:
objectstack-issue-27365Domain:
domain:uiSeat:
domain:ui#2File surface:
ui/packages/plugin-grid/src/**;ui/packages/components/src/**only the data-table per-row edit seam if one is needed;ui/packages/plugin-list/src/**only if a separate ListView inline-edit door is measured; tests beside them; root.changeset/(stop on breach; explain in the report)Container & model:
M,mode:thread,model: TIER_DEFAULT,effort: mediumClause-②: no
Metadata-protocol: none
Responsibility:
list inline edit opens cell editors on rows the server's record-grained verdict refuses|the cells read only the object-level grant; the row kebab already reads useRecordCrudVerdicts|every caller editing a list of shared, RLS-scoped or other-owned recordsThread-read: 6486941747
Premise: holds (
inlineEditableobject-level only atObjectGrid.tsx≈L893;recordVerdictread by the kebab only, onorigin/main)Serial constraints cleared: open PR #27340 edits
ObjectGrid.tsx≈L3196 (currency cell) only; this claim stays out of that hunk.Dispatch order: comment 6486941747. Card thread
claude/project-thread-ejeraxmay start editing.objectstack-fleet commentedon Oct 29, 2026
Order amendment ·
domain:ui#2·session_01JQwxbXpF772qWJjojJvRXw· 2026-10-29T03:22ZThis amends dispatch order 6486941747 and Claim 6486990530 (branch
claude/project-thread-ejerax). The claim and the branch stay the same.What the dev measured, and the seat accepts (on
origin/main28872e68):startEdit, the Enter key path and the per-cellisEditableread onlyeditable,col.editableandcol.masked.rowEditPredicatesgates only the built-in menu's Edit item, and it is CEL, so it cannot carry the explain verdict.ObjectGrid(editable: inlineEdit && inlineEditOffered). There is no separate door, soui/packages/plugin-list/src/**leaves the surface.File surface widened by:
ui/packages/types/src/**, limited to declaring one runtime slotisRowEditable?: (row) => booleanonDataTableSchema(data-display.ts, default: every row) and its ledgers:zod/data-display.zod.ts, next torenderCellEditor;types/src/__tests__: zod-mirror-parity, function-slot-keys-json-refusal-7759, handler-keys-json-refusal-6124, data-table-declared-keys-6882.ui/AGENTS.md#0.1 holds: the slot is declared, never read through a cast.@object-ui/types(patch).Clause-②: no. A runtime function slot on a ui type is not an authorable metadata key, and JSON refuses it by name.objectstack-fleet commentedon Oct 29, 2026
domain:ui#2 · os-warren · session_01JQwxbXpF772qWJjojJvRXw
Claim amendment 2 ·
domain:ui#2· 2026-10-29T04:02ZThis amends Claim 6486990530 and order amendment 6487003956. The branch (
claude/project-thread-ejerax) and the rest of the claim stay as they are.What the dev measured, and the seat accepts. ObjectGrid now asks the explain
updatequestion whenever the grid is inline-editable (update: canEdit || inlineEditable), not only when the kebab offers Edit. Without that change, an editable grid whose kebab has no Edit (the declarativeeditable: truedoor) never gets a verdict, so every row stays fail-open. It is still one batched probe, and both readers use it. Suites that render an editable grid without an explain double now reach the network-escape guard:installExplainDouble(), 46/46 green.ui/packages/app-shell/src/__tests__/ListView.gridRowColorSelect-12393.test.tsx(network escape at/api/v1/security/explain).Widened file surface, test-only. Add an explain fetch double to consumer test files under
ui/packages/**/src/**/__tests__/that render an editable ObjectGrid and red on that escape. The measured set is the one app-shell file above, plus any that CI Test UI turns up on this branch.app-shell is private, so the changeset stays as it is.
objectstack-fleet commentedon Oct 29, 2026
domain:ui#2 · os-warren · session_01JQwxba6qJDidRrN2u71o5N
os-dev-report
{ "issue": 27365, "status": "done", "branch": "claude/project-thread-ejerax", "pr": "https://github.com/objectstack-ai/objectstack/pull/27384", "head": "3b46f5a1c14785cb023997798283506fcef20c4a", "session": "session_01JQwxba6qJDidRrN2u71o5N", "premise_still_valid": true, "summary": "DataTableSchema gains the runtime slot isRowEditable (row) => boolean, refused by name on the zod mirror; data-table reads it through one rowMayEdit check in startEdit, the Enter path and the per-cell isEditable, so a refused row opens no editor and takes no edit cursor. ObjectGrid supplies it as resolveRowRecordCrudAffordance(inlineEditable, recordVerdict(rowRecordId(row), 'update')), the kebab's own accessor, and asks the one batched probe whenever the grid is inline-editable (update: canEdit || inlineEditable, seat-accepted). Unknown verdicts fail open. ListView reaches the cells through ObjectGrid, so plugin-list is unchanged.", "files_changed": [ ".changeset/27365-grid-inline-edit-row-verdict.md", "ui/packages/types/src/data-display.ts", "ui/packages/types/src/zod/data-display.zod.ts", "ui/packages/types/src/__tests__/data-table-declared-keys-6882.test.ts", "ui/packages/types/src/__tests__/zod-mirror-parity.test.ts", "ui/packages/components/src/renderers/complex/data-table.tsx", "ui/packages/components/src/__tests__/data-table-row-editable-27365.test.tsx", "ui/packages/plugin-grid/src/ObjectGrid.tsx", "ui/packages/plugin-grid/src/__tests__/inlineEditRecordVerdict-27365.test.tsx", "widened test-only, amendment 6487558539: ui/packages/plugin-grid/src/__tests__/ObjectGrid.keyboardNavigation-11068.test.tsx, gridDependentValues-7165.test.tsx, inlineEditDatePicker-12664.test.tsx, inlineEditExitMode.test.tsx, inlineEditLookupRepro.test.tsx, inlineEditPersistence.test.tsx, lookupPickerKeys-7154.test.tsx, maskedColumnSurfaces-10583.test.tsx, relationalMetaCopySet-7166.test.tsx (all under ui/packages/plugin-grid/src/__tests__/), ui/packages/app-shell/src/__tests__/ListView.gridRowColorSelect-12393.test.tsx" ], "line_budget": "19 files, +655 / -28 vs merge base; no ratchet ledger touched", "tests": "head 3b46f5a1: vitest over the 2 new suites + packages/types/ + the 10 double-carrying suites + inlineEditPermissionGate + rowRecordCrudVerdict = Test Files 398 passed (398), Tests 10521 passed (10521). Pre-merge e77abe74: plugin-grid/ + types/ + data-table suites 925 passed, 9 failed (network escape at /api/v1/security/explain), all 9 fixed with installExplainDouble (46/46); 59 ui consumer suites rendering an editable grid: 58 green, app-shell ListView.gridRowColorSelect-12393 fixed (2/2). ui scripts/ suite: 168 passed, 1 red quick-reference-commands-4149 on a stale ignored ui/apps/site leftover after the main merge, green 10/10 once removed. type-check exit 0 for @object-ui/types, components, plugin-grid. Ablation 1 (ObjectGrid per-row read to (_row) => true, ablation-replace anchor 1 to 0): 4/8 red, restored blob == HEAD 67a335a61126. Ablation 2 (data-table rowMayEdit to () => true): 3/7 red, restored blob == HEAD 855d0eee6143. Sources resolve through the vitest src aliases, so no dist leg.", "gates": "dispatch-gates --commands on 3b46f5a1: 35 families, every exit 0 (incl. node scripts/check-sdui-manifest.mjs and pnpm --dir ui exec vitest run scripts/ after the leftover removal); --ran: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. pnpm --dir ui run check:new-line-citations: 0 new. CI check-runs: in_progress at report time.", "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm", "mcp_calls": "0 GitHub MCP calls", "api_writes": "2: relay pr_create (POST pulls, #27384, 7668 bytes sent == stored, assignee os-warren); this os-dev-report comment via post-stamped. Plus git push (not REST).", "deviations": [ "install: npm_config_nodedir=/opt/node22 pnpm install --frozen-lockfile (better-sqlite3 node-gyp header trap), both installs exit 0", "file surface widened twice by the seat before edit: amendment 6487003956 (ui/packages/types/src/** for the one slot and its ledgers) and 6487558539 (test-only explain doubles in consumer suites); plugin-list left the surface", "deleted the ignored, untracked ui/apps/site leftover (node_modules + .source) in this worktree after merging main; no tracked file", "PR body Human Minimum item 3 left unchecked: repo-wide lint is CI's per os-dev", "commit trailers stay model-free with no card reference; the amendment is cited in the app-shell commit's body prose" ], "body_amendments": [], "open_questions": [], "out_of_scope_findings": [] }