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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user