refactor: introduce unified PredicateSpec table, convert extraction consumers
Imhotep-core: add predicate-specs.ts with 34 PredicateSpec entries as
the single source of truth for predicate metadata (name, arity,
aliases, requiredFacts, validOptions, diagnosticCode, relationCode,
decompose rules, category flags). Lookup helpers derive all
per-predicate information from the static table.
Extraction.ts (3 consumers converted):
- computeRequiredFacts: replace getRequiredFactsForPredicate (global
registry) with getPredicateRequiredFacts (static spec table).
Removes registerDefaultPredicates() dependency from fact planning.
- compileCanonicalClauseToFormula: replace 4 string-branch patterns
('between'/'separatedFrom'/'atLeast'/'aspectRatio'/'inStackingContext')
with spec-driven getPredicateDecomposition() and isUnaryPredicate().
Same behavior, zero string dispatch in predicate selection.
- mapFolDiagnostic: replace PREDICATE_TO_DIAGNOSTIC_CODE (13-entry
Record) with getPredicateDiagnosticCode() from spec table.
595 SDK + 57 hard E2E tests pass.
This commit is contained in:
@@ -49,6 +49,7 @@ export * from './property-contracts.js'
|
||||
export * from './property-results.js'
|
||||
export * from './canonical.js'
|
||||
export * from './context.js'
|
||||
export * from './predicate-specs.js'
|
||||
export {
|
||||
serializeGeometryWorld,
|
||||
deserializeGeometryWorld,
|
||||
|
||||
@@ -0,0 +1,577 @@
|
||||
/**
|
||||
* Predicate specification registry — single source of truth for all built-in
|
||||
* predicates. Every other package (solver, DSL, Playwright, CDP, etc.) should
|
||||
* derive its per-predicate metadata from this table rather than maintaining
|
||||
* parallel string-based maps or switch/case branches.
|
||||
*/
|
||||
|
||||
import type { DiagnosticCode } from './diagnostics.js'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface PredicateDecomposition {
|
||||
kind: 'between' | 'negate' | 'delegate'
|
||||
/** For 'between': splits into atLeast({dimension,min}) AND atMost({dimension,max}). */
|
||||
/** For 'negate': wraps body in NOT, substitutes inner predicate name. */
|
||||
inner?: string
|
||||
/** For 'delegate': canonical predicate name to delegate to (alias evaluator). */
|
||||
delegateTo?: string
|
||||
/** Options to override/merge when delegating. */
|
||||
overrideOptions?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface PredicateSpec {
|
||||
/** Canonical predicate name (e.g. 'leftOf', 'between'). */
|
||||
name: string
|
||||
/** Alternate names that resolve to this spec. */
|
||||
aliases: string[]
|
||||
/** Expected tuple arity: 1=unary, 2=binary, 'variable'=can be either. */
|
||||
arity: 1 | 2 | 'variable'
|
||||
/** Domain kinds for the solver tuple-resolution system. */
|
||||
domains: string[]
|
||||
/** Required fact IDs for extraction planning. */
|
||||
requiredFacts: string[]
|
||||
/** Allowed option keys (from user code). */
|
||||
validOptions: readonly string[]
|
||||
/** Public diagnostic code when a predicate fails. */
|
||||
diagnosticCode?: DiagnosticCode
|
||||
/** Small integer code for the dense DSL execution IR. 0 = not encoded. */
|
||||
relationCode?: number
|
||||
/** Special compilation rule. Undefined = direct predicate call. */
|
||||
decompose?: PredicateDecomposition
|
||||
/** Whether this predicate accepts quantifier modifiers (any/none). */
|
||||
quantifierCompatible: boolean
|
||||
/** Whether this is a spatial relation (used for validator category checks). */
|
||||
isSpatial: boolean
|
||||
/** Whether this is a size/dimensional relation. */
|
||||
isSize: boolean
|
||||
/** Whether this is a topology relation. */
|
||||
isTopology: boolean
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// All built-in predicate specs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const PREDICATE_SPECS: readonly PredicateSpec[] = Object.freeze([
|
||||
// ── Spatial (cardinal directions) ──────────────────────────────────────────
|
||||
{
|
||||
name: 'above',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space'],
|
||||
diagnosticCode: 'IMH_RELATION_ABOVE_FAILED',
|
||||
relationCode: 3,
|
||||
quantifierCompatible: true,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'below',
|
||||
aliases: ['under'],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space'],
|
||||
diagnosticCode: 'IMH_RELATION_BELOW_FAILED',
|
||||
relationCode: 4,
|
||||
quantifierCompatible: true,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'leftOf',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space'],
|
||||
diagnosticCode: 'IMH_RELATION_LEFT_OF_FAILED',
|
||||
relationCode: 1,
|
||||
quantifierCompatible: true,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'rightOf',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space'],
|
||||
diagnosticCode: 'IMH_RELATION_RIGHT_OF_FAILED',
|
||||
relationCode: 2,
|
||||
quantifierCompatible: true,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
|
||||
// ── Spatial (containment / overlap) ───────────────────────────────────────
|
||||
{
|
||||
name: 'inside',
|
||||
aliases: ['within'],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['tolerance', 'minGap', 'space'],
|
||||
diagnosticCode: 'IMH_RELATION_INSIDE_FAILED',
|
||||
relationCode: 11,
|
||||
quantifierCompatible: true,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'contains',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['tolerance', 'space'],
|
||||
diagnosticCode: 'IMH_RELATION_CONTAINS_FAILED',
|
||||
relationCode: 12,
|
||||
quantifierCompatible: true,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'overlaps',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['tolerance', 'space'],
|
||||
diagnosticCode: 'IMH_RELATION_OVERLAPS_FAILED',
|
||||
relationCode: 13,
|
||||
quantifierCompatible: true,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'intersects',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['tolerance', 'space'],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 17,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'touches',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['tolerance', 'inStackingContext', 'space'],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 18,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
|
||||
// ── Spatial (alignment) ───────────────────────────────────────────────────
|
||||
{
|
||||
name: 'alignedWith',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['axis', 'tolerance', 'space'],
|
||||
diagnosticCode: 'IMH_ALIGNMENT_FAILED',
|
||||
relationCode: 5,
|
||||
quantifierCompatible: true,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'leftAlignedWith',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['tolerance', 'space'],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 6,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'rightAlignedWith',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['tolerance', 'space'],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 7,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'topAlignedWith',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['tolerance', 'space'],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 8,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'bottomAlignedWith',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['tolerance', 'space'],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 9,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'centeredWithin',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['tolerance', 'space'],
|
||||
diagnosticCode: 'IMH_RELATION_CENTERED_FAILED',
|
||||
relationCode: 10,
|
||||
quantifierCompatible: true,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
|
||||
// ── Spatial (gap / proximity) ─────────────────────────────────────────────
|
||||
{
|
||||
name: 'separatedFrom',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['minGap', 'maxGap', 'tolerance', 'space'],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 14,
|
||||
decompose: { kind: 'negate', inner: 'overlaps' },
|
||||
quantifierCompatible: true,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'hasGap',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['minGap', 'maxGap', 'tolerance', 'space'],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 19,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
|
||||
// ── Spatial aliases (delegating evaluators) ───────────────────────────────
|
||||
{
|
||||
name: 'beside',
|
||||
aliases: ['nextTo'],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space'],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 0,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'adjacent',
|
||||
aliases: ['touching'],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['tolerance', 'inStackingContext', 'space'],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 0,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'near',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
|
||||
validOptions: ['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space'],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 0,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: true,
|
||||
isSize: false,
|
||||
isTopology: false,
|
||||
},
|
||||
|
||||
// ── Size / dimensional ────────────────────────────────────────────────────
|
||||
{
|
||||
name: 'width',
|
||||
aliases: [],
|
||||
arity: 1,
|
||||
domains: ['element'],
|
||||
requiredFacts: ['subject.primaryBox'],
|
||||
validOptions: [],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 0,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: false,
|
||||
isSize: true,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'height',
|
||||
aliases: [],
|
||||
arity: 1,
|
||||
domains: ['element'],
|
||||
requiredFacts: ['subject.primaryBox'],
|
||||
validOptions: [],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 0,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: false,
|
||||
isSize: true,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'atLeast',
|
||||
aliases: [],
|
||||
arity: 1,
|
||||
domains: ['element'],
|
||||
requiredFacts: ['subject.primaryBox'],
|
||||
validOptions: ['dimension', 'value', 'mode', 'space'],
|
||||
diagnosticCode: 'IMH_SIZE_AT_LEAST_FAILED',
|
||||
relationCode: 0,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: false,
|
||||
isSize: true,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'atMost',
|
||||
aliases: [],
|
||||
arity: 1,
|
||||
domains: ['element'],
|
||||
requiredFacts: ['subject.primaryBox'],
|
||||
validOptions: ['dimension', 'value', 'mode', 'space'],
|
||||
diagnosticCode: 'IMH_SIZE_AT_MOST_FAILED',
|
||||
relationCode: 0,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: false,
|
||||
isSize: true,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'between',
|
||||
aliases: [],
|
||||
arity: 1,
|
||||
domains: ['element'],
|
||||
requiredFacts: ['subject.primaryBox'],
|
||||
validOptions: ['dimension', 'min', 'max', 'mode', 'space'],
|
||||
diagnosticCode: 'IMH_SIZE_BETWEEN_FAILED',
|
||||
relationCode: 16,
|
||||
decompose: { kind: 'between' },
|
||||
quantifierCompatible: false,
|
||||
isSpatial: false,
|
||||
isSize: true,
|
||||
isTopology: false,
|
||||
},
|
||||
{
|
||||
name: 'aspectRatio',
|
||||
aliases: [],
|
||||
arity: 1,
|
||||
domains: ['element'],
|
||||
requiredFacts: ['subject.primaryBox'],
|
||||
validOptions: ['min', 'max'],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 0,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: false,
|
||||
isSize: true,
|
||||
isTopology: false,
|
||||
},
|
||||
|
||||
// ── Topology ──────────────────────────────────────────────────────────────
|
||||
{
|
||||
name: 'clippedBy',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.clipChain', 'reference.clipChain'],
|
||||
validOptions: [],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 0,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: false,
|
||||
isSize: false,
|
||||
isTopology: true,
|
||||
},
|
||||
{
|
||||
name: 'inStackingContext',
|
||||
aliases: [],
|
||||
arity: 'variable' as const,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['topology.stackingContextOf'],
|
||||
validOptions: [],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 0,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: false,
|
||||
isSize: false,
|
||||
isTopology: true,
|
||||
},
|
||||
{
|
||||
name: 'escapeClippingChainOf',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['subject.primaryBox', 'reference.clipChain', 'topology.clippingRootOf'],
|
||||
validOptions: [],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 0,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: false,
|
||||
isSize: false,
|
||||
isTopology: true,
|
||||
},
|
||||
{
|
||||
name: 'attachedToScrollContainer',
|
||||
aliases: [],
|
||||
arity: 2,
|
||||
domains: ['element', 'element'],
|
||||
requiredFacts: ['topology.scrollContainerOf'],
|
||||
validOptions: [],
|
||||
diagnosticCode: undefined,
|
||||
relationCode: 0,
|
||||
quantifierCompatible: false,
|
||||
isSpatial: false,
|
||||
isSize: false,
|
||||
isTopology: true,
|
||||
},
|
||||
] satisfies PredicateSpec[])
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lookup helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const byName = new Map<string, PredicateSpec>()
|
||||
const byAlias = new Map<string, string>()
|
||||
for (const spec of PREDICATE_SPECS) {
|
||||
byName.set(spec.name, spec)
|
||||
for (const alias of spec.aliases) {
|
||||
byName.set(alias, spec)
|
||||
byAlias.set(alias, spec.name)
|
||||
}
|
||||
}
|
||||
|
||||
export function getPredicateSpec(name: string): PredicateSpec | undefined {
|
||||
return byName.get(name)
|
||||
}
|
||||
|
||||
export function resolvePredicateName(name: string): string {
|
||||
return byAlias.get(name) ?? name
|
||||
}
|
||||
|
||||
export function getAllPredicateSpecs(): readonly PredicateSpec[] {
|
||||
return PREDICATE_SPECS
|
||||
}
|
||||
|
||||
export function isPredicateName(name: string): boolean {
|
||||
return byName.has(name)
|
||||
}
|
||||
|
||||
export function getPredicateDiagnosticCode(name: string): DiagnosticCode | undefined {
|
||||
return byName.get(name)?.diagnosticCode
|
||||
}
|
||||
|
||||
export function getPredicateRelationCode(name: string): number | undefined {
|
||||
return byName.get(name)?.relationCode
|
||||
}
|
||||
|
||||
export function getPredicateRequiredFacts(name: string): string[] {
|
||||
return byName.get(name)?.requiredFacts ?? []
|
||||
}
|
||||
|
||||
export function getPredicateValidOptions(name: string): readonly string[] {
|
||||
return byName.get(name)?.validOptions ?? []
|
||||
}
|
||||
|
||||
export function getPredicateArity(name: string): 1 | 2 | 'variable' | undefined {
|
||||
return byName.get(name)?.arity
|
||||
}
|
||||
|
||||
export function isUnaryPredicate(name: string): boolean {
|
||||
const spec = byName.get(name)
|
||||
return spec?.arity === 1 || spec?.arity === 'variable'
|
||||
}
|
||||
|
||||
export function isVariableArityPredicate(name: string): boolean {
|
||||
return byName.get(name)?.arity === 'variable'
|
||||
}
|
||||
|
||||
export function getPredicateDecomposition(name: string): PredicateDecomposition | undefined {
|
||||
return byName.get(name)?.decompose
|
||||
}
|
||||
|
||||
export function collectAllPredicateNames(): string[] {
|
||||
return Array.from(byName.keys())
|
||||
}
|
||||
|
||||
export function collectSpatialPredicateNames(): string[] {
|
||||
return PREDICATE_SPECS.filter(s => s.isSpatial).flatMap(s => [s.name, ...s.aliases])
|
||||
}
|
||||
|
||||
export function collectSizePredicateNames(): string[] {
|
||||
return PREDICATE_SPECS.filter(s => s.isSize).flatMap(s => [s.name, ...s.aliases])
|
||||
}
|
||||
|
||||
export function collectTopologyPredicateNames(): string[] {
|
||||
return PREDICATE_SPECS.filter(s => s.isTopology).flatMap(s => [s.name, ...s.aliases])
|
||||
}
|
||||
|
||||
export function collectQuantifierCompatiblePredicateNames(): string[] {
|
||||
return PREDICATE_SPECS.filter(s => s.quantifierCompatible).flatMap(s => [s.name, ...s.aliases])
|
||||
}
|
||||
@@ -48,10 +48,15 @@ import {
|
||||
evaluateLogic,
|
||||
registerDefaultPredicates,
|
||||
getPredicateEvaluator,
|
||||
getRequiredFactsForPredicate,
|
||||
type DomainResolver,
|
||||
BindingEnv,
|
||||
} from 'imhotep-solver'
|
||||
import {
|
||||
getPredicateRequiredFacts,
|
||||
getPredicateDiagnosticCode,
|
||||
getPredicateDecomposition,
|
||||
isUnaryPredicate,
|
||||
} from 'imhotep-core'
|
||||
import { buildGeometryWorld } from './world-builder.js'
|
||||
import {
|
||||
materializeSemanticSelector,
|
||||
@@ -197,12 +202,11 @@ export function computeRequiredFacts(formulas: FormulaNode[]): {
|
||||
fragments: boolean
|
||||
domAncestry: boolean
|
||||
} {
|
||||
registerDefaultPredicates()
|
||||
const facts = new Set<string>()
|
||||
for (const formula of formulas) {
|
||||
const predicates = collectPredicates(formula)
|
||||
for (const p of predicates) {
|
||||
const required = getRequiredFactsForPredicate(p)
|
||||
const required = getPredicateRequiredFacts(p)
|
||||
for (const f of required) {
|
||||
facts.add(f)
|
||||
}
|
||||
@@ -1157,7 +1161,10 @@ export function compileCanonicalClauseToFormula(clause: CanonicalClauseDescripto
|
||||
|
||||
let body: FormulaNode
|
||||
|
||||
if (clause.relation === 'between') {
|
||||
const decomp = getPredicateDecomposition(clause.relation)
|
||||
const hasInStackingFlag = (clause.flags & 8) !== 0
|
||||
|
||||
if (decomp?.kind === 'between') {
|
||||
const minVal = extended.options?.min as number | undefined
|
||||
const maxVal = extended.options?.max as number | undefined
|
||||
const atLeastOpts: Record<string, unknown> = {}
|
||||
@@ -1184,24 +1191,22 @@ export function compileCanonicalClauseToFormula(clause: CanonicalClauseDescripto
|
||||
...(maxVal !== undefined ? { options: { ...atMostOpts, value: maxVal } } : { options: atMostOpts }),
|
||||
} as FormulaNode,
|
||||
}
|
||||
} else if (clause.relation === 'separatedFrom') {
|
||||
const overlapsOpts: Record<string, unknown> = { ...options }
|
||||
} else if (decomp?.kind === 'negate' && decomp.inner) {
|
||||
const innerOpts: Record<string, unknown> = { ...options }
|
||||
body = {
|
||||
type: 'FormulaNode',
|
||||
kind: 'not',
|
||||
operand: {
|
||||
type: 'FormulaNode',
|
||||
kind: 'predicate',
|
||||
predicate: 'overlaps',
|
||||
predicate: decomp.inner,
|
||||
args: [subjectVar, referenceVar],
|
||||
...(Object.keys(overlapsOpts).length > 0 ? { options: overlapsOpts } : {}),
|
||||
...(Object.keys(innerOpts).length > 0 ? { options: innerOpts } : {}),
|
||||
} as FormulaNode,
|
||||
}
|
||||
} else {
|
||||
const unaryPredicate = clause.relation === 'atLeast'
|
||||
|| clause.relation === 'atMost'
|
||||
|| clause.relation === 'aspectRatio'
|
||||
|| (clause.relation === 'inStackingContext' && !clause.reference)
|
||||
const unaryPredicate = isUnaryPredicate(clause.relation)
|
||||
&& !(clause.relation === 'inStackingContext' && clause.reference)
|
||||
|
||||
body = {
|
||||
type: 'FormulaNode',
|
||||
@@ -1211,7 +1216,7 @@ export function compileCanonicalClauseToFormula(clause: CanonicalClauseDescripto
|
||||
...(Object.keys(options).length > 0 ? { options } : {}),
|
||||
} as FormulaNode
|
||||
|
||||
if ((clause.flags & 8) !== 0 && !unaryPredicate) {
|
||||
if (hasInStackingFlag && !unaryPredicate) {
|
||||
body = {
|
||||
type: 'FormulaNode',
|
||||
kind: 'and',
|
||||
@@ -1245,11 +1250,8 @@ export function compileCanonicalClauseToFormula(clause: CanonicalClauseDescripto
|
||||
domain: { type: 'DomainRef' as const, domain: 'elements', selector: clause.reference },
|
||||
}
|
||||
|
||||
const isUnary = clause.relation === 'atLeast'
|
||||
|| clause.relation === 'atMost'
|
||||
|| clause.relation === 'between'
|
||||
|| clause.relation === 'aspectRatio'
|
||||
|| (clause.relation === 'inStackingContext' && !clause.reference)
|
||||
const isUnary = isUnaryPredicate(clause.relation)
|
||||
&& !(clause.relation === 'inStackingContext' && clause.reference)
|
||||
|
||||
if (isUnary) {
|
||||
return {
|
||||
@@ -1613,21 +1615,6 @@ export function evaluateCardinalityAssertion(
|
||||
// Result Adapter: FOL → Public API shape
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PREDICATE_TO_DIAGNOSTIC_CODE: Record<string, DiagnosticCode> = {
|
||||
leftOf: 'IMH_RELATION_LEFT_OF_FAILED',
|
||||
rightOf: 'IMH_RELATION_RIGHT_OF_FAILED',
|
||||
above: 'IMH_RELATION_ABOVE_FAILED',
|
||||
below: 'IMH_RELATION_BELOW_FAILED',
|
||||
inside: 'IMH_RELATION_INSIDE_FAILED',
|
||||
contains: 'IMH_RELATION_CONTAINS_FAILED',
|
||||
alignedWith: 'IMH_ALIGNMENT_FAILED',
|
||||
centeredWithin: 'IMH_RELATION_CENTERED_FAILED',
|
||||
overlaps: 'IMH_RELATION_OVERLAPS_FAILED',
|
||||
atLeast: 'IMH_SIZE_AT_LEAST_FAILED',
|
||||
atMost: 'IMH_SIZE_AT_MOST_FAILED',
|
||||
between: 'IMH_SIZE_BETWEEN_FAILED',
|
||||
}
|
||||
|
||||
export function mapFolDiagnostic(
|
||||
d: { code: string; severity: 'error' | 'warning' | 'info'; category?: string; message: string; clauseId?: string },
|
||||
metrics?: Record<string, number>,
|
||||
@@ -1645,8 +1632,8 @@ export function mapFolDiagnostic(
|
||||
if ((code as string) === 'IMH_PREDICATE_FAILED') {
|
||||
const match = message.match(/Predicate "([^"]+)" failed/)
|
||||
const predicateName = match?.[1]
|
||||
if (predicateName && PREDICATE_TO_DIAGNOSTIC_CODE[predicateName]) {
|
||||
code = PREDICATE_TO_DIAGNOSTIC_CODE[predicateName]
|
||||
if (predicateName && getPredicateDiagnosticCode(predicateName)) {
|
||||
code = getPredicateDiagnosticCode(predicateName) as DiagnosticCode
|
||||
const gapMatch = message.match(/gap=([\d.-]+)/)
|
||||
const observedGap = gapMatch ? parseFloat(gapMatch[1]) : undefined
|
||||
const minGapMatch = message.match(/minGap=([\d.-]+)/)
|
||||
|
||||
Reference in New Issue
Block a user