atlas-project-harness 0.2.0-beta.2 → 0.2.0-beta.3

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 CHANGED
@@ -4,6 +4,22 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## [v0.2.0-beta.3] - 2026-10-23
8
+
9
+ ### 🩹 `value` 断言读可见文本(D133)
10
+
11
+ * **假红修复**:Ant Design Select 交互后会在触发器内注入 `aria-live` 播报 span(`opacity:0`),旧实现用 `innerText` 把选中文案读两遍,`value` 断言在「先选择再断言」的用例上必红而页面本身正常。
12
+ * **定案**:新增 `atlasVisibleText` 助手(临时移出不可见子树 → 读 `innerText` → `try/finally` 恢复 style),`atlasValueOf` 与 `atlasExpectValue` 统一走它;原生表单元素的 `toHaveValue` 路径不变。
13
+ * **钉**:`test_value_reads_visible_text_excluding_a11y_mirror` 从生成物抽助手在 Node 里真求值(隐藏播报不计入、style 恢复、发射面无裸 `innerText` 回退)。
14
+
15
+ ### ⌨️ E2E 新增 `type` 逐字输入动词(D134)
16
+
17
+ * **场景**:带掩码 / 受控输入的控件(如 Ant Design RangePicker 的日期输入)用 `fill` 一次性写 DOM 不会触发组件内部解析,模型状态不提交;逐条 `press` 每次只发单键、无法形成连续输入链。
18
+ * **定案**:`type <tid> <text>` → `locator.pressSequentially(text)`,逐字触发 `keydown/keypress/input`;**不先清空**原值(替换语义用 `fill`),值必填;动作词表 13 → 14。
19
+ * **钉**:样例守恒 +1、`test_type_verb_emits_press_sequentially_without_clearing`(发射形态 / 空值中止 / `[N]`)+ 校验器动作侧 `test_e2e_index_type_is_action_not_assertion`;变异 **M134**。
20
+
21
+ ---
22
+
7
23
  ## [v0.2.0-beta.2] - 2026-10-22 - 2026-10-22
8
24
 
9
25
  ### ✨ E2E hash 查询参数(D132)
package/DESIGN.md CHANGED
@@ -599,6 +599,8 @@ adapters:
599
599
  | D130 | E2E 环引入 **前端适配版 C01-C14 覆盖维度矩阵** 与缺口对账(2026-10-21;`rings/e2e/reference.md` §5.1/§5.2/§5.5/§8.2;`_support/case_block.py`、`validators/validate_e2e_index.py`、`templates/prd/domain.md`、`templates/e2e/case-shard.md`) | ① **病灶**:原「AC 反向覆盖门」仅能回答「每条 AC 是否至少被一条用例指向」,**无法回答每条 AC 的异常流、边界值、权限、会话恢复、幂等、状态转换等高危场景是否覆盖**;导致 Agent 生成大量表面正向用例,真正易崩场景系统性遗漏。② **定案**:① 前端适配裁剪版 C01-C14 场景维度闭集(C01正常流、C02异常流、C03边界值、C04权限、C05并发、C06重试、C07审计、C08会话恢复、C09一致性、C10依赖容错、C11幂等、C12输入校验、C13状态转换、C14安全);② 用例分片块体增加可选数组字段 `覆盖维度:`,单源解析器 `_support/case_block.py` 纳入键空间;③ 域级 PRD 验收标准表增加可选第 4 列 `适用覆盖维度`(如 `C01, C02`),前三列保持不变;④ 校验器 `validators/validate_e2e_index.py` 增加「AC × 覆盖维度」对账项:已声明但未全量覆盖 ⇒ WARN;未知维度词 ⇒ WARN;未声明第 4 列的项目 ⇒ N/A 兜底,绝不产生破坏存量的永久红门;⑤ 控制台 snapshot case 对象透传 `dimensions` 字段。③ **被否**:在 PRD 新建第 4 张手工对账表(严重违反 single-source 契约,且会与 AC 唯一性定义门发生行号碰撞);对所有 AC 强制 14 维度全覆盖(引发无脑生成几十条形式用例的膨胀灾难)。④ **钉与变异**:`tests/test_validators.py` 增加 `test_e2e_index_coverage_dimensions_pass_and_warn`(N/A / WARN / PASS 三态验证);变异 **M131**(摘掉 dimensions 聚合回落全 PASS ⇒ 钉红),还原逐字节一致。全量测试通过。 |
600
600
  | D131 | 收口契约引入 **变更风险定档、Always-Human 路径、修复验证 7 检查点与事故追问闭环**(2026-10-21;`shared/closeout.md` §2 步骤 3/4/11;`duty-review/README.md` §2;`templates/reviews/README.md`) | ① **病灶**:① 收口阶段缺乏系统性风险定档,高危底层改动(如权限、路由、CI、包依赖)容易被 Agent 自述误导而跳过严格人工亲测;② 缺陷修复任务(bug fix)常直接修改业务代码并自报已修复,缺乏「先失败后转绿」的 Red First 证明与边界/回归核对,导致同类问题反复逃逸;③ 线上/联调事故复盘停留在修代码,未追问「哪一道门本该接住」,事故没有转化为常态化回归用例或变异钉。② **定案**:① `shared/closeout.md` 补齐步骤 3「变更风险定档与人工审批」:三档风险矩阵,明确 `auth / permission / routing / CI / package deps / shared contracts` 为 Always-Human 永远人工路径,严禁 Agent 代勾放行;② 补齐步骤 4「缺陷修复验证 7 检查点」(复现明确、Red First 先失败测试、最小 diff、边界值、状态机完整、回归范围界定、闭环证据留存);③ 步骤 11 与 `duty-review/README.md` 引入事故追问机制:每个确认逃逸缺陷必须在 `bugs.md` 标注原因,并在关闭前补齐 E2E 回归用例或 M 系列变异钉;④ 提供 `templates/reviews/README.md` 代码审查指引轻量模板(≤10 行,聚焦失败特征与证据链)。③ **被否**:用文件名/正则做脆弱的机器强制路径阻断脚本(极易被重命名或参数绕过,且易对合法改动造成假阻断);由 Agent 自动签署审批(违背人类作为最终交付负责人的第一原则)。④ **钉与变异**:`test_canonical_consistency.py` 计数与跨文件一致性全量跑绿。 |
601
601
  | D132 | 新增 **E2E hash 查询参数契约**(`hash参数:` + `e2e.query_preset.hash_keys`)(2026-10-22;`rings/e2e/reference.md` §5.5/§5.7/§8.2;`shared/stack-profile.md` §2;`_support/case_block.py` / `_support/url_merge.py` / `scripts/gen_e2e_scripts.py` / `validators/validate_e2e_index.py`) | ① **病灶**:HashRouter 页面的 `useSearchParams` 只读 `#` 之后的查询段;D109 `参数:` 只覆盖 `#` 之前的常规查询,`会话:` 是 sessionStorage,导致快照页这类「`#` 后查询前提」用例无合法写法,只能手补生成物(重跑即丢)。② **定案**:`hash参数:` 只声明键,值内联在 `goto` 的 hash 查询串;白名单 `query_preset.hash_keys`;守卫导航把同一段 hash 查询追加到 hash 段(`append_hash_query`),base query 仍在 `#` 前;零步基线不施加;路径门两侧 `strip_queries` 后比对;同键不得跨 `参数:`/`会话:`/`hash参数:` 双写。③ **被否**:把 `#` 后查询硬塞进 `参数:`(会丢 hash 段 / 与 base query 合并语义冲突);项目侧手补生成物(不可重生成,属已否的 D98 类补丁)。④ **钉**:生成器 6 条 + 校验器 7 条(含 `test_hash_param_carried_to_guard_and_goto`、`test_zero_step_baseline_carries_no_hash_param`、`test_hash_query_merge_helpers` 与正向/未白名单/白名单外/死声明/值形态/未声明 hash 键/双载体);`tests/test_validators.py` 计数 138 已登记 `shared/gates.md`。 |
602
+ | D133 | `value` 断言/探针改读**可见文本**,排除触发器内的视觉隐藏 a11y 镜像(2026-10-23;`scripts/gen_e2e_scripts.py` 发射点;契约 §5.5 `value` 行) | ① **病灶**:`value` 对非原生控件用 `innerText` 读触发器文本。Ant Design Select 在用户交互后向触发器内注入 `aria-live="polite"` 的无障碍播报 span(`width:0;height:0;opacity:0`),其文本与选中项相同;`innerText` 不排除视觉隐藏节点 ⇒ 选中文案被读两遍(现场:`模式一…\n模式一…`),同一标签的 `value` 断言在交互后必红,而交互前同断言正常(基线 green / 动作后 red 的假红)。契约本体写的是「触发器**可见**文本」,实现与契约不符。② **定案**:新增发射期助手 `atlasVisibleText(loc)`:采值前遍历触发器子树,用 `checkVisibility({opacityProperty:true,visibilityProperty:true})`(缺 API 时退 `getComputedStyle`)判定,把不可见节点的 `style` 暂存后置 `display:none !important`,读 `innerText` 后**按逆序逐字节恢复 style**(`try/finally` 覆盖异常路径),保留 `innerText` 的排版归一半径;`atlasValueOf`(探针,含三态语义)与 `atlasExpectValue`(正式断言,改用 `expect.poll` 保留轮询)统一走该助手;原生 `input/select/textarea` 的 `toHaveValue` 路径不变。③ **被否**:按 `aria-live` / `aria-hidden` 选择器硬排除(只治 Ant Design 一处,其他组件库的视觉隐藏节点仍会污染;`aria-hidden` 还可能承载可见文本);读 `textContent`(会把 `display:none` 的隐藏下拉项也算进来,扩大假绿面);对读数做去重(同一文案合法重复出现时无法区分,属语义猜测)。④ **钉与变异**:`tests/test_gen_e2e_scripts.py::test_value_reads_visible_text_excluding_a11y_mirror` —— 从生成物抽出助手源码,在 Node 里以假 DOM **真求值**:可见项 + `opacity:0` 的同文案播报项 ⇒ 只返回一次;采值后隐藏节点 `style` 恢复为 `null`;同时钉发射面含 `atlasVisibleText`、不再出现 `toHaveText(v, { useInnerText: true })`。 |
603
+ | D134 | 新增 E2E **`type` 逐字输入动词**(2026-10-23;契约 §5.5;`scripts/gen_e2e_scripts.py` / `validators/validate_e2e_index.py`) | ① **病灶**:Playwright `fill` 一次性写 DOM、只触发 `input`,不触发带掩码 / 受控输入组件的内部解析(现场:Ant Design RangePicker 日期输入,文本可见但保存 payload 无 `startTime/endTime`);逐条 `press` 每次先 focus、只发单键,无法形成连续输入链(实测同样不提交)⇒ 项目侧只能留人工或用 `_support` 钩子代输入(步骤不可见、绕过 DSL)。② **定案**:`type <tid> <text>` → `locator.pressSequentially(text)`,逐字触发 `keydown/keypress/input`,**不先清空**原值(替换语义用 `fill`);值必填,空值 ⇒ 生成器报错中止;动作词表 13 → 14,校验器词表同步,样例守恒表 +1。③ **被否**:用 N 条 `press` 拼输入(不可读且实测不提交);`_support` 钩子代输入(步骤不可见、绕过 case DSL);移除产品侧掩码(UX 变更、超出 E2E 能力面)。④ **钉**:`test_type_verb_emits_press_sequentially_without_clearing`(发射形态 + 空值中止 + `[N]` 定位)与 `test_e2e_index_type_is_action_not_assertion`(step 侧放行 / expected 侧拒收);变异 **M134**(摘生成器 `type` 分支 ⇒ 钉红)。 |
602
604
 
603
605
 
604
606
 
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.2-blue.svg" alt="Version" />
4
+ <img src="https://img.shields.io/badge/version-0.2.0--beta.3-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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-project-harness",
3
- "version": "0.2.0-beta.2",
3
+ "version": "0.2.0-beta.3",
4
4
  "description": "践行 Project Harness 的全流程 AI 编程工作流治理引擎",
5
5
  "bin": {
6
6
  "atlas": "bin/atlas.js"
@@ -254,6 +254,7 @@ testid:
254
254
  |---|---|---|
255
255
  | 动作 | `click` | 点击 testid。**缺省追加 `{ force: true }`**(2026-10-16 补,issue #R4):动机 = 真实 UI 里悬浮遮罩 / 动画 / 父容器事件拦截会造出动作执行超时的**假红**,把「元素真的在那」这件事误报成「点不动」。**适用边界(硬)**:被测属性本身是「可操作性 / 不可交互 / 用户不可达」(例:「被遮罩覆盖点的点不动」「动画中不可点」)的用例**不得**依赖该默认 —— `force` 会跳过 *receives events / stable* 检查,使该属性**无法被证伪**(假绿通道)。**步级开关**:`click <tid> native` ⇒ 该步按运行器原生语义执行(**不**追加 `force`),保留 `.first()` 的 strict-mode 修复。**值域封闭**:第三位**仅允许** `native` 或缺省,其它值 ⇒ 校验器 FAIL + 生成器报错中止 |
256
256
  | | `fill` | 在输入框填入值 |
257
+ | | `type` | **逐字键入**(2026-10-23 补,`DESIGN.md` D134):把值按字符逐个键入(Playwright `pressSequentially`),触发 `keydown / keypress / input` 的真实按键链;**不先清空**原值(替换语义用 `fill`)。用于带掩码 / 受控输入的控件(如 Ant Design RangePicker 的日期输入):`fill` 一次性写 DOM 不会触发组件内部解析,模型状态不更新;`press` 只发单键、无法形成连续输入链。值必填,空值 ⇒ 生成器报错中止 |
257
258
  | | `select` | 在该控件上**选中 label 的那一项**(2026-10-04 实现无关化,`3509 §B173-1` / `3508 §173-§174`:发射器产出按元素自适应的 helper —— 原生 `<select>` 走 `selectOption`;否则视作 trigger+listbox 组合(如 Radix/shadcn Select),点开触发器后**按选项自身的标签**点选。**语义只定「行为」不定「DOM 机制」**——组件实现(原生 / 基座)不被 E2E 应然约束。**选项定位口径(2026-10-19,`DESIGN.md` D112)**:**不得以「可访问名全等」定位选项**——组件常把描述文案塞进选项内,其可访问名因此 = 标签 + 描述,全等必失配(现场:ant Select 的选项名为 `文本对话基于LLM的对话型交互…`,用例卡在 30s 超时)。三级降级 = **① `title` 属性全等 → ② 可见文本前缀 → ③ 名称包含**;**命中不唯一即拒绝并列出可见选项**(宁可响亮失败,也不 `.first()` 静默选错项 = 假绿通道);候选只取**几何可见**的元素,仅非原生分支适用。**候选集(2026-10-20,`DESIGN.md` D122)**:ARIA 的 `[role=option]` / `[role=menuitem]` 之外,须含组件库自身的可见选项类名(`[class*="select-item-option"]`——Antd Select 的可见项只有 `title`、无 `role=option`,role 项是零宽 a11y 镜像;`[class*=]` 兼容自定义前缀)。**就绪门(2026-10-19,`DESIGN.md` D117)**:点开触发器后**不得**以全局候选集的 `attached` 作就绪判据——页面上常驻的 `[role=menuitem]`(顶部导航等)会让它秒满足 ⇒ 采样落在下拉尚未定位(零盒)的窗口内,候选只剩环境项(现场失败文案 `可见选项=["应用市场",…]`;失败快照反证触发器已 `[expanded]`、listbox 内选项齐)。改为**按命中态轮询采样**(轮询原语 = `expect.poll`,与页面就绪锚点同一机制,**不引入固定等待**):**0 命中 = 未就绪** ⇒ 轮询至 `ATLAS_WAIT_TIMEOUT_MS` 上限;**>1 命中 = 歧义** ⇒ **立即**拒绝(D112 语义不变);唯一 ⇒ 定。) |
258
259
  | | `check` / `uncheck` | 勾选 / 取消勾选 |
259
260
  | | `press` | 按键(值 = 键名) |
@@ -270,7 +271,7 @@ testid:
270
271
  | | `visible` / `hidden` | 可见 / 不可见 |
271
272
  | | `enabled` / `disabled` | 可用 / 不可用(**不要求可见**,与 `visible` 正交) |
272
273
  | | `checked` / `unchecked` | 复选框已勾选 / 未勾选(**不要求可见**,与 `enabled` 同理) |
273
- | | `value` | 控件**当前值**等于值(2026-10-04 实现无关化,同 `select`:原生表单元素读 input 值;trigger+listbox 组合读触发器可见文本);值写作 `""`(或 `(空)`)时表示**空串**,用于「清空后为空」类断言 |
274
+ | | `value` | 控件**当前值**等于值(2026-10-04 实现无关化,同 `select`:原生表单元素读 input 值;trigger+listbox 组合读触发器可见文本);值写作 `""`(或 `(空)`)时表示**空串**,用于「清空后为空」类断言。**「可见文本」排除视觉隐藏的 a11y 镜像**(2026-10-23,`DESIGN.md` D133):组件库会在触发器内注入 `opacity:0` 的播报节点(如 Ant Design Select 交互后的 `aria-live` span),取可见文本时临时移出布局、读完恢复;不得用裸 `innerText` 把同一文案读两遍 |
274
275
  | | `countOptions` | 该元素内的 `option` 子元素个数等于值(整数),用于「枚举选项个数」类断言;**结构动词、仅适用原生 `<select>`**(选项集合在 DOM 结构上是其子元素;trigger+listbox 组合的选项在打开前不存在于 DOM,本动词对其无语义) |
275
276
  | | `attr` | 元素属性 `<name>` 的值等于 `<value>`(**不要求可见**,与 `enabled` / `checked` 同理:属性可读性与渲染可见性正交);值段写作 `<name> <value>`,`name` 取**首个空白前**的 token、其余全部为 `value`(可含空格);属性不存在 ⇒ 取值为空 ⇒ 对**非空**期望值恒失败(2026-09-25 补,依据 `3509 §B35`)。**值写作 `""`(或 `(空)`)= 断言「该属性缺席或为空串」**(2026-10-19 补,`DESIGN.md` D116):`getAttribute` 对缺席属性返回 `null`(**不是** `""`)⇒ 发射侧一律 `?? ''` 归一后轮询比对;`toHaveAttribute(name, "")` **不适用**(实测缺席时判 FAIL = 半通状态)。该形态是**否定属性**的表达面(「该入口**不带**某标记」类 AC 的机器判据)|
276
277
  | | `delta` | **数值增量**(相对断言,2026-10-07 补,`issues` P5):值段 = 带符号整数 `±N`。语义 = 行动前采样该元素文本中的**首个整数**、行动后复读,两者差值必须恰为 `±N`。真栈单 run 累加态下「待生成 +1」类断言的合法形态 —— 绝对值断言(`text X 待生成 3 条`)在同前提的兄弟用例上只可能首跑者成立。渲染为**轮询断言**(node `expect.poll` / python 行动后复读);采样点 = **第一个提交性动作之前**(与 `unchanged:` 先例同点,准备步骤不污染基准)。**不得**与 `initial:` / `unchanged:` / `after:` 叠写、用例必须有 `step`(均 ⇒ 校验器 FAIL,生成器中止双保险);**不参与零步基线**(它没有「初始态成立 / 不成立」语义)。首例 = `E2E-PROJECTS-014`(拆分后项目级待生成递增,2026-10-07 转正) |
@@ -5,9 +5,9 @@
5
5
  改为「操作脚本」):
6
6
  * 读 `product/e2e/cases/<page>.md` 的 `atlas-case` 块,**一页一脚本**;
7
7
  * 生成**可运行的操件脚本**:每条用例一个测试——`goto(<页面 URL>)` → 按 `step`
8
- **逐行执行动作**(click / fill / select / check / uncheck / press / hover /
9
- refresh / goto)→ 按 `expected` **逐行执行断言**(text / contains / count /
10
- visible / hidden / enabled / disabled / value);`#` 之后的中文说明作为注释保留;
8
+ **逐行执行动作**(动词表见契约 §5.5;含 click / fill / type / press 等)→ 按
9
+ `expected` **逐行执行断言**(text / contains / count / visible / hidden /
10
+ enabled / disabled / value);`#` 之后的中文说明作为注释保留;
11
11
  * **动词解析失败即中止**:`step` / `expected` 出现词表外的动词 → 报错并列出违规行,
12
12
  不得静默跳过或降级为注释;
13
13
  * `testid:` 数组作为**元素齐全守卫**:每用例运行前按**自动等待**断言其列出的 testid 全部
@@ -93,7 +93,7 @@ APP_LOGIN_MODES = ("real", "fabricated", "none")
93
93
  MOCK_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE")
94
94
 
95
95
  # 分片 step / expected 的动词词表(契约 `rings/e2e/reference.md` §5.5,2026-09-23 定)
96
- ACTION_VERBS = ("click", "fill", "select", "check", "uncheck", "press", "hover", "refresh", "goto", "waitFor", "download", "upload", "dispatch")
96
+ ACTION_VERBS = ("click", "fill", "select", "check", "uncheck", "press", "type", "hover", "refresh", "goto", "waitFor", "download", "upload", "dispatch")
97
97
  ASSERT_VERBS = ("text", "contains", "count", "countOptions", "visible", "hidden", "enabled", "disabled", "checked", "unchecked", "value", "attr", "delta")
98
98
  NO_TESTID_VERBS = ("refresh", "goto") # 这两个动词第二个 token 不是 testid(goto 的值是路径)
99
99
  # 契约 §5.5(2026-10-16 补,issue #R4):`click` / `hover` 默认追加 `{ force: true }`
@@ -526,6 +526,15 @@ def ts_case(case: dict, url: str, base: str = "", esm: bool = False) -> str:
526
526
  actions.append(f"await {tid_locator(tid)}.uncheck();")
527
527
  elif verb == "press":
528
528
  actions.append(f"await {tid_locator(tid)}.press({json.dumps(value)});")
529
+ elif verb == "type":
530
+ # 契约 §5.5(D134,2026-10-23):逐字键入 —— 触发 keydown/keypress/input 的真实
531
+ # 按键链,供带掩码 / 受控输入的控件使用(`fill` 一次性写 DOM 不触发组件内部解析,
532
+ # 如 Ant Design RangePicker 的日期输入)。**不先清空原值**;替换语义用 `fill`。
533
+ if not value.strip():
534
+ raise ValueError(
535
+ f"{cid}: `type` 需要输入文本,得空值:{s!r}(契约 §5.5)")
536
+ actions.append(
537
+ f"await {tid_locator(tid)}.pressSequentially({json.dumps(value, ensure_ascii=False)});")
529
538
  elif verb == "hover":
530
539
  actions.append(
531
540
  f"await {tid_locator(tid)}.hover("
@@ -1636,6 +1645,31 @@ def main() -> int:
1636
1645
  " const l = loc.nth(nth);\n"
1637
1646
  " return (await l.isVisible()) ? l : null;\n"
1638
1647
  "}\n"
1648
+ "async function atlasVisibleText(loc: any): Promise<string> {\n"
1649
+ " // 契约 §5.5(D133,2026-10-23):`value` 读**可见文本**。组件库会在触发器内放\n"
1650
+ " // 视觉隐藏的 a11y 播报节点(Ant Design Select 交互后注入的 aria-live span,opacity:0),\n"
1651
+ " // innerText 会把同一文案读两遍(现场:模式选择器断言收到两行相同标签)⇒ 采值前把\n"
1652
+ " // 不可见子树临时移出布局,读完(含异常路径)原样恢复 style;innerText 的排版归一半径不变。\n"
1653
+ " return loc.evaluate((e: HTMLElement) => {\n"
1654
+ " const hidden: { el: HTMLElement; style: string | null }[] = [];\n"
1655
+ " for (const node of Array.from(e.querySelectorAll('*')) as HTMLElement[]) {\n"
1656
+ " const check = (node as any).checkVisibility;\n"
1657
+ " const visible = typeof check === 'function'\n"
1658
+ " ? check.call(node, { opacityProperty: true, visibilityProperty: true })\n"
1659
+ " : (() => { const s = getComputedStyle(node); return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0'; })();\n"
1660
+ " if (visible) continue;\n"
1661
+ " hidden.push({ el: node, style: node.getAttribute('style') });\n"
1662
+ " node.style.setProperty('display', 'none', 'important');\n"
1663
+ " }\n"
1664
+ " try { return (e.innerText || '').trim(); }\n"
1665
+ " finally {\n"
1666
+ " for (let i = hidden.length - 1; i >= 0; i--) {\n"
1667
+ " const { el, style } = hidden[i];\n"
1668
+ " if (style === null) el.removeAttribute('style'); else el.setAttribute('style', style);\n"
1669
+ " }\n"
1670
+ " }\n"
1671
+ " });\n"
1672
+ "}\n"
1639
1673
  "async function atlasValueOf(page: any, tid: string): Promise<string | null> {\n"
1640
1674
  " // 契约 §5.5(B173-1,2026-10-04):`value` 断言/探针的实现无关入口。\n"
1641
1675
  " // 原生表单元素读 input 值;否则视作 trigger+listbox 组合(如 Radix/shadcn\n"
@@ -1645,7 +1679,7 @@ def main() -> int:
1645
1679
  " const tag = await l.evaluate((e) => e.tagName.toLowerCase());\n"
1646
1680
  " return (tag === 'select' || tag === 'input' || tag === 'textarea')\n"
1647
1681
  " ? l.inputValue()\n"
1648
- " : (await l.innerText()).trim();\n"
1682
+ " : atlasVisibleText(l);\n"
1649
1683
  "}\n"
1650
1684
  "async function atlasSelect(page: any, tid: string, label: string, nth: number = 0) {\n"
1651
1685
  " // 契约 §5.5(B173-1,2026-10-04;**2026-10-19 D112** 选项定位放宽):`select` 的语义 =\n"
@@ -1706,7 +1740,10 @@ def main() -> int:
1706
1740
  " if (tag === 'select' || tag === 'input' || tag === 'textarea') {\n"
1707
1741
  " await expect(el).toHaveValue(v);\n"
1708
1742
  " } else {\n"
1709
- " await expect(el).toHaveText(v, { useInnerText: true });\n"
1743
+ " // 契约 §5.5(D133):读可见文本,保留 expect 的自动轮询语义(元素迟到/重渲染)。\n"
1744
+ " await expect.poll(async () => {\n"
1745
+ " try { return await atlasVisibleText(el); } catch { return null; }\n"
1746
+ " }, { timeout: ATLAS_WAIT_TIMEOUT_MS }).toBe(v);\n"
1710
1747
  " }\n"
1711
1748
  "}\n"
1712
1749
  "async function atlasProbeOnce(page: any, p: any): Promise<boolean | null> {\n"
package/shared/gates.md CHANGED
@@ -42,8 +42,8 @@
42
42
  | `validators/validate_stack_profile.py` | 项目声明 ⇔ 共享契约 §2/§3.3 | `[机器]` 字段契约、`origin` 枚举、`apps[]` 枚举与路径、`adapters` 名字可解析、**`graph` 段契约(键白名单 {`backend`, `pinned`} + backend 枚举 {`cgc`} + pinned 语义版本形态;段缺失 = 合法,声明非法 = FAIL,2026-10-05 图谱批 1)**;**`e2e.app_login.mode` 枚举(real/fabricated/none,缺省 real)+ `body_format` 枚举(form/json,缺省 form)+ `mode: none` 字段互斥 + 登录形态未声明 WARN**(`app_base_url` 已声明而 `app_login` 未声明 ⇒ WARN 指名,中性文案;2026-10-16,`RFC-20261015-02` / issue #11) | `prototype` 段 = N/A(E1 退役) | 适配器**跑出来的 JSON 对不对**(那是适配器自己的事);`stack-profile` 与代码实际栈是否相符;**cgc 是否真装了 / 版本是否对齐 pin**(运行时消费者显式 WARN / not_ready,本器只判声明形态) | 装配 / 任一环启动前;**调度者 = `atlas_check`**(`§B120`:纳入 `VALIDATORS` + 三态前置,无 stack-profile ⇒ `SKIP`) | `出厂级(历史实测)`:`test_stack_profile_origin_vocabulary_in_sync`(词表守恒,加第三个取值 ⇒ 红)+ **M27**(调度判据⑥:从 `VALIDATORS` 摘本门 ⇒ 红,`§B120`)+ **图谱批 1**:`tests/test_stack_profile_graph.py`(未知 backend / 未知键 / pinned 非法 / 标量段 ⇒ 各自红;`graph: null` 与缺失 ⇒ 合法)+ `test_graph_backend_vocabulary_in_sync`(GRAPH_BACKENDS/GRAPH_KEYS ⇔ 契约正文守恒)+ **RFC-20261015-02(issue #11)**:`test_stack_profile_app_login_none_mode_is_explicit_not_degraded`(none = 显式声明 PASS / 残留字段 FAIL;含 **APP_LOGIN_MODES 词表守恒钉**——此前该 MODES 族是唯一无守恒钉的)/ `test_stack_profile_login_shape_undeclared_warns_neutrally`(WARN 中性文案 + 未到真跑不提前吹);变异 **M69**(none 互斥判据卸恒真 ⇒ 残留夹具绿)/ **M70**(WARN 前提挂错缺省就吹 ⇒ 未到期项目变吹) |
43
43
  | `validators/validate_prd.py` | PRD 产物 ⇔ PRD 环契约 | `[机器]` 文档头 / 零占位符 / 链接可达 / 功能→规则→AC 链路 / 范围形态与 §六 形态一致 / 最小必填集 / 追溯总表计数对账 / **追溯总表行形态与未覆盖登记** / **§五 AC 表列头(三列、不得含 `样例类型`)** / **域间依赖**(域级 frontmatter `depends_on` ⇔ 全局 §五 mermaid **实线**边集:悬空引用 / 实线子图成环 / 不一致 ⇒ FAIL;虚线(增强链路・数据回流)不入判据;无声明或无图 ⇒ N/A。**节点写法 / 箭头方向 / 连字符 / 含空格中文名的规格见 `rings/prd/reference.md` §4 的「§五 mermaid 写法约定」**——2026-10-15 补,issue #7:判据存在却无对应规格 = 把契约藏进了实现,只读契约的执行者过不了门。2026-10-04 补,`3509 §B186-1`) / **清单自洽(结构类 + 不可验)**(两清单小节里的 AC 必须在本文档 AC 表存在——幽灵豁免会给出覆盖门开后门;2026-10-12 补,`3509 §B201-3`)+ **域级 PRD 核心资产门(二/四/五章及结构类清单小节必填,issue #29)** | AC 是否**真的可判定**、`Then` 是否断言实现、四类样例覆盖(**已移交 E2E 环**,`3509 §B74`)、`Then` 与用例断言是否同源 | 域级 PRD 落笔后(`apply` / 无需求卡修订收口) | `出厂级(本轮实测)`:M1 删「`样例类型` 列」判据 / M2 删「未覆盖行数」判据 / M3 删「行形态非法」判据 ⇒ 各自红;另有历史 M1–M3(`3509 §B109` 链路);**M6** 实线正则改吃虚线(`-.?->`)/ **M7** 删成环判据 / **M8** 删不一致判据 ⇒ 各自红(2026-10-04 实测,随域间依赖门 +4 钉子);**M61(2026-10-15,issue #7)** 节点 id 正则退回 `\w+` ⇒ `test_f39_hyphenated_slug_nodes_resolve` 红 / `domain_name` 退回 `\S+` ⇒ `test_f39_spaced_chinese_domain_name_resolves` 红 / 边抽取退回整段 `findall` ⇒ `test_f39_chained_edges_are_not_silently_dropped` 与 `test_f39_labelled_edges_are_not_silently_dropped` 红(链式与边标签本来就是**静默丢边**) |
44
44
  | `validators/validate_testids.py` | testid **应然(用例分片)⇔ 实然(前端源码)** | `[机器]` 按页双向集合(`已实现` 页必须相等;`实现中`/`未开始` 降 WARN + 清单;测试目录/文件与夹具不入实然抽取面,issue #27/#33)、命名规范(分片新声明命名违规 ⇒ FAIL;存量实然命名违规降 WARN,契约 §4 保持现状承诺)、全站唯一、未实现页 SKIP、归不到页的实然 WARN;**模板字面量展开**(`data-testid={`…${slug}…`}`,页 slug = 页面表、其余 = 同文件静态属性;2026-10-04 互锁修复)+ **应然落点分档(已实现页声明了但不对 FAIL;未实现/实现中页待落地 WARN,issue #34)** | testid 名字**取得对不对**(语义);元素是否真该有 testid(语义);**模板展开值的「多余」**(外壳挂载范围静态未知 ⇒ 只判缺失不判多余,2026-10-04);`data-atlas-panel` 是否标(运行期门用得到,但本器不判) | **该页源码出现时**(静态对账,零执行成本)——检查点搭便车于必然存在的工件 | `出厂级(历史实测)`:`3509 §B114` 轮(实然侧失明 ⇒ 5 红)+ `§B106` 轮 M1/M2 |
45
- | `validators/validate_e2e_index.py` | 索引 ⇔ 分片 ⇔ 契约 | `[机器]` 结构门(列头 / ID 唯一 / 链接与标题一致 / 字段齐 / 无孤儿死链 / `pages` 合法 / 反向视图一致 / 词表 / `关联AC` 格式与可达)+ 有意义门(空占位 / 不可判定词 / 纯跳转 / 瞬时断言 / 未复断言 / 「或」式 / 文案来源(**先剥 `#` 注释**,`§B95`)/ `initial:` 无理由 / `intent` / 断言数 / `skipped` 原因 / **`goto` 路径 ∈ 页面表**(2026-09-30 补)/ **`auth` 取值合法**(2026-09-30 补))+ **seed × models 对账**(`seed:` 的实体×字段 ⇔ structure 的 `data-models` 事实;无事实 ⇒ `NA`)+ **AC 反向覆盖**(追溯门,2026-10-04 补 `§B186-2`:域内页面全 `已实现` ⇒ 该域 §五 定义的每个 AC 须被 ≥1 条 `关联AC` 指向,零挂靠 ⇒ **WARN** + 逐域清单;含 `实现中`/`未开始` 页面的域不参与;覆盖关系为派生量、不落第 4 张手工表;豁免落点定形后升 FAIL) | 「这条断言是否真的异源」(语义,§12 独立审查);`precondition` 是否说谎(语义);**零步基线(运行期门,见 2.4)**;播种**是否真执行**(归生成物 + 项目钩子) | 用例落笔后(`apply` / 修订收口) | `出厂级(本轮实测)`:`tests/test_validators.py` **138 条**本轮跑绿(2026-10-22 D132 hash 查询参数 +7;2026-10-21 D130 覆盖维度 +1;2026-10-19 D113「动态名诊断」+2:动态名命中模板结构 ⇒ 提示改指 §4 正规形态且不再宣称已失效的豁免(正向 1)/ 真漏实现不附动态指引(反向 1);变异 M96(整段回退旧诊断)/ M97(只退判据为词形子串);2026-10-18 D109「宿主注入型 URL 参数」通道 +7:门重靶 1 改名(`goto 不带查询参数` → `goto 查询键须已声明`,判据 = 键须 ∈ `e2e.query_preset.keys` ∧ ∈ 本用例 `参数:` 声明)+ 新门 3(`参数 声明有效` / `参数 值形态` / `会话·参数 同键不叠写`)+ 正向 1 + 坏输入 6(未声明白名单 / 键在白名单外 / 声明带值 / 死声明 / 值形态越界 / 同键双载体);2026-10-17 `dispatch` 动词值域门 +1(第三位 ⇒ `step 开关取值合法` FAIL / 缺省 ⇒ PASS + `step 动词合法` PASS;与生成器中止双保险——治旧安装副本「陌生第三词静默吸收」,2026-10-17 v3 现场)/ 2026-10-16 issue #R2 空 `testid:` 出声钉 +1(AC-3/R2:空应然清单 ⇒ `分片块字段齐` FAIL,文本期把「空守卫面」挡下)/ 2026-10-16 issue #R3 守卫门内剥注释钉 +1(**绕过 `parse_block`** 回注行内注释的坏输入 ⇒ 撤掉 F3 门内剥注释即红)/ 同日 issue #R4 `click`/`hover` 开关值域门 +1(坏值 ⇒ `step 开关取值合法` FAIL / `native` ⇒ PASS);2026-10-16 issue #23 守卫路径门 +2:正向 1(`/home/42` 前缀命中 `/home` ⇒ PASS)/ 坏输入 1(页面表外路径 ⇒ FAIL);同日 issue #13 会话预设门 +4:正向 1(键在白名单内 ⇒ PASS)/ 坏输入 3(未声明白名单 ⇒ FAIL / 键在白名单外 ⇒ FAIL / 行缺 `=` ⇒ FAIL);同日 issue #20 应然落点门 +4:正向 1(落点齐且可机检 ⇒ PASS)/ 坏输入 3(指向不存在文件 ⇒ FAIL / 文件内解析不到该 id ⇒ FAIL / 未登记落点 ⇒ WARN);同日 issue #19 同文件静态字符串常量 + 单层引用解析 +4:正向 1(常量引用 ⇒ PASS)/ 坏输入 3(非静态初始化式 ⇒ 不解析报缺失 / 跳文件导入 ⇒ 不解析 / 同常量被引两次 ⇒ 同文件重复 WARN);2026-10-16 F45 用例 ID 文法放宽「PAGE 段可含连字符」+3:多段可收 / 六类反例仍拒 / 单源接线;2026-10-12 随 §5.8 打桩门 +2:stack_profile mock/app_login.mode 门、e2e_mocks 门矩阵;同日 B201-1 upload 门 +1、B201-3 不可验豁免门 +2(豁免减法 / 幽灵豁免);2026-10-13 命名门存量实然豁免 +1;2026-10-14 F20 存量 adopt 模式未引 testid 降级 +1;2026-10-14 mock.mode auto 与 fixtures_dir +1;2026-10-15 v0.1.0-beta.14 +3)(含 seed×models 四侧 + **M16**;索引坏输入侧:孤儿 / 死链 / 标题不一致 / 词表外状态…)(`3509 §119`/`§121`;2026-09-30 随 goto/auth 门 +2;2026-10-04 随 **AC 反向覆盖门** +3:已实现域零挂靠 ⇒ WARN 吹 / 全覆盖 ⇒ PASS / 含未实现页的域不参与 ⇒ 不吹;**D130 覆盖维度**:`_parse_dims` 前后界定符(`C01正常流` 中文邻接须命中)+ `C01-C03` 区间展开 + 未知写法出声,钉 = `test_e2e_index_coverage_dimensions_pass_and_warn`(N/A / WARN / PASS / 未知维度 四态),变异 **M131**(摘维度缺口聚合 ⇒ 缺口钉不红)四段协议还原一致) |
46
- | `scripts/gen_e2e_scripts.py` | 用例分片 ⇔ 运行器脚本(派生) | `[机器]` 动词词表、testid 清单守卫、`--target` 已退役、URL = 索引页面表、**登录形态(`e2e.app_login.mode` = real / fabricated / none)+ 取证四值;`none` ⇒ 不生成登录预置设施、不打警告(声明形态非降级,段内取证字段残留 ⇒ 生成报错;`RFC-20261015-02`)**、未声明(第一阶段 = 占位生成 + WARN 升档预告;真跑准备日并入 `not_ready`)、审查门(有 `Critical` 拒绝写盘)、**前置未声明 ⇒ 退出码 3 + `not_ready`**、**`seed:` 行 ⇒ `_seed(...)` 调用**(未声明 `e2e.seed.hook` ⇒ 生成期 WARN + 运行期警告) | 深断言(放 `_support/`,不覆盖);运行器本身的正确性;播种**钩子实现**是否正确(项目自定) | 用例定稿后(生成即对账) | `出厂级(本轮实测)`:M2 未就绪退回退出码 2 ⇒ 红;**M17 不发射 seed 调用** ⇒ 红;另有历史变异(回潮硬编码 ⇒ 3 红,`ATLAS-UPSTREAM #113`)+ **RFC-20261015-02(issue #11)**:`test_setup_mode_none_generates_no_login_facilities`(**M71** none 分支仍生成 setup/storageState ⇒ 红)/ `test_setup_mode_none_rejects_leftover_evidence_fields`(残留取证字段 ⇒ 生成报错)/ `test_setup_without_app_login_degrades_loudly`(未声明 = 占位生成 + WARN 指路,升档后改推 `not_ready: "app_login"`)/ `test_app_login_section_incomplete_fails_generation`;**`dispatch` 动词(2026-10-17,v3 现场实证补)**:`dispatch <tid>` = React 合成事件直发(`dispatchEvent('click')`,不经 hit-testing / 可点性检查,不追加 force——常态遮罩+入场动画表面 force/native 双模实测双灭后的第三条动能,探针实证动画窗内 0 节拍直发两删连中;适用边界同 `native` 家族:被测属性 = 可操作性时不得用);样例守恒表 +1 词条、发射/值域钉 +1(`test_dispatch_verb_emits_synthetic_event_without_force`,第三位 ⇒ 报错中止) |
45
+ | `validators/validate_e2e_index.py` | 索引 ⇔ 分片 ⇔ 契约 | `[机器]` 结构门(列头 / ID 唯一 / 链接与标题一致 / 字段齐 / 无孤儿死链 / `pages` 合法 / 反向视图一致 / 词表 / `关联AC` 格式与可达)+ 有意义门(空占位 / 不可判定词 / 纯跳转 / 瞬时断言 / 未复断言 / 「或」式 / 文案来源(**先剥 `#` 注释**,`§B95`)/ `initial:` 无理由 / `intent` / 断言数 / `skipped` 原因 / **`goto` 路径 ∈ 页面表**(2026-09-30 补)/ **`auth` 取值合法**(2026-09-30 补))+ **seed × models 对账**(`seed:` 的实体×字段 ⇔ structure 的 `data-models` 事实;无事实 ⇒ `NA`)+ **AC 反向覆盖**(追溯门,2026-10-04 补 `§B186-2`:域内页面全 `已实现` ⇒ 该域 §五 定义的每个 AC 须被 ≥1 条 `关联AC` 指向,零挂靠 ⇒ **WARN** + 逐域清单;含 `实现中`/`未开始` 页面的域不参与;覆盖关系为派生量、不落第 4 张手工表;豁免落点定形后升 FAIL) | 「这条断言是否真的异源」(语义,§12 独立审查);`precondition` 是否说谎(语义);**零步基线(运行期门,见 2.4)**;播种**是否真执行**(归生成物 + 项目钩子) | 用例落笔后(`apply` / 修订收口) | `出厂级(本轮实测)`:`tests/test_validators.py` **139 条**本轮跑绿(2026-10-23 D134 type 动作侧 +1;2026-10-22 D132 hash 查询参数 +7;2026-10-21 D130 覆盖维度 +1;2026-10-19 D113「动态名诊断」+2:动态名命中模板结构 ⇒ 提示改指 §4 正规形态且不再宣称已失效的豁免(正向 1)/ 真漏实现不附动态指引(反向 1);变异 M96(整段回退旧诊断)/ M97(只退判据为词形子串);2026-10-18 D109「宿主注入型 URL 参数」通道 +7:门重靶 1 改名(`goto 不带查询参数` → `goto 查询键须已声明`,判据 = 键须 ∈ `e2e.query_preset.keys` ∧ ∈ 本用例 `参数:` 声明)+ 新门 3(`参数 声明有效` / `参数 值形态` / `会话·参数 同键不叠写`)+ 正向 1 + 坏输入 6(未声明白名单 / 键在白名单外 / 声明带值 / 死声明 / 值形态越界 / 同键双载体);2026-10-17 `dispatch` 动词值域门 +1(第三位 ⇒ `step 开关取值合法` FAIL / 缺省 ⇒ PASS + `step 动词合法` PASS;与生成器中止双保险——治旧安装副本「陌生第三词静默吸收」,2026-10-17 v3 现场)/ 2026-10-16 issue #R2 空 `testid:` 出声钉 +1(AC-3/R2:空应然清单 ⇒ `分片块字段齐` FAIL,文本期把「空守卫面」挡下)/ 2026-10-16 issue #R3 守卫门内剥注释钉 +1(**绕过 `parse_block`** 回注行内注释的坏输入 ⇒ 撤掉 F3 门内剥注释即红)/ 同日 issue #R4 `click`/`hover` 开关值域门 +1(坏值 ⇒ `step 开关取值合法` FAIL / `native` ⇒ PASS);2026-10-16 issue #23 守卫路径门 +2:正向 1(`/home/42` 前缀命中 `/home` ⇒ PASS)/ 坏输入 1(页面表外路径 ⇒ FAIL);同日 issue #13 会话预设门 +4:正向 1(键在白名单内 ⇒ PASS)/ 坏输入 3(未声明白名单 ⇒ FAIL / 键在白名单外 ⇒ FAIL / 行缺 `=` ⇒ FAIL);同日 issue #20 应然落点门 +4:正向 1(落点齐且可机检 ⇒ PASS)/ 坏输入 3(指向不存在文件 ⇒ FAIL / 文件内解析不到该 id ⇒ FAIL / 未登记落点 ⇒ WARN);同日 issue #19 同文件静态字符串常量 + 单层引用解析 +4:正向 1(常量引用 ⇒ PASS)/ 坏输入 3(非静态初始化式 ⇒ 不解析报缺失 / 跳文件导入 ⇒ 不解析 / 同常量被引两次 ⇒ 同文件重复 WARN);2026-10-16 F45 用例 ID 文法放宽「PAGE 段可含连字符」+3:多段可收 / 六类反例仍拒 / 单源接线;2026-10-12 随 §5.8 打桩门 +2:stack_profile mock/app_login.mode 门、e2e_mocks 门矩阵;同日 B201-1 upload 门 +1、B201-3 不可验豁免门 +2(豁免减法 / 幽灵豁免);2026-10-13 命名门存量实然豁免 +1;2026-10-14 F20 存量 adopt 模式未引 testid 降级 +1;2026-10-14 mock.mode auto 与 fixtures_dir +1;2026-10-15 v0.1.0-beta.14 +3)(含 seed×models 四侧 + **M16**;索引坏输入侧:孤儿 / 死链 / 标题不一致 / 词表外状态…)(`3509 §119`/`§121`;2026-09-30 随 goto/auth 门 +2;2026-10-04 随 **AC 反向覆盖门** +3:已实现域零挂靠 ⇒ WARN 吹 / 全覆盖 ⇒ PASS / 含未实现页的域不参与 ⇒ 不吹;**D130 覆盖维度**:`_parse_dims` 前后界定符(`C01正常流` 中文邻接须命中)+ `C01-C03` 区间展开 + 未知写法出声,钉 = `test_e2e_index_coverage_dimensions_pass_and_warn`(N/A / WARN / PASS / 未知维度 四态),变异 **M131**(摘维度缺口聚合 ⇒ 缺口钉不红)四段协议还原一致) |
46
+ | `scripts/gen_e2e_scripts.py` | 用例分片 ⇔ 运行器脚本(派生) | `[机器]` 动词词表、testid 清单守卫、`--target` 已退役、URL = 索引页面表、**登录形态(`e2e.app_login.mode` = real / fabricated / none)+ 取证四值;`none` ⇒ 不生成登录预置设施、不打警告(声明形态非降级,段内取证字段残留 ⇒ 生成报错;`RFC-20261015-02`)**、未声明(第一阶段 = 占位生成 + WARN 升档预告;真跑准备日并入 `not_ready`)、审查门(有 `Critical` 拒绝写盘)、**前置未声明 ⇒ 退出码 3 + `not_ready`**、**`seed:` 行 ⇒ `_seed(...)` 调用**(未声明 `e2e.seed.hook` ⇒ 生成期 WARN + 运行期警告) | 深断言(放 `_support/`,不覆盖);运行器本身的正确性;播种**钩子实现**是否正确(项目自定) | 用例定稿后(生成即对账) | `出厂级(本轮实测)`:M2 未就绪退回退出码 2 ⇒ 红;**M17 不发射 seed 调用** ⇒ 红;另有历史变异(回潮硬编码 ⇒ 3 红,`ATLAS-UPSTREAM #113`)+ **RFC-20261015-02(issue #11)**:`test_setup_mode_none_generates_no_login_facilities`(**M71** none 分支仍生成 setup/storageState ⇒ 红)/ `test_setup_mode_none_rejects_leftover_evidence_fields`(残留取证字段 ⇒ 生成报错)/ `test_setup_without_app_login_degrades_loudly`(未声明 = 占位生成 + WARN 指路,升档后改推 `not_ready: "app_login"`)/ `test_app_login_section_incomplete_fails_generation`;**`dispatch` 动词(2026-10-17,v3 现场实证补)**:`dispatch <tid>` = React 合成事件直发(`dispatchEvent('click')`,不经 hit-testing / 可点性检查,不追加 force——常态遮罩+入场动画表面 force/native 双模实测双灭后的第三条动能,探针实证动画窗内 0 节拍直发两删连中;适用边界同 `native` 家族:被测属性 = 可操作性时不得用);样例守恒表 +1 词条、发射/值域钉 +1(`test_dispatch_verb_emits_synthetic_event_without_force`,第三位 ⇒ 报错中止);**`type` 逐字输入动词(2026-10-23,D134)**:`type <tid> <text>` = `pressSequentially`(逐字触发 keydown/keypress/input;不清空原值;空值中止);样例守恒 +1、发射钉 +1(`test_type_verb_emits_press_sequentially_without_clearing`) |
47
47
  | `validators/validate_design_exemptions.py`(2026-10-08,`#182`) | 设计档案 detect 豁免 ⇔ `.impeccable/config.json` `detector.ignoreRules` | `[机器]` 双向集合相等:`product/design.md` 内 `detect:<rule-id>` ⇔ config ignoreRules,单侧漂移 ⇒ FAIL 指名(配置缺 = 报警没真关 / 档案缺 = 关报警没台账);无档案 ⇒ SKIP(三态 `§B120`);config 损坏 ⇒ FAIL | 豁免理由是否成立(语义,人侧终审);rule id 是否存在于 impeccable 规则表(不内嵌第三方规则表——升级会误伤);档案其余事实行内容(frontend-design skill 辖区) | 档案 / 配置改动后 + 每批收口 | `出厂级(本轮实测)`:`tests/test_validate_design_exemptions.py` **8 条**(**M43** 摘双向差集判据 ⇒ 两侧漂移钉子双红) |
48
48
  | `validators/validate_ac_alignment.py`(2026-10-12,`3507 BT` / B209-1 Phase 1) | 后端与全栈测试用例 ⇔ PRD 验收标准(AC)资产 | `[机器]` ① 无主 AC 拦截(测试挂载不存在的 AC ID ⇒ FAIL);② 无效跳过拦截(测试被 skip/xfail 但无 waiver 理由 ⇒ FAIL);③ 双向覆盖对账(输出已声明 AC 与测试挂载覆盖比;enforce 模式下零测试挂载 ⇒ FAIL);④ 差量防退化(对比基线快照,挂载测试数与覆盖 AC 数净增不得为负 ⇒ WARN) | 测试断言是否语义有效(Phase 2/3 变异测试与证据链承接);非 Python 测试提取(v1 仅原生支持 Python AST) | 后端代码实现完成 / 收口;**调度者 = `atlas_check`**(三态前置:无 PRD 资产 ⇒ `SKIP`) | `出厂级(本轮实测)`:`tests/test_validate_ac_alignment.py` 覆盖四维判据坏输入变异(无主 AC 报 FAIL / 无 waiver skip 报 FAIL / 有 waiver 放行 / enforce 零测试挂载报 FAIL / 差量退化报 WARN) |
49
49
  | `scripts/refresh_structure.py` | 结构事实区 ⇔ 适配器输出 | `[机器]` `atlas:facts` 标记内的行键级单元格重写、语义列保留、预算(每域 L1+L2 字符上限);**键级增删 ⇒ 未登记漂移记录**(落 `.adapter-out/_drift.json`,2026-10-04,⇒ 2.2 `结构漂移` 门) | 语义区内容;无 `.adapter-out/` 时**跳过**(那是合法降级,不是通过) | 收口 / `after_finish` hook | `出厂级(本轮实测)`:`tests/test_refresh_facts.py` **20 条**本轮跑绿(`3509 §119`;漂移注入/清除 + 竖线往返为 2026-10-04 门禁负向用例 #3 补;F25 `--chain` 序 + F26 基线自进四前置 + F27 单遍化 + F29 tour 消费行为 R7 追加;**F41**(2026-10-15,issue #8)`.adapter-out/` 单一位点三消费者同址钉;**F47**(2026-10-16,issue #16)渲染层「写出 ⇒ 读回」**逐单元格不变式** + 渲染即自校(单元格换行 → `<br>`,不再把一行撑成多行 ⇒ 事实不再丢行、幻影漂移不再常驻;`_norm` 把 `<br>` 与换行视作**等价**以保键稳定);**M-R1** 摘掉单行化 ⇒ `test_multiline_cell_value_round_trips` 红 / **M-R2** 换行→**空格** ⇒ 行结构断言红(防「用空格糊过去也算修好」)/ **M-R9** `_norm` 退回不折叠 `<br>` ⇒ `test_key_column_with_newline_does_not_phantom_drift` 红 / **M-R10** 摘掉 `_selfcheck_round_trip` 调用 ⇒ `test_render_selfcheck_fires_on_unroundtrippable_row` 红(自校必须**活着能咬**)) |
@@ -1016,6 +1016,79 @@ def test_select_and_value_are_implementation_agnostic() -> None:
1016
1016
  assert frag in ts, (frag, ts[:2000])
1017
1017
 
1018
1018
 
1019
+ def test_value_reads_visible_text_excluding_a11y_mirror() -> None:
1020
+ """契约 §5.5(D133,2026-10-23):`value` 读**可见文本**,不得把视觉隐藏的 a11y
1021
+ 播报节点算进去。
1022
+
1023
+ 现场(百炼/v3 邮件工作流):Ant Design Select 在用户交互后向触发器内注入
1024
+ `aria-live` 播报 span(`opacity:0`),`innerText` 把选中文案读两遍
1025
+ (`模式一…\\n模式一…`)⇒ `value` 断言假红。本钉从生成物抽出 `atlasVisibleText`
1026
+ 在 Node 里真求值:隐藏子树不计入、读完 style 原样恢复;同时钉发射面改用该助手。
1027
+ """
1028
+ with tempfile.TemporaryDirectory() as td:
1029
+ root = Path(td)
1030
+ make(root)
1031
+ assert run(root, "--apply").returncode == 0
1032
+ text = spec_of(root)
1033
+ assert "async function atlasVisibleText(" in text, text[:2000]
1034
+ assert "await atlasVisibleText(el)" in text, text
1035
+ # 探针侧(atlasValueOf)也必须走同一助手;只钉断言侧会让探针回退 innerText 仍绿
1036
+ assert " : atlasVisibleText(l);" in text, "atlasValueOf 探针未走可见文本助手"
1037
+ # 回归:非原生元素不得再走 innerText 直读
1038
+ assert "toHaveText(v, { useInnerText: true })" not in text, text
1039
+
1040
+ m = re.search(r"async function atlasVisibleText\(loc: any\): Promise<string> \{.*?\n\}\n", text, re.S)
1041
+ assert m, "未找到 atlasVisibleText 定义"
1042
+ # Node 不能直接 eval TS 注解:仅剥类型标注,逻辑逐字保留。
1043
+ js_src = (m.group(0)
1044
+ .replace("(loc: any): Promise<string>", "(loc)")
1045
+ .replace("(e: HTMLElement)", "(e)")
1046
+ .replace("const hidden: { el: HTMLElement; style: string | null }[] = []", "const hidden = []")
1047
+ .replace(" as HTMLElement[]", "")
1048
+ .replace("(node as any)", "node"))
1049
+ node = shutil.which("node")
1050
+ if not node:
1051
+ pytest.skip("node 不可用,跳过可见文本求值")
1052
+ script = """
1053
+ const factory = new Function(__SRC__ + "\\nreturn atlasVisibleText;");
1054
+ const helper = factory();
1055
+ function node({ text = "", opacity = 1, children = [] } = {}) {
1056
+ const n = {
1057
+ _hidden: false,
1058
+ _styleAttr: opacity === 0 ? "opacity: 0;" : null,
1059
+ children,
1060
+ text,
1061
+ style: { setProperty(name, value) { if (name === "display") { n._hidden = true; n._styleAttr = `display: ${value};`; } } },
1062
+ getAttribute(name) { return name === "style" ? n._styleAttr : null; },
1063
+ setAttribute(name, value) { if (name === "style") { n._styleAttr = value; n._hidden = /display\\s*:\\s*none/.test(String(value)); } },
1064
+ removeAttribute(name) { if (name === "style") { n._styleAttr = null; n._hidden = false; } },
1065
+ checkVisibility(opts = {}) { const opacityHidden = opts.opacityProperty === true && opacity === 0; return !n._hidden && !opacityHidden; },
1066
+ querySelectorAll() { return children.flatMap((c) => [c, ...c.querySelectorAll()]); },
1067
+ };
1068
+ Object.defineProperty(n, "innerText", {
1069
+ get() { return [n.text, ...children.filter((c) => !c._hidden).map((c) => c.innerText)].join(""); },
1070
+ });
1071
+ return n;
1072
+ }
1073
+ (async () => {
1074
+ const label = "模式一:读取邮件(获取全文,可带附件)";
1075
+ const shown = node({ text: label });
1076
+ const live = node({ text: label, opacity: 0 });
1077
+ const rootEl = node({ children: [shown, live] });
1078
+ const loc = { evaluate: async (fn) => fn(rootEl) };
1079
+ const value = await helper(loc);
1080
+ process.stdout.write(JSON.stringify({ value, restored: live.getAttribute("style"), hidden: live._hidden, label }));
1081
+ })().catch((err) => { console.error(err); process.exit(1); });
1082
+ """
1083
+ script = script.replace("__SRC__", json.dumps(js_src, ensure_ascii=False))
1084
+ proc = subprocess.run([node, "-e", script], capture_output=True, text=True)
1085
+ assert proc.returncode == 0, proc.stderr
1086
+ got = json.loads(proc.stdout)
1087
+ assert got["value"] == got["label"], f"隐藏的 a11y 播报被计入 value:{got}"
1088
+ assert got["restored"] == "opacity: 0;", f"采值后未恢复隐藏节点原始 style:{got}"
1089
+ assert got["hidden"] is False, f"采值后隐藏节点仍处于 display:none:{got}"
1090
+
1091
+
1019
1092
  def test_download_renders_register_then_click() -> None:
1020
1093
  """契约 §5.5 / §7.1(`3509 §B26`):`download` 属**动作**,必须渲染为「**先注册下载监听 →
1021
1094
  再点击 → 校验建议文件名**」—— 下载是动作的副作用,事后无法补取证。
@@ -1171,6 +1244,7 @@ ACTION_SAMPLES = {
1171
1244
  "check": "home-cta-btn",
1172
1245
  "uncheck": "home-cta-btn",
1173
1246
  "press": "home-cta-btn Enter",
1247
+ "type": "home-title 首页",
1174
1248
  "hover": "home-cta-btn",
1175
1249
  "refresh": "",
1176
1250
  "goto": "/home",
@@ -1201,6 +1275,32 @@ def test_every_action_verb_renders() -> None:
1201
1275
  assert "home-cta-btn" in spec, (verb, spec)
1202
1276
 
1203
1277
 
1278
+ def test_type_verb_emits_press_sequentially_without_clearing() -> None:
1279
+ """契约 §5.5(D134,2026-10-23):`type` 渲染为 `pressSequentially`(逐字键入,
1280
+ 不清空原值),支持 `[N]` 下标;空值 ⇒ 报错中止(不静默空转)。
1281
+ """
1282
+ gn = _load_gen()
1283
+ case = {
1284
+ "id": "E2E-TYPE-001",
1285
+ "_title": "逐字输入日期",
1286
+ "step": ["type date-input[1] 2026-10-01"],
1287
+ "expected": ["visible date-input"],
1288
+ "testid": ["date-input"],
1289
+ }
1290
+ spec = gn.ts_case(case, "/")
1291
+ assert "await page.getByTestId('date-input').nth(1).pressSequentially(\"2026-10-01\");" in spec
1292
+ assert ".fill(" not in spec
1293
+ assert ".clear(" not in spec
1294
+ with pytest.raises(ValueError, match="type"):
1295
+ gn.ts_case({
1296
+ "id": "E2E-TYPE-002",
1297
+ "_title": "空值中止",
1298
+ "step": ["type date-input"],
1299
+ "expected": ["visible date-input"],
1300
+ "testid": ["date-input"],
1301
+ }, "/")
1302
+
1303
+
1204
1304
  def test_dispatch_verb_emits_synthetic_event_without_force() -> None:
1205
1305
  """契约 §5.5(2026-10-17):`dispatch` 渲染为 `dispatchEvent('click')`(合成事件直发,
1206
1306
  不做 hit-testing / 可点性检查,不追加 force);无第三位(值域封闭,出现 ⇒ 报错中止)。
@@ -2430,6 +2530,7 @@ def test_strict_mode_actions_and_asserts_use_first() -> None:
2430
2530
  "click item-btn",
2431
2531
  "hover item-card",
2432
2532
  "fill item-input hello",
2533
+ "type item-input 你好",
2433
2534
  "press item-input Enter",
2434
2535
  "check item-checkbox",
2435
2536
  "uncheck item-checkbox",
@@ -2452,6 +2553,7 @@ def test_strict_mode_actions_and_asserts_use_first() -> None:
2452
2553
  assert "await page.getByTestId('item-btn').first().click({ force: true });" in spec
2453
2554
  assert "await page.getByTestId('item-card').first().hover({ force: true });" in spec
2454
2555
  assert "await page.getByTestId('item-input').first().fill(\"hello\");" in spec
2556
+ assert "await page.getByTestId('item-input').first().pressSequentially(\"你好\");" in spec
2455
2557
  assert "await page.getByTestId('item-input').first().press(\"Enter\");" in spec
2456
2558
  assert "await page.getByTestId('item-checkbox').first().check();" in spec
2457
2559
  assert "await page.getByTestId('item-checkbox').first().uncheck();" in spec
@@ -1487,6 +1487,25 @@ def test_e2e_index_download_is_action_not_assertion() -> None:
1487
1487
  assert bad["expected 动词合法"] == "FAIL", bad
1488
1488
 
1489
1489
 
1490
+ def test_e2e_index_type_is_action_not_assertion() -> None:
1491
+ """契约 §5.5(D134,2026-10-23):`type` 属**动作**(写在 `step` 侧)。
1492
+
1493
+ 两侧同测:写在 `step` ⇒ 校验器放行(`step 动词合法` PASS);写在 `expected` ⇒ 被拒
1494
+ (断言词表里没有它)——反向证据:它确实落在动作侧,不是两边都收。
1495
+ """
1496
+ ok = _levels(CASE_GOOD.replace(
1497
+ "- click home-cta-btn # 点击主操作按钮",
1498
+ "- type home-title 首页 # 逐字键入标题",
1499
+ ))
1500
+ assert ok["step 动词合法"] == "PASS", ok
1501
+
1502
+ bad = _levels(CASE_GOOD.replace(
1503
+ "- text home-title 首页 # 页面标题文案为「首页」",
1504
+ "- type home-title 首页",
1505
+ ))
1506
+ assert bad["expected 动词合法"] == "FAIL", bad
1507
+
1508
+
1490
1509
  def test_e2e_index_upload_is_action_and_fixture_reference_closed() -> None:
1491
1510
  """契约 §5.5(B201-1,2026-10-12):`upload` 属**动作**(写在 `step` 侧),
1492
1511
  值 = **相对 `product/e2e/fixtures/` 的文件路径**(与 §5.8 mock 同一 fixtures 树)。
@@ -102,7 +102,7 @@ REFRESH_WORDS = ("刷新", "reload", "refresh")
102
102
  I18N_HINTS = (re.compile(r"\bt\("), re.compile(r"\b[a-z]+\.[a-z]+\.[a-z]+\b"), re.compile(r"\b[A-Z]{2,}_[A-Z_]+\b"))
103
103
 
104
104
  # 分片 step / expected 的动词词表(契约 `rings/e2e/reference.md` §5.5,2026-09-23 定)
105
- ACTION_VERBS = ("click", "fill", "select", "check", "uncheck", "press", "hover", "refresh", "goto", "waitFor", "download", "upload", "dispatch")
105
+ ACTION_VERBS = ("click", "fill", "select", "check", "uncheck", "press", "type", "hover", "refresh", "goto", "waitFor", "download", "upload", "dispatch")
106
106
  ASSERT_VERBS = ("text", "contains", "count", "countOptions", "visible", "hidden", "enabled", "disabled", "checked", "unchecked", "value", "attr", "delta")
107
107
  # 契约 §5.6 R9:`expected` 行可加**行首前缀**,显式声明该断言的判据形状。
108
108
  # 本校验器只负责**形态项**(前缀后是词表内的动词 / 不嵌套 / `unchanged:` 的用例有提交性动作);