atlas-project-harness 0.2.0-beta.20 → 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.
Files changed (29) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/DESIGN.md +1 -0
  3. package/README.md +1 -1
  4. package/adapters/__pycache__/_lib.cpython-312.pyc +0 -0
  5. package/adapters/_lib.py +1 -1
  6. package/adapters/api.py +47 -7
  7. package/package.json +1 -1
  8. package/patches/project-wiring/__pycache__/apply-wiring.cpython-312.pyc +0 -0
  9. package/scripts/__pycache__/refresh_structure.cpython-312.pyc +0 -0
  10. package/scripts/__pycache__/release.cpython-312.pyc +0 -0
  11. package/shared/gates.md +5 -3
  12. package/shared/stack-profile.md +3 -0
  13. package/tests/__pycache__/test_adapter_api.cpython-312-pytest-9.1.1.pyc +0 -0
  14. package/tests/__pycache__/test_validate_ac_alignment.cpython-312-pytest-9.1.1.pyc +0 -0
  15. package/tests/__pycache__/test_validators.cpython-312-pytest-9.1.1.pyc +0 -0
  16. package/tests/test_adapter_api.py +74 -0
  17. package/tests/test_validate_ac_alignment.py +54 -0
  18. package/tests/test_validators.py +91 -0
  19. package/validators/__pycache__/validate_ac_alignment.cpython-312.pyc +0 -0
  20. package/validators/__pycache__/validate_e2e_index.cpython-312.pyc +0 -0
  21. package/validators/__pycache__/validate_testids.cpython-312.pyc +0 -0
  22. package/validators/validate_ac_alignment.py +11 -2
  23. package/validators/validate_e2e_index.py +21 -3
  24. package/validators/validate_testids.py +35 -2
  25. package/adapters/__pycache__/api.cpython-312.pyc +0 -0
  26. package/adapters/__pycache__/models.cpython-312.pyc +0 -0
  27. package/adapters/__pycache__/tests.cpython-312.pyc +0 -0
  28. package/tests/__pycache__/test_gen_e2e_scripts.cpython-312.pyc +0 -0
  29. package/tests/__pycache__/test_tools.cpython-312.pyc +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@
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
+
18
+ ## [v0.2.0-beta.21] - 2026-10-29
19
+
20
+ ### 🐛 api 适配器运行时 schema 只认构造调用(D172 / #46)
21
+
22
+ * **修复**:`adapters/api.py` 的运行时 schema 判定从文本正则改为 AST 构造调用——注释、文档字符串、正则源码、测试夹具里的 `FastAPI(`/`APIRouter(` 字面量不再把项目误判成「有描述」⇒ 不再产出悬空的 `spec_entry="/openapi.json"` + `items=0` 假指针(此前适配器自身源码即可自触发,实测复现于本仓根)。
23
+ * **口径**:`from fastapi import FastAPI as App` 的别名构造也认;测试文件(`tests?/`、`test_*.py`、`*_test.py`、`conftest.py`)整类不作描述源判定;无法解析的源码 WARN 跳过、不回退文本匹配。真构造调用仍产指针(有回归钉防修成假阴性)。
24
+ * **共享忽略集**补 `.adapter-out`(atlas 中间产物单一位点,不再被当业务源码枚举)。
25
+ * **同步面**:`shared/stack-profile.md` §3.5(字面量不是证据的口径)、`DESIGN.md` D172、map 行数锚。
26
+
27
+ ---
28
+
7
29
  ## [v0.2.0-beta.20] - 2026-10-29
8
30
 
9
31
  ### 🔌 AC 门适配器化与三形态适用性(D163 / RFC-20261029-01)
package/DESIGN.md CHANGED
@@ -645,6 +645,7 @@ adapters:
645
645
  | D169 | **JPA 跨文件继承与 Embeddable 展开(Batch D3a)**(2026-10-29;`adapters/_jpa.py`、`adapters/models.py`) | ① **病灶**:JPA 字段只解析单个 `@Entity` 类体;`@MappedSuperclass` 继承字段与 `@Embeddable` 内嵌字段缺失,事实表少列。② **定案**:`_jpa.py` 建跨文件类图,`@Entity` 合并 `@MappedSuperclass` 链字段(只合并 mapped,不推断 JPA 继承策略),`@Embedded` 按类型展开为 `field.nested`;`@Table`/`@Column`/`@Enumerated`/关系注解沿用;无 `@Table` 表名 = 类名,不猜命名策略。③ **被否**:JavaParser(行扫描+类图足够);合并 `@Entity` 继承(JOINED/SINGLE_TABLE 语义不同,会编事实);snake_case 命名策略猜测;Embeddable 循环展开(v1 用 stack 阻断)。④ **钉**:`tests/test_adapter_models.py` 12 条(同文件与跨文件 mapped superclass、Embeddable 展开、普通父类不合并);变异 **M-JPA4/M-JPA5** 均咬,逐字节还原一致。⑤ **边界**:JPA 继承策略/`@ElementCollection`/Kotlin 不在 v1;`source_file` 记实体文件,字段来源类不单独归属。 |
646
646
  | D170 | **Go 结构适配器(API / 测试清单 / 模型)(Batch D3b)**(2026-10-29;`adapters/api.py`、`adapters/tests.py`、`adapters/models.py`) | ① **病灶**:Go 后端的 `api` / `tests` / `models` 事实只能 agent 手写,结构↔代码对账缺口大。② **定案**:`api.py` 识别 gin/echo/chi/net/http 常见注册(`r.GET`、`http.HandleFunc("GET /x")`、`mux.HandleFunc("/x")`);无动词记 `ANY`,`_test.go` 不扫。`tests.py` 盘点 `*_test.go` + `go.mod`,命令 `go test ./...`;其它配置族冲突置 null + WARN。`models.py` 解析 Go struct 的 `gorm`/`json`/`db` tag(主键/自增/非空/唯一/size/column、`TableName()`),不猜表名。③ **被否**:Go AST/JavaParser(行级正则足够);按 `.Any` 猜具体动词(记 ANY);内置 Go AC extractor(v1 用 `extractor_command` 外部适配器,避免再铺一套测试标记 parser);Go 内嵌 struct/接口纳入 v1(边界明示)。④ **钉**:`tests/test_adapter_api.py` 9 条(Go 路由/注释忽略);`tests/test_tools.py` 31 条(Go 命令/配置冲突);`tests/test_adapter_models.py` 12 条(Go struct/tag/TableName/_test.go 排除);变异 **M-GO1/M-GO2/M-GO3** 均咬,逐字节还原一致。⑤ **边界**:Go AC 门需 `extractor_command`(无内置 Go AC extractor);Go 内嵌 struct/接口/多 backend app 逐 app 归属不在 v1;Kotlin/TestNG 不在 v1。 |
647
647
  | D171 | **真实仓烟测修复:`.scratch` / MyBatis-Plus / NestJS-Midway**(2026-10-29;`adapters/_lib.py`、`adapters/_jpa.py`、`adapters/api.py`) | ① **病灶(真实仓复现)**:Java 仓 `.scratch/` 下的影子测试被当测试源(182→166 的重复事实);MyBatis-Plus `@TableName` 实体不被 JPA 解析器识别(0 张表),且 getter 的 `return id;` 被误当字段(`type=return`、重复行);NestJS/Midway `@Controller` + `@Get` 控制器产出 0 端点。② **定案**:共享忽略集补 `.scratch`/`target`/`.gradle`/`.idea`;`_jpa.py` 支持 `@TableName`/`@TableId`/`@TableField`、普通基类字段合并(仅 MyBatis entity)、跳过 `return/throw/...` 行;`api.py` 新增 `@Controller(prefix)` + `@Get/@Post/.../@All` 路由(无动词 `ANY`)。③ **被否**:把 `.scratch` 当合法测试源;继续只认 JPA(真实仓是 MyBatis-Plus);用裸 `@Get` 全仓扫描(只处理含 `@Controller` 的 TS/JS 文件)。④ **钉**:`tests/test_tools.py` 33 条(`.scratch` / `.atlas` 排除);`tests/test_adapter_models.py` 14 条(MyBatis 表/基类字段/getter 不产 `return` 行);`tests/test_adapter_api.py` 10 条(NestJS 控制器);变异 **M-REAL1..3** 均咬,逐字节还原。真仓烟测:nanwang AC 1/1 PASS、MyBatis 数据模型生成;cn-console AC 1/1 PASS、API 51 条、测试 20 条、`atlas check --fast` 无失败。⑤ **边界**:`.scratch` 是项目约定(不保证别名);MyBatis XML/注解 SQL 不产模型;NestJS DTO `class` 未纳入 TS 模型(真实 core 无 DTO 类,暂不铺)。 |
648
+ | D172 | **api 适配器运行时 schema 只认构造调用(AST)**(2026-10-29;issue #46) | ① **病灶**:`SCHEMA_MARKERS` 按原始文本匹配 `.py`,任何文件里的 `APIRouter(`/`FastAPI(` 字面量——适配器自身注释、自身正则源码、测试夹具字符串——都被当成框架事实 ⇒ 输出 `spec_entry="/openapi.json"` + `items=0` 的**假阳性指针**(仓内无描述文件,且 `refresh_structure`/`validate_structure` 会把它当合法指针接线)。issue 上报的另一半「`_lib.IGNORE_DIRS` 未排除 `.atlas/.trellis`」已随 D171 落地(带回归钉)。② **定案**:判定改走 AST——`ast.Call` 的 `func` 为 `Name`/`Attribute` 且名字 ∈ {`FastAPI`,`APIRouter`}(含 `from fastapi import FastAPI as App` 的别名展开);注释/文档字符串/字符串字面量天然不是 `Call`。文本预筛(不含标记名不解析)控开销;解析失败(py2/非法源码)WARN 跳过该文件,**不回退文本匹配**(回退即把缺陷带回;跑不起来的文件不构成运行时 schema 证据)。测试文件(`tests?/`、`test_*.py`、`*_test.py`、`conftest.py`)整类不作描述源判定——夹具里真起一个 app 也不是产品接口面。共享忽略集补 `.adapter-out`。③ **被否**:按路径豁免扫描器自身(挡不住把适配器源码拷进项目的形态);把源码枚举根限定 `apps[].path`(生产本就按 `--app` 调用,根级 `path: .` 的 app 触发面不变;§3.5「整项目」只约束**描述文件**探测);语法错误回退文本正则;`openapi_url=None` 等框架细节(登记边界不铺)。④ **钉**:`tests/test_adapter_api.py` +3(真构造调用仍产指针 / 别名构造 / 注释+字符串+夹具+适配器源码拷贝不产指针)+ `.adapter-out` 夹具入既有排除钉。⑤ **同步面**:`shared/stack-profile.md` §3.5、`adapters/api.py` 注释、map 行数锚。 |
648
649
  ---
649
650
 
650
651
  ## 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.20-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/adapters/_lib.py CHANGED
@@ -20,7 +20,7 @@ IGNORE_DIRS = {
20
20
  ".git", "node_modules", "__pycache__", "site-packages", "dist", "build",
21
21
  ".next", ".turbo", ".mypy_cache", ".pytest_cache", ".pyxle-build",
22
22
  "coverage", ".coverage", ".venv", "venv", ".scratch", "target",
23
- ".gradle", ".idea", ".atlas", ".atlas-run", ".trellis",
23
+ ".gradle", ".idea", ".atlas", ".atlas-run", ".trellis", ".adapter-out",
24
24
  }
25
25
 
26
26
 
package/adapters/api.py CHANGED
@@ -29,6 +29,7 @@
29
29
  from __future__ import annotations
30
30
 
31
31
  import argparse
32
+ import ast
32
33
  import os
33
34
  import re
34
35
  import sys
@@ -40,11 +41,18 @@ from _lib import ( # noqa: E402
40
41
  )
41
42
 
42
43
  ADAPTER = "api"
43
- VERSION = "0.5.0"
44
+ VERSION = "0.5.1"
44
45
 
45
46
  DESCRIPTOR_FILES = {"openapi.json", "openapi.yaml", "openapi.yml", "swagger.json", "swagger.yaml"}
46
- # 运行时自动生成 schema 的框架信号(框架知识仅允许在适配器出现)
47
- SCHEMA_MARKERS = (re.compile(r"\bAPIRouter\s*\("), re.compile(r"\bFastAPI\s*\("))
47
+ # 运行时自动生成 schema 的框架信号(框架知识仅允许在适配器出现)。**只认代码里的
48
+ # 构造调用**(AST `Call`):注释 / 文档字符串 / 正则源码 / 测试夹具里的同名字面量
49
+ # 都不是框架事实(2026-10-29 issue #46:适配器把自己的注释与测试夹具认成了描述源)。
50
+ RUNTIME_SCHEMA_CALLS = ("FastAPI", "APIRouter")
51
+ RUNTIME_SCHEMA_MODULE = "fastapi"
52
+ # 测试文件不是产品接口面:夹具里真起一个 app 也不构成「有描述」
53
+ TEST_PATH_RE = re.compile(
54
+ r"(?:^|/)(?:tests?|__tests__)/|(?:^|/)test_[^/]*\.py$|(?:^|/)[^/]*_test\.py$|(?:^|/)conftest\.py$"
55
+ )
48
56
  DECO_RE = re.compile(r"@(\w+)\.(get|post|put|delete|patch|options|head)\(\s*['\"]([^'\"]*)['\"]")
49
57
  PREFIX_RE = re.compile(r"(\w+)\s*=\s*\w+\([^)]*prefix\s*=\s*['\"]([^'\"]+)['\"]")
50
58
  JS_RE = re.compile(r"\b(?:app|router)\.(get|post|put|delete|patch)\(\s*['\"]([^'\"]+)['\"]")
@@ -433,6 +441,32 @@ def extract_nest_routes(root: Path, f: Path) -> list[dict]:
433
441
  return items
434
442
 
435
443
 
444
+ def uses_runtime_schema(text: str) -> bool:
445
+ """文件里是否有**真实的**运行时 schema 构造调用(AST `Call` 节点)。
446
+
447
+ 字符串 / 注释 / 正则源码不是证据;`from fastapi import FastAPI as App` 的别名也算。
448
+ 解析失败由调用方 WARN 跳过——注释里的字面量曾把适配器自己认成描述源
449
+ (2026-10-29 issue #46),退回文本匹配会把该缺陷原样带回。
450
+ """
451
+ tree = ast.parse(text)
452
+ names = set(RUNTIME_SCHEMA_CALLS)
453
+ for node in ast.walk(tree):
454
+ if (isinstance(node, ast.ImportFrom)
455
+ and (node.module or "").split(".")[0] == RUNTIME_SCHEMA_MODULE):
456
+ for a in node.names:
457
+ if a.name in RUNTIME_SCHEMA_CALLS and a.asname:
458
+ names.add(a.asname)
459
+ for node in ast.walk(tree):
460
+ if not isinstance(node, ast.Call):
461
+ continue
462
+ fn = node.func
463
+ if isinstance(fn, ast.Name) and fn.id in names:
464
+ return True
465
+ if isinstance(fn, ast.Attribute) and fn.attr in names:
466
+ return True
467
+ return False
468
+
469
+
436
470
  def find_descriptors(root: Path) -> list[str]:
437
471
  """按**文件名**找机器可读描述文件(§3.5 的「有描述」探测)。
438
472
 
@@ -481,8 +515,13 @@ def main() -> int:
481
515
  text = f.read_text(encoding="utf-8")
482
516
  except OSError:
483
517
  continue
484
- if any(mk.search(text) for mk in SCHEMA_MARKERS):
485
- schema_sources.append(rel)
518
+ if not TEST_PATH_RE.search(rel) and any(n in text for n in RUNTIME_SCHEMA_CALLS):
519
+ try:
520
+ if uses_runtime_schema(text):
521
+ schema_sources.append(rel)
522
+ except (SyntaxError, ValueError) as e:
523
+ print(f"WARN[{ADAPTER}]: 跳过无法解析的文件 {rel}(运行时 schema 探测): {e}",
524
+ file=sys.stderr)
486
525
  items.extend(extract_py_routes(root, f))
487
526
  elif f.suffix == ".pyxl":
488
527
  items.extend(extract_actions(root, f))
@@ -497,8 +536,9 @@ def main() -> int:
497
536
 
498
537
  if desc_files or schema_sources:
499
538
  # 转指针形态 = 整体丢弃已抽到的 items ⇒ 必须出声,绝不静默清空
500
- # (2026-10-15 补:任何 `.py` 文本含 `APIRouter(`/`FastAPI(` 都会走到这里,
501
- # 演示脚本也算;不出声的话用户看到的是「接口清单是空的」而无从查因)。
539
+ # (2026-10-15 补:探测到运行时 schema 信号也会走到这里;2026-10-29 起信号只认
540
+ # AST 构造调用,注释/字符串/正则源码不再触发——issue #46。不出声的话用户看到的是
541
+ # 「接口清单是空的」而无从查因)。
502
542
  entry = desc_files[0] if desc_files else "/openapi.json"
503
543
  if items:
504
544
  print(f"WARN[{ADAPTER}]: 探测到机器可读描述({entry})⇒ 按 D 类指针形态只记"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-project-harness",
3
- "version": "0.2.0-beta.20",
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`)钉红,四段协议还原逐字节一致) |
@@ -210,6 +210,9 @@
210
210
  - **转指针形态必须出声**(2026-10-15 补):探测到描述而**丢弃**源码抽到的端点时,适配器须在
211
211
  stderr 报出丢弃条数——静默切换形态会让用户只看到「接口清单是空的」而无从查因
212
212
  (同族:`gates.md` 的「看起来在跑」)。
213
+ - **「运行时自动生成 schema」只认代码里的真实构造调用**(2026-10-29 补,issue #46):
214
+ 注释、文档字符串、正则源码、测试夹具(含测试文件整类)里的同名字面量都**不是证据**——
215
+ 字面量命中就把整个类别降成悬空指针,比缺口更糟。无法解析的源码文件跳过并 WARN,不回退文本匹配。
213
216
  - **纯前端项目也是合法形态**:**仅当用户确认「无描述」**时,前端调用点(如点分方法名的 RPC 调用)
214
217
  才是 D 类唯一的机械可抽源;D42 的「不收前端 client 调用」以「存在后端 / 描述侧真相源」为前提
215
218
  (见 `DESIGN.md` D42 的窄化声明)。
@@ -16,6 +16,8 @@ Python 装饰器路由与 `.pyxl` action——对前端 adopt 的主力形态(
16
16
  且**出声**(丢弃了多少条端点,不静默清空);
17
17
  5. **适配器不做行键去重**:同一(方法, 路径)从两个域的两个文件被调 ⇒ **两条都在**
18
18
  (跨域重复是合法事实,各进各的域文件;同文件内的重复行键由渲染期折叠)。
19
+ 6. **运行时 schema 只认构造调用**(issue #46):注释 / 字符串 / 测试夹具 / 适配器自身源码
20
+ 里的标记字面量不得触发指针形态;**真构造调用仍要产指针**(防修成假阴性)。
19
21
 
20
22
  变异 M54:让 `find_descriptors` 退回「搭源码后缀遍历」(描述文件恒找不到)⇒ 第 4 侧红。
21
23
  变异 M56:在适配器里加回「按(方法, 路径)全局去重」⇒ 第 5 侧红。
@@ -87,6 +89,22 @@ def run(root: Path) -> dict:
87
89
  return json.loads(out.read_text(encoding="utf-8"))
88
90
 
89
91
 
92
+ def run_root(root: Path) -> dict:
93
+ """整根扫描(不带 --app)——issue #46 的触发形态就是「源码根 = 项目根」。"""
94
+ out = root / "out.json"
95
+ p = subprocess.run([sys.executable, str(ADAPTER), "--root", str(root), "--out", str(out)],
96
+ capture_output=True, text=True, timeout=120)
97
+ assert p.returncode == 0, p.stdout + p.stderr
98
+ return json.loads(out.read_text(encoding="utf-8"))
99
+
100
+
101
+ def mk_plain(root: Path, files: dict) -> None:
102
+ for rel, body in files.items():
103
+ p = root / rel
104
+ p.parent.mkdir(parents=True, exist_ok=True)
105
+ p.write_text(body, encoding="utf-8")
106
+
107
+
90
108
  def test_ts_rpc_calls_are_extracted() -> None:
91
109
  """TS 的 RPC / action 调用:泛型调用与跨行调用都要收;`path` 记方法名本身。"""
92
110
  with tempfile.TemporaryDirectory() as td:
@@ -334,6 +352,10 @@ def test_atlas_dir_is_not_scanned_as_api_source() -> None:
334
352
  fake = root / ".atlas" / "adapters"
335
353
  fake.mkdir(parents=True)
336
354
  (fake / "fake.py").write_text("from fastapi import FastAPI\napp = FastAPI()\n", encoding="utf-8")
355
+ artifacts = root / ".adapter-out"
356
+ artifacts.mkdir()
357
+ (artifacts / "probe.py").write_text(
358
+ "from fastapi import FastAPI\napp = FastAPI()\n", encoding="utf-8")
337
359
  out = root / "out.json"
338
360
  p = subprocess.run([sys.executable, str(ADAPTER), "--root", str(root),
339
361
  "--app", "api", "--out", str(out)],
@@ -342,3 +364,55 @@ def test_atlas_dir_is_not_scanned_as_api_source() -> None:
342
364
  data = json.loads(out.read_text(encoding="utf-8"))
343
365
  assert data.get("spec_entry") is None, data
344
366
  assert [(i["method"], i["path"]) for i in data["items"]] == [("ANY", "/healthz")], data
367
+
368
+
369
+ def test_runtime_schema_constructor_yields_pointer() -> None:
370
+ """真构造调用 ⇒ 仍产运行时 schema 指针(防「修假阳性修成假阴性」)。"""
371
+ with tempfile.TemporaryDirectory() as td:
372
+ root = Path(td)
373
+ mk_plain(root, {"main.py": "from fastapi import FastAPI\napp = FastAPI()\n"})
374
+ data = run_root(root)
375
+ assert data.get("spec_entry") == "/openapi.json", data
376
+ assert not data.get("items"), data
377
+ assert data["source_files"] == ["main.py"], data
378
+
379
+
380
+ def test_runtime_schema_alias_constructor_yields_pointer() -> None:
381
+ """`import FastAPI as App` 的别名构造调用也要认(AST 判定不得窄于文本判定的真实形态)。"""
382
+ with tempfile.TemporaryDirectory() as td:
383
+ root = Path(td)
384
+ mk_plain(root, {"main.py": "from fastapi import FastAPI as App\napp = App()\n"})
385
+ assert run_root(root).get("spec_entry") == "/openapi.json"
386
+
387
+
388
+ def test_schema_marker_literals_are_not_descriptor_evidence() -> None:
389
+ """issue #46:注释 / 字符串 / 测试夹具 / 适配器自身源码里的标记字面量不算描述源。
390
+
391
+ `tests/test_fixture.py` 里是**真的** `app = FastAPI()`——夹具文件不是产品接口面;
392
+ `vendor/api.py` 是适配器源码原样拷贝(正则源码 + 注释 + 字符串三面齐全)。
393
+ """
394
+ flask_app = (
395
+ "from flask import Flask, jsonify\n"
396
+ "app = Flask(__name__)\n"
397
+ "\n"
398
+ "# 旧写法备忘:APIRouter( 与 FastAPI( 都是运行时 schema,本仓不用\n"
399
+ 'SNIPPET = "from fastapi import FastAPI\\napp = FastAPI()\\n"\n'
400
+ "\n"
401
+ "@app.get('/ping')\n"
402
+ "def ping():\n"
403
+ " return jsonify(ok=True)\n"
404
+ )
405
+ with tempfile.TemporaryDirectory() as td:
406
+ root = Path(td)
407
+ mk_plain(root, {
408
+ "app/main.py": flask_app,
409
+ "tests/test_fixture.py": (
410
+ "from fastapi import FastAPI\n"
411
+ "app = FastAPI() # 夹具自起的 app,不是产品接口面\n"
412
+ ),
413
+ "vendor/api.py": ADAPTER.read_text(encoding="utf-8"),
414
+ })
415
+ data = run_root(root)
416
+ assert data.get("spec_entry") is None, data
417
+ assert [(i["method"], i["path"]) for i in data["items"]] == [("GET", "/ping")], data
418
+ assert data["source_files"] == ["app/main.py"], data
@@ -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: