import fs from 'node:fs' 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 home = read('packages/kbx-ui/src/shell/KbxHomePage.vue') for (const label of ['긴급·예외','미저장','진행 작업','알림']) { expect(home.includes(`label:'${label}'`), `home operational triage includes ${label} lane`) } expect(home.includes('attentionTriageGroups') && home.includes('slice(0,perGroup)'), 'home triage limits each source lane instead of letting one source monopolize the workbench') expect(home.includes('attentionDisplayedCount') && home.includes('kbx-home__triage-group'), 'home exposes visible triage volume and stable lane anatomy') expect(home.includes("['ArrowLeft','ArrowRight','Home','End']") && home.includes('focusModuleRail'), 'home module rail retains keyboard-first wayfinding') const navigator = read('packages/kbx-ui/src/components/KbxRecordNavigator.vue') expect(navigator.includes("itemLabel:'레코드'") && navigator.includes('current>=total'), 'record navigator exposes record position and hard previous/next boundaries') expect(navigator.includes("emit('previous')") && navigator.includes("emit('next')"), 'record navigator has explicit previous/next interaction contract') expect(navigator.includes('button:focus-visible') && navigator.includes('@media(forced-colors:active)'), 'record navigator preserves keyboard focus and high-contrast feedback') const itemMaster = read('apps/web/src/modules/erp/items/ItemMasterPage.vue') expect(itemMaster.includes('KbxRecordNavigator') && itemMaster.includes('currentRowPosition'), 'T02 item master exposes current-record wayfinding beside the detail work surface') expect(itemMaster.includes(':previous-disabled="dirty') && itemMaster.includes(':next-disabled="dirty'), 'T02 record navigation is fail-closed while the detail has unsaved changes') expect(itemMaster.includes('function navigateRecord') && itemMaster.includes('requestChoose([target])'), 'T02 record navigation reuses the existing dirty-safe selection path instead of bypassing it') const workflow = read('packages/kbx-ui/src/components/KbxWorkflowBar.vue') expect(workflow.includes('다음 작업') && workflow.includes('nextActionLabel'), 'workflow bar explicitly tells the user what can happen next') expect(workflow.includes("aria-current=\"state.value===current?'step':undefined\"") && workflow.includes('progressLabel'), 'workflow bar exposes current step semantics and progress') expect(workflow.includes('현재 상태에서 실행 가능한 다음 작업이 없습니다.'), 'workflow bar explains terminal/blocked states instead of showing an unexplained blank action area') const fast = read('packages/kbx-ui/src/components/KbxFastEntryPage.vue') expect(fast.includes('gridTitle?:string') && fast.includes('rowCount?:number') && fast.includes('rowCountUnit?:string'), 'T04 owns fast-entry grid title and row-count anatomy') expect(fast.includes('slot name="grid-actions"') && fast.includes('KbxSectionHeader'), 'T04 reserves standard grid-level actions beside the work-surface heading') const itemPrice = read('apps/web/src/modules/erp/item-prices/ItemPriceFastEntryPage.vue') expect(itemPrice.includes('grid-title="품목 단가 입력"') && itemPrice.includes(':row-count="vm.enteredCount.value"'), 'real T04 item-price screen uses the standard work-surface heading/count contract') expect(itemPrice.includes('