Skip to content

参数门拒绝「差一个下划线」的内建键时无提示:selectedIds 报 Unknown param,不指向 _selectedIds(#5568 验证副产品) #5622

Closed

Description

@baozhoutao

观察类发现,来自 #5568 的验证过程。当前拒绝行为是正确的,这里只是 DX 打磨,不是缺陷。

现象

ADR-0104 D2 参数门(validateActionParams,packages/spec/src/ui/action-params.zod.ts)对未声明键统一报:

Invalid action params: Unknown action param "selectedIds" — not declared on this action

当这个键与某个内建键(ACTION_PARAM_BUILTIN_KEYS = recordId / objectName / _selectedIds)仅差一个前导下划线时,消息不提示那个内建键存在。调用方看到的是「这个键没声明」,自然的下一步是去 action 上把它声明成 param —— 而 _selectedIds 恰恰不能被声明成 param(它是渲染器注入的内建键),于是这条路走不通,人就转去猜别的形状。

代价(已发生一次)

#5568 的报告者正是卡在这里:三条证据中 B 条就是 params.selectedIds,拿到上面这条消息后判定「选择集不属于 declared params」,进而得出「REST 无任何合法形状携带 selectedIds」的结论并开了平台单。实际声明通道 params._selectedIds 在 rc.2 上是通的(#5568 已用真机端到端证据证伪该前提)。一条 near-miss 提示本可以在第一次调用就终结这个问题。

建议

validateActionParams 产出 unknown_field 时,若 '_' + key 或 key.replace(/^_/, '') 命中 allow 集合,在消息尾部追加一句指路,例如:

Unknown action param "selectedIds" — not declared on this action.
Did you mean the built-in "_selectedIds"? It is injected by an aggregate
bulk dispatch and read as ctx.params._selectedIds; it is never declared.

纯消息层改动,判定逻辑不变(仍然拒绝),因此不改变任何契约。

备注

  • 不是「另造一条 selectedIds 通道」—— 契约只有一条 params._selectedIds,这里只让拒绝消息指向它。
  • 相邻先例:recordId / objectName 同样是内建键,同样有下划线/无下划线的近似拼写风险。
  • 查重:已按关键词与 action-params 路径检索 open + closed,未见同类单。

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 5, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    domain:specDomain lane — seat card indexed by label:pm:seat
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 5, 2026
  3. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    发现分诊(#4949 纪律):晋级 pm:queue + domain:spec(摘 finding)。

    一行理由:纯消息层的 near-miss 指路(判定逻辑与契约不变),已实际烧掉一次完整调查(#5568 的错误前提正是从这条无指路的拒绝消息长出来的),修法具体 —— 落点 packages/spec/src/ui/action-params.zod.ts 的 validateActionParams,对 ACTION_PARAM_BUILTIN_KEYS 做前导下划线双向比对即可。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  4. added and removed
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 7, 2026
  5. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    Triage: domain:spec → domain:spec-surface (seat #6298; existing-stock migration pass, maintainer-instructed 2026-08-07). Error guidance, in the seat's declared face: selectedIds is rejected before and after — the fix only adds the near-miss hint pointing at _selectedIds, so the accepted parameter set does not move.


    Generated by Claude Code

  6. self-assigned this
    on Aug 7, 2026
  7. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    Claim: PM loop round 2 (domain:spec-surface seat #6298)
    Session: session_01JTSZAjgtL3oR6YcpNDhW3T
    Branch: claude/issue-5622-param-near-miss-hint
    Worktree: objectstack-issue-5622
    Domain: domain:spec-surface
    File surface: packages/spec/src/ui/action-params.zod.ts (message construction in validateActionParams only — the rejection verdict itself is untouched), its co-located test file, .changeset/*.md (stop on breach; explain in the report)
    Serial constraints cleared: none — no in-flight PR or claim touches action-params.zod.ts (in-flight sweep #6243's file list checked; #4415 is flow-family). ACTION_PARAM_BUILTIN_KEYS verified present on origin/main.
    Container weight: S, mode:subagent shared container.


    Generated by Claude Code

  8. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    Review: ACCEPT on substance — PR #6368 (landing gated on CI convergence)

    Reviewed by the domain:spec-surface seat (#6298, session session_01JTSZAjgtL3oR6YcpNDhW3T) against the diff, not the report.

    Scope — 3 files, all within the declared claim surface: .changeset/action-param-builtin-near-miss-hint.md (@objectstack/spec: patch), action-params.zod.ts (+71/−1), action-params.test.ts (+139). No content/docs/releases/ edits, nothing unrelated.

    The verdict does not move, and it is pinned. The only production change is appending ${builtinNearMissHint(key, allow)} to the existing unknown_field message; the issues.push and its code are otherwise byte-identical. A dedicated case (moves the ACCEPTED SET by nothing) asserts selectedIds still rejects, _selectedIds still accepts, and ACTION_PARAM_BUILTIN_KEYS is unchanged. No second acceptance channel was opened.

    The dev overrode the issue's proposed copy, on measurement, and was right to

    The issue prescribed one hint sentence — "injected by an aggregate bulk dispatch". That is true of _selectedIds and false of the other two: recordId / objectName are merged in server-side by the dispatcher. The dev traced all three producers to file:line and made the origin sentence per-key.

    The part worth recording is what that verification prevented: for recordId, a generic "send params.recordId" hint would have been actively wrong, because the dispatcher's params: { ...reqParams, recordId, objectName } spread overwrites whatever the caller put under that key — with undefined when neither route nor body carries a record id. A hint that confidently prescribes a broken call is worse than the terse message this card set out to improve; a card about fixing misleading copy would have shipped new misleading copy. Keys arriving via a custom builtinKeys override correctly fall back to a generic sentence that is true by that option's own definition.

    Test discipline meets the clause it was dispatched under. Every case asserts code and message content — never bare failure, which here is zero information since the key is refused on both sides of the fix. The negative case pins the untouched message byte for byte, and a six-variant case (selected_ids, selectedIDs, recordID, …) pins that the match is one leading underscore and not a similarity score. Reverse verification was predicted before it was run and landed exactly as predicted: stubbing builtinNearMissHint to '' turns 4 message cases red while every codes(...) assertion stays green.

    Called out for the record: the honest companion labelling. The dev flags that "an ordinary unknown key keeps today's message" is a companion, not a pin of this change — green on both sides of the revert by construction — and then argues for keeping it anyway, because it is the one assertion in the file that would catch a future loosening into fuzzy matching. Correctly distinguishing a test that proves your change from a test that guards a different regression, and saying so in the diff rather than letting a green count imply coverage it does not have, is the standard this lane wants.

    Checked independently: the key.replace(/^_/, '') candidate collapsing to key for a non-underscore key cannot produce a spurious self-hint — the loop only reaches this push for keys allow does not hold, so allow.has(key) is false there. The reasoning in the JSDoc is sound.

    Landing

    ⛔ Not flipped to ready — ESLint and TypeScript Type Check are both in_progress, and those two jobs carry the check:* gate family. ACCEPT requires each to reach conclusion: success, not merely "no failure yet". Flip check is armed; ready + merge queue follow once both conclude green.

    Landing order in this batch: #5622 (this, first) → #5767 → #5783 → #5955. This PR has no generated-artifact exposure, so it is not subject to the four-step resync.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions