69 lines
5.4 KiB
JavaScript
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 })
|