SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。 == 此快照内容 == - 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091) - 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static) - 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB) - 交接文档 go.md + go-code-reference/endpoints/entities/database.md - 多代理建设脚本 .claude/wf-*.js == 状态 == - 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板) - 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用 - W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成) - 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456 == 排除(gitignore, 可再生) == node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui) 完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules) 时间戳: 20260615-191517 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,477 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { access, mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { constants } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const scriptPath = fileURLToPath(import.meta.url)
|
||||
const appRoot = path.resolve(path.dirname(scriptPath), '..')
|
||||
const repoRoot = path.resolve(appRoot, '../../..')
|
||||
const inventoryFile = path.join(repoRoot, 'plugins/modern-api/generated/ui-inventory.json')
|
||||
const publicInventoryFile = path.join(appRoot, 'public/generated/ui-inventory.json')
|
||||
const publicPagesDir = path.join(appRoot, 'public/generated/pages')
|
||||
const builtInventoryFile = path.join(repoRoot, 'plugins/modern-ui/webapp/modern/app/generated/ui-inventory.json')
|
||||
const builtPagesDir = path.join(repoRoot, 'plugins/modern-ui/webapp/modern/app/generated/pages')
|
||||
const outDir = path.join(repoRoot, 'plugins/modern-ui/verification')
|
||||
const outJson = path.join(outDir, 'coverage-verification.json')
|
||||
const outMd = path.join(outDir, 'coverage-verification.md')
|
||||
|
||||
const supportedBlockTypes = new Set([
|
||||
'legacy-screen',
|
||||
'section',
|
||||
'form',
|
||||
'table',
|
||||
'actions',
|
||||
'menu',
|
||||
'links',
|
||||
'permission',
|
||||
'client-behavior',
|
||||
'report',
|
||||
'search-workspace',
|
||||
'entity-editor',
|
||||
'domain-workspace',
|
||||
'tree-workspace',
|
||||
'calendar',
|
||||
'lookup-workspace',
|
||||
'commerce-surface',
|
||||
'pos-workspace',
|
||||
'route-workspace',
|
||||
'template-adapter',
|
||||
'html-template',
|
||||
'empty'
|
||||
])
|
||||
|
||||
function passed(id, label, actual = '') {
|
||||
return { id, label, status: 'passed', actual }
|
||||
}
|
||||
|
||||
function failed(id, label, expected, actual, samples = []) {
|
||||
return { id, label, status: 'failed', expected, actual, samples: samples.slice(0, 25) }
|
||||
}
|
||||
|
||||
function incomplete(id, label, expected, actual, samples = []) {
|
||||
return { id, label, status: 'incomplete', expected, actual, samples: samples.slice(0, 25) }
|
||||
}
|
||||
|
||||
function skipped(id, label, actual = '') {
|
||||
return { id, label, status: 'skipped', actual }
|
||||
}
|
||||
|
||||
function countBy(items, getKey) {
|
||||
const counts = {}
|
||||
for (const item of items) {
|
||||
const key = getKey(item) || 'unknown'
|
||||
counts[key] = (counts[key] || 0) + 1
|
||||
}
|
||||
return Object.fromEntries(Object.entries(counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])))
|
||||
}
|
||||
|
||||
async function exists(file) {
|
||||
try {
|
||||
await access(file, constants.R_OK)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function readJson(file) {
|
||||
return JSON.parse(await readFile(file, 'utf8'))
|
||||
}
|
||||
|
||||
async function listJsonFiles(dir) {
|
||||
try {
|
||||
return (await readdir(dir)).filter((file) => file.endsWith('.json'))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function routePageUrl(route) {
|
||||
return String(route.pageDefinitionUrl || '')
|
||||
}
|
||||
|
||||
function pageFileForRoute(route, pagesDir) {
|
||||
const url = routePageUrl(route)
|
||||
if (!url) return ''
|
||||
return path.join(pagesDir, path.basename(url))
|
||||
}
|
||||
|
||||
function isBlockSupported(type = '') {
|
||||
return supportedBlockTypes.has(type) || type.endsWith('-workspace')
|
||||
}
|
||||
|
||||
function actionIdOf(action) {
|
||||
return typeof action === 'string' ? action : action?.actionId
|
||||
}
|
||||
|
||||
function samplePage(page) {
|
||||
return {
|
||||
pageId: page.pageId,
|
||||
title: page.title,
|
||||
legacyPath: page.legacy?.path || '',
|
||||
domain: page.domain || '',
|
||||
status: page.acceptance?.status || ''
|
||||
}
|
||||
}
|
||||
|
||||
function actionReferenceSamples(pages, actionDefinitions) {
|
||||
const samples = []
|
||||
for (const page of pages) {
|
||||
for (const action of page.actions || []) {
|
||||
const actionId = actionIdOf(action)
|
||||
if (actionId && !actionDefinitions[actionId]) {
|
||||
samples.push({ pageId: page.pageId, actionId, source: 'page.actions' })
|
||||
}
|
||||
}
|
||||
for (const block of page.blocks || []) {
|
||||
const submitAction = block.submitAction
|
||||
const actionId = submitAction?.actionId
|
||||
const status = String(submitAction?.status || '')
|
||||
const mustResolve = submitAction?.apiExecutable
|
||||
&& !['navigation-target', 'navigation-action', 'local-submit-contract', 'local-draft-contract', 'readonly-display', 'dynamic-target', 'unmapped-target-contract'].includes(status)
|
||||
if (mustResolve && actionId && !actionDefinitions[actionId]) {
|
||||
samples.push({ pageId: page.pageId, actionId, formName: block.formName, source: 'block.submitAction' })
|
||||
}
|
||||
}
|
||||
}
|
||||
return samples
|
||||
}
|
||||
|
||||
function routeDuplicates(routes) {
|
||||
const byPage = new Map()
|
||||
for (const route of routes) {
|
||||
const items = byPage.get(route.pageId) || []
|
||||
items.push(route)
|
||||
byPage.set(route.pageId, items)
|
||||
}
|
||||
return [...byPage.entries()]
|
||||
.filter(([, items]) => items.length > 1)
|
||||
.map(([pageId, items]) => ({
|
||||
pageId,
|
||||
count: items.length,
|
||||
legacyPaths: items.map((item) => item.legacyPath).filter(Boolean)
|
||||
}))
|
||||
}
|
||||
|
||||
async function verifySplitInventory(label, inventoryPath, pagesDir, sourceInventory, checks) {
|
||||
if (!await exists(inventoryPath)) {
|
||||
checks.push(skipped(`${label}-inventory`, `${label} split inventory exists`, 'not generated yet'))
|
||||
return
|
||||
}
|
||||
|
||||
const splitInventory = await readJson(inventoryPath)
|
||||
const pageCount = Object.keys(sourceInventory.pageDefinitions || {}).length
|
||||
const pageFiles = await listJsonFiles(pagesDir)
|
||||
const routeManifest = splitInventory.routeManifest || []
|
||||
const missingUrls = routeManifest.filter((route) => !routePageUrl(route))
|
||||
const embeddedPageDefinitions = Object.keys(splitInventory.pageDefinitions || {}).length
|
||||
const embeddedActionDefinitions = Object.keys(splitInventory.actionDefinitions || {}).length
|
||||
const missingFiles = []
|
||||
|
||||
if (pageFiles) {
|
||||
const pageFileSet = new Set(pageFiles)
|
||||
for (const route of routeManifest) {
|
||||
const pageFile = pageFileForRoute(route, pagesDir)
|
||||
if (pageFile && !pageFileSet.has(path.basename(pageFile))) {
|
||||
missingFiles.push({ pageId: route.pageId, pageDefinitionUrl: route.pageDefinitionUrl })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checks.push(routeManifest.length === (sourceInventory.routeManifest || []).length
|
||||
? passed(`${label}-route-count`, `${label} route manifest mirrors generated inventory`, routeManifest.length)
|
||||
: failed(`${label}-route-count`, `${label} route manifest mirrors generated inventory`, (sourceInventory.routeManifest || []).length, routeManifest.length))
|
||||
checks.push(pageFiles?.length === pageCount
|
||||
? passed(`${label}-page-file-count`, `${label} split PageDefinition file count`, pageFiles.length)
|
||||
: failed(`${label}-page-file-count`, `${label} split PageDefinition file count`, pageCount, pageFiles?.length ?? 'missing pages dir'))
|
||||
checks.push(embeddedPageDefinitions === 0 && embeddedActionDefinitions === 0
|
||||
? passed(`${label}-light-index`, `${label} inventory index stays lightweight`, 0)
|
||||
: failed(`${label}-light-index`, `${label} inventory index stays lightweight`, 0, embeddedPageDefinitions + embeddedActionDefinitions))
|
||||
checks.push(missingUrls.length === 0
|
||||
? passed(`${label}-page-definition-urls`, `${label} every route points to a split PageDefinition`, routeManifest.length)
|
||||
: failed(`${label}-page-definition-urls`, `${label} every route points to a split PageDefinition`, 0, missingUrls.length, missingUrls))
|
||||
checks.push(missingFiles.length === 0
|
||||
? passed(`${label}-page-files-resolve`, `${label} every route PageDefinition URL resolves to a file`, routeManifest.length)
|
||||
: failed(`${label}-page-files-resolve`, `${label} every route PageDefinition URL resolves to a file`, 0, missingFiles.length, missingFiles))
|
||||
}
|
||||
|
||||
const inventory = await readJson(inventoryFile)
|
||||
const routes = inventory.routeManifest || []
|
||||
const pagesById = inventory.pageDefinitions || {}
|
||||
const pages = Object.values(pagesById)
|
||||
const actionDefinitions = inventory.actionDefinitions || {}
|
||||
const counts = inventory.counts || {}
|
||||
const coverage = inventory.coverage || {}
|
||||
const checks = []
|
||||
|
||||
const missingPageDefinitions = routes.filter((route) => !pagesById[route.pageId])
|
||||
const pagesWithoutRoutes = pages.filter((page) => !routes.some((route) => route.pageId === page.pageId))
|
||||
const duplicateRoutes = routeDuplicates(routes)
|
||||
const unsupportedBlocks = []
|
||||
const pagesMissingBasics = []
|
||||
const pagesMissingAcceptance = []
|
||||
const pagesMissingPermissions = []
|
||||
const pagesMissingApiContract = []
|
||||
const pagesPendingBusinessE2e = []
|
||||
const pagesNotBusinessVerified = []
|
||||
const pagesMissingBlocks = []
|
||||
|
||||
for (const page of pages) {
|
||||
const blocks = page.blocks || []
|
||||
const acceptance = page.acceptance || {}
|
||||
const scenario = acceptance.e2eScenario || {}
|
||||
if (!page.pageId || !page.title || !page.layout || !page.legacy?.path || !page.legacy?.controller) {
|
||||
pagesMissingBasics.push(samplePage(page))
|
||||
}
|
||||
if (!blocks.length) {
|
||||
pagesMissingBlocks.push(samplePage(page))
|
||||
}
|
||||
for (const block of blocks) {
|
||||
if (!isBlockSupported(block.type)) {
|
||||
unsupportedBlocks.push({ pageId: page.pageId, blockType: block.type, title: block.title || '' })
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(page.permissions) || page.permissions.length === 0) {
|
||||
pagesMissingPermissions.push(samplePage(page))
|
||||
}
|
||||
if (!acceptance.scenarioId || !Array.isArray(scenario.steps) || scenario.steps.length === 0 || !Array.isArray(scenario.assertions) || scenario.assertions.length === 0) {
|
||||
pagesMissingAcceptance.push(samplePage(page))
|
||||
}
|
||||
if (!Array.isArray(scenario.apiContracts) || !scenario.apiContracts.some((contract) => contract.method === 'GET' && String(contract.path || '').startsWith('/api/v1/pages/'))) {
|
||||
pagesMissingApiContract.push(samplePage(page))
|
||||
}
|
||||
if (acceptance.scenarioStatus !== 'ready-for-business-e2e' && acceptance.businessParityStatus !== 'verified') {
|
||||
pagesPendingBusinessE2e.push(samplePage(page))
|
||||
}
|
||||
if (acceptance.businessParityStatus !== 'verified') {
|
||||
pagesNotBusinessVerified.push(samplePage(page))
|
||||
}
|
||||
}
|
||||
|
||||
const requestActions = []
|
||||
for (const controller of inventory.controllers || []) {
|
||||
for (const request of controller.requests || []) {
|
||||
if (request.actionId) requestActions.push({ ...request, controller: controller.file, webapp: controller.webapp?.name || '' })
|
||||
}
|
||||
}
|
||||
const missingRequestActions = requestActions.filter((request) => !actionDefinitions[request.actionId])
|
||||
const missingActionReferences = actionReferenceSamples(pages, actionDefinitions)
|
||||
const formContractCounts = coverage.formContractStatusCounts || {}
|
||||
const explicitNonExecutableContracts = [
|
||||
'readonly-display',
|
||||
'local-submit-contract',
|
||||
'local-draft-contract',
|
||||
'dynamic-target',
|
||||
'navigation-target',
|
||||
'unmapped-target-contract'
|
||||
].reduce((sum, key) => sum + (formContractCounts[key] || 0), 0)
|
||||
|
||||
checks.push(routes.length === counts.viewRoute
|
||||
? passed('view-route-count', 'generated route count matches scanned legacy view-map count', routes.length)
|
||||
: failed('view-route-count', 'generated route count matches scanned legacy view-map count', counts.viewRoute, routes.length))
|
||||
checks.push(pages.length === counts.pageDefinition
|
||||
? passed('page-definition-count', 'generated PageDefinition count matches inventory count', pages.length)
|
||||
: failed('page-definition-count', 'generated PageDefinition count matches inventory count', counts.pageDefinition, pages.length))
|
||||
checks.push(missingPageDefinitions.length === 0
|
||||
? passed('missing-page-definitions', 'every legacy view route has a PageDefinition', 0)
|
||||
: failed('missing-page-definitions', 'every legacy view route has a PageDefinition', 0, missingPageDefinitions.length, missingPageDefinitions))
|
||||
checks.push(pagesWithoutRoutes.length === 0
|
||||
? passed('orphan-page-definitions', 'every PageDefinition is reachable through a generated route', 0)
|
||||
: failed('orphan-page-definitions', 'every PageDefinition is reachable through a generated route', 0, pagesWithoutRoutes.length, pagesWithoutRoutes.map(samplePage)))
|
||||
checks.push(coverage.missingRoutes === 0
|
||||
? passed('inventory-missing-routes', 'inventory reports missingRoutes=0', coverage.missingRoutes)
|
||||
: failed('inventory-missing-routes', 'inventory reports missingRoutes=0', 0, coverage.missingRoutes))
|
||||
checks.push(coverage.missingActions === 0
|
||||
? passed('inventory-missing-actions', 'inventory reports missingActions=0', coverage.missingActions)
|
||||
: failed('inventory-missing-actions', 'inventory reports missingActions=0', 0, coverage.missingActions))
|
||||
checks.push(missingRequestActions.length === 0
|
||||
? passed('request-action-definitions', 'every legacy request-map has an ActionDefinition', requestActions.length)
|
||||
: failed('request-action-definitions', 'every legacy request-map has an ActionDefinition', 0, missingRequestActions.length, missingRequestActions))
|
||||
checks.push(missingActionReferences.length === 0
|
||||
? passed('page-action-references', 'every PageDefinition action reference resolves', pages.length)
|
||||
: failed('page-action-references', 'every PageDefinition action reference resolves', 0, missingActionReferences.length, missingActionReferences))
|
||||
checks.push(coverage.formMissingContractBlocks === 0
|
||||
? passed('form-contracts', 'every generated form block has an explicit submit/read contract', coverage.formContractBlocks || 0)
|
||||
: failed('form-contracts', 'every generated form block has an explicit submit/read contract', 0, coverage.formMissingContractBlocks))
|
||||
checks.push(pagesMissingBasics.length === 0
|
||||
? passed('page-definition-basics', 'every PageDefinition has title, layout and legacy source trace', pages.length)
|
||||
: failed('page-definition-basics', 'every PageDefinition has title, layout and legacy source trace', 0, pagesMissingBasics.length, pagesMissingBasics))
|
||||
checks.push(pagesMissingBlocks.length === 0
|
||||
? passed('page-blocks', 'every PageDefinition has renderable blocks', pages.length)
|
||||
: failed('page-blocks', 'every PageDefinition has renderable blocks', 0, pagesMissingBlocks.length, pagesMissingBlocks))
|
||||
checks.push(unsupportedBlocks.length === 0
|
||||
? passed('renderer-block-types', 'every generated block type is supported by the modern renderer layer', pages.length)
|
||||
: failed('renderer-block-types', 'every generated block type is supported by the modern renderer layer', 0, unsupportedBlocks.length, unsupportedBlocks))
|
||||
checks.push(pagesMissingPermissions.length === 0
|
||||
? passed('permissions', 'every PageDefinition carries mapped OFBiz permissions', pages.length)
|
||||
: failed('permissions', 'every PageDefinition carries mapped OFBiz permissions', 0, pagesMissingPermissions.length, pagesMissingPermissions))
|
||||
checks.push(pagesMissingAcceptance.length === 0
|
||||
? passed('acceptance-scenarios', 'every PageDefinition has an acceptance scenario', pages.length)
|
||||
: failed('acceptance-scenarios', 'every PageDefinition has an acceptance scenario', 0, pagesMissingAcceptance.length, pagesMissingAcceptance))
|
||||
checks.push(pagesMissingApiContract.length === 0
|
||||
? passed('page-api-contracts', 'every PageDefinition has a GET /api/v1/pages/:pageId contract', pages.length)
|
||||
: failed('page-api-contracts', 'every PageDefinition has a GET /api/v1/pages/:pageId contract', 0, pagesMissingApiContract.length, pagesMissingApiContract))
|
||||
checks.push(pagesPendingBusinessE2e.length === 0
|
||||
? passed('business-e2e-readiness', 'every page is ready for or has passed business e2e', pages.length)
|
||||
: incomplete('business-e2e-readiness', 'pages still need domain business e2e execution before final rewrite completion', 0, pagesPendingBusinessE2e.length, pagesPendingBusinessE2e))
|
||||
checks.push(pagesNotBusinessVerified.length === 0
|
||||
? passed('business-parity-complete', 'every page has verified old/new business parity', pages.length)
|
||||
: incomplete('business-parity-complete', 'full OFBiz functional parity is not complete until businessParityStatus is verified for every page', 0, pagesNotBusinessVerified.length, pagesNotBusinessVerified))
|
||||
|
||||
await verifySplitInventory('public', publicInventoryFile, publicPagesDir, inventory, checks)
|
||||
await verifySplitInventory('built', builtInventoryFile, builtPagesDir, inventory, checks)
|
||||
|
||||
const failures = checks.filter((check) => check.status === 'failed')
|
||||
const incompletes = checks.filter((check) => check.status === 'incomplete')
|
||||
const byDomain = countBy(pages, (page) => page.domain)
|
||||
const byLayout = countBy(pages, (page) => page.layout)
|
||||
const byBusinessParityStatus = countBy(pages, (page) => page.acceptance?.businessParityStatus)
|
||||
const byScenarioStatus = countBy(pages, (page) => page.acceptance?.scenarioStatus)
|
||||
|
||||
const report = {
|
||||
status: failures.length ? 'failed' : incompletes.length ? 'structural-passed-business-incomplete' : 'passed',
|
||||
generatedAt: new Date().toISOString(),
|
||||
inventoryFile,
|
||||
verificationBoundary: {
|
||||
frontendRewriteCoverageGate: failures.length === 0 ? 'passed' : 'failed',
|
||||
businessDepthParityGate: pagesNotBusinessVerified.length === 0 ? 'passed' : 'pending',
|
||||
finalGate: failures.length === 0 && pagesNotBusinessVerified.length === 0
|
||||
? 'full-business-parity-complete'
|
||||
: failures.length === 0
|
||||
? 'frontend-coverage-passed-business-depth-parity-pending'
|
||||
: 'frontend-coverage-failed',
|
||||
pendingBusinessParityPages: pagesNotBusinessVerified.length,
|
||||
pendingBusinessE2ePages: pagesPendingBusinessE2e.length,
|
||||
reportPath: outJson
|
||||
},
|
||||
summary: {
|
||||
controllers: counts.controllerXml || 0,
|
||||
widgetXml: counts.widgetXml || 0,
|
||||
legacyViewRoutes: counts.viewRoute || 0,
|
||||
routeManifest: routes.length,
|
||||
uniquePageDefinitions: pages.length,
|
||||
duplicateLegacyRouteAliases: duplicateRoutes.length,
|
||||
requestActions: requestActions.length,
|
||||
actionDefinitions: Object.keys(actionDefinitions).length,
|
||||
serviceDefinitions: counts.service || 0,
|
||||
renderedPages: coverage.renderablePages || 0,
|
||||
structuredWidgetPages: coverage.structuredPages || 0,
|
||||
adapterCoveredPages: coverage.adapterCoveredPages || 0,
|
||||
pendingBusinessE2ePages: coverage.pendingE2ePages ?? pagesPendingBusinessE2e.length,
|
||||
verifiedBusinessParityPages: byBusinessParityStatus.verified || 0,
|
||||
explicitNonExecutableFormContracts: explicitNonExecutableContracts,
|
||||
tableBlocks: coverage.tableBlocks || 0,
|
||||
tableDataSourceBlocks: coverage.tableDataSourceBlocks || 0,
|
||||
formContractBlocks: coverage.formContractBlocks || 0
|
||||
},
|
||||
checks,
|
||||
routeAliases: duplicateRoutes,
|
||||
distributions: {
|
||||
byDomain,
|
||||
byLayout,
|
||||
byBusinessParityStatus,
|
||||
byScenarioStatus,
|
||||
formContractStatusCounts: formContractCounts,
|
||||
blockTypeCounts: coverage.blockTypeCounts || {}
|
||||
},
|
||||
completionVerdict: {
|
||||
structuralCoverageReady: failures.length === 0,
|
||||
frontendRewriteCoverageGate: failures.length === 0,
|
||||
fullBusinessParityVerified: pagesNotBusinessVerified.length === 0,
|
||||
canClaimFrontendRewriteCoverageComplete: failures.length === 0,
|
||||
canClaimFullBusinessParityComplete: failures.length === 0 && pagesNotBusinessVerified.length === 0,
|
||||
mustNotClaim100PercentBusinessParity: pagesNotBusinessVerified.length > 0,
|
||||
pendingBusinessParityPages: pagesNotBusinessVerified.length,
|
||||
reason: pagesNotBusinessVerified.length === 0
|
||||
? 'All generated legacy pages have verified business parity.'
|
||||
: `Frontend rewrite coverage may pass independently, but ${pagesNotBusinessVerified.length} generated legacy pages still have pending business parity verification. Do not claim 100% full OFBiz business parity.`
|
||||
}
|
||||
}
|
||||
|
||||
function markdownTable(rows) {
|
||||
return [
|
||||
'| Check | Status | Actual |',
|
||||
'| --- | --- | --- |',
|
||||
...rows.map((check) => `| ${check.label.replaceAll('|', '\\|')} | ${check.status} | ${String(check.actual ?? '').replaceAll('|', '\\|')} |`)
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
const md = [
|
||||
'# Modern UI Coverage Verification',
|
||||
'',
|
||||
`Generated: ${report.generatedAt}`,
|
||||
'',
|
||||
`Status: ${report.status}`,
|
||||
'',
|
||||
'## Summary',
|
||||
'',
|
||||
`- Legacy view routes: ${report.summary.legacyViewRoutes}`,
|
||||
`- Route manifest entries: ${report.summary.routeManifest}`,
|
||||
`- Unique PageDefinitions: ${report.summary.uniquePageDefinitions}`,
|
||||
`- Duplicate route aliases: ${report.summary.duplicateLegacyRouteAliases}`,
|
||||
`- Legacy request actions: ${report.summary.requestActions}`,
|
||||
`- ActionDefinitions: ${report.summary.actionDefinitions}`,
|
||||
`- Rendered pages: ${report.summary.renderedPages}`,
|
||||
`- Adapter-covered pages: ${report.summary.adapterCoveredPages}`,
|
||||
`- Pending business e2e pages: ${report.summary.pendingBusinessE2ePages}`,
|
||||
`- Verified business parity pages: ${report.summary.verifiedBusinessParityPages}`,
|
||||
`- Pending business parity pages: ${report.verificationBoundary.pendingBusinessParityPages}`,
|
||||
'',
|
||||
'## Checks',
|
||||
'',
|
||||
markdownTable(checks),
|
||||
'',
|
||||
'## Completion Verdict',
|
||||
'',
|
||||
`- Structural coverage ready: ${report.completionVerdict.structuralCoverageReady}`,
|
||||
`- Frontend rewrite coverage gate: ${report.verificationBoundary.frontendRewriteCoverageGate}`,
|
||||
`- Business depth parity gate: ${report.verificationBoundary.businessDepthParityGate}`,
|
||||
`- Final gate: ${report.verificationBoundary.finalGate}`,
|
||||
`- Full business parity verified: ${report.completionVerdict.fullBusinessParityVerified}`,
|
||||
`- Can claim frontend rewrite coverage complete: ${report.completionVerdict.canClaimFrontendRewriteCoverageComplete}`,
|
||||
`- Can claim full business parity complete: ${report.completionVerdict.canClaimFullBusinessParityComplete}`,
|
||||
`- Must not claim 100% business parity: ${report.completionVerdict.mustNotClaim100PercentBusinessParity}`,
|
||||
`- Reason: ${report.completionVerdict.reason}`,
|
||||
''
|
||||
].join('\n')
|
||||
|
||||
await mkdir(outDir, { recursive: true })
|
||||
await writeFile(outJson, JSON.stringify(report, null, 2))
|
||||
await writeFile(outMd, md)
|
||||
|
||||
console.log(JSON.stringify({
|
||||
status: report.status,
|
||||
verificationBoundary: report.verificationBoundary,
|
||||
summary: report.summary,
|
||||
failedChecks: failures.map((check) => check.id),
|
||||
incompleteChecks: incompletes.map((check) => check.id),
|
||||
reports: {
|
||||
json: outJson,
|
||||
markdown: outMd
|
||||
},
|
||||
completionVerdict: report.completionVerdict
|
||||
}, null, 2))
|
||||
|
||||
if (failures.length) {
|
||||
process.exitCode = 1
|
||||
}
|
||||
Reference in New Issue
Block a user