feat: derive flow refs and refine maintenance contracts
Parse legacy flow blocks from prototype guides into flowRefs so prompts can rely on extracted evidence instead of hardcoded JSP flow knowledge. Refine API endpoint matching for special route roles and prototype types, and expand maintenance contracts with row action conditions and checklist-derived business rules. Update README docs to reflect the new contract fields and app-map output.feat: derive flow refs and refine maintenance contracts Parse legacy flow blocks from prototype guides into flowRefs so prompts can rely on extracted evidence instead of hardcoded JSP flow knowledge. Refine API endpoint matching for special route roles and prototype types, and expand maintenance contracts with row action conditions and checklist-derived business rules. Update README docs to reflect the new contract fields and app-map output.
This commit is contained in:
@@ -41,6 +41,7 @@ test('CLI runs doctor and scan against one prototype', async () => {
|
||||
|
||||
assert.match(doctor.stdout, /ok prototype directory/)
|
||||
assert.match(contract, /Customer Portal/)
|
||||
assert.doesNotMatch(contract, /Recommended template/)
|
||||
assert.equal(spec.pageContract.title, null)
|
||||
assert.deepEqual(pick(spec.pageContract.forms[0].fields[0], ['name', 'label', 'type', 'required']), {
|
||||
name: 'email',
|
||||
@@ -52,6 +53,8 @@ test('CLI runs doctor and scan against one prototype', async () => {
|
||||
assert.equal(appMap.routes[0].prototype, 'index.html')
|
||||
assert.equal(appMap.routes[0].kind, 'feature-page')
|
||||
assert.equal(appMap.routes[0].layout, 'template-app')
|
||||
assert.equal(appMap.routes[0].recommendedTemplate, undefined)
|
||||
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')
|
||||
|
||||
+155
-1
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { buildApiCatalog, matchApiEndpoints } from '../src/lib/api-docs.js'
|
||||
import { buildApiCatalog, matchApiEndpointCandidates, matchApiEndpoints } from '../src/lib/api-docs.js'
|
||||
import { buildMaintenanceContract } from '../src/lib/maintenance.js'
|
||||
import { buildAppMap, buildAppMapWithGuides, buildPageContract, extractRegions, inferRegionSpec, parsePrototypeGuide, summarizeHtml, validatePageContract } from '../src/lib/html.js'
|
||||
|
||||
@@ -91,6 +91,28 @@ test('buildPageContract infers legacy table field labels and action semantics',
|
||||
assert.equal(contract.tables[1].role, 'resultTable')
|
||||
})
|
||||
|
||||
test('buildPageContract keeps page actions out of row actions', () => {
|
||||
const html = `
|
||||
<main>
|
||||
<input type="button" value="修改密碼" onclick="changePassword()">
|
||||
<input type="button" value="列印" onclick="window.print()">
|
||||
<table><tr><th>申請單號</th><th>操作</th></tr><tr><td>A001</td><td><input type="button" value="刪除"></td></tr></table>
|
||||
</main>
|
||||
`
|
||||
const contract = buildPageContract({
|
||||
page: 'applications-list.html',
|
||||
source: '/prototype/applications-list.html',
|
||||
html,
|
||||
regions: extractRegions(html),
|
||||
domSummary: summarizeHtml(html),
|
||||
screenshotPath: null
|
||||
})
|
||||
|
||||
assert.equal(contract.actions.find((action) => action.label === '修改密碼').scope, 'formAction')
|
||||
assert.equal(contract.actions.find((action) => action.label === '列印').scope, 'formAction')
|
||||
assert.equal(contract.actions.find((action) => action.label === '刪除').scope, 'rowAction')
|
||||
})
|
||||
|
||||
test('validatePageContract reports evidence mismatches', () => {
|
||||
const report = validatePageContract({
|
||||
textSamples: ['Missing'],
|
||||
@@ -138,6 +160,16 @@ test('buildAppMap enriches routes with prototype markdown guide entries', () =>
|
||||
| 雛型檔 | 舊 JSP 來源 | 舊 PB NVO 來源 | 對應 Vue view(M9) |
|
||||
| --- | --- | --- | --- |
|
||||
| [\`query-room.html\`](venue/query-room.html) | \`zte_pro/zte451_02.jsp\` + \`zte451_02_1.jsp\` | \`n_zte451.of_zte451_02\` / \`of_zte451_02_1\` | \`RoomQueryView.vue\` |
|
||||
|
||||
## 舊系統整體流程
|
||||
|
||||
\`\`\`text
|
||||
[使用者] 點 menu ZTE451
|
||||
→ zte451_choose.jsp
|
||||
① 場地查詢 → zte451_02.jsp → of_zte451_02 (場地下拉 + 日期)
|
||||
↓ 「查詢」
|
||||
zte451_02_1.jsp → of_zte451_02_1 (回 HTML:每日可借節次)
|
||||
\`\`\`
|
||||
`)
|
||||
const appMap = buildAppMapWithGuides([
|
||||
buildSpec('/repo/prototype/venue/query-room.html', '<main><h1>全校場地查詢</h1><button>查詢</button></main>')
|
||||
@@ -149,6 +181,11 @@ test('buildAppMap enriches routes with prototype markdown guide entries', () =>
|
||||
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')
|
||||
})
|
||||
|
||||
test('parsePrototypeGuide attaches checklist items to guide entries', () => {
|
||||
@@ -168,6 +205,33 @@ test('parsePrototypeGuide attaches checklist items to guide entries', () => {
|
||||
assert.deepEqual(guide.entries[0].checklist, ['活動名稱必填', '使用節次:14 個 checkbox'])
|
||||
})
|
||||
|
||||
test('parsePrototypeGuide extracts legacy flow references for matched entries', () => {
|
||||
const guide = parsePrototypeGuide('venue.md', `
|
||||
# Venue
|
||||
|
||||
| 雛型檔 | 舊 JSP 來源 | 舊 PB NVO 來源 |
|
||||
| --- | --- | --- |
|
||||
| [\`apply-room.html\`](venue/apply-room.html) | \`zte_pro/zte450_01.jsp\` + \`zte450_med01.jsp\` | \`n_zte450.of_zte450_01\` / \`of_zte450_med01\` |
|
||||
|
||||
## 舊系統整體流程
|
||||
|
||||
\`\`\`text
|
||||
[使用者] 點 menu ZTE450
|
||||
→ zte450_choose.jsp
|
||||
① 場地申請 → zte450_00.jsp?ls_stat=1 → of_zte450_00 (回 HTML 表單:身份+日期+stat)
|
||||
↓ 「我要借用」
|
||||
zte450_01.jsp → of_zte450_01 (回 HTML 表單:5 志願 + 14 節次)
|
||||
↓ 存檔
|
||||
zte450_ins.jsp → of_zte450_ins → 寫 s55_app_mst + s55_app_dtl
|
||||
\`\`\`
|
||||
`)
|
||||
|
||||
assert.equal(guide.legacyFlows.length, 1)
|
||||
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.jsp.includes('zte450_01.jsp')))
|
||||
})
|
||||
|
||||
test('buildApiCatalog parses generic markdown API docs and matches routes', () => {
|
||||
const catalog = buildApiCatalog([
|
||||
{
|
||||
@@ -225,6 +289,36 @@ test('buildApiCatalog parses generic markdown API docs and matches routes', () =
|
||||
assert.equal(matches[0].path, '/api/v1/orders')
|
||||
})
|
||||
|
||||
test('matchApiEndpointCandidates narrows chooser and print pages', () => {
|
||||
const catalog = buildApiCatalog([{
|
||||
source: 'apps/backend/API.md',
|
||||
markdown: `
|
||||
| 方法 | 路徑 | 說明 |
|
||||
| --- | --- | --- |
|
||||
| GET | \`/service/api/v1/venue/applications/rooms\` | 查詢我的場地申請 |
|
||||
| POST | \`/service/api/v1/venue/applications/rooms\` | 新增場地借用申請 |
|
||||
| GET | \`/service/api/v1/venue/applications/rooms/{appNo}/print-view\` | 取得場地申請列印資料 |
|
||||
| GET | \`/service/api/v1/venue/rooms\` | 取得場地清單 |
|
||||
`
|
||||
}])
|
||||
|
||||
const chooser = matchApiEndpointCandidates({
|
||||
prototype: 'venue/apply-choose.html',
|
||||
page: 'apply-choose.html',
|
||||
module: 'venue',
|
||||
evidence: { actions: ['場地申請'], textSamples: ['場地設備借用申請'] }
|
||||
}, catalog)
|
||||
const print = matchApiEndpointCandidates({
|
||||
prototype: 'venue/apply-room-print.html',
|
||||
page: 'apply-room-print.html',
|
||||
module: 'venue',
|
||||
evidence: { actions: ['列印'], textSamples: ['場地申請表列印'] }
|
||||
}, catalog)
|
||||
|
||||
assert.deepEqual(chooser.matches.map((endpoint) => endpoint.usage), ['lookup'])
|
||||
assert.deepEqual(print.matches.map((endpoint) => endpoint.usage), ['print'])
|
||||
})
|
||||
|
||||
test('buildMaintenanceContract recommends generic templates from evidence', () => {
|
||||
const spec = buildSpec('/repo/prototype/orders/apply.html', `
|
||||
<main>
|
||||
@@ -255,6 +349,66 @@ test('buildMaintenanceContract recommends generic templates from evidence', () =
|
||||
assert.equal(contract.dataModel.primaryEntity, 'OrderApply')
|
||||
})
|
||||
|
||||
test('buildMaintenanceContract extracts business rules and row action status rules', () => {
|
||||
const spec = buildSpec('/repo/prototype/venue/applications-list.html', `
|
||||
<main>
|
||||
<table>
|
||||
<tr><th>申請單號</th><th>狀態</th><th>操作</th></tr>
|
||||
<tr><td>A001</td><td>審核中(Z)</td><td><input type="button" value="修改"><input type="button" value="刪除"><input type="button" value="列印"></td></tr>
|
||||
<tr><td>A002</td><td>已核准(Y)</td><td>aprv≠'Z' 修改/刪除 disabled</td></tr>
|
||||
</table>
|
||||
</main>
|
||||
`)
|
||||
spec.prototypeGuide = {
|
||||
checklist: [
|
||||
"操作按鈕:修改 / 刪除 / 列印(aprv_yn!='Z' 時前兩者 disabled,舊系統 UI 用顯示與否)",
|
||||
'設備明細(最多 15 項):每行設備下拉 + 數量 + 移除',
|
||||
'截止日(asOfDate;不填用今天)',
|
||||
'JS 驗證:home_phone 不可空白、IsNumeric 檢查、email 含 @'
|
||||
]
|
||||
}
|
||||
const contract = buildMaintenanceContract({
|
||||
route: {
|
||||
kind: 'feature-page',
|
||||
prototype: 'venue/applications-list.html',
|
||||
page: 'applications-list.html',
|
||||
guide: { targetView: 'ApplicationsListView.vue' }
|
||||
},
|
||||
spec,
|
||||
apiMatches: []
|
||||
})
|
||||
|
||||
assert.equal(contract.rowActions.find((action) => action.actionType === 'edit').enabledWhen, "aprvYn === 'Z'")
|
||||
assert.equal(contract.rowActions.find((action) => action.actionType === 'delete').enabledWhen, "aprvYn === 'Z'")
|
||||
assert.equal(contract.rowActions.find((action) => action.actionType === 'print').enabledWhen, null)
|
||||
assert.equal(contract.businessRules.collectionLimits[0].max, 15)
|
||||
assert.ok(contract.businessRules.dataSources.some((rule) => rule.ruleType === 'default-today'))
|
||||
assert.ok(contract.businessRules.validationRules.some((rule) => rule.ruleType === 'email'))
|
||||
})
|
||||
|
||||
test('buildMaintenanceContract ignores non-vue guide text for primary entity', () => {
|
||||
const spec = buildSpec('/repo/prototype/venue/apply-room-print.html', `
|
||||
<main>
|
||||
<h1>場地申請表列印</h1>
|
||||
<input type="button" value="列印">
|
||||
</main>
|
||||
`)
|
||||
const contract = buildMaintenanceContract({
|
||||
route: {
|
||||
kind: 'feature-page',
|
||||
prototype: 'venue/apply-room-print.html',
|
||||
page: 'apply-room-print.html',
|
||||
guide: { targetView: 'M9 列印頁尚未實作,列出供下批補做' }
|
||||
},
|
||||
spec,
|
||||
apiMatches: [{ method: 'GET', path: '/service/api/v1/venue/applications/rooms/{appNo}/print-view', description: '取得場地申請列印資料' }]
|
||||
})
|
||||
|
||||
assert.equal(contract.pageKind, 'print')
|
||||
assert.equal(contract.dataModel.primaryEntity, 'PrintView')
|
||||
assert.deepEqual(contract.capabilities, ['print'])
|
||||
})
|
||||
|
||||
function buildSpec(source, html) {
|
||||
const regions = extractRegions(html)
|
||||
const domSummary = summarizeHtml(html)
|
||||
|
||||
Reference in New Issue
Block a user