diff --git a/.agents/skills/jsdoc/SKILL.md b/.agents/skills/jsdoc/SKILL.md new file mode 100644 index 0000000..6b4b151 --- /dev/null +++ b/.agents/skills/jsdoc/SKILL.md @@ -0,0 +1,237 @@ +--- +name: jsdoc +description: Guidelines for writing minimal, high-quality JSDoc comments in TypeScript. +--- + +# JSDoc Skill + +This skill provides focused guidelines for writing JSDoc comments consistently across the codebase. + +## When to Use + +- Documenting exported type properties and configuration options +- Adding context that TypeScript types don't convey on their own +- Providing usage examples for non-obvious or multi-variant APIs +- Writing inline documentation for generated docs + +## What It Does + +- Defines `@example` format conventions (label + inline backtick or fenced block) +- Ensures every exported type and property has meaningful documentation +- Avoids redundant tags that duplicate TypeScript type information +- Ensures consistent documentation style across all packages + +--- + +## `@example` Format + +### Short one-liner — label on the `@example` line, code as inline backtick on the next line + +```typescript +/** + * @example Required parameter + * `name: Type` + * + * @example Optional parameter + * `name?: Type` + */ +``` + +### Multi-line — fenced code block immediately after `@example` + +````typescript +/** + * @example + * ```ts + * const result = buildParams(node, { + * paramsType: 'inline', + * }) + * ``` + */ +```` + +### Multiple variants — use multiple `@example` blocks + +```typescript +/** + * @example Object mode + * `{ id, data, params }: { id: string; data: Data; params?: QueryParams }` + * + * @example Inline mode + * `id: string, data: Data, params?: QueryParams` + */ +``` + +### Rules + +| Rule | ✅ Correct | ❌ Incorrect | +| ----------------------- | ----------------------------------- | -------------------------------------------- | +| Label + inline code | `@example Required\n\`name: Type\`` | `@example \`name: Type\`` (code on tag line) | +| Multi-line code | Fenced ` ```ts ``` ` block | Bare code lines without a fence | +| Short examples | Inline backtick | Triple-backtick fence (too heavy) | +| One concern per example | Separate `@example` blocks | One example covering all cases | + +--- + +## Tags + +### Use Frequently + +| Tag | Purpose | Notes | +| ------------- | ------------------ | ------------------------------------------------------ | +| `@default` | Default value | Only when default is non-obvious; omit for `undefined` | +| `@example` | Usage example | Prefer for complex or multi-variant APIs | +| `@note` | Important caveat | Version info, breaking changes | +| `@deprecated` | Mark as deprecated | Include migration path | + +### Use Sparingly + +| Tag | Purpose | +| ----------- | ----------------------- | +| `@see` | Reference external docs | +| `@internal` | Internal API | +| `@beta` | Experimental | + +### Avoid (TypeScript Provides) + +- ❌ `@param` — use TypeScript parameter types +- ❌ `@returns` — use TypeScript return type +- ❌ `@type` — use TypeScript type annotation +- ❌ `@typedef` — use `type` or `interface` +- ❌ `@default undefined` — optional (`?`) already implies this + +--- + +## Documentation Patterns + +### Simple property — always multi-line + +```typescript +/** + * Output directory for generated files. + */ +outDir?: string +``` + +> ❌ Never use single-line `/** description */` — always expand to multi-line. + +### Property with non-obvious default + +```typescript +/** + * Maximum number of concurrent callbacks during traversal. + * Higher values overlap I/O-bound work; lower values reduce memory pressure. + * + * @default 30 + */ +concurrency?: number +``` + +> Do **not** add `@default false` or `@default undefined` when the TypeScript type already makes the default obvious. + +### Enum / union with options + +```typescript +/** + * How path parameters are emitted in the function signature. + * - `'object'` groups them as a single destructured parameter + * - `'inline'` spreads them as individual parameters + * - `'inlineSpread'` emits a single rest parameter + */ +pathParamsType: 'object' | 'inline' | 'inlineSpread' +``` + +### Property with example + +```typescript +/** + * Applies a uniform transformation to every resolved type string. + * Use this for framework-level type wrappers. + * + * @example Wrap every type in a reactive container + * `typeWrapper: (t) => \`Reactive<${t}>\`` + */ +typeWrapper?: (type: string) => string +``` + +### Nested properties — every field gets its own multi-line JSDoc + +```typescript +names?: { + /** + * Name for the request body parameter. + * @default 'data' + */ + data?: string + /** + * Name for the query parameters group parameter. + * @default 'params' + */ + params?: string + /** + * Name for the header parameters group parameter. + * @default 'headers' + */ + headers?: string +} +``` + +### Function documentation + +Only add JSDoc when it adds value beyond the signature: + +```typescript +// ✅ No JSDoc needed — signature is self-explanatory +function camelCase(str: string): string { ... } + +// ✅ JSDoc adds value — explains behaviour and non-obvious edge cases +/** + * Returns `true` when the schema resolves to a plain string output. + * + * - `string`, `uuid`, `email`, `url`, `datetime` are always plain strings. + * - `date` and `time` are plain strings when their `representation` is `'string'`. + * + * @example UUID resolves to a plain string + * `isStringType(uuidSchema) // true` + * + * @example Date with date representation is not a plain string + * `isStringType(dateSchema) // false` + */ +function isStringType(node: SchemaNode): boolean { ... } +``` + +--- + +## Guidelines + +**✅ DO:** + +- Document **what** the property does, not its TypeScript type +- Give every exported type, property, and function a JSDoc comment +- Always use multi-line JSDoc blocks — never single-line `/** ... */` +- Use concrete, full-sentence descriptions — not "Enum schema." or "Boolean value." +- Include `@default` only when the default is non-obvious (not `undefined`, not `false`) +- Use multiple `@example` blocks to show different variants or modes +- Keep `@example` labels short and descriptive + +**❌ DON'T:** + +- Write single-line `/** description */` — always use multi-line +- Write `@default undefined` — optional `?` already implies this +- Put code directly on the `@example` line: `@example \`foo: string\`` → move code to next line +- Use `@param` or `@returns` tags +- Over-document trivial, self-explanatory properties + +--- + +## Tag Order + +For consistency, use this order within a JSDoc block: + +1. Description (required) +2. Bullet-list of variants or behaviours (if applicable) +3. `@default` (if non-obvious) +4. `@example` (one or more) +5. `@note` (if needed) +6. `@deprecated` (if applicable) +7. `@see` (if providing references) diff --git a/AGENTS.md b/AGENTS.md index 4616d78..96320f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,6 +113,14 @@ packages/result # output - 不要讓 agent 修改 `prototype/`、`frontend/`、`backend/` 原始輸入資料夾;生成與執行 artifact 應限制在 `output/` 與 `.ht/`。 - 保持 MVP 邊界,避免加泛用抽象或一開始支援所有 monorepo 形態。 +## JSDoc 原則 + +- 依專案的 `jsdoc` skill 撰寫 JSDoc;這裡只補充 HTML Transform 專案特有規則。 +- JavaScript 的 JSDoc 優先補在 exported API、stage 入口、跨模組 helper,或描述非顯而易見契約與限制的位置;不為測試檔或區域性小函式刻意補註解。 +- JSDoc 使用繁體中文,並把說明重點放在 artifact、pipeline stage、MVP 限制、cache/placeholder 行為、輸入輸出契約等專案脈絡。 +- 不新增 `jsdoc` 產生 HTML 文件的設定、script 或 build step,除非使用者明確要求。 +- 補 JSDoc 不應順手 refactor、改行為、加抽象或擴大 scope。 + ## 驗證 完成任何改動後至少執行: diff --git a/src/index.js b/src/index.js index 64972ce..eb2519b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,9 @@ +/** + * 提供 `ht.config.*` 使用的設定包裝器,讓使用者能保留日後型別提示的入口。 + * + * @param {object} config 使用者提供的 HTML Transform 設定。 + * @returns {object} 原樣回傳的設定物件。 + */ export function defineConfig(config) { return config } - diff --git a/src/lib/agent.js b/src/lib/agent.js index 8ae1f74..d1f1e15 100644 --- a/src/lib/agent.js +++ b/src/lib/agent.js @@ -6,10 +6,22 @@ const commands = { gemini: ['gemini'] } +/** + * 將設定中的 agent 名稱解析成可執行命令。 + * + * @param {string} agent `ht.config.*` 中的 agent 名稱。 + * @returns {string[]} 命令與預設參數。 + */ export function resolveAgentCommand(agent) { return commands[agent] ?? [agent] } +/** + * 檢查命令是否能從目前 shell PATH 找到。 + * + * @param {string} command 要檢查的可執行檔名稱。 + * @returns {Promise} 找得到命令時為 `true`。 + */ export async function commandExists(command) { return new Promise((resolve) => { const child = spawn('which', [command], { stdio: 'ignore' }) @@ -17,4 +29,3 @@ export async function commandExists(command) { child.on('error', () => resolve(false)) }) } - diff --git a/src/lib/config.js b/src/lib/config.js index 643fc72..69e3f7e 100644 --- a/src/lib/config.js +++ b/src/lib/config.js @@ -36,6 +36,12 @@ const defaultConfig = { } } +/** + * 從專案根目錄載入 `ht.config.*`,並補齊 CLI pipeline 需要的預設路徑與 stage 設定。 + * + * @param {string} cwd 要搜尋設定檔的工作目錄。 + * @returns {Promise} 正規化後的設定物件,包含解析後的絕對路徑。 + */ export async function loadConfig(cwd = process.cwd()) { const candidates = ['ht.config.mjs', 'ht.config.js', 'ht.config.json', 'ht.config.ts'] for (const filename of candidates) { @@ -88,4 +94,3 @@ function normalizeConfig(config, cwd) { htDir: resolve(cwd, '.ht') } } - diff --git a/src/lib/files.js b/src/lib/files.js index d3dbe56..d0101eb 100644 --- a/src/lib/files.js +++ b/src/lib/files.js @@ -2,24 +2,56 @@ import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises' import { createHash } from 'node:crypto' import { dirname, extname, join, relative } from 'node:path' +/** + * 確保資料夾存在,供 stage 在寫入 `.ht/` 與 output artifact 前使用。 + * + * @param {string} path 要建立的資料夾路徑。 + * @returns {Promise} + */ export async function ensureDir(path) { await mkdir(path, { recursive: true }) } +/** + * 以穩定格式寫入 JSON artifact。 + * + * @param {string} path 輸出檔案路徑。 + * @param {unknown} data 要序列化的資料。 + * @returns {Promise} + */ export async function writeJson(path, data) { await ensureDir(dirname(path)) await writeFile(path, `${JSON.stringify(data, null, 2)}\n`) } +/** + * 讀取 JSON artifact。 + * + * @param {string} path JSON 檔案路徑。 + * @returns {Promise} 解析後的 JSON 值。 + */ export async function readJson(path) { return JSON.parse(await readFile(path, 'utf8')) } +/** + * 計算檔案內容雜湊,供 prototype cache 判斷是否命中。 + * + * @param {string} path 檔案路徑。 + * @returns {Promise} 十六進位 SHA-256 digest。 + */ export async function sha256File(path) { const content = await readFile(path) return createHash('sha256').update(content).digest('hex') } +/** + * 遞迴列出資料夾內檔案,缺少資料夾時回傳空陣列以符合 MVP doctor/scan 行為。 + * + * @param {string} root 搜尋根目錄。 + * @param {string[]} extensions 要保留的副檔名;空陣列代表不過濾。 + * @returns {Promise} 依字典序排序的檔案路徑。 + */ export async function listFiles(root, extensions = []) { const output = [] async function walk(dir) { @@ -39,6 +71,12 @@ export async function listFiles(root, extensions = []) { return output.sort() } +/** + * 檢查路徑是否存在。 + * + * @param {string} path 要檢查的路徑。 + * @returns {Promise} 路徑存在時為 `true`。 + */ export async function exists(path) { try { await stat(path) @@ -48,7 +86,13 @@ export async function exists(path) { } } +/** + * 將輸入檔路徑轉成 `.ht/` artifact 使用的穩定相對名稱。 + * + * @param {string} root 輸入根目錄。 + * @param {string} file 輸入檔案路徑。 + * @returns {string} 去除 `.html` 副檔名並標準化分隔符的 artifact 名稱。 + */ export function artifactPath(root, file) { return relative(root, file).replaceAll('\\', '/').replace(/\.html$/i, '') } - diff --git a/src/lib/html.js b/src/lib/html.js index 76d0dc3..ded90fb 100644 --- a/src/lib/html.js +++ b/src/lib/html.js @@ -1,3 +1,9 @@ +/** + * 從 HTML source 萃取 deterministic summary,作為 MVP 階段替代 browser snapshot 的 evidence。 + * + * @param {string} html HTML source。 + * @returns {object} 頁面標題、文字、表單欄位與互動元素摘要。 + */ export function summarizeHtml(html) { const title = matchText(html, /]*>([\s\S]*?)<\/title>/i) const headings = collect(html, /]*>([\s\S]*?)<\/h\1>/gi).map((item) => cleanText(item[2])) @@ -15,6 +21,12 @@ export function summarizeHtml(html) { return { title, headings, buttons, labels, inputs, textSamples } } +/** + * 依 semantic landmark 將 HTML source 拆成可追蹤的頁面區塊。 + * + * @param {string} html HTML source。 + * @returns {Array} 區塊清單;找不到 landmark 時會回傳單一 `main` 區塊。 + */ export function extractRegions(html) { const landmarks = [ ['header', /]*>([\s\S]*?)<\/header>/gi, 'banner'], @@ -50,6 +62,12 @@ export function extractRegions(html) { return regions } +/** + * 取出 MVP pipeline 可用於比對與生成的可見文字樣本。 + * + * @param {string} html HTML source。 + * @returns {string[]} 去除 script、style 與標籤後的文字 token。 + */ export function visibleText(html) { const withoutScripts = html .replace(//gi, ' ') @@ -66,6 +84,12 @@ export function visibleText(html) { .slice(0, 50) } +/** + * 將 deterministic region evidence 轉成目前 MVP 使用的 Vuetify-oriented RegionSpec。 + * + * @param {object} region `extractRegions` 產生的區塊。 + * @returns {object} 後續 plan/run stage 使用的 RegionSpec。 + */ export function inferRegionSpec(region) { const summary = summarizeHtml(region.html) const hasForm = /<(form|input|select|textarea)\b/i.test(region.html) diff --git a/src/lib/playwright-cli.js b/src/lib/playwright-cli.js index d562e2c..f578803 100644 --- a/src/lib/playwright-cli.js +++ b/src/lib/playwright-cli.js @@ -3,6 +3,12 @@ import { join } from 'node:path' import { exists } from './files.js' import { commandExists } from './agent.js' +/** + * 依專案規則解析 Playwright CLI,不依賴使用者全域環境作為第一選擇。 + * + * @param {string} cwd 專案工作目錄。 + * @returns {Promise} 可執行命令、參數與來源;找不到時為 `null`。 + */ export async function resolvePlaywrightCli(cwd = process.cwd()) { const localBin = join(cwd, 'node_modules/.bin/playwright-cli') if (await exists(localBin)) return { command: localBin, args: [], source: 'local' } @@ -11,6 +17,12 @@ export async function resolvePlaywrightCli(cwd = process.cwd()) { return null } +/** + * 確認解析到的 Playwright CLI 能實際執行。 + * + * @param {string} cwd 專案工作目錄。 + * @returns {Promise} `playwright-cli --version` 成功時為 `true`。 + */ export async function playwrightCliAvailable(cwd = process.cwd()) { const resolved = await resolvePlaywrightCli(cwd) if (!resolved) return false @@ -20,4 +32,3 @@ export async function playwrightCliAvailable(cwd = process.cwd()) { child.on('error', () => resolve(false)) }) } - diff --git a/src/stages/diff.js b/src/stages/diff.js index 8d02b23..74de719 100644 --- a/src/stages/diff.js +++ b/src/stages/diff.js @@ -3,6 +3,11 @@ import { join } from 'node:path' import { loadConfig } from '../lib/config.js' import { ensureDir, listFiles, readJson } from '../lib/files.js' +/** + * 執行 Stage 4:依現有 LayoutSpec 產生 deterministic placeholder diff report。 + * + * @returns {Promise} + */ export async function diff() { const config = await loadConfig() const specs = await listFiles(join(config.htDir, 'spec'), ['.json']) @@ -44,4 +49,3 @@ function renderDiffReport(config, spec) { ` } - diff --git a/src/stages/doctor.js b/src/stages/doctor.js index 8905303..46f6695 100644 --- a/src/stages/doctor.js +++ b/src/stages/doctor.js @@ -4,6 +4,11 @@ import { commandExists, resolveAgentCommand } from '../lib/agent.js' import { exists } from '../lib/files.js' import { playwrightCliAvailable, resolvePlaywrightCli } from '../lib/playwright-cli.js' +/** + * 檢查本機執行 HTML Transform pipeline 所需的資料夾與命令。 + * + * @returns {Promise} + */ export async function doctor() { const config = await loadConfig() const agentCommand = resolveAgentCommand(config.agent)[0] diff --git a/src/stages/plan.js b/src/stages/plan.js index 43bb84c..1f6a579 100644 --- a/src/stages/plan.js +++ b/src/stages/plan.js @@ -4,6 +4,13 @@ import { basename, join } from 'node:path' import { loadConfig } from '../lib/config.js' import { listFiles, readJson, writeJson } from '../lib/files.js' +/** + * 執行 Stage 2:依 LayoutSpec 建立 tasklist、component mapping 與專案/API contract artifacts。 + * + * @param {object} options 執行選項。 + * @param {boolean} [options.assumeYes] 跳過互動 review 提示。 + * @returns {Promise} + */ export async function plan(options = {}) { const config = await loadConfig() const specs = await listFiles(join(config.htDir, 'spec'), ['.json']) @@ -117,4 +124,3 @@ async function writeMarkdown(path, content) { function renderPreview(taskList) { return taskList.tasks.map((task) => `${task.id} -> ${task.targetFile}`).join('\n') } - diff --git a/src/stages/run.js b/src/stages/run.js index 2fa05a4..858805d 100644 --- a/src/stages/run.js +++ b/src/stages/run.js @@ -3,6 +3,13 @@ import { basename, dirname, join } from 'node:path' import { loadConfig } from '../lib/config.js' import { ensureDir, exists, listFiles, readJson, writeJson } from '../lib/files.js' +/** + * 執行 Stage 3:根據 TaskList 產生 output 內的 Vue/Vuetify 檔案與 run metadata。 + * + * @param {object} options 執行選項。 + * @param {boolean} [options.retryFailed] 僅重跑狀態為 `error` 的 task。 + * @returns {Promise} + */ export async function runTasks(options = {}) { const config = await loadConfig() const taskListPath = join(config.htDir, 'plan/tasklist.json') @@ -94,4 +101,3 @@ function topoSort(tasks) { function escapeHtml(value) { return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('"', '"') } - diff --git a/src/stages/scan.js b/src/stages/scan.js index 4311ec5..7081a4a 100644 --- a/src/stages/scan.js +++ b/src/stages/scan.js @@ -10,6 +10,11 @@ const viewports = [ { name: 'mobile', width: 375, height: 812 } ] +/** + * 執行 Stage 1:掃描 prototype HTML,產生 deterministic evidence 與 LayoutSpec artifacts。 + * + * @returns {Promise} + */ export async function scan() { const config = await loadConfig() const htmlFiles = await listFiles(config.prototypeDir, ['.html']) diff --git a/src/stages/status.js b/src/stages/status.js index cf5a34a..41655a4 100644 --- a/src/stages/status.js +++ b/src/stages/status.js @@ -2,6 +2,11 @@ import { join } from 'node:path' import { loadConfig } from '../lib/config.js' import { exists, readJson } from '../lib/files.js' +/** + * 顯示目前 TaskList 中各 task 狀態的數量。 + * + * @returns {Promise} + */ export async function status() { const config = await loadConfig() const path = join(config.htDir, 'plan/tasklist.json') @@ -15,4 +20,3 @@ export async function status() { console.log(`${state}: ${counts[state]?.length ?? 0}`) } } - diff --git a/src/stages/verify.js b/src/stages/verify.js index f8901a0..cec1062 100644 --- a/src/stages/verify.js +++ b/src/stages/verify.js @@ -3,6 +3,11 @@ import { join } from 'node:path' import { loadConfig } from '../lib/config.js' import { exists, listFiles, readJson, writeJson } from '../lib/files.js' +/** + * 執行 Stage 5:執行設定中的 quality commands,並寫出 MVP verification report。 + * + * @returns {Promise} + */ export async function verify() { const config = await loadConfig() const quality = {} @@ -43,4 +48,3 @@ async function runDomChecks(config) { const missing = taskList.tasks.filter((task) => !outputFiles.some((file) => file.endsWith(task.targetFile))) return { status: missing.length === 0 ? 'passed' : 'failed', missingTargets: missing.map((task) => task.targetFile) } } -