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 must=(text,needles,scope)=>needles.forEach(needle=>{if(!text.includes(needle))failures.push(`${scope} missing ${needle}`)}) const lookupContract=read('packages/kbx-contracts/src/lookup.ts') must(lookupContract,['KbxLookupColumnDefinition','metadata.${string}','columns?: KbxLookupColumnDefinition[]','pageSize?: number'],'lookup presentation contract') const lookup=read('packages/kbx-ui/src/components/KbxLookup.vue') must(lookup,['resolveSequence','조회 공급자가 구성되지 않았습니다.','선택 정보를 불러오지 못했습니다.','코드를 확인하지 못했습니다.',':columns="columns"',':page-size="pageSize"'],'lookup async guard') const lookupDialog=read('packages/kbx-ui/src/components/KbxLookupDialog.vue') must(lookupDialog,["import KbxDialog from './KbxDialog.vue'",'requestSequence','pageCount','조회하지 못했습니다.','조회된 항목이 없습니다.','aria-selected','scrollSelectedIntoView'],'lookup dialog maturity') if(/primevue\//.test(lookupDialog))failures.push('KbxLookupDialog must compose KBX primitives instead of importing PrimeVue directly') const excelContract=read('packages/kbx-contracts/src/excel.ts') must(excelContract,['export interface KbxImportFailure','failure?: KbxImportFailure'],'import failure contract') const importGuard=read('packages/kbx-ui/src/excel/importGuard.ts') must(importGuard,['validateKbxImportMappings','duplicate-target','required-missing','validateKbxImportFileCandidate',"endsWith('.xlsx')"],'import guard') const excel=read('packages/kbx-ui/src/components/KbxExcelImport.vue') must(excel,['localMapping = ref(null)','watch(() => props.session?.id','mappingIssues','KbxConfirm','requestCommit','isFailed','isCancelled','isPartial','새 파일로 다시 시작'],'Excel import recovery/mapping maturity') const operations=read('packages/kbx-contracts/src/operations.ts') must(operations,["permissionMode?: 'hide' | 'disable'"],'exception permission UX contract') const exceptionDrawer=read('packages/kbx-ui/src/components/KbxExceptionDetailDrawer.vue') must(exceptionDrawer,['KbxPermissionHostKey','KbxDrawer','permissionMode===\'disable\'','이 작업을 실행할 권한이 없습니다.','data-kbx-component="exception-detail-drawer"'],'exception drawer hardening') const aiContract=read('packages/kbx-contracts/src/ai.ts') must(aiContract,['KbxAiProposalValidationState','validation?: KbxAiProposalValidation','KbxAiAnswerAction','requiredCapability?: KbxAiCapability','requiredPermission?: string'],'AI action/validation contract') const assistant=read('packages/kbx-ui/src/components/KbxAiAssistant.vue') must(assistant,['KbxPermissionHostKey','actionAllowed','proposalAllowed','lastSubmitted','다시 질문','currentScreenLabel','현재 사용자 권한 또는 AI 허용 범위를 벗어난'],'AI assistant guard/recovery') if(assistant.includes('{{ context.screenId }}'))failures.push('AI assistant must not expose ScreenId as the default user-facing current-screen label') const proposal=read('packages/kbx-ui/src/components/KbxProposalPanel.vue') must(proposal,['KbxPermissionHostKey','permissionAllowed','validationAllowed','data-validation','서버에서 대상·권한·업무규칙','대상 {{proposal.targets.length.toLocaleString()}}건'],'AI proposal enforcement') const appFrame=read('apps/web/src/shell/KbxAppFrame.vue') must(appFrame,['utilityAiError','네트워크 상태를 확인한 후 다시 질문하세요.',':ai-error="utilityAiError"',':ai-current-screen-label="utilityScreen?.title"'],'AI shell error recovery') const barcodeGuard=read('packages/kbx-ui/src/wms/barcodeGuard.ts') must(barcodeGuard,['normalizeKbxBarcode','isKbxBarcodeDuplicate'],'barcode guard') const barcode=read('packages/kbx-ui/src/wms/KbxBarcodeCapture.vue') must(barcode,['maxLength?: number','duplicateDebounceMs?: number','duplicateIgnored','isKbxBarcodeDuplicate','defineExpose({submitCamera,reset})','visibilitychange'],'barcode capture hardening') const home=read('packages/kbx-ui/src/shell/KbxHomePage.vue') must(home,['regularOpenCount','open:regularOpenCount.value','watch(modules','visibleScreenCount','aria-live="polite"'],'Home navigation resilience') const templateManifest=read('packages/kbx-ui/src/registry/templateManifest.ts') const componentManifest=read('packages/kbx-ui/src/registry/componentManifest.ts') const catalog=read('packages/kbx-ui/src/catalog/componentCatalog.ts') const coreGroups=[...templateManifest.matchAll(/coreComponents:\[([^\]]+)\]/g)] const coreComponents=new Set(coreGroups.flatMap(match=>[...match[1].matchAll(/'([^']+)'/g)].map(item=>item[1]))) const manifestNames=new Set([...componentManifest.matchAll(/name: '([^']+)'/g)].map(match=>match[1])) const catalogNames=new Set([...catalog.matchAll(/component:'([^']+)'/g)].map(match=>match[1])) for(const component of coreComponents){ if(!manifestNames.has(component))failures.push(`template core component missing manifest: ${component}`) if(!catalogNames.has(component))failures.push(`template core component missing catalog scenarios: ${component}`) } for(const component of ['KbxLookupDialog','KbxValidationSummary','KbxExceptionSummary','KbxExceptionDetailDrawer','KbxRecordLifecycle','KbxBarcodeCapture','KbxNetworkIndicator','KbxWmsActionButton']){ if(!catalogNames.has(component))failures.push(`v34 required catalog entry missing: ${component}`) } must(componentManifest,["KbxLookup', category: 'business', purpose: '비동기 race/error", "KbxLookupDialog', category: 'business', purpose: 'Provider 오류", "KbxExcelImport', category: 'business', purpose: '파일 preflight", "KbxExceptionDetailDrawer', category: 'business', purpose: '표준 Drawer", "KbxAiAssistant', category: 'business', purpose: 'Capability/Permission guard", "KbxBarcodeCapture', category: 'wms', purpose: 'Keyboard-wedge/Camera", "KbxHomePage', category: 'shell', purpose: '미저장·실패 작업"],'v34 component versions/descriptions') if(failures.length){console.error(failures.join('\n'));process.exit(1)} console.log(`v34 template/component/home validation passed: ${coreComponents.size} template core components have manifest+catalog coverage; Lookup/Import/Exception/AI/Barcode recovery and permission boundaries hardened; Home counts/filter recovery corrected.`)