From 6f82849b4fb068817ce2f0e54ee140fe0da1af24 Mon Sep 17 00:00:00 2001 From: John Dvorak Date: Fri, 22 May 2026 12:46:41 -0700 Subject: [PATCH] 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. --- packages/imhotep-dsl/src/validator.ts | 133 ++++++++------------------ 1 file changed, 41 insertions(+), 92 deletions(-) diff --git a/packages/imhotep-dsl/src/validator.ts b/packages/imhotep-dsl/src/validator.ts index 8774426..44274d5 100644 --- a/packages/imhotep-dsl/src/validator.ts +++ b/packages/imhotep-dsl/src/validator.ts @@ -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> = { - 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> = { '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 { + // 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 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',