refactor: convert validator to use predicate spec table
Replace 5 static Sets/maps (SPATIAL_RELATIONS, SIZE_RELATIONS,
VALID_OPTIONS, QUANTIFIER_COMPATIBLE, UNARY_RELATIONS) with
derivation from the unified PredicateSpec table:
- collectSpatialPredicateNames / collectSizePredicateNames
for category sets
- getPredicateValidOptions for option validation
- collectQuantifierCompatiblePredicateNames for quantifier checks
- isUnaryPredicate for unary detection
Local override map retained for fluent API dotted size variants
('size.atLeast' etc.) and 'aspectRatioBetween' DSL keyword, which
are input conventions, not distinct predicates.
595 SDK + 57 E2E tests pass.
This commit is contained in:
@@ -10,6 +10,13 @@ import type {
|
||||
} from 'imhotep-core'
|
||||
|
||||
import { createDiagnostic } from 'imhotep-core'
|
||||
import {
|
||||
getPredicateValidOptions,
|
||||
collectSpatialPredicateNames,
|
||||
collectSizePredicateNames,
|
||||
collectQuantifierCompatiblePredicateNames,
|
||||
isUnaryPredicate,
|
||||
} from 'imhotep-core'
|
||||
// ---- Validation Result ----
|
||||
|
||||
export interface ValidationResult {
|
||||
@@ -19,109 +26,53 @@ export interface ValidationResult {
|
||||
|
||||
// ---- Supported Relations ----
|
||||
|
||||
const SPATIAL_RELATIONS = new Set([
|
||||
'leftOf',
|
||||
'rightOf',
|
||||
'above',
|
||||
'below',
|
||||
'alignedWith',
|
||||
'leftAlignedWith',
|
||||
'rightAlignedWith',
|
||||
'topAlignedWith',
|
||||
'bottomAlignedWith',
|
||||
'centeredWithin',
|
||||
'inside',
|
||||
'contains',
|
||||
'overlaps',
|
||||
'intersects',
|
||||
'touches',
|
||||
'separatedFrom',
|
||||
'hasGap',
|
||||
// Spatial aliases
|
||||
'beside',
|
||||
'nextTo',
|
||||
'adjacent',
|
||||
'touching',
|
||||
'near',
|
||||
'under',
|
||||
'within',
|
||||
])
|
||||
const SPATIAL_RELATIONS = new Set(collectSpatialPredicateNames())
|
||||
|
||||
const SIZE_RELATIONS = new Set([
|
||||
'aspectRatioBetween',
|
||||
'between',
|
||||
])
|
||||
const SIZE_RELATIONS = new Set(collectSizePredicateNames())
|
||||
|
||||
// Fluent API dotted size variants — not standalone predicates, but valid input syntax.
|
||||
const DOTTED_SIZE_RELATIONS = new Set([
|
||||
'size.atLeast',
|
||||
'size.atMost',
|
||||
'size.between',
|
||||
])
|
||||
|
||||
const ALL_RELATIONS = new Set([...SPATIAL_RELATIONS, ...SIZE_RELATIONS, ...DOTTED_SIZE_RELATIONS])
|
||||
// DSL keyword that maps to 'aspectRatio' canonical — not a distinct predicate.
|
||||
const ASPECT_RATIO_DSL_KEYWORD = 'aspectRatioBetween'
|
||||
|
||||
const ALL_RELATIONS = new Set([
|
||||
...collectSpatialPredicateNames(),
|
||||
...collectSizePredicateNames(),
|
||||
...DOTTED_SIZE_RELATIONS,
|
||||
ASPECT_RATIO_DSL_KEYWORD,
|
||||
])
|
||||
|
||||
// ---- Valid Options Per Relation ----
|
||||
|
||||
const VALID_OPTIONS: Record<string, Set<string>> = {
|
||||
leftOf: new Set(['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space']),
|
||||
rightOf: new Set(['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space']),
|
||||
above: new Set(['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space']),
|
||||
below: new Set(['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space']),
|
||||
alignedWith: new Set(['axis', 'tolerance', 'space']),
|
||||
leftAlignedWith: new Set(['tolerance', 'space']),
|
||||
rightAlignedWith: new Set(['tolerance', 'space']),
|
||||
topAlignedWith: new Set(['tolerance', 'space']),
|
||||
bottomAlignedWith: new Set(['tolerance', 'space']),
|
||||
centeredWithin: new Set(['tolerance', 'space']),
|
||||
inside: new Set(['tolerance', 'minGap', 'space']),
|
||||
contains: new Set(['tolerance', 'space']),
|
||||
overlaps: new Set(['tolerance', 'space']),
|
||||
intersects: new Set(['tolerance', 'space']),
|
||||
touches: new Set(['tolerance', 'inStackingContext', 'space']),
|
||||
separatedFrom: new Set(['minGap', 'maxGap', 'tolerance', 'space']),
|
||||
hasGap: new Set(['minGap', 'maxGap', 'tolerance', 'space']),
|
||||
// Spatial alias options
|
||||
beside: new Set(['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space']),
|
||||
nextTo: new Set(['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space']),
|
||||
adjacent: new Set(['tolerance', 'inStackingContext', 'space']),
|
||||
touching: new Set(['tolerance', 'inStackingContext', 'space']),
|
||||
near: new Set(['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space']),
|
||||
under: new Set(['minGap', 'maxGap', 'tolerance', 'inStackingContext', 'space']),
|
||||
within: new Set(['tolerance', 'minGap', 'space']),
|
||||
aspectRatioBetween: new Set(['min', 'max']),
|
||||
between: new Set(['dimension', 'min', 'max']),
|
||||
const DOTTED_SIZE_OPTIONS: Record<string, Set<string>> = {
|
||||
'size.atLeast': new Set(['dimension', 'value', 'mode', 'space']),
|
||||
'size.atMost': new Set(['dimension', 'value', 'mode', 'space']),
|
||||
'size.between': new Set(['dimension', 'min', 'max', 'mode', 'space']),
|
||||
}
|
||||
|
||||
const ASPECT_RATIO_OPTIONS = new Set(['min', 'max'])
|
||||
|
||||
function getValidOptionsForRelation(relation: string): Set<string> {
|
||||
// Dotted size variants have their own option schema.
|
||||
if (DOTTED_SIZE_OPTIONS[relation]) return DOTTED_SIZE_OPTIONS[relation]
|
||||
if (relation === ASPECT_RATIO_DSL_KEYWORD) return ASPECT_RATIO_OPTIONS
|
||||
// Resolve alias names (e.g. 'under' → 'below') to get canonical spec options.
|
||||
const options = getPredicateValidOptions(relation)
|
||||
if (options.length > 0) return new Set(options)
|
||||
return new Set()
|
||||
}
|
||||
|
||||
// ---- Valid Quantifiers Per Relation ----
|
||||
|
||||
const QUANTIFIER_COMPATIBLE = new Set([
|
||||
'leftOf',
|
||||
'rightOf',
|
||||
'above',
|
||||
'below',
|
||||
'alignedWith',
|
||||
'centeredWithin',
|
||||
'inside',
|
||||
'contains',
|
||||
'overlaps',
|
||||
'intersects',
|
||||
'touches',
|
||||
'separatedFrom',
|
||||
'hasGap',
|
||||
'beside',
|
||||
'nextTo',
|
||||
'adjacent',
|
||||
'touching',
|
||||
'near',
|
||||
'under',
|
||||
'within',
|
||||
'size.atLeast',
|
||||
'size.atMost',
|
||||
'size.between',
|
||||
'between',
|
||||
...collectQuantifierCompatiblePredicateNames(),
|
||||
// Fluent API dotted size variants also support quantifiers.
|
||||
...DOTTED_SIZE_RELATIONS,
|
||||
])
|
||||
|
||||
// ---- Tolerance Parsing ----
|
||||
@@ -195,7 +146,7 @@ export function validateRelation(assertion: RelationAssertion): ValidationResult
|
||||
}
|
||||
|
||||
// 2. Invalid options for relation
|
||||
const allowed = VALID_OPTIONS[assertion.relation] || new Set()
|
||||
const allowed = getValidOptionsForRelation(assertion.relation)
|
||||
const opts = assertion.options || {}
|
||||
const rawOpts = opts as unknown as Record<string, unknown>
|
||||
const provided = Object.keys(rawOpts).filter(k => k !== 'type')
|
||||
@@ -295,14 +246,12 @@ export function validateRelation(assertion: RelationAssertion): ValidationResult
|
||||
)
|
||||
}
|
||||
|
||||
const UNARY_RELATIONS = new Set([
|
||||
'size.atLeast',
|
||||
'size.atMost',
|
||||
'size.between',
|
||||
'aspectRatioBetween',
|
||||
])
|
||||
// Unary relations: spec says arity=1, plus dotted size variants
|
||||
const isUnary = isUnaryPredicate(assertion.relation)
|
||||
|| DOTTED_SIZE_RELATIONS.has(assertion.relation)
|
||||
|| assertion.relation === ASPECT_RATIO_DSL_KEYWORD
|
||||
|
||||
if (!UNARY_RELATIONS.has(assertion.relation) && !assertion.reference.value.trim()) {
|
||||
if (!isUnary && !assertion.reference.value.trim()) {
|
||||
diagnostics.push(
|
||||
makeDiag(
|
||||
'IMH_SELECTOR_ZERO_MATCHES',
|
||||
|
||||
Reference in New Issue
Block a user