diff --git a/apps/web/bun.lock b/apps/web/bun.lock index 58dde62a3c..f206be793b 100644 --- a/apps/web/bun.lock +++ b/apps/web/bun.lock @@ -73,7 +73,7 @@ "tailwind-merge": "^3.7.0", "three": "0.180.0", "web-vitals": "^6.2.2", - "zod": "^4.6.5", + "zod": "^4.6.6", }, "devDependencies": { "@next/bundle-analyzer": "16.4.2", @@ -1863,7 +1863,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="], + "zod": ["zod@4.6.6", "", {}, "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA=="], "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], diff --git a/apps/web/components/restaurant/RestaurantEvidenceCarousel.tsx b/apps/web/components/restaurant/RestaurantEvidenceCarousel.tsx index 29567edfd7..c0de65c683 100644 --- a/apps/web/components/restaurant/RestaurantEvidenceCarousel.tsx +++ b/apps/web/components/restaurant/RestaurantEvidenceCarousel.tsx @@ -93,7 +93,7 @@ export function RestaurantEvidenceCarousel({ frames }: { frames: readonly Restau {visible.some((frame) => !isLevel1SourceObserved(frame)) && (

현장 장면은 영상의 맥락을 보여주며, 매장 신원을 확인하는 간판·매장 안내와 구분해요.

)} -
+
{visible.map((frame) => { const presentation = publicFramePresentation(frame); return ( diff --git a/apps/web/lib/home-map-keyboard-navigation.ts b/apps/web/lib/home-map-keyboard-navigation.ts index 76a79b5664..11e2303d55 100644 --- a/apps/web/lib/home-map-keyboard-navigation.ts +++ b/apps/web/lib/home-map-keyboard-navigation.ts @@ -26,7 +26,7 @@ type ArrowNavigationGuardInput = { swipeableCount: number; }; -const KEYBOARD_NAV_INTERACTIVE_SELECTOR = 'button, a, [role="button"], [role="link"]'; +const KEYBOARD_NAV_INTERACTIVE_SELECTOR = 'button, a, [role="button"], [role="link"], [aria-roledescription="carousel"]'; const KEYBOARD_NAV_MAP_CONTROL_SELECTOR = '[data-testid="map-container"] button'; const EDITABLE_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT']); diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index 01cde625bc..615b76fb79 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -77,7 +77,7 @@ "tailwind-merge": "^3.7.0", "three": "0.180.0", "web-vitals": "^6.2.2", - "zod": "^4.6.5" + "zod": "^4.6.6" }, "devDependencies": { "@next/bundle-analyzer": "16.4.2", @@ -7969,6 +7969,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -14000,9 +14001,9 @@ } }, "node_modules/zod": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", - "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz", + "integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==", "license": "MIT", "peer": true, "funding": { diff --git a/apps/web/package.json b/apps/web/package.json index 04b7ba6643..60e6648703 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -122,7 +122,7 @@ "tailwind-merge": "^3.7.0", "three": "0.180.0", "web-vitals": "^6.2.2", - "zod": "^4.6.5" + "zod": "^4.6.6" }, "devDependencies": { "@next/bundle-analyzer": "16.4.2", diff --git a/apps/web/tests-unit/admin-detail-close-focus-browser.test.ts b/apps/web/tests-unit/admin-detail-close-focus-browser.test.ts index 9155fe3013..488c3be0d8 100644 --- a/apps/web/tests-unit/admin-detail-close-focus-browser.test.ts +++ b/apps/web/tests-unit/admin-detail-close-focus-browser.test.ts @@ -39,7 +39,8 @@ async function bundle() { `; const built = await Bun.build({ entrypoints: ['detail-focus-fixture'], target: 'browser', format: 'iife', write: false, - define: { 'process.env.NODE_ENV': '"test"' }, + // Isolated browser build: no Node process or operator environment is present. + define: { 'process.env': '{}', 'process.env.NODE_ENV': '"test"' }, plugins: [{ name: 'detail-focus-fixture', setup(builder) { builder.onResolve({ filter: /^detail-focus-fixture$/ }, () => ({ path: 'fixture', namespace: 'fixture' })); builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ contents: entry, loader: 'tsx', resolveDir: appRoot })); diff --git a/apps/web/tests-unit/admin-record-action-failure-once-browser.test.ts b/apps/web/tests-unit/admin-record-action-failure-once-browser.test.ts index a51cdf28c7..34788f2346 100644 --- a/apps/web/tests-unit/admin-record-action-failure-once-browser.test.ts +++ b/apps/web/tests-unit/admin-record-action-failure-once-browser.test.ts @@ -25,7 +25,7 @@ const entry = ` if (url.pathname === '/api/admin/evaluations' && url.searchParams.get('view') === 'page') { return json(fixture.deleted ? { ...onePage, records: [], warnings: {}, filteredTotal: 0 } : onePage); } - if (fixture.refreshOk && url.pathname === '/api/admin/submissions') return json({ submissions: [] }); + if (fixture.refreshOk && url.pathname === '/api/admin/submissions') return json({ submissions: [], items: [] }); if (fixture.refreshOk && url.pathname === '/api/admin/restaurant-requests') return json({ requests: [] }); if (fixture.refreshOk && url.pathname === '/api/admin/pending-counts') return json({ submissions: 0, reviews: 0 }); if (url.pathname === '/api/admin/record-actions') { diff --git a/apps/web/tests-unit/admin-storyboard-unknown-count-browser.test.ts b/apps/web/tests-unit/admin-storyboard-unknown-count-browser.test.ts index 45510d2981..08ee44bdf3 100644 --- a/apps/web/tests-unit/admin-storyboard-unknown-count-browser.test.ts +++ b/apps/web/tests-unit/admin-storyboard-unknown-count-browser.test.ts @@ -25,7 +25,8 @@ async function bundle() { `; const built = await Bun.build({ entrypoints: ['storyboard-count-fixture'], target: 'browser', format: 'iife', write: false, - define: { 'process.env.NODE_ENV': '"test"' }, + // Isolated browser build: no Node process or operator environment is present. + define: { 'process.env': '{}', 'process.env.NODE_ENV': '"test"' }, plugins: [{ name: 'storyboard-count-fixture', setup(builder) { builder.onResolve({ filter: /^storyboard-count-fixture$/ }, () => ({ path: 'fixture', namespace: 'fixture' })); builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ contents: entry, loader: 'tsx', resolveDir: appRoot })); diff --git a/apps/web/tests-unit/admin-storyboard-worker-visible-status-browser.test.ts b/apps/web/tests-unit/admin-storyboard-worker-visible-status-browser.test.ts index 8f6d8a8118..3400ef1bdc 100644 --- a/apps/web/tests-unit/admin-storyboard-worker-visible-status-browser.test.ts +++ b/apps/web/tests-unit/admin-storyboard-worker-visible-status-browser.test.ts @@ -30,7 +30,8 @@ const entry = ` async function bundle() { const built = await Bun.build({ entrypoints: ['worker-visible-status-fixture'], target: 'browser', format: 'iife', write: false, - define: { 'process.env.NODE_ENV': '"test"' }, + // Isolated browser build: no Node process or operator environment is present. + define: { 'process.env': '{}', 'process.env.NODE_ENV': '"test"' }, plugins: [{ name: 'worker-visible-status-fixture', setup(builder) { builder.onResolve({ filter: /^worker-visible-status-fixture$/ }, () => ({ path: 'fixture', namespace: 'fixture' })); builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ contents: entry, loader: 'tsx', resolveDir: appRoot })); diff --git a/apps/web/tests-unit/evidence-carousel-navigation-browser.test.ts b/apps/web/tests-unit/evidence-carousel-navigation-browser.test.ts new file mode 100644 index 0000000000..2646bb8997 --- /dev/null +++ b/apps/web/tests-unit/evidence-carousel-navigation-browser.test.ts @@ -0,0 +1,117 @@ +import { expect, test } from 'bun:test'; +import { existsSync, readFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import ts from '@typescript/old'; +import { chromium, expect as ui } from '@playwright/test'; + +const appRoot = join(import.meta.dir, '..'); +const chrome = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; +const localTest = existsSync(chrome) ? test : test.skip; +const rows = JSON.parse(readFileSync(resolve(appRoot, '../../docs/evidence/marker-evidence-live-20261021-v1/reviewed-frames.json'), 'utf8')); +const frames = rows.frames.slice(0, 3).map((row: { frame: unknown }) => row.frame); + +function actualPanelSwipeDeclarations(): string { + const source = ts.createSourceFile('RestaurantDetailPanel.tsx', readFileSync(join(appRoot, 'components/restaurant/RestaurantDetailPanel.tsx'), 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); + const names = new Set(['RESTAURANT_DETAIL_SWIPE_THRESHOLD', 'RESTAURANT_DETAIL_SWIPE_INTENT_RATIO', + 'contentSwipeStartXRef', 'contentSwipeStartYRef', 'contentSwipeDirectionRef', 'isContentSwipingRef', + 'handleContentSwipeStart', 'handleContentSwipeMove', 'handleContentSwipeEnd', 'handleContentSwipeCancel']); + const declarations: string[] = []; + function visit(node: ts.Node) { + if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && names.has(node.name.text)) { + names.delete(node.name.text); + declarations.push(`const ${node.getText(source)};`); + } + ts.forEachChild(node, visit); + } + visit(source); + expect([...names]).toEqual([]); + return declarations.join('\n'); +} + +async function bundle() { + const entry = `import React, { useCallback, useRef } from 'react'; import { createRoot } from 'react-dom/client'; + import { RestaurantEvidenceCarousel } from './components/restaurant/RestaurantEvidenceCarousel'; + import { handleDesktopArrowNavigationEvent } from './lib/home-map-keyboard-navigation'; + let navigations = 0, panelStarts = 0; + window.sceneFixture = { reset: () => { navigations = 0; panelStarts = 0; }, count: () => navigations, starts: () => panelStarts }; + window.addEventListener('keydown', event => handleDesktopArrowNavigationEvent({ event, + isDesktop: innerWidth >= 1024, isPanelOpen: true, hasCurrentRestaurant: true, swipeableCount: 2, + onNavigate: () => { navigations++; return true; } })); + function App() { + const onSwipeLeft = () => { navigations++; }, onSwipeRight = onSwipeLeft, hideSwipeHint = () => { panelStarts++; }; + ${actualPanelSwipeDeclarations()} + return
+
Restaurant navigation area
+ +
; + } + createRoot(document.getElementById('root')).render();`; + const built = await Bun.build({ entrypoints: ['scene-navigation-fixture'], target: 'browser', format: 'iife', write: false, + define: { 'process.env.NODE_ENV': '"test"' }, plugins: [{ name: 'scene-navigation', setup(builder) { + builder.onResolve({ filter: /^scene-navigation-fixture$/ }, () => ({ path: 'entry', namespace: 'fixture' })); + builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ contents: entry, loader: 'tsx', resolveDir: appRoot })); + builder.onResolve({ filter: /^@\// }, args => ({ path: Bun.resolveSync('./' + args.path.slice(2), appRoot) })); + }}] }); + expect(built.success).toBe(true); + return built.outputs[0].text(); +} + +for (const width of [1440, 390, 820]) { + localTest(`scene navigation keeps the restaurant selected at ${width}px`, async () => { + const browser = await chromium.launch({ executablePath: chrome, headless: true }); + try { + const context = await browser.newContext({ viewport: { width, height: 1000 }, hasTouch: true }); + const page = await context.newPage(); + const errors: string[] = [], external: string[] = []; + page.on('pageerror', error => errors.push(error.message)); + await page.route('**/*', route => { + const url = new URL(route.request().url()); + if (url.origin !== 'https://scene-navigation.test') { external.push(url.origin); return route.abort(); } + if (url.pathname.startsWith('/evidence-frames/')) return route.fulfill({contentType:'image/webp', body:readFileSync(join(appRoot,'public',url.pathname))}); + return route.fulfill({contentType:'text/html; charset=utf-8',body:`
`}); + }); + await page.goto('https://scene-navigation.test/'); await page.addScriptTag({ content: await bundle() }); + const scroller = page.getByLabel('영상 장면 가로 스크롤', {exact:true}); + await ui(scroller.locator('img')).toHaveCount(3); + const metrics = await scroller.evaluate(element => ({width:element.clientWidth, contentWidth:element.scrollWidth, overflow:getComputedStyle(element).overflowX})); + expect(metrics.overflow).toBe('auto'); + expect(metrics.contentWidth).toBeGreaterThan(metrics.width); + const count = () => page.evaluate(() => (window as unknown as {sceneFixture:{count():number}}).sceneFixture.count()); + const starts = () => page.evaluate(() => (window as unknown as {sceneFixture:{starts():number}}).sceneFixture.starts()); + const reset = () => page.evaluate(() => (window as unknown as {sceneFixture:{reset():void}}).sceneFixture.reset()); + if (width === 1440) { + await scroller.focus(); await page.keyboard.press('ArrowRight'); + expect(await count()).toBe(0); + await ui.poll(() => scroller.evaluate(element => element.scrollLeft)).toBeGreaterThan(0); + await page.locator('#outside').focus(); await page.keyboard.press('ArrowRight'); + expect(await count()).toBe(1); + await reset(); + } + const session = await context.newCDPSession(page); + async function swipe(selector: string) { + const box = await page.locator(selector).boundingBox(); + if (!box) throw new Error('SWIPE_TARGET_MISSING'); + const x = box.x + Math.min(box.width - 20, 300), y = box.y + Math.min(box.height / 2, 80); + await session.send('Input.dispatchTouchEvent', {type:'touchStart',touchPoints:[{x,y,id:1}]}); + for (let step = 1; step <= 8; step++) { + await session.send('Input.dispatchTouchEvent', {type:'touchMove',touchPoints:[{x:x-step*25,y,id:1}]}); + await page.waitForTimeout(20); + } + await session.send('Input.dispatchTouchEvent', {type:'touchEnd',touchPoints:[]}); + } + await swipe('[aria-label="영상 장면 가로 스크롤"]'); + expect(await starts()).toBe(0); + expect(await count()).toBe(0); + await ui.poll(() => scroller.evaluate(element => element.scrollLeft)).toBeGreaterThan(0); + await swipe('#outside'); + expect(await starts()).toBe(1); + expect(await count()).toBe(1); + expect(external).toEqual([]); expect(errors).toEqual([]); + await context.close(); + } finally { await browser.close(); } + }, 60_000); +} diff --git a/apps/web/tests-unit/local-storyboard-catalog-error-browser.test.ts b/apps/web/tests-unit/local-storyboard-catalog-error-browser.test.ts index ead9ddac27..274c140e8f 100644 --- a/apps/web/tests-unit/local-storyboard-catalog-error-browser.test.ts +++ b/apps/web/tests-unit/local-storyboard-catalog-error-browser.test.ts @@ -43,7 +43,8 @@ createRoot(document.getElementById('root')).render() `; const bundle = await Bun.build({ entrypoints: ['storyboard-catalog-fixture'], target: 'browser', format: 'iife', write: false, - define: { 'process.env.NODE_ENV': '"test"' }, + // Isolated browser build: no Node process or operator environment is present. + define: { 'process.env': '{}', 'process.env.NODE_ENV': '"test"' }, plugins: [{ name: 'offline-storyboard', setup(builder) { builder.onResolve({ filter: /^storyboard-catalog-fixture$/ }, () => ({ path: 'fixture.tsx', namespace: 'fixture' })); builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ contents: fixture, loader: 'tsx', resolveDir: join(import.meta.dir, '..') })); diff --git a/apps/web/tests-unit/local-storyboard-control-focus-browser.test.ts b/apps/web/tests-unit/local-storyboard-control-focus-browser.test.ts index da79d06cfe..b421d78bf1 100644 --- a/apps/web/tests-unit/local-storyboard-control-focus-browser.test.ts +++ b/apps/web/tests-unit/local-storyboard-control-focus-browser.test.ts @@ -49,7 +49,8 @@ createRoot(document.getElementById('root')).render() `; const bundle = await Bun.build({ entrypoints: ['storyboard-control-fixture'], target: 'browser', format: 'iife', write: false, - define: { 'process.env.NODE_ENV': '"test"' }, + // Isolated browser build: no Node process or operator environment is present. + define: { 'process.env': '{}', 'process.env.NODE_ENV': '"test"' }, plugins: [{ name: 'offline-storyboard', setup(builder) { builder.onResolve({ filter: /^storyboard-control-fixture$/ }, () => ({ path: 'fixture.tsx', namespace: 'fixture' })); builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ contents: fixture, loader: 'tsx', resolveDir: join(import.meta.dir, '..') })); diff --git a/apps/web/tests-unit/local-storyboard-departure-browser.test.ts b/apps/web/tests-unit/local-storyboard-departure-browser.test.ts index 3fcb9ac168..7f0fd83e8f 100644 --- a/apps/web/tests-unit/local-storyboard-departure-browser.test.ts +++ b/apps/web/tests-unit/local-storyboard-departure-browser.test.ts @@ -73,7 +73,8 @@ createRoot(document.getElementById('root')).render(<> `; const bundle = await Bun.build({ entrypoints: ['storyboard-departure-fixture'], target: 'browser', format: 'iife', write: false, - define: { 'process.env.NODE_ENV': '"test"' }, + // Isolated browser build: no Node process or operator environment is present. + define: { 'process.env': '{}', 'process.env.NODE_ENV': '"test"' }, plugins: [{ name: 'offline-storyboard', setup(builder) { builder.onResolve({ filter: /^storyboard-departure-fixture$/ }, () => ({ path: 'fixture.tsx', namespace: 'fixture' })); builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ contents: fixture, loader: 'tsx', resolveDir: join(import.meta.dir, '..') })); diff --git a/apps/web/tests-unit/local-storyboard-history-focus-browser.test.ts b/apps/web/tests-unit/local-storyboard-history-focus-browser.test.ts index ff3fe673ea..2b6e0ac3f5 100644 --- a/apps/web/tests-unit/local-storyboard-history-focus-browser.test.ts +++ b/apps/web/tests-unit/local-storyboard-history-focus-browser.test.ts @@ -58,7 +58,8 @@ createRoot(document.getElementById('root')).render() `; const bundle = await Bun.build({ entrypoints: ['storyboard-history-fixture'], target: 'browser', format: 'iife', write: false, - define: { 'process.env.NODE_ENV': '"test"' }, + // Isolated browser build: no Node process or operator environment is present. + define: { 'process.env': '{}', 'process.env.NODE_ENV': '"test"' }, plugins: [{ name: 'offline-storyboard', setup(builder) { builder.onResolve({ filter: /^storyboard-history-fixture$/ }, () => ({ path: 'fixture.tsx', namespace: 'fixture' })); builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ contents: fixture, loader: 'tsx', resolveDir: join(import.meta.dir, '..') })); diff --git a/apps/web/tests-unit/local-storyboard-project-refresh-focus-browser.test.ts b/apps/web/tests-unit/local-storyboard-project-refresh-focus-browser.test.ts index 97a8cc4210..0ce45b0dc8 100644 --- a/apps/web/tests-unit/local-storyboard-project-refresh-focus-browser.test.ts +++ b/apps/web/tests-unit/local-storyboard-project-refresh-focus-browser.test.ts @@ -42,7 +42,8 @@ createRoot(document.getElementById('root')).render() `; const bundle = await Bun.build({ entrypoints: ['storyboard-project-refresh-fixture'], target: 'browser', format: 'iife', write: false, - define: { 'process.env.NODE_ENV': '"test"' }, + // Isolated browser build: no Node process or operator environment is present. + define: { 'process.env': '{}', 'process.env.NODE_ENV': '"test"' }, plugins: [{ name: 'offline-storyboard', setup(builder) { builder.onResolve({ filter: /^storyboard-project-refresh-fixture$/ }, () => ({ path: 'fixture.tsx', namespace: 'fixture' })); builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ contents: fixture, loader: 'tsx', resolveDir: join(import.meta.dir, '..') })); diff --git a/apps/web/tests-unit/local-storyboard-worker-refresh-browser.test.ts b/apps/web/tests-unit/local-storyboard-worker-refresh-browser.test.ts index 9d8341b588..40a5b7fdbb 100644 --- a/apps/web/tests-unit/local-storyboard-worker-refresh-browser.test.ts +++ b/apps/web/tests-unit/local-storyboard-worker-refresh-browser.test.ts @@ -41,7 +41,8 @@ createRoot(document.getElementById('root')).render() `; const bundle = await Bun.build({ entrypoints: ['storyboard-worker-fixture'], target: 'browser', format: 'iife', write: false, - define: { 'process.env.NODE_ENV': '"test"' }, + // Isolated browser build: no Node process or operator environment is present. + define: { 'process.env': '{}', 'process.env.NODE_ENV': '"test"' }, plugins: [{ name: 'offline-storyboard', setup(builder) { builder.onResolve({ filter: /^storyboard-worker-fixture$/ }, () => ({ path: 'fixture.tsx', namespace: 'fixture' })); builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ contents: fixture, loader: 'tsx', resolveDir: join(import.meta.dir, '..') })); diff --git a/apps/web/tests-unit/storyboard-project-library-browser.test.ts b/apps/web/tests-unit/storyboard-project-library-browser.test.ts index 462a0e1949..717c2643ea 100644 --- a/apps/web/tests-unit/storyboard-project-library-browser.test.ts +++ b/apps/web/tests-unit/storyboard-project-library-browser.test.ts @@ -58,7 +58,8 @@ createRoot(document.getElementById('root')).render(); `; const bundle = await Bun.build({ entrypoints: ["storyboard-library-fixture"], target: "browser", format: "iife", write: false, - define: { "process.env.NODE_ENV": '"test"' }, + // Isolated browser build: no Node process or operator environment is present. + define: { "process.env": "{}", "process.env.NODE_ENV": '"test"' }, plugins: [{ name: "library-fixture", setup(builder) { builder.onResolve({ filter: /^storyboard-library-fixture$/ }, () => ({ path: "fixture.tsx", namespace: "fixture" })); builder.onLoad({ filter: /.*/, namespace: "fixture" }, () => ({ contents: fixture, loader: "tsx", resolveDir: root })); diff --git a/apps/web/tests-unit/zod-collection-size-boundary.test.ts b/apps/web/tests-unit/zod-collection-size-boundary.test.ts new file mode 100644 index 0000000000..9f27d27f77 --- /dev/null +++ b/apps/web/tests-unit/zod-collection-size-boundary.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from 'bun:test'; +import { z } from 'zod'; + +// GHSA-mrhx-c7xc-m3w5: rejecting an oversized collection must not first +// parse every element. Keep the fixture small; the visit count is the oracle. +const cases = [ + ['array.max', (item: z.ZodNumber) => z.array(item).max(1), (size: number) => Array.from({ length: size }, (_, i) => i)], + ['array.length', (item: z.ZodNumber) => z.array(item).length(1), (size: number) => Array.from({ length: size }, (_, i) => i)], + ['set.max', (item: z.ZodNumber) => z.set(item).max(1), (size: number) => new Set(Array.from({ length: size }, (_, i) => i))], + ['set.size', (item: z.ZodNumber) => z.set(item).size(1), (size: number) => new Set(Array.from({ length: size }, (_, i) => i))], + ['map.max', (item: z.ZodNumber) => z.map(z.string(), item).max(1), (size: number) => new Map(Array.from({ length: size }, (_, i) => [String(i), i]))], + ['map.size', (item: z.ZodNumber) => z.map(z.string(), item).size(1), (size: number) => new Map(Array.from({ length: size }, (_, i) => [String(i), i]))], +] as const; + +describe('collection size admission before element validation', () => { + for (const [name, schema, input] of cases) { + test(`${name} rejects oversize without visiting elements`, () => { + let visited = 0; + const item = z.number().superRefine(() => { visited += 1; }); + const result = schema(item).safeParse(input(101)); + + expect(result.success).toBe(false); + expect(result.error?.issues[0]?.code).toBe('too_big'); + expect(visited).toBe(0); + }); + + test(`${name} still parses a collection within its limit`, () => { + let visited = 0; + const item = z.number().superRefine(() => { visited += 1; }); + const result = schema(item).safeParse(input(1)); + + expect(result.success).toBe(true); + expect(visited).toBe(1); + }); + } +}); diff --git a/backend/bin/tests/node-dependency-compatibility.test.mjs b/backend/bin/tests/node-dependency-compatibility.test.mjs index 13afe54dcb..abcdf94db3 100644 --- a/backend/bin/tests/node-dependency-compatibility.test.mjs +++ b/backend/bin/tests/node-dependency-compatibility.test.mjs @@ -11,7 +11,9 @@ const backendRoot = fileURLToPath(new URL('../../', import.meta.url)); // Stage the actual callers with their local imports. Never load an operator's // .env, start a browser, or contact a provider/database during these checks. function stage(t, entry) { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'node-dependency-compatibility-')); + // macOS /var is a symlink. Match Node's canonical import.meta.url path so + // the copied callers' direct-entry guards actually execute their main. + const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'node-dependency-compatibility-'))); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const copied = new Set(); function copy(relativePath) { diff --git a/backend/package-lock.json b/backend/package-lock.json index 9a43be8350..656e1cc475 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,7 +16,7 @@ "ffprobe-static": "^3.1.0", "js-yaml": "^5.4.2", "pg": "^8.23.1", - "puppeteer": "^25.12.0", + "puppeteer": "^25.13.0", "puppeteer-extra": "^3.3.6", "puppeteer-extra-plugin-stealth": "^2.11.2", "winston": "^3.19.0" @@ -254,12 +254,12 @@ "license": "BSD-3-Clause" }, "node_modules/@puppeteer/browsers": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", - "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.4.tgz", + "integrity": "sha512-okJ6yQMc0SBexOKxMi966omwAV1uAPwgF/YMZdb04gZQPXVjWHl9bmXjZYtIcsN1+PN3Bz4iFfGK3g+c6lZJ0Q==", "license": "Apache-2.0", "dependencies": { - "modern-tar": "^0.8.4", + "modern-tar": "^0.8.5", "yargs": "^18.0.0" }, "bin": { @@ -454,13 +454,13 @@ "license": "Apache-2.0" }, "node_modules/chromium-bidi": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", - "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", + "version": "157.0.8090-0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-157.0.8090-0.tgz", + "integrity": "sha512-KJw7IQgqXwxpJgJlEIPg5mr2ZoqA+jkTRtc5EABI7sUJ3bu3cpI7NuEBoj4Gn1AsF78CaT9So5WQWspK3/wYag==", "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", - "zod": "^3.24.1" + "zod": "^4.6.5" }, "engines": { "node": ">=20.19.0 <22.0.0 || >=22.12.0" @@ -684,10 +684,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1687809", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", - "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", - "license": "BSD-3-Clause" + "version": "0.0.1696802", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1696802.tgz", + "integrity": "sha512-FO5/SENfvc2K8wRnWQflKcwuoTH4dBkXCWTQ9ebIWtuX8JM5+ZvCotaEmkfh21pO7TvIKGTM3hG4xFiq9WTwmg==", + "license": "BSD-3-Clause", + "peer": true }, "node_modules/dotenv": { "version": "18.0.3", @@ -1382,9 +1383,9 @@ } }, "node_modules/modern-tar": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.4.tgz", - "integrity": "sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g==", + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.6.tgz", + "integrity": "sha512-LYSks0tNVqbtez/9OB0YFP6qLRSqkdAxJWOQpVaR67lNTbj2zNLyYpowkRMia5lXLCZKzxwoxkxH2MbbBR2aEg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -1530,6 +1531,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz", "integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.14.1", "pg-pool": "^3.14.0", @@ -1686,18 +1688,19 @@ } }, "node_modules/puppeteer": { - "version": "25.12.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", - "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", + "version": "25.13.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.13.0.tgz", + "integrity": "sha512-vB4KLXTT5QqlikcOQ/xV0RY18bBVtNyLUTfqJ9mW+penHyoM+5DmANYuDCI4jgBtiL0eD9I5ZFSafW6/a6Trpg==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { - "@puppeteer/browsers": "3.2.3", - "chromium-bidi": "17.0.2", - "devtools-protocol": "0.0.1687809", + "@puppeteer/browsers": "3.2.4", + "chromium-bidi": "157.0.8090-0", + "devtools-protocol": "0.0.1696802", "lilconfig": "^3.1.3", - "puppeteer-core": "25.12.0", - "typed-query-selector": "^2.12.2" + "puppeteer-core": "25.13.0", + "typed-query-selector": "^2.12.3" }, "bin": { "puppeteer": "lib/puppeteer/node/cli.js" @@ -1707,16 +1710,16 @@ } }, "node_modules/puppeteer-core": { - "version": "25.12.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", - "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", + "version": "25.13.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.13.0.tgz", + "integrity": "sha512-cyBS0OEx3e9NS53B5F6+UazXtR7f9Tw0cR1/FbPW59/hV6JPZbkIiMnxK2543IkFp3rsGlLn1iPDLF77TJsJsA==", "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "3.2.3", - "chromium-bidi": "17.0.2", - "devtools-protocol": "0.0.1687809", - "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.3", + "@puppeteer/browsers": "3.2.4", + "chromium-bidi": "157.0.8090-0", + "devtools-protocol": "0.0.1696802", + "typed-query-selector": "^2.12.3", + "webdriver-bidi-protocol": "1.0.1", "ws": "^8.21.3" }, "engines": { @@ -1728,6 +1731,7 @@ "resolved": "https://registry.npmjs.org/puppeteer-extra/-/puppeteer-extra-3.3.6.tgz", "integrity": "sha512-rsLBE/6mMxAjlLd06LuGacrukP2bqbzKCLzV1vrhHFavqQE/taQ2UXv3H5P0Ls7nsrASa+6x3bDbXHpqMwq+7A==", "license": "MIT", + "peer": true, "dependencies": { "@types/debug": "^4.1.0", "debug": "^4.1.1", @@ -2093,9 +2097,9 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "license": "MIT" }, "node_modules/typedarray": { @@ -2141,9 +2145,9 @@ } }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", - "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-1.0.1.tgz", + "integrity": "sha512-WXWs/aXF2a6Al1Z2/V/7Gp2oPZOAAmHjJOTmv4KCXRJ122IMj3tSSMc6a4q3K4QOmhQbse/Ahr+oTVJOU2dUiw==", "license": "Apache-2.0" }, "node_modules/which": { @@ -2409,9 +2413,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/backend/package.json b/backend/package.json index 4165a4de9a..ee6294ee81 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,7 +12,7 @@ "ffprobe-static": "^3.1.0", "js-yaml": "^5.4.2", "pg": "^8.23.1", - "puppeteer": "^25.12.0", + "puppeteer": "^25.13.0", "puppeteer-extra": "^3.3.6", "puppeteer-extra-plugin-stealth": "^2.11.2", "winston": "^3.19.0" diff --git a/docs/evidence/formal-map-invariants-20261023-v1/artifact-map.json b/docs/evidence/formal-map-invariants-20261023-v1/artifact-map.json index c2cd8edc21..bb32b2c61f 100644 --- a/docs/evidence/formal-map-invariants-20261023-v1/artifact-map.json +++ b/docs/evidence/formal-map-invariants-20261023-v1/artifact-map.json @@ -1,6 +1,6 @@ { "schemaVersion": 2, - "sourceRevision": "a62683bae44ce8d2036e7352f9ea7d40d8efd0e5", + "sourceRevision": "6c81d79485af9c4c571ecf74ed1de428af10c0da", "reviewedSourceRevision": "fe4b73dac510d4670517c7d3eca4f0c2470d4297", "files": [ { @@ -33,25 +33,40 @@ "bytes": 10566, "sha256": "2fcc009f3454d2eb0f2fc327467994f4be434ba7fca83409896c3022b8e8f0f3" }, - { - "path": "docs/evidence/formal-map-invariants-20261023-v1/artifact-map.sha256", - "bytes": 84, - "sha256": "80bc1113ecca8de769e597beb1d4d2f3a0c3fa6205f65877eadb3e3cf12aafc8" - }, { "path": "docs/evidence/formal-map-invariants-20261023-v1/boundary-counterexamples.txt", "bytes": 3251, "sha256": "34658edffd3b18a8faed0c2605ccf0ff348b113d2cef8079277caf1137d3de46" }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/ci-publication-version-after.txt", + "bytes": 3164, + "sha256": "ef16d813127a9f239c6b526455217c0e61ff76e695f4ba1822bfa5f52c752540" + }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/ci-publication-version-before.txt", + "bytes": 1821, + "sha256": "5b4176b643c20fd7a94db3bde8594dcc24ef2266cc3371fe29e7979806a6ac19" + }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/ci-publication-version-followup.json", + "bytes": 3646, + "sha256": "a057f45dde6d805f541429d93220b651df34c87e321574ae4b018012427dc509" + }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/ci-publication-version-parity.txt", + "bytes": 428, + "sha256": "5e9d180726647fa910a7e1e3cc090589778499806f327f66698a633b30e367ec" + }, { "path": "docs/evidence/formal-map-invariants-20261023-v1/completion-manifest.json", - "bytes": 4508, - "sha256": "83d52da4988a314885db88139db363e30f36674c19b8c16a939a8c9cd3e54ba2" + "bytes": 4985, + "sha256": "dbf3898a2f3ffd1033deabe342adae88024863a3292ed3141d8136609514192b" }, { "path": "docs/evidence/formal-map-invariants-20261023-v1/delta-resolution.md", - "bytes": 11212, - "sha256": "293db98248947898fb6e2e34a7c32bb3ac2255715c8ea6d2960ed772c106a16e" + "bytes": 13832, + "sha256": "98013e1c52924d315d6f4c7af4cfb31b41b1a1b9e1f769d92d5d81389086e9e9" }, { "path": "docs/evidence/formal-map-invariants-20261023-v1/external-review-checkpoint.json", @@ -229,39 +244,34 @@ "sha256": "7549fdaea4bb23290958732ca72a3d8682ce72f871d1bbd02a8d0493fbb99d08" }, { - "path": "docs/evidence/formal-map-invariants-20261023-v1/source_bundle.txt", - "bytes": 374176, - "sha256": "c3fb6be051942230565e8e551f6f9988d6c0da2175c2a9df553a5398e6865f23" + "path": "docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-after.txt", + "bytes": 6426, + "sha256": "d4a21066f9be9819aa82b486be40039d42fe58fb7acf3b564cce29690a5fee66" }, { - "path": "docs/evidence/formal-map-invariants-20261023-v1/state-transitions.mmd", - "bytes": 1790, - "sha256": "5fc488c0a304aaf927a49845c696fbefa080193f3a7eb56ffa1af91181b7962f" + "path": "docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-before.txt", + "bytes": 2313, + "sha256": "eaabbd45722a383b4e9779131455c60e808e8f02042388e6b57a7d6f6af0910f" }, { - "path": "docs/evidence/formal-map-invariants-20261023-v1/verification.json", - "bytes": 1262, - "sha256": "2cdf058cf42b4745eaae400b810c5ef99bded33d1e4b4478952eaa644bd00023" + "path": "docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-followup.json", + "bytes": 3017, + "sha256": "fd9a748630577c968614a6bef370f7628569a0284f49884772c7dda78c33aefa" }, { - "path": "docs/evidence/formal-map-invariants-20261023-v1/ci-publication-version-followup.json", - "bytes": 3646, - "sha256": "a057f45dde6d805f541429d93220b651df34c87e321574ae4b018012427dc509" - }, - { - "path": "docs/evidence/formal-map-invariants-20261023-v1/ci-publication-version-before.txt", - "bytes": 1821, - "sha256": "5b4176b643c20fd7a94db3bde8594dcc24ef2266cc3371fe29e7979806a6ac19" + "path": "docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-lint.txt", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, { - "path": "docs/evidence/formal-map-invariants-20261023-v1/ci-publication-version-after.txt", - "bytes": 3164, - "sha256": "ef16d813127a9f239c6b526455217c0e61ff76e695f4ba1822bfa5f52c752540" + "path": "docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-parity.txt", + "bytes": 510, + "sha256": "44476130b7876580f7395aba77cd43c63a22add832a4df4806e7308c4aad1b6d" }, { - "path": "docs/evidence/formal-map-invariants-20261023-v1/ci-publication-version-parity.txt", - "bytes": 428, - "sha256": "5e9d180726647fa910a7e1e3cc090589778499806f327f66698a633b30e367ec" + "path": "docs/evidence/formal-map-invariants-20261023-v1/sdk-loss-after-tests.txt", + "bytes": 1703, + "sha256": "01fbf99cbc8ad58f6f0c1551180c3229d1897244ea2b10fbc909c1622785f7fe" }, { "path": "docs/evidence/formal-map-invariants-20261023-v1/sdk-loss-followup.json", @@ -269,41 +279,96 @@ "sha256": "a02f836d75c3989df322eb447ed97ce369325e593e98eaf9e1083a9dade9db02" }, { - "path": "docs/evidence/formal-map-invariants-20261023-v1/sdk-loss-after-tests.txt", - "bytes": 1703, - "sha256": "01fbf99cbc8ad58f6f0c1551180c3229d1897244ea2b10fbc909c1622785f7fe" + "path": "docs/evidence/formal-map-invariants-20261023-v1/security-zod-audits.json", + "bytes": 782, + "sha256": "229ba6a342936e990fcc89c3406036a33c3600ccc3d2e301f1f81ca5df9c80b2" + }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/security-zod-backend-after.txt", + "bytes": 1355, + "sha256": "bb122b10142c32bf7465bbc7abe7ed1010f2bf6d029054bb73153b2acb3a7311" + }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/security-zod-boundary-after.txt", + "bytes": 1623, + "sha256": "a5a9e25f215981b177920e22bcb768bae255d42dd7f632bed1ab9876cae25e23" + }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/security-zod-boundary-before.txt", + "bytes": 4868, + "sha256": "018a33ae2d03e1036842bc825b1681985d042df2f9740125e8b85d55fa23d28f" + }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/security-zod-browser-after.txt", + "bytes": 3491, + "sha256": "0f0074260d1d9623a50e7a7fc10957bb22d7994bc88b03923334e6339764b252" + }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/security-zod-followup.json", + "bytes": 4060, + "sha256": "d1ceebfd3e09a4125cf281d2a050712736dcdd2f455daa451b6587d11b645441" + }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/security-zod-parity.txt", + "bytes": 510, + "sha256": "37fb6ab3b4e890c48f4b453789414525dd0fd7adb18297f51d5e0a4b2efc7772" + }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/security-zod-record-readback-after.txt", + "bytes": 351, + "sha256": "51111e4c40444157a57c50387986423a2212061af93a2f5233684568648cae8c" + }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/source_bundle.txt", + "bytes": 374176, + "sha256": "c3fb6be051942230565e8e551f6f9988d6c0da2175c2a9df553a5398e6865f23" + }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/state-transitions.mmd", + "bytes": 1790, + "sha256": "5fc488c0a304aaf927a49845c696fbefa080193f3a7eb56ffa1af91181b7962f" + }, + { + "path": "docs/evidence/formal-map-invariants-20261023-v1/verification.json", + "bytes": 1262, + "sha256": "2cdf058cf42b4745eaae400b810c5ef99bded33d1e4b4478952eaa644bd00023" } ], "sourceFiles": [ { - "path": "apps/web/lib/marker-pool.ts", - "bytes": 14532, - "sha256": "7541c064ab4190abc0fc7500ee7d5f91ac862ca4e3429b56b8b9b59659c0db4e" + "path": "apps/web/bun.lock", + "bytes": 248263, + "sha256": "73e0359f0c5527258e482fa407ca31d94fd44f3a34bb1b3a956bc28873af760f" }, { - "path": "apps/web/lib/retained-marker-accessibility.ts", - "bytes": 3837, - "sha256": "a3833232541307a61e5a914ac0ad96d6c804ded7e086cf084339e68ddab76cc4" + "path": "apps/web/components/map/NaverMapView.tsx", + "bytes": 172218, + "sha256": "da5dd1884a9fa027027711307477723352c25781fd696498d7915954140da5b3" }, { - "path": "apps/web/lib/naver-map-event-cleanup.ts", - "bytes": 1747, - "sha256": "ee4c8bef6103c98b75238cc45eb2f119b1f5eab01202fb44979b05007c3957d6" + "path": "apps/web/components/restaurant/RestaurantEvidenceCarousel.tsx", + "bytes": 5742, + "sha256": "c58ecf950b911ee600063b291329a406654c4d7846ea8da9502c98869779399e" }, { - "path": "apps/web/lib/youtube-url.ts", - "bytes": 3682, - "sha256": "2dfac7f47bd227540c89250229d1c9fc29eb2cdcdc0aad50e63144b783251faa" + "path": "apps/web/constants/overseas-regions.ts", + "bytes": 3087, + "sha256": "4d986e8cb4b9166ec56ed9d12b3d8d32db2ed63b861a772888c7e8b319fde4af" }, { - "path": "apps/web/lib/evidence-engineering/popularity-marker.ts", - "bytes": 2969, - "sha256": "65fd5cc14b916e92720dfdaf81f20545b8caf3d8b3e56d9c06a4e4a9baea49d6" + "path": "apps/web/hooks/use-restaurants.tsx", + "bytes": 30154, + "sha256": "2313e3e7f76ad9e3137bd75aac525238809c3ffa120c36e55d856dddf396b1a7" }, { - "path": "apps/web/lib/evidence-engineering/scoring.ts", - "bytes": 7062, - "sha256": "0e3f195bf06636d6a8718378ac7a93eccbd3f439ee5d6ab5eebedbbaa6d02276" + "path": "apps/web/lib/evidence-engineering/constants.ts", + "bytes": 7408, + "sha256": "471834208104d092365b8203350e7b9c4aba6a716a602f2a29589361d8fc9a43" + }, + { + "path": "apps/web/lib/evidence-engineering/errors.ts", + "bytes": 2008, + "sha256": "02bda67b89fadf3d2f5aa27f6a146c0dc746a7de1f9a6cd91dc82cf177e901c9" }, { "path": "apps/web/lib/evidence-engineering/frames.ts", @@ -316,19 +381,19 @@ "sha256": "5ba23587a7efd71f83867875292eb0255f5d5d34daeca98ca8418623961127a4" }, { - "path": "apps/web/lib/evidence-engineering/constants.ts", - "bytes": 7408, - "sha256": "471834208104d092365b8203350e7b9c4aba6a716a602f2a29589361d8fc9a43" + "path": "apps/web/lib/evidence-engineering/popularity-marker.ts", + "bytes": 2969, + "sha256": "65fd5cc14b916e92720dfdaf81f20545b8caf3d8b3e56d9c06a4e4a9baea49d6" }, { - "path": "apps/web/lib/evidence-engineering/errors.ts", - "bytes": 2008, - "sha256": "02bda67b89fadf3d2f5aa27f6a146c0dc746a7de1f9a6cd91dc82cf177e901c9" + "path": "apps/web/lib/evidence-engineering/published-popularity.ts", + "bytes": 5687, + "sha256": "84ee9979ace9756456178449f6d831fe796cee0d68d66e155fc127aaef4163f1" }, { - "path": "apps/web/lib/evidence-engineering/types.ts", - "bytes": 3726, - "sha256": "9d64ad2c6e57bea45af5066d89a26535b616429c9032d16fccfefcdeb78bad6b" + "path": "apps/web/lib/evidence-engineering/scoring.ts", + "bytes": 7062, + "sha256": "0e3f195bf06636d6a8718378ac7a93eccbd3f439ee5d6ab5eebedbbaa6d02276" }, { "path": "apps/web/lib/evidence-engineering/storage-key.mjs", @@ -336,24 +401,34 @@ "sha256": "4f49025a1c93baf0f732e3ab57fb8b576a3857dab4d4a9910534b21358df3148" }, { - "path": "apps/web/lib/evidence-engineering/published-popularity.ts", - "bytes": 5687, - "sha256": "84ee9979ace9756456178449f6d831fe796cee0d68d66e155fc127aaef4163f1" + "path": "apps/web/lib/evidence-engineering/types.ts", + "bytes": 3726, + "sha256": "9d64ad2c6e57bea45af5066d89a26535b616429c9032d16fccfefcdeb78bad6b" }, { - "path": "apps/web/scripts/build-public-popularity.ts", - "bytes": 5900, - "sha256": "566113925e5c6a8598b28be3bf84ef53e7e525c4d727977475d8693082bb7eeb" + "path": "apps/web/lib/home-map-keyboard-navigation.ts", + "bytes": 4019, + "sha256": "b1f9d240f88138920fcc87ed6487459c634b00982494ed0d1346c6f12d21cd8c" }, { - "path": "apps/web/hooks/use-restaurants.tsx", - "bytes": 30154, - "sha256": "2313e3e7f76ad9e3137bd75aac525238809c3ffa120c36e55d856dddf396b1a7" + "path": "apps/web/lib/marker-pool.ts", + "bytes": 14532, + "sha256": "7541c064ab4190abc0fc7500ee7d5f91ac862ca4e3429b56b8b9b59659c0db4e" }, { - "path": "apps/web/types/restaurant.ts", - "bytes": 3942, - "sha256": "ef43ac1b93a751cb03fed1fcabecacf21d245e0b54f47a66dd67639c34ebd94b" + "path": "apps/web/lib/naver-map-cluster-visuals.ts", + "bytes": 5849, + "sha256": "e6827ce69615a202ca866aa5ced193b78d8b16c3ab9f44d3abb665e7b94f4d92" + }, + { + "path": "apps/web/lib/naver-map-event-cleanup.ts", + "bytes": 1747, + "sha256": "ee4c8bef6103c98b75238cc45eb2f119b1f5eab01202fb44979b05007c3957d6" + }, + { + "path": "apps/web/lib/naver-map-marker-visuals.ts", + "bytes": 9365, + "sha256": "6b73821b062691fc9d1139448bca5ff7c690379e1ba9e50322f8b7c3a9ac83b8" }, { "path": "apps/web/lib/overseas-region-matching.ts", @@ -361,14 +436,44 @@ "sha256": "35333c22a6a6b677a7612ffe461774a54a84f9a8c6fa3e324885104a8d072c4f" }, { - "path": "apps/web/constants/overseas-regions.ts", - "bytes": 3087, - "sha256": "4d986e8cb4b9166ec56ed9d12b3d8d32db2ed63b861a772888c7e8b319fde4af" + "path": "apps/web/lib/profile-avatar-url.ts", + "bytes": 9151, + "sha256": "cc3c22cea33d2e2ea89ab52e3264baac3b406956aac2dbc4b671a07fe219a039" }, { - "path": "apps/web/tests-formal/marker-state-model.test.mjs", - "bytes": 14175, - "sha256": "160f8b703aa906a01e03008980429aea859b1b222c17003740d8658322f9cc47" + "path": "apps/web/lib/retained-marker-accessibility.ts", + "bytes": 3837, + "sha256": "a3833232541307a61e5a914ac0ad96d6c804ded7e086cf084339e68ddab76cc4" + }, + { + "path": "apps/web/lib/review-photo-url.ts", + "bytes": 14933, + "sha256": "811a669cada5b4938afc012a2e0f26ca82e080522306cfd424c2892cb979187e" + }, + { + "path": "apps/web/lib/visible-marker-review-bubbles.ts", + "bytes": 11862, + "sha256": "96ab311a5582f4498d13da0347973ba94cad2575418394f78036501b99218ddb" + }, + { + "path": "apps/web/lib/youtube-url.ts", + "bytes": 3682, + "sha256": "2dfac7f47bd227540c89250229d1c9fc29eb2cdcdc0aad50e63144b783251faa" + }, + { + "path": "apps/web/package-lock.json", + "bytes": 503832, + "sha256": "e161fb64f78d4105f5077bce74fb63c1651672cfa4923d79c11d19b06e52ae8d" + }, + { + "path": "apps/web/package.json", + "bytes": 7264, + "sha256": "5fe39c74cda34f0a121a3a158625b02ef3de136c3628f96fee2cd0c6accffc19" + }, + { + "path": "apps/web/scripts/build-public-popularity.ts", + "bytes": 5900, + "sha256": "566113925e5c6a8598b28be3bf84ef53e7e525c4d727977475d8693082bb7eeb" }, { "path": "apps/web/scripts/verify-marker-formal.mjs", @@ -376,14 +481,34 @@ "sha256": "53c6676d8f9bbc357a6e1df273892cb7723aba9dcc1a218aeef0807fc8e202a9" }, { - "path": "apps/web/tests-unit/evidence-numeric-bounds.test.ts", - "bytes": 1575, - "sha256": "7bccbf8f20109bc5b87f1d77a6687167f12e988d0040523f6fe5a0c152298d02" + "path": "apps/web/tests-formal/marker-state-model.test.mjs", + "bytes": 14175, + "sha256": "160f8b703aa906a01e03008980429aea859b1b222c17003740d8658322f9cc47" }, { - "path": "apps/web/tests-unit/popularity-input-boundary.test.ts", - "bytes": 5525, - "sha256": "9b2b12533448cbb60b92adfaf27d6e559afb78868fc2d860a34a20c536c43d78" + "path": "apps/web/tests-unit/admin-detail-close-focus-browser.test.ts", + "bytes": 7881, + "sha256": "4874bd723e16b00dca0f3c5c86934ada90a35c4f67fb9db4270a596ec0660ca1" + }, + { + "path": "apps/web/tests-unit/admin-record-action-failure-once-browser.test.ts", + "bytes": 11064, + "sha256": "f9feb84f45694cbb058d3d83eccb036c32bdc17abc4ea16cfbf01cbc9f0bb783" + }, + { + "path": "apps/web/tests-unit/admin-storyboard-unknown-count-browser.test.ts", + "bytes": 4729, + "sha256": "2c87a824284f9cdfef872641fee05c6c46a75448603605a972015be6ae230a93" + }, + { + "path": "apps/web/tests-unit/admin-storyboard-worker-visible-status-browser.test.ts", + "bytes": 5624, + "sha256": "06ccb04bb53c241a6e5c2905f4558fcfb469db0ff32359df686ef3f93ff7b237" + }, + { + "path": "apps/web/tests-unit/evidence-carousel-navigation-browser.test.ts", + "bytes": 7470, + "sha256": "e28e8aee88cad1bc15cb3e19b5601e45bb41f32b6e2171dec116e12502a5546b" }, { "path": "apps/web/tests-unit/evidence-cohort-shadow.test.ts", @@ -391,79 +516,116 @@ "sha256": "abce3ecd887e5fbb89bbac7715a9ce71753935367a502e6ec96f464eee585dac" }, { - "path": "apps/web/package.json", - "bytes": 7264, - "sha256": "a434a12086453effcdc577a0c04c5e213037b327ba04b29f48b5d1bc65a0aad5" - }, - { - "path": "apps/web/tsconfig.json", - "bytes": 1368, - "sha256": "cdbd2b57d55c77ea1ef2b81124b7007fec7df15d55cc917c8c9eca5704234c49" + "path": "apps/web/tests-unit/evidence-numeric-bounds.test.ts", + "bytes": 1575, + "sha256": "7bccbf8f20109bc5b87f1d77a6687167f12e988d0040523f6fe5a0c152298d02" }, { - "path": "apps/web/components/map/NaverMapView.tsx", - "bytes": 172218, - "sha256": "da5dd1884a9fa027027711307477723352c25781fd696498d7915954140da5b3" + "path": "apps/web/tests-unit/evidence-publication-runtime.test.ts", + "bytes": 6657, + "sha256": "d9c475863c41902724e8309ea5893e3bfbd81109ba83470b13c44226adec4b01" }, { - "path": "apps/web/lib/visible-marker-review-bubbles.ts", - "bytes": 11862, - "sha256": "96ab311a5582f4498d13da0347973ba94cad2575418394f78036501b99218ddb" + "path": "apps/web/tests-unit/local-storyboard-catalog-error-browser.test.ts", + "bytes": 7813, + "sha256": "474ef3f3afea7463743b5dcfc9d7000159b229a3f867ea2cae383de7d3c4d4cd" }, { - "path": "apps/web/lib/review-photo-url.ts", - "bytes": 14933, - "sha256": "811a669cada5b4938afc012a2e0f26ca82e080522306cfd424c2892cb979187e" + "path": "apps/web/tests-unit/local-storyboard-control-focus-browser.test.ts", + "bytes": 6438, + "sha256": "659f0a00fd445121847756f03c768d1e0811025e84b2ff61c8915d1eede49428" }, { - "path": "apps/web/lib/profile-avatar-url.ts", - "bytes": 9151, - "sha256": "cc3c22cea33d2e2ea89ab52e3264baac3b406956aac2dbc4b671a07fe219a039" + "path": "apps/web/tests-unit/local-storyboard-departure-browser.test.ts", + "bytes": 18016, + "sha256": "c1111a3e91f45c465ba3666ec5285384b59afde6395e318c45111b6eba317dda" }, { - "path": "apps/web/lib/naver-map-marker-visuals.ts", - "bytes": 9365, - "sha256": "6b73821b062691fc9d1139448bca5ff7c690379e1ba9e50322f8b7c3a9ac83b8" + "path": "apps/web/tests-unit/local-storyboard-history-focus-browser.test.ts", + "bytes": 13440, + "sha256": "72843ddc04e3bd83aaa4a36ea706517ddf9175fdbb41e60f648339fe8ecd5d82" }, { - "path": "apps/web/tests-unit/visible-marker-review-bubbles.test.ts", - "bytes": 3456, - "sha256": "de9884b661b31e3b06274eb1c17fa5ea176b73d2f557a529d7e4333178b6391e" + "path": "apps/web/tests-unit/local-storyboard-project-refresh-focus-browser.test.ts", + "bytes": 8406, + "sha256": "b8b82b95111a7a6b28b003f448fc94e8be7e22e1f15df7c634ecf88f33ec685b" }, { - "path": "apps/web/lib/naver-map-cluster-visuals.ts", - "bytes": 5849, - "sha256": "e6827ce69615a202ca866aa5ced193b78d8b16c3ab9f44d3abb665e7b94f4d92" + "path": "apps/web/tests-unit/local-storyboard-worker-refresh-browser.test.ts", + "bytes": 5678, + "sha256": "55bd2fa6dcc2d0cf2b18576706354a9b2bdefdafd794fd7076ddcbc8423dd82b" }, { "path": "apps/web/tests-unit/marker-pool.test.ts", "bytes": 17824, "sha256": "68303094ee4dae127b3a0b9483169dccf02e5b4e17642f967830911c96400fa6" }, + { + "path": "apps/web/tests-unit/naver-map-cluster-visuals.test.ts", + "bytes": 5142, + "sha256": "dee3a5c6fbeb08d7d527aec187b32ac81a115cf55e692a0fe3808363c27a82e2" + }, + { + "path": "apps/web/tests-unit/naver-map-sdk-loss.test.ts", + "bytes": 3271, + "sha256": "624f75931280b2c9daa5dcbcefe41e2024a7f725f37cb9adb257781f0c27d474" + }, + { + "path": "apps/web/tests-unit/popularity-input-boundary.test.ts", + "bytes": 5525, + "sha256": "9b2b12533448cbb60b92adfaf27d6e559afb78868fc2d860a34a20c536c43d78" + }, { "path": "apps/web/tests-unit/retained-marker-accessibility.test.ts", "bytes": 8292, "sha256": "e0b3266209b2943eccc7672b3597db10df4faf358b304e3f1440235ea74fd1e3" }, { - "path": "apps/web/tests-unit/naver-map-cluster-visuals.test.ts", - "bytes": 5142, - "sha256": "dee3a5c6fbeb08d7d527aec187b32ac81a115cf55e692a0fe3808363c27a82e2" + "path": "apps/web/tests-unit/storyboard-project-library-browser.test.ts", + "bytes": 14700, + "sha256": "f3d073f6502f460fdbbad3e8aba703c3656696d29c9187d3773314c0ecc89676" }, { - "path": "apps/web/tests-unit/evidence-publication-runtime.test.ts", - "bytes": 6657, - "sha256": "d9c475863c41902724e8309ea5893e3bfbd81109ba83470b13c44226adec4b01" + "path": "apps/web/tests-unit/visible-marker-review-bubbles.test.ts", + "bytes": 3456, + "sha256": "de9884b661b31e3b06274eb1c17fa5ea176b73d2f557a529d7e4333178b6391e" }, { - "path": "apps/web/tests-unit/naver-map-sdk-loss.test.ts", - "bytes": 3271, - "sha256": "624f75931280b2c9daa5dcbcefe41e2024a7f725f37cb9adb257781f0c27d474" + "path": "apps/web/tests-unit/zod-collection-size-boundary.test.ts", + "bytes": 1864, + "sha256": "a54bcff00f2a4efd7e28504035ec5c70738a6cbbbce71a09b9095c6cd85285c2" + }, + { + "path": "apps/web/tsconfig.json", + "bytes": 1368, + "sha256": "cdbd2b57d55c77ea1ef2b81124b7007fec7df15d55cc917c8c9eca5704234c49" + }, + { + "path": "apps/web/types/restaurant.ts", + "bytes": 3942, + "sha256": "ef43ac1b93a751cb03fed1fcabecacf21d245e0b54f47a66dd67639c34ebd94b" + }, + { + "path": "backend/bin/tests/node-dependency-compatibility.test.mjs", + "bytes": 9538, + "sha256": "d7932a181e050488286285b65f2e8875af9000db617b4caede0ae3c8d3c75080" + }, + { + "path": "backend/package-lock.json", + "bytes": 89955, + "sha256": "b7838f95e50d29dfa929ddf6d6ee7eca3beaf710cdf0159b1c3d32b23bb8e14c" + }, + { + "path": "backend/package.json", + "bytes": 591, + "sha256": "de686dcad2bd1fbeaf4cee88cef598d5c1e03980a98fbd88072635b74397dc7f" } ], "scope": "Supplied-scope ordinary delta and native fixes complete at a1dd; routine CI expectation fix at ciTestRevision. New exact-head CI, hosted SDK/UI, protected release and Nightly remain separate.", "selfReference": "artifact-map.json and its detached digest are intentionally excluded to avoid a circular or stale digest claim", "ciTestRevision": "1c0461bb114e9f6085d33c85a0eb911d0a98cab9", "formalRuntimeRevision": "a1dd40016ff7cf23055073c8637d5b3ee00d6781", - "priorDeliveryMapSha256": "d89227abc214f5aa132d7b2d061eee85b5eb2cef39338b4a3c3475a5c71914eb" + "priorDeliveryMapSha256": "b676fcd90f069892502e9b2c29579b33aa6f3a17717a897e20e85f1cd707ea23", + "securityRevision": "1ca3d984598e7caca575ceeca4118ee418da60cf", + "sceneNavigationRevision": "6c81d79485af9c4c571ecf74ed1de428af10c0da" } diff --git a/docs/evidence/formal-map-invariants-20261023-v1/artifact-map.sha256 b/docs/evidence/formal-map-invariants-20261023-v1/artifact-map.sha256 index 1d9ca7e68d..4238c22cca 100644 --- a/docs/evidence/formal-map-invariants-20261023-v1/artifact-map.sha256 +++ b/docs/evidence/formal-map-invariants-20261023-v1/artifact-map.sha256 @@ -1 +1 @@ -40d42e185c01afe01a39176c423d816c1c389343ea71c52c67c4215fc9e27213 artifact-map.json +fc77d22a5e2067dc509d7837c9f54577ec7900a0af501a5090d927739b2e4081 artifact-map.json diff --git a/docs/evidence/formal-map-invariants-20261023-v1/completion-manifest.json b/docs/evidence/formal-map-invariants-20261023-v1/completion-manifest.json index e803077290..23a52b1393 100644 --- a/docs/evidence/formal-map-invariants-20261023-v1/completion-manifest.json +++ b/docs/evidence/formal-map-invariants-20261023-v1/completion-manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "status": "SCOPED_EXTERNAL_DELTA_AND_VALID_LOCAL_FINDINGS_CLOSED_CI_HOST_RELEASE_PENDING", "overallComplete": false, - "sourceRevision": "a62683bae44ce8d2036e7352f9ea7d40d8efd0e5", + "sourceRevision": "6c81d79485af9c4c571ecf74ed1de428af10c0da", "baseRevision": "56427320a44fdd65a86dc49e7e63d80dd9935c2e", "findingIds": [ "FORMAL-01", @@ -122,5 +122,17 @@ "runtimeRevision": "a62683bae44ce8d2036e7352f9ea7d40d8efd0e5", "receipt": "sdk-loss-followup.json", "status": "NATIVE_FIXED_EXACT_HOST_RECHECK_PENDING" + }, + "securityFollowup": { + "finding": "SEC-F-01", + "sourceRevision": "1ca3d984598e7caca575ceeca4118ee418da60cf", + "receipt": "security-zod-followup.json", + "status": "LOCAL_PATCH_VERIFIED_NEW_CI_PREVIEW_RELEASE_PENDING" + }, + "sceneNavigationFollowup": { + "finding": "UI-F-02", + "sourceRevision": "6c81d79485af9c4c571ecf74ed1de428af10c0da", + "receipt": "scene-navigation-followup.json", + "status": "LOCAL_NATIVE_INPUT_FIXED_NEW_HOST_AND_CI_PENDING" } } diff --git a/docs/evidence/formal-map-invariants-20261023-v1/delta-resolution.md b/docs/evidence/formal-map-invariants-20261023-v1/delta-resolution.md index 6013aca965..f8b895396a 100644 --- a/docs/evidence/formal-map-invariants-20261023-v1/delta-resolution.md +++ b/docs/evidence/formal-map-invariants-20261023-v1/delta-resolution.md @@ -50,3 +50,13 @@ Related 23 tests in three files / 903 assertions, scoped lint, and parity 0 diag ## SDK-F-01: SDK revoked after load The exact develop preview `e725f566` reached READY on 2026-10-23T22:56:27Z, then the PC domestic detail reached the root error boundary at 22:57:16Z: its retained map outlived `window.naver.maps`. The deployed chunk maps the null `Event` access to the selection/centering effect. The actual effect regression reproduced three failing unavailable-SDK cases and one passing available-SDK control. Runtime `a62683bae44ce8d2036e7352f9ea7d40d8efd0e5` checks current SDK capabilities before any selection bookkeeping or map call. Related 15 tests / four files / 45 assertions, scoped lint and parity 0 diagnostics / 3,382 inputs pass. See `sdk-loss-followup.json` and the raw after-run. This does not alter the pool proof contract or replace its source-bound reports. Exact new host checks and protected production release remain pending; no production write or rollback occurred. + +## SEC-F-01: patched collection admission and backend browser dependency + +Security run `40039695632` failed npm audit on both package graphs. Maintainer advisory GHSA-mrhx-c7xc-m3w5 requires Zod >=4.6.6. Source `53b79a97721840d2d2c4acda2799994cd02a608d` uses web Zod4.6.6 (matching npm/Bun integrity) and minimal Puppeteer25.13.0 with its maintained BiDi dependency and patched transitive Zod4.6.7. No incompatible override is used. Six bounded oversize cases failed before the update by visiting all101 elements; all12 boundary/control cases now pass (30 assertions). Both npm audits report zero vulnerabilities. Related browser29/10files/182assertions and backend12/0fail pass after correcting absent browser build-env substitution and macOS temp-path identity in their offline fixtures. Scoped lint and parity0diagnostics/3388inputs pass. The initial broadened local run is not claimed as a new full-suite pass. See `security-zod-followup.json` and retained raw evidence. + +The in-progress production candidate9887 was canceled before custom aliases moved; current production remained4fa0. This was no operational rollback. New exact-source CI, preview and production verification remain pending. Prior formal source/report bindings and separate historical Nightly failure remain unchanged. Independent readback also found the prior91-entry map contents valid but its detached digest stale (40d42e versus actual143b2173). This delivery records both and regenerates the detached digest after all map metadata is finalized. + +## UI-F-02: scene input incorrectly navigated restaurants + +Exact previewe8b52464 reproduced a focused-scene ArrowRight changing the selected restaurant. The strip lacked the carousel semantic already expected by the parent touch guard; global desktop navigation also did not recognize that target. Source `6c81d79485af9c4c571ecf74ed1de428af10c0da` supplies the existing ownership semantic and keyboard exclusion, preserving native scrolling and ordinary navigation outside the strip. A valid overflowing UTF-8 fixture with the actual component, global keyboard helper and extracted unchanged parent touch handlers fails three cases at the original source (desktop keyboard navigation; mobile/tablet parent gesture ownership). Afterward, native Chrome touch scrolling and outside-area positive controls pass at1440/390/820. Related52tests/7files/313assertions, scoped lint and parity0diagnostics/3388inputs pass. See `scene-navigation-followup.json`. An earlier bad-charset fixture is excluded, not a product rendering failure. New-source CI and hosted/production input readback remain pending. diff --git a/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-after.txt b/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-after.txt new file mode 100644 index 0000000000..8649acf300 --- /dev/null +++ b/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-after.txt @@ -0,0 +1,72 @@ +bun test v1.4.0 (34cbb9a40) + +tests-unit/evidence-carousel-navigation-browser.test.ts: +(pass) scene navigation keeps the restaurant selected at 1440px [1862.56ms] +(pass) scene navigation keeps the restaurant selected at 390px [1686.45ms] +(pass) scene navigation keeps the restaurant selected at 820px [1673.18ms] + +tests-unit/evidence-frame-failure-browser.test.ts: +(pass) context-only frames keep an honest heading and original video link after image failure, and recover for a new source [2446.62ms] + +tests-unit/home-map-keyboard-navigation.test.ts: +(pass) home map keyboard navigation guards > returns step for ArrowRight when guard passes [0.09ms] +(pass) home map keyboard navigation guards > returns step for ArrowLeft when guard passes [0.01ms] +(pass) home map keyboard navigation guards > returns null for editable target [0.02ms] +(pass) home map keyboard navigation guards > returns null for interactive target [0.02ms] +(pass) home map keyboard navigation guards > returns null when modifiers or composition are active [0.04ms] +(pass) home map keyboard navigation guards > recognizes contenteditable target [0.01ms] +(pass) home map keyboard adjacent restaurant selection > selects next and previous restaurant by step [0.06ms] +(pass) home map keyboard adjacent restaurant selection > returns null on boundary and never selects outside current list (mode boundary safety) [0.03ms] +(pass) desktop arrow event handler > moves to next and previous restaurants according to arrow key when enabled [0.05ms] +(pass) desktop arrow event handler > does not process when desktop=false [0.01ms] +(pass) desktop arrow event handler > does not process when panel is closed +(pass) desktop arrow event handler > does not process when no current restaurant exists [0.01ms] +(pass) desktop arrow event handler > does not process when swipeable count is 1 +(pass) desktop arrow event handler > calls preventDefault only when movement happened [0.03ms] + +tests-unit/evidence-frame-conflict.test.ts: +(pass) frame conflicts fail closed at both public read boundaries while strict publication validation still rejects [1.21ms] + +tests-unit/evidence-engineering-remaining.test.ts: +(pass) evidence engineering remaining tasks > uploads only the immutable webp key and refuses collisions [0.47ms] +(pass) evidence engineering remaining tasks > moderation requires preview, confirm, apply, and readback before an audit record [0.32ms] +(pass) evidence engineering remaining tasks > public carousel keeps approved sign evidence and its seek url [0.35ms] +(pass) evidence engineering remaining tasks > operational caution ignores a plain search miss and does not use device coordinates [0.04ms] +(pass) evidence engineering remaining tasks > marker wrapper stays on transform and opacity [0.39ms] +(pass) evidence engineering remaining tasks > merged media unions approved frames and does not promote a higher tier [0.14ms] +(pass) evidence engineering remaining tasks > missing evidence embed falls back without dropping restaurants [0.19ms] +(pass) evidence engineering remaining tasks > badge viewer stays closed until experimental mode is opened and can close again [0.11ms] +(pass) evidence engineering remaining tasks > open cross-check clauses stay locked without new formulas [0.54ms] +(pass) evidence engineering remaining tasks > render summary records percentiles without a 60fps gate [0.05ms] + +tests-unit/home-map-swipe-restaurants.test.ts: +(pass) home map swipe restaurant dedupe > matches the pairwise reference over randomized fixtures [8.18ms] +(pass) home map swipe restaurant dedupe > matches the reference when merged ids point backwards and forwards [0.17ms] +(pass) home map swipe restaurant dedupe > treats the same id as a duplicate and keeps the first occurrence [0.02ms] +(pass) home map swipe restaurant dedupe > drops a later restaurant that shares name and coordinates [0.02ms] +(pass) home map swipe restaurant dedupe > keeps restaurants with the same name but coordinates past the tolerance [0.02ms] +(pass) home map swipe restaurant dedupe > treats null, zero, and non-numeric coordinates as not comparable [0.02ms] +(pass) home map swipe restaurant dedupe > maps an empty list, a single row, and falsy rows without throwing [0.03ms] +(pass) home map swipe restaurant dedupe > isSameRestaurantForSwipe keeps the pairwise contract [0.06ms] +(pass) home map swipe restaurant dedupe > hasSameSwipeCoordinates is false when either coordinate is missing [0.02ms] + +tests-unit/mobile-home-map-regressions.test.ts: +(pass) mobile home map regression guards > search-selected restaurant can still swipe to a different visible restaurant [0.04ms] +(pass) mobile home map regression guards > marker render is not skipped when another marker takes over after a search selection [0.14ms] +(pass) mobile home map regression guards > marker render is not skipped when the sheet closes and a new marker opens with cleared search state [0.02ms] +(pass) mobile home map regression guards > single-visible post-search swipe falls back to the nearest restaurant [0.21ms] +(pass) mobile home map regression guards > search ownership release keeps the current detail selection active [0.04ms] +(pass) mobile home map regression guards > peek-state downward sheet gesture dismisses only after intentional distance or speed [0.02ms] +(pass) mobile home map regression guards > blank map taps collapse the sheet, enter fullscreen map, then restore the peek sheet [0.02ms] +(pass) mobile home map regression guards > blank map fullscreen toggle is inactive outside a mobile restaurant panel +(pass) mobile home map regression guards > cluster marker zoom jumps immediately without Naver morph animation [0.41ms] +(pass) mobile home map regression guards > mobile restaurant detail sheet peeks on map interaction without skipping fullscreen tap sequence [0.70ms] +(pass) mobile home map regression guards > mobile visible marker sheet receives map interaction epoch before requesting peek height [0.26ms] +(pass) mobile home map regression guards > G002 mobile category sheet commits immediately without delayed apply affordance [0.20ms] +(pass) mobile home map regression guards > G012 mobile sheets and details own fixed map-control safe area [0.14ms] +(pass) mobile home map regression guards > home detail route history uses app-owned list/detail states and browser back restoration contracts [0.15ms] + + 52 pass + 0 fail + 313 expect() calls +Ran 52 tests across 7 files. [7.98s] diff --git a/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-before.txt b/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-before.txt new file mode 100644 index 0000000000..bb8f9865cf --- /dev/null +++ b/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-before.txt @@ -0,0 +1,50 @@ +bun test v1.4.0 (34cbb9a40) + +tests-unit/scene-navigation-original-source-probe.test.ts: +84 | const count = () => page.evaluate(() => (window as unknown as {sceneFixture:{count():number}}).sceneFixture.count()); +85 | const starts = () => page.evaluate(() => (window as unknown as {sceneFixture:{starts():number}}).sceneFixture.starts()); +86 | const reset = () => page.evaluate(() => (window as unknown as {sceneFixture:{reset():void}}).sceneFixture.reset()); +87 | if (width === 1440) { +88 | await scroller.focus(); await page.keyboard.press('ArrowRight'); +89 | expect(await count()).toBe(0); + ^ +error: expect(received).toBe(expected) + +Expected: 0 +Received: 1 + + at (/Users/twoimo/.codex/worktrees/marker-evidence-live-20261021/tzudong/apps/web/tests-unit/scene-navigation-original-source-probe.test.ts:89:31) +(fail) scene navigation keeps the restaurant selected at 1440px [1164.82ms] +103 | await page.waitForTimeout(20); +104 | } +105 | await session.send('Input.dispatchTouchEvent', {type:'touchEnd',touchPoints:[]}); +106 | } +107 | await swipe('[aria-label="영상 장면 가로 스크롤"]'); +108 | expect(await starts()).toBe(0); + ^ +error: expect(received).toBe(expected) + +Expected: 0 +Received: 1 + + at (/Users/twoimo/.codex/worktrees/marker-evidence-live-20261021/tzudong/apps/web/tests-unit/scene-navigation-original-source-probe.test.ts:108:30) +(fail) scene navigation keeps the restaurant selected at 390px [1373.73ms] +103 | await page.waitForTimeout(20); +104 | } +105 | await session.send('Input.dispatchTouchEvent', {type:'touchEnd',touchPoints:[]}); +106 | } +107 | await swipe('[aria-label="영상 장면 가로 스크롤"]'); +108 | expect(await starts()).toBe(0); + ^ +error: expect(received).toBe(expected) + +Expected: 0 +Received: 1 + + at (/Users/twoimo/.codex/worktrees/marker-evidence-live-20261021/tzudong/apps/web/tests-unit/scene-navigation-original-source-probe.test.ts:108:30) +(fail) scene navigation keeps the restaurant selected at 820px [1375.01ms] + + 0 pass + 3 fail + 15 expect() calls +Ran 3 tests across 1 file. [4.21s] diff --git a/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-followup.json b/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-followup.json new file mode 100644 index 0000000000..8f0be5094c --- /dev/null +++ b/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-followup.json @@ -0,0 +1,42 @@ +{ + "finding": "UI-F-02", + "sourceRevision": "6c81d79485af9c4c571ecf74ed1de428af10c0da", + "observedPreviewSource": "e8b52464bfc55941980a20f370d5d54b491504d5", + "observedPreviewDeployment": "dpl_3FYe1FyvRGykmsPGMKL3ybbUvgjs", + "observedAt": "2026-10-24T00:16:59.383Z", + "observedFailure": "ArrowRight on the focused 1440px scene scroller invokes the global restaurant keyboard handler, changes selection away from Ilmi, and prevents native horizontal scrolling. Actual URL/title/screenshot retained privately.", + "cause": "The scene strip had tabindex=0 but no carousel ownership semantic. The existing panel touch-start guard checks aria-roledescription=carousel, and the global keyboard target guard omitted carousels.", + "fix": "Give the scene strip role=region and aria-roledescription=carousel; recognize the same carousel ownership in the existing keyboard guard. Preserve default native scrolling and restaurant navigation outside that strip.", + "nativeBefore": { + "source": "e8b52464bfc55941980a20f370d5d54b491504d5", + "cases": 3, + "failed": 3, + "desktop": "Actual keyboard helper navigated once from the focused scene strip.", + "mobileAndTablet": "Actual extracted panel touch-start handler claimed the scene gesture (starts=1). This does not claim every native overflow gesture changed the restaurant; browsers may cancel touch dispatch during scrolling.", + "fixture": "UTF-8 HTML, real overflowing carousel, actual component/global keyboard helper and exact parent touch declarations, native Chrome CDP touch events; all external requests blocked." + }, + "after": { + "tests": 52, + "files": 7, + "assertions": 313, + "failed": 0, + "viewportWidths": [ + 1440, + 390, + 820 + ], + "nativeGesture": "Inside scene strip: native scrolling advances, parent touch-start count=0 and restaurant navigation count=0. Outside strip: normal restaurant navigation still fires once.", + "imageErrorBoundary": "Real 404 fallback and new-source image recovery remain passing.", + "lint": "PASS", + "parityDiagnostics": 0, + "logicalInputs": 3388 + }, + "oracleCorrection": "An initial offline fixture omitted UTF-8 HTML charset, so its Korean CSS selector did not apply. That invalid layout attempt is retained privately and excluded. The corrected fixture asserts actual overflow before each input test.", + "priorSecurity": "80dbce08 source CI6/6, Security6/6 and CodeQL6/6 actually settled success. Full local tests4226pass/12skip/0fail are historical for that source. The new input-boundary change uses its own targeted checks and new delivery CI.", + "externalReview": "Routine reproduced input-boundary defect; retain all completed source-bound formal reports without duplicate dispatch. MarkerPool/SDK ownership contracts and frozen publications unchanged.", + "newHostReverification": "PENDING", + "protectedRelease": "PENDING", + "productionChanged": false, + "overallComplete": false, + "nightly": "SEPARATE_HISTORICAL_FAILURE_39457197351_NO_RERUN" +} diff --git a/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-lint.txt b/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-lint.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-parity.txt b/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-parity.txt new file mode 100644 index 0000000000..483f88d47f --- /dev/null +++ b/docs/evidence/formal-map-invariants-20261023-v1/scene-navigation-parity.txt @@ -0,0 +1,5 @@ + +> web@1.2.4 typecheck:parity +> node scripts/run-typecheck.mjs --compiler parity + +{"status":"passed","diagnostics":0,"logicalInputs":3388,"logicalInputNamesSha256":"c7ec56809642317b67b6229b867b3da299d8ccdf8bacd6bab18bfdbb37d5f20a","nonLibraryContentSha256":"36496046bb0d14dad78ed4c9a30b937d71af147631906b0907175525bffb88b5","standardLibraryContentSha256":{"native":"d328ef5dcbac7d4c183cd37a26eb383e8e6921ab2147b20e5d73f3c149223a64","compat":"82afdd90154e9b315a83eb66f2d4ee59972c492094bbf66d7f0791f584e41eb0"}} diff --git a/docs/evidence/formal-map-invariants-20261023-v1/security-zod-audits.json b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-audits.json new file mode 100644 index 0000000000..dda054b6b1 --- /dev/null +++ b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-audits.json @@ -0,0 +1,44 @@ +{ + "web": { + "metadata": { + "vulnerabilities": { + "info": 0, + "low": 0, + "moderate": 0, + "high": 0, + "critical": 0, + "total": 0 + }, + "dependencies": { + "prod": 401, + "dev": 421, + "optional": 175, + "peer": 26, + "peerOptional": 0, + "total": 920 + } + }, + "vulnerabilities": {} + }, + "backend": { + "metadata": { + "vulnerabilities": { + "info": 0, + "low": 0, + "moderate": 0, + "high": 0, + "critical": 0, + "total": 0 + }, + "dependencies": { + "prod": 204, + "dev": 0, + "optional": 2, + "peer": 4, + "peerOptional": 0, + "total": 209 + } + }, + "vulnerabilities": {} + } +} diff --git a/docs/evidence/formal-map-invariants-20261023-v1/security-zod-backend-after.txt b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-backend-after.txt new file mode 100644 index 0000000000..477d5fb153 --- /dev/null +++ b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-backend-after.txt @@ -0,0 +1,20 @@ +✔ map crawler loads js-yaml named exports before the no-channel exit (107.11875ms) +✔ transcript crawler loads YAML and dotenv without starting collection (73.4885ms) +✔ URL extractor import preserves JSON stdout and existing environment (131.319916ms) +✔ build_google_maps_browser_review_queue.mjs emits one JSON document with a local empty DB double (64.866458ms) +✔ build_supabase_address_consistency_guarded_plan.mjs emits one JSON document with a local empty DB double (70.924375ms) +✔ build_tzuyang_address_evidence_ledger.mjs emits one JSON document with a local empty DB double (68.070167ms) +✔ validate_supabase_review_queue_live.mjs preserves JSON stdout for an empty local queue (65.884666ms) +✔ validate_supabase_same_origin_candidates.mjs preserves JSON stdout for an empty local queue (59.405458ms) +✔ apply ledger environment loader stays silent without executing apply (59.032792ms) +✔ GenAI caller imports and consumed SDK methods remain available offline (99.163791ms) +✔ brace expansion remains compatible and bounded through node_modules/minimatch/package.json (171.113917ms) +✔ brace expansion remains compatible and bounded through node_modules/gaxios/node_modules/minimatch/package.json (205.507333ms) +ℹ tests 12 +ℹ suites 0 +ℹ pass 12 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 1233.92925 diff --git a/docs/evidence/formal-map-invariants-20261023-v1/security-zod-boundary-after.txt b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-boundary-after.txt new file mode 100644 index 0000000000..c711360b94 --- /dev/null +++ b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-boundary-after.txt @@ -0,0 +1,20 @@ +bun test v1.4.0 (34cbb9a40) + +tests-unit/zod-collection-size-boundary.test.ts: +(pass) collection size admission before element validation > array.max rejects oversize without visiting elements [1.52ms] +(pass) collection size admission before element validation > array.max still parses a collection within its limit [0.18ms] +(pass) collection size admission before element validation > array.length rejects oversize without visiting elements [0.19ms] +(pass) collection size admission before element validation > array.length still parses a collection within its limit [0.04ms] +(pass) collection size admission before element validation > set.max rejects oversize without visiting elements [0.23ms] +(pass) collection size admission before element validation > set.max still parses a collection within its limit [0.08ms] +(pass) collection size admission before element validation > set.size rejects oversize without visiting elements [0.09ms] +(pass) collection size admission before element validation > set.size still parses a collection within its limit [0.07ms] +(pass) collection size admission before element validation > map.max rejects oversize without visiting elements [0.29ms] +(pass) collection size admission before element validation > map.max still parses a collection within its limit [0.15ms] +(pass) collection size admission before element validation > map.size rejects oversize without visiting elements [0.13ms] +(pass) collection size admission before element validation > map.size still parses a collection within its limit [0.04ms] + + 12 pass + 0 fail + 30 expect() calls +Ran 12 tests across 1 file. [61.00ms] diff --git a/docs/evidence/formal-map-invariants-20261023-v1/security-zod-boundary-before.txt b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-boundary-before.txt new file mode 100644 index 0000000000..90b8591e42 --- /dev/null +++ b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-boundary-before.txt @@ -0,0 +1,98 @@ +bun test v1.4.0 (34cbb9a40) + +tests-unit/zod-collection-size-boundary.test.ts: +19 | const item = z.number().superRefine(() => { visited += 1; }); +20 | const result = schema(item).safeParse(input(101)); +21 | +22 | expect(result.success).toBe(false); +23 | expect(result.error?.issues[0]?.code).toBe('too_big'); +24 | expect(visited).toBe(0); + ^ +error: expect(received).toBe(expected) + +Expected: 0 +Received: 101 + + at (/Users/twoimo/.codex/worktrees/marker-evidence-live-20261021/tzudong/apps/web/tests-unit/zod-collection-size-boundary.test.ts:24:23) +(fail) collection size admission before element validation > array.max rejects oversize without visiting elements [1.55ms] +(pass) collection size admission before element validation > array.max still parses a collection within its limit [0.11ms] +19 | const item = z.number().superRefine(() => { visited += 1; }); +20 | const result = schema(item).safeParse(input(101)); +21 | +22 | expect(result.success).toBe(false); +23 | expect(result.error?.issues[0]?.code).toBe('too_big'); +24 | expect(visited).toBe(0); + ^ +error: expect(received).toBe(expected) + +Expected: 0 +Received: 101 + + at (/Users/twoimo/.codex/worktrees/marker-evidence-live-20261021/tzudong/apps/web/tests-unit/zod-collection-size-boundary.test.ts:24:23) +(fail) collection size admission before element validation > array.length rejects oversize without visiting elements [0.24ms] +(pass) collection size admission before element validation > array.length still parses a collection within its limit [0.09ms] +19 | const item = z.number().superRefine(() => { visited += 1; }); +20 | const result = schema(item).safeParse(input(101)); +21 | +22 | expect(result.success).toBe(false); +23 | expect(result.error?.issues[0]?.code).toBe('too_big'); +24 | expect(visited).toBe(0); + ^ +error: expect(received).toBe(expected) + +Expected: 0 +Received: 101 + + at (/Users/twoimo/.codex/worktrees/marker-evidence-live-20261021/tzudong/apps/web/tests-unit/zod-collection-size-boundary.test.ts:24:23) +(fail) collection size admission before element validation > set.max rejects oversize without visiting elements [0.43ms] +(pass) collection size admission before element validation > set.max still parses a collection within its limit [0.10ms] +19 | const item = z.number().superRefine(() => { visited += 1; }); +20 | const result = schema(item).safeParse(input(101)); +21 | +22 | expect(result.success).toBe(false); +23 | expect(result.error?.issues[0]?.code).toBe('too_big'); +24 | expect(visited).toBe(0); + ^ +error: expect(received).toBe(expected) + +Expected: 0 +Received: 101 + + at (/Users/twoimo/.codex/worktrees/marker-evidence-live-20261021/tzudong/apps/web/tests-unit/zod-collection-size-boundary.test.ts:24:23) +(fail) collection size admission before element validation > set.size rejects oversize without visiting elements [0.29ms] +(pass) collection size admission before element validation > set.size still parses a collection within its limit [0.04ms] +19 | const item = z.number().superRefine(() => { visited += 1; }); +20 | const result = schema(item).safeParse(input(101)); +21 | +22 | expect(result.success).toBe(false); +23 | expect(result.error?.issues[0]?.code).toBe('too_big'); +24 | expect(visited).toBe(0); + ^ +error: expect(received).toBe(expected) + +Expected: 0 +Received: 101 + + at (/Users/twoimo/.codex/worktrees/marker-evidence-live-20261021/tzudong/apps/web/tests-unit/zod-collection-size-boundary.test.ts:24:23) +(fail) collection size admission before element validation > map.max rejects oversize without visiting elements [0.61ms] +(pass) collection size admission before element validation > map.max still parses a collection within its limit [0.10ms] +19 | const item = z.number().superRefine(() => { visited += 1; }); +20 | const result = schema(item).safeParse(input(101)); +21 | +22 | expect(result.success).toBe(false); +23 | expect(result.error?.issues[0]?.code).toBe('too_big'); +24 | expect(visited).toBe(0); + ^ +error: expect(received).toBe(expected) + +Expected: 0 +Received: 101 + + at (/Users/twoimo/.codex/worktrees/marker-evidence-live-20261021/tzudong/apps/web/tests-unit/zod-collection-size-boundary.test.ts:24:23) +(fail) collection size admission before element validation > map.size rejects oversize without visiting elements [0.26ms] +(pass) collection size admission before element validation > map.size still parses a collection within its limit [0.03ms] + + 6 pass + 6 fail + 30 expect() calls +Ran 12 tests across 1 file. [64.00ms] diff --git a/docs/evidence/formal-map-invariants-20261023-v1/security-zod-browser-after.txt b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-browser-after.txt new file mode 100644 index 0000000000..9fd15f84ae --- /dev/null +++ b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-browser-after.txt @@ -0,0 +1,55 @@ +bun test v1.4.0 (34cbb9a40) + +tests-unit/local-storyboard-departure-browser.test.ts: +(pass) project/new/sidebar/Escape guards preserve the draft; accepted discard works [732.22ms] +(pass) unchanged editor closes without confirmation; browser back cancellation retains B and its URL [696.11ms] +(pass) in-flight save blocks departure, then a matching response/readback permits clean navigation [632.64ms] +(pass) lost save response plus failed GET retains draft and uncertainty; only explicit retry writes [773.26ms] +(pass) a committed save with lost POST response is confirmed by GET without a duplicate write [542.62ms] +(pass) a structurally valid wrong POST view cannot clear the edit or claim it saved [591.45ms] +(pass) an unrelated newer revision preserves the uncertain draft and blocks stale retry [577.15ms] +(pass) later readback of an earlier save rebases without dropping newer local edits [654.10ms] + +tests-unit/local-storyboard-catalog-error-browser.test.ts: +(pass) failed catalog read is not reported as a database hold on the new-project screen (1280px) [606.96ms] +(pass) failed catalog read is not reported as a database hold on the new-project screen (390px) [556.07ms] +(pass) failed catalog read keeps an open project locked with a reload path instead of a hold notice [594.15ms] +(pass) a genuine database hold is still reported as a hold [585.73ms] + +tests-unit/admin-detail-close-focus-browser.test.ts: +(pass) admin operations and sentry details return focus without losing it to the page [2700.19ms] + +tests-unit/local-storyboard-worker-refresh-browser.test.ts: +(pass) an active storyboard job refreshes worker connection without a manual catalog reload [6626.69ms] + +tests-unit/local-storyboard-project-refresh-focus-browser.test.ts: +(pass) storyboard project refresh keeps keyboard focus while the saved project reloads [979.09ms] +(pass) a failed storyboard project read stays unread while refreshing [889.23ms] + +tests-unit/storyboard-project-library-browser.test.ts: +(pass) library filters, clears filters, sorts newest first and opens detail with the keyboard [582.15ms] +(pass) library keeps loaded rows during refresh errors and exposes recovery and loading [1302.06ms] +(pass) held readiness preserves project reads while blocking create and edit mutations [560.10ms] +(pass) choosing a library row retains unsaved edits until departure is explicitly confirmed [817.22ms] +(pass) library and selected detail fit 390px with real CSS [535.26ms] +(pass) library and selected detail fit 834px with real CSS [519.85ms] +(pass) library and selected detail fit 1423px with real CSS [530.55ms] + +tests-unit/local-storyboard-history-focus-browser.test.ts: +(pass) version history keeps keyboard focus when picking, failing a preview and restoring [1391.94ms] +(pass) failed version history stays visible while the list refresh is in flight [1406.53ms] +(pass) visible version history keeps its status while the list is read again [1161.93ms] + +tests-unit/admin-storyboard-worker-visible-status-browser.test.ts: +(pass) a visible storyboard worker keeps its connection status while the list is read again [2015.45ms] + +tests-unit/admin-storyboard-unknown-count-browser.test.ts: +(pass) a failed storyboard catalog does not show zero saved projects [1708.89ms] + +tests-unit/local-storyboard-control-focus-browser.test.ts: +(pass) storyboard cancel, new project and a failed library pick keep a keyboard target [1192.29ms] + + 29 pass + 0 fail + 182 expect() calls +Ran 29 tests across 10 files. [38.79s] diff --git a/docs/evidence/formal-map-invariants-20261023-v1/security-zod-followup.json b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-followup.json new file mode 100644 index 0000000000..85b34f9d57 --- /dev/null +++ b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-followup.json @@ -0,0 +1,68 @@ +{ + "finding": "SEC-F-01", + "sourceRevision": "1ca3d984598e7caca575ceeca4118ee418da60cf", + "checkedAt": "2026-10-23T23:51:13.926272+00:00", + "failure": { + "workflow": 40039695632, + "sha": "5afda8b8546a8f298dc843fb6e7219c17c71d910", + "job": "npm-audit", + "advisory": "GHSA-mrhx-c7xc-m3w5", + "directWeb": "zod 4.6.5", + "backendTransitive": "chromium-bidi 17.0.2 -> zod 3.25.76" + }, + "sources": [ + "https://github.com/advisories/GHSA-mrhx-c7xc-m3w5", + "https://github.com/colinhacks/zod/commit/a0ebf97", + "https://github.com/puppeteer/puppeteer/releases/tag/puppeteer-v25.13.0" + ], + "fix": { + "web": "zod 4.6.6; npm and Bun lock integrity match", + "backend": "puppeteer 25.13.0; chromium-bidi 157.0.8090-0; transitive zod 4.6.7", + "npmAuthority": "11.6.2 / Node24", + "override": "No incompatible major-version Zod override" + }, + "verification": { + "sizeAdmissionBefore": "6 pass / 6 fail; rejected collections still visited all 101 elements", + "sizeAdmissionAfter": "12 pass / 30 assertions; six oversized collection shapes visit zero elements; six valid controls parse normally", + "browserFixturesAfter": "29 pass / 10 files / 182 assertions", + "backendCompatibilityAfter": "12 pass / 0 fail; isolated callers, no external I/O", + "npmAudit": "web and backend zero vulnerabilities", + "lint": "PASS scoped changed TypeScript tests", + "parity": "0 diagnostics / 3388 inputs", + "fullLocalSuite": "Initial broadened general run: 3953 pass, 4 skip, 17 fail. Sixteen were absent browser process.env build inputs; the remaining readback fixture omitted required submissions.items. Targeted browser 29/182 plus readback2/10 pass after corrections. Fresh full suite running; no full pass claimed.", + "recordReadbackFixtureAfter": "2 pass / 10 assertions; unchanged real component, complete submissions/items API fixture" + }, + "testHarnessCorrections": [ + "Browser-only fixtures replace process.env with a literal empty object and NODE_ENV test; site-config public build inputs otherwise access absent Node process. Real product code unchanged.", + "Canonical realpath for macOS temp fixture: argv /var and import.meta.url /private/var previously prevented direct-entry guards executing. All 12 actual caller checks now pass.", + "Keep the submission readback fixture complete with items: []; production correctly retains its fence when that API response is incomplete." + ], + "candidateCancellation": { + "deploymentId": "dpl_Ed4A6bwgLVeebfexV9DXHh77Twxt", + "sha": "9887f816cf5847f8f8e1c609fdafe9f8cfc74a2c", + "state": "CANCELED", + "phase": "Before production aliases moved", + "operationalRollback": false, + "productionReadbackSha": "4fa0edb9d0cb4ae10c09f8aafaec443a3f7a556c" + }, + "artifactCorrection": { + "priorMapSha256": "143b2173c4598d7e7433950cc71e98635ad9c1d6dcafef4375313e4ead3a4714", + "priorDetachedDigest": "40d42e185c01afe01a39176c423d816c1c389343ea71c52c67c4215fc9e27213", + "matched": false, + "priorCommittedEntriesVerified": 91, + "decision": "The old map included its detached digest despite declaring that excluded. Current map explicitly excludes both map and digest entries before serialization; final committed bytes are independently verified.", + "excludedSelfPaths": [ + "docs/evidence/formal-map-invariants-20261023-v1/artifact-map.json", + "docs/evidence/formal-map-invariants-20261023-v1/artifact-map.sha256" + ] + }, + "priorFormalRuntimeRevision": "a1dd40016ff7cf23055073c8637d5b3ee00d6781", + "hostGuardRuntimeRevision": "a62683bae44ce8d2036e7352f9ea7d40d8efd0e5", + "externalReview": "Completed source-bound ordinary reports preserved. This maintained dependency patch and verification-harness corrections do not replace or rerun them.", + "newPreview": "PENDING", + "protectedRelease": "PENDING", + "productionVerification": "NOT_RUN_FOR_THIS_SOURCE", + "overallComplete": false, + "nightly": "SEPARATE_HISTORICAL_FAILURE_39457197351_NO_RERUN", + "dependencyRevision": "53b79a97721840d2d2c4acda2799994cd02a608d" +} diff --git a/docs/evidence/formal-map-invariants-20261023-v1/security-zod-parity.txt b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-parity.txt new file mode 100644 index 0000000000..0f273b7f0e --- /dev/null +++ b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-parity.txt @@ -0,0 +1,5 @@ + +> web@1.2.4 typecheck:parity +> node scripts/run-typecheck.mjs --compiler parity + +{"status":"passed","diagnostics":0,"logicalInputs":3388,"logicalInputNamesSha256":"c7ec56809642317b67b6229b867b3da299d8ccdf8bacd6bab18bfdbb37d5f20a","nonLibraryContentSha256":"a783e421675243cee9fb35893f2e01df33871adca12afef106be772d50a5b40b","standardLibraryContentSha256":{"native":"d328ef5dcbac7d4c183cd37a26eb383e8e6921ab2147b20e5d73f3c149223a64","compat":"82afdd90154e9b315a83eb66f2d4ee59972c492094bbf66d7f0791f584e41eb0"}} diff --git a/docs/evidence/formal-map-invariants-20261023-v1/security-zod-record-readback-after.txt b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-record-readback-after.txt new file mode 100644 index 0000000000..82601f35b5 --- /dev/null +++ b/docs/evidence/formal-map-invariants-20261023-v1/security-zod-record-readback-after.txt @@ -0,0 +1,10 @@ +bun test v1.4.0 (34cbb9a40) + +tests-unit/admin-record-action-failure-once-browser.test.ts: +(pass) a record action failure shown in its dialog is not repeated as a toast [2759.14ms] +(pass) after a confirmed apply removes the row, keyboard focus returns to the list area [2108.40ms] + + 2 pass + 0 fail + 10 expect() calls +Ran 2 tests across 1 file. [5.18s]