import fs from 'node:fs' import path from 'node:path' const root=process.cwd() const read=p=>fs.readFileSync(path.join(root,p),'utf8') const json=p=>JSON.parse(read(p)) const failures=[] const check=(ok,message)=>{if(!ok)failures.push(message)} const web=json('apps/web/package.json') check(web.name==='@kbx/web','apps/web must be an explicit workspace package') check(/^\^?8\./.test(web.devDependencies?.vite??''),'apps/web must target Vite 8') check(String(web.scripts?.['dev:demo']??'').includes('--mode demo'),'demo runtime must require explicit Vite demo mode') check(String(web.scripts?.build??'').includes('vue-tsc --noEmit'),'web build must typecheck before Vite build') check(fs.existsSync(path.join(root,'apps/web/index.html')),'apps/web/index.html missing') check(fs.existsSync(path.join(root,'apps/web/vite.config.ts')),'apps/web/vite.config.ts missing') check(fs.existsSync(path.join(root,'apps/web/src/main.ts')),'apps/web/src/main.ts missing') check(fs.existsSync(path.join(root,'apps/web/src/App.vue')),'apps/web/src/App.vue missing') check(fs.existsSync(path.join(root,'apps/web/.env.demo')),'explicit .env.demo missing') const main=read('apps/web/src/main.ts') for(const token of ['createPinia','VueQueryPlugin','createRouter','PrimeVue','installKbx(app)','VITE_KBX_DEMO_MODE === \'true\'','installFrontendDemoApi']) check(main.includes(token),`main.ts runtime wiring missing: ${token}`) const app=read('apps/web/src/App.vue') check(app.includes("demoMode ? kbxPermissionCatalog.map(permission => permission.id) : []"),'production permissions must default to empty, not demo all-access') check(app.includes("anonymous:secure-default"),'production preference scope must be secure-default') const demo=read('apps/web/src/http/demo/installFrontendDemoApi.ts') check(demo.includes('kbxHttp.defaults.adapter = demoAdapter'),'demo mode must replace only the HTTP transport boundary') check(demo.includes('DEMO_OPERATION_UNSUPPORTED'),'unknown demo write/read paths must remain explicit, not silently succeed') check(demo.includes("stage:'ready'")&&demo.includes("stage='await-location'")&&demo.includes("stage='await-item'"),'WMS demo must exercise explicit picking stage transitions') const useOrders=read('apps/web/src/modules/oms/orders/search/useOrderSearch.ts') for(const token of ['useRoute','hydrateSearchFromRoute','exceptionOnly === \'true\'','keyword.slice(0, 100)','channelId.slice(0, 64)']) check(useOrders.includes(token),`Home→Order context hardening missing: ${token}`) check(useOrders.includes('const contextual = hydrateSearchFromRoute()')&&useOrders.includes('if (contextual) void executeSearch()'),'Home→Order context must still auto-execute after safe preference hydration') const orderPage=read('apps/web/src/modules/oms/orders/search/OrderListPage.vue') for(const token of ["id === 'excel.export'","id === 'excel.template'","id === 'excel.import'","id === 'excel.paste'","id === 'excel.history'",'전체 조회결과 Export API가 연결될 때까지 다운로드를 차단']) check(orderPage.includes(token),`OMS Excel completion guard missing: ${token}`) check(!/\n\s+exportable\n/.test(orderPage),'T01 must not duplicate Excel output in both Command Bar and Grid toolbar') const commandContract=read('packages/kbx-contracts/src/command.ts') check(commandContract.includes("menu?: 'excel'"),'command contract missing standard Excel menu capability') const commandBar=read('packages/kbx-ui/src/components/KbxCommandBar.vue') check(commandBar.includes("command.menu==='excel'"),'KbxCommandBar must render standard Excel menu') const excel=read('packages/kbx-ui/src/components/KbxExcelMenu.vue') for(const token of ['role="menu"','role="menuitem"',"event.key === 'Escape'","event.key === 'ArrowDown'","event.key === 'ArrowUp'","event.key === 'Home'","event.key === 'End'",'onDocumentPointerDown','focusTrigger']) check(excel.includes(token),`KbxExcelMenu interaction contract missing: ${token}`) const globalHeader=read('packages/kbx-ui/src/shell/KbxGlobalHeader.vue') check(globalHeader.includes('@media(max-width:56rem)')&&globalHeader.includes('grid-template-columns:auto minmax(0,1fr) auto'),'Global Header must release the 220px navigation column under zoom/narrow pressure') check(globalHeader.includes('@media(max-width:40rem)')&&globalHeader.includes('grid-template-columns:repeat(4,1fr)'),'Global Header actions must reflow instead of forcing horizontal overflow') const sideNav=read('packages/kbx-ui/src/shell/KbxSideNavigation.vue') check(sideNav.includes('.side-nav:not(.collapsed){width:var(--kbx-side-nav-collapsed-width)'),'Side Navigation must visually compact under 56rem/200% zoom pressure') check(sideNav.includes('.side-nav:not(.collapsed) .module-block{display:none}'),'Expanded navigation detail must yield workspace width under narrow desktop pressure') if(failures.length){console.error('FE runtime v42 validation failed:');for(const failure of failures)console.error(`- ${failure}`);process.exit(1)} console.log('FE runtime v42 validation passed: runnable Vite shell, secure demo boundary, Home context, Excel menu contract.')