/** * Mock helpers for cross-package integration tests. * * Provides dependency-injected builders for geometry worlds, * extraction results, solver results, and complete mock scenes. */ import type { GeometryWorld, ClauseDescriptor, ClauseResult, } from 'imhotep-solver' import type { EvaluationOutput } from 'imhotep-solver' import type { GeometrySnapshot, SnapshotMetadata } from 'imhotep-state' import type { Diagnostic as CoreDiagnostic } from './diagnostics.js' import type { ImhotepId } from './types.js' import type { SemanticIr } from './ir.js' // --------------------------------------------------------------------------- // Geometry World Builder // --------------------------------------------------------------------------- export interface MockSubject { id: number selector: string box: { top: number bottom: number left: number right: number } } export interface MockFrame { id: number kind: string subjectId: number } export interface MockWorldOptions { sceneId?: string snapshotId?: string subjects: MockSubject[] frames?: MockFrame[] topology?: { nearestPositionedAncestorOf?: number[] scrollContainerOf?: number[] stackingContextOf?: number[] containingBlockOf?: number[] } } /** * Build a solver-compatible geometry world from a declarative description. */ export function buildMockGeometryWorld(opts: MockWorldOptions): GeometryWorld { const subjectCount = opts.subjects.length const world: GeometryWorld = { sceneId: opts.sceneId ?? 'scene_test', snapshotId: opts.snapshotId ?? 'snap_test', env: { viewportWidth: 1280, viewportHeight: 720, deviceScaleFactor: 1, colorScheme: 'light', pointer: 'fine', hover: false, reducedMotion: false, locale: 'en', writingMode: 'horizontal-tb', }, strings: { values: [] }, subjects: { ids: opts.subjects.map((s) => s.id), domNodeId: new Array(subjectCount).fill(0), subjectKind: new Array(subjectCount).fill(0), primaryBoxId: opts.subjects.map((_, i) => i), firstFragmentId: new Array(subjectCount).fill(0), fragmentCount: new Array(subjectCount).fill(0), }, dom: { nodeId: opts.subjects.map((s) => s.id), parentNodeId: new Array(subjectCount).fill(0), childCount: new Array(subjectCount).fill(0), tagNameStringId: new Array(subjectCount).fill(0), }, boxes: { boxId: opts.subjects.map((_, i) => i), subjectId: opts.subjects.map((s) => s.id), frameId: new Array(subjectCount).fill(0), borderLeft: opts.subjects.map((s) => s.box.left), borderTop: opts.subjects.map((s) => s.box.top), borderRight: opts.subjects.map((s) => s.box.right), borderBottom: opts.subjects.map((s) => s.box.bottom), paddingLeft: opts.subjects.map((s) => s.box.left), paddingTop: opts.subjects.map((s) => s.box.top), paddingRight: opts.subjects.map((s) => s.box.right), paddingBottom: opts.subjects.map((s) => s.box.bottom), contentLeft: opts.subjects.map((s) => s.box.left), contentTop: opts.subjects.map((s) => s.box.top), contentRight: opts.subjects.map((s) => s.box.right), contentBottom: opts.subjects.map((s) => s.box.bottom), }, rects: { rectId: opts.subjects.map((_, i) => i), left: opts.subjects.map((s) => s.box.left), top: opts.subjects.map((s) => s.box.top), right: opts.subjects.map((s) => s.box.right), bottom: opts.subjects.map((s) => s.box.bottom), }, topology: { containingBlockOf: opts.topology?.containingBlockOf ?? [], nearestPositionedAncestorOf: opts.topology?.nearestPositionedAncestorOf ?? [], scrollContainerOf: opts.topology?.scrollContainerOf ?? [], stackingContextOf: opts.topology?.stackingContextOf ?? [], formattingContextOf: [], clippingRootOf: [], paintOrderBucket: [], paintOrderIndex: [], }, scroll: { containerId: [], scrollLeft: [], scrollTop: [], scrollWidth: [], scrollHeight: [], clientWidth: [], clientHeight: [], }, visualBoxes: { boxId: opts.subjects.map((_, i) => i), subjectId: opts.subjects.map((s) => s.id), frameId: new Array(subjectCount).fill(0), borderLeft: opts.subjects.map((s) => s.box.left), borderTop: opts.subjects.map((s) => s.box.top), borderRight: opts.subjects.map((s) => s.box.right), borderBottom: opts.subjects.map((s) => s.box.bottom), paddingLeft: opts.subjects.map((s) => s.box.left), paddingTop: opts.subjects.map((s) => s.box.top), paddingRight: opts.subjects.map((s) => s.box.right), paddingBottom: opts.subjects.map((s) => s.box.bottom), contentLeft: opts.subjects.map((s) => s.box.left), contentTop: opts.subjects.map((s) => s.box.top), contentRight: opts.subjects.map((s) => s.box.right), contentBottom: opts.subjects.map((s) => s.box.bottom), }, transforms: { transformId: [], subjectId: [], matrixStart: [], matrixLength: [], originX: [], originY: [], }, matrices: { values: [], }, clipping: { clipNodeId: [], subjectId: [], clipKind: [], clipLeft: [], clipTop: [], clipRight: [], clipBottom: [], parentClipNodeId: [], }, visibility: { subjectId: [], isRendered: [], isVisible: [], visibleArea: [], clippedArea: [], }, } return world } // --------------------------------------------------------------------------- // Clause Descriptor Builder // --------------------------------------------------------------------------- export function buildClauseDescriptor(opts: { clauseId: string clauseKind: string subjectRef: number referenceRef?: number frameRef?: number bounds?: { minGap?: number; maxGap?: number; tolerance?: number; min?: number; max?: number } options?: Record }): ClauseDescriptor { return { clauseId: opts.clauseId, clauseKind: opts.clauseKind, version: 1, subjectRef: opts.subjectRef, referenceRef: opts.referenceRef, frameRef: opts.frameRef, bounds: opts.bounds ?? {}, options: opts.options ?? {}, } } // --------------------------------------------------------------------------- // Solver Result Builder // --------------------------------------------------------------------------- export function buildMockSolverResult(opts: { clauseResults?: ClauseResult[] diagnostics?: Array<{ code: string severity: 'error' | 'warning' | 'info' category: string message: string clauseId?: string }> }): EvaluationOutput { return { clauseResults: opts.clauseResults ?? [], groupResults: [], proofs: [], diagnostics: opts.diagnostics ?? [], trace: [], } } // --------------------------------------------------------------------------- // Extraction Result Builder // --------------------------------------------------------------------------- export function buildMockExtractionResult(opts: { requestId?: string worlds: GeometryWorld[] diagnostics?: CoreDiagnostic[] }): { requestId: string status: 'ok' | 'partial' | 'error' worlds: GeometryWorld[] diagnostics: CoreDiagnostic[] } { return { requestId: opts.requestId ?? 'req_1', status: 'ok', worlds: opts.worlds, diagnostics: opts.diagnostics ?? [], } } // --------------------------------------------------------------------------- // Scene Builder // --------------------------------------------------------------------------- export interface MockScene { world: GeometryWorld clauses: ClauseDescriptor[] } export function createMockScene(opts: { subjects: MockSubject[] relations: Array<{ kind: string subjectId: number referenceId: number bounds?: { minGap?: number; maxGap?: number } }> }): MockScene { const world = buildMockGeometryWorld({ subjects: opts.subjects }) const clauses = opts.relations.map((r, i) => buildClauseDescriptor({ clauseId: `clause_${i + 1}`, clauseKind: r.kind, subjectRef: r.subjectId, referenceRef: r.referenceId, bounds: r.bounds, }), ) return { world, clauses } } // --------------------------------------------------------------------------- // Semantic IR to Solver Clause Mapping // --------------------------------------------------------------------------- /** * Map a compiled Semantic IR to solver ClauseDescriptors. * * This bridges the DSL compiler output to the solver evaluation input. * The `subjectIdMap` translates semantic IR subject ids to world subject ids. */ export function mapSemanticIrToClauses( semanticIr: SemanticIr, subjectIdMap: Map, ): ClauseDescriptor[] { const clauses: ClauseDescriptor[] = [] for (const [, clause] of semanticIr.clauses) { const subjectRef = subjectIdMap.get(clause.subjectRef) const referenceRef = clause.referenceRef ? subjectIdMap.get(clause.referenceRef) : undefined if (subjectRef === undefined) continue const tolerance = semanticIr.tolerances.get(clause.toleranceRef) const bounds: Record = {} if (clause.bounds.minGap) { bounds.minGap = clause.bounds.minGap.value } if (clause.bounds.maxGap) { bounds.maxGap = clause.bounds.maxGap.value } if (tolerance) { bounds.tolerance = tolerance.value } clauses.push({ clauseId: clause.id, clauseKind: `relation.${clause.relation}`, version: 1, subjectRef, referenceRef, bounds, }) } return clauses } // --------------------------------------------------------------------------- // Snapshot Builder // --------------------------------------------------------------------------- export function buildMockSnapshot(opts: { id?: string stateKind?: string stateSource?: string selector?: string world: GeometryWorld }): GeometrySnapshot { const metadata: SnapshotMetadata = { snapshotId: opts.id ?? 'snap_1', stateKind: (opts.stateKind as any) ?? 'default', stateSource: (opts.stateSource as any) ?? 'synthetic', selector: opts.selector, timestamp: Date.now(), } return { id: opts.id ?? 'snap_1', metadata, world: opts.world, } }