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:
John Dvorak
2026-05-22 12:44:57 -07:00
parent aa69ddc52f
commit c0357b152f
3 changed files with 600 additions and 35 deletions
+1
View File
@@ -49,6 +49,7 @@ export * from './property-contracts.js'
export * from './property-results.js' export * from './property-results.js'
export * from './canonical.js' export * from './canonical.js'
export * from './context.js' export * from './context.js'
export * from './predicate-specs.js'
export { export {
serializeGeometryWorld, serializeGeometryWorld,
deserializeGeometryWorld, 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])
}
+22 -35
View File
@@ -48,10 +48,15 @@ import {
evaluateLogic, evaluateLogic,
registerDefaultPredicates, registerDefaultPredicates,
getPredicateEvaluator, getPredicateEvaluator,
getRequiredFactsForPredicate,
type DomainResolver, type DomainResolver,
BindingEnv, BindingEnv,
} from 'imhotep-solver' } from 'imhotep-solver'
import {
getPredicateRequiredFacts,
getPredicateDiagnosticCode,
getPredicateDecomposition,
isUnaryPredicate,
} from 'imhotep-core'
import { buildGeometryWorld } from './world-builder.js' import { buildGeometryWorld } from './world-builder.js'
import { import {
materializeSemanticSelector, materializeSemanticSelector,
@@ -197,12 +202,11 @@ export function computeRequiredFacts(formulas: FormulaNode[]): {
fragments: boolean fragments: boolean
domAncestry: boolean domAncestry: boolean
} { } {
registerDefaultPredicates()
const facts = new Set<string>() const facts = new Set<string>()
for (const formula of formulas) { for (const formula of formulas) {
const predicates = collectPredicates(formula) const predicates = collectPredicates(formula)
for (const p of predicates) { for (const p of predicates) {
const required = getRequiredFactsForPredicate(p) const required = getPredicateRequiredFacts(p)
for (const f of required) { for (const f of required) {
facts.add(f) facts.add(f)
} }
@@ -1157,7 +1161,10 @@ export function compileCanonicalClauseToFormula(clause: CanonicalClauseDescripto
let body: FormulaNode 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 minVal = extended.options?.min as number | undefined
const maxVal = extended.options?.max as number | undefined const maxVal = extended.options?.max as number | undefined
const atLeastOpts: Record<string, unknown> = {} const atLeastOpts: Record<string, unknown> = {}
@@ -1184,24 +1191,22 @@ export function compileCanonicalClauseToFormula(clause: CanonicalClauseDescripto
...(maxVal !== undefined ? { options: { ...atMostOpts, value: maxVal } } : { options: atMostOpts }), ...(maxVal !== undefined ? { options: { ...atMostOpts, value: maxVal } } : { options: atMostOpts }),
} as FormulaNode, } as FormulaNode,
} }
} else if (clause.relation === 'separatedFrom') { } else if (decomp?.kind === 'negate' && decomp.inner) {
const overlapsOpts: Record<string, unknown> = { ...options } const innerOpts: Record<string, unknown> = { ...options }
body = { body = {
type: 'FormulaNode', type: 'FormulaNode',
kind: 'not', kind: 'not',
operand: { operand: {
type: 'FormulaNode', type: 'FormulaNode',
kind: 'predicate', kind: 'predicate',
predicate: 'overlaps', predicate: decomp.inner,
args: [subjectVar, referenceVar], args: [subjectVar, referenceVar],
...(Object.keys(overlapsOpts).length > 0 ? { options: overlapsOpts } : {}), ...(Object.keys(innerOpts).length > 0 ? { options: innerOpts } : {}),
} as FormulaNode, } as FormulaNode,
} }
} else { } else {
const unaryPredicate = clause.relation === 'atLeast' const unaryPredicate = isUnaryPredicate(clause.relation)
|| clause.relation === 'atMost' && !(clause.relation === 'inStackingContext' && clause.reference)
|| clause.relation === 'aspectRatio'
|| (clause.relation === 'inStackingContext' && !clause.reference)
body = { body = {
type: 'FormulaNode', type: 'FormulaNode',
@@ -1211,7 +1216,7 @@ export function compileCanonicalClauseToFormula(clause: CanonicalClauseDescripto
...(Object.keys(options).length > 0 ? { options } : {}), ...(Object.keys(options).length > 0 ? { options } : {}),
} as FormulaNode } as FormulaNode
if ((clause.flags & 8) !== 0 && !unaryPredicate) { if (hasInStackingFlag && !unaryPredicate) {
body = { body = {
type: 'FormulaNode', type: 'FormulaNode',
kind: 'and', kind: 'and',
@@ -1245,11 +1250,8 @@ export function compileCanonicalClauseToFormula(clause: CanonicalClauseDescripto
domain: { type: 'DomainRef' as const, domain: 'elements', selector: clause.reference }, domain: { type: 'DomainRef' as const, domain: 'elements', selector: clause.reference },
} }
const isUnary = clause.relation === 'atLeast' const isUnary = isUnaryPredicate(clause.relation)
|| clause.relation === 'atMost' && !(clause.relation === 'inStackingContext' && clause.reference)
|| clause.relation === 'between'
|| clause.relation === 'aspectRatio'
|| (clause.relation === 'inStackingContext' && !clause.reference)
if (isUnary) { if (isUnary) {
return { return {
@@ -1613,21 +1615,6 @@ export function evaluateCardinalityAssertion(
// Result Adapter: FOL → Public API shape // 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( export function mapFolDiagnostic(
d: { code: string; severity: 'error' | 'warning' | 'info'; category?: string; message: string; clauseId?: string }, d: { code: string; severity: 'error' | 'warning' | 'info'; category?: string; message: string; clauseId?: string },
metrics?: Record<string, number>, metrics?: Record<string, number>,
@@ -1645,8 +1632,8 @@ export function mapFolDiagnostic(
if ((code as string) === 'IMH_PREDICATE_FAILED') { if ((code as string) === 'IMH_PREDICATE_FAILED') {
const match = message.match(/Predicate "([^"]+)" failed/) const match = message.match(/Predicate "([^"]+)" failed/)
const predicateName = match?.[1] const predicateName = match?.[1]
if (predicateName && PREDICATE_TO_DIAGNOSTIC_CODE[predicateName]) { if (predicateName && getPredicateDiagnosticCode(predicateName)) {
code = PREDICATE_TO_DIAGNOSTIC_CODE[predicateName] code = getPredicateDiagnosticCode(predicateName) as DiagnosticCode
const gapMatch = message.match(/gap=([\d.-]+)/) const gapMatch = message.match(/gap=([\d.-]+)/)
const observedGap = gapMatch ? parseFloat(gapMatch[1]) : undefined const observedGap = gapMatch ? parseFloat(gapMatch[1]) : undefined
const minGapMatch = message.match(/minGap=([\d.-]+)/) const minGapMatch = message.match(/minGap=([\d.-]+)/)