atlas-project-harness 0.2.0-beta.2 → 0.2.0-beta.21
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 +267 -0
- package/DESIGN.md +46 -3
- package/README.md +24 -14
- package/_support/__pycache__/ac_coverage.cpython-312.pyc +0 -0
- package/_support/__pycache__/case_block.cpython-312.pyc +0 -0
- package/_support/__pycache__/layout.cpython-312.pyc +0 -0
- package/_support/__pycache__/manual_block.cpython-312.pyc +0 -0
- package/_support/__pycache__/stack_profile.cpython-312.pyc +0 -0
- package/_support/ac_coverage.py +120 -0
- package/_support/case_block.py +3 -3
- package/_support/layout.py +411 -0
- package/_support/manual_block.py +67 -0
- package/_support/stack_profile.py +3 -1
- package/adapters/__pycache__/_graph.cpython-312.pyc +0 -0
- package/adapters/__pycache__/_jpa.cpython-312.pyc +0 -0
- package/adapters/__pycache__/_lib.cpython-312.pyc +0 -0
- package/adapters/__pycache__/api.cpython-312.pyc +0 -0
- package/adapters/__pycache__/models.cpython-312.pyc +0 -0
- package/adapters/__pycache__/tests.cpython-312.pyc +0 -0
- package/adapters/_graph.py +7 -2
- package/adapters/_jpa.py +291 -0
- package/adapters/_lib.py +8 -3
- package/adapters/ac/__init__.py +80 -0
- package/adapters/ac/__pycache__/__init__.cpython-312.pyc +0 -0
- package/adapters/ac/__pycache__/command.cpython-312.pyc +0 -0
- package/adapters/ac/__pycache__/js.cpython-312.pyc +0 -0
- package/adapters/ac/__pycache__/junit.cpython-312.pyc +0 -0
- package/adapters/ac/__pycache__/pytest.cpython-312.pyc +0 -0
- package/adapters/ac/command.py +88 -0
- package/adapters/ac/js.py +334 -0
- package/adapters/ac/junit.py +266 -0
- package/adapters/ac/pytest.py +380 -0
- package/adapters/api.py +331 -8
- package/adapters/models.py +126 -2
- package/adapters/routes.py +7 -3
- package/adapters/tests.py +43 -5
- package/apply/reference.md +6 -3
- package/bin/atlas.js +66 -28
- package/extensions/pi.ts +39 -8
- package/install.sh +179 -19
- package/package.json +1 -1
- package/patches/project-wiring/README.md +4 -3
- package/patches/project-wiring/__pycache__/apply-wiring.cpython-312.pyc +0 -0
- package/patches/project-wiring/agent-pointer.md +1 -1
- package/patches/project-wiring/apply-wiring.py +146 -153
- package/patches/workflow-plan-apply/1.6-plan-apply.insert.md +1 -1
- package/rings/e2e/reference.md +37 -20
- package/rings/prd/reference.md +26 -8
- package/rings/structure/reference.md +3 -2
- package/scripts/__pycache__/atlas_check.cpython-312.pyc +0 -0
- package/scripts/__pycache__/atlas_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/atlas_verify.cpython-312.pyc +0 -0
- package/scripts/__pycache__/closeout_derive.cpython-312.pyc +0 -0
- package/scripts/__pycache__/e2e_console.cpython-312.pyc +0 -0
- package/scripts/__pycache__/gen_e2e_scripts.cpython-312.pyc +0 -0
- package/scripts/__pycache__/gen_testid_stub.cpython-312.pyc +0 -0
- package/scripts/__pycache__/make_review_inputs.cpython-312.pyc +0 -0
- package/scripts/__pycache__/pkg_digest.cpython-312.pyc +0 -0
- package/scripts/__pycache__/refresh_structure.cpython-312.pyc +0 -0
- package/scripts/__pycache__/release.cpython-312.pyc +0 -0
- package/scripts/__pycache__/write_claim.cpython-312.pyc +0 -0
- package/scripts/atlas_check.py +329 -86
- package/scripts/atlas_report.py +10 -2
- package/scripts/atlas_verify.py +435 -0
- package/scripts/closeout_derive.py +296 -0
- package/scripts/e2e_console.py +46 -31
- package/scripts/gen_e2e_scripts.py +424 -66
- package/scripts/gen_testid_stub.py +8 -3
- package/scripts/make_review_inputs.py +20 -6
- package/scripts/pkg_digest.py +42 -5
- package/scripts/refresh_structure.py +29 -8
- package/scripts/release.py +238 -7
- package/scripts/write_claim.py +204 -0
- package/shared/closeout.md +7 -0
- package/shared/gates.md +24 -19
- package/shared/global-rules.md +8 -1
- package/shared/independent-review.md +4 -3
- package/shared/layout.md +28 -3
- package/shared/single-source.md +2 -0
- package/shared/stack-profile.md +19 -10
- package/skills/atlas-apply/SKILL.md +1 -0
- package/skills/atlas-design-review/SKILL.md +3 -3
- package/skills/atlas-e2e/SKILL.md +1 -0
- package/skills/atlas-prd/SKILL.md +1 -0
- package/skills/atlas-release/SKILL.md +9 -3
- package/skills/atlas-structure/SKILL.md +1 -0
- package/skills/frontend-design/SKILL.md +2 -2
- package/skills/frontend-design/references/tuning-verbs.md +1 -1
- package/templates/e2e/case-shard.md +1 -1
- package/templates/e2e/console/app.js +25 -4
- package/templates/e2e/console/console.css +2 -0
- package/templates/e2e/console/index.html +2 -2
- package/templates/prd/domain.md +14 -12
- package/templates/prd/global.md +2 -2
- package/templates/stack-profile.yaml +12 -11
- package/tests/__pycache__/test_ac_adapters.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_adapter_api.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_adapter_models.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_apply_wiring.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_atlas_check.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_atlas_verify.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_atlas_verify_playground.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_canonical_consistency.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_closeout_derive.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_duty_bug_families.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_e2e_console.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_gen_e2e_scripts.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_gen_e2e_scripts.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_install_selfcheck.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_layout.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_map_anchors.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_no_framework_names.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_project_wiring.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_refresh_facts.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_shared_kernel.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_tools.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_tools.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_validate_ac_alignment.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_validate_adr.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_validate_walkthrough.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_validators.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_write_claim.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/fixtures/e2e_coverage_dims_golden.json +12 -0
- package/tests/fixtures/verify_playground/product/e2e/cases/home.md +40 -0
- package/tests/fixtures/verify_playground/product/e2e/e2e-index.md +18 -0
- package/tests/fixtures/verify_playground/product/e2e/reviews/2026-01-01-fixture/README.md +3 -0
- package/tests/fixtures/verify_playground/product/e2e/reviews/2026-01-01-fixture/review.json +17 -0
- package/tests/fixtures/verify_playground/product/e2e/scripts/atlas-prepare.mjs +4 -0
- package/tests/fixtures/verify_playground/product/prd/README.md +5 -0
- package/tests/fixtures/verify_playground/product/prd/home-prd.md +62 -0
- package/tests/fixtures/verify_playground/product/prd/prd.md +52 -0
- package/tests/fixtures/verify_playground/product/stack-profile.yaml.tpl +18 -0
- package/tests/fixtures/verify_playground/site/index.html +6 -0
- package/tests/test_ac_adapters.py +312 -0
- package/tests/test_adapter_api.py +253 -0
- package/tests/test_adapter_models.py +236 -0
- package/tests/test_apply_wiring.py +136 -23
- package/tests/test_atlas_check.py +371 -0
- package/tests/test_atlas_verify.py +335 -0
- package/tests/test_atlas_verify_playground.py +162 -0
- package/tests/test_canonical_consistency.py +5 -2
- package/tests/test_closeout_derive.py +264 -0
- package/tests/test_duty_bug_families.py +355 -3
- package/tests/test_e2e_console.py +49 -0
- package/tests/test_gen_e2e_scripts.py +650 -17
- package/tests/test_install_selfcheck.py +76 -0
- package/tests/test_layout.py +252 -0
- package/tests/test_map_anchors.py +475 -0
- package/tests/test_no_framework_names.py +86 -0
- package/tests/test_project_wiring.py +12 -4
- package/tests/test_refresh_facts.py +18 -3
- package/tests/test_shared_kernel.py +50 -0
- package/tests/test_tools.py +257 -1
- package/tests/test_validate_ac_alignment.py +1081 -0
- package/tests/test_validate_adr.py +24 -0
- package/tests/test_validate_walkthrough.py +313 -0
- package/tests/test_validators.py +499 -1
- package/tests/test_write_claim.py +236 -0
- package/validators/__pycache__/validate_ac_alignment.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_adr.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_design_exemptions.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_e2e_index.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_e2e_mocks.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_prd.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_stack_profile.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_structure.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_testids.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_walkthrough.cpython-312.pyc +0 -0
- package/validators/validate_ac_alignment.py +439 -183
- package/validators/validate_adr.py +15 -5
- package/validators/validate_design_exemptions.py +15 -6
- package/validators/validate_e2e_index.py +109 -38
- package/validators/validate_e2e_mocks.py +12 -5
- package/validators/validate_prd.py +109 -14
- package/validators/validate_stack_profile.py +62 -8
- package/validators/validate_structure.py +9 -2
- package/validators/validate_testids.py +36 -23
- package/validators/validate_walkthrough.py +300 -0
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
"""atlas 校验器 —— 架构决策记录(ADR)长效资产守卫(2026-10-13)。
|
|
3
3
|
|
|
4
4
|
守一件事:
|
|
5
|
-
项目级长效架构决策唯一落地于
|
|
5
|
+
项目级长效架构决策唯一落地于 `资产根/adr/NNNN-<slug>.md`(资产根默认 `product/`,
|
|
6
|
+
由项目根 `atlas.config.yaml` 的 `layout.assets_root` 解析)。
|
|
6
7
|
规范文档形态,保障状态合法性、决策确定性与版本演进追溯。
|
|
7
8
|
|
|
8
9
|
判定(三态 + WARN):
|
|
9
|
-
- 无
|
|
10
|
+
- 无 `资产根/adr/` 目录或目录为空 ⇒ SKIP(尚未创建任何 ADR,非失败)。
|
|
10
11
|
- 有 ADR 文件:
|
|
11
12
|
* 编号唯一:无同号异名冲突(0001-foo ⇔ 0001-bar)。
|
|
12
13
|
* Frontmatter 合法:status 必须属于 {proposed, accepted, superseded, deprecated}。
|
|
@@ -30,7 +31,10 @@ import re
|
|
|
30
31
|
import sys
|
|
31
32
|
from pathlib import Path
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
if str(Path(__file__).resolve().parents[1]) not in sys.path:
|
|
35
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
36
|
+
from _support.layout import LayoutError, load_layout # noqa: E402
|
|
37
|
+
|
|
34
38
|
ADR_FILENAME_PATTERN = re.compile(r"^(\d{4})-[a-z0-9_-]+\.md$", re.IGNORECASE)
|
|
35
39
|
VALID_STATUSES = {"proposed", "accepted", "superseded", "deprecated"}
|
|
36
40
|
|
|
@@ -89,10 +93,16 @@ def extract_section(text: str, section_kw: str) -> str | None:
|
|
|
89
93
|
|
|
90
94
|
def validate_adr(root: Path) -> Result:
|
|
91
95
|
res = Result()
|
|
92
|
-
|
|
96
|
+
try:
|
|
97
|
+
lay = load_layout(root)
|
|
98
|
+
except LayoutError as exc:
|
|
99
|
+
res.add("项目布局配置", "FAIL", str(exc))
|
|
100
|
+
return res
|
|
101
|
+
adr_dir = lay.adr_dir
|
|
93
102
|
|
|
94
103
|
if not adr_dir.exists() or not adr_dir.is_dir():
|
|
95
|
-
res.add("adr_dir_exists", "SKIP",
|
|
104
|
+
res.add("adr_dir_exists", "SKIP",
|
|
105
|
+
f"前置未声明(未就绪,非失败):无 {lay.print_value('adr')}/")
|
|
96
106
|
return res
|
|
97
107
|
|
|
98
108
|
files = [f for f in adr_dir.iterdir() if f.is_file() and not f.name.startswith(".")]
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
守一件事(机器对账取代 prose 约定):
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
`资产根/design.md`(资产根默认 `product/`,由项目根 `atlas.config.yaml` 解析;
|
|
7
|
+
视觉设计档案,frontend-design skill 写、atlas 只读)里
|
|
7
8
|
「已有豁免」中的 detect 类豁免写作 `detect:<rule-id>`;同一组 rule id 必须
|
|
8
9
|
出现在项目根 `.impeccable/config.json` 的 `detector.ignoreRules` —— **双向
|
|
9
10
|
集合相等**。
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
单侧缺失 = 报警关了没人知道为什么(缺台账),或台账说关了其实没关(缺配置)。
|
|
14
15
|
|
|
15
16
|
判定(三态,`§B120`):
|
|
16
|
-
- 无
|
|
17
|
+
- 无 `资产根/design.md` ⇒ **SKIP**(档案未建 = 未就绪,不是失败)。
|
|
17
18
|
- 档案 `detect:<id>` 集合 ⇔ 配置 `detector.ignoreRules` 集合相等 ⇒ PASS
|
|
18
19
|
(两侧皆空是合法退化态)。
|
|
19
20
|
- 任一侧多出 ⇒ FAIL,双向差集指名。
|
|
@@ -38,7 +39,10 @@ import re
|
|
|
38
39
|
import sys
|
|
39
40
|
from pathlib import Path
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
if str(Path(__file__).resolve().parents[1]) not in sys.path:
|
|
43
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
44
|
+
from _support.layout import LayoutError, load_layout # noqa: E402
|
|
45
|
+
|
|
42
46
|
CONFIG_RELPATH = Path(".impeccable") / "config.json"
|
|
43
47
|
DETECT_TOKEN = re.compile(r"detect:([A-Za-z0-9][A-Za-z0-9_-]*)")
|
|
44
48
|
|
|
@@ -87,12 +91,17 @@ def config_ignore_rules(cfg_path: Path) -> set[str]:
|
|
|
87
91
|
|
|
88
92
|
def run(root: Path) -> Result:
|
|
89
93
|
res = Result()
|
|
90
|
-
|
|
94
|
+
try:
|
|
95
|
+
lay = load_layout(root)
|
|
96
|
+
except LayoutError as exc:
|
|
97
|
+
res.add("项目布局配置", "FAIL", str(exc))
|
|
98
|
+
return res
|
|
99
|
+
design = lay.design
|
|
91
100
|
if not design.is_file():
|
|
92
101
|
res.add("设计档案存在", "SKIP",
|
|
93
|
-
"无
|
|
102
|
+
f"无 {lay.print_value('design')}(档案未建,未就绪非失败)")
|
|
94
103
|
return res
|
|
95
|
-
res.add("设计档案存在", "PASS",
|
|
104
|
+
res.add("设计档案存在", "PASS", lay.print_value("design"))
|
|
96
105
|
|
|
97
106
|
ids = design_detect_ids(design.read_text(encoding="utf-8", errors="replace"))
|
|
98
107
|
cfg = root / CONFIG_RELPATH
|
|
@@ -40,6 +40,10 @@ import re
|
|
|
40
40
|
import sys
|
|
41
41
|
from pathlib import Path
|
|
42
42
|
|
|
43
|
+
if str(Path(__file__).resolve().parents[1]) not in sys.path:
|
|
44
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
45
|
+
from _support.layout import LayoutError, load_layout # noqa: E402
|
|
46
|
+
|
|
43
47
|
|
|
44
48
|
def _format_list(items: list[str], max_show: int = 20) -> str:
|
|
45
49
|
if not items:
|
|
@@ -61,6 +65,9 @@ url_merge = load_module("url_merge")
|
|
|
61
65
|
# 用例 ID 文法体单源(正文归 rings/e2e/reference.md §5.3;锚点本处自留)
|
|
62
66
|
case_id = load_module("case_id")
|
|
63
67
|
stack_profile = load_module("stack_profile")
|
|
68
|
+
# 维度 token 单源(D153):区间展开 / 中文邻接 / 疑似写法出声归 `_support/ac_coverage.py`。
|
|
69
|
+
ac_coverage = load_module("ac_coverage")
|
|
70
|
+
parse_dims_cell = ac_coverage.parse_dims_cell
|
|
64
71
|
# 契约 §5.5(2026-10-16,issue #R1/#R3):行内注释剥除口径与内核**同一函数对象**(I3)——
|
|
65
72
|
# 剥注释口径**全域单源**(`_support/stack_profile.strip_inline_comment`,2026-10-17 D104 ⑦ 清偿):
|
|
66
73
|
# `_code()` 是该口径在本文件的唯一出口,本文件内**不得**再出现 `split("#", 1)[0]`(MB8 钉守着)。
|
|
@@ -102,8 +109,8 @@ REFRESH_WORDS = ("刷新", "reload", "refresh")
|
|
|
102
109
|
I18N_HINTS = (re.compile(r"\bt\("), re.compile(r"\b[a-z]+\.[a-z]+\.[a-z]+\b"), re.compile(r"\b[A-Z]{2,}_[A-Z_]+\b"))
|
|
103
110
|
|
|
104
111
|
# 分片 step / expected 的动词词表(契约 `rings/e2e/reference.md` §5.5,2026-09-23 定)
|
|
105
|
-
ACTION_VERBS = ("click", "fill", "select", "check", "uncheck", "press", "hover", "refresh", "goto", "waitFor", "download", "upload", "dispatch")
|
|
106
|
-
ASSERT_VERBS = ("text", "contains", "count", "countOptions", "visible", "hidden", "enabled", "disabled", "checked", "unchecked", "value", "attr", "delta")
|
|
112
|
+
ACTION_VERBS = ("click", "fill", "select", "check", "uncheck", "press", "type", "hover", "refresh", "goto", "waitFor", "download", "upload", "dispatch")
|
|
113
|
+
ASSERT_VERBS = ("text", "contains", "count", "countOptions", "visible", "hidden", "enabled", "disabled", "checked", "unchecked", "value", "attr", "delta", "request")
|
|
107
114
|
# 契约 §5.6 R9:`expected` 行可加**行首前缀**,显式声明该断言的判据形状。
|
|
108
115
|
# 本校验器只负责**形态项**(前缀后是词表内的动词 / 不嵌套 / `unchanged:` 的用例有提交性动作);
|
|
109
116
|
# **不判**断言是否空转,也不判初始态成不成立(那是运行期零步基线的职责,§6.0 C-new)。
|
|
@@ -264,10 +271,11 @@ def seed_models_check(root: Path, blocks: dict[str, dict]) -> tuple[str, str]:
|
|
|
264
271
|
("PASS", f"{len(seeds)} 条用例的 seed 与 {len(models)} 个表/模型对账通过")
|
|
265
272
|
|
|
266
273
|
|
|
267
|
-
def upload_fixture_check(root: Path, blocks: dict[str, dict]) -> tuple[str, str]:
|
|
274
|
+
def upload_fixture_check(root: Path, blocks: dict[str, dict], lay=None) -> tuple[str, str]:
|
|
268
275
|
"""`upload` 行的 fixture 引用闭环(契约 §5.5,B201-1,2026-10-12)。
|
|
269
276
|
|
|
270
|
-
值 = 相对
|
|
277
|
+
值 = 相对 `<资产根>/e2e/fixtures/` 的文件路径(资产根默认 `product/`;
|
|
278
|
+
与 §5.8 mock 同基线、同一 fixtures 树)。
|
|
271
279
|
判据(结构门):**值形态**(非空 / 非绝对路径 / 无 `..` 段)+ **文件存在**。无用例声明
|
|
272
280
|
`upload` ⇒ PASS(空集合法,N/A 不是通过的反面——空集就是合法形态)。生成器侧生成期
|
|
273
281
|
同款检查(缺 ⇒ exit 2 列明)——两侧同步,防「校验器放行、生成器认不得」的半通状态。
|
|
@@ -280,7 +288,7 @@ def upload_fixture_check(root: Path, blocks: dict[str, dict]) -> tuple[str, str]
|
|
|
280
288
|
uploads.append((cid, parts[2].strip() if len(parts) > 2 else ""))
|
|
281
289
|
if not uploads:
|
|
282
290
|
return "PASS", "无用例声明 `upload`(可选动作词)"
|
|
283
|
-
fixtures =
|
|
291
|
+
fixtures = (lay or load_layout(root)).e2e_fixtures_dir
|
|
284
292
|
bad: list[str] = []
|
|
285
293
|
for cid, rel in uploads:
|
|
286
294
|
if not rel:
|
|
@@ -334,7 +342,12 @@ def _route_hit(p: str, page_routes) -> bool:
|
|
|
334
342
|
|
|
335
343
|
def validate(root: Path) -> Result:
|
|
336
344
|
res = Result()
|
|
337
|
-
|
|
345
|
+
try:
|
|
346
|
+
lay = load_layout(root)
|
|
347
|
+
except LayoutError as exc:
|
|
348
|
+
res.add("项目布局配置", "FAIL", str(exc))
|
|
349
|
+
return res
|
|
350
|
+
e2e = lay.e2e_dir
|
|
338
351
|
index = e2e / "e2e-index.md"
|
|
339
352
|
cases = e2e / "cases"
|
|
340
353
|
shard_files = sorted(cases.glob("*.md")) if cases.is_dir() else []
|
|
@@ -444,7 +457,7 @@ def validate(root: Path) -> Result:
|
|
|
444
457
|
res.add("页面表合法", "PASS" if not page_bad2 else "FAIL",
|
|
445
458
|
f"路由非/开头或重复={page_bad2[:8]}" if page_bad2 else "")
|
|
446
459
|
if known is None:
|
|
447
|
-
known = {md.stem for md in
|
|
460
|
+
known = {md.stem for md in lay.e2e_cases_dir.glob("*.md")}
|
|
448
461
|
res.add("页面表合法", "WARN", "索引无页面表(E1 起应有;暂按分片文件名推导页清单)")
|
|
449
462
|
if known is not None and page_rows:
|
|
450
463
|
miss = sorted({g["页面"] for g in grid} - known)
|
|
@@ -505,7 +518,7 @@ def validate(root: Path) -> Result:
|
|
|
505
518
|
# 关联AC
|
|
506
519
|
ac_bad = [g["用例ID"] for g in grid if not AC_RE.match(g["关联AC"])]
|
|
507
520
|
res.add("关联AC 格式", "PASS" if not ac_bad else "FAIL", f"{ac_bad[:8]}")
|
|
508
|
-
prd_files = sorted(
|
|
521
|
+
prd_files = sorted(lay.prd_dir.glob("*-prd.md")) if lay.prd_dir.is_dir() else []
|
|
509
522
|
if not prd_files:
|
|
510
523
|
res.add("关联AC 可达", "N/A", "门控态:无域级 PRD")
|
|
511
524
|
else:
|
|
@@ -583,34 +596,13 @@ def validate(root: Path) -> Result:
|
|
|
583
596
|
(detail + ";残留待裁决(清零后升 FAIL,规则在 shared/gates.md §3 AC⇔用例行)")
|
|
584
597
|
if gaps else detail)
|
|
585
598
|
|
|
586
|
-
#
|
|
587
|
-
#
|
|
588
|
-
# 仅对已实现域且已有用例挂靠的 AC 计算;未声明适用维度的项目按 N/A 兜底,不报假缺口。
|
|
589
|
-
# 解析口径(评审 Minor 修正):① `\b` 对 CJK 邻接失效(「C01正常流」不命中)⇒ 改前后界定符;
|
|
590
|
-
# ② 「C01-C03」区间须展开(否则只命中两端、中间维度被误报缺口);③ 不做「含『维度』二字即跳过」
|
|
591
|
-
# 的粗暴表头过滤(会误杀「C13 状态维度」这类数据行)——表头/分隔行首格不是 AC 形态,天然不被外层正则命中。
|
|
592
|
-
_DIM_TOKEN = r"C(?:0[1-9]|1[0-4])"
|
|
593
|
-
_dim_range_re = re.compile(rf"(?<![0-9A-Za-z])({_DIM_TOKEN})\s*[-~至]\s*({_DIM_TOKEN})(?![0-9])")
|
|
594
|
-
_dim_code_re = re.compile(rf"(?<![0-9A-Za-z]){_DIM_TOKEN}(?![0-9])")
|
|
595
|
-
_dim_looks_re = re.compile(r"[Cc]\d{1,2}")
|
|
596
|
-
|
|
597
|
-
def _parse_dims(raw) -> tuple[set[str], bool]:
|
|
598
|
-
"""覆盖维度文本 → (命中维度集合, 是否含疑似维度写法)。区间展开、中文邻接可命中。"""
|
|
599
|
-
text = str(raw or "")
|
|
600
|
-
dims: set[str] = set()
|
|
601
|
-
for m in _dim_range_re.finditer(text):
|
|
602
|
-
lo, hi = int(m.group(1)[1:]), int(m.group(2)[1:])
|
|
603
|
-
if lo > hi:
|
|
604
|
-
lo, hi = hi, lo
|
|
605
|
-
dims |= {f"C{i:02d}" for i in range(lo, hi + 1)}
|
|
606
|
-
dims |= set(_dim_code_re.findall(text))
|
|
607
|
-
return dims, bool(_dim_looks_re.search(text))
|
|
608
|
-
|
|
599
|
+
# 解析口径归 `_support/ac_coverage.py::parse_dims_cell`(D153 单源,行为与 D130 逐字节一致):
|
|
600
|
+
# 区间展开、中文邻接、疑似写法出声;本文件不再自持第二份 parser。
|
|
609
601
|
declared_ac_dims: dict[str, set[str]] = {}
|
|
610
602
|
for p in prd_files:
|
|
611
603
|
seg = p.read_text(encoding="utf-8")
|
|
612
604
|
for m in re.finditer(r"^\|\s*`?(AC-[A-Z0-9]+-\d{3})`?\s*\|[^|\n]*\|[^|\n]*\|([^|\n]+)\|", seg, re.M):
|
|
613
|
-
dims, _looks =
|
|
605
|
+
dims, _looks = parse_dims_cell(m.group(2))
|
|
614
606
|
if dims:
|
|
615
607
|
declared_ac_dims[m.group(1)] = dims
|
|
616
608
|
|
|
@@ -621,7 +613,7 @@ def validate(root: Path) -> Result:
|
|
|
621
613
|
acid = g["关联AC"]
|
|
622
614
|
blk = blocks.get(cid, {})
|
|
623
615
|
for d_str in (blk.get("覆盖维度") or []):
|
|
624
|
-
found, looks =
|
|
616
|
+
found, looks = parse_dims_cell(d_str)
|
|
625
617
|
if not found and looks:
|
|
626
618
|
bad_case_dims.append(f"{cid}:{d_str}")
|
|
627
619
|
covered_ac_dims.setdefault(acid, set()).update(found)
|
|
@@ -683,6 +675,7 @@ def validate(root: Path) -> Result:
|
|
|
683
675
|
verb_bad, assert_bad, goto_param, contradict = [], [], [], []
|
|
684
676
|
goto_path: list[str] = []
|
|
685
677
|
auth_bad: list[str] = []
|
|
678
|
+
target_bad: list[str] = [] # `靶场:` 取值不在 `e2e.targets` 闭集内(多靶场,beta.16)
|
|
686
679
|
guard_bad: list[str] = []
|
|
687
680
|
session_bad: list[str] = []
|
|
688
681
|
switch_bad: list[str] = []
|
|
@@ -697,7 +690,7 @@ def validate(root: Path) -> Result:
|
|
|
697
690
|
# 段缺失 / keys 为空 ⇒ 该靶场不允许任何该类预设(用例出现对应行即 FAIL)。
|
|
698
691
|
# `session_preset`(2026-10-16,issue #13)与 `query_preset`(2026-10-18,`DESIGN.md` D109)
|
|
699
692
|
# 同族:同为**前提面**,两个载体各一段。
|
|
700
|
-
_prof_path =
|
|
693
|
+
_prof_path = lay.stack_profile
|
|
701
694
|
|
|
702
695
|
def _preset_keys(sub: str, field: str = "keys") -> set[str]:
|
|
703
696
|
if not _prof_path.is_file():
|
|
@@ -714,11 +707,30 @@ def validate(root: Path) -> Result:
|
|
|
714
707
|
_session_keys = _preset_keys("session_preset")
|
|
715
708
|
_query_keys = _preset_keys("query_preset")
|
|
716
709
|
_hash_query_keys = _preset_keys("query_preset", "hash_keys")
|
|
710
|
+
|
|
711
|
+
# 契约 §5.4/§5.5(多靶场,beta.16):具名靶场登记表 `e2e.targets`(可选闭集)。
|
|
712
|
+
# 用例 `靶场:` 名字必须 ∈ 此表;表内路由与页面表路由同属「登记地址」——
|
|
713
|
+
# `goto` / `守卫` 路径门同样放行(地址仍单源于 stack-profile,不开放自由路径)。
|
|
714
|
+
def _named_targets() -> dict[str, str]:
|
|
715
|
+
if not _prof_path.is_file():
|
|
716
|
+
return {}
|
|
717
|
+
try:
|
|
718
|
+
cfg = stack_profile.parse_profile(_prof_path.read_text(encoding="utf-8")).get("e2e") or {}
|
|
719
|
+
except Exception:
|
|
720
|
+
return {}
|
|
721
|
+
t = cfg.get("targets") if isinstance(cfg, dict) else None
|
|
722
|
+
if not isinstance(t, dict):
|
|
723
|
+
return {}
|
|
724
|
+
return {str(k).strip(): str(v or "").strip() for k, v in t.items() if str(k).strip()}
|
|
725
|
+
|
|
726
|
+
_named_targets_map = _named_targets()
|
|
727
|
+
_target_routes = list(_named_targets_map.values())
|
|
717
728
|
nested_prefix, unchanged_no_action = [], []
|
|
718
729
|
initial_no_reason = []
|
|
719
730
|
after_bad = []
|
|
720
731
|
waitfor_assert = []
|
|
721
732
|
delta_bad = []
|
|
733
|
+
request_bad = []
|
|
722
734
|
cross_page_bad = []
|
|
723
735
|
# R10 门用反查表 page_routes_map 已在页面表解析处(上方)构建
|
|
724
736
|
for g in grid:
|
|
@@ -857,7 +869,8 @@ def validate(root: Path) -> Result:
|
|
|
857
869
|
p = parts[1].strip()
|
|
858
870
|
# 契约 §5.5(2026-10-18,D109 / 2026-10-22 hash 查询):`goto` 可带常规查询与
|
|
859
871
|
# hash 查询 ⇒ 路径门按**去双查询后的路径形态**匹配(`_route_hit` 内部同口径)。
|
|
860
|
-
|
|
872
|
+
# 契约 §5.4/§5.5(多靶场,beta.16):已登记靶场路由同属「登记地址」,同门放行。
|
|
873
|
+
if _route_hit(p, page_routes) or _route_hit(p, _target_routes):
|
|
861
874
|
continue
|
|
862
875
|
goto_path.append(f"{g['用例ID']}:{p}")
|
|
863
876
|
# 契约 §5.7(2026-09-30 补):`auth` 取值白名单(preset | none)——文本期挡住,
|
|
@@ -865,6 +878,11 @@ def validate(root: Path) -> Result:
|
|
|
865
878
|
auth = d.get("auth", "preset")
|
|
866
879
|
if auth not in ("preset", "none"):
|
|
867
880
|
auth_bad.append(f"{g['用例ID']}:auth={auth}")
|
|
881
|
+
# 契约 §5.4/§5.5(多靶场,beta.16):`靶场:` 取值闭集 —— 名字必须 ∈ `e2e.targets`;
|
|
882
|
+
# 整段未声明而用例钉名 ⇒ FAIL(与生成器中止双保险,不猜)。
|
|
883
|
+
_target_decl = d.get("靶场")
|
|
884
|
+
if isinstance(_target_decl, str) and _target_decl.strip() and _target_decl.strip() not in _named_targets_map:
|
|
885
|
+
target_bad.append(f"{g['用例ID']}:{_target_decl.strip()}")
|
|
868
886
|
# 契约 §5.5(2026-10-16 补,issue #13):`会话:` 行形态 + 键白名单。
|
|
869
887
|
# 与生成器中止**双保险**(文本期挡 + 生成期中止):白名单是项目级声明,
|
|
870
888
|
# 防把「数据态」走私成会话参数(§5.7 禁令不破)。
|
|
@@ -898,7 +916,8 @@ def validate(root: Path) -> Result:
|
|
|
898
916
|
# 出现绕过 `parse_block` 的带注释原始值(上游 F1 之后端到端已造不出污染值),
|
|
899
917
|
# 本门也不把注释当路径。与 `goto` 门 / `会话` 门共用同一 `strip_inline_comment`。
|
|
900
918
|
_guard_path = strip_inline_comment(_guard).strip()
|
|
901
|
-
|
|
919
|
+
# 契约 §5.4/§5.5(多靶场,beta.16):已登记靶场路由同属「登记地址」,同门放行。
|
|
920
|
+
if page_routes and not _route_hit(_guard_path, page_routes) and not _route_hit(_guard_path, _target_routes):
|
|
902
921
|
guard_bad.append(f"{g['用例ID']}:{_guard_path}")
|
|
903
922
|
|
|
904
923
|
# 契约 §5.6 R7(2026-09-23 补;**2026-09-25 修正为真矛盾对**):同一条用例内,对同一
|
|
@@ -919,7 +938,7 @@ def validate(root: Path) -> Result:
|
|
|
919
938
|
for raw in raw_exp:
|
|
920
939
|
e = _strip_prefix(raw)
|
|
921
940
|
bits = _code(e).split(" ")
|
|
922
|
-
if len(bits) >= 2 and bits[0] in ASSERT_VERBS:
|
|
941
|
+
if len(bits) >= 2 and bits[0] in ASSERT_VERBS and bits[0] != "request":
|
|
923
942
|
verbs_by_slice.setdefault((_step_anchor(raw), bits[1]), set()).add(bits[0])
|
|
924
943
|
for (slice_n, tid), verbs in verbs_by_slice.items():
|
|
925
944
|
where = f"{g['用例ID']}:{tid}" + (f"@after:{slice_n}" if slice_n else "")
|
|
@@ -950,6 +969,26 @@ def validate(root: Path) -> Result:
|
|
|
950
969
|
m_anchor = AFTER_RE.match(raw.lstrip())
|
|
951
970
|
if m_anchor is not None or (m_anchor.group(2) if m_anchor else raw).lstrip().startswith(LINE_PREFIXES):
|
|
952
971
|
delta_bad.append(f"{g['用例ID']}:叠写")
|
|
972
|
+
# D142:`request <api> <json-path> <expected>` 形态门。三态缺席语义、api 后缀、
|
|
973
|
+
# 必须有 step(动作锚点);`(空)` 因无法区分「缺失 / null」而拒绝。
|
|
974
|
+
for raw in raw_exp:
|
|
975
|
+
has_kind = raw.lstrip().startswith(LINE_PREFIXES)
|
|
976
|
+
code = _code(_strip_prefix(raw))
|
|
977
|
+
if not code.startswith("request "):
|
|
978
|
+
continue
|
|
979
|
+
mreq = re.match(r"^request\s+(\S+)\s+(\S+)\s+(.+)$", code)
|
|
980
|
+
if not mreq:
|
|
981
|
+
request_bad.append(f"{g['用例ID']}:形态(需 request <api> <path> <expected>)")
|
|
982
|
+
continue
|
|
983
|
+
api, _path, expected = mreq.groups()
|
|
984
|
+
if has_kind:
|
|
985
|
+
request_bad.append(f"{g['用例ID']}:与 initial/unchanged 叠写")
|
|
986
|
+
if not re.fullmatch(r"[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+", api):
|
|
987
|
+
request_bad.append(f"{g['用例ID']}:api 须为带点后缀({api})")
|
|
988
|
+
if expected.strip() == "(空)":
|
|
989
|
+
request_bad.append(f"{g['用例ID']}:(空) 三态不明,请用 (缺席)/null/\"\"")
|
|
990
|
+
if not step:
|
|
991
|
+
request_bad.append(f"{g['用例ID']}:无 step(动作锚点不存在)")
|
|
953
992
|
# 契约 §5.6 R10(2026-10-05 补,P11):跨页用例终态断言必须落首列页(断言落点页)。
|
|
954
993
|
# 多页用例中,未加 after: 步骤锚的 expected 断言如果在非首列页上求值,会因为目标 DOM 缺失而沦为空洞真。
|
|
955
994
|
pgs = d.get("pages", [])
|
|
@@ -1070,6 +1109,8 @@ def validate(root: Path) -> Result:
|
|
|
1070
1109
|
res.add("守卫 路径 ∈ 页面表", "PASS" if not guard_bad else "FAIL",
|
|
1071
1110
|
f"页面表外路径={_format_list(guard_bad)}(契约 §5.5:守卫导航用的具体路径也不得私藏于分片)" if guard_bad else "")
|
|
1072
1111
|
res.add("auth 取值合法", "PASS" if not auth_bad else "FAIL", f"白名单外(允许 preset|none)={_format_list(auth_bad)}")
|
|
1112
|
+
res.add("靶场 取值合法", "PASS" if not target_bad else "FAIL",
|
|
1113
|
+
f"不在 `e2e.targets` 闭集内 / 未登记即钉名={_format_list(target_bad)}(契约 §5.4/§5.5:具名靶场必须先在 stack-profile 登记)" if target_bad else "")
|
|
1073
1114
|
res.add("会话 取值合法", "PASS" if not session_bad else "FAIL",
|
|
1074
1115
|
f"形态非法 / 白名单外={_format_list(session_bad)}(契约 §5.5:键须在 `e2e.session_preset.keys` 内)" if session_bad else "")
|
|
1075
1116
|
res.add("参数 声明有效", "PASS" if not inject_bad else "FAIL",
|
|
@@ -1093,6 +1134,8 @@ def validate(root: Path) -> Result:
|
|
|
1093
1134
|
f"`waitFor` 出现在 expected(应写在 step)= {_format_list(waitfor_assert)}")
|
|
1094
1135
|
res.add("delta 相对断言形态", "PASS" if not delta_bad else "FAIL",
|
|
1095
1136
|
f"`delta` 值须为 `±N` 且不得与豁免通道/步骤锚叠写 = {_format_list(delta_bad)}")
|
|
1137
|
+
res.add("request 断言形态", "PASS" if not request_bad else "FAIL",
|
|
1138
|
+
f"`request` 需 api 带点后缀 + json-path + 三态值,且必须有 step = {_format_list(request_bad)}")
|
|
1096
1139
|
res.add("豁免前缀不嵌套", "PASS" if not nested_prefix else "FAIL", f"{_format_list(nested_prefix)}")
|
|
1097
1140
|
res.add("步骤锚合法", "PASS" if not after_bad else "FAIL", f"{_format_list(after_bad)}")
|
|
1098
1141
|
res.add("跨页终态断言落首列页", "PASS" if not cross_page_bad else "FAIL", f"{_format_list(cross_page_bad)}")
|
|
@@ -1112,8 +1155,36 @@ def validate(root: Path) -> Result:
|
|
|
1112
1155
|
_lvl, _det = seed_models_check(root, blocks)
|
|
1113
1156
|
res.add("seed × models 对账", _lvl, _det)
|
|
1114
1157
|
# 契约 §5.5(B201-1,2026-10-12):upload 行的 fixture 引用存在(值形态 + 文件存在)
|
|
1115
|
-
_lvl, _det = upload_fixture_check(root, blocks)
|
|
1158
|
+
_lvl, _det = upload_fixture_check(root, blocks, lay)
|
|
1116
1159
|
res.add("upload fixture 引用存在", _lvl, _det)
|
|
1160
|
+
# D143:manual.md 旧行 + `atlas-manual` 块(解析单源 `_support/manual_block.py`)
|
|
1161
|
+
manual_path = e2e / "manual.md"
|
|
1162
|
+
if not manual_path.is_file():
|
|
1163
|
+
res.add("manual 走查项形态", "N/A", "无 manual.md(可选增强)")
|
|
1164
|
+
else:
|
|
1165
|
+
try:
|
|
1166
|
+
from _support.manual_block import parse_manual
|
|
1167
|
+
manual_items = parse_manual(manual_path.read_text(encoding="utf-8"))
|
|
1168
|
+
except Exception as exc: # 坏块必须响亮失败,不得静默忽略
|
|
1169
|
+
res.add("manual 走查项形态", "FAIL", f"解析失败:{exc}")
|
|
1170
|
+
else:
|
|
1171
|
+
manual_bad: list[str] = []
|
|
1172
|
+
seen_ids: set[str] = set()
|
|
1173
|
+
for m in manual_items:
|
|
1174
|
+
mid = m.get("id") or ""
|
|
1175
|
+
if not mid:
|
|
1176
|
+
manual_bad.append("缺 ID")
|
|
1177
|
+
elif mid in seen_ids:
|
|
1178
|
+
manual_bad.append(f"{mid}:ID 重复")
|
|
1179
|
+
seen_ids.add(mid)
|
|
1180
|
+
if m.get("page") not in known:
|
|
1181
|
+
manual_bad.append(f"{mid}:页面不在页面表({m.get('page')})")
|
|
1182
|
+
steps = m.get("steps") or []
|
|
1183
|
+
for i, s in enumerate(steps, 1):
|
|
1184
|
+
if not s.get("action") or not s.get("expect"):
|
|
1185
|
+
manual_bad.append(f"{mid}:步骤{i} 缺操作/观察")
|
|
1186
|
+
res.add("manual 走查项形态", "PASS" if not manual_bad else "FAIL",
|
|
1187
|
+
f"{_format_list(manual_bad)}" if manual_bad else "")
|
|
1117
1188
|
return res
|
|
1118
1189
|
|
|
1119
1190
|
|
|
@@ -39,6 +39,7 @@ HERE = Path(__file__).resolve().parent
|
|
|
39
39
|
if str(HERE.parent) not in sys.path:
|
|
40
40
|
sys.path.insert(0, str(HERE.parent))
|
|
41
41
|
from _support.siblings import load_module # noqa: E402
|
|
42
|
+
from _support.layout import LayoutError, load_layout # noqa: E402
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
class Result:
|
|
@@ -152,15 +153,21 @@ def mock_after_step_violations(text: str, case_block_re, list_keys: set[str]) ->
|
|
|
152
153
|
|
|
153
154
|
def validate(root: Path) -> Result:
|
|
154
155
|
res = Result()
|
|
155
|
-
|
|
156
|
+
try:
|
|
157
|
+
lay = load_layout(root)
|
|
158
|
+
except LayoutError as exc:
|
|
159
|
+
res.add("项目布局配置", "FAIL", str(exc))
|
|
160
|
+
return res
|
|
161
|
+
e2e_dir = lay.e2e_dir
|
|
156
162
|
cases_dir = e2e_dir / "cases"
|
|
157
163
|
index = e2e_dir / "e2e-index.md"
|
|
158
164
|
if not index.is_file() or not cases_dir.is_dir():
|
|
159
|
-
res.add("mock 门前置", "N/A",
|
|
165
|
+
res.add("mock 门前置", "N/A",
|
|
166
|
+
f"E2E 用例资产未建(缺 {lay.print_value('e2e')}/e2e-index.md 或 cases/)")
|
|
160
167
|
return res
|
|
161
168
|
|
|
162
169
|
gen = load_generator()
|
|
163
|
-
prof_path =
|
|
170
|
+
prof_path = lay.stack_profile
|
|
164
171
|
e2e_cfg: dict = {}
|
|
165
172
|
if prof_path.is_file():
|
|
166
173
|
e2e_cfg = gen.parse_profile(prof_path.read_text(encoding="utf-8")).get("e2e") or {}
|
|
@@ -175,7 +182,7 @@ def validate(root: Path) -> Result:
|
|
|
175
182
|
raw_prefix = mock_cfg.get("api_prefix")
|
|
176
183
|
prefix = raw_prefix if raw_prefix is not None else ("/" if mode in ("on", "auto") else None)
|
|
177
184
|
|
|
178
|
-
shards = gen.gather_cases(cases_dir)
|
|
185
|
+
shards = gen.gather_cases(cases_dir, lay.print_value("e2e_cases"))
|
|
179
186
|
mock_pages = sorted(p for p, items in shards.items() if any(c.get("mock") for c in items))
|
|
180
187
|
mock_case_ids = [c["id"] for items in shards.values() for c in items if c.get("mock")]
|
|
181
188
|
|
|
@@ -237,7 +244,7 @@ def validate(root: Path) -> Result:
|
|
|
237
244
|
|
|
238
245
|
missing = sorted({r for r in refs if not _resolve_fixture(r).is_file()})
|
|
239
246
|
res.add("fixture 引用存在", "PASS" if not missing else "FAIL",
|
|
240
|
-
f"缺={missing[:8]}(引用相对
|
|
247
|
+
f"缺={missing[:8]}(引用相对 {lay.print_value('e2e')}/,须以 fixtures/ 开头)")
|
|
241
248
|
if fixtures_root.is_dir():
|
|
242
249
|
on_disk = sorted("fixtures/" + p.relative_to(fixtures_root).as_posix()
|
|
243
250
|
for p in fixtures_root.glob("**/*.json"))
|