import fs from 'node:fs' const read=p=>fs.readFileSync(p,'utf8') const failures=[];const check=(ok,msg)=>{if(!ok)failures.push(msg)} const pref=read('apps/web/src/preferences/screenPreferenceStore.ts') for(const token of ["kbx.screen.preference.v2","KbxScreenPreferenceScopeKey","screenVersion","MAX_STORED_BYTES","MAX_SEARCH_KEYS","try { localStorage.setItem","try { raw = localStorage.getItem"]) check(pref.includes(token),`screen preference hardening missing: ${token}`) check(pref.includes('encodeURIComponent(scope)')&&pref.includes('encodeURIComponent(screenId)')&&pref.includes('encodeURIComponent(screenVersion)'),'screen preference key must be scope + ScreenId + ScreenVersion') check(!pref.includes("PREFIX + screenId"),'legacy unscoped ScreenId-only preference key must not return') const orderVm=read('apps/web/src/modules/oms/orders/search/useOrderSearch.ts') for(const token of ['rememberSearch','savedSearchDefaults','safePersistableSearch','saveCurrentSearchDefaults','setRememberSearch','loadScreenPreference','saveScreenPreference','Free-text keyword is intentionally never restored']) check(orderVm.includes(token),`OMS T01 safe preference flow missing: ${token}`) const persistBlock=orderVm.match(/function safePersistableSearch\(\) \{([\s\S]*?)\n \}/)?.[1]??'' check(persistBlock&&!/keyword\s*:/.test(persistBlock),'persistable OMS search must exclude free-text keyword') const orderPage=read('apps/web/src/modules/oms/orders/search/OrderListPage.vue') for(const token of ['remember','saved-search',':remember-checked="vm.rememberSearch.value"','@update:remember-checked="vm.setRememberSearch"','@saved-search-requested="vm.saveCurrentSearchDefaults"'])check(orderPage.includes(token),`OMS T01 SearchPanel preference binding missing: ${token}`) const frame=read('apps/web/src/shell/KbxAppFrame.vue') for(const token of ['KbxScreenPreferenceScopeKey','provide(KbxScreenPreferenceScopeKey,screenPreferenceScope)','runtimePanelTargetId','toggleRuntimePanel(\'operations\',item.operationId)',':active-id="runtimePanelTargetId"'])check(frame.includes(token),`Home/context navigation hardening missing: ${token}`) const operations=read('packages/kbx-ui/src/components/KbxOperationCenter.vue') const notifications=read('packages/kbx-ui/src/components/KbxNotificationCenter.vue') for(const [name,source,id] of [['Operation',operations,'data-operation-id'],['Notification',notifications,'data-notification-id']]){ check(source.includes('activeId?: string | null'),`${name} center active target contract missing`) check(source.includes(id)&&source.includes('aria-current')&&source.includes('scrollIntoView'),`${name} center must focus/identify Home target`) } const ref=read('apps/web/fe-reference/kbx-fe.js') for(const token of ['orderRememberSearch','orderSavedDefaults','safeOrderDefaults','기본조건 저장','자유검색어는 저장하지'])check(ref.includes(token),`FE Reference safe search preference parity missing: ${token}`) const refCss=read('apps/web/fe-reference/kbx-fe.css') check(refCss.includes('.search-preference')&&refCss.includes('@media (forced-colors: active)'),'safe preference UI must coexist with theme/high-contrast CSS') if(failures.length){console.error('FE preference/navigation v45 validation failed:');for(const f of failures)console.error(`- ${f}`);process.exit(1)} console.log('FE preference/navigation v45 PASS: scoped versioned preferences, sensitive free-text exclusion, Home target focus, themed Reference parity.')