import fs from 'node:fs' import path from 'node:path' const root=process.cwd();const failures=[] const read=p=>fs.readFileSync(path.join(root,p),'utf8') const json=p=>JSON.parse(read(p)) const must=(text,needles,scope)=>needles.forEach(needle=>{if(!text.includes(needle))failures.push(`${scope} missing ${needle}`)}) const recipes=json('contracts/screens/kbx.screen-recipes.json') if(recipes.recipes.length!==9)failures.push(`screen recipes expected 9, found ${recipes.recipes.length}`) const expected={T01:'list',T02:'master',T03:'transaction',T04:'fast-entry',T05:'master-detail',T06:'queue',T07:'reconcile',T08:'import',T09:'wms-mobile'} const scenarioIds=new Set(json('contracts/testing/kbx.test-scenarios.json').scenarios.map(item=>item.id)) for(const [code,type] of Object.entries(expected)){ const recipe=recipes.recipes.find(item=>item.code===code) if(!recipe)continue if(recipe.type!==type)failures.push(`${code} recipe type must be ${type}`) for(const key of ['templateComponent','defaultCommands','requiredPolicies','recoveryPolicies','securityPolicies','canonicalScenarioIds','scaffoldSurfaces'])if(!recipe[key]||!recipe[key].length&&key!=='defaultCommands')failures.push(`${code} recipe missing ${key}`) for(const scenarioId of recipe.canonicalScenarioIds)if(!scenarioIds.has(scenarioId))failures.push(`${code} recipe references unknown scenario ${scenarioId}`) } const screenContract=read('packages/kbx-contracts/src/screen.ts') must(screenContract,['KbxScreenTemplateCode','KbxScreenRecipeDefinition','templateCode: KbxScreenTemplateCode'],'screen recipe/type contract') const generator=read('scripts/generate-screen-recipe-contracts.mjs') must(generator,['generated/screen-recipe-manifest.json','screenRecipeCatalog.ts'],'screen recipe generator') const generated=json('generated/screen-recipe-manifest.json') if(generated.recipes.length!==9)failures.push('generated screen recipe manifest must contain 9 recipes') const screenManifest=json('generated/screen-manifest.json') for(const screen of screenManifest){ const recipe=recipes.recipes.find(item=>item.code===screen.templateCode) if(!recipe)failures.push(`${screen.id} missing/unknown templateCode ${screen.templateCode}`) else if(recipe.type!==screen.type)failures.push(`${screen.id} ${screen.templateCode}/${screen.type} mismatch`) } const frame=read('packages/kbx-ui/src/components/KbxScreenFrame.vue') must(frame,['KbxScreenTemplateCode','props.screen.templateCode!==props.templateCode'],'runtime templateCode guard') const wms=read('packages/kbx-ui/src/wms/KbxWmsMobilePage.vue') must(wms,["props.screen.templateCode!=='T09'"],'T09 runtime recipe guard') const scaffolder=read('scripts/create-kbx-screen.mjs') must(scaffolder,["contracts/screens/kbx.screen-recipes.json",'--write-permission is required','permissionKind===\'write\'',"templateCode: '${recipe.code}'",'canonical scenarios:'],'recipe-driven secure scaffolder') if(scaffolder.includes("const commandMap ="))failures.push('scaffolder must not keep a second hard-coded commandMap beside recipe contract') const navigation=read('packages/kbx-contracts/src/navigation.ts') must(navigation,['KbxHomeAttentionSource','KbxHomeAttentionItem','operationId?: string','notificationId?: string'],'Home attention contract') const homeNav=read('packages/kbx-ui/src/shell/homeNavigation.ts') must(homeNav,['buildKbxHomeAttention','operation-failed','notification-urgent','operation-running','allowedIds.has'],'Home attention orchestration') const home=read('packages/kbx-ui/src/shell/KbxHomePage.vue') must(home,['buildKbxHomeAttention',"@click=\"emit('attention',item)\""],'Home attention UI') if(home.includes(':operations="homeOperations"'))failures.push('HomePage must receive operations as props, not reference AppFrame state directly') must(home,['operations?:KbxOperationRun[]','notifications?:KbxUserNotification[]','kbx-home__attention-list','attention:[item:KbxHomeAttentionItem]'],'Home actionable workbench') const app=read('apps/web/src/shell/KbxAppFrame.vue') must(app,['homeOperations=computed','homeNotifications=computed','openHomeAttention','safeWorkspacePath(item.screenId,candidate)','알림에 포함된 허용되지 않은 이동 경로를 차단했습니다.',':operations="homeOperations"',':notifications="homeNotifications"','@attention="openHomeAttention"'],'Home runtime security orchestration') if(failures.length){console.error(failures.join('\n'));process.exit(1)} console.log(`v35 recipe/navigation validation passed: recipes=${recipes.recipes.length}, screens=${screenManifest.length}; explicit templateCode, canonical scenario closure, secure write scaffolding, and actionable Home attention routing are enforced.`)