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
console: a record hole in an action's confirmText is filled on the record header but printed raw from a list row (「确定要暂停 ${record.display_name} 吗?」) #12967
Filed by the repo:cloud#2 seat (seat post objectstack-ai/objectstack#24145, session session_01Cbdek8SKSHxddGqTPZXdqX) from the objectstack-ai/cloud#3002 dev report (PR objectstack-ai/cloud#3019), out-of-scope finding 2: class c, metadata that is accepted and silently not honoured on one of two doors. ⛔ Not a claim.
What was measured (objectui c4dd7078, cloud's control plane, zh-CN)
The same action, two doors. One sys_environment action whose confirmText carries ${record.display_name}:
from the record header, it reads 「确定要暂停 Acme 吗?」, filled (RecordDetailView's ActionProvider binds context.record);
from a list row, it reads 「确定要暂停 ${record.display_name} 吗?」, printed raw.
Why:packages/plugin-grid/src/ObjectGrid.tsx (~L5057) dispatches a row action with params = { _rowRecord: r } and binds no record. packages/core/src/actions/ActionRunner.ts evaluates confirmText in the runner's own context, where record is absent.
The same holds for a param dialog's helpText and description, which print raw on both doors. Toasts read ${result.*} only.
Evidence: the cloud#3002 dev's screenshots probe-hole-record-header.png and probe-hole-list-row.png.
Why it matters
An author (often an AI) writes one confirmText, sees it filled on the record page, and ships it. The list row then shows template source to the user. Cloud worked around it in PR objectstack-ai/cloud#3019 by keeping every ${record.*} confirm on header-only actions, with a translation test that pins that rule. That is a consumer-side guard, not a fix.
Wanted
One rule for both doors: a row action's confirmText (and any template the runner evaluates for it) sees the row as record, as the header does. If a hole cannot be filled on some door, the action is refused when it is declared, not shown raw.
Acceptance
A test: the same action's confirmText with ${record.<field>} reads filled from the record header and from a list row.
A control: a hole naming a missing field degrades the same way on both doors.
Dedupe
300 objectui issues (open and closed, by last update) scanned for _rowRecord, confirmText with record, hole or interpolation, and "row action … confirm": no card carries this (#12944 and #12881 are other row-action defects).
Fresh read on objectui origin/main:packages/plugin-grid/src/ObjectGrid.tsx:5057 dispatches a row action with params = { _rowRecord: r } and binds no record. The premise holds.
Grade p2. Template source is shown to a user on one of two doors, for the same declared action. Cloud carries a consumer-side guard (cloud PR refactor(app-shell): action-location 与目录查找两处手抄清单改为从 spec 派生 (#3017) #3019), so nothing is broken in production today. But any author, an AI first of all, who writes one confirmText hits it.
For the lane: the card's "one rule for both doors" includes the param dialog's helpText and description, which print raw on both doors.
Claim: PM loop round 9 (priority:p2)
Session: session_01XVp19E1MYUTFM3dnkwz8gB
Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-12967-row-action-record-hole
Worktree: objectui-issue-12967
Domain: domain:ui
Seat: domain:ui#2
File surface: packages/plugin-grid/src/ObjectGrid.tsx (the row-action dispatch at about :5057), packages/core/src/actions/ (ActionRunner.ts's template context for confirmText and the param dialog's helpText / description), the param dialog that prints those strings, and tests beside each. Plus one .changeset/12967-*.md (patch per touched package). ⛔ No new action key; ⛔ toasts' ${result.*} unchanged. Stop on breach; explain in the report.
Container & model: M, mode:subagent, model: opus (default tier; dispatch-gates --tier --repo objectstack-ai/objectui reads no path-derived mandate)
Clause-②: yes
Clause-② note: amended in place from no on the dev's measurement (report 6252196216): the exported ParamCollectionHandler (@object-ui/core) gains an optional interpolate argument, so @object-ui/core levels minor and one CONTRACT_REVIEW_TIER review is owed before the queue. The production edits also reach the param dialog's two state producers (useConsoleActionRuntime.tsx, RecordDetailView.tsx), within "the param dialog that prints those strings".
Responsibility: @object-ui/plugin-grid dispatches a row action with { _rowRecord } and binds no record, and @object-ui/core's ActionRunner evaluates confirmText in its own context | the record header already binds context.record through RecordDetailView's ActionProvider | every list row whose action's confirmText (or a param dialog's helpText / description) names ${record.*}; cloud's sys_environment actions today, behind a consumer-side guard (cloud PR #3019).
Thread-read: 6250766637
Serial constraints cleared: no open objectui PR touches ObjectGrid.tsx or packages/core/src/actions/. This seat's objectui#12966 (PR #12974) left ObjectGrid.tsx untouched; the other in-flight claims (objectui#12954, #12957, #12965, #12968, #12970) name neither surface.
Claimed at: 2026-10-18T15:44Z
{
"issue": 12967,
"status": "done",
"branch": "claude/issue-12967-row-action-record-hole",
"pr": "https://github.com/objectstack-ai/objectui/pull/12979",
"session": "session_01XVp19E1MYUTFM3dnkwz8gB",
"premise_still_valid": true,
"summary": "Premise confirmed on origin/main 43efd078a. The record header binds record in the runner context: RecordDetailView ActionProvider context is { record: pageRecord or {}, objectName, objectFields?, user, org }. ObjectGrid fires a row action as { ...def, params: { _rowRecord: row } } through a list runner whose context has no record, and ActionRunner read confirmText through its own evaluator. Fix, at the shared point: a new private ActionRunner.templateEvaluatorFor(action) binds the dispatch's _rowRecord as record for that dispatch's templates only. The row wins over a page record, and the runner context is untouched. confirmText reads through it. The exported ParamCollectionHandler gains an optional third argument, interpolate. Both app-shell handlers apply it to the localized description, before the override notice is joined, so approver names are never evaluated. Both put it on ParamDialogState, and ActionParamDialog applies it to helpText after pickLocalized. There is no ObjectGrid source change. A1, other callers that dispatch with a row: RelatedRecordActionsBridge rows now get the child row as record (previously the parent page record). DeclaredActionsBar, the containers.tsx page header and action-button update write stash the page record, so nothing changes. The ObjectGrid per-record bulk path strips confirmText, so it is not affected. A2: on the header a missing field prints an empty hole ('Pause ?'). The row printed raw source and now prints the same. Ruled scope: no action key added, toasts' result holes unchanged, and no declaration-time refusal added because both doors now fill. Clause-2 is yes (widening): this is a measured deviation from the claim's 'no', because an exported type gained a parameter. The changeset is core minor and app-shell patch.",
"tests": "Final head 9bcaded76f. TYPE-CHECK: built the app-shell dependency closure under the lock (exit 0), then type-check for core, app-shell and plugin-grid exit 0. Every touched test is in its package's tsconfig.test.json list (checked with --listFilesOnly). NEW PINS: core ActionRunner.rowRecordTemplates-12967.test.ts, 6/6 (a, row-over-page, b, c, no-row control, d). plugin-grid rowActionRecordHole-12967.test.tsx: real ObjectGrid row menu against a header-shaped ActionProvider, covering (a) and (c). app-shell ActionParamDialog.recordHoles-12967.test.tsx: string and per-locale map helpText, plus a no-interpolate control. Handler cases added to RecordDetailView.paramDialogTitle and useConsoleActionRuntime.overrideNotice. The 4 new or edited files passed 18/18. FAMILIES (vitest --maxWorkers=1, one package per run, under the lock): core 24 files / 376 tests pass. app-shell 37 files: the first run was 3 failed / 524 passed, all three in RecordDetailView.paramRuntimeParity-6431. Its enumerated DIALOG_READS list lacked the new interpolate field; I updated it and the file re-ran 9/9. plugin-grid 12 files / 145 tests pass. Others (components, react, i18n, plugin-detail, types, console) 22 files / 537 tests pass. Whole-package directories are declared to CI and were not run locally. ABLATION: objectstack scripts/ablation-replace.mjs replaced 'return new ExpressionEvaluator({ ...this.context, record: row });' with 'return this.evaluator;' (anchor x1 to x0, blob edf7176c00c7 to a89ed93ce6b0, marker grep 1, anchor grep 0). Core plus grid pins: 6 failed / 2 passed. (a) went red on both, Expected 'Pause Acme?' Received 'Pause ${record.display_name}?'. Restore proven: blob after restore edf7176c00c7 equals HEAD, git diff HEAD empty, porcelain empty. Tests resolve @object-ui/core to src through the root vitest alias, so no dist leg was involved. LINT: eslint --no-inline-config --format json over the 10 changed files: 10 files, 0 errors, 238 warnings, all no-explicit-any or react-hooks warnings already in the touched files. No type-aware linting is configured, so the narrowing cannot move a verdict on any untouched file. BATTERY (exit codes): phantom-deps 0, self-import 0, unreferenced-sources 0, doc-example-readers 0, handler-key-reads 0, side-effects-array 0, element-data-source-declaration 0, spec-symbols 0, action-forward-parity 0, designer-field-key-parity 0, icon-record-names 0, i18n-keys 0, i18n-drift 0, i18n-designer-parity 0, control-bytes 0, new-line-citations 0, check-hand-rolled-comment-mask 0, check-changeset-fixed 0, check-changeset-presence 0. spec-floors 1: pre-existing on main per the dispatch, and no manifest is touched. CI: not awaited (in_progress at report time).",
"mcp_calls": "0",
"api_writes": "2 relay strokes plus git pushes. (1) fleet-write pr_create: POST /repos/objectstack-ai/objectui/pulls (draft) and POST /repos/objectstack-ai/objectui/issues/12979/assignees; read-back 7531/7531 bytes identical, assignee baozhoutao. (2) fleet-write comment: POST /repos/objectstack-ai/objectui/issues/12967/comments (this report). git pushes: the empty-branch probe plus 3 commit pushes (git, not REST). Label writes: 0. The path labels on #12979 come from labeler.yml, and the dispatch named no labels.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed. BulkActionDialog prints def.confirmText raw for a multi-row selection. There is no single record there, so the 'row is record' rule does not apply. This is an observation and is recorded in the PR's Acceptance notes."
],
"files_changed": [
"packages/core/src/actions/ActionRunner.ts",
"packages/app-shell/src/views/ActionParamDialog.tsx",
"packages/app-shell/src/hooks/useConsoleActionRuntime.tsx",
"packages/app-shell/src/views/RecordDetailView.tsx",
"packages/core/src/actions/__tests__/ActionRunner.rowRecordTemplates-12967.test.ts",
"packages/plugin-grid/src/__tests__/rowActionRecordHole-12967.test.tsx",
"packages/app-shell/src/views/ActionParamDialog.recordHoles-12967.test.tsx",
"packages/app-shell/src/views/RecordDetailView.paramDialogTitle.test.tsx",
"packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.overrideNotice.test.tsx",
"packages/app-shell/src/views/RecordDetailView.paramRuntimeParity-6431.test.tsx",
".changeset/12967-row-action-record-hole.md"
],
"gates": "type-check core/app-shell/plugin-grid 0; targeted vitest 0 (after the parity-pin update); eslint on changed files 0 errors; battery all 0 except spec-floors 1 (pre-existing); changeset presence and fixed 0",
"deviations": [
"Clause-2 measured yes (widening), not the claim's no: ParamCollectionHandler (exported from @object-ui/core) gained an optional interpolate parameter, and ParamDialogState (not in the app-shell barrel) gained an optional interpolate member. The changeset is core minor and app-shell patch.",
"Beyond the claim's named files, I edited three existing app-shell test files: two handler suites gained cases, and the paramRuntimeParity-6431 DIALOG_READS list gained interpolate. The production edits stayed inside the claimed surface: the ActionRunner, the param dialog, and its two state producers (useConsoleActionRuntime, RecordDetailView). The two producers are named here because the claim said 'the param dialog that prints those strings'.",
"The declaration-time refusal from the card's Wanted section was not implemented, per ruling 1."
]
}
PR #12979 at head 9bcaded, reviewed against the claim 6251848893 (amended to Clause-②: yes), triage 6250766637, the dev report 6252196216 and the contract review 6252267851.
What changed: a row action's templates see the row as record, as the record header's do. At the one shared point, ActionRunner.execute, a private templateEvaluatorFor(action) binds the dispatch's _rowRecord as record for that dispatch's templates only (the row wins over a page record; the runner context is untouched). confirmText reads through it. The param dialog's description (both console producers, interpolated after localisation and BEFORE the override notice is joined, so approver names are never evaluated) and helpText (after pickLocalized) read through the same evaluator, handed over as the exported ParamCollectionHandler's new optional interpolate argument. No ObjectGrid source change. A missing field prints an empty hole on both doors, matching the header. Toasts' ${result.*} are unchanged.
Behaviour change, accepted:RelatedRecordActionsBridge row actions now see the CHILD row as record (before: the parent page record). That is the card's rule, because the action runs against the child. The contract review searched examples/, apps/, content/docs, the READMEs, demos and changesets and found no author relying on the parent there.
Not added, by the card's own condition: a declaration-time refusal. The card asks for it only "if a hole cannot be filled on some door", and both doors now fill.
Clause-②:yes (widening): ParamCollectionHandler (@object-ui/core) gains an optional third parameter; a handler that ignores it is unchanged. ParamDialogState is not in @object-ui/app-shell's barrel. Changeset @object-ui/core: minor, @object-ui/app-shell: patch.
Contract review: PASS 6252267851 at CONTRACT_REVIEW_TIER on head 9bcaded: the two surface changes are the only ones, the runner is the narrowest shared point, and the dialog ordering keeps the override notice unevaluated.
Tests:ActionRunner.rowRecordTemplates-12967 (6) drives one action through a header-shaped and a list-shaped runner; rowActionRecordHole-12967 goes through the real ObjectGrid row menu; ActionParamDialog.recordHoles-12967 covers string and per-locale helpText plus a no-interpolate CONTROL; handler cases were added to the two producers' suites, and paramRuntimeParity-6431's DIALOG_READS gained interpolate. Ablation of the binding line turns 6 of 8 red; restore proven by blob hash. core (376), plugin-grid (145), app-shell and the other families pass; type-check and the pre-tsc battery exit 0.
Acceptance notes, not filed:BulkActionDialog prints a multi-row selection's confirmText raw; there is no single record for the rule to bind.
Trailers: all three commits carry only the model-free pair.
CI at head: all 36 check-runs on 9bcaded are complete: 33 success, 3 skipped (dependabot and the two coverage jobs), none failed; mergeable_state: clean. Path surface (11 files, +454 / −12): packages/core/src/actions/**, packages/app-shell/src/{views,hooks}/**, one plugin-grid test and one changeset; no governed path.
PR #12979 merged through the merge queue as 025130ca01f5cc4e84e9cd0e2b810922f5570fb8, which is on main; Fixes #12967 closes this card. Verified by content: all 11 of the PR's files on main are blob-identical to the accepted head 9bcaded. The merge commit's message names no model.
What landed:
A row action's confirmText, and the param dialog's description / helpText, read ${record.*} from the row the action runs against, on a list row as on the record header. A related-list row action reads the child row. A missing field prints empty on both doors; toasts' ${result.*} are unchanged. @object-ui/core: minor (ParamCollectionHandler gains the optional interpolate), @object-ui/app-shell: patch. Contract review PASS 6252267851.
For cloud (the repo:cloud#2 seat): the consumer guard in objectstack-ai/cloud PR #3019 (every ${record.*} confirm on header-only actions, pinned by a translation test) can be relaxed once cloud's objectui pin covers the merge commit above. Whether to relax it is cloud's call. This seat now removes pm:dispatched.
Activity
objectstack-fleet commentedon Oct 18, 2026
Triage: p2 ·
domain:ui·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-18T13:57ZTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/main:packages/plugin-grid/src/ObjectGrid.tsx:5057dispatches a row action withparams = { _rowRecord: r }and binds norecord. The premise holds.confirmTexthits it.helpTextanddescription, which print raw on both doors.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Claim: PM loop round 9 (
priority:p2)Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12967-row-action-record-holeWorktree:
objectui-issue-12967Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/plugin-grid/src/ObjectGrid.tsx(the row-action dispatch at about :5057),packages/core/src/actions/(ActionRunner.ts's template context forconfirmTextand the param dialog'shelpText/description), the param dialog that prints those strings, and tests beside each. Plus one.changeset/12967-*.md(patchper touched package). ⛔ No new action key; ⛔ toasts'${result.*}unchanged. Stop on breach; explain in the report.Container & model:
M,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuireads no path-derived mandate)Clause-②: yes
Clause-② note: amended in place from
noon the dev's measurement (report6252196216): the exportedParamCollectionHandler(@object-ui/core) gains an optionalinterpolateargument, so@object-ui/corelevelsminorand oneCONTRACT_REVIEW_TIERreview is owed before the queue. The production edits also reach the param dialog's two state producers (useConsoleActionRuntime.tsx,RecordDetailView.tsx), within "the param dialog that prints those strings".Responsibility:
@object-ui/plugin-griddispatches a row action with{ _rowRecord }and binds norecord, and@object-ui/core'sActionRunnerevaluatesconfirmTextin its own context | the record header already bindscontext.recordthroughRecordDetailView'sActionProvider| every list row whose action'sconfirmText(or a param dialog'shelpText/description) names${record.*}; cloud'ssys_environmentactions today, behind a consumer-side guard (cloud PR #3019).Thread-read: 6250766637
Serial constraints cleared: no open objectui PR touches
ObjectGrid.tsxorpackages/core/src/actions/. This seat's objectui#12966 (PR #12974) leftObjectGrid.tsxuntouched; the other in-flight claims (objectui#12954, #12957, #12965, #12968, #12970) name neither surface.Claimed at: 2026-10-18T15:44Z
objectstack-fleet commentedon Oct 18, 2026
os-dev-report
{ "issue": 12967, "status": "done", "branch": "claude/issue-12967-row-action-record-hole", "pr": "https://github.com/objectstack-ai/objectui/pull/12979", "session": "session_01XVp19E1MYUTFM3dnkwz8gB", "premise_still_valid": true, "summary": "Premise confirmed on origin/main 43efd078a. The record header binds record in the runner context: RecordDetailView ActionProvider context is { record: pageRecord or {}, objectName, objectFields?, user, org }. ObjectGrid fires a row action as { ...def, params: { _rowRecord: row } } through a list runner whose context has no record, and ActionRunner read confirmText through its own evaluator. Fix, at the shared point: a new private ActionRunner.templateEvaluatorFor(action) binds the dispatch's _rowRecord as record for that dispatch's templates only. The row wins over a page record, and the runner context is untouched. confirmText reads through it. The exported ParamCollectionHandler gains an optional third argument, interpolate. Both app-shell handlers apply it to the localized description, before the override notice is joined, so approver names are never evaluated. Both put it on ParamDialogState, and ActionParamDialog applies it to helpText after pickLocalized. There is no ObjectGrid source change. A1, other callers that dispatch with a row: RelatedRecordActionsBridge rows now get the child row as record (previously the parent page record). DeclaredActionsBar, the containers.tsx page header and action-button update write stash the page record, so nothing changes. The ObjectGrid per-record bulk path strips confirmText, so it is not affected. A2: on the header a missing field prints an empty hole ('Pause ?'). The row printed raw source and now prints the same. Ruled scope: no action key added, toasts' result holes unchanged, and no declaration-time refusal added because both doors now fill. Clause-2 is yes (widening): this is a measured deviation from the claim's 'no', because an exported type gained a parameter. The changeset is core minor and app-shell patch.", "tests": "Final head 9bcaded76f. TYPE-CHECK: built the app-shell dependency closure under the lock (exit 0), then type-check for core, app-shell and plugin-grid exit 0. Every touched test is in its package's tsconfig.test.json list (checked with --listFilesOnly). NEW PINS: core ActionRunner.rowRecordTemplates-12967.test.ts, 6/6 (a, row-over-page, b, c, no-row control, d). plugin-grid rowActionRecordHole-12967.test.tsx: real ObjectGrid row menu against a header-shaped ActionProvider, covering (a) and (c). app-shell ActionParamDialog.recordHoles-12967.test.tsx: string and per-locale map helpText, plus a no-interpolate control. Handler cases added to RecordDetailView.paramDialogTitle and useConsoleActionRuntime.overrideNotice. The 4 new or edited files passed 18/18. FAMILIES (vitest --maxWorkers=1, one package per run, under the lock): core 24 files / 376 tests pass. app-shell 37 files: the first run was 3 failed / 524 passed, all three in RecordDetailView.paramRuntimeParity-6431. Its enumerated DIALOG_READS list lacked the new interpolate field; I updated it and the file re-ran 9/9. plugin-grid 12 files / 145 tests pass. Others (components, react, i18n, plugin-detail, types, console) 22 files / 537 tests pass. Whole-package directories are declared to CI and were not run locally. ABLATION: objectstack scripts/ablation-replace.mjs replaced 'return new ExpressionEvaluator({ ...this.context, record: row });' with 'return this.evaluator;' (anchor x1 to x0, blob edf7176c00c7 to a89ed93ce6b0, marker grep 1, anchor grep 0). Core plus grid pins: 6 failed / 2 passed. (a) went red on both, Expected 'Pause Acme?' Received 'Pause ${record.display_name}?'. Restore proven: blob after restore edf7176c00c7 equals HEAD, git diff HEAD empty, porcelain empty. Tests resolve @object-ui/core to src through the root vitest alias, so no dist leg was involved. LINT: eslint --no-inline-config --format json over the 10 changed files: 10 files, 0 errors, 238 warnings, all no-explicit-any or react-hooks warnings already in the touched files. No type-aware linting is configured, so the narrowing cannot move a verdict on any untouched file. BATTERY (exit codes): phantom-deps 0, self-import 0, unreferenced-sources 0, doc-example-readers 0, handler-key-reads 0, side-effects-array 0, element-data-source-declaration 0, spec-symbols 0, action-forward-parity 0, designer-field-key-parity 0, icon-record-names 0, i18n-keys 0, i18n-drift 0, i18n-designer-parity 0, control-bytes 0, new-line-citations 0, check-hand-rolled-comment-mask 0, check-changeset-fixed 0, check-changeset-presence 0. spec-floors 1: pre-existing on main per the dispatch, and no manifest is touched. CI: not awaited (in_progress at report time).", "mcp_calls": "0", "api_writes": "2 relay strokes plus git pushes. (1) fleet-write pr_create: POST /repos/objectstack-ai/objectui/pulls (draft) and POST /repos/objectstack-ai/objectui/issues/12979/assignees; read-back 7531/7531 bytes identical, assignee baozhoutao. (2) fleet-write comment: POST /repos/objectstack-ai/objectui/issues/12967/comments (this report). git pushes: the empty-branch probe plus 3 commit pushes (git, not REST). Label writes: 0. The path labels on #12979 come from labeler.yml, and the dispatch named no labels.", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed. BulkActionDialog prints def.confirmText raw for a multi-row selection. There is no single record there, so the 'row is record' rule does not apply. This is an observation and is recorded in the PR's Acceptance notes." ], "files_changed": [ "packages/core/src/actions/ActionRunner.ts", "packages/app-shell/src/views/ActionParamDialog.tsx", "packages/app-shell/src/hooks/useConsoleActionRuntime.tsx", "packages/app-shell/src/views/RecordDetailView.tsx", "packages/core/src/actions/__tests__/ActionRunner.rowRecordTemplates-12967.test.ts", "packages/plugin-grid/src/__tests__/rowActionRecordHole-12967.test.tsx", "packages/app-shell/src/views/ActionParamDialog.recordHoles-12967.test.tsx", "packages/app-shell/src/views/RecordDetailView.paramDialogTitle.test.tsx", "packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.overrideNotice.test.tsx", "packages/app-shell/src/views/RecordDetailView.paramRuntimeParity-6431.test.tsx", ".changeset/12967-row-action-record-hole.md" ], "gates": "type-check core/app-shell/plugin-grid 0; targeted vitest 0 (after the parity-pin update); eslint on changed files 0 errors; battery all 0 except spec-floors 1 (pre-existing); changeset presence and fixed 0", "deviations": [ "Clause-2 measured yes (widening), not the claim's no: ParamCollectionHandler (exported from @object-ui/core) gained an optional interpolate parameter, and ParamDialogState (not in the app-shell barrel) gained an optional interpolate member. The changeset is core minor and app-shell patch.", "Beyond the claim's named files, I edited three existing app-shell test files: two handler suites gained cases, and the paramRuntimeParity-6431 DIALOG_READS list gained interpolate. The production edits stayed inside the claimed surface: the ActionRunner, the param dialog, and its two state producers (useConsoleActionRuntime, RecordDetailView). The two producers are named here because the claim said 'the param dialog that prints those strings'.", "The declaration-time refusal from the card's Wanted section was not implemented, per ruling 1." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
ACCEPT ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-18T16:31ZPR #12979 at head
9bcaded, reviewed against the claim6251848893(amended toClause-②: yes), triage6250766637, the dev report6252196216and the contract review6252267851.record, as the record header's do. At the one shared point,ActionRunner.execute, a privatetemplateEvaluatorFor(action)binds the dispatch's_rowRecordasrecordfor that dispatch's templates only (the row wins over a page record; the runner context is untouched).confirmTextreads through it. The param dialog'sdescription(both console producers, interpolated after localisation and BEFORE the override notice is joined, so approver names are never evaluated) andhelpText(afterpickLocalized) read through the same evaluator, handed over as the exportedParamCollectionHandler's new optionalinterpolateargument. NoObjectGridsource change. A missing field prints an empty hole on both doors, matching the header. Toasts'${result.*}are unchanged.RelatedRecordActionsBridgerow actions now see the CHILD row asrecord(before: the parent page record). That is the card's rule, because the action runs against the child. The contract review searchedexamples/,apps/,content/docs, the READMEs, demos and changesets and found no author relying on the parent there.yes (widening):ParamCollectionHandler(@object-ui/core) gains an optional third parameter; a handler that ignores it is unchanged.ParamDialogStateis not in@object-ui/app-shell's barrel. Changeset@object-ui/core: minor,@object-ui/app-shell: patch.6252267851atCONTRACT_REVIEW_TIERon head9bcaded: the two surface changes are the only ones, the runner is the narrowest shared point, and the dialog ordering keeps the override notice unevaluated.ActionRunner.rowRecordTemplates-12967(6) drives one action through a header-shaped and a list-shaped runner;rowActionRecordHole-12967goes through the realObjectGridrow menu;ActionParamDialog.recordHoles-12967covers string and per-localehelpTextplus a no-interpolate CONTROL; handler cases were added to the two producers' suites, andparamRuntimeParity-6431'sDIALOG_READSgainedinterpolate. Ablation of the binding line turns 6 of 8 red; restore proven by blob hash. core (376), plugin-grid (145), app-shell and the other families pass; type-check and the pre-tsc battery exit 0.BulkActionDialogprints a multi-row selection'sconfirmTextraw; there is no single record for the rule to bind.9bcadedare complete: 33 success, 3 skipped (dependabot and the two coverage jobs), none failed;mergeable_state: clean. Path surface (11 files, +454 / −12):packages/core/src/actions/**,packages/app-shell/src/{views,hooks}/**, oneplugin-gridtest and one changeset; no governed path.Next:
pr_ready+automerge_enable.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Landed ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-18T16:47ZPR #12979 merged through the merge queue as
025130ca01f5cc4e84e9cd0e2b810922f5570fb8, which is onmain;Fixes #12967closes this card. Verified by content: all 11 of the PR's files onmainare blob-identical to the accepted head9bcaded. The merge commit's message names no model.What landed:
confirmText, and the param dialog'sdescription/helpText, read${record.*}from the row the action runs against, on a list row as on the record header. A related-list row action reads the child row. A missing field prints empty on both doors; toasts'${result.*}are unchanged.@object-ui/core: minor(ParamCollectionHandlergains the optionalinterpolate),@object-ui/app-shell: patch. Contract review PASS6252267851.For cloud (the
repo:cloud#2seat): the consumer guard in objectstack-ai/cloud PR #3019 (every${record.*}confirm on header-only actions, pinned by a translation test) can be relaxed once cloud's objectui pin covers the merge commit above. Whether to relax it is cloud's call. This seat now removespm:dispatched.Generated by Claude Code