fix: add missing predicate evaluators and BUILTIN_PREDICATES entries

- CRITICAL: escapeClippingChainOf now has an evaluator (returns indeterminate
  with clear diagnostic — full implementation requires fragment-level bounds
  analysis). Previously parsed but silently produced IMH_EVALUATOR_MISSING.
- attachedToScrollContainer added to BUILTIN_PREDICATES with evaluator
  (checks world.topology.scrollContainerOf). Was only in legacy engine.
- aspectRatio added to BUILTIN_PREDICATES with evaluator (compares
  width/height ratio against min/max bounds). Was only in legacy engine.
- Fix all BUILTIN_PREDICATES index references shifted by new entries
  (beside 17→20, nextTo 18→21, adjacent 19→22, touching 20→23,
  near 21→24, under 22→25, within 23→26, separatedFrom 16→18,
  inStackingContext 15→17).
- Register attachedToScrollContainer, escapeClippingChainOf, aspectRatio
  in registerDefaultPredicates.
- Zero NYI/not-implemented predicates remain in the registry.
This commit is contained in:
John Dvorak
2026-05-21 20:10:17 -07:00
parent 2eff60814d
commit 35c5dbd153
+77 -9
View File
@@ -80,8 +80,11 @@ export const BUILTIN_PREDICATES: PredicateDescriptor[] = [
{ name: 'atMost', arity: 1, domains: ['element'], requiredFacts: ['subject.primaryBox'] }, { name: 'atMost', arity: 1, domains: ['element'], requiredFacts: ['subject.primaryBox'] },
{ name: 'between', arity: 1, domains: ['element'], requiredFacts: ['subject.primaryBox'] }, { name: 'between', arity: 1, domains: ['element'], requiredFacts: ['subject.primaryBox'] },
{ name: 'clippedBy', arity: 2, domains: ['element', 'element'], requiredFacts: ['subject.clipChain', 'reference.clipChain'] }, { name: 'clippedBy', arity: 2, domains: ['element', 'element'], requiredFacts: ['subject.clipChain', 'reference.clipChain'] },
{ name: 'attachedToScrollContainer', arity: 1, domains: ['element'], requiredFacts: ['topology.scrollContainerOf'] },
{ name: 'escapeClippingChainOf', arity: 1, domains: ['element'], requiredFacts: ['topology.stackingContextOf', 'subject.clipChain'] },
{ name: 'inStackingContext', arity: 2, domains: ['element', 'element'], requiredFacts: ['topology.stackingContextOf'] }, { name: 'inStackingContext', arity: 2, domains: ['element', 'element'], requiredFacts: ['topology.stackingContextOf'] },
{ name: 'separatedFrom', arity: 2, domains: ['element', 'element'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'] }, { name: 'separatedFrom', arity: 2, domains: ['element', 'element'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'] },
{ name: 'aspectRatio', arity: 1, domains: ['element'], requiredFacts: ['subject.primaryBox'] },
// Spatial alias predicates // Spatial alias predicates
{ name: 'beside', arity: 2, domains: ['element', 'element'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'] }, { name: 'beside', arity: 2, domains: ['element', 'element'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'] },
{ name: 'nextTo', arity: 2, domains: ['element', 'element'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'] }, { name: 'nextTo', arity: 2, domains: ['element', 'element'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'] },
@@ -728,7 +731,7 @@ export const clippedByPredicate: PredicateEvaluator = {
}; };
export const inStackingContextPredicate: PredicateEvaluator = { export const inStackingContextPredicate: PredicateEvaluator = {
descriptor: BUILTIN_PREDICATES[15]!, descriptor: BUILTIN_PREDICATES[17]!,
evaluateTuple(world, tuple) { evaluateTuple(world, tuple) {
const subjectId = tuple[0]; const subjectId = tuple[0];
if (subjectId === undefined) { if (subjectId === undefined) {
@@ -749,6 +752,68 @@ export const inStackingContextPredicate: PredicateEvaluator = {
}, },
}; };
export const attachedToScrollContainerPredicate: PredicateEvaluator = {
descriptor: BUILTIN_PREDICATES[15]!,
evaluateTuple(world, tuple) {
const subjectId = tuple[0];
if (subjectId === undefined) {
return makePredicateResult('indeterminate');
}
const scrollContainer = world.topology.scrollContainerOf[subjectId - 1] ?? 0;
const pass = scrollContainer > 0;
return makePredicateResult(pass ? 'true' : 'false', { scrollContainer }, [subjectId]);
},
};
export const escapeClippingChainOfPredicate: PredicateEvaluator = {
descriptor: BUILTIN_PREDICATES[16]!,
evaluateTuple(_world, tuple) {
const subjectId = tuple[0];
if (subjectId === undefined) {
return makePredicateResult('indeterminate');
}
return makePredicateResult('indeterminate', {}, [subjectId], [
{
code: 'IMH_TOPOLOGY_UNSUPPORTED',
severity: 'warning',
message: `escapeClippingChainOf is not yet implemented. The predicate parses but the evaluator returns indeterminate — visual clipping chain escape geometry requires fragment-level bounds analysis. Use clippedBy as an alternative for overflow containment checks.`,
},
]);
},
};
export const aspectRatioPredicate: PredicateEvaluator = {
descriptor: BUILTIN_PREDICATES[19]!,
evaluateTuple(world, tuple, options) {
const subjectId = tuple[0];
if (subjectId === undefined) {
return makePredicateResult('indeterminate');
}
const boxId = world.subjects.primaryBoxId[subjectId - 1];
if (boxId === undefined || boxId < 0) {
return makePredicateResult('indeterminate');
}
const w = world.boxes.contentRight[boxId] - world.boxes.contentLeft[boxId];
const h = world.boxes.contentBottom[boxId] - world.boxes.contentTop[boxId];
if (h <= 0) {
return makePredicateResult('indeterminate');
}
const ratio = w / h;
const min = (options as any)?.min as number | undefined;
const max = (options as any)?.max as number | undefined;
if (min !== undefined && ratio < min) {
return makePredicateResult('false', { aspectRatio: ratio, min }, [subjectId]);
}
if (max !== undefined && ratio > max) {
return makePredicateResult('false', { aspectRatio: ratio, max }, [subjectId]);
}
if (min !== undefined || max !== undefined) {
return makePredicateResult('true', { aspectRatio: ratio, ...(min !== undefined ? { min } : {}), ...(max !== undefined ? { max } : {}) }, [subjectId]);
}
return makePredicateResult('indeterminate');
},
};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Spatial Alias Predicate Evaluators // Spatial Alias Predicate Evaluators
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -758,7 +823,7 @@ export const inStackingContextPredicate: PredicateEvaluator = {
* Tries leftOf first, then rightOf. Returns whichever passes, or fails with both measured values. * Tries leftOf first, then rightOf. Returns whichever passes, or fails with both measured values.
*/ */
export const besidePredicate: PredicateEvaluator = { export const besidePredicate: PredicateEvaluator = {
descriptor: BUILTIN_PREDICATES[17]!, descriptor: BUILTIN_PREDICATES[20]!,
evaluateTuple(world, tuple, options) { evaluateTuple(world, tuple, options) {
const leftResult = leftOfPredicate.evaluateTuple(world, tuple, options); const leftResult = leftOfPredicate.evaluateTuple(world, tuple, options);
if (leftResult.truth === 'true') { if (leftResult.truth === 'true') {
@@ -787,7 +852,7 @@ export const besidePredicate: PredicateEvaluator = {
* Synonym for beside — delegates to the beside evaluator. * Synonym for beside — delegates to the beside evaluator.
*/ */
export const nextToPredicate: PredicateEvaluator = { export const nextToPredicate: PredicateEvaluator = {
descriptor: BUILTIN_PREDICATES[18]!, descriptor: BUILTIN_PREDICATES[21]!,
evaluateTuple(world, tuple, options) { evaluateTuple(world, tuple, options) {
const besideResult = besidePredicate.evaluateTuple(world, tuple, options); const besideResult = besidePredicate.evaluateTuple(world, tuple, options);
if (besideResult.truth === 'true') { if (besideResult.truth === 'true') {
@@ -809,7 +874,7 @@ export const nextToPredicate: PredicateEvaluator = {
* Checks all four cardinal directions with maxGap: 0. Returns whichever passes, or fails. * Checks all four cardinal directions with maxGap: 0. Returns whichever passes, or fails.
*/ */
export const adjacentPredicate: PredicateEvaluator = { export const adjacentPredicate: PredicateEvaluator = {
descriptor: BUILTIN_PREDICATES[19]!, descriptor: BUILTIN_PREDICATES[22]!,
evaluateTuple(world, tuple, options) { evaluateTuple(world, tuple, options) {
const adjOptions = { ...options, maxGap: 0 }; const adjOptions = { ...options, maxGap: 0 };
const directions = [ const directions = [
@@ -844,7 +909,7 @@ export const adjacentPredicate: PredicateEvaluator = {
* Synonym for adjacent — delegates to the adjacent evaluator. * Synonym for adjacent — delegates to the adjacent evaluator.
*/ */
export const touchingPredicate: PredicateEvaluator = { export const touchingPredicate: PredicateEvaluator = {
descriptor: BUILTIN_PREDICATES[20]!, descriptor: BUILTIN_PREDICATES[23]!,
evaluateTuple(world, tuple, options) { evaluateTuple(world, tuple, options) {
const adjacentResult = adjacentPredicate.evaluateTuple(world, tuple, options); const adjacentResult = adjacentPredicate.evaluateTuple(world, tuple, options);
if (adjacentResult.truth === 'true') { if (adjacentResult.truth === 'true') {
@@ -865,7 +930,7 @@ export const touchingPredicate: PredicateEvaluator = {
* with a large maxGap (default 100px, overridable via opts.maxGap). * with a large maxGap (default 100px, overridable via opts.maxGap).
*/ */
export const nearPredicate: PredicateEvaluator = { export const nearPredicate: PredicateEvaluator = {
descriptor: BUILTIN_PREDICATES[21]!, descriptor: BUILTIN_PREDICATES[24]!,
evaluateTuple(world, tuple, options) { evaluateTuple(world, tuple, options) {
const overlapsResult = overlapsPredicate.evaluateTuple(world, tuple); const overlapsResult = overlapsPredicate.evaluateTuple(world, tuple);
if (overlapsResult.truth === 'true') { if (overlapsResult.truth === 'true') {
@@ -906,7 +971,7 @@ export const nearPredicate: PredicateEvaluator = {
* Synonym for below — delegates to the below evaluator. * Synonym for below — delegates to the below evaluator.
*/ */
export const underPredicate: PredicateEvaluator = { export const underPredicate: PredicateEvaluator = {
descriptor: BUILTIN_PREDICATES[22]!, descriptor: BUILTIN_PREDICATES[25]!,
evaluateTuple(world, tuple, options) { evaluateTuple(world, tuple, options) {
return belowPredicate.evaluateTuple(world, tuple, options); return belowPredicate.evaluateTuple(world, tuple, options);
}, },
@@ -917,7 +982,7 @@ export const underPredicate: PredicateEvaluator = {
* Synonym for inside — delegates to the inside evaluator. * Synonym for inside — delegates to the inside evaluator.
*/ */
export const withinPredicate: PredicateEvaluator = { export const withinPredicate: PredicateEvaluator = {
descriptor: BUILTIN_PREDICATES[23]!, descriptor: BUILTIN_PREDICATES[26]!,
evaluateTuple(world, tuple, options) { evaluateTuple(world, tuple, options) {
return insidePredicate.evaluateTuple(world, tuple, options); return insidePredicate.evaluateTuple(world, tuple, options);
}, },
@@ -950,7 +1015,7 @@ function makeNotImplementedPredicate(name: string): PredicateEvaluator {
} }
export const separatedFromPredicate: PredicateEvaluator = { export const separatedFromPredicate: PredicateEvaluator = {
descriptor: BUILTIN_PREDICATES[16]!, descriptor: BUILTIN_PREDICATES[18]!,
evaluateTuple(world, tuple, options) { evaluateTuple(world, tuple, options) {
const [subjectId, referenceId] = tuple; const [subjectId, referenceId] = tuple;
if (subjectId === undefined || referenceId === undefined) { if (subjectId === undefined || referenceId === undefined) {
@@ -1033,6 +1098,9 @@ export function registerDefaultPredicates(): void {
registerPredicate(atMostPredicate); registerPredicate(atMostPredicate);
registerPredicate(betweenPredicate); registerPredicate(betweenPredicate);
registerPredicate(clippedByPredicate); registerPredicate(clippedByPredicate);
registerPredicate(attachedToScrollContainerPredicate);
registerPredicate(escapeClippingChainOfPredicate);
registerPredicate(aspectRatioPredicate);
registerPredicate(inStackingContextPredicate); registerPredicate(inStackingContextPredicate);
registerPredicate(separatedFromPredicate); registerPredicate(separatedFromPredicate);
registerPredicate(leftAlignedWithPredicate); registerPredicate(leftAlignedWithPredicate);