import type { ImhotepId, StateKind, Environment, ExecutionContext } from 'imhotep-core' import { getDefaultContext } from 'imhotep-core' // --------------------------------------------------------------------------- // Snapshot Identity // --------------------------------------------------------------------------- /** Stable identifier for a geometry snapshot. */ export type SnapshotId = ImhotepId // Module-level monotonic counter ensures unique ids even when deterministic // test contexts return identical clock/idGenerator values per call. let _snapshotCounter = 0 /** Generates a unique snapshot id with an optional prefix. * When no context is provided, uses the global default context. * A module-level counter guarantees uniqueness across consecutive calls. */ export function createSnapshotId(prefix = 'snap', ctx?: ExecutionContext): SnapshotId { const context = ctx ?? getDefaultContext() const counter = ++_snapshotCounter return `${prefix}_${context.clock()}_${context.idGenerator()}_${counter}` } // --------------------------------------------------------------------------- // State Source Classification // --------------------------------------------------------------------------- /** * Describes how a state was materialized. * * - interaction: Real Playwright interaction (hover, focus, mousedown). * - css-pseudo: Synthetic emulation via forced CSS pseudo-class styles. * - synthetic: Computed projection without browser interaction. * - component-hook: Materialized via a component adapter hook. */ export type StateSource = 'interaction' | 'css-pseudo' | 'synthetic' | 'component-hook' // --------------------------------------------------------------------------- // Snapshot Metadata // --------------------------------------------------------------------------- /** * Metadata attached to every geometry snapshot so diagnostics can * trace which state, source, and environment produced it. */ export interface SnapshotMetadata { /** Unique id for this snapshot instance. */ snapshotId: SnapshotId /** The UI state kind that was materialized (hover, focus, etc). */ stateKind: StateKind /** How the state was produced (real interaction vs synthetic). */ stateSource: StateSource /** Selector of the element that received the state, if any. */ selector?: string /** Milliseconds since epoch when the snapshot was captured. */ timestamp: number /** Environment case active when the snapshot was taken. */ env?: Environment /** Duration of the transition sample in ms, if applicable. */ durationMs?: number /** Approximation note for diagnostics when state is not fully determinate. */ approximationNote?: string } // --------------------------------------------------------------------------- // Geometry Snapshot // --------------------------------------------------------------------------- /** * A captured geometry world for a specific UI state. * * The `world` property holds the extractor output (geometry world schema). * It is typed as `unknown` here because the exact schema is owned by the * extractor package; imhotep-state materializes and tags snapshots without * needing to inspect world internals. */ export interface GeometrySnapshot { /** Snapshot identifier used by the solver and diagnostics. */ id: SnapshotId /** State metadata for traceability and comparison. */ metadata: SnapshotMetadata /** The materialized geometry world produced by the extractor. */ world: unknown } // --------------------------------------------------------------------------- // Snapshot Comparison // --------------------------------------------------------------------------- /** Result of comparing two geometry snapshots. */ export interface SnapshotDiff { /** True when both snapshots have identical world and metadata. */ identical: boolean /** Human-readable list of detected differences. */ differences: string[] } /** * Deep-compares two geometry snapshots for equality. * * Used by tests and diagnostics to detect unexpected changes between * states (e.g. hover caused the element to shift). */ export function compareSnapshots(a: GeometrySnapshot, b: GeometrySnapshot): SnapshotDiff { const differences: string[] = [] if (a.id !== b.id) { differences.push(`id changed from "${a.id}" to "${b.id}"`) } if (a.metadata.stateKind !== b.metadata.stateKind) { differences.push( `stateKind changed from "${a.metadata.stateKind}" to "${b.metadata.stateKind}"` ) } if (a.metadata.stateSource !== b.metadata.stateSource) { differences.push( `stateSource changed from "${a.metadata.stateSource}" to "${b.metadata.stateSource}"` ) } if (a.metadata.selector !== b.metadata.selector) { differences.push( `selector changed from "${a.metadata.selector}" to "${b.metadata.selector}"` ) } // Compare world via JSON serialization for a stable deep equality check. // This is sufficient for unit tests and diagnostic diffs. const worldA = JSON.stringify(a.world) const worldB = JSON.stringify(b.world) if (worldA !== worldB) { differences.push('world content differs') } return { identical: differences.length === 0, differences, } } // --------------------------------------------------------------------------- // Snapshot Registry // --------------------------------------------------------------------------- /** * In-memory store for geometry snapshots during a test run. * * The materializer populates this store; the solver reads from it. */ export interface SnapshotStore { /** Add a snapshot to the store. */ add(snapshot: GeometrySnapshot): void /** Retrieve a snapshot by id. */ get(id: SnapshotId): GeometrySnapshot | undefined /** List all snapshots in insertion order. */ list(): GeometrySnapshot[] /** Remove all snapshots. */ clear(): void } /** Factory for a default Map-backed snapshot store. */ export function createSnapshotStore(): SnapshotStore { const store = new Map() return { add(snapshot) { store.set(snapshot.id, snapshot) }, get(id) { return store.get(id) }, list() { return Array.from(store.values()) }, clear() { store.clear() }, } }