atlas-project-harness 0.2.0-beta.5 → 0.2.0-beta.7

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 (33) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/DESIGN.md +7 -1
  3. package/README.md +1 -1
  4. package/install.sh +8 -0
  5. package/package.json +1 -1
  6. package/rings/e2e/reference.md +4 -2
  7. package/rings/prd/reference.md +14 -3
  8. package/scripts/__pycache__/atlas_check.cpython-312.pyc +0 -0
  9. package/scripts/__pycache__/gen_e2e_scripts.cpython-312.pyc +0 -0
  10. package/scripts/__pycache__/pkg_digest.cpython-312.pyc +0 -0
  11. package/scripts/atlas_check.py +81 -5
  12. package/scripts/gen_e2e_scripts.py +40 -19
  13. package/scripts/pkg_digest.py +42 -5
  14. package/shared/gates.md +8 -7
  15. package/shared/global-rules.md +8 -1
  16. package/templates/prd/domain.md +8 -6
  17. package/tests/__pycache__/test_atlas_check.cpython-312-pytest-9.1.1.pyc +0 -0
  18. package/tests/__pycache__/test_gen_e2e_scripts.cpython-312-pytest-9.1.1.pyc +0 -0
  19. package/tests/__pycache__/test_install_selfcheck.cpython-312-pytest-9.1.1.pyc +0 -0
  20. package/tests/__pycache__/test_map_anchors.cpython-312-pytest-9.1.1.pyc +0 -0
  21. package/tests/__pycache__/test_tools.cpython-312-pytest-9.1.1.pyc +0 -0
  22. package/tests/__pycache__/test_validators.cpython-312-pytest-9.1.1.pyc +0 -0
  23. package/tests/test_atlas_check.py +87 -0
  24. package/tests/test_gen_e2e_scripts.py +178 -17
  25. package/tests/test_install_selfcheck.py +8 -0
  26. package/tests/test_map_anchors.py +469 -0
  27. package/tests/test_tools.py +59 -0
  28. package/tests/test_validators.py +89 -0
  29. package/validators/__pycache__/validate_prd.cpython-312.pyc +0 -0
  30. package/validators/__pycache__/validate_structure.cpython-312.pyc +0 -0
  31. package/validators/validate_prd.py +83 -1
  32. package/validators/validate_structure.py +2 -1
  33. package/tests/__pycache__/test_gen_e2e_scripts.cpython-312.pyc +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,43 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## [v0.2.0-beta.7] - 2026-10-25
8
+
9
+ ### 🏃 真跑日批:D80② 升档兑现(D149)+ 真跑日协议(D148)+ D108 落点勘误
10
+
11
+ * **D80② 升档兑现(D149)**:`e2e.app_login` 未声明 ⇒ 生成器推 **`NOT_READY`(退出码 3 + 机读键 `"app_login"`)**,与 `app_base_url` 同一通道族(`--json` 面 `{"ok": false, "not_ready": "app_login", …}`),不再「占位生成 + WARN」半生成——硬前提的输入不得靠「缺失即静默放行」吸收;文案按 D103 相抵点指路:「有门但三态都装不下」(图形验证码 / 服务端 cookie 会话 / 登录响应体无令牌)⇒ 人工会话取出,不是「补声明」的对象。RFC-20261015-02 预告的时间锚「下一次真跑准备日」到期执行。钉:旧钉改判(半生成 ⇒ 全不生成)+ 新 `--json` 面钉;变异 **MB17**(退回 WARN 半生成)/ **MB18**(机读键写错或缺 message)。
12
+ * **真跑日协议(D148)**:runbook `docs/guide/real-run-day.md`——detached worktree 隔离执行(不碰用户 checkout、不提交)/ D145 manifest 配对先行 / 七项咬合清单 / **注红条款**(注红 red 不计缺陷判据)。首轮已执行出库,回写见 duty-review 三件(QUEUE / README / bugs.md)。
13
+ * **D108 落点勘误 + 留存窗口补定**:失败留证实际落 **`.atlas-run/`**(生成物早有显式 `outputDir`,Playwright 构件目录跟着 outputDir 走——D108 首版契约误写缺省 `test-results/`,2026-10-25 真跑日实咬勘误,构件形态 = `trace.zip + test-failed-*.png` 成对);**留存窗口 = 到下一次 `playwright test` 起跑**(Playwright 启动即清空 outputDir ⇒ 跨轮取证须起跑前归档)。契约 §7.1 / 生成物注释 / RFC 勘误节同批改口;咬合点不变 = 重装配(`install --update` / 首装),不溯及已装配副本。
14
+ * **真跑日首轮三发现入档**(均为观察、不计缺陷):① 落点勘误(上行);② 留证留存窗口(升格条件 = 首个有缺陷指控价值的构件在复跑起跑后被清空);③ 「栈起没起」探针盲区首个活体实证(前端 200 / 内网后端不可达 ⇒ 白屏全红、形似用例失败实为环境红)+ 半就绪变体(网关 200 / 业务 API 悬挂,唯有打真实业务 API 才能判)。另:案 9(`e2e_index` WARN 基线 7 条)升室待拍板;WS-C 回执状态收窄、WS-D 观察锚写作约定随批落地;消融首跑复核留档 `research/2026-10-23-ablation-first-run-recheck.md`。
15
+
16
+ ## [v0.2.0-beta.6] - 2026-10-23
17
+
18
+ ### 🧱 装配基线 manifest:漂移逐文件指名(D145)
19
+
20
+ * **能力**:`.atlas/MANIFEST.json` 逐文件摘要(`{digest, files}`),与 `.atlas/VERSION` 同点、**同一次遍历**产出;写序「先 MANIFEST 后 VERSION」⇒ VERSION 在场即基线已齐(半态可判定)。`check_version` 先做 manifest↔戳配对校验,配对后漂移**逐文件指名**(改/增/删各 cap 10 + 计数);旧装配无 manifest ⇒ 聚合口径 + ok 行尾注「正式判据未齐」,不新增红;`install.sh` 落戳后硬校验 manifest 在场。
21
+ * **口径**:判定「装配态一致」= 摘要相等 ∧ manifest 完整;未逐项解释的差异(`backflow.py` 三态)只能记「不一致(已解释)」,运行时仍 WARN(漂移是常态信号)。
22
+ * **钉**:manifest 形态/配对/空装配 + 逐文件指名 / cap 10 / 不配套 / 形态非法 / 旧装配尾注 + F54 干跑扩 manifest 断言;变异 **M-new-1/4/5**。
23
+
24
+ ### 📋 域级 PRD 变更记录 5 列 + 「只重验受影响项」通则(D146)
25
+
26
+ * **契约**:§七 表头升级为 `版本/日期/变更内容及原因/影响/结论与后续`;「影响」点名受影响域/AC/用例,未触达验证面写 `无(未触达验证面)`。validator 独立 check 行:5 列 PASS / legacy 3 列 WARN(下次真实变更时随迁,不单跑迁移轮)/ 表头不可辨或「影响」空 ⇒ FAIL / 节在无表 WARN。
27
+ * **通则**:`global-rules` §16「变更后只重验受影响项;未受影响结论沿用并留痕」,机器落点指认 D128 stale / `backflow` 三态 / apply §3.2,不新增门;全局 `十一` 修订记录 4 列不扩(边界句)。
28
+ * **钉**:改变更记录四态;变异 **M-new-3**。
29
+
30
+ ### 🕳 显式「无」通则 + `FORBIDDEN` 单源(D147)
31
+
32
+ * **契约**:可无内容不得留空白,`无`(理由可选)或既有语义词(`不适用(理由:…)` / `未覆盖(沿用实然)` / 控制台「未创建」);词表单源 = `global-rules` §5,`FORBIDDEN` 增补 `[必填` / `[条件必填` 并三处同步 + 守恒钉。
33
+ * **机器检查(首版 WARN)**:域级 §六 空表/空节无显式结论 ⇒ WARN(翻转触发器:连续两收口周期真实遗漏 ≥1 且误报 0 ⇒ 升 FAIL;统计落 per-域收口留痕 README「可无项空表:N / 误报:N」);§七「影响」空白 ⇒ FAIL。
34
+ * **钉**:§六 四态 + 词表守恒;变异 **M-new-2**。
35
+
36
+ ### 🐛 E2E `select` 点中错误选项(D144)
37
+
38
+ * **病灶**:`atlasSelect` 采样后按**整页 DOM 下标**再点击。一个面板同时挂多个下拉(关闭态节点仍在 DOM、rc-select 另有零宽 `role=option` 镜像),组件重排后同一数字指向别当下拉的选项 ⇒ 选「不限时间」却点中「自定义范围」镜像节点,重试至 30s 超时。全量 41 条 2/2 红、单跑 2/2 绿(顺序依赖的假绿)。
39
+ * **修复**:唯一命中改为给该元素打一次性标记,点击锚定标记本身(走 Playwright actionability);每次采样先清遗留标记 ⇒ 选择器无歧义;标记失效 ⇒ 重新采样打标再点,有界 3 次,仍不成则带可见选项响亮失败。轮询、三级降级、歧义即拒、原生 `<select>` 分支不变。
40
+ * **钉**:`tests/test_gen_e2e_scripts.py::test_select_clicks_the_marked_option_not_a_global_index`;既有 D112/D117 两条钉按新形态更新(返回 token 而非 idx、候选选择器出现次数 2 → 1)。
41
+
42
+ ---
43
+
7
44
  ## [v0.2.0-beta.5] - 2026-10-23
8
45
 
9
46
  ### ✨ E2E `request` 请求载荷断言(D142)
package/DESIGN.md CHANGED
@@ -574,7 +574,7 @@ adapters:
574
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
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
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
- | D108 | E2E 真跑失败**自动留证**——trace / screenshot 只在失败落盘(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence`;外部发版雷达首跑 5 条机制级情报中唯一采纳项,独立评审两轮 + 维护者拍板「按推荐继续」) | ① **病灶**:真跑偶发红(节拍类)现场的取证上限 = 文本尾部 80 行(`e2e_console.py:520`(采集即截)/ `:582`(docstring 失败才带尾)/ `:591-592`(落账条件)),元素几何 / 命中点 / 时序类判据只能靠 `/tmp` 临时探针(跨会话不可复算;D98 动词批与案 6 两次付费先例);`duty-review/bugs.md` 未解观察(hover 禁用按钮首跑偶发 `Element is not visible`,3 连跑全绿)的**升判条件**「复现 ≥2 次且能给出固定节拍证据(trace / 连续截图)⇒ 转 DR4」悬空在等证据源(`bugs.md:17`)。② **定案**:生成物两轨 playwright 配置补 `trace: 'retain-on-failure'` + `screenshot: 'only-on-failure'`——失败现场自动落 trace(DOM 快照 + 网络 + 操作日志,trace viewer 可逐帧回放)与截图到 Playwright 缺省 `test-results/`,**全绿轮零新增构件**;不溯及已装配副本 ⇒ **咬合点 = 下一次 `install.sh --update` / 首装重装配**,重装配前「契约已声明留证而现场无 trace」属**如实态、非缺陷**,巡检不得误报(对齐 `shared/single-source.md` §3 迁移仪式「新源生效 ⇔ 旧源显式失效」);留证含业务内容,只落采纳仓本地、不进 atlas 上报通道(§15 零业务上下文出站面不破);不解决 D103「人工会话取出 ⇒ CI 不可复现」盲区(trace 只重放**已发生**的失败,真身是登录态不是取证手段)。③ **被否**:`video` 缓行(trace 自带回放器,video 是粗粒度投影,多一键 = 双轨同构面 +2 无收益;缓行条件 = 实跑出现 trace 覆盖不了的取证需求才另起决策行);全量常录 `on`(磁盘 × 每用例,成功轮留证无消费方);留证键加环境开关(= 给「成功轮也留证」开后门);独立录制管理器 / 上传通道 / 轮转清理(运行时平台机制,Atlas 零运行时);**失败自动重试 N 次**(与「节拍类首跑不坐实、复跑复现才改判」(`bugs.md` E2E 段)方向相反,会把真缺陷静默吸收成绿——**防复读机裁决**已落 `duty-review/QUEUE.md` 归档区,外部情报再提 retry 引用可径行否决);托管云沙盒会话(违「项目自足」,同归档区裁决)。④ **钉与变异**:钉 3 条——`test_failure_evidence_both_tracks_declare_trace`(双轨同构:`mode:none` 轨与预置登录轨都必须含两键)/ `test_failure_evidence_values_are_closed`(值形态封闭:恰为 retain-on-failure / only-on-failure,`video` 混入 / `on` 常录 ⇒ 红)/ `test_failure_evidence_has_no_override_switch`(无覆盖口),`tests/test_gen_e2e_scripts.py` 74 → **77** 条;变异 **MB10**(摘 none 轨 trace/screenshot 行)/ **MB11**(值改 `on`),`mutation_proof` 四段协议双绿(基线 3 passed → 注入 rc=1 → 还原逐字节一致 → 复绿),注入 diff 摘录于 `proposals/REVIEW-2026-10-18-e2e-failure-evidence.md`;契约条 = `rings/e2e/reference.md` §7.1「失败留证(硬,失败才落盘)」,门行 = `shared/gates.md` 2.2。⑤ **出库判定**:接线面(生成器 + 契约 + 门行)已立并全量 497 绿;**真跑留证咬合 = 下一次重装配后的真跑**(注红用例 ⇒ `test-results/` 出 trace、全绿轮零构件),咬合记录登 `shared/gates.md` 2.2 行(D98 / D101 先例)后出库。 |
577
+ | D108 | E2E 真跑失败**自动留证**——trace / screenshot 只在失败落盘(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence`;外部发版雷达首跑 5 条机制级情报中唯一采纳项,独立评审两轮 + 维护者拍板「按推荐继续」) | ① **病灶**:真跑偶发红(节拍类)现场的取证上限 = 文本尾部 80 行(`e2e_console.py:520`(采集即截)/ `:582`(docstring 失败才带尾)/ `:591-592`(落账条件)),元素几何 / 命中点 / 时序类判据只能靠 `/tmp` 临时探针(跨会话不可复算;D98 动词批与案 6 两次付费先例);`duty-review/bugs.md` 未解观察(hover 禁用按钮首跑偶发 `Element is not visible`,3 连跑全绿)的**升判条件**「复现 ≥2 次且能给出固定节拍证据(trace / 连续截图)⇒ 转 DR4」悬空在等证据源(`bugs.md:17`)。② **定案**:生成物两轨 playwright 配置补 `trace: 'retain-on-failure'` + `screenshot: 'only-on-failure'`——失败现场自动落 trace(DOM 快照 + 网络 + 操作日志,trace viewer 可逐帧回放)与截图到 Playwright 缺省 `test-results/`,**全绿轮零新增构件**;不溯及已装配副本 ⇒ **咬合点 = 下一次 `install.sh --update` / 首装重装配**,重装配前「契约已声明留证而现场无 trace」属**如实态、非缺陷**,巡检不得误报(对齐 `shared/single-source.md` §3 迁移仪式「新源生效 ⇔ 旧源显式失效」);留证含业务内容,只落采纳仓本地、不进 atlas 上报通道(§15 零业务上下文出站面不破);不解决 D103「人工会话取出 ⇒ CI 不可复现」盲区(trace 只重放**已发生**的失败,真身是登录态不是取证手段)。③ **被否**:`video` 缓行(trace 自带回放器,video 是粗粒度投影,多一键 = 双轨同构面 +2 无收益;缓行条件 = 实跑出现 trace 覆盖不了的取证需求才另起决策行);全量常录 `on`(磁盘 × 每用例,成功轮留证无消费方);留证键加环境开关(= 给「成功轮也留证」开后门);独立录制管理器 / 上传通道 / 轮转清理(运行时平台机制,Atlas 零运行时);**失败自动重试 N 次**(与「节拍类首跑不坐实、复跑复现才改判」(`bugs.md` E2E 段)方向相反,会把真缺陷静默吸收成绿——**防复读机裁决**已落 `duty-review/QUEUE.md` 归档区,外部情报再提 retry 引用可径行否决);托管云沙盒会话(违「项目自足」,同归档区裁决)。④ **钉与变异**:钉 3 条——`test_failure_evidence_both_tracks_declare_trace`(双轨同构:`mode:none` 轨与预置登录轨都必须含两键)/ `test_failure_evidence_values_are_closed`(值形态封闭:恰为 retain-on-failure / only-on-failure,`video` 混入 / `on` 常录 ⇒ 红)/ `test_failure_evidence_has_no_override_switch`(无覆盖口),`tests/test_gen_e2e_scripts.py` 74 → **77** 条;变异 **MB10**(摘 none 轨 trace/screenshot 行)/ **MB11**(值改 `on`),`mutation_proof` 四段协议双绿(基线 3 passed → 注入 rc=1 → 还原逐字节一致 → 复绿),注入 diff 摘录于 `proposals/REVIEW-2026-10-18-e2e-failure-evidence.md`;契约条 = `rings/e2e/reference.md` §7.1「失败留证(硬,失败才落盘)」,门行 = `shared/gates.md` 2.2。⑤ **出库判定(2026-10-25 真跑日出库,D148 首轮)**:红轮 28 失败构件(trace.zip + 截图成对)/ 绿轮零构件,两面实证(咬合记录 = `shared/gates.md` 2.2「D108 首次咬合」);**落点勘误随批**(实际 `.atlas-run/`,非首版误写的缺省 `test-results/`)+ **留存窗口观察在案**(Playwright 起动清空 outputDir ⇒ 留证只存到下一次起跑,`duty-review/bugs.md` E2E 段带升格条件)。 |
578
578
  | D109 | **逐用例宿主注入 URL 参数通道**:`goto` 查询门**重靶** + 项目级白名单(2026-10-18;字段与门落在 `rings/e2e/reference.md` §5.5 / §8.2;采用方现场驱动) | ① **病灶**:`会话:`(D89)解决的是 **sessionStorage** 那一个消费面;而某类应用的被测参数**只从载入地址的常规查询串读**(`window.location.search`)⇒ **无合法写法**(`goto` 带 `?` 是硬 FAIL、`auth` 只管登录态、`seed:` 绑数据实体)—— D104 ① 已把这条记为「三条各自成立 ⇒ 无合法写法」,当日改选 B′(base query 全局转发)并把「`会话:` 扩 URL 参数档」列入被否清单。采用方现场(2026-10-18):两条用例被迫写成**同一前提**(都走 sessionStorage)⇒ 重复,而查询串那一面**零覆盖**。② **重靶取证(关键,本行的依据)**:原门(§5.7,2026-09-23;事故 `3509 §B20` = `E2E-LOGIN-003` 断言 `?state=forbidden` 演示横幅**自身文案**)的**靶子随 E1 原型环退役而消失** —— `?state=` 只出现在本文件 `:460` 的**已作废**决策行(D8 两段式 / D56 原型交互分级);E1 后**无任何决策行**重新论证该门;`duty-review/bugs.md` 与 `CHANGELOG.md` **零起**该门事故;今日它在 D104 ① 里的身份是「无合法写法」的**阻碍** = 制造成本,而非拦事故。该门今日真正剩下的(`reference.md` 旧 §5.7 邻域的那条)是**另一族、不限于原型**的纪律:「前置数据态只能用真实界面动作达到」—— **而这族边界在重靶前后都由项目级白名单守**,与「`goto` 带不带 `?`」无关 ⇒ 取舍只剩成本与表达力。③ **定案**:(a)门**重靶** `goto 不带查询参数` → **`goto 查询键须已声明`**(键须 ① ∈ `e2e.query_preset.keys` ∧ ② ∈ 本用例 `参数:`;**原靶仍在 FAIL 面内**:未声明键照样红);(b)用例块新增可选 `参数:`(**只声明键**,值内联在 `step` 的 `goto` 查询串里 = 单一来源),**守卫导航取该用例首个带参 `goto` 的查询串**(否则守卫读的是**无前提的 DOM**:参数决定「哪些元素在场」的页面上守卫会在 `goto` 后即红);(c)`_support/url_merge.merge_base_query` 的「目标自带 `?` ⇒ 原样返回」**暗位转正为并集档**(base 在前、**同键 ⇒ 用例胜**)——该分支原注释即写着「§5.7 下不可达,**为未来扩展留位**」,且既有生成物无一带 query 的 `goto` ⇒ **零回归**;(d)三道新门(声明有效 / 值形态 / 同键双载体)+ 生成期同源中止(双保险)。④ **被否**:(A)「值只写 `参数:` 行、`goto` 保持无 query」—— 其立论之一是「保住 §5.7 事故防护」,取证后该论据**作废**(见 ②),取舍只剩成本与表达力,B 胜;其余照 D104 ⑤(运行期 TS 助手 / 把「同名键以目标为准」写成通则)。⑤ **边界(如实登记,不得 oversell)**:允许带白名单查询串后,「数据态直达」那族纪律的**机器面只剩白名单本身**(「项目说合法就合法」)—— 与 `会话:` 同族、是本设计的本来形态;登记以防后人以为还有别的网在兜。⑥ **钉与变异**:`tests/test_validators.py` **116 → 123**(重靶钉改名 1 + `参数:` 门 6);`tests/test_gen_e2e_scripts.py` **+6**(两处导航面承载 / 基线不施加 / 无白名单中止 / 白名单外中止 / 死声明中止 / 值形态中止);`_support/url_merge.py` 并集档(原「原样保留」单元钉随之改判)。变异 **MB12**(并集档退回「原样返回」⇒ 单元钉 + 守卫承载钉齐红)、**MB13**(守卫不追加用例参数 ⇒ 守卫面钉红),两枚均四段协议通过、还原逐字节一致,注入 diff 见任务件 `.trellis/tasks/10-18-atlas-url-param-channel/check-report.md`。设计依据 = 采用方现场(`src/entry/main/app.tsx:18/:36` 两个消费面 + `validators/validate_e2e_index.py` 旧门 + `_support/url_merge.py:48`)。 |
579
579
  | D110 | 控制台**逐用例数据准备钩子**:项目自带 `product/e2e/scripts/atlas-prepare.mjs` ⇒ 执行前必跑、失败即拒(2026-10-18,v3 现场实证移植批;病灶账 = `duty-review/bugs.md` 005/006 链 companion;契约条 = `rings/e2e/reference.md` §10「执行」行 / `e2e_console` docstring) | ① **病灶**:共享状态型靶场(SPA + 服务端态)的用例 `precondition` 属**文档性声明**、运行器不自动归零 ⇒ 残留实体使 count 类断言系统性偏移(现场实测 Expected 1 · Received 5);且控制台此前对「执行前重置」零消费——`e2e.reset`(`shared/stack-profile.md` §2)只被 `atlas_check` 真跑门执行(**批级**:整轮一次),逐条执行面无 per-case 前提通道。② **定案**:执行器在每条用例**计时起点之前**跑该脚本(`node <e2e.scripts_dir>/atlas-prepare.mjs`,cwd = 项目根,180s 上限,**不占**用例超时窗);失败(非零)/ 超时 / 起不动 ⇒ **拒绝执行该条**:`done` 事件按完整形状写入(`ok=False` / `exit` 如实 / `prep_failed=True`),失败原文进「输出」面板——不静默跑注定偏移的用例(对齐执行器「解析不成立 ⇒ 诚实拒绝」同族口径)。③ **与 `e2e.reset` 的关系(登记,防后人重复起案)**:**不并轨**——`reset` 是批级且值形态为 shell 命令串(项目在 profile 声明)、本钩子是逐条且文件约定(项目在 `<e2e.scripts_dir>` 放脚本),现场 `reset: null`、两粒度各占一格;控制台若要消费 `reset` 须另起决策行。④ **被否**:由生成脚本内自查归零(把环境初值达序藏进生成物 = 静默吸收);自动执行 precondition 声明(人读文档,无机器可执行形态);prepare 失败仍照跑(偏离移用例 = 假绿通道)。⑤ **钉与变异**:`tests/test_e2e_console.py` **13 → 14**(钩子先于真跑 / 失败拒绝并带 `prep_failed` / 起不动 `exit=-2` 三型一钉);变异 **M94**(摘 prepare 钩子段 ⇒ 钉红,还原逐字节一致)。设计依据 = v3 装配副本手改(现场实证可用);主仓移植时审校掉装配副本失败路径的 `self._lines` 属性错拼(会 AttributeError 让执行线程挂死、`running` 永真),统一改走 `self._emit`(持锁 + notify)。 |
580
580
  | D111 | `refresh` 落盘节拍参数化:发射 `ATLAS_REFRESH_SETTLE` 注入的 `waitForTimeout`(先等后刷),默认 0 行为不变(2026-10-18,v3 现场实证移植批;病灶账 = `duty-review/bugs.md` 005 链;契约条 = `rings/e2e/reference.md` §7.1 / §5.5 `refresh` 行) | ① **病灶**:`refresh` 裸 `reload` 对启用**节流型自动保存**的项目(距上次成功保存 < 节流窗则延迟落库)必丢步骤编辑内容(现场实测 Expected 1 · Received 0——刷新发生在保存落库之前)。② **定案**:发射**先等后刷**两行——`await page.waitForTimeout(Number(process.env.ATLAS_REFRESH_SETTLE || "0"));` + `await page.reload();`;**默认 0 ⇒ 生成物行为与既有契约一致**。通道选择同 F11:Playwright `test` CLI 无逐用例注入面,env 是唯一通道。③ **零业务常数(硬)**:毫秒数**不进生成器**——它属项目侧**运行时**前提,现场以 `ATLAS_REFRESH_SETTLE=<毫秒>` 于启动面注入(对齐 `ATLAS_SLOWMO` / `ATLAS_EXPECT_TIMEOUT` 同族);它也不是 §5.5「固定 sleep」禁令所指(该禁令管**用例正文**不把共享靶场节拍写死;本行是执行环境配置面)。④ **被否**:硬编码任何项目常数进源包(§15 零业务上下文纪律);生成期读项目配置算节拍(生成物 = 确定性派生物,节拍是运行时动态值)。⑤ **钉与变异**:`tests/test_gen_e2e_scripts.py` **83 → 84**(先等后刷顺序钉);变异 **M95**(摘 `waitForTimeout` 发射行 ⇒ 钉红,还原逐字节一致)。 |
@@ -610,6 +610,12 @@ adapters:
610
610
  | D141 | `validate_testids` 提取器**剥注释只做一次**(2026-10-23;`validators/validate_testids.py`、`tests/test_validators.py`、`shared/gates.md`;P5-A 性能批) | ① **病灶**:`collect_testids_expanded` 对每个源文件调用 `strip_comments` 两次(第一次循环取 `all_tids`、第二次循环取 `file_testid_decls`),同一份文本重复剥注释;隔离副本 profile 显示 `strip_comments` 3167 次 / 1611 文件、累计 0.238s,提取器占本门 95%。② **定案**:第一次循环就把 `strip_comments` 结果存进 `texts`,第二次循环复用;判据 / 输出不变。隔离副本 5 轮交错实测 796.5ms → 686.4ms(-110.1ms / -13.8%),输出逐字不变。③ **被否**:删判据省时间(页面集合 WARN 构造约 0ms、落点判据约 50ms,删了丢真实 WARN);并行化扫描(引入并发与顺序不确定性,当前不必要);按 mtime 缓存(跨轮脏读风险)。④ **钉与变异**:`tests/test_validators.py` 139 → **140 条**;`test_validate_testids_strip_comments_once_per_file` 数调用次数(每文件 1 次,修复前 2 次)+ 抽取值断言;变异 **M137**(还原第二次 `strip_comments` 调用 ⇒ 钉红;还原逐字节一致;注入 diff 见 `research/2026-10-23-ablation-p5a-mutation.md`);`shared/gates.md` 门行与测试计数同步。 |
611
611
  | D142 | 新增 E2E **`request` 请求载荷断言动词**(2026-10-23;`rings/e2e/reference.md` §5.5/§7.1/§8.2;`scripts/gen_e2e_scripts.py`;`validators/validate_e2e_index.py`;`validators/validate_testids.py`;`tests/test_gen_e2e_scripts.py`、`tests/test_validators.py`) | ① **病灶**:断言动词只覆盖 DOM,无法验证「某提交请求的 JSON 载荷中字段缺席/等于某值」⇒ 只能人工开 DevTools;前端临时脚本用 `page.on('request')` 十行即可稳定断言,证明是 DSL 表达力缺口。② **定案**:`request <api> <json-path> <expected>`;api = `?api=` 的带点唯一后缀(歧义 FAIL);json-path 支持点号键/`[N]`/`[key=value]`,只有叶子可缺席;`(缺席)` / `null` / `""` 三态分离,`(空)` 拒绝;**动作锚定**:`after:N` 在第 N 步前记序号、无锚锚最后一步,只认该时点后**新出现的第一条**匹配请求(历史/自动保存/重试不得满足),无新请求 FAIL;中间缺失/过滤 0 或多命中/越界/请求体解析失败 = 解析错误;不参与零步基线;含该断言的用例默认 `trace: off`(`ATLAS_REQUEST_TRACE=1` opt-in),失败消息不打印载荷。③ **被否**:取「最近一次匹配请求」(历史请求可假绿)、`(空)` 合并缺失与 null、用 mock 替代请求断言(只能验渲染)、完整 JSONPath 依赖(零依赖契约)、允许用例写任意脚本。④ **钉与变异**:生成器“动作锚定 + trace off + 无 request 零变化”、Node 真求值三态/新请求/解析错误、校验器形态正反例、testid 提取不把 api 当 testid;变异 **M145**(摘 watcher 发射)/ **M146**(缺失叶子按空串)/ **M147**(接受无 step 的 request),四段协议还原一致。 |
612
612
  | D143 | 人工走查项**结构化块**(扁平行式),id/title 由标题行提供(2026-10-23;`rings/e2e/reference.md` §8.2/§10.1;`_support/manual_block.py`;`scripts/e2e_console.py`;`templates/e2e/console/app.js`;`validators/validate_e2e_index.py`) | ① **病灶**:`manual.md` 只有 `- ID \| 页面 \| 标题 \| 一段说明`,控制台只能原样渲染 prose;步骤、命令、观察点、证据没有字段承载,UI 无法分步/复制,人工走查体验差。② **定案**:兼容旧行;新增 `## <ID> <标题>` + `atlas-manual` 块(`page / precondition / step / evidence`),`step` 每行 `操作 \| 观察 \| 命令(可空)`,复用 `parse_block` 扁平数组语义;id/title 只从标题行取,块内重写即 FAIL;解析单源 `_support/manual_block.py`(控制台/校验器共用);控制台按步渲染、命令可复制;账本仍保持用例级,不扩 schema。③ **被否**:嵌套 `steps` map + `command: \|` 块标量(需新语法/解析器,零依赖下脆弱)、逐步骤账本(扩账本 schema)、把结构化信息继续塞进 prose。④ **钉与变异**:控制台块解析/旧行兼容/块内 id 拒绝、校验器页面/步骤/重复 ID/解析失败;变异 **M148**(摘块解析或步骤校验),四段协议还原一致。 |
613
+ | D144 | `select` 唯一命中后点**打标元素本身**,废除整页 DOM 下标点击(2026-10-23;`scripts/gen_e2e_scripts.py` 的 `atlasSelect` 发射点;契约 §5.5 `select` 行) | ① **病灶**:旧实现采样时用整页候选集 `[role=option],[role=menuitem],[class*="select-item-option"]`,命中后返回 `idx = all.indexOf(hit[0])`,再点击仍按**整页同一选择器的第 idx 个**。一个面板可同时挂多个下拉(Ant Design 关闭态的 dropdown 节点仍留在 DOM,rc-select 另有零宽 `role=option` a11y 镜像),下标只对「采样那一刻的 DOM 快照」有效 ⇒ 组件一重排,同一数字指向另一个下拉的选项或不可点的镜像节点。现场(百炼 v3 测试环境,全量 41 条 2/2 红、单跑 2/2 绿):步骤要选「不限时间」,点击却解析成 `<div role="option" aria-label="自定义范围" id="rc_select_4_list_3">custom</div>`,重试 52 次 `element is not visible` 至 30s 超时;单跑时挂载的选项节点少、下标恰好还对得上 ⇒ **顺序依赖的假绿**。② **定案**:`atlasPick` 在页面内给唯一命中元素打一次性标记 `data-atlas-pick=<token>` 并返回 token(每次采样先清除全部遗留标记,保证标记至多一枚 ⇒ 选择器无歧义);点击改为 `page.locator('[data-atlas-pick="<token>"]')`,即点**命中的那个元素本身**,由 Playwright 走可见性/稳定性等 actionability 检查;元素被重排回收导致标记失效 ⇒ 重新采样打标再点,**有界 3 次**,仍不成则带可见选项清单响亮失败(不静默换目标、不静默通过)。就绪轮询(`expect.poll`)、三级降级匹配、歧义即拒、原生 `<select>` 分支与失败文案全部不变。③ **被否**:把点击作用域缩到「当前打开的 dropdown」类名(吃组件库 CSS 前缀,破坏实现无关契约,其他库仍会漏);按 label 用 `getByRole('option', {name})` 直取(antd 选项可访问名含描述文案,前缀/包含级联语义会退化,且多个下拉同名时无解);加固定等待等 DOM 稳定(D117 已否决固定等待,且治不了指错元素);把候选集限定为 `role=option`(D122 已证 antd 可见项无该 role,会退回「下拉已打开也采不到」)。④ **钉**:`tests/test_gen_e2e_scripts.py::test_select_clicks_the_marked_option_not_a_global_index`(① 病灶字符串 `all.indexOf` / `pick.idx` / `.nth(pick.idx)` 必须消失 ② 打标与按标点击成对 ③ 遗留标记清理行 ④ 有界重试 ⑤ 失败文案 ⑥ 仍用 `expect.poll` 且无 `waitForTimeout`);既有 D112/D117 两条钉按新形态更新(唯一命中返回 token 而非 idx、候选选择器出现次数由 2 降为 1)。**(编号说明:D143 已被人工走查结构化块占用,本决策顺延为 D144。)** |
614
+ | D145 | **装配基线 manifest**:`.atlas/MANIFEST.json` 逐文件摘要 + 配对校验 + 「未解释差异不得判一致」(2026-10-23;`RFC-20261023-02` ②;`scripts/pkg_digest.py`、`scripts/atlas_check.py`、`install.sh`、`shared/gates.md` §2.2) | ① **病灶**:聚合摘要只回答「变没变」,回答不了「变在哪」——`check_version` 的漂移 WARN 自证「差异明细跑 `backflow.py` 三态对账」;而外部流程规范给的机制是**逐项内容摘要 + 解释制一致性**(缺失字段 / 采集失败 / 未解释差异均不得判一致)。② **定案**:`manifest()` 与 `digest()` 共用同一次遍历产 `{digest, files}`;`stamp_file()` **先 MANIFEST 后 VERSION**(VERSION 在场即 manifest 已齐 ⇒ 半态可判定);`check_version` **配对校验先行**(manifest.digest ≠ 装配戳 ⇒ WARN 指名重装),配对后漂移**逐文件指名**(改/增/删各 cap 10 + 计数);旧装配无 manifest ⇒ 聚合口径 + ok 行尾注「正式判据未齐」(不新增红);两分支 WARN 保 M68 三词;`install.sh` 落戳后硬校验 manifest 在场;判定「一致」= 摘要相等 ∧ manifest 齐,未解释差异只能记「不一致(已解释)」(运行时仍 WARN,漂移是常态信号)。③ **被否**:扩 VERSION 为多行/JSON(旧副本全军 WARN,干跑守卫重走);漂移直接升红门(打断正常开发循环,解释制只属收口/发布判定);接线 `release.py`(过度设计,列为后续可选项)。④ **钉与变异**:`tests/test_tools.py` manifest 形态/配对/空装配三钉;`tests/test_atlas_check.py` 逐文件指名 / cap 10 / 不配套 / 形态非法 / 旧装配尾注五钉;F54 干跑扩 `MANIFEST.json` 逐字节断言;变异 **M-new-1**(摘 diff 分支)、**M-new-4**(摘配对校验)、**M-new-5**(摘干跑闸),四段协议还原逐字节一致。 |
615
+ | D146 | **域级 PRD 变更记录 5 列** + 「变更后只重验受影响项」通则(2026-10-23;`RFC-20261023-02` ③;`templates/prd/domain.md` §六/§七、`validators/validate_prd.py`、`rings/prd/reference.md`、`shared/global-rules.md` §16) | ① **病灶**:§七 只有「版本/日期/变更说明」,无影响、无结论与后续;「受影响才重验」原则散落 `apply` §3.2 / e2e 收口三值② / D128 `stale` 三处而未收拢,变更后哪些结论可沿用无处承载。② **定案**:表头升级 `版本/日期/变更内容及原因/影响/结论与后续`;「影响」点名受影响域/AC/用例,未触达验证面写 `无(未触达验证面)`;validator 独立 check 行:5 列 PASS / legacy(含`变更说明`)WARN「下次真实变更时随迁」/ 表头不可辨或「影响」空 FAIL / 节在无表 WARN;通则落 `global-rules` §16(只重验受影响项、未受影响结论沿用并留痕),机器落点指认 D128 stale / `backflow` 三态 / apply §3.2,不新增门;全局 `十一` 修订记录 4 列**不扩**(边界句)。③ **被否**:语义塞进「变更说明」单元格(无判据);新增「变更→受影响用例集」对账门(语义依赖,先立字段与通则);全局修订记录同步扩列(无消费者)。④ **钉与变异**:`test_rfc20261023_02_change_record_shape_and_impact_column` 四态;**M-new-3** 删 legacy 分支 ⇒ 钉红;测试夹具随迁 5 列(不为迁移单跑一轮)。 |
616
+ | D147 | **显式「无」通则** + `FORBIDDEN` 三处单源 + 两处机器检查首版 WARN(2026-10-23;`RFC-20261023-02` ④;`shared/global-rules.md` §5、`validators/validate_prd.py`、`validators/validate_structure.py`、`templates/prd/domain.md`、`rings/prd/reference.md` §7、`rings/e2e/reference.md` §10.1) | ① **病灶**:`无` / `不适用(理由:…)` / `未覆盖(沿用实然)` / 控制台「未创建」四套写法并存,「空表/空节」与「评估过为无」同形;`FORBIDDEN` 不抓空表,也不抓 `[必填:…]` 占位残留;两校验器与 §5 的词表各说各话。② **定案**:通则 = 「可无内容不得留空白,必须有结论」(`无`(理由可选)/既有语义词,词表单源 = §5);`FORBIDDEN` 增补 `[必填` / `[条件必填` 并三处同步 + 守恒钉 `test_forbidden_vocabulary_single_source`;机器落点只两处(§六 空表/空节无 `无` ⇒ **WARN 首版**;§七「影响」空 ⇒ FAIL),均独立 check 行、登记门册;模板占位指令统一「无则写 无(理由可选)」;翻转触发器 = 连续两收口周期真实遗漏 ≥1 且误报 0 ⇒ 升 FAIL,统计落 per-域收口留痕 README「可无项空表:N / 误报:N」。③ **被否**:全量扫描所有可无节 FAIL(违反「落门先问语料」);把四词语义合并成单一写法(丢语义边界)。④ **钉与变异**:`test_rfc20261023_02_optional_section_explicit_none`(空节 / 空表 / 显式 `无` / 开户最小集四态);**M-new-2** 判据恒真 ⇒ 钉红。 |
617
+ | D148 | **真跑日协议**:一次「重装配 + 真跑」把已立待咬的能力一次咬合,**detached worktree 隔离执行**(2026-10-24;runbook = `docs/guide/real-run-day.md`;维护者拍板「都按你推荐的来」+ 边界「v3 用户分支 `dev_20261120` 不碰、自建 worktree、不提交」;源自设计优化方案 v2 WS-B,评审结论已并入) | ① **病灶**:咬合点分散且都挂在「下一次真跑」上(D108 留证 / adapters 面 / hover 与 goto 未解观察 / WARN 基线 / D80② 升档 / 副本漂移正式解法),逐个等真跑 ⇒ 凑不齐;且用户 checkout 常被占用(现场:v3 `dev_20261120` 在用)。② **定案**:runbook 六步(worktree 隔离 → 重装配 → D145 manifest 配对校验 → 重生成对账 → 净场协议 → 真跑留档 `product/e2e/reviews/<日期>/`)+ 七项咬合清单(逐项勾、咬合一处回写一处:gates 对应行 / bugs.md / QUEUE)+ **注红条款**(注红 = 取证动作,red 不计缺陷判据、不进 bugs.md 复发计数、不触发巡检升判——与「首跑不坐实」纪律的摩擦预先写死)+ 不做清单(不顺手改用例语义/产品代码、不在用户 checkout 留痕、不提交 worktree 产物;登录态只读复制不覆盖)。③ **被否**:逐项单等真跑(凑不齐);用户 checkout 直跑(分支占用 + 痕迹);GUI/看板类真跑仪表(运行时平台机制,Atlas 零运行时)。④ **钉与变异**:本条为协议页 + 决策行,无新机器面(咬合证据回写既有登记面);D149(app_login 升档)与 D108(失败留证)是首批两个咬合对象。⑤ **出库判定(2026-10-25 首轮执行完,出库)**:七项结果——① D108 两面咬到(红 28 构件/绿零构件;**附带三发现**:落点勘误〔`.atlas-run/` 非缺省 `test-results/`〕、留存窗口〔Playwright 起动清空 outputDir,观察入 bugs.md〕、「栈起没起」探针盲区首个活体实证〔前端 200/后端不可达⇒门放行全红〕);② adapters 面实况已采集(models 渲染层 82/1859 行、routes 19/55——与既有记录同族,无升档动作);③ hover 未咬到(靶场白屏无从复现);④ goto 计数未咬到(同因);⑤ WARN 基线实测 **7 条**(≥3 且形态重复 ⇒ 观察室升室条件满足 ⇒ QUEUE 待拍板案 9);⑥ D149 已声明面咬到;⑦ worktree 面咬到(首装即最新 + D145 manifest 配对 ok;用户 checkout 漂移副本未动,留用户自行 `--update`);D124 就绪门顺带 ✓(chromium launch 实跑)。协议页随首轮实况修订(落点/留存窗口)。 |
618
+ | D149 | **D80② 升档兑现:`e2e.app_login` 未声明 ⇒ 生成器推 `NOT_READY`(退出码 3 + 机读键 `"app_login"`)**,不再「占位生成 + WARN」半生成(2026-10-24;RFC-20261015-02 预告的时间锚「下一次真跑准备日」到期执行;真跑日批) | ① **病灶**:占位形态 = 硬前提的输入被「缺失即静默放行」吸收(未登录态跑用例 ⇒ 假红/假绿都解释不了)。② **定案**:与 `app_base_url` 同一通道族(`--json` 面 `{"ok": false, "not_ready": "app_login", "message": …}`);文案按 **D103 相抵点**指路——「有门但三态都装不下」(图形验证码 / 服务端 cookie 会话 / 登录响应体无令牌)⇒ 人工会话取出,是描述路径、不是「补声明」的对象(排除类不被罚 not_ready,出路写进文案);`atlas_check` 消费面走既有 `GEN_NOT_READY` 三态 SKIP 中继(message 透传,无需改);`validate_stack_profile` WARN 保留(两道机器面都看得见)。③ **被否**:直接 FAIL(「未走到这一步」≠ 失败,`3509 §B110`);文案只说「请补声明」(丢 D103 排除类出路 ⇒ 把「如实未声明」罚成必填错)。④ **钉与变异**:`test_setup_without_app_login_pushes_not_ready`(改判自旧钉「degrades_loudly」:rc=3 + stderr 指路含 `mode: none` 出口与「人工会话取出」+ **不写盘**)+ `test_app_login_not_ready_json_face_matches_channel`(机读键 + message 面);变异 **MB17**(退回 WARN 半生成 ⇒ 红)/ **MB18**(机读键写错/缺 message ⇒ 红)。⑤ **出库判定(2026-10-25 出库)**:已声明靶场(v3 fabricated)行为不变实证——真跑日 regen 正常、setup 预置照跑、无 not_ready(D148 首轮第 6 项);未声明面由钉 MB17/MB18 守。 |
613
619
 
614
620
 
615
621
 
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.5-blue.svg" alt="Version" />
4
+ <img src="https://img.shields.io/badge/version-0.2.0--beta.7-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/install.sh CHANGED
@@ -524,6 +524,14 @@ print(stamp_file(Path(sys.argv[1]), Path(sys.argv[2])))
524
524
  PY
525
525
  )"; then
526
526
  say " [ok] .atlas/VERSION 装配版本戳(${OUT})"
527
+ # RFC-20261023-02 ②(2026-10-23):装配基线 manifest 必须与戳同点落下——落戳成功而
528
+ # manifest 缺失 = 逐文件对账面不存在,按装配失败处理(硬错,与 F37 同款理由)。
529
+ if [ -s "$TARGET/.atlas/MANIFEST.json" ]; then
530
+ say " [ok] .atlas/MANIFEST.json 装配基线(逐文件摘要)"
531
+ else
532
+ say " [FAIL] 装配基线 manifest 未落下(.atlas/MANIFEST.json)⇒ 装配态无法逐文件对账。"
533
+ exit 1
534
+ fi
527
535
  else
528
536
  # 硬错而非 WARN(F37,2026-10-15):落戳失败 = 装配集为空(摘要算不出来)或脚本缺失。
529
537
  # 「装配 0 文件」不得被记成一次合法装配,且 WARN 会让后续步骤照常往下走。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-project-harness",
3
- "version": "0.2.0-beta.5",
3
+ "version": "0.2.0-beta.7",
4
4
  "description": "践行 Project Harness 的全流程 AI 编程工作流治理引擎",
5
5
  "bin": {
6
6
  "atlas": "bin/atlas.js"
@@ -501,7 +501,7 @@ testid:
501
501
  - **断言超时下限(硬)**(2026-10-17,issue #28 / `DESIGN.md` D105):生成的运行器配置**必须**声明 `expect.timeout`(缺省 **20s**,`ATLAS_EXPECT_TIMEOUT` 可覆盖)。理由:Playwright 的 `expect` 默认只等 **5s**,而 SPA 首屏(dev bundle 可达 10MB+,现场实测 14s)会让「元素还没渲染」被误判成「元素不存在」= **假红**(本轮实测:导航 URL 全对、元素真在,却判 3 failed)。**它不是「固定等待」**(§5.5 的禁令指 `sleep`):`expect` 是轮询式 auto-wait,本条只抬**上限**。
502
502
  - **等待超时下限(硬)**(2026-10-19,`DESIGN.md` D118;v2 真跑现场驱动):生成物里的 `ATLAS_WAIT_TIMEOUT_MS`(**页面就绪锚点** / `waitFor` / `download` / `select` 就绪轮询**共用同一个常量**)**缺省 20s**、且**必须留环境覆盖口** `ATLAS_WAIT_TIMEOUT_MS`。理由与上一条同源:该应用首屏实测 **14,184ms**,而此常量原**写死 10s、无覆盖口** ⇒ 同一份生成物里「等首屏」比「等断言」还短 ⇒ 冷启 / 负载下**间歇性假红**(v2 现场:整份首跑 9 passed / **1 failed**,红的那条倒在锚点 10s 超时 `元素齐全守卫:声明的 testid 无一在场`;同口径探针实测该元素 **2.2~2.5s** 出现且 `disabled=true` + 合规 `aria-label` 全对;复跑 `--repeat-each=3` ⇒ 3/3 绿、全量 **28 passed / 0 failed / 0 flaky**)。**它不是「固定等待」**(只抬上限,轮询语义不变);**它仍会咬人**:`ATLAS_WAIT_TIMEOUT_MS=200` 跑同一条用例必红(现场实测:同一页同一 URL 各轮渲染耗时在 **<1.5s ~ 2.2s** 之间波动,故取远低于它的值;实测 `=1500` 那轮碰巧绿 —— **这种逐轮波动正是 10s 写死会成为枚硬币投掷的原因**)。
503
503
  - **refresh 落盘节拍(`ATLAS_REFRESH_SETTLE`,2026-10-18 补,`DESIGN.md` D111;v3 现场实证移植批)**:`refresh` 动词发射**先等后刷**两行——`await page.waitForTimeout(Number(process.env.ATLAS_REFRESH_SETTLE || "0"));` 先于 `await page.reload();`;接续在 `rings/e2e/reference.md` §5.5 动词表 `refresh` 行、发射面见 `tests/test_gen_e2e_scripts.py` 顺序钉。三条口径:① **默认 0 ⇒ 生成物行为与既有契约一致**(毫秒数属项目侧**运行时**前提,运行时设 `ATLAS_REFRESH_SETTLE=<毫秒>`;零业务常数进生成器,对齐 F11 `ATLAS_SLOWMO` 注入先例与 §5.5 的 `ATLAS_EXPECT_TIMEOUT` 同族);② 它**不是 §5.5「固定 sleep」禁令所指**——该禁令禁的是**用例侧**把共享靶场节拍写死进用例;本行是执行**环境配置**面,不进用例正文;③ 单一注入面 = 生成物字面内联(`process.env`),无生成期开关/无覆盖口。
504
- - **失败留证(硬,失败才落盘)**(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence`;独立评审两轮 + 维护者拍板「按推荐继续」,源自 OpenHands `browser record` 机制级审查):生成的运行器配置(两轨)**必须**补 `trace: 'retain-on-failure'` + `screenshot: 'only-on-failure'`——失败现场自动留 Playwright trace(DOM 快照 + 网络 + 操作日志,trace viewer 可逐帧回放)与截图,落 Playwright 缺省 `test-results/`;**全绿轮零新增构件**;`video` 缓行(trace 就是回放器,粗粒度投影无收益)。三条口径:① **不溯及已装配副本**(`playwright.config.ts` 由装配的生成器发射 ⇒ 本能力的**咬合点 = 下一次 `install.sh --update` / 首装重装配**;重装配前「契约已声明留证而现场无 trace」属**如实态、非缺陷**,巡检不得误报——对齐 `shared/single-source.md` §3 迁移仪式「新源生效 ⇔ 旧源显式失效」);② **留证含业务内容,只落采纳仓本地**(`test-results/`),不进 atlas 上报通道(§15 零业务上下文出站面不受牵连);**例外(2026-10-23 D142)**:含 `request` 断言的用例默认 `trace: off`(trace 会保存请求体,与 request 的「载荷不落盘」承诺冲突),`ATLAS_REQUEST_TRACE=1` 才恢复 `retain-on-failure`;③ **它不解决 D103 盲区**(人工会话取出类的「CI 不可复现」真身是登录态,不是取证手段——trace 只重放**已发生**的失败)。**判据接线(动机)**:`duty-review/bugs.md` 未解观察(hover 禁用按钮首跑偶发 `Element is not visible`)的升判条件「复现 ≥2 次且能给出固定节拍证据(trace / 连续截图)」由此取得**常驻证据源**,缺陷调查不再依赖落 `/tmp` 的临时探针(不可复算,D98 / 案 6 两次付费先例)。钉 3 条(双轨同构 / 值形态封闭 / 无覆盖口)+ 变异 **MB10**(摘任一轨 trace 键)/ **MB11**(值改 `on`),四段协议含注入 diff、还原逐字节一致;证据与升问题通见门行:`shared/gates.md` 2.2「E2E 真跑」。
504
+ - **失败留证(硬,失败才落盘)**(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence`;独立评审两轮 + 维护者拍板「按推荐继续」,源自 OpenHands `browser record` 机制级审查;**落点勘误 + 留存窗口 2026-10-25 真跑日实咬补定**):生成的运行器配置(两轨)**必须**补 `trace: 'retain-on-failure'` + `screenshot: 'only-on-failure'`——失败现场自动留 Playwright trace(DOM 快照 + 网络 + 操作日志,trace viewer 可逐帧回放)与截图,落 **`.atlas-run/`**(生成物显式 `outputDir` 覆盖 Playwright 缺省 `test-results/`——**D108 首版契约误写缺省目录,2026-10-25 实咬勘误**,构件形态 = `.atlas-run/<用例目录>/trace.zip + test-failed-*.png` 成对);**全绿轮零新增构件**;**留存窗口 = 到下一次 `playwright test` 起跑**(Playwright 启动即清空 outputDir——「首跑红→复跑绿」场景复跑启动瞬间旧构件蒸发,跨轮取证须在起跑前归档;观察与升格条件见 `duty-review/bugs.md` E2E 段);`video` 缓行(trace 就是回放器,粗粒度投影无收益)。三条口径:① **不溯及已装配副本**(`playwright.config.ts` 由装配的生成器发射 ⇒ 本能力的**咬合点 = 下一次 `install.sh --update` / 首装重装配**;重装配前「契约已声明留证而现场无 trace」属**如实态、非缺陷**,巡检不得误报——对齐 `shared/single-source.md` §3 迁移仪式「新源生效 ⇔ 旧源显式失效」);② **留证含业务内容,只落采纳仓本地**(`.atlas-run/`),不进 atlas 上报通道(§15 零业务上下文出站面不受牵连);**例外(2026-10-23 D142)**:含 `request` 断言的用例默认 `trace: off`(trace 会保存请求体,与 request 的「载荷不落盘」承诺冲突),`ATLAS_REQUEST_TRACE=1` 才恢复 `retain-on-failure`;③ **它不解决 D103 盲区**(人工会话取出类的「CI 不可复现」真身是登录态,不是取证手段——trace 只重放**已发生**的失败)。**判据接线(动机)**:`duty-review/bugs.md` 未解观察(hover 禁用按钮首跑偶发 `Element is not visible`)的升判条件「复现 ≥2 次且能给出固定节拍证据(trace / 连续截图)」由此取得**常驻证据源**,缺陷调查不再依赖落 `/tmp` 的临时探针(不可复算,D98 / 案 6 两次付费先例)。钉 3 条(双轨同构 / 值形态封闭 / 无覆盖口)+ 变异 **MB10**(摘任一轨 trace 键)/ **MB11**(值改 `on`),四段协议含注入 diff、还原逐字节一致;证据与升问题通见门行:`shared/gates.md` 2.2「E2E 真跑」。
505
505
  - **零步探针的缺席语义(硬)**(2026-10-17,issue #30 / `DESIGN.md` D107):基线探针**必须在元素缺席时立即判「不成立」**(`count() > 0` 或经 `tsVis` / `atlasValueOf`),**不得调用会等挂载的 actionability API 裸用**(`getAttribute` / `isDisabled` / `isChecked` / `inputValue` 直接挂在 locator 上且无 `count()` 前置)——带参页面上基线本就探不到元素,等挂载会把 `null = 不成立`(§5.5)变成「挂到 test 超时」。
506
506
  - **宿主注入型 base query 的转发(硬)**(2026-10-17,issue #27 / `DESIGN.md` D104):`e2e.app_base_url` 携带的 **query 是宿主注入型前提**(现场:应用只从 `window.location.search` 读 tenant,无 sessionStorage 回落,见 `DESIGN.md` D104)——`会话:` 通道写的是 sessionStorage(`D89`)、**送不到**这类消费点;而 `goto` 自带 `?` 在 `D109` 之前一律 FAIL(§5.7)。故由**生成物负责把它带到每一跳**:三处导航发射面(`step` 的 `goto`、元素齐全守卫、零步基线 `data.url`)在**生成期**把 base 的 query 并进目标路径(插在 path 与 hash 之间;实现单源 = `_support/url_merge.py`)。三条口径:① **base 无 query ⇒ 生成物逐字节零变化**(`生成物最新` 门看守派生件,钉见 `tests/test_gen_e2e_scripts.py`);② **`ATLAS_BASE_URL` 只覆盖 host、不覆盖 base 的 query**(换 tenant 请改 `stack-profile.yaml`);③ **边界(明示,不假装支持)**:base query 一旦成为前提,就**无法用同一份用例表达「它缺席时」的行为**——`会话:`(sessionStorage)与 `auth: none`(storageState)**都不在同一消费面**、不构成逃逸口;该场景当前**超出本契约范围**,需要时另起决策行(新增 opt-out 键 = 触 `D99` 字段集冻结)。**消费者侧**:控制台手工项「打开」的地址构造同样走该口径(`e2e_console.manual_open_url`;旧实现 `base + path` 在 base 带 query 时产畸形 URL)。④ **D109 追加(2026-10-18)**:**用例级**宿主注入参数走 §5.5 `参数:` + 内联 `goto` 查询串,与 base 的 query **求并集**(base 在前、**同键 ⇒ 用例胜**);**守卫导航同样带**(取该用例首个带参 `goto` 的查询串);**零步基线不施加**;本函数原本的「目标自带 `?` ⇒ 原样返回」暗位随之**转正为并集档**(既有生成物无一带 query 的 `goto` ⇒ 零回归)。
507
507
  - **运行器前置(硬)**(issue #24 / `DESIGN.md` D101):生成物 import `@playwright/test` ⇒ **从 `e2e.scripts_dir` 出发必须可解析**(判据 = 依赖可导入性,不按路径存在;与门同口径)。生成器**不校正**它(生成期代表不了运行机器),由消费者在**起跑前**判:`atlas_check`「E2E 真跑」门不成立 ⇒ `FAIL` 点名;控制台不成立 ⇒ 诚实拒绝,不起注定失败的子进程。**全局兜底**(本机只 `npm i -g` 了 playwright)⇒ 执行器在 Node 的 `node_modules` 搜索路径上补**幂等软链**并复检,且把「用了全局什么」输出(门侧同步降 `WARN`——跑得起来,但结论依赖机器全局状态);链接位被非本工具占用 ⇒ **拒绝覆盖**。`NODE_PATH` 保留(CJS 项目的真通道)但**不作为判据**:ESM 项目下它无效(Node 的 ESM 解析器不读它)。
@@ -642,7 +642,7 @@ testid:
642
642
  2. **反偏见**:绝不改用例迁就实现;绝不无根因删用例;**绝不用 `skipped` 绕过失败**;绝不弱化 `expected` 求绿;改用例必须写明理由(AC 变了 / 假设错了 / 非确定)。
643
643
  3. **去重 3-Gate**:判重必须过「同目标 / 同意图 / 同层」三关并附证据;文件名或标题相似不算证据。
644
644
  4. **完成门**:需求相关用例在真实应用上全 `green`,或标 `blocked` 并写明原因与去向。
645
- 5. **手工项盘点(`RFC-20261015-01` / issue #10,2026-10-16;D143 2026-10-23 结构化)**:分片与索引页面表就绪后,识别**机器覆盖不到的人工作业**(控制台盖不住的项)⇒ 落 `product/e2e/manual.md`:旧行 `- <ID> | <页面> | <标题> | <操作说明>` 继续可用;新项可用 `## <ID> <标题>` + `atlas-manual` 块(`page / precondition / step / evidence`,`step` 每行 `操作 | 观察 | 命令(可空)`),id/title 只从标题行取。评估了却没有 ⇒ 域收口留痕 README 写「已评估:0」;**没盘点过** = §5.4 留痕缺该行,采用者由此看到「忘了」。**验收走查现场**新识别的手工项 ⇒ 授权**人侧当场直写** `manual.md`(格式同 §10.1),下场 e2e 报告披露增量;不为此新增设施(控制台的唯一写盘点仍是账本 + 索引,§10.1)。
645
+ 5. **手工项盘点(`RFC-20261015-01` / issue #10,2026-10-16;D143 2026-10-23 结构化)**:分片与索引页面表就绪后,识别**机器覆盖不到的人工作业**(控制台盖不住的项)⇒ 落 `product/e2e/manual.md`:旧行 `- <ID> | <页面> | <标题> | <操作说明>` 继续可用;新项可用 `## <ID> <标题>` + `atlas-manual` 块(`page / precondition / step / evidence`,`step` 每行 `操作 | 观察 | 命令(可空)`),id/title 只从标题行取。**观察锚写作约定(2026-10-24,设计优化 v2 WS-D;非字段、不进校验器)**:`step` 行「观察」列建议首 token 写产物锚 `→<用例ID> S<步骤号>`(如 `→E2E-APP-WORKFLOW-DETAIL-001 S3`),把人侧反馈钉到产物坐标(「点哪改哪」的文本化);升格为机器字段的前提 = 「观察项引用错位」现场实证 ≥2 例(QUEUE 观察室,届时走独立决策行)。评估了却没有 ⇒ 域收口留痕 README 写「已评估:0」;**没盘点过** = §5.4 留痕缺该行,采用者由此看到「忘了」。**验收走查现场**新识别的手工项 ⇒ 授权**人侧当场直写** `manual.md`(格式同 §10.1),下场 e2e 报告披露增量;不为此新增设施(控制台的唯一写盘点仍是账本 + 索引,§10.1)。
646
646
 
647
647
  ---
648
648
 
@@ -650,6 +650,8 @@ testid:
650
650
 
651
651
  「把用例逐条执行 + 逐条人工评审」的载体**随包分发**,不再由项目自建(原为项目私有产物,见已决 `3509 §B16`)。
652
652
 
653
+ **显式「无」通则(2026-10-23,`RFC-20261023-02` ④)**:`manual.md` 缺失时控制台披露「未创建」是本通则的既有先例(D81 / M73);可无内容不得留空白,统一语义词表与守恒钉归属 = `shared/global-rules.md` §5。本环不重复定义词表。
654
+
653
655
  | 面 | 内容 |
654
656
  |---|---|
655
657
  | 形态 | 本地**纯标准库**小服务(`http.server` + SSE),一张 4 列的列:用例分类 / 用例描述 / 执行用例 / 人工 review 用例;**手工走查项**(2026-10-01,`3508 §171`)并入同一列表——类型 `manual`,数据源 = `product/e2e/manual.md`(旧行 `- <ID> | <页面> | <标题> | <操作说明>` 与 D143 `atlas-manual` 结构化块共存;块按步渲染、命令可复制,账本仍为用例级;文件缺失 = 空清单**且控制台显式披露「未创建」**——不静默空栏,「没有」与「忘了」不得同形,`RFC-20261015-01`;不走用例契约面,不进索引/生成器/统计),按钮 =「打开」:按**索引页面表**路由开系统浏览器到对应页、其余人工自己点,review **直勾**(它的执行就是人自己点,无执行解锁前提);提交/账本与用例同流(索引回写天然跳过非 E2E 行) |
@@ -81,7 +81,7 @@ product/prd/
81
81
  | 八 | 非功能需求 | 性能 / 安全 / 兼容性 / 可维护性 |
82
82
  | 九 | 术语表 | 术语 + 定义(跨域共享的才写这里) |
83
83
  | 十 | 风险与依赖 | 风险(描述/影响/应对)+ 外部依赖 |
84
- | 十一 | 修订记录 | 版本 / 日期 / 修订人 / 变更说明。**不设修订记录的产品**写一行 `不适用(理由:<一句>)` 即合法(与 §4「状态机」的「不适用」同源);把整节**静默删除**不合法(§10 幂等)。 |
84
+ | 十一 | 修订记录 | 版本 / 日期 / 修订人 / 变更说明。**不设修订记录的产品**写一行 `不适用(理由:<一句>)` 即合法(与 §4「状态机」的「不适用」同源);把整节**静默删除**不合法(§10 幂等)。**边界(`RFC-20261023-02` ③)**:全局本节默认 4 列**不扩**——5 列只落在域级 §七。 |
85
85
 
86
86
  **最小必填集(`增量起步` 时)**:只强制 **五 功能架构 / 六 范围与处置 / 七 页面结构全景** 三节。判据是「**有没有下游消费者**」:五 → 域级文件组织;六 → 防静默丢功能;七 → e2e 环页面表的**页清单来源**(`product/e2e/e2e-index.md` 页面表,`rings/e2e/reference.md` §3.1;E1 迁入)。其余章节在 `增量起步` 下可缺省,**但一旦存在就必须满足本节规格**。
87
87
 
@@ -142,7 +142,7 @@ product/prd/
142
142
  | 四 | 业务规则(Rule Set) | 规则 ID / 内容 / **所属功能** / 触发条件 / 优先级 / 影响范围 |
143
143
  | 五 | 验收标准 | GWT **三列**(`AC ID` | 场景 | 回指规则),每条回指规则 ID;末附**结构类 AC 清单**(见下) |
144
144
  | 六 | Assumptions & Defaults | 未证实但按此推进的假设,逐条带默认值 |
145
- | 七 | 变更记录 | 版本 / 日期 / 变更说明 |
145
+ | 七 | 变更记录 | 版本 / 日期 / **变更内容及原因** / **影响** / **结论与后续**(5 列;legacy 3 列过渡见 §7 附加机器检查) |
146
146
 
147
147
  ### 开户最小集(`origin: adopt` 的**首轮开户**;2026-09-27 补,依据 `3509 §B85`)
148
148
 
@@ -156,7 +156,7 @@ product/prd/
156
156
  | 五 验收标准 | `AC-<CODE>-NNN`(GWT + 回指规则)+ **结构类 AC 清单** | 同上;每条断言必须能指回代码事实 |
157
157
 
158
158
  - **三 功能详细说明(六件套)与 六 Assumptions & Defaults 在开户最小集里缺省**,留到该域**真的有需求**时补——那时才有依据写状态机 / 字段校验 / 六类文案 / 异常处理。测试:该域首次由 `apply <需求ID>` 增量时,把这两节一次补齐。
159
- - **缺省 ≠ 占位符**:被缺省的两节**整节不出现**,**不得**写 `TODO`、空表或「待补充」;其余各节的实质内容也不得留空。**章节号不重排**(仍用 一/二/四/五/七),以便日后补齐时编号稳定。
159
+ - **缺省 ≠ 占位符**:被缺省的两节**整节不出现**,**不得**写 `TODO`、空表或「待补充」;其余各节的实质内容也不得留空。**章节号不重排**(仍用 一/二/四/五/七),以便日后补齐时编号稳定。**可无项通则**(2026-10-23,`RFC-20261023-02` ④):允许「无」的内容一律显式 `无`(理由可选)/ 既有语义词,空白与「评估过为无」不得同形(语义词表与守恒钉归属 = `shared/global-rules.md` §5)。
160
160
  - **§五 的 AC 在开户轮是「基线登记断言」**:它断言的是该能力**当前可观察的行为**(即下一条需求要 diff 的对照基线),不是本轮新作出的验收承诺——与 §六 的 `未覆盖(沿用实然)` 同口径。
161
161
  - **「四类样例」不在本环承载(2026-09-28 移交,`3509 §B74`)**:原「开户轮按域计四类样例(正向/反向/边界/异常)」的义务由 **E2E 环的用例 `类型` 字段**承担(`rings/e2e/reference.md` §5.2);本环不再逐 AC 标样例类型,也不再要求「每个关键功能覆盖四类」。
162
162
  - **为何要这一集**(`3509 §B85` 的「先量再动手」实测):开户时若按全量写,7 域 / 24 能力 ≈ **2,006 行**,其中六件套占 **74%**,而这些内容在开户时**只能从代码反推产品意图**(属推断而非事实);开户最小集 ≈ **518 行**(**−74%**),且每一行都能指回结构事实。
@@ -389,6 +389,17 @@ product/prd/
389
389
 
390
390
  **判定**:4/4 → `PASS`;3/4 → `WARN`(列整改项);≤2 或参数 2 失败 → `FAIL`(**拒绝交付**)。
391
391
 
392
+ **附加机器检查(2026-10-23,`RFC-20261023-02` ③④;独立 check 行,不并入参数 4 聚合)**:
393
+
394
+ | 检查 | 判据 | 档位 |
395
+ |---|---|---|
396
+ | 变更记录形态 | 域级 §七 表头含 `影响`(及 `变更内容及原因` 列)⇒ 逐行检查:`影响` 单元格空 ⇒ 该行点名 FAIL;仅含 `变更说明`(legacy 3 列)⇒ WARN(**下次真实变更时迁移 5 列**,不单跑迁移轮——「只重验受影响项」同精神);§七 表头两种都不像 ⇒ FAIL;节在但无表 ⇒ WARN | FAIL / WARN |
397
+ | 可无项显式(§六) | §六 整节不出现(开户最小集)⇒ 跳过;出现且含 `无` / `不适用(理由:…)` ⇒ PASS;空表 / 空节且无显式结论 ⇒ **WARN 首版** | WARN |
398
+
399
+ - **翻转触发器(显式)**:可无项检查连续两个收口周期「真实遗漏 ≥1 且误报 0」⇒ 升 FAIL;出现误报 ⇒ 维持 WARN 并细分判据。统计落 **per-域收口留痕 README** 追加一行「可无项空表:N / 误报:N」(与「手工项」行同格式,`rings/e2e/reference.md` §10 执行纪律 5 先例),由采用项目收口评审填写。
400
+ - **边界(边界句,防执行者自行发挥)**:5 列仅**域级** §七;全局 `十一` 修订记录默认 4 列**不扩**(`不适用(理由:…)` 语义不动)。
401
+ - **词表单源**:`[必填` / `[条件必填` 等 `FORBIDDEN` 以 `shared/global-rules.md` §5 为单源,两校验器同步(守恒钉 = `test_forbidden_vocabulary_single_source`)。
402
+
392
403
  **能力覆盖的判据边界(重要)**:`变更` 形态要求「基线能力都有结论」,但基线的**能力条目没有 ID 体系**(`structure/legacy/*` 按小节组织,上一版实现更没有)⇒ 机器比对只能做到「按小节标题近似」,**误报率高**。故本条**不进校验器**,归 §8 维度 8(人工)。**落一道门之前先问「语料能不能满足它」**:`3508 §74/§75` 两次因为跳过这一步而把门落成了长期不可满足的红门。
393
404
 
394
405
  **Waste Test(原 6 条,拆成「机器」与「人工」两栏)**:
@@ -416,6 +416,22 @@ def check_drift(root: Path, rows: Row) -> None:
416
416
  duration_ms=_elapsed_ms(started))
417
417
 
418
418
 
419
+ def _parse_manifest(path: Path) -> dict | None:
420
+ """读 `.atlas/MANIFEST.json`:`{digest: str, files: {rel: sha256}}` 两键齐才认。
421
+
422
+ 形态非法返回 None(调用方据此给「形态非法」WARN,不猜、不静默)。
423
+ """
424
+ try:
425
+ data = json.loads(path.read_text(encoding="utf-8"))
426
+ except (OSError, ValueError):
427
+ return None
428
+ if (isinstance(data, dict) and isinstance(data.get("digest"), str)
429
+ and isinstance(data.get("files"), dict)
430
+ and all(isinstance(k, str) and isinstance(v, str) for k, v in data["files"].items())):
431
+ return data
432
+ return None
433
+
434
+
419
435
  def check_version(root: Path, rows: Row) -> None:
420
436
  """「装配版本」对账(gates.md §2.2,P3;2026-10-05):.atlas/VERSION 记装配时的
421
437
  源包内容摘要,与当前源包重算摘要比对——不一致 ⇒ WARN(源包已前进/落后于装配态,
@@ -428,6 +444,13 @@ def check_version(root: Path, rows: Row) -> None:
428
444
  安装的源包位于 `<prefix>/lib/node_modules/<pkg>` ⇒ 每个文件都被滤掉 ⇒ 空摘要落戳,
429
445
  而副本侧同错重算 ⇒ 两侧恒等 ⇒ 本门恒 ok。修后 `digest()` 对空装配直接抛
430
446
  `EmptyAssembly`,故该哨兵只可能对应「修复前落的戳」。
447
+
448
+ `RFC-20261023-02` ②(2026-10-23):`.atlas/MANIFEST.json` 装配基线(逐文件摘要)。
449
+ * **配对校验先行**:manifest.digest ≠ 装配戳 ⇒ 半态,退聚合口径并指名重装
450
+ (写序「先 MANIFEST 后 VERSION」保证 VERSION 在场即 manifest 已齐,B1);
451
+ * 配对成立且漂移 ⇒ **逐文件指名**(改/增/删,各 cap 10 + 计数);
452
+ * 旧装配无 manifest ⇒ 聚合口径 + ok 行尾注「正式判据未齐」(status 仍 ok);
453
+ * 两分支 WARN 文案均保留 M68 三词(`atlas upgrade` / `install.sh` / `backflow`)。
431
454
  """
432
455
  version_file = root / ".atlas" / "VERSION"
433
456
  if not version_file.is_file():
@@ -449,14 +472,14 @@ def check_version(root: Path, rows: Row) -> None:
449
472
  return
450
473
  try:
451
474
  sys.path.insert(0, str(Path(__file__).resolve().parent))
452
- from pkg_digest import EmptyAssembly, digest # noqa: E402 延迟导入:旧装配无此文件时走 SKIP
475
+ from pkg_digest import EmptyAssembly, manifest # noqa: E402 延迟导入:旧装配无此文件时走 SKIP
453
476
  except ImportError:
454
477
  rows.add("装配版本", "SKIP",
455
478
  "无 scripts/pkg_digest.py 或版本过旧(无 EmptyAssembly)——重跑 install.sh 补齐")
456
479
  return
457
480
  started = time.perf_counter()
458
481
  try:
459
- current = digest(source_dir)
482
+ current = manifest(source_dir)
460
483
  except EmptyAssembly as e:
461
484
  # 独立子句且**排在 (OSError, ValueError) 之前**:EmptyAssembly 是 ValueError 子类,
462
485
  # 并进同一个 tuple 会被当成 SKIP,而「装配集为空」绝不是合法降级。
@@ -468,12 +491,65 @@ def check_version(root: Path, rows: Row) -> None:
468
491
  duration_ms=_elapsed_ms(started))
469
492
  return
470
493
  duration_ms = _elapsed_ms(started)
471
- if current == stamped:
472
- rows.add("装配版本", "ok", f"源包与装配态一致({stamped})",
494
+
495
+ manifest_file = root / ".atlas" / "MANIFEST.json"
496
+ recorded = _parse_manifest(manifest_file) if manifest_file.is_file() else None
497
+ if manifest_file.is_file() and recorded is None:
498
+ rows.add("装配版本", "WARN",
499
+ ".atlas/MANIFEST.json 形态非法(应为 {digest, files} 两键)——"
500
+ "重跑 install.sh 补齐;源包侧 `atlas upgrade` / `install.sh` 落戳,"
501
+ "细节走 `backflow.py` 三态对账",
502
+ duration_ms=duration_ms)
503
+ return
504
+ if recorded is not None:
505
+ if recorded["digest"] != stamped:
506
+ rows.add("装配版本", "WARN",
507
+ f"manifest 与 VERSION 不配套(manifest {recorded['digest']} ≠ 装配戳 {stamped})"
508
+ "——半态装配,重跑 install.sh;`atlas upgrade` / `install.sh` 落戳后 "
509
+ "`backflow.py` 可对账细节",
510
+ duration_ms=duration_ms)
511
+ return
512
+ if current["digest"] == stamped:
513
+ rows.add("装配版本", "ok",
514
+ f"源包与装配态一致({stamped},{len(current['files'])} 文件基线齐)",
515
+ duration_ms=duration_ms)
516
+ return
517
+ old, new = recorded["files"], current["files"]
518
+ changed = sorted(k for k in set(old) & set(new) if old[k] != new[k])
519
+ added = sorted(set(new) - set(old))
520
+ removed = sorted(set(old) - set(new))
521
+
522
+ def _name(keys: list[str]) -> str:
523
+ shown = "、".join(keys[:10])
524
+ return shown + (f" 等 {len(keys)} 项" if len(keys) > 10 else "")
525
+
526
+ named = []
527
+ if changed:
528
+ named.append("改=" + _name(changed))
529
+ if added:
530
+ named.append("增=" + _name(added))
531
+ if removed:
532
+ named.append("删=" + _name(removed))
533
+ if not named:
534
+ named.append("摘要不一致但逐文件无差异(manifest 与摘要算法不同代)——重跑 install.sh")
535
+ rows.add("装配版本", "WARN",
536
+ f"源包已前进/落后于装配态(装配 {stamped} → 当前 {current['digest']}):"
537
+ f"改 {len(changed)} / 增 {len(added)} / 删 {len(removed)};" + ";".join(named)
538
+ + "。生效:npm 全局源包态 `atlas upgrade` 后在项目里跑 `atlas update`;"
539
+ "本地源仓态重跑 `install.sh`;差异明细跑 `backflow.py` 三态对账"
540
+ "(SOURCE_ONLY 拒绝护栏;D79 ⑤)",
541
+ duration_ms=duration_ms)
542
+ return
543
+
544
+ # 旧装配(VERSION 在场、manifest 缺):干净的历史语义,不新增红;聚合口径 + 补装提示。
545
+ if current["digest"] == stamped:
546
+ rows.add("装配版本", "ok",
547
+ f"源包与装配态一致({stamped});旧装配无 manifest——正式判据未齐,重跑 install.sh 补",
473
548
  duration_ms=duration_ms)
474
549
  return
475
550
  rows.add("装配版本", "WARN",
476
- f"源包已前进/落后于装配态(装配 {stamped} → 当前 {current})——"
551
+ f"源包已前进/落后于装配态(装配 {stamped} → 当前 {current['digest']});"
552
+ "旧装配无 manifest(无法逐文件指名)——重跑 install.sh 补齐。"
477
553
  "生效:npm 全局源包态 `atlas upgrade` 后在项目里跑 `atlas update`;"
478
554
  "本地源仓态重跑 `install.sh`;差异明细跑 `backflow.py` 三态对账"
479
555
  "(SOURCE_ONLY 拒绝护栏;D79 ⑤)",
@@ -33,9 +33,9 @@
33
33
  凭据只来自运行期环境变量 `ATLAS_APP_USER` / `ATLAS_APP_PASSWORD`(不读项目 `.env`、不硬编码);
34
34
  未设置 ⇒ 跳过预置 + 明确警告;换取令牌失败 ⇒ 报错中止
35
35
  (不得静默续跑——那会退化成「未登录态跑用例」的假绿)。
36
- **段缺失 = 未声明(不再是「合法降级」)**:`validate_stack_profile` 已 WARN 指名;
37
- 生成器升档点 = 下一次真跑准备日(见 `shared/gates.md`「E2E 真跑」行——届时改推
38
- `not_ready`:退出码 3 + 机读 `not_ready: "app_login"`)。
36
+ **段缺失 = 未声明(不再是「合法降级」,D80② 升档已落地 2026-10-24 / `DESIGN.md` D149)**:
37
+ `validate_stack_profile` WARN 指名在前;生成器直接推 `not_ready`:退出码 3 + 机读
38
+ `not_ready: "app_login"`(文案按 D103 指路「有门但三态都装不下 ⇒ 人工会话取出」)。
39
39
  * **网络边界打桩**(契约 §5.8,2026-10-12):`mock:` 行(用例级)在 `e2e.mock.mode: on`
40
40
  时编译为**单条** `context.route` 派发表(pathname+method 命中、query 免疫;前缀内未命中
41
41
  fail-loud abort + 用例末审计断言点名;OPTIONS 自动 204 + CORS 头);fixture 内容**生成期
@@ -51,8 +51,8 @@
51
51
  退出码(`3509 §B110`):**0** 正常 · **2** 真错(缺 profile / 动词越界 / 审查门不过…) ·
52
52
  **3** 前置未声明 —— **3 不是失败**:调用方应判「未就绪」而不是「生成失败」,
53
53
  且 `--json` 下 stdout 给出 `{"ok": false, "not_ready": <键>, …}`。
54
- 当阶段键 = `app_base_url`(地址未声明);`app_login` 未声明按 RFC-20261015-02 于
55
- 下一次真跑准备日并入同一通道(机读键 `"app_login"`,升档记录见 `shared/gates.md`「E2E 真跑」行)。
54
+ 键 = `app_base_url`(地址未声明)/ `app_login`(登录形态未声明;D80② 升档 2026-10-24 落地,
55
+ `DESIGN.md` D149,升档记录见 `shared/gates.md`「E2E 真跑」行)。
56
56
  """
57
57
  from __future__ import annotations
58
58
 
@@ -1402,14 +1402,21 @@ def main() -> int:
1402
1402
 
1403
1403
  app_login = e2e.get("app_login") or None
1404
1404
  if app_login is None:
1405
- # RFC-20261015-02(issue #11):段缺失不再是「合法降级」。当阶段(第一阶段)仍按既有
1406
- # 占位形态生成 + 警告;**升档点 = 下一次真跑准备日**(`shared/gates.md`「E2E 真跑」行):
1407
- # 届时把这里的警告改推 `NOT_READY`(机读键 `"app_login"`,message 带显式 token)。
1408
- # `validate_stack_profile` 侧已先行 WARN 指名 —— 缺口在两道机器面上都看得见。
1409
- print("WARN: stack-profile 未声明 e2e.app_login —— 将跳过登录态预置"
1410
- "(依赖预置登录态的用例会失败。有门项目请补 real 取证四值;无门项目请显式声明"
1411
- " mode: none;升档记录见 shared/gates.md「E2E 真跑」行)", file=sys.stderr)
1412
- login_mode = "real"
1405
+ # RFC-20261015-02(issue #11)/ D80② 升档落地(2026-10-24,D149,真跑日批):
1406
+ # 段缺失不再是「合法降级」——直接推 NOT_READY(退出码 3 + 机读键 "app_login",
1407
+ # 与 app_base_url 同一通道)。硬前提的输入不得靠「缺失即静默放行」吸收。
1408
+ # 文案按 D103 相抵点指路:「有门但三态都装不下」(图形验证码 / 服务端 cookie 会话 /
1409
+ # 登录响应体无令牌)⇒ 人工会话取出路径,不是「补声明」能解决的形态,不属本判据。
1410
+ msg = ("e2e.app_login 未声明(E2E 硬前提:登录形态。三选一:real 取证四值 / "
1411
+ "mode: none(靶场无门,显式声明)/ mode: fabricated(桩令牌,必填收窄 "
1412
+ "storage_key);「有门但三态都装不下」(图形验证码 / 服务端 cookie 会话 / "
1413
+ "登录响应体无令牌)⇒ 人工会话取出(rings/e2e/reference.md §7.1 / "
1414
+ "DESIGN.md D103),是描述路径、不是本判据的对象)")
1415
+ if args.json:
1416
+ print(json.dumps({"ok": False, "not_ready": "app_login", "message": msg},
1417
+ ensure_ascii=False, indent=2))
1418
+ print(f"NOT_READY: {msg}", file=sys.stderr)
1419
+ return NOT_READY
1413
1420
  else:
1414
1421
  login_mode = app_login.get("mode") or "real"
1415
1422
  if login_mode not in APP_LOGIN_MODES:
@@ -1893,22 +1900,34 @@ def main() -> int:
1893
1900
  " await el.click();\n"
1894
1901
  " const atlasPick = () => page.evaluate((want: string) => {\n"
1895
1902
  " const all = Array.from(document.querySelectorAll('[role=option],[role=menuitem],[class*=\"select-item-option\"]:not([class*=\"select-item-option-content\"]):not([class*=\"select-item-option-state\"])'));\n"
1903
+ " document.querySelectorAll('[data-atlas-pick]').forEach((e) => e.removeAttribute('data-atlas-pick'));\n"
1896
1904
  " const shape = (e: Element) => { const r = (e as HTMLElement).getBoundingClientRect(); return r.width > 0 && r.height > 0; };\n"
1897
1905
  " const nameOf = (e: Element) => (e.getAttribute('title') || (e.textContent || '').trim());\n"
1898
1906
  " const shown = all.filter(shape);\n"
1899
1907
  " let hit = shown.filter((e) => e.getAttribute('title') === want);\n"
1900
1908
  " if (hit.length !== 1) { const p = shown.filter((e) => nameOf(e).startsWith(want)); if (p.length) hit = p; }\n"
1901
1909
  " if (hit.length !== 1) { const s = shown.filter((e) => nameOf(e).includes(want)); if (s.length) hit = s; }\n"
1902
- " if (hit.length === 1) return { status: 'unique', idx: all.indexOf(hit[0]), visible: shown.map(nameOf) };\n"
1903
- " return { status: hit.length > 1 ? 'ambiguous' : 'none', idx: -1, visible: shown.map(nameOf) };\n"
1910
+ " if (hit.length !== 1) return { status: hit.length > 1 ? 'ambiguous' : 'none', token: '', visible: shown.map(nameOf) };\n"
1911
+ " const token = 'p' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);\n"
1912
+ " hit[0].setAttribute('data-atlas-pick', token);\n"
1913
+ " return { status: 'unique', token, visible: shown.map(nameOf) };\n"
1904
1914
  " }, label);\n"
1905
- " let pick: any = { status: 'none', idx: -1, visible: [] };\n"
1915
+ " let pick: any = { status: 'none', token: '', visible: [] };\n"
1906
1916
  " try {\n"
1907
1917
  " await expect.poll(async () => { pick = await atlasPick(); return pick.status; },\n"
1908
1918
  " { timeout: ATLAS_WAIT_TIMEOUT_MS }).not.toBe('none');\n"
1909
1919
  " } catch { /* 未就绪超时 ⇒ 落到下面统一抛出(带可见选项) */ }\n"
1910
1920
  " if (pick.status !== 'unique') throw new Error(`select:${tid} 的选项里找不到唯一「${label}」;可见选项=${JSON.stringify(pick.visible)}`);\n"
1911
- " await page.locator('[role=option],[role=menuitem],[class*=\"select-item-option\"]:not([class*=\"select-item-option-content\"]):not([class*=\"select-item-option-state\"])').nth(pick.idx).click();\n"
1921
+ " // D144:点击**打标的那个元素本身**,不按整页 DOM 下标点。一个面板可同时挂多个下拉\n"
1922
+ " // (关闭态的选项节点仍留在 DOM 里),下标只对采样那一刻的 DOM 有效 ⇒ 组件一重排,\n"
1923
+ " // 同一个下标就指向另一下拉的选项(v3 实测:要选「不限时间」却点中「自定义范围」的\n"
1924
+ " // 零宽 a11y 镜像节点,重试至超时)。标记随元素消失而失效 ⇒ 重新采样打标再点,限 3 次。\n"
1925
+ " let clicked = false;\n"
1926
+ " for (let retry = 0; retry < 3 && !clicked; retry++) {\n"
1927
+ " try { await page.locator(`[data-atlas-pick=\"${pick.token}\"]`).click({ timeout: 5000 }); clicked = true; }\n"
1928
+ " catch { pick = await atlasPick(); }\n"
1929
+ " }\n"
1930
+ " if (!clicked) throw new Error(`select:${tid} 的选项「${label}」点击未生效(下拉重排后命中元素不可点);可见选项=${JSON.stringify(pick.visible || [])}`);\n"
1912
1931
  "}\n"
1913
1932
  "async function atlasIntOf(loc: any): Promise<number> {\n"
1914
1933
  " // 契约 §5.5(P5,2026-10-07):`delta` 相对断言的取数 —— 元素文本中的首个整数。\n"
@@ -2095,7 +2114,8 @@ def main() -> int:
2095
2114
  " // 3 failed 1 passed)。提升到 20s(仍远小于 test 超时),并留 `ATLAS_EXPECT_TIMEOUT` 覆盖口。\n"
2096
2115
  " expect: { timeout: Number(process.env.ATLAS_EXPECT_TIMEOUT ?? 20_000) },\n"
2097
2116
  f" use: {{ baseURL: process.env.ATLAS_BASE_URL ?? '{base}',\n"
2098
- " // 失败留证(2026-10-18,RFC-20261018-01):失败才落 test-results/,全绿轮零产物;\n"
2117
+ " // 失败留证(2026-10-18,RFC-20261018-01;落点勘误 2026-10-25):失败才落 .atlas-run/(本配置 outputDir),\n"
2118
+ " // 全绿轮零产物;留存窗口 = 到下一次起跑(Playwright 启动清空 outputDir)。\n"
2099
2119
  " // 生效咬合点 = 重装配(install --update / 首装),不溯及已装配副本。\n"
2100
2120
  " trace: 'retain-on-failure',\n"
2101
2121
  " screenshot: 'only-on-failure',\n"
@@ -2130,7 +2150,8 @@ def main() -> int:
2130
2150
  " // 3 failed 1 passed)。提升到 20s(仍远小于 test 超时),并留 `ATLAS_EXPECT_TIMEOUT` 覆盖口。\n"
2131
2151
  " expect: { timeout: Number(process.env.ATLAS_EXPECT_TIMEOUT ?? 20_000) },\n"
2132
2152
  f" use: {{ baseURL: process.env.ATLAS_BASE_URL ?? '{base}', // E1 单段\n"
2133
- " // 失败留证(2026-10-18,RFC-20261018-01):失败才落 test-results/,全绿轮零产物;\n"
2153
+ " // 失败留证(2026-10-18,RFC-20261018-01;落点勘误 2026-10-25):失败才落 .atlas-run/(本配置 outputDir),\n"
2154
+ " // 全绿轮零产物;留存窗口 = 到下一次起跑(Playwright 启动清空 outputDir)。\n"
2134
2155
  " // 生效咬合点 = 重装配(install --update / 首装),不溯及已装配副本。\n"
2135
2156
  " trace: 'retain-on-failure',\n"
2136
2157
  " screenshot: 'only-on-failure',\n"