Files
KArtSell.Aegis/docs/Design/kbx-foundation-v36/scripts/validate-scaffolder.mjs
T

69 lines
5.4 KiB
JavaScript

import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { spawnSync } from 'node:child_process'
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kbx-scaffold-'))
fs.mkdirSync(path.join(root, 'apps/web/src/help'), { recursive: true })
fs.writeFileSync(path.join(root, 'apps/web/src/help/helpRegistry.ts'), "import type { KbxHelpContent } from '@kbx/contracts'\nexport const helpRegistry: Record<string, KbxHelpContent> = {\n}\n")
const recipeContract=JSON.parse(fs.readFileSync('contracts/screens/kbx.screen-recipes.json','utf8'))
const recipeByType=new Map(recipeContract.recipes.map(recipe=>[recipe.type,recipe]))
const cases=[
['list','KbxListPage',['KbxSearchPanel','KbxDataGrid','KbxSummaryBar']],
['master','KbxMasterPage',['KbxDataGrid','KbxFormGrid','KbxFormSection']],
['transaction','KbxTransactionPage',['KbxDataGrid','KbxFormGrid','KbxSummaryBar']],
['fast-entry','KbxFastEntryPage',['KbxDataGrid','KbxValidationSummary','KbxSummaryBar']],
['master-detail','KbxMasterDetailPage',['KbxSearchPanel','KbxDataGrid']],
['queue','KbxQueuePage',['KbxExceptionSummary','KbxDataGrid','KbxSummaryBar']],
['reconcile','KbxReconcilePage',['KbxSearchPanel','KbxDataGrid']],
['import','KbxImportPage',['KbxProgressSteps','KbxExcelImport']],
['wms-mobile','KbxWmsMobilePage',['KbxBarcodeCapture','KbxWmsActionButton']],
]
for(const [index,[type,component,needles]] of cases.entries()){
const number=String(901+index)
const modulePath=`erp/scaffold/${type}`
const args=[
'scripts/create-kbx-screen.mjs','--module','ERP','--area','SCF','--number',number,'--type',type,
'--name',`스캐폴드 ${type}`,'--path',modulePath,'--permission','erp.item.read',
]
if(['master','transaction','fast-entry'].includes(type))args.push('--write-permission','erp.item.write')
const result = spawnSync(process.execPath,args,{ cwd: process.cwd(), env: { ...process.env, KBX_ROOT: root }, encoding: 'utf8' })
if (result.status !== 0) { console.error(result.stdout, result.stderr); process.exit(result.status ?? 1) }
const target=path.join(root,'apps/web/src/modules',modulePath)
const definition=fs.readdirSync(target).find(file=>file.endsWith('.definition.ts'))
const page=fs.readdirSync(target).find(file=>file.endsWith('Page.vue'))
if(!definition||!page)throw new Error(`scaffolder ${type} missing definition/page`)
const definitionText=fs.readFileSync(path.join(target,definition),'utf8')
const text=fs.readFileSync(path.join(target,page),'utf8')
const readme=fs.readFileSync(path.join(target,'README.md'),'utf8')
const testPlanFile=fs.readdirSync(target).find(file=>file.endsWith('.test-plan.ts'))
if(!testPlanFile)throw new Error(`scaffolder ${type} missing generated recipe test plan`)
const testPlan=fs.readFileSync(path.join(target,testPlanFile),'utf8')
const recipe=recipeByType.get(type)
if(!recipe)throw new Error(`recipe missing for ${type}`)
if(!definitionText.includes(`templateCode: '${recipe.code}'`))throw new Error(`scaffolder ${type} missing ${recipe.code}`)
if(!text.includes(component))throw new Error(`scaffolder ${type} must use ${component}`)
for(const needle of needles)if(!text.includes(needle))throw new Error(`scaffolder ${type} missing ${needle}`)
for(const scenario of recipe.canonicalScenarioIds){
if(!readme.includes(scenario))throw new Error(`scaffolder ${type} README missing canonical scenario ${scenario}`)
if(!testPlan.includes(scenario))throw new Error(`scaffolder ${type} test plan missing canonical scenario ${scenario}`)
}
for(const check of recipe.testProfile.requiredChecks)if(!testPlan.includes(check))throw new Error(`scaffolder ${type} test plan missing required check ${check}`)
for(const evidence of recipe.testProfile.requiredEvidence)if(!testPlan.includes(evidence))throw new Error(`scaffolder ${type} test plan missing evidence ${evidence}`)
if(/primevue\//.test(text)||/ag-grid-vue3/.test(text))throw new Error(`scaffolder ${type} leaks underlying UI libraries`)
if(['master','transaction','fast-entry'].includes(type)&&!/permission:'erp\.item\.write'/.test(definitionText))throw new Error(`scaffolder ${type} mutating commands must carry explicit write permission`)
}
const unsafeRoot=fs.mkdtempSync(path.join(os.tmpdir(),'kbx-scaffold-deny-'))
fs.mkdirSync(path.join(unsafeRoot,'apps/web/src/help'),{recursive:true})
fs.writeFileSync(path.join(unsafeRoot,'apps/web/src/help/helpRegistry.ts'),"export const helpRegistry = {}\n")
const denied=spawnSync(process.execPath,['scripts/create-kbx-screen.mjs','--module','ERP','--area','DEN','--number','999','--type','transaction','--name','권한 없는 거래','--path','erp/scaffold/deny','--permission','erp.item.read'],{cwd:process.cwd(),env:{...process.env,KBX_ROOT:unsafeRoot},encoding:'utf8'})
if(denied.status===0||!`${denied.stderr}${denied.stdout}`.includes('--write-permission is required'))throw new Error('scaffolder must fail closed when a mutating recipe has no explicit write permission')
fs.rmSync(unsafeRoot,{recursive:true,force:true})
const help = fs.readFileSync(path.join(root, 'apps/web/src/help/helpRegistry.ts'),'utf8')
if((help.match(/^ 'ERP-SCF-9\d\d':/gm)??[]).length!==9)throw new Error('scaffolder did not add all help registry stubs')
console.log('KBX scaffolder contract PASS (T01-T09 recipe-driven composition, generated test plans, canonical scenarios, templateCode, and fail-closed write permissions)')
fs.rmSync(root, { recursive: true, force: true })