atlas-project-harness 0.2.0-beta.7 → 0.2.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +1 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/rings/e2e/reference.md +1 -1
- package/scripts/__pycache__/atlas_verify.cpython-312.pyc +0 -0
- package/scripts/__pycache__/pkg_digest.cpython-312.pyc +0 -0
- package/scripts/atlas_verify.py +201 -0
- package/shared/gates.md +2 -2
- package/tests/__pycache__/test_atlas_verify.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/test_atlas_verify.py +183 -0
package/DESIGN.md
CHANGED
|
@@ -616,6 +616,7 @@ adapters:
|
|
|
616
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
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
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 守。 |
|
|
619
|
+
| D150 | **验收执行器 = 执行层最小立项面 ①**(2026-10-25;`proposals/RFC-20261025-02` v2 + 评审 `REVIEW-20261025-02`;前置链 = R-1 表态 → RFC-20261025-01 前置研究(两道闸)→ 三张清单(评核过);维护者「继续」拍板进实现批,同日落刀) | ① **病灶**:真跑验收六步是 runbook 非机器行为(prepare 调用/结果汇总/留档/断点续跑靠纪律);runbook 六步中唯一无机器承载的 = 编排 + 统一报告 + 留档三件事(评审建设性判定)。② **定案**:`scripts/atlas_verify.py` 四步——pre(项目钩子 `atlas-prepare.mjs`,cwd=项目根、180s、**批级 `ATLAS_CASE_ID=verify-batch` 显式批标识**〔新声明:非用例 ID,逐用例分段仍归控制台〕、`node_modules` 存在 ⇒ 注入 `NODE_PATH`);**run = `atlas_check` 默认模式为执行体(全批只跑一次真跑——v1 评审 MUST:双跑 = 首跑 D108 留证被 Playwright 二次起跑清空;`--fast` 分支被否因门行缺席)**;report(stdout/`--json` 沿行形态 + 留档 `reviews/<日期>-verify/` **仅全链绿落档**——失败批不落档故重跑不被短路);backfill **缓行**(写面零触碰:不写 e2e-index、不进评审账本、不取 claim——第二写手四题另案)。续跑语义无独立状态文件:留档在 ⇒ 整批短路;pre 恒重跑(如实);run 按 `results.json` 产物判(新鲜度依赖 Playwright 起跑清空行为,明写)。失败停环沿 apply §7 条文化。③ **被否**:执行器自起 Playwright(双真跑);独立净场步(净场已机制化于 prepare,D110);复用 `ATLAS_REFRESH_SETTLE` 当静置窗(换用途复用旋钮);独立状态机文件(死结同族);零变化钉的「全仓逐字节」口径(收窄为既有套件全绿 + 行集零新增红)。④ **钉与变异**:`tests/test_atlas_verify.py` 七钉(pre 失败停环 / 批级 ID 值形态 / run 失败含门行+留证路径 / 留档短路 / 断点语义·pre 恒重跑·run 按产物 / **全批恰一次真跑** / 参数错不写盘);变异 **MB19**(摘留档短路)/ **MB20**(摘 run 续跑判据)四段协议双咬、还原逐字节一致。⑤ **出库判定**:首轮真跑日经它起跑(四步各有如实状态 + 单次真跑 + 留证不蒸发 + 留档在)后出库;实现批验收线一句话版在 RFC §4 尾。 |
|
|
619
620
|
|
|
620
621
|
|
|
621
622
|
|
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
|
+
<img src="https://img.shields.io/badge/version-0.2.0--beta.8-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/package.json
CHANGED
package/rings/e2e/reference.md
CHANGED
|
@@ -660,7 +660,7 @@ testid:
|
|
|
660
660
|
| 启动 | `python3 .atlas/scripts/e2e_console.py --root .`(默认端口 4190;`--slowmo` / `--timeout` / `--no-head` / `--auto-close` 可选) |
|
|
661
661
|
| 静态资源 | `.atlas/templates/e2e/console/`(`index.html` / `app.js` / `console.css`);环境变量 `ATLAS_CONSOLE_ASSETS` 可覆盖 |
|
|
662
662
|
| 只读输入 | `product/e2e/e2e-index.md` + `product/e2e/cases/*.md`;格式解析**复用** `validate_e2e_index.py`,不写第二份 |
|
|
663
|
-
| 执行 | 一次一条(**并发 = 1**)、node 单轨(`<e2e.node_modules>/.bin/playwright test -c playwright.config.ts <file>.spec.ts:<line> --headed`(慢动作经 `ATLAS_SLOWMO` 注入配置的 `launchOptions.slowMo`;F11,2026-10-14),可执行解析与 `atlas_check`「E2E 真跑」门同一规则(含**浏览器就绪判据**,2026-10-20,`DESIGN.md` D124:`ATLAS_PLAYWRIGHT_CHANNEL` 未设时,playwright 自算的 chromium 可执行路径必须**存在**——npm 装包不自动下载浏览器,缺 ⇒ 起跑前诚实拒绝并点名 `npx playwright install chromium`);缺或不可达 ⇒ 服务端诚实拒绝,不起注定失败的子进程;「跑完留窗 + 结果哨兵」由 `atlas-hold.ts` 承载,2026-10-12 迁移批;**持窗模式向运行器传 `--timeout=0`(2026-10-21,`DESIGN.md` D126)**——Playwright 默认 test timeout(30s)**计入 fixture teardown**,留窗等待会在约 30s 处被判超时并强制关窗(浏览器「跑完仍自动关闭」的真因;哨兵此时已写 `exitstatus:0`,界面还会误示「已保留」);控制台的 wall-clock `--timeout` 是唯一超时真相源(到点 kill 并如实报「超时」),自动关窗模式不传该旗标、保留运行器原生 30s 兜底);**用例执行前数据准备钩子(硬,2026-10-18,`DESIGN.md` D110;v3 现场实证移植批)**:项目自带 `product/e2e/scripts/atlas-prepare.mjs` ⇒ 每条用例执行前先跑它(`node`,cwd = 项目根,180s 上限、**不占**用例超时窗——在计时起点之前;运行时 env 必带 `ATLAS_CASE_ID` = 当前用例 ID 且同步注入 `NODE_PATH`(与真跑子进程依赖解析环境一致,2026-10-20,`DESIGN.md` D125);项目脚本可据此只准备该用例依赖的数据,不读该变量的存量脚本行为不变——2026-10-20,`DESIGN.md` D121);失败/超时/起不动 ⇒ **诚实拒绝执行该条并在 UI 显式分流为「前置失败」**(`done` 事件完整形状 + `prep_failed` 标记,UI 呈现为 warn 级 pill 与「与用例脚本本身无关」通告,消除误导;失败原文进「输出」面板),不静默跑注定偏移的用例(共享状态型靶场的残留实体会让 count 类断言系统性偏移);地址 = `e2e.app_base_url`(E1 单段;来源下拉随 E1 删除,`#155`——meta 行保留 base_url 展示)。**已提交视图(2026-10-21,`DESIGN.md` D127)**:控制台「已提交」= 事件流中**所有 submit 快照按序累积合并**(后提交覆盖同名用例),不是最后一条事件;单次提交只携带页面草稿(差量),避免部分提交抹掉历史用例。**blocked / skipped 用例执行按钮禁用**(title 带索引状态原因;服务端同判诚实拒绝,`#158`——生成物为 `test.skip` 本无 node id
|
|
663
|
+
| 执行 | 一次一条(**并发 = 1**)、node 单轨(`<e2e.node_modules>/.bin/playwright test -c playwright.config.ts <file>.spec.ts:<line> --headed`(慢动作经 `ATLAS_SLOWMO` 注入配置的 `launchOptions.slowMo`;F11,2026-10-14),可执行解析与 `atlas_check`「E2E 真跑」门同一规则(含**浏览器就绪判据**,2026-10-20,`DESIGN.md` D124:`ATLAS_PLAYWRIGHT_CHANNEL` 未设时,playwright 自算的 chromium 可执行路径必须**存在**——npm 装包不自动下载浏览器,缺 ⇒ 起跑前诚实拒绝并点名 `npx playwright install chromium`);缺或不可达 ⇒ 服务端诚实拒绝,不起注定失败的子进程;「跑完留窗 + 结果哨兵」由 `atlas-hold.ts` 承载,2026-10-12 迁移批;**持窗模式向运行器传 `--timeout=0`(2026-10-21,`DESIGN.md` D126)**——Playwright 默认 test timeout(30s)**计入 fixture teardown**,留窗等待会在约 30s 处被判超时并强制关窗(浏览器「跑完仍自动关闭」的真因;哨兵此时已写 `exitstatus:0`,界面还会误示「已保留」);控制台的 wall-clock `--timeout` 是唯一超时真相源(到点 kill 并如实报「超时」),自动关窗模式不传该旗标、保留运行器原生 30s 兜底);**用例执行前数据准备钩子(硬,2026-10-18,`DESIGN.md` D110;v3 现场实证移植批)**:项目自带 `product/e2e/scripts/atlas-prepare.mjs` ⇒ 每条用例执行前先跑它(`node`,cwd = 项目根,180s 上限、**不占**用例超时窗——在计时起点之前;运行时 env 必带 `ATLAS_CASE_ID` = 当前用例 ID 且同步注入 `NODE_PATH`(与真跑子进程依赖解析环境一致,2026-10-20,`DESIGN.md` D125);项目脚本可据此只准备该用例依赖的数据,不读该变量的存量脚本行为不变——2026-10-20,`DESIGN.md` D121);失败/超时/起不动 ⇒ **诚实拒绝执行该条并在 UI 显式分流为「前置失败」**(`done` 事件完整形状 + `prep_failed` 标记,UI 呈现为 warn 级 pill 与「与用例脚本本身无关」通告,消除误导;失败原文进「输出」面板),不静默跑注定偏移的用例(共享状态型靶场的残留实体会让 count 类断言系统性偏移);地址 = `e2e.app_base_url`(E1 单段;来源下拉随 E1 删除,`#155`——meta 行保留 base_url 展示)。**已提交视图(2026-10-21,`DESIGN.md` D127)**:控制台「已提交」= 事件流中**所有 submit 快照按序累积合并**(后提交覆盖同名用例),不是最后一条事件;单次提交只携带页面草稿(差量),避免部分提交抹掉历史用例。**blocked / skipped 用例执行按钮禁用**(title 带索引状态原因;服务端同判诚实拒绝,`#158`——生成物为 `test.skip` 本无 node id,不得误报「重跑生成」);**验收执行器(2026-10-25,`RFC-20261025-02` / `DESIGN.md` D150)**:真跑可经 `scripts/atlas_verify.py` 四步起跑(pre/run=atlas_check/report 留档/backfill 缓行·写面零触碰;**全批只跑一次真跑**);**批级 `ATLAS_CASE_ID` 语义 = 显式串 `verify-batch`(非用例 ID——逐用例分段准备仍归控制台逐条执行,D121 不变;不读该变量的存量脚本行为逐字节不变)** |
|
|
664
664
|
| **桩披露(2026-10-12,§5.8)** | 含 `mock:` 行的用例卡片渲染 **MOCKED 徽标**(分片静态事实)+ 展开区 mock 清单;执行输出的 `[atlas-mock]` 命中行随日志流呈现(运行时事实)——人工走查者必须能看见「这条绿是桩撑的」 |
|
|
665
665
|
| **评审指纹与陈旧度(2026-10-21,`DESIGN.md` D128)** | 账本快照与每条提交绑定 **HEAD 提交与脚本散列指纹**(`prov: {head_sha, scripts_hash, git_dirty, actor_type}`)。`scripts_hash` 白名单覆盖 `scripts/` 下源码及 `atlas-prepare.mjs`(**候选顺序 = Runner 实际执行面:`product/e2e/scripts/` 优先**),排除运行时产物(`test-results/`、trace、png 等)——否则跑一轮就脏一次。控制台按当前工作区指纹实时重算 `review_state`(`fresh` / `stale` / `legacy` / `unreviewed`):代码或脚本变动 ⇒ **`stale`**,旧账本无指纹 ⇒ **`legacy`**;**两者同等按「需重审」处理**(剔除已评审、禁用复选框、顶栏单列计数、纳入「只看未评审」筛选),**读不到当前指纹时按 `legacy` 而非 `fresh`**(fail-closed,不冒充已核验);重新执行该用例自动清空旧评审态。`actor_type` ∈ `human \| agent`,由环境变量 **`ATLAS_ACTOR_TYPE`** 声明(缺省 `human`;自动化跑请显式设 `agent`)——它是**操作者自述、不是密码学证明**,故本环只如实暴露该字段,收口阻断仍由门与 `shared/closeout.md` 承担。提交另记 `reviewed_scope`(本次签字覆盖的用例集),消费者 = 人读账本 `ledger.md` 的「本次评审范围」行与控制台顶栏(不写不读 = 死字段,禁止)。**当前无 git / 生成脚本未落盘** ⇒ 相应维度记 `null`,不得用空串哈希冒充「匹配」 |
|
|
666
666
|
| **评审维度与执行状态正交** | 页面 `localStorage` 草稿 = 人的勾选;索引 `状态` 列(`red` / `green` / `blocked` / `skipped`;E0 历史态 `prototype-pass` 见 §5.4)= 执行结果。**两者不得互相推导**;「人工 review 通过」只在**执行通过**后解锁 |
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""atlas 验收执行器(执行层最小立项面 ①)——`RFC-20261025-02` v2 / `DESIGN.md` D150。
|
|
3
|
+
|
|
4
|
+
四步(**执行器 = 编排者 + 统一报告者 + 留档者,不重造真跑**):
|
|
5
|
+
|
|
6
|
+
pre 项目数据准备钩子 `product/e2e/scripts/atlas-prepare.mjs`(缺 ⇒ SKIP+原因,不算失败);
|
|
7
|
+
cwd = 项目根、180s 上限(控制台 D110 同款);env 带 `ATLAS_CASE_ID=verify-batch`
|
|
8
|
+
(显式批标识——RFC-20261025-02 批级语义:非用例 ID,D121 的逐用例分段仍归控制台);
|
|
9
|
+
`<root>/node_modules` 存在 ⇒ 同步注入 `NODE_PATH`(与真跑子进程依赖解析环境一致)。
|
|
10
|
+
run = `python3 .atlas/scripts/atlas_check.py --root <root>`(默认模式含全量真跑与装配门)
|
|
11
|
+
——**全批只跑一次真跑**(双跑 = 首跑 D108 留证被 Playwright 二次起跑清空)。
|
|
12
|
+
report 汇总行集 → stdout(+`--json`)并落档 `product/e2e/reviews/<日期>-verify/`
|
|
13
|
+
(README + report.json);**仅全链绿时落档**——失败批只出结构化报告不落档,
|
|
14
|
+
重跑因此不被短路。
|
|
15
|
+
backfill **缓行**(首批写面零触碰:不写 e2e-index 状态列、不进评审账本、不取写面 claim)。
|
|
16
|
+
|
|
17
|
+
续跑语义(无独立状态文件——产物存在性 = 唯一判据,失败停环语义沿 `apply/reference.md` §7):
|
|
18
|
+
- 留档目录在 ⇒ 整批短路(已成功的批不重跑);
|
|
19
|
+
- pre 无文件产物(改的是靶场服务端态)⇒ **恒重跑**(幂等廉价,如实);
|
|
20
|
+
- run 产物 = `product/e2e/scripts/.atlas-run/results.json`:缺 ⇒ 执行;在 ⇒ 跳过并注记
|
|
21
|
+
「沿用既有 results,原批退出码未知」(正常中途失败时 Playwright 起跑清空 `.atlas-run`
|
|
22
|
+
⇒ 产物必缺;在而留档无 = 上批死于 run 成功与落档之间的窄窗);
|
|
23
|
+
- 任一步失败 ⇒ 停该步、结构化报告(哪步/为何/已完步产物清单),下游不静默跳过。
|
|
24
|
+
|
|
25
|
+
退出码:**0** 全链绿或短路;**1** 失败(结构化报告);**2** 参数错(不写盘)。纯标准库。
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
import time
|
|
34
|
+
from datetime import datetime
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
PRE_TIMEOUT_S = 180
|
|
38
|
+
BATCH_CASE_ID = "verify-batch" # RFC-20261025-02 批级语义:显式批标识,非用例 ID(D121)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _prep_env(root: Path) -> dict:
|
|
42
|
+
env = dict(__import__("os").environ)
|
|
43
|
+
env["ATLAS_CASE_ID"] = BATCH_CASE_ID
|
|
44
|
+
nm = root / "node_modules"
|
|
45
|
+
if nm.is_dir():
|
|
46
|
+
env["NODE_PATH"] = str(nm)
|
|
47
|
+
return env
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def step_pre(root: Path) -> dict:
|
|
51
|
+
"""项目数据准备钩子;缺 ⇒ SKIP,失败/超时 ⇒ 失败(停环)。"""
|
|
52
|
+
prep = root / "product" / "e2e" / "scripts" / "atlas-prepare.mjs"
|
|
53
|
+
started = time.perf_counter()
|
|
54
|
+
if not prep.is_file():
|
|
55
|
+
return {"name": "pre", "status": "SKIP",
|
|
56
|
+
"detail": "无项目数据准备钩子 atlas-prepare.mjs(合法态,不算失败)",
|
|
57
|
+
"duration_ms": int((time.perf_counter() - started) * 1000)}
|
|
58
|
+
try:
|
|
59
|
+
pr = subprocess.run(["node", str(prep)], cwd=str(root), capture_output=True,
|
|
60
|
+
text=True, timeout=PRE_TIMEOUT_S, env=_prep_env(root))
|
|
61
|
+
except FileNotFoundError:
|
|
62
|
+
return {"name": "pre", "status": "FAIL", "detail": "node 可执行不存在", "duration_ms": 0}
|
|
63
|
+
except subprocess.TimeoutExpired:
|
|
64
|
+
return {"name": "pre", "status": "FAIL",
|
|
65
|
+
"detail": f"atlas-prepare 超时(>{PRE_TIMEOUT_S}s,不占真跑超时窗)",
|
|
66
|
+
"duration_ms": PRE_TIMEOUT_S * 1000}
|
|
67
|
+
dur = int((time.perf_counter() - started) * 1000)
|
|
68
|
+
if pr.returncode != 0:
|
|
69
|
+
return {"name": "pre", "status": "FAIL",
|
|
70
|
+
"detail": f"atlas-prepare 失败(exit={pr.returncode}):{(pr.stderr or pr.stdout or '')[-800:]}",
|
|
71
|
+
"duration_ms": dur}
|
|
72
|
+
return {"name": "pre", "status": "ok",
|
|
73
|
+
"detail": ((pr.stdout or "").strip()[-400:] or "(无输出)"), "duration_ms": dur}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _parse_rows(output: str) -> list[dict]:
|
|
77
|
+
"""解析 atlas_check 行形态(`ok|WARN|FAIL|SKIP <名称> <摘要>`,与门行原样)。"""
|
|
78
|
+
rows = []
|
|
79
|
+
for line in output.splitlines():
|
|
80
|
+
toks = line.split(None, 2)
|
|
81
|
+
if toks and toks[0] in {"ok", "WARN", "FAIL", "SKIP"} and len(toks) >= 2:
|
|
82
|
+
rows.append({"status": toks[0], "name": toks[1],
|
|
83
|
+
"summary": (toks[2] if len(toks) > 2 else "").strip()})
|
|
84
|
+
return rows
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def step_run(root: Path, py: str) -> dict:
|
|
88
|
+
"""run = atlas_check 默认模式(含全量真跑);产物在 ⇒ 跳过(见模块 docstring 续跑语义)。"""
|
|
89
|
+
started = time.perf_counter()
|
|
90
|
+
results = root / "product" / "e2e" / "scripts" / ".atlas-run" / "results.json"
|
|
91
|
+
if results.is_file():
|
|
92
|
+
return {"name": "run", "status": "SKIP",
|
|
93
|
+
"detail": "沿用既有 results.json(原批退出码未知;强制重跑请先删留档与 .atlas-run)",
|
|
94
|
+
"duration_ms": 0, "rows": [], "real_run_count": 0}
|
|
95
|
+
check = root / ".atlas" / "scripts" / "atlas_check.py"
|
|
96
|
+
if not check.is_file():
|
|
97
|
+
return {"name": "run", "status": "FAIL", "detail": "atlas_check.py 不存在(未装配?)",
|
|
98
|
+
"duration_ms": 0, "rows": [], "real_run_count": 0}
|
|
99
|
+
pr = subprocess.run([py, str(check), "--root", str(root)], capture_output=True, text=True)
|
|
100
|
+
dur = int((time.perf_counter() - started) * 1000)
|
|
101
|
+
out = (pr.stdout or "") + (pr.stderr or "")
|
|
102
|
+
return {"name": "run", "status": "ok" if pr.returncode == 0 else "FAIL",
|
|
103
|
+
"detail": f"atlas_check exit={pr.returncode}" + ("" if pr.returncode == 0 else f":{out.strip()[-400:]}"),
|
|
104
|
+
"duration_ms": dur, "rows": _parse_rows(out), "real_run_count": 1}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _evidence_paths(root: Path) -> list[str]:
|
|
108
|
+
d = root / "product" / "e2e" / "scripts" / ".atlas-run"
|
|
109
|
+
if not d.is_dir():
|
|
110
|
+
return []
|
|
111
|
+
hits = [str(p.relative_to(root)) for p in sorted(d.rglob("*"))
|
|
112
|
+
if p.is_file() and (p.name == "trace.zip" or p.name.startswith("test-failed"))]
|
|
113
|
+
return hits[:20]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def archive_dir(root: Path) -> Path:
|
|
117
|
+
return root / "product" / "e2e" / "reviews" / f"{datetime.now().date().isoformat()}-verify"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def write_archive(root: Path, report: dict) -> str:
|
|
121
|
+
d = archive_dir(root)
|
|
122
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
(d / "report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
124
|
+
lines = [f"# 验收执行器留档({datetime.now().isoformat(timespec='seconds')};RFC-20261025-02/D150)",
|
|
125
|
+
"", "| 步 | 态 | 用时(ms) | 说明 |", "|---|---|---|---|"]
|
|
126
|
+
for st in report["steps"]:
|
|
127
|
+
lines.append(f"| {st['name']} | {st['status']} | {st['duration_ms']} | {st['detail'][:120]} |")
|
|
128
|
+
lines += ["", "| 门行 | 态 | 摘要 |", "|---|---|---|"]
|
|
129
|
+
for r in report["rows"]:
|
|
130
|
+
lines.append(f"| {r['name']} | {r['status']} | {r['summary'][:120]} |")
|
|
131
|
+
(d / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
132
|
+
return str(d.relative_to(root))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main(argv: list[str] | None = None) -> int:
|
|
136
|
+
ap = argparse.ArgumentParser(description="atlas 验收执行器(编排+统一报告+留档;不重造真跑)")
|
|
137
|
+
ap.add_argument("--root", required=True, help="项目根")
|
|
138
|
+
ap.add_argument("--json", action="store_true", help="机读报告(stdout)")
|
|
139
|
+
args = ap.parse_args(argv)
|
|
140
|
+
root = Path(args.root).resolve()
|
|
141
|
+
if not root.is_dir():
|
|
142
|
+
print("ERROR: --root 不是目录", file=sys.stderr)
|
|
143
|
+
return 2
|
|
144
|
+
|
|
145
|
+
# 留档在 ⇒ 整批短路(已成功的批不重跑)
|
|
146
|
+
if archive_dir(root).is_dir():
|
|
147
|
+
report = {"ok": True, "short_circuit": True,
|
|
148
|
+
"detail": f"留档 {archive_dir(root).relative_to(root)} 已存在——已成功的批不重跑",
|
|
149
|
+
"steps": [], "rows": [], "real_run_count": 0, "archive": str(archive_dir(root).relative_to(root))}
|
|
150
|
+
print(json.dumps(report, ensure_ascii=False, indent=2) if args.json
|
|
151
|
+
else f"[短路] {report['detail']}")
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
steps: list[dict] = []
|
|
155
|
+
real_run = 0
|
|
156
|
+
|
|
157
|
+
pre = step_pre(root)
|
|
158
|
+
steps.append(pre)
|
|
159
|
+
if pre["status"] == "FAIL":
|
|
160
|
+
return _fail(root, steps, [], 0, args.json)
|
|
161
|
+
|
|
162
|
+
run = step_run(root, py=sys.executable)
|
|
163
|
+
steps.append(run)
|
|
164
|
+
real_run += run.get("real_run_count", 0)
|
|
165
|
+
if run["status"] == "FAIL":
|
|
166
|
+
return _fail(root, steps, run.get("rows", []), real_run, args.json)
|
|
167
|
+
|
|
168
|
+
rows = run.get("rows", [])
|
|
169
|
+
report = {"ok": True, "short_circuit": False, "steps": steps, "rows": rows,
|
|
170
|
+
"real_run_count": real_run, "archive": None}
|
|
171
|
+
report["archive"] = write_archive(root, report)
|
|
172
|
+
if args.json:
|
|
173
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
174
|
+
else:
|
|
175
|
+
for st in steps:
|
|
176
|
+
print(f"[{st['status']}] {st['name']} ({st['duration_ms']}ms) {st['detail'][:160]}")
|
|
177
|
+
for r in rows:
|
|
178
|
+
print(f" {r['status']:<4} {r['name']:<24} {r['summary'][:100]}")
|
|
179
|
+
print(f"[留档] {report['archive']}")
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _fail(root: Path, steps: list[dict], rows: list[dict], real_run: int, as_json: bool) -> int:
|
|
184
|
+
"""失败停环:停在该步、结构化报告、下游不静默跳过;失败不落档(重跑不被短路)。"""
|
|
185
|
+
report = {"ok": False, "short_circuit": False, "steps": steps, "rows": rows,
|
|
186
|
+
"real_run_count": real_run, "archive": None,
|
|
187
|
+
"failed_step": next((s["name"] for s in reversed(steps) if s["status"] == "FAIL"), None),
|
|
188
|
+
"completed_steps": [s["name"] for s in steps if s["status"] in {"ok", "SKIP"}],
|
|
189
|
+
"evidence": _evidence_paths(root)}
|
|
190
|
+
if as_json:
|
|
191
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
192
|
+
else:
|
|
193
|
+
print(f"[FAIL] 停在 {report['failed_step']} 步(下游不执行);已完步:{report['completed_steps']}",
|
|
194
|
+
file=sys.stderr)
|
|
195
|
+
if report["evidence"]:
|
|
196
|
+
print(f"[留证] 失败构件:{report['evidence']}", file=sys.stderr)
|
|
197
|
+
return 1
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
sys.exit(main())
|
package/shared/gates.md
CHANGED
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
| `validate_*`(**每器一行**,数量 = `atlas_check.VALIDATORS` 长度,当前 10;**缺失/漏挂即红 = `test_canonical_consistency` 判据⑥**,`§B120`) | 同 2.1 | `[机器]` 逐个调用校验器,一门一行(前置未声明 ⇒ `SKIP` + 原因) | 判据本身(**不二次实现、不放宽**) | 每批收口 | 同 2.1 + **M27**(摘 `validate_structure` ⇒ 判据⑥与逐器一行测试双红,`§B120`) |
|
|
65
65
|
| `对齐测试(.atlas/tests)` | 包面 ⇔ 出厂测试 | `[机器]` `.atlas/tests` 全绿;**前置 = 该路径是目录**(**F53**,2026-10-16,issue #17 残余;`RFC-20261016-02` D91:不存在或不是目录 ⇒ `SKIP` 带实测路径与出路——旧发行版装配态(npm 旧版不发布 `tests/` ⇒ install.sh 拷贝空操作)/ 源包开发态 / 装配损坏三种形态同吃;D86 收窄注记见 `DESIGN.md`——根因面(装配三表同步)不靠本 SKIP 吸收) | 测试**够不够**(那是本表);pytest 解释器可用性(无 pytest ⇒ 本行 `FAIL` 指名,属环境缺失非装配残缺,装 pytest 可修——评审 M-2);目录在场但零用例 ⇒ `no tests ran` 仍 `FAIL`(装配损坏值得查——评审 m-4,不改判据) | 每批收口 | `出厂级(本轮实测)`:注入一条必红测试 ⇒ 本行 `FAIL 对齐测试(.atlas/tests) 1 failed` 且退出码 1;移除后复绿(`3509 §119`);**F53**:缺 `.atlas/tests` 的三形态(无 `.atlas/` / 有 `.atlas/` 无 `tests/` / 路径被文件占用)全量模式 ⇒ `SKIP`、退出码 0、不进 `failures`;目录在场注入必红 ⇒ 仍 `FAIL`(反面钉);**M76** 摘掉前置分支 ⇒ `test_f53_factory_gate_skips_when_tests_dir_absent` 红(注入→还原逐字节一致→复绿);`tests/test_atlas_check.py` **41 条**本轮跑绿(36 随 D124 浏览器判据钉 + P2 事件行钉 3 条 + 评审修复钉 2 条) |
|
|
66
66
|
| `生成物最新` | 用例分片 ⇔ 生成物 | `[机器]` **三态**:未就绪(`SKIP`:前置未声明,如 `app_base_url` 空)≠ 生成物过期(`WARN`)≠ 生成失败(`FAIL`) | 生成物**内容**是否正确(那是运行期门) | 用例或 profile 变更后 | `出厂级(本轮实测)`:M1 摘掉「未就绪」分支 ⇒ 红(`3509 §B110`);**零变化承诺钉**(2026-10-17,issue #27 / D104):`base query` 转发只在「`app_base_url` 带 query」一个分支生效 —— `tests/test_gen_e2e_scripts.py::test_base_query_absent_leaves_generation_byte_identical`断言无 query 时三处发射面**逐字节不变**(变异 MB3 去掉短路 ⇒ 红) |
|
|
67
|
-
| `E2E 真跑` | 用例 ⇔ 真实应用 | `[机器]` 真栈上跑 pytest;解释器按**依赖可导入性**选(不按路径存在);地址为空**或靶场不可达**(TCP 探测,2026-10-04 补 `§B189-1`)⇒ `SKIP` + 原因(后者带 `ENV_ISSUE:<地址>;<原因>`);探针只判「栈起没起」(任何 HTTP 状态码都算在跑),不判页面对不对;**node 轨运行器判据(2026-10-17,issue #24 / `DESIGN.md` D101)= 「从 `e2e.scripts_dir` 出发 `@playwright/test` 可解析」**——把 node 轨对齐本行早写明的「按**依赖可导入性**,不按路径存在」(ESM 下 `NODE_PATH` 是 CJS 通道、无效;二进制存在只是**必要**条件,曾把它当充要条件 ⇒ 半生效:找得到 bin、解析不到模块,错误落到运行期并被计成「跑失败 / 无结果」);判据不成立且全局兜底不可修复 ⇒ `FAIL` 点名真原因;**全局兜底修复成功(幂等软链 + 复检)⇒ 本行降 `WARN`**(跑得起来,但结论依赖**机器全局状态**、非项目自足);**浏览器就绪判据(2026-10-20,issue #38 / `DESIGN.md` D124)**:模块可解析 ≠ launch 可成——npm 装包**不自动下载浏览器** ⇒ 判据 ② = playwright 自算的 chromium 可执行路径**存在**(吃 `PLAYWRIGHT_BROWSERS_PATH`;实证「算得出 ≠ 装了」——未下载的浏览器 `executablePath()` 返回计算路径而不抛错)或 `ATLAS_PLAYWRIGHT_CHANNEL` 已设(launch 走系统通道浏览器 ⇒ 旁路);不成立 ⇒ **FAIL 指名 `npx playwright install chromium`**(与「运行器可执行不存在 ⇒ FAIL + npm i 提示」同族——E2E 意图已声明、依赖装了一半 ≠ 「前置未声明」的 SKIP,issue #38 原案 SKIP 被否) | 真栈没起来时的任何结论 —— 那时它只报 `SKIP`(**两态均适用**:地址未声明 / 地址已声明但不可达) | 真栈就绪后(**已发生**:逐页真跑,`3508 §166`/`§187`) | `真跑级(2026-10-04,逐页真跑,咬出真红 `3508 §190`;首个真跑级行)` + **M12a** 摘掉可达性探针 ⇒ 「不可达 ⇒ SKIP」钉子红(会跑到 pytest 并报 FAIL)/ **M12b** 探针改恒不可达 ⇒ 「可达不误 SKIP」钉子红(报 SKIP 而不是 ok);两条均咬、还原逐字节一致。旧记「从未红过」(`§B99`/`§111`)已过期。**升档预告(2026-10-16,`RFC-20261015-02` / issue #11)**:`e2e.app_login` 未声明从「WARN 指名」升为生成器 `not_ready` 的**时间锚 = 下一次真跑准备日(无条件)**——届时 SKIP 原因由两态扩三态(地址未声明 / 登录形态未声明 / 靶场不可达),机读键第三态 = `"app_login"`;**第 1 次咬合(升档依据)= 2026-10-15 现场**:pass1→pass5 会话失效 + 人工登录复用(issue #11 取证留痕);**第 2 次咬合 + 协议定形首跑全绿(2026-10-17,百炼/v3 platform-frontend)**:`app-workflow-detail` 1 页切片 **11/11**(零失败,22.2s;起跑走净场协议;分片触发面已全 D98 `dispatch` 直发 + 产品侧同日修「编辑内容保存管道」,trace 探针 A/B 双版复核);留档 = 采纳仓 `product/e2e/reviews/2026-10-17/`(README + results.json 快照);逐格纪律:本轮只升本行证据,其余门行不动;**第 3 次咬合(2026-10-17,issue #24 / `DESIGN.md` D101,百炼/v2前端)**:判据本体更换(binary 存在 → **可解析**)+ 全局兜底改为可生效的修复 ⇒ 门与执行器同一内核 `_support/pw_runner.py`;钉 5 条——门侧 2(`test_check_e2e_module_unresolvable_fails_loudly`:binary 在、模块不在、无全局 ⇒ `FAIL` 点名「依赖可导入性」;`test_check_e2e_global_playwright_detected` 由假 ok 改钉 **WARN + 行内写明全局与软链 + 幂等再跑**)、执行器侧 3(修复后复检通过且 note 点名全局版本 / 无全局时如实拒绝 / 链接位被占 ⇒ **拒绝覆盖**);变异 **M88** 判据退回「binary 存在即 ok」⇒ 门侧坏输入钉红、**M89** 摘软链步 ⇒ 执行器侧修复钉红、**M90** 摘 `global_note` ⇒ 降档 WARN 与「用了全局」输出齐红;三枚还原后逐字节一致;**2026-10-17 独立评审后复验(M88/M89/M90 全部重跑)**:M88 原报告的红因是**夹具错位**(假 `Proc` 缺 `stderr` ⇒ `atlas_check.run()` 崩,从未触到判据断言),补夹具后重跑,红因已对齐为「status 仍 FAIL 但 detail 退化成『无结果』」——即「判据被绕过 ⇒ 真原因丢失」本身;M90 断言加具名消息(原为空的 `AssertionError:`);三枚报告现已含**注入 diff**(工具同批补);**issue #25(2026-10-17,D94 ③ 残余的现场实证)**:本门四处标量取值(`cases_dir` / `app_base_url` / `reset` / `node_modules`)过内核 `strip_inline_comment`(此前各用正则直取 ⇒ `reset: null # 说明` 被当命令跑,**假红且在跑到用例之前**);钉 2 条(注释不污染取值 / 真发令照发)+ 变异 **M92**(还原逐字节一致);`tests/test_atlas_check.py` 25 → 27 → 28 → 30 → 31 → 34 → 36 → 41 条(后两枚随 D106 的执行 cwd 钉 / D94③ 的引号清洗钉;31 随 D124 浏览器判据钉;34 随 P2 事件行钉 3 条;36 随评审修复钉 2 条);**宿主注入型 base query 转发**(2026-10-17,issue #27 / `DESIGN.md` D104):`e2e.app_base_url` 的 query 由生成物带到**三处**导航发射面(step goto / 守卫 / 零步基线 `data.url`;单源 `_support/url_merge.py`),base 无 query ⇒ 逐字节零变化;控制台手工「打开」(第五消费者,`manual_open_url`)同步修正;钉 4 条(值保真 / 三处齐并 / 零变化 / 控制台地址)+ 变异 MB1–MB3;`tests/test_gen_e2e_scripts.py` / `tests/test_e2e_console.py` 与本表校验器行按实收条数全绿;**失败留证(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence` / 独立评审两轮,维护者拍板「按推荐继续」;来源 = OpenHands `browser record` 机制级审查)**:生成物两轨 playwright 配置补 `trace: 'retain-on-failure'` / `screenshot: 'only-on-failure'` ⇒ 失败现场自动落 trace(DOM 快照/网络/操作日志)+ 截图到 `test-results/`,全绿轮零新增构件;契约条 = `rings/e2e/reference.md` §7.1「失败留证(硬)」;**咬合点 = 下一次 `install.sh --update` / 首装重装配(不溯及已装配副本)**——重装配前「契约已声明留证而现场无 trace」属**如实态、非缺陷**,巡检不得误报;`duty-review/bugs.md` 未解观察(hover 禁用按钮偶发不可见)的升判条件(复现 ≥2 次 + trace / 连续截图)由此取得常驻证据源;钉 3 条(双轨同构 / 值形态封闭 / 无覆盖口,`tests/test_gen_e2e_scripts.py`)+ 变异 **MB10**(摘任一轨 trace 键)/ **MB11**(值改 `on`),四段协议含注入 diff、还原逐字节一致;video 缓行(trace 自带回放器,`proposals/RFC-20261018-01` §2.2 被否区);**D108 首次咬合(2026-10-25 真跑日,D148 首轮;靶场 = v3 `dev_20261120`@09dd31104 的 detached worktree 隔离副本,内网后端不可达 ⇒ 环境红面)**:红轮(mail-detail 分片,元素齐全守卫全红于白屏)自动落 **28 个失败构件**(`.atlas-run/<用例目录>/trace.zip + test-failed-*.png` 成对)——`retain-on-failure` 面实证;绿轮(2 passed,注红条款同族的临时取证 spec,跑完即删)**零新增构件**——`only-on-failure` 面实证;**落点勘误(同批全仓改口)**:实际落 `.atlas-run/`(生成物显式 `outputDir` 覆盖 Playwright 缺省 `test-results/`,D108 首版契约误写缺省目录);**新观察(bugs.md E2E 段)**:Playwright 每次 `playwright test` 起动即清空 outputDir ⇒ 留证**留存窗口 = 到下一次起跑**(「首跑红→复跑绿」场景复跑起动瞬间旧构件蒸发)+ 本轮环境红形态 = 「栈起没起」探针盲区首个活体实证(前端 200、后端不可达 ⇒ 门放行、用例全红);**D80② 升档兑现(2026-10-24,`DESIGN.md` D149,真跑日批)**:`e2e.app_login` 未声明已由「占位生成 + WARN」改推 **`NOT_READY`(退出码 3 + 机读键 `"app_login"`)**——本行上方预告的时间锚「下一次真跑准备日」到期执行;SKIP 三态(地址未声明 / 登录形态未声明 / 靶场不可达)经 `atlas_check` 既有 `GEN_NOT_READY` 中继自动成立;D103 排除类(有门但三态装不下 ⇒ 人工会话取出)出路写进 not_ready 文案;钉 2(`test_setup_without_app_login_pushes_not_ready` 改判旧钉 + `test_app_login_not_ready_json_face_matches_channel`)+ 变异 **MB17**(退回 WARN 半生成)/ **MB18**(机读键写错/缺 message);**D148 真跑日协议**(runbook = `docs/guide/real-run-day.md`:detached worktree 隔离执行、D145 manifest 配对先行、注红不计缺陷判据)——首轮七项咬合记录逐项回写本行 / `duty-review/bugs.md` / QUEUE
|
|
67
|
+
| `E2E 真跑` | 用例 ⇔ 真实应用 | `[机器]` 真栈上跑 pytest;解释器按**依赖可导入性**选(不按路径存在);地址为空**或靶场不可达**(TCP 探测,2026-10-04 补 `§B189-1`)⇒ `SKIP` + 原因(后者带 `ENV_ISSUE:<地址>;<原因>`);探针只判「栈起没起」(任何 HTTP 状态码都算在跑),不判页面对不对;**node 轨运行器判据(2026-10-17,issue #24 / `DESIGN.md` D101)= 「从 `e2e.scripts_dir` 出发 `@playwright/test` 可解析」**——把 node 轨对齐本行早写明的「按**依赖可导入性**,不按路径存在」(ESM 下 `NODE_PATH` 是 CJS 通道、无效;二进制存在只是**必要**条件,曾把它当充要条件 ⇒ 半生效:找得到 bin、解析不到模块,错误落到运行期并被计成「跑失败 / 无结果」);判据不成立且全局兜底不可修复 ⇒ `FAIL` 点名真原因;**全局兜底修复成功(幂等软链 + 复检)⇒ 本行降 `WARN`**(跑得起来,但结论依赖**机器全局状态**、非项目自足);**浏览器就绪判据(2026-10-20,issue #38 / `DESIGN.md` D124)**:模块可解析 ≠ launch 可成——npm 装包**不自动下载浏览器** ⇒ 判据 ② = playwright 自算的 chromium 可执行路径**存在**(吃 `PLAYWRIGHT_BROWSERS_PATH`;实证「算得出 ≠ 装了」——未下载的浏览器 `executablePath()` 返回计算路径而不抛错)或 `ATLAS_PLAYWRIGHT_CHANNEL` 已设(launch 走系统通道浏览器 ⇒ 旁路);不成立 ⇒ **FAIL 指名 `npx playwright install chromium`**(与「运行器可执行不存在 ⇒ FAIL + npm i 提示」同族——E2E 意图已声明、依赖装了一半 ≠ 「前置未声明」的 SKIP,issue #38 原案 SKIP 被否) | 真栈没起来时的任何结论 —— 那时它只报 `SKIP`(**两态均适用**:地址未声明 / 地址已声明但不可达) | 真栈就绪后(**已发生**:逐页真跑,`3508 §166`/`§187`) | `真跑级(2026-10-04,逐页真跑,咬出真红 `3508 §190`;首个真跑级行)` + **M12a** 摘掉可达性探针 ⇒ 「不可达 ⇒ SKIP」钉子红(会跑到 pytest 并报 FAIL)/ **M12b** 探针改恒不可达 ⇒ 「可达不误 SKIP」钉子红(报 SKIP 而不是 ok);两条均咬、还原逐字节一致。旧记「从未红过」(`§B99`/`§111`)已过期。**升档预告(2026-10-16,`RFC-20261015-02` / issue #11)**:`e2e.app_login` 未声明从「WARN 指名」升为生成器 `not_ready` 的**时间锚 = 下一次真跑准备日(无条件)**——届时 SKIP 原因由两态扩三态(地址未声明 / 登录形态未声明 / 靶场不可达),机读键第三态 = `"app_login"`;**第 1 次咬合(升档依据)= 2026-10-15 现场**:pass1→pass5 会话失效 + 人工登录复用(issue #11 取证留痕);**第 2 次咬合 + 协议定形首跑全绿(2026-10-17,百炼/v3 platform-frontend)**:`app-workflow-detail` 1 页切片 **11/11**(零失败,22.2s;起跑走净场协议;分片触发面已全 D98 `dispatch` 直发 + 产品侧同日修「编辑内容保存管道」,trace 探针 A/B 双版复核);留档 = 采纳仓 `product/e2e/reviews/2026-10-17/`(README + results.json 快照);逐格纪律:本轮只升本行证据,其余门行不动;**第 3 次咬合(2026-10-17,issue #24 / `DESIGN.md` D101,百炼/v2前端)**:判据本体更换(binary 存在 → **可解析**)+ 全局兜底改为可生效的修复 ⇒ 门与执行器同一内核 `_support/pw_runner.py`;钉 5 条——门侧 2(`test_check_e2e_module_unresolvable_fails_loudly`:binary 在、模块不在、无全局 ⇒ `FAIL` 点名「依赖可导入性」;`test_check_e2e_global_playwright_detected` 由假 ok 改钉 **WARN + 行内写明全局与软链 + 幂等再跑**)、执行器侧 3(修复后复检通过且 note 点名全局版本 / 无全局时如实拒绝 / 链接位被占 ⇒ **拒绝覆盖**);变异 **M88** 判据退回「binary 存在即 ok」⇒ 门侧坏输入钉红、**M89** 摘软链步 ⇒ 执行器侧修复钉红、**M90** 摘 `global_note` ⇒ 降档 WARN 与「用了全局」输出齐红;三枚还原后逐字节一致;**2026-10-17 独立评审后复验(M88/M89/M90 全部重跑)**:M88 原报告的红因是**夹具错位**(假 `Proc` 缺 `stderr` ⇒ `atlas_check.run()` 崩,从未触到判据断言),补夹具后重跑,红因已对齐为「status 仍 FAIL 但 detail 退化成『无结果』」——即「判据被绕过 ⇒ 真原因丢失」本身;M90 断言加具名消息(原为空的 `AssertionError:`);三枚报告现已含**注入 diff**(工具同批补);**issue #25(2026-10-17,D94 ③ 残余的现场实证)**:本门四处标量取值(`cases_dir` / `app_base_url` / `reset` / `node_modules`)过内核 `strip_inline_comment`(此前各用正则直取 ⇒ `reset: null # 说明` 被当命令跑,**假红且在跑到用例之前**);钉 2 条(注释不污染取值 / 真发令照发)+ 变异 **M92**(还原逐字节一致);`tests/test_atlas_check.py` 25 → 27 → 28 → 30 → 31 → 34 → 36 → 41 条(后两枚随 D106 的执行 cwd 钉 / D94③ 的引号清洗钉;31 随 D124 浏览器判据钉;34 随 P2 事件行钉 3 条;36 随评审修复钉 2 条);**宿主注入型 base query 转发**(2026-10-17,issue #27 / `DESIGN.md` D104):`e2e.app_base_url` 的 query 由生成物带到**三处**导航发射面(step goto / 守卫 / 零步基线 `data.url`;单源 `_support/url_merge.py`),base 无 query ⇒ 逐字节零变化;控制台手工「打开」(第五消费者,`manual_open_url`)同步修正;钉 4 条(值保真 / 三处齐并 / 零变化 / 控制台地址)+ 变异 MB1–MB3;`tests/test_gen_e2e_scripts.py` / `tests/test_e2e_console.py` 与本表校验器行按实收条数全绿;**失败留证(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence` / 独立评审两轮,维护者拍板「按推荐继续」;来源 = OpenHands `browser record` 机制级审查)**:生成物两轨 playwright 配置补 `trace: 'retain-on-failure'` / `screenshot: 'only-on-failure'` ⇒ 失败现场自动落 trace(DOM 快照/网络/操作日志)+ 截图到 `test-results/`,全绿轮零新增构件;契约条 = `rings/e2e/reference.md` §7.1「失败留证(硬)」;**咬合点 = 下一次 `install.sh --update` / 首装重装配(不溯及已装配副本)**——重装配前「契约已声明留证而现场无 trace」属**如实态、非缺陷**,巡检不得误报;`duty-review/bugs.md` 未解观察(hover 禁用按钮偶发不可见)的升判条件(复现 ≥2 次 + trace / 连续截图)由此取得常驻证据源;钉 3 条(双轨同构 / 值形态封闭 / 无覆盖口,`tests/test_gen_e2e_scripts.py`)+ 变异 **MB10**(摘任一轨 trace 键)/ **MB11**(值改 `on`),四段协议含注入 diff、还原逐字节一致;video 缓行(trace 自带回放器,`proposals/RFC-20261018-01` §2.2 被否区);**D108 首次咬合(2026-10-25 真跑日,D148 首轮;靶场 = v3 `dev_20261120`@09dd31104 的 detached worktree 隔离副本,内网后端不可达 ⇒ 环境红面)**:红轮(mail-detail 分片,元素齐全守卫全红于白屏)自动落 **28 个失败构件**(`.atlas-run/<用例目录>/trace.zip + test-failed-*.png` 成对)——`retain-on-failure` 面实证;绿轮(2 passed,注红条款同族的临时取证 spec,跑完即删)**零新增构件**——`only-on-failure` 面实证;**落点勘误(同批全仓改口)**:实际落 `.atlas-run/`(生成物显式 `outputDir` 覆盖 Playwright 缺省 `test-results/`,D108 首版契约误写缺省目录);**新观察(bugs.md E2E 段)**:Playwright 每次 `playwright test` 起动即清空 outputDir ⇒ 留证**留存窗口 = 到下一次起跑**(「首跑红→复跑绿」场景复跑起动瞬间旧构件蒸发)+ 本轮环境红形态 = 「栈起没起」探针盲区首个活体实证(前端 200、后端不可达 ⇒ 门放行、用例全红);**D80② 升档兑现(2026-10-24,`DESIGN.md` D149,真跑日批)**:`e2e.app_login` 未声明已由「占位生成 + WARN」改推 **`NOT_READY`(退出码 3 + 机读键 `"app_login"`)**——本行上方预告的时间锚「下一次真跑准备日」到期执行;SKIP 三态(地址未声明 / 登录形态未声明 / 靶场不可达)经 `atlas_check` 既有 `GEN_NOT_READY` 中继自动成立;D103 排除类(有门但三态装不下 ⇒ 人工会话取出)出路写进 not_ready 文案;钉 2(`test_setup_without_app_login_pushes_not_ready` 改判旧钉 + `test_app_login_not_ready_json_face_matches_channel`)+ 变异 **MB17**(退回 WARN 半生成)/ **MB18**(机读键写错/缺 message);**D148 真跑日协议**(runbook = `docs/guide/real-run-day.md`:detached worktree 隔离执行、D145 manifest 配对先行、注红不计缺陷判据)——首轮七项咬合记录逐项回写本行 / `duty-review/bugs.md` / QUEUE;**验收执行器(2026-10-25,`RFC-20261025-02` v2 / `DESIGN.md` D150)= 行为者非门**:`scripts/atlas_verify.py` 四步(pre 钩子·批级 `ATLAS_CASE_ID=verify-batch` / **run = 本行 `atlas_check` 默认模式为执行体——全批只跑一次真跑**(双跑 = 首跑 D108 留证被二次起跑清空)/ report 汇总+留档 `reviews/<日期>-verify/` 仅全链绿落档 / backfill 缓行·写面零触碰);本行**不新增判据**——执行器只是编排者,验收仍由本行与既有门承担;钉 7(`tests/test_atlas_verify.py`)+ 变异 **MB19**(摘留档短路)/ **MB20**(摘 run 续跑判据)四段协议双咬;真跑咬合 = 下一次真跑日经它起跑; **select 点击目标 = 打标元素本身(2026-10-23,`DESIGN.md` D144)**:`atlasSelect` 采样后按**整页 DOM 下标**点击,而一个面板可同时挂多个下拉(关闭态的选项节点仍留在 DOM,rc-select 另有零宽 `role=option` a11y 镜像)⇒ 组件一重排,同一数字指向别当下拉的选项(现场:v3 测试环境步骤选「不限时间」却解析到「自定义范围」镜像节点,重试 52 次 not visible 至 30s 超时;全量 41 条 2/2 红、单跑 2/2 绿 = 顺序依赖的假绿)。定案 = 唯一命中打一次性标 `data-atlas-pick`、点击锚定标记本身(走 Playwright actionability)、每次采样先清遗留标记、标记失效 ⇒ 重采再点(有界 3 次,仍不成带可见选项响亮失败);钉 = 结构钉 `test_select_clicks_the_marked_option_not_a_global_index`(病灶字符串消失 / 打标成对 / 清理行 / 有界重试 / 响亮失败) + 行为钉 `test_select_click_hits_the_marked_option_across_a_virtualized_list`(生成物抽 `atlasSelect` 在 Node 假 DOM 真求值:目标在全局下标 25、下标 20 为另一下拉镜像 ⇒ 实测点中「不限时间」,且走到标记失效重采路径);变异 **M149**(回退整页下标点击 ⇒ 4 钉红)、**M150**(打标目标改指 `all[20]` ⇒ 2 钉红),还原 sha256 逐字节一致;`tests/test_gen_e2e_scripts.py` 105 → 107 → **108 条**(+1 随 D149 升档批:旧钉「degrades_loudly」改判 −1 + 新钉 2);真跑 = v3 测试环境全量 41 用例两连绿(修复前 005 全量必红);夹具教训:行为钉首跑红根因是夹具漏定义 `ATLAS_WAIT_TIMEOUT_MS`(helper 内 ReferenceError 撞自家 catch ⇒ 伪装成「找不到唯一选项」),已记录防再犯。 | **人工会话取出 = 纪律与盲区面(2026-10-17,`DESIGN.md` D103,无机器判据、不假装是门)**:靶场有门但 `app_login` 三态装不下(图形验证码 / cookie 会话 / 响应体无令牌)⇒ 无形态可声明,保持未声明 + AI 主动给人 `playwright codegen --save-storage=<scripts_dir>/.atlas-auth/state.json <app_base_url>`、人登录一次(关窗才落盘 / 会话短命令要立即跑 / `.atlas-auth/` 已忽略 + setup 幂等守卫);**明示不可复现**(每轮要人登录 ⇒ CI 不可用);**与 D80 ② 升档的相抵点已登记**:该升档落地时须先把「有门但无形态」这一类排除,否则会把「如实未声明」罚成 `not_ready`。**数据准备钩子 + refresh 落盘节拍(2026-10-18,`DESIGN.md` D110/D111,v3 现场实证移植批)**:控制台逐条执行前必跑项目自带 `atlas-prepare.mjs`(`node`,180s 上限不占用例超时窗;失败/超时/起不动 ⇒ 诚实拒绝并带 `prep_failed` 如实上报);`refresh` 发射先等后刷两行(`ATLAS_REFRESH_SETTLE` 默认 0 行为不变,毫秒常数属项目侧运行时前提不进生成器);钉 2(console **13 → 14** / gen **83 → 84**)+ 变异 **M94**(摘钩子段)/ **M95**(摘 waitForTimeout 发射行)四段协议、还原逐字节一致;**数据准备钩子用例上下文(2026-10-20,`DESIGN.md` D121)**:运行器调用项目 prepare 时注入 `ATLAS_CASE_ID` = 当前用例 ID(与 `ATLAS_BASE_URL`/`ATLAS_SLOWMO` 同一通道),项目脚本可按用例分段准备;不读该变量的存量脚本行为逐字节不变;钉 = `tests/test_e2e_console.py` prepare 四元组含 case id,变异 **M121**(摘 env 注入 ⇒ 钉红),还原逐字节一致;**select 候选集组件类名(2026-10-20,`DESIGN.md` D122)**:Antd Select 的可见项无 `role=option`(role 项为零宽 a11y 镜像)⇒ 候选集补 `[class*="select-item-option"]`,定位不再假红;钉 = gen 套件候选集断言 2 处,变异 **M122**(摘类名 ⇒ 钉红),还原逐字节一致;**upload ESM 路径(2026-10-20,`DESIGN.md` D123)**:`type: module` 的项目生成物改用 `import.meta.url` 解析 fixture(`__dirname` 在 ESM 下不存在 ⇒ upload 用例必红);CJS 形态零变化;钉 = gen 套件新增 ESM 用例(91 → 92),变异 **M123**,还原逐字节一致;**浏览器就绪判据落地(2026-10-20,issue #38 / `DESIGN.md` D124)**:控制台起跑前置接同一内核 `_support/pw_runner.py::probe_browser` ⇒ 起跑前诚实拒绝(不起注定 launch 失败的子进程,出路文案点名安装命令);钉 3 条——门侧 `test_check_e2e_fails_loud_when_browser_missing`(FAIL + install 指名 + 真跑不起:`failures[0].command` 空、无 e2e-summary)/ 控制台 `test_probe_browser_three_states`(路径存在 / 不存在给 install 出路 / 探针报错给尾行原因 / **channel 旁路不 spawn**)/ `test_start_refuses_when_browser_missing`(起跑前拒绝);既有 6 条真跑钉(主题 ≠ 浏览器判据)补旁路假件;变异 **M124**(摘门内判据退回恒真 ⇒ 门侧钉红)/ **M125**(摘控制台判据 ⇒ 起跑钉红),四段协议含注入 diff、还原逐字节一致;`tests/test_atlas_check.py` 30 → 31 → 34 → 36 → 41 条;咬合点 = 下一次 `install.sh --update` / 首装重装配(不溯及已装配副本,同 D108);契约条 = `rings/e2e/reference.md` §5.5 + §7.1 + §10「执行」+ §7.1/§5.5。 **已提交视图累积合并(2026-10-21,`DESIGN.md` D127)**:控制台 committed = 所有 submit 快照按序合并(后写覆盖同名),部分提交不再抹掉历史用例;钉 = `test_committed_snapshot_merges_partial_submits`,变异 **M128**,还原逐字节一致。 **评审指纹与陈旧度(2026-10-21,`DESIGN.md` D128)**:submit 事件与逐用例快照绑定 `prov: {head_sha, scripts_hash, git_dirty, actor_type}`,`scripts_hash` 白名单源码脚本 + `atlas-prepare.mjs`(候选顺序 = Runner 实际执行面 `product/e2e/scripts/` 优先),**产物免疫**(`test-results/`、trace、png、`.last-run.json` 不入指纹);`committed_snapshot(source, current)` 复算 `review_state` 四态闭集,HEAD/脚本变动 ⇒ `stale`、旧账本无指纹 ⇒ `legacy`(两者同等「需重审」:剔除已评审、禁勾、单列计数),`current=None` 不冒充已核验;重跑自动清 `reviewed`(防旧评审挂到新执行)。钉 4 条(`test_scripts_hash_excludes_runtime_artifacts_and_whitelists_scripts` / `test_case_review_state_lifecycle` / `test_submit_attaches_provenance_and_committed_computes_stale` / `test_console_app_js_handles_stale_and_legacy_and_provenance`——末条按 `const canCheck` **出现次数**判,防「grep 读得到、JS 其实炸」的假绿;另 `node --check` 实测可解析 + 真实 git 靶场端到端五态复验);变异 **M129**(摘 stale 判定 ⇒ 新鲜度钉红)四段协议还原一致。 |
|
|
68
68
|
| `写面残留`(M3,`RFC-20261022-01` 批 3,2026-10-23) | `.atlas/runs/write-claims/*.json` ⇔ 任务收口 | `[机器]` glob claims 目录:有未释放 claim ⇒ `WARN` 指名文件与在办任务(最多 3 个 + 总数);无 ⇒ `ok`;无 `write_claim.py`(旧装配)⇒ `SKIP`;损坏 claim 也计入并标「不可解析」 | claims **该不该被释放**(任务是否真完成——归收口人);`.atlas/` 不入库 ⇒ 无 git 历史兜底;`validate_e2e_index` 的事后兜底**保留不撤**(本行不是它的替代);claim 是否能真正阻止**纪律外的绕过**(绕过命令直接写文件 = 无法判) | 每批收口(随 `atlas_check`) | `出厂级(本轮实测)`:`tests/test_write_claim.py` **17 条**(含 8 进程并发 take 唯一赢家、大小写变体同一把锁、EEXIST 后消失重试、空串 release 拒绝、atlas_check 行 ok/WARN 两侧);变异 **M143**(摘 `O_EXCL` ⇒ 并发 8 take 全成功、冲突不再拒绝,钉红)/ **M144**(摘残留 glob ⇒ 残留钉红),四段协议含注入 diff、还原逐字节一致 |
|
|
69
69
|
| `结构漂移`(2026-10-04 新增;2026-10-15 基准改**项目根**,issue #8) | 适配器产物 ⇔ 既有事实表(**键级**) | `[机器]` `refresh_structure` 渲染时对既有事实表做键级对比,增/删行键 ⇒ 落 `<项目根>/.adapter-out/_drift.json`(追加 `_drift-history.jsonl` 供收口对账);本门**只读标记**:有键级变更 ⇒ `WARN` 指名,无 ⇒ `ok`,无 `.adapter-out` ⇒ `SKIP`**且文案带实际路径**(`未找到 <abs path>`——2026-10-15 补:此前只说「无 .adapter-out 产物目录」,而图谱层的同名目录在项目根 ⇒ SKIP 与现场自相矛盾,排查者会先怀疑门坏);事实文件此前不存在(首建)不算漂移 | 键级变更**是否已走 apply 登记**(台账语义机器判不了,归收口对账人终审);语义列内容(由 2.1 `refresh_structure` 行承担) | 收口 / `after_finish` hook 刷新后;**调度者 = `atlas_check`** | `出厂级(本轮实测)`:注入改名路由到 `.adapter-out/routes.json` ⇒ `WARN` 指名 +/−;下一次无键级变更的刷新 ⇒ 标记清除复绿;`tests/test_refresh_facts.py` / `tests/test_atlas_check.py` 喂坏输入必红(2026-10-04 门禁负向用例 #3);**F41** 三消费者(本门 / `refresh_structure` / `_graph`)同址钉 |
|
|
70
70
|
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
|
|
105
105
|
| 门 | 对账平面 | 查什么 | **不查什么** | 预算 + 触发物 | 假绿测试(等级) |
|
|
106
106
|
|---|---|---|---|---|---|
|
|
107
|
-
| `check-all.py --only atlas` | 包面 ⇔ 回归 | `[机器]` **
|
|
107
|
+
| `check-all.py --only atlas` | 包面 ⇔ 回归 | `[机器]` **38 个**测试文件全绿 | 测试**覆盖到的门**是否等于本表的门(本表 2.1–2.4 的覆盖情况逐行标注) | 改任一包内件时 | `出厂级(本轮实测)`:注入必红测试 ⇒ 报 `failed: atlas/tests/test_zzz_fail.py`;移除后全绿且无残留(`3509 §119`;计数随测试文件增加而变,2026-09-29 复核、2026-10-05 图谱批 1 复核为 22、2026-10-07 批 6 残留清偿回灌后为 24、B191-1 对账门后为 25、设计档案豁免对账门后为 26、自感知上报测试后为 27、后端AC对账门后为 28、回灌内核批后为 31、新增 CLI 命令测试后为 32、ADR架构决策资产门后为 33;2026-10-14 知识库/台账子系统退役后为 29;2026-10-15 v0.1.0-beta.14 项目接线测试后为 30;2026-10-15 适配器 TS 形态门(`tests/test_adapter_api.py`)后为 31;2026-10-15 §15 上报体形态门(`tests/test_atlas_report.py`)后为 32;2026-10-25 实现地图锚存活性门(`tests/test_map_anchors.py`)后为 37) |
|
|
108
108
|
| `tests/test_canonical_consistency.py` | **canonical ⇔ canonical**(跨文件自洽) | `[机器]` ① **环引用自明**(`rings/<x>/` 若 x 不存在 ⇒ 该行必须带退役/历史标记) ② **模板键 ⊆ 契约字段**(`templates/stack-profile.yaml` 的段与子键必须在 `shared/stack-profile.md` §2 声明过) ③ **shared 互引可达** ④ **门总账计数 / 状态对账**(计数按对象实算 + `DESIGN.md §13` 禁状态词)⑤ **契约字段引用可达(反向)**(契约正文引用的 `stack-profile` 字段必须在 §2 声明;退役语境 / 自声明历史节除外)+ **⑦ D77 否决面词汇钉(治标,2026-10-15,D79 ③)**(`docs/**.md`〔除 `.vitepress`〕+ `shared/**.md` + 顶层 `README.md` 的活语境行出现 `Leiden / 拓扑聚类 / 自动归域 / 端点到模型 / 阻断全仓` 且无 `D77/D78` 锚 ⇒ 红;**rings/apply/patches 不扫 = 显式盲区**,由 closeout 宣称 sweep 承担;豁免锚词表局部自持、不得并进 `RETIRE_MARKS`——共享会把豁免串门到 ①③⑤)(`gates.md` 里的「N 个测试文件」/「N/N」/「自检 N 条 check 声明」必须与实况一致 —— `3509 §B105` 的「写死计数无对账」教训) | 契约正文的**语义自洽**(例:「第 29 行说可启用原型环、第 9 行说环已移除」)—— **机器判不了** ⇒ 归人(`closeout.md` 触发表「改契约」行 + 独立复核);**实例(2026-10-15)**:F32 已把「薄桩缺失」改成**自愈 + `PASS`**,而 `rings/e2e/reference.md` §4.1/§8.1 与存量接入指南仍写「缺失 ⇒ 硬 FAIL」(同日不同批未传播)—— 代码与契约的**陈述级分叉**正是本条盲区,只有人看得到;表格转义管道符造成的假断链 | 改任一契约 / 模板 / 环目录时 | `出厂级(本轮实测)`:M7 幽灵环引用 / M8 模板未声明键 / M9 幽灵 shared 引用(`3509 §116`)/ **M20 计数写回旧值** / **M21 §13 写回状态词** / **M22 校验器数写回 3** / **M23 DESIGN 校验器数写回 5** / **M24 逐文件计数写回 55** / **M25 摘掉 §2 的 `seed` 字段**(`3509 §125`–`§128` + `§130`,独立审计驱动) |
|
|
109
109
|
| `tests/test_backfill_contract.py` | 回流原语(`apply backfill`)⇔ 契约 | **弱判据(形态)**:模式行 / §5.2 / 分组(上游目标 + 改动类型)/ 攒批(≤ 10 条)/ 独立写面(§5.1)/ 闭环钩子(下游重跑 + 台账挪卷 + 各环门照跑)/ 分型表 —— 片段是否还在 | **「回填被正确执行」本身**(语义,归 agent 纪律 + 各环门 + 台账挪卷);批内是否真的 ≤ 10 条 | 改 `apply/reference.md` 或该瘦桩时 | `出厂级(本轮实测)`:M11 删「攒批 ≤ 10 条」/ M12 删「台账随批挪卷」⇒ 各自红(`3509 §117`) |
|
|
110
110
|
| `tests/test_atlas_report.py`(2026-10-15,§15 上报体) | `atlas report` 上报体 ⇔ §15 | **弱判据(形态)**:无「业务背景」段 + 自述范围(零业务上下文)+ 四段齐(设计缺陷本体 / 触发形态 / 建议 / 证据);`--trigger user` ⇒ 来源写「漏认」、不冒充自动感知;CLI 面有 `--shape`、无 `--context` | **「报告内容是否真是设计问题」本身**(语义,归上报者与架构师);`main()` 的 GitHub 网络与鉴权路径(判据只跑纯函数与 `--help`,不入网) | 改 `scripts/atlas_report.py` 或 §15 时 | `出厂级(2026-10-15 实测)`:`tests/test_atlas_report.py` **3 条**本轮跑绿;形态面**未配变异钉子**(弱判据,语义归人) |
|
|
Binary file
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""atlas_verify(验收执行器,RFC-20261025-02 v2 / D150)七钉。
|
|
3
|
+
|
|
4
|
+
四步版:pre / run=atlas_check / report(留档) / backfill 缓行(写面零触碰)。
|
|
5
|
+
钉对应 RFC §4 ①–⑦;变异 **MB19**(摘「留档短路」⇒ 钉④红)/ **MB20**(run 跳过判据
|
|
6
|
+
恒重跑 ⇒ 单次真跑语义破 ⇒ 钉⑥红),四段协议还原逐字节一致。
|
|
7
|
+
|
|
8
|
+
夹具:temp root + 假 `atlas-prepare.mjs`(node,记调用数/批级 ID/可注入失败)
|
|
9
|
+
+ 假 `.atlas/scripts/atlas_check.py`(记真跑调用数,可注入失败行/成功行/留证构件)。
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import json
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
PKG_ROOT = Path(__file__).resolve().parents[1]
|
|
21
|
+
VERIFY = PKG_ROOT / "scripts" / "atlas_verify.py"
|
|
22
|
+
|
|
23
|
+
FAKE_PREPARE = """import fs from 'fs';
|
|
24
|
+
const cnt = '.verify-prep-count';
|
|
25
|
+
let n = 0; try { n = parseInt(fs.readFileSync(cnt, 'utf8') || '0', 10) || 0; } catch (e) {}
|
|
26
|
+
fs.writeFileSync(cnt, String(n + 1));
|
|
27
|
+
if (process.env.ATLAS_CASE_ID) fs.writeFileSync('.verify-prep-caseid', process.env.ATLAS_CASE_ID);
|
|
28
|
+
try { if (fs.readFileSync('.verify-prep-mode', 'utf8').trim() === 'fail') { console.error('prep boom'); process.exit(1); } } catch (e) {}
|
|
29
|
+
console.log('prepared');
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
FAKE_CHECK = """import json as _json, sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
root = Path(sys.argv[sys.argv.index("--root") + 1])
|
|
35
|
+
inv = root / ".atlas-run-invocations"
|
|
36
|
+
n = int(inv.read_text()) + 1 if inv.exists() else 1
|
|
37
|
+
inv.write_text(str(n))
|
|
38
|
+
mode = "ok"
|
|
39
|
+
mp = root / ".verify-check-mode"
|
|
40
|
+
if mp.exists():
|
|
41
|
+
mode = mp.read_text().strip()
|
|
42
|
+
run_dir = root / "product/e2e/scripts/.atlas-run"
|
|
43
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
if mode == "fail":
|
|
45
|
+
(run_dir / "trace.zip").write_bytes(b"fake-trace")
|
|
46
|
+
print(" FAIL e2e_real_run [FAIL] 判据不成立(注入)")
|
|
47
|
+
print("FAIL (详细错误堆栈已落盘)")
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
(run_dir / "results.json").write_text("{}")
|
|
50
|
+
(run_dir / "e2e-summary.json").write_text("{}")
|
|
51
|
+
print(" ok assembly_version 源包与装配态一致")
|
|
52
|
+
print(" ok e2e_real_run 11/11")
|
|
53
|
+
print("OK")
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_verify():
|
|
58
|
+
spec = importlib.util.spec_from_file_location("atlas_verify_under_test", VERIFY)
|
|
59
|
+
mod = importlib.util.module_from_spec(spec)
|
|
60
|
+
spec.loader.exec_module(mod)
|
|
61
|
+
return mod
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def make(root: Path, prep: bool = True, check: bool = True) -> None:
|
|
65
|
+
(root / "product" / "e2e" / "scripts").mkdir(parents=True, exist_ok=True)
|
|
66
|
+
(root / ".atlas" / "scripts").mkdir(parents=True, exist_ok=True)
|
|
67
|
+
if prep:
|
|
68
|
+
(root / "product/e2e/scripts/atlas-prepare.mjs").write_text(FAKE_PREPARE, encoding="utf-8")
|
|
69
|
+
if check:
|
|
70
|
+
(root / ".atlas/scripts/atlas_check.py").write_text(FAKE_CHECK, encoding="utf-8")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run(root: Path, *extra: str) -> subprocess.CompletedProcess:
|
|
74
|
+
return subprocess.run([sys.executable, str(VERIFY), "--root", str(root), *extra],
|
|
75
|
+
capture_output=True, text=True, timeout=300)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _invocations(root: Path) -> int:
|
|
79
|
+
p = root / ".atlas-run-invocations"
|
|
80
|
+
return int(p.read_text()) if p.exists() else 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_pre_failure_stops_batch() -> None:
|
|
84
|
+
"""钉①:prepare 失败 ⇒ 停在 pre、结构化报告、atlas_check 不被调、无留档(下游不静默跳过)。"""
|
|
85
|
+
with tempfile.TemporaryDirectory() as td:
|
|
86
|
+
root = Path(td)
|
|
87
|
+
make(root)
|
|
88
|
+
(root / ".verify-prep-mode").write_text("fail", encoding="utf-8")
|
|
89
|
+
r = run(root, "--json")
|
|
90
|
+
assert r.returncode == 1, (r.returncode, r.stdout, r.stderr)
|
|
91
|
+
data = json.loads(r.stdout)
|
|
92
|
+
assert data["failed_step"] == "pre" and data["ok"] is False, data
|
|
93
|
+
assert _invocations(root) == 0 # run 未执行
|
|
94
|
+
assert not (root / "product/e2e/reviews").exists() or not list((root / "product/e2e/reviews").iterdir())
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_batch_case_id_is_explicit_batch_token() -> None:
|
|
98
|
+
"""钉②:批级 `ATLAS_CASE_ID` 值形态 = `verify-batch`(显式批标识,非用例 ID;D121 逐用例归控制台)。"""
|
|
99
|
+
with tempfile.TemporaryDirectory() as td:
|
|
100
|
+
root = Path(td)
|
|
101
|
+
make(root)
|
|
102
|
+
assert run(root).returncode == 0
|
|
103
|
+
assert (root / ".verify-prep-caseid").read_text().strip() == "verify-batch"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_run_failure_report_keeps_gate_rows_and_evidence() -> None:
|
|
107
|
+
"""钉③:run 失败 ⇒ 报告含门行原样 + 留证构件路径(D108 构件在失败报告可见)。"""
|
|
108
|
+
with tempfile.TemporaryDirectory() as td:
|
|
109
|
+
root = Path(td)
|
|
110
|
+
make(root)
|
|
111
|
+
(root / ".verify-check-mode").write_text("fail", encoding="utf-8")
|
|
112
|
+
r = run(root, "--json")
|
|
113
|
+
assert r.returncode == 1, (r.stdout, r.stderr)
|
|
114
|
+
data = json.loads(r.stdout)
|
|
115
|
+
assert data["failed_step"] == "run"
|
|
116
|
+
assert any(row["name"] == "e2e_real_run" and row["status"] == "FAIL" for row in data["rows"]), data
|
|
117
|
+
assert any("trace.zip" in p for p in data["evidence"]), data["evidence"]
|
|
118
|
+
# 失败不落档(重跑不被短路)
|
|
119
|
+
assert data["archive"] is None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_existing_archive_short_circuits_whole_batch() -> None:
|
|
123
|
+
"""钉④:留档在 ⇒ 整批短路——pre 与 atlas_check 都不执行。
|
|
124
|
+
变异 **MB19**:摘掉留档短路 ⇒ 本钉红。"""
|
|
125
|
+
with tempfile.TemporaryDirectory() as td:
|
|
126
|
+
root = Path(td)
|
|
127
|
+
make(root)
|
|
128
|
+
d = root / "product/e2e/reviews"
|
|
129
|
+
(d / "2026-01-01-verify").mkdir(parents=True) # 任意既有 -verify 留档形态(当日由脚本判定,此处经 --json 验证语义)
|
|
130
|
+
(d / "2026-01-01-verify/README.md").write_text("# old", encoding="utf-8")
|
|
131
|
+
mod = load_verify()
|
|
132
|
+
# 直接验证短路判据:archive_dir 存在 ⇒ main 短路(用 monkeypatch 日期不可行 ⇒ 以目录形态构造当日)
|
|
133
|
+
import datetime as _dt
|
|
134
|
+
today_dir = d / f"{_dt.date.today().isoformat()}-verify"
|
|
135
|
+
today_dir.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
(today_dir / "README.md").write_text("# old", encoding="utf-8")
|
|
137
|
+
r = run(root, "--json")
|
|
138
|
+
assert r.returncode == 0, (r.stdout, r.stderr)
|
|
139
|
+
data = json.loads(r.stdout)
|
|
140
|
+
assert data["short_circuit"] is True and data["real_run_count"] == 0, data
|
|
141
|
+
assert not (root / ".verify-prep-count").exists() # pre 未执行
|
|
142
|
+
assert _invocations(root) == 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_resume_semantics_pre_always_reruns_run_by_artifact() -> None:
|
|
146
|
+
"""钉⑤:断点语义——pre 恒重跑(幂等廉价,如实);run 按产物跳过(results 在 ⇒ 沿用并注记)。"""
|
|
147
|
+
with tempfile.TemporaryDirectory() as td:
|
|
148
|
+
root = Path(td)
|
|
149
|
+
make(root)
|
|
150
|
+
run_dir = root / "product/e2e/scripts/.atlas-run"
|
|
151
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
(run_dir / "results.json").write_text("{}", encoding="utf-8")
|
|
153
|
+
(root / ".verify-check-mode").write_text("fail", encoding="utf-8") # 若误调 check ⇒ 立刻暴露
|
|
154
|
+
r = run(root, "--json")
|
|
155
|
+
assert r.returncode == 0, (r.stdout, r.stderr)
|
|
156
|
+
data = json.loads(r.stdout)
|
|
157
|
+
assert (root / ".verify-prep-count").read_text().strip() == "1" # pre 恒重跑
|
|
158
|
+
assert _invocations(root) == 0 # run 按产物跳过
|
|
159
|
+
run_step = next(s for s in data["steps"] if s["name"] == "run")
|
|
160
|
+
assert run_step["status"] == "SKIP" and "原批退出码未知" in run_step["detail"], run_step
|
|
161
|
+
assert data["archive"] # 留档补齐
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_exactly_one_real_run_per_green_batch() -> None:
|
|
165
|
+
"""钉⑥:全批只跑一次真跑(atlas_check 恰被调一次;执行器不另起 Playwright)。
|
|
166
|
+
变异 **MB20**:run 跳过判据摘除/恒重跑 ⇒ 语义破 ⇒ 本钉(与钉⑤)红。"""
|
|
167
|
+
with tempfile.TemporaryDirectory() as td:
|
|
168
|
+
root = Path(td)
|
|
169
|
+
make(root)
|
|
170
|
+
assert run(root).returncode == 0
|
|
171
|
+
assert _invocations(root) == 1, _invocations(root)
|
|
172
|
+
assert json.loads(run(root, "--json").stdout)["short_circuit"] is True # 二跑被留档短路,不二次真跑
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_missing_root_fails_clean_without_writes() -> None:
|
|
176
|
+
"""钉⑦(收窄口径的机器面):参数错 ⇒ 退出码 2、不写任何盘。"""
|
|
177
|
+
with tempfile.TemporaryDirectory() as td:
|
|
178
|
+
empty = Path(td) / "nope"
|
|
179
|
+
empty.mkdir()
|
|
180
|
+
r = subprocess.run([sys.executable, str(VERIFY), "--root", str(Path(td) / "absent")],
|
|
181
|
+
capture_output=True, text=True)
|
|
182
|
+
assert r.returncode == 2, (r.returncode, r.stderr)
|
|
183
|
+
assert list(empty.iterdir()) == [] # 未写盘
|