refactor(scan): stop emitting markdown contract artifacts
Remove generated `.ui-contract.md` and `.bdd.md` outputs from the scan pipeline and documentation so downstream consumers rely on structured JSON artifacts instead. Also drop unused API `fieldRules` catalog output and remove stale app-map references to UI contract files. Update legacy flow matching to preserve only the nodes that directly matched the current entry.refactor(scan): stop emitting markdown contract artifacts Remove generated `.ui-contract.md` and `.bdd.md` outputs from the scan pipeline and documentation so downstream consumers rely on structured JSON artifacts instead. Also drop unused API `fieldRules` catalog output and remove stale app-map references to UI contract files. Update legacy flow matching to preserve only the nodes that directly matched the current entry.
This commit is contained in:
@@ -193,8 +193,6 @@ node packages/html-transform/src/cli.js scan
|
|||||||
.ht/app-map.json
|
.ht/app-map.json
|
||||||
.ht/spec/{page}.spec.json
|
.ht/spec/{page}.spec.json
|
||||||
.ht/spec/{page}.validation.json
|
.ht/spec/{page}.validation.json
|
||||||
.ht/spec/{page}.ui-contract.md
|
|
||||||
.ht/spec/{page}.bdd.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`.ht/api-catalog.json` 是跨頁面的 backend API catalog,來源是 `backendDocs` 目錄下的 markdown 文件。它會包含:
|
`.ht/api-catalog.json` 是跨頁面的 backend API catalog,來源是 `backendDocs` 目錄下的 markdown 文件。它會包含:
|
||||||
@@ -216,8 +214,6 @@ node packages/html-transform/src/cli.js scan
|
|||||||
- `bddContract`:以頁面 evidence 產生的 Gherkin 草稿、scenario evidence trace 與人工 review warnings。
|
- `bddContract`:以頁面 evidence 產生的 Gherkin 草稿、scenario evidence trace 與人工 review warnings。
|
||||||
- `regions`:目前仍保留,目的是相容既有資料形狀;不要把它視為 Stage 2 decomposition。
|
- `regions`:目前仍保留,目的是相容既有資料形狀;不要把它視為 Stage 2 decomposition。
|
||||||
|
|
||||||
`.bdd.md` 是 BDD 起始模版,會依 `pageKind` 產生 auth、query、maintenance、application、print 或 generic scenario。每個 scenario 會保留欄位、actions、tables、API endpoints、prototype checklist 與 legacy flow refs,供人工 review 後再轉成可執行測試。若 scan 看見錯誤流程、必填驗證、狀態限制、API error format 或尚未被 scenario 使用的 checklist/action/API,會列入 `candidateScenarios` 或 `uncoveredEvidence`,避免 evidence 被靜默漏掉。
|
|
||||||
|
|
||||||
`.ht/app-map.json` 是跨頁面的應用結構推論。通用 prompt 應先讀它,再決定每個 prototype 是 `auth`、`legacy-shell-reference`、`feature-page` 或其他角色。MVP 固定策略是 template layout/style 優先,prototype 只提供內容與功能證據。
|
`.ht/app-map.json` 是跨頁面的應用結構推論。通用 prompt 應先讀它,再決定每個 prototype 是 `auth`、`legacy-shell-reference`、`feature-page` 或其他角色。MVP 固定策略是 template layout/style 優先,prototype 只提供內容與功能證據。
|
||||||
|
|
||||||
route 也會包含 implementation hints,例如:
|
route 也會包含 implementation hints,例如:
|
||||||
@@ -286,10 +282,14 @@ node packages/html-transform/src/cli.js scan
|
|||||||
- 新增輸出:`.ht/api-catalog.json`。
|
- 新增輸出:`.ht/api-catalog.json`。
|
||||||
- `.ht/spec/{page}.spec.json` 新增 `apiContract`、`prototypeGuide`、`maintenanceContract`。
|
- `.ht/spec/{page}.spec.json` 新增 `apiContract`、`prototypeGuide`、`maintenanceContract`。
|
||||||
- `.ht/app-map.json` route 新增 `pageKind`、`capabilities`、`primaryEntity`、`apiCount`。
|
- `.ht/app-map.json` route 新增 `pageKind`、`capabilities`、`primaryEntity`、`apiCount`。
|
||||||
- `.ui-contract.md` 會顯示 page kind、capabilities、prototype checklist 與 API endpoints。
|
|
||||||
|
|
||||||
因此既有 `doctor` / `scan` 指令不用改;但使用 `.ht` 產物的 prompt 或下游工具,應改讀新增欄位。
|
因此既有 `doctor` / `scan` 指令不用改;但使用 `.ht` 產物的 prompt 或下游工具,應改讀新增欄位。
|
||||||
|
|
||||||
|
## 產物中的路徑
|
||||||
|
|
||||||
|
`.spec.json` 內 `source`、`screenshot`、`domSummary`、`accessibilityTree`、`metadata` 等欄位使用絕對路徑,而非相對路徑。這是 scan pipeline 的自然結果:`config.prototypeDir`、`config.htDir` 都以 `path.resolve(cwd, ...)` 轉為絕對路徑,後續 `listFiles` 和 `path.join` 產出的路徑字串均繼承絕對形式,不做 `path.relative` 轉換。
|
||||||
|
|
||||||
|
因為 `.ht/` 目錄已由 `.gitignore` 排除,這些檔案不會進入版本控制,每個開發者在自己機器上跑 `scan` 會重新生成,路徑自然指向自己的專案目錄,因此不影響可攜性。
|
||||||
|
|
||||||
## 驗證
|
## 驗證
|
||||||
|
|
||||||
修改本 package 後至少執行:
|
修改本 package 後至少執行:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { basename } from 'node:path'
|
|||||||
|
|
||||||
export function buildApiCatalog(documents = []) {
|
export function buildApiCatalog(documents = []) {
|
||||||
const endpoints = []
|
const endpoints = []
|
||||||
const fieldRules = []
|
|
||||||
let errorContract = null
|
let errorContract = null
|
||||||
|
|
||||||
for (const document of documents) {
|
for (const document of documents) {
|
||||||
@@ -21,7 +20,6 @@ export function buildApiCatalog(documents = []) {
|
|||||||
if (/API_Manual\.md$/i.test(source)) {
|
if (/API_Manual\.md$/i.test(source)) {
|
||||||
const manual = parseApiManual(source, markdown)
|
const manual = parseApiManual(source, markdown)
|
||||||
endpoints.push(...manual.endpoints)
|
endpoints.push(...manual.endpoints)
|
||||||
fieldRules.push(...manual.fieldRules)
|
|
||||||
errorContract ??= manual.errorContract
|
errorContract ??= manual.errorContract
|
||||||
} else {
|
} else {
|
||||||
endpoints.push(...parseApiIndex(source, markdown))
|
endpoints.push(...parseApiIndex(source, markdown))
|
||||||
@@ -34,7 +32,6 @@ export function buildApiCatalog(documents = []) {
|
|||||||
sources: documents.map((document) => document.source),
|
sources: documents.map((document) => document.source),
|
||||||
endpoints: merged,
|
endpoints: merged,
|
||||||
schemas: buildSchemas(merged),
|
schemas: buildSchemas(merged),
|
||||||
fieldRules,
|
|
||||||
errorContract
|
errorContract
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -220,7 +220,6 @@ export function buildAppMapWithGuides(layoutSpecs, prototypeDir, prototypeGuides
|
|||||||
flowRefs: guideEntry.flowRefs ?? []
|
flowRefs: guideEntry.flowRefs ?? []
|
||||||
} : null,
|
} : null,
|
||||||
evidence: {
|
evidence: {
|
||||||
uiContract: `.ht/spec/${relativeSource.replace(/\.html$/, '.ui-contract.md')}`,
|
|
||||||
spec: `.ht/spec/${relativeSource.replace(/\.html$/, '.spec.json')}`,
|
spec: `.ht/spec/${relativeSource.replace(/\.html$/, '.spec.json')}`,
|
||||||
prototypeGuide: guideEntry?.source ?? null,
|
prototypeGuide: guideEntry?.source ?? null,
|
||||||
screenshot: spec.pageContract.screenshot,
|
screenshot: spec.pageContract.screenshot,
|
||||||
@@ -369,14 +368,11 @@ function matchLegacyFlowRefs(entry, legacyFlows) {
|
|||||||
})
|
})
|
||||||
if (matchingNodes.length === 0) return []
|
if (matchingNodes.length === 0) return []
|
||||||
const tasks = unique(matchingNodes.map((node) => node.task).filter(Boolean))
|
const tasks = unique(matchingNodes.map((node) => node.task).filter(Boolean))
|
||||||
const nodes = tasks.length > 0
|
|
||||||
? flow.nodes.filter((node) => tasks.includes(node.task))
|
|
||||||
: matchingNodes
|
|
||||||
return [{
|
return [{
|
||||||
title: flow.title,
|
title: flow.title,
|
||||||
tasks,
|
tasks,
|
||||||
matchedKeys: unique(matchingNodes.flatMap((node) => extractLegacyKeys(`${node.jsp.join(' ')} ${node.pb.join(' ')}`)).filter((key) => entryKeys.includes(key))),
|
matchedKeys: unique(matchingNodes.flatMap((node) => extractLegacyKeys(`${node.jsp.join(' ')} ${node.pb.join(' ')}`)).filter((key) => entryKeys.includes(key))),
|
||||||
nodes
|
nodes: matchingNodes
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-132
@@ -1,10 +1,10 @@
|
|||||||
import { spawn } from 'node:child_process'
|
import { spawn } from 'node:child_process'
|
||||||
import { readFile, writeFile } from 'node:fs/promises'
|
import { readFile } from 'node:fs/promises'
|
||||||
import { basename, join, relative } from 'node:path'
|
import { basename, join, relative } from 'node:path'
|
||||||
import { loadConfig } from '../lib/config.js'
|
import { loadConfig } from '../lib/config.js'
|
||||||
import { artifactPath, ensureDir, exists, listFiles, readJson, sha256File, writeJson } from '../lib/files.js'
|
import { artifactPath, ensureDir, exists, listFiles, readJson, sha256File, writeJson } from '../lib/files.js'
|
||||||
import { buildApiCatalog, matchApiEndpointCandidates } from '../lib/api-docs.js'
|
import { buildApiCatalog, matchApiEndpointCandidates } from '../lib/api-docs.js'
|
||||||
import { buildBddContract, renderBddContract } from '../lib/bdd.js'
|
import { buildBddContract } from '../lib/bdd.js'
|
||||||
import { buildAppMapWithGuides, buildPageContract, extractRegions, inferRegionSpec, parsePrototypeGuide, summarizeHtml, validatePageContract } from '../lib/html.js'
|
import { buildAppMapWithGuides, buildPageContract, extractRegions, inferRegionSpec, parsePrototypeGuide, summarizeHtml, validatePageContract } from '../lib/html.js'
|
||||||
import { buildMaintenanceContract } from '../lib/maintenance.js'
|
import { buildMaintenanceContract } from '../lib/maintenance.js'
|
||||||
import { resolvePlaywrightCli } from '../lib/playwright-cli.js'
|
import { resolvePlaywrightCli } from '../lib/playwright-cli.js'
|
||||||
@@ -45,8 +45,6 @@ export async function scan() {
|
|||||||
spec.bddContract = buildBddContract(spec)
|
spec.bddContract = buildBddContract(spec)
|
||||||
await writeJson(join(config.htDir, 'spec', `${name}.spec.json`), spec)
|
await writeJson(join(config.htDir, 'spec', `${name}.spec.json`), spec)
|
||||||
await writeJson(join(config.htDir, 'spec', `${name}.validation.json`), spec.validation)
|
await writeJson(join(config.htDir, 'spec', `${name}.validation.json`), spec.validation)
|
||||||
await writeFile(join(config.htDir, 'spec', `${name}.ui-contract.md`), renderUiContract(spec))
|
|
||||||
await writeFile(join(config.htDir, 'spec', `${name}.bdd.md`), renderBddContract(spec))
|
|
||||||
}
|
}
|
||||||
await writeJson(join(config.htDir, 'api-catalog.json'), apiCatalog)
|
await writeJson(join(config.htDir, 'api-catalog.json'), apiCatalog)
|
||||||
await writeJson(join(config.htDir, 'app-map.json'), appMap)
|
await writeJson(join(config.htDir, 'app-map.json'), appMap)
|
||||||
@@ -104,7 +102,11 @@ function enrichSpecsWithRouteContracts(layoutSpecs, appMap, apiCatalog, prototyp
|
|||||||
rejectedCandidates: apiCandidates.rejected,
|
rejectedCandidates: apiCandidates.rejected,
|
||||||
errorHandling: apiCatalog.errorContract
|
errorHandling: apiCatalog.errorContract
|
||||||
}
|
}
|
||||||
spec.apiCatalog = { fieldRules: apiCatalog.fieldRules ?? [] }
|
const matchedEndpointIds = new Set(apiMatches.map(ep => ep.id))
|
||||||
|
const allFieldRules = (apiCatalog.endpoints ?? []).flatMap(ep =>
|
||||||
|
(ep.fieldRules ?? []).map(r => ({ ...r, endpointId: ep.id }))
|
||||||
|
)
|
||||||
|
spec.apiCatalog = { fieldRules: allFieldRules.filter(r => matchedEndpointIds.has(r.endpointId)) }
|
||||||
spec.prototypeGuide = route.guide ? {
|
spec.prototypeGuide = route.guide ? {
|
||||||
source: route.guide.source,
|
source: route.guide.source,
|
||||||
legacyJsp: route.guide.legacyJsp,
|
legacyJsp: route.guide.legacyJsp,
|
||||||
@@ -115,13 +117,10 @@ function enrichSpecsWithRouteContracts(layoutSpecs, appMap, apiCatalog, prototyp
|
|||||||
flowRefs: route.guide.flowRefs ?? []
|
flowRefs: route.guide.flowRefs ?? []
|
||||||
} : null
|
} : null
|
||||||
spec.maintenanceContract = buildMaintenanceContract({ route, spec, apiMatches })
|
spec.maintenanceContract = buildMaintenanceContract({ route, spec, apiMatches })
|
||||||
route.apiCount = apiMatches.length
|
|
||||||
route.primaryEntity = spec.maintenanceContract.dataModel.primaryEntity
|
|
||||||
route.capabilities = spec.maintenanceContract.capabilities
|
|
||||||
route.pageKind = spec.maintenanceContract.pageKind
|
|
||||||
route.evidence.apiCount = apiMatches.length
|
route.evidence.apiCount = apiMatches.length
|
||||||
route.evidence.primaryEntity = route.primaryEntity
|
route.evidence.primaryEntity = spec.maintenanceContract.dataModel.primaryEntity
|
||||||
route.evidence.capabilities = route.capabilities
|
route.evidence.capabilities = spec.maintenanceContract.capabilities
|
||||||
|
route.pageKind = spec.maintenanceContract.pageKind
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +161,10 @@ async function scanFile(config, plan, serverUrl) {
|
|||||||
await writeJson(metadataPath, capture)
|
await writeJson(metadataPath, capture)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (domSummary?.layoutEvidence) {
|
||||||
|
domSummary.layoutEvidence = stripBbox(domSummary.layoutEvidence)
|
||||||
|
}
|
||||||
|
|
||||||
const regions = extractRegions(html)
|
const regions = extractRegions(html)
|
||||||
const regionSpecs = regions.map(inferRegionSpec)
|
const regionSpecs = regions.map(inferRegionSpec)
|
||||||
const screenshot = capture.screenshots.find((item) => item.viewport === 'desktop' && item.state === 'default')
|
const screenshot = capture.screenshots.find((item) => item.viewport === 'desktop' && item.state === 'default')
|
||||||
@@ -196,7 +199,6 @@ async function scanFile(config, plan, serverUrl) {
|
|||||||
|
|
||||||
await writeJson(join(config.htDir, 'spec', `${name}.spec.json`), layoutSpec)
|
await writeJson(join(config.htDir, 'spec', `${name}.spec.json`), layoutSpec)
|
||||||
await writeJson(join(config.htDir, 'spec', `${name}.validation.json`), validationReport)
|
await writeJson(join(config.htDir, 'spec', `${name}.validation.json`), validationReport)
|
||||||
await writeFile(join(config.htDir, 'spec', `${name}.ui-contract.md`), renderUiContract(layoutSpec))
|
|
||||||
return layoutSpec
|
return layoutSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,128 +541,17 @@ function validateSpecs(specs, pageContract, domSummary) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function stripBbox(obj) {
|
||||||
* 產生人工 review 用的 UI contract Markdown。
|
if (Array.isArray(obj)) return obj.map(stripBbox)
|
||||||
* JSON spec 是機器可讀來源;這份 Markdown 只摘要最關鍵的 UI、BDD、browser 與 API evidence。
|
if (obj && typeof obj === 'object') {
|
||||||
*/
|
const result = {}
|
||||||
function renderUiContract(spec) {
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
const lines = [`# UI Contract: ${spec.page}`, '']
|
if (key === 'bbox') continue
|
||||||
const contract = spec.pageContract
|
result[key] = stripBbox(value)
|
||||||
lines.push(`- Source: ${contract.source}`)
|
|
||||||
lines.push(`- Screenshot: ${contract.screenshot ?? 'none'}`)
|
|
||||||
lines.push(`- Title: ${contract.title ?? 'none'}`)
|
|
||||||
lines.push(`- Text: ${contract.textSamples.join(', ') || 'none'}`)
|
|
||||||
lines.push(`- Vuetify: ${contract.vuetifyComponents.join(', ') || 'none'}`)
|
|
||||||
if (spec.maintenanceContract) {
|
|
||||||
lines.push(`- Page kind: ${spec.maintenanceContract.pageKind}`)
|
|
||||||
lines.push(`- Capabilities: ${spec.maintenanceContract.capabilities.join(', ') || 'none'}`)
|
|
||||||
lines.push(`- Primary entity: ${spec.maintenanceContract.dataModel.primaryEntity ?? 'none'}`)
|
|
||||||
}
|
}
|
||||||
if (spec.prototypeGuide) {
|
return result
|
||||||
lines.push(`- Target view: ${spec.prototypeGuide.targetView ?? 'none'}`)
|
|
||||||
lines.push(`- Legacy JSP: ${spec.prototypeGuide.legacyJsp ?? 'none'}`)
|
|
||||||
lines.push(`- Legacy PB: ${spec.prototypeGuide.legacyPb ?? 'none'}`)
|
|
||||||
}
|
}
|
||||||
lines.push('')
|
return obj
|
||||||
lines.push('## Sections', '')
|
|
||||||
for (const section of contract.sections) {
|
|
||||||
lines.push(`- ${section.id}: ${section.name} (${section.role})`)
|
|
||||||
}
|
|
||||||
if (spec.prototypeGuide?.checklist?.length) {
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## Prototype Checklist', '')
|
|
||||||
for (const item of spec.prototypeGuide.checklist) lines.push(`- ${item}`)
|
|
||||||
}
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## Forms', '')
|
|
||||||
if (contract.forms.length === 0) lines.push('- none')
|
|
||||||
for (const form of contract.forms) {
|
|
||||||
lines.push(`- Labels: ${form.labels.join(', ') || 'none'}`)
|
|
||||||
lines.push(`- Fields: ${form.fields.map((field) => `${field.label ?? field.name ?? 'field'}${field.required ? ' required' : ''}${field.readonly ? ' readonly' : ''}`).join(', ') || 'none'}`)
|
|
||||||
lines.push(`- Actions: ${[...form.primaryActions, ...form.secondaryActions].join(', ') || 'none'}`)
|
|
||||||
}
|
|
||||||
if (spec.maintenanceContract?.dataModel?.detailEntities?.length) {
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## Detail Collections', '')
|
|
||||||
for (const detail of spec.maintenanceContract.dataModel.detailEntities) lines.push(`- ${detail.name}`)
|
|
||||||
}
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## Row Actions', '')
|
|
||||||
if (!spec.maintenanceContract?.rowActions?.length) lines.push('- none')
|
|
||||||
for (const action of spec.maintenanceContract?.rowActions ?? []) {
|
|
||||||
lines.push(`- ${action.actionType}: ${action.label}${action.enabledWhen ? ` enabledWhen ${action.enabledWhen}` : ''}`)
|
|
||||||
}
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## Layout Evidence', '')
|
|
||||||
const layout = contract.layoutEvidence
|
|
||||||
if (!layout) lines.push('- none')
|
|
||||||
else {
|
|
||||||
lines.push(`- First viewport: sections=${layout.firstViewport?.visibleSectionCount ?? 0}, fields=${layout.firstViewport?.visibleFormFieldCount ?? 0}, tables=${layout.firstViewport?.visibleTableCount ?? 0}, occupied=${layout.firstViewport?.approximateOccupiedContentArea ?? 'unknown'}`)
|
|
||||||
lines.push(`- Form rows: ${layout.formRows?.length ?? 0}`)
|
|
||||||
lines.push(`- Primary actions: ${(layout.primaryActions ?? []).map((action) => action.label).filter(Boolean).join(', ') || 'none'}`)
|
|
||||||
}
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## Tables', '')
|
|
||||||
if (contract.tables.length === 0) lines.push('- none')
|
|
||||||
for (const table of contract.tables) {
|
|
||||||
lines.push(`- ${table.id} (${table.role}): ${table.headers.join(', ') || 'no headers'}`)
|
|
||||||
}
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## Actions', '')
|
|
||||||
if (contract.actions.length === 0) lines.push('- none')
|
|
||||||
for (const action of contract.actions) {
|
|
||||||
lines.push(`- ${action.kind}/${action.scope}/${action.actionType}: ${action.label}`)
|
|
||||||
}
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## BDD Scenarios', '')
|
|
||||||
if (!spec.bddContract?.scenarios?.length) lines.push('- none')
|
|
||||||
for (const scenario of spec.bddContract?.scenarios ?? []) {
|
|
||||||
lines.push(`- ${scenario.type}: ${scenario.name}`)
|
|
||||||
}
|
|
||||||
if (spec.bddContract?.candidateScenarios?.length) lines.push(`- Candidate scenarios: ${spec.bddContract.candidateScenarios.length}`)
|
|
||||||
if (spec.bddContract?.uncoveredEvidence?.length) lines.push(`- Uncovered evidence: ${spec.bddContract.uncoveredEvidence.length}`)
|
|
||||||
if (spec.bddContract?.warnings?.length) {
|
|
||||||
lines.push(`- Requires human review: ${spec.bddContract.requiresHumanReview ? 'yes' : 'no'}`)
|
|
||||||
}
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## Capture Artifacts', '')
|
|
||||||
if (!spec.captureArtifacts) lines.push('- none')
|
|
||||||
else {
|
|
||||||
lines.push(`- DOM summary: ${spec.captureArtifacts.domSummary}`)
|
|
||||||
lines.push(`- Accessibility tree: ${spec.captureArtifacts.accessibilityTree}`)
|
|
||||||
lines.push(`- Metadata: ${spec.captureArtifacts.metadata}`)
|
|
||||||
lines.push(`- Screenshots: ${(spec.captureArtifacts.screenshots ?? []).map((item) => `${item.viewport}/${item.state}`).join(', ') || 'none'}`)
|
|
||||||
}
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## Browser Evidence', '')
|
|
||||||
const resourceFailures = spec.browserEvidence?.externalResourceFailures ?? []
|
|
||||||
const consoleMessages = spec.browserEvidence?.consoleMessages ?? []
|
|
||||||
lines.push(`- Resource failures: ${resourceFailures.length}`)
|
|
||||||
for (const failure of resourceFailures.slice(0, 5)) lines.push(` - ${failure.status ?? failure.reason}: ${failure.url}`)
|
|
||||||
lines.push(`- Console warnings/errors: ${consoleMessages.length}`)
|
|
||||||
for (const message of consoleMessages.slice(0, 5)) lines.push(` - ${message.type}: ${message.text}`)
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## API Endpoints', '')
|
|
||||||
if (!spec.apiContract?.endpoints?.length) lines.push('- none')
|
|
||||||
for (const endpoint of spec.apiContract?.endpoints ?? []) {
|
|
||||||
lines.push(`- ${endpoint.usage ?? 'unknown'}: ${endpoint.method} ${endpoint.path}: ${endpoint.description}`)
|
|
||||||
}
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## Field Rules', '')
|
|
||||||
const endpointIds = new Set((spec.apiContract?.endpoints ?? []).map((endpoint) => endpoint.id))
|
|
||||||
const rules = (spec.apiCatalog?.fieldRules ?? []).filter((rule) => endpointIds.has(rule.endpointId))
|
|
||||||
if (rules.length === 0) lines.push('- none')
|
|
||||||
for (const rule of rules) lines.push(`- ${rule.field}: ${rule.rule}`)
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## Rejected API Candidates', '')
|
|
||||||
if (!spec.apiContract?.rejectedCandidates?.length) lines.push('- none')
|
|
||||||
for (const candidate of spec.apiContract?.rejectedCandidates ?? []) lines.push(`- ${candidate.method} ${candidate.path}: ${candidate.reason}`)
|
|
||||||
lines.push('')
|
|
||||||
lines.push('## Warnings', '')
|
|
||||||
const warnings = spec.validation.warnings
|
|
||||||
if (warnings.length === 0) lines.push('- none')
|
|
||||||
for (const warning of warnings) lines.push(`- ${warning}`)
|
|
||||||
return `${lines.join('\n')}\n`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const vuetifyWhitelist = new Set([
|
const vuetifyWhitelist = new Set([
|
||||||
|
|||||||
@@ -34,20 +34,11 @@ test('CLI runs doctor and scan against one prototype', async () => {
|
|||||||
const doctor = await exec('node', [cli, 'doctor'], { cwd })
|
const doctor = await exec('node', [cli, 'doctor'], { cwd })
|
||||||
await exec('node', [cli, 'scan'], { cwd })
|
await exec('node', [cli, 'scan'], { cwd })
|
||||||
|
|
||||||
const contract = await readFile(join(cwd, '.ht/spec/index.ui-contract.md'), 'utf8')
|
|
||||||
const bdd = await readFile(join(cwd, '.ht/spec/index.bdd.md'), 'utf8')
|
|
||||||
const spec = JSON.parse(await readFile(join(cwd, '.ht/spec/index.spec.json'), 'utf8'))
|
const spec = JSON.parse(await readFile(join(cwd, '.ht/spec/index.spec.json'), 'utf8'))
|
||||||
const validation = JSON.parse(await readFile(join(cwd, '.ht/spec/index.validation.json'), 'utf8'))
|
const validation = JSON.parse(await readFile(join(cwd, '.ht/spec/index.validation.json'), 'utf8'))
|
||||||
const appMap = JSON.parse(await readFile(join(cwd, '.ht/app-map.json'), 'utf8'))
|
const appMap = JSON.parse(await readFile(join(cwd, '.ht/app-map.json'), 'utf8'))
|
||||||
|
|
||||||
assert.match(doctor.stdout, /ok prototype directory/)
|
assert.match(doctor.stdout, /ok prototype directory/)
|
||||||
assert.match(contract, /Customer Portal/)
|
|
||||||
assert.match(contract, /BDD Scenarios/)
|
|
||||||
assert.match(contract, /Capture Artifacts/)
|
|
||||||
assert.match(contract, /Browser Evidence/)
|
|
||||||
assert.match(bdd, /Feature: Customer portal entry/)
|
|
||||||
assert.match(bdd, /Scenario: 使用者填寫必要資料並送出成功/)
|
|
||||||
assert.doesNotMatch(contract, /Recommended template/)
|
|
||||||
assert.equal(spec.pageContract.title, null)
|
assert.equal(spec.pageContract.title, null)
|
||||||
assert.equal(spec.bddContract.feature, 'Customer portal entry')
|
assert.equal(spec.bddContract.feature, 'Customer portal entry')
|
||||||
assert.equal(spec.bddContract.scenarios[0].type, 'application-submit')
|
assert.equal(spec.bddContract.scenarios[0].type, 'application-submit')
|
||||||
|
|||||||
+3
-2
@@ -229,7 +229,7 @@ test('parsePrototypeGuide extracts legacy flow references for matched entries',
|
|||||||
|
|
||||||
assert.equal(guide.legacyFlows.length, 1)
|
assert.equal(guide.legacyFlows.length, 1)
|
||||||
assert.equal(guide.entries[0].flowRefs[0].tasks[0], '場地申請')
|
assert.equal(guide.entries[0].flowRefs[0].tasks[0], '場地申請')
|
||||||
assert.ok(guide.entries[0].flowRefs[0].nodes.some((node) => node.nodeType === 'precondition'))
|
assert.ok(guide.entries[0].flowRefs[0].nodes.some((node) => node.nodeType === 'form'))
|
||||||
assert.ok(guide.entries[0].flowRefs[0].nodes.some((node) => node.jsp.includes('zte450_01.jsp')))
|
assert.ok(guide.entries[0].flowRefs[0].nodes.some((node) => node.jsp.includes('zte450_01.jsp')))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -278,7 +278,8 @@ test('buildApiCatalog parses generic markdown API docs and matches routes', () =
|
|||||||
])
|
])
|
||||||
|
|
||||||
assert.ok(catalog.endpoints.some((endpoint) => endpoint.id === 'POST /api/v1/orders'))
|
assert.ok(catalog.endpoints.some((endpoint) => endpoint.id === 'POST /api/v1/orders'))
|
||||||
assert.equal(catalog.fieldRules[0].field, 'orderName')
|
const ordersEndpoint = catalog.endpoints.find((endpoint) => endpoint.id === 'POST /api/v1/orders')
|
||||||
|
assert.equal(ordersEndpoint.fieldRules[0].field, 'orderName')
|
||||||
const matches = matchApiEndpoints({
|
const matches = matchApiEndpoints({
|
||||||
prototype: 'orders/apply.html',
|
prototype: 'orders/apply.html',
|
||||||
page: 'apply.html',
|
page: 'apply.html',
|
||||||
|
|||||||
Reference in New Issue
Block a user