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:
skytek_xinliang
2026-05-26 16:44:36 +08:00
parent 8d40f6972b
commit 23fc852321
6 changed files with 35 additions and 159 deletions
+6 -6
View File
@@ -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 後至少執行:
-3
View File
@@ -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
View File
@@ -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
View File
@@ -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([
-9
View File
@@ -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
View File
@@ -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',