export type CycleDirection = 'forward' | 'reverse'; export interface SafeViewport { safeX: number; safeY: number; width: number; height: number } export interface ViewportGeometry { offsetLeft: number; offsetTop: number; width: number; height: number } export interface SafeInsets { left: number; right: number; top: number; bottom: number } export interface TargetBox { left: number; top: number; right: number; bottom: number } type PointerAction = | { type: 'pointerMove'; duration: number; origin: 'viewport'; x: number; y: number } | { type: 'pointerDown'; button: 0 } | { type: 'pointerUp'; button: 0 } | { type: 'pause'; duration: number }; type KeyAction = | { type: 'keyDown'; value: string } | { type: 'keyUp'; value: string } | { type: 'pause'; duration: number }; export const roundHalfUp = (value: number): number => Math.floor(value + 0.5); export const computeSafeViewport = (viewport: ViewportGeometry, insets: SafeInsets): SafeViewport => { const width = viewport.width - insets.left - insets.right; const height = viewport.height - insets.top - insets.bottom; if (!(width > 0) || !(height > 0)) throw new Error('Safe viewport dimensions must be positive.'); return { safeX: viewport.offsetLeft + insets.left, safeY: viewport.offsetTop + insets.top, width, height, }; }; export const buildSwipeCycle = (direction: CycleDirection, safe: SafeViewport) => { const startFraction = direction === 'forward' ? 0.8 : 0.2; const delta = direction === 'forward' ? -0.7 : 0.7; const x = roundHalfUp(safe.safeX + 0.5 * safe.width); const actions: PointerAction[] = [ { type: 'pointerMove', duration: 0, origin: 'viewport', x, y: roundHalfUp(safe.safeY + safe.height * startFraction) }, { type: 'pointerDown', button: 0 }, ]; for (let index = 1; index <= 15; index += 1) { actions.push({ type: 'pointerMove', duration: 40, origin: 'viewport', x, y: roundHalfUp(safe.safeY + safe.height * (startFraction + delta * index / 15)), }); } actions.push({ type: 'pointerUp', button: 0 }, { type: 'pause', duration: 400 }); return { pointer: { type: 'pointer' as const, id: 'driveTouch' as const, parameters: { pointerType: 'touch' as const }, actions }, keyboard: { type: 'key' as const, id: 'driveKeys' as const, actions: [{ type: 'pause' as const, duration: 1_000 }] }, }; }; const driveSources = (pointerActions: PointerAction[], keyboardActions: KeyAction[]) => ({ pointer: { type: 'pointer' as const, id: 'driveTouch' as const, parameters: { pointerType: 'touch' as const }, actions: pointerActions, }, keyboard: { type: 'key' as const, id: 'driveKeys' as const, actions: keyboardActions }, }); export const buildTargetActivation = (first: TargetBox, second: TargetBox, safe: SafeViewport) => { for (const edge of ['left', 'top', 'right', 'bottom'] as const) { if (Math.abs(first[edge] - second[edge]) >= 1) throw new Error('Activation target box must remain stable across both samples.'); } const left = Math.max(safe.safeX, second.left); const top = Math.max(safe.safeY, second.top); const right = Math.min(safe.safeX + safe.width, second.right); const bottom = Math.min(safe.safeY + safe.height, second.bottom); if (!(right > left) || !(bottom > top)) throw new Error('Activation target must intersect the safe viewport.'); return driveSources([ { type: 'pointerMove', duration: 0, origin: 'viewport', x: roundHalfUp((left + right) / 2), y: roundHalfUp((top + bottom) / 2) }, { type: 'pause', duration: 60 }, { type: 'pointerDown', button: 0 }, { type: 'pause', duration: 60 }, { type: 'pointerUp', button: 0 }, ], [{ type: 'pause', duration: 120 }]); }; export const buildVolumeKeySequence = () => driveSources( [{ type: 'pause', duration: 16_040 }], [ { type: 'pause', duration: 15_900 }, { type: 'keyDown', value: '\uE004' }, { type: 'pause', duration: 20 }, { type: 'keyUp', value: '\uE004' }, { type: 'pause', duration: 80 }, { type: 'keyDown', value: '\uE012' }, { type: 'pause', duration: 40 }, { type: 'keyUp', value: '\uE012' }, ], ); export const chooseCycleDirection = ( direction: CycleDirection, rawScrollY: number, rawMaxScrollY: number, ): CycleDirection => { const maxScrollY = Math.max(0, rawMaxScrollY); const scrollY = Math.min(maxScrollY, Math.max(0, rawScrollY)); if (direction === 'forward' && scrollY >= maxScrollY - 2) return 'reverse'; if (direction === 'reverse' && scrollY <= 2) return 'forward'; return direction; }; const integerRange = (start: number, endExclusive: number): number[] => Array.from( { length: endExclusive - start }, (_, index) => start + index, ); export const physicalCycleOffsets = (): number[] => [ ...integerRange(30, 120), ...integerRange(125, 180), ...integerRange(275, 510).filter((offset) => offset !== 389 && offset !== 390), ]; interface ProtocolEvent { offsetMs: number; action: string; assertions: string[] } interface ProtocolBranch { offsetMs: number; action: string; assertions: string[] } export const buildPhysicalDriveProtocol = () => { const events: ProtocolEvent[] = [ { offsetMs: 0, action: 'begin-idle-portrait', assertions: ['one-canvas-loop', 'geometry'] }, { offsetMs: 15_000, action: 'activate-start-the-drive', assertions: ['state-by-15500'] }, { offsetMs: 15_900, action: 'volume-tab-keydown', assertions: [] }, { offsetMs: 15_920, action: 'volume-tab-keyup', assertions: ['slider-focused'] }, { offsetMs: 16_000, action: 'volume-arrow-left-keydown', assertions: [] }, { offsetMs: 16_040, action: 'volume-arrow-left-keyup', assertions: ['volume-35-by-16500'] }, ...physicalCycleOffsets().map((offset) => ({ offsetMs: offset * 1_000, action: 'scroll-cycle', assertions: offset === 391 ? ['sample-direction-and-safe-viewport', 'cancel-exact-pending-navigation-epoch', 'release-barrier'] : ['sample-direction-and-safe-viewport'], })), { offsetMs: 120_000, action: 'rotate-landscape', assertions: ['geometry-by-125000', 'one-canvas-loop', 'tier-at-most-maximum'] }, { offsetMs: 180_000, action: 'background', assertions: ['no-page-input-for-30s'] }, { offsetMs: 210_000, action: 'foreground', assertions: ['lifecycle-counts-by-215000'] }, { offsetMs: 225_000, action: 'audio-interruption', assertions: ['suspended-and-resume-sound-by-227000'] }, { offsetMs: 230_000, action: 'activate-resume-sound', assertions: ['state-by-230500'] }, { offsetMs: 250_000, action: 'context-loss', assertions: [] }, { offsetMs: 252_000, action: 'context-restore', assertions: ['one-running-generation-or-static-by-257000'] }, { offsetMs: 270_000, action: 'rotate-portrait', assertions: ['geometry-by-275000'] }, { offsetMs: 390_000, action: 'arm-navigation-settlement-barrier', assertions: [] }, { offsetMs: 390_000, action: 'navigate-infrastructure', assertions: ['one-navigation-epoch'] }, { offsetMs: 510_000, action: 'background', assertions: ['no-page-input-for-30s'] }, { offsetMs: 540_000, action: 'foreground', assertions: ['lifecycle-counts-by-545000'] }, { offsetMs: 575_000, action: 'navigate-contact', assertions: ['state-by-575500'] }, { offsetMs: 580_000, action: 'settle-contact', assertions: ['contact-usable', 'final-geometry'] }, { offsetMs: 600_000, action: 'finish', assertions: ['no-crash', 'no-duplicates', 'monotonic-quality'] }, ].sort((left, right) => left.offsetMs - right.offsetMs); const branches: { live: ProtocolBranch[]; static: ProtocolBranch[] } = { live: [ { offsetMs: 550_000, action: 'pause-motion', assertions: ['zero-scene-raf', 'retained-frame', 'audio-independent'] }, { offsetMs: 565_000, action: 'resume-motion', assertions: ['one-canvas-loop-by-570000'] }, ], static: [ { offsetMs: 550_000, action: 'assert-static-without-input', assertions: ['poster', 'zero-raf', 'motion-unavailable', 'audio-independent'] }, { offsetMs: 565_000, action: 'assert-static-without-input', assertions: ['unchanged-poster', 'zero-raf', 'controls-usable'] }, ], }; return { version: 'physical-drive.v1' as const, durationMs: 600_000, events, branches }; };