ast.ts: Add optional compoundOperator and compoundParts fields to
RelationAssertion, allowing compound fluent assertions to carry
their .and / .or structure through the AST layer.
fluent.ts: toAst() now emits compoundOperator/compoundParts when
isCompound, closing the gap where compound state was silently
dropped in canonical extraction, preset building, and validation.
lower-to-canonical.ts: Handle RelationAssertion with compound metadata
by lowering each part as a separate grouped clause (same groupId,
same compoundOperator), matching DSL parser compound behavior.
validator.ts: Validate options on each compound part, closing the
false-positive validation gap where only the final relation was checked.
3 lossy code paths (extractCanonicalFromAssertion, presets.toPresetResult,
validateRelation) now preserve compound structure. Primary FOL evaluation
path was already correct (fol-compiler reads compoundParts directly).
658 tests pass.
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.