Repository navigation
[P3] objectui ActionDef's [key: string]: any absorbs every retired and misspelled action key — removing a spec key produces no compile error #4046
Description
Migrated from objectstack-ai/objectstack#4075 under the file-at-destination ruling (objectstack-ai/objectstack#7167, maintainer 2026-08-10). Originally filed 2026-07-30T08:22:35Z. The full prior thread — including triage rulings and hold/restart conditions — remains on the source issue and MUST be read before acting on this card.
⚠️ 状态头(2026-08-06 更新——派发者读这里,不要按下文的 staged plan 派单)
- Step 1(构造点清查 + dev-mode 未识别键警告)✅ 已完成:objectui#3032(2026-07-30 合并,
@object-ui/core17.1.0;actionKeys.ts+ AST 再派生 pin 测试)- Step 2(合法键位提升为显式字段)✅ 已完成:objectui#3190(2026-08-02 合并,17.2.0;18 个 spec 键全部提升)
- Step 3(删除索引签名)🟡 已裁决、待上游:维护者 2026-08-06 裁定
visible/disabled统一收敛为boolean | string(CEL) | {dialect,source},spec 侧先行(spec:ActionSchema的visible/disabled统一为boolean | string(CEL) | {dialect,source}(#4075 step 3 前置,维护者 2026-08-06 已裁决) objectstack#5970);合并后 objectui 按 spec 派生类型收敛两键、删索引签名、删DeclaredActionsBar.tsx的(action as any).disabled强转(裁决全文见 2026-08-06 PM 裁决评论,分析见验收评论 5156971051)。- 本单曾两次被按下文 staged plan 误派 step 1(2026-08-02、2026-08-06,均由 premise 检查拦下)——下文正文是立单时的计划,不再是现状,以本状态头为准。
Blocked-by: objectstack-ai/objectstack#5970
Split out of objectstack-ai/objectstack#3856 / objectui#2990, which hit this directly and documented it rather than working around it silently.
The mechanism
ActionDef (packages/core/src/actions/ActionRunner.ts:277, interface at :78) ends with:
/** Any additional properties */
[key: string]: any;at ActionRunner.ts:185. So ActionDef accepts any key of any type. Concretely, in objectui#2990:
- deleting
ActionDef.executeproduced zero compile errors, even though the field had just been removed from the interface; - stale metadata still authoring
execute: 'markDone'continues to type-check today; - the same deletion on
@object-ui/types'ActionSchema— which has no index signature — correctly producedTS2353at the authoring site.
That asymmetry is the whole issue: one of the two readers is capable of catching a retired key, the other is structurally incapable.
Because the compiler couldn't help, objectui#2990 had to add a runtime branch to keep the removal audible — executeScript now returns the rename prescription when it finds only the retired key, rather than a bare "No script provided". That branch exists solely to compensate for this index signature. Binding no handler silently is the objectstack-ai/objectstack#2169 "Mark Done does nothing" shape, so the compensation was necessary — but it is compensation.
Not the same as ActionContext's
ActionContext has an index signature too (ActionRunner.ts:53) and it should keep it: it is a runtime data bag whose keys are genuinely open (data, record, pageVariables, user, plus whatever a host passes). ActionDef is the opposite — it is a declared metadata contract that mirrors @objectstack/spec's ActionSchema. An open key set on a contract is what lets a typo (targt, exectue) and a tombstoned key (execute) both sail through to a runner that then silently does nothing.
Why it can't just be deleted
The runner deliberately accepts shapes wider than current spec, and ActionDef already enumerates a lot of them explicitly (actionType "legacy action type field", api as string | ApiConfig, onClick "legacy", navigate?: any). Some callers likely pass keys not declared anywhere. A bare removal would surface as a wall of TS2353 across components / plugin-* / host apps, some legitimate.
Staged narrowing is the realistic path:
- Inventory what is actually passed — grep the repo's
ActionDefconstruction sites and anyaction[...]dynamic reads, and add a dev-mode warning on unrecognized keys (the "declared, warned, lintable shim" form AGENTS.md PD [WIP] Enhance UI components for forms and layouts #12 asks for, instead of a bare open type). - Promote every legitimate key found into an explicit optional field, keeping the legacy ones marked
@deprecated. - Remove the index signature. At that point
tsccatches both typos and retired spec keys, and Security: attachment blob download has no record-level RLS — bytes served by fileId with only a committed-status check (IDOR) objectstack#2990's runtime prescription branch can eventually retire with it.
Step 1 is independently useful and non-breaking: it turns an invisible failure into a warning without changing types.
Relation to objectstack-ai/objectstack#3903
objectstack-ai/objectstack#3903 established that the spec contract stops at authored source — stored sys_metadata rows are rehydrated unparsed. This is the same "declared ≠ enforced" pattern on the type channel rather than the parse channel: @object-ui/core is the reader that consumes those unparsed rows (runner(action)), and its type accepts anything they contain. The two issues bound the same gap from opposite ends; fixing objectstack-ai/objectstack#3903 alone would still leave a reader that cannot describe what it accepts.
Refs objectstack-ai/objectstack#3856 (objectui#2990), objectstack-ai/objectstack#3903, objectstack-ai/objectstack#2169, objectstack-ai/objectstack#3855, objectstack-ai/objectstack#3883, AGENTS.md PD #12.
Activity
yinlianghui commentedon Aug 11, 2026
CLAIM — step 3, unblocked (objectui whole-repo seat PM, session
session_017Qqyix2QcnpUC9XeYVDzx3) — dispatching now. State reconciliation per the status header: steps 1+2 landed (#3032, #3190); theBlocked-by: objectstack#5970restart condition has FIRED — that card closed completed 2026-08-07 (PR objectstack#6450, merged pre-rc.6), so the installed spec already carries the unifiedboolean | string(CEL) | {dialect,source}shape for both keys. This dispatch executes the 2026-08-06 maintainer ruling's objectui half.claude/issue-4046-actiondef-index-signature, worktreeobjectui-issue-4046(M container)visible/disabledconverge to the spec-derived shape, the[key: string]: anyindex signature deletes,DeclaredActionsBar.tsx's(action as any).disabledcast deletes.ActionContext's index signature STAYS (the card's own distinction — a runtime data bag, not a declared contract).usePredicateRecordContext#4080's agent is inDeclaredActionsBar.tsxNOW — first edit waits until its PR is onorigin/main(git-layer polling).paramsreads are dead vocabulary — the url interpolation scope andparams.newTabwere retired by the spec ruling on objectstack#6828 (finding) #4097's ruling B): the in-code documentation block atinterpolateTarget's non-array branch — INTERNAL runtime value-bag channel, not an authoring surface, citing fix(data-objectstack): lower rule-shaped filter arrays on aggregate()'s analytics path #6828 + the ruling, flagging the spec-loosening hazard.Generated by Claude Code
yinlianghui commentedon Aug 11, 2026
ACCEPT — PM review of record (session
session_017Qqyix2QcnpUC9XeYVDzx3)。PR #4285 执行了裁定的 step 3,验收要点:
visible/disabled各自单点派生自 spec(SpecActionInput[...]),step 2 的本地| boolean因 rc.6 spec 已收编而删除——复述会变成第二份契约。condition不动(objectui 方言,裁决范围外)。execute活墓碑不声明而是从显式无类型视图读——重新声明等于给退役键复活,处理正确。paths时工作区派生类型经 dist 降解为any——移 pin 至 typetests project(在依赖图内)并本地复现验证。如实披露,加分。paramsreads are dead vocabulary — the url interpolation scope andparams.newTabwere retired by the spec ruling on objectstack#6828 (finding) #4097 rider 落位:interpolateTarget 非数组分支(~:1612),注释性 ruling-B 记录 + 上游拒绝 pin + 两个本地守卫,零行为变化。fix(core,app-shell): close ActionDef — delete the index signature, converge visible/disabled on the spec (#4046) #4285 落地后 ActionRunner's non-arrayparamsreads are dead vocabulary — the url interpolation scope andparams.newTabwere retired by the spec ruling on objectstack#6828 (finding) #4097 即可关闭(retire(core): drop the legacyparams.newTabfallback on url actions (#4097) #4262 已落退役半场)。范围外发现处置:#4281 入队(它如实界定了本卡自身主张的边界——4 个转发点中 2 个因 spread 关闭了多余属性检查,守卫只在一半生效);#4282 留 finding 池。
Flipping ready + auto-merge。
Generated by Claude Code
fix(core,app-shell): close ActionDef — delete the index signature, co…