refactor: eliminate remaining hardcoded predicate-name dispatch

Extraction.ts (3 fixes):
  - Replace 2 'inStackingContext' string checks with isVariableArityPredicate()
  - Replace 7-name diagnostic formatting if/else with spec-driven
    getPredicateSpec() checks (isDirectional → gap message,
    isSize → threshold hint, else generic)

Grammar.ts: Replace 8 hardcoded parser routing checks
  (atLeast/atMost/aspectRatio/between/clippedBy/attachedToScrollContainer/
  escapeClippingChainOf/inStackingContext) with SIZE_PREDICATE_NAMES and
  TOPOLOGY_PREDICATE_NAMES Sets derived from spec table.

Pipeline.ts: Replace 15-entry CODE_TO_CLAUSE_KIND map with runtime
  generation from PREDICATE_SPECS. Prefix derived from spec.isSize
  ('size.*') / validOptions.includes('axis') ('alignment.*') /
  else ('relation.*'). Manual override for aspectRatio code 15.

Proofs.ts: Replace 11-case switch(kind) with 5 spec-driven if/else
  branches categorized by validOptions presence (hasGap→directional,
  hasAxis→alignment) + 2 specific name checks (inside overflow,
  aspectRatio ratio). 11 predicate names → 0 hardcoded.

Lexer.ts: Export KEYWORDS map for conformance testing.

Conformance tests:
  - Solver: every BUILTIN_PREDICATES entry matches its PREDICATE_SPECS
    counterpart; every spec name (incl. aliases) has a registered
    evaluator with matching descriptor (2 tests)
  - DSL: every predicate name from collectAllPredicateNames() appears
    in the lexer KEYWORDS table (1 test)

598 SDK + 3 conformance + 57 E2E = 658 tests pass.
This commit is contained in:
John Dvorak
2026-05-22 13:15:35 -07:00
parent 283ab1b39f
commit 9df295b915
7 changed files with 238 additions and 165 deletions
+130 -126
View File
@@ -16,6 +16,8 @@ import {
type GeometryWorld,
} from './registry.js';
import { getPredicateSpec } from 'imhotep-core'
let proofCounter = 0;
function nextProofId(): string {
@@ -49,134 +51,136 @@ function buildFailedPredicate(
relationKind: kind,
};
switch (kind) {
case 'leftOf':
case 'rightOf':
case 'above':
case 'below': {
const gap = metrics.observedGap ?? metrics.gap ?? 0;
const min = metrics.minGap ?? 0;
const max = metrics.maxGap ?? Infinity;
return {
...base,
op: gap < min ? '<' : '>',
left: gap,
right: gap < min ? min : max,
measuredGap: gap,
expectedMinGap: Number.isFinite(min) ? min : undefined,
expectedMaxGap: Number.isFinite(max) ? max : undefined,
subjectRect:
metrics.subjectLeft !== undefined
? {
left: metrics.subjectLeft,
top: metrics.subjectTop ?? 0,
right: metrics.subjectRight ?? 0,
bottom: metrics.subjectBottom ?? 0,
}
: undefined,
referenceRect:
metrics.refLeft !== undefined
? {
left: metrics.refLeft,
top: metrics.refTop ?? 0,
right: metrics.refRight ?? 0,
bottom: metrics.refBottom ?? 0,
}
: undefined,
};
}
const spec = getPredicateSpec(kind)
// Determine diagnostic shape family from spec metadata.
const hasGap = spec?.validOptions.includes('minGap') && spec?.validOptions.includes('maxGap')
const hasAxis = spec?.validOptions.includes('axis')
const isSize = spec?.isSize
case 'inside': {
const overflowLeft = metrics.overflowLeft ?? 0;
const overflowTop = metrics.overflowTop ?? 0;
const overflowRight = metrics.overflowRight ?? 0;
const overflowBottom = metrics.overflowBottom ?? 0;
return {
...base,
op: 'not-contained',
left: 0,
right: 0,
overflowEdges: {
left: overflowLeft,
top: overflowTop,
right: overflowRight,
bottom: overflowBottom,
},
subjectRect:
metrics.subjectLeft !== undefined
? {
left: metrics.subjectLeft,
top: metrics.subjectTop ?? 0,
right: metrics.subjectRight ?? 0,
bottom: metrics.subjectBottom ?? 0,
}
: undefined,
referenceRect:
metrics.refLeft !== undefined
? {
left: metrics.refLeft,
top: metrics.refTop ?? 0,
right: metrics.refRight ?? 0,
bottom: metrics.refBottom ?? 0,
}
: undefined,
};
}
case 'atLeast':
case 'atMost':
case 'between': {
const observed = metrics.observed ?? metrics.value ?? 0;
const min = metrics.min ?? -Infinity;
const max = metrics.max ?? Infinity;
return {
...base,
op: kind === 'atMost' ? '>' : '<',
left: observed,
right: kind === 'atMost' ? max : min,
measuredValue: observed,
expectedMin: Number.isFinite(min) ? min : undefined,
expectedMax: Number.isFinite(max) ? max : undefined,
};
}
case 'aspectRatio': {
const observed = metrics.observed ?? 0;
const minRatio = metrics.minRatio ?? -Infinity;
const maxRatio = metrics.maxRatio ?? Infinity;
return {
...base,
op: observed < minRatio ? '<' : '>',
left: observed,
right: observed < minRatio ? minRatio : maxRatio,
measuredValue: observed,
expectedMin: Number.isFinite(minRatio) ? minRatio : undefined,
expectedMax: Number.isFinite(maxRatio) ? maxRatio : undefined,
};
}
case 'alignedWith':
case 'centeredWithin': {
const delta = metrics.delta ?? metrics.deltaX ?? metrics.deltaY ?? 0;
const tolerance = metrics.tolerance ?? 0;
return {
...base,
op: '>',
left: delta,
right: tolerance,
measuredValue: delta,
expectedMax: tolerance,
};
}
default:
// Fallback to generic synthesis from the first two numeric metrics.
const fallback = synthesizeGenericFailedPredicate(metrics);
if (fallback) {
return { ...fallback, relationKind: kind };
}
return fallback;
// --- Directional gap (leftOf / rightOf / above / below) ---
if (hasGap) {
const gap = metrics.observedGap ?? metrics.gap ?? 0;
const min = metrics.minGap ?? 0;
const max = metrics.maxGap ?? Infinity;
return {
...base,
op: gap < min ? '<' : '>',
left: gap,
right: gap < min ? min : max,
measuredGap: gap,
expectedMinGap: Number.isFinite(min) ? min : undefined,
expectedMaxGap: Number.isFinite(max) ? max : undefined,
subjectRect:
metrics.subjectLeft !== undefined
? {
left: metrics.subjectLeft,
top: metrics.subjectTop ?? 0,
right: metrics.subjectRight ?? 0,
bottom: metrics.subjectBottom ?? 0,
}
: undefined,
referenceRect:
metrics.refLeft !== undefined
? {
left: metrics.refLeft,
top: metrics.refTop ?? 0,
right: metrics.refRight ?? 0,
bottom: metrics.refBottom ?? 0,
}
: undefined,
};
}
// --- Containment / inside (unique overflow metrics) ---
if (kind === 'inside') {
const overflowLeft = metrics.overflowLeft ?? 0;
const overflowTop = metrics.overflowTop ?? 0;
const overflowRight = metrics.overflowRight ?? 0;
const overflowBottom = metrics.overflowBottom ?? 0;
return {
...base,
op: 'not-contained',
left: 0,
right: 0,
overflowEdges: {
left: overflowLeft,
top: overflowTop,
right: overflowRight,
bottom: overflowBottom,
},
subjectRect:
metrics.subjectLeft !== undefined
? {
left: metrics.subjectLeft,
top: metrics.subjectTop ?? 0,
right: metrics.subjectRight ?? 0,
bottom: metrics.subjectBottom ?? 0,
}
: undefined,
referenceRect:
metrics.refLeft !== undefined
? {
left: metrics.refLeft,
top: metrics.refTop ?? 0,
right: metrics.refRight ?? 0,
bottom: metrics.refBottom ?? 0,
}
: undefined,
};
}
// --- Size threshold (atLeast / atMost / between) ---
if (isSize && kind !== 'aspectRatio') {
const observed = metrics.observed ?? metrics.value ?? 0;
const min = metrics.min ?? -Infinity;
const max = metrics.max ?? Infinity;
return {
...base,
op: kind === 'atMost' ? '>' : '<',
left: observed,
right: kind === 'atMost' ? max : min,
measuredValue: observed,
expectedMin: Number.isFinite(min) ? min : undefined,
expectedMax: Number.isFinite(max) ? max : undefined,
};
}
// --- Aspect ratio ---
if (kind === 'aspectRatio') {
const observed = metrics.observed ?? 0;
const minRatio = metrics.minRatio ?? -Infinity;
const maxRatio = metrics.maxRatio ?? Infinity;
return {
...base,
op: observed < minRatio ? '<' : '>',
left: observed,
right: observed < minRatio ? minRatio : maxRatio,
measuredValue: observed,
expectedMin: Number.isFinite(minRatio) ? minRatio : undefined,
expectedMax: Number.isFinite(maxRatio) ? maxRatio : undefined,
};
}
// --- Alignment (alignedWith / centeredWithin) ---
if (hasAxis || kind === 'centeredWithin') {
const delta = metrics.delta ?? metrics.deltaX ?? metrics.deltaY ?? 0;
const tolerance = metrics.tolerance ?? 0;
return {
...base,
op: '>',
left: delta,
right: tolerance,
measuredValue: delta,
expectedMax: tolerance,
};
}
// Fallback to generic synthesis from the first two numeric metrics.
const fallback = synthesizeGenericFailedPredicate(metrics);
if (fallback) {
return { ...fallback, relationKind: kind };
}
return fallback;
}
function synthesizeGenericFailedPredicate(