atlas-project-harness 0.1.0-beta.21 → 0.1.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 (43) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/DESIGN.md +16 -4
  3. package/_support/__pycache__/pw_runner.cpython-312.pyc +0 -0
  4. package/_support/__pycache__/stack_profile.cpython-312.pyc +0 -0
  5. package/_support/__pycache__/url_merge.cpython-312.pyc +0 -0
  6. package/_support/pw_runner.py +257 -0
  7. package/_support/url_merge.py +62 -0
  8. package/install.sh +3 -0
  9. package/package.json +1 -1
  10. package/rings/e2e/reference.md +10 -1
  11. package/scripts/__pycache__/atlas_check.cpython-312.pyc +0 -0
  12. package/scripts/__pycache__/e2e_console.cpython-312.pyc +0 -0
  13. package/scripts/__pycache__/gen_e2e_scripts.cpython-312.pyc +0 -0
  14. package/scripts/__pycache__/mutation_proof.cpython-312.pyc +0 -0
  15. package/scripts/atlas_check.py +75 -82
  16. package/scripts/duty_signals.sh +62 -0
  17. package/scripts/e2e_console.py +47 -81
  18. package/scripts/gen_e2e_scripts.py +52 -13
  19. package/scripts/mutation_proof.py +20 -1
  20. package/shared/gates.md +10 -8
  21. package/shared/stack-profile.md +2 -2
  22. package/tests/__pycache__/test_atlas_check.cpython-312-pytest-9.1.1.pyc +0 -0
  23. package/tests/__pycache__/test_duty_bug_families.cpython-312-pytest-9.1.1.pyc +0 -0
  24. package/tests/__pycache__/test_e2e_console.cpython-312-pytest-9.1.1.pyc +0 -0
  25. package/tests/__pycache__/test_gen_e2e_scripts.cpython-312-pytest-9.1.1.pyc +0 -0
  26. package/tests/__pycache__/test_shared_kernel.cpython-312-pytest-9.1.1.pyc +0 -0
  27. package/tests/__pycache__/test_tools.cpython-312-pytest-9.1.1.pyc +0 -0
  28. package/tests/__pycache__/test_validators.cpython-312-pytest-9.1.1.pyc +0 -0
  29. package/tests/test_atlas_check.py +219 -13
  30. package/tests/test_duty_bug_families.py +96 -0
  31. package/tests/test_e2e_console.py +166 -6
  32. package/tests/test_gen_e2e_scripts.py +143 -0
  33. package/tests/test_shared_kernel.py +21 -0
  34. package/tests/test_tools.py +7 -1
  35. package/tests/test_validators.py +106 -2
  36. package/validators/__pycache__/validate_e2e_index.cpython-312.pyc +0 -0
  37. package/validators/__pycache__/validate_stack_profile.cpython-312.pyc +0 -0
  38. package/validators/__pycache__/validate_testids.cpython-312.pyc +0 -0
  39. package/validators/validate_e2e_index.py +26 -14
  40. package/validators/validate_stack_profile.py +5 -1
  41. package/tests/__pycache__/test_gen_e2e_scripts.cpython-312.pyc +0 -0
  42. package/tests/__pycache__/test_tools.cpython-312.pyc +0 -0
  43. package/tests/__pycache__/test_validators.cpython-312.pyc +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,156 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ### 🔧 E2E 真跑:node 轨运行器判据换本体(D101;issue #24)
10
+
11
+ **背景**:百炼/v2前端真跑准备日,`product/e2e/scripts` 下 `playwright test --list` 报 `Cannot find package '@playwright/test'`——而 `atlas_check` 的「E2E 真跑」门与 E2E 控制台**两侧都放行了**:判据都是「可执行文件存在」。项目 `package.json` 声明 `"type": "module"` ⇒ 生成物走 ESM 解析 ⇒ Node 的 ESM 解析器**不读 `NODE_PATH`**(该通道属 CJS)⇒ 全局兜底(F19)找得到 binary、解析不到模块:错误落到运行期,且被门计成「跑失败 / 无结果」(真原因不可见)。`shared/gates.md` 2.2「E2E 真跑」行早已写明「解释器按**依赖可导入性**选(不按路径存在)」——是实现漂离了已落口径。
12
+
13
+ **定案**:
14
+ - **判据本体** = 「从 `e2e.scripts_dir` 出发 `@playwright/test` 可解析」(探针 = `node --input-type=module -e "import('@playwright/test')"`,解析基准 = cwd,本机实测)。
15
+ - **单源内核** `_support/pw_runner.py`(新):二进制解析 / 探针 / 修复收敛到一处——消除 `scripts/atlas_check.py` 与 `scripts/e2e_console.py` 的两份拷贝(含两份 `_is_node_playwright`)。
16
+ - **全局兜底改为可生效的修复**:不可解析且运行器来自全局 ⇒ 执行器在 Node 的 `node_modules` 搜索路径上补**幂等软链** + 复检,并输出「用了全局什么」;门侧修复成功 ⇒ 降 **`WARN`**(跑得起来,但结论依赖机器全局状态、非项目自足),不可修复 ⇒ `FAIL` 点名真原因。项目自装目录里缺模块 = 项目自身缺口 ⇒ **不修只报**(避免把全局版本混进项目依赖);链接位被非本工具占用 ⇒ **拒绝覆盖**。
17
+ - `NODE_PATH` 保留(CJS 项目的真通道)但**不再冒充判据**。
18
+
19
+ **取证**:`pytest tests -q` ⇒ **476 passed / 0 failed / 4 skipped**(beta.22 基线 472 passed ⇒ +4 钉);`tests/test_atlas_check.py` 24 → **25 条**、`tests/test_e2e_console.py` 9 → **12 条**;变异 **M88**(判据退回「binary 存在即 ok」)/ **M89**(摘软链步)/ **M90**(摘 `global_note`)三枚咬合、还原逐字节一致;`install.sh` 自检 44 → **46 条**(+`_support/pw_runner.py`;并顺手补存量漏登记的 `_support/case_id.py`——它一直在装配面上却不在自检清单里)。
20
+
21
+ **落地文件**:`DESIGN.md`(D101)/ `shared/gates.md`(2.2 真跑行判据与钉 + 2.3 计数行)/ `shared/stack-profile.md`(`e2e.node_modules` 字段语义)/ `rings/e2e/reference.md` §7(运行器前置)/ `_support/pw_runner.py`(新)/ `scripts/atlas_check.py` / `scripts/e2e_console.py` / 两个 tests / `install.sh`。**未做**:未改生成器(生成期代表不了运行机器,运行器可解析性由消费者在起跑前判)。
22
+
23
+ ### 📋 复发自动可见(DR4 口径 B)+「可执行性判据本体」写成流程门(D102;承接 issue #24)
24
+
25
+ **背景**:D101 那条属「**判据可被形态满足**」族(`#1/#4/#6/#7` 首批 beta.17 已修一轮,`#20` / `#24` 两轮复见)。而 DR4「同类 ≥3 次 ⇒ 需立机器门**或契约条款**」此前**全凭人工裁量**——族规模没人算、复发靠撞见。维护者 2026-10-17 拍板选项 **B**:两件都做。
26
+
27
+ - **口径 B(族规模)**:实现 = **单源模块** `duty-review/families.py`(原稿是 shell 内联 python —— 无钉、形状一漂即**静默归零**,而静默归零恰好让本机制要治的「复发靠撞见」原样回来),`scripts/duty_signals.sh` 只读调它。聚类单元 = `duty-review/bugs.md` 的段 + 段首「同根归纳」行,计数 = 段内 `#NN` 去重(上界近似),**升室判据 = 同根归纳在案 ∧ 案数 ≥3 ∧ 段内最近日期在近 30 天**;只满足前两项 ⇒ 记「族已静默」不升室(防同一族清算完还天天喊 = 红灯训练成噪声)。与既有**口径 A**(fix 类提交触达路径 30 天 ≥3,判**复发速率**)互补。实跑 = **3 族升室候选**(E2E 14 案 / 安装与装配 3 案 / 自感知 8 案)+ **1 族自动静默**(structure)。新增 `tests/test_duty_bug_families.py`(4 条:解析面 = 文件面 / 真实库数得出族规模 / 判定真值表 + 活跃窗边界 / 空输入不炸),变异 **M91**(计数正则退回行首 ⇒ 静默归零 ⇒ 钉红);全量 **480 passed / 4 skipped**、测试文件 32 → **33**(gates 计数行同步)。
28
+ - **契约条款(语义项,明示不设机器判据)**:`shared/gates.md` 2.5 新增「**可执行性判据本体**」流程门——判据必须问「**能不能真的用**」,不得以「东西在不在 / 文字写没写」代替;这类前置判据须具备 ① 一次性功能性探针(真跑一次:import / `--list` / dry-run)② **反面包**(东西在、但用不了 ⇒ 必须红)③ 失败原因不得被吞。附同族先例六条与人工追问清单三条。
29
+ - **为什么不做通解**:判「判据 ≠ 性质」要读意图 ⇒ 语义,机器判不了;硬立通用门 = 用可被形态满足的门治可被形态满足的病(同族递归)。该族复发**都发生在老门老代码上**(纪律只覆盖「新增/修改门」那一刻)——机器门治不了「没人回头看」,故本决策只做「让复发被看见 + 给写门一把尺子」。
30
+ - **登记面**:`DESIGN.md` D102 / `shared/gates.md` 2.5 / `duty-review/README.md` §2+§4 / `scripts/duty_signals.sh` / `duty-review/bugs.md`(自感知段补「同根归纳」锚行——此前该族无锚,只能靠 E2E 段虚高计数间接看见)。
31
+
32
+ ### 🪲 「E2E 真跑」门:标量取值剥行内注释(issue #25;D94 ③ 残余的现场实证)
33
+
34
+ **现场**:百炼/v2前端全量门(重装配到 D101 后首跑)—— `e2e.reset 失败(exit 127):/bin/sh: null: command not found`。`stack-profile.yaml` 写的是 `reset: null # 真跑前靶场重置 shell 命令串`(**合法 YAML**:`#` 前有空白 ⇒ 行内注释),门把注释整段当值去执行 ⇒ **假红,且在跑到用例之前**(真跑还没开始,「谁红、为什么红」两件事一起被错认)。
35
+
36
+ **修法**:`scripts/atlas_check.py` 的 `check_e2e` 四处标量取值(`cases_dir` / `app_base_url` / `reset` / `node_modules`)统一过内核 `strip_inline_comment`(与 D94 **同一函数对象**;本文件首次引入该内核依赖)。此前四处各用正则直取 ⇒ 正是 D94 ③ 明写「其余文本期门仍在本次收敛面之外,登记为残余」的那一类。
37
+
38
+ **钉与取证**:`tests/test_atlas_check.py` 25 → **27 条**(双侧:注释不得污染取值 ⇒ `reset: null # 说明` 不执行;真写的发令必须照发 ⇒ `reset: echo …` 落文件 —— 防「把门修哑」);变异 **M92**(四处取值退回 `m.group(1)` ⇒ 红,还原逐字节一致);`pytest tests -q` ⇒ **482 passed / 4 skipped**。
39
+
40
+ **残留(仍开,非本批)**:全源包其余剥注释站点未收(`validators/validate_e2e_index.py` 10 处手写 `split("#",1)[0].strip()` 等)⇒ D94 ③ 的待办**部分**清偿、未字面全真;**新增观察**:`reset` 取值清洗用 `.strip("\"'")`(两端同时剥,非「剥成对引号」)⇒ 结尾带引号的合法发令会被吃一个字符(无现场证据,未改,登记备查)。
41
+
42
+ ### 📋 `e2e.app_login` 不开第四形态:不可自动预置的靶场走「人工会话取出」**描述路径**(D103;issue #26)
43
+
44
+ **现场**(百炼/v2前端真跑准备日):靶场**有访问门**但三态**都装不下** —— 图形验证码(`login_captcha`)+ 服务端 **cookie 会话**(`COOKIE_JSESSIONID`)+ 登录**响应体无令牌**(`data` 是 URL 串);而 `validate_stack_profile` 只喊「请声明」⇒ **死胡同**(真跑门已能跑到用例,三条红全在未登录态)。
45
+
46
+ **定案(维护者拍板原话)**:「我想让 ai 主动给人生成这种类似的命令,然后登录完就完全没问题了」+「不用非要机器门吧,描述不是也可以」⇒ **不加字段、不加门**,落**描述 + 出路文案**:
47
+
48
+ - `rings/e2e/reference.md` §7.1 增「**人工会话取出(描述路径,非机器门)**」节:AI 主动给人 `playwright codegen --save-storage=<e2e.scripts_dir>/.atlas-auth/state.json "<e2e.app_base_url>"`,人登录一次;**四条纪律** = 关窗才落盘 / 会话短命令要**立即**跑 / `<scripts_dir>/.gitignore` 已含 `.atlas-auth/` 且生成器 setup 有**幂等守卫**(无凭据 + 已有合法 state ⇒ 保留,不覆盖人的会话)/ 它是**运行期凭据不是应然资产**。
49
+ - `shared/stack-profile.md` §2:`app_login` 行注明「**三态之外不设第四态**」。
50
+ - `validate_stack_profile` 的 WARN:由「请声明」改为**给出路**(含命令原型);钉 `test_stack_profile_login_unstated_warn_gives_a_way_out`(`tests/test_validators.py` 112 → **113 条**)+ 变异 **M93**(把出路换成一句空话 ⇒ 红;还原逐字节一致)。
51
+ - **对 D80 ② 的注记(显式登记,不静默)**:D80 的「段缺失不再是合法降级 ⇒ 升档 = `not_ready: app_login`」与本行**相抵于一类项目**(有门但无形态可声明)——该升档落地时**须先把这一类排除**,否则会把「如实未声明」罚成 `not_ready`(`shared/gates.md` 2.2 行已登记该相抵点)。
52
+
53
+ **被否**:补第四形态 `manual`/`session_import`(契约增量四处同改 + 把**不可复现**固化成标准姿势);把这类靶场塞进 `real`(一个形态说两件事);用 `mode: none` 充数(与事实相反)。
54
+
55
+ **能力边界(不得 oversell)**:该路径**不可复现**(每轮要人登录 ⇒ CI 上不可用)、会话短命 ⇒ 是**兜底**而非 `app_login` 的替代形态;**无机器判据**,如实登记为纪律与盲区面。
56
+
57
+ **取证**:`pytest tests -q` ⇒ **483 passed / 4 skipped**。
58
+
59
+ ### 🪲 变异证明**证据形态**补强:报告必须含注入 diff(独立评审驱动;M88/M90 复验)
60
+
61
+ **来源**:`proposals/REVIEW-2026-10-17-source-batch.md`(独立评审,干净上下文)判「同批 M88/M90 的变异证据**不足以支撑自述机制**,全批缺注入 diff ⇒ 独立复现性不足」。本条只动**证据面**,不动任何判定逻辑。
62
+
63
+ - **工具**(`scripts/mutation_proof.py`):报告新增 **`## 注入 diff`** 段(`injected_diff()`,`difflib` unified diff ±3 行)——没有它,「反向破坏的是不是自述那一处」无法独立复现,证据止于作者自述。四段协议不变。
64
+ - **M88 复验(红因错位已修)**:原报告红因实为**夹具假 `Proc` 缺 `stderr`** ⇒ `atlas_check.run()`(`:122`)崩,**从未触到 status 断言** ⇒ 钉子会响但机制未被证明。六处假 `Proc` 补 `stderr=""` 后重跑:红因已对齐为「status 仍 `FAIL`、但 detail 退化成『无结果』」= **判据被绕过导致真原因丢失**本身。
65
+ - **M90 复验**:断言补具名消息(原为空 `AssertionError:`),报告现含注入描述 + diff ⇒ 与 M89 共用用例但**注入面不同**,可辨。
66
+ - **M88–M93 六枚全部带 diff 重跑**:`/tmp/atlas-mutation-2026-10-17/`(每份含原/后 sha256 + 基线 + **注入 diff** + 注入后 + 还原后)。
67
+ - **钉**:`tests/test_tools.py::test_mutation_proof_happy_path` 补三条断言(报告含 `## 注入 diff` / `-ok` / `+bad`)⇒ 工具能力不会静默丢。
68
+ - **登记面**:`shared/gates.md` 2.5「变异证明」行(查什么 + 假绿测试列)与 2.2「E2E 真跑」行(复验注记)。
69
+ - **取证**:`pytest tests -q` ⇒ **483 passed / 4 skipped**。
70
+
71
+ ### 🔧 E2E:宿主注入型 base query 由生成物转发(D104;issue #27;两轮独立评审后落刀)
72
+
73
+ **病灶**:某类应用把宿主注入的前提(tenant)**只从 `window.location.search` 读**(无 sessionStorage 回落)⇒ `会话:` 通道(D89,sessionStorage)**送不到**;`goto` 又不得带 `?`(§5.7);而 base 一旦带 query,**绝对路径会丢 query**(`new URL('/console/#/x', '…?switchAgent=7202')` ⇒ query 消失)。三条各自成立 ⇒ 无合法写法 ⇒ 真跑导航落到「请选择业务空间」,白屏假红。
74
+
75
+ **两轮评审**:`proposals/REVIEW-2026-10-17-source-batch.md`(2 MUST / 5 SHOULD)+ `proposals/REVIEW-2026-10-17-Bprime.md`(10 MUST / 2 SHOULD / 3 OBSERVE)。
76
+
77
+ - **第一案 A 被落刀实测推翻**:让路由列允许 `#/…` 相对形态(`new URL('#/x', base)` 能保住 base 的 path+query),但 `.atlas-case` 里 `goto #/…` 的 `#` 前有空格 ⇒ 按 D94 行内注释语义被截断成 `'goto'`(`守卫:` 标量同样)⇒ **物理不可写**;救它要改 D94 刚收敛的共享注释语义(`proposals/RFC-20261017-01` §9.1)。
78
+ - **定案 B′(评审建议的生成期纯函数变换,不是运行期助手)**:路由列保持**路径形态**(`/console/#/app/agent/detail`,`#` 紧邻 `/` ⇒ 不被注释吞)⇒ 上一轮评审的四处消费者**无需改**;改由**生成期**把 base 的 query 并进**三处**导航发射面(step `goto` / 元素齐全守卫 / 零步基线 `data.url`),单源 = 新增 `_support/url_merge.py`;控制台手工「打开」(**第五消费者**)的 `base + path` 拼接同步修正为 `origin + 合并路径`。
79
+ - **口径三条**:base 无 query ⇒ 生成物**逐字节零变化**;`ATLAS_BASE_URL` **只覆盖 host、不覆盖 base 的 query**;**边界明示**——base query 成为前提后,「它缺席时」的行为无法用同一份用例表达(`会话:`/`auth: none` 不同消费面,不构成逃逸口),要 opt-out 须另起决策行(触 D99)。
80
+ - **落刀现场补一处(D104 ⑦,D94 ③ 残余清偿)**:step 的 `goto` 曾丢 hash 段 —— `parse_line` 用 `line.partition("#")`(任何 `#` 都截断,D94 ② 的列表项例外),路径形态 `/console/#/…` 被砍成 `/console/`;改走内核 `strip_inline_comment` 后值保真、尾随说明照剥(改判 D94 ② 该例外,旧行号 + 新证据已登记)。余两处 `partition("#")` 无现场证据、未动。
81
+ - **钉与变异**:值保真(含列表项)/ 三处齐并 / 无 query 零变化 / 控制台绝对地址;变异 **MB1/MB2/MB3/MB4** 四枚咬合、还原逐字节一致(报告在 `/tmp/atlas-mutation-2026-10-17/`,含注入 diff)。
82
+ - **取证**:`pytest tests -q` ⇒ **487 passed / 4 skipped**;`install.sh` 自检 46 → **47 条**(+`_support/url_merge.py`)。
83
+ - **登记面**:`DESIGN.md` D104 / `rings/e2e/reference.md` §3.2 + §5.5 / `shared/stack-profile.md` §2 / `shared/gates.md`(`生成物最新` 零变化钉 + 2.2 真跑行)/ `duty-review/bugs.md` #27。
84
+
85
+ ### ⏱️ E2E 断言超时下限:生成的运行器配置必须声明 `expect.timeout`(D105;issue #28)
86
+
87
+ **现场**(#27 落地后):三处导航 URL 全对、元素**真在**(探针实测 `disabled=true` + 合规 `aria-label`),真跑仍 `3 failed 1 passed`。根因 = 生成的 `playwright.config.ts` **没有 `expect` 段** ⇒ 继承 Playwright **默认 5s**,而该应用首屏实测 **14,184 ms**(dev bundle 13.8MB)⇒「元素还没渲染」被误判成「元素不存在」= **假红**。
88
+
89
+ - **定案**:生成器在两处 config 分支发射 `expect: { timeout: Number(process.env.ATLAS_EXPECT_TIMEOUT ?? 20_000) }`。
90
+ - **它不是「固定等待」**:`expect` 是轮询式 auto-wait,本改只抬**上限**(不引入 sleep,不破 §5.5 禁令)。
91
+ - **被否**:只给守卫加 timeout(其余断言仍 5s);调 test 超时(兜底不解决断言级假红);固定 sleep(契约明禁)。
92
+ - **代价(如实)**:真失败从 5s 变 20s 才红;慢环境可用 `ATLAS_EXPECT_TIMEOUT` 调低。
93
+ - **钉与变异**:`test_runner_config_declares_expect_timeout_floor` + 变异 **MB5**(摘发射 ⇒ 红,还原逐字节一致)。
94
+ - **取证**:`pytest tests -q` ⇒ **488 passed / 4 skipped**。
95
+
96
+ ### 🧭 E2E 执行入口的 cwd 口径统一:真跑门改以 `<scripts_dir>` 为 cwd(D106;issue #29)
97
+
98
+ **现场**:#27 / #28 落地后真跑仍 `3 failed 1 passed`,而**同一份产物** `cd scripts` 直跑 **8.2s 全绿**。差异 = **cwd**:生成物里的相对路径(`storageState: ".atlas-auth/state.json"`、`outputDir: ".atlas-run"`、mock fixture)**全是 cwd 相对**的,`atlas_check` 却用 `cwd=项目根` + `-c <相对路径>` 跑 ⇒ storageState 被解析到 `<root>/.atlas-auth/`(**并在那儿写下空 state**,实测 `{"cookies":[],"origins":[]}`)⇒ 读不到人工会话 ⇒ 未登录态跑用例 ⇒ 断言等满 20s **假红**。控制台本来就是 `cd scripts` 再跑。
99
+
100
+ - **定案**:真跑门改 `cwd=<scripts_dir>` + `-c playwright.config.ts`(与执行器同一口径);契约侧新增「**执行 cwd(硬)**」条。
101
+ - **被否**:把生成物相对路径改成绝对路径(派生件带机器绝对路径 ⇒ 不可移植、破「生成物 = 确定性派生物」);只修 setup 的 state 路径(其余相对路径仍漂)。
102
+ - **教训(写进 D106 ④)**:**「同一产物在两个入口下跑出不同结论」先查 cwd / 环境**,否则会把执行器差异误判成用例或实现缺陷(本轮先误判为超时不足——由 D105 兜住超时面——再查出真因)。
103
+ - **钉与变异**:`test_check_e2e_runs_with_scripts_dir_as_cwd`(截 `run()` 的 (cmd, cwd))+ 变异 **MB6**(退回 root ⇒ 红,还原逐字节一致)。
104
+ - **取证**:`pytest tests -q` ⇒ **489 passed / 4 skipped**。
105
+
106
+ ### 🧪 零步基线的每个探针必须「缺席即判不成立」(D107;issue #30)
107
+
108
+ **现场**:`atlas_check --page app-agent-detail` ⇒ 两条真用例全绿,唯零步基线 `Test timeout of 30000ms exceeded`。带参页面的页级路由本就不渲染详情(`D104` 允许的形态),而 `attr` 探针发射 `getByTestId(t).first().getAttribute(n)` —— **没有 `count()` 守卫**,Playwright 对**未挂载**元素的 `getAttribute` 会等到默认超时 ⇒ 4 探针 × 2 趟撑爆 test 预算。契约 §5.5 早已明写「基线探不到元素按 `null = 不成立` 处理」⇒ **等挂载 = 违约**。
109
+
110
+ - **定案**:`attr` 探针加 `count() > 0 &&` 前置(缺席 ⇒ `false` = 不成立);并升为**生成器通用不变式**:任何探针表达式都必须带缺席守卫(`count()) > 0` / `tsVis(` / `tsVisible(` / `atlasValueOf(`)。
111
+ - **被否**:加大 test 超时(把等挂载合法化);带参页面跳过基线(§5.5 已选择回 `null`);只在项目侧声明 `守卫:`(那是资产选择题,不修 atlas 违约面)。
112
+ - **钉与变异**:`test_zero_step_probes_never_wait_for_attachment`(对**全部**探针做通用守卫检查)+ 变异 **MB7**。
113
+ - **出库**:探针行为属**运行面** ⇒ 待下一轮真跑复验后出库。
114
+ - **取证**:`pytest tests -q` ⇒ **490 passed / 4 skipped**。
115
+
116
+ ### 🧹 D94 ③ 残余清偿:剥注释口径**全域单源**(`D94 ⑥`;MB8/MB9)
117
+
118
+ D94 当初把「其余文本期门」登记为残余(改口径会变动其它门的既有判定)。本批一次清偿:
119
+
120
+ | 面 | 原状 | 改法 |
121
+ |---|---|---|
122
+ | `validators/validate_e2e_index.py` 的 `_code()` | `line.split("#", 1)[0].strip()` = **第二份口径**(任何 `#` 都截断) | 走内核 `strip_inline_comment` |
123
+ | 同文件 **9 处**内联 `split("#", 1)[0]` | 同上 | 全改 `_code(...)` |
124
+ | `scripts/gen_e2e_scripts.py` **3 处**内联 `split("#", 1)[0]` | 同上(本批结构钉抓出来的,原清单没数到) | 全改 `strip_inline_comment(...)` |
125
+ | `_parse_mock_line` 的 `partition("#")` | 同上 | 走内核 |
126
+ | `initial:` 理由检测的 `partition("#")` | 撞上断言值里的 `#`(如 `/console/#/x`)⇒ 把**路径尾巴当理由** = **假「有理由」**(假 PASS) | 走内核,理由 = **真注释**之后的内容 |
127
+ | `e2e.reset` 取值清洗 `.strip("\"\'")` | 从**两端**剥集合内任意字符 ⇒ 结尾带引号的合法发令被吃掉一个字符(静默改义) | 只剥**成对**引号(`_reset_command`) |
128
+
129
+ - **钉**:**结构钉**(行解析器的**代码行**里不得出现 `split("#"` —— 它当场抓出 3 处漏网)+ `initial:` 假理由**双侧**(无真理由 ⇒ WARN / 有真理由 ⇒ PASS)+ `_code` 保真 + `_reset_command` 行为(**穿门实际调用的那个函数**,不钉 helper 本体)。
130
+ - **变异**:**MB8**(`_code` 退回 split ⇒ 结构钉 + 假理由钉红)· **MB9**(`_reset_command` 退回两端同剥 ⇒ 红因正是「结尾引号被吃」)。
131
+ - **残留**:**无** —— D94 ③「剥注释全局一个函数对象」从「收敛面内成立」变为**字面全真**。
132
+ - **取证**:`pytest tests -q` ⇒ **494 passed / 4 skipped**(`test_validators` 114→116、`test_atlas_check` 28→29)。
133
+
134
+ ## [v0.1.0-beta.22] - 2026-10-17
135
+
136
+ ### ⚙️ E2E 生成链第三动能 `dispatch` + 台账收口三件(D98/D99/D100;v3 定形首跑全绿)
137
+
138
+ **背景**:v3 platform-frontend 切片定形首跑四轮迭代(2026-10-16 首跑 7/11 → 四跑聚焦)。004「连发新增 → 点选 → 删除」复跑复现,暴露「常态遮罩 + 入场动画」表面下 **force/native 双模双灭**(force 点击在动画窗口被吞 5s 轮询不动;native 因可点性检查永不过而整用例挂死);黑盒探针定界(动画窗内直发两删连中)后维护者拍板新增第三动能。同日**产品侧(spark-flow)修复便利贴编辑内容保存管道**(追路探针 A/B 双版复核:零停顿链贴纸+内容双落库),净场协议(清零 → 静置 >15s 节流窗 → 复清)复跑 **11/11 全绿**,留档采纳仓 `product/e2e/reviews/2026-10-17/`。
139
+
140
+ **契约与生成器(D98 `dispatch` 动词)**:
141
+ - `rings/e2e/reference.md` §5.5 新增 `dispatch <tid>`:React 合成事件直发——`dispatchEvent('click')`,**不经 hit-testing / 不做可点性检查**,不追加 force(绕行本义无此概念);适用边界(硬)与 `native` 同家族——被测属性是「可操作性 / 不可交互 / 用户不可达」的用例**不得用**(可点性绕过 ⇒ 无法证伪);**值域封闭**:无第三位,出现任何取值 ⇒ 校验器 FAIL + 生成器报错中止。
142
+ - `scripts/gen_e2e_scripts.py`:`ACTION_VERBS` +1 词条(守恒样例表同步)、发射分支 + 值域报错;`validators/validate_e2e_index.py`:词表同步 + 值域邻位 FAIL(文本期双保险——兼治「旧安装副本对陌生第三词静默吸收」,v3 现场实证)。
143
+ - 变异 **M87**:摘 dispatch 发射分支 ⇒ 发射钉红,还原逐字节一致(sha `348d8316…`);钉子 `test_dispatch_verb_emits_synthetic_event_without_force` + `test_dispatch_verb_value_domain_in_index_gate`;`tests/test_validators.py` **112 条**(gates 行计数同步)。
144
+
145
+ **台账收口**:
146
+ - `DESIGN.md` §13①:协议定形首跑**已完成**(2026-10-17 v3 全片 11/11);确知仍开 = E0 产物残留重生成(绑真跑准备日)。
147
+ - **D99(原 §14 首行)**:`stack-profile.yaml` 字段集**定稿**——冻结现行实现实收面为 v1(读取面 `_support/stack_profile.py` ⇔ 声明面 `shared/stack-profile.md` §3 对账即清单),后续字段变更一律起独立决策行;两栏制不采(第二真相源,D86 同族)。
148
+ - **D100(原 §14 第二行)**:git pre-commit 校验兜底**不启用**——校验靠显式跑(`pytest tests/` 全量 + `scripts/atlas_check.py` 前闸),启用留待 CI 一起落(D72 ④ 前置形态同步收口);pytest 最小钩不采(维护者明示接受「跳过验证直接提交」窗口风险)。
149
+ - `shared/gates.md` 2.2「E2E 真跑」行补**第 2 次咬合 + 定形全片证据**(2026-10-17 v3,逐格纪律:只升本行)。
150
+
151
+ **被否 / 边界**:固定 sleep(「共享靶场必飘 ⇒ 假绿通道」原判据不变);产品侧 settle/防抖(本条不约束 spark-flow,005 的内容缺陷由产品自修走);005 用例降级只证贴纸本体(案 6 备选 B 留档未启用)。
152
+
153
+ **内部运营**:`duty-review/` 三件(README 巡检协议 + QUEUE 决策队列 + bugs.md 按环分类存量 bug 库——未随包分发)与 `scripts/duty_signals.sh`(只读信号收集,随包)上线——每日 20:30 定时巡检,DR1–DR5 拍板判据切断「AI 提题 → AI 评审 → AI 拍板」无人在环闭环;bugs.md 全部「已修·待复验」条目当日真跑复验出库(007/#R2/004/006/005)。
154
+
155
+ ---
156
+
7
157
  ## [v0.1.0-beta.21] - 2026-10-16
8
158
 
9
159
  ### 🪲 E2E 生成链四处缺陷闭环:标量注释污染 · 元素守卫空转 · 守卫门不剥注释 · `force` 与被测属性相抵(Issue #R1–#R4 / D94–D97)
package/DESIGN.md CHANGED
@@ -560,10 +560,22 @@ adapters:
560
560
  | D91 | `atlas_check` 包自检门的**前置降级**:缺 `.atlas/tests` 记 `SKIP`(2026-10-16,`RFC-20261016-02` / issue #17 残余,tech-proposal-reviewer 评审「有阻塞问题」修后采纳) | ① **残余面**:D86 修根因(分发表补 `tests`),但 `check_factory`(`scripts/atlas_check.py`)无前置判据——存量装配(beta.17 及以前发行版的 npm 装配态)与源包开发态(仓根无 `.atlas/`)仍 pytest exit-4 恒红 `FAIL`,且全量恒红 / `--fast` 恒绿两相反结论并存(issue #17 本体:「红灯训练成噪声」)。② **定案**:前置判据「`.atlas/tests` 不是目录(含路径被文件占用的装配损坏形态——评审 M-1,判定一道覆盖不对在场者谎称「未找到」)⇒ `SKIP` + 两层文案(判据面 = 实测路径 + 三形态点名:旧发行版装配态 / 源包开发态 / 装配损坏;出路 = `pytest tests`(源包)+ `install.sh` / `atlas upgrade`(重装走齐)+ pytest 可用性提示——评审 m-3/m-2 的动作词齐组进钉);目录**在场** ⇒ 行为逐字节不变,真红仍 `FAIL`(反面钉 A-2 防「把真失败一并降级成 SKIP」)。③ **与 D86 的收窄关系(评审 B-1,阻塞件)**:见 D86 ② 的 D91 收窄注记——D86 的否决只在「出路不可达」时成立,根因落地后 SKIP = 合法降级(本仓章程);**装配面根因永远不得改靠 SKIP 吸收(F47 三表同步钉维持)**。④ **盲区显式化而不改判据(评审 M-2/m-4)**:pytest 解释器可用性(无 pytest ⇒ `FAIL` 指名,环境缺失可自修)与「目录在场但零用例 ⇒ `no tests ran` 仍 `FAIL`(装配损坏值得查)」两条不进机器判据,落 `gates.md` 2.2 行「不查什么」列。⑤ 被否:缺目录记 `ok`(把「未走到」伪装成「走过且绿」);`WARN`(性质是「没跑」不是「跑过、判据不咬」,混污染真 WARN 语义);挪进 `--fast`(丢真实自检能力且门行同样缺席);装配时打印「包自检需在源包跑」(issue 原后半句——常装配 = 常态噪声,吸收进 SKIP 文案的出路层)。变异 **M76**(前置分支摘除 ⇒ `test_f53_factory_gate_skips_when_tests_dir_absent` 红;还原逐字节一致)。`tests/test_atlas_check.py` 22 → 24 条。 |
561
561
  | D92 | 装配器版本戳挂**干跑闸门**——「干跑不产生装配态变更」(2026-10-16,`RFC-20261016-03` / issue #21,tech-proposal-reviewer 评审「修后采纳」) | ① **病灶**:戳写入是 install.sh 全脚本唯一的裸写(run() 外、无 DRY_RUN 判据),守卫挂 `[ "$DRY_RUN" -eq 0 ]`(预览语义,合理)⇒ `--update --dry-run` 一次(守卫放行、载荷未拷、戳却更新为源包当前摘要)+ 一次被守卫拒住的真跑 = 副本「戳新/内容旧」,被装配版本门(**自写自读闭环**,只比戳与源包摘要)报 ok「一致」——对账门被干跑洗成假绿;干跑**首装**还经 `stamp_file` 的 mkdir 残留 `<target>/.atlas/`(未上报同族面,评审 A-2),卡随后的真首装。② **定案**:戳写入块整体挂 `DRY_RUN` 判据(干跑只出声 `[dry-run] 装配版本戳(未落盘)`);真跑路径逐字节不变(落戳仍最后一步、失败仍 F37 硬错);不采「包进 run()」——run 只对子命令生效,对裸 heredoc 管段属假闸门。③ **不采**(issue 建议③):给装配版本门加副本内容抽检——摘要算法口径为**源包布局**而立,副本清单口径(副本本地改动 / VERSION 自环 / `__pycache__` / `runs/`)未立案就上判据 ⇒ 回灌批在路即 WARN 风暴,留待独立 RFC。变异 **M77**(摘干跑闸 ⇒ `test_f54_dry_run_writes_nothing_and_leaves_no_residue_on_fresh_target` 红;还原逐字节一致)。 |
562
562
  | D93 | 脏树守卫按 porcelain **状态列分两语义**——已跟踪未提交改动 ⇒ 拒,触点整目录未跟踪 ⇒ WARN 放行 + 双摘要可见化(2026-10-16,`RFC-20261016-03` / issue #22,评审「修后采纳」) | ① **病灶**:守卫只读路径列(`cut -c4-` 後判 `case`),`.atlas/` 整目录未跟踪被 porcelain 折叠成 `?? .atlas/`、`.atlas/*` 判据照命中 ⇒ 守卫恒拒 ⇒ `--force` 被逼成常规、主力语义(护已跟踪的未提交改动)被噪声淹没时反照失效。② **定案**:守卫读整行,XY 列分桶——`??` 进 `UNTRACKED_TOUCH`,其余进 `DIRTY_TOUCH`;`DIRTY_TOUCH` 非空 ⇒ F34 行为逐字节不变(`--force` 越过,文案保留);`UNTRACKED_TOUCH` 非空且 `DIRTY_TOUCH` 为空 ⇒ WARN 放行,文案**双摘要可见化**(副本 `.atlas/VERSION` 首行 ⇔ 当前源包 `pkg_digest` 摘要,同录出声)+ 出路先行(ATLAS-UPSTREAM.md / `backflow.py`);两桶皆非空 ⇒ 拒优先,拒通知并录未跟踪项。`--force` 语义未漂移:仍 = 越过拒绝的 exceptional 头寸。③ **评审 B-1(major)取舍**:其首选「戳相等才放行、不等/缺回落拒」不采——npm 升级链路最常见形态恰是「未跟踪且已过期」(副本旧发行版装配、源包已前进)⇒ 每次合法升级被逼 `--force` = **issue #22 病灶复现**;采纳下限 = 双摘要可见化让丢失风险出声,而非把风险藏进恒拒;「未跟踪 ⇒ git 无基线 ⋯ 本就不在守卫主语义的护力范围内」留档。④ **不采**:porcelain v2(触点全 ASCII、rename 前缀保守命中);项目级「装配目录不入库」开关(porcelain 状态列已携带全部所需信息,不造第二份声明);「未跟踪必答确认」(= `--force` 改名常规化)。变异 **M78**(未跟踪桶改指脏桶 ⇒ F55 钉前半红;后半为 F34 回归钉非变异钉;还原逐字节一致)。`tests/test_install_selfcheck.py` 7 → 9 条。 |
563
- | D94 | 行内 `#` 注释剥除**收敛为单源出口**(内核 `strip_inline_comment`),守卫门自带剥注释(2026-10-16,issue #R1 + #R3,I3) | ① **病灶**:同一 `atlas-case` 块内口径分裂 —— 列表项由消费侧 `parse_line` 的 `partition("#")` 剥(「机器只看 `#` 之前」),**标量键不剥**(`_support/case_block.py` 的 `d[k] = v` 分支)⇒ `守卫:` 值把注释整段当值,又作为 JS 单引号字面量发射(`page.goto('/x # 说明')`),且路径门的前缀命中口径把「合法前缀 + 尾随垃圾」判 PASS ⇒ 无门可见;`validate_e2e_index.py` 另有多处手写 `split("#", 1)[0].strip()`,而守卫门**完全不剥**。② **定案**:`case_block.py` 的**标量分支**改经内核 `_support/stack_profile.strip_inline_comment`(经包内 bootstrap 取用;**不**新增第二份实现 ⇒ `test_definitions_are_single_sourced` 与 `is` 同一性钉同时咬合);**列表项逐字节不变**(其 `#` 是给人读的 note,由 `parse_line` 单点消费);校验器多处手写口径收敛为同一函数对象;**守卫门内取值路径**(`:684-687`)自带剥注释(F3,上下游双保险)。③ **能力边界(不 oversell)**:F1/F3 **不**关闭「合法前缀 + **无注释的真垃圾尾**」(`/x/1/FUZZY` 仍 PASS)——那需要「值形态」判据 + 契约增量(`:285` 的前缀口径属**有意设计**,单独收紧会把合法带参路径打红),登记为独立待办;前缀命中口径**不动**;`validators/validate_e2e_index.py` 既有的 `_code()`(按任意 `#` 截断)与其余文本期门仍在本次收敛面之外,登记为残余。④ **不采**:契约层「块内注释只允许出现在列表项」+ 门拒绝(把合法的给人读注释当违规,且要改全部存量分片)。变异 **M83**(标量分支还原 `d[k] = v` ⇒ 内核钉红)/ **M84**(守卫门摘 `strip_inline_comment` ⇒ 门钉红);还原逐字节一致。 |
563
+ | D94 | 行内 `#` 注释剥除**收敛为单源出口**(内核 `strip_inline_comment`),守卫门自带剥注释(2026-10-16,issue #R1 + #R3,I3) | ① **病灶**:同一 `atlas-case` 块内口径分裂 —— 列表项由消费侧 `parse_line` 的 `partition("#")` 剥(「机器只看 `#` 之前」),**标量键不剥**(`_support/case_block.py` 的 `d[k] = v` 分支)⇒ `守卫:` 值把注释整段当值,又作为 JS 单引号字面量发射(`page.goto('/x # 说明')`),且路径门的前缀命中口径把「合法前缀 + 尾随垃圾」判 PASS ⇒ 无门可见;`validate_e2e_index.py` 另有多处手写 `split("#", 1)[0].strip()`,而守卫门**完全不剥**。② **定案**:`case_block.py` 的**标量分支**改经内核 `_support/stack_profile.strip_inline_comment`(经包内 bootstrap 取用;**不**新增第二份实现 ⇒ `test_definitions_are_single_sourced` 与 `is` 同一性钉同时咬合);**列表项逐字节不变**(其 `#` 是给人读的 note,由 `parse_line` 单点消费);校验器多处手写口径收敛为同一函数对象;**守卫门内取值路径**(`:684-687`)自带剥注释(F3,上下游双保险)。③ **能力边界(不 oversell)**:F1/F3 **不**关闭「合法前缀 + **无注释的真垃圾尾**」(`/x/1/FUZZY` 仍 PASS)——那需要「值形态」判据 + 契约增量(`:285` 的前缀口径属**有意设计**,单独收紧会把合法带参路径打红),登记为独立待办;前缀命中口径**不动**;`validators/validate_e2e_index.py` 既有的 `_code()`(按任意 `#` 截断)与其余文本期门仍在本次收敛面之外,登记为残余。④ **不采**:契约层「块内注释只允许出现在列表项」+ 门拒绝(把合法的给人读注释当违规,且要改全部存量分片)。变异 **M83**(标量分支还原 `d[k] = v` ⇒ 内核钉红)/ **M84**(守卫门摘 `strip_inline_comment` ⇒ 门钉红);还原逐字节一致。 | **⑥ ③ 的残余已清偿(2026-10-17,issue #30 收尾批)**:`validate_e2e_index` 的 `_code()`(`line.split("#", 1)[0].strip()` = 第二份口径)与其**9 处**内联 `split("#", 1)[0]`、`gen_e2e_scripts` 的 **3 处**同类、`_parse_mock_line` 的 `partition("#")`、`initial:` 理由检测的 `partition("#")`(撞路径里的 `#` ⇒ 把路径尾巴当理由 = **假「有理由」**)全部改走内核 `strip_inline_comment`;`e2e.reset` 取值清洗的 `.strip("\"'")`(两端同剥 ⇒ 吃掉结尾引号)改为只剥**成对**引号(`_reset_command`)。钉:**结构钉**(行解析器的代码行里不得出现 `split("#"`)+ `initial:` 假理由双侧 + `_code` 保真 + `_reset_command` 行为;变异 **MB8**(`_code` 退回 split)· **MB9**(`_reset_command` 退回两端同剥)两枚咬中、还原逐字节一致。**残留**:无(D94 ③ 字面全真)。 | **⑦ 值形态判据 —— 判接受(非待办,2026-10-17 独立评审)**:前缀命中的「合法前缀 + 垃圾尾」假 PASS 通道判**接受**:① 无现场实例(全仓仅 ③ 自陈的理论例);② 前缀命中是 `rings/e2e/reference.md` §8.2 **契约有意设计**(带参页面友好),单独收紧会误伤合法带参路径;③ 唯一可行的收紧(structure 事实派生值形态)会引入**跨环第二真相源**(撞 `reference.md:470`「页面 URL 唯一来源 = 页面表」)并需重写规范化规则 ⇒ 成本/风险高于收益。**本项登记为「已知边界(明示接受)」,不是未修缺陷 —— 下次巡检不得重报。** 不做案 A(字符白名单)。**明确不成立的兜底理由**:「真跑 404 ⇒ 元素守卫必红」**不成立** —— 壳层 testid 也在页面应然清单(`reference.md` §3.2)⇒ NotFound 在壳内渲染时守卫仍绿;且真跑在地址空 / 靶场不可达时 `SKIP`、`blocked` 不可执行、D103 人工会话不可复现(评审 MUST)。**升判口径(维护者,非自动)**:仅当维护者拍板,按「**页面表路由自派生壳前缀 + structure 事实仅作 `:param` 段数佐证**」立项;**不设**「出现实例即自动升判」。评审记录:`proposals/REVIEW-2026-10-17-route-value-form.{md,json}`、方案 `RFC-20261017-03`。 |
564
564
  | D95 | 元素齐全守卫的 id 面 **回归契约正文**(`reference.md` §7.1「每条用例的 `testid:` 清单守卫」),空表不再静默吸收(2026-10-16,issue #R2/#R2',**行为恢复**) | ① **病灶**:守卫面取自 `case["step"][0]`(commit `b18df53` 把 `case["testid"]` 应然清单**替换**而来)⇒ `goto` / `refresh` 首步无 testid ⇒ **守卫恒空**(`for (const id of [])` = 守卫消失),而解死锁的专门机制契约早已给出(§4 常驻槽位「动作产物元素以 **hidden 占位槽**满足『用例开始时 count=1』」)——实现绕过了既有机制。② **定案**:`ids = json.dumps(case.get("testid") or [])`;**尺子重定向**(不删断言):原把「空表」钉成期望的 `test_guard_only_watches_first_step_target` 改为断「守卫面 = 该用例 `testid:` 应然清单且非空(含动态项)」,另立新钉「首步为 `goto` 的用例 ⇒ 守卫面非空」。③ **已知代价(显式登记,不静默)**:这是**对采用方的行为恢复**,非纯 bugfix——未被契约合规(无占位槽)的动态元素会开始真红,那是正确反馈;`validate_testids.py` 的「应然落点 / 常驻槽位」门是静态侧前置拦截,二者互为双保险。变异 **M79**(守卫面改回 `first_step_tids` ⇒ 新钉红;还原逐字节一致)。 |
565
565
  | D96 | `click` / `hover` 的 **`{ force: true }` 默认写进契约** + `native` 步级开关(2026-10-16,issue #R4,契约 `rings/e2e/reference.md` §5.5) | ① **病灶**:生成器对 `click`/`hover` 无条件追加 `{ force: true }`(跳过 *receives events / stable* 检查),而环契约内 **0 处**记载 ⇒ 对「某控件不可交互 / 置灰点不动」这类**被测属性本身**,该属性无法被证伪;动机只留在 CHANGELOG ⇒ 「产出与依据」脱节。② **定案**:契约 §5.5 的 `click` / `hover` 条目补——**缺省 `force: true`**(动机:遮罩 / 动画 / 父容器拦截造成假红)+ **适用边界(硬)**(被测属性 = 可操作性 / 不可交互 / 用户不可达的用例**不得**依赖该默认)+ **步级开关** `click <tid> native` / `hover <tid> native`(不追加 `force`,保留 `.first()` 的 strict-mode 修复)+ **值域封闭**(第三位仅 `native` 或缺省,其它值 ⇒ 校验器 FAIL + 生成器报错中止)。③ **不采**:撤销默认 `force`(遮罩类假红是更常见的病灶,收窄 = 把常态逼回假红);全局关闭开关(粒度应是步级 —— 同一用例内两种需要可共存)。变异 **M80**(`step_force_arg` 恒返 force ⇒ `native` 钉红)/ **M81**(值域判据摘除 ⇒ 坏值钉红);还原逐字节一致。 |
566
566
  | D97 | `testid:` **项**带行内注释时「纯在场守卫豁免」静默失效(2026-10-16,issue #R1 同族未爆现场 S1,`tests/test_gen_e2e_scripts.py` 钉子) | ① **病灶**:`gen_e2e_scripts.py` 零步基线探针的 `case_testids = set(c.get("testid") or [])` 用**未剥注释**的值,而紧邻的 `step_operated_testids` 走已剥的 `parse_line` ⇒ 集合比对碰带注释项即静默不命中(F24 规则二的「纯在场守卫豁免」判据失效,无任何机器面可见)。② **定案**:`case_testids` 逐项经**同一** `strip_inline_comment`(I3);行为等价性由钉子证明(无注释时集合逐元素不变)。③ **不采**:改 F24 规则二判据口径(报告的是**解析层**缺口,不是规则问题)。变异 **M82**(`case_testids` 改回未剥 ⇒ 钉子红;还原逐字节一致)。 |
567
+ | D98 | E2E 动词 **`dispatch`**:合成事件直发成文(2026-10-17,v3 真跑 004 复跑复现驱动,用户拍板采纳) | ① **病灶(force/native 双灭)**:v3 便利贴表面常态遮罩 + 新增入场动画——`force` 点击在连发窗口被吞(期望 2→1,5s 轮询纹丝不动,二跑复跑复现);`native` 因可点性检查永不过而**整用例挂死**(三跑/四跑实证,点选与删除两个 testid 均遭遇)⇒ 现行点击二模在该表面表达不出「等稳定」(固定 sleep 被契约明禁、`waitFor` 只能等可见而元素恒可见)。② **定案**:`dispatch <tid>` = `dispatchEvent('click')` 直达 React 合成事件 handler(绕 hit-testing / 可点性检查的整面;预置脚本的 `dispatchEvent` 为运行先例;黑盒探针实证**动画窗口内 0 节拍直发两删连中**,同一窗口 force 全灭);不追加 `force`(绕行本义,无此概念);**适用边界(硬)**与 `native` 同家族——被测属性 = 可操作性 / 不可交互 / 用户不可达的用例不得用(可点性绕过 ⇒ 该属性无法被证伪);**值域封闭**:无第三位,出现任何取值 ⇒ 校验器 FAIL + 生成器报错中止——兼职治当日实证的「旧安装副本对陌生第三词静默吸收」(.atlas 副本无 D96 时 `native` 不生效也不报错)。③ **被否**:固定 sleep(「固定等待在共享靶场必飘 ⇒ 假绿通道」原有口径不变);产品侧 settle(去留由维护者对 spark-flow 自行决,本条不约束产品);把连发 add 重排成串行(相邻步仍零间隔,无立足点)。④ **发射/值域钉**:`tests/test_gen_e2e_scripts.py` 动词样例守恒表 +1 词条 + `test_dispatch_verb_emits_synthetic_event_without_force` +1(直发形态 + force 不沾 + 第三位 ValueError);`tests/test_validators.py` `dispatch` 值域门 +1(gates 行计数 **112 条**已同步);变异 **M87**(摘 dispatch 发射分支 ⇒ 发射钉红,还原逐字节一致,sha `348d8316…`)。设计依据 = v3 三跑/四跑 error-context + 探针(/tmp,不合入)。 |
568
+ | D99 | `stack-profile.yaml` 字段集**定稿**(2026-10-17,巡检决策队列**案 2** 维护者拍 A——「等 v3 切片全绿日一次定稿」) | ① **冻结口径**:以**现行实现实收面**为 v1 基线——读取面 = `_support/stack_profile.py`,声明面 = `shared/stack-profile.md` §3(两侧正本对账即为清单);**此后新增 / 删除字段一律起独立决策行**(D80/D89/D90 先例路径),「草稿」语境退役。② 文档 / 模板存量「以草稿为准」措辞**随触碰顺手清**,不专项发扫(最小动作)。③ §14 首行由本行承接退役。④ 被否:两栏制(稳定字段 / 活字段分列)——冻结后再开「活」栏 = 给字段集开第二真相源(D86 同族教训);现在冻结字段本身(切片全绿前字段还在被真实场景加压,冻结会立刻吃变更流程)。 |
569
+ | D100 | git **pre-commit 校验兜底不启用**(2026-10-17,巡检决策队列**案 4** 维护者拍 A) | ① **定案**:校验兜底维持显式跑——`pytest tests/` 全量 + `scripts/atlas_check.py` 前闸;pre-commit 不上;启用留待 CI 一起落(本行同步收口 `DESIGN.md` §14 悬案原第二行 + 属 D72 ④「CI 分级与回滚延后(依赖 B4)」的前置形态)。② 被否:**pytest 最小钩**——多会话并行开发下「跳过验证直接提交」的窗口真实存在,维护者明示接受该风险:批次闭环的全量测试纪律(CHANGELOG 各批带测试计数)优先于提交时延。③ 载体口径(factcheck v2「硬条款必须指名载体」):现行校验的载体 = **显式跑纪律**,pre-commit / CI 是未来形态——不假装已是门。 |
570
+ | D101 | node 轨运行器判据**换本体**:从「可执行文件存在」改为「`@playwright/test` 可解析」,全局兜底改为**可生效的修复**(2026-10-17,issue #24;现场 = 百炼/v2前端真跑准备日) | ① **病灶**:判据写在两处(`scripts/atlas_check.py` + `scripts/e2e_console.py`,连 `_is_node_playwright` 都是两份拷贝),且两处都只问「binary 在不在」;而 `shared/gates.md` 2.2「E2E 真跑」行**早已写明**「解释器按**依赖可导入性**选(不按路径存在)」——是实现漂离了已落口径,**不是设计选择**。ESM 项目(`package.json` `"type": "module"`)上暴露为**半生效**:全局兜底(F19)找得到 binary,但 Node 的 ESM 解析器**不读 `NODE_PATH`**(该通道属 CJS)⇒ `@playwright/test` 解析不到,错误落到运行期(`Cannot find package '@playwright/test'`),且被门计成「跑失败 / 无结果」——**真原因不可见**。② **定案**:判据本体 = 「**从 `e2e.scripts_dir` 出发 `@playwright/test` 可解析**」(探针 = `node --input-type=module -e "import('@playwright/test')"`,解析基准 = cwd,本机实测),解析 / 探针 / 修复**全部收敛到 `_support/pw_runner.py`**(`_support/` 单源惯例,消除两份拷贝);修复手段 = **幂等软链**(本机实测能让 ESM 解析生效、且不把全局版本混进项目依赖的唯一通道),且只在 `source == "global"` 时发生——项目自装目录里缺模块是**项目自身缺口**,给软链会把全局版本混进项目依赖(版本漂移源)⇒ 不修只报;软链落点 = **Node 从 scripts 起向上搜索的首个 `node_modules`**(声明的 `e2e.node_modules` 是二进制所在目录,**未必在搜索路径上**)。③ **档位(同一判据、两种反应——判据本体单源,反应面正当分叉)**:门 = 不成立 ⇒ `FAIL` 点名真原因;全局兜底修复成功 ⇒ **降 `WARN`**(跑得起来、但结论依赖**机器全局状态**、非项目自足——不假装 `ok`,也不把有确定性补救的事判红成噪声,§§「红灯训练成噪声」旧教训);执行器 = 修复 + 复检 + **输出「用了全局什么」**,修不好 ⇒ 起跑前诚实拒绝(不起注定失败的子进程)。④ **被否**:`NODE_PATH` 继续当判据(ESM 下无效);链接位被占时覆盖(= 替人删文件);把「用了全局」做成**一次性事件**(它是**跑的前提事实**,每次结论都该降档 / 输出 ⇒ `global_note` 与「本次是否动手」解耦);项目自装缺模块时也给软链(版本漂移源)。⑤ **发射/值域钉**:`tests/test_atlas_check.py` **24 → 25 条**(+`test_check_e2e_module_unresolvable_fails_loudly`:坏输入 ⇒ `FAIL` 点名「依赖可导入性」;`test_check_e2e_global_playwright_detected` 由假 `ok` 改钉 **`WARN` + 行内写明全局与软链 + 幂等再跑**),`tests/test_e2e_console.py` **+3**(修复合规路径 / 无全局如实拒绝 / 链接位被占拒绝覆盖);同批顺手补 `install.sh` 对 `_support/case_id.py` 的装配自检(存量漏登记:包内件一直在装配面上、却不在自检清单 ⇒ 自检 44 → **46 条**);变异 **M88**(判据退回「binary 存在即 ok」⇒ 门侧坏输入钉红)/ **M89**(摘软链步 ⇒ 执行器侧修复钉红)/ **M90**(摘 `global_note` ⇒ 降档与输出齐红),三枚咬合、还原逐字节一致。设计依据 = 百炼/v2前端现场(`product/e2e/scripts` 下 `playwright test --list` 报 `Cannot find package '@playwright/test'`;加 `NODE_PATH` 仍红;补软链后 10 条用例可收集)。 |
571
+ | D102 | 复发**自动可见**(DR4 口径 B:段级族规模 × 活跃窗)+ 「**可执行性判据本体**」写成流程门(2026-10-17,维护者拍板选项 **B**;承接 issue #24 / D101 的复发信号) | ① **病灶**:D101 那条属「**判据可被形态满足**」族(`#1/#4/#6/#7` 首批,beta.17 修过一轮;`#20`、`#24` 两轮复见),而 DR4「同类 ≥3 次 ⇒ 需立机器门**或契约条款**」的判定**此前全凭人工裁量**——族规模没人算、复发靠撞见。同日已落**口径 A**(fix 类提交触达路径 30 天 ≥3)判**复发速率**;**口径 B 补族规模**:聚类单元 = `duty-review/bugs.md` 的**段 + 段首「同根归纳」行**,计数 = 段内 `#NN` 去重(**上界近似**,含交叉引用),**升室判据 = 同根归纳在案 ∧ 案数 ≥3 ∧ 段内最近日期落在近 30 天**(三项齐 = 这族**近来还在长**)⇒ 巡检输出一行「升室候选」;只满足前两项 ⇒ 记「族已静默」**不升室**(同一族清算完还天天喊 = 把红灯训练成噪声,本仓旧教训故设活跃窗)。② **契约条款(语义项,不假装是门)**:`shared/gates.md` 2.5 新增「**可执行性判据本体**」流程门——判据必须问「**能不能真的用**」,不得以「东西在不在 / 文字写没写」代替;这类前置判据(运行器 / 解释器 / 依赖模块 / 外部工具)须具备 ① 一次性功能性探针(真跑一次:import / `--list` / dry-run)② **反面包**(东西在、但用不了 ⇒ 必须红)③ 失败原因**不得被吞**;**明示不设机器判据**(`3509 §96` 口径),给写门者与独立审查一把尺子,附人工追问清单三条。③ **为什么不做通解**:判「判据 ≠ 它要保护的性质」要读**意图** ⇒ 语义,机器判不了;硬立通用门 = **用可被形态满足的门去治可被形态满足的病**(同族递归)。故本决策刻意只做「让复发被看见 + 给写门一把尺子」,并如实记录:该族复发**都发生在老门老代码上**(纪律只覆盖「新增/修改门」那一刻)——机器门治不了「没人回头看」。④ **被否**:立通用机器门(同上);只按「有没有探针调用」这个**形态**判(写个假探针即过);口径 B 不带活跃窗(落刀后实测:4 段全被点名,其中 structure 段是**无活跃案的历史族** ⇒ 噪声,故加窗)。⑤ **落地与实跑**:口径 B 的实现抽为**单源模块** `duty-review/families.py`(原稿是 shell 内联 python —— 本仓大忌:无钉、形状一漂即**静默归零**,而静默归零恰好让本机制要治的「复发靠撞见」原样回来);`scripts/duty_signals.sh` 改为调它(只读、不落盘;实跑 = **3 族升室候选**(E2E 14 案 / 安装与装配 3 案 / 自感知 8 案,均有 2026-10-17 新案)+ **1 族自动静默**(structure,无日期取证));钉 = 新增 `tests/test_duty_bug_families.py`(4 条:解析面 = 文件面 / 真实库数得出族规模 / 判定真值表 + 活跃窗边界(日期**正好** = cutoff ⇒ 仍活跃)/ 空输入不炸),变异 **M91**(计数正则从段内全文退回行首 ⇒ 静默归零 ⇒ 钉红,还原逐字节一致);`duty-review/README.md` §2 DR4 补口径 B + §4 信号面**指名单源实现** + 口径 A 显式标注;
572
+ | D103 | `e2e.app_login` **不开第四形态**:不可自动预置的靶场走「**人工会话取出**」**描述路径**(2026-10-17,维护者拍板;承接 issue #26 / QUEUE 案 7) | ① **现场**:靶场**有访问门**但三态**都装不下**——图形验证码(`login_captcha`)+ 服务端 **cookie 会话**(`COOKIE_JSESSIONID`)+ 登录**响应体无令牌**(`data` 是 URL 串)⇒ **无形态可声明**,而 `validate_stack_profile` 的 WARN 只说「请声明」= **死胡同**(编号 #26 已出库;取证见本行 ① 的现场描述)。② **定案(维护者原话:「我想让 ai 主动给人生成这种类似的命令」+「不用非要机器门吧,描述不是也可以」)**:**不加字段、不加门**,落**描述 + 出路文案**——`rings/e2e/reference.md` §7.1 增「人工会话取出(描述路径,非机器门)」节:AI 主动给人 `playwright codegen --save-storage=<e2e.scripts_dir>/.atlas-auth/state.json "<e2e.app_base_url>"`,人登录一次;**四条纪律**(关窗才落盘 / 会话短命令要立即跑 / `<scripts_dir>/.gitignore` 已含 `.atlas-auth/` 且生成器 setup 有**幂等守卫**(无凭据 + 已有合法 state ⇒ 保留,不覆盖人的会话)/ 它是**运行期凭据不是应然资产**);`shared/stack-profile.md` §2 `app_login` 行注明「**三态之外不设第四态**」;`validate_stack_profile` 的 WARN 由「请声明」改为**给出路**(含命令原型,钉 `test_stack_profile_login_unstated_warn_gives_a_way_out` + 变异 **M93**)。③ **对 D80 ② 的注记(显式登记,不静默)**:D80 的「段缺失不再是合法降级 ⇒ 升档 = `not_ready: app_login`」与本行**相抵于一类项目**(有门但无形态可声明)——该升档落地时**须先把这一类排除**,否则会把「如实未声明」罚成 not_ready(`gates.md` 2.2 行已登记该相抵点)。④ **被否**:补第四形态 `manual` / `session_import`(= 契约增量四处同改 + 把**不可复现**固化成标准姿势;当前只有一个项目撞到);把这类靶场塞进 `real`(一个形态说两件事);用 `mode: none` 充数(与事实相反)。⑤ **能力边界(不得 oversell)**:该路径**不可复现**(每轮要人登录 ⇒ CI 上不可用)、会话短命 ⇒ 是**兜底**而非 `app_login` 的替代形态;**无机器判据**,如实登记为纪律与盲区面。设计依据 = 百炼/v2前端真跑准备日现场(登录表单三字段 + cookie 会话取证;真跑门已能跑到用例,三条红全在未登录态)。 |
573
+ | D104 | **宿主注入型 base query 由生成物转发**(`e2e.app_base_url` 的 query 带到每一跳);路由列保持**路径形态**(可含 `#`)(2026-10-17,维护者拍板「先 2 后 1」+ 两轮独立评审通过后落刀;issue #27) | ① **病灶**:某类应用把宿主注入的前提(tenant)**只从 `window.location.search` 读**(无 sessionStorage 回落)⇒ `会话:` 通道(`D89`,sessionStorage)**送不到**;`goto` 又不得带 `?`(§5.7);而 base 一旦带 query,**绝对路径会丢 query**(`new URL('/console/#/x', '…?switchAgent=7202')` ⇒ query 消失)。三条各自成立 ⇒ **无合法写法**。② **被否的第一案(A)**:让页面表 `路由` 允许 hash 相对形态 `#/…`(`new URL('#/x', base)` 能保住 base 的 path+query)。**落刀时被实测推翻**:`.atlas-case` 里 `goto #/…` 的 `#` 前有空格 ⇒ 按 `D94` 的行内注释语义被截断成 `'goto'`(`守卫:` 标量同样)⇒ **该值形态物理不可写**;救它要改 D94 刚收敛的共享注释语义,回归面过大(`proposals/RFC-20261017-01` §9.1)。③ **定案(B′,评审建议的「生成期纯函数变换」而非运行期助手)**:路由列保持**路径形态**(`/console/#/app/agent/detail`;`#` 紧邻 `/` ⇒ 不被注释语义吞)⇒ 上一轮评审的四处消费者(validator `page_rows`/`_route_hit`、生成器 `page_urls`、控制台 `page_routes`)**无需改动**(形态换成 path-form 后全命中);改由**生成期**把 base 的 query 并进**三处**导航发射面(`step` 的 `goto` / 元素齐全守卫 / 零步基线 `data.url`;单源 `_support/url_merge.py`,与评审 MUST 的「三处不是一处」对齐);控制台手工「打开」的地址构造同步修正(**第五消费者**,旧 `base + path` 在 base 带 query 时产畸形 URL)。④ **口径(三条,写进 `rings/e2e/reference.md` §5.5)**:base 无 query ⇒ 生成物**逐字节零变化**;`ATLAS_BASE_URL` **只覆盖 host、不覆盖 base 的 query**;**边界明示**——base query 成为前提后,「它缺席时」的行为**无法用同一份用例表达**(`会话:`/`auth: none` 不在同一消费面、不构成逃逸口),该场景超出契约范围,要 opt-out 须另起决策行(触 `D99` 字段集冻结)。⑤ **被否**:`会话:` 扩「URL 参数档」(触 D99 + reload 打断 SPA);运行期 TS 助手(要反取 `test.info().project.use.baseURL`,隐式脆);把「同名键以目标为准」写成生效口径(§5.7 下目标永无 query ⇒ 不可达死条款,只保留为扩展位)。⑥ **钉与变异**:值保真(`/console/#/…` 在 case 块可写 + 两道门 PASS)/ 三处齐并 / 无 query 零变化 / 控制台绝对地址,变异 **MB1**(注释语义改成任何 `#` 都截断)· **MB2**(合并函数退回 identity)· **MB3**(去掉无 query 短路),三枚均咬、还原逐字节一致;评审记录 = `proposals/REVIEW-2026-10-17-source-batch.md`(A 轮,2 MUST)+ `proposals/REVIEW-2026-10-17-Bprime.md`(B′ 轮,10 MUST)。 |`duty-review/bugs.md` 自感知段补「同根归纳」**锚行**(此前该族无锚 ⇒ 只能靠 E2E 段的虚高计数间接看见)。全量 **480 passed / 4 skipped**、测试文件 32 → **33**(`shared/gates.md` 计数行同步)。**明示盲区**:跨段同族机器串不起来(靠条目里手写的「同族复发」交叉引用,且**被指段落也要留一行**);段是人工按环分的(分段漂 ⇒ 计数漂);`bugs.md` 无日期字段,时间窗只认段内文本日期(无 ⇒ 视为静默)。 | ⑦ **列表项剥注释口径统一(D94 ③ 残余清偿,改判 D94 ② 对列表项的例外)**:落刀现场发现 **step 的 `goto` 丢了 hash 段**(守卫/基线正常、只有列表项受害)——`parse_line` 用的是 `line.partition("#")`(**任何** `#` 都截断,D94 ② 的旧例外),而 `/console/#/app/agent/detail/1` 这类**路径形态含 hash** 的值必然被砍成 `/console/`。**旧行号** = `DESIGN.md` D94 ②「列表项逐字节不变(其 `#` 是给人读的 note,由 `parse_line` 单点消费)」;**新证据** = 本批 v2 重生成产物的 `page.goto('/console/?switchAgent=7202')`(hash 段消失)。改判:`parse_line` 改走内核 `strip_inline_comment`(`#` 前有空白/行首才算注释、引号内不算)⇒ 路径里的 `#` 保留、尾随说明照剥;钉(`parse_line` 值保真)+ 变异 **MB4**(退回 `partition` ⇒ 红)。其余两处 `partition("#")`(`_parse_mock_line` / `initial:` 理由检测)**无现场证据、未动**,仍在 D94 ③ 的残余登记内。 |
574
+ | D105 | 生成的运行器配置**必须声明 `expect.timeout` 下限**(2026-10-17,issue #28;真跑现场驱动) | ① **病灶**:#27 落地后真跑仍 `3 failed 1 passed`,而**元素真在**(探针实测 `disabled=true` + 合规 `aria-label`)、三处导航 URL 全对 ⇒ 红因是**断言超时**:生成的 `playwright.config.ts` **没有 `expect` 段** ⇒ 继承 Playwright **默认 5s**,而该应用首屏实测 **14,184ms**(dev bundle 13.8MB)⇒「元素还没渲染」被误判成「元素不存在」= **假红**。② **定案**:生成器在两处 config 分支都发射 `expect: { timeout: Number(process.env.ATLAS_EXPECT_TIMEOUT ?? 20_000) }`(缺省 20s,仍远小于 test 超时;`ATLAS_EXPECT_TIMEOUT` 为运维覆盖口,与 `ATLAS_SLOWMO` 同族)。**它不是「固定等待」**:`expect` 是轮询式 auto-wait,本改只抬高**上限**,不引入 sleep,不破 §5.5「不得固定等待」。③ **被否**:只给守卫 `toHaveCount` 加 timeout(其余断言仍 5s,慢一点又撞);调 test 超时(那只是兜底,不解决断言级假红);固定 sleep(契约明禁,且共享靶场必飘)。④ **代价(如实)**:真失败从 5s 变 20s 才红;可在慢环境调低。⑤ **钉与变异**:`test_runner_config_declares_expect_timeout_floor`(配置含 `expect: {` + `20_000` + 覆盖口),变异 **MB5**(摘掉发射 ⇒ 红,还原逐字节一致)。 |
575
+ | D106 | **真跑门的执行 cwd = 生成物目录**(2026-10-17,issue #29;真跑现场驱动) | ① **病灶**:#27/#28 落地后真跑仍 `3 failed 1 passed`,而同一份产物 **`cd scripts` 直跑 8.2s 全绿** (`--grep E2E-APP-AGENT-DETAIL-001`)——差异 = **cwd**:`atlas_check` 用 `cwd=项目根` + `-c <相对路径>` 跑,而生成物里的相对路径全是 **cwd 相对**的(`storageState: ".atlas-auth/state.json"`、`outputDir: ".atlas-run"`、mock fixture)⇒ Playwright 把 storageState 解析到 `<root>/.atlas-auth/`(**并在那儿写了一个空 state**)⇒ **读不到人的会话** ⇒ 未登录态跑用例 ⇒ 断言 20s 超时**假红**。控制台(`e2e_console`)本来就是 `cd scripts` 再跑。② **定案**:本门改为 **`cwd=<scripts_dir>` + `-c playwright.config.ts`**(与执行器同一口径,两个入口不再各说各话)。③ **被否**:把生成物里的相对路径全改成绝对路径(派生件带机器绝对路径 = 不可移植、且破「生成物 = 确定性派生物」);只修 setup 的 state 路径(其余相对路径仍会漂)。④ **教训**:**「同一份产物在两个入口下跑出不同结论」必须先查 cwd / 环境**,否则会把执行器差异误判成用例或实现缺陷(本轮依次误判为「超时不足」→ 已由 D105 兜住超时面,再查出真因 = cwd)。⑤ **钉与变异**:`test_check_e2e_runs_with_scripts_dir_as_cwd`(截 `run()` 的 (cmd, cwd),断言 cwd = scripts 且 cmd 里无相对配置路径),变异 **MB6**(退回 root + 相对 `-c` ⇒ 红,还原逐字节一致)。 |
576
+ | D107 | 零步基线**每个探针都必须「缺席即判不成立」,不得等挂载**(2026-10-17,issue #30;真跑现场驱动) | ① **病灶**:`atlas_check --page app-agent-detail` ⇒ 两条真用例全绿,唯零步基线 `Test timeout of 30000ms exceeded`。带参页面的页级路由本就不渲染详情(`D104` 允许的形态),而 `attr` 探针发射的是 `getByTestId(t).first().getAttribute(n)` —— **无 `count()` 守卫**,Playwright 对**未挂载**元素的 `getAttribute` 会等到默认超时 ⇒ 4 探针 × 2 趟撑爆 test 预算。契约 §5.5 早已明写「基线探不到元素按 `null = 不成立` 处理」⇒ **等挂载 = 违约**(A1 判错,非项目缺陷)。② **定案**:`attr` 探针加 `count() > 0 &&` 前置(缺席 ⇒ `false` = 不成立);并把该性质上升为**生成器的通用不变式**:**任何探针表达式都必须带缺席守卫**(`count()) > 0` 或 `tsVis(` / `tsVisible(` / `atlasValueOf(`)——由通用钉守着,防未来新增动词再犯。③ **被否**:给基线设更大的 test 超时(把「等挂载」合法化,掩盖违约);带参页面跳过基线(契约 §5.5 已选择「回 null」而非「跳过」);只在 v2 侧声明 `守卫:`(那是**资产选择题**,不修 atlas 违约面)。④ **钉与变异**:`test_zero_step_probes_never_wait_for_attachment`(对**全部**探针做守卫检查)+ 变异 **MB7**(摘 `attr` 的守卫 ⇒ 红,还原逐字节一致)。⑤ **出库判定**:探针行为属**运行面** ⇒ 待下一轮真跑复验后出库。 |
577
+
578
+
567
579
 
568
580
  ---
569
581
 
@@ -573,14 +585,14 @@ adapters:
573
585
  > **进度与下一步**随每轮实际产出就近登记;门的取证等级 = `shared/gates.md`。
574
586
  > 本节只留**确知仍开、且不属批 6** 的项(机器判据:本节不得出现落地状态词,见 `tests/test_canonical_consistency.py` 判据 ④)。
575
587
 
576
- 1. **真栈收口**(E1 单段):E2E 真跑门**已于 2026-10-04 逐页真跑并咬出真红**(`shared/gates.md` 2.2「E2E 真跑」行,`3508 §166`/`§187`/`§190`——2026-09-28「暂不真跑」裁定(`3508 §111`)已被执行反推);确知仍开 = ① **协议定形首跑**(1 app × 1 页最小切片,升档规则见 gates 上限声明——逐格 + 咬合记录,首次真实**域收口**与 `E2E 真跑` 行的两个独立通道各自升档);② **E0 产物残留重生成**(绑真跑准备日一次清,`shared/single-source.md` §3)。
588
+ 1. **真栈收口**(E1 单段):E2E 真跑门**已于 2026-10-04 逐页真跑并咬出真红**(`shared/gates.md` 2.2「E2E 真跑」行,`3508 §166`/`§187`/`§190`——2026-09-28「暂不真跑」裁定(`3508 §111`)已被执行反推);**协议定形首跑已完成(2026-10-17,百炼/v3 platform-frontend:1 app × 1 页最小切片全绿 11/11,留档采纳仓 `product/e2e/reviews/2026-10-17/`,咬合证据登记 gates 2.2 行)**;确知仍开 = ① **E0 产物残留重生成**(绑真跑准备日一次清,`shared/single-source.md` §3)。
577
589
 
578
590
  ---
579
591
 
580
592
  ## 14. 未决项(实现前定)
581
593
 
582
- - `stack-profile.yaml` 的最终字段集(当前为草稿)。
583
- - 是否启用 git pre-commit 作为校验兜底。
594
+ - ~~`stack-profile.yaml` 的最终字段集(当前为草稿)~~【已定稿 **D99**(2026-10-17):冻结现行实现实收面为 v1,后续字段变更一律起独立决策行】。
595
+ - ~~是否启用 git pre-commit 作为校验兜底~~【已结 **D100**(2026-10-17):不启用,留待 CI 一起落】。
584
596
  - 地址唯一来源 = `e2e.app_base_url`(E1 单段);`prototype.base_url` / `prototype.serve_command` 随原型环退役。
585
597
  - "本次需求影响哪些页面"的识别:由 `apply` 按 **D61** 从需求文本 + 结构基线路由表 + 索引页面表**语义推导**,产出结构化影响面清单;低置信 / 歧义 / 映射不到时**一次性 ≤3 问**向用户确认(不猜、不用 regex 判语义)。
586
598
  - 多 app 仓库(monorepo)下 `product/` 是单份(产品维度)还是按 app 分片,待第一个 monorepo 场景出现时定。
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env python3
2
+ """atlas 共享内核 —— node 运行器:二进制解析 / **可解析性判据** / 全局兜底修复(单源)。
3
+
4
+ **为什么存在**(issue #24,2026-10-17 现场 = 百炼/v2前端;决策行见 `DESIGN.md` D101):
5
+
6
+ 「找得到 playwright」此前被实现为**判据 = 可执行文件存在**(`<e2e.node_modules>/.bin/playwright`
7
+ 或系统全局命令),而真跑需要的性质是「**从 `product/e2e/scripts/` 出发 `@playwright/test`
8
+ 可解析**」。`shared/gates.md` 2.2「E2E 真跑」行早已写明「解释器按**依赖可导入性**选
9
+ (不按路径存在)」——node 轨的实现漂离了该口径(**是实现漂了,不是设计选择**)。
10
+
11
+ 漂离的代价在 ESM 项目上暴露为**半生效**:项目 `package.json` 声明 `"type": "module"`
12
+ ⇒ 生成物(`playwright.config.ts` / `*.spec.ts`)走 ESM 解析 ⇒ Node 的 ESM 解析器
13
+ **不读 `NODE_PATH`**(该通道属 CJS)⇒ 全局兜底(F19)找得到 binary、却解析不到模块:
14
+ 错误落到运行期(`Cannot find package '@playwright/test'`),且被上层计成「跑失败 / 无结果」。
15
+ `NODE_PATH` 保留(对 CJS 项目仍是真通道、对 ESM 项目无害),但它**不再冒充判据**。
16
+
17
+ 本模块的两个语义**分开**,不得混用:
18
+
19
+ - **`resolve()`** —— binary 在哪(项目声明优先,全局兜底)。`source` 出口告知来自哪一侧。
20
+ - **`probe()`** —— **判据本体**:可解析性。两道消费者共用同一实现:
21
+ `atlas_check` 真跑门用它判红绿,`e2e_console` 用它复检修复结果(不造第二份)。
22
+ - **`ensure_resolvable()`** —— 判据不成立时**怎么办**由调用方定:执行器要「让这轮能跑」⇒ 修复;
23
+ 门要「这个结论可信吗」⇒ 修复后照样在行里写明(依赖机器全局状态 = 不是项目自足)。
24
+
25
+ 修复 = **幂等软链**(本机实测能让 ESM 解析生效、且不把全局版本混进项目依赖的唯一通道);
26
+ 只在 `source == "global"` 时发生——项目自装目录里缺模块是**项目自身缺口**,给软链会把全局版本
27
+ 混进项目依赖(版本漂移源),故该情形一律**不修、只报**。
28
+
29
+ - **随包分发**:本目录随 `install.sh` 整包装配;**纯标准库**(契约 `rings/e2e/reference.md` §8.3)。
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ import os
35
+ import shutil
36
+ import subprocess
37
+ import sys
38
+ from pathlib import Path
39
+
40
+ #: 生成物 import 的模块名(判据对象;`_support/` 侧一切判定以它为准)
41
+ MODULE_SPEC = "@playwright/test"
42
+
43
+ #: 可解析性探针:`node --input-type=module -e` 的 **解析基准 = cwd**(本机实测,2026-10-17:
44
+ #: cwd 内可解析 ⇒ exit 0;换 cwd ⇒ exit 1 + `ERR_MODULE_NOT_FOUND`)。故调用方必须以
45
+ #: `scripts_dir` 为 cwd 执行——那正是生成物所在目录,等价于它们自己 import 时的解析起点。
46
+ PROBE_SNIPPET = (
47
+ "import('" + MODULE_SPEC + "').then(()=>process.exit(0),"
48
+ "e=>{console.error('ERR '+((e&&e.code)||e));process.exit(1)})"
49
+ )
50
+
51
+ PW_BIN_NAME = "playwright.cmd" if sys.platform == "win32" else "playwright"
52
+
53
+
54
+ # ---------------------------------------------------------------- 全局侧探测
55
+
56
+ def is_node_playwright(p: Path | str) -> bool:
57
+ """排除 Python 版 CLI(同名的 `playwright` 命令可能是 `pip install playwright` 的)。"""
58
+ try:
59
+ path = Path(p).resolve()
60
+ if not path.is_file():
61
+ return False
62
+ if "node_modules" in str(path) or path.suffix in (".js", ".mjs", ".cjs"):
63
+ return True
64
+ head = path.read_text(encoding="utf-8", errors="ignore")[:300]
65
+ if "python" in head.lower() or "playwright.__main__" in head:
66
+ return False
67
+ return True
68
+ except Exception:
69
+ return True
70
+
71
+
72
+ def global_nm_root() -> str | None:
73
+ """npm 全局 node_modules 根:先环境变量 `NODE_PATH`,再 `npm root -g`。"""
74
+ g = os.environ.get("NODE_PATH")
75
+ if g:
76
+ return g
77
+ try:
78
+ res = subprocess.run(["npm", "root", "-g"], capture_output=True, text=True, timeout=5)
79
+ if res.returncode == 0 and res.stdout.strip():
80
+ return res.stdout.strip()
81
+ except Exception:
82
+ pass
83
+ return None
84
+
85
+
86
+ def has_module(nm_root: str | None) -> bool:
87
+ """全局根下是否存在 node 轨 playwright 模块(`@playwright/test` 或 `playwright`)。"""
88
+ if not nm_root:
89
+ return False
90
+ for p in nm_root.split(os.pathsep):
91
+ gnp = Path(p).resolve()
92
+ if (gnp / "@playwright" / "test").exists() or (gnp / "playwright").exists():
93
+ return True
94
+ return False
95
+
96
+
97
+ def global_playwright(nm_root: str | None) -> Path | None:
98
+ """全局 playwright 可执行文件(PATH 命中优先,其次按 npm prefix / 全局根推候选)。"""
99
+ if not has_module(nm_root):
100
+ return None
101
+ cand = shutil.which("playwright")
102
+ if cand and is_node_playwright(cand):
103
+ return Path(cand)
104
+ candidates: list[Path] = []
105
+ if nm_root:
106
+ for p in nm_root.split(os.pathsep):
107
+ gnp = Path(p).resolve()
108
+ candidates.append(gnp.parent / "bin" / PW_BIN_NAME)
109
+ candidates.append(gnp.parent.parent / "bin" / PW_BIN_NAME)
110
+ candidates.append(gnp / ".bin" / PW_BIN_NAME)
111
+ try:
112
+ res_bin = subprocess.run(["npm", "prefix", "-g"], capture_output=True, text=True, timeout=5)
113
+ if res_bin.returncode == 0 and res_bin.stdout.strip():
114
+ candidates.append(Path(res_bin.stdout.strip()) / "bin" / PW_BIN_NAME)
115
+ except Exception:
116
+ pass
117
+ for c in candidates:
118
+ if c.is_file() and is_node_playwright(c):
119
+ return c
120
+ return None
121
+
122
+
123
+ # ---------------------------------------------------------------- 二进制解析
124
+
125
+ def resolve(root: Path, nm_rel: str | None) -> tuple[Path | None, Path | None, str, str]:
126
+ """运行器二进制解析(项目声明优先,全局兜底)。
127
+
128
+ → `(pw_bin, nm_dir, source, why)`;`source ∈ {"local", "global", "none"}`。
129
+ `why` 非空 ⇔ `pw_bin is None`。**本函数不判可解析性**(那是 `probe` 的事)。
130
+ """
131
+ nm_dir = (root / nm_rel).resolve() if nm_rel else None
132
+ pw_bin = (nm_dir / ".bin" / PW_BIN_NAME) if nm_dir else None
133
+ if pw_bin and pw_bin.is_file():
134
+ return pw_bin, nm_dir, "local", ""
135
+
136
+ g_root = global_nm_root()
137
+ g_pw = global_playwright(g_root)
138
+ if g_pw is not None and g_root:
139
+ return g_pw, Path(g_root), "global", ""
140
+
141
+ if not nm_rel:
142
+ return None, None, "none", "stack-profile 未声明 e2e.node_modules(node-playwright 依赖目录,契约 §7)"
143
+ return None, None, "none", f"node-playwright 可执行文件不存在:{pw_bin}(e2e.node_modules)"
144
+
145
+
146
+ # ---------------------------------------------------------------- 判据本体
147
+
148
+ def probe(scripts_dir: Path, node: str | None = None) -> tuple[bool, str]:
149
+ """**判据本体**:从 `scripts_dir` 出发 `MODULE_SPEC` 可解析?
150
+
151
+ → `(ok, detail)`;`detail` 非空 ⇔ 不成立(末行错误,便于行内点名)。
152
+ """
153
+ if not scripts_dir.is_dir():
154
+ return False, f"脚本目录不存在:{scripts_dir}"
155
+ exe = node or shutil.which("node")
156
+ if not exe:
157
+ return False, "未找到 node 可执行文件(可解析性探针无法运行;playwright CLI 亦依赖它)"
158
+ try:
159
+ p = subprocess.run([exe, "--input-type=module", "-e", PROBE_SNIPPET],
160
+ cwd=str(scripts_dir), capture_output=True, text=True, timeout=120)
161
+ except Exception as e: # 超时 / OSError:不可解析的判定同样成立,但如实给原因
162
+ return False, f"探针无法运行:{e}"
163
+ if p.returncode == 0:
164
+ return True, ""
165
+ # `getattr`:工程里的测试会把 `subprocess.run` 换成轻量假件(无 stderr 字段)——
166
+ # 探针不得因此抛错(抛错 = 把「判据不成立」变成「工具坏了」)。
167
+ tail = [ln for ln in str(getattr(p, "stderr", "") or "").strip().splitlines() if ln.strip()]
168
+ return False, (tail[-1] if tail else f"exit {p.returncode}")
169
+
170
+
171
+ def resolution_root(scripts_dir: Path, root: Path) -> Path:
172
+ """Node 从 `scripts_dir` 逐级向上搜索 `node_modules` 时**最先命中**的那个(都不存在 ⇒ `<root>/node_modules`)。
173
+
174
+ 软链必须落在这里才可能被解析到:声明的 `e2e.node_modules` 是**二进制所在目录**,
175
+ 未必在 Node 的搜索路径上(两者在常规布局下重合,但不重合时判据才算得准)。
176
+ """
177
+ try:
178
+ cur = scripts_dir.resolve()
179
+ root_r = root.resolve()
180
+ except OSError:
181
+ return root / "node_modules"
182
+ chain: list[Path] = []
183
+ while True:
184
+ chain.append(cur)
185
+ if cur == root_r or cur.parent == cur:
186
+ break
187
+ cur = cur.parent
188
+ for d in chain:
189
+ nm = d / "node_modules"
190
+ if nm.is_dir():
191
+ return nm
192
+ return root / "node_modules"
193
+
194
+
195
+ def _module_version(src: Path) -> str:
196
+ for cand in (src / "package.json", src.parent / "playwright" / "package.json"):
197
+ try:
198
+ if cand.is_file():
199
+ return str(json.loads(cand.read_text(encoding="utf-8")).get("version") or "")
200
+ except Exception:
201
+ continue
202
+ return ""
203
+
204
+
205
+ def global_note(source: str, nm_dir: Path | None, nm_target: Path) -> str:
206
+ """运行器来自全局兜底时的**标准说明**(`source != "global"` ⇒ 空串)。
207
+
208
+ 与「本次有没有动手」**无关**:用了机器全局状态是**跑的前提事实**,不是一次性事件
209
+ (每一次都该说、每一次结论都因此降档)。两道消费者共用本函数给文案,不各写一份。
210
+ """
211
+ if source != "global":
212
+ return ""
213
+ ver = _module_version(Path(nm_dir) / "@playwright" / "test") if nm_dir else ""
214
+ link = Path(nm_target) / "@playwright" / "test"
215
+ return (f"运行器来自全局兜底:{MODULE_SPEC}{('@' + ver) if ver else ''}({nm_dir})"
216
+ f"——结论依赖**机器全局状态**而非项目自足,解析链在 {link};"
217
+ f"建议项目自装(npm i -D {MODULE_SPEC})以便可复现")
218
+
219
+
220
+ def ensure_resolvable(scripts_dir: Path, nm_target: Path, *, source: str,
221
+ nm_dir: Path | None = None) -> tuple[str, str, str]:
222
+ """判据不成立时的处置:全局兜底 ⇒ 幂等软链 + 复检;其余 ⇒ 不修只报。
223
+
224
+ → `(status, detail, note)`:
225
+ - `status ∈ {"ok", "repaired", "fail"}`:`ok` = 探针直接通过(无需动手);
226
+ `repaired` = 原本不通过、已补链并**复检通过**;`fail` = 仍不成立。
227
+ - `detail`:`fail` 时的真原因(不得回退成「跑失败 / 无结果」);`repaired` 时 = 本次动作说明。
228
+ - `note`:`global_note()` 的标准说明(全局兜底恒非空)——**调用方必须输出**。
229
+ """
230
+ note = global_note(source, nm_dir, nm_target)
231
+ ok, detail = probe(scripts_dir)
232
+ if ok:
233
+ return "ok", "", note
234
+ if source != "global":
235
+ return "fail", (f"{MODULE_SPEC} 从 {scripts_dir} 不可解析({detail})"
236
+ "——判据 = 依赖可导入性,不按路径存在;ESM 下 NODE_PATH 无效"), ""
237
+ if nm_dir is None:
238
+ return "fail", f"{MODULE_SPEC} 从 {scripts_dir} 不可解析({detail})且全局根未知", ""
239
+ src = Path(nm_dir) / "@playwright" / "test"
240
+ if not src.exists():
241
+ return "fail", f"全局 {MODULE_SPEC} 不存在:{src}", note
242
+ link = Path(nm_target) / "@playwright" / "test"
243
+ try:
244
+ if os.path.lexists(link):
245
+ same = os.path.realpath(str(link)) == os.path.realpath(str(src))
246
+ if not same:
247
+ return "fail", (f"{MODULE_SPEC} 从 {scripts_dir} 不可解析,且链接位已被占用"
248
+ f"(非本工具所建,不覆盖):{link}"), note
249
+ else:
250
+ link.parent.mkdir(parents=True, exist_ok=True)
251
+ os.symlink(src, link)
252
+ except OSError as e:
253
+ return "fail", f"{MODULE_SPEC} 从 {scripts_dir} 不可解析,软链失败:{link} → {src}({e})", note
254
+ ok2, detail2 = probe(scripts_dir)
255
+ if not ok2:
256
+ return "fail", f"{MODULE_SPEC} 在补软链后仍不可解析({detail2})", note
257
+ return "repaired", f"本次已在 {link} 补软链(幂等、可删)", note