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(n=>{if(!text.includes(n))failures.push(`${scope} missing ${n}`)}) const screenFrame=read('packages/kbx-ui/src/components/KbxScreenFrame.vue') must(screenFrame,['expectedType?: KbxScreenType','templateMismatch','화면 템플릿 구성이 올바르지 않습니다.','data-template="templateCode"'],'ScreenFrame template contract') const templateMap={ KbxListPage:['list','T01'],KbxMasterPage:['master','T02'],KbxTransactionPage:['transaction','T03'],KbxFastEntryPage:['fast-entry','T04'], KbxMasterDetailPage:['master-detail','T05'],KbxQueuePage:['queue','T06'],KbxReconcilePage:['reconcile','T07'],KbxImportPage:['import','T08'], } for(const [name,[type,code]] of Object.entries(templateMap)){ const text=read(`packages/kbx-ui/src/components/${name}.vue`) must(text,[`expected-type="${type}"`,`template-code="${code}"`],name) } const mobile=read('packages/kbx-ui/src/wms/KbxWmsMobilePage.vue') must(mobile,["props.screen.type!=='wms-mobile'",'현장 화면 구성이 올바르지 않습니다.'],'T09 template contract') const searchPanel=read('packages/kbx-ui/src/components/KbxSearchPanel.vue') const searchField=read('packages/kbx-ui/src/components/KbxSearchFieldControl.vue') must(searchPanel,['KbxSearchFieldControl','v-for="field in primary"','v-for="field in secondary"','role="search"'],'SearchPanel composition') must(searchField,["field.type==='date-range'","field.type==='lookup'",'`${field.label} 시작일`','`${field.label} 종료일`'],'SearchFieldControl') if(searchPanel.includes("field.type==='text'")||searchPanel.includes("field.type==='select'")||searchPanel.includes("field.type==='lookup'"))failures.push('SearchPanel must delegate field-type rendering to KbxSearchFieldControl') const homeLogic=read('packages/kbx-ui/src/shell/homeNavigation.ts') must(homeLogic,['launchKey:tab.key','seenLaunchKeys','coveredScreenIds','addTab(tab','instanceLabel:routeInstanceLabel','Boolean(b.pinned)'],'Home instance workbench') const home=read('packages/kbx-ui/src/shell/KbxHomePage.vue') must(home,[':key="item.launchKey"','item.instanceLabel','firstDirty','class="resume"'],'Home workbench UI') if(home.includes('{{item.screenId}}')||home.includes('{{entry.screenId}}'))failures.push('Home must not expose ScreenId in normal browse surfaces') const tabs=read('packages/kbx-ui/src/shell/KbxWorkspaceTabs.vue') must(tabs,['tabButtons','selectAndFocus','ArrowLeft','ArrowRight','togglePin','aria-pressed="Boolean(tab.pinned)"'],'Workspace tabs') if(tabs.includes("event.key==='Delete'"))failures.push('Workspace tabs must not overload Delete as close') const store=read('apps/web/src/shell/workspaceStore.ts') must(store,['togglePinned(tabKey:string)','!tab.dirty&&!tab.pinned'],'workspace pin policy') const shell=read('packages/kbx-ui/src/shell/KbxApplicationShell.vue') must(shell,['본문 바로가기','id="kbx-main-workspace"','toggleTabPin','@toggle-pin'],'ApplicationShell accessibility/pin contract') const app=read('apps/web/src/shell/KbxAppFrame.vue') must(app,["@toggle-tab-pin=\"tab=>store.togglePinned(tab.key)\"","resolveKbxSafeRecentPath(navEntry,safe)"],'AppFrame pin/recent persistence') const menu=read('packages/kbx-ui/src/shell/KbxMenuSearch.vue') must(menu,['restoreFocus','document.activeElement','검색 결과 {{results.length}}건'],'MenuSearch focus restoration') const security=read('packages/kbx-ui/src/shell/navigationSecurity.ts') must(security,['hasUnsafeEncodedPath','%2f','candidate.username','candidate.pathname:entry.path','Query/hash are deliberately stripped'],'Navigation security') if(failures.length){console.error(failures.join('\n'));process.exit(1)} console.log('v31 template/component/home navigation validation passed: runtime template-type guards, SearchPanel de-duplication, multi-instance Home workbench, roving tab focus/pinning, focus recovery, skip navigation, and persisted-route hardening.')