Skip to content

fix(pipeline): violation-precedence 去重键在 run.at 缺失时会把不同 run 的违规合并 #461

Closed

Description

@modusensus

现象

src/pipeline.js 的 suggestRetirements 里,前置违规的去重键是:

const vk = `${run.at ?? ""}|${step.id}|${need}`;

两条 run.at 都缺失(null/undefined)的 run,即使步骤证据完全独立,同一 (step, need) 组合的第二条违规会被键冲突吞掉——只报一条,because 里还写着第一条的时间。

现网风险:低

写入侧 normalizeRun(src/pipeline-runs.js)已强制 run.at 必填(缺失直接抛,不做补默认),合法账本里 at 恒存在。这是防御深度问题:只有读侧被独立喂入非法形状(或未来写入契约放宽)时才会触发。

发现来源:CodeRabbit 对 PR #458 的评审意见,已用最小复现脚本验证键冲突真实存在。

建议修法

为每条 run 保留数组索引,run.at 缺失时把索引进去重键(如 `${run.at ?? `#${i}`}|...`),at 存在时行为不变。补一条读侧回归测试锁住。

Activity

  1. modusensus commented on Oct 20, 2026

    @modusensus
    MemberAuthor

    现状(2026-10-21 对照 main 实测)

    代码里一行没动,问题仍在。

    dsh-mneme/src/pipeline.js:127:

    const vk = `${run.at ?? ""}|${step.id}|${need}`;

    run.at 缺失时该键塌缩成 |step|need,不同 run 的同类违规会被合并——去重本意是「同一轮里同一条违规只报一次」,塌缩后变成「所有无时间戳的轮次共享一条」。现网风险低(run.at 正常都有值),属于防御深度缺口。

    还缺什么

    1. 缺失 run.at 时用 run.id 或进程内单调序号兜底,而不是空串;
    2. 加一条回归用例:两个 run.at 均为 undefined 的 run 产生同 step.id 违规,断言产出两条。

    接手建议

    三行以内可修,test/pipeline-activities.test.js 里已有同类断言可照抄。这条不需要设计讨论。

  2. z2Ace0107 commented on Oct 23, 2026

    @z2Ace0107
    Contributor

    想接这条,计划按你给的缺口清单来:

    1. src/pipeline.js:127 的去重键在 run.at 缺失时用 run.id 兜底(run.id 也缺就退到进程内单调序号),不用空串;
    2. test/pipeline-activities.test.js 补一条回归:两个 run.at 均为 undefined 的 run 产生同 step.id 违规,断言产出两条。

    at 存在时行为逐字节不变,纯加固,不动写入侧契约。

    说明一下我的安排:我打算把 #383(/context 档位信号)和 #421(distill_records 的 HTTP 出口)也认下来,按由易到难串行做完——这条最先,#383 次之,#421 最后。三个都会在两周内开出 PR,不让认领过期。有哪个你更想自己收,随时说。

  3. modusensus commented on Nov 2, 2026

    @modusensus
    MemberAuthor

    已在 0.9.5 交付,本 issue 关闭。

    落点(两步)

    第一步 PR #507 f7dacac:dsh-mneme/src/pipeline.js:147 的键改成用位置索引兜底——${runKey}|${step.id}|${need},runKey 在 run.at 缺失时取 #${i}。同时把「缺失」的判定从 ?? "" 扩到含空串:run.at = "" 以前会被当成合法时间戳参与拼键。

    第二步 PR #513 a27c4ef:补的是 #507 合并后复盘指出的遗漏——匿名指纹原先只取 step 的 id,于是两条都缺 run.at、其余全同、只在 steps[0].at 上不同的 run 是两次不同的观测却共键。那是同一类键塌缩的反面形态(该分开的被并了),所以指纹补进 step.at 等结构化字段,并把 null 与「字段缺失」区分开写进 JSON。

    一处与建议不同的取舍

    你建议用 run.id 兜底。实际做的是位置索引——因为 run 对象根本没有 id 字段:normalizeRun(dsh-mneme/src/pipeline-runs.js)只产出 { at, by, triggeredBy, steps }。位置索引是这里唯一现成的判别项。

    回归测试两侧都补了:两个 run.at 均为 undefined 的 run 产生同 step.id 违规时产出两条;at 存在时行为逐字节不变。

    @z2Ace0107 多谢。这条的价值不在于修好了什么现网问题(normalizeRun 已经强制 at 必填,写入侧进不来这种形状),而在于你说清了它是防御深度问题——读侧被独立喂入非法形状时不该静默塌缩。这种「按现网风险算很低、但按契约算必须修」的判断,比一堆高危报告都难得。

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions