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')
|