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'
|
} from 'imhotep-core'
|
||||||
|
|
||||||
import { createDiagnostic } from 'imhotep-core'
|
import { createDiagnostic } from 'imhotep-core'
|
||||||
|
import {
|
||||||
|
getPredicateValidOptions,
|
||||||
|
collectSpatialPredicateNames,
|
||||||
|
collectSizePredicateNames,
|
||||||
|
collectQuantifierCompatiblePredicateNames,
|
||||||
|
isUnaryPredicate,
|
||||||
|
} from 'imhotep-core'
|
||||||
// ---- Validation Result ----
|
// ---- Validation Result ----
|
||||||
|
|
||||||
export interface ValidationResult {
|
export interface ValidationResult {
|
||||||
@@ -19,109 +26,53 @@ export interface ValidationResult {
|
|||||||
|
|
||||||
// ---- Supported Relations ----
|
// ---- Supported Relations ----
|
||||||
|
|
||||||
const SPATIAL_RELATIONS = new Set([
|
const SPATIAL_RELATIONS = new Set(collectSpatialPredicateNames())
|
||||||
'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 SIZE_RELATIONS = new Set([
|
const SIZE_RELATIONS = new Set(collectSizePredicateNames())
|
||||||
'aspectRatioBetween',
|
|
||||||
'between',
|
|
||||||
])
|
|
||||||
|
|
||||||
|
// Fluent API dotted size variants — not standalone predicates, but valid input syntax.
|
||||||
const DOTTED_SIZE_RELATIONS = new Set([
|
const DOTTED_SIZE_RELATIONS = new Set([
|
||||||
'size.atLeast',
|
'size.atLeast',
|
||||||
'size.atMost',
|
'size.atMost',
|
||||||
'size.between',
|
'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 ----
|
// ---- Valid Options Per Relation ----
|
||||||
|
|
||||||
const VALID_OPTIONS: Record<string, Set<string>> = {
|
const DOTTED_SIZE_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']),
|
|
||||||
'size.atLeast': new Set(['dimension', 'value', 'mode', 'space']),
|
'size.atLeast': new Set(['dimension', 'value', 'mode', 'space']),
|
||||||
'size.atMost': new Set(['dimension', 'value', 'mode', 'space']),
|
'size.atMost': new Set(['dimension', 'value', 'mode', 'space']),
|
||||||
'size.between': new Set(['dimension', 'min', 'max', '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 ----
|
// ---- Valid Quantifiers Per Relation ----
|
||||||
|
|
||||||
const QUANTIFIER_COMPATIBLE = new Set([
|
const QUANTIFIER_COMPATIBLE = new Set([
|
||||||
'leftOf',
|
...collectQuantifierCompatiblePredicateNames(),
|
||||||
'rightOf',
|
// Fluent API dotted size variants also support quantifiers.
|
||||||
'above',
|
...DOTTED_SIZE_RELATIONS,
|
||||||
'below',
|
|
||||||
'alignedWith',
|
|
||||||
'centeredWithin',
|
|
||||||
'inside',
|
|
||||||
'contains',
|
|
||||||
'overlaps',
|
|
||||||
'intersects',
|
|
||||||
'touches',
|
|
||||||
'separatedFrom',
|
|
||||||
'hasGap',
|
|
||||||
'beside',
|
|
||||||
'nextTo',
|
|
||||||
'adjacent',
|
|
||||||
'touching',
|
|
||||||
'near',
|
|
||||||
'under',
|
|
||||||
'within',
|
|
||||||
'size.atLeast',
|
|
||||||
'size.atMost',
|
|
||||||
'size.between',
|
|
||||||
'between',
|
|
||||||
])
|
])
|
||||||
|
|
||||||
// ---- Tolerance Parsing ----
|
// ---- Tolerance Parsing ----
|
||||||
@@ -195,7 +146,7 @@ export function validateRelation(assertion: RelationAssertion): ValidationResult
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Invalid options for relation
|
// 2. Invalid options for relation
|
||||||
const allowed = VALID_OPTIONS[assertion.relation] || new Set()
|
const allowed = getValidOptionsForRelation(assertion.relation)
|
||||||
const opts = assertion.options || {}
|
const opts = assertion.options || {}
|
||||||
const rawOpts = opts as unknown as Record<string, unknown>
|
const rawOpts = opts as unknown as Record<string, unknown>
|
||||||
const provided = Object.keys(rawOpts).filter(k => k !== 'type')
|
const provided = Object.keys(rawOpts).filter(k => k !== 'type')
|
||||||
@@ -295,14 +246,12 @@ export function validateRelation(assertion: RelationAssertion): ValidationResult
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const UNARY_RELATIONS = new Set([
|
// Unary relations: spec says arity=1, plus dotted size variants
|
||||||
'size.atLeast',
|
const isUnary = isUnaryPredicate(assertion.relation)
|
||||||
'size.atMost',
|
|| DOTTED_SIZE_RELATIONS.has(assertion.relation)
|
||||||
'size.between',
|
|| assertion.relation === ASPECT_RATIO_DSL_KEYWORD
|
||||||
'aspectRatioBetween',
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!UNARY_RELATIONS.has(assertion.relation) && !assertion.reference.value.trim()) {
|
if (!isUnary && !assertion.reference.value.trim()) {
|
||||||
diagnostics.push(
|
diagnostics.push(
|
||||||
makeDiag(
|
makeDiag(
|
||||||
'IMH_SELECTOR_ZERO_MATCHES',
|
'IMH_SELECTOR_ZERO_MATCHES',
|
||||||
|
|||||||
Reference in New Issue
Block a user