atlas-project-harness 0.2.0-beta.6 → 0.2.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/DESIGN.md +3 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/rings/e2e/reference.md +2 -2
- package/scripts/__pycache__/gen_e2e_scripts.cpython-312.pyc +0 -0
- package/scripts/__pycache__/pkg_digest.cpython-312.pyc +0 -0
- package/scripts/gen_e2e_scripts.py +24 -15
- package/shared/gates.md +3 -2
- package/tests/__pycache__/test_gen_e2e_scripts.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_map_anchors.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/test_gen_e2e_scripts.py +28 -11
- package/tests/test_map_anchors.py +469 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [v0.2.0-beta.7] - 2026-10-25
|
|
8
|
+
|
|
9
|
+
### 🏃 真跑日批:D80② 升档兑现(D149)+ 真跑日协议(D148)+ D108 落点勘误
|
|
10
|
+
|
|
11
|
+
* **D80② 升档兑现(D149)**:`e2e.app_login` 未声明 ⇒ 生成器推 **`NOT_READY`(退出码 3 + 机读键 `"app_login"`)**,与 `app_base_url` 同一通道族(`--json` 面 `{"ok": false, "not_ready": "app_login", …}`),不再「占位生成 + WARN」半生成——硬前提的输入不得靠「缺失即静默放行」吸收;文案按 D103 相抵点指路:「有门但三态都装不下」(图形验证码 / 服务端 cookie 会话 / 登录响应体无令牌)⇒ 人工会话取出,不是「补声明」的对象。RFC-20261015-02 预告的时间锚「下一次真跑准备日」到期执行。钉:旧钉改判(半生成 ⇒ 全不生成)+ 新 `--json` 面钉;变异 **MB17**(退回 WARN 半生成)/ **MB18**(机读键写错或缺 message)。
|
|
12
|
+
* **真跑日协议(D148)**:runbook `docs/guide/real-run-day.md`——detached worktree 隔离执行(不碰用户 checkout、不提交)/ D145 manifest 配对先行 / 七项咬合清单 / **注红条款**(注红 red 不计缺陷判据)。首轮已执行出库,回写见 duty-review 三件(QUEUE / README / bugs.md)。
|
|
13
|
+
* **D108 落点勘误 + 留存窗口补定**:失败留证实际落 **`.atlas-run/`**(生成物早有显式 `outputDir`,Playwright 构件目录跟着 outputDir 走——D108 首版契约误写缺省 `test-results/`,2026-10-25 真跑日实咬勘误,构件形态 = `trace.zip + test-failed-*.png` 成对);**留存窗口 = 到下一次 `playwright test` 起跑**(Playwright 启动即清空 outputDir ⇒ 跨轮取证须起跑前归档)。契约 §7.1 / 生成物注释 / RFC 勘误节同批改口;咬合点不变 = 重装配(`install --update` / 首装),不溯及已装配副本。
|
|
14
|
+
* **真跑日首轮三发现入档**(均为观察、不计缺陷):① 落点勘误(上行);② 留证留存窗口(升格条件 = 首个有缺陷指控价值的构件在复跑起跑后被清空);③ 「栈起没起」探针盲区首个活体实证(前端 200 / 内网后端不可达 ⇒ 白屏全红、形似用例失败实为环境红)+ 半就绪变体(网关 200 / 业务 API 悬挂,唯有打真实业务 API 才能判)。另:案 9(`e2e_index` WARN 基线 7 条)升室待拍板;WS-C 回执状态收窄、WS-D 观察锚写作约定随批落地;消融首跑复核留档 `research/2026-10-23-ablation-first-run-recheck.md`。
|
|
15
|
+
|
|
7
16
|
## [v0.2.0-beta.6] - 2026-10-23
|
|
8
17
|
|
|
9
18
|
### 🧱 装配基线 manifest:漂移逐文件指名(D145)
|
package/DESIGN.md
CHANGED
|
@@ -574,7 +574,7 @@ adapters:
|
|
|
574
574
|
| D105 | 生成的运行器配置**必须声明 `expect.timeout` 下限**(2026-10-17,issue #28;真跑现场驱动) | ① **病灶**:#27 落地后真跑仍 `3 failed 1 passed`,而**元素真在**(探针实测 `disabled=true` + 合规 `aria-label`)、三处导航 URL 全对 ⇒ 红因是**断言超时**:生成的 `playwright.config.ts` **没有 `expect` 段** ⇒ 继承 Playwright **默认 5s**,而该应用首屏实测 **14,184ms**(dev bundle 13.8MB)⇒「元素还没渲染」被误判成「元素不存在」= **假红**。② **定案**:生成器在两处 config 分支都发射 `expect: { timeout: Number(process.env.ATLAS_EXPECT_TIMEOUT ?? 20_000) }`(缺省 20s,仍远小于 test 超时;`ATLAS_EXPECT_TIMEOUT` 为运维覆盖口,与 `ATLAS_SLOWMO` 同族)。**它不是「固定等待」**:`expect` 是轮询式 auto-wait,本改只抬高**上限**,不引入 sleep,不破 §5.5「不得固定等待」。③ **被否**:只给守卫 `toHaveCount` 加 timeout(其余断言仍 5s,慢一点又撞);调 test 超时(那只是兜底,不解决断言级假红);固定 sleep(契约明禁,且共享靶场必飘)。④ **代价(如实)**:真失败从 5s 变 20s 才红;可在慢环境调低。⑤ **钉与变异**:`test_runner_config_declares_expect_timeout_floor`(配置含 `expect: {` + `20_000` + 覆盖口),变异 **MB5**(摘掉发射 ⇒ 红,还原逐字节一致)。 |
|
|
575
575
|
| D106 | **真跑门的执行 cwd = 生成物目录**(2026-10-17,issue #29;真跑现场驱动) | ① **病灶**:#27/#28 落地后真跑仍 `3 failed 1 passed`,而同一份产物 **`cd scripts` 直跑 8.2s 全绿** (`--grep E2E-APP-AGENT-DETAIL-001`)——差异 = **cwd**:`atlas_check` 用 `cwd=项目根` + `-c <相对路径>` 跑,而生成物里的相对路径全是 **cwd 相对**的(`storageState: ".atlas-auth/state.json"`、`outputDir: ".atlas-run"`、mock fixture)⇒ Playwright 把 storageState 解析到 `<root>/.atlas-auth/`(**并在那儿写了一个空 state**)⇒ **读不到人的会话** ⇒ 未登录态跑用例 ⇒ 断言 20s 超时**假红**。控制台(`e2e_console`)本来就是 `cd scripts` 再跑。② **定案**:本门改为 **`cwd=<scripts_dir>` + `-c playwright.config.ts`**(与执行器同一口径,两个入口不再各说各话)。③ **被否**:把生成物里的相对路径全改成绝对路径(派生件带机器绝对路径 = 不可移植、且破「生成物 = 确定性派生物」);只修 setup 的 state 路径(其余相对路径仍会漂)。④ **教训**:**「同一份产物在两个入口下跑出不同结论」必须先查 cwd / 环境**,否则会把执行器差异误判成用例或实现缺陷(本轮依次误判为「超时不足」→ 已由 D105 兜住超时面,再查出真因 = cwd)。⑤ **钉与变异**:`test_check_e2e_runs_with_scripts_dir_as_cwd`(截 `run()` 的 (cmd, cwd),断言 cwd = scripts 且 cmd 里无相对配置路径),变异 **MB6**(退回 root + 相对 `-c` ⇒ 红,还原逐字节一致)。 |
|
|
576
576
|
| D107 | 零步基线**每个探针都必须「缺席即判不成立」,不得等挂载**(2026-10-17,issue #30;真跑现场驱动) | ① **病灶**:`atlas_check --page app-agent-detail` ⇒ 两条真用例全绿,唯零步基线 `Test timeout of 30000ms exceeded`。带参页面的页级路由本就不渲染详情(`D104` 允许的形态),而 `attr` 探针发射的是 `getByTestId(t).first().getAttribute(n)` —— **无 `count()` 守卫**,Playwright 对**未挂载**元素的 `getAttribute` 会等到默认超时 ⇒ 4 探针 × 2 趟撑爆 test 预算。契约 §5.5 早已明写「基线探不到元素按 `null = 不成立` 处理」⇒ **等挂载 = 违约**(A1 判错,非项目缺陷)。② **定案**:`attr` 探针加 `count() > 0 &&` 前置(缺席 ⇒ `false` = 不成立);并把该性质上升为**生成器的通用不变式**:**任何探针表达式都必须带缺席守卫**(`count()) > 0` 或 `tsVis(` / `tsVisible(` / `atlasValueOf(`)——由通用钉守着,防未来新增动词再犯。③ **被否**:给基线设更大的 test 超时(把「等挂载」合法化,掩盖违约);带参页面跳过基线(契约 §5.5 已选择「回 null」而非「跳过」);只在 v2 侧声明 `守卫:`(那是**资产选择题**,不修 atlas 违约面)。④ **钉与变异**:`test_zero_step_probes_never_wait_for_attachment`(对**全部**探针做守卫检查)+ 变异 **MB7**(摘 `attr` 的守卫 ⇒ 红,还原逐字节一致)。⑤ **出库判定**:探针行为属**运行面** ⇒ 待下一轮真跑复验后出库。 |
|
|
577
|
-
| D108 | E2E 真跑失败**自动留证**——trace / screenshot 只在失败落盘(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence`;外部发版雷达首跑 5 条机制级情报中唯一采纳项,独立评审两轮 + 维护者拍板「按推荐继续」) | ① **病灶**:真跑偶发红(节拍类)现场的取证上限 = 文本尾部 80 行(`e2e_console.py:520`(采集即截)/ `:582`(docstring 失败才带尾)/ `:591-592`(落账条件)),元素几何 / 命中点 / 时序类判据只能靠 `/tmp` 临时探针(跨会话不可复算;D98 动词批与案 6 两次付费先例);`duty-review/bugs.md` 未解观察(hover 禁用按钮首跑偶发 `Element is not visible`,3 连跑全绿)的**升判条件**「复现 ≥2 次且能给出固定节拍证据(trace / 连续截图)⇒ 转 DR4」悬空在等证据源(`bugs.md:17`)。② **定案**:生成物两轨 playwright 配置补 `trace: 'retain-on-failure'` + `screenshot: 'only-on-failure'`——失败现场自动落 trace(DOM 快照 + 网络 + 操作日志,trace viewer 可逐帧回放)与截图到 Playwright 缺省 `test-results/`,**全绿轮零新增构件**;不溯及已装配副本 ⇒ **咬合点 = 下一次 `install.sh --update` / 首装重装配**,重装配前「契约已声明留证而现场无 trace」属**如实态、非缺陷**,巡检不得误报(对齐 `shared/single-source.md` §3 迁移仪式「新源生效 ⇔ 旧源显式失效」);留证含业务内容,只落采纳仓本地、不进 atlas 上报通道(§15 零业务上下文出站面不破);不解决 D103「人工会话取出 ⇒ CI 不可复现」盲区(trace 只重放**已发生**的失败,真身是登录态不是取证手段)。③ **被否**:`video` 缓行(trace 自带回放器,video 是粗粒度投影,多一键 = 双轨同构面 +2 无收益;缓行条件 = 实跑出现 trace 覆盖不了的取证需求才另起决策行);全量常录 `on`(磁盘 × 每用例,成功轮留证无消费方);留证键加环境开关(= 给「成功轮也留证」开后门);独立录制管理器 / 上传通道 / 轮转清理(运行时平台机制,Atlas 零运行时);**失败自动重试 N 次**(与「节拍类首跑不坐实、复跑复现才改判」(`bugs.md` E2E 段)方向相反,会把真缺陷静默吸收成绿——**防复读机裁决**已落 `duty-review/QUEUE.md` 归档区,外部情报再提 retry 引用可径行否决);托管云沙盒会话(违「项目自足」,同归档区裁决)。④ **钉与变异**:钉 3 条——`test_failure_evidence_both_tracks_declare_trace`(双轨同构:`mode:none` 轨与预置登录轨都必须含两键)/ `test_failure_evidence_values_are_closed`(值形态封闭:恰为 retain-on-failure / only-on-failure,`video` 混入 / `on` 常录 ⇒ 红)/ `test_failure_evidence_has_no_override_switch`(无覆盖口),`tests/test_gen_e2e_scripts.py` 74 → **77** 条;变异 **MB10**(摘 none 轨 trace/screenshot 行)/ **MB11**(值改 `on`),`mutation_proof` 四段协议双绿(基线 3 passed → 注入 rc=1 → 还原逐字节一致 → 复绿),注入 diff 摘录于 `proposals/REVIEW-2026-10-18-e2e-failure-evidence.md`;契约条 = `rings/e2e/reference.md` §7.1「失败留证(硬,失败才落盘)」,门行 = `shared/gates.md` 2.2。⑤
|
|
577
|
+
| D108 | E2E 真跑失败**自动留证**——trace / screenshot 只在失败落盘(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence`;外部发版雷达首跑 5 条机制级情报中唯一采纳项,独立评审两轮 + 维护者拍板「按推荐继续」) | ① **病灶**:真跑偶发红(节拍类)现场的取证上限 = 文本尾部 80 行(`e2e_console.py:520`(采集即截)/ `:582`(docstring 失败才带尾)/ `:591-592`(落账条件)),元素几何 / 命中点 / 时序类判据只能靠 `/tmp` 临时探针(跨会话不可复算;D98 动词批与案 6 两次付费先例);`duty-review/bugs.md` 未解观察(hover 禁用按钮首跑偶发 `Element is not visible`,3 连跑全绿)的**升判条件**「复现 ≥2 次且能给出固定节拍证据(trace / 连续截图)⇒ 转 DR4」悬空在等证据源(`bugs.md:17`)。② **定案**:生成物两轨 playwright 配置补 `trace: 'retain-on-failure'` + `screenshot: 'only-on-failure'`——失败现场自动落 trace(DOM 快照 + 网络 + 操作日志,trace viewer 可逐帧回放)与截图到 Playwright 缺省 `test-results/`,**全绿轮零新增构件**;不溯及已装配副本 ⇒ **咬合点 = 下一次 `install.sh --update` / 首装重装配**,重装配前「契约已声明留证而现场无 trace」属**如实态、非缺陷**,巡检不得误报(对齐 `shared/single-source.md` §3 迁移仪式「新源生效 ⇔ 旧源显式失效」);留证含业务内容,只落采纳仓本地、不进 atlas 上报通道(§15 零业务上下文出站面不破);不解决 D103「人工会话取出 ⇒ CI 不可复现」盲区(trace 只重放**已发生**的失败,真身是登录态不是取证手段)。③ **被否**:`video` 缓行(trace 自带回放器,video 是粗粒度投影,多一键 = 双轨同构面 +2 无收益;缓行条件 = 实跑出现 trace 覆盖不了的取证需求才另起决策行);全量常录 `on`(磁盘 × 每用例,成功轮留证无消费方);留证键加环境开关(= 给「成功轮也留证」开后门);独立录制管理器 / 上传通道 / 轮转清理(运行时平台机制,Atlas 零运行时);**失败自动重试 N 次**(与「节拍类首跑不坐实、复跑复现才改判」(`bugs.md` E2E 段)方向相反,会把真缺陷静默吸收成绿——**防复读机裁决**已落 `duty-review/QUEUE.md` 归档区,外部情报再提 retry 引用可径行否决);托管云沙盒会话(违「项目自足」,同归档区裁决)。④ **钉与变异**:钉 3 条——`test_failure_evidence_both_tracks_declare_trace`(双轨同构:`mode:none` 轨与预置登录轨都必须含两键)/ `test_failure_evidence_values_are_closed`(值形态封闭:恰为 retain-on-failure / only-on-failure,`video` 混入 / `on` 常录 ⇒ 红)/ `test_failure_evidence_has_no_override_switch`(无覆盖口),`tests/test_gen_e2e_scripts.py` 74 → **77** 条;变异 **MB10**(摘 none 轨 trace/screenshot 行)/ **MB11**(值改 `on`),`mutation_proof` 四段协议双绿(基线 3 passed → 注入 rc=1 → 还原逐字节一致 → 复绿),注入 diff 摘录于 `proposals/REVIEW-2026-10-18-e2e-failure-evidence.md`;契约条 = `rings/e2e/reference.md` §7.1「失败留证(硬,失败才落盘)」,门行 = `shared/gates.md` 2.2。⑤ **出库判定(2026-10-25 真跑日出库,D148 首轮)**:红轮 28 失败构件(trace.zip + 截图成对)/ 绿轮零构件,两面实证(咬合记录 = `shared/gates.md` 2.2「D108 首次咬合」);**落点勘误随批**(实际 `.atlas-run/`,非首版误写的缺省 `test-results/`)+ **留存窗口观察在案**(Playwright 起动清空 outputDir ⇒ 留证只存到下一次起跑,`duty-review/bugs.md` E2E 段带升格条件)。 |
|
|
578
578
|
| D109 | **逐用例宿主注入 URL 参数通道**:`goto` 查询门**重靶** + 项目级白名单(2026-10-18;字段与门落在 `rings/e2e/reference.md` §5.5 / §8.2;采用方现场驱动) | ① **病灶**:`会话:`(D89)解决的是 **sessionStorage** 那一个消费面;而某类应用的被测参数**只从载入地址的常规查询串读**(`window.location.search`)⇒ **无合法写法**(`goto` 带 `?` 是硬 FAIL、`auth` 只管登录态、`seed:` 绑数据实体)—— D104 ① 已把这条记为「三条各自成立 ⇒ 无合法写法」,当日改选 B′(base query 全局转发)并把「`会话:` 扩 URL 参数档」列入被否清单。采用方现场(2026-10-18):两条用例被迫写成**同一前提**(都走 sessionStorage)⇒ 重复,而查询串那一面**零覆盖**。② **重靶取证(关键,本行的依据)**:原门(§5.7,2026-09-23;事故 `3509 §B20` = `E2E-LOGIN-003` 断言 `?state=forbidden` 演示横幅**自身文案**)的**靶子随 E1 原型环退役而消失** —— `?state=` 只出现在本文件 `:460` 的**已作废**决策行(D8 两段式 / D56 原型交互分级);E1 后**无任何决策行**重新论证该门;`duty-review/bugs.md` 与 `CHANGELOG.md` **零起**该门事故;今日它在 D104 ① 里的身份是「无合法写法」的**阻碍** = 制造成本,而非拦事故。该门今日真正剩下的(`reference.md` 旧 §5.7 邻域的那条)是**另一族、不限于原型**的纪律:「前置数据态只能用真实界面动作达到」—— **而这族边界在重靶前后都由项目级白名单守**,与「`goto` 带不带 `?`」无关 ⇒ 取舍只剩成本与表达力。③ **定案**:(a)门**重靶** `goto 不带查询参数` → **`goto 查询键须已声明`**(键须 ① ∈ `e2e.query_preset.keys` ∧ ② ∈ 本用例 `参数:`;**原靶仍在 FAIL 面内**:未声明键照样红);(b)用例块新增可选 `参数:`(**只声明键**,值内联在 `step` 的 `goto` 查询串里 = 单一来源),**守卫导航取该用例首个带参 `goto` 的查询串**(否则守卫读的是**无前提的 DOM**:参数决定「哪些元素在场」的页面上守卫会在 `goto` 后即红);(c)`_support/url_merge.merge_base_query` 的「目标自带 `?` ⇒ 原样返回」**暗位转正为并集档**(base 在前、**同键 ⇒ 用例胜**)——该分支原注释即写着「§5.7 下不可达,**为未来扩展留位**」,且既有生成物无一带 query 的 `goto` ⇒ **零回归**;(d)三道新门(声明有效 / 值形态 / 同键双载体)+ 生成期同源中止(双保险)。④ **被否**:(A)「值只写 `参数:` 行、`goto` 保持无 query」—— 其立论之一是「保住 §5.7 事故防护」,取证后该论据**作废**(见 ②),取舍只剩成本与表达力,B 胜;其余照 D104 ⑤(运行期 TS 助手 / 把「同名键以目标为准」写成通则)。⑤ **边界(如实登记,不得 oversell)**:允许带白名单查询串后,「数据态直达」那族纪律的**机器面只剩白名单本身**(「项目说合法就合法」)—— 与 `会话:` 同族、是本设计的本来形态;登记以防后人以为还有别的网在兜。⑥ **钉与变异**:`tests/test_validators.py` **116 → 123**(重靶钉改名 1 + `参数:` 门 6);`tests/test_gen_e2e_scripts.py` **+6**(两处导航面承载 / 基线不施加 / 无白名单中止 / 白名单外中止 / 死声明中止 / 值形态中止);`_support/url_merge.py` 并集档(原「原样保留」单元钉随之改判)。变异 **MB12**(并集档退回「原样返回」⇒ 单元钉 + 守卫承载钉齐红)、**MB13**(守卫不追加用例参数 ⇒ 守卫面钉红),两枚均四段协议通过、还原逐字节一致,注入 diff 见任务件 `.trellis/tasks/10-18-atlas-url-param-channel/check-report.md`。设计依据 = 采用方现场(`src/entry/main/app.tsx:18/:36` 两个消费面 + `validators/validate_e2e_index.py` 旧门 + `_support/url_merge.py:48`)。 |
|
|
579
579
|
| D110 | 控制台**逐用例数据准备钩子**:项目自带 `product/e2e/scripts/atlas-prepare.mjs` ⇒ 执行前必跑、失败即拒(2026-10-18,v3 现场实证移植批;病灶账 = `duty-review/bugs.md` 005/006 链 companion;契约条 = `rings/e2e/reference.md` §10「执行」行 / `e2e_console` docstring) | ① **病灶**:共享状态型靶场(SPA + 服务端态)的用例 `precondition` 属**文档性声明**、运行器不自动归零 ⇒ 残留实体使 count 类断言系统性偏移(现场实测 Expected 1 · Received 5);且控制台此前对「执行前重置」零消费——`e2e.reset`(`shared/stack-profile.md` §2)只被 `atlas_check` 真跑门执行(**批级**:整轮一次),逐条执行面无 per-case 前提通道。② **定案**:执行器在每条用例**计时起点之前**跑该脚本(`node <e2e.scripts_dir>/atlas-prepare.mjs`,cwd = 项目根,180s 上限,**不占**用例超时窗);失败(非零)/ 超时 / 起不动 ⇒ **拒绝执行该条**:`done` 事件按完整形状写入(`ok=False` / `exit` 如实 / `prep_failed=True`),失败原文进「输出」面板——不静默跑注定偏移的用例(对齐执行器「解析不成立 ⇒ 诚实拒绝」同族口径)。③ **与 `e2e.reset` 的关系(登记,防后人重复起案)**:**不并轨**——`reset` 是批级且值形态为 shell 命令串(项目在 profile 声明)、本钩子是逐条且文件约定(项目在 `<e2e.scripts_dir>` 放脚本),现场 `reset: null`、两粒度各占一格;控制台若要消费 `reset` 须另起决策行。④ **被否**:由生成脚本内自查归零(把环境初值达序藏进生成物 = 静默吸收);自动执行 precondition 声明(人读文档,无机器可执行形态);prepare 失败仍照跑(偏离移用例 = 假绿通道)。⑤ **钉与变异**:`tests/test_e2e_console.py` **13 → 14**(钩子先于真跑 / 失败拒绝并带 `prep_failed` / 起不动 `exit=-2` 三型一钉);变异 **M94**(摘 prepare 钩子段 ⇒ 钉红,还原逐字节一致)。设计依据 = v3 装配副本手改(现场实证可用);主仓移植时审校掉装配副本失败路径的 `self._lines` 属性错拼(会 AttributeError 让执行线程挂死、`running` 永真),统一改走 `self._emit`(持锁 + notify)。 |
|
|
580
580
|
| D111 | `refresh` 落盘节拍参数化:发射 `ATLAS_REFRESH_SETTLE` 注入的 `waitForTimeout`(先等后刷),默认 0 行为不变(2026-10-18,v3 现场实证移植批;病灶账 = `duty-review/bugs.md` 005 链;契约条 = `rings/e2e/reference.md` §7.1 / §5.5 `refresh` 行) | ① **病灶**:`refresh` 裸 `reload` 对启用**节流型自动保存**的项目(距上次成功保存 < 节流窗则延迟落库)必丢步骤编辑内容(现场实测 Expected 1 · Received 0——刷新发生在保存落库之前)。② **定案**:发射**先等后刷**两行——`await page.waitForTimeout(Number(process.env.ATLAS_REFRESH_SETTLE || "0"));` + `await page.reload();`;**默认 0 ⇒ 生成物行为与既有契约一致**。通道选择同 F11:Playwright `test` CLI 无逐用例注入面,env 是唯一通道。③ **零业务常数(硬)**:毫秒数**不进生成器**——它属项目侧**运行时**前提,现场以 `ATLAS_REFRESH_SETTLE=<毫秒>` 于启动面注入(对齐 `ATLAS_SLOWMO` / `ATLAS_EXPECT_TIMEOUT` 同族);它也不是 §5.5「固定 sleep」禁令所指(该禁令管**用例正文**不把共享靶场节拍写死;本行是执行环境配置面)。④ **被否**:硬编码任何项目常数进源包(§15 零业务上下文纪律);生成期读项目配置算节拍(生成物 = 确定性派生物,节拍是运行时动态值)。⑤ **钉与变异**:`tests/test_gen_e2e_scripts.py` **83 → 84**(先等后刷顺序钉);变异 **M95**(摘 `waitForTimeout` 发射行 ⇒ 钉红,还原逐字节一致)。 |
|
|
@@ -614,6 +614,8 @@ adapters:
|
|
|
614
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
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
616
|
| D147 | **显式「无」通则** + `FORBIDDEN` 三处单源 + 两处机器检查首版 WARN(2026-10-23;`RFC-20261023-02` ④;`shared/global-rules.md` §5、`validators/validate_prd.py`、`validators/validate_structure.py`、`templates/prd/domain.md`、`rings/prd/reference.md` §7、`rings/e2e/reference.md` §10.1) | ① **病灶**:`无` / `不适用(理由:…)` / `未覆盖(沿用实然)` / 控制台「未创建」四套写法并存,「空表/空节」与「评估过为无」同形;`FORBIDDEN` 不抓空表,也不抓 `[必填:…]` 占位残留;两校验器与 §5 的词表各说各话。② **定案**:通则 = 「可无内容不得留空白,必须有结论」(`无`(理由可选)/既有语义词,词表单源 = §5);`FORBIDDEN` 增补 `[必填` / `[条件必填` 并三处同步 + 守恒钉 `test_forbidden_vocabulary_single_source`;机器落点只两处(§六 空表/空节无 `无` ⇒ **WARN 首版**;§七「影响」空 ⇒ FAIL),均独立 check 行、登记门册;模板占位指令统一「无则写 无(理由可选)」;翻转触发器 = 连续两收口周期真实遗漏 ≥1 且误报 0 ⇒ 升 FAIL,统计落 per-域收口留痕 README「可无项空表:N / 误报:N」。③ **被否**:全量扫描所有可无节 FAIL(违反「落门先问语料」);把四词语义合并成单一写法(丢语义边界)。④ **钉与变异**:`test_rfc20261023_02_optional_section_explicit_none`(空节 / 空表 / 显式 `无` / 开户最小集四态);**M-new-2** 判据恒真 ⇒ 钉红。 |
|
|
617
|
+
| D148 | **真跑日协议**:一次「重装配 + 真跑」把已立待咬的能力一次咬合,**detached worktree 隔离执行**(2026-10-24;runbook = `docs/guide/real-run-day.md`;维护者拍板「都按你推荐的来」+ 边界「v3 用户分支 `dev_20261120` 不碰、自建 worktree、不提交」;源自设计优化方案 v2 WS-B,评审结论已并入) | ① **病灶**:咬合点分散且都挂在「下一次真跑」上(D108 留证 / adapters 面 / hover 与 goto 未解观察 / WARN 基线 / D80② 升档 / 副本漂移正式解法),逐个等真跑 ⇒ 凑不齐;且用户 checkout 常被占用(现场:v3 `dev_20261120` 在用)。② **定案**:runbook 六步(worktree 隔离 → 重装配 → D145 manifest 配对校验 → 重生成对账 → 净场协议 → 真跑留档 `product/e2e/reviews/<日期>/`)+ 七项咬合清单(逐项勾、咬合一处回写一处:gates 对应行 / bugs.md / QUEUE)+ **注红条款**(注红 = 取证动作,red 不计缺陷判据、不进 bugs.md 复发计数、不触发巡检升判——与「首跑不坐实」纪律的摩擦预先写死)+ 不做清单(不顺手改用例语义/产品代码、不在用户 checkout 留痕、不提交 worktree 产物;登录态只读复制不覆盖)。③ **被否**:逐项单等真跑(凑不齐);用户 checkout 直跑(分支占用 + 痕迹);GUI/看板类真跑仪表(运行时平台机制,Atlas 零运行时)。④ **钉与变异**:本条为协议页 + 决策行,无新机器面(咬合证据回写既有登记面);D149(app_login 升档)与 D108(失败留证)是首批两个咬合对象。⑤ **出库判定(2026-10-25 首轮执行完,出库)**:七项结果——① D108 两面咬到(红 28 构件/绿零构件;**附带三发现**:落点勘误〔`.atlas-run/` 非缺省 `test-results/`〕、留存窗口〔Playwright 起动清空 outputDir,观察入 bugs.md〕、「栈起没起」探针盲区首个活体实证〔前端 200/后端不可达⇒门放行全红〕);② adapters 面实况已采集(models 渲染层 82/1859 行、routes 19/55——与既有记录同族,无升档动作);③ hover 未咬到(靶场白屏无从复现);④ goto 计数未咬到(同因);⑤ WARN 基线实测 **7 条**(≥3 且形态重复 ⇒ 观察室升室条件满足 ⇒ QUEUE 待拍板案 9);⑥ D149 已声明面咬到;⑦ worktree 面咬到(首装即最新 + D145 manifest 配对 ok;用户 checkout 漂移副本未动,留用户自行 `--update`);D124 就绪门顺带 ✓(chromium launch 实跑)。协议页随首轮实况修订(落点/留存窗口)。 |
|
|
618
|
+
| D149 | **D80② 升档兑现:`e2e.app_login` 未声明 ⇒ 生成器推 `NOT_READY`(退出码 3 + 机读键 `"app_login"`)**,不再「占位生成 + WARN」半生成(2026-10-24;RFC-20261015-02 预告的时间锚「下一次真跑准备日」到期执行;真跑日批) | ① **病灶**:占位形态 = 硬前提的输入被「缺失即静默放行」吸收(未登录态跑用例 ⇒ 假红/假绿都解释不了)。② **定案**:与 `app_base_url` 同一通道族(`--json` 面 `{"ok": false, "not_ready": "app_login", "message": …}`);文案按 **D103 相抵点**指路——「有门但三态都装不下」(图形验证码 / 服务端 cookie 会话 / 登录响应体无令牌)⇒ 人工会话取出,是描述路径、不是「补声明」的对象(排除类不被罚 not_ready,出路写进文案);`atlas_check` 消费面走既有 `GEN_NOT_READY` 三态 SKIP 中继(message 透传,无需改);`validate_stack_profile` WARN 保留(两道机器面都看得见)。③ **被否**:直接 FAIL(「未走到这一步」≠ 失败,`3509 §B110`);文案只说「请补声明」(丢 D103 排除类出路 ⇒ 把「如实未声明」罚成必填错)。④ **钉与变异**:`test_setup_without_app_login_pushes_not_ready`(改判自旧钉「degrades_loudly」:rc=3 + stderr 指路含 `mode: none` 出口与「人工会话取出」+ **不写盘**)+ `test_app_login_not_ready_json_face_matches_channel`(机读键 + message 面);变异 **MB17**(退回 WARN 半生成 ⇒ 红)/ **MB18**(机读键写错/缺 message ⇒ 红)。⑤ **出库判定(2026-10-25 出库)**:已声明靶场(v3 fabricated)行为不变实证——真跑日 regen 正常、setup 预置照跑、无 not_ready(D148 首轮第 6 项);未声明面由钉 MB17/MB18 守。 |
|
|
617
619
|
|
|
618
620
|
|
|
619
621
|
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Atlas — 践行 Project Harness 的全流程 AI 编程工作流
|
|
2
2
|
|
|
3
3
|
<p align="left">
|
|
4
|
-
<img src="https://img.shields.io/badge/version-0.2.0--beta.
|
|
4
|
+
<img src="https://img.shields.io/badge/version-0.2.0--beta.7-blue.svg" alt="Version" />
|
|
5
5
|
<img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License" />
|
|
6
6
|
<img src="https://img.shields.io/badge/tests-passing-success.svg" alt="Tests" />
|
|
7
7
|
<a href="https://sunwenzhe-git.github.io/atlas/"><img src="https://img.shields.io/badge/docs-VitePress-green.svg" alt="Docs" /></a>
|
package/package.json
CHANGED
package/rings/e2e/reference.md
CHANGED
|
@@ -501,7 +501,7 @@ testid:
|
|
|
501
501
|
- **断言超时下限(硬)**(2026-10-17,issue #28 / `DESIGN.md` D105):生成的运行器配置**必须**声明 `expect.timeout`(缺省 **20s**,`ATLAS_EXPECT_TIMEOUT` 可覆盖)。理由:Playwright 的 `expect` 默认只等 **5s**,而 SPA 首屏(dev bundle 可达 10MB+,现场实测 14s)会让「元素还没渲染」被误判成「元素不存在」= **假红**(本轮实测:导航 URL 全对、元素真在,却判 3 failed)。**它不是「固定等待」**(§5.5 的禁令指 `sleep`):`expect` 是轮询式 auto-wait,本条只抬**上限**。
|
|
502
502
|
- **等待超时下限(硬)**(2026-10-19,`DESIGN.md` D118;v2 真跑现场驱动):生成物里的 `ATLAS_WAIT_TIMEOUT_MS`(**页面就绪锚点** / `waitFor` / `download` / `select` 就绪轮询**共用同一个常量**)**缺省 20s**、且**必须留环境覆盖口** `ATLAS_WAIT_TIMEOUT_MS`。理由与上一条同源:该应用首屏实测 **14,184ms**,而此常量原**写死 10s、无覆盖口** ⇒ 同一份生成物里「等首屏」比「等断言」还短 ⇒ 冷启 / 负载下**间歇性假红**(v2 现场:整份首跑 9 passed / **1 failed**,红的那条倒在锚点 10s 超时 `元素齐全守卫:声明的 testid 无一在场`;同口径探针实测该元素 **2.2~2.5s** 出现且 `disabled=true` + 合规 `aria-label` 全对;复跑 `--repeat-each=3` ⇒ 3/3 绿、全量 **28 passed / 0 failed / 0 flaky**)。**它不是「固定等待」**(只抬上限,轮询语义不变);**它仍会咬人**:`ATLAS_WAIT_TIMEOUT_MS=200` 跑同一条用例必红(现场实测:同一页同一 URL 各轮渲染耗时在 **<1.5s ~ 2.2s** 之间波动,故取远低于它的值;实测 `=1500` 那轮碰巧绿 —— **这种逐轮波动正是 10s 写死会成为枚硬币投掷的原因**)。
|
|
503
503
|
- **refresh 落盘节拍(`ATLAS_REFRESH_SETTLE`,2026-10-18 补,`DESIGN.md` D111;v3 现场实证移植批)**:`refresh` 动词发射**先等后刷**两行——`await page.waitForTimeout(Number(process.env.ATLAS_REFRESH_SETTLE || "0"));` 先于 `await page.reload();`;接续在 `rings/e2e/reference.md` §5.5 动词表 `refresh` 行、发射面见 `tests/test_gen_e2e_scripts.py` 顺序钉。三条口径:① **默认 0 ⇒ 生成物行为与既有契约一致**(毫秒数属项目侧**运行时**前提,运行时设 `ATLAS_REFRESH_SETTLE=<毫秒>`;零业务常数进生成器,对齐 F11 `ATLAS_SLOWMO` 注入先例与 §5.5 的 `ATLAS_EXPECT_TIMEOUT` 同族);② 它**不是 §5.5「固定 sleep」禁令所指**——该禁令禁的是**用例侧**把共享靶场节拍写死进用例;本行是执行**环境配置**面,不进用例正文;③ 单一注入面 = 生成物字面内联(`process.env`),无生成期开关/无覆盖口。
|
|
504
|
-
- **失败留证(硬,失败才落盘)**(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence`;独立评审两轮 + 维护者拍板「按推荐继续」,源自 OpenHands `browser record`
|
|
504
|
+
- **失败留证(硬,失败才落盘)**(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence`;独立评审两轮 + 维护者拍板「按推荐继续」,源自 OpenHands `browser record` 机制级审查;**落点勘误 + 留存窗口 2026-10-25 真跑日实咬补定**):生成的运行器配置(两轨)**必须**补 `trace: 'retain-on-failure'` + `screenshot: 'only-on-failure'`——失败现场自动留 Playwright trace(DOM 快照 + 网络 + 操作日志,trace viewer 可逐帧回放)与截图,落 **`.atlas-run/`**(生成物显式 `outputDir` 覆盖 Playwright 缺省 `test-results/`——**D108 首版契约误写缺省目录,2026-10-25 实咬勘误**,构件形态 = `.atlas-run/<用例目录>/trace.zip + test-failed-*.png` 成对);**全绿轮零新增构件**;**留存窗口 = 到下一次 `playwright test` 起跑**(Playwright 启动即清空 outputDir——「首跑红→复跑绿」场景复跑启动瞬间旧构件蒸发,跨轮取证须在起跑前归档;观察与升格条件见 `duty-review/bugs.md` E2E 段);`video` 缓行(trace 就是回放器,粗粒度投影无收益)。三条口径:① **不溯及已装配副本**(`playwright.config.ts` 由装配的生成器发射 ⇒ 本能力的**咬合点 = 下一次 `install.sh --update` / 首装重装配**;重装配前「契约已声明留证而现场无 trace」属**如实态、非缺陷**,巡检不得误报——对齐 `shared/single-source.md` §3 迁移仪式「新源生效 ⇔ 旧源显式失效」);② **留证含业务内容,只落采纳仓本地**(`.atlas-run/`),不进 atlas 上报通道(§15 零业务上下文出站面不受牵连);**例外(2026-10-23 D142)**:含 `request` 断言的用例默认 `trace: off`(trace 会保存请求体,与 request 的「载荷不落盘」承诺冲突),`ATLAS_REQUEST_TRACE=1` 才恢复 `retain-on-failure`;③ **它不解决 D103 盲区**(人工会话取出类的「CI 不可复现」真身是登录态,不是取证手段——trace 只重放**已发生**的失败)。**判据接线(动机)**:`duty-review/bugs.md` 未解观察(hover 禁用按钮首跑偶发 `Element is not visible`)的升判条件「复现 ≥2 次且能给出固定节拍证据(trace / 连续截图)」由此取得**常驻证据源**,缺陷调查不再依赖落 `/tmp` 的临时探针(不可复算,D98 / 案 6 两次付费先例)。钉 3 条(双轨同构 / 值形态封闭 / 无覆盖口)+ 变异 **MB10**(摘任一轨 trace 键)/ **MB11**(值改 `on`),四段协议含注入 diff、还原逐字节一致;证据与升问题通见门行:`shared/gates.md` 2.2「E2E 真跑」。
|
|
505
505
|
- **零步探针的缺席语义(硬)**(2026-10-17,issue #30 / `DESIGN.md` D107):基线探针**必须在元素缺席时立即判「不成立」**(`count() > 0` 或经 `tsVis` / `atlasValueOf`),**不得调用会等挂载的 actionability API 裸用**(`getAttribute` / `isDisabled` / `isChecked` / `inputValue` 直接挂在 locator 上且无 `count()` 前置)——带参页面上基线本就探不到元素,等挂载会把 `null = 不成立`(§5.5)变成「挂到 test 超时」。
|
|
506
506
|
- **宿主注入型 base query 的转发(硬)**(2026-10-17,issue #27 / `DESIGN.md` D104):`e2e.app_base_url` 携带的 **query 是宿主注入型前提**(现场:应用只从 `window.location.search` 读 tenant,无 sessionStorage 回落,见 `DESIGN.md` D104)——`会话:` 通道写的是 sessionStorage(`D89`)、**送不到**这类消费点;而 `goto` 自带 `?` 在 `D109` 之前一律 FAIL(§5.7)。故由**生成物负责把它带到每一跳**:三处导航发射面(`step` 的 `goto`、元素齐全守卫、零步基线 `data.url`)在**生成期**把 base 的 query 并进目标路径(插在 path 与 hash 之间;实现单源 = `_support/url_merge.py`)。三条口径:① **base 无 query ⇒ 生成物逐字节零变化**(`生成物最新` 门看守派生件,钉见 `tests/test_gen_e2e_scripts.py`);② **`ATLAS_BASE_URL` 只覆盖 host、不覆盖 base 的 query**(换 tenant 请改 `stack-profile.yaml`);③ **边界(明示,不假装支持)**:base query 一旦成为前提,就**无法用同一份用例表达「它缺席时」的行为**——`会话:`(sessionStorage)与 `auth: none`(storageState)**都不在同一消费面**、不构成逃逸口;该场景当前**超出本契约范围**,需要时另起决策行(新增 opt-out 键 = 触 `D99` 字段集冻结)。**消费者侧**:控制台手工项「打开」的地址构造同样走该口径(`e2e_console.manual_open_url`;旧实现 `base + path` 在 base 带 query 时产畸形 URL)。④ **D109 追加(2026-10-18)**:**用例级**宿主注入参数走 §5.5 `参数:` + 内联 `goto` 查询串,与 base 的 query **求并集**(base 在前、**同键 ⇒ 用例胜**);**守卫导航同样带**(取该用例首个带参 `goto` 的查询串);**零步基线不施加**;本函数原本的「目标自带 `?` ⇒ 原样返回」暗位随之**转正为并集档**(既有生成物无一带 query 的 `goto` ⇒ 零回归)。
|
|
507
507
|
- **运行器前置(硬)**(issue #24 / `DESIGN.md` D101):生成物 import `@playwright/test` ⇒ **从 `e2e.scripts_dir` 出发必须可解析**(判据 = 依赖可导入性,不按路径存在;与门同口径)。生成器**不校正**它(生成期代表不了运行机器),由消费者在**起跑前**判:`atlas_check`「E2E 真跑」门不成立 ⇒ `FAIL` 点名;控制台不成立 ⇒ 诚实拒绝,不起注定失败的子进程。**全局兜底**(本机只 `npm i -g` 了 playwright)⇒ 执行器在 Node 的 `node_modules` 搜索路径上补**幂等软链**并复检,且把「用了全局什么」输出(门侧同步降 `WARN`——跑得起来,但结论依赖机器全局状态);链接位被非本工具占用 ⇒ **拒绝覆盖**。`NODE_PATH` 保留(CJS 项目的真通道)但**不作为判据**:ESM 项目下它无效(Node 的 ESM 解析器不读它)。
|
|
@@ -642,7 +642,7 @@ testid:
|
|
|
642
642
|
2. **反偏见**:绝不改用例迁就实现;绝不无根因删用例;**绝不用 `skipped` 绕过失败**;绝不弱化 `expected` 求绿;改用例必须写明理由(AC 变了 / 假设错了 / 非确定)。
|
|
643
643
|
3. **去重 3-Gate**:判重必须过「同目标 / 同意图 / 同层」三关并附证据;文件名或标题相似不算证据。
|
|
644
644
|
4. **完成门**:需求相关用例在真实应用上全 `green`,或标 `blocked` 并写明原因与去向。
|
|
645
|
-
5. **手工项盘点(`RFC-20261015-01` / issue #10,2026-10-16;D143 2026-10-23 结构化)**:分片与索引页面表就绪后,识别**机器覆盖不到的人工作业**(控制台盖不住的项)⇒ 落 `product/e2e/manual.md`:旧行 `- <ID> | <页面> | <标题> | <操作说明>` 继续可用;新项可用 `## <ID> <标题>` + `atlas-manual` 块(`page / precondition / step / evidence`,`step` 每行 `操作 | 观察 | 命令(可空)`),id/title
|
|
645
|
+
5. **手工项盘点(`RFC-20261015-01` / issue #10,2026-10-16;D143 2026-10-23 结构化)**:分片与索引页面表就绪后,识别**机器覆盖不到的人工作业**(控制台盖不住的项)⇒ 落 `product/e2e/manual.md`:旧行 `- <ID> | <页面> | <标题> | <操作说明>` 继续可用;新项可用 `## <ID> <标题>` + `atlas-manual` 块(`page / precondition / step / evidence`,`step` 每行 `操作 | 观察 | 命令(可空)`),id/title 只从标题行取。**观察锚写作约定(2026-10-24,设计优化 v2 WS-D;非字段、不进校验器)**:`step` 行「观察」列建议首 token 写产物锚 `→<用例ID> S<步骤号>`(如 `→E2E-APP-WORKFLOW-DETAIL-001 S3`),把人侧反馈钉到产物坐标(「点哪改哪」的文本化);升格为机器字段的前提 = 「观察项引用错位」现场实证 ≥2 例(QUEUE 观察室,届时走独立决策行)。评估了却没有 ⇒ 域收口留痕 README 写「已评估:0」;**没盘点过** = §5.4 留痕缺该行,采用者由此看到「忘了」。**验收走查现场**新识别的手工项 ⇒ 授权**人侧当场直写** `manual.md`(格式同 §10.1),下场 e2e 报告披露增量;不为此新增设施(控制台的唯一写盘点仍是账本 + 索引,§10.1)。
|
|
646
646
|
|
|
647
647
|
---
|
|
648
648
|
|
|
Binary file
|
|
Binary file
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
凭据只来自运行期环境变量 `ATLAS_APP_USER` / `ATLAS_APP_PASSWORD`(不读项目 `.env`、不硬编码);
|
|
34
34
|
未设置 ⇒ 跳过预置 + 明确警告;换取令牌失败 ⇒ 报错中止
|
|
35
35
|
(不得静默续跑——那会退化成「未登录态跑用例」的假绿)。
|
|
36
|
-
**段缺失 =
|
|
37
|
-
|
|
38
|
-
`not_ready
|
|
36
|
+
**段缺失 = 未声明(不再是「合法降级」,D80② 升档已落地 2026-10-24 / `DESIGN.md` D149)**:
|
|
37
|
+
`validate_stack_profile` WARN 指名在前;生成器直接推 `not_ready`:退出码 3 + 机读
|
|
38
|
+
`not_ready: "app_login"`(文案按 D103 指路「有门但三态都装不下 ⇒ 人工会话取出」)。
|
|
39
39
|
* **网络边界打桩**(契约 §5.8,2026-10-12):`mock:` 行(用例级)在 `e2e.mock.mode: on`
|
|
40
40
|
时编译为**单条** `context.route` 派发表(pathname+method 命中、query 免疫;前缀内未命中
|
|
41
41
|
fail-loud abort + 用例末审计断言点名;OPTIONS 自动 204 + CORS 头);fixture 内容**生成期
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
退出码(`3509 §B110`):**0** 正常 · **2** 真错(缺 profile / 动词越界 / 审查门不过…) ·
|
|
52
52
|
**3** 前置未声明 —— **3 不是失败**:调用方应判「未就绪」而不是「生成失败」,
|
|
53
53
|
且 `--json` 下 stdout 给出 `{"ok": false, "not_ready": <键>, …}`。
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
键 = `app_base_url`(地址未声明)/ `app_login`(登录形态未声明;D80② 升档 2026-10-24 落地,
|
|
55
|
+
`DESIGN.md` D149,升档记录见 `shared/gates.md`「E2E 真跑」行)。
|
|
56
56
|
"""
|
|
57
57
|
from __future__ import annotations
|
|
58
58
|
|
|
@@ -1402,14 +1402,21 @@ def main() -> int:
|
|
|
1402
1402
|
|
|
1403
1403
|
app_login = e2e.get("app_login") or None
|
|
1404
1404
|
if app_login is None:
|
|
1405
|
-
# RFC-20261015-02(issue #11
|
|
1406
|
-
#
|
|
1407
|
-
#
|
|
1408
|
-
#
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1405
|
+
# RFC-20261015-02(issue #11)/ D80② 升档落地(2026-10-24,D149,真跑日批):
|
|
1406
|
+
# 段缺失不再是「合法降级」——直接推 NOT_READY(退出码 3 + 机读键 "app_login",
|
|
1407
|
+
# 与 app_base_url 同一通道)。硬前提的输入不得靠「缺失即静默放行」吸收。
|
|
1408
|
+
# 文案按 D103 相抵点指路:「有门但三态都装不下」(图形验证码 / 服务端 cookie 会话 /
|
|
1409
|
+
# 登录响应体无令牌)⇒ 人工会话取出路径,不是「补声明」能解决的形态,不属本判据。
|
|
1410
|
+
msg = ("e2e.app_login 未声明(E2E 硬前提:登录形态。三选一:real 取证四值 / "
|
|
1411
|
+
"mode: none(靶场无门,显式声明)/ mode: fabricated(桩令牌,必填收窄 "
|
|
1412
|
+
"storage_key);「有门但三态都装不下」(图形验证码 / 服务端 cookie 会话 / "
|
|
1413
|
+
"登录响应体无令牌)⇒ 人工会话取出(rings/e2e/reference.md §7.1 / "
|
|
1414
|
+
"DESIGN.md D103),是描述路径、不是本判据的对象)")
|
|
1415
|
+
if args.json:
|
|
1416
|
+
print(json.dumps({"ok": False, "not_ready": "app_login", "message": msg},
|
|
1417
|
+
ensure_ascii=False, indent=2))
|
|
1418
|
+
print(f"NOT_READY: {msg}", file=sys.stderr)
|
|
1419
|
+
return NOT_READY
|
|
1413
1420
|
else:
|
|
1414
1421
|
login_mode = app_login.get("mode") or "real"
|
|
1415
1422
|
if login_mode not in APP_LOGIN_MODES:
|
|
@@ -2107,7 +2114,8 @@ def main() -> int:
|
|
|
2107
2114
|
" // 3 failed 1 passed)。提升到 20s(仍远小于 test 超时),并留 `ATLAS_EXPECT_TIMEOUT` 覆盖口。\n"
|
|
2108
2115
|
" expect: { timeout: Number(process.env.ATLAS_EXPECT_TIMEOUT ?? 20_000) },\n"
|
|
2109
2116
|
f" use: {{ baseURL: process.env.ATLAS_BASE_URL ?? '{base}',\n"
|
|
2110
|
-
" // 失败留证(2026-10-18,RFC-20261018-01):失败才落
|
|
2117
|
+
" // 失败留证(2026-10-18,RFC-20261018-01;落点勘误 2026-10-25):失败才落 .atlas-run/(本配置 outputDir),\n"
|
|
2118
|
+
" // 全绿轮零产物;留存窗口 = 到下一次起跑(Playwright 启动清空 outputDir)。\n"
|
|
2111
2119
|
" // 生效咬合点 = 重装配(install --update / 首装),不溯及已装配副本。\n"
|
|
2112
2120
|
" trace: 'retain-on-failure',\n"
|
|
2113
2121
|
" screenshot: 'only-on-failure',\n"
|
|
@@ -2142,7 +2150,8 @@ def main() -> int:
|
|
|
2142
2150
|
" // 3 failed 1 passed)。提升到 20s(仍远小于 test 超时),并留 `ATLAS_EXPECT_TIMEOUT` 覆盖口。\n"
|
|
2143
2151
|
" expect: { timeout: Number(process.env.ATLAS_EXPECT_TIMEOUT ?? 20_000) },\n"
|
|
2144
2152
|
f" use: {{ baseURL: process.env.ATLAS_BASE_URL ?? '{base}', // E1 单段\n"
|
|
2145
|
-
" // 失败留证(2026-10-18,RFC-20261018-01):失败才落
|
|
2153
|
+
" // 失败留证(2026-10-18,RFC-20261018-01;落点勘误 2026-10-25):失败才落 .atlas-run/(本配置 outputDir),\n"
|
|
2154
|
+
" // 全绿轮零产物;留存窗口 = 到下一次起跑(Playwright 启动清空 outputDir)。\n"
|
|
2146
2155
|
" // 生效咬合点 = 重装配(install --update / 首装),不溯及已装配副本。\n"
|
|
2147
2156
|
" trace: 'retain-on-failure',\n"
|
|
2148
2157
|
" screenshot: 'only-on-failure',\n"
|
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
|
|
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; **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,13 +104,14 @@
|
|
|
104
104
|
|
|
105
105
|
| 门 | 对账平面 | 查什么 | **不查什么** | 预算 + 触发物 | 假绿测试(等级) |
|
|
106
106
|
|---|---|---|---|---|---|
|
|
107
|
-
| `check-all.py --only atlas` | 包面 ⇔ 回归 | `[机器]` **
|
|
107
|
+
| `check-all.py --only atlas` | 包面 ⇔ 回归 | `[机器]` **37 个**测试文件全绿 | 测试**覆盖到的门**是否等于本表的门(本表 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 条**本轮跑绿;形态面**未配变异钉子**(弱判据,语义归人) |
|
|
111
111
|
| `tests/test_suite_hygiene.py` | 测试套件 ⇔ 自身卫生 | `[机器]` 每个测试文件可解析 + 顶层 `test_*` 名字唯一 | 测试**是否在测真东西**(`3509 §B92` 族) | 改任一测试文件时 | `出厂级(历史实测)`:注入同名测试 ⇒ 红(`ATLAS-UPSTREAM #115`) |
|
|
112
112
|
| `tests/test_shared_kernel.py`(2026-10-12 单源批) | 共享内核 ⇔ 消费者别名 ⇔ 单源结构 | `[机器]` 黄金期望(profile / 用例块解析值断言);**结构唯一性守卫**(`parse_profile` / `parse_block` / `strip_inline_comment` / `yaml_scalar` 全包只准各存单源一处);消费者别名 = 内核函数对象;加载器缺失响亮 + 缓存同物 | 内核语义之外的新字段(归消费者);解析器的泛 YAML 能力(非目标,只服务 atlas 自产形态) | 改 `_support/` / 任一消费者 / 增共享函数时 | `出厂级(本轮实测)`:**M45**(删 e2e 子块分支 ⇒ 黄金期望红)/ **M46**(就地复制本地实现 ⇒ 唯一性守卫红)/ **M47**(加载器静默 None ⇒ 红)+ **RFC-20261015-02(issue #11)**:`yaml_scalar` 的 null 家族**收窄为 YAML 口径**(只认 `null`/`~`/空;裸 `none` = 合法字符串——`e2e.app_login.mode: none` 必须原样读出,否则被吞成 null ⇒ 缺省判据读成 real);**M72**(收窄退回旧口径 ⇒ `test_profile_scalar_forms` 第二组断言红) |
|
|
113
113
|
| `tests/test_nested_repo_gitlink_guard.py`(2026-10-12 单源批) | 父仓 index ⇔ 嵌套源包仓布局 | `[机器]` `git ls-files -s atlas/` 无 mode `160000`(gitlink)——`git add atlas/` 后未提交即已可见;前置不满足(无嵌套仓 / 未跟踪)⇒ `SKIP` 带原因;沙箱正反控(注入必红 / 松散跟踪不误报) | 嵌套仓布局**该不该存在**(用户 2026-10-09 拍板的事实);已入历史的 gitlink 不回溯(须人工回退) | 每批测试(随套件;本仓双树现场恒跑) | `出厂级(本轮实测)`:沙箱注入 gitlink ⇒ 必红(`test_guard_bites_on_staged_gitlink`)/ 松散跟踪 ⇒ 不误报(正控) |
|
|
114
|
+
| `tests/test_map_anchors.py`(2026-10-25,docs/map 二期 ③) | **docs/map 锚 ⇔ 仓内实码**(活文档防漂移;README 徽章漂移先例的 docs/map 版) | `[机器]` ① 行号锚三档:路径消失 ⇒ 红 / 邻接符号漂移 ⇒ 红并报「旧 → 新」(`--fix` 可改写)/ 符号消失 ⇒ 红 ② 锚形态值域:代码目标裸行号锚无邻接符号 ⇒ 红(须写成 符号(`path:line`),与 D98 同哲学)③ 纯路径锚 + atlas-map.html 文件条目存活 ④ index.md 数字快照机器实收(校验器 AST 计数 / 测试文件 glob / 自检 `^check ` 口径等八项,复用 canonical 计数口径)⑤ **「始终最新」保活(2026-10-25 用户要求当日加)**:index/html 版本串 ⇔ `package.json`;md 表格与括注、html 条目描述里的「N 行」声明 ⇔ 实况行数;html「×N / N 文件」计数串 ⇔ 实收 | **目标仓运行态路径**(`product/` `.trellis/` `.adapter-out/` `.atlas/`);`.md` 目标无 § 邻接的行锚只验行存活;**锚的语义正确性**(那行是否仍是作者想指的行——符号匹配是近似);编排门行「8 类」需语义识别不实收;**正文语义与机制要点的时效**(保活只管可机判的版本/行数/计数,内容刷新仍归发版刷新纪律) | 改 `docs/map/**` 或任何被锚代码件时(随套件) | `出厂级(本轮实测)`:**M151** 锚指不存在路径 ⇒ 红 / **M152** 行号漂移 ⇒ 红且 `--fix` 改写后复绿 / **M153** 邻接符号删除 ⇒ 红 / **M154** 无符号裸锚 ⇒ 红 / **M155** 版本串漂与行数声明漂 ⇒ 红(钉子均沙箱夹具实测;首跑即咬出真实漂移:测试数 36→37、最大单件 2374→2381→2383〔并行会话当日仍在改该件〕、门册 145→146 行——文档落盘当天即漂的实证) |
|
|
114
115
|
|
|
115
116
|
| **成本预算上限**(`global-rules.md` §12 + `independent-review.md` §6 第 7/8/9 条) | 改动产物义务 / 审查宽度 ⇔ 实测成本 | `[语义]` 「先量再动手」(先量当前成本与预期收益,实测不足即不做);四处上限(审查并发 ≤ 4 / 裁决每批 ≤ 10 / 体量 9500 ⇒ 拆分评审 / 产出分级随模式);预算击穿登记为**流水线缺陷**族 | **「应审 N / 实审 M」对账**的机器面(**部分落地**:实例 ⑤ 经 `cli_review_convert`;①②④ 仍缺 —— 见 §4);「量得对不对」(语义) | 改产物义务 / 审查宽度前 | **部分机器化**:语义项不设机器判据;①②④ 的对账器仍为缺口(`3509 §117`) |
|
|
116
117
|
|
|
Binary file
|
|
Binary file
|
|
@@ -2140,22 +2140,39 @@ run().then((r) => console.log(JSON.stringify(r))).catch((e) => { console.log('ER
|
|
|
2140
2140
|
assert data["retried"] == "不限时间", data
|
|
2141
2141
|
|
|
2142
2142
|
|
|
2143
|
-
def
|
|
2144
|
-
"""契约 §7.1(RFC-20261015-02,2026-10-16
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2143
|
+
def test_setup_without_app_login_pushes_not_ready() -> None:
|
|
2144
|
+
"""契约 §7.1(RFC-20261015-02,2026-10-16 改判;**D80② 升档落地 2026-10-24 / D149**):
|
|
2145
|
+
stack-profile 未声明 e2e.app_login ⇒ 生成器推 **NOT_READY(退出码 3)**,不再「占位生成 +
|
|
2146
|
+
WARN」半生成——硬前提的输入不得靠「缺失即静默放行」吸收。
|
|
2147
|
+
|
|
2148
|
+
文案按 D103 相抵点指路:「有门但三态都装不下」⇒ 人工会话取出,不是「补声明」的对象。
|
|
2149
|
+
变异 **MB17**:退回 WARN 半生成(退出码 0)⇒ 本钉红。
|
|
2148
2150
|
"""
|
|
2149
2151
|
with tempfile.TemporaryDirectory() as td:
|
|
2150
2152
|
root = Path(td)
|
|
2151
2153
|
# app_login 是 e2e 段最后一个键(夹具约定)⇒ 切到它之前 = 整段去掉
|
|
2152
2154
|
make(root, profile=PROFILE.split(" app_login:")[0])
|
|
2153
|
-
r = run(root
|
|
2154
|
-
assert r.returncode ==
|
|
2155
|
-
assert "e2e.app_login" in r.stderr, r.stderr
|
|
2156
|
-
assert "mode: none" in r.stderr, r.stderr
|
|
2157
|
-
|
|
2158
|
-
assert
|
|
2155
|
+
r = run(root) # dry-run 即判(前置链在写盘前)
|
|
2156
|
+
assert r.returncode == 3, (r.returncode, r.stderr, r.stdout)
|
|
2157
|
+
assert "NOT_READY" in r.stderr and "e2e.app_login" in r.stderr, r.stderr
|
|
2158
|
+
assert "mode: none" in r.stderr, r.stderr # 指路:无门项目的显式声明出口
|
|
2159
|
+
assert "人工会话取出" in r.stderr, r.stderr # D103 相抵点排除类的出路在文案里
|
|
2160
|
+
assert not (root / "product" / "e2e" / "scripts" / "atlas-auth.setup.ts").is_file()
|
|
2161
|
+
|
|
2162
|
+
|
|
2163
|
+
def test_app_login_not_ready_json_face_matches_channel() -> None:
|
|
2164
|
+
"""D80② 升档(D149):`--json` 面与 `app_base_url` 同一通道族——`{"ok": false,
|
|
2165
|
+
"not_ready": "app_login", "message": …}`,机读键供消费者(atlas_check 三态 SKIP)分流。
|
|
2166
|
+
变异 **MB18**:机读键写错 / 缺 message ⇒ 本钉红。
|
|
2167
|
+
"""
|
|
2168
|
+
with tempfile.TemporaryDirectory() as td:
|
|
2169
|
+
root = Path(td)
|
|
2170
|
+
make(root, profile=PROFILE.split(" app_login:")[0])
|
|
2171
|
+
j = run(root, "--json")
|
|
2172
|
+
assert j.returncode == 3, (j.returncode, j.stdout, j.stderr)
|
|
2173
|
+
data = json.loads(j.stdout)
|
|
2174
|
+
assert data.get("ok") is False and data.get("not_ready") == "app_login", j.stdout
|
|
2175
|
+
assert "message" in data and "app_login" in data["message"], j.stdout
|
|
2159
2176
|
|
|
2160
2177
|
|
|
2161
2178
|
def test_app_login_section_incomplete_fails_generation() -> None:
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""docs/map 实现地图的锚存活性门(2026-10-25 用户拍板,docs/map 二期 ③)。
|
|
3
|
+
|
|
4
|
+
对账平面:**docs/map/*.md 与 atlas-map.html 里的文件锚 ⇔ 仓内实码**。
|
|
5
|
+
先例:`tests/test_canonical_consistency.py` 守 shared/ 契约(同为「仓内文档 ⇔ 实码」,
|
|
6
|
+
走出厂测试面而非 VALIDATORS——docs/map 是 atlas 自仓资产,不装配进 .atlas/)。
|
|
7
|
+
|
|
8
|
+
三档语义(响亮原则;DRIFT 在本测试面一律置红,修法 = 改文档或 `--fix`):
|
|
9
|
+
* path_missing —— 锚指路径不存在 ⇒ 红(含 atlas-map.html 的 DETAIL/tip 文件条目);
|
|
10
|
+
* symbol_drift —— 符号仍在文件里、但不在锚定行窗口(±3)⇒ 红,报「旧 → 新」;
|
|
11
|
+
* symbol_gone —— 邻接符号在目标文件中已不存在 ⇒ 红;
|
|
12
|
+
* bare_anchor —— 代码目标的 `path:line` 锚无邻接符号 ⇒ 红(锚形态收进值域:
|
|
13
|
+
必须写成 符号(`path:line`)——与 D98 动词值域封闭同哲学);
|
|
14
|
+
* doc_link / snapshot_drift —— 同目录 md 互链存活 / index.md 数字快照机器实收
|
|
15
|
+
(README 徽章漂移先例的 docs/map 版,口径复用 canonical 的
|
|
16
|
+
`^check ` 与 `tests/test_*.py` glob)。
|
|
17
|
+
|
|
18
|
+
显式盲区(登记于 `shared/gates.md` 2.6):
|
|
19
|
+
* 目标仓运行态路径(`product/` `.trellis/` `.adapter-out/` `.atlas/`)不在校验域;
|
|
20
|
+
* `.md` 目标的无 § 邻接行锚只验「行存活」(节标题锚定不做符号对账);
|
|
21
|
+
* 锚的**语义正确性**(那行是否仍是作者想指的那行)机器判不了——符号匹配是近似。
|
|
22
|
+
|
|
23
|
+
变异钉(出厂级,本轮实测):M151 路径消失必红 / M152 行号漂移必红且 --fix 复绿 /
|
|
24
|
+
M153 符号消失必红 / M154 无符号裸锚必红。
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import re
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
PKG_ROOT = Path(__file__).resolve().parents[1]
|
|
33
|
+
MAP_DIR = PKG_ROOT / "docs" / "map"
|
|
34
|
+
|
|
35
|
+
# atlas 自有源码的校验值域(锚指这些顶层才算「本仓可判」)
|
|
36
|
+
OWNED_PREFIXES = (
|
|
37
|
+
"adapters/", "scripts/", "shared/", "tests/", "apply/", "rings/",
|
|
38
|
+
"skills/", "patches/", "_support/", "templates/", "validators/",
|
|
39
|
+
"bin/", "docs/", "duty-review/",
|
|
40
|
+
)
|
|
41
|
+
OWNED_ROOT_FILES = {"install.sh", "DESIGN.md", "README.md", "CHANGELOG.md"}
|
|
42
|
+
|
|
43
|
+
ANCHOR_RE = re.compile(r"`([^`\s]+):(\d+)(?:-(\d+))?`") # `path:line` / `path:l1-l2`
|
|
44
|
+
TOK_RE = re.compile(r"`([^`\s]+)`")
|
|
45
|
+
MD_LINK_RE = re.compile(r"\]\(([^)#\s]+\.md)\)")
|
|
46
|
+
HTML_FILE_RE = re.compile(r"\['([^']+)'(?:,'([^']*)')?\]")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _is_owned(path: str) -> bool:
|
|
50
|
+
top = path.split("/")[0]
|
|
51
|
+
return top in OWNED_ROOT_FILES or path.startswith(OWNED_PREFIXES)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _strip_section(path: str) -> str:
|
|
55
|
+
return re.sub(r"\s*§.*$", "", path)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve(path: str, repo: Path) -> Path | None:
|
|
59
|
+
"""全路径优先;裸文件名(如 `_graph.py`)在自有树内唯一命中才认。"""
|
|
60
|
+
p = path.split("::")[0]
|
|
61
|
+
if (repo / p).is_file():
|
|
62
|
+
return repo / p
|
|
63
|
+
base = Path(p).name
|
|
64
|
+
hits: list[Path] = []
|
|
65
|
+
for pref in OWNED_PREFIXES:
|
|
66
|
+
d = repo / pref
|
|
67
|
+
if d.is_dir():
|
|
68
|
+
hits += [x for x in d.rglob(base) if x.is_file()]
|
|
69
|
+
if (repo / base).is_file() and base in OWNED_ROOT_FILES:
|
|
70
|
+
hits.append(repo / base)
|
|
71
|
+
return hits[0] if len(hits) == 1 else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _candidates(ctx: str, anchor_tok: str) -> list[str]:
|
|
75
|
+
"""符号候选:反引号 token(剥尾 ())+ 邻近 identifier 型 token(含 desc 的 FN() 形态)。
|
|
76
|
+
锚 token 自身先抹掉,防「路径里的片段」被当成符号(自匹配假漂移)。"""
|
|
77
|
+
masked = ctx.replace(anchor_tok, " " * len(anchor_tok))
|
|
78
|
+
out: list[str] = []
|
|
79
|
+
for m in TOK_RE.finditer(masked):
|
|
80
|
+
tok = m.group(1)
|
|
81
|
+
name = tok.rstrip("()")
|
|
82
|
+
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_.]*", name):
|
|
83
|
+
out.append(name)
|
|
84
|
+
for name in re.findall(r"[A-Za-z_][A-Za-z0-9_]*\(\)", masked):
|
|
85
|
+
out.append(name.rstrip("()"))
|
|
86
|
+
head = re.match(r"^[\W_0-9]*([A-Za-z][A-Za-z0-9_]*)", masked)
|
|
87
|
+
if head:
|
|
88
|
+
out.append(head.group(1)) # 首词(html desc 惯例:'query 查询族:…')
|
|
89
|
+
for t in re.findall(r"[A-Za-z_][A-Za-z0-9_]{2,}", masked):
|
|
90
|
+
if ("_" in t) or t.isupper() or any(c.isupper() for c in t[1:]):
|
|
91
|
+
out.append(t)
|
|
92
|
+
seen: set[str] = set()
|
|
93
|
+
res: list[str] = []
|
|
94
|
+
for n in out:
|
|
95
|
+
if n not in seen:
|
|
96
|
+
seen.add(n)
|
|
97
|
+
res.append(n)
|
|
98
|
+
return res
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _sym_hit_lines(name: str, lines: list[str]) -> list[int]:
|
|
102
|
+
"""符号变体(点号全串 → 末段)在文件中的出现行(1 起)。"""
|
|
103
|
+
for variant in (name, name.rsplit(".", 1)[-1]):
|
|
104
|
+
hits = [i for i, ln in enumerate(lines, 1) if variant in ln]
|
|
105
|
+
if hits:
|
|
106
|
+
return hits
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _looks_like_path(path: str) -> bool:
|
|
111
|
+
"""锚的路径段须像仓内文件:含 `/`,或带已知扩展名的裸文件名(`_graph.py` 形态)。
|
|
112
|
+
排除 `127.0.0.1:4190` 这类 host:port 散文。"""
|
|
113
|
+
return "/" in path or bool(re.match(r"^[\w.-]+\.(py|md|js|sh|ya?ml|json|ts)$", path))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def check_anchor(tok: str, start: int, end: int, ctx: str, where: str,
|
|
117
|
+
repo: Path, full_tok: str | None = None) -> dict | None:
|
|
118
|
+
if not _looks_like_path(tok):
|
|
119
|
+
return None
|
|
120
|
+
path = _strip_section(tok)
|
|
121
|
+
target = _resolve(path, repo)
|
|
122
|
+
if target is None or not target.is_file():
|
|
123
|
+
return {"sev": "FAIL", "code": "path_missing", "where": where,
|
|
124
|
+
"msg": f"`{tok}` 目标不存在({path})"}
|
|
125
|
+
lines = target.read_text(encoding="utf-8").splitlines()
|
|
126
|
+
if start > len(lines):
|
|
127
|
+
return {"sev": "FAIL", "code": "path_missing", "where": where,
|
|
128
|
+
"msg": f"`{tok}` 行号 {start} 超出文件末行 {len(lines)}"}
|
|
129
|
+
|
|
130
|
+
rel = target.relative_to(repo).as_posix()
|
|
131
|
+
if target.suffix == ".md":
|
|
132
|
+
m = re.search(r"§(\d+(?:\.\d+)?)", ctx)
|
|
133
|
+
if not m:
|
|
134
|
+
return None # .md 弱锚:行存活即过(盲区声明)
|
|
135
|
+
sec = m.group(1)
|
|
136
|
+
window = lines[max(0, start - 4): end + 3]
|
|
137
|
+
if any(sec in ln and ln.lstrip().startswith("#") for ln in window) or \
|
|
138
|
+
any(re.match(r"^#{1,6}\s*" + re.escape(sec) + r"[\s\.、::((]", ln) for ln in window):
|
|
139
|
+
return None
|
|
140
|
+
new = next((i for i, ln in enumerate(lines, 1)
|
|
141
|
+
if re.match(r"^#{1,6}\s*" + re.escape(sec) + r"[\s\.、::((]", ln)), None)
|
|
142
|
+
return {"sev": "FAIL", "code": "symbol_drift", "where": where,
|
|
143
|
+
"msg": f"`{tok}` 窗口内无 §{sec} 标题"
|
|
144
|
+
+ (f"(新位置约 {rel}:{new})" if new else "(全文件未找到该节)")
|
|
145
|
+
+ ";--fix 不改写 § 锚,请手工对位"}
|
|
146
|
+
|
|
147
|
+
cands = _candidates(ctx, tok)
|
|
148
|
+
if not cands:
|
|
149
|
+
return {"sev": "FAIL", "code": "bare_anchor", "where": where,
|
|
150
|
+
"msg": f"`{tok}` 无邻接符号——请改写成 符号(`path:line`)形态"}
|
|
151
|
+
hit_map = {c: _sym_hit_lines(c, lines) for c in cands}
|
|
152
|
+
live = {c: hs for c, hs in hit_map.items() if hs}
|
|
153
|
+
if not live:
|
|
154
|
+
return {"sev": "FAIL", "code": "symbol_gone", "where": where,
|
|
155
|
+
"msg": f"`{tok}` 邻接符号 {cands} 在 {rel} 中均不存在(删除或改名?)"}
|
|
156
|
+
window = range(max(1, start - 3), end + 4)
|
|
157
|
+
if any(any(x in window for x in hs) for hs in live.values()):
|
|
158
|
+
return None
|
|
159
|
+
# 锚指函数体/docstring/常量块:窗口上方最近 def/class(或模块级大写赋值)名在候选中 ⇒ 活
|
|
160
|
+
above = lines[max(0, start - 40): start - 1]
|
|
161
|
+
for ln in reversed(above):
|
|
162
|
+
m2 = re.match(r"\s*(?:def|class)\s+([A-Za-z_][A-Za-z0-9_]*)", ln)
|
|
163
|
+
if m2:
|
|
164
|
+
if any(m2.group(1) == c or f".{m2.group(1)}" in c for c in cands):
|
|
165
|
+
return None
|
|
166
|
+
break
|
|
167
|
+
for ln in reversed(above):
|
|
168
|
+
m2 = re.match(r"([A-Z_][A-Z0-9_]*)\s*=", ln)
|
|
169
|
+
if m2 and m2.group(1) in cands:
|
|
170
|
+
return None
|
|
171
|
+
if re.match(r"\s*(?:def|class)\s", ln):
|
|
172
|
+
break
|
|
173
|
+
best = min(((c, min(hs)) for c, hs in live.items()),
|
|
174
|
+
key=lambda x: min(abs(h - start) for h in live[x[0]]))
|
|
175
|
+
return {"sev": "FAIL", "code": "symbol_drift", "where": where,
|
|
176
|
+
"full_tok": full_tok or f"{tok}:{start}" + (f"-{end}" if end != start else ""),
|
|
177
|
+
"new": best[1], "rel": rel,
|
|
178
|
+
"msg": f"`{tok}` 符号 {best[0]} 已漂移到 {rel}:{best[1]}(旧 {tok})"
|
|
179
|
+
f";python3 tests/test_map_anchors.py --fix 可改写"}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def check_path_token(tok: str, where: str, repo: Path) -> dict | None:
|
|
183
|
+
if any(ch in tok for ch in "<>{}*"):
|
|
184
|
+
return None # 占位符 / 集合简写 / glob 简写不在本门值域
|
|
185
|
+
p = _strip_section(tok)
|
|
186
|
+
if "/" not in p and p not in OWNED_ROOT_FILES:
|
|
187
|
+
return None # 无斜杠的裸名(可能是运行态文件名)不判
|
|
188
|
+
if not _is_owned(p):
|
|
189
|
+
return None # 目标仓运行态路径:显式盲区
|
|
190
|
+
if p.endswith("/"):
|
|
191
|
+
return None if (repo / p).is_dir() else {
|
|
192
|
+
"sev": "FAIL", "code": "path_missing", "where": where,
|
|
193
|
+
"msg": f"`{tok}` 目录不存在"}
|
|
194
|
+
if (repo / p).is_dir():
|
|
195
|
+
return None # 无尾斜线的目录(`skills/atlas-release`)也算活
|
|
196
|
+
if _resolve(p, repo) is None:
|
|
197
|
+
return {"sev": "FAIL", "code": "path_missing", "where": where,
|
|
198
|
+
"msg": f"`{tok}` 路径不存在({p})"}
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def collect_docs(repo: Path) -> list[dict]:
|
|
203
|
+
"""docs/map/*.md + atlas-map.html 全量校验;返回违规清单。"""
|
|
204
|
+
out: list[dict] = []
|
|
205
|
+
docs_dir = repo / "docs" / "map"
|
|
206
|
+
md_files = sorted(docs_dir.glob("*.md"))
|
|
207
|
+
for f in md_files:
|
|
208
|
+
for i, ln in enumerate(f.read_text(encoding="utf-8").splitlines(), 1):
|
|
209
|
+
where = f"docs/map/{f.name}:{i}"
|
|
210
|
+
for m in ANCHOR_RE.finditer(ln):
|
|
211
|
+
tok, s, e = m.group(1), int(m.group(2)), int(m.group(3) or m.group(2))
|
|
212
|
+
v = check_anchor(tok, s, e, ln, where, repo, full_tok=m.group(0)[1:-1])
|
|
213
|
+
if v:
|
|
214
|
+
out.append(v)
|
|
215
|
+
anchored_spans = [(m.start(), m.end()) for m in ANCHOR_RE.finditer(ln)]
|
|
216
|
+
for m in TOK_RE.finditer(ln):
|
|
217
|
+
if any(s <= m.start() < e for s, e in anchored_spans):
|
|
218
|
+
continue # 行号锚的整段 token 不再当纯路径复检
|
|
219
|
+
v = check_path_token(m.group(1), where, repo)
|
|
220
|
+
if v:
|
|
221
|
+
out.append(v)
|
|
222
|
+
for m in MD_LINK_RE.finditer(ln):
|
|
223
|
+
if not (docs_dir / m.group(1)).is_file():
|
|
224
|
+
out.append({"sev": "FAIL", "code": "doc_link", "where": where,
|
|
225
|
+
"msg": f"同目录文档链接 {m.group(1)} 不存在"})
|
|
226
|
+
html = docs_dir / "atlas-map.html"
|
|
227
|
+
if html.is_file():
|
|
228
|
+
for m in HTML_FILE_RE.finditer(html.read_text(encoding="utf-8")):
|
|
229
|
+
entry, desc = m.group(1), m.group(2) or ""
|
|
230
|
+
where = "docs/map/atlas-map.html"
|
|
231
|
+
am = re.match(r"^([^:]+):(\d+)$", entry)
|
|
232
|
+
if am:
|
|
233
|
+
v = check_anchor(am.group(1), int(am.group(2)), int(am.group(2)),
|
|
234
|
+
desc or entry, where, repo, full_tok=entry)
|
|
235
|
+
else:
|
|
236
|
+
v = check_path_token(entry, where, repo)
|
|
237
|
+
if v:
|
|
238
|
+
out.append(v)
|
|
239
|
+
return out
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ---------------- index.md 数字快照 ⇔ 机器实收 ----------------
|
|
243
|
+
|
|
244
|
+
def _derive_snapshot(repo: Path) -> dict[str, int]:
|
|
245
|
+
import ast
|
|
246
|
+
out: dict[str, int] = {}
|
|
247
|
+
chk = repo / "scripts" / "atlas_check.py"
|
|
248
|
+
if chk.is_file():
|
|
249
|
+
tree = ast.parse(chk.read_text(encoding="utf-8"))
|
|
250
|
+
for node in ast.walk(tree): # AST 实收(README 徽章漂移先例的口径),不碰注释/文档串
|
|
251
|
+
if isinstance(node, ast.Assign) and any(
|
|
252
|
+
getattr(t, "id", "") == "VALIDATORS" for t in node.targets):
|
|
253
|
+
out["校验器"] = len(node.value.elts)
|
|
254
|
+
if (repo / "tests").is_dir():
|
|
255
|
+
out["出厂测试文件"] = len(list((repo / "tests").glob("test_*.py")))
|
|
256
|
+
if (repo / "skills").is_dir():
|
|
257
|
+
out["skills"] = len([d for d in (repo / "skills").iterdir() if d.is_dir()])
|
|
258
|
+
if (repo / "rings").is_dir():
|
|
259
|
+
out["环"] = len(list((repo / "rings").glob("*/reference.md")))
|
|
260
|
+
if (repo / "shared").is_dir():
|
|
261
|
+
out["共享契约"] = len(list((repo / "shared").glob("*.md")))
|
|
262
|
+
if (repo / "_support").is_dir():
|
|
263
|
+
out["共享内核"] = len(list((repo / "_support").glob("*.py")))
|
|
264
|
+
inst = repo / "install.sh"
|
|
265
|
+
if inst.is_file():
|
|
266
|
+
out["install.sh 自检声明"] = len(re.findall(r"^check ", inst.read_text(encoding="utf-8"), re.M))
|
|
267
|
+
scripts = list((repo / "scripts").glob("*.py")) if (repo / "scripts").is_dir() else []
|
|
268
|
+
if scripts:
|
|
269
|
+
out["最大单件"] = max(len(p.read_text(encoding="utf-8").splitlines()) for p in scripts)
|
|
270
|
+
return out
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def check_snapshot(repo: Path) -> list[dict]:
|
|
274
|
+
idx = (repo / "docs" / "map" / "index.md").read_text(encoding="utf-8")
|
|
275
|
+
section = idx[idx.index("## 数字快照"):]
|
|
276
|
+
section = section[:section.index("\n## ")] if "\n## " in section else section
|
|
277
|
+
derived = _derive_snapshot(repo)
|
|
278
|
+
out: list[dict] = []
|
|
279
|
+
for m in re.finditer(r"^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|", section, re.M):
|
|
280
|
+
name, val = m.group(1).strip(), m.group(2)
|
|
281
|
+
if name in ("量", "---") or name not in derived:
|
|
282
|
+
continue # 编排门行 8 类需语义识别:显式不校验(盲区)
|
|
283
|
+
num = re.search(r"(\d+)\s*行", val) or re.search(r"(\d+)", val)
|
|
284
|
+
if not num or int(num.group(1)) != derived[name]:
|
|
285
|
+
out.append({"sev": "FAIL", "code": "snapshot_drift",
|
|
286
|
+
"where": "docs/map/index.md",
|
|
287
|
+
"msg": f"数字快照「{name}」文档 {num.group(1) if num else '无数字'}"
|
|
288
|
+
f" ≠ 实况 {derived[name]}"})
|
|
289
|
+
return out
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def fix_drifts(repo: Path) -> int:
|
|
293
|
+
"""把 symbol_drift(代码目标)在 docs 内就地重写为新行号。"""
|
|
294
|
+
import collections
|
|
295
|
+
fixes: dict[Path, list[tuple[str, str]]] = collections.defaultdict(list)
|
|
296
|
+
for v in collect_docs(repo):
|
|
297
|
+
if v["code"] == "symbol_drift" and "full_tok" in v:
|
|
298
|
+
old = v["full_tok"]
|
|
299
|
+
new = re.sub(r":\d+(-\d+)?$", f":{v['new']}", old)
|
|
300
|
+
fixes[repo / "docs" / "map" / Path(v["where"].split(":")[0]).name].append((old, new))
|
|
301
|
+
n = 0
|
|
302
|
+
for path, subs in fixes.items():
|
|
303
|
+
text = path.read_text(encoding="utf-8")
|
|
304
|
+
for old, new in subs:
|
|
305
|
+
if old in text:
|
|
306
|
+
text = text.replace(old, new)
|
|
307
|
+
n += 1
|
|
308
|
+
path.write_text(text, encoding="utf-8")
|
|
309
|
+
return n
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# ---------------- 「始终最新」保活(2026-10-25 用户要求:html 与活文档不许过期) ----------------
|
|
313
|
+
# 三类可机器判的过期:版本串漂(index/html 锚定版本 ≠ package.json)、行数声明漂
|
|
314
|
+
# (md 表格与括注里的「N 行」、html 文件条目描述里的「N 行」≠ 实况 wc)、计数串漂
|
|
315
|
+
# (html「出厂测试 ×N / 契约 ×N / 内核 ×N / tests/ N 文件」≠ 实收)。语义内容仍归发版刷新纪律。
|
|
316
|
+
|
|
317
|
+
def _pkg_version(repo: Path) -> str:
|
|
318
|
+
import json
|
|
319
|
+
return json.loads((repo / "package.json").read_text(encoding="utf-8"))["version"]
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def check_freshness(repo: Path) -> list[dict]:
|
|
323
|
+
out: list[dict] = []
|
|
324
|
+
ver = _pkg_version(repo)
|
|
325
|
+
derived = _derive_snapshot(repo)
|
|
326
|
+
where_idx = "docs/map/index.md"
|
|
327
|
+
idx = (repo / "docs" / "map" / "index.md").read_text(encoding="utf-8")
|
|
328
|
+
m = re.search(r"锚定版本\**\s*[::]\s*\**\s*v([\d][\w.\-]+)", idx)
|
|
329
|
+
if not m:
|
|
330
|
+
out.append({"sev": "FAIL", "code": "version_stale", "where": where_idx,
|
|
331
|
+
"msg": "index.md 缺「锚定版本」行"})
|
|
332
|
+
elif m.group(1) != ver:
|
|
333
|
+
out.append({"sev": "FAIL", "code": "version_stale", "where": where_idx,
|
|
334
|
+
"msg": f"index.md 锚定版本 v{m.group(1)} ≠ package.json {ver}——发版后须刷新 docs/map"})
|
|
335
|
+
html_p = repo / "docs" / "map" / "atlas-map.html"
|
|
336
|
+
html = html_p.read_text(encoding="utf-8") if html_p.is_file() else ""
|
|
337
|
+
for vm in re.finditer(r"v(\d+\.\d+\.\d+[\w.\-]*)", html):
|
|
338
|
+
if vm.group(1) != ver:
|
|
339
|
+
out.append({"sev": "FAIL", "code": "version_stale", "where": "docs/map/atlas-map.html",
|
|
340
|
+
"msg": f"html 版本串 v{vm.group(1)} ≠ package.json {ver}"})
|
|
341
|
+
break
|
|
342
|
+
|
|
343
|
+
def _claim(path_tok: str, n: int, where: str):
|
|
344
|
+
target = _resolve(_strip_section(path_tok), repo)
|
|
345
|
+
if target is None or not target.is_file():
|
|
346
|
+
return # 路径存活已由锚门承担;这里只管「行数对不对」
|
|
347
|
+
real = len(target.read_text(encoding="utf-8").splitlines())
|
|
348
|
+
if real != n:
|
|
349
|
+
out.append({"sev": "FAIL", "code": "linecount_stale", "where": where,
|
|
350
|
+
"msg": f"`{path_tok}` 声明 {n} 行 ≠ 实况 {real} 行"})
|
|
351
|
+
|
|
352
|
+
for f in sorted((repo / "docs" / "map").glob("*.md")):
|
|
353
|
+
text = f.read_text(encoding="utf-8")
|
|
354
|
+
where = f"docs/map/{f.name}"
|
|
355
|
+
for pm in re.finditer(r"`([^`]+)`\((\d+)\s*行", text):
|
|
356
|
+
_claim(pm.group(1), int(pm.group(2)), where)
|
|
357
|
+
for pm in re.finditer(r"\|\s*`([^`]+)`\s*\|\s*(\d+)\s*\|", text):
|
|
358
|
+
_claim(pm.group(1), int(pm.group(2)), where)
|
|
359
|
+
if html:
|
|
360
|
+
for em in HTML_FILE_RE.finditer(html):
|
|
361
|
+
entry, desc = em.group(1), em.group(2) or ""
|
|
362
|
+
for dm in re.finditer(r"(\d+)\s*行", desc):
|
|
363
|
+
_claim(re.sub(r":\d+$", "", entry), int(dm.group(1)), "docs/map/atlas-map.html")
|
|
364
|
+
for pat, key in ((r"出厂测试 ×(\d+)", "出厂测试文件"),
|
|
365
|
+
(r"tests/ (\d+) 文件", "出厂测试文件"),
|
|
366
|
+
(r"契约 ×(\d+)", "共享契约"),
|
|
367
|
+
(r"内核 ×(\d+)", "共享内核")):
|
|
368
|
+
for cm in re.finditer(pat, html):
|
|
369
|
+
if int(cm.group(1)) != derived[key]:
|
|
370
|
+
out.append({"sev": "FAIL", "code": "snapshot_drift", "where": "docs/map/atlas-map.html",
|
|
371
|
+
"msg": f"html「{cm.group(0)}」≠ 实况 {derived[key]}"})
|
|
372
|
+
return out
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# ---------------- 出厂测试 ----------------
|
|
378
|
+
|
|
379
|
+
def test_docs_map_anchors_alive() -> None:
|
|
380
|
+
vs = collect_docs(PKG_ROOT)
|
|
381
|
+
assert not vs, "docs/map 锚存活违规 " + str(len(vs)) + " 条:\n" + \
|
|
382
|
+
"\n".join(f"[{v['code']}] {v['where']} {v['msg']}" for v in vs)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_docs_map_snapshot_matches_reality() -> None:
|
|
386
|
+
vs = check_snapshot(PKG_ROOT)
|
|
387
|
+
assert not vs, "数字快照与实况不符:\n" + \
|
|
388
|
+
"\n".join(v["msg"] for v in vs)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def test_map_claims_fresh() -> None:
|
|
392
|
+
vs = check_freshness(PKG_ROOT)
|
|
393
|
+
assert not vs, "docs/map 声明过期(版本/行数/计数):\n" + \
|
|
394
|
+
"\n".join(f"[{v['code']}] {v['msg']}" for v in vs)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_m155_stale_claims_must_fail(tmp_path: Path) -> None:
|
|
398
|
+
repo = _mk_repo(tmp_path)
|
|
399
|
+
(repo / "scripts" / "a.py").write_text("x = 1\n" * 3, encoding="utf-8")
|
|
400
|
+
(repo / "package.json").write_text('{"version": "0.2.0-beta.6"}', encoding="utf-8")
|
|
401
|
+
(repo / "docs" / "map" / "index.md").write_text(
|
|
402
|
+
"> 锚定版本:v0.1.0\n\n`a`(`scripts/a.py:1`),`scripts/a.py`(99 行)\n", encoding="utf-8")
|
|
403
|
+
vs = check_freshness(repo)
|
|
404
|
+
codes = {v["code"] for v in vs}
|
|
405
|
+
assert "version_stale" in codes and "linecount_stale" in codes, \
|
|
406
|
+
"M155:版本串漂与行数声明漂必须红(喂坏输入未咬 = 假门)"
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _mk_repo(tmp: Path) -> Path:
|
|
410
|
+
(tmp / "scripts").mkdir()
|
|
411
|
+
(tmp / "docs" / "map").mkdir(parents=True)
|
|
412
|
+
return tmp
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def test_m151_missing_anchor_path_must_fail(tmp_path: Path) -> None:
|
|
416
|
+
repo = _mk_repo(tmp_path)
|
|
417
|
+
(repo / "scripts" / "alive.py").write_text("def keep():\n pass\n", encoding="utf-8")
|
|
418
|
+
(repo / "docs" / "map" / "a.md").write_text(
|
|
419
|
+
"`keep`(`scripts/gone.py:1`) 死路径\n", encoding="utf-8")
|
|
420
|
+
vs = collect_docs(repo)
|
|
421
|
+
assert any(v["code"] == "path_missing" for v in vs), \
|
|
422
|
+
"M151:指向不存在文件的锚必须红(喂坏输入未咬 = 假门)"
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def test_m152_drifted_line_must_fail_and_fix_recovers(tmp_path: Path) -> None:
|
|
426
|
+
repo = _mk_repo(tmp_path)
|
|
427
|
+
(repo / "scripts" / "a.py").write_text(
|
|
428
|
+
"x = 1\nx = 2\nx = 3\nx = 4\nx = 5\nx = 6\nx = 7\nx = 8\nx = 9\nx = 10\n\ndef fresh():\n pass\n",
|
|
429
|
+
encoding="utf-8")
|
|
430
|
+
(repo / "docs" / "map" / "a.md").write_text(
|
|
431
|
+
"`fresh`(`scripts/a.py:1`) 漂移\n", encoding="utf-8")
|
|
432
|
+
vs = collect_docs(repo)
|
|
433
|
+
d = next((v for v in vs if v["code"] == "symbol_drift"), None)
|
|
434
|
+
assert d is not None and d["new"] == 12, "M152:符号在、行号漂必须红并报新行号"
|
|
435
|
+
assert fix_drifts(repo) == 1
|
|
436
|
+
assert "`scripts/a.py:12`" in (repo / "docs" / "map" / "a.md").read_text(encoding="utf-8")
|
|
437
|
+
assert not [v for v in collect_docs(repo) if v["code"] == "symbol_drift"], \
|
|
438
|
+
"M152:--fix 改写后必须复绿"
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def test_m153_removed_symbol_must_fail(tmp_path: Path) -> None:
|
|
442
|
+
repo = _mk_repo(tmp_path)
|
|
443
|
+
(repo / "scripts" / "a.py").write_text("def other():\n pass\n", encoding="utf-8")
|
|
444
|
+
(repo / "docs" / "map" / "a.md").write_text(
|
|
445
|
+
"`gone_fn`(`scripts/a.py:1`) 符号已删\n", encoding="utf-8")
|
|
446
|
+
vs = collect_docs(repo)
|
|
447
|
+
assert any(v["code"] == "symbol_gone" for v in vs), \
|
|
448
|
+
"M153:邻接符号在目标文件消失必须红"
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def test_m154_bare_line_anchor_must_fail(tmp_path: Path) -> None:
|
|
452
|
+
repo = _mk_repo(tmp_path)
|
|
453
|
+
(repo / "scripts" / "a.py").write_text("def fresh():\n pass\n", encoding="utf-8")
|
|
454
|
+
(repo / "docs" / "map" / "a.md").write_text(
|
|
455
|
+
"说明文字(`scripts/a.py:1`) 无符号\n", encoding="utf-8")
|
|
456
|
+
vs = collect_docs(repo)
|
|
457
|
+
assert any(v["code"] == "bare_anchor" for v in vs), \
|
|
458
|
+
"M154:无邻接符号的裸行号锚必须红(锚形态收进值域)"
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
if __name__ == "__main__":
|
|
462
|
+
if "--fix" in sys.argv:
|
|
463
|
+
print(f"已改写 {fix_drifts(PKG_ROOT)} 处漂移锚")
|
|
464
|
+
else:
|
|
465
|
+
bad = collect_docs(PKG_ROOT) + check_snapshot(PKG_ROOT) + check_freshness(PKG_ROOT)
|
|
466
|
+
for v in bad:
|
|
467
|
+
print(f"[{v['code']}] {v['where']} {v['msg']}")
|
|
468
|
+
print(f"docs/map 锚门:{'红 ' + str(len(bad)) + ' 条' if bad else '全绿'}")
|
|
469
|
+
sys.exit(1 if bad else 0)
|