358 lines
10 KiB
TypeScript
358 lines
10 KiB
TypeScript
/**
|
|
* 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<string, unknown>
|
|
}): 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<ImhotepId, number>,
|
|
): 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<string, number> = {}
|
|
|
|
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,
|
|
}
|
|
}
|