atlas-project-harness 0.2.0-beta.25 → 0.2.0-beta.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/DESIGN.md +6 -1
  3. package/README.md +4 -2
  4. package/_support/__pycache__/review_policy.cpython-312.pyc +0 -0
  5. package/_support/review_policy.py +247 -15
  6. package/install.sh +14 -0
  7. package/package.json +1 -1
  8. package/scripts/__pycache__/ablation.cpython-312.pyc +0 -0
  9. package/scripts/__pycache__/atlas_check.cpython-312.pyc +0 -0
  10. package/scripts/__pycache__/validate_review_report.cpython-312.pyc +0 -0
  11. package/scripts/ablation.py +915 -0
  12. package/scripts/atlas_check.py +29 -0
  13. package/scripts/cli_review_convert.py +192 -19
  14. package/scripts/review_closure.py +431 -0
  15. package/scripts/validate_review_report.py +30 -5
  16. package/shared/closeout.md +3 -2
  17. package/shared/gates.md +28 -9
  18. package/shared/independent-review.md +26 -6
  19. package/tests/__pycache__/test_ablation.cpython-312-pytest-9.1.1.pyc +0 -0
  20. package/tests/__pycache__/test_atlas_check.cpython-312-pytest-9.1.1.pyc +0 -0
  21. package/tests/__pycache__/test_cli_review_convert.cpython-312-pytest-9.1.1.pyc +0 -0
  22. package/tests/__pycache__/test_graph_backend.cpython-312-pytest-9.1.1.pyc +0 -0
  23. package/tests/__pycache__/test_review_closure.cpython-312-pytest-9.1.1.pyc +0 -0
  24. package/tests/__pycache__/test_review_report.cpython-312-pytest-9.1.1.pyc +0 -0
  25. package/tests/__pycache__/test_validate_ac_alignment.cpython-312-pytest-9.1.1.pyc +0 -0
  26. package/tests/test_ablation.py +430 -0
  27. package/tests/test_atlas_check.py +196 -0
  28. package/tests/test_cli_review_convert.py +520 -5
  29. package/tests/test_graph_backend.py +1 -1
  30. package/tests/test_review_closure.py +678 -0
  31. package/tests/test_review_report.py +284 -1
  32. package/tests/test_validate_ac_alignment.py +53 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,58 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## [v0.2.0-beta.26] - 2026-11-02
8
+
9
+ ### 🧭 签字与取证必须是非空字符串(RFC-20261101-03)
10
+
11
+ * **收紧**:`disposition_by` / `disposition_evidence` / `resolves[].by` / `resolves[].evidence` / `verification.by` 此前经 `str()` 判非空 ⇒ `by: 123`、`evidence: ["x"]` 算「已签字取证」;而 Critical 脱断与台账判 `resolved` 都吃这一条。新增单源 `review_policy.signed_str` / `has_signed_disposition`,五处调用改齐。**合并前评审补第二批**:`--resolves` 顶层**批级缺省签字人**同判据(`{"by": 123}` 曾被 `str()` 洗成 `"123"` 并继承进每一条 ⇒ Critical 被数字签名关掉,端到端复现后修掉)、`verification.evidence`、`escalation.approved_by` 一并走 `signed_str`。
12
+ * **形状层**:`--resolves` 里 `by`/`evidence` 给了就必须是字符串 ⇒ 转换器拒绝产出、中间轮**逐条取舍**(坏的那条不作关闭证据 + warnings,同轮合法声明照常承重;整批没收=假失忆)、**重复指纹整组作废**(同一 fingerprint 多条冲突声明不按书写顺序挑赢家,其他指纹照常承重)、最新轮 exit 2。
13
+ * **零回扫**:实测全仓 + 真实四轮语料的非字符串签名命中 0 处,语料台账 `PASS resolved 16` 与形态门 `PASS` 均不变。
14
+ * **测试/设计**:`tests/test_review_closure.py` 37 → 42(M17 / M98 / M102)、`tests/test_review_report.py` 41 → 45(M48 / M99 / M100 / M101)、`tests/test_cli_review_convert.py` 49 → 51(M49 / M51);第二批五枚新钉四段协议 5/5 按预期红并逐字节还原,第三轮复核补 M102(重复指纹整组作废)单钉红/还原复绿;`DESIGN.md` D181;契约 §3/§11、`shared/gates.md`、`docs/map` 快照。
15
+
16
+ ### 🧭 消融事务工具 + 消融残留门(RFC-20261101-04)
17
+
18
+ * **新增**:`scripts/ablation.py`(begin/restore/status;项目外 per-root 状态 + pending/active + 锁/原子写;全路径 preflight、备份校验、smoke 后复算);`atlas_check` 内置「消融残留」门(无 active=N/A;健康 active=ok+剩余,异常=WARN;出生 WARN、永不 FAIL)。
19
+ * **范围**:P3-a 只支持现有普通文件的修改/删除;P3-b 已补 run / install.sh --update 互锁 / host F5 WARN / --state 状态面。
20
+ * **测试/设计**:`tests/test_ablation.py` 23 条(M164)、`tests/test_atlas_check.py` 49 → 60 条(M163);`install.sh` 自检 62 → 63;P3-b 的 run/state/host/install 互锁均有用例;`DESIGN.md` D182;`docs/map` 计数同步。
21
+
22
+ ### 🧭 留出验证自述盲区硬化(D156 补强)
23
+
24
+ * **触发 (b) 取证**:作者侧 §4.5 / closeout 定档必须粘贴 `git diff --name-only` 与 `scripts/closeout_derive.py` 的原始输出;评审侧 §9 必须自行重跑并留原文,不得只引作者自述。
25
+ * **不新增机器判据**:语义映射不可机判,维持 `shared/gates.md` §2.5 流程门;本次只加固取证形状,不建硬门。
26
+
27
+ ### 🧭 非门约束登记 + C-03 基线不写钉(RFC-20261101-01)
28
+
29
+ * **新增(非门)**:`shared/gates.md` §4.1 约束登记表,8 条代码/设计孤岛约束;不参与判定、不计入门总数、不进 `atlas_check`;不新建 shared 文件。
30
+ * **测试**:`tests/test_validate_ac_alignment.py` 62 → 63;新增 FAIL / parse_errors / PASS 三态基线不写钉;变异 M162a/M162b。
31
+ * **编号卫生**:graph 非 git 变异 M59 → M161,消除与装配版本 M59 重号。
32
+ * **同步面**:`docs/map` 行数 153 → 170、`atlas-map.html` 计数;`shared/gates.md` AC 行 62 → 63。
33
+
34
+ ### 🧭 实例⑤审查报告 coverage 权威 + CLI 失败面(Batch A/B,RFC-20261031-01)
35
+
36
+ * **修复**:`cli_review_convert.py` 以 CLI `manifest.coverage.completed[]` 为实审 M 权威(无 coverage 才回退 `summary.files_reviewed`);`summary` 与 coverage 冲突以 coverage 为准并写入 `cli_warnings`;报告新增 `cli_status` / `failed_files` / `waived_files` / `reused_files` / `cli_warnings`。
37
+ * **状态**:`partial` / `failed` 或 `failed_files` 非空 ⇒ 状态下限 `WARN`(不降级阻断性 `Critical`);`skipped` / `unknown` 只记录;coverage 缺 `completed[]` 拒绝产出。
38
+ * **校验**:`validate_review_report.py` 复算 CLI WARN 下限并新增 CLI 状态行;`_support/review_policy.py` 单源 `cli_warn_floor` 与字段形态校验。
39
+ * **测试**:`tests/test_cli_review_convert.py` 12 → 25;`tests/test_review_report.py` 18 → 27。
40
+ * **契约/设计**:`shared/independent-review.md` §3/§10.2、`docs/concepts/review.md` §3、`docs/gates/iron-gates.md` §8、`shared/gates.md`、`DESIGN.md` D178。
41
+ * **Batch B(delta v3)**:`delta_only` 新报告使用 `schema_version=3`,必须 `head_sha` + 非空 `reviewed_files`(coverage completed / 显式 `--reviewed-files`);`selected_files`/数量/成员/target 对账只 WARN;v2 delta 缺 `head_sha` 只 WARN(兼容);clean delta 不再误红(2 completed + 1 reused ⇒ 如实 M<N WARN)。测试 `test_cli_review_convert.py` 25 → 41、`test_review_report.py` 27 → 39。
42
+
43
+ ### 🧭 跨轮 finding 闭环台账(D5,RFC-20261031-02)
44
+
45
+ * **新增(观察面)**:`scripts/review_closure.py` WARN observer——读多轮 `findings/<unit>.r*.json` + 最新 `review.json.resolves`;prior open Critical/Major(`pre_existing` 除外)未 `carried`/`resolved` ⇒ WARN;<2 轮 SKIP;不进 `atlas_check`。
46
+ * **是否算关闭按上一轮级别从严**:prior `Critical` 在新一轮降级成 `Major` 再挂 `intentional`/`ignored`(无签字取证)不免责,仍按 `carried` 跟踪,且降级本身记 `level_downgraded` WARN;`findings/` 出现非本 unit 的孤儿文件 ⇒ WARN(跨轮改 `--unit` 名会让闭环义务凭空清零)。
47
+ * **真实报告**:r1/r2 首跑 **12 条 prior Major unaccounted**;r2-v3 单轮目录 SKIP。
48
+ * **测试/设计**:`tests/test_review_closure.py` 该批 19 条(经 `43dca7f` 与 `RFC-20261101-02` 扩至 34 条、M1–M13);`DESIGN.md` D179(D180 续);契约 `shared/independent-review.md` §11、`shared/closeout.md` 步骤 10、`shared/gates.md` §4。
49
+
50
+ ### 🧭 跨轮关闭持久化 + 升级可见性 + scan 通道词表(D5-a/b/c,RFC-20261101-02)
51
+
52
+ * **修复(D5-a)**:`review_closure.py` 的闭合改按**该指纹最后一次证据**判——任一中间轮正式关闭(`resolves` 声明或同指纹带取证重报)即记 `resolved`(`via` 记 `resolves@rN` / `re-reported-closed@rN`),不再要求每轮重贴历史声明;更晚一轮又以 open 出现则作废,声明轮次必须晚于最后一次 open。真实四轮语料:修前 `WARN unaccounted 13` ⇒ 修后 `PASS resolved 16`。
53
+ * **可见性(D5-b)**:README 渲染 `escalation`(round ≥ 4 收敛上限的升级复议)与跨轮关闭声明条数,不再只留在 `review.json`。
54
+ * **scan 通道(D5-c)**:`CLI_STATUSES` 加 `success`(`ocr scan` 完成态,不再假记 `unknown`);无 `manifest.coverage` 时落 `cli_warnings` 点名「实审数取自评、可被高估」(只点名,不改状态)。
55
+ * **入口**:`cli_review_convert.py --resolves <file>`(`[...]` 或 `{by, resolves}`,条目缺 `by` 继承顶层缺省);`review_closure.py --emit-skeleton <file>` 输出待填义务骨架。
56
+ * **单源**:`resolves` 形状判据下沉 `_support/review_policy.resolves_shape_errors`(转换器与闭环共用)。
57
+ * **测试/设计**:`tests/test_review_closure.py` 19 → 34(M6–M13)、`tests/test_cli_review_convert.py` 41 → 49(M42–M46)、`tests/test_review_report.py` 39 → 41(M45 编号属转换器 / M47);`DESIGN.md` D180;契约 §11 / closeout 步骤 10 / gates §4 / docs+map 快照。
58
+
7
59
  ## [v0.2.0-beta.25] - 2026-10-31
8
60
 
9
61
  ### 🧭 前端复用优先:程序契约 + Trellis 面包屑注入(D177)
package/DESIGN.md CHANGED
@@ -628,7 +628,7 @@ adapters:
628
628
  | D153 | **后端通道 AC × 覆盖维度对账 = PRD 第 5 列 + `dims=` + WARN 门**(2026-10-26;`proposals/RFC-20261026-02` v4.1 / Batch A;独立评审 `REVIEW-20261026-05`「修后通过」;`validators/validate_ac_alignment.py`、`_support/ac_coverage.py`、`scripts/atlas_check.py`、`scripts/atlas_verify.py`) | ① **病灶**:后端/全栈 AC 可声明覆盖维度,但 `validate_ac_alignment` 只收 AC ID、不读维度;E2E 门只读第 4 列 ⇒ 后端维度声明无对账面。② **定案**:PRD §五 新增可选列 `后端适用覆盖维度`(按表头名定位;3/4 列 N/A;错位/备注含维度形态 ⇒ WARN;逐 PRD/逐表聚合;模板占位留空/N/A);测试侧 `@pytest.mark.ac(..., dims=[...])`(精确 C01-C14;只作用于 marker AC;docstring 不参与;多 AC/空/非字面量/未知/孤儿 ⇒ WARN);后端门只 WARN(缺维度/仅弱证据/零测试 ⇒ WARN);通道 = backend/fullstack app + `backend_test` + pytest + test_dir,多后端/不可用 ⇒ N/A;`Result` 加 N/A(早退 N/A),`atlas_check` 子项 `[N/A ]` 进 detail、整门 N/A 才行级 N/A,`atlas_verify._parse_rows` 接受 N/A;`_support/ac_coverage.py` 为维度解析单源(E2E 纯抽取,D130 行为不变);`backend_test.mode` 只管 AC 挂载,维度恒 WARN。③ **被否**:跨通道并集(UI 缺口被后端吞)/纯后端收窄(全栈缺口)/单列内联限定(新语法)/新 marker(改 apply-wiring)/docstring 声明/独立 manifest/复制 parser/直接 FAIL/B01-B14。④ **钉与变异**:`tests/test_validate_ac_alignment.py` **39 条**(后端 32 + 既有 7)、`tests/test_atlas_check.py` 45、`tests/test_atlas_verify.py` 11、`tests/test_shared_kernel.py` 12;**M156a-f**(dims 解析 / 缺失聚合 / 零测试 / weak-only / 通道闸 / 本地 parser)四段协议全咬;全量 `pytest tests/` **743 passed / 4 skipped** + `install.sh` 空目录探针 exit 0。⑤ **边界**:v1 单 `test_dir`、pytest only、`test_*.py` only、无 app 维度;E2E 第 4 列表头/weak-only 归 Batch B(另立 RFC)。 |
629
629
  | D154 | **bug 台账条目级机器字段 = 「已知 bug 资产化」最小落地面**(2026-10-26;`proposals/RFC-20261026-04` v2 + 独立评审 `proposals/REVIEW-20261026-07`〔tech-proposal-reviewer 只读,总判定「修后通过」,4 必修全落 + 建议项全采;评审基线 d762ea4、试跑基线五段冻结〕;2026-10-26 发版雷达手动补跑首送〔外部来源 = OpenHands v1.28.0 `verify-openhands`「executable known bugs + 可重复修复证明 + 共享 known-bug 契约」;**同场②条 claude-code 钩子 `onFailure:"block"` 撤案**——评审前置代码取证发现三面已全覆盖/已兑现有档:D110〔失败即拒 + `prep_failed` 如实上报 + M94〕/D124〔就绪门 FAIL 指名安装命令 + M124/M125〕/D150〔pre 失败停环 + MB19/MB20〕,外部互证不立项〕;维护者拍「A」照 v2 落刀) | ① **病灶**:DR4 口径 B 判据(归纳在案 ∧ 案数≥3 ∧ 近 30 天有新案)的机器实现只能从散文段近似——族=段、案数=段内 `#NN` 上界、日期=段内任意文本最大日期串(`families.py:26-49/64/80-81`);README 明置三条盲区(跨段族机不可见 / 段漂计数 / 无日期字段)。实病形四型:陈旧收尾说明日期抬活跃窗(前瞻性失真——现行基线五段无翻面试跑实证)、新案漏日期即「静默」(`render` 自认「无日期 ⇒ 视为已静默」)、D131 证明件出库后台账无引用锚断链(育成 "共享 known-bug 契约" 真缺口 = 条目↔证明件机读面)。② **定案**:`bugs.md` 新条目必带全角尾缀 `〔日期|状态|族|证〕`(闭集键;坏形状 = 运行面 WARN + 按无字段回退,**不静默**——防「静默吸收」族);`families.py` 日期来源**分层**(带字段条目 = 字段日期;无字段条目行 = 行内散文日期;段头/归纳行/收尾说明等其余行不再抬窗——(a) 计量字面不变性保留);**判据三项与「案数上界」计量基准原样不动**(评审必修 1 采 (i):上界 ⇄ 精确的重议 = 对 10-17「口径 B」拍板的重新解释,**必须 DR1 另案**,旧行号 = README §2 DR4 + `DESIGN.md` D102;本批只做显示列);`render` 增「精确病案数」展示列(带字段条目 `#NN` 自身去重,不参与判据);`族` 字段只解析记录、不参与判据(升档条件 = 库内第 2 个跨段同族实例,grep 可判);`证` 锚优先级 = D 决策行 > M 变异号/钉名 > 留档路径辅注(`.atlas-run/` 每起跑被 Playwright 清空 = 易腐路径实证 bugs.md E2E 段「留证留存窗口」观察);§3 五件同批(README §2 制式小节+盲区收窄 / README §4 入账条款〔明示 = 巡检 prompt 仓内裁决面〕 / bugs.md 头部制式+两套括号标记归属 / families 六钉 / `duty_signals.sh` **零改动**——唯一接点 :30 只调 families.py)。≥3 条 `证:` ⇒ 缓行升档条件(grep 可判)。③ **被否**:判据计量直接精确化(须 DR1,本批不自授权);JSON/SQLite 搬介质(「库是工作面不是台账」,README §4 原话);独立 validate_bugs_ledger 校验门(非流水线产物,形状纪律归钉 + WARN);`证` runner 进巡检链(只读铁则);旧条目 git 考古回填(不改变任何判据结果,只吃成本与失真源)。④ **钉与变异**:`tests/test_duty_bug_families.py` 4 → **11** 条——字段四型解析钉 / 坏形状 WARN+回退钉(5 坏形 + 行内散文回退实证)/ 半字段合法钉 / 混合段并集聚合钉 / 显示列出缺钉 / **现行库基线冻结对照钉**(2026-10-26 五段 verdict/案数/最近日期逐段冻结 = 字段制式对旧库零扰动实证);既有真值表/形状钉双绿未变;变异 **M157a**(摘分层日期来源 ⇒ 回全行散文日期 ⇒「陈旧说明不抬窗」正向钉红)/ **M157b**(摘坏形状 WARN ⇄ 静默归零从另一门归来 ⇒ 钉红),四段协议含注入 diff、还原逐字节一致,原始输出 `.atlas/runs/mutation-proof/`(运行态不入库)。⑤ **真跑/咬合**:无独立咬合点(台账为工作面非流水线产物;咬合 = 当日 + 下一例实地落账即第一现场——新案条目按 `〔日期 …〕` 落账即为机制首咬)。 |
630
630
  | D155 | **台账字段日期越界校验 = 日历关 + 未来关(「判据可被形态满足」族复见落刀;RFC-03 Phase 0 试点)**(2026-10-26;`proposals/RFC-20261026-05` v3 + 独立评审 `proposals/REVIEW-20261026-08`〔轮 1 否「换日期串当留出」、轮 2 否「换调用通道当留出」、轮 3 复核 v3「修后通过」〕;维护者拍「按建议来」) | ① **病灶**:D154 字段日期只过形态正则 `^20\d\d-\d\d-\d\d$`——`2026-13-40`(日历非法)/ `2027-01-01`(晚于当日)被当合法且**静默**抬活跃窗(未来日期 ⇒ 段永久「升室候选」,零 WARN);契约分叉:RFC-04:60/116 写「日期越界」、canonical README §2 与机器面只做「形态」⇒ 自感知上报族「判据可被形态满足」跨段复见(`bugs.md:60`,行号以 D155 工作区为基)。② **定案**:`_tail_fields` 形态过正则后再过两关——**日历关**(`date.fromisoformat` 失败 ⇒ 坏)+ **未来关**(`parsed > today` ⇒ 坏;`today` 由 `families()` 恒透传,直接调 `parse_sections` 不传 ⇒ 只校日历 = 已知降级面,docstring 明写);均走既有坏形状出口(WARN + 按无字段回退),不造第二语义。真实库**条目区**非法 / 未来日期 **0 条**(全文 6 个 distinct 日期含头部制式块坏例 1 个,落非条目行不参与取数 ⇒ 「全文 0 条」为假,不得如此写)⇒ 冻结基线零扰动(两版 md5 均 `7a461f99…`)。散文日期不校两关 = 显式残留盲区,载体 = `duty-review/README.md` §2 口径 B ③(升档条件 = 出现一条因散文非法 / 未来日期判面翻错的实例 ⇒ DR2 另批)。③ **被否**:散文日期同批扩校(改 D154 既有回退语义、存量收益 0);只 WARN 不回退(响亮成装饰);时钟容差配置键(投机灵活性);独立 `validate_bugs_ledger` 门(D154 备选 C 已否);改写 D154 既有钉(混入他人批次,判别补强一律新增)。④ **钉与变异**:`tests/test_duty_bug_families.py` **11 → 18** 条(v1 4 + 轮 2 后 v3 补 3)——日历非法钉 / 未来日期钉 / **合法边界反面包**(防「只挡越界」写成恒红)/ 判别性补强(尾注**晚于**字段日期,既有钉 #1 咬不住,**新增不替换**)/ **归零路径钉**(段内字段日期全越界且零散文日期 ⇒ `last_date=""` 走「无日期⇒已静默」分支且 `exact=None` 不虚计;既有坏形状钉总留一条带散文日期的行,从无一条走到此路)/ **`T` vs `T+1` 紧边界对**(G2 用 2027 远日期,只按年比较的偷懒实现能骗过它)/ **CLI 送达钉**(`main()` argv → 读文件 → WARN 抵达 stdout = `duty_signals.sh:30` 的真实消费面;日期由真实当日推导 ⇒ 不绑死某天)。**三条 v3 钉在 D154 版上实测全红(`/tmp` 副本 `3 failed`,不碰仓):归零钉 = 判据级留出(verdict / `last_date` / `exact` 三层翻转;夹具不可由既有夹具只改日期串得到);`T+1` 紧边界 = 判别性补强、不冒留出名(轮 3 年粒度变异实测:`parsed.year > today.year` 下 G2 绿、唯它与 CLI 钉红——它走的是与 G2 同一条未来关分支)**。变异 **M158a**(摘日历关)咬 2 条 / **M158b**(摘未来关)咬 4 条 / **M158c**(`print`→`stderr.write`)**恰咬 1 条 = 只 CLI 钉、17 条判据面全绿**——红集恰等于依赖被摘那关的钉,无无关钉牵连;四段协议含注入 diff、还原逐字节一致,报告 sha 已与交付件对齐(首轮记的是改 docstring 前的旧 sha,轮 2 Major 4 指出后重跑),原始输出 `.atlas/runs/mutation-proof/M158{a,b,c}.txt`(运行态不入库);`duty_signals.sh` 零改动(唯一接点 `:30` 只调 `families.py`)。⑤ **试点咬合(RFC-03 正式 Phase 0,经三轮评审才站住)**:轮 1 否 v1——留出 `2026-02-30` 与缺口 `2026-13-40` 同段形只差日期串 = §2.1.4 明列的「同 fixture 改措辞」,且 pre 红来自按探针新写的钉、留出只有 post 记录;轮 2 否 v2——改走 CLI 通道后核算「同输入下 stdout 结论由 `_tail_fields` 唯一决定,通道不新增判据级代码」,只承认它值得钉**送达**面(实测 `print→stderr` 时 15 钉全绿而告警消失)。**v3 的合格证据 = 归零路径留出(pre 红 → post 绿,判据级)+ `T+1` 判别钉**;归因如实剥离:11 段矩阵 7/11 判定变化中**仅 2/11 属 D155**,其余 5/11 属 D154(pre=`d762ea4` vs D154 即 5/11)。「日期越界 ⇒ 坏形状 WARN」子声明至此**具备闭族证据**;但按本机制自定规矩(试点结果须独立评审确认、不得自评代过),**轮 3 复核判「修后通过」:B1(本行补全)+ M1(留出口径降格为「一条留出 + 一条判别钉」)+ M2(触发 (b) 手工对照落盘)修完即判「通过」**;Phase 1(`proposals/README.md` / `TEMPLATE.md` / `gates.md` §2.5)未启动。三轮驳回的都是证据学说法、非代码实质——说法降到证据能撑住的位置。口径与复算见 `proposals/RFC-20261026-05` §4.2/§4.5/§4.6,评审链见 `proposals/REVIEW-20261026-08`。 |
631
- | D156 | **系统性修复的留出验证 = Phase 1 制度化(`RFC-20261026-03`)**(2026-10-26;`proposals/RFC-20261026-03` v4 + 独立评审 `proposals/REVIEW-20261026-06`〔轮 1/2 修后通过、轮 3 试点确认〕;试点 = `RFC-20261026-05` / `REVIEW-20261026-08` 轮 3「修后通过」;维护者拍「继续」) | ① **病灶**:系统性修复(改门 / 校验器 / 契约判据)合入只靠「同批测试绿 + 变异钉」,不证明换一批未参与挖弱点的案例仍有效——「修好了」与「没人再撞到」不可分(自感知上报子族跨段复发实证,`bugs.md:60`)。② **定案**:触发 (a) RFC §1 引子族锚或 ≥3 条独立审查重复 finding;(b) 改门 / 校验器 / 契约判据且 diff 落在 DR4 段级锚路径族(独立评审用 `git diff --name-only` + `closeout_derive.py` 路径族表手工对照,记映射与误报)。触发者 RFC §4.5 必写留出集 + pre-fix 红证 + post-fix 绿 + 反面包 + 三态 `闭族` / `未闭族` / `单案修复(无留出集:评审确认 + QUEUE 观察行 owner/期限 + 残留风险)`。**留出独立性三条**(缺一按「同夹具变体」驳回,独立评审当场重放、不由作者自证)= ①判据输出翻转(verdict / 案数 / 最近日期 / 精确病案数;仅 WARN 文本 / 日志 / 通道变化不算)②变异增量杀伤(既有钉全绿而留出案红)③结构不可达(不可由既有钉夹具只改日期串 / 通道 / 增删行得到)。唯一判定记录 = 那份 RFC §4.5 + 其 REVIEW(不新增台账);`bugs.md` 指针行 `- 留出验证:RFC-… / REVIEW-…`(无状态 / 无日期 / 无 `#NN`)。落点 = `proposals/README.md` §2/§3、`proposals/TEMPLATE.md` §3/§4.5、`duty-review/README.md` §2/§3/§4、`shared/gates.md` §2.5 流程门行(语义项、无机器判据)、`shared/independent-review.md` §9(触发 (b) 检查项)、`shared/closeout.md` 步骤 3(作者侧定档落点)。③ **被否**:每份 RFC 强制留出(一次性修复无族可闭,过宽);自动 LLM 聚类 / 自动改契约(过拟合、门禁不信模型自评);Spec Fidelity 百分比硬门(数字未核);先制度化再试点(违「先量再动手」);继续用「无新案」当闭族(无证据 ≠ 已泛化)。④ **钉与变异**:`tests/test_duty_bug_families.py` **18 → 19**(留出验证指针行形状钉:正向合规指针不改三值 / 反向带日期改 `last_date`、带 `#NN` 改 `count`——反向即格式禁令的机器面);`shared/gates.md` §2.5 **+1 行(146 → 147)**,`docs/map/gates.md:18` 与 `docs/map/atlas-map.html` 计数同批同步;无新增校验器、无新台账、无生产代码。⑤ **边界/盲区**:**触发靠自述、可被逃**((b) 无机器判据——作者侧落点 = `shared/closeout.md` 步骤 3 定档附交叉检查结论、评审侧 = `shared/independent-review.md` §9;试点实测误报 0,不构成保证);「未参与挖弱点」语义不可机判,只靠证据链与评审回放;已归档族不追溯(重开仍走 DR1);Phase 2 = 第 3 份带「留出验证」章的 REVIEW 落盘即触发评估(成本 / 收益 / 失义),失义即退役。 |
631
+ | D156 | **系统性修复的留出验证 = Phase 1 制度化(`RFC-20261026-03`)**(2026-10-26;`proposals/RFC-20261026-03` v4 + 独立评审 `proposals/REVIEW-20261026-06`〔轮 1/2 修后通过、轮 3 试点确认〕;试点 = `RFC-20261026-05` / `REVIEW-20261026-08` 轮 3「修后通过」;维护者拍「继续」) | ① **病灶**:系统性修复(改门 / 校验器 / 契约判据)合入只靠「同批测试绿 + 变异钉」,不证明换一批未参与挖弱点的案例仍有效——「修好了」与「没人再撞到」不可分(自感知上报子族跨段复发实证,`bugs.md:60`)。② **定案**:触发 (a) RFC §1 引子族锚或 ≥3 条独立审查重复 finding;(b) 改门 / 校验器 / 契约判据且 diff 落在 DR4 段级锚路径族(独立评审用 `git diff --name-only` + `closeout_derive.py` 路径族表手工对照,记映射与误报)。触发者 RFC §4.5 必写留出集 + pre-fix 红证 + post-fix 绿 + 反面包 + 三态 `闭族` / `未闭族` / `单案修复(无留出集:评审确认 + QUEUE 观察行 owner/期限 + 残留风险)`。**留出独立性三条**(缺一按「同夹具变体」驳回,独立评审当场重放、不由作者自证)= ①判据输出翻转(verdict / 案数 / 最近日期 / 精确病案数;仅 WARN 文本 / 日志 / 通道变化不算)②变异增量杀伤(既有钉全绿而留出案红)③结构不可达(不可由既有钉夹具只改日期串 / 通道 / 增删行得到)。唯一判定记录 = 那份 RFC §4.5 + 其 REVIEW(不新增台账);`bugs.md` 指针行 `- 留出验证:RFC-… / REVIEW-…`(无状态 / 无日期 / 无 `#NN`)。落点 = `proposals/README.md` §2/§3、`proposals/TEMPLATE.md` §3/§4.5、`duty-review/README.md` §2/§3/§4、`shared/gates.md` §2.5 流程门行(语义项、无机器判据)、`shared/independent-review.md` §9(触发 (b) 检查项)、`shared/closeout.md` 步骤 3(作者侧定档落点)。③ **被否**:每份 RFC 强制留出(一次性修复无族可闭,过宽);自动 LLM 聚类 / 自动改契约(过拟合、门禁不信模型自评);Spec Fidelity 百分比硬门(数字未核);先制度化再试点(违「先量再动手」);继续用「无新案」当闭族(无证据 ≠ 已泛化)。④ **钉与变异**:`tests/test_duty_bug_families.py` **18 → 19**(留出验证指针行形状钉:正向合规指针不改三值 / 反向带日期改 `last_date`、带 `#NN` 改 `count`——反向即格式禁令的机器面);`shared/gates.md` §2.5 **+1 行(146 → 147)**,`docs/map/gates.md:18` 与 `docs/map/atlas-map.html` 计数同批同步;无新增校验器、无新台账、无生产代码。⑤ **边界/盲区**:**触发靠自述、可被逃**((b) 无机器判据——作者侧落点 = `shared/closeout.md` 步骤 3 定档附交叉检查结论、评审侧 = `shared/independent-review.md` §9;试点实测误报 0,不构成保证);「未参与挖弱点」语义不可机判,只靠证据链与评审回放;已归档族不追溯(重开仍走 DR1);Phase 2 = 第 3 份带「留出验证」章的 REVIEW 落盘即触发评估(成本 / 收益 / 失义),失义即退役。**2026-11-01 盲区硬化**:触发 (b) 的作者侧定档与评审侧 §9 均要求粘贴原始命令输出并由评审自行复跑,不得只写「无命中」;仍无机器判据(不新增门、不假装是门)。 |
632
632
  | D157 | **E2E 多靶场 = 用例级 `靶场:` 字段 + `e2e.targets` 登记表(闭集)**(2026-10-27;现场驱动:某前端项目同一页的用例子集需要目标的**结构性前提**(另一文档实例),`seed:` 只播数据态、prepare 只能操作已有结构 ⇒ 契约唯一出口 `blocked` 等环境,长期卡死后诱发生成器外路由篡改——整页路由被改指另一实例,同分片其余用例的结构性前提被静默换掉) | ① **病灶**:`rings/e2e/reference.md` §5.4「靶场唯一」被读成「全项目永远恰好一个实例」,而页面分片全部用例共享页面表单格路由——结构性前提互斥的用例组无法共存,也没有「第二个目标」的合法声明形态。② **定案**:`stack-profile.yaml` 的 `e2e` 段新增可选子段 **`targets`**(`<名>: <路由>` 映射,闭集;名字 `[a-z0-9-]+`——进生成测试名 `__<名>` 后缀;路由形态同页面表 `路由` 列);分片 `atlas-case` 块新增可选标量字段 **`靶场:`**(值 = 登记名,缺省 = default 靶场)。生成器:用例 URL = `targets[<名>] ?? 页面表路由`(与 base query 合并口径同邻域);`守卫:` 缺省、`goto`/`refresh` 落点同源;**零步基线按解析后靶场 URL 分组**——default 组保持现名(`test_<页>__zero_step_baseline`,零变化承诺),具名组加 `__<名>` 后缀且只收该组用例探针(R9 每靶场各验各的初始态;blocked/skipped 本就不进探针)。校验双保险:`validate_stack_profile` 验 `targets` 形状;`validate_e2e_index` 新门「靶场 取值合法」(钉名必须在闭集内,整段未声明而钉名 ⇒ FAIL)+ `goto`/`守卫` 路径门放行**已登记靶场路由**(地址仍单源于 profile,自由路径仍禁)。**边界**:本字段只解决「同一页、同一批元素、不同目标实例」;断言另一页面元素 ⇒ 新建页 slug(testid 归属与双向集合门不受靶场影响)。**被否**:命令行 `--target` 旗标回归(E1 已退役;运行器临时换靶 = 地址脱单源);新页 slug 当隔离手段(对「同页同元素不同实例」过重——分裂 testid 命名空间与 AC 对齐);用例内联 URL(地址散落,违 §7 单源)。③ **零变化承诺**:未声明 `targets` 且无用例钉名 ⇒ 生成物逐字节一致(真实项目实测 sha 前后一致 + 出厂测试钉住)。④ **钉**:`tests/test_gen_e2e_scripts.py` 补具名靶场路由/基线分组/闭集中止三钉;`tests/test_validators.py` 补「靶场 取值合法」正反钉;`validate_stack_profile` 形状钉随 `test_tools`/`test_validators` 既有夹具口径。⑤ **边界/盲区**:多靶场共享同一运行器配置(baseURL/登录态/storageState)——具名靶场若跨域(不同 host),登录态预置按 default 靶场换取,跨 host 会话不在本批语义内(登记表内路由建议同源;跨 host 靶场出现时另案)。 |
633
633
  | D158 | **`fill` 空串约定 = 值 `""` / `(空)` 清空输入(B173-1 对称面;现场实证 DSL 缺口)**(2026-10-27) | ① **病灶**:`value` 断言自 B173-1 起把 `""` / `(空)` 读作空串,但 `fill` 动作无对称口径——字面发射两个引号字符(现场:搜索框被填进 `""` 字面量 ⇒ 过滤恒不命中,「清空即恢复」类判据在 DSL 内无法表达)。② **定案**:`fill` 值 `""` / `(空)` ⇒ 发射 `fill("")`(Playwright fill 先清后写 = 清空);普通值逐字节不变。`type` 不变(文档明写空值报错,逐字键入无清空语义)。③ **被否**:新造 `clear` 动词(fill('') 即清空,动词增殖无增益);`press` 组合表达(多行才能表达一件事)。④ **钉**:`tests/test_gen_e2e_scripts.py` +2(清空发射钉 / 普通值零变化钉)。⑤ **边界**:仅 `fill`;`goto`/`select` 等其他带值动作不涉。 |
634
634
 
@@ -651,6 +651,11 @@ adapters:
651
651
  | D175 | **登记「测试有效性 / CRAP」候选观测面(用户 2026-10-30 拍板;`shared/gates.md` §3/§4)** | ① **病灶**:对账平面索引缺「测试有效性」与「复杂度×覆盖组合」两面——现行套件(含 M 系列变异钉)证明「判据/实现坏了测试会红」,未证明「测试能杀死生产代码变异」;覆盖率与复杂度单看各有盲区。② **定案**:按「少建投机硬门」先登记缺口不建门——§3 新增「测试 ⇔ 生产代码变异(变异得分)」「实现代码 ⇔ 覆盖×复杂度(CRAP)」两行候选观测面(❌ 缺口、**非门**),§4 另立「候选观测面(非门)」小节;来源 = ub-mode(Uncle Bob)思想;升格条件 = 真实仓**副本**试跑信噪比证据(原目录只读),升格形态 WARN/N-A、不设 profile 开关。③ **被否**:直接装 Crapper/Dryer/Mutator 进 Atlas 依赖并建 FAIL 门(投机硬门;三工具为 Python 生态,候选烟测仓 cn-console / nanwang 均无 Python);把变异测试写成 M 系列的「工具化对偶」(两者**正交**:M 系列向判据注入脏数据,变异测试篡改生产代码考测试)。④ **钉**:纯文档登记,无新机器判据;行数锚同步(`docs/map/gates.md` / `atlas-map.html`,149 → 152 行)。⑤ **边界**:试跑批(语言 go/no-go 最小夹具 → 真仓副本信噪比报告 → 升格/搁置/退役)另行确认后执行;本登记不构成任何门的取证。**试跑收口(2026-10-30 同日,用户拍板「跑吧」)**:go/no-go 已过——三工具原生支持 Clojure/Java/Go/TypeScript/Rust/Python(②③里「Python 生态、需换 StrykerJS / PIT」系转述 ub-mode 文章的错误说法,实测推翻);Python 夹具三工具全通(CRAP 公式手算核对 17.2、重复对 score=1.00 指名、kill/survive/uncovered 三态正确);cn-console TS 副本:crapper 10.7s 全仓 248 函数(vendor `cn-cdn` 霸榜信噪比注意点,第一方 69 函数最高 CC=8)、dryer 0.8 阈值零重复、mutator kill 1/1(3.7s;薄胶水文件 0 变异位;绿基线套件 8/18,本仓单测仅 9 条);nanwang Java 副本:crapper 1.2s 3086 函数(65 个 CC≥10,榜首 `DmSqlScriptParser.parse` CC=41)、dryer 咬出真实仓储层重复对(0.80–0.81)、mutator 实测 `PeriodUtil` 13 位点 11 杀 2 活(84.6%,`currentForecastPeriod` 2 存活 ⇒ 测试虚;**~6.6s/变异体**——mvn 冷启动,须 `-Pskip-frontend` + `mvnw`(系统 mvn 3.6.1 过旧),成本证实只可窄域按批、不可全仓常开)。安装障碍:本机 npm `allow-remote=none` 拦 lockfile remote 依赖 ⇒ 经用户拍板在一次性 /tmp 副本 `--allow-remote=all --ignore-scripts` 覆盖。**升格建议(待拍板):crapper/dryer 全仓扫描作 WARN 观测 + mutator 按批 `--changed` 触发,仍非门。已拍板采纳(2026-10-30 同日):按建议采纳为 WARN 级观测面——crapper/dryer 全仓扫描按第一方范围圈定(vendor 霸榜实测教训)+ Dryer 阈值 0.8(非默认 0.82)+ mutator 按批 `--changed`;仍非门、不进 `atlas_check`、不装进 Atlas 依赖,仅登记为采用者操作侧可选观测动作。** |
652
652
  | D176 | **#47 落地:E2E 容忍前提措辞 WARN + PRD 派生字段默认语义 WARN(用户 2026-10-30 拍板「要做」;`validators/validate_e2e_index.py`、`validators/validate_prd.py`、`rings/e2e/reference.md` §5.5/§8.2、`rings/prd/reference.md` §3/§六/§7、`templates/prd/domain.md`)** | ① **病灶**:应然资产没有为「用户可编辑 + 系统派生/重置」字段定义默认/重置语义时,字段行为天然无 AC;E2E 用例把「实现现在恰好是什么」写成合法前提(step 注释「存量行可能停在 X」+ expected 仅 visible),旧状态长期为绿;PRD §六 虽要求默认值,但无机制强制。② **定案**:两条独立 WARN(不建 FAIL 门,不并入参数 4 聚合)——**A 容忍前提措辞**:`step`/`expected` 注释尾巴 + `precondition` 全文命中词表(存量 / 可能停在 / 视情况 / 随机 / 取决于历史)⇒ WARN,行内 `容忍: <理由>` 豁免;本项**刻意读注释**(§5.5「机器忽略 `#` 之后」的唯一例外,契约已写明),代码部分不扫(业务文案不误报)。**B 派生字段默认语义**:PRD 字段规范表可选列 `派生/重置`(表头前缀匹配、不绑列位置;模板默认带列,存量无列 = opt-in N/A),列非空的字段须在 §六 有一行含字段名且「采用的默认值」非空,缺 ⇒ WARN 逐字段指名(`—` / `无` / 空视为未声明)。③ **被否**:升 FAIL(首版证据不足,`3509 §B36` 过早严判教训);按 `FN-*` 小节定位字段表(仓库无该结构 ⇒ 0 命中空门,评审抓出);把 WARN 并入参数 4 聚合(会被参数 4 升 FAIL,与 WARN 首版冲突);issue 建议 ③「共享靶场前置复位约定」不进 v1(先让声明与用例两端可见)。④ **钉**:`tests/test_validators.py` 180 → **182**;变异 **M160a**(摘词表匹配)/ **M160b**(改扫代码不扫注释)/ **M160c**(判据恒 N/A)/ **M160d**(只认列不查 §六)四段协议全咬、还原逐字节一致。⑤ **边界**:容忍理由「够不够」、派生默认值的业务正确性均归独立审查;`precondition` 说谎(语义)仍不查;列存在但全 `—` ⇒ PASS(声明过、零派生)。 |
653
653
  | D177 | **前端复用优先:程序契约 + Trellis 面包屑注入(用户 2026-10-31 拍板「你就做就行」;`shared/frontend-reuse.md`、`shared/single-source.md`、`patches/workflow-skill-routing/`、`rings/e2e/reference.md` §4/§5.8、`tests/test_frontend_reuse_patch.py`)** | ① **病灶**:V3 现场同一筛选面板已有「来源系统」选择器的单个删除 + 清空全部,新建「企业级/领域级标签」选择器时未做同面板能力差集,交付后补修;skill 自动触发绑请求时点,流程中段的前端复用需求等不到评估。② **定案**:新增执行期程序契约 `shared/frontend-reuse.md`(适用四档、当前模块交互清点、四层查找(同面板 sibling → 业务组件 → 公共组件 → 组件库)、五列记录、surface 维度、新模块目录一次性确认;**非门、非真相源**,普通修复不强制);扩展现有 `patches/workflow-skill-routing` 4 条面包屑(planning/in_progress + inline)+ 2.2 末注,直接指向契约,**不新建 skill / 不新建补丁包**;`shared/single-source.md` 登记复用决策记录 owner = Trellis 任务 Plan/Design;E2E 契约补两句澄清(同一能力不同 surface 都算必要元素;临时打桩 ≠ E2E 通过)。③ **被否**:新建 `skills/atlas-reuse`(破 `tests/test_install_selfcheck.py` 环退役对称);新建 `patches/frontend-reuse`(复制 266 行 applier,且 `install.sh` 只认含 applier 的包);P0 生成 `.trellis/spec/conventions/frontend-reuse.md`(V3 宿主 `.zcode/config.json` 无 `Read/Edit/Write` 钩子,`spec_inject.py` 无调用方,按路径注入不会触发);加 validator/WARN 门(`validate_testids` 只能做声明⇔源码差集,识别不了未声明的新控件);把 frontend-index schema 放进 Atlas(仍是高漂移组件清单,违背 `rings/structure/reference.md:147`)。④ **钉**:`tests/test_frontend_reuse_patch.py` 9 条(契约四档/五列/surface/打桩边界、单源登记、跨包 id 唯一、片段指向契约、完整 fixture 干跑不写、首跑 11 条 applied、二跑 11 条 skipped、存量 7 条升级 4+1+6、锚点缺失报错不写、真实 V3 workflow 副本干跑);真实 V3 workflow 临时副本实跑 = 4 applied + 1 replaced(ui-skill-note 扩段)+ 6 skipped,重放 11 skipped;`docs/map` 快照同步(共享契约 7→8、出厂测试 42→43)。⑤ **边界**:只对新增/重构/新文件,普通修复不强制;复用记录归执行层,Atlas 不持有数据/索引;未接宿主文件触碰钩子前,面包屑是唯一每轮注入通道;「是否真的复用」仍归独立审查语义判断。 |
654
+ | D178 | **实例⑤审查报告 coverage 权威 + CLI 失败面可见(Batch A;`RFC-20261031-01`)**(2026-10-31;`scripts/cli_review_convert.py`、`_support/review_policy.py`、`scripts/validate_review_report.py`、`shared/independent-review.md` §3/§10.2、`docs/concepts/review.md` §3、`docs/gates/iron-gates.md` §8) | ① **病灶**:真实 r1 CLI `status=partial`、`manifest.coverage.completed=4`/`failed=2`,但 `summary.files_reviewed=6`,转换器只读 summary ⇒ 报告 `reviewed=6`、6/6 假绿;r2 delta `head_sha` 缺失、`reviewed_files` 只收 1/3;clean delta 空 `reviewed_files` 误红;CLI partial/failed 与 failed_files 不可见。② **定案(Batch A,本批)**:coverage 为实审权威——`reviewed` = `completed[]` 去重数(无 coverage 才回退 summary);报告新增 `cli_status`/`failed_files`/`waived_files`/`reused_files`/`cli_warnings`;`partial|failed` 或 `failed_files` 非空 ⇒ 状态下限 `WARN`(不降级阻断性 Critical);`skipped|unknown` 只记录;coverage 缺 completed 拒绝产出。**Batch B(同 RFC,已实现)**:新 delta 报告 v3、`head_sha` 必备、`reviewed_files` 取 completed 或显式 `--reviewed-files`、`selected_files`/数量/成员/target 对账(WARN);v2 delta 缺 `head_sha` 只 WARN;D5 跨轮 `resolves` 另案。③ **被否**:继续信任 summary(E1/E4 假绿);用 findings 反推 reviewed_files(1/3 + clean 误红);用 `--to` 兼取 head(会改 N);v2 直接加必备 head_sha(破坏存量兼容);partial 直接 FAIL(工具失败非代码 finding)。④ **钉**:`tests/test_cli_review_convert.py` 12 → **41**;`tests/test_review_report.py` 18 → **39**;变异 **M37**(coverage)/ **M38**(head_sha)/ **M39**(reviewed_files 对账)/ **M40**(CLI WARN 下限)/ **M41**(Critical 不降级);H1 6/4/2 post-fix = 4/6 + failed_files 2 + WARN(真实 r1 同形复跑已红→绿);H2 2 completed + 1 reused post-fix = 2/3 + FAIL→WARN(不再误红,`reused` 不计 M)。⑤ **边界**:`reused`/`waived` 语义**已于 2026-11-01 取证确认**(OCR v1.13.3 原生二进制 `strings` 命中 `Coverage: %d selected = %d completed + %d reused + %d failed + %d waived`、`[ocr] Resume %s: reusing %d file(s), reviewing %d file(s)`、`manifest: waived item %s requires a non-empty reason`)⇒ 四态互斥分区,`reused` 是 resume 沿用旧结果、`waived` 是带理由的显式豁免,两者本轮都没再审 ⇒ **不计 M 是正确口径**(非保守猜测),本批只记录 `reused_files`/`waived_files`;coverage 字段跨 CLI 版本稳定性;跨轮闭环另案;不回扫存量报告;v2 delta 的 `reviewed_files` 仍按 findings/兼容语义,v3 才取 completed/显式。 |
655
+ | D179 | **跨轮 finding 闭环台账(D5;`RFC-20261031-02`)**(2026-10-31;`scripts/review_closure.py`、`tests/test_review_closure.py`、`shared/independent-review.md` §11、`shared/closeout.md` 步骤 10、`shared/gates.md` §4) | ① **病灶**:`findings/<unit>.r<round>.json` 有多轮原始结论,形态门只看最新 `review.json`;真实 r1 的 12 条 Major 在 r2 既未重报也无处置,静默丢失;契约又禁止强制同指纹重报。② **定案**:新增独立 WARN observer `review_closure.py`——prior open Critical/Major(`pre_existing` 除外)必须在最新轮同指纹 `carried` 或 `review.json.resolves` 关闭;`fixed`/`false_positive` 需 `by`+`evidence`,Critical 不接受 `intentional`/`ignored`;**是否算关闭按上一轮级别从严**(只把新一轮的 `level` 当依据 ⇒ 先降级别再挂 intentional 即可免签,实测复现过),且 prior Critical 降级本身记 `level_downgraded` WARN;**`findings/` 里非本 unit 的孤儿文件不得静默跳过**(跨轮改 `--unit` 名会让闭环义务凭空清零、结果退化成 SKIP)⇒ WARN;unknown/重复/坏形状分别 WARN/exit 2;<2 轮 SKIP;不进 `atlas_check`。**三轮只读评审的修订**:① 关闭判定取 `worst_level(义务最严档, 本条级别)`,且加宽读**全部出现**(含被标 `pre_existing` / 已关闭的条目)——第一版只挡降级方向、第二版加宽又挂在 `relevant()` 之后(升级那一轮标 `pre_existing` 即跳过),两处都是「自称两个方向都挡、实际只有一条正向夹具」;② 中间轮坏形状降为 warnings+WARN(不再 exit 2 钉死历史),最新轮仍 exit 2;③ N/M 下限改 `count_floor`(M≠N:少审与多报都抬),转换器与形态门同口径,对账行与 README 三态文案随实况;④ `ROUND_CAP` 单源进内核;⑤ 最新轮同指纹 open+已签字关闭混排按未关闭跟踪并抬 WARN(与中间轮同口径);⑥ 轮次文件名只认纯 ASCII 数字(`²` 会裸崩、`2` 会被静默当成 round 2);⑦ 报告目录不可写 ⇒ exit 2 带错误,不抛裸 traceback;⑧ `resolves[].round` 与义务轮不一致时出声。③ **被否**:塞进 `validate_review_report` FAIL(永久红门);强制同指纹重报(与「同一问题跨轮不得重复贴」冲突);自动启发式关闭(语义不可机判);converter 自动写 resolves(triage 声明,另案)。④ **钉**:`tests/test_review_closure.py` 19 条;M1 unaccounted / M2 缺取证 / M3 Critical intentional 不关闭 / M4 关闭判定改回按新一轮级别(降级即免签)/ M5 孤儿文件重新静默跳过;真实 r1/r2 报告首跑 12 条 unaccounted,r2-v3 单轮 SKIP。⑤ **边界**:`resolves` 由 triage/人写;`fixed` 语义真实性归独立审查;跨报告目录/需求不并集;不自动修复;升档条件 = 真实仓连续多轮 WARN 被忽略后的证据。 |
656
+ | D180 | **跨轮关闭持久化 + 升级可见性 + scan 通道词表(D5-a/b/c;`RFC-20261101-02`)**(2026-11-01;`scripts/review_closure.py`、`scripts/cli_review_convert.py`、`_support/review_policy.py`、`shared/independent-review.md` §11、`shared/closeout.md` 步骤 10、`shared/gates.md` §4) | ① **病灶**:真实四轮运行(nanwang `2026083101.462`,r1→r4 + 整文件补审)撞出三处——(a) D5-a:闭环义务集覆盖全部 prior 轮,但关闭证据只看最新轮 ⇒ r3 已 `PASS 13/13` 的 13 条到 r4 变 `WARN unaccounted 13`,要不误红就得每轮重贴整本历史,与 §4「同一问题跨轮不得重复贴」直接冲突;(b) D5-b:4 轮上限的 `escalation` 只写 `review.json`,README 0 次出现,纪律事件在人读面不可见;(c) D5-c:`ocr scan` 的 `status="success"` 不在 `CLI_STATUSES` ⇒ 假记 `unknown`,且 scan 不产 `manifest.coverage` ⇒ M 只剩自评而报告不点名。另有手写 13 条 `resolves` 实测 93 行 / 4849 字节、机械字段占 23%、`by` 逐条重复。② **定案**:闭合改按**该指纹最后一次证据**判——任一中间轮 `k<r<latest` 正式关闭即 `resolved`(`via` 记 `resolves@rN` / `re-reported-closed@rN`),更晚一轮又 open 则作废,声明轮次必须晚于最后一次 open;中间轮判据与最新轮同源(取证要求、Critical 不接受 intentional/ignored、形状错 exit 2),形状判据下沉 `_support/review_policy.resolves_shape_errors` 单源;README 渲染 `escalation` 与跨轮声明条数;`CLI_STATUSES` 加 `success`,无 coverage 落 `cli_warnings` 点名「M 取自评、可被高估」(只点名不改状态);转换器加 `--resolves`(`[...]` 或 `{by, resolves}`,条目缺 `by` 继承顶层),`review_closure.py --emit-skeleton` 出待填义务骨架。③ **被否**:每轮要求重贴历史(撞 §4,成本不可持续);用 `closure.json` 当历史基线(另立真相源,产物缺失即跑错,轮次历史已在 `findings/`);把义务集缩到「仅上一轮」(漏隔轮丢失,正是 D5 要治的形态);中间轮的冗余声明抬 WARN(只进 `warnings`);给 scan 造 coverage(外部 CLI 不由我们决定)。④ **钉**:`tests/test_review_closure.py` 19 → **37**(M6 中间轮证据 / M7 轮次单调与重置 / M8 骨架输出 / M9 降级方向 / M10 中间轮坏形状不钉死历史 / M11 `pre_existing` 跳过加宽 / M12 损坏 `level` 不裸崩 / M13 最新轮 open+已签字混排 / M14 非 ASCII 轮次文件名 / M15 目录不可写 ⇒ exit 2 / M16 `round` 死字段出声);`tests/test_cli_review_convert.py` 41 → **48**(M42 `--resolves` 落键 / M43 无 coverage 点名 / M44 README 渲染);`tests/test_review_report.py` 39 → **40**(M45 `success` 词表,摘掉即两文件红)。留出:**H3** 真实四轮报告修前 `WARN unaccounted 13` ⇒ 修后 `PASS resolved 16(13 via resolves@r3 + 3 via resolves)`;**H4** 同语料把中间轮关闭改成「更晚一轮又 open」⇒ 回 `unaccounted`(不放水侧)。七枚钉子四段协议全部 sha256 逐字节还原(日志 `/tmp/atlas-rfc01-evidence/`)。⑤ **边界**:义务锚定仍按「最早未关闭出现轮」,隔轮重复出现的取哪一轮待真实歧义再定;`via` 的 `@rN` 是否需要结构化字段待有消费方再定;scan 通道的 M 无权威只点名不判据;`resolves` 语义仍归 `review_closure`,形态门不判。 |
657
+ | D181 | **签字与取证必须是非空字符串(收紧台账与形态门的共同承重件;`RFC-20261101-03`)**(2026-11-01;`_support/review_policy.py`、`scripts/review_closure.py`、`shared/independent-review.md` §3/§11、`shared/gates.md`) | ① **病灶**:`disposition_by` / `disposition_evidence` / `resolves[].by` / `verification.by` 的判据是 `bool(str(x or "").strip())` ⇒ `by: 123`、`evidence: ["x"]` 经 `str()` 变非空即算「已签字 + 已取证」;而 Critical 正是靠这一条脱离阻断(`is_blocking_critical` 豁免支)、台账正是靠它把义务判 `resolved`——闭环的承重件「谁签的、凭什么关」可以被一个整数满足。第三轮评审把这条列进「不改」(理由:真实性归人判),主审推翻:**内容**归人判,**形式**可以机判,两件事。② **定案**:内核新增 `signed_str(container, key)` 与 `has_signed_disposition(container, by_key, evidence_key)`(必须是非空字符串),五处旧调用统一改齐(`is_blocking_critical` / `validate_v2_finding` 的 fixed|false_positive / `verification.by` / 台账 `has_evidence` / `declares_close`);`resolves_shape_errors` 增加「`by`/`evidence` 给了就必须是字符串」⇒ 转换器拒绝产出、中间轮该条不作关闭证据、最新轮 exit 2。**合并前评审补的第二批**(同一决策,不另开 D 号):批级缺省签字人 `--resolves` 顶层 `by` 同判据(`str(123)`→`"123"` 会继承进每一条 ⇒ 数字签名在批级复活,blocker 已端到端复现后修掉);`verification.evidence` 与 `escalation.approved_by` 走 `signed_str`(此前同对象一侧严一侧松);中间轮坏形状改**逐条取舍**(`resolves_entry_errors` 单源逐条判据),整批没收会让同轮合法声明清零 = 假失忆。**第三轮复核补的同一决策**:重复指纹改**整组作废**——逐条取舍若对重复只留第一条,`fixed`+`intentional` 会按书写顺序洗成已关闭;现同一 fingerprint 的全部条目(含坏条目,与最新轮形态口径一致)都不作关闭证据,其他指纹照常承重(M102)。语义不变(仍要求两者齐备;Critical 仍不接受 intentional/ignored)。③ **被否**:只改台账不改形态门(同判据两处口径 ⇒ 门绿台账红);接受数字签名(承重件不留宽松口子);「签名者须在名册里」校验(无权威名册,属臆造硬门)。④ **钉**:`tests/test_review_closure.py` M17(`by: 123` + `evidence: [..]` ⇒ 回到 carried,并配字符串签名正例对照,防把正常收口刷红)与 M98(中间轮一条坏形状不没收同轮合法声明)、M102(中间轮重复指纹整组不作关闭证据)、`tests/test_review_report.py` M48(`by` 数字 / `evidence` 为 0 两侧)、M99(`verification.evidence`)、M100(**内核级**:`is_blocking_critical` 宽松时当时三文件 131 条全绿——现 138 条中单红,只能直接断言内核)、M101(`escalation.approved_by`)、`tests/test_cli_review_convert.py` M49(条目级非字符串签名 ⇒ 拒绝产出)与 M51(**批级缺省**签字人四侧:123 / 数组 / 纯空白 / true);四段协议注入 5/5 按预期红、sha256 逐字节还原。⑤ **边界**:零回扫风险已实测(全仓 + 真实四轮语料的非字符串签名命中 **0 处**,语料台账与形态门结果不变);签名值的**格式**(是否须 `agent:<id>` / `@handle`)不设判据,有真实滥用再加。 |
658
+ | D182 | **消融事务工具 + 「消融残留」WARN 门(P3 首切片;`RFC-20261101-04`)**(2026-11-01;`scripts/ablation.py`、`scripts/atlas_check.py`、`tests/test_ablation.py`、`tests/test_atlas_check.py`、`shared/gates.md`) | ① **病灶**:P3 只有手工流程(`/tmp` 副本拷/删/还原靠人记 sha256),F1 忘装回、F2 字节不对、F3 覆盖他人在途改动均无机器载体。② **定案**:项目外 per-root 状态(`~/.atlas-ablation/roots/<root_hash>/`)+ `pending/active/restoring` + 锁 + 原子写;`begin` 先写 pending active/marker 再 apply,apply 后失败不删 before;`restore` 全路径 preflight + 备份校验 + 写回/ smoke 后复算;MVP 只支持现有普通文件的修改/删除;`atlas_check` 内置「消融残留」门(无 active=N/A;健康 active=ok+详情;超时/分叉/manifest/备份/hash 异常=WARN;出生 WARN、永不 FAIL)。③ **被否**:自动还原、仓内备份、生产级 `ATLAS_ABLATE` 开关、整线束 on/off、LLM 分类、自动冲突合并;`run` 子命令/`install.sh --update` 互锁/host F5 自动判据后置 P3-b。④ **钉**:`tests/test_atlas_check.py` **49 → 60 条**(**M163**:摘掉 `check_ablation_residue` 调用 ⇒ 门行消失红);`tests/test_ablation.py` **23 条**(**M164**:restore 退回无条件写回 ⇒ 冲突用例红);四段协议在 `/tmp` 副本。⑤ **边界**:F3 同字节第三人改动不可区分;smoke trivial/缺失只记 `completed_hash_only`(不冒充 F7);P3-b 已实现(run / install 互锁 / host_warning 门 / --state);F5 仍只做 WARN,不自动阻断。 |
654
659
  ---
655
660
 
656
661
  ## 13. 待实现事项
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Atlas — 践行 Project Harness 的全流程 AI 编程工作流
2
2
 
3
3
  <p align="left">
4
- <img src="https://img.shields.io/badge/version-0.2.0--beta.25-blue.svg" alt="Version" />
4
+ <img src="https://img.shields.io/badge/version-0.2.0--beta.26-blue.svg" alt="Version" />
5
5
  <img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License" />
6
6
  <img src="https://img.shields.io/badge/tests-passing-success.svg" alt="Tests" />
7
7
  <a href="https://sunwenzhe-git.github.io/atlas/"><img src="https://img.shields.io/badge/docs-VitePress-green.svg" alt="Docs" /></a>
@@ -436,7 +436,9 @@ Atlas 坚持**变异证明(Mutation Proving / M 系列钉子测试)**原则
436
436
  * **断言**:用例分片中声明的应然 `testid:` 清单,与前端源码中真实扫描出的 `data-testid` **按页面双向集合严格相等**。漏写控件报 FAIL,多写无用埋点报 FAIL。
437
437
  2. **实例 ⑤:实现代码 ⇔ 应然资产独立审查(防漏审;机制与对账门已定形,外部执行器接入中,见 shared/gates.md)**:
438
438
  * 需求实现后、归档收口前,由独立进程执行只读 diff 审查(Fresh Context 隔离;契约与转换器见 scripts/cli_review_convert.py);
439
- * **应审 N / 实审 M 严格对账**:实测表明大模型审查大体积 diff 时会悄悄跳过文件;若实际评审文件数 `M < N`,无论结论如何一律报红拒绝,发现 `Critical` 缺陷一票否决。
439
+ * **应审 N / 实审 M 严格对账**:实测表明大模型审查大体积 diff 时会悄悄跳过文件;若实际评审文件数 `M < N`,报告至少 `WARN`(不直接阻断),发现 `Critical` 缺陷一票否决。`M` 取 CLI `manifest.coverage.completed`(无 coverage 才回退 summary)。
440
+ * **delta v3**:差量复审记录 `head_sha` 与 `reviewed_files`(coverage completed;无 coverage 用显式 `--reviewed-files`);`selected`/target/数量不一致只 `WARN`;v2 delta 缺 `head_sha` 只 `WARN`(兼容)。
441
+ * **跨轮闭环(D5)**:`review_closure.py` 读多轮 `findings/`;prior open Critical/Major 未 `carried`/`resolves` ⇒ `WARN`(观察面,非门);关闭按上一轮级别从严(Critical 降级不免责)、按该指纹最后一次证据判(中间轮关过不必重贴)、孤儿单元文件也 WARN;<2 轮 SKIP;`--resolves` 落声明、`--emit-skeleton` 出待填骨架。
440
442
  3. **后端/全栈测试用例 ⇔ AC 跨语言对账门 (`validate_ac_alignment.py`)**:
441
443
  * 校验后端与全栈测试用例中挂载的验收标记(标记语法由 `adapters/ac/` 的 extractor 解释)与 PRD 声明的 AC 集合严格对齐,防止实现测试漏测或假挂载。D153 起另对账测试记录的 `dims` 与 PRD 第 5 列 `后端适用覆盖维度`(维度缺口只 WARN,不随 `mode: enforce` 升 FAIL)。
442
444
  * 三形态 / 未验证语义:无 backend/fullstack app ⇒ 整门 `N/A`;未注册 extractor / apps 缺失 ⇒ `[UNVERIFIED]`(advisory WARN / enforce FAIL);零挂载 ⇒ `[ZERO-COVERAGE]`、解析失败 ⇒ `[PARSE]`、扫描面非空 ⇒ `[SCAN]`,均不伪装 PASS。
@@ -12,8 +12,55 @@ import hashlib
12
12
  import re
13
13
 
14
14
  LEVELS = ("Critical", "Major", "Minor")
15
+ ROUND_CAP = 4 # D129 过程收敛上限(转换器 / 形态门 / 台账共用同一个数)
15
16
  DISPOSITIONS = ("open", "fixed", "false_positive", "intentional", "ignored")
17
+ # `resolves` 声明只接受「关闭」四类(open 不是关闭);取证面 = fixed / false_positive
18
+ RESOLVES_STATUSES = ("fixed", "false_positive", "intentional", "ignored")
19
+ RESOLVES_EVIDENCE_REQUIRED = ("fixed", "false_positive")
16
20
  VERIFICATION_STATUSES = ("confirmed", "rejected", "inconclusive")
21
+ # `success` 是 `ocr scan`(整文件补审通道)的完成态:与 review 通道的 `complete` 同义,两家词表不同源
22
+ CLI_STATUSES = ("complete", "completed", "success", "partial", "failed", "skipped", "unknown")
23
+
24
+
25
+ def worst_level(left: object, right: object) -> str | None:
26
+ """取更严的一档(`LEVELS` 序:Critical > Major > Minor);不认识的一侧让另一侧说话。
27
+
28
+ 闭环「能否算关闭」必须按同一问题**所有出现里最严的级别**判:只看义务首现级别 ⇒
29
+ `Major 升级为 Critical` 一侧可免签关掉;只看新一轮级别 ⇒ `Critical 降级为 Major`
30
+ 一侧可免签关掉(`43dca7f` 修的是后者)。两个方向都要挡住。
31
+ """
32
+ ranks = {lvl: i for i, lvl in enumerate(LEVELS)}
33
+ # 只认字符串档位:损坏历史里的 level 可能是 list / dict(不可哈希),
34
+ # 内核不得因此抛裸 traceback(本族脚本的退出码只有 0 与 2)
35
+ lk = left if isinstance(left, str) else None
36
+ rk = right if isinstance(right, str) else None
37
+ rl, rr = ranks.get(lk), ranks.get(rk)
38
+ if rl is None:
39
+ return rk
40
+ if rr is None:
41
+ return lk
42
+ return left if rl <= rr else right
43
+
44
+
45
+ def signed_str(container: dict, key: str) -> bool:
46
+ """该字段是**非空字符串**才算数。
47
+
48
+ `by: 123` / `evidence: ["x"]` 经 `str()` 也「非空」,但那就不是可追责的签名;
49
+ 台账与阻断性 Critical 的承重件正是「谁签的、凭什么关」,宽松等于把免签通道换个写法留下。
50
+ """
51
+ value = container.get(key)
52
+ return isinstance(value, str) and bool(value.strip())
53
+
54
+
55
+ def has_signed_disposition(container: dict, by_key: str = "disposition_by",
56
+ evidence_key: str = "disposition_evidence") -> bool:
57
+ """签字 + 取证齐备且都是非空字符串。"""
58
+ return signed_str(container, by_key) and signed_str(container, evidence_key)
59
+
60
+
61
+ def count_floor(reviewed: object, expected: object) -> bool:
62
+ """N/M 只要不相等就抬 WARN 下限:M<N 是静默少审,M>N 是多报(无 coverage 时只剩自评)。"""
63
+ return isinstance(reviewed, int) and isinstance(expected, int) and reviewed != expected
17
64
 
18
65
  _LINE_STRIP_RE = re.compile(r":\d+(?:-\d+)?$")
19
66
 
@@ -24,6 +71,30 @@ def clean_target_file(target: str) -> str:
24
71
  return _LINE_STRIP_RE.sub("", s)
25
72
 
26
73
 
74
+ def normalize_paths(items: object) -> list[str]:
75
+ """coverage / 报告路径列表:取 dict.path 或裸 string,统一 `/`,去重排序。"""
76
+ out: set[str] = set()
77
+ if not isinstance(items, list):
78
+ return []
79
+ for item in items:
80
+ p = item.get("path") if isinstance(item, dict) else item
81
+ if isinstance(p, str) and p.strip():
82
+ out.add(p.strip().replace("\\", "/"))
83
+ return sorted(out)
84
+
85
+
86
+ def target_file(target: object) -> str:
87
+ """从 finding target 提取文件路径;`?` / 空 ⇒ ''(无法归属,跳过成员对账)。"""
88
+ s = str(target or "").strip()
89
+ if not s or s == "?":
90
+ return ""
91
+ s = clean_target_file(s)
92
+ if ":?" in s:
93
+ s = s.split(":?", 1)[0]
94
+ s = s.replace("\\", "/").strip()
95
+ return "" if s == "?" else s
96
+
97
+
27
98
  def compute_fingerprint(target: str, check: str, detail: str) -> str:
28
99
  """计算 finding 去重指纹(16 字符 hex)。
29
100
 
@@ -54,9 +125,7 @@ def is_blocking_critical(check: dict, schema_version: int = 2) -> bool:
54
125
  return False
55
126
  disp = check.get("disposition")
56
127
  if disp in ("fixed", "false_positive"):
57
- has_signer = bool(str(check.get("disposition_by") or "").strip())
58
- has_evidence = bool(str(check.get("disposition_evidence") or "").strip())
59
- if has_signer and has_evidence:
128
+ if has_signed_disposition(check):
60
129
  return False
61
130
  return True
62
131
 
@@ -67,7 +136,7 @@ def calculate_status(checks: list[dict], warn_floor: bool = False,
67
136
 
68
137
  状态映射规则(§3 写死 + §6 下限 + D129 处置):
69
138
  - 存在阻断性 Critical ⇒ FAIL
70
- - 存在存量 Critical、Major、或 warn_floor 为 True(M<N、预算熔断、executed=False 等)⇒ WARN
139
+ - 存在存量 Critical、Major、或 warn_floor 为 True(M≠N(少审或多报)、预算熔断、executed=False 等)⇒ WARN
71
140
  - 否则 PASS
72
141
  """
73
142
  valid_checks = [c for c in checks if isinstance(c, dict)]
@@ -81,6 +150,129 @@ def calculate_status(checks: list[dict], warn_floor: bool = False,
81
150
  return "PASS"
82
151
 
83
152
 
153
+ def cli_warn_floor(data: dict) -> bool:
154
+ """CLI 失败面 ⇒ 状态下限 WARN(工具失败 ≠ 代码 finding,不直接 FAIL)。
155
+
156
+ `partial` / `failed` 或 `failed_files` 非空都算「本次审查没跑全」;
157
+ `skipped` / `unknown` 不额外抬升,交给 `count_floor`(M≠N)等既有下限。
158
+ """
159
+ status = str(data.get("cli_status") or "").strip().lower()
160
+ failed = data.get("failed_files")
161
+ return status in ("partial", "failed") or (isinstance(failed, list) and bool(failed))
162
+
163
+
164
+ def delta_warnings(data: dict, checks: list[dict] | None = None,
165
+ other: list[dict] | None = None) -> list[str]:
166
+ """delta 对账的 WARN 面(只对 v3 生效;不改变 FAIL 优先级)。
167
+
168
+ v2 只出提示(`delta_notes`),**不进入 warn_floor**,保证旧 v2 PASS 报告不变。
169
+ v3:reviewed_files 数量/与 completed 一致性/与 failed 交集、selected 对账、target 成员。
170
+ """
171
+ if data.get("delta_only") is not True:
172
+ return []
173
+ schema_version = data.get("schema_version", 1)
174
+ if not isinstance(schema_version, int) or schema_version < 3:
175
+ return []
176
+
177
+ issues: list[str] = []
178
+ reviewed_files = normalize_paths(data.get("reviewed_files"))
179
+ completed = normalize_paths(data.get("completed_files"))
180
+ failed = normalize_paths(data.get("failed_files"))
181
+ waived = normalize_paths(data.get("waived_files"))
182
+ reused = normalize_paths(data.get("reused_files"))
183
+ selected = normalize_paths(data.get("selected_files"))
184
+ reviewed = data.get("reviewed")
185
+ expected = data.get("expected")
186
+ if isinstance(reviewed, int) and len(reviewed_files) != reviewed:
187
+ issues.append(f"`reviewed_files` {len(reviewed_files)} 个 ≠ reviewed {reviewed}")
188
+ if completed and reviewed_files and set(reviewed_files) != set(completed):
189
+ issues.append("`reviewed_files` 与 coverage.completed 不一致(显式列表优先)")
190
+ both = set(reviewed_files) & set(failed)
191
+ if both:
192
+ issues.append(f"`reviewed_files` 与 `failed_files` 有交集:{sorted(both)}")
193
+ if "selected_files" in data:
194
+ if isinstance(expected, int) and len(selected) != expected:
195
+ issues.append(f"`selected_files` {len(selected)} 个 ≠ expected {expected}")
196
+ union = set(reviewed_files) | set(failed) | set(waived) | set(reused)
197
+ if set(selected) != union:
198
+ issues.append("`selected_files` 无法被 reviewed/failed/waived/reused 解释:"
199
+ f"missing={sorted(set(selected) - union)} extra={sorted(union - set(selected))}")
200
+ for item in (checks or []) + (other or []):
201
+ if not isinstance(item, dict):
202
+ continue
203
+ p = target_file(item.get("target"))
204
+ if not p:
205
+ continue
206
+ if p in reviewed_files:
207
+ continue
208
+ if p in failed:
209
+ issues.append(f"finding target `{p}` 落在 failed_files")
210
+ elif p in waived:
211
+ issues.append(f"finding target `{p}` 落在 waived_files")
212
+ elif p in reused:
213
+ issues.append(f"finding target `{p}` 落在 reused_files(未计入 reviewed)")
214
+ else:
215
+ issues.append(f"finding target `{p}` 超出 reviewed_files")
216
+ return issues
217
+
218
+
219
+ def delta_notes(data: dict) -> list[str]:
220
+ """v2 delta 的兼容提示:只显示,不进入状态下限(旧 v2 PASS 报告保持 PASS)。"""
221
+ if data.get("delta_only") is not True:
222
+ return []
223
+ schema_version = data.get("schema_version", 1)
224
+ if not isinstance(schema_version, int) or schema_version < 3:
225
+ if not str(data.get("head_sha") or "").strip():
226
+ return ["v2 delta 缺 head_sha(兼容模式,仅提示,不改变 report status)"]
227
+ return []
228
+
229
+
230
+ def resolves_entry_errors(item: object, label: str) -> list[str]:
231
+ """**单条** `resolves` 声明的形状判据(列表级 = 本函数 + 重复指纹,见 `resolves_shape_errors`)。
232
+
233
+ 拆到逐条是为了跨轮台账:中间轮一条写歪,不该没收同轮其它合法声明的关闭证据 —— 那样
234
+ 「响亮但不阻断」就退化成「整轮凭空失忆」。语义判定仍归 `review_closure.py`。
235
+ """
236
+ if not isinstance(item, dict):
237
+ return [f"{label} 必须为对象"]
238
+ errs: list[str] = []
239
+ fp = item.get("fingerprint")
240
+ if not (isinstance(fp, str) and fp.strip()):
241
+ errs.append(f"{label}.fingerprint 必须为非空字符串")
242
+ elif fp != fp.strip():
243
+ errs.append(f"{label}.fingerprint 带首尾空白(先规范化再声明,不猜)")
244
+ if item.get("status") not in RESOLVES_STATUSES:
245
+ errs.append(f"{label}.status={item.get('status')!r} 不在闭集 {RESOLVES_STATUSES}")
246
+ if "round" in item and (not isinstance(item["round"], int) or item["round"] < 1):
247
+ errs.append(f"{label}.round 必须为 ≥1 的整数")
248
+ for key in ("by", "evidence"):
249
+ if key in item and not isinstance(item[key], str):
250
+ errs.append(f"{label}.{key} 必须为字符串(签字与取证要可追责,不给数字/数组面子)")
251
+ return errs
252
+
253
+
254
+ def resolves_shape_errors(raw: object, where: str = "resolves") -> list[str]:
255
+ """`resolves` 声明列表的形状判据(单源):调用方决定是 exit 2 还是记错。
256
+
257
+ 语义判定(取证、Critical 是否可豁免、匹配哪条义务)不在此处 —— 归 `review_closure.py`。
258
+ """
259
+ if raw is None:
260
+ return []
261
+ if not isinstance(raw, list):
262
+ return [f"{where} 必须为数组"]
263
+ errs: list[str] = []
264
+ seen: set[str] = set()
265
+ for i, item in enumerate(raw):
266
+ errs.extend(resolves_entry_errors(item, f"{where}[{i}]"))
267
+ fp = item.get("fingerprint") if isinstance(item, dict) else None
268
+ if isinstance(fp, str) and fp.strip() and fp == fp.strip():
269
+ if fp in seen:
270
+ errs.append(f"{where} 出现重复 fingerprint:{fp}")
271
+ else:
272
+ seen.add(fp)
273
+ return errs
274
+
275
+
84
276
  def validate_v2_finding(c: dict, idx: int) -> list[str]:
85
277
  """校验 v2 单条 finding 字段形态与合法性。"""
86
278
  errs: list[str] = []
@@ -96,10 +288,10 @@ def validate_v2_finding(c: dict, idx: int) -> list[str]:
96
288
  if disp not in DISPOSITIONS:
97
289
  errs.append(f"checks[{idx}].disposition={disp!r} 不在闭集 {DISPOSITIONS}")
98
290
  elif disp in ("fixed", "false_positive"):
99
- if not str(c.get("disposition_by") or "").strip():
100
- errs.append(f"checks[{idx}].disposition={disp} 必须带非空 disposition_by 签名")
101
- if not str(c.get("disposition_evidence") or "").strip():
102
- errs.append(f"checks[{idx}].disposition={disp} 必须带非空 disposition_evidence 取证")
291
+ if not signed_str(c, "disposition_by"):
292
+ errs.append(f"checks[{idx}].disposition={disp} 必须带非空字符串 disposition_by 签名")
293
+ if not signed_str(c, "disposition_evidence"):
294
+ errs.append(f"checks[{idx}].disposition={disp} 必须带非空字符串 disposition_evidence 取证")
103
295
  if "verification" in c:
104
296
  v = c["verification"]
105
297
  if not isinstance(v, dict):
@@ -108,10 +300,10 @@ def validate_v2_finding(c: dict, idx: int) -> list[str]:
108
300
  st = v.get("status")
109
301
  if st not in VERIFICATION_STATUSES:
110
302
  errs.append(f"checks[{idx}].verification.status={st!r} 不在闭集 {VERIFICATION_STATUSES}")
111
- if not str(v.get("by") or "").strip():
303
+ if not signed_str(v, "by"):
112
304
  errs.append(f"checks[{idx}].verification 必须带非空 by")
113
- if not str(v.get("evidence") or "").strip():
114
- errs.append(f"checks[{idx}].verification 必须带非空 evidence")
305
+ if not signed_str(v, "evidence"):
306
+ errs.append(f"checks[{idx}].verification 必须带非空 evidence(字符串;数组/数字不算取证)")
115
307
  return errs
116
308
 
117
309
 
@@ -119,10 +311,10 @@ def validate_v2_report_fields(data: dict) -> list[str]:
119
311
  """校验 v2 报告级扩展字段与轮次收敛规则。"""
120
312
  errs: list[str] = []
121
313
  r = data.get("round")
122
- if isinstance(r, int) and r >= 4:
314
+ if isinstance(r, int) and r >= ROUND_CAP:
123
315
  esc = data.get("escalation")
124
316
  if not isinstance(esc, dict) or not str(esc.get("to") or "").strip() or not str(esc.get("reason") or "").strip():
125
- errs.append(f"round={r} 达到或超过 4 轮收敛上限,必须登记有效 escalation(需含 to 与 reason)")
317
+ errs.append(f"round={r} 达到或超过 {ROUND_CAP} 轮收敛上限,必须登记有效 escalation(需含 to 与 reason)")
126
318
  if "delta_only" in data:
127
319
  if not isinstance(data["delta_only"], bool):
128
320
  errs.append("`delta_only` 必须为 bool")
@@ -134,6 +326,46 @@ def validate_v2_report_fields(data: dict) -> list[str]:
134
326
  esc = data["escalation"]
135
327
  if not isinstance(esc, dict):
136
328
  errs.append("`escalation` 若存在必须为对象")
137
- elif "approved_by" in esc and not str(esc.get("approved_by") or "").strip():
138
- errs.append("`escalation.approved_by` 若存在必须为非空字符串")
329
+ elif "approved_by" in esc and not signed_str(esc, "approved_by"):
330
+ errs.append("`escalation.approved_by` 若存在必须为非空字符串(拍板人可追责,不给数字面子)")
331
+ if "cli_status" in data:
332
+ cs = data["cli_status"]
333
+ if not isinstance(cs, str) or cs not in CLI_STATUSES:
334
+ errs.append(f"`cli_status`={cs!r} 不在闭集 {CLI_STATUSES}")
335
+ for key in ("completed_files", "failed_files", "waived_files", "reused_files"):
336
+ if key in data:
337
+ v = data[key]
338
+ if not isinstance(v, list) or any(not isinstance(x, str) or not x.strip() for x in v):
339
+ errs.append(f"`{key}` 必须为字符串列表且元素非空")
340
+ elif len(set(v)) != len(v):
341
+ errs.append(f"`{key}` 不得有重复项")
342
+ if "cli_warnings" in data:
343
+ v = data["cli_warnings"]
344
+ if not isinstance(v, list) or any(not isinstance(x, str) for x in v):
345
+ errs.append("`cli_warnings` 必须为字符串列表")
346
+ if "head_sha" in data and (not isinstance(data["head_sha"], str) or not data["head_sha"].strip()):
347
+ errs.append("`head_sha` 若存在必须为非空字符串")
348
+ if "selected_files" in data:
349
+ v = data["selected_files"]
350
+ if not isinstance(v, list) or any(not isinstance(x, str) or not x.strip() for x in v):
351
+ errs.append("`selected_files` 必须为字符串列表且元素非空")
352
+ elif len(set(v)) != len(v):
353
+ errs.append("`selected_files` 不得有重复项")
354
+ return errs
355
+
356
+
357
+ def validate_v3_report_fields(data: dict) -> list[str]:
358
+ """v3 报告:delta 必须有 head_sha + 非空且不重复的 reviewed_files。"""
359
+ errs: list[str] = []
360
+ if data.get("delta_only") is True:
361
+ head = data.get("head_sha")
362
+ if not isinstance(head, str) or not head.strip():
363
+ errs.append("v3 delta 报告必须提供非空 `head_sha`")
364
+ rf = data.get("reviewed_files")
365
+ if not isinstance(rf, list) or not rf:
366
+ errs.append("v3 delta 报告必须提供非空 `reviewed_files` 列表")
367
+ elif any(not isinstance(x, str) or not x.strip() for x in rf):
368
+ errs.append("`reviewed_files` 必须为字符串列表且元素非空")
369
+ elif len(set(rf)) != len(rf):
370
+ errs.append("`reviewed_files` 不得有重复项")
139
371
  return errs
package/install.sh CHANGED
@@ -322,6 +322,19 @@ say " 资产根 : $ASSETS_ROOT"
322
322
  say " ADR 根 : $ADR_DIR_REL"
323
323
  say ""
324
324
 
325
+ # P3-b:active 消融期间拒绝 --update 覆盖 .atlas/(--force + ATLAS_ALLOW_ABLATION_OVERWRITE=1 越过)
326
+ if [ "$IS_UPDATE" -eq 1 ]; then
327
+ set +e
328
+ python3 "$SCRIPT_DIR/scripts/ablation.py" status --root "$TARGET" --exit-active --quiet 2>/dev/null
329
+ ABLATION_STATUS=$?
330
+ set -e
331
+ if [ "$ABLATION_STATUS" -eq 10 ] && { [ "$FORCE" -ne 1 ] || [ "${ATLAS_ALLOW_ABLATION_OVERWRITE:-0}" != "1" ]; }; then
332
+ say "错误:目标存在未关闭的消融事务(ablation active);先运行 ablation restore;"
333
+ say " 确需覆盖:--force + ATLAS_ALLOW_ABLATION_OVERWRITE=1(会放弃消融现场)。"
334
+ exit 2
335
+ fi
336
+ fi
337
+
325
338
  # ---------------------------------------------------------------- 1. 整包 → .atlas/
326
339
 
327
340
  say "[1/6] 装配 atlas 包到 .atlas/"
@@ -575,6 +588,7 @@ check "testid 薄桩生成器" "$ATLAS_DEST/scripts/gen_testid_stub.py"
575
588
  check "审查输入清单生成器" "$ATLAS_DEST/scripts/make_review_inputs.py"
576
589
  check "变异证明工具" "$ATLAS_DEST/scripts/mutation_proof.py"
577
590
  check "atlas 自检器" "$ATLAS_DEST/scripts/atlas_check.py"
591
+ check "消融事务工具" "$ATLAS_DEST/scripts/ablation.py"
578
592
  check "E2E 评审控制台" "$ATLAS_DEST/scripts/e2e_console.py"
579
593
  check "控制台静态资源" "$ATLAS_DEST/templates/e2e/console/index.html"
580
594
  check "适配器脚手架" "$ATLAS_DEST/adapters/_scaffold.py"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-project-harness",
3
- "version": "0.2.0-beta.25",
3
+ "version": "0.2.0-beta.26",
4
4
  "description": "践行 Project Harness 的全流程 AI 编程工作流治理引擎",
5
5
  "bin": {
6
6
  "atlas": "bin/atlas.js"