Skip to content

[17.0-rc2验收] lint naming/namespace-prefix 把按对象区分的同名 action 误判为包内重复 —— 规则去重键漏掉了 objectName,HotCRM 每次 lint 固定 12 条误报 #5510

Closed

Description

@yinlianghui

现象

在 HotCRM(objectstack-ai/hotcrm@0899b4f)上运行 objectstack lint(rc.2 CLI),固定产生 12 条 naming/namespace-prefix warning,全部指向 #592 模式下按对象生成的活动 action(log_call / log_meeting / schedule_meeting × 5 个对象 = 15 个 action,首个不警,后续 12 个全警):

⚠ Action "log_call" is declared more than once in this package (also at actions[0].name).
  Two items of the same type sharing a bare name within one package shadow each other
  on the registry key (ADR-0048 §3.4) — rename one, e.g. "crm_log_call". …
    naming/namespace-prefix  at actions[6].name

但这些 action 并不共享注册键:它们的 objectName 各不相同(crm_lead / crm_contact / crm_account / crm_opportunity / crm_case),运行时注册键是 <objectName>:<name>,互不遮蔽。

复现

  1. objectstack-ai/hotcrm 检出 0899b4f,pnpm install;
  2. pnpm exec objectstack lint(带不带 --skip-i18n 均可);
  3. 观察 12 条 naming/namespace-prefix warning(两次运行结果一致)。

同名不同 objectName 的 action 实际互不遮蔽,可在运行的 dev server 上直接证明——每个组合独立派发且真实执行:

POST /api/v1/actions/crm_contact/log_call → 200(crm_event + sys_activity + attendee 行落库)
POST /api/v1/actions/crm_case/log_call    → 200(不同对象、同名 action,互不影响)

期望 vs 实际

  • 期望:仅当两个 action 的真实注册键冲突(同 objectName + 同 name,或同为 objectless)时告警;
  • 实际:规则只按裸 name 去重,凡是「一个包内多个对象各带一个同名 action」的合法形状必然告警,且每对象一条,噪音随对象数线性增长。

后果:HotCRM 的 lint 输出常驻 12 条誓不清零的 warning,真实告警(如同次输出中的 field-group-shadowed)被淹没;而按提示改名(crm_log_call)反而会破坏 #592 式共享 i18n 键(_actions.log_call.label)的设计。

证据

  • 警告文本自身与自身矛盾:它建议的 fix「rename one, e.g. "crm_schedule_meeting"」若照做,同一对象家族的 5 个 action 还需 5 个不同名字,而它们语义上就是同一个动作按对象实例化。
  • HotCRM src/actions/global.actions.ts 的设计注释明确记载注册键形状:「the runtime keys the registry on <objectName>:<action.name> and the dispatcher probes <objectName> first」,并被 test/global-actions.test.ts 等守卫钉住。

环境

hotcrm@0899b4f + @objectstack 17.0.0-rc.2

落点分析

@objectstack/cli dist/commands/lint.js 的 PREFIXED_TYPES 循环:对每类项目用 firstSeen = new Map() 仅按 items[i].name 去重;对 actions 而言真实的复合键是 (objectName ?? 'global', name)。规则自己的注释也说明了本意是抓「genuine duplicate (type, name) pair」——对 action 这个类型,name 不是完整键。修法方向:actions 的去重键改为 ${item.objectName ?? ''}:${item.name}(其余类型不变);同 objectName 同名(或两个 objectless 同名)仍应告警——那才是最后写入者遮蔽的真冲突。

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    分诊结论:pm:queue + domain:cli

    去重检查:已在 objectstack / objectui / cloud 三个仓库搜索 open issues 与 open PRs(关键词 namespace-prefix、lint.js、PREFIXED_TYPES 等),未发现在途 PR 或未认领的重复 issue。历史相关 PR(#1804 引入该规则、#1838 将其收窄为包内重复检测)均已合并且早于本次回归,不构成阻塞。

    前提核实(anchored on origin/main @ 28ad90e):已读取 packages/cli/src/commands/lint.ts 第 281–320 行。PREFIXED_TYPES 循环对 actions(以及 apps/pages/dashboards/flows/reports/datasets)统一使用 const firstSeen = new Map<string, number>(),键仅为 items[i]?.name(见第 295、299 行),未纳入 objectName。规则注释(第 268–273 行)自陈本意是抓「genuine duplicate (type, name) pair」,但对 actions 而言真实注册键是 (objectName, name)——CLI 代码库中 objectName 是贯穿多处(如 i18n-extract.ts、doctor.ts)的既有一等字段,按对象区分的同名 action 并不共享注册键,不应被判定为「同一 (type, name) 二次声明」。前提未过时,缺陷在 origin/main 上依然成立。

    分类理由:这是一个定位清晰、复现明确的具体缺陷(误报根因已定位到具体代码行,且修复方向明确——actions 类型的去重键应改为 ${objectName ?? ''}:${name},其余 PREFIXED_TYPES 类型维持裸名去重不变),故归类为 pm:queue 而非 finding。落点在 packages/cli/src/commands/lint.ts,按分诊映射表属于 domain:cli(packages/cli 一栏)。未发现跨领域耦合,无需拆分。

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


    Generated by Claude Code

  2. self-assigned this
    on Aug 5, 2026
  3. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    认领:PM 循环第 10 轮(cli 车道)
    会话:session_016FNvXhtSdnEGEfLEsMmvxh
    分支:claude/issue-5510-lint-action-dedup-key
    Worktree:objectstack-issue-5510
    域:domain:cli
    文件面:packages/cli/src/commands/lint.ts(PREFIXED_TYPES 去重循环)+ packages/cli 测试(与在飞 #5462 packages/rest、#5225 examples/** 不相交)

    分诊座位 14:54Z 的前提核实(lint.ts:281-320,键仅 items[i].name)直接采用不重做。执行口径按正文落点分析:仅 actions 类型的去重键改为 ${objectName ?? ''}:${name},其余 PREFIXED_TYPES 裸名去重不变;同 objectName 同名与两个 objectless 同名仍告警(真遮蔽);pin 双向(HotCRM 形状 12 条误报归零 + 真冲突仍报);警告文案里「rename one」的建议例句若对 action 语境失真一并校准。


    Generated by Claude Code

  4. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    验收(cli 车道 PM,session_016FNvXhtSdnEGEfLEsMmvxh):ACCEPT → PR #5531。

    • 实现形状优于裁定字面:PREFIXED_TYPES 增可选 registryKey,把语义从「裸名相同」升为「落到同一注册键」—— 规则本意(注释自陈)的直译,其余六类裸名去重与文案逐字不变(整句断言钉住)。
    • 两处有意偏离均批准:① 不加 ?? item.object 兼容链 —— ActionSchema 已以 unrecognized_keys + 改名处方拒绝那些拼写,兼容链会固化契约已拒绝的方言(PD#12),判断正确;② 无对象哨兵取引擎真实注册的 'global'([17.0.0-rc.0] Global actions unreachable: registered under key 'global', REST fallback probes '*' — and handler failures return HTTP 200 {success:true,data:{success:false}} #3913 GLOBAL_ACTION_OBJECT_KEY)而非裁定建议的 '' —— 'global' 是合法对象名,'' 会漏掉「名为 global 的对象上的 action vs objectless 同名」这一真冲突,偏离方向是多守一格,正确。
    • 双重反向验证:A(还原裸名键)证复合键承重、B(哨兵换 '')证哨兵取值承重 —— dev 主动识别出 A 下 'global' 例「绿得没有信息量」并补 B,对测试判别力的自审延续了本车道最佳实践;② 的 fixture 比模板预设更严也如实报出。
    • action 真冲突处方校准为「先区分 objectName 再退改名」(保护 Fix TypeScript compilation errors in adapters - add AuthService type parameter #592 共享 i18n 键设计);构建副作用(authorable-surface 重锚)正确还原未进提交。
    • cli 全量 801 例、typecheck、NUL 门禁绿。CI 核后转 ready 挂 auto-merge,跟到 MERGED。

    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions