Files
Imhotep/packages/imhotep-solver/src/proofs.test.ts
T

315 lines
8.9 KiB
TypeScript

/**
* Tests for relation-specific proof generation.
*
* Verifies that generateProof produces rich, relation-specific failedPredicate
* details instead of generic left/right metric comparisons.
*/
import { describe, it, beforeEach } from 'node:test'
import assert from 'node:assert'
import {
generateProof,
resetProofCounter,
} from './proofs.js'
import type {
ClauseResult,
ClauseDescriptor,
GeometryWorld,
} from './registry.js'
// ---------------------------------------------------------------------------
// Mock World
// ---------------------------------------------------------------------------
const world: GeometryWorld = {
sceneId: 'scene_1',
snapshotId: 'snap_1',
env: {
viewportWidth: 1280,
viewportHeight: 800,
deviceScaleFactor: 1,
colorScheme: 'light',
pointer: 'fine',
hover: false,
reducedMotion: false,
locale: 'en',
writingMode: 'horizontal-tb',
},
strings: { values: [] },
subjects: {
ids: [1, 2],
domNodeId: [10, 20],
subjectKind: [1, 1],
primaryBoxId: [100, 200],
firstFragmentId: [0, 0],
fragmentCount: [0, 0],
},
dom: {
nodeId: [10, 20],
parentNodeId: [0, 0],
childCount: [0, 0],
tagNameStringId: [0, 0],
},
boxes: {
boxId: [100, 200],
subjectId: [1, 2],
frameId: [1, 1],
borderLeft: [0, 110],
borderTop: [0, 50],
borderRight: [100, 210],
borderBottom: [40, 90],
paddingLeft: [0, 0],
paddingTop: [0, 0],
paddingRight: [0, 0],
paddingBottom: [0, 0],
contentLeft: [0, 0],
contentTop: [0, 0],
contentRight: [0, 0],
contentBottom: [0, 0],
},
visualBoxes: {
boxId: [], subjectId: [], frameId: [],
borderLeft: [], borderTop: [], borderRight: [], borderBottom: [],
paddingLeft: [], paddingTop: [], paddingRight: [], paddingBottom: [],
contentLeft: [], contentTop: [], contentRight: [], contentBottom: [],
},
transforms: {
transformId: [], subjectId: [], matrixStart: [], matrixLength: [],
originX: [], originY: [],
},
matrices: { values: [] },
rects: { rectId: [], left: [], top: [], right: [], bottom: [] },
topology: {
containingBlockOf: [0, 0],
nearestPositionedAncestorOf: [0, 0],
scrollContainerOf: [0, 0],
stackingContextOf: [0, 0],
formattingContextOf: [0, 0],
clippingRootOf: [0, 0],
paintOrderBucket: [0, 0],
paintOrderIndex: [0, 0],
},
scroll: {
containerId: [],
scrollLeft: [],
scrollTop: [],
scrollWidth: [],
scrollHeight: [],
clientWidth: [],
clientHeight: [],
},
clipping: {
clipNodeId: [],
subjectId: [],
clipKind: [],
clipLeft: [],
clipTop: [],
clipRight: [],
clipBottom: [],
parentClipNodeId: [],
},
visibility: {
subjectId: [],
isRendered: [],
isVisible: [],
visibleArea: [],
clippedArea: [],
},
}
function makeClause(kind: string): ClauseDescriptor {
return {
clauseId: 'clause_1',
clauseKind: kind,
version: 1,
subjectRef: 1,
referenceRef: 2,
}
}
function makeResult(status: 'pass' | 'fail' | 'error', metrics?: Record<string, number>): ClauseResult {
return {
clauseId: 'clause_1',
status,
truth: status === 'error' ? 'indeterminate' : 'determinate',
metrics,
witness: { subjectId: 1, referenceId: 2 },
}
}
// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------
beforeEach(() => {
resetProofCounter()
})
// ---------------------------------------------------------------------------
// Relation-Specific Proof Tests
// ---------------------------------------------------------------------------
describe('relation-specific proofs', () => {
it('leftOf proof includes measured gap and expected bounds', () => {
const clause = makeClause('relation.leftOf')
const result = makeResult('fail', {
observedGap: -5,
minGap: 0,
maxGap: Infinity,
subjectLeft: 0,
subjectTop: 0,
subjectRight: 100,
subjectBottom: 40,
refLeft: 90,
refTop: 50,
refRight: 190,
refBottom: 90,
})
const proof = generateProof(result, clause, world)
assert.strictEqual(proof.outcome, 'fail')
assert.ok(proof.failedPredicate)
assert.strictEqual(proof.failedPredicate!.relationKind, 'leftOf')
assert.strictEqual(proof.failedPredicate!.measuredGap, -5)
assert.strictEqual(proof.failedPredicate!.expectedMinGap, 0)
assert.ok(proof.failedPredicate!.subjectRect)
assert.strictEqual(proof.failedPredicate!.subjectRect!.right, 100)
assert.ok(proof.failedPredicate!.referenceRect)
assert.strictEqual(proof.failedPredicate!.referenceRect!.left, 90)
})
it('above proof includes vertical gap and positions', () => {
const clause = makeClause('relation.above')
const result = makeResult('fail', {
observedGap: -3,
minGap: 0,
maxGap: Infinity,
subjectLeft: 0,
subjectTop: 0,
subjectRight: 100,
subjectBottom: 40,
refLeft: 0,
refTop: 35,
refRight: 100,
refBottom: 75,
})
const proof = generateProof(result, clause, world)
assert.strictEqual(proof.failedPredicate!.relationKind, 'above')
assert.strictEqual(proof.failedPredicate!.measuredGap, -3)
assert.ok(proof.failedPredicate!.subjectRect)
assert.ok(proof.failedPredicate!.referenceRect)
})
it('inside proof includes overflow edges', () => {
const clause = makeClause('relation.inside')
const result = makeResult('fail', {
overflowLeft: 10,
overflowTop: 0,
overflowRight: -5,
overflowBottom: 0,
subjectLeft: 10,
subjectTop: 0,
subjectRight: 105,
subjectBottom: 40,
refLeft: 0,
refTop: 0,
refRight: 100,
refBottom: 40,
})
const proof = generateProof(result, clause, world)
assert.strictEqual(proof.failedPredicate!.relationKind, 'inside')
assert.ok(proof.failedPredicate!.overflowEdges)
assert.strictEqual(proof.failedPredicate!.overflowEdges!.left, 10)
assert.strictEqual(proof.failedPredicate!.overflowEdges!.right, -5)
assert.ok(proof.failedPredicate!.subjectRect)
assert.ok(proof.failedPredicate!.referenceRect)
})
it('atLeast proof includes measured vs expected dimensions', () => {
const clause = makeClause('size.atLeast')
const result = makeResult('fail', {
observed: 80,
min: 100,
})
const proof = generateProof(result, clause, world)
assert.strictEqual(proof.failedPredicate!.relationKind, 'atLeast')
assert.strictEqual(proof.failedPredicate!.measuredValue, 80)
assert.strictEqual(proof.failedPredicate!.expectedMin, 100)
})
it('atMost proof includes measured vs expected dimensions', () => {
const clause = makeClause('size.atMost')
const result = makeResult('fail', {
observed: 120,
max: 100,
})
const proof = generateProof(result, clause, world)
assert.strictEqual(proof.failedPredicate!.relationKind, 'atMost')
assert.strictEqual(proof.failedPredicate!.measuredValue, 120)
assert.strictEqual(proof.failedPredicate!.expectedMax, 100)
})
it('alignedWith proof includes delta and tolerance', () => {
const clause = makeClause('alignment.alignedWith')
const result = makeResult('fail', {
delta: 5,
tolerance: 1,
})
const proof = generateProof(result, clause, world)
assert.strictEqual(proof.failedPredicate!.relationKind, 'alignedWith')
assert.strictEqual(proof.failedPredicate!.measuredValue, 5)
assert.strictEqual(proof.failedPredicate!.expectedMax, 1)
})
it('centeredWithin proof includes deltaX, deltaY and tolerance', () => {
const clause = makeClause('alignment.centeredWithin')
const result = makeResult('fail', {
deltaX: 3,
deltaY: 4,
tolerance: 2,
})
const proof = generateProof(result, clause, world)
assert.strictEqual(proof.failedPredicate!.relationKind, 'centeredWithin')
assert.strictEqual(proof.failedPredicate!.measuredValue, 3)
assert.strictEqual(proof.failedPredicate!.expectedMax, 2)
})
it('omits failedPredicate on pass', () => {
const clause = makeClause('relation.leftOf')
const result = makeResult('pass', { observedGap: 10, minGap: 0 })
const proof = generateProof(result, clause, world)
assert.strictEqual(proof.outcome, 'pass')
assert.strictEqual(proof.failedPredicate, undefined)
})
it('falls back to generic synthesis for unknown relation kinds', () => {
const clause = makeClause('relation.unknownRelation')
const result = makeResult('fail', { foo: 10, bar: 20 })
const proof = generateProof(result, clause, world)
assert.ok(proof.failedPredicate)
assert.strictEqual(proof.failedPredicate!.relationKind, 'unknownRelation')
assert.strictEqual(proof.failedPredicate!.op, '<')
assert.strictEqual(proof.failedPredicate!.left, 10)
assert.strictEqual(proof.failedPredicate!.right, 20)
})
})