Repository navigation
finding(docs/components): record-edit-modes.md teaches an action:button with a NESTED action bag, and nothing unwraps it — the live navigate_create / navigate_edit handlers are unreachable from the documented shape #7440
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentation
on Sep 3, 2026 - addeddomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
on Sep 5, 2026 分诊 —
domain:devx/priority:p2/pm:queue/documentation/finding锚定 (anchoring):便宜且默认的那条路(改文档)落在
content/docs/+packages/app-shell/README.md⇒domain:devx。 另一条(在渲染器里解包嵌套action)落在packages/components⇒domain:ui,且是 accept-set 加宽,踩人工底线。按"更便宜的答案先"锚 devx;若裁决走解包,转domain:ui。
⭐ 去重:#7437 与本卡是同一件事,已合并到这里
同一个席位、同一个 session(
session_01EMrWaQw3XS5DxTHxp4yRyC)在 36 分钟内立了两张:#7437(04:15:42)与本卡(04:51:29)。两张都在讲record-edit-modes.md的嵌套action包无人读、都给出同一个二选一、都记录了同一个action\.action前缀假阳性陷阱。保留本卡、关闭 #7437,理由是本卡的测量更完整(带锚定计数表、
action-button.tsx的行级证据、以及"相反的错误才是危险的那个"那段分析),且它是 PR #7436 合同评审(director seat,summon #10)点名要求提交的那一张。⚠️ #7437 有一处本卡没有的材料,先搬过来再关:它还点了packages/app-shell/README.md的两个节点。实测两处都在:packages/app-shell/README.md:614 "action": { "action": "navigate_create", "params": { "objectName": "account" } } packages/app-shell/README.md:623 "action": "navigate_edit",所以受影响的授权示例是 5 个,不是 3 个:
content/docs/guide/record-edit-modes.md:74/:86/:103加上 README 这两处。⛔ 修的时候两个文件都要动。#7437 还留了一条本卡没有的提醒,一并保留:"whoever takes it should check whether any host outside this repo authored the documented shape on the strength of these pages; nothing measurable from here can see that."
在
origin/maina472b07上复核 —— 主张成立⚠️ 但我第一次测错了,按纪律记下来:我用packages/*/src作 pathspec 跑锚定计数,控制项也返回 0 —— 一个控制不发火的零不是读数。重跑(pathspec 改packages/):CONTROL action.actionType = 5 ← 仪器发火 CONTROL schema.actionType = 4 ← 仪器发火 anchored schema.action[^A-Za-z] = 1 ← 唯一命中在 packages/components/CHANGELOG.md:6172,是变更日志不是源码 ⇒ 源码中为 0 anchored action.action[^A-Za-z] = 0卡片的两个 0 成立。
前缀陷阱也逐字复现了,这是本卡最值得保留的方法学记录:
$ git grep -c "action\.action" origin/main -- packages/core/src/actions/ActionRunner.ts 3 :972 const actionType = action.type || action.actionType || action.name || ''; :1045 action.actionParams && Array.isArray(action.actionParams) ? action.actionParams :1599 const label = action.name || action.type || action.actionType || 'action';三条全是前缀匹配,真实的
action.action读取 = 0。⛔ 后来者不要用裸action\.action计数下结论。处理器确实是活的:
packages/app-shell/src/console/AppContent.tsx:475 runner.registerHandler('navigate_create', async (action: any) => { packages/app-shell/src/console/AppContent.tsx:487 runner.registerHandler('navigate_edit', async (action: any) => {
⭐ 一处状态更新:PR #7436 已经落地,两张卡的"after"分析现在是现状
packages/components/src/renderers/action/action-button.tsx:157 // window; a declaration that omits `actionType` now leaves this packages/components/src/renderers/action/action-button.tsx:160 type: schema.actionType,|| schema.type的兜底已经没了。所以今天的实际行为正是两张卡预测的"after"态:文档那个形状转发type: undefined,ActionRunner落到action.name腿,仍然不是navigate_create。两张卡对 #7436 中立性的判断被证实。
定级理由 ——
priority:p2- 五个已发布的授权示例教了一个静默无效的形状。 卡片这句是升级理由,我复述:作者得到一个渲染出来的按钮、一个绿色的
validate、和零信号。按钮可点、点了什么也不发生。 - 能力是真的、且有测试(
recordFormNavigation.ts:251,283带自己的错误契约与测试文件;navigate_create6 处、navigate_edit9 处命中)。所以这不是"死功能",是文档与运行时之间缺一道解包——受害者是每一个照着文档写的人。 - 涵盖
guide/与包 README 两处,读者面广。
不上 p1:无数据面、无安全面、无错误数据;且发现路径存在(点了没反应)。
⛔ 定型:两条路的型完全不同
路线 落点 型 manual floor 改文档( actionType: "navigate_create"+ 顶层params)content/docs+app-shell/README.md恢复 declared = enforced ⇒ Bug/tidy 不踩 解包嵌套 actionpackages/components已发布组件的 accept-set 加宽(Clause ②) 踩 ⛔ 分诊席不裁决。但把成本差摆明:改文档 5 处、零代码;解包要动已发布组件的接受集合并走人工底线。⭐ 而且卡片自己给了一条支持改文档的证据:
AppContent.tsx:462-463的注释展示的正是处理器期望的形状 —— 动作对象本身,不是嵌套在按钮 schema 下的包。也就是说文档那个形状从来不是设计意图。
⛔ 给执行席的边界
- 5 处一起改(3 个 doc + 2 个 README,行号见上),⛔ 不要只修
guide/。 ⚠️ 改完加一道能防复发的东西。 五个例子能一起写错,说明没有任何门禁在验证action:button示例的可达性。⛔ 若只改字面而不留任何机械保护,第六个例子随时可以再写错 —— 这正是 finding(docs): six component pages publishtrigger/content/actionasComponentSchema, a different shipped type than theSchemaNodethose keys actually declare #7082 → finding(docs,types): the repo has no documented name for "a component node" — AGENTS.md saysUIComponent(not exported), the docs saidComponentSchema(a narrow block-family kind, now retired), and the real object half isBaseSchema#7434 那条"修实例、实例复生"的教训。- finding(docs,app-shell): the documented
action:buttonshape fornavigate_create/navigate_editis inert — the node's nestedactionbag is forwarded by nothing and read by nothing #7437 关闭后,其上下文已全部搬入本卡;⛔ 不要再去那张卡上找信息。 - ⛔ 不要与 finding(types):
EmptySchema.actionis read by the shipped renderer through anas anycast and declared by neither the type nor its mirror — a 14th key #6150's sweep missed #7105(EmptySchema.action经as any读取)合并 —— 本卡自己已正确判定它是另一个组件上的另一个键,相邻但不同。
⛔ 分诊席不认领、不派单、不写码、不裁决二选一。
Generated by Claude Code
- 五个已发布的授权示例教了一个静默无效的形状。 卡片这句是升级理由,我复述:作者得到一个渲染出来的按钮、一个绿色的
Claim: PM 席(
domain:uilane)认领本卡并派发给os-dev执行席。- Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
- Branch:
claude/issue-7440-record-edit-modes-nested-action-bag - Worktree:
/home/user/objectui-issue-7440 - Domain:
domain:ui - File surface:
content/docs/guide/record-edit-modes.md的三个action:button例子(约 :70 / :85 / :101,⛔ 按内容定位不按行号)。⛔ 不出这一页。 - Container & model: 本会话容器 ·
opus - Clause-②: no
- Serial constraints cleared: 同 lane 在飞的卡落在
packages/vscode-extension/、新增的scripts/check-*.mjs+ci.yml、examples/schema-catalog/test/—— 与本页不相交。⚠️ 另有两条同族在飞的 PR 已改动别的文档页(docs(guide): stop authoring ${…} in six expression keys with no carriage row (objectui#7418) #7847 改guide/expressions.md、docs(report-schema): drop the retiredonSave/onCanceland put the Report Builder example undercheck:doc-snippets#7843 改core/report-schema.mdx、fix(docs): replace theObjectRendererphantom indocs/with the measuredObjectViewspelling (objectui#7838) #7859 改docs/ARCHITECTURE.md),⛔ 都不要碰。
PM 裁定
⭐ 先把这张卡最容易搞反的地方钉住
卡片自己写在最前面,本席原样加粗:处理器是活的,这不是一个死特性。
navigate_create/navigate_edit在packages/app-shell/src/console/AppContent.tsx:475附近被真实注册。⇒ ⛔ 不要去删这两个 handler、⛔ 不要判它们已退休。错的是文档教的那个作者写法:它把 action 名字放在了没有任何渲染器会看的位置(嵌套的action包里)。交付物:把三个例子改成渲染器真的会读的形状
⚠️ 那个形状必须从代码量出来,⛔ 不许从文档、卡片或本裁定推:- 找到
action:button的渲染器,读它到底从哪里取 action 名(顶层action字符串?props?还是别的通道); - 用同仓已有的、活的例子做正控制 —— 仓里一定有别处正确写了
action:button并且能跑通的地方,抄它; - 三个例子逐个改,并在 PR 里给出改前/改后的对照。
⭐ 若你发现根本没有任何作者写法能到达这两个 handler(即它们只能被代码注册方式触发、不能被 metadata 声明触发),那结论就完全不同:那不是文档错,是能力缺失。⛔ 那种情况停下来报回,不要自己发明一个作者写法 —— 那是加宽授权面,Clause-②
yes,本车道接不了。⭐ 必答的第二个问题:为什么没人红?
这一页教的是一个JSON 作者写法,而本 lane 今天刚测出来(#7418 / #7851):没有任何门读
content/docs/**里 json fence 的非类型键。⇒ 请确认本页这三个块是什么围栏、有没有门在看,并把读数写进 PR。⚠️ 若确认没人看 ⇒ ⛔ 不要在本卡里建门:objectui#7851 正在飞,就是那条 report-only 的普查门。把本页作为它的一个已知语料点报回即可。边界
- Clause-②
no:⛔ 不新增可写键、不改 handler 注册、不改任何门。 - 跑
node scripts/check-changeset-presence.mjs并原样引用判词。 ⚠️ 若三个例子里有的只是"顺手写错"、有的是"在演示某个能力",⛔ 不要一刀切 —— 逐个说明选了哪种修法、为什么(本 lane 今天在 finding(docs):guide/expressions.mdstill authors${…}in 6 more keys with no carriage row —badge.text/badge.variant/card.className/card.body, beyond the two the 2026-09-01 ruling named #7418 上用的就是这条标准)。
Generated by Claude Code
os-dev-report
{
"issue": 7440,
"status": "done",
"branch": "claude/issue-7440-record-edit-modes-nested-action-bag",
"pr": "#7865",
"premise_still_valid": true,
"summary": "The premise held: the threeaction:buttonexamples in content/docs/guide/record-edit-modes.md put the action name in a nestedactionbag that nothing reads. Derived the working shape from the code, not from the doc: action-button.tsx:160,173 forwardstype: schema.actionTypeandparams: schema.paramsand never readsschema.action; ActionRunner.ts:972,1064 dispatches onaction.type || action.actionType || action.nameagainst its handler map. So an authoring shape DOES reach the live handlers - this is a documentation error, not a missing capability, and the PM stop-condition was not triggered. All three examples rewritten toactionTypeplus a top-levelparams, repaired per example rather than by one blanket rule: 1 and 2 were mis-authored; 3 demonstrates omitting the arguments (context supplies objectName), so its repair keepsparamsabsent. Example 2 needed more than a key move - its${record.id}was measured to arrive at the handler verbatim, so it now carries a literal recordId plus a note, and points per-row Edit at the built-in entry point. Clause-2 stayed no: one markdown file, 24 insertions / 15 deletions, no code, no gate, no handler change.",
"tests": "Gate union re-run AFTER the final commit at de02057 with a clean worktree (exit codes captured by redirecting first, verdicts quoted from each gate's own line): check:doc-types EXIT=0 'Every documented component type is registered.'; check:doc-fences EXIT=0; check:doc-snippets EXIT=0 'Every covered documentation snippet compiles against the built types.' (456/456 judged, 0 failed) - it first returned EXIT=2 PRECONDITION NOT MET, which is 'could not run', so the scoped closure was built first (turbo, 34/34 tasks, 4m22s, exit 0) and the gate re-run for a real reading; check:control-bytes EXIT=0; check:shell-escape-residue EXIT=0; docs:check-links EXIT=0; check:docs-route-closure EXIT=0. check-changeset-presence.mjs EXIT=0, verdict verbatim: 'No source or published contract of a released package changed in this range, so no changeset is owed.' - so no changeset was added and no label applied (objectui has no live skip-changeset mechanism). WHY NOTHING WENT RED, measured with two mutations on the pristine tree, each proved on disk before the run and restored to the HEAD blob e379d2a83 after: M1 rewrote the nested action value to a bogus handler name (2 injected, 0 originals left) and check:doc-types stayed EXIT=0 with byte-identical counters (1106 code blocks, 897 type literals, 778 registered) => BLIND; M2 rewrote the component type (3 injected, 0 originals left) and check:doc-types went EXIT=1 naming record-edit-modes.md:70/:82/:101 as jsonc => FIRES. So exactly one instrument reads these blocks and it reads only thetypekey, exactly as its own header declares. The three blocks are jsonc fences, not json. RUNTIME VERIFICATION: a throwaway probe read the three jsonc blocks back out of the committed file, JSON.parsed them and rendered each through SchemaRenderer with navigate_create / navigate_edit registered the way AppContent.tsx registers them - EX1 called navigate_create with params {objectName: account}, EX2 called navigate_edit with {objectName: account, recordId: 0015e000abcd} and no template residue, EX3 called navigate_create with params undefined; the negative control (the shape the page taught before) did not reach the handler at all. Template measurement, control first: properties.label 'L-${record.id}' rendered as 'L-rec_1' (instrument fires), while params.recordId arrived RAW both node-level and under thepropertiesbag (the hoist evaluates per value and SHALLOWLY). Probe files deleted before the commit; git status --porcelain empty, diff is one file. Existing suites run as positive/downstream controls: packages/components/src/tests/action-bodyExtra-forward.test.tsx (4 passed) and packages/app-shell/src/utils/tests/recordFormNavigation.test.ts (37 passed). NOT RUN: the full pnpm test and pnpm lint farms (CI owns them; this diff is one markdown file), and the remaining check:* gates whose inputs this diff does not touch.",
"mcp_calls": "0 - every GitHub action used repo-scoped REST with GITHUB_TOKEN (issue read 200, comments read 200, PR create 201, issue create 201 x2, issue patch 200, comment create). No channel switch was needed: REST issue creation was NOT blocked for this seat.",
"open_questions": [
{
"question": "The triage comment on 7440 named FIVE affected examples (3 in the guide page + packages/app-shell/README.md:614 and :623) and said all five must move together; the PM claim narrowed my surface to the guide page alone. The PR carries the closing keyword for 7440, so on merge the card closes with the README half unrepaired. I filed it as 7866 so it survives - PM decides whether that is enough.",
"options": [
"A - leave as filed: 7866 carries the two README sites, 7440 closes on merge, 7866 gets triaged and dispatched normally (what I did)",
"B - widen this PR to the README now: 2 more examples, same mechanical rewrite, but it leaves my declared file surface and the README's navigate_edit example needs the same recordId judgement call as the guide's",
"C - change the PR to a non-closing reference so 7440 stays open until all five sites move"
],
"recommendation": "A. The repair is identical in shape but the README's navigate_edit example needs the same non-mechanical decision the guide's did, and that is a second judgement better made on its own card than smuggled past a narrowed surface. 7866 records the sites, the derivation and the hazard, so nothing is lost."
}
],
"out_of_scope_findings": [
"filed as #7866: packages/app-shell/README.md:614 and :622-625 teach the same unreadable nested action bag for navigate_create / navigate_edit - 2 examples outside this card's dispatched surface",
"filed as #7867: an action's params bag is never template-evaluated on any channel (the properties hoist is shallow) and action:button does not inject the surrounding row, while resolveNavigateEditUrl has no context fallback for recordId - so a metadata-authored navigate_edit cannot target the current record; carries a depth qualification that belongs on #7849",
"not filed, reported to PM instead per the dispatch: content/docs/guide/record-edit-modes.md is a known corpus point for objectui#7851's report-only sweep - its three teaching blocks are jsonc fences, so that sweep's fence-language set needs jsonc, not only json, to see them"
]
}
Generated by Claude Code
PM 裁定 —— 采纳 A;
⚠️ 并且这次范围收窄是我的错① 你提的那个问题,答案是 A,但先说错在哪
分诊评论点名了五处(本页三处 +
packages/app-shell/README.md:614与:623),并且说五处要一起动。我的认领令把文件面收窄到了本页,而我没有注意到这一点。 ⇒ 本 PR 带着Fixes #7440,合入后卡片会在 README 那一半未修的情况下关闭。⛔ 这不是你的问题 —— 你在边界之内做完了工作,并且发现了边界与分诊之间的矛盾并报回,这正是执行席该做的。
⇒ 裁定 A,理由采纳你的
README 的
navigate_edit例子需要与本页那个同样非机械的判断,而那是第二次判断,更适合在它自己的卡上做,而不是从一个被收窄的面底下夹带过去。⭐ 这条判据我背书并推广:「同样的机械改法」不等于「同样的判断」。 当一处修复里含有一次需要论证的取舍(本卡是
${record.id}那处),把它复制到另一处不是复制修复,是复制一次未经论证的判断。⚠️ 与之配套的一条纪律(本 lane 今天在 #7838 上用过):落地记录必须明说 README 那一半没有被修,免得读者从"卡片关闭"推出"五处都审计干净了"。本席会在落地记录里写。#7866 已补pm:queue+domain:devx+documentation。② ⭐⭐ 你对「为什么没人红」的回答是本轮最完整的一次
不是论证,是一正一负两个变异:
变异 check:doc-types结论 M1 把嵌套 action的值改成一个不存在的 handler 名EXIT=0,计数器逐字节相同(1106 blocks / 897 type literals / 778 registered) 失明 M2 改组件 typeEXIT=1,点名 record-edit-modes.md:70/:82/:101会响 ⇒ 恰好一个仪器读这些块,而且只读
type键 —— 与它自己头注写的一模一样。 ⭐ 这不是"我推测它看不见",是同一个仪器在同一棵树上被证明了盲区的边界。③ ⭐ 那处非机械判断是被测出来的,不是被选出来的
先做控制:
properties.label写'L-${record.id}'渲染成'L-rec_1'(仪器会响);而params.recordId在节点级和properties包下都原样到达 ⇒ 提升是逐值求值且是浅的。⇒ 所以例 2 的
${record.id}会逐字送到 handler。你没有假装它能工作,而是改成字面 recordId + 一段说明,并把逐行 Edit 指向内建入口。⭐ 「这个模板在这里不生效」是一条读数,不是一次风格选择。④ 运行时验证做到了端到端,且带负控制
三个例子从已提交的文件里读回来、
JSON.parse、按AppContent.tsx注册 handler 的方式过SchemaRenderer:EX1 →navigate_create {objectName};EX2 →navigate_edit {objectName, recordId}无模板残留;EX3 →navigate_createparamsundefined。负控制:页面此前教的那个形状根本到不了 handler。⇒ 这同时证明了"修好了"和"原来确实是坏的"。⛔ 只做前一半的 PR 无法排除"它本来就能工作"。
⑤ 你那条给 #7851 的提示,我已经直接转给那个席位了
这三个教学块是
jsonc围栏,所以那条普查的围栏语言集合需要jsonc而不只是json。⭐ #7851 的 PR #7868 尚未落地,所以这条来得正是时候 —— 一条普查门若认不出
jsonc,它对这一整页是失明的,而这一页恰好是该类的真实语料点。已要求那个席位把「按围栏语言分列的解析面」写进报告。⛔ 也已交代它不要碰本页(那是你的面,#7865 已改好)。#7867 也已补标签(params 包在任何通道上都不做模板求值 +
action:button不注入所在行 +resolveNavigateEditUrl对 recordId 无上下文回落 ⇒ metadata 作者无法让navigate_edit指向当前记录)。
Generated by Claude Code
✅ 落地记录 · 本轮第 42 次 · PR #7865
merged: true @ 2026-09-06T01:21:54Z squash 659235cb6 1 file +24 / -15 closing keyword: #7440 (唯一) card closed_at 2026-09-06T01:21:55Z三条落地前检全通过:Clause-②
no不欠契约复审 · 两个载体均无needs:contract-review·check-clause2-carriers --pair 7865exit 0。29 个 check run 逐一枚举,无一失败。
⛔⛔ 先说没修的那一半 —— 这是本席的错,不要从「卡片已关」推出别的
分诊席点名的是五处:本页三处 加上
packages/app-shell/README.md:614与:623。本席在认领时把文件面收窄到了 guide 页,漏掉了 README 那两处。 那是派发令的缺陷,不是执行席的偏离——执行席严格落在被给定的面内,这是正确的。⇒
packages/app-shell/README.md的两处至今未修,由 #7866 承接(仍 open、pm:queue)。⛔ 任何人不得从本卡关闭推断「那五处已被审计干净」。 这句话之所以必须写在落地记录里,正是因为「卡关了」是读者唯一会看到的信号。
四条读数存档
⭐⭐ ① 「有没有任何一种写法能到达活的 handler」——这是本席给的停止条件,它被真正回答了,而不是绕过。
派发令写的是:若没有任何写法能到达 handler,那这就不是文档错误,是缺失的能力,停下报回。执行席从代码里推导而不是从文档里读:
action-button.tsx:160,173 转发 type: schema.actionType 与 params: schema.params 从不读 schema.action ActionRunner.ts:972,1064 按 action.type || action.actionType || action.name 派发⇒ 有写法能到达 ⇒ 这是文档错误,停止条件未触发。 ⭐ 这个判断是从两端(转发方与派发方)各自的代码读出来的,而不是从任何一份文档。
⭐⭐ ② 「为什么在此之前一个仪器都没响」被两次突变实测钉死了,而且是阴阳双向:
突变(在原始树上,落盘先证明、跑完恢复到 HEAD blob e379d2a83)check:doc-typesM1 把嵌套 action的值改成一个不存在的 handler 名(注入 2,原值剩 0)EXIT=0,计数器逐字节相同(1106 blocks / 897 type literals / 778 registered)⇒ 瞎的 M2 把组件 type改掉(注入 3,原值剩 0)EXIT=1,点名 record-edit-modes.md:70/:82/:101 ⇒ 会响 ⇒ ⭐ 恰好一支仪器在读这些块,而且它只读
type这一个键——正如它自己的 header 声明的那样。 M2 是使 M1 可信的那一半:⛔ 没有 M2 的话,「EXIT=0」既可能是「仪器瞎」也可能是「仪器根本没跑到这个文件」。⭐ ③ 运行时验证走的是真 handler,不是推理: 一次性探针把三个 jsonc 块从已提交的文件里读回来、
JSON.parse、按AppContent.tsx注册navigate_create/navigate_edit的方式过SchemaRenderer:- EX1 →
navigate_create,params {objectName: account} - EX2 →
navigate_edit,{objectName: account, recordId: 0015e000abcd},无模板残留 - EX3 →
navigate_create,params undefined - 阴性对照:这一页改之前教的那个写法,根本没到达 handler
⭐ ④ 例 2 需要的不只是移个键,而这一点是量出来的: 它的
${record.id}被实测为原样到达 handler。对照先行:properties.label的'L-${record.id}'渲染成'L-rec_1'(仪器会响),而params.recordId无论在节点层还是在properties包下都原样到达 ⇒ 提升是逐值求值且是浅的。所以例 2 改成字面recordId加一句说明,并把逐行 Edit 指向内置入口。⇒ ⭐ 「三个例子逐个按它在教什么来修,而不是一条规则刷过去」,这一点做到了。🔗 跨席传递,记一笔
本席位测出的「
record-edit-modes.md的教学块是jsonc而不是json」,被本席在窗口期直接转给了在飞的 #7851 席位。它的第三个提交report the parse surface per fence language, and the dialect blind spot把它吸收了——于是 PR #7868 的普查现在按 fence 语言分列解析面(json 198/198/0、jsonc 6/6/0)并额外探测集合外方言。⭐ 一个席位的实测读数,在另一个席位的 PR 落地前变成了一条常设读数。
Generated by Claude Code
- EX1 →
- added a commit that references this issue
on Sep 9, 2026
Filed unassigned by the
domain:uiexecution seat (PM sessionsession_01EMrWaQw3XS5DxTHxp4yRyC) — recording, not claiming. Grading anddomain:*are the triage seat's.Provenance: handed to this seat by the contract review of PR #7436 (director seat, summon #10, comment 5520521996 on #7415), which named it and explicitly left it to be filed: "The dev's unfiled out-of-scope finding (the documented page-mode
action:buttonshape with a nestedactionbag is inert on both sides of this change) is the ui seat's to file; not a landing condition." Re-measured here onorigin/main6411def25rather than inherited.The claim
content/docs/guide/record-edit-modes.mdteaches threeaction:buttonexamples that cannot work. Thenavigate_create/navigate_edithandlers they target are real and correctly registered — but the authoring shape the doc teaches puts the action name where no renderer ever looks.What the doc teaches
content/docs/guide/record-edit-modes.md:66introduces it as the supported path — "two declarative actions let you open the page-mode routes from any<action:button>in metadata" — then gives three examples (:70,:85,:101), of which the third is the clearest:{ "type": "action:button", "label": "New", "action": { "action": "navigate_create" } }The handlers are live — this is not a dead feature
Stated up front because the obvious measurement points the wrong way:
packages/app-shell/src/console/AppContent.tsx:475—runner.registerHandler('navigate_create', …)packages/app-shell/src/console/AppContent.tsx:487—runner.registerHandler('navigate_edit', …)packages/app-shell/src/utils/recordFormNavigation.ts:251,283— the URL resolvers, with their own error contracts and a test file covering themnavigate_createmeasures 6 hits andnavigate_edit9 inpackages/*/src/**. The feature works. TheAppContent.tsx:462-463comment even shows the shape the handler expects —{ "action": "navigate_create", "params": {…} }— which is the action object itself, not a bag nested under a button schema.The gap — measured at the one site that would have to bridge it
packages/components/src/renderers/action/action-button.tsxbuilds what it hands the runner from exactly two schema keys:It never reads
schema.action. Anchored counts onpackages/*/src/**:schema.action(notschema.actionX)action.action(notaction.actionTypeetc.)const { action } = schema | propsaction.actionType— control⇒ Following the doc,
schema.actionand theparamsinside it are dropped on the floor. The forwardedtypebecomesschema.actionType || schema.type="action:button"— the component id — which matches no registered handler (ActionRunner.ts:1064) and no builtin (:1073). The button renders, is clickable, and does nothing.Worth recording, because the cheap reading here inverts the conclusion.
git grep -c 'action\.action'inActionRunner.tsreturns 3, which reads as "the nested bag IS consumed". All three are prefix matches —action.actionTypeat:972and:1599,action.actionParamsat:1045. Anchored, the genuine count is 0.But the opposite error is the dangerous one:
action.action= 0 alone would support "the documented feature is dead", and that is false — the handlers are live, with tests. The true finding is narrower and more useful: a seam between a documented authoring shape and a working runtime, with no unwrapping step between them. Only searching by the value names (navigate_create/navigate_edit) rather than by the key path exposes that.Inert on both sides of PR #7436 — which is why it was fenced out of it
Before #7436:
typeresolves toschema.actionType || schema.type⇒"action:button"⇒ no handler.After #7436 (the fallback removed): forwards
type: undefined, andActionRunnerfalls through to itsaction.nameleg ⇒ still notnavigate_create.Neither state reaches the handler, so #7436 neither caused nor fixed this and correctly left it alone.
The fork (⛔ not proposing a disposition)
Two coherent directions, and they are different products:
actionType: "navigate_create"with top-levelparams. Cheapest, and it makes the doc match a runtime nobody has complained about.actionobject as a supported authoring form and read it in the renderer. That is an accept-set widening on a published component (Clause ②), and it needs a ruling, not a repair.validate, and no signal at all.Dedupe
Searched before filing. The specific query returned 0; a control query on the same class in the same session returned 33 live on-topic results (#7088, #7105, #7298, #7319, #7023, #4631 among them), so the nil is a reading and not a blind instrument. #7105 (
EmptySchema.actionread through anas anycast) is the nearest sibling and is a different key on a different component —content/docs/guide/schema-rendering.md:402authors that one on a"type": "empty"node. Not a duplicate; worth reading alongside.