import assert from 'node:assert/strict' import { writeFile } from 'node:fs/promises' import { performance } from 'node:perf_hooks' import { createSketch, solveSketch, type SketchConstraint, type SketchDiagnostic, type SketchGeometry, type SketchSolverStatus } from '../src/facade/sketcher' type SolverFuzzCase = { name: string geometry: SketchGeometry[] constraints: SketchConstraint[] expectedStatus?: SketchSolverStatus expectedCode?: SketchDiagnostic['code'] } let state = 0xa5a5f00d const random = () => { state ^= state << 13 state ^= state >>> 17 state ^= state << 5 return state >>> 0 } const value = () => (random() % 100_000) / 1000 const line = (id: string, offset = value()): Extract => ({ id, type: 'line', start: { x: offset, y: value() }, end: { x: offset + 1 + value(), y: value() } }) const createCase = (index: number): SolverFuzzCase => { const first = line('first') const second = line('second') switch (index % 10) { case 0: return { name: 'horizontal', geometry: [first], constraints: [{ id: 'horizontal', type: 'horizontal', geometryId: 'first' }], expectedStatus: 'under-constrained' } case 1: return { name: 'parallel', geometry: [first, second], constraints: [{ id: 'parallel', type: 'parallel', firstGeometryId: 'first', secondGeometryId: 'second' }], expectedStatus: 'under-constrained' } case 2: return { name: 'unknown-geometry', geometry: [first], constraints: [{ id: 'missing', type: 'vertical', geometryId: 'absent' }], expectedStatus: 'invalid', expectedCode: 'UNKNOWN_GEOMETRY' } case 3: return { name: 'invalid-dimension', geometry: [first], constraints: [{ id: 'distance', type: 'distance', first: { geometryId: 'first', point: 'start' }, second: { geometryId: 'first', point: 'end' }, value: Number.NaN }], expectedStatus: 'invalid', expectedCode: 'INVALID_VALUE' } case 4: return { name: 'redundant', geometry: [first], constraints: [{ id: 'h1', type: 'horizontal', geometryId: 'first' }, { id: 'h2', type: 'horizontal', geometryId: 'first' }], expectedStatus: 'under-constrained', expectedCode: 'REDUNDANT_CONSTRAINT' } case 5: return { name: 'reference-distance', geometry: [first], constraints: [{ id: 'reference', type: 'distanceX', first: { geometryId: 'first', point: 'start' }, second: { geometryId: 'first', point: 'end' }, value: value(), driving: false }], expectedStatus: 'under-constrained' } case 6: return { name: 'line-circle-tangent', geometry: [first, { id: 'circle', type: 'circle', center: { x: value(), y: value() }, radius: 1 + value() }], constraints: [{ id: 'tangent', type: 'tangent', firstGeometryId: 'first', secondGeometryId: 'circle' }], expectedStatus: 'under-constrained' } case 7: return { name: 'unsupported-bspline-weight', geometry: [{ id: 'curve', type: 'bspline', degree: 2, controlPoints: [{ x: 0, y: 0 }, { x: 1, y: value() }, { x: 2, y: 0 }], weights: [1, 1, 1] }], constraints: [{ id: 'weight', type: 'weight', geometryId: 'curve', controlPointIndex: 1, value: 2 }], expectedStatus: 'invalid', expectedCode: 'UNSUPPORTED_CONSTRAINT' } case 8: return { name: 'unknown-point', geometry: [first], constraints: [{ id: 'bad-point', type: 'coincident', first: { geometryId: 'first', point: 'center' }, second: { geometryId: 'first', point: 'end' } }], expectedStatus: 'invalid', expectedCode: 'UNKNOWN_POINT' } default: return { name: 'blocked-conflict', geometry: [first], constraints: [{ id: 'block', type: 'block', geometryId: 'first' }, { id: 'horizontal', type: 'horizontal', geometryId: 'first' }], expectedStatus: 'conflicting', expectedCode: 'SOLVER_NOT_CONVERGED' } } } const durations: number[] = [] const categories = new Map() const statuses: Record = { solved: 0, 'under-constrained': 0, conflicting: 0, invalid: 0 } for (let index = 0; index < 2000; index += 1) { const fuzzCase = createCase(index) categories.set(fuzzCase.name, (categories.get(fuzzCase.name) ?? 0) + 1) const snapshot = createSketch(`solver-fuzz-${index}`, fuzzCase.geometry, fuzzCase.constraints) const before = JSON.stringify(snapshot) const options = { tolerance: 1e-6, maxIterations: random() % 32 + 1 } const started = performance.now() const first = solveSketch(snapshot, options) const second = solveSketch(snapshot, options) durations.push(performance.now() - started) statuses[first.status] += 1 assert.equal(JSON.stringify(first), JSON.stringify(second), `Sketch solver fuzz category ${fuzzCase.name} is non-deterministic.`) assert.equal(JSON.stringify(snapshot), before, `Sketch solver fuzz category ${fuzzCase.name} mutated its input Snapshot.`) assert.equal(first.status, fuzzCase.expectedStatus, `Sketch solver fuzz category ${fuzzCase.name} returned ${first.status}.`) assert.ok(first.iterations <= options.maxIterations, `Sketch solver fuzz category ${fuzzCase.name} exceeded its iteration limit.`) assert.ok(!Number.isNaN(first.residual), `Sketch solver fuzz category ${fuzzCase.name} produced NaN residual.`) if (fuzzCase.expectedCode) assert.ok(first.diagnostics.some((diagnostic) => diagnostic.code === fuzzCase.expectedCode), `Sketch solver fuzz category ${fuzzCase.name} omitted ${fuzzCase.expectedCode}.`) if (first.status === 'conflicting') assert.ok(first.diagnostics.some((diagnostic) => diagnostic.code === 'SOLVER_NOT_CONVERGED')) if (first.status === 'invalid') assert.ok(first.diagnostics.some((diagnostic) => diagnostic.code !== 'REDUNDANT_CONSTRAINT')) } durations.sort((left, right) => left - right) const p95Ms = durations[Math.floor(durations.length * 0.95)] assert.ok(p95Ms < 10, `Sketch solver fuzz p95 exceeded 10 ms: ${p95Ms.toFixed(3)} ms.`) const report = { status: 'sketch-solver-fuzz-pass', seed: '0xa5a5f00d', models: durations.length, solves: durations.length * 2, categories: Object.fromEntries([...categories].sort(([left], [right]) => left.localeCompare(right))), statuses, timing: { p95Ms: Number(p95Ms.toFixed(3)), maxMs: Number(durations.at(-1)?.toFixed(3)) }, } await writeFile(new URL('../config/qa04-sketch-fuzz-verification.json', import.meta.url), `${JSON.stringify(report, null, 2)}\n`) console.log(JSON.stringify(report, null, 2))