atlas-project-harness 0.2.0-beta.4 → 0.2.0-beta.6

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 (60) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/DESIGN.md +13 -1
  3. package/README.md +2 -2
  4. package/_support/__pycache__/case_block.cpython-312.pyc +0 -0
  5. package/_support/__pycache__/manual_block.cpython-312.pyc +0 -0
  6. package/_support/case_block.py +3 -3
  7. package/_support/manual_block.py +67 -0
  8. package/apply/reference.md +2 -0
  9. package/install.sh +15 -1
  10. package/package.json +1 -1
  11. package/rings/e2e/reference.md +9 -4
  12. package/rings/prd/reference.md +14 -3
  13. package/scripts/__pycache__/atlas_check.cpython-312.pyc +0 -0
  14. package/scripts/__pycache__/closeout_derive.cpython-312.pyc +0 -0
  15. package/scripts/__pycache__/e2e_console.cpython-312.pyc +0 -0
  16. package/scripts/__pycache__/gen_e2e_scripts.cpython-312.pyc +0 -0
  17. package/scripts/__pycache__/pkg_digest.cpython-312.pyc +0 -0
  18. package/scripts/__pycache__/write_claim.cpython-312.pyc +0 -0
  19. package/scripts/atlas_check.py +240 -34
  20. package/scripts/closeout_derive.py +283 -0
  21. package/scripts/e2e_console.py +4 -12
  22. package/scripts/gen_e2e_scripts.py +228 -10
  23. package/scripts/pkg_digest.py +42 -5
  24. package/scripts/write_claim.py +204 -0
  25. package/shared/closeout.md +6 -0
  26. package/shared/gates.md +15 -12
  27. package/shared/global-rules.md +8 -1
  28. package/templates/e2e/console/app.js +22 -1
  29. package/templates/e2e/console/console.css +2 -0
  30. package/templates/prd/domain.md +8 -6
  31. package/tests/__pycache__/test_atlas_check.cpython-312-pytest-9.1.1.pyc +0 -0
  32. package/tests/__pycache__/test_canonical_consistency.cpython-312-pytest-9.1.1.pyc +0 -0
  33. package/tests/__pycache__/test_closeout_derive.cpython-312-pytest-9.1.1.pyc +0 -0
  34. package/tests/__pycache__/test_e2e_console.cpython-312-pytest-9.1.1.pyc +0 -0
  35. package/tests/__pycache__/test_gen_e2e_scripts.cpython-312-pytest-9.1.1.pyc +0 -0
  36. package/tests/__pycache__/test_install_selfcheck.cpython-312-pytest-9.1.1.pyc +0 -0
  37. package/tests/__pycache__/test_tools.cpython-312-pytest-9.1.1.pyc +0 -0
  38. package/tests/__pycache__/test_validate_walkthrough.cpython-312-pytest-9.1.1.pyc +0 -0
  39. package/tests/__pycache__/test_validators.cpython-312-pytest-9.1.1.pyc +0 -0
  40. package/tests/__pycache__/test_write_claim.cpython-312-pytest-9.1.1.pyc +0 -0
  41. package/tests/test_atlas_check.py +193 -0
  42. package/tests/test_canonical_consistency.py +5 -2
  43. package/tests/test_closeout_derive.py +254 -0
  44. package/tests/test_e2e_console.py +49 -0
  45. package/tests/test_gen_e2e_scripts.py +318 -6
  46. package/tests/test_install_selfcheck.py +8 -0
  47. package/tests/test_tools.py +59 -0
  48. package/tests/test_validate_walkthrough.py +302 -0
  49. package/tests/test_validators.py +215 -0
  50. package/tests/test_write_claim.py +236 -0
  51. package/validators/__pycache__/validate_e2e_index.cpython-312.pyc +0 -0
  52. package/validators/__pycache__/validate_prd.cpython-312.pyc +0 -0
  53. package/validators/__pycache__/validate_structure.cpython-312.pyc +0 -0
  54. package/validators/__pycache__/validate_testids.cpython-312.pyc +0 -0
  55. package/validators/__pycache__/validate_walkthrough.cpython-312.pyc +0 -0
  56. package/validators/validate_e2e_index.py +53 -2
  57. package/validators/validate_prd.py +83 -1
  58. package/validators/validate_structure.py +2 -1
  59. package/validators/validate_testids.py +6 -6
  60. package/validators/validate_walkthrough.py +288 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,56 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## [v0.2.0-beta.6] - 2026-10-23
8
+
9
+ ### 🧱 装配基线 manifest:漂移逐文件指名(D145)
10
+
11
+ * **能力**:`.atlas/MANIFEST.json` 逐文件摘要(`{digest, files}`),与 `.atlas/VERSION` 同点、**同一次遍历**产出;写序「先 MANIFEST 后 VERSION」⇒ VERSION 在场即基线已齐(半态可判定)。`check_version` 先做 manifest↔戳配对校验,配对后漂移**逐文件指名**(改/增/删各 cap 10 + 计数);旧装配无 manifest ⇒ 聚合口径 + ok 行尾注「正式判据未齐」,不新增红;`install.sh` 落戳后硬校验 manifest 在场。
12
+ * **口径**:判定「装配态一致」= 摘要相等 ∧ manifest 完整;未逐项解释的差异(`backflow.py` 三态)只能记「不一致(已解释)」,运行时仍 WARN(漂移是常态信号)。
13
+ * **钉**:manifest 形态/配对/空装配 + 逐文件指名 / cap 10 / 不配套 / 形态非法 / 旧装配尾注 + F54 干跑扩 manifest 断言;变异 **M-new-1/4/5**。
14
+
15
+ ### 📋 域级 PRD 变更记录 5 列 + 「只重验受影响项」通则(D146)
16
+
17
+ * **契约**:§七 表头升级为 `版本/日期/变更内容及原因/影响/结论与后续`;「影响」点名受影响域/AC/用例,未触达验证面写 `无(未触达验证面)`。validator 独立 check 行:5 列 PASS / legacy 3 列 WARN(下次真实变更时随迁,不单跑迁移轮)/ 表头不可辨或「影响」空 ⇒ FAIL / 节在无表 WARN。
18
+ * **通则**:`global-rules` §16「变更后只重验受影响项;未受影响结论沿用并留痕」,机器落点指认 D128 stale / `backflow` 三态 / apply §3.2,不新增门;全局 `十一` 修订记录 4 列不扩(边界句)。
19
+ * **钉**:改变更记录四态;变异 **M-new-3**。
20
+
21
+ ### 🕳 显式「无」通则 + `FORBIDDEN` 单源(D147)
22
+
23
+ * **契约**:可无内容不得留空白,`无`(理由可选)或既有语义词(`不适用(理由:…)` / `未覆盖(沿用实然)` / 控制台「未创建」);词表单源 = `global-rules` §5,`FORBIDDEN` 增补 `[必填` / `[条件必填` 并三处同步 + 守恒钉。
24
+ * **机器检查(首版 WARN)**:域级 §六 空表/空节无显式结论 ⇒ WARN(翻转触发器:连续两收口周期真实遗漏 ≥1 且误报 0 ⇒ 升 FAIL;统计落 per-域收口留痕 README「可无项空表:N / 误报:N」);§七「影响」空白 ⇒ FAIL。
25
+ * **钉**:§六 四态 + 词表守恒;变异 **M-new-2**。
26
+
27
+ ### 🐛 E2E `select` 点中错误选项(D144)
28
+
29
+ * **病灶**:`atlasSelect` 采样后按**整页 DOM 下标**再点击。一个面板同时挂多个下拉(关闭态节点仍在 DOM、rc-select 另有零宽 `role=option` 镜像),组件重排后同一数字指向别当下拉的选项 ⇒ 选「不限时间」却点中「自定义范围」镜像节点,重试至 30s 超时。全量 41 条 2/2 红、单跑 2/2 绿(顺序依赖的假绿)。
30
+ * **修复**:唯一命中改为给该元素打一次性标记,点击锚定标记本身(走 Playwright actionability);每次采样先清遗留标记 ⇒ 选择器无歧义;标记失效 ⇒ 重新采样打标再点,有界 3 次,仍不成则带可见选项响亮失败。轮询、三级降级、歧义即拒、原生 `<select>` 分支不变。
31
+ * **钉**:`tests/test_gen_e2e_scripts.py::test_select_clicks_the_marked_option_not_a_global_index`;既有 D112/D117 两条钉按新形态更新(返回 token 而非 idx、候选选择器出现次数 2 → 1)。
32
+
33
+ ---
34
+
35
+ ## [v0.2.0-beta.5] - 2026-10-23
36
+
37
+ ### ✨ E2E `request` 请求载荷断言(D142)
38
+
39
+ * **能力**:`request <api> <json-path> <expected>`,断言某 step 后**新出现**的匹配请求;api 带点后缀(歧义拒绝),path 支持点号/`[N]`/`[key=value]`,值分 `(缺席)` / `null` / `""` 三态;无新请求、中间路径缺失、过滤不唯一、越界、解析失败均 fail-loud。
40
+ * **隐私**:含 request 断言的用例默认 `trace: off`(`ATLAS_REQUEST_TRACE=1` opt-in),失败消息不打印载荷;零步基线、testid 提取均豁免 api 后缀。
41
+ * **钉**:生成器动作锚定/trace off/零变化、Node 真求值三态与新请求、校验器正反例;变异 **M145–M147**。
42
+
43
+ ### 🧭 人工走查项结构化块(D143)
44
+
45
+ * **能力**:`manual.md` 兼容旧单行;新增 `## <ID> <标题>` + `atlas-manual` 块(`page / precondition / step / evidence`),步骤行 `操作 | 观察 | 命令(可空)`;id/title 只从标题行取。
46
+ * **控制台**:按步渲染、命令可复制;账本仍用例级。解析单源 `_support/manual_block.py`,旧行零迁移。
47
+ * **钉**:控制台解析/旧行兼容/块内 id 拒绝、校验器页面/步骤/重复 ID;变异 **M148**。
48
+
49
+ ### 🩹 `after:<N>` 步骤锚错位(D136)
50
+
51
+ * **假红修复**:`refresh` 发射「先等后刷」两个动作槽位,而锚点按动作下标挂载 ⇒ 从 `refresh` 起 `after:N` 整体前移一格(实测 `after:9` 落在「重新打开面板」之前,面板未开时必红)。
52
+ * **定案**:`refresh` 两行合并为一个动作槽位,动作槽位与 step 严格 1:1,生成物逐行内容不变。
53
+ * **钉**:`test_after_anchor_stays_aligned_when_step_emits_multiple_lines`。
54
+
55
+ ---
56
+
7
57
  ## [v0.2.0-beta.4] - 2026-10-23
8
58
 
9
59
  ### 🧷 testid 提取器容忍 `setAttribute` 尾逗号(D135)
package/DESIGN.md CHANGED
@@ -427,7 +427,7 @@ adapters:
427
427
 
428
428
  默认在 atlas 各环与 `apply` 时跑;**可选**再加 git pre-commit 钩子做兜底。
429
429
 
430
- **实现状态**:9 个校验器(`validate_{structure,stack_profile,prd,testids,e2e_index,design_exemptions,e2e_mocks,ac_alignment,adr}.py`)+ `scripts/atlas_check.py`(门总览:校验器 / 出厂测试 / 生成物 / 真跑,**一门一行 `ok|FAIL|WARN|SKIP`**)。
430
+ **实现状态**:10 个校验器(`validate_{structure,stack_profile,prd,testids,e2e_index,design_exemptions,e2e_mocks,ac_alignment,adr,walkthrough}.py`)+ `scripts/atlas_check.py`(门总览:校验器 / 出厂测试 / 生成物 / 真跑,**一门一行 `ok|FAIL|WARN|SKIP`**)。
431
431
 
432
432
  **门总账与收口清单(2026-09-28 落地,`3509 §B112`/`§B114`)**:所有门登记在 `shared/gates.md`(含**对账平面 / 显式盲区 / 验证预算 / 假绿测试取证等级**三列)—— **不在表 = 不存在**;人侧收口步骤表 = `shared/closeout.md`(此前被多处定形引用却长期不存在)。`SKIP` 是**一等结论**(未就绪),不是通过。
433
433
 
@@ -600,8 +600,20 @@ adapters:
600
600
  | D131 | 收口契约引入 **变更风险定档、Always-Human 路径、修复验证 7 检查点与事故追问闭环**(2026-10-21;`shared/closeout.md` §2 步骤 3/4/11;`duty-review/README.md` §2;`templates/reviews/README.md`) | ① **病灶**:① 收口阶段缺乏系统性风险定档,高危底层改动(如权限、路由、CI、包依赖)容易被 Agent 自述误导而跳过严格人工亲测;② 缺陷修复任务(bug fix)常直接修改业务代码并自报已修复,缺乏「先失败后转绿」的 Red First 证明与边界/回归核对,导致同类问题反复逃逸;③ 线上/联调事故复盘停留在修代码,未追问「哪一道门本该接住」,事故没有转化为常态化回归用例或变异钉。② **定案**:① `shared/closeout.md` 补齐步骤 3「变更风险定档与人工审批」:三档风险矩阵,明确 `auth / permission / routing / CI / package deps / shared contracts` 为 Always-Human 永远人工路径,严禁 Agent 代勾放行;② 补齐步骤 4「缺陷修复验证 7 检查点」(复现明确、Red First 先失败测试、最小 diff、边界值、状态机完整、回归范围界定、闭环证据留存);③ 步骤 11 与 `duty-review/README.md` 引入事故追问机制:每个确认逃逸缺陷必须在 `bugs.md` 标注原因,并在关闭前补齐 E2E 回归用例或 M 系列变异钉;④ 提供 `templates/reviews/README.md` 代码审查指引轻量模板(≤10 行,聚焦失败特征与证据链)。③ **被否**:用文件名/正则做脆弱的机器强制路径阻断脚本(极易被重命名或参数绕过,且易对合法改动造成假阻断);由 Agent 自动签署审批(违背人类作为最终交付负责人的第一原则)。④ **钉与变异**:`test_canonical_consistency.py` 计数与跨文件一致性全量跑绿。 |
601
601
  | D132 | 新增 **E2E hash 查询参数契约**(`hash参数:` + `e2e.query_preset.hash_keys`)(2026-10-22;`rings/e2e/reference.md` §5.5/§5.7/§8.2;`shared/stack-profile.md` §2;`_support/case_block.py` / `_support/url_merge.py` / `scripts/gen_e2e_scripts.py` / `validators/validate_e2e_index.py`) | ① **病灶**:HashRouter 页面的 `useSearchParams` 只读 `#` 之后的查询段;D109 `参数:` 只覆盖 `#` 之前的常规查询,`会话:` 是 sessionStorage,导致快照页这类「`#` 后查询前提」用例无合法写法,只能手补生成物(重跑即丢)。② **定案**:`hash参数:` 只声明键,值内联在 `goto` 的 hash 查询串;白名单 `query_preset.hash_keys`;守卫导航把同一段 hash 查询追加到 hash 段(`append_hash_query`),base query 仍在 `#` 前;零步基线不施加;路径门两侧 `strip_queries` 后比对;同键不得跨 `参数:`/`会话:`/`hash参数:` 双写。③ **被否**:把 `#` 后查询硬塞进 `参数:`(会丢 hash 段 / 与 base query 合并语义冲突);项目侧手补生成物(不可重生成,属已否的 D98 类补丁)。④ **钉**:生成器 6 条 + 校验器 7 条(含 `test_hash_param_carried_to_guard_and_goto`、`test_zero_step_baseline_carries_no_hash_param`、`test_hash_query_merge_helpers` 与正向/未白名单/白名单外/死声明/值形态/未声明 hash 键/双载体);`tests/test_validators.py` 计数 138 已登记 `shared/gates.md`。 |
602
602
  | D133 | `value` 断言/探针改读**可见文本**,排除触发器内的视觉隐藏 a11y 镜像(2026-10-23;`scripts/gen_e2e_scripts.py` 发射点;契约 §5.5 `value` 行) | ① **病灶**:`value` 对非原生控件用 `innerText` 读触发器文本。Ant Design Select 在用户交互后向触发器内注入 `aria-live="polite"` 的无障碍播报 span(`width:0;height:0;opacity:0`),其文本与选中项相同;`innerText` 不排除视觉隐藏节点 ⇒ 选中文案被读两遍(现场:`模式一…\n模式一…`),同一标签的 `value` 断言在交互后必红,而交互前同断言正常(基线 green / 动作后 red 的假红)。契约本体写的是「触发器**可见**文本」,实现与契约不符。② **定案**:新增发射期助手 `atlasVisibleText(loc)`:采值前遍历触发器子树,用 `checkVisibility({opacityProperty:true,visibilityProperty:true})`(缺 API 时退 `getComputedStyle`)判定,把不可见节点的 `style` 暂存后置 `display:none !important`,读 `innerText` 后**按逆序逐字节恢复 style**(`try/finally` 覆盖异常路径),保留 `innerText` 的排版归一半径;`atlasValueOf`(探针,含三态语义)与 `atlasExpectValue`(正式断言,改用 `expect.poll` 保留轮询)统一走该助手;原生 `input/select/textarea` 的 `toHaveValue` 路径不变。③ **被否**:按 `aria-live` / `aria-hidden` 选择器硬排除(只治 Ant Design 一处,其他组件库的视觉隐藏节点仍会污染;`aria-hidden` 还可能承载可见文本);读 `textContent`(会把 `display:none` 的隐藏下拉项也算进来,扩大假绿面);对读数做去重(同一文案合法重复出现时无法区分,属语义猜测)。④ **钉与变异**:`tests/test_gen_e2e_scripts.py::test_value_reads_visible_text_excluding_a11y_mirror` —— 从生成物抽出助手源码,在 Node 里以假 DOM **真求值**:可见项 + `opacity:0` 的同文案播报项 ⇒ 只返回一次;采值后隐藏节点 `style` 恢复为 `null`;同时钉发射面含 `atlasVisibleText`、不再出现 `toHaveText(v, { useInnerText: true })`。 |
603
+ | D136 | `after:<N>` 步骤锚按 **step** 计数,生成器内部 action 槽位必须与 step 一一对应(2026-10-23;`scripts/gen_e2e_scripts.py` 组装段;契约 §5.5 步骤锚行) | ① **病灶**:`after:<N>` 的语义是「第 N 条 step 之后」(契约 §5.5),生成器按 `actions` 列表下标挂锚,却假定一个 step 产出一个 action 元素。`refresh` 为「先等后刷」发射了**两个** action 元素 ⇒ 从 refresh 起每个后续 step 的锚点整体前移一格:现场用例 10 条 step,`after:9`(刷新后重新打开面板)实际落在「点击节点」之前,面板未刷新打开时断言必红;`after:10` 落在第 9 步之后,断言对象错位。用例越靠后错位越大,且只有「锚点落在动作语义分界上」时才暴露。② **定案**:`refresh` 的两行合并为一个 action 元素(内部换行 + 自带缩进),保持 `actions` 与 `step` 严格 1:1;锚仍按 action/step 序号对位。生成物逐行内容不变(仅组装槽位变化)。③ **被否**:把锚改成按「非 refresh 步」计数(破坏契约 N = step 序号的语义);在锚点前动态重算 step 边界(依赖动作文本,脆弱且无法覆盖未来多行动作);把 `refresh` 回退成单行(丢失 `ATLAS_REFRESH_SETTLE` 的落库等待窗,D111 已否决)。④ **钉**:`tests/test_gen_e2e_scripts.py::test_after_anchor_stays_aligned_when_step_emits_multiple_lines` —— 构造 `refresh` + `click` 两步,断言生成顺序为 `reload → after:1 → click → after:2`;修复前 `after:1` 落到 reload 之前即红。**(编号说明:D134 已被 `type` 动词占用、D135 为 testid 尾逗号,本决策顺延为 D136。)** |
603
604
  | D134 | 新增 E2E **`type` 逐字输入动词**(2026-10-23;契约 §5.5;`scripts/gen_e2e_scripts.py` / `validators/validate_e2e_index.py`) | ① **病灶**:Playwright `fill` 一次性写 DOM、只触发 `input`,不触发带掩码 / 受控输入组件的内部解析(现场:Ant Design RangePicker 日期输入,文本可见但保存 payload 无 `startTime/endTime`);逐条 `press` 每次先 focus、只发单键,无法形成连续输入链(实测同样不提交)⇒ 项目侧只能留人工或用 `_support` 钩子代输入(步骤不可见、绕过 DSL)。② **定案**:`type <tid> <text>` → `locator.pressSequentially(text)`,逐字触发 `keydown/keypress/input`,**不先清空**原值(替换语义用 `fill`);值必填,空值 ⇒ 生成器报错中止;动作词表 13 → 14,校验器词表同步,样例守恒表 +1。③ **被否**:用 N 条 `press` 拼输入(不可读且实测不提交);`_support` 钩子代输入(步骤不可见、绕过 case DSL);移除产品侧掩码(UX 变更、超出 E2E 能力面)。④ **钉**:`test_type_verb_emits_press_sequentially_without_clearing`(发射形态 + 空值中止 + `[N]` 定位)与 `test_e2e_index_type_is_action_not_assertion`(step 侧放行 / expected 侧拒收);变异 **M134**(摘生成器 `type` 分支 ⇒ 钉红)。 |
604
605
  | D135 | testid 提取器容忍 **`setAttribute` 尾逗号**(2026-10-23;`validators/validate_testids.py`;F22 扩展) | ① **病灶**:Prettier `trailingComma: all` 会把多行 `el.setAttribute("data-testid", "x")` 写成尾逗号形态(`"x",\n)`),旧正则要求 `"x"` 后紧邻 `)`,导致已实现 testid 被判缺失、双向集合 FAIL(现场:邮件面板 RangePicker 日期输入的多行 setAttribute)。② **定案**:`SETATTR_TESTID` / `SETATTR_TESTID_TPL` 在值后允许可选 `,`;与静态属性同档,不改变 counts / 页面归属。③ **被否**:项目侧改成单行调用(lint-staged 的 Prettier 仍会加回尾逗号);把 testid 挂到 RangePicker 容器(`fill` / `value` 指不到内层 input)。④ **钉**:`test_f22_imperative_testid_extraction_and_counts` 增加多行 + 尾逗号样本(Red First:修前 `notes-avatar` 缺失;修后抽取成功);变异 **M135**(去掉 `,?` ⇒ 钉红)。 |
606
+ | D137 | `atlas_check` 增加**逐门耗时**与**运行事件行**(2026-10-23;`scripts/atlas_check.py`、`tests/test_atlas_check.py`、`shared/gates.md`;P2 观测面,不参与判定) | ① **病灶**:消融首跑(`research/2026-10-23-ablation-first-run.md`)发现仓内只有失败日志与 E2E 汇总,没有逐门耗时、运行历史与「门是否真跑过」的记录 ⇒ A2「未触发」/ A4「被绕过」/ A5「预算晚」不可判,成本面 C1/C6 全空。② **定案**:`Row.add` 增可选 `duration_ms`(默认 `None`,不参与任何判据);校验器逐器计时,出厂测试 / 生成物 / 审查报告 / 串行 / 漂移 / 装配版本 / E2E 真跑按执行段计时;每轮结束向 `.atlas/runs/check-events.jsonl` **追加**一行 `{timestamp, git_head, git_dirty, ok, fast, page, ablation_id, rows:[{name,status,duration_ms}]}`;写失败只吞掉(`except Exception` + `ensure_ascii=True`,序列化/编码异常不得把检查判红);`_git_state` 的 `git_dirty` 排除 `.atlas/`(运行态产物),git 不可用为 null;`duration_ms=None` = 未计量或无可计时段(E2E 计整门 wall time,含 reset/probe);JSON 输出同步带 `duration_ms`(additive)。③ **被否**:把事件行做成硬门(观测面坏了反把检查判红);把 token 写进 Atlas(宿主无关前提,仓内拿不到);只加 E2E 耗时(静态门仍无账);改任何判据分支(本批零行为变化)。④ **钉与变异**:`tests/test_atlas_check.py` 31 → **36 条**(失败轮含耗时且追加不覆盖 / 全绿轮也写 / `runs` 被文件占用时检查仍 ok / 序列化失败不抛 / `git_dirty` 排除 `.atlas`);变异 **M136** 四段实测:基线 36 passed → 摘掉 `_write_check_event` 调用 ⇒ 2 failed / 34 passed(`test_check_events_log_records_durations_on_fail` / `test_check_events_log_written_on_ok_run`)→ 还原 sha256 一致 → 36 passed;注入 diff 见 `research/2026-10-23-ablation-p2-mutation.md`;`shared/gates.md` 的 `atlas_check` 行与 `tests/test_atlas_check.py` 计数同步。**编号说明:D136 已被 `after:<N>` 步骤锚占用,本决策顺延为 D137。** |
607
+ | D138 | 走查单确认门**部分机器化**:新增 `validators/validate_walkthrough.py`(M1,`RFC-20261022-01` 批 1)(2026-10-23;`shared/gates.md` §2.1/§2.5;`apply/reference.md` §3.1;`scripts/atlas_check.py`) | ① **病灶**:走查单确认门在门册自记 `[语义]` 且不设机器判据,而「走查节首有无 `> 已确认:<日期>` 行」是路由形(0/1 可判);事故本体 = 走查断链(`RFC-20261015-01`),两道同名走查门靠人交叉指认。② **定案**:时空域 = 只扫 `task.json.status ∈ {planning, in_progress}`(已归档不扫);第 2 问=「是」⇒ 报告引用的 `product/prd/<domain>-prd.md` 走查节须有 `> 已确认:YYYY-MM-DD` 行(缺失 / 形态非法 / PRD 不存在 ⇒ FAIL 指名域与文件);三问小节在但第 2 问不可解析 ⇒ WARN(不降 SKIP,防写歪措辞静默旁路);无 apply 记录 / 第 2 问否 / 无三问小节 / task 记录损坏 ⇒ SKIP 带原因;本器纳入 `VALIDATORS`。③ **被否**:让校验器脱离 apply 记录自判三问语义(重蹈 regex 判句意,即**三问语义机器判**);对已归档任务也扫(老记录挂永久红账);确认行**自动补写**(把凭据变成自证,锁失去意义)。④ **钉与变异**:`tests/test_validate_walkthrough.py` **19 条**(评审后 +3:未知 status 可见 SKIP / 第 2 问多行冲突 WARN / 一致多行取值);M138(摘确认行存在性判据)/ M139(不可解析分支降 SKIP)/ M140(摘时空域过滤)四段协议,报告含注入 diff、还原逐字节一致。 |
608
+ | D139 | 收口触发表**派生器**:新增 `scripts/closeout_derive.py`(M2,`RFC-20261022-01` 批 2)(2026-10-23;`shared/closeout.md` §1/步骤 1;`shared/gates.md` §2.1;`install.sh`) | ① **病灶**:`closeout.md` §1 是一张路由表(事件 → 必跑步骤),执行靠模型收口时「记得查表」;这份清单本身曾长期不存在(canonical 全仓 grep = 0 命中),说明人防成本支付不起。② **定案**:派生器只解析表、不复制表;`--diff-base` 取 `git diff` ∪ 未跟踪文件(`.gitignore` 除外);输出 stdout(不落派生文件面);触发表引用空号 / 表结构解析失败 ⇒ FAIL 响亮;告警宇宙收敛到 atlas 自有对账面,业务面只计条数;`closeout.md` 步骤 1 写「收口第一步 = closeout_derive(旧装配无此命令 ⇒ 查表照旧)」。③ **被否**:把派生结果落成**派生步骤文件**(第二真相源,会与表漂移);对业务面文件也**业务面逐条告警**(狼来了,把真信号训练成噪声,同 gates「E2E 真跑」2026-10-04 先例);让派生器替模型执行步骤(驱动权仍在模型)。④ **钉与变异**:`tests/test_closeout_derive.py` **14 条**(含非 git 短句拒绝);M141(摘告警宇宙收敛)/ M142(摘空号校验)四段协议,报告含注入 diff、还原逐字节一致;真收口对本仓 `--diff-base HEAD` 实测输出 1/2/3/5/7、零未命中告警。 |
609
+ | D140 | one-writer-per-file **写面 claim 机器面**:新增 `scripts/write_claim.py`(M3,`RFC-20261022-01` 批 3)(2026-10-23;`apply/reference.md` §5.1;`shared/gates.md` §2.2;`scripts/atlas_check.py`;`install.sh`) | ① **病灶**:`apply/reference.md` §5.1 的写面仲裁纯纪律(「冲突时停下报告,不得抢占」实测第三次才守住),`validate_e2e_index` 只能**事后**抓主表与反向视图不一致;并发写互踩的代价是返工。② **定案**:每 claim 一个原子文件 `.atlas/runs/write-claims/<hash>.json`,`os.open(O_CREAT|O_EXCL)` 天然原子——并发 take 同一文件时后者退出非零并指名在办任务;`release` 即删(幂等;`--task` 不符拒绝释放他人 claim;空串拒绝);claim 键按 NFC+casefold 归一(大小写不敏感文件系统同一物理文件只有一把锁);EEXIST 后文件消失重试一次;残留 = glob 目录,`atlas_check` 新行 `写面残留` WARN 点名;claims 是运行态,`.atlas/` 不入库 ⇒ 不进 git 历史;`validate_e2e_index` 事后兜底保留不撤;旧装配无此命令 ⇒ 纪律照旧。③ **被否**:单 JSON 台账读改写(锁本身会被并发打穿,即**单 JSON 锁**);把 claims 搬进 git 历史做事实(运行态不是事实,即**claim 入 git**);用事后一致性检查替代事前仲裁(返工已经发生)。④ **钉与变异**:`tests/test_write_claim.py` **17 条**(含 8 进程并发 take 唯一赢家、大小写变体同一把锁、EEXIST 后消失重试、空串 release 拒绝、atlas_check 行 ok/WARN 两侧);M143(摘 `O_EXCL`)/ M144(摘残留 glob)四段协议,报告含注入 diff、还原逐字节一致。 |
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
+ | 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
+ | 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** 判据恒真 ⇒ 钉红。 |
605
617
 
606
618
 
607
619
 
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.4-blue.svg" alt="Version" />
4
+ <img src="https://img.shields.io/badge/version-0.2.0--beta.6-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>
@@ -69,7 +69,7 @@ NBER(美国国家经济研究局)2026 年工作论文 35275 追踪了超过
69
69
  | :--- | :--- | :--- | :--- |
70
70
  | **CPU / 算力核心** | **LLM(大语言模型)** | Claude 3.7 / GPT-5 / GLM-5 | 负责纯粹的逻辑推理与自回归计算(纯算力输入与输出) |
71
71
  | **操作系统 / Runtime (OS)** | **Agent Harness** | Claude Code / Cursor / Pi Agent / OpenCode | **提供系统调用(Syscall)与运行环境**<br>• 文件读写、Bash 执行(工具调用)<br>• 内存与缓存管理(KV Cache 保温、上下文窗口截断)<br>• 多进程调度(Subagent 派生) |
72
- | **工程框架 / 工业级脚手架** | **Project Harness** | **Atlas** / Trellis / OpenSpec | **定义项目物理秩序与全生命周期防线**<br>• 规范与应然资产基线(PRD / AC 账本)<br>• 依赖图谱影响面基准(可选增强,advisory)<br>• 机器物理阻断门禁(防止 AI 敷衍实现与伪造测试) |
72
+ | **工程框架 / 工业级脚手架** | **Project Harness** | **Atlas** / Trellis / OpenSpec | **定义项目物理秩序与全生命周期防线**<br>• 规范与应然资产基线(PRD / AC 账本)<br>• 依赖图谱影响面基准(advisory;后端栈条件项,纯前端栈近零收益)<br>• 机器物理阻断门禁(防止 AI 敷衍实现与伪造测试) |
73
73
 
74
74
  ### 3. 为什么说 Project Harness 是 AI 时代的 Spring Boot?
75
75
 
@@ -51,14 +51,14 @@ strip_inline_comment = load_module("stack_profile").strip_inline_comment
51
51
  _BLOCK_KEYS = ("pages", "precondition", "seed", "mock", "step", "expected", "testid", "落点", "会话", "参数", "hash参数", "覆盖维度")
52
52
 
53
53
 
54
- def parse_block(body: str) -> dict:
55
- """解析 `atlas-case` 块体 → dict(键见 `_BLOCK_KEYS`)。
54
+ def parse_block(body: str, extra_keys: tuple[str, ...] = ()) -> dict:
55
+ """解析 `atlas-case` 块体 → dict(键见 `_BLOCK_KEYS`;`extra_keys` 供人工块复用同一行式解析)。
56
56
 
57
57
  语义(与原两处实现逐字一致;**唯一例外** = 键空间外的标量键剥行内注释,见模块 docstring):
58
58
  `- ` 开头的行归当前键;`<key>:` 开新键(键在键空间内);`key: value` 带值形态追加一条;
59
59
  键空间外的 `key: value` 以标量入表(**标量值剥行内 `#` 注释**)。
60
60
  """
61
- d: dict = {k: [] for k in _BLOCK_KEYS}
61
+ d: dict = {k: [] for k in _BLOCK_KEYS + tuple(extra_keys)}
62
62
  key = None
63
63
  for line in body.splitlines():
64
64
  s = line.strip()
@@ -0,0 +1,67 @@
1
+ """手工走查清单解析(D143)——旧单行格式 + 可选 `atlas-manual` 块,共用同一行式解析。
2
+
3
+ 旧行:`- <ID> | <页面> | <标题> | <操作说明>`。
4
+ 新块:`## <ID> <标题>` 后接 ```` ```atlas-manual ````;id/title 只从标题行取,块里只写
5
+ `page / precondition / step / evidence`。`step` 每行 `操作 | 观察 | 命令(可空)`,复用
6
+ `_support/case_block.parse_block` 的扁平数组语义,不引入第二套嵌套语法。
7
+
8
+ 纯标准库;控制台(e2e_console.py)与校验器共用本模块,避免两份解析漂移。
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import re
13
+
14
+ from _support.case_block import parse_block
15
+
16
+ _HEADING = re.compile(r"^##\s+(MANUAL-[A-Za-z0-9-]+)\s+(.+?)\s*$")
17
+ _FENCE = "```atlas-manual"
18
+
19
+
20
+ def parse_manual(text: str) -> list[dict]:
21
+ items: list[dict] = []
22
+ heading: tuple[str, str] | None = None
23
+ lines = text.splitlines()
24
+ i = 0
25
+ while i < len(lines):
26
+ line = lines[i]
27
+ m = _HEADING.match(line)
28
+ if m:
29
+ heading = (m.group(1), m.group(2).strip())
30
+ if line.strip().startswith(_FENCE):
31
+ body: list[str] = []
32
+ i += 1
33
+ while i < len(lines) and not lines[i].strip().startswith("```"):
34
+ body.append(lines[i])
35
+ i += 1
36
+ parsed = parse_block("\n".join(body), ("page", "evidence"))
37
+ if "id" in parsed or "title" in parsed:
38
+ raise ValueError("atlas-manual 块的 id/title 由 ## 标题行提供,块内不得再写")
39
+ page = (parsed.get("page") or [""])[0].strip() if parsed.get("page") else ""
40
+ steps: list[dict] = []
41
+ for raw in parsed.get("step") or []:
42
+ parts = [p.strip() for p in raw.split("|", 2)]
43
+ steps.append({
44
+ "action": parts[0] if parts else "",
45
+ "expect": parts[1] if len(parts) > 1 else "",
46
+ "command": parts[2] if len(parts) > 2 else "",
47
+ })
48
+ id_, title = heading or ("", "")
49
+ items.append({
50
+ "id": id_, "page": page, "title": title,
51
+ "note": f"结构化走查({len(steps)} 步)" if steps else "",
52
+ "precondition": list(parsed.get("precondition") or []),
53
+ "steps": steps,
54
+ "evidence": list(parsed.get("evidence") or []),
55
+ })
56
+ i += 1
57
+ continue
58
+ s = line.strip()
59
+ if s.startswith("- "):
60
+ parts = [x.strip() for x in s[2:].split("|")]
61
+ if len(parts) >= 4:
62
+ items.append({
63
+ "id": parts[0], "page": parts[1], "title": parts[2],
64
+ "note": " | ".join(parts[3:]),
65
+ })
66
+ i += 1
67
+ return items
@@ -76,6 +76,7 @@
76
76
  - **门是语义判断,机器判不了**(与 `§8 末注` 同源):所以要求把**判定 + 依据**写进 `atlas-apply.md`,让复核者一眼看出它有没有偷懒。
77
77
  - **E2E 首跑取证兜底(2026-10-16,`RFC-20261015-02` / issue #11)**:第 3 问判「是」且 `e2e.app_login` 仍未声明 ⇒ 向用户**追问一次**(`real` 四值 / `mode: none` / `fabricated` + `storage_key`,三选一必须显式),落 `product/stack-profile.yaml` 后再进 e2e 环。追问是 UX 加速器,不是判据——机器判据是生成器 `not_ready`(升档 = 下一次真跑准备日,`shared/gates.md`「E2E 真跑」行);adopt 开户已问过(同批「靶场接入取证」)的项目此处自然不触发。
78
78
  - **环间人工门(E1 起;2026-09-29 重议 = `3507 AQ`,B124 结论)**:两道人侧门——① **走查单确认门(实现前,硬)**:改变应然 UI 的需求,域级 PRD 的「用户旅程走查」节须用户显式确认(节首 `已确认:<日期>`;`rings/prd/reference.md` §4·八,确认前对照 `product/design.md`(存在时)视觉方向,`3507 BE`),确认前 **`apply` 不得进 `e2e` 环**;② **验收走查门(收口前)**:该域 E2E 在真实应用全 green 后,用户按域级 PRD 功能清单逐条在真实应用上过一遍(可用 + 符合已确认走查单),确认后才算域收口;不符 ⇒ 差异登记且域不收口;真实应用不可用 ⇒ 与 E2E 同为 `blocked` 显式带原因(`rings/e2e/reference.md` §5.4 末)。`e2e` 用例另走独立审查门(`rings/e2e/reference.md` §12)+ 首跑分类协议(同文件 §5.4)。原型增量人工 review 门已随环退役(`3508 §92`)。
79
+ - **走查单确认门的机器面(2026-10-23,`RFC-20261022-01` M1)**:`validators/validate_walkthrough.py` 读本报告(`atlas-apply.md`)第 2 问**已写下**的判定,与 `product/prd/<domain>-prd.md` 的 `> 已确认:YYYY-MM-DD` 行对账。因此报告里第 2 问的答句必须写成「是 / 否」开头;受影响域以报告内对 `product/prd/<domain>-prd.md` 的路径引用为准(解析不到 ⇒ WARN 要求人工核对)。本器只判「写下的『是』与确认行的对账」,不判三问语义对错(那仍归人)。
79
80
  - 结构事实的**实现后**回写不属于 apply:由 `after_finish` hook(`refresh_structure.py`,事实区)与 Finish 的 `trellis-update-spec`(语义区)负责(D38)。
80
81
 
81
82
  `all` 模式的顺序固定为 **`structure → prd → e2e`**(含 structure 全量)。
@@ -194,6 +195,7 @@ apply 只对**本次受影响集合**落笔,**不整文件覆盖**任何项目
194
195
 
195
196
  - **单写者表面(硬)**:每个共享文件在任一时刻**只允许一个在办任务**持有写权。
196
197
  - **声明与分派**:并发前,每个任务必须**声明自己要写哪些项目级文件**;由用户 / 编排者按 `one writer per file` 分派;冲突时**停下报告**,不得抢占(实测第三次才守住,`3508 §55 一`)。
198
+ - **claim 钩子(2026-10-23,`RFC-20261022-01` M3)**:共享文件动手前 `python3 .atlas/scripts/write_claim.py take --file <路径> --task <需求ID>`,收口时 `release --file <路径>`。每 claim 一个原子文件(`.atlas/runs/write-claims/<hash>.json`,`O_CREAT|O_EXCL`)⇒ 并发 take 同一文件时后者**直接退出非零并指名在办任务**;未释放的 claim 由 `atlas_check` 的 `写面残留` 行 WARN 点名。**降级路径**:旧装配无此命令 ⇒ 纪律照旧(本钩子的主语义是「有命令则机器判」,安全性不押在降级上)。claims 是运行态,`.atlas/` 不入库 ⇒ 不进 git 历史;`validate_e2e_index` 的事后兜底保留不撤。
197
199
  - **降串行度的建议(非硬)**:索引类表面可**按用例 ID 分区**(或拆成每页一份的反向索引);但反向视图现在是**生成**的(`single-source` §2),所以首选是「一次只让一个任务改主表」。
198
200
  - **不属本节**:需求级文件(`.trellis/tasks/**`)天然一任务一份,无需仲裁。
199
201
 
package/install.sh CHANGED
@@ -7,7 +7,8 @@
7
7
  #
8
8
  # 装配行为:
9
9
  # 1. 整包装配到 <目标>/.atlas/(shared / rings / apply / templates / adapters /
10
- # validators / patches / tests)。.atlas/ 属包产物,默认刷新。
10
+ # validators / patches / tests)。.atlas/ 属包产物:白名单子目录默认刷新,
11
+ # `.atlas/runs/`(运行态:E2E 汇总 / check 事件 / write-claims)保留不删。
11
12
  # 2. 每个 skills/atlas-*/ 以一个瘦桩 SKILL.md 装到各平台 skill 目录。
12
13
  # 3. 清理历史遗留的单入口 skill(<平台>/skills/atlas)。
13
14
  # 4. 建 product/ 骨架(已有内容永不覆盖)。
@@ -427,7 +428,9 @@ check "校验器 validate_testids" "$ATLAS_DEST/validators/validate_testids.py"
427
428
  check "校验器 validate_e2e_index" "$ATLAS_DEST/validators/validate_e2e_index.py"
428
429
  check "校验器 validate_design_exemptions" "$ATLAS_DEST/validators/validate_design_exemptions.py"
429
430
  check "校验器 validate_e2e_mocks" "$ATLAS_DEST/validators/validate_e2e_mocks.py"
431
+ check "校验器 validate_ac_alignment" "$ATLAS_DEST/validators/validate_ac_alignment.py"
430
432
  check "校验器 validate_adr" "$ATLAS_DEST/validators/validate_adr.py"
433
+ check "校验器 validate_walkthrough" "$ATLAS_DEST/validators/validate_walkthrough.py"
431
434
  check "E2E 脚本生成器" "$ATLAS_DEST/scripts/gen_e2e_scripts.py"
432
435
  check "结构事实刷新器" "$ATLAS_DEST/scripts/refresh_structure.py"
433
436
  check "testid 薄桩生成器" "$ATLAS_DEST/scripts/gen_testid_stub.py"
@@ -441,9 +444,12 @@ check "适配器指南" "$ATLAS_DEST/adapters/README.md"
441
444
  check "图谱后端隔离层" "$ATLAS_DEST/adapters/_graph.py"
442
445
  check "影响面基准报告器" "$ATLAS_DEST/scripts/impact_report.py"
443
446
  check "回灌助手" "$ATLAS_DEST/scripts/backflow.py"
447
+ check "收口派生器" "$ATLAS_DEST/scripts/closeout_derive.py"
448
+ check "写面 claim 仲裁" "$ATLAS_DEST/scripts/write_claim.py"
444
449
  check "共享内核(模块加载器)" "$ATLAS_DEST/_support/siblings.py"
445
450
  check "共享内核(profile 解析)" "$ATLAS_DEST/_support/stack_profile.py"
446
451
  check "共享内核(用例块解析)" "$ATLAS_DEST/_support/case_block.py"
452
+ check "共享内核(手工走查块解析)" "$ATLAS_DEST/_support/manual_block.py"
447
453
  check "共享内核(用例 ID 文法)" "$ATLAS_DEST/_support/case_id.py"
448
454
  check "共享内核(node 运行器判据)" "$ATLAS_DEST/_support/pw_runner.py"
449
455
  check "共享内核(base query 转发)" "$ATLAS_DEST/_support/url_merge.py"
@@ -518,6 +524,14 @@ print(stamp_file(Path(sys.argv[1]), Path(sys.argv[2])))
518
524
  PY
519
525
  )"; then
520
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
521
535
  else
522
536
  # 硬错而非 WARN(F37,2026-10-15):落戳失败 = 装配集为空(摘要算不出来)或脚本缺失。
523
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.4",
3
+ "version": "0.2.0-beta.6",
4
4
  "description": "践行 Project Harness 的全流程 AI 编程工作流治理引擎",
5
5
  "bin": {
6
6
  "atlas": "bin/atlas.js"
@@ -244,7 +244,7 @@ testid:
244
244
  - 第二个 token = **`data-testid`**(`refresh` / `goto` 除外,见词表)。
245
245
  - 其余为**值**(可含空格;是**渲染后可见文案**或数值)。
246
246
  - `#` 之后是**中文说明**,供人阅读,**机器忽略**。
247
- - **可选步骤锚** `after:<N>`(2026-09-25 补,`3509 §B57`):写在 `expected` 行行首,表示「这条断言在分片里**第 N 条 `step` 执行完之后**立刻取样」(N 从 1 起、按 `step` 书写顺序编号)。不带锚的 `expected` 仍是**终态**断言。用途:把「先准备、再提交」型用例的**中途态**变成证据(如「先打开覆盖层再取消」里的「打开」;因为终态回到初态、本身无法作为证据)。约束:锚不得与 `initial:` / `unchanged:` 叠写;N 必须是正整数且 ≤ `step` 条数(否则**校验器 FAIL**,该项为「步骤锚合法」);带锚的断言**不属豁免通道**,必须像无前缀断言一样参与零步基线(即不得在初始态成立)。
247
+ - **可选步骤锚** `after:<N>`(2026-09-25 补,`3509 §B57`):写在 `expected` 行行首,表示「这条断言在分片里**第 N 条 `step` 执行完之后**立刻取样」(N 从 1 起、按 `step` 书写顺序编号)。不带锚的 `expected` 仍是**终态**断言。用途:把「先准备、再提交」型用例的**中途态**变成证据(如「先打开覆盖层再取消」里的「打开」;因为终态回到初态、本身无法作为证据)。约束:锚不得与 `initial:` / `unchanged:` 叠写;N 必须是正整数且 ≤ `step` 条数(否则**校验器 FAIL**,该项为「步骤锚合法」);带锚的断言**不属豁免通道**,必须像无前缀断言一样参与零步基线(即不得在初始态成立)。**发射面(2026-10-23,`DESIGN.md` D136)**:锚按 **step 序号**计数,生成器内部「动作槽位」必须与 step 严格 1:1——一条 step 发射多行语句(如 `refresh` 的先等后刷)时须合并为一个槽位,否则后续 `after:N` 整体前移(实测 `after:9` 落到「打开面板」之前,面板未开必红)。
248
248
 
249
249
  > **为何中文降为注释**(2026-09-23 定):`step` / `expected` 必须机器可解析——散文格式下机器只能抽出 testid、抽不出「做什么 / 断言什么」,生成器退化为存在性断言;**不设**「中文 `step` + 结构化 `actions`」两字段(同一信息两处正文 = 漂移,`shared/single-source.md` §2)。语义由 `## <ID> <标题>` + `intent` + 注释承载。
250
250
 
@@ -275,6 +275,7 @@ testid:
275
275
  | | `countOptions` | 该元素内的 `option` 子元素个数等于值(整数),用于「枚举选项个数」类断言;**结构动词、仅适用原生 `<select>`**(选项集合在 DOM 结构上是其子元素;trigger+listbox 组合的选项在打开前不存在于 DOM,本动词对其无语义) |
276
276
  | | `attr` | 元素属性 `<name>` 的值等于 `<value>`(**不要求可见**,与 `enabled` / `checked` 同理:属性可读性与渲染可见性正交);值段写作 `<name> <value>`,`name` 取**首个空白前**的 token、其余全部为 `value`(可含空格);属性不存在 ⇒ 取值为空 ⇒ 对**非空**期望值恒失败(2026-09-25 补,依据 `3509 §B35`)。**值写作 `""`(或 `(空)`)= 断言「该属性缺席或为空串」**(2026-10-19 补,`DESIGN.md` D116):`getAttribute` 对缺席属性返回 `null`(**不是** `""`)⇒ 发射侧一律 `?? ''` 归一后轮询比对;`toHaveAttribute(name, "")` **不适用**(实测缺席时判 FAIL = 半通状态)。该形态是**否定属性**的表达面(「该入口**不带**某标记」类 AC 的机器判据)|
277
277
  | | `delta` | **数值增量**(相对断言,2026-10-07 补,`issues` P5):值段 = 带符号整数 `±N`。语义 = 行动前采样该元素文本中的**首个整数**、行动后复读,两者差值必须恰为 `±N`。真栈单 run 累加态下「待生成 +1」类断言的合法形态 —— 绝对值断言(`text X 待生成 3 条`)在同前提的兄弟用例上只可能首跑者成立。渲染为**轮询断言**(node `expect.poll` / python 行动后复读);采样点 = **第一个提交性动作之前**(与 `unchanged:` 先例同点,准备步骤不污染基准)。**不得**与 `initial:` / `unchanged:` / `after:` 叠写、用例必须有 `step`(均 ⇒ 校验器 FAIL,生成器中止双保险);**不参与零步基线**(它没有「初始态成立 / 不成立」语义)。首例 = `E2E-PROJECTS-014`(拆分后项目级待生成递增,2026-10-07 转正) |
278
+ | | `request` | **请求载荷断言**(2026-10-23,`DESIGN.md` D142):`request <api> <json-path> <expected>`。`<api>` = `?api=` 查询值的**带点唯一后缀**(如 `app.updateApp`);多个不同 API 命中同名后缀 ⇒ FAIL。`<json-path>` = 点号键 + `[N]` + `[key=value]`(纯遍历、无 eval);`<expected>` = `(缺席)`(叶子键不存在)/ `null` / `""` / JSON 字面量;**`(空)` 不接受**(无法区分缺失与 null)。**动作锚定**:`after:N` ⇒ 第 N 步执行前记请求序号;无锚 ⇒ 最后一步前记序号;只断言该时点后**新出现**的第一条匹配请求,无新请求 ⇒ FAIL(历史请求/自动保存/重试不得满足断言)。中间路径缺失、过滤 0/多命中、数组越界、请求体解析失败 ⇒ **解析错误**(不得折算成「缺席」)。不得与 `initial:` / `unchanged:` 叠写;必须有 `step`;**不参与零步基线**。含该断言的用例默认 `trace: off`(请求体可能含敏感数据;`ATLAS_REQUEST_TRACE=1` 才保留失败 trace);失败消息只打印 api/path/期望类型与实际类型,不打印载荷。 |
278
279
 
279
280
  - **同名多实例元素序位(`[index]`,0 起始,D120 / issue #31)**:当页面存在同名 testid 的多个实例(列表行、表格行、重复项控件等)时,步骤动词与断言动词的目标 testid 均支持以 `[N]` 下标指定操作或断言第 N 项(0 为首项,1 为第二项,依此类推):
280
281
  - 示例:`click list-move-up-btn[1]`、`fill list-name-input[1] 张三`、`visible list-item[2]`、`click list-delete-btn[1] native`;
@@ -500,7 +501,7 @@ testid:
500
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,本条只抬**上限**。
501
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 写死会成为枚硬币投掷的原因**)。
502
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`),无生成期开关/无覆盖口。
503
- - **失败留证(硬,失败才落盘)**(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 零业务上下文出站面不受牵连);③ **它不解决 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` 机制级审查):生成的运行器配置(两轨)**必须**补 `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
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 超时」。
505
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` ⇒ 零回归)。
506
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 解析器不读它)。
@@ -560,9 +561,11 @@ testid:
560
561
  | `unchanged:` 用例有提交性动作 | 声明 `unchanged:` 的用例,其 `step` 里必须有 `save`/`submit`/`confirm`/`delete`/`remove`/`bind`/`cancel`/`logout` 类的提交性动作,否则「动作不改变它」无动作可验 ⇒ **FAIL**(§5.6 R9)。**只判形态**;「行动前必须成立」在**运行期**零步基线判(§6.0 `C-new`) |
561
562
  | 步骤锚合法 | `expected` 行首的 `after:<N>`(§5.5):写法必须为 `after:<正整数> `,且 N ≤ 该用例 `step` 条数;与 `initial:` / `unchanged:` **不得叠写** ⇒ 否则 **FAIL**(2026-09-25 补,`3509 §B57`) |
562
563
  | `delta` 相对断言形态 | `delta <testid> <值>`(§5.5,2026-10-07 补,P5):值必须 = 带符号整数 `±N`;与 `initial:` / `unchanged:` / `after:` **不得叠写** ⇒ 否则 **FAIL**。(delta 须有 `step` 前提由「分片块字段齐」门承接——`step` 必填非空;生成器渲染期对缺 step 的 delta 另有中止双保险。)「行动前采样 / 行动后差值」在生成器渲染期执行(不参与零步基线) |
564
+ | `request` 断言形态 | `request <api> <json-path> <expected>`(§5.5,2026-10-23 D142):api 必须为带点后缀;json-path 不得含空白;`(空)` 不接受;与 `initial:` / `unchanged:` 不得叠写;必须有 `step`(动作锚点)⇒ 否则 **FAIL** |
563
565
  | 零证据用例须挂结构类 AC | `expected` 全部由 `initial:` 承担的用例(零证据用例)**只能挂结构类 AC**(PRD 环 §五「结构类 AC 清单」)⇒ 否则 **FAIL**(2026-09-25 补,`3509 §B36` + `§B50`)。两项之外的情形均 PASS:① 非全 `initial:` 的用例(它有派生断言);② 挂了结构类 AC 的用例(它显式声明被測属性只能在初始态观测) |
564
566
  | 词表 | `类型` / `状态` ∈ §5.2;`状态原因` 前缀合法且冒号后有内容 |
565
567
  | `关联AC` | 格式 `AC-<CODE>-NNN`;域级 PRD 存在时校验该 AC 可达,门控态 N/A |
568
+ | `manual` 走查项形态 | `product/e2e/manual.md`(可选):旧行 `- <ID> \| <页面> \| <标题> \| <说明>` 与 D143 `atlas-manual` 块共存;ID 唯一、页面 ∈ 页面表、块步骤含 `操作 \| 观察`;块内再写 id/title ⇒ **FAIL**(id/title 只从 `## <ID> <标题>` 取) |
566
569
 
567
570
  **有意义门(B 档)**
568
571
 
@@ -639,7 +642,7 @@ testid:
639
642
  2. **反偏见**:绝不改用例迁就实现;绝不无根因删用例;**绝不用 `skipped` 绕过失败**;绝不弱化 `expected` 求绿;改用例必须写明理由(AC 变了 / 假设错了 / 非确定)。
640
643
  3. **去重 3-Gate**:判重必须过「同目标 / 同意图 / 同层」三关并附证据;文件名或标题相似不算证据。
641
644
  4. **完成门**:需求相关用例在真实应用上全 `green`,或标 `blocked` 并写明原因与去向。
642
- 5. **手工项盘点(`RFC-20261015-01` / issue #10,2026-10-16)**:分片与索引页面表就绪后,识别**机器覆盖不到的人工作业**(控制台盖不住的项)⇒ 落 `product/e2e/manual.md`(行式 `- <ID> | <页面> | <标题> | <操作说明>`,同 §10.1)——评估了却没有 ⇒ 域收口留痕 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 只从标题行取。评估了却没有 ⇒ 域收口留痕 README 写「已评估:0」;**没盘点过** = §5.4 留痕缺该行,采用者由此看到「忘了」。**验收走查现场**新识别的手工项 ⇒ 授权**人侧当场直写** `manual.md`(格式同 §10.1),下场 e2e 报告披露增量;不为此新增设施(控制台的唯一写盘点仍是账本 + 索引,§10.1)。
643
646
 
644
647
  ---
645
648
 
@@ -647,9 +650,11 @@ testid:
647
650
 
648
651
  「把用例逐条执行 + 逐条人工评审」的载体**随包分发**,不再由项目自建(原为项目私有产物,见已决 `3509 §B16`)。
649
652
 
653
+ **显式「无」通则(2026-10-23,`RFC-20261023-02` ④)**:`manual.md` 缺失时控制台披露「未创建」是本通则的既有先例(D81 / M73);可无内容不得留空白,统一语义词表与守恒钉归属 = `shared/global-rules.md` §5。本环不重复定义词表。
654
+
650
655
  | 面 | 内容 |
651
656
  |---|---|
652
- | 形态 | 本地**纯标准库**小服务(`http.server` + SSE),一张 4 列的列:用例分类 / 用例描述 / 执行用例 / 人工 review 用例;**手工走查项**(2026-10-01,`3508 §171`)并入同一列表——类型 `manual`,数据源 = `product/e2e/manual.md`(行式 `- <ID> | <页面> | <标题> | <操作说明>`,文件缺失 = 空清单**且控制台显式披露「未创建」**——不静默空栏,「没有」与「忘了」不得同形,`RFC-20261015-01`;不走用例契约面,不进索引/生成器/统计),按钮 =「打开」:按**索引页面表**路由开系统浏览器到对应页、其余人工自己点,review **直勾**(它的执行就是人自己点,无执行解锁前提);提交/账本与用例同流(索引回写天然跳过非 E2E 行) |
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 行) |
653
658
  | **手工项****产出方**(2026-10-16,`RFC-20261015-01`)| 正文产出 = **e2e 环**(页面表就绪后按 §10 执行纪律 5 盘点落项);**验收走查现场**新识别的项授权人侧当场直写(控制台写盘点不变);`manual.md` 不存在 ⇒ 控制台显式披露(见「形态」行)。与之配对的评估留痕 = §5.4 留痕 README 的「手工项」行 |
654
659
  | **手工项写法与同步纪律(2026-10-04,`3508 §177`)** | 操作说明**写给人看**:每条 = 做什么 + 点哪里(点名控件**可见标签**)+ 应该看到什么;被点名控件的标签 / 存在性 / 流程变更 ⇒ 手工项随实现**同步改写**(与 §4 testid「改名必须同步全部引用」同族;手工面无机器门,同步靠本条 + 走查人当场发现) |
655
660
  | 启动 | `python3 .atlas/scripts/e2e_console.py --root .`(默认端口 4190;`--slowmo` / `--timeout` / `--no-head` / `--auto-close` 可选) |
@@ -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 条,拆成「机器」与「人工」两栏)**: