import fs from 'node:fs' import path from 'node:path' const read=file=>fs.readFileSync(file,'utf8') const expect=(condition,message)=>{if(!condition){console.error(`FAIL: ${message}`);process.exit(1)}console.log(`PASS: ${message}`)} const app=read('apps/web/src/App.vue') expect(/app-version="v(?:5[8-9]|[6-9][0-9])-/.test(app),'Runtime metadata identifies v58 or a later workflow-compatible build') const claims=read('apps/web/src/modules/oms/claims/ClaimsPage.vue') const claimDefinition=read('apps/web/src/modules/oms/claims/claims.definition.ts') expect(!claims.includes('v-model:selection'),'OMS claims removes the invalid grid selection v-model contract') expect(claims.includes(':selection-count="selected.length"')&&claims.includes('@selection-changed="selected=$event"'),'OMS claims passes real selection count to CommandBar and consumes KbxDataGrid selectionChanged') expect(claims.includes("allowedTransitions=new Set(Object.keys(actionLabel))")&&claims.includes("actionLabel:Record={approve:'승인',hold:'보류',start:'처리 시작',complete:'처리 완료'}"),'OMS claims connects every visible workflow transition to a real API action') expect(claims.includes('for(const row of targets)')&&claims.includes('catch(cause)')&&claims.includes('failed:failures.length'),'OMS claims bulk transition continues through per-row failures and records partial outcome') expect(claims.includes('data-kbx-surface="claim-action-receipt"')&&claims.includes('receiptEl.value?.focus'),'OMS claims leaves a persistent focusable authoritative action receipt') expect(claimDefinition.includes("{value:'HOLD',label:'보류',semantic:'hold',terminal:true}")&&claimDefinition.includes("{id:'hold',from:['REQUESTED'],to:'HOLD'"),'OMS claims workflow represents HOLD as an explicit domain state instead of an orphan command') expect(claimDefinition.includes("version:'1.1.0'"),'OMS claims screen versions the repaired workflow contract') const demo=read('apps/web/src/http/demo/installFrontendDemoApi.ts') for(const marker of ["status:'REQUESTED'","status:'APPROVED'","status:'IN_PROGRESS'","status:'HOLD'",'channelName','requestedQty','ownerName'])expect(demo.includes(marker),`Demo claims carries real DTO/state field: ${marker}`) expect(demo.includes("policy:any={approve:{from:'REQUESTED',to:'APPROVED'},hold:{from:'REQUESTED',to:'HOLD'},start:{from:'APPROVED',to:'IN_PROGRESS'},complete:{from:'IN_PROGRESS',to:'COMPLETED'}}"),'Demo claims mirrors backend transition preconditions') expect(demo.includes("return fail(config,{type:'business-rule',code:'CLAIM_TRANSITION_NOT_ALLOWED'")&&demo.includes('new AxiosError'),'Demo invalid claim transition rejects as an actual HTTP/business failure rather than resolving a fake success') const workPage=read('apps/web/src/modules/wms/work/WmsWorkPage.vue') const workApi=read('apps/web/src/modules/wms/work/workApi.ts') const workDefinition=read('apps/web/src/modules/wms/work/work.definition.ts') expect(workPage.includes("const search=ref({taskType:'',status:'',owner:'mine',keyword:''})")&&workPage.includes('onMounted(()=>void executeSearch())'),'T06 WMS queue starts with all statuses and loads actionable work on entry') expect(workApi.includes('interface WmsWorkSearchSummary')&&workApi.includes('const summaryRows=demoRows.filter(row=>matchesBase(row,search))'),'T06 WMS queue computes status summary independently from the active status filter') expect(workPage.includes('summary.value.blockedCount')&&workPage.includes("label:'확인 필요한 작업'"),'T06 WMS queue surfaces blocked work even before the user narrows to BLOCKED') expect(workPage.includes(':disabled="!canOpenTask||selectedBlocked"')&&workPage.includes('예외 작업은 행을 열어 원인을 먼저 확인하세요.'),'T06 WMS queue fails closed instead of presenting BLOCKED work as startable') expect(/version:\s*'1\.(?:[3-9]|[1-9]\d+)\.\d+'/.test(workDefinition)&&workDefinition.includes("{value:'READY',label:'대기'}"),'T06 WMS screen versions the queue-signal contract without duplicating the implicit 전체 option') const home=read('packages/kbx-ui/src/shell/KbxHomePage.vue') expect(home.includes("launchableEntries=computed(()=>props.entries.filter(entry=>entry.menuVisible!==false))")&&home.includes('{{launchableScreenCount}}'),'Home counts only user-launchable menu screens instead of hidden utility entries') // Whole-object v-model must be writable. reactive() + is a runtime assignment trap. const vueFiles=[] function walk(dir){for(const entry of fs.readdirSync(dir,{withFileTypes:true})){const full=path.join(dir,entry.name);if(entry.isDirectory())walk(full);else if(entry.isFile()&&entry.name.endsWith('.vue'))vueFiles.push(full)}} walk('apps/web/src') const unsafe=[] for(const file of vueFiles){const source=read(file);const reactive=[...source.matchAll(/const\s+(\w+)\s*=\s*reactive(?:<[^>]+>)?\s*\(/g)].map(match=>match[1]);for(const name of reactive)if(new RegExp(`]*v-model=["']${name}["']`).test(source))unsafe.push(`${file}:${name}`)} expect(unsafe.length===0,`SearchPanel whole-object v-model uses writable refs; unsafe bindings: ${unsafe.join(', ')||'none'}`) const metrics=read('apps/web/src/modules/common/ux-metrics/UxMetricsPage.vue') expect(metrics.includes('const search=ref(')&&metrics.includes('search.value.screenId'),'UX metrics fixes the same writable-search runtime contract instead of leaving a known sibling defect') const claimE2e=read('tests/e2e/oms-claims.spec.ts') expect(claimE2e.includes('claim-action-receipt')&&claimE2e.includes("name: '처리 시작'")&&claimE2e.includes("selectOption('HOLD')"),'Claims E2E covers selection, start transition, authoritative receipt, and HOLD state') const workE2e=read('tests/e2e/wms-work-queue.spec.ts') expect(workE2e.includes('PICK-260811-009')&&workE2e.includes('업무 예외 요약')&&workE2e.includes('toBeDisabled()'),'WMS queue E2E proves initial exception visibility and fail-closed blocked action') const reference=read('apps/web/fe-reference/index.html') expect(/KBX FE Reference Lab v(?:5[8-9]|[6-9][0-9]) ·/.test(reference),'Static HTML reference identifies v58 or a later compatible FE build') console.log('PASS: FE workflow truth + queue signal + runtime hardening v58 contract')