From 8421090f97c2817636bf4adffdbdf82451bf4e68 Mon Sep 17 00:00:00 2001 From: zyh Date: Tue, 7 Apr 2026 01:17:11 +0000 Subject: [PATCH] feat(d8d-dev): merge 14+ round journey verification enhancements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged from bmad-dev-pipeline (bar-party project) back to upstream template: New files: - reference/platform-uni-app.md: uni-app platform knowledge (icons, tabbar, nav, layout) - visual-verification.md: independent visual verification checkpoint spec Major enhancements: - AUTO-FIX #15-#21: english enum→CN mapping, constraint mismatch, SVG data URI→Iconify, dual navbar fix, pull-to-refresh unification, API try-catch, onMounted→onShow - R7 constraint consistency rule (journey-authenticity-rules.md) - Zero-interaction element detection (FAIL #5 diagnostic) - Per-step visual verification checkpoint (not just final screenshot) - "Known issues no exemption" iron law - Design system generalization (Neon Pulse → dynamic from 04-ui-ux-design.md) - Icon/layout reference extraction to platform-uni-app.md - Categorized commit phase (6 categories) Co-Authored-By: Claude Opus 4.6 --- .claude/skills/d8d-dev/auto-fix-engine.md | 280 +++++++++++- .claude/skills/d8d-dev/e2e-testing.md | 80 +--- .claude/skills/d8d-dev/iron-laws.md | 41 +- .../d8d-dev/journey-authenticity-rules.md | 90 +++- .claude/skills/d8d-dev/journey-mode.md | 104 +++-- .../skills/d8d-dev/phase-02-create-spec.md | 2 +- .claude/skills/d8d-dev/phase-03-implement.md | 72 +--- .../d8d-dev/reference/platform-uni-app.md | 399 ++++++++++++++++++ .../d8d-dev/reference/practical-experience.md | 6 +- .../d8d-dev/reference/upstream-integration.md | 2 +- .claude/skills/d8d-dev/validation-mode.md | 83 ++-- .claude/skills/d8d-dev/visual-verification.md | 314 ++++++++++++++ 12 files changed, 1271 insertions(+), 202 deletions(-) create mode 100644 .claude/skills/d8d-dev/reference/platform-uni-app.md create mode 100644 .claude/skills/d8d-dev/visual-verification.md diff --git a/.claude/skills/d8d-dev/auto-fix-engine.md b/.claude/skills/d8d-dev/auto-fix-engine.md index 6fda97b..f48ad12 100644 --- a/.claude/skills/d8d-dev/auto-fix-engine.md +++ b/.claude/skills/d8d-dev/auto-fix-engine.md @@ -33,7 +33,7 @@ | 2 | API 返回 500 / 错误状态码 | Bug | AUTO-FIX | 空 body 返回 500 | | 3 | 前端组件/按钮未渲染 | 未实现 | AUTO-IMPLEMENT | 登录入口按钮缺失 | | 4 | 页面跳转到错误位置 | Bug | AUTO-FIX | 导航逻辑错误 | -| 5 | 页面停留在"加载中"/空白/错误态 | Bug | AUTO-FIX | fetchProfile 空 catch + 响应结构不匹配 | +| 5 | 页面停留在非功能态("加载中"/空白/错误态/有内容但零交互元素) | Bug | AUTO-FIX | hydration 失败 / 渲染 Bug / fetchProfile 空 catch / 响应结构不匹配 | | 6 | 可交互元素无法通过 Playwright 操作 | Bug | AUTO-FIX | uni-input 编译后 browser_type 失败 | | 7 | 功能提示"暂未开放"/"开发中"/"暂未实现" | 未实现 | AUTO-IMPLEMENT | 手机号登录只有占位符 | | 8 | 测试数据过期/状态不对 | 数据问题 | AUTO-DATA-FIX | 收益冻结期不可提现 | @@ -43,6 +43,13 @@ | 12 | 操作被前端表单校验拦截(Toast/提示阻止提交)| 环境限制/校验 | ENV-ADAPT | 按 Toast 内容分流:缺少必填→补充填写;文件上传→browser_file_upload;环境限制→代码添加 dev 替代 | | 13 | 表单校验失败但页面无任何提示(无 Toast/无错误文字)| Bug(UX 缺陷)| AUTO-FIX | 用户不知道为什么操作无效 → 添加 Toast 或内联错误提示 | | 14 | 即时使用数据(验证码等)仅通过 Toast 显示,无 console.log 或自动填充,下一步骤无法获取 | Bug(数据丢失风险)| AUTO-FIX | sendCode() 用 Toast 显示验证码但无 console.log,流程卡死 | +| 15 | UI 直接输出英文枚举值(如 APPROVED/REJECTED/PENDING/PAID/CANCELLED 等)而非本地化中文标签 | Bug(UI 本地化缺失)| AUTO-FIX | 任何前端页面(管理后台/小程序/H5)状态字段显示英文枚举而非中文 | +| 16 | 约束声明与代码行为不一致(Constraint Mismatch): 旅程描述/UI 文案说"可选/可跳过/选填"但代码实际必填,或 UI 标记"选填"但接口必填 | 约束不一致 | JOURNEY-FIX / AUTO-FIX | 旅程说"可跳过上传证件"但代码 `if (!idCardFront)` Toast "请上传所有证件照片" | +| 17 | 前端代码 CSS/style 含 `url("data:image/svg+xml")` 或 `url('data:image/svg+xml')`(SVG data URI 充当图标/背景/装饰) | Bug(平台不兼容) | AUTO-IMPLEMENT | 微信小程序不支持 SVG data URI。⛔ 必须按 platform-uni-app.md §1 完整安装 Iconify + tailwindcss-icons + weapp-tailwindcss,将所有 SVG data URI 替换为 Iconify 类名。**禁止用另一种 SVG data URI 替代原来的** | +| 18 | 页面同时有系统导航栏(pages.json navigationBarTitleText 非 custom)和自定义 fixed header,导致双层标题栏 | Bug(双重导航栏) | AUTO-FIX | 截图中顶部出现两层标题(系统导航+自定义头部),参见 ./reference/platform-uni-app.md §3 | +| 19 | Tab 页面下拉刷新方式不一致(scroll-view refresher / onPullDownRefresh / 无 混用),或使用列表级 scroll-view refresher | Bug(UX 不一致) | AUTO-FIX | 首页用 scroll-view refresher,其他 Tab 无刷新,参见 ./reference/platform-uni-app.md §7 | +| 20 | API route handler 无 try-catch 包裹,导致未处理异常直接返回原始 500 且无日志 | Bug(健壮性) | AUTO-FIX | Admin 提现审批 PUT 返回 500 但状态仍更新(异常被吞) | +| 21 | uni-app 页面使用 `onMounted` 获取数据,但该页面是 `uni.navigateBack()` 的返回目标,导致回退时数据不刷新 | Bug(数据陈旧) | AUTO-FIX | 提现后返回收益页余额未刷新(onMounted 不再触发) | | **99** | **以上都不匹配的任何 FAIL** | **默认: Bug** | **AUTO-FIX** | **兜底规则,确保无遗漏** | ### "可接受"严格限定(⛔ 仅以下 2 种,其余一律必须修复) @@ -52,17 +59,50 @@ **除以上 2 种外,任何 FAIL 不得标记为"可接受"。功能缺失、代码错误、交互故障均必须修复。** +### ⛔ "已知问题不豁免"规则(⛔ 强制 — 跨轮次生效) + +**核心原则**: "已知" ≠ "已修复"。任何在之前的验证轮次中被发现但未实际修复的问题,在后续轮次再次检测到时,**必须**立即触发修复,不得以任何理由跳过。 + +**判定流程**: +``` +检测到 FAIL → 查分类表确定修复类型 + ↓ +读取 pipeline-state.yaml 前一次验证结果 + ↓ +该问题是否在上一轮已被记录(pending_fixes / verification_detail 中存在)? + ├── 是(跨轮次未修复)→ ⛔ 提升优先级,立即执行修复(不得再次"仅记录") + └── 否(新问题)→ 按正常分类触发修复 +``` + +**⛔ 禁止行为**: +- ❌ 以"已知/遗留/历史/V3遗留"为由跳过修复 +- ❌ 只记录到 `pending_fixes` 而不执行实际修复动作 +- ❌ 将修复推迟到"旅程完成后"或"下次验证" +- ❌ 标记"已记录"作为修复动作("已记录"不是修复) +- ❌ 以"非当前 EPIC/旅程范围"为由跳过 pending_fixes 中的问题(范围外 ≠ 豁免) + +**跨轮次问题升级规则**: +- 第 1 轮检测到 → 正常修复流程 +- 第 2 轮仍未修复 → **强制修复**(不得延迟,不得降级为 WARN) +- 第 3+ 轮仍未修复 → **阻断当前旅程**(标记 FAIL + 输出修复失败报告) + ### 关键判断流程 - API 404 → `grep` 检查路由文件是否存在 → 存在 = Bug(#2) → 不存在 = 未实现(#1) - 页面显示"暂未开放"/"开发中"/"暂未实现" → 一律未实现(#7) → AUTO-IMPLEMENT - 元素可见但不可操作(`browser_type` 失败) → Bug(#6) → ⛔ **先诊断**(`browser_evaluate` 检查 DOM/CSS) → 按诊断结果分流修复(禁止直接用 `browser_run_code` 绕过) - 页面卡在"加载中..."/空白 → Bug(#5) → AUTO-FIX(调查根因:空 catch、响应结构不匹配、状态管理错误) +- 页面有静态内容但零交互元素(有文字但无 button/input/a/form)→ `browser_evaluate` 执行 FAIL #5 诊断 → 按诊断结果分流(见 FAIL #5 诊断子步骤) - 操作后页面无变化(无跳转/无反馈)→ browser_snapshot 检查 Toast → 按 Toast 内容分流(#12) → ENV-ADAPT - 有 Toast/错误提示 → 读取内容 → 按 #12 分流(补充填写 / browser_file_upload / ENV-ADAPT) - ⛔ 无任何提示 → Bug(UX 缺陷:校验失败但用户看不到原因)→ AUTO-FIX(添加 Toast/错误提示) - 即时使用数据(验证码)仅通过 Toast 显示且源码无 console.log → Bug(#14) → AUTO-FIX(添加 console.log + 自动填充) +- UI 显示英文枚举值(APPROVED/REJECTED/PENDING/PAID/CANCELLED 等)→ Bug(#15) → AUTO-FIX(添加中文映射函数,适用于所有前端:管理后台 statusLabel / 小程序 computed 映射) +- 前端代码含 `data:image/svg+xml`(SVG data URI)→ `grep -rn "data:image/svg+xml" app/mini-program/src/` 扫描所有 `.vue` 文件 → 发现 → 平台不兼容(#17) → AUTO-IMPLEMENT(⛔ 必须按 platform-uni-app.md §1 完整安装 Iconify + 替换所有 SVG data URI,**禁止用另一种 SVG data URI 替代原来的**) +- Tab 页面下拉刷新方式不一致(scroll-view refresher / onPullDownRefresh / 无 混用)→ Bug(#19) → AUTO-FIX(参见 ./reference/platform-uni-app.md §7 统一为 onPullDownRefresh) - 无法归类的 FAIL → 默认 Bug(#99) → AUTO-FIX +- API 返回 500 + `Read` 源码发现 handler 无 try-catch → Bug(#20) → AUTO-FIX(添加 try-catch 包裹 + console.error + JSON 500 响应) +- `uni.navigateBack()` 返回后 `browser_snapshot` 数据未刷新 + 源码用 `onMounted` 非 `onShow` → Bug(#21) → AUTO-FIX(替换 `onMounted` 为 `onShow`) --- @@ -90,8 +130,49 @@ 6. 修复后重试 `browser_type` 7. 仍失败 → 标记 journey-step-fail,继续下一步骤 +**⛔ FAIL #5 诊断子步骤(页面有内容但零交互元素时强制执行)**: + +**触发条件**: `browser_snapshot` 显示页面有静态内容(标题/文字),但没有 button、input、a、form 等可交互元素。 + +1. `browser_evaluate` 检查页面状态: + ```javascript + () => { + const interactive = document.querySelectorAll('button, input, a, form, [role="button"], [role="link"]'); + const hasContent = document.body.innerText.trim().length > 0; + // 检测框架挂载状态 + const hasNextRoot = !!document.getElementById('__next'); + const hasReactRoot = !!document.querySelector('[data-reactroot]'); + const hasVueApp = !!document.querySelector('[data-v-app]') || !!document.querySelector('#app')?.__vue_app__; + const frameworkMounted = hasNextRoot || hasReactRoot || hasVueApp; + // 检测 JS 是否加载 + const scriptTags = document.querySelectorAll('script[src]').length; + return { + interactiveCount: interactive.length, + hasContent, + frameworkMounted, + hasNextRoot, hasReactRoot, hasVueApp, + scriptTags, + bodyHTMLLength: document.body.innerHTML.length, + bodyText: document.body.innerText.substring(0, 300) + }; + } + ``` + +2. 根据诊断结果分流: + - `interactiveCount === 0 && frameworkMounted === true` → **框架已挂载,当前页面源码就是无交互的展示页** + → `grep/Read` 检查当前页面源码:源码无交互组件 → **功能缺失** → AUTO-IMPLEMENT(添加 redirect/导航逻辑) + → 源码有交互组件但未渲染 → **渲染 Bug(#5)** → AUTO-FIX + - `interactiveCount === 0 && frameworkMounted === false && scriptTags > 0` → **JS 加载但框架未挂载** → **Hydration 失败 Bug(#5)** → AUTO-FIX(检查控制台错误、检查构建产物) + - `interactiveCount === 0 && !hasContent` → **空白页** → Bug(#5) → AUTO-FIX(检查加载逻辑) + +3. 修复后重测: + - AUTO-IMPLEMENT 添加 redirect 后 → `browser_navigate` 重试 → 应自动跳转到功能页 → `browser_snapshot` 确认有交互元素 + - AUTO-FIX 修复 hydration/渲染后 → `browser_navigate` 重试 → `browser_snapshot` 确认交互元素已渲染 + **⛔ FAIL #6 诊断子步骤(`browser_type`/`browser_click` 失败时强制执行)**: +> uni-app/小程序编译特性诊断(如 uni-input height:0px)详见 **./reference/platform-uni-app.md 第 3.1 节**。 + 1. `browser_evaluate` 检查目标元素的 DOM 结构和计算样式: ```javascript (el) => { @@ -140,6 +221,9 @@ - API 响应结构与前端期望不匹配 → `Edit` 修正数据提取逻辑 - input 元素不可交互 → `browser_evaluate` 诊断 → 按根因修复(见 FAIL #6 诊断子步骤) - uni-app input `height: 0px` → `Edit` 给 `.input-field` 添加显式 `height: 44px` +- 管理后台首页缺少 redirect → 源码检查发现 page.tsx 是纯展示 → AUTO-IMPLEMENT: 添加 `redirect("/login")` +- React hydration 失败 → `browser_evaluate` 确认框架未挂载 → AUTO-FIX: 检查构建产物和 `use client` 指令 +- 任何前端页面状态字段显示英文枚举 → `Edit` 添加中文映射函数(管理后台: statusLabel / 小程序: computed 属性)→ 替换直接输出为映射函数调用 ### AUTO-IMPLEMENT(功能缺失,max 2 次) @@ -157,6 +241,20 @@ - 后端路由缺失 → `Write` 新 `route.ts` + `handler` - 图标方案未实施 → 按方案安装 Tailwind + Iconify +**Emoji → Iconify 替换方案(检测到 emoji 充当功能图标时使用 ⛔ 强制)**: + +⛔ **完整安装配置、使用方式、禁止方案列表见 ./reference/platform-uni-app.md 第 1 节。禁止自行发明替代方案。** + +适用条件: icon/btn 元素内含 emoji 字符(如 🔍🔔💬🪪💳📷🏠📱) + +执行步骤: +1. **Read fully and follow: ./reference/platform-uni-app.md 第 1 节**(⛔ 不可跳过 — 含 Admin/Mini-program 两条路径完整安装步骤) +2. **替换源码**: 将所有 emoji 字符替换为 Iconify 类名(如 🔍 → `class="i-lucide-search"`) +3. **Bash** 安装依赖 + 重新构建 +4. **重测** 验证图标渲染正常 + +⛔ **禁止的替代方案**(完整列表见 ./reference/platform-uni-app.md 第 1 节"禁止的替代方案") + **手机号+验证码登录实现方案(#11 触发时使用)**: 当旅程验证发现 Mock 登录(非真实用户流程)且手机号登录未实现时,按以下方案 AUTO-IMPLEMENT: @@ -201,6 +299,173 @@ if (res.code) { code.value = res.code; } uni.showToast({ title: "验证码已发送", icon: "success" }); ``` +### AUTO-FIX #18: 双重导航栏修复(max 1 次) + +**适用**: FAIL #18 — 页面在 pages.json 有 `navigationBarTitleText`(非 custom)且页面源码有 `position:fixed` 的自定义 header + +**为什么这是 Bug**: 系统导航栏与自定义头部同时显示,用户看到两层标题(如"首页"+"NEON PULSE"),浪费屏幕空间且视觉混乱。uni-app 小程序项目应统一使用 `navigationStyle: "custom"`。 + +**流程**: +1. `Read` pages.json → 找到该页面的 style 配置 +2. `Edit` pages.json → 将该页面 style 改为 `{ "navigationStyle": "custom" }`(移除或保留 navigationBarTitleText 均可,custom 模式下不显示) +3. `Read` 页面源码 → 找到自定义 header 的 CSS(通常为 `position: fixed; top: 0` 的元素) +4. `Edit` 页面源码 CSS: + - header 添加 `padding-top: env(safe-area-inset-top, 20px)` + - header height 改为 `calc(原高度 + env(safe-area-inset-top, 20px))` + - content margin-top 与新 header height 一致(含 safe-area) +5. 重测 → 截图确认只有一层标题 + +**修复示例**: +```css +/* 修复前(首页 header 与系统导航栏重叠): */ +.header { position: fixed; top: 0; left: 0; right: 0; height: 64px; } +.content { margin-top: 64px; } + +/* 修复后(统一使用自定义导航栏): */ +.header { + position: fixed; top: 0; left: 0; right: 0; + height: calc(64px + env(safe-area-inset-top, 20px)); + padding-top: env(safe-area-inset-top, 20px); +} +.content { + margin-top: calc(64px + env(safe-area-inset-top, 20px)); +} +``` + +```jsonc +// pages.json 修复: +// 修复前: +{ "path": "pages/home/index", "style": { "navigationBarTitleText": "首页" } } +// 修复后: +{ "path": "pages/home/index", "style": { "navigationStyle": "custom" } } +``` + +### AUTO-FIX #19: 下拉刷新方式统一(max 1 次) + +**适用**: FAIL #19 — Tab 页面下拉刷新方式不一致(scroll-view refresher / onPullDownRefresh / 无 混用) + +**⛔ 详细修复步骤见 ./reference/platform-uni-app.md §7** + +**流程**: +1. `Edit` pages.json → globalStyle 添加 `"enablePullDownRefresh": true` +2. 对 LIST_LEVEL 页面(scroll-view refresher): + - 移除 scroll-view 的 `refresher-enabled` / `:refresher-triggered` / `@refresherrefresh` + - 移除 `refreshing` ref 和 `onRefresh` 函数 + - 添加 `onPullDownRefresh` 生命周期(from `@dcloudio/uni-app`) + - 数据加载 `finally` 中调用 `uni.stopPullDownRefresh()` +3. 对 NONE 页面:添加 `onPullDownRefresh` + 对应数据刷新逻辑 + `uni.stopPullDownRefresh()` +4. 重测 → 所有 Tab 页面整页下拉刷新效果一致 + +### AUTO-FIX #20: API route try-catch 包裹(max 1 次) + +**适用**: API endpoint 返回 500,`Read` 源码发现 handler 函数无 try-catch 包裹 + +**为什么这是 Bug**: 未处理异常直接返回原始 500 且无日志,无法诊断根因。常见的二次操作(如审批后创建通知)失败会吞噬前面的成功状态。 + +**检测方法**: API 返回 500 时,`Read` 对应 route.ts 源码,检查 `export async function POST/PUT/DELETE/GET` 内是否有 try-catch + +**修复流程**: +1. `Read` 失败的 route.ts 文件 +2. 确认 handler 无 try-catch 包裹 +3. 在 requireAdmin/鉴权检查之后,将剩余逻辑包裹在 try-catch 中 +4. catch 块:`console.error("Xxx error:", error)` + `NextResponse.json({error: "处理失败"}, {status: 500})` +5. 参考模式:同项目中已有 try-catch 的 route(如 admin auth login) + +**修复模板**: +```typescript +// 修复前: +export async function PUT(req, { params }) { + const result = requireAdmin(req); + if ("error" in result) return result.error; + const body = await req.json(); // ← 无 try-catch,异常直接变 500 + // ... 业务逻辑 ... +} + +// 修复后: +export async function PUT(req, { params }) { + const result = requireAdmin(req); + if ("error" in result) return result.error; + try { + const body = await req.json(); + // ... 业务逻辑不变 ... + } catch (error) { + console.error("Xxx error:", error); + return NextResponse.json({ error: "处理失败" }, { status: 500 }); + } +} +``` + +### AUTO-FIX #21: uni-app 页面 onMounted→onShow 替换(max 1 次) + +**适用**: 旅程中 `uni.navigateBack()` 返回目标页面后,页面数据未刷新 + +**为什么这是 Bug**: `onMounted` 仅在组件首次创建时触发。当用户从子页面 `navigateBack()` 返回时,父页面组件仍在导航栈中未被销毁,`onMounted` 不会再次触发。uni-app 的 `onShow` 在每次页面显示时都会触发,是正确的数据刷新时机。 + +**触发条件**(三条件全部满足): +- 条件 A: 旅程中使用 `uni.navigateBack()` 返回到该页面 +- 条件 B: 返回后 `browser_snapshot` 显示数据未更新(如余额、列表数量未变化) +- 条件 C: `Read` 页面源码确认数据获取在 `onMounted` 而非 `onShow` 中 + +**修复流程**: +1. `Read` 页面 `.vue` 文件 +2. 修改 import:将 `import { ref, onMounted } from "vue"` 中 `onMounted` 移除(若 vue 导入不再需要 onMounted) +3. 添加 `import { onShow } from "@dcloudio/uni-app"`(新 import 行) +4. 将 `onMounted(() => { fetchXxx() })` 替换为 `onShow(() => { fetchXxx() })` +5. 参考模式:项目中已有正确使用 `onShow` 的页面(如 profile/index.vue) + +**修复模板**: +```typescript +// 修复前: +import { ref, onMounted } from "vue"; +// ... +onMounted(() => { fetchEarnings(); }); + +// 修复后: +import { ref } from "vue"; +import { onShow } from "@dcloudio/uni-app"; +// ... +onShow(() => { fetchEarnings(); }); +``` + +### AUTO-FIX/JOURNEY-FIX #16: 约束一致性修复(max 1 次) + +**适用**: FAIL #16 — 旅程描述/UI 文案说"可选"但代码实际必填,或 UI 标记"选填"但接口必填 + +**检测触发条件**(双条件同时满足): +- 条件 1:声明方说"可选"(旅程描述含"可跳过/可选/选填/skip/optional/使用占位图" 或 页面文本含"(选填)/(可选)") +- 条件 2:代码实际"必填"(Toast 含"请上传/请填写/请选择/不能为空" 或 源码有 `if (!field)` 验证) + +**为什么这是问题**: 用户被"可选"承诺误导,但操作时被强制校验拦截。真实用户体验受损。 + +**流程**: +1. `Read` 对应页面源码 → 定位必填验证逻辑(`if (!field)` / `required` / `validate`) +2. 判断正确设计意图: + - PRD/UX 规范说可选 → AUTO-FIX 代码(移除必填验证或改为可选提交) + - PRD/UX 规范说必填 → JOURNEY-FIX 旅程定义(更新描述为必填)+ AUTO-FIX UI 文案(移除"选填"标记) + - 无明确 PRD 依据 → JOURNEY-FIX(旅程定义改为反映代码行为)+ 记录 `constraint_mismatch` 到 pending_fixes +3. 修复后重测该步骤 + +**代码修复示例**(设计意图=可选时): +```javascript +// 修复前(强制必填): +if (!form.idCardFront || !form.idCardBack || !form.idCardHolding) { + uni.showToast({ title: "请上传所有证件照片", icon: "none" }); + return; +} + +// 修复后(改为可选): +// 移除强制校验,允许不上传照片直接提交 +``` + +**旅程修复示例**(设计意图=必填时): +```markdown +// 修复前: +9. 用户第二步上传身份证照片(可跳过或使用占位图)→ 点击"下一步" + +// 修复后: +9. 用户第二步上传身份证照片(必填:身份证正面、反面、手持照片)→ 点击"下一步" +``` + ### AUTO-DATA-FIX(数据状态,max 1 次) **适用**: 测试数据过期、冻结期收益不可用、关联数据缺失 @@ -248,10 +513,20 @@ uni.showToast({ title: "验证码已发送", icon: "success" }); GATE-CHECK: ├── 所有交互使用真实用户操作(无 browser_run_code 绕过 input/button) ← 真实用户完成规则 ├── browser_take_screenshot 已执行 ← 证据检查 +├── ⛔ analyze_image 已执行(所有截图都有对应 AI 视觉分析结果) ← 视觉验证检查 +│ 验证:pipeline-state 中 analyze_image_executed === true +│ 未执行 → 不得标记完成,必须回退执行视觉验证(见 ./visual-verification.md) ├── browser_network_requests 已执行 ← 网络检查 ├── failed_steps 数量 = 0 ← 断言检查 ├── 即时使用数据有 console.log 或自动填充(无 #14 违规) ← 数据可追溯检查 -└── auto_fix 对所有 FAIL 已触发 ← 修复检查 +├── auto_fix 对所有 FAIL 已触发 ← 修复检查 +├── ⛔ verification_detail 中每个 FAIL 项都有对应的 auto_fix 记录 ← FAIL-修复交叉验证 +│ 逐一比对 verification_detail 与 auto_fix 记录: +│ english_enum_check.found = FOUND → auto_fix 必须包含 #15 修复记录 +│ icons_check.emoji_as_icons = FOUND → auto_fix 必须包含 Iconify 替换修复记录 +│ icons_check.tabbar_icons_visible = FAIL → auto_fix 必须包含 TabBar 修复记录 +│ 缺少对应记录 → 不得标记完成,必须回退执行修复 +└── ⛔ 视觉验证 FAIL 的修复在内联关卡已执行(非推迟到步骤 4) ← 内联修复检查 结果判定: ├── 全部 PASS → 继续记录阶段,status: pass @@ -268,6 +543,7 @@ GATE-CHECK: - 用 `browser_evaluate` 绕过不可交互的 input/button 后标记步骤 PASS - 把"加载中..."/"暂未开放"判定为 PASS - 任何 FAIL 标记为"可接受"但不符合上述严格限定的 2 种情况 +- **用 SVG data URI / CSS background-image 替代 Iconify 图标方案**(微信小程序不支持 SVG data URI,必须用 @egoist/tailwindcss-icons + Iconify) --- diff --git a/.claude/skills/d8d-dev/e2e-testing.md b/.claude/skills/d8d-dev/e2e-testing.md index 09917df..c0c170c 100644 --- a/.claude/skills/d8d-dev/e2e-testing.md +++ b/.claude/skills/d8d-dev/e2e-testing.md @@ -32,7 +32,7 @@ - 步骤 0(resize)不能省略,即使"上一个 Story 刚 resize 过" - 步骤 3-5(交互)在页面有表单/按钮时**绝对不能省略** - 步骤 7(截图)是强制证据,不是可选装饰 -- 步骤 8(视觉验证)**不调用等于验证未完成** +- 步骤 8(视觉验证)**不调用等于验证未完成**(⛔ 详细规则和标准提示词见 ./visual-verification.md) - 步骤 9(API 目标检测)**前端 Story 必须执行**,检测页面 JS 发起的 API 请求是否指向 localhost 而非外网域名 ### API 调用目标检测(步骤 9 详解) @@ -95,16 +95,8 @@ INLINE_COUNT=$(echo "$INLINE_FILES" | grep -c ":" || echo 0) echo "inline_api=$INLINE_COUNT" [ "$INLINE_COUNT" -gt 0 ] && echo "INLINE_FILES:" && echo "$INLINE_FILES" -# 检查 3: 图标方案合规(小程序/移动端) -if find app/ -name "pages.json" -not -path "*/node_modules/*" -print0 | xargs -0 grep -l '"iconPath": ""' 2>/dev/null | head -1; then - echo "tabbar_iconpath_empty=true" -fi - -# 检查 4: Emoji 充当图标 -EMOJI_ICONS=$(grep -rn 'class="[^"]*icon[^"]*"[^>]*>[🔍🔔🏠📱💬👤⚙️📝➕❤️⭐📢✅❌]' app/*/src/ --include="*.vue" --include="*.tsx" | grep -v node_modules) -EMOJI_COUNT=$(echo "$EMOJI_ICONS" | grep -c ":" || echo 0) -echo "emoji_as_icons=$EMOJI_COUNT" -[ "$EMOJI_COUNT" -gt 0 ] && echo "EMOJI_FILES:" && echo "$EMOJI_ICONS" +# 检查 3: 图标方案合规 + Emoji 充当图标 +# 完整检测命令见 ./reference/platform-uni-app.md 第 6 节 ``` **判定规则**: @@ -124,7 +116,7 @@ echo "emoji_as_icons=$EMOJI_COUNT" 3. 重启前端服务 → 重新执行步骤 9.5 验证 **AUTO-IMPLEMENT(未实现类型)**: -按照 `phase-03-implement.md` 第 5 节的方案执行完整实现: +按照 `./reference/platform-uni-app.md` 第 1 节(图标方案)和第 2 节(TabBar 方案)执行完整实现: 1. 安装依赖(Tailwind + Iconify + weapp-tailwindcss) 2. 创建配置文件(tailwind.config.js, vite.config.ts) 3. 创建组件(自定义 TabBar) @@ -135,18 +127,16 @@ echo "emoji_as_icons=$EMOJI_COUNT" ### 增强 analyze_image 提示词(步骤 8 详解) -**目的**:当前 analyze_image 只检查 Neon Pulse 设计颜色,需增加多个维度,每个维度必须明确 PASS/FAIL。 +**目的**:当前 analyze_image 只检查项目设计系统颜色,需增加多个维度,每个维度必须明确 PASS/FAIL。 **提示词模板**: ``` 分析此截图,按以下维度逐一给出 PASS/FAIL 判定: -1. **设计规范(Neon Pulse)**: - - 背景:深色 #000 或接近黑色 - - 按钮:渐变色(粉→紫→青 135deg) - - 卡片:玻璃态效果(半透明 + blur) - - 无 1px 实线边框(用背景色区分区域) +1. **设计规范**(从项目 _bmad-output/04-ui-ux-design.md 提取): + - {执行时:Read _bmad-output/04-ui-ux-design.md → 提取设计系统规则 → 填入} + - 回退:文件不存在 → grep 项目 CSS :root 变量 → PASS/FAIL + 具体问题 2. **图标/图片完整性**: @@ -165,10 +155,14 @@ echo "emoji_as_icons=$EMOJI_COUNT" - 列表项是否有数据(非空列表且无"暂无数据"提示) → PASS/FAIL + 具体问题 -4. **布局合理性**: - - 内容是否在视口范围内(无严重溢出/裁剪) - - 文字是否可读(无截断/重叠) - - 底部 TabBar 是否完整可见(小程序页面) +4. **布局质量**(⛔ 逐一检查以下每一项,不得跳过): + a. 溢出检测:内容在视口范围内,无水平溢出或裁剪 + b. 文字可读:文字不被截断,不显示省略号,对比度足够 + c. 重叠检测:元素无重叠(标题栏与自定义头部不碰撞,导航栏不遮盖内容) + d. 间距合理性:无异常大间距(如导航栏与内容之间的过大空白),无紧贴元素(相邻交互元素应有明确间距) + e. 元素变形检测:按钮/输入框/卡片未被挤压变形(宽高比异常),文字未被截断 + f. 对齐检测:同行元素水平对齐,同列元素垂直对齐 + g. TabBar:底部 TabBar 完整可见(非 TabBar 页面此项 PASS) → PASS/FAIL + 具体问题 总结:列出所有 FAIL 项,如全部 PASS 则输出"全部 PASS"。 @@ -177,10 +171,10 @@ echo "emoji_as_icons=$EMOJI_COUNT" **结果记录**: ```yaml verification_detail: - design_check: { neon_pulse: PASS/FAIL, details: "..." } + design_check: { design_compliance: PASS/FAIL, design_spec_source: "04-ui-ux-design.md", details: "..." } icons_check: { tabbar_icons_visible: PASS/FAIL, broken_images: PASS/FAIL, emoji_as_icons: PASS/FAIL } functional_check: { elements_rendered: PASS/FAIL, text_readable: PASS/FAIL } - layout_check: { viewport_fit: PASS/FAIL, tabbar_visible: PASS/FAIL } + layout_check: { overflow: PASS/FAIL, spacing_anomaly: NONE/FOUND, element_deformation: NONE/FOUND, alignment: PASS/FAIL, tabbar_visible: PASS/FAIL } ``` ### 步骤 8.5: 布局溢出检测(小程序强制) @@ -188,34 +182,10 @@ verification_detail: **目的**:小程序页面在真实移动设备上经常出现宽度/高度溢出视口、底部 TabBar 被遮挡的问题。此步骤通过 JavaScript 检测这类布局问题。 **检测方法**: +> 完整的检测 JS 代码和 TabBar CSS 选择器见 **./reference/platform-uni-app.md 第 5.2 节**。 +> 防溢出规则见 **./reference/platform-uni-app.md 第 5.1 节**。 -在步骤 8(analyze_image)之后、步骤 9(network_requests)之前执行: - -``` -browser_evaluate: - function: () => { - const html = document.documentElement; - const body = document.body; - const tabBar = document.querySelector('.uni-tabbar, .tabbar, [class*="tabbar"], [class*="tab-bar"]'); - - return { - // 水平溢出检测 - horizontal_overflow: html.scrollWidth > window.innerWidth, - document_width: html.scrollWidth, - viewport_width: window.innerWidth, - overflow_diff: html.scrollWidth - window.innerWidth, - - // 底部 TabBar 可见性检测(仅小程序页面) - tabbar_found: tabBar !== null, - tabbar_visible: tabBar ? (tabBar.getBoundingClientRect().bottom <= window.innerHeight) : null, - tabbar_rect: tabBar ? tabBar.getBoundingClientRect() : null, - - // 页面总高度(用于判断内容是否超出视口太多) - page_height: html.scrollHeight, - viewport_height: window.innerHeight, - }; - } -``` +在步骤 8(analyze_image)之后、步骤 9(network_requests)之前执行。 **判定规则**: @@ -244,12 +214,6 @@ verification_detail: tabbar_visible: true/false/null # null = 页面无 TabBar ``` -**失败处理**: -1. 检查页面根元素是否有超过 `100vw` 的宽度设置 -2. 检查固定定位元素的 `bottom` 值是否考虑了 TabBar 高度 -3. 检查可滚动内容高度是否减去了 TabBar + Header 高度 -4. 修复后重新执行步骤 8.5 验证 - ### 用户旅程测试(Epic 级集成验证) **⛔ 工具选择强制规则**:执行旅程测试时,每个用户步骤必须遵循上述 Playwright MCP 测试序列(步骤 0-9)。**禁止使用 curl 替代 browser_click / browser_type / browser_snapshot / browser_navigate**。curl 仅用于数据状态的前后对比验证(补充,非替代用户交互)。详见 `journey-mode.md` 的"工具选择守卫"部分。 @@ -261,7 +225,7 @@ verification_detail: **用户旅程 ≠ 单元测试。必须模拟真实用户从自然入口开始的行为路径。** **规则 1:起始页必须是自然入口** -- 小程序:起始页 = 用户打开网址看到的第一个页面(pages.json 第一个 page,通常是首页) +- 小程序:起始页 = 用户打开网址看到的第一个页面(参见 ./reference/platform-uni-app.md §4 入口页面) - 管理后台:起始页 = 根 URL `/`(通常重定向到登录或 Dashboard) - ❌ `browser_navigate("/pages/login/index")` — 用户不会直接输入登录页 URL - ✅ `browser_navigate("http://mini-xxx.dev.d8d.fun/")` → 用户看到首页 → 自然触发登录流程 diff --git a/.claude/skills/d8d-dev/iron-laws.md b/.claude/skills/d8d-dev/iron-laws.md index eea4346..4c5ffa4 100644 --- a/.claude/skills/d8d-dev/iron-laws.md +++ b/.claude/skills/d8d-dev/iron-laws.md @@ -118,7 +118,7 @@ Epic N+1 完成 browser_resize → browser_navigate → browser_snapshot → browser_type → browser_click → browser_wait_for → browser_snapshot → browser_take_screenshot → analyze_image - → "表单提交成功、数据已更新、视觉符合 Neon Pulse" → 下一个 + → "表单提交成功、数据已更新、视觉符合项目设计系统" → 下一个 ``` ### 按Story类型的验证要求矩阵 @@ -146,8 +146,8 @@ Epic N+1 完成 |------|---------|---------|---------| | **渲染验证** | browser_snapshot | 组件树包含预期元素 | 只做了这一步就跳过 | | **交互验证** | browser_type + browser_click | 表单可填写、按钮可点击、操作有结果响应 | 管理后台不填表单、小程序不测试流程 | -| **视觉验证** | analyze_image(对截图) | Neon Pulse + 图标/图片加载完整 + 无 emoji 占位符 + 无 broken image | 完全不调用 analyze_image | -| **截图存档** | browser_take_screenshot | 保存到 tests/e2e/screenshots/story-x-y.png | 只保存了极少数截图 | +| **视觉验证** | analyze_image(对每步截图) | 项目设计系统(04-ui-ux-design.md) + 图标/图片加载完整 + 无 emoji 占位符 + 无 broken image | 只在最后做一次,中间步骤视觉问题被遗漏 | +| **截图存档** | browser_take_screenshot(每步) | 每步保存到 tests/e2e/screenshots/(含步骤编号) | 只在最后保存一张截图 | | **API 目标验证** | browser_network_requests | 前端 JS 发起的 API 请求不指向 localhost(外网域名模式下) | 完全不检测,导致外网用户无法使用 | | **布局溢出验证** | browser_evaluate(JS 检测) | scrollWidth <= viewportWidth、TabBar 可见(小程序页面) | 完全不检测布局溢出 | @@ -202,15 +202,10 @@ Epic N+1 完成 - ❌ 不点击按钮就说"交互功能正常" - ❌ 不调用 analyze_image 就说"视觉验证通过" - ❌ 不保存截图就继续下一个 Story +- ❌ 只在最后阶段截图和 analyze_image,中间步骤无视觉证据 - ❌ 不执行 browser_network_requests 检测就说"API 调用正确" - ❌ 前端页面 JS 调用 localhost:3001 但仍标记 Story 为 done - ❌ 混用视口(管理后台用 375 宽度或小程序用 1440 宽度) -- ❌ 旅程验证中用户操作步骤使用 curl 替代 Playwright(点击、导航、表单填写、页面验证必须用 Playwright 工具) -- ❌ 整条旅程只用 curl API 调用而未使用任何 Playwright 浏览器工具 -- ❌ 旅程 FAIL 被记录但未触发分类判定(Bug/未实现/数据问题) -- ❌ 旅程 FAIL 分类为 Bug 或未实现但未触发 AUTO-FIX/AUTO-IMPLEMENT -- ❌ 旅程 status: pass 但存在 failed_steps > 0 且未全部修复成功 -- ❌ 一次 Playwright 会话验证多个 Story 的多个页面 ### 检测即阻断规则(⛔ 铁律四补充) @@ -219,13 +214,14 @@ Epic N+1 完成 | 检测项 | FAIL 条件 | 分类 | 修复动作 | |--------|----------|------|---------| | 布局溢出 | `horizontal_overflow === true` | Bug | AUTO-FIX: CSS 修复 | -| TabBar 图标缺失 | 只有文本无图标 | 未实现 | AUTO-IMPLEMENT: 实现自定义 TabBar | +| TabBar 图标缺失 | 只有文本无图标 | 未实现 | AUTO-IMPLEMENT: 实现自定义 TabBar(参见 ./reference/platform-uni-app.md §2) | | API 内联声明 | 任何前端代码有独立 `const API_BASE =` | Bug/未实现 | 集中模块存在→AUTO-FIX,否则→AUTO-IMPLEMENT | | API 目标错误 | 外网模式下发现 localhost | Bug | AUTO-FIX: 替换 fallback 值 | | 源码 localhost fallback | grep 发现 `localhost:[port]` | Bug | AUTO-FIX: 替换为外网域名 | -| TabBar iconPath 为空 | `pages.json` iconPath 是空字符串 | 未实现 | AUTO-IMPLEMENT: 自定义 TabBar + Iconify | -| Emoji 充当功能图标 | icon/btn 元素内含 emoji 字符 | 未实现 | AUTO-IMPLEMENT: Iconify 方案替换 | -| Tailwind/Iconify 未配置 | mini-program 无 tailwindcss-icons | 未实现 | AUTO-IMPLEMENT: 完整安装配置 | +| TabBar iconPath 为空 | `pages.json` iconPath 是空字符串 | 未实现 | AUTO-IMPLEMENT: 自定义 TabBar + Iconify(参见 ./reference/platform-uni-app.md §1+§2) | +| Emoji 充当功能图标 | icon/btn 元素内含 emoji 字符 | 未实现 | AUTO-IMPLEMENT: **⛔ 必须按 ./reference/platform-uni-app.md §1 完整安装 Iconify + tailwindcss-icons + weapp-tailwindcss(小程序),禁止 SVG data URI 等不支持的替代方案** | +| SVG data URI 充当图标/装饰 | `grep -rn "data:image/svg+xml" app/mini-program/src/` 在 CSS/style 中发现 `url("data:image/svg+xml...")` 或 `url('data:image/svg+xml...')` | Bug(平台不兼容) | AUTO-IMPLEMENT: **⛔ 必须按 ./reference/platform-uni-app.md §1 完整安装 Iconify + tailwindcss-icons + weapp-tailwindcss/vite,将所有 SVG data URI 替换为 Iconify 类名,⛔ 禁止用另一种 SVG data URI 替代原来的** | +| Tailwind/Iconify 未配置 | mini-program 无 tailwindcss-icons | 未实现 | AUTO-IMPLEMENT: 完整安装配置(参见 ./reference/platform-uni-app.md §1) | | Phase 衔接跳步 | Phase 0V 未完成就进入 Story 验证 | 阻断 | 不得开始 Story 验证直到 Phase 0V COMPLETE GATE 通过 | | 三写不一致 | pipeline-state 和 pipeline-context 的 done 计数不匹配 | 阻断 | 不得标记 Story 完成,必须定位漏写的文件 | | 用户旅程未执行 | `pipeline-state` user_journeys 为空 | 阻断 | 不得标记 complete,必须回退执行 | @@ -234,6 +230,13 @@ Epic N+1 完成 | 旅程 FAIL 未触发修复 | FAIL 被记录但未执行 AUTO-FIX/AUTO-IMPLEMENT/AUTO-DATA-FIX 分类判定和修复流程 | 阻断 | 必须执行分类判定并触发对应修复流程 | | 即时使用数据仅 Toast 传递 | 验证码等下一步骤需要的数据仅通过 Toast 显示且源码无 console.log 且无自动填充 | Bug | AUTO-FIX(#14): 添加 console.log + 自动填充 | | 框架编译 input 不可交互 | `browser_type` 失败后 `browser_evaluate` 诊断发现内部 input `height: 0px`(如 uni-app `uni-input` 在 flex 容器中无显式 height) | Bug | AUTO-FIX(#6): 修复 CSS 添加显式 height/width | +| 页面有内容但零交互元素 | `browser_evaluate` 确认 `interactiveCount===0` 且 `hasContent=true`(用户打开页面看到标题但无任何 button/input/a/form 可操作) | Bug(#5) 或 未实现(#3) | 诊断分流:框架已挂载+源码无交互→AUTO-IMPLEMENT(添加 redirect/导航);框架未挂载→AUTO-FIX(hydration) | +| UI 硬编码英文枚举值 | `browser_evaluate` 检测页面可见文本含 APPROVED/REJECTED/PENDING/PAID/CANCELLED/REFUNDED/DRAFT/ONGOING/COMPLETED 等英文大写枚举(适用于所有前端页面,不限于管理后台) | Bug(#15) | AUTO-FIX: 添加中文映射函数(管理后台 statusLabel / 小程序 computed) | +| 约束声明与代码行为不一致 | 旅程描述含"可选/可跳过/选填"或页面 UI 含"(选填)/(可选)"标记,但代码实际有必填验证(Toast 含"请上传/请填写/不能为空"或源码 `if (!field)` 拦截) | 约束不一致(#16) | JOURNEY-FIX: 修复旅程定义 / AUTO-FIX: 修复代码或 UI 文案,使声明与行为一致 | +| analyze_image 视觉验证未执行 | pipeline-state 无 `analyze_image_called: true` 或 verification_detail.design_check 缺失 | 阻断 | 必须回退执行 analyze_image 后才能标记完成(见 ./visual-verification.md) | +| 双重导航栏 | 页面在 pages.json 中有 `navigationBarTitleText`(非 custom)且页面源码含 `position:fixed` 的自定义 header 元素,导致系统导航栏与自定义头部同时显示(截图中可见两层标题) | Bug(#18) | AUTO-FIX: pages.json 该页面添加 `navigationStyle: "custom"` + 页面 header CSS 添加 safe-area padding(参见 ./reference/platform-uni-app.md §3) | +| API route 无 try-catch | API 返回 500 + `Read` 源码发现 handler 函数无 try-catch 包裹(异常直接返回原始 500 且无日志) | Bug(#20) | AUTO-FIX: 添加 try-catch 包裹 + console.error + JSON 500 响应(参见 ./auto-fix-engine.md #20) | +| 页面 navigateBack 后数据陈旧 | `uni.navigateBack()` 返回目标页后 `browser_snapshot` 数据未更新 + 源码用 `onMounted` 非 `onShow` | Bug(#21) | AUTO-FIX: 替换 `onMounted` 为 `onShow`(from `@dcloudio/uni-app`,参见 ./auto-fix-engine.md #21) | > 完整的 FAIL 分类标准(含兜底规则 #99)和修复流程见 ./auto-fix-engine.md 第 2-4 节 @@ -243,5 +246,17 @@ Epic N+1 完成 - ❌ 跳过用户旅程测试直接标记 Epic 完成 - ❌ 发现 localhost fallback 但标记"已通过 env 覆盖"然后 done - ❌ 发现 emoji 当图标但写"视觉可接受"然后 done +- ❌ **将视觉问题标记为任何非检测即阻断表中定义的分类**(模式性禁止) + - 禁止的分类包括但不限于:"已知遗留项"、"技术债"、"后续优化"、"暂时保留"、"非本次引入" + - 唯一允许的分类:检测即阻断表中的分类(Bug/未实现/阻断) +- ❌ **将修复从 AUTO-FIX/AUTO-IMPLEMENT 降级为"仅记录"或"WARN"** +- ❌ **推迟视觉验证修复到"旅程完成后"或"步骤 4 修复阶段"**(必须在关卡内立即执行) - ❌ 发现 pages.json iconPath 为空但标记"TabBar 可见"然后 done +- ❌ 页面有静态标题但无交互元素(用户无法操作)时标记"渲染通过" +- ❌ 遇到零交互元素但不诊断原因直接判定为 hydration Bug(可能是缺少 redirect) +- ❌ 任何前端页面直接显示英文枚举值(如 APPROVED/REJECTED/PENDING)但标记"显示正常" +- ❌ 旅程说"可跳过/可选/选填"但代码必填验证拦截后仅静默补填继续(不标记 Constraint Mismatch) +- ❌ 页面 UI 含"(选填)/(可选)"标记但后端/前端实际必填(不标记 UX Mismatch) +- ❌ API 返回 500 但未检查源码 try-catch 就标记"环境问题"或"已知问题" +- ❌ 页面 navigateBack 后数据未刷新但标记"已通过 API 验证"(必须检查 onShow) diff --git a/.claude/skills/d8d-dev/journey-authenticity-rules.md b/.claude/skills/d8d-dev/journey-authenticity-rules.md index 024cd7b..e63c384 100644 --- a/.claude/skills/d8d-dev/journey-authenticity-rules.md +++ b/.claude/skills/d8d-dev/journey-authenticity-rules.md @@ -268,10 +268,98 @@ uni.showToast({ title: "验证码已发送", icon: "success" }) --- +## 规则 R7:约束声明必须与代码实际行为一致 + +> **⛔ 通用规则,检测旅程描述和页面 UI 文案中的约束声明与代码实际验证逻辑的不一致。** + +### 为什么需要 R7 + +当旅程定义或页面 UI 告诉用户"某步骤/字段可选",但代码实际强制必填时: +- 旅程验证会"静默补填继续",不标记为问题 +- 真实用户会被意外的校验错误困惑("明明说可选的?") +- 这是比功能缺失更隐蔽的问题:**用户被误导了** + +### R7-A:旅程步骤约束 vs 源码验证逻辑 + +``` +检测方法: +1. 扫描旅程"用户视角步骤"中的约束关键词: + - 可选/可跳过/选填/optional/skip/跳过/使用占位图 + - 必填/必须/required/不可为空 +2. 对含约束描述的步骤,读取对应页面源码的提交/下一步函数: + - 搜索 if (!field) / required / validate + - 搜索 Toast/提示中的"请上传/请填写/请选择/不能为空" +3. 比对旅程约束 vs 代码实际验证 +``` + +### R7-B:页面 UI 文案 vs 代码/接口实际约束 + +``` +检测方法: +1. browser_snapshot / analyze_image 后,扫描页面文本中的约束标识: + (选填)/(可选)/(必填)/ * /required +2. 对含约束标记的字段,读取对应源码: + - 前端 JS:表单提交函数中该字段的验证逻辑 + - 后端 API:接口对该字段是否 required +3. 比对 UI 文案 vs 前后端实际约束 + +自动触发时机: +- 旅程验证执行时 browser_snapshot 显示含约束标识的字段标签 +- analyze_image 检测到页面含"选填/可选"文本 +``` + +### 判定表 + +| 声明方 | 声明内容 | 代码实际 | 判定 | 分类 | +|-------|---------|---------|------|------| +| 旅程描述 | 可选/可跳过 | 必填验证存在 | ❌ 不一致 | Constraint Mismatch | +| 旅程描述 | 必填/必须 | 无验证 | ⚠️ 遗漏验证 | Bug / Journey Update | +| 页面 UI | (选填) | 前端/后端必填 | ❌ Bug | UX Mismatch | +| 页面 UI | *(必填标记) | 无验证 | ⚠️ 遗漏验证 | Bug | +| 旅程描述 | 未提及约束 | 有验证 | ⚠️ 旅程遗漏 | Journey Update | + +### 修复策略 + +``` +检测到 Constraint Mismatch / UX Mismatch: + ↓ +判断正确设计意图: +├── PRD/UX 说可选 → 旅程/UI 正确 → AUTO-FIX 代码(移除必填或改为可选) +├── PRD/UX 说必填 → 代码正确 → AUTO-FIX 旅程定义或 UI 文案 +└── 无法判断 → 标记 WARN + 记录到 pending_fixes + 同时修复旅程定义使其准确反映代码行为 + 执行时按代码实际行为执行 +``` + +### 执行时约束检测(⛔ 强制) + +旅程验证执行过程中,每个用户步骤首次操作被拦截时: + +``` +操作被 Toast 拦截(含"请上传/请填写/请选择/不能为空") + ↓ +检查触发条件(双条件): + 条件 A: 旅程描述含"可跳过/可选/选填/skip/optional" + 条件 B: browser_snapshot 页面含"(选填)/(可选)"文本 + ↓ +A 或 B 任一满足 + Toast 拦截 → Constraint Mismatch (FAIL #16) + → 不得静默补填继续,必须标记并记录 + → 读取源码确认 → 按设计意图修复 +A 和 B 均不满足 → 普通 FAIL #12 (ENV-ADAPT) → 按提示补填继续 +``` + +### 通过标准 + +- 旅程中含约束关键词的步骤,与源码验证逻辑一致 +- 页面上含"选填/可选/必填"标记的字段,与前后端验证一致 +- 不一致项必须有修复动作,禁止仅记录不修复 + +--- + ## 自动校验执行流程(定义和验证通用) ``` -步骤 1:逐规则校验(R1 → R2 → R3 → R4 → R5) +步骤 1:逐规则校验(R1 → R2 → R3 → R4 → R5 → R7) ↓ 步骤 2:汇总结果 ├── 全部 PASS → 返回校验通过 diff --git a/.claude/skills/d8d-dev/journey-mode.md b/.claude/skills/d8d-dev/journey-mode.md index f7d4f2b..e26a98f 100644 --- a/.claude/skills/d8d-dev/journey-mode.md +++ b/.claude/skills/d8d-dev/journey-mode.md @@ -31,7 +31,7 @@ Story 验证全部通过 ≠ 用户能正常使用系统。旅程模式弥补这 1. 读取 PRD + Epic/Story 定义 + sprint-status.yaml ↓ 2. 读取入口配置: - - 小程序: app/mini-program/src/pages.json(第一个 page = 自然入口) + - 小程序: 参见 ./reference/platform-uni-app.md §4(入口页面 = pages.json 第一个 page) - 管理后台: app/admin/src/app/ 路由结构(根 URL = 自然入口) ↓ 3. 为每个 Epic 生成/重写 epic-N-journeys.md @@ -64,7 +64,7 @@ Story 验证全部通过 ≠ 用户能正常使用系统。旅程模式弥补这 详细检测方法和自动修复策略见 **./journey-authenticity-rules.md**(统一规则集 R1-R5)。 **规则 1:起始页必须是自然入口** -- 小程序:起始页 = 用户打开网址后看到的第一个页面(pages.json 的第一个 page) +- 小程序:起始页 = 用户打开网址后看到的第一个页面(参见 ./reference/platform-uni-app.md §4 入口页面) - 管理后台:起始页 = 根 URL `/`(通常重定向到登录或 Dashboard) - ❌ `browser_navigate("/pages/login/index")` — 用户不会直接输入登录页 URL - ✅ `browser_navigate("http://mini-xxx.dev.d8d.fun/")` — 用户打开网址看到首页 @@ -153,7 +153,9 @@ Story 验证全部通过 ≠ 用户能正常使用系统。旅程模式弥补这 ↓ 1.5 执行前校验与自动修复(⛔ 强制) **Read fully and follow: ./journey-authenticity-rules.md** - - 按统一规则 R1-R5 校验所有旅程文件 + - 按统一规则 R1-R5 + R7 校验所有旅程文件 + - R7 约束一致性校验:对含"可选/可跳过/选填"关键词的步骤,grep 对应源码验证逻辑 + - 不一致 → 自动修复旅程定义(使其准确反映代码)→ 重新校验 - 违规 → 自动修复旅程文件 → 重新校验(max 3x) - 环境适配:读取 pipeline-context.md 确定当前环境,按 R3 适配 ↓ @@ -332,26 +334,9 @@ Story 验证全部通过 ≠ 用户能正常使用系统。旅程模式弥补这 ### ⛔ 真实用户完成规则(Real User Completion Rule) -**核心原则**: 旅程中的每个用户步骤,必须是一个**仅使用鼠标/键盘/触摸屏的真实用户**能够完成的方式。如果真实用户无法完成,那就是 Bug 或未实现功能,必须修复。 - -**具体规则**: -- `browser_type` 在可见 input 上失败 → Bug → AUTO-FIX(修复 input 组件的可操作性) -- `browser_click` 在可见按钮上失败 → Bug → AUTO-FIX(修复按钮事件绑定或渲染) -- 禁止用 `browser_evaluate` / `browser_run_code` 绕过不可交互的元素 -- "需要 evaluate 才能操作" 本身就是 Bug - -**`browser_run_code` 仅允许场景**: -- 检测布局溢出等非用户交互操作 -- 读取 localStorage / cookie 等数据状态验证 - -**如果 `browser_type` 在可见 input 上失败**: -1. 标记该步骤 FAIL -2. 分类: Bug(#6 前端 input 组件交互问题) -3. ⛔ **诊断**: `browser_evaluate` 检查 DOM 结构和计算样式(见 auto-fix-engine.md FAIL #6 诊断子步骤) -4. 根据诊断结果选择修复策略(CSS Bug / 可见性 Bug / 事件 Bug / 框架编译 Bug) -5. 触发 AUTO-FIX: 修复具体根因 -6. 修复后重试 `browser_type` -7. 仍失败 → 标记 journey-step-fail,继续下一步骤 +**Read fully and follow: ./auto-fix-engine.md 第 3 节"真实用户完成规则"** +- 核心原则:browser_type/click 失败 = Bug → AUTO-FIX,禁止 evaluate/run_code 绕过 +- browser_type 失败后必须执行 FAIL #6 诊断子步骤(含完整修复流程) ### 执行模板(每条旅程 — 规定式,不得偏离) @@ -367,26 +352,91 @@ Story 验证全部通过 ≠ 用户能正常使用系统。旅程模式弥补这 2. 执行阶段(按用户视角步骤逐一执行 — ⛔ 只能用 Playwright): - browser_navigate → 起始页(自然入口)(⛔ 禁止用 curl 替代) - browser_snapshot → 验证起始页面(⛔ 禁止用 curl GET 替代) - - 循环每个用户步骤: + - ⛔ **零交互检查**: 如果 snapshot 显示页面有文字但无任何 button/input/a/form: + - 标记步骤 FAIL(用户无法从此页面继续操作) + - `browser_evaluate` 执行 FAIL #5 诊断(检查 interactiveCount / frameworkMounted — 见 auto-fix-engine.md) + - 按诊断结果分流 → AUTO-FIX 或 AUTO-IMPLEMENT + - ⛔ **起始页视觉验证**: browser_take_screenshot(journey-epic{N}-{M}-step0.png)+ analyze_image + - 循环每个用户步骤(步骤编号 S = 1, 2, 3...): a. browser_snapshot → 找到目标元素 b. browser_click / browser_type / browser_run_code → 执行操作(⛔ 禁止用 curl 替代) c. browser_wait_for → 等待响应/跳转 d. browser_snapshot → 验证操作结果(⛔ 禁止用 curl GET 替代) - e. 如涉及数据变化 → browser_evaluate 或 curl 验证数据(✅ 允许 curl 补充) + e. ⛔ **视觉验证关卡**(⛔ 强制 — Read fully and follow: ./visual-verification.md) + 此关卡为独立步骤,不是子步骤。每个用户步骤完成后必须通过此关卡才能继续。 + 1. browser_take_screenshot → 保存到 tests/e2e/screenshots/journey-epic{N}-{M}-step{S}.png + 2. analyze_image → 使用 visual-verification.md 中的标准提示词 + 3. 结果记录到 pipeline-state.yaml 的 verification_detail(含 analyze_image_executed: true) + 4. 视觉验证 FAIL → 按分流规则处理(见 auto-fix-engine.md + iron-laws.md 检测即阻断表) + 4.5. ⛔ **源码 SVG data URI 扫描**(视觉验证关卡内 — 每次 snapshot 后强制执行): + - Bash: `grep -rn "data:image/svg+xml" app/mini-program/src/`(扫描小程序源码目录) + - 发现匹配 → svg_data_uri_check.found = FOUND → 记录文件列表 + - → 按检测即阻断表 "SVG data URI 充当图标/装饰" 条目处理 + - → AUTO-IMPLEMENT: 完整安装 Iconify + 替换所有 SVG data URI(参见 ./reference/platform-uni-app.md §1) + 5. ⛔ **视觉验证修复必须内联执行**(不得推迟到步骤 4): + - 视觉 FAIL 后,必须在当前关卡内**立即**执行 AUTO-FIX/AUTO-IMPLEMENT + - 修复后重测:重新 browser_take_screenshot + analyze_image 确认问题已修复 + - 修复失败 → 标记 journey-step-fail → 继续下一步骤 + - 步骤 4(修复阶段)仅处理功能断言 FAIL,不处理视觉验证 FAIL + f. 如涉及数据变化 → browser_evaluate 或 curl 验证数据(✅ 允许 curl 补充) + g. ⛔ **约束一致性检查**(每个用户步骤首次执行时 — R7 执行时强制检测): + 触发条件(操作被拦截 + 声明方说"可选"): + - 操作后 Toast 出现且含"请上传/请填写/请选择/不能为空/不能为空" + - 且满足以下任一: + a. 旅程定义中该步骤描述含"可跳过/可选/选填/optional/skip/跳过/使用占位图" + b. browser_snapshot 页面文本含"(选填)/(可选)"标记 + 满足 → Constraint Mismatch (FAIL #16): + - ❌ 不得静默补填继续 + - 记录到 pipeline-state.yaml 的 constraint_mismatches + - 读取源码确认验证逻辑 → 按设计意图修复代码或旅程定义 + - 修复后重测该步骤 + 不满足(仅 Toast 拦截但无"可选"声明)→ 普通 FAIL #12 → 按提示补填继续 3. 证据阶段(⛔ 强制,不得跳过): - - browser_take_screenshot → 保存截图到 tests/e2e/screenshots/journey-epicN-M.png + - browser_take_screenshot → 保存最终截图到 tests/e2e/screenshots/journey-epic{N}-{M}-final.png - browser_network_requests → 检查无 localhost API 调用 -4. 修复阶段(⛔ 步骤 2-3 有任何 FAIL 时必须执行,不得跳过): +4. 修复阶段(⛔ 以下任一条件满足时必须执行,不得跳过): **Read fully and follow: ./auto-fix-engine.md** (包含: PASS/FAIL 判定标准 → FAIL 分类表 → 真实用户完成规则 → AUTO-FIX/IMPLEMENT/DATA-FIX 流程 → GATE-CHECK) + **触发条件(⛔ 明确化 — 以下任一即触发)**: + - `steps_failed > 0`(功能断言失败) + - `constraint_mismatches` 非空(R7 约束不一致检测到 — 即使 steps_passed == steps_executed) + - 跨轮次累计的视觉问题 ≥ 1(emoji_as_icons / english_enum / broken_images 等 — 检测到即修复) + - 前一次验证的 `pending_fixes` 中有未修复项(⛔ 即使是其他 EPIC 范围的问题,也必须在本轮修复 — 参见 auto-fix-engine.md "已知问题不豁免"规则) + - `verification_detail` 中任一检查项为 FOUND 且非设计系统主观视觉评价(WARN) + + **⛔ 禁止的"记录但不修复"模式**: + - ❌ 将问题添加到 `pending_fixes` 但不执行实际修复动作 + - ❌ 标记"已记录"/"已知"作为修复结果("已记录"不是修复) + - ❌ 以"不影响本次旅程流程"为由跳过修复 + - ❌ 以"V3遗留/历史问题"为由降级为 WARN + - ❌ 以"非当前 EPIC/旅程范围"为由跳过 pending_fixes 中的问题(范围外 ≠ 豁免) + 5. 记录阶段: - 更新 pipeline-state.yaml(含截图路径、coverage_frs、auto_fix 记录) - 更新 pipeline-state.yaml 的 user_journeys - 输出: "旅程 {name}: {PASS/FAIL} — {摘要}" + +6. 分类提交推送阶段(⛔ 每条旅程完成后强制执行): + - git status → 确认变更范围 + - 按以下分类顺序逐类提交(无变更则跳过): + + | 顺序 | 类型 | 文件范围 | Commit Message 格式 | + |------|------|---------|-------------------| + | 1️⃣ | 基础设施/配置 | `package.json`、`vite.config.*`、`tailwind.config.*`、`postcss.config.*`、`tsconfig.json` | `chore: {描述}` | + | 2️⃣ | 后端代码 | `api/src/` | `feat(api): {描述}` | + | 3️⃣ | 小程序代码 | `app/mini-program/src/` | `feat(mini): {描述}` | + | 4️⃣ | 管理后台代码 | `app/admin/src/` | `feat(admin): {描述}` | + | 5️⃣ | 测试产物 | `tests/e2e/screenshots/`、`tests/e2e/fixtures/` | `test: journey screenshots` | + | 6️⃣ | 流水线状态 | `_bmad-output/implementation-artifacts/pipeline-state.yaml`、`_bmad-output/implementation-artifacts/pipeline-report.md` | `ci(pipeline): update journey verification status` | + + - 提交前 `git diff --stat` 确认变更范围 + - 排除 `.env`、`node_modules/`、`.next/`、`dist/` + - 所有分类提交完成后: `git push origin main` + - 推送失败 → 记录到 error_log,不中断 ``` ### 结果记录格式 diff --git a/.claude/skills/d8d-dev/phase-02-create-spec.md b/.claude/skills/d8d-dev/phase-02-create-spec.md index 063fd51..f166897 100644 --- a/.claude/skills/d8d-dev/phase-02-create-spec.md +++ b/.claude/skills/d8d-dev/phase-02-create-spec.md @@ -22,7 +22,7 @@ - pnpm workspace 初始化命令 - uni-app 项目脚手架(`npx degit dcloudio/uni-preset-vue#vite-ts`) - Next.js 项目脚手架(`npx create-next-app@latest`) - - Neon Pulse 设计令牌 CSS 变量 + - 项目设计令牌 CSS 变量(按 04-ui-ux-design.md 规范) - 初始管理员种子数据 3. **更新管道状态** diff --git a/.claude/skills/d8d-dev/phase-03-implement.md b/.claude/skills/d8d-dev/phase-03-implement.md index aa23884..ee22cc1 100644 --- a/.claude/skills/d8d-dev/phase-03-implement.md +++ b/.claude/skills/d8d-dev/phase-03-implement.md @@ -56,71 +56,13 @@ grep -rn "const.*API_BASE.*=" app/mini-program/src/pages/ | grep -v "import" || echo "PASS: 无独立 API 常量" ``` -5. **图标方案:@egoist/tailwindcss-icons + Iconify(按项目类型分路径)** +5. **图标方案**:**Read fully and follow: ./reference/platform-uni-app.md 第 1 节(⛔ 强制)** + - 含 Admin (Next.js) 和 Mini-program (uni-app) 两条路径的完整安装配置 + - 含使用方式、TabBar 图标方案、禁止方案列表 + - **所有前端项目必须使用 Iconify 图标方案,禁止 emoji 充当功能图标** - **所有前端项目必须使用 Iconify 图标方案,禁止 emoji 充当功能图标。** - - **A. Admin (Next.js) — 已有 Tailwind CSS v3** - - 安装:`pnpm add -D @egoist/tailwindcss-icons @iconify-json/mdi @iconify-json/lucide` - - 配置:在 `tailwind.config.ts` 的 `plugins` 数组中加入: - ```ts - import { iconsPlugin, getIconCollections } from "@egoist/tailwindcss-icons" - // plugins 中添加: - iconsPlugin({ collections: getIconCollections(["mdi", "lucide"]) }) - ``` - - **B. Mini-program (uni-app) — 无 Tailwind CSS,需完整安装** - - 安装依赖: - ```bash - pnpm add -D tailwindcss autoprefixer postcss - pnpm add -D weapp-tailwindcss/vite - pnpm add -D @egoist/tailwindcss-icons @iconify-json/mdi @iconify-json/lucide - ``` - - `vite.config.ts` 配置(⚠️ uvwt 必须在 uni() 之后): - ```ts - import { UnifiedViteWeappTailwindcssPlugin as uvwt } from 'weapp-tailwindcss/vite' - export default defineConfig({ - plugins: [uni(), uvwt()], - css: { - postcss: { - plugins: [require('tailwindcss'), require('autoprefixer')], - }, - }, - }) - ``` - - 创建 `tailwind.config.js`: - ```js - const { iconsPlugin, getIconCollections } = require("@egoist/tailwindcss-icons") - module.exports = { - content: ['./src/**/*.{html,js,ts,jsx,tsx,vue}', '!./src/uni_modules/**/*'], - plugins: [ - iconsPlugin({ collections: getIconCollections(["mdi", "lucide"]) }), - ], - } - ``` - - **通用规则**: - - 使用方式:`` 或 `` - - **TabBar 图标**:不使用 pages.json 的 iconPath(需维护 PNG 文件),改用自定义 TabBar 组件 + Iconify 类名 - - **禁止 emoji 充当功能图标**(不同平台显示不一致,如 🏠📱💬) - - **可缺失图片**(头像等)使用 CSS 渐变色占位符(如 `background: linear-gradient(135deg, #ff5167, #7508a5)`),不引用不存在的文件路径 - - **图标预览**:https://icones.js.org 或 https://icon-sets.iconify.design/ - -6. **布局防溢出规则(⛔ 强制,小程序页面尤其重要)** - - **问题背景**:小程序页面在真实移动设备上经常出现宽度/高度溢出视口、底部 TabBar 被遮挡的问题。 - - **规则**: - - 页面根元素宽度不得超过 `100vw`(或 `100%`),禁止固定像素宽度 - - TabBar 页面可滚动内容高度 = `100vh - Header高度 - TabBar高度`(约 50px) - - 固定定位(`position: fixed`)元素的 `bottom` 值必须考虑 TabBar 高度 - - 图片/卡片组件必须 `max-width: 100%` + `overflow: hidden` - - 不允许出现水平滚动条(除非是刻意设计的横滑组件) - - **检测命令**(E2E 测试中由步骤 8.5 自动执行): - ```js - document.documentElement.scrollWidth > window.innerWidth // 水平溢出 - ``` +6. **布局防溢出规则**:**参见 ./reference/platform-uni-app.md 第 5 节(⛔ 强制,小程序页面尤其重要)** + - 含防溢出规则、TabBar 遮挡处理、布局溢出检测 JS 代码 #### 3B: Epic 0 特殊实现 @@ -136,7 +78,7 @@ 5. Prisma schema + 首次迁移(连接容器内置 PG) 6. .env 配置(指向容器内置服务) 7. 基础配置文件 (TypeScript, Tailwind, ESLint) - 8. Neon Pulse 设计令牌全局 CSS + 8. 项目设计令牌全局 CSS(按 04-ui-ux-design.md 规范) ``` 2. **容器环境连接信息** diff --git a/.claude/skills/d8d-dev/reference/platform-uni-app.md b/.claude/skills/d8d-dev/reference/platform-uni-app.md new file mode 100644 index 0000000..93e005f --- /dev/null +++ b/.claude/skills/d8d-dev/reference/platform-uni-app.md @@ -0,0 +1,399 @@ +# Uni-app 微信小程序平台参考 + +> **⛔ 主流程文件统一引用此文件**:所有 uni-app/微信小程序的平台特定内容集中于此。 +> 主流程文件(auto-fix-engine / iron-laws / journey-mode / visual-verification / validation-mode / e2e-testing / phase-03-implement)中不再内联平台特定方案,改为引用本文件对应章节。 + +--- + +## 1. 图标方案(⛔ 强制) + +**所有前端项目必须使用 Iconify 图标方案,禁止 emoji 充当功能图标。** + +### A. Admin (Next.js) — 已有 Tailwind CSS v3 + +- 安装:`pnpm add -D @egoist/tailwindcss-icons @iconify-json/mdi @iconify-json/lucide` +- 配置:在 `tailwind.config.ts` 的 `plugins` 数组中加入: + ```ts + import { iconsPlugin, getIconCollections } from "@egoist/tailwindcss-icons" + // plugins 中添加: + iconsPlugin({ collections: getIconCollections(["mdi", "lucide"]) }) + ``` + +### B. Mini-program (uni-app) — 无 Tailwind CSS,需完整安装 + +- 安装依赖: + ```bash + pnpm add -D tailwindcss autoprefixer postcss + pnpm add -D weapp-tailwindcss/vite + pnpm add -D @egoist/tailwindcss-icons @iconify-json/mdi @iconify-json/lucide + ``` +- `vite.config.ts` 配置(⚠️ uvwt 必须在 uni() 之后): + ```ts + import { UnifiedViteWeappTailwindcssPlugin as uvwt } from 'weapp-tailwindcss/vite' + export default defineConfig({ + plugins: [uni(), uvwt()], + css: { + postcss: { + plugins: [require('tailwindcss'), require('autoprefixer')], + }, + }, + }) + ``` +- 创建 `tailwind.config.js`: + ```js + const { iconsPlugin, getIconCollections } = require("@egoist/tailwindcss-icons") + module.exports = { + content: ['./src/**/*.{html,js,ts,jsx,tsx,vue}', '!./src/uni_modules/**/*'], + plugins: [ + iconsPlugin({ collections: getIconCollections(["mdi", "lucide"]) }), + ], + } + ``` + +### 通用规则 + +- 使用方式:`` 或 `` +- **TabBar 图标**:不使用 pages.json 的 iconPath(需维护 PNG 文件),改用自定义 TabBar 组件 + Iconify 类名 +- **禁止 emoji 充当功能图标**(不同平台显示不一致,如 🏠📱💬) +- **可缺失图片**(头像等)使用 CSS 渐变色占位符(如 `background: linear-gradient(135deg, #ff5167, #7508a5)`),不引用不存在的文件路径 +- **图标预览**:https://icones.js.org 或 https://icon-sets.iconify.design/ + +### ⛔ 禁止的替代方案(微信小程序不支持) + +- ❌ SVG data URI / CSS background-image(微信小程序不支持内联 SVG) +- ❌ Emoji 字符(不同平台渲染不一致) +- ❌ Unicode 符号替换(不同平台显示不一致) +- ❌ img 标签引用 SVG 文件(不符合 Iconify 规范) +- ❌ 直接删除不替换任何图标 + +--- + +## 2. TabBar 方案 + +### 问题 + +- `pages.json` 的 `iconPath` 需要维护 PNG 文件,不同分辨率需要多套图片 +- 小程序 TabBar 在真实设备上经常被内容遮挡 + +### 方案 + +- 不使用 `pages.json` 的 `iconPath`,改用自定义 TabBar 组件 + Iconify 类名 +- TabBar 页面可滚动内容高度 = `100vh - Header高度 - TabBar高度`(约 50px) +- 固定定位(`position: fixed`)元素的 `bottom` 值必须考虑 TabBar 高度 + +### 检测方法 + +```bash +# pages.json iconPath 为空 +grep '"iconPath": ""' app/mini-program/src/pages.json +``` + +### TabBar CSS 选择器(E2E 测试用) + +```css +.uni-tabbar, .tabbar, [class*="tabbar"], [class*="tab-bar"] +``` + +--- + +## 3. 导航栏方案(⛔ 强制) + +### 原则 + +- **uni-app 小程序项目统一使用 `navigationStyle: "custom"`**,禁止混合使用系统导航栏和自定义头部 +- 原因:系统导航栏样式不可控(无法实现 Neon Pulse 深色/玻璃态/霓虹渐变),且与自定义 header 同时显示会导致**双重导航栏 Bug** + +### 检测方法(⛔ 每次视觉验证关卡必须执行) + +```bash +# 1. 提取 pages.json 中每个页面的导航栏配置 +cat app/mini-program/src/pages.json | python3 -c " +import sys, json +cfg = json.load(sys.stdin) +pages = cfg.get('pages', []) +global_style = cfg.get('globalStyle', {}) +for p in pages: + path = p.get('path', '') + style = p.get('style', {}) + nav_title = style.get('navigationBarTitleText', global_style.get('navigationBarTitleText', '')) + nav_custom = style.get('navigationStyle', '') == 'custom' + if nav_title and not nav_custom: + print(f'DUAL_NAV_RISK: {path} title={nav_title} custom={nav_custom}') +" + +# 2. 对有风险的页面检查是否有自定义 fixed header +# grep -l "position.*fixed\|class=\"header" app/mini-program/src/pages/{page-path}/*.vue +# 发现匹配 → 双重导航栏 confirmed +``` + +### 判定标准 + +| 条件 | 判定 | +|------|------| +| 页面有 `navigationBarTitleText`(非 custom)+ 页面源码有 `position:fixed` 的自定义 header | ❌ **双重导航栏 Bug** → AUTO-FIX | +| 页面有 `navigationBarTitleText`(非 custom)+ 无自定义 header | ✅ 正常(使用系统导航栏) | +| 页面 `navigationStyle: "custom"` + 有自定义 header | ✅ 正确(纯自定义) | +| 页面 `navigationStyle: "custom"` + 无自定义 header | ⚠️ 无导航栏(可能设计意图) | + +### AUTO-FIX 修复策略 + +1. `Edit` pages.json → 该页面 style 改为 `{ "navigationStyle": "custom" }` +2. `Edit` 页面源码 → 自定义 header 添加 `padding-top: env(safe-area-inset-top, 20px)` 和 `height` 调整 +3. `Edit` 内容区 → `margin-top` 与新 header 高度一致(含 safe-area) +4. 重测截图确认只有一层标题 + +--- + +## 4. 编译特性与已知问题 + +### 3.1 uni-input 编译后 height:0px + +**问题**:uni-app `` 编译为 `` 包裹 ``,在 flex 容器中内部 input 可能 `height: 0px`,导致 `browser_type` 失败。 + +**诊断代码**: +```javascript +(el) => { + const tag = el.tagName?.toLowerCase(); + const computed = getComputedStyle(el); + const info = { + tag, height: computed.height, width: computed.width, + opacity: computed.opacity, display: computed.display, + innerInputs: [] + }; + el.querySelectorAll('input').forEach(i => { + const c = getComputedStyle(i); + info.innerInputs.push({ + height: c.height, width: c.width, + opacity: c.opacity, pointerEvents: c.pointerEvents + }); + }); + return info; +} +``` + +**分流**: +- `innerInputs[0].height === '0px'` → **框架编译 CSS Bug** → AUTO-FIX: 给 input 或包裹容器添加显式 `height: 44px` +- `opacity === '0'` 或 `pointerEvents === 'none'` → **可见性 Bug** → AUTO-FIX +- 诊断结果正常但 `browser_type` 仍失败 → **Playwright 适配** → 允许 `browser_run_code` + `force: true`(仅限此场景,须记录适配原因) + +### 3.2 uni-app API 约定 + +- `uni.showToast` — 提示消息(注意:即时使用数据不应仅通过 Toast 传递) +- `uni.chooseImage` — 图片选择(H5 模式下触发 file chooser) +- `uni.request` — 网络请求 +- `uni.navigateTo` / `uni.switchTab` — 页面导航 + +### 3.3 即时使用数据与 uni.showToast + +如果 `uni.showToast` 传递了下一步骤立即需要的数据(如验证码),必须同时添加: +```javascript +console.log('[DEV] 验证码:', res.code); +if (res.code) { code.value = res.code; } +uni.showToast({ title: "验证码已发送", icon: "success" }); +``` + +--- + +## 4. 测试配置 + +### 视口尺寸 + +| 平台 | 视口 | +|------|------| +| 小程序 (uni-app H5) | 375 × 812 | +| 管理后台 (Next.js) | 1440 × 900 | + +### 入口页面 + +| 平台 | 入口 | +|------|------| +| 小程序 | `pages.json` 第一个 page(通常为首页) | +| 管理后台 | 根 URL `/`(通常重定向到登录或 Dashboard) | + +### 入口 URL 格式 + +- 小程序:`http://mini-{project}.dev.d8d.fun/`(首页,非直接 navigate 到 login) +- 管理后台:`http://admin-{project}.dev.d8d.fun/` + +--- + +## 5. 布局规则 + +### 5.1 防溢出规则(⛔ 强制,小程序页面尤其重要) + +**问题背景**:小程序页面在真实移动设备上经常出现宽度/高度溢出视口、底部 TabBar 被遮挡的问题。 + +**规则**: +- 页面根元素宽度不得超过 `100vw`(或 `100%`),禁止固定像素宽度 +- TabBar 页面可滚动内容高度 = `100vh - Header高度 - TabBar高度`(约 50px) +- 固定定位(`position: fixed`)元素的 `bottom` 值必须考虑 TabBar 高度 +- 图片/卡片组件必须 `max-width: 100%` + `overflow: hidden` +- 不允许出现水平滚动条(除非是刻意设计的横滑组件) + +### 5.2 布局溢出检测(E2E 步骤 8.5) + +```javascript +function checkLayoutOverflow() { + const html = document.documentElement; + const tabBar = document.querySelector('.uni-tabbar, .tabbar, [class*="tabbar"], [class*="tab-bar"]'); + return { + horizontal_overflow: html.scrollWidth > window.innerWidth, + document_width: html.scrollWidth, + viewport_width: window.innerWidth, + overflow_diff: html.scrollWidth - window.innerWidth, + tabbar_found: tabBar !== null, + tabbar_visible: tabBar ? (tabBar.getBoundingClientRect().bottom <= window.innerHeight) : null, + tabbar_rect: tabBar ? tabBar.getBoundingClientRect() : null, + page_height: html.scrollHeight, + viewport_height: window.innerHeight, + }; +} +``` + +--- + +## 6. 源码检测命令 + +### 图标方案合规检查 + +```bash +# A. pages.json tabBar iconPath 是否为空 +if find app/ -name "pages.json" -not -path "*/node_modules/*" -print0 | xargs -0 grep -l '"iconPath": ""' 2>/dev/null | head -1; then + echo "tabbar_iconpath_empty=true" +fi + +# B. static 目录存在性 +STATIC_EXISTS=false +for dir in app/*/static app/*/src/static; do [ -d "$dir" ] && STATIC_EXISTS=true; done +echo "static_dir_exists=$STATIC_EXISTS" + +# C. Tailwind + Iconify 配置(如果 mini-program 存在) +if [ -d "app/mini-program" ]; then + HAS_TW=$(grep -rl "tailwindcss" app/mini-program/ --include="*.js" --include="*.ts" --include="*.json" | grep -v node_modules | wc -l) + HAS_ICONIFY=$(grep -rl "iconify\|tailwindcss-icons" app/mini-program/ --include="*.js" --include="*.ts" --include="*.json" | grep -v node_modules | wc -l) + echo "mini_tw=$HAS_TW mini_iconify=$HAS_ICONIFY" +fi +``` + +### Emoji 充当图标检查 + +```bash +# 搜索模板中 icon/btn 相关元素内含 emoji 的情况 +EMOJI_ICONS=$(grep -rn 'class="[^"]*icon[^"]*"[^>]*>[🔍🔔🏠📱💬👤⚙️📝➕❤️⭐📢✅❌]' app/*/src/ --include="*.vue" --include="*.tsx" | grep -v node_modules) +EMOJI_COUNT=$(echo "$EMOJI_ICONS" | grep -c ":" || echo 0) +echo "emoji_as_icons=$EMOJI_COUNT" +[ "$EMOJI_COUNT" -gt 0 ] && echo "EMOJI_FILES:" && echo "$EMOJI_ICONS" +``` + +--- + +## 7. 下拉刷新方案(⛔ 强制) + +### 问题 + +uni-app 中有两种下拉刷新实现: +- **页面级**:`pages.json` 配置 `enablePullDownRefresh: true` + 组件内 `onPullDownRefresh()` 生命周期 → **整个页面下拉**,体验一致 +- **列表级**:`scroll-view` 的 `refresher-enabled` + `@refresherrefresh` → **仅列表区域下拉**,header 固定不动,体验割裂 + +Tab 页面间下拉刷新方式不一致(有的用列表级、有的没有),用户体验混乱。应统一使用**页面级 `onPullDownRefresh`**。 + +### 检测方法(⛔ 每次视觉验证关卡必须执行) + +```bash +# 步骤 1: 从 pages.json 提取 tabBar 页面路径 +cat app/mini-program/src/pages.json | python3 -c " +import sys, json +cfg = json.load(sys.stdin) +tab_list = cfg.get('tabBar', {}).get('list', []) +for t in tab_list: + print(t.get('pagePath', '')) +" + +# 步骤 2: 对每个 tabBar 页面检查刷新方式 +# LIST_LEVEL: scroll-view refresher +grep -l "refresher-enabled" app/mini-program/src/pages/{home,square,messages,profile}/index.vue 2>/dev/null + +# PAGE_LEVEL: onPullDownRefresh +grep -l "onPullDownRefresh" app/mini-program/src/pages/{home,square,messages,profile}/index.vue 2>/dev/null + +# NONE: 无任何刷新 +# 不在以上两个结果中的 tabBar 页面 +``` + +### 判定标准 + +| 条件 | 标记 | +|------|------| +| 使用 `scroll-view refresher-enabled` | LIST_LEVEL | +| 使用 `onPullDownRefresh`(from `@dcloudio/uni-app`) | PAGE_LEVEL | +| 无任何下拉刷新实现 | NONE | + +| 组合情况 | 判定 | +|----------|------| +| 所有 Tab 页面均为 PAGE_LEVEL | ✅ CONSISTENT | +| 所有 Tab 页面均为 NONE(内容简单的项目) | ✅ CONSISTENT | +| 混用 LIST_LEVEL / PAGE_LEVEL / NONE | ❌ **INCONSISTENT → AUTO-FIX #19** | +| 存在 LIST_LEVEL(即使只有一处) | ❌ **INCONSISTENT → AUTO-FIX #19**(应全部改为 PAGE_LEVEL) | + +### AUTO-FIX #19 修复策略 + +**目标**:所有内容 Tab 页面统一使用页面级 `onPullDownRefresh`。 + +**步骤**: + +1. **pages.json** → `globalStyle` 添加 `"enablePullDownRefresh": true` + +2. **对 LIST_LEVEL 页面**(scroll-view refresher): + - 移除 `scroll-view` 上的 `refresher-enabled` / `:refresher-triggered` / `@refresherrefresh` 属性 + - 移除 `const refreshing = ref(false)` 和 `function onRefresh()` + - 添加 `import { onPullDownRefresh } from "@dcloudio/uni-app"` + - 添加 `onPullDownRefresh(() => { page.value = 1; fetchEvents(); })` + - 在数据加载函数的 `finally` 块中添加 `uni.stopPullDownRefresh()` + +3. **对 NONE 页面**(无刷新的内容 Tab 页面): + - 添加 `import { onPullDownRefresh } from "@dcloudio/uni-app"` + - 添加 `onPullDownRefresh(() => { /* 对应数据刷新逻辑 */ })` + - 数据加载完成后调用 `uni.stopPullDownRefresh()` + +4. 重测 → 所有 Tab 页面整页下拉刷新效果一致 + +### 修复模板 + +**LIST_LEVEL → PAGE_LEVEL 改造(首页示例)**: + +```vue + + + + + +``` + +```typescript +// 改前 script: +const refreshing = ref(false); +function onRefresh() { + refreshing.value = true; + page.value = 1; + fetchEvents(); +} + +// 改后 script: +import { onPullDownRefresh } from "@dcloudio/uni-app"; +// 移除 refreshing ref 和 onRefresh 函数 +onPullDownRefresh(() => { + page.value = 1; + fetchEvents(); +}); +// fetchEvents 的 finally 中: uni.stopPullDownRefresh(); +``` + +```typescript +// fetchEvents finally 块添加: +} finally { + loading.value = false; + loadingMore.value = false; + uni.stopPullDownRefresh(); // ← 新增 +} +``` diff --git a/.claude/skills/d8d-dev/reference/practical-experience.md b/.claude/skills/d8d-dev/reference/practical-experience.md index eafc0ae..eea168a 100644 --- a/.claude/skills/d8d-dev/reference/practical-experience.md +++ b/.claude/skills/d8d-dev/reference/practical-experience.md @@ -177,7 +177,7 @@ CREATE-SPEC → IMPLEMENT → TEST → REVIEW → MARK-DONE 6. browser_snapshot → 验证操作结果(数据已创建/更新/删除) 7. browser_take_screenshot → 保存截图到 tests/e2e/screenshots/story-x-y-admin.png(强制) 8. analyze_image(对截图文件路径调用)→ AI 视觉验证: - - Neon Pulse 设计规范:深色背景(#000)、霓虹色按钮、玻璃态卡片、无1px边框 + - 项目设计系统(从 _bmad-output/04-ui-ux-design.md 提取) - 功能完整性:表单字段齐全、按钮可操作、数据正确渲染 - 布局合理性:响应式排列、无重叠溢出、内容可读 如果 analyze_image 发现问题 → 记录到验证报告,标记为需修复 @@ -194,7 +194,7 @@ CREATE-SPEC → IMPLEMENT → TEST → REVIEW → MARK-DONE 6. browser_snapshot → 验证操作结果 7. browser_take_screenshot → 保存截图到 tests/e2e/screenshots/story-x-y-mini.png(强制) 8. analyze_image(对截图文件路径调用)→ AI 视觉验证: - - Neon Pulse 设计规范一致性 + - 项目设计系统一致性(从 04-ui-ux-design.md 提取) - 移动端布局合理性(375 宽度下无溢出) - 交互元素可操作(按钮尺寸、间距合理) 如果 analyze_image 发现问题 → 记录到验证报告,标记为需修复 @@ -248,7 +248,7 @@ CREATE-SPEC → IMPLEMENT → TEST → REVIEW → MARK-DONE #### 教训 3: analyze_image 不是可选项 - **发生了什么**:35 个 Story 的验证中完全没有使用 analyze_image - **根本原因**:文档中 analyze_image 没有强调为强制步骤 -- **修复要求**:每个前端 Story 必须调用 analyze_image 验证 Neon Pulse 设计规范 +- **修复要求**:每个前端 Story 必须调用 analyze_image 验证项目设计系统规范 #### 教训 4: 截图是验证的证据,不是装饰 - **发生了什么**:35 个 Story 只有 2 个截图文件 diff --git a/.claude/skills/d8d-dev/reference/upstream-integration.md b/.claude/skills/d8d-dev/reference/upstream-integration.md index 1a7404d..31c74d0 100644 --- a/.claude/skills/d8d-dev/reference/upstream-integration.md +++ b/.claude/skills/d8d-dev/reference/upstream-integration.md @@ -103,5 +103,5 @@ AI 自动执行 d8d-dev - [ ] pipeline-report.md 已生成 - [ ] 数据库迁移可重现(`npx prisma migrate reset` 成功) - [ ] 所有开发服务器无错误启动 -- [ ] Neon Pulse 设计令牌在两端一致应用 +- [ ] 项目设计令牌在两端一致应用(按 04-ui-ux-design.md) diff --git a/.claude/skills/d8d-dev/validation-mode.md b/.claude/skills/d8d-dev/validation-mode.md index 69dda0f..0736874 100644 --- a/.claude/skills/d8d-dev/validation-mode.md +++ b/.claude/skills/d8d-dev/validation-mode.md @@ -120,7 +120,7 @@ Phase 0V: 批量创建 Task + 读取 Story 规格文档 │ ├── browser_type + browser_click(交互操作) │ │ ├── browser_wait_for + browser_snapshot(验证) │ │ ├── browser_take_screenshot(保存证据) │ - │ └── analyze_image(AI 视觉验证 Neon Pulse) │ + │ └── analyze_image(AI 视觉验证项目设计系统) │ │ 4.5. GATE-CHECK(⛔ 必须全部通过才能继续) │ │ 检查项: │ │ - layout_check.horizontal_overflow === false │ @@ -262,36 +262,14 @@ echo "localhost_fallback=$LOCALHOST_COUNT inline_api=$INLINE_COUNT" - 全部为 0 → **PASS** **检查 2: 图标方案合规(小程序/移动端 Story 强制)** -```bash -# A. pages.json tabBar iconPath 是否为空 -if find app/ -name "pages.json" -not -path "*/node_modules/*" -print0 | xargs -0 grep -l '"iconPath": ""' 2>/dev/null | head -1; then - echo "tabbar_iconpath_empty=true" -fi -# B. static 目录存在性 -STATIC_EXISTS=false -for dir in app/*/static app/*/src/static; do [ -d "$dir" ] && STATIC_EXISTS=true; done -echo "static_dir_exists=$STATIC_EXISTS" -# C. Tailwind + Iconify 配置(如果 mini-program 存在) -if [ -d "app/mini-program" ]; then - HAS_TW=$(grep -rl "tailwindcss" app/mini-program/ --include="*.js" --include="*.ts" --include="*.json" | grep -v node_modules | wc -l) - HAS_ICONIFY=$(grep -rl "iconify\|tailwindcss-icons" app/mini-program/ --include="*.js" --include="*.ts" --include="*.json" | grep -v node_modules | wc -l) - echo "mini_tw=$HAS_TW mini_iconify=$HAS_ICONIFY" -fi -``` +> 完整检测命令和判定规则见 **./reference/platform-uni-app.md 第 6 节**。 **判定**: - `tabbar_iconpath_empty=true` → **FAIL (未实现)** → AUTO-IMPLEMENT -- `STATIC_EXISTS=false` 且有 tabBar → **FAIL (未实现)** → AUTO-IMPLEMENT - mini-program 缺 Tailwind/Iconify → **WARN (未实现)** → AUTO-IMPLEMENT **检查 3: Emoji 充当功能图标(所有前端强制)** -```bash -# 搜索模板中 icon/btn 相关元素内含 emoji 的情况 -EMOJI_ICONS=$(grep -rn 'class="[^"]*icon[^"]*"[^>]*>[🔍🔔🏠📱💬👤⚙️📝➕❤️⭐📢✅❌]' app/*/src/ --include="*.vue" --include="*.tsx" | grep -v node_modules) -EMOJI_COUNT=$(echo "$EMOJI_ICONS" | grep -c ":" || echo 0) -echo "emoji_as_icons=$EMOJI_COUNT" -[ "$EMOJI_COUNT" -gt 0 ] && echo "EMOJI_FILES:" && echo "$EMOJI_ICONS" -``` +> 完整检测命令和判定规则见 **./reference/platform-uni-app.md 第 6 节**。 **判定**: - `EMOJI_COUNT > 0` → **FAIL (未实现)** → AUTO-IMPLEMENT @@ -325,6 +303,44 @@ echo "placeholder_functions=$PLACEHOLDER_COUNT" - `PLACEHOLDER_COUNT > 0` → **FAIL (未实现)** → AUTO-IMPLEMENT: 实现该功能 - `PLACEHOLDER_COUNT === 0` → **PASS** +**检查 5: 零交互元素检测(Story 涉及前端页面时浏览器端检查)** + +在 E2E 验证步骤(步骤 4)中,`browser_navigate` 到目标页面后,`browser_snapshot` 如显示有内容但无交互元素(button/input/a/form),执行: + +```javascript +// browser_evaluate 检查 +() => { + const interactive = document.querySelectorAll('button, input, a, form, [role="button"], [role="link"]'); + const hasContent = document.body.innerText.trim().length > 0; + const hasNextRoot = !!document.getElementById('__next'); + const hasReactRoot = !!document.querySelector('[data-reactroot]'); + const hasVueApp = !!document.querySelector('[data-v-app]'); + return { interactiveCount: interactive.length, hasContent, frameworkMounted: hasNextRoot || hasReactRoot || hasVueApp }; +} +``` + +**判定**: +- `interactiveCount === 0` 且 `hasContent === true` → **FAIL** → 触发 FAIL #5 诊断子步骤(见 auto-fix-engine.md) +- `interactiveCount > 0` → **PASS** → 继续正常 E2E 流程 + +**检查 6: UI 硬编码英文枚举值(所有前端 Story 强制)** + +在 E2E 验证步骤(步骤 4)中,`browser_navigate` 到任何前端页面后,执行: + +```javascript +// browser_evaluate 检查页面可见文本是否包含英文枚举值 +() => { + const text = document.body.innerText; + const enumPatterns = /\b(APPROVED|REJECTED|PENDING|PAID|CANCELLED|REFUNDED|DRAFT|ONGOING|COMPLETED|AVAILABLE|FROZEN|WITHDRAWN)\b/g; + const matches = text.match(enumPatterns); + return { hasEnglishEnum: matches !== null, matches: matches || [] }; +} +``` + +**判定**: +- `hasEnglishEnum === true` → **FAIL (#15)** → AUTO-FIX(添加中文映射函数) +- `hasEnglishEnum === false` → **PASS** + #### 步骤 4: E2E 交互验证(Story 涉及前端页面时) - **必须使用完整 8 步 Playwright 测试序列**(见下方"Playwright MCP 测试序列模式") - 按 Story 规格文档的 V2 Validation Tasks 逐项验证 @@ -332,18 +348,23 @@ echo "placeholder_functions=$PLACEHOLDER_COUNT" - 管理后台:必须填写表单、点击按钮、验证提交结果 - 小程序:必须模拟用户交互流程(登录/浏览/报名等) - 切换前端目标时必须 `browser_resize` 到正确视口 +- **⛔ 视觉验证关卡**(⛔ 强制 — Read fully and follow: ./visual-verification.md): + - 每个关键操作后必须通过视觉验证关卡(独立步骤,不可跳过) + - 关卡 = browser_take_screenshot + analyze_image(使用 visual-verification.md 标准提示词) + - 关卡未通过 → 按分流规则处理,不得进入下一个操作 -#### 步骤 5: 截图保存(Story 涉及前端页面时) -- **强制保存**,不是可选 -- 路径:`tests/e2e/screenshots/story-x-y.png` +#### 步骤 5: 最终截图保存(Story 涉及前端页面时) +- 保存最终状态截图到 `tests/e2e/screenshots/story-x-y.png` - 管理后台和小程序分别截图时使用不同后缀: - `story-x-y-admin.png`(管理后台页面) - `story-x-y-mini.png`(小程序页面) +- 此步骤保留作为最终证据归档(中间步骤截图已在步骤 4 中完成) #### 步骤 6: AI 视觉验证(Story 涉及前端页面时) -- **强制调用 `analyze_image` 验证截图** -- 验证维度:Neon Pulse 设计规范 / 功能完整性 / 布局合理性 -- **不调用 analyze_image 等于未完成该 Story 的验证** +- **⛔ 详细规则和标准提示词见 ./visual-verification.md** +- 对最终截图强制调用 `analyze_image` +- **不调用 analyze_image 等于未完成该 Story 的验证**(GATE-CHECK 会验证 `analyze_image_called: true`) +- 注:中间步骤的视觉验证已在步骤 4 逐步完成(视觉验证关卡),此步骤是最终确认 #### 步骤 6.5: GATE-CHECK(⛔ 所有前端 Story 必须通过才能继续) diff --git a/.claude/skills/d8d-dev/visual-verification.md b/.claude/skills/d8d-dev/visual-verification.md new file mode 100644 index 0000000..12f2ffe --- /dev/null +++ b/.claude/skills/d8d-dev/visual-verification.md @@ -0,0 +1,314 @@ +# 视觉验证共享模块(Visual Verification Module) + +> **⛔ 所有模式共享**:旅程验证模式、回归验证模式、E2E 测试、Phase 4 测试均引用此文件。 +> +> **被引用位置**: +> - `journey-mode.md` 执行模板步骤 2e +> - `validation-mode.md` Story 验证流程 +> - `e2e-testing.md` 步骤 8 +> - `phase-04-test.md` 测试流程 + +--- + +## 核心原则 + +**截图 ≠ 视觉验证**。`browser_take_screenshot` 只保存像素证据,`analyze_image` 才是检查设计合规性的动作。两者必须配对执行。 + +**任何模式在完成前端页面验证时,必须对每个截图执行 `analyze_image` AI 视觉分析。** + +--- + +## 三层防护机制 + +### 第一层:关卡机制(结构保障) + +视觉验证是**独立关卡**,不是循环内的子步骤。每个用户步骤/验证步骤完成后,必须经过视觉验证关卡才能继续。 + +``` +每个步骤的执行流程: + a. browser_snapshot → 找到目标元素 + b. browser_click / browser_type → 执行操作 + c. browser_wait_for → 等待响应 + d. browser_snapshot → 验证操作结果 + ↓ + ⛔ 视觉验证关卡(独立,不可跳过): + 1. browser_take_screenshot → 保存截图 + 2. analyze_image → AI 视觉分析(使用下方标准提示词) + 3. 分析结果记录到 verification_detail + 4. 发现问题 → 按分流规则处理 + 5. 关卡未通过 → 不得进入下一个用户步骤 +``` + +### 第二层:GATE-CHECK 验证(闭环保障) + +`auto-fix-engine.md` GATE-CHECK 增加: + +``` +├── ⛔ analyze_image 已执行(所有截图都有对应视觉分析结果) +│ 验证方法:pipeline-state.yaml 中 analyze_image_executed === true +│ 未执行 → 不得标记完成,必须回退执行视觉验证 +``` + +### 第三层:执行后自检清单(审计保障) + +每个 Story/旅程验证完成后强制执行: + +``` +执行后自检清单(⛔ 强制): +1. ✅ browser_take_screenshot 保存了截图文件(路径含步骤编号) +2. ✅ analyze_image 对截图执行了视觉分析(结果已记录) +3. ✅ verification_detail.design_check 存在且值为 PASS 或 FAIL(不能为 null) +4. ✅ verification_detail.icons_check.emoji_as_icons 已检查 +5. ✅ 发现 FAIL 后执行了分类判定和修复流程 + +任一为"否" → Story 不得标记为 verified/done。 +``` + +--- + +## analyze_image 标准提示词 + +每个截图的 AI 视觉分析使用以下提示词: + +``` +详细分析此移动端/桌面端页面截图,检查以下维度: + +1. **设计规范**(从项目 _bmad-output/04-ui-ux-design.md 提取): + - {执行时:Read _bmad-output/04-ui-ux-design.md → 提取 🎨 设计系统节的色彩/卡片/边框/渐变/阴影规则 → 填入具体值} + - 回退:文件不存在 → grep 项目 CSS :root 变量 → 仍无 → 仅检查下方 2-4 维度 + +2. **图标/图片完整性**: + - TabBar 图标可见且有实际图标(非纯文本) + - 功能图标使用图标库(非 emoji) + - 无 broken image(加载失败的图片占位符) + - 无 emoji 充当功能图标 + +3. **英文枚举检测**: + - 页面可见文本中无英文大写枚举值(如 APPROVED/REJECTED/PENDING/PAID 等) + +3.5. **SVG data URI 源码扫描**(⛔ 强制 — 每次视觉验证关卡必须执行): + - `grep -rn "data:image/svg+xml" app/mini-program/src/`(或当前小程序源码目录) + - 发现任何匹配 → 标记 svg_data_uri_check.found = FOUND → 记录文件列表 + - → 按检测即阻断表 "SVG data URI 充当图标/装饰" 条目处理(AUTO-INSTALL Iconify) + +3.6. **双重导航栏源码扫描**(⛔ 强制 — 每次视觉验证关卡必须执行): + - 步骤 1: 解析 pages.json,提取每个页面的 `navigationBarTitleText` 和 `navigationStyle` +: + - 步骤 2: 对每个 DUAL_NAV_RISK 页面,检查源码是否有 `position:fixed` 的自定义 header: +](step 2) + - 发现匹配 → 标记 dual_nav_check.found = FOUND → 记录文件列表 + - → 按检测即阻断表 "双重导航栏" 条目处理(AUTO-FIX #18,参见 ./reference/platform-uni-app.md §3) + +) + +- +4. **布局质量**(⛔ 逐一检查以下每一项,不得跳过): ```bash + cat app/mini-program/src/pages.json | python3 -c " + import sys, json + cfg = json.load(sys.stdin) + pages = cfg.get('pages', []) + global_style = cfg.get('globalStyle', {}) + for p in pages: + path = p.get('path', '') + style = p.get('style', {}) + nav_title = style.get('navigationBarTitleText', global_style.get('navigationBarTitleText', '')) + nav_custom = style.get('navigationStyle', '') == 'custom' + if nav_title and not nav_custom: + print(f'DUAL_NAV_RISK: {path} title={nav_title} custom={nav_custom}') + " + ``` + - 步骤 2: 对每个 DUAL_NAV_RISK 页面,检查源码是否有 `position:fixed` 的自定义 header: + ```bash + grep -l "position.*fixed" app/mini-program/src/pages/{page-path}/*.vue + # 或 + grep -l "class=\"header" app/mini-program/src/pages/{page-path}/*.vue + ``` + - 发现匹配 → 标记 dual_nav_check.found = FOUND → 记录文件列表 + - → 按检测即阻断表 "双重导航栏" 条目处理(AUTO-FIX #18,参见 ./reference/platform-uni-app.md §3) + + ⛔ **源码扫描优先规则**(适用于所有源码扫描检测项:dual_nav、svg_data_uri、emoji 等): + - 当源码扫描检测到 FOUND → **必须标记为 FOUND**,按对应修复流程处理 + - analyze_image 只能**确认**问题,不能**否认**源码扫描结果 + - 当源码扫描说有问题但 analyze_image 说没问题 → **以源码扫描为准**,执行修复 + - 理由:源码扫描是确定性检测(grep 文件内容),analyze_image 是概率性检测(AI 视觉分析可能误判) + - ❌ 禁止以 analyze_image 结果覆盖源码扫描的 FOUND 结论 + +3.7. **下拉刷新一致性源码扫描**(⛔ 强制 — 每次视觉验证关卡必须执行,仅 uni-app 项目): + - 步骤 1: 从 pages.json 提取 tabBar 页面路径列表 + - 步骤 2: 对每个 tabBar 页面源码检查刷新方式: + - `refresher-enabled` → LIST_LEVEL + - `onPullDownRefresh` → PAGE_LEVEL + - 无上述代码 → NONE + - 步骤 3: tabBar 页面间刷新方式不一致(混用 LIST_LEVEL/PAGE_LEVEL/NONE,或存在 LIST_LEVEL) + → 标记 refresh_check.found = INCONSISTENT → 记录各页面刷新方式 + - → 按检测即阻断表 "下拉刷新方式不一致" 条目处理(AUTO-FIX #19,参见 ./reference/platform-uni-app.md §7) + +4. **布局质量**(⛔ 逐一检查以下每一项,不得跳过): + a. 溢出检测:无水平溢出,内容不超出视口 + b. 文字可读:对比度足够,文字不被截断 + c. 重叠检测:元素无重叠(标题栏与自定义头部不碰撞,导航栏不遮盖内容) + d. 间距合理性:无异常大间距(如导航栏与内容之间的过大空白),无紧贴元素(相邻交互元素应有明确间距,不能挤在一起) + e. 元素变形检测:按钮/输入框/卡片未被挤压变形(宽高比异常),文字未被截断显示省略号 + f. 对齐检测:同行元素水平对齐,同列元素垂直对齐 + g. TabBar:可见且完整(非 TabBar 页面此项 PASS) + h. 导航冗余检测(⛔ 强制):页面顶部不应同时存在两层标题栏。 + - 判断标准:如果页面最顶部有标准系统导航栏(显示页面标题如"首页""我的""广场"等), + 紧接着下方又有另一行品牌名/logo/标题(如"NEON PULSE""极音派对"),标记为 FAIL(双重导航栏)。 + - 只允许一种:要么系统导航栏(有 navigationBarTitleText),要么自定义头部(品牌名/logo)。 + - ❌ 示例 FAIL:顶部显示"首页"(系统) + 下方紧接"NEON PULSE"(自定义) = 双重导航栏 + - ✅ 示例 PASS:只有"NEON PULSE"(自定义,系统栏已设custom) 或只有"首页"(系统栏,无自定义header) + +列出所有发现的问题,按严重程度排序。 +``` + +--- + +## 视觉检查结果记录格式 + +每步视觉验证结果必须记录到 `pipeline-state.yaml`: + +```yaml +verification_detail: + analyze_image_executed: true # ⛔ GATE-CHECK 验证此字段 + design_check: + design_spec_source: "04-ui-ux-design.md" # 或 "css-fallback" 或 "none" + design_compliance: PASS/FAIL/WARN + details: "具体描述" + icons_check: + tabbar_icons_visible: PASS/FAIL + emoji_as_icons: NONE/FOUND # NONE=无emoji, FOUND=发现emoji + broken_images: NONE/FOUND + details: "具体描述" + english_enum_check: + found: NONE/FOUND + details: "具体枚举值列表" + svg_data_uri_check: + source_scan_executed: true # ⛔ 源码扫描必须执行 + found: NONE/FOUND + files: [] # FOUND 时列出文件路径 + details: "具体描述" + dual_nav_check: + source_scan_executed: true # ⛔ 源码扫描必须执行 + found: NONE/FOUND + files: [] # FOUND 时列出文件路径 + details: "具体描述" + refresh_check: # ⛔ uni-app 项目必须执行 + source_scan_executed: true + found: CONSISTENT/INCONSISTENT + pages: # 各 tabBar 页面刷新方式 + home: PAGE_LEVEL/LIST_LEVEL/NONE + square: PAGE_LEVEL/LIST_LEVEL/NONE + messages: PAGE_LEVEL/LIST_LEVEL/NONE + profile: PAGE_LEVEL/LIST_LEVEL/NONE + details: "具体描述" + layout_check: + horizontal_overflow: false # a. 溢出检测 + text_readable: true # b. 文字可读 + element_overlap: false # c. 重叠检测 + spacing_anomaly: NONE/FOUND # d. 间距合理性(异常大间距/紧贴元素) + element_deformation: NONE/FOUND # e. 元素变形(按钮/输入框被挤压) + alignment_issues: NONE/FOUND # f. 对齐问题 + details: "具体描述" +``` + +--- + +## 视觉检查问题分流规则 + +**⛔ 强制声明**:除"设计系统主观视觉评价"(改进建议/WARN)外,下表所有问题的修复动作均为 **强制**。 +不得以任何理由跳过、推迟或重新分类。以下表达均属于**无效分类**,禁止使用: +"已知遗留项"、"技术债"、"后续优化"、"暂时保留"、"视觉可接受"、"非本次旅程引入"、"记录但推迟修复"。 +任何未在下表中定义的分类均为无效分类,必须按原分类执行修复。 + +`analyze_image` 发现问题后的分流路径: + +| 发现的问题 | 分类 | 修复动作 | 铁律依据 | +|-----------|------|---------|---------| +| TabBar 图标缺失(只有文本无图标) | 未实现 | AUTO-IMPLEMENT: 自定义 TabBar + Iconify(参见 ./reference/platform-uni-app.md §1+§2) | iron-laws L223, L227 | +| Emoji 充当功能图标 | 未实现 | AUTO-IMPLEMENT: **⛔ 必须按 ./reference/platform-uni-app.md §1 完整安装 Iconify + tailwindcss-icons(小程序需 weapp-tailwindcss/vite),禁止 SVG data URI** | iron-laws L228 | +| Tailwind/Iconify 未配置 | 未实现 | AUTO-IMPLEMENT: 完整安装配置(参见 ./reference/platform-uni-app.md §1) | iron-laws L229 | +| SVG data URI 充当图标/装饰 | Bug(平台不兼容 #17) | AUTO-IMPLEMENT: **⛔ 必须按 ./reference/platform-uni-app.md §1 完整安装 Iconify + tailwindcss-icons + weapp-tailwindcss/vite** | iron-laws L223 | +| 英文枚举值显示 | Bug(#15) | AUTO-FIX: 添加中文映射函数 | iron-laws L239 | +| Broken image | Bug | AUTO-FIX: 修复图片路径/占位符 | — | +| 布局溢出 | Bug | AUTO-FIX: CSS 修复 | iron-laws L222 | +| 元素变形/挤压(按钮/输入框/卡片宽高比异常、被挤压、文字截断显示省略号) | Bug | AUTO-FIX: 修复 CSS flex/width/padding/gap | — | +| 间距异常(导航栏与内容之间过大空白、相邻交互元素紧贴无间距、异常大空白区域) | Bug | AUTO-FIX: 修复 margin/padding/gap | — | +| 设计系统客观指标不合规(背景色/主色调/卡片样式/边框规则/按钮渐变等可从源码验证项) | Bug | AUTO-FIX: 修复 CSS/class,参照 _bmad-output/04-ui-ux-design.md 设计规范 | — | +| 设计系统主观视觉评价不合规(发光效果强度/动画质感等无法从源码客观验证的维度) | 改进建议 | WARN | — | + +> **注意**:视觉检查发现的问题分流后,走 `auto-fix-engine.md` 标准修复流程(AUTO-FIX/AUTO-IMPLEMENT),不在此文件定义修复细节。 + +### 跨步骤累计检测规则(⛔ 强制) + +同一类型的问题在多个步骤被 `analyze_image` 检测到时,必须累计计数: + +``` +累计检测流程: + 每个 analyze_image 执行后: + - 检查本次发现的视觉问题类型 + - 与前序步骤的 verification_detail 合并累计 + - 同一类型累计 FOUND ≥ 1 → 必须在当前关卡立即触发修复,不得推迟(视觉问题确定性高,不需多次确认) + - 如果修复在步骤 N 完成,步骤 N+1 起不再累计该类型(已修复) +``` + +禁止行为: +- ❌ 将同一类型问题在每个步骤独立记录为 FOUND 但从不触发修复 +- ❌ 发现第 1 次标记"记录",发现第 2 次仍标记"记录",发现第 N 次仍不修复 + +### 跨验证轮次累计检测规则(⛔ 强制 — 修复"每轮 count=1 永远不触发") + +**问题**: 同一类型问题在 V1 检测到 count=1,V2 检测到 count=1,V3 检测到 count=1… 每轮都不够阈值 ≥ 2,永远不触发修复。 + +**解决**: 累计计数必须跨验证轮次(V1→V2→V3→V4…)累加。 + +``` +跨轮次累计流程: + 验证开始前: + 1. 读取 pipeline-state.yaml 中前一次验证的 verification_detail + 2. 提取每个视觉问题类型的检测状态: + - emoji_as_icons: FOUND → 跨轮次累计基础 +1 + - english_enum_check: FOUND → 跨轮次累计基础 +1 + - broken_images: FOUND → 跨轮次累计基础 +1 + - tabbar_icons_visible: FAIL → 跨轮次累计基础 +1 + 3. 将跨轮次基础值设为本次验证的累计起点 + (注意:基础值来自"前一次仍为 FOUND 的项") + + 每步 analyze_image 执行后: + - 本次 FOUND + 跨轮次基础值 = 实际累计值 + - 实际累计值 ≥ 1 → ⛔ 立即触发修复(视觉问题确定性高,不需多次确认) + - 修复完成后,后续步骤不再累计该类型 +``` + +**判定示例**: +``` +V3: emoji_as_icons=FOUND → 记录到 pending_fixes(未修复) +V4 开始前: 读取 V3 → emoji_as_icons 跨轮次基础=1 +V4 第一步 analyze_image: emoji FOUND → 1+1=2 → ⛔ 立即触发 AUTO-IMPLEMENT + +V3: english_enum_check=FOUND → 记录到 pending_fixes(未修复) +V4 开始前: 读取 V3 → english_enum 跨轮次基础=1 +V4 第一步 analyze_image: english FOUND → 1+1=2 → ⛔ 立即触发 AUTO-FIX + +V5: 局头认证页 👤 emoji FOUND → 跨轮次基础=0(V5标记FIXED但实际仅部分替换)→ 0+1=1 → ⛔ 立即触发 AUTO-IMPLEMENT +``` + +**⛔ 禁止行为**: +- ❌ 只看本次验证的 count,忽略前一次验证的 FOUND 记录 +- ❌ 以"V3遗留/已知"为由重置累计计数为 0 +- ❌ 跨轮次基础值 + 本次 FOUND ≥ 1 但仍标记"记录"不触发修复 + +--- + +## 禁止行为(⛔) + +- ❌ 截图但不执行 analyze_image 就标记"视觉验证通过" +- ❌ analyze_image 报告 FAIL 但 Story 仍标记 done +- ❌ 发现 emoji 当图标但写"视觉可接受"然后 done +- ❌ 发现 TabBar iconPath 为空但标记"TabBar 可见"然后 done +- ❌ 只在最后做一次 analyze_image,中间步骤跳过 +- ❌ analyze_image 使用过于宽松的提示词(如"看起来正常吗?") +- ❌ **将视觉问题标记为任何非分流规则表中定义的分类**(模式性禁止 — 覆盖所有语义变体) + - 禁止使用的分类包括但不限于:"已知遗留项"、"技术债"、"后续优化"、"暂时保留"、"非本次引入" + - 唯一允许的分类:分流规则表中的 7 种(未实现×3、Bug×3、改进建议×1) +- ❌ **将修复动作从 AUTO-FIX/AUTO-IMPLEMENT 降级为"仅记录"或"WARN"**(除设计系统主观视觉评价外) +- ❌ **推迟视觉验证修复到"旅程完成后"或"步骤 4 修复阶段"**(必须在关卡内立即执行)