atlas-project-harness 0.2.0-beta.21 → 0.2.0-beta.22

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## [v0.2.0-beta.22] - 2026-10-30
8
+
9
+ ### 🧭 FAIL 修复指引试点(RFC-20261030-01)
10
+
11
+ * **新增**:`validate_testids` / `validate_e2e_index` / `validate_ac_alignment` 的 7 条核心 FAIL 追加 `|修复: <最小动作>|复跑: <命令>` 后缀;不改判定、状态与退出码,PASS / WARN / N/A 不加。
12
+ * **命令自适配**:装配态提示 `python3 .atlas/scripts/atlas_check.py --fast`,源包态提示 `python3 scripts/atlas_check.py --fast`,不依赖全局 CLI。
13
+ * **文档**:`docs/concepts/architecture.md` 新增「Agent Harness 五子系统 ⇄ Atlas 资产」类比映射;`shared/gates.md` 登记反馈格式约定。
14
+ * **同步面**:`shared/gates.md` 门册计数(`tests/test_validators.py` 168 条 / `tests/test_validate_ac_alignment.py` 62 条)、`docs/map/gates.md` 行数与测试文件数(36 → 42)。
15
+
16
+ ---
17
+
7
18
  ## [v0.2.0-beta.21] - 2026-10-29
8
19
 
9
20
  ### 🐛 api 适配器运行时 schema 只认构造调用(D172 / #46)
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.21-blue.svg" alt="Version" />
4
+ <img src="https://img.shields.io/badge/version-0.2.0--beta.22-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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-project-harness",
3
- "version": "0.2.0-beta.21",
3
+ "version": "0.2.0-beta.22",
4
4
  "description": "践行 Project Harness 的全流程 AI 编程工作流治理引擎",
5
5
  "bin": {
6
6
  "atlas": "bin/atlas.js"
package/shared/gates.md CHANGED
@@ -31,6 +31,8 @@
31
31
 
32
32
  > **上限声明(2026-10-15 改写,D79 真跑裁决)**:用户 2026-09-28 曾裁定**暂不真跑**(历史留痕:`3509 §111` 与 `3508 §111` 同裁定两处均记);**2026-10-04 E2E 真跑门已逐页真跑并咬出真红**(`3508 §166`/`§187`/`§190`,见 2.2「E2E 真跑」行)——历史裁定已被执行推翻,**全表上限改为逐格**:`真跑级` 只在字面咬过该门的行上成立,必须附咬合证据(哪条用例、哪个栈、是否咬过红);未真跑过的行保持出厂级(本轮/历史实测),**不得**给无咬合的行预填真跑级。
33
33
  > **本表本身不冒充已取证**:`待复核` 是**可清零**的待办 —— 2026-09-28 已逐条补齐(见各行的取证等级列);本轮新加的门一律标 `出厂级(本轮实测)` 并附变异编号。
34
+ >
35
+ > **FAIL 反馈格式(2026-10-30 试点,`RFC-20261030-01`)**:`validate_testids` / `validate_e2e_index` / `validate_ac_alignment` 的 FAIL detail 统一以 `|修复: <最小动作>|复跑: <命令>` 结尾;复跑命令按装配态自动选 `.atlas/scripts/atlas_check.py` 或源包态 `scripts/atlas_check.py`。这是 agent 自修复的反馈契约,不是新门;PASS / WARN / N/A 不加,未覆盖的校验器不强制。
34
36
 
35
37
  ## 2. 登记表
36
38
 
@@ -41,11 +43,11 @@
41
43
  | `validators/validate_structure.py` | structure 产物 ⇔ 结构环契约 | `[机器]` `_meta.json` / `tour.md` / `domains/<d>.md` / `global/` / `{current,legacy}/<domain>/` 的存在与形态、事实区标记(`rings/structure/reference.md` §7)+ **routes 语义列留空 ⇒ WARN 指名**(`用途`/`关键交互`/`实现状态`,WARN 不置红但门总览可见,2026-09-29,`§B121`)+ **D 类接线态 ⇔ 形态 / 缺口登记一致**(`adapters.api` 已接线但 `categories.D` 锚定声明为指针 / 未取证形态、或 `adapter_gaps` 登记 `api` ⇒ `WARN` 指名;跨 `product/stack-profile.yaml` 声明侧对账,profile 缺失 / api 未声明 ⇒ 不产行;2026-10-15) | 事实**内容是否与代码一致**(那是对账平面 `structure↔代码`,见 §4 缺口);语义列**是否写对**(仍归人;WARN 只判留空,「—」算合法占位);profile 的**字段合法性**(归 `validate_stack_profile`,本器只在两边都存在时判接线态矛盾) | 结构环收口(`after_archive` hook 刷事实区之后);**调度者 = `atlas_check`**(`§B120`:纳入 `VALIDATORS` + 三态前置,结构环未跑 ⇒ `SKIP`) | `出厂级(本轮实测)`:**新增 `tests/test_structure_validator.py`**(六侧坏输入:缺 `_meta.json` / 域 id 非法重复 / 缺 `apis.md` / `paths:` 写标量 / 来源文件不可解析 / 列头不符)+ **M14**(类别覆盖判据恒真 ⇒ 红)+ **M26**(语义列判据恒不告警 ⇒ WARN 测试红)+ 实测咬合(判据落地首跑即对 6 行空语义路由指名,补语义后复绿)+ **F38(2026-10-15)**:`categories.A/H` 判据由**全文子串匹配**改**锚定声明位置**(枚举 + 显式分隔符;issue #6 —— 旧实现把「描述未完成」的措辞判成「已完成」,而 H 恰是 adopt 下唯一不得 `skipped` 的类别)+ **M60** 判据退回全文子串匹配 ⇒ `test_f38_categories_prose_mention_is_not_a_declaration` 红(`3509 §119`/`§B121`)+ **F42(2026-10-15,issue #3 ④)**:`_meta.json.adapter_gaps` 非空 ⇒ `WARN` 逐条指名(未接 ≠ 缺陷,故不置红;但必须**可见**——不登记则「设计结论:源即真相不接」与「工具覆盖不足:判定表判接但 0 命中」在产物里同形,采用者会把后者读成前者),形态非法 ⇒ `FAIL` + **M63** 摘掉 `add_warn` 分支 ⇒ `test_f42_adapter_gaps_are_warned_not_failed` 红 + **F43(2026-10-15)**:D 类接线态混写判据(F38 锚定声明同族;同日实测咬合:标签前缀正则会把「未取证」本身当 `<标签>:` 前缀吃掉 ⇒ 判据顺序定为「声明位直取优先、标签兜底」)+ **M64** 摘判定分支 ⇒ `test_f43_wired_api_with_pointer_d_warns` 红 + **F44(2026-10-15)**:`apis.md` 指针形态判据由全文子串 `"真相源" in t` 改**锚定声明**(行首 / 单元格首个 token 接显式分隔符——散文提及的「真相源」不再把清单型文件误判「混写」假 FAIL;同族 issue #6 的兄弟残留)+ **M66** 判据退回全文子串 ⇒ `test_f44_list_form_prose_mention_of_zhenxiangyuan_not_flagged` 红(还原逐字节一致) |
42
44
  | `validators/validate_stack_profile.py` | 项目声明 ⇔ 共享契约 §2/§3.3 | `[机器]` 字段契约、`origin` 枚举、`apps[]` 枚举与路径、`adapters` 名字可解析、**`graph` 段契约(键白名单 {`backend`, `pinned`} + backend 枚举 {`cgc`} + pinned 语义版本形态;段缺失 = 合法,声明非法 = FAIL,2026-10-05 图谱批 1)**;**`e2e.app_login.mode` 枚举(real/fabricated/none,缺省 real)+ `body_format` 枚举(form/json,缺省 form)+ `mode: none` 字段互斥 + 登录形态未声明 WARN**(`app_base_url` 已声明而 `app_login` 未声明 ⇒ WARN 指名,中性文案;2026-10-16,`RFC-20261015-02` / issue #11) | `prototype` 段 = N/A(E1 退役) | 适配器**跑出来的 JSON 对不对**(那是适配器自己的事);`stack-profile` 与代码实际栈是否相符;**cgc 是否真装了 / 版本是否对齐 pin**(运行时消费者显式 WARN / not_ready,本器只判声明形态) | 装配 / 任一环启动前;**调度者 = `atlas_check`**(`§B120`:纳入 `VALIDATORS` + 三态前置,无 stack-profile ⇒ `SKIP`) | `出厂级(历史实测)`:`test_stack_profile_origin_vocabulary_in_sync`(词表守恒,加第三个取值 ⇒ 红)+ **M27**(调度判据⑥:从 `VALIDATORS` 摘本门 ⇒ 红,`§B120`)+ **图谱批 1**:`tests/test_stack_profile_graph.py`(未知 backend / 未知键 / pinned 非法 / 标量段 ⇒ 各自红;`graph: null` 与缺失 ⇒ 合法)+ `test_graph_backend_vocabulary_in_sync`(GRAPH_BACKENDS/GRAPH_KEYS ⇔ 契约正文守恒)+ **RFC-20261015-02(issue #11)**:`test_stack_profile_app_login_none_mode_is_explicit_not_degraded`(none = 显式声明 PASS / 残留字段 FAIL;含 **APP_LOGIN_MODES 词表守恒钉**——此前该 MODES 族是唯一无守恒钉的)/ `test_stack_profile_login_shape_undeclared_warns_neutrally`(WARN 中性文案 + 未到真跑不提前吹);变异 **M69**(none 互斥判据卸恒真 ⇒ 残留夹具绿)/ **M70**(WARN 前提挂错缺省就吹 ⇒ 未到期项目变吹) |
43
45
  | `validators/validate_prd.py` | PRD 产物 ⇔ PRD 环契约 | `[机器]` 文档头 / 零占位符 / 链接可达 / 功能→规则→AC 链路 / 范围形态与 §六 形态一致 / 最小必填集 / 追溯总表计数对账 / **追溯总表行形态与未覆盖登记** / **§五 AC 表列头(三列、不得含 `样例类型`)** / **域间依赖**(域级 frontmatter `depends_on` ⇔ 全局 §五 mermaid **实线**边集:悬空引用 / 实线子图成环 / 不一致 ⇒ FAIL;虚线(增强链路・数据回流)不入判据;无声明或无图 ⇒ N/A。**节点写法 / 箭头方向 / 连字符 / 含空格中文名的规格见 `rings/prd/reference.md` §4 的「§五 mermaid 写法约定」**——2026-10-15 补,issue #7:判据存在却无对应规格 = 把契约藏进了实现,只读契约的执行者过不了门。2026-10-04 补,`3509 §B186-1`) / **清单自洽(结构类 + 不可验)**(两清单小节里的 AC 必须在本文档 AC 表存在——幽灵豁免会给出覆盖门开后门;2026-10-12 补,`3509 §B201-3`)+ **域级 PRD 核心资产门(二/四/五章及结构类清单小节必填,issue #29)** | AC 是否**真的可判定**、`Then` 是否断言实现、四类样例覆盖(**已移交 E2E 环**,`3509 §B74`)、`Then` 与用例断言是否同源 | 域级 PRD 落笔后(`apply` / 无需求卡修订收口) | `出厂级(本轮实测)`:M1 删「`样例类型` 列」判据 / M2 删「未覆盖行数」判据 / M3 删「行形态非法」判据 ⇒ 各自红;另有历史 M1–M3(`3509 §B109` 链路);**M6** 实线正则改吃虚线(`-.?->`)/ **M7** 删成环判据 / **M8** 删不一致判据 ⇒ 各自红(2026-10-04 实测,随域间依赖门 +4 钉子);**M61(2026-10-15,issue #7)** 节点 id 正则退回 `\w+` ⇒ `test_f39_hyphenated_slug_nodes_resolve` 红 / `domain_name` 退回 `\S+` ⇒ `test_f39_spaced_chinese_domain_name_resolves` 红 / 边抽取退回整段 `findall` ⇒ `test_f39_chained_edges_are_not_silently_dropped` 与 `test_f39_labelled_edges_are_not_silently_dropped` 红(链式与边标签本来就是**静默丢边**);**变更记录形态与可无项显式(2026-10-23,`RFC-20261023-02` ③④ / D146/D147)**:§七 5 列 PASS / legacy 3 列 WARN / 表头不可辨或「影响」空 ⇒ FAIL;§六 空表/空节无 `无` ⇒ WARN 首版(翻转触发器与统计落点见 `rings/prd/reference.md` §7);两条均为**独立 check 行**(不并入参数 4 聚合,避免 WARN 被聚成 FAIL) |
44
- | `validators/validate_testids.py` | testid **应然(用例分片)⇔ 实然(前端源码)** | `[机器]` 按页双向集合(`已实现` 页必须相等;`实现中`/`未开始` 降 WARN + 清单;测试目录/文件与夹具不入实然抽取面,issue #27/#33)、命名规范(分片新声明命名违规 ⇒ FAIL;存量实然命名违规降 WARN,契约 §4 保持现状承诺)、全站唯一、未实现页 SKIP、归不到页的实然 WARN;**模板字面量展开**(`data-testid={`…${slug}…`}`,页 slug = 页面表、其余 = 同文件静态属性;2026-10-04 互锁修复)+ **应然落点分档(已实现页声明了但不对 FAIL;未实现/实现中页待落地 WARN,issue #34)** | testid 名字**取得对不对**(语义);元素是否真该有 testid(语义);**模板展开值的「多余」**(外壳挂载范围静态未知 ⇒ 只判缺失不判多余,2026-10-04);`data-atlas-panel` 是否标(运行期门用得到,但本器不判) | **该页源码出现时**(静态对账,零执行成本)——检查点搭便车于必然存在的工件 | `出厂级(历史实测)`:`3509 §B114` 轮(实然侧失明 ⇒ 5 红)+ `§B106` 轮 M1/M2 + **P5-A(2026-10-23,D141)**:提取器每文件复用一次 `strip_comments`(原两次),输出逐字不变;隔离副本 5 轮交错实测 **-110ms / -13.8%**;钉 = `test_validate_testids_strip_comments_once_per_file`(调用次数 = 文件数);变异 **M137**(还原二次调用 ⇒ 钉红,逐字节还原一致) |
45
- | `validators/validate_e2e_index.py` | 索引 ⇔ 分片 ⇔ 契约 | `[机器]` 结构门(列头 / ID 唯一 / 链接与标题一致 / 字段齐 / 无孤儿死链 / `pages` 合法 / 反向视图一致 / 词表 / `关联AC` 格式与可达)+ 有意义门(空占位 / 不可判定词 / 纯跳转 / 瞬时断言 / 未复断言 / 「或」式 / 文案来源(**先剥 `#` 注释**,`§B95`)/ `initial:` 无理由 / `intent` / 断言数 / `skipped` 原因 / **`goto` 路径 ∈ 页面表**(2026-09-30 补)/ **`auth` 取值合法**(2026-09-30 补))+ **seed × models 对账**(`seed:` 的实体×字段 ⇔ structure 的 `data-models` 事实;无事实 ⇒ `NA`)+ **AC 反向覆盖**(追溯门,2026-10-04 补 `§B186-2`:域内页面全 `已实现` ⇒ 该域 §五 定义的每个 AC 须被 ≥1 条 `关联AC` 指向,零挂靠 ⇒ **WARN** + 逐域清单;含 `实现中`/`未开始` 页面的域不参与;覆盖关系为派生量、不落第 4 张手工表;豁免落点定形后升 FAIL) | 「这条断言是否真的异源」(语义,§12 独立审查);`precondition` 是否说谎(语义);**零步基线(运行期门,见 2.4)**;播种**是否真执行**(归生成物 + 项目钩子) | 用例落笔后(`apply` / 修订收口) | `出厂级(本轮实测)`:`tests/test_validators.py` **162 条**本轮跑绿(2026-10-23 D142 request 断言形态 +1、D143 manual 块形态 +1)(2026-10-23 D141 P5-A 提取器复用 +1;2026-10-23 D134 type 动作侧 +1;2026-10-22 D132 hash 查询参数 +7;2026-10-21 D130 覆盖维度 +1;2026-10-19 D113「动态名诊断」+2:动态名命中模板结构 ⇒ 提示改指 §4 正规形态且不再宣称已失效的豁免(正向 1)/ 真漏实现不附动态指引(反向 1);变异 M96(整段回退旧诊断)/ M97(只退判据为词形子串);2026-10-18 D109「宿主注入型 URL 参数」通道 +7:门重靶 1 改名(`goto 不带查询参数` → `goto 查询键须已声明`,判据 = 键须 ∈ `e2e.query_preset.keys` ∧ ∈ 本用例 `参数:` 声明)+ 新门 3(`参数 声明有效` / `参数 值形态` / `会话·参数 同键不叠写`)+ 正向 1 + 坏输入 6(未声明白名单 / 键在白名单外 / 声明带值 / 死声明 / 值形态越界 / 同键双载体);2026-10-17 `dispatch` 动词值域门 +1(第三位 ⇒ `step 开关取值合法` FAIL / 缺省 ⇒ PASS + `step 动词合法` PASS;与生成器中止双保险——治旧安装副本「陌生第三词静默吸收」,2026-10-17 v3 现场)/ 2026-10-16 issue #R2 空 `testid:` 出声钉 +1(AC-3/R2:空应然清单 ⇒ `分片块字段齐` FAIL,文本期把「空守卫面」挡下)/ 2026-10-16 issue #R3 守卫门内剥注释钉 +1(**绕过 `parse_block`** 回注行内注释的坏输入 ⇒ 撤掉 F3 门内剥注释即红)/ 同日 issue #R4 `click`/`hover` 开关值域门 +1(坏值 ⇒ `step 开关取值合法` FAIL / `native` ⇒ PASS);2026-10-16 issue #23 守卫路径门 +2:正向 1(`/home/42` 前缀命中 `/home` ⇒ PASS)/ 坏输入 1(页面表外路径 ⇒ FAIL);同日 issue #13 会话预设门 +4:正向 1(键在白名单内 ⇒ PASS)/ 坏输入 3(未声明白名单 ⇒ FAIL / 键在白名单外 ⇒ FAIL / 行缺 `=` ⇒ FAIL);同日 issue #20 应然落点门 +4:正向 1(落点齐且可机检 ⇒ PASS)/ 坏输入 3(指向不存在文件 ⇒ FAIL / 文件内解析不到该 id ⇒ FAIL / 未登记落点 ⇒ WARN);同日 issue #19 同文件静态字符串常量 + 单层引用解析 +4:正向 1(常量引用 ⇒ PASS)/ 坏输入 3(非静态初始化式 ⇒ 不解析报缺失 / 跳文件导入 ⇒ 不解析 / 同常量被引两次 ⇒ 同文件重复 WARN);2026-10-16 F45 用例 ID 文法放宽「PAGE 段可含连字符」+3:多段可收 / 六类反例仍拒 / 单源接线;2026-10-12 随 §5.8 打桩门 +2:stack_profile mock/app_login.mode 门、e2e_mocks 门矩阵;同日 B201-1 upload 门 +1、B201-3 不可验豁免门 +2(豁免减法 / 幽灵豁免);2026-10-13 命名门存量实然豁免 +1;2026-10-14 F20 存量 adopt 模式未引 testid 降级 +1;2026-10-14 mock.mode auto 与 fixtures_dir +1;2026-10-15 v0.1.0-beta.14 +3)(含 seed×models 四侧 + **M16**;索引坏输入侧:孤儿 / 死链 / 标题不一致 / 词表外状态…)(`3509 §119`/`§121`;2026-09-30 随 goto/auth 门 +2;2026-10-04 随 **AC 反向覆盖门** +3:已实现域零挂靠 ⇒ WARN 吹 / 全覆盖 ⇒ PASS / 含未实现页的域不参与 ⇒ 不吹;**D130 覆盖维度**:`_parse_dims` 前后界定符(`C01正常流` 中文邻接须命中)+ `C01-C03` 区间展开 + 未知写法出声,钉 = `test_e2e_index_coverage_dimensions_pass_and_warn`(N/A / WARN / PASS / 未知维度 四态),变异 **M131**(摘维度缺口聚合 ⇒ 缺口钉不红)四段协议还原一致) |
46
+ | `validators/validate_testids.py` | testid **应然(用例分片)⇔ 实然(前端源码)** | `[机器]` 按页双向集合(`已实现` 页必须相等;`实现中`/`未开始` 降 WARN + 清单;测试目录/文件与夹具不入实然抽取面,issue #27/#33)、命名规范(分片新声明命名违规 ⇒ FAIL;存量实然命名违规降 WARN,契约 §4 保持现状承诺)、全站唯一、未实现页 SKIP、归不到页的实然 WARN;**模板字面量展开**(`data-testid={`…${slug}…`}`,页 slug = 页面表、其余 = 同文件静态属性;2026-10-04 互锁修复)+ **应然落点分档(已实现页声明了但不对 FAIL;未实现/实现中页待落地 WARN,issue #34)** | testid 名字**取得对不对**(语义);元素是否真该有 testid(语义);**模板展开值的「多余」**(外壳挂载范围静态未知 ⇒ 只判缺失不判多余,2026-10-04);`data-atlas-panel` 是否标(运行期门用得到,但本器不判) | **该页源码出现时**(静态对账,零执行成本)——检查点搭便车于必然存在的工件 | `出厂级(历史实测)`:`3509 §B114` 轮(实然侧失明 ⇒ 5 红)+ `§B106` 轮 M1/M2 + **P5-A(2026-10-23,D141)**:提取器每文件复用一次 `strip_comments`(原两次),输出逐字不变;隔离副本 5 轮交错实测 **-110ms / -13.8%**;钉 = `test_validate_testids_strip_comments_once_per_file`(调用次数 = 文件数);变异 **M137**(还原二次调用 ⇒ 钉红,逐字节还原一致);**RFC-20261030-01(2026-10-30)**:+2 修复指引钉(双向集合 `missing` / `orphan` 方向、应然落点);变异 **M159a/M159d** 四段协议、还原逐字节一致 |
47
+ | `validators/validate_e2e_index.py` | 索引 ⇔ 分片 ⇔ 契约 | `[机器]` 结构门(列头 / ID 唯一 / 链接与标题一致 / 字段齐 / 无孤儿死链 / `pages` 合法 / 反向视图一致 / 词表 / `关联AC` 格式与可达)+ 有意义门(空占位 / 不可判定词 / 纯跳转 / 瞬时断言 / 未复断言 / 「或」式 / 文案来源(**先剥 `#` 注释**,`§B95`)/ `initial:` 无理由 / `intent` / 断言数 / `skipped` 原因 / **`goto` 路径 ∈ 页面表**(2026-09-30 补)/ **`auth` 取值合法**(2026-09-30 补))+ **seed × models 对账**(`seed:` 的实体×字段 ⇔ structure 的 `data-models` 事实;无事实 ⇒ `NA`)+ **AC 反向覆盖**(追溯门,2026-10-04 补 `§B186-2`:域内页面全 `已实现` ⇒ 该域 §五 定义的每个 AC 须被 ≥1 条 `关联AC` 指向,零挂靠 ⇒ **WARN** + 逐域清单;含 `实现中`/`未开始` 页面的域不参与;覆盖关系为派生量、不落第 4 张手工表;豁免落点定形后升 FAIL) | 「这条断言是否真的异源」(语义,§12 独立审查);`precondition` 是否说谎(语义);**零步基线(运行期门,见 2.4)**;播种**是否真执行**(归生成物 + 项目钩子) | 用例落笔后(`apply` / 修订收口) | `出厂级(本轮实测)`:`tests/test_validators.py` **168 条**本轮跑绿(2026-10-23 D142 request 断言形态 +1、D143 manual 块形态 +1)(2026-10-23 D141 P5-A 提取器复用 +1;2026-10-23 D134 type 动作侧 +1;2026-10-22 D132 hash 查询参数 +7;2026-10-21 D130 覆盖维度 +1;2026-10-19 D113「动态名诊断」+2:动态名命中模板结构 ⇒ 提示改指 §4 正规形态且不再宣称已失效的豁免(正向 1)/ 真漏实现不附动态指引(反向 1);变异 M96(整段回退旧诊断)/ M97(只退判据为词形子串);2026-10-18 D109「宿主注入型 URL 参数」通道 +7:门重靶 1 改名(`goto 不带查询参数` → `goto 查询键须已声明`,判据 = 键须 ∈ `e2e.query_preset.keys` ∧ ∈ 本用例 `参数:` 声明)+ 新门 3(`参数 声明有效` / `参数 值形态` / `会话·参数 同键不叠写`)+ 正向 1 + 坏输入 6(未声明白名单 / 键在白名单外 / 声明带值 / 死声明 / 值形态越界 / 同键双载体);2026-10-17 `dispatch` 动词值域门 +1(第三位 ⇒ `step 开关取值合法` FAIL / 缺省 ⇒ PASS + `step 动词合法` PASS;与生成器中止双保险——治旧安装副本「陌生第三词静默吸收」,2026-10-17 v3 现场)/ 2026-10-16 issue #R2 空 `testid:` 出声钉 +1(AC-3/R2:空应然清单 ⇒ `分片块字段齐` FAIL,文本期把「空守卫面」挡下)/ 2026-10-16 issue #R3 守卫门内剥注释钉 +1(**绕过 `parse_block`** 回注行内注释的坏输入 ⇒ 撤掉 F3 门内剥注释即红)/ 同日 issue #R4 `click`/`hover` 开关值域门 +1(坏值 ⇒ `step 开关取值合法` FAIL / `native` ⇒ PASS);2026-10-16 issue #23 守卫路径门 +2:正向 1(`/home/42` 前缀命中 `/home` ⇒ PASS)/ 坏输入 1(页面表外路径 ⇒ FAIL);同日 issue #13 会话预设门 +4:正向 1(键在白名单内 ⇒ PASS)/ 坏输入 3(未声明白名单 ⇒ FAIL / 键在白名单外 ⇒ FAIL / 行缺 `=` ⇒ FAIL);同日 issue #20 应然落点门 +4:正向 1(落点齐且可机检 ⇒ PASS)/ 坏输入 3(指向不存在文件 ⇒ FAIL / 文件内解析不到该 id ⇒ FAIL / 未登记落点 ⇒ WARN);同日 issue #19 同文件静态字符串常量 + 单层引用解析 +4:正向 1(常量引用 ⇒ PASS)/ 坏输入 3(非静态初始化式 ⇒ 不解析报缺失 / 跳文件导入 ⇒ 不解析 / 同常量被引两次 ⇒ 同文件重复 WARN);2026-10-16 F45 用例 ID 文法放宽「PAGE 段可含连字符」+3:多段可收 / 六类反例仍拒 / 单源接线;2026-10-12 随 §5.8 打桩门 +2:stack_profile mock/app_login.mode 门、e2e_mocks 门矩阵;同日 B201-1 upload 门 +1、B201-3 不可验豁免门 +2(豁免减法 / 幽灵豁免);2026-10-13 命名门存量实然豁免 +1;2026-10-14 F20 存量 adopt 模式未引 testid 降级 +1;2026-10-14 mock.mode auto 与 fixtures_dir +1;2026-10-15 v0.1.0-beta.14 +3)(含 seed×models 四侧 + **M16**;索引坏输入侧:孤儿 / 死链 / 标题不一致 / 词表外状态…)(`3509 §119`/`§121`;2026-09-30 随 goto/auth 门 +2;2026-10-04 随 **AC 反向覆盖门** +3:已实现域零挂靠 ⇒ WARN 吹 / 全覆盖 ⇒ PASS / 含未实现页的域不参与 ⇒ 不吹;**D130 覆盖维度**:`_parse_dims` 前后界定符(`C01正常流` 中文邻接须命中)+ `C01-C03` 区间展开 + 未知写法出声,钉 = `test_e2e_index_coverage_dimensions_pass_and_warn`(N/A / WARN / PASS / 未知维度 四态),变异 **M131**(摘维度缺口聚合 ⇒ 缺口钉不红)四段协议还原一致);**RFC-20261030-01(2026-10-30)**:+4 修复指引钉(行字段 / 孤儿死链 / 分片块 / PASS 无后缀);变异 **M159b/M159d** 四段协议、还原逐字节一致 |
46
48
  | `scripts/gen_e2e_scripts.py` | 用例分片 ⇔ 运行器脚本(派生) | `[机器]` 动词词表、testid 清单守卫、`--target` 已退役、URL = 索引页面表、**登录形态(`e2e.app_login.mode` = real / fabricated / none)+ 取证四值;`none` ⇒ 不生成登录预置设施、不打警告(声明形态非降级,段内取证字段残留 ⇒ 生成报错;`RFC-20261015-02`)**、未声明(第一阶段 = 占位生成 + WARN 升档预告;真跑准备日并入 `not_ready`)、审查门(有 `Critical` 拒绝写盘)、**前置未声明 ⇒ 退出码 3 + `not_ready`**、**`seed:` 行 ⇒ `_seed(...)` 调用**(未声明 `e2e.seed.hook` ⇒ 生成期 WARN + 运行期警告) | 深断言(放 `_support/`,不覆盖);运行器本身的正确性;播种**钩子实现**是否正确(项目自定) | 用例定稿后(生成即对账) | `出厂级(本轮实测)`:M2 未就绪退回退出码 2 ⇒ 红;**M17 不发射 seed 调用** ⇒ 红;另有历史变异(回潮硬编码 ⇒ 3 红,`ATLAS-UPSTREAM #113`)+ **RFC-20261015-02(issue #11)**:`test_setup_mode_none_generates_no_login_facilities`(**M71** none 分支仍生成 setup/storageState ⇒ 红)/ `test_setup_mode_none_rejects_leftover_evidence_fields`(残留取证字段 ⇒ 生成报错)/ `test_setup_without_app_login_degrades_loudly`(未声明 = 占位生成 + WARN 指路,升档后改推 `not_ready: "app_login"`)/ `test_app_login_section_incomplete_fails_generation`;**`dispatch` 动词(2026-10-17,v3 现场实证补)**:`dispatch <tid>` = React 合成事件直发(`dispatchEvent('click')`,不经 hit-testing / 可点性检查,不追加 force——常态遮罩+入场动画表面 force/native 双模实测双灭后的第三条动能,探针实证动画窗内 0 节拍直发两删连中;适用边界同 `native` 家族:被测属性 = 可操作性时不得用);样例守恒表 +1 词条、发射/值域钉 +1(`test_dispatch_verb_emits_synthetic_event_without_force`,第三位 ⇒ 报错中止);**`type` 逐字输入动词(2026-10-23,D134)**:`type <tid> <text>` = `pressSequentially`(逐字触发 keydown/keypress/input;不清空原值;空值中止);样例守恒 +1、发射钉 +1(`test_type_verb_emits_press_sequentially_without_clearing`) |
47
49
  | `validators/validate_design_exemptions.py`(2026-10-08,`#182`) | 设计档案 detect 豁免 ⇔ `.impeccable/config.json` `detector.ignoreRules` | `[机器]` 双向集合相等:`product/design.md` 内 `detect:<rule-id>` ⇔ config ignoreRules,单侧漂移 ⇒ FAIL 指名(配置缺 = 报警没真关 / 档案缺 = 关报警没台账);无档案 ⇒ SKIP(三态 `§B120`);config 损坏 ⇒ FAIL | 豁免理由是否成立(语义,人侧终审);rule id 是否存在于 impeccable 规则表(不内嵌第三方规则表——升级会误伤);档案其余事实行内容(frontend-design skill 辖区) | 档案 / 配置改动后 + 每批收口 | `出厂级(本轮实测)`:`tests/test_validate_design_exemptions.py` **8 条**(**M43** 摘双向差集判据 ⇒ 两侧漂移钉子双红) |
48
- | `validators/validate_ac_alignment.py`(2026-10-12,`3507 BT` / B209-1 Phase 1) | 后端与全栈测试用例 ⇔ PRD 验收标准(AC)资产 | `[机器]` ① 无主 AC 拦截(测试挂载不存在的 AC ID ⇒ FAIL);② 无效跳过拦截(测试被 skip/xfail 但无 waiver 理由 ⇒ FAIL);③ 双向覆盖对账(输出已声明 AC 与测试挂载覆盖比;enforce 模式下零测试挂载 ⇒ FAIL);④ 差量防退化(对比基线快照,挂载测试数与覆盖 AC 数净增不得为负 ⇒ WARN);⑤ **AC × 覆盖维度(后端通道,D153)**:读域级 PRD 可选列 `后端适用覆盖维度` ⇔ 归一化测试记录的 `dims`;缺维度 / 仅弱证据 / 零测试 / 未知 / 孤儿 ⇒ WARN;无合法后端列、未声明 `backend_test` ⇒ N/A;多 backend app 用统一 `test_dir` 做全局对账;无 backend/fullstack app ⇒ 整门 N/A;apps 缺失 / 未注册 extractor ⇒ `[UNVERIFIED]` WARN(enforce 下 FAIL);parse_errors ⇒ `[PARSE]` WARN(enforce 下 FAIL);零挂载 ⇒ `[ZERO-COVERAGE]` WARN(enforce 下 FAIL);扫描面非空 ⇒ `[SCAN]` WARN;逐 PRD/逐表聚合,错位/备注含维度形态 ⇒ WARN | 测试断言是否语义有效(Phase 2/3 变异测试与证据链承接);未注册 / 不可用的测试提取器(走 `adapters/ac/` 注册表;非 Python 后端需提供 extractor);**E2E 第 4 列(D130 归 validate_e2e_index,不并集)**;**维度是否真被断言(语义,归独立审查)**;**资产是否真执行(执行证据归 E2E 真跑 / backend_test.command)** | 后端代码实现完成 / 收口;**调度者 = `atlas_check`**(三态前置:无 PRD 资产 ⇒ `SKIP`;无 PRD AC 且无测试 ⇒ `N/A`) | `出厂级(本轮实测)`:`tests/test_validate_ac_alignment.py` **59 条**(既有四维 7 条 + D153 后端维度 32 条 + Batch A/B/C1/C2/D2a/D2b 20 条;无主 AC 报 FAIL / 无 waiver skip 报 FAIL / 有 waiver 放行 / enforce 零测试挂载报 FAIL / 差量退化报 WARN / 后端列 PASS·缺维度·零测试·弱证据·形态·N/A);**M156a-f** 四段协议全咬(dims 解析 / 缺失聚合 / 零测试 / weak-only / 通道闸 / 本地 parser;原始输出 `.atlas/runs/mutation-proof/`) |
50
+ | `validators/validate_ac_alignment.py`(2026-10-12,`3507 BT` / B209-1 Phase 1) | 后端与全栈测试用例 ⇔ PRD 验收标准(AC)资产 | `[机器]` ① 无主 AC 拦截(测试挂载不存在的 AC ID ⇒ FAIL);② 无效跳过拦截(测试被 skip/xfail 但无 waiver 理由 ⇒ FAIL);③ 双向覆盖对账(输出已声明 AC 与测试挂载覆盖比;enforce 模式下零测试挂载 ⇒ FAIL);④ 差量防退化(对比基线快照,挂载测试数与覆盖 AC 数净增不得为负 ⇒ WARN);⑤ **AC × 覆盖维度(后端通道,D153)**:读域级 PRD 可选列 `后端适用覆盖维度` ⇔ 归一化测试记录的 `dims`;缺维度 / 仅弱证据 / 零测试 / 未知 / 孤儿 ⇒ WARN;无合法后端列、未声明 `backend_test` ⇒ N/A;多 backend app 用统一 `test_dir` 做全局对账;无 backend/fullstack app ⇒ 整门 N/A;apps 缺失 / 未注册 extractor ⇒ `[UNVERIFIED]` WARN(enforce 下 FAIL);parse_errors ⇒ `[PARSE]` WARN(enforce 下 FAIL);零挂载 ⇒ `[ZERO-COVERAGE]` WARN(enforce 下 FAIL);扫描面非空 ⇒ `[SCAN]` WARN;逐 PRD/逐表聚合,错位/备注含维度形态 ⇒ WARN | 测试断言是否语义有效(Phase 2/3 变异测试与证据链承接);未注册 / 不可用的测试提取器(走 `adapters/ac/` 注册表;非 Python 后端需提供 extractor);**E2E 第 4 列(D130 归 validate_e2e_index,不并集)**;**维度是否真被断言(语义,归独立审查)**;**资产是否真执行(执行证据归 E2E 真跑 / backend_test.command)** | 后端代码实现完成 / 收口;**调度者 = `atlas_check`**(三态前置:无 PRD 资产 ⇒ `SKIP`;无 PRD AC 且无测试 ⇒ `N/A`) | `出厂级(本轮实测)`:`tests/test_validate_ac_alignment.py` **62 条**(既有四维 7 条 + D153 后端维度 32 条 + Batch A/B/C1/C2/D2a/D2b 20 条;无主 AC 报 FAIL / 无 waiver skip 报 FAIL / 有 waiver 放行 / enforce 零测试挂载报 FAIL / 差量退化报 WARN / 后端列 PASS·缺维度·零测试·弱证据·形态·N/A);**M156a-f** 四段协议全咬(dims 解析 / 缺失聚合 / 零测试 / weak-only / 通道闸 / 本地 parser;原始输出 `.atlas/runs/mutation-proof/`);**RFC-20261030-01(2026-10-30)**:+3 修复指引钉(无主 AC / 无效跳过 / PASS 无后缀);变异 **M159c/M159d** 四段协议、还原逐字节一致 |
49
51
  | `scripts/refresh_structure.py` | 结构事实区 ⇔ 适配器输出 | `[机器]` `atlas:facts` 标记内的行键级单元格重写、语义列保留、预算(每域 L1+L2 字符上限);**键级增删 ⇒ 未登记漂移记录**(落 `.adapter-out/_drift.json`,2026-10-04,⇒ 2.2 `结构漂移` 门) | 语义区内容;无 `.adapter-out/` 时**跳过**(那是合法降级,不是通过) | 收口 / `after_archive` hook | `出厂级(本轮实测)`:`tests/test_refresh_facts.py` **21 条**本轮跑绿(`3509 §119`;漂移注入/清除 + 竖线往返为 2026-10-04 门禁负向用例 #3 补;F25 `--chain` 序 + F26 基线自进四前置 + F27 单遍化 + F29 tour 消费行为 R7 追加;**F41**(2026-10-15,issue #8)`.adapter-out/` 单一位点三消费者同址钉;**F47**(2026-10-16,issue #16)渲染层「写出 ⇒ 读回」**逐单元格不变式** + 渲染即自校(单元格换行 → `<br>`,不再把一行撑成多行 ⇒ 事实不再丢行、幻影漂移不再常驻;`_norm` 把 `<br>` 与换行视作**等价**以保键稳定);**M-R1** 摘掉单行化 ⇒ `test_multiline_cell_value_round_trips` 红 / **M-R2** 换行→**空格** ⇒ 行结构断言红(防「用空格糊过去也算修好」)/ **M-R9** `_norm` 退回不折叠 `<br>` ⇒ `test_key_column_with_newline_does_not_phantom_drift` 红 / **M-R10** 摘掉 `_selfcheck_round_trip` 调用 ⇒ `test_render_selfcheck_fires_on_unroundtrippable_row` 红(自校必须**活着能咬**)) |
50
52
  | `scripts/gen_testid_stub.py` | 分片应然 ⇔ 实现期薄桩 | `[机器]` 派生薄桩的存在与稳定重生成(缺失由 `validate_testids` **就地自动补齐并记 `PASS`** —— F32 兜底,2026-10-15;`FAIL` 只在生成失败) | 薄桩被实现者**读了没有** | 用例定稿后 | `出厂级(本轮实测)`:`tests/test_tools.py` **33 条**本轮跑绿(`3509 §119`;F23 测试命令形态 + F30 摘要装配集同步钉为 R7 追加;**F45**(2026-10-16,issue #14)用例 ID 文法单源污染门(唯一实现 + 登记面对账);**F37**(2026-10-15,issue #1)摘要根相对过滤 + 空装配响亮报错、**F40**(issue #4)运行命令由测试文件**后缀族**推定 + 冲突置空出声;**M62** 退回「只看配置存在性」⇒ `test_f40_ts_test_with_only_pytest_config_gets_no_command` 红 / 配置探测退回 app 作用域 ⇒ `test_f40_root_config_found_under_app_scope` 红;**F48**(2026-10-16,issue #17)装配面**三张表**同步补第三腿(`package.json.files` ⊇ `install.sh` 拷贝列表;此前漏 `tests` ⇒ npm 分发不含 `tests/` ⇒ 经 npm 装配的项目恒无 `.atlas/tests` ⇒ `atlas_check` 全量模式的包自检门**恒红且采用者自己修不了**);**M-R8** 从 `files` 摘掉 `tests` ⇒ `test_f47_npm_files_cover_assembly_list` 红) |
51
53
  | `scripts/cli_review_convert.py`(实例 ⑤ 专用,2026-09-29) | CLI 审查器输出 ⇔ `review.json` 契约 + **应审 N / 实审 M** | `[机器]` 严重度四档→三档写死映射(未知档位 exit 2);category 闭集外归 `other`;N(diff 文件数)/ M(CLI 汇总实审数)对账(M<N 或预算击穿 ⇒ `WARN`;**缺实审数 ⇒ 拒绝产出**);`(unit, round)` 落盘不得覆盖、round 递增 | findings 的**语义对错**(那是实例 ⑤ 审查者本身);回退环 triage(默认 `implement`,人 / agent 改判) | 实现代码审查产物落盘时(实例 ⑤ 运行后;执行器未接 ⇒ 当前未触发) | `出厂级(本轮实测)`:`tests/test_cli_review_convert.py` **12 条**(未知严重度 / 缺 summary / 缺应审口径 / 覆盖旧轮 ⇒ 各自 exit 2;Critical ⇒ FAIL 退出码 1;M<N ⇒ WARN;other 归档不计门禁;四档→三档映射;三件产物形态;D129 v2 指纹与 round 4 升级复议;**D129 评审 Major 修正**:`test_fingerprint_stable_across_line_numbers` 钉「行号漂移后指纹不变 + 内容不同指纹不同」两侧;**M130c**(`clean_target_file` 行号正则退回 `:\d+$` ⇒ 剥不掉 CLI 恒产的 `path:12-15`)钉红,四段协议还原逐字节一致) |
@@ -133,6 +133,60 @@ def test_skipped_without_waiver():
133
133
  assert "test_skipped_without_waiver" in skip_check["detail"]
134
134
 
135
135
 
136
+ def test_dangling_ac_fail_includes_repair_hint(tmp_path: Path):
137
+ """RFC-20261030-01:无主 AC 的 FAIL 必须给出 PRD 声明 / 移除标记的修复动作与复跑命令。"""
138
+ test_code = """
139
+ import pytest
140
+
141
+ @pytest.mark.ac("AC-GHOST-999")
142
+ def test_fake_case():
143
+ assert True
144
+ """
145
+ proj = create_minimal_project(tmp_path, prd_acs=["AC-TEST-001"], test_code=test_code)
146
+ res = validate_ac_alignment(proj)
147
+ check = [c for c in res.checks if c["check"] == "无主AC拦截"][0]
148
+ assert check["status"] == "FAIL"
149
+ assert "AC-GHOST-999" in check["detail"]
150
+ assert "先在 PRD 声明该 AC" in check["detail"]
151
+ assert "|修复:" in check["detail"] and "复跑: python3" in check["detail"] and "--fast" in check["detail"]
152
+
153
+
154
+ def test_unwaived_skip_fail_includes_repair_hint(tmp_path: Path):
155
+ """RFC-20261030-01:无效跳过的 FAIL 必须给出 waiver 或去 skip 的修复动作与复跑命令。"""
156
+ test_code = """
157
+ import pytest
158
+
159
+ @pytest.mark.ac("AC-TEST-001")
160
+ @pytest.mark.skip(reason="暂未实现")
161
+ def test_skipped_without_waiver():
162
+ assert False
163
+ """
164
+ proj = create_minimal_project(tmp_path, prd_acs=["AC-TEST-001"], test_code=test_code)
165
+ res = validate_ac_alignment(proj)
166
+ check = [c for c in res.checks if c["check"] == "无效跳过拦截"][0]
167
+ assert check["status"] == "FAIL"
168
+ assert "test_skipped_without_waiver" in check["detail"]
169
+ assert "waiver:<理由>" in check["detail"]
170
+ assert "|修复:" in check["detail"] and "复跑: python3" in check["detail"] and "--fast" in check["detail"]
171
+
172
+
173
+ def test_pass_detail_has_no_repair_hint(tmp_path: Path):
174
+ """PASS 分支不得带修复后缀。"""
175
+ test_code = """
176
+ import pytest
177
+
178
+ @pytest.mark.ac("AC-TEST-001")
179
+ def test_valid_case():
180
+ assert True
181
+ """
182
+ proj = create_minimal_project(tmp_path, prd_acs=["AC-TEST-001"], test_code=test_code)
183
+ res = validate_ac_alignment(proj)
184
+ for name in ("无主AC拦截", "无效跳过拦截", "AC覆盖对账"):
185
+ check = [c for c in res.checks if c["check"] == name][0]
186
+ assert check["status"] == "PASS"
187
+ assert "修复:" not in check["detail"]
188
+
189
+
136
190
  def test_waived_skip_passes(tmp_path: Path):
137
191
  # 显式附带 waiver: 豁免理由的 skip -> 放行 PASS
138
192
  test_code = """
@@ -1288,6 +1288,93 @@ def test_testids_plain_missing_gets_no_dynamic_hint() -> None:
1288
1288
  assert "容器 + 计数" not in detail and "稳定样例行" not in detail, detail
1289
1289
 
1290
1290
 
1291
+ # ── RFC-20261030-01:FAIL 修复指引(试点)────────────────────────────
1292
+
1293
+ def _repair_detail(res, check: str) -> str:
1294
+ return [c["detail"] for c in res.checks if c["check"] == check][0]
1295
+
1296
+
1297
+ def test_testids_fail_detail_includes_repair_hint() -> None:
1298
+ """FAIL 修复指引:缺项必须指向源码补 testid 或确认断言多余后删除,不能反向补应然。"""
1299
+ with tempfile.TemporaryDirectory() as td:
1300
+ root = Path(td)
1301
+ mk_prototype(root)
1302
+ mk_cases(root, CASE_GOOD.replace(
1303
+ " - text home-title 首页 # 页面标题文案为「首页」\n",
1304
+ " - text home-title 首页 # 页面标题文案为「首页」\n - visible home-missing-btn\n"))
1305
+ mk_index(root, _with_page_table("已实现"))
1306
+ detail = _repair_detail(vt.validate(root), "分页双向集合相等")
1307
+ assert "home-missing-btn" in detail
1308
+ assert "源码补 data-testid" in detail
1309
+ assert "在分片补 testid 声明" not in detail
1310
+ assert "|修复:" in detail and "复跑: python3" in detail and "--fast" in detail
1311
+ assert "item" not in detail
1312
+
1313
+
1314
+ def test_testids_landing_fail_detail_includes_repair_hint() -> None:
1315
+ """应然落点 FAIL:必须指向分片 落点 行的可解析路径。"""
1316
+ with tempfile.TemporaryDirectory() as td:
1317
+ root = Path(td)
1318
+ mk_prototype(root)
1319
+ mk_cases(root, CASE_GOOD.replace("web/src/home.html", "web/src/nope.html"))
1320
+ row = {c["check"]: c for c in vt.validate(root).checks}["应然落点"]
1321
+ assert row["level"] == "FAIL", row
1322
+ assert "落点:" in row["detail"] and "可解析" in row["detail"]
1323
+ assert "|修复:" in row["detail"] and "--fast" in row["detail"]
1324
+
1325
+
1326
+ def test_e2e_index_row_field_fail_includes_repair_hint() -> None:
1327
+ with tempfile.TemporaryDirectory() as td:
1328
+ root = Path(td)
1329
+ mk_prototype(root)
1330
+ mk_cases(root)
1331
+ bad = INDEX_GOOD.replace(
1332
+ "| E2E-HOME-001 | home | 首页加载并可进入空态 | smoke | AC-HOME-001 | | red | | [cases/home.md#e2e-home-001](cases/home.md#e2e-home-001) |",
1333
+ "| E2E-HOME-001 | home | 首页加载并可进入空态 | smoke | AC-HOME-001 | | red | |")
1334
+ mk_index(root, bad)
1335
+ detail = _repair_detail(ve.validate(root), "行字段完整")
1336
+ assert "E2E-HOME-001:分片空" in detail
1337
+ assert "补齐缺失列" in detail and "|修复:" in detail and "--fast" in detail
1338
+
1339
+
1340
+ def test_e2e_index_orphan_dead_fail_includes_repair_hint() -> None:
1341
+ with tempfile.TemporaryDirectory() as td:
1342
+ root = Path(td)
1343
+ mk_prototype(root)
1344
+ mk_cases(root, CASE_GOOD.replace("E2E-HOME-001", "E2E-HOME-002"))
1345
+ mk_index(root)
1346
+ detail = _repair_detail(ve.validate(root), "索引↔分片无孤儿/死链")
1347
+ assert "E2E-HOME-002" in detail and "E2E-HOME-001" in detail
1348
+ assert "死链 ⇒ 补/修分片或删该索引行" in detail
1349
+ assert "孤儿 ⇒ 补索引行或删分片" in detail
1350
+ assert "|修复:" in detail and "--fast" in detail
1351
+
1352
+
1353
+ def test_e2e_index_block_field_fail_includes_repair_hint() -> None:
1354
+ with tempfile.TemporaryDirectory() as td:
1355
+ root = Path(td)
1356
+ mk_prototype(root)
1357
+ mk_cases(root, CASE_GOOD.replace("testid:\n - home-cta-btn\n - home-title\n", ""))
1358
+ mk_index(root)
1359
+ detail = _repair_detail(ve.validate(root), "分片块字段齐")
1360
+ assert "缺:testid" in detail
1361
+ assert "id/pages/precondition/step/expected/testid" in detail
1362
+ assert "|修复:" in detail and "--fast" in detail
1363
+
1364
+
1365
+ def test_repair_hint_not_added_on_pass() -> None:
1366
+ """PASS 分支不得带修复后缀:提示只在 FAIL 时出现。"""
1367
+ with tempfile.TemporaryDirectory() as td:
1368
+ root = Path(td)
1369
+ mk_prototype(root)
1370
+ mk_cases(root)
1371
+ mk_index(root)
1372
+ assert "修复:" not in _repair_detail(vt.validate(root), "分页双向集合相等")
1373
+ e2e_res = ve.validate(root)
1374
+ for name in ("行字段完整", "分片块字段齐", "索引↔分片无孤儿/死链"):
1375
+ assert "修复:" not in _repair_detail(e2e_res, name)
1376
+
1377
+
1291
1378
  def test_e2e_index_gate_na() -> None:
1292
1379
  with tempfile.TemporaryDirectory() as td:
1293
1380
  assert ve.validate(Path(td)).status == "N/A"
@@ -2887,6 +2974,8 @@ def test_testids_adopt_unidirectional_subset_check() -> None:
2887
2974
  checks_gf = {c["check"]: c for c in res_gf.checks}
2888
2975
  assert checks_gf["分页双向集合相等"]["level"] == "FAIL", checks_gf
2889
2976
  assert "home-extra-legacy" in checks_gf["分页双向集合相等"]["detail"]
2977
+ assert "分片补 testid:/step/expected 引用" in checks_gf["分页双向集合相等"]["detail"]
2978
+ assert "删源码多余 data-testid" in checks_gf["分页双向集合相等"]["detail"]
2890
2979
  assert res_gf.status == "FAIL"
2891
2980
 
2892
2981
  # 3. 切回 adopt 模式:若用例引用的代码缺失(正向缺失),依然严格判 FAIL
@@ -2898,6 +2987,8 @@ def test_testids_adopt_unidirectional_subset_check() -> None:
2898
2987
  res_missing = vt.validate(root)
2899
2988
  checks_missing = {c["check"]: c for c in res_missing.checks}
2900
2989
  assert checks_missing["分页双向集合相等"]["level"] == "FAIL", checks_missing
2990
+ assert "源码补 data-testid" in checks_missing["分页双向集合相等"]["detail"]
2991
+ assert "删源码多余 data-testid" not in checks_missing["分页双向集合相等"]["detail"]
2901
2992
  assert res_missing.status == "FAIL"
2902
2993
 
2903
2994
 
@@ -220,6 +220,13 @@ def _cap(items: list[str], n: int = 8) -> str:
220
220
  return "、".join(items[:n]) + f"…(共 {len(items)})"
221
221
 
222
222
 
223
+ def _fix(root: Path, action: str) -> str:
224
+ cmd = ("python3 .atlas/scripts/atlas_check.py --fast"
225
+ if (root / ".atlas/scripts/atlas_check.py").is_file()
226
+ else "python3 scripts/atlas_check.py --fast")
227
+ return f"|修复: {action}|复跑: {cmd}"
228
+
229
+
223
230
  def _as_str_set(value: Any) -> set[str] | None:
224
231
  if not isinstance(value, (list, tuple, set)):
225
232
  return None
@@ -316,7 +323,8 @@ def _run_checks(
316
323
  if dangling_acs:
317
324
  res.add(
318
325
  "无主AC拦截", "FAIL",
319
- f"测试中引用的 AC ID 未在任何 {lay.print_value('prd')}/*.md 中声明:{sorted(dangling_acs)}",
326
+ f"测试中引用的 AC ID 未在任何 {lay.print_value('prd')}/*.md 中声明:{sorted(dangling_acs)}"
327
+ + _fix(root, "先在 PRD 声明该 AC;确认测试标记写错则移除标记"),
320
328
  )
321
329
  else:
322
330
  res.add("无主AC拦截", "PASS", f"测试中引用的 {len(all_cited_acs)} 个 AC 均有合法 PRD 声明")
@@ -324,7 +332,8 @@ def _run_checks(
324
332
  if unwaived_skips:
325
333
  res.add(
326
334
  "无效跳过拦截", "FAIL",
327
- f"挂载 AC 的测试被 skip/xfail 但未附带合法的 waiver 豁免理由:{unwaived_skips}",
335
+ f"挂载 AC 的测试被 skip/xfail 但未附带合法的 waiver 豁免理由:{unwaived_skips}"
336
+ + _fix(root, "去掉 skip/xfail,或在测试上补 waiver:<理由>"),
328
337
  )
329
338
  else:
330
339
  res.add("无效跳过拦截", "PASS", "所有挂载 AC 的跳过测试均具备有效豁免声明或无跳过")
@@ -52,6 +52,13 @@ def _format_list(items: list[str], max_show: int = 20) -> str:
52
52
  return f"{items}"
53
53
  return f"{items[:max_show]} (共 {len(items)} 项,截断前 {max_show} 项)"
54
54
 
55
+
56
+ def _fix(root: Path, action: str) -> str:
57
+ cmd = ("python3 .atlas/scripts/atlas_check.py --fast"
58
+ if (root / ".atlas/scripts/atlas_check.py").is_file()
59
+ else "python3 scripts/atlas_check.py --fast")
60
+ return f"|修复: {action}|复跑: {cmd}"
61
+
55
62
  # 随包内核引导(`_support/`,2026-10-12 单源批):用例块解析取 `_support/case_block.py`
56
63
  # 单源实现(原与 gen_e2e_scripts 逐字节双拷贝;本文件被控制台经内核加载,见 _support/siblings.py)。
57
64
  if str(Path(__file__).resolve().parents[1]) not in sys.path:
@@ -386,7 +393,11 @@ def validate(root: Path) -> Result:
386
393
  reason_ok = (g["状态"] in ("blocked", "skipped")) == bool(g["状态原因"])
387
394
  if not reason_ok:
388
395
  field_bad.append(f"{g['用例ID']}:状态原因/状态不匹配")
389
- res.add("行字段完整", "PASS" if not field_bad else "FAIL", f"{_format_list(field_bad)}")
396
+ res.add("行字段完整", "PASS" if not field_bad else "FAIL",
397
+ (f"{_format_list(field_bad)}"
398
+ + _fix(root, f"在索引行补齐缺失列({', '.join(field_bad[:3])});"
399
+ "状态原因只在 blocked/skipped 时填写并与状态匹配"))
400
+ if field_bad else "")
390
401
 
391
402
  # 分片块
392
403
  blocks, file_page = gather_shards(cases) if cases.is_dir() else ({}, {})
@@ -396,12 +407,19 @@ def validate(root: Path) -> Result:
396
407
  missing_keys = [k for k in needed if not d.get(k)]
397
408
  if missing_keys:
398
409
  block_bad.append(f"{cid}在{d.get('_file', '')}(缺:{','.join(missing_keys)})")
399
- res.add("分片块字段齐", "PASS" if not block_bad else "FAIL", f"缺字段={_format_list(block_bad)}")
410
+ res.add("分片块字段齐", "PASS" if not block_bad else "FAIL",
411
+ (f"缺字段={_format_list(block_bad)}"
412
+ + _fix(root, "在分片的 atlas-case 块补齐 "
413
+ "id/pages/precondition/step/expected/testid"))
414
+ if block_bad else "")
400
415
 
401
416
  orphan = sorted(set(blocks) - set(ids))
402
417
  dead = sorted(set(ids) - set(blocks))
403
418
  res.add("索引↔分片无孤儿/死链", "PASS" if not orphan and not dead else "FAIL",
404
- f"孤儿(分片有索引无)={_format_list(orphan)} 死链(索引有分片无)={_format_list(dead)}")
419
+ f"孤儿(分片有索引无)={_format_list(orphan)} 死链(索引有分片无)={_format_list(dead)}"
420
+ + (_fix(root, "死链 ⇒ 补/修分片或删该索引行;"
421
+ "孤儿 ⇒ 补索引行或删分片")
422
+ if orphan or dead else ""))
405
423
 
406
424
  # 分片链接可达 + 锚点标题
407
425
  # 索引↔分片标题一致(契约 §8.2,2026-09-25 补,依据 `3509 §B31`):
@@ -51,6 +51,34 @@ def _format_list(items: list[str] | set[str], max_show: int = 20) -> str:
51
51
  return f"{lst[:max_show]} (共 {len(lst)} 项,截断前 {max_show} 项)"
52
52
 
53
53
 
54
+ def _fix(root: Path, action: str) -> str:
55
+ cmd = ("python3 .atlas/scripts/atlas_check.py --fast"
56
+ if (root / ".atlas/scripts/atlas_check.py").is_file()
57
+ else "python3 scripts/atlas_check.py --fast")
58
+ return f"|修复: {action}|复跑: {cmd}"
59
+
60
+
61
+ def _repair_action(unequal: list[dict], origin: str) -> str:
62
+ parts = []
63
+ for det in unequal[:3]:
64
+ page = det.get("page", "?")
65
+ missing = det.get("缺失(抽取源无)") or []
66
+ orphan = det.get("多余(用例未引)") or []
67
+ if missing:
68
+ parts.append(
69
+ f"{page} 页缺 {', '.join(missing[:3])} ⇒ 源码补 data-testid,"
70
+ f"或确认断言多余后从分片移除"
71
+ )
72
+ if orphan and origin != "adopt":
73
+ parts.append(
74
+ f"{page} 页多 {', '.join(orphan[:3])} ⇒ 分片补 testid:/step/expected 引用,"
75
+ f"或删源码多余 data-testid"
76
+ )
77
+ if len(unequal) > 3:
78
+ parts.append(f"…共 {len(unequal)} 页")
79
+ return ";".join(parts) or "按分页差集修正 testid 应然/实然"
80
+
81
+
54
82
  def _load_stub_generator():
55
83
  """F32:定位随包的 `gen_testid_stub`(副本态 = `.atlas` 同级装配;包态 = 源包同级)。"""
56
84
  here = Path(__file__).resolve()
@@ -707,7 +735,9 @@ def validate(root: Path) -> Result:
707
735
  if any(template_pattern_hit(n, tpl_patterns) for n in missing_all)
708
736
  else ""
709
737
  )
710
- res.add("分页双向集合相等", "FAIL", json.dumps(unequal, ensure_ascii=False) + hint)
738
+ res.add("分页双向集合相等", "FAIL",
739
+ json.dumps(unequal, ensure_ascii=False) + hint
740
+ + _fix(root, _repair_action(unequal, origin)))
711
741
  elif unequal_soft:
712
742
  hint_msg = (
713
743
  "(存量 adopt 模式未引 testid 降级为 WARN,或页实现状态非「已实现」⇒ 缺口列此)"
@@ -760,7 +790,10 @@ def validate(root: Path) -> Result:
760
790
  else:
761
791
  bad_land_soft.append(f"{err}[{page_state}]")
762
792
  if bad_land_hard:
763
- res.add("应然落点", "FAIL", f"声明与实然不符={_format_list(bad_land_hard)}")
793
+ res.add("应然落点", "FAIL",
794
+ f"声明与实然不符={_format_list(bad_land_hard)}"
795
+ + _fix(root, "在分片 `落点:` 行补/改 `<tid> <存在的相对路径>`,"
796
+ "并确保该文件内可解析到该 testid"))
764
797
  elif bad_land_soft:
765
798
  warn_msg = f"未实现页落点先行待落地={_format_list(bad_land_soft)}"
766
799
  if no_land: