From 75a12f3760d2893ebbf89242c49249ffb23d2ce3 Mon Sep 17 00:00:00 2001 From: skytek_xinliang Date: Mon, 8 Jun 2026 11:53:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=94=A2=E5=87=BA=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PRD.md | 190 ------------------------------------------- README.md | 121 ++++++++++++++++++++++++++- TODO.md | 173 --------------------------------------- src/lib/html.js | 54 ++++-------- src/stages/scan.js | 43 +++++++++- test/cli-e2e.test.js | 7 +- test/html.test.js | 9 +- 7 files changed, 182 insertions(+), 415 deletions(-) delete mode 100644 PRD.md delete mode 100644 TODO.md diff --git a/PRD.md b/PRD.md deleted file mode 100644 index 40d48d2..0000000 --- a/PRD.md +++ /dev/null @@ -1,190 +0,0 @@ -# Product Requirement Document -## HTML Transform - -| 欄位 | 內容 | -|------|------| -| 版本 | v0.3 | -| 日期 | 2026-05-04 | -| 狀態 | MVP | - ---- - -## 1. 背景與目標 - -HTML Transform 是 prototype evidence 工具。它的目標不是完整自動化前端生成,而是把 HTML prototype 轉成可 review、可追溯、可交給 coding agent 實作的前置證據。 - -目前 MVP 只保留有實際價值的功能: - -- 檢查 `scan` 前置條件。 -- 用真實瀏覽器擷取 prototype evidence。 -- 推論跨頁面的 App Structure Map。 -- 產生頁面級 UI Contract。 -- 驗證 UI Contract 與 DOM evidence 是否明顯衝突。 - -不保留未完成的 pipeline 骨架。`plan`、`run`、`diff`、`verify`、`go`、`status` 已不屬於目前產品面。 - ---- - -## 2. CLI Scope - -| 指令 | 說明 | -|------|------| -| `ht doctor` | 檢查 `scan` 所需環境 | -| `ht scan` | 產生 browser evidence、page contract、validation report | - -沒有 Codex slash command。根目錄輸入 `/scan` 不會自動執行本工具。 - ---- - -## 3. 設定檔 - -MVP 只使用 `prototype`: - -```js -export default { - prototype: './prototype' -} -``` - -本 repo 的根目錄設定檔可以保留其他欄位給外部工作流參考,但 `packages/html-transform` 不會執行 frontend/backend/output 相關流程。 - ---- - -## 4. Stage 1:Capture - -目的:取得 prototype 的瀏覽器真相,避免只看 HTML source 或讓 agent 腦補畫面。 - -需求: - -| ID | 說明 | -|----|------| -| F1-1 | 對每個 `.html` 檔案截取 desktop `1440 x 900` full-page screenshot | -| F1-2 | 使用本地 Vite static server,不使用 `file://` | -| F1-3 | 截圖前等待 `networkidle`,再等待 500ms | -| F1-4 | 以 HTML SHA-256 hash 作為 cache key,未變更時跳過重新截圖 | -| F1-5 | 截圖寫入 `.ht/cache/prototype/{page}/desktop-default.png` | -| F1-6 | 產出 `dom-summary.json` 與 `accessibility-tree.json` | -| F1-7 | 產出 `capture-metadata.json`,記錄 resource failure 與 console error/warning | -| F1-8 | 產出 `.ht/app-map.json`,推論頁面角色與 layout 策略 | - -產出: - -```text -.ht/cache/prototype/{page}/desktop-default.png -.ht/cache/prototype/{page}/dom-summary.json -.ht/cache/prototype/{page}/accessibility-tree.json -.ht/cache/prototype/{page}/capture-metadata.json -.ht/app-map.json -``` - ---- - -## 5. Stage 2:Decompose - -MVP 不執行。 - -不做的項目: - -- 不呼叫 vision agent 切主要區域。 -- 不產生 `decomposition.json`。 -- 不用 Playwright `clip` 截取 table/form/main 等局部圖。 -- 不建立 region-first extraction pipeline。 - ---- - -## 6. Stage 3-lite:Page Contract - -目的:從 Stage 1 evidence 與 HTML source 產生頁面級 UI Contract。 - -需求: - -| ID | 說明 | -|----|------| -| F3-1 | 每個 HTML 產出一份 `pageContract` | -| F3-2 | `pageContract` 以整頁為單位,不以 region screenshot 為單位 | -| F3-3 | 記錄 source、screenshot、title、sections、forms、tables、actions、textSamples | -| F3-4 | 建議 Vuetify components 必須來自 MVP 白名單 | -| F3-5 | forms 記錄 labels、fields、required 狀態與 actions | -| F3-6 | tables 記錄 headers 與少量 sample rows | -| F3-7 | 不確定或 evidence 對不上時保留 warnings | - -產出: - -```text -.ht/spec/{page}.spec.json -.ht/spec/{page}.ui-contract.md -``` - -`.spec.json` 目前仍保留 `regions` 欄位作為相容層,但它不是 MVP 的主要設計中心。 - ---- - -## 6.1 App Structure Map - -目的:讓通用 prompt 不必靠人工說明,就能知道每個 prototype 應如何放進前端 app。 - -產出: - -```text -.ht/app-map.json -``` - -每個 route 應包含: - -| 欄位 | 說明 | -|------|------| -| `prototype` | prototype 相對路徑 | -| `kind` | `auth`、`auth-support`、`legacy-shell-reference`、`prototype-navigation`、`feature-page` 或 `reference` | -| `module` | 由資料夾推論的功能模組 | -| `targetRole` | 對前端實作的角色提示 | -| `layout` | `template-auth`、`template-app` 或 `ignore` | -| `usePrototypeStyle` | MVP 固定為 `false` | -| `usePrototypeContent` | 是否採用 prototype 的內容資訊架構 | -| `routeHint` | 可用的路由建議,無正式頁面時為 `null` | - -推論規則: - -- password input 或登入/忘記密碼文字:`auth`。 -- `portal/app-layout.html`、`portal/menu-pane.html`、`portal/index.html` 或含登出/隱藏選單:`legacy-shell-reference`。 -- 非 portal 且有 table/form/action:`feature-page`。 -- module index 若像 prototype 導覽頁:`prototype-navigation`。 -- layout 與 login style 來源固定為 `preparation/skt-vuetify-templates`。 - ---- - -## 7. Stage 4-lite:Validate Contract - -目的:確認 UI Contract 與 browser evidence 沒有明顯衝突。 - -需求: - -| ID | 說明 | -|----|------| -| F4-1 | 驗證 `textSamples` 是否可在 DOM summary 找到 | -| F4-2 | 驗證 actions 是否可在 DOM summary 找到 | -| F4-3 | 驗證 form labels 是否可在 DOM summary 找到 | -| F4-4 | 驗證建議 Vuetify components 是否在白名單內 | -| F4-5 | 產出 `ValidationReport`,包含 warnings、nullFieldCount、nullFieldRatio、requiresHumanReview | - -產出: - -```text -.ht/spec/{page}.validation.json -``` - ---- - -## 8. 非目標 - -MVP 不追求: - -- tablet/mobile 多 viewport capture。 -- region decomposition。 -- 每個 region 獨立 vision extraction。 -- TaskList 產生。 -- 自動寫入 frontend/output。 -- visual similarity scoring。 -- flow smoke test。 -- 全自動 coding agent invocation。 - -這些能力只有在實際需求明確出現後才新增。 diff --git a/README.md b/README.md index 8d9b270..a53adb9 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,23 @@ pnpm --filter html-transform exec playwright-cli --version 若環境還沒有 Chromium: ```bash +HOME=/tmp \ +XDG_CACHE_HOME=/tmp \ +PLAYWRIGHT_DAEMON_SESSION_DIR=/tmp/ms-playwright/daemon \ pnpm --filter html-transform exec playwright-cli install-browser chromium ``` +若是在受限制環境中執行(例如 home directory 唯讀、sandbox 不允許寫入 `~/.cache`),建議同時指定: + +```bash +HOME=/tmp +XDG_CACHE_HOME=/tmp +PLAYWRIGHT_DAEMON_SESSION_DIR=/tmp/ms-playwright/daemon +PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright +``` + +這樣 Playwright 的 browser、cache 與 daemon session 都會寫到可寫目錄。 + ## 設定檔 設定檔不是工具執行的絕對必要條件;沒有設定檔時,工具會使用內建預設並讀取: @@ -138,6 +152,99 @@ export default { `frontend`、`backend`、`output`、`plan` 等欄位不會被 `doctor` 或 `scan` 使用,不需要為本 MVP 加入設定檔。 +## 本 repo 實際執行 + +這個 monorepo 目前要能穩定跑 `scan`,至少需要處理三件事: + +1. 根目錄 `ht.config.mjs` 將 prototype 指到 `./prototype` +2. Playwright Chromium 已安裝 +3. 若根目錄 `.playwright/cli.config.json` 有指定 `channel`,需避免 `scan` 誤走系統 Chrome + +目前本 repo 的最小設定是: + +```js +export default { + prototype: "./prototype", +}; +``` + +若根目錄 `.playwright/cli.config.json` 類似: + +```json +{ + "browser": { + "browserName": "chromium", + "launchOptions": { + "channel": "chromium" + } + } +} +``` + +`scan` 可能會失敗,並出現類似: + +```text +Chromium distribution 'chrome' is not found at /opt/google/chrome/chrome +``` + +原因是 `html-transform` 會在 `.ht/cache/playwright-cli` 底下呼叫 `playwright-cli`。在這種情況下,可在該工作目錄放一份最小覆寫設定: + +```text +.ht/cache/playwright-cli/.playwright/cli.config.json +``` + +內容: + +```json +{ + "browser": { + "browserName": "chromium" + } +} +``` + +在這個 repo 內,實際可重現的流程如下。 + +1. 確認設定與 CLI: + +```bash +pnpm --filter html-transform exec playwright-cli --version +HOME=/tmp \ +XDG_CACHE_HOME=/tmp \ +PLAYWRIGHT_DAEMON_SESSION_DIR=/tmp/ms-playwright/daemon \ +PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright \ +node packages/html-transform/src/cli.js doctor +``` + +2. 安裝 Chromium: + +```bash +HOME=/tmp \ +XDG_CACHE_HOME=/tmp \ +PLAYWRIGHT_DAEMON_SESSION_DIR=/tmp/ms-playwright/daemon \ +PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright \ +pnpm --filter html-transform exec playwright-cli install-browser chromium +``` + +3. 準備 Playwright CLI 的本地覆寫設定(只有在根目錄 `.playwright/cli.config.json` 含 `channel` 時需要): + +```bash +mkdir -p .ht/cache/playwright-cli/.playwright +printf '%s\n' '{' ' "browser": {' ' "browserName": "chromium"' ' }' '}' > .ht/cache/playwright-cli/.playwright/cli.config.json +``` + +4. 執行 scan: + +```bash +HOME=/tmp \ +XDG_CACHE_HOME=/tmp \ +PLAYWRIGHT_DAEMON_SESSION_DIR=/tmp/ms-playwright/daemon \ +PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright \ +node packages/html-transform/src/cli.js scan +``` + +在受限制 sandbox 中,`scan` 可能因為 Vite 無法綁定本機 port、或 Chromium 啟動時的系統呼叫受限而失敗。這種情況下,需要在不受 sandbox 限制的 shell 執行上面的 `scan` 指令。 + ## Doctor ```bash @@ -157,6 +264,16 @@ node packages/html-transform/src/cli.js doctor node packages/html-transform/src/cli.js scan ``` +在本 repo 的受限制環境中,建議實際使用: + +```bash +HOME=/tmp \ +XDG_CACHE_HOME=/tmp \ +PLAYWRIGHT_DAEMON_SESSION_DIR=/tmp/ms-playwright/daemon \ +PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright \ +node packages/html-transform/src/cli.js scan +``` + `scan` 會自動: - 用 Vite static server 提供 prototype。 @@ -297,6 +414,6 @@ node packages/html-transform/src/cli.js scan ```bash pnpm --filter html-transform typecheck pnpm --filter html-transform test -node packages/html-transform/src/cli.js doctor -node packages/html-transform/src/cli.js scan +HOME=/tmp XDG_CACHE_HOME=/tmp PLAYWRIGHT_DAEMON_SESSION_DIR=/tmp/ms-playwright/daemon PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright node packages/html-transform/src/cli.js doctor +HOME=/tmp XDG_CACHE_HOME=/tmp PLAYWRIGHT_DAEMON_SESSION_DIR=/tmp/ms-playwright/daemon PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright node packages/html-transform/src/cli.js scan ``` diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 1a414c2..0000000 --- a/TODO.md +++ /dev/null @@ -1,173 +0,0 @@ -# TODO - -## 目前狀態 - -`html-transform scan` 已從單純 prototype evidence,推進到可輸出 Page Implementation Contract 的第一版。 - -已完成: - -- 強化 prototype form/table/action 萃取。 - - 支援 legacy table-form 的 `/` 欄位 label 推論。 - - 欄位輸出包含 `label`、`name`、`type`、`required`、`readonly`、`maxLength`、`options`、`defaultValue`、`sourceTable`、`sourceRow`。 - - action 會輸出 `actionType` 與 `scope`,例如 `search`、`save`、`edit`、`delete`、`print`、`back`、`formAction`、`rowAction`。 - - table 會輸出 `role`,例如 `searchTable`、`resultTable`、`detailTable`、`printTable`、`layoutTable`。 -- 解析 `prototype/*.md` guide。 - - 保留 `legacyJsp`、`legacyPb`、`targetView`、`description`。 - - 解析各 prototype 段落 checklist,寫入 route guide 與 page spec。 -- 解析 backend API 文件。 - - 從 `apps/backend/API.md` 解析 endpoint index。 - - 從 `apps/backend/API_Manual.md` 解析 method/path、request/response JSON、field rules、notes、ProblemDetails examples。 - - 輸出 `.ht/api-catalog.json`。 -- 建立 prototype-to-api matching。 - - 依 prototype path/module、guide、title/text/actions 與 endpoint path/description tokens 做通用匹配。 - - 不針對 Venue 寫死規則;Venue 只是目前驗證案例。 -- 產出維護頁 `maintenanceContract`。 - - 輸出 `pageKind`、`capabilities`、`recommendedTemplate`、`confidence`、`reasons`、`warnings`、`dataModel`。 - - 可初步分辨 `maintenance`、`query`、`application`、`print`、`chooser`、`layout-reference`。 -- 更新 `.ui-contract.md`。 - - 加入 page kind、recommended template、capabilities、primary entity、target view、legacy source、prototype checklist、API endpoints。 -- 補測試。 - - 使用 generic `orders` domain 測試 API catalog、API matching 與 maintenance template 推論,避免只驗證 Venue。 - -已驗證: - -```bash -pnpm --filter html-transform test -pnpm --filter html-transform run typecheck -node packages/html-transform/src/cli.js scan -``` - -目前 Venue prototype 重新 scan 後的分類: - -```text -applications-list.html maintenance single-record -apply-choose.html chooser none -apply-facility.html application master-detail-c -apply-room-print.html print none -apply-room.html application master-detail-c -query-choose.html chooser none -query-facility.html query none -query-room.html query none -``` - -## 目標 - -讓 `html-transform scan` 產出的 `.ht/` artifact 不只描述 prototype 畫面,也能結合 backend API 文件與維護頁範本規則,形成可穩定交給 LLM 生成 Vue/Vuetify 頁面的實作契約。 - -核心輸出: - -- prototype 提供畫面結構、欄位文案、表格、按鈕與舊流程線索。 -- backend API docs 提供 endpoint、request/response DTO、欄位規則、狀態碼與錯誤格式。 -- template guide 或 maint README 提供專案層的頁面範本選擇規則。 -- `.ht/app-map.json`、`.ht/api-catalog.json`、`.ht/spec/{page}.spec.json`、`.ht/spec/{page}.ui-contract.md` 說明頁面型態、資料模型、API 對應、建議範本與信心理由。 - -## 待辦 - -### 1. Layout Evidence - -目前 `scan` evidence 可以找出頁面結構、labels、inputs、buttons、tables、screenshots 與 app-map hints。`GEN-FE-PROMPT.md` 的 UI 實作規則要求 feature page 使用現代 Vuetify 承載 prototype 的功能與資訊架構,不逐像素複刻舊 HTML/JSP,但仍要保留資訊密度、區塊順序、欄位行列對齊與主要操作流。 - -待補強: - -- 擷取重要可見元素 bounding boxes: - - headings - - labels - - inputs/selects/textareas - - buttons/links - - tables - - section containers -- 用 bounding boxes 將表單欄位分群成可能的 rows / columns。 -- 擷取 first-viewport density: - - visible section count - - visible form field count - - visible table/header count - - approximate occupied content area -- 在 `.ht/spec/{page}.spec.json` 輸出: - - `layoutEvidence.sections` - - `layoutEvidence.formRows` - - `layoutEvidence.tables` - - `layoutEvidence.primaryActions` - - `layoutEvidence.firstViewport` -- 在 `.ht/spec/{page}.ui-contract.md` 呈現 layout hints。 - -Non-goals: - -- 不要求 pixel-perfect HTML/JSP reproduction。 -- 不複製 legacy CSS。 -- 除非是功能辨識必要,不把精確 colors/fonts 編成硬限制。 - -### 2. API Matching 品質 - -目前 API matching 是通用 token scoring,已可用,但仍偏粗。 - -待補強: - -- 降低同 module 但不同 page 的誤配風險。 -- 把 guide 的 `targetView`、JSP/PB method、prototype path 與 endpoint path 做更細的權重拆分。 -- 將 matched endpoints 分成用途: - - `lookup` - - `search` - - `detail` - - `create` - - `update` - - `delete` - - `print` - - `customAction` -- 在 `apiContract` 裡輸出 rejected candidates 與 rejection reason,方便檢查錯配。 - -### 3. Maintenance Contract 精準度 - -目前 `maintenanceContract` 已能初步推薦範本,但仍是 heuristic。 - -待補強: - -- 加入 row action rule: - - 從 prototype checklist、button disabled、API notes 推論 `enabledWhen`。 - - 例如 `aprvYn === 'Z'` 才能 edit/delete。 -- 更精準拆出: - - `searchFields` - - `formFields` - - `tableColumns` - - `detailCollections` - - `rowActions` -- 將 `recommendedTemplate` 與特定前端專案範本解耦。 - - 通用輸出保留 `single-record`、`master-detail-c` 等抽象 template id。 - - 專案若有自己的 README 或 guide,再由 prompt 對應到實際檔案。 -- 加 validation warnings: - - `maintenance` 頁缺少 search/table/action。 - - 有 `edit/delete` action 但沒有對應 API endpoint。 - - request schema 有欄位但 prototype 找不到對應欄位。 - - prototype 有欄位但 API schema 找不到對應欄位。 - - row action 有狀態限制但未產出 `enabledWhen`。 - -### 4. Backend Docs 通用性 - -目前 backend docs 預設讀 `./apps/backend`,且 parser 針對 markdown heading、table、code fence 做通用解析。 - -待補強: - -- 允許 config 指定多個 backend docs 來源。 -- 支援 OpenAPI JSON/YAML 作為輸入來源。 -- 支援沒有 `API.md` index、只有 manual 的情境。 -- 支援不同語言的章節標籤,例如 `Request body`、`Response body`、`Validation`。 - -### 5. UI Contract Markdown - -目前 `.ui-contract.md` 已加入主要 contract 摘要。 - -待補強: - -- 輸出 field rules 與 schema 摘要。 -- 輸出 row action rules。 -- 輸出 layout evidence。 -- 對 `recommendedTemplate: none` 的頁面明確標示原因,例如 chooser/query/print/layout-reference。 - -### 6. 文件與範例 - -待補強: - -- 加一個最小 fixture,示範非 Venue domain 如何產出 api catalog 與 maintenance contract。 -- 在 README 補充 config 範例: - - 只指定 `prototype` - - 同時指定 `prototype` 與 `backendDocs` - - 沒有 backend docs 時的輸出行為 diff --git a/src/lib/html.js b/src/lib/html.js index f62d246..a7a7264 100644 --- a/src/lib/html.js +++ b/src/lib/html.js @@ -229,29 +229,10 @@ export function buildAppMapWithGuides(layoutSpecs, prototypeDir, prototypeGuides tableCount: spec.pageContract.tables.length } } - }) + }).filter((route) => ['auth', 'auth-support', 'feature-page'].includes(route.kind)) return { version: 1, generatedAt: new Date().toISOString(), - rules: { - layoutSource: 'preparation/skt-vuetify-templates', - loginStyleSource: 'preparation/skt-vuetify-templates', - prototypeStylePolicy: 'content-only', - prototypeOuterFramePolicy: 'ignore' - }, - guideSources: prototypeGuides.map((guide) => ({ - source: guide.source, - title: guide.title, - entryCount: guide.entries.length, - legacyFlowCount: guide.legacyFlows.length - })), - legacyFlows: prototypeGuides.flatMap((guide) => guide.legacyFlows.map((flow) => ({ - source: guide.source, - title: flow.title, - nodeCount: flow.nodes.length, - tasks: unique(flow.nodes.map((node) => node.task).filter(Boolean)) - }))), - modules: buildModules(routes), routes } } @@ -433,7 +414,9 @@ function buildPrototypeGuideIndex(prototypeGuides) { const index = new Map() for (const guide of prototypeGuides) { for (const entry of guide.entries) { - index.set(entry.prototype, entry) + const normalized = normalizePrototypeGuidePath(entry.prototype) + index.set(normalized, entry) + index.set(`module/${normalized}`, entry) } } return index @@ -469,7 +452,7 @@ function findGuideValue(values, needles) { function inferRoute(spec, relativeSource) { const path = relativeSource.replace(/\\/g, '/') const segments = path.split('/') - const module = segments.length > 1 ? segments[0] : 'root' + const module = inferModuleName(segments) const basename = segments.at(-1) ?? spec.page const text = [ spec.pageContract.title, @@ -548,24 +531,15 @@ function inferRoute(spec, relativeSource) { } } -function buildModules(routes) { - const modules = new Map() - for (const route of routes) { - const current = modules.get(route.module) ?? { - name: route.module, - featurePageCount: 0, - authPageCount: 0, - referencePageCount: 0 - } - if (route.kind === 'feature-page') current.featurePageCount += 1 - else if (route.kind === 'auth' || route.kind === 'auth-support') current.authPageCount += 1 - else current.referencePageCount += 1 - modules.set(route.module, current) - } - return [...modules.values()].map((module) => ({ - ...module, - kind: module.featurePageCount > 0 ? 'feature-module' : module.authPageCount > 0 ? 'entry-module' : 'reference-module' - })) +function normalizePrototypeGuidePath(path) { + return String(path ?? '').replace(/^\.?\/*/, '').replace(/^module\//, '') +} + +function inferModuleName(segments) { + if (segments.length === 1) return 'root' + if (segments[0] === 'module' && segments[1]) return segments[1] + if (segments[0] === 'guide' && segments[1]) return segments[1] + return segments[0] ?? 'root' } function hasPasswordField(spec) { diff --git a/src/stages/scan.js b/src/stages/scan.js index 7dbb838..6a24fc9 100644 --- a/src/stages/scan.js +++ b/src/stages/scan.js @@ -1,5 +1,5 @@ import { spawn } from 'node:child_process' -import { readFile } from 'node:fs/promises' +import { readFile, rm, writeFile } from 'node:fs/promises' import { basename, join, relative } from 'node:path' import { loadConfig } from '../lib/config.js' import { artifactPath, ensureDir, exists, listFiles, readJson, sha256File, writeJson } from '../lib/files.js' @@ -19,11 +19,12 @@ const viewports = [ */ export async function scan() { const config = await loadConfig() - const htmlFiles = await listFiles(config.prototypeDir, ['.html']) + const htmlFiles = await listScanHtmlFiles(config) if (htmlFiles.length === 0) throw new Error(`找不到 HTML prototype:${config.prototypeDir}`) const prototypeGuides = await readPrototypeGuides(config) const apiCatalog = await readApiCatalog(config) await ensureDir(config.htDir) + if (isModuleScopedScan(config, htmlFiles)) await resetModuleScopedArtifacts(config) const plans = [] for (const file of htmlFiles) { plans.push(await prepareScanFile(config, file)) @@ -51,6 +52,22 @@ export async function scan() { console.log(`scan 完成:${htmlFiles.length} 個 HTML 檔案`) } +async function listScanHtmlFiles(config) { + const moduleDir = join(config.prototypeDir, 'module') + if (await exists(moduleDir)) return listFiles(moduleDir, ['.html']) + return listFiles(config.prototypeDir, ['.html']) +} + +function isModuleScopedScan(config, htmlFiles) { + const moduleDir = join(config.prototypeDir, 'module') + return htmlFiles.length > 0 && htmlFiles.every((file) => file.startsWith(moduleDir)) +} + +async function resetModuleScopedArtifacts(config) { + await rm(join(config.htDir, 'spec'), { recursive: true, force: true }) + await rm(join(config.htDir, 'cache/prototype'), { recursive: true, force: true }) +} + /** * 讀取 prototype 旁的人工導覽文件。 * 這些 guide 是 legacy JSP/PB、checklist 與 flow refs 的來源,後續 BDD 與 maintenance contract 都會使用。 @@ -213,6 +230,7 @@ async function captureRenderedPrototype(config, plan, serverUrl) { const playwrightCwd = join(config.htDir, 'cache/playwright-cli') await ensureDir(playwrightCwd) + await ensurePlaywrightCliConfig(playwrightCwd, config.cwd) const commandCwd = resolved.source === 'npx-local' ? config.cwd : playwrightCwd const session = `ht-${process.pid}-${plan.hash.slice(0, 8)}` const url = new URL(`${encodePath(plan.name)}.html`, serverUrl).href @@ -261,6 +279,27 @@ async function captureRenderedPrototype(config, plan, serverUrl) { } } +async function ensurePlaywrightCliConfig(playwrightCwd, repoCwd) { + const sourcePath = join(repoCwd, '.playwright/cli.config.json') + let browserName = 'chromium' + if (await exists(sourcePath)) { + try { + const parsed = JSON.parse(await readFile(sourcePath, 'utf8')) + browserName = parsed?.browser?.browserName ?? browserName + } catch { + browserName = 'chromium' + } + } + const override = { + browser: { + browserName + } + } + const targetDir = join(playwrightCwd, '.playwright') + await ensureDir(targetDir) + await writeFile(join(targetDir, 'cli.config.json'), `${JSON.stringify(override, null, 2)}\n`) +} + /** * 建立 Playwright CLI 執行的頁面擷取程式。 * 這段字串刻意集中處理 rendered DOM evidence,讓 source HTML 與實際瀏覽器狀態的差異能被 capture artifacts 保留下來。 diff --git a/test/cli-e2e.test.js b/test/cli-e2e.test.js index 830361f..620e9e9 100644 --- a/test/cli-e2e.test.js +++ b/test/cli-e2e.test.js @@ -30,6 +30,12 @@ test('CLI runs doctor and scan against one prototype', async () => { | --- | --- | --- | | [\`index.html\`](index.html) | \`legacy/index.jsp\` | Customer portal entry | `) + await mkdir(join(cwd, '.playwright'), { recursive: true }) + await writeFile(join(cwd, '.playwright/cli.config.json'), JSON.stringify({ + browser: { + browserName: 'chromium' + } + }, null, 2)) const doctor = await exec('node', [cli, 'doctor'], { cwd }) await exec('node', [cli, 'scan'], { cwd }) @@ -59,7 +65,6 @@ test('CLI runs doctor and scan against one prototype', async () => { assert.equal(appMap.routes[0].evidence.recommendedTemplate, undefined) assert.equal(appMap.routes[0].guide.legacyJsp, 'legacy/index.jsp') assert.equal(appMap.routes[0].guide.description, 'Customer portal entry') - assert.equal(appMap.guideSources[0].source, 'portal.md') }) function pick(object, keys) { diff --git a/test/html.test.js b/test/html.test.js index c79ba2d..cc7a035 100644 --- a/test/html.test.js +++ b/test/html.test.js @@ -131,7 +131,7 @@ test('validatePageContract reports evidence mismatches', () => { assert.match(report.warnings.join('\n'), /Save/) }) -test('buildAppMap classifies auth, shell references, and feature pages', () => { +test('buildAppMap only keeps AI-implementable auth and feature pages', () => { const prototypeDir = '/repo/prototype' const specs = [ buildSpec('/repo/prototype/portal/login.html', '
'), @@ -146,11 +146,9 @@ test('buildAppMap classifies auth, shell references, and feature pages', () => { assert.equal(buildAppMap([ buildSpec('/repo/prototype/portal/forget-password.html', '

忘記密碼

') ], prototypeDir).routes[0].targetRole, 'forgot-password') - assert.equal(appMap.routes.find((route) => route.prototype === 'portal/app-layout.html').kind, 'legacy-shell-reference') - assert.equal(appMap.routes.find((route) => route.prototype === 'portal/app-layout.html').usePrototypeContent, false) + assert.equal(appMap.routes.find((route) => route.prototype === 'portal/app-layout.html'), undefined) assert.equal(appMap.routes.find((route) => route.prototype === 'venue/applications-list.html').kind, 'feature-page') assert.equal(appMap.routes.find((route) => route.prototype === 'venue/applications-list.html').layout, 'template-app') - assert.equal(appMap.modules.find((module) => module.name === 'venue').kind, 'feature-module') }) test('buildAppMap enriches routes with prototype markdown guide entries', () => { @@ -177,13 +175,10 @@ test('buildAppMap enriches routes with prototype markdown guide entries', () => ], prototypeDir, [guide]) const route = appMap.routes[0] - assert.equal(appMap.guideSources[0].source, 'venue.md') assert.equal(route.evidence.prototypeGuide, 'venue.md') assert.equal(route.guide.legacyJsp, 'zte_pro/zte451_02.jsp + zte451_02_1.jsp') assert.equal(route.guide.legacyPb, 'n_zte451.of_zte451_02 / of_zte451_02_1') assert.equal(route.guide.targetView, 'RoomQueryView.vue') - assert.equal(appMap.guideSources[0].legacyFlowCount, 1) - assert.equal(appMap.legacyFlows[0].tasks[0], '場地查詢') assert.equal(route.guide.flowRefs[0].tasks[0], '場地查詢') assert.deepEqual(route.guide.flowRefs[0].matchedKeys, ['zte451_02.jsp', 'of_zte451_02', 'zte451_02_1.jsp', 'of_zte451_02_1']) assert.equal(route.guide.flowRefs[0].nodes[0].nodeType, 'query')