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:
@@ -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