compiler.ts: normalizeOptionValue now rejects NaN numbers and empty
strings. Previously typeof NaN === 'number' passed through and
propagated into option values, causing predicate comparisons like
value >= NaN to produce silent wrong results.
grammar.ts: parseAssertion now emits console.warn when silently
skipping unexpected tokens or failed clauses. Previously these
returns-null were invisible to developers.
454 solver+DSL + 57 E2E tests pass.
lower-to-canonical.ts: clauseEquivalent now compares compoundOperator
and compoundGroupId. Previously, compound assertions with different
operators (.and vs .or) were considered equivalent.
fol-compiler.ts: adaptGrammarFormulaToLogicAst validates node.kind
against known formula kinds (forall/exists/and/or/not/implies/predicate)
before passing through as FormulaNode. Previously any object with a
'kind' property was blindly cast.
predicates.ts / registry.ts: @deprecated tags on globalPredicateRegistry
and globalClauseRegistry. Both are still functional but consumers should
transition to explicit injection via LogicEngineOptions / EvaluationOptions.
454 solver+DSL tests pass, zero regressions.
geometry-cache.ts: replace 5 empty catch blocks with console.warn
- statSync failure, rmSync failure (x2), readCachedWorld failure,
readCachedExtractionResult failure were all silently swallowed.
Now emit context-bearing warnings so stale/corrupt caches are visible.
predicates.ts: replace __boxIndex as any mutation with WeakMap
- getBorderRect used (world as any).__boxIndex to cache a subject-to-
box-index map on the world object. Replaced with module-level WeakMap
that auto-collects when the world is GC'd. Eliminates 2 as any casts.
extraction.ts: serialize materializeSemanticSelector + debug cleanup
- 3 Promise.all sites over page.evaluate changed to sequential for..of
to eliminate DOM modification race conditions.
- 2 .catch(()=>{}) cleanup blocks now use console.debug so failed
cleanup is traceable when debugging.
- resolveViewport catch now emits console.warn on zero-viewport fallback.
648 SDK + 57 E2E tests pass.
pipeline.ts: || undefined → ?? undefined (9 occurrences)
- || converts valid subject ID 0 to undefined because 0 is falsy in JS.
This broke clause witnesses and topology references for the first subject.
domain-index.ts: remove .toLowerCase() on CSS selectors
- CSS selectors are case-sensitive (IDs, class names, attribute values).
Lowercasing on lookup but not on storage (selectorIndex) meant case-
sensitive selectors never matched — returning empty arrays silently.
canonical.ts: add warning when visualBoxes falls back to layout boxes
- visualBoxes ?? boxes silently substituted layout coordinates for visual
space, producing incorrect results for transform-dependent assertions.
Now emits console.warn so silent data corruption is visible.
extraction.ts: serialize materializeSemanticSelector calls (3 sites)
- Changed Promise.all over page.evaluate() to sequential for..of. While
Playwright serializes CDP calls internally, concurrent DOM-modifying
evaluate() calls create undefined execution order. Sequential resolution
eliminates theoretical race conditions for semantic selector injection.
engine.ts: include stack trace in evaluator exception diagnostics
- Catch-all converted ALL exceptions (including TypeError from programming
bugs) to IMH_EVALUATOR_EXCEPTION with just err.message. Now includes
stack trace and logs to console.warn for visibility. Distinguishes
TypeError (programming bug) from other evaluation errors.
648 SDK tests + 57 E2E hard tests pass, zero regressions.
Topology array order mismatch (critical bug):
- CDP browser script iterates elements in document order (querySelectorAll),
but solver accesses topology arrays by selector-resolution order.
- Fix: add subjectIds array to TopologyRecord tracking the backendNodeId
at each document-order position; remapTopologyIds now reorders all 6
topology arrays before remapping backendNodeIds to solver IDs.
- Fallback: when subjectIds is missing (cached pre-fix data), falls back
to simple remap without reordering.
Unary inStackingContext compilation:
- compileCanonicalClauseToFormula treated inStackingContext as always binary,
creating referenceBinding with undefined selector for unary assertions.
- Fix: add (inStackingContext && !clause.reference) to unaryPredicate/isUnary.
Hard test fixture (fixtures package):
- 29-element geometric fixture with 10 scenarios
- 57 assertions (54 spatial + 3 topology) all pass end-to-end
- grammar.ts: add 'between' to isKeywordThatCanBePredicate() so the parser
recognizes it as a valid predicate keyword in forall/exists formula bodies
- compiler.ts: add special case in compileDenseFOLToFormula for
between(, min, max, dimension?) that extracts numeric args into
options ({min, max, dimension}) instead of dropping them in the generic
arg loop. The existing betweenPredicate evaluator already handles these.
- fol-dense-combinations.test.ts: replace GAP test with two verified-working
tests for between and between with dimension
All 27 BUILTIN_PREDICATES now have real evaluators. The
makeNotImplementedPredicate factory and its IMH_FEATURE_NOT_YET_IMPLEMENTED
path were the last remaining NYI scaffolding — no caller existed for it.
IMH_FEATURE_NOT_YET_IMPLEMENTED diagnostic code kept in the taxonomy
as a future fallback, but no evaluator produces it.
- Add children branch in convertDomain() alongside descendants —
children() now correctly maps to parentVar: '$parent'
instead of falling through to the default branch which reversed
the parentVar/selector mapping.
- SelectorDomainResolver now handles parentVar domains with no
CSS selector filter (children domain collects all registered
subject IDs, then filters by parent via ancestor index).
- All 1072 tests pass across 14 packages.
- 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.
- computeRequiredFacts now returns domAncestry flag by scanning formula
bindings for parentVar references (descendants/children domains)
- extractWorld fast-path gate now requires domAncestry === false — formulas
with parentVar domains will always use CDP extraction, which provides
the DOM parentNodeId data needed for ancestor index construction
- Prevents silent indeterminate results when descendants(, sel) is
used on the fast path (which lacks DOM ancestry data)
- Cache key updated to include domAncestry flag ('a') so cached fast vs
CDP results for the same selectors don't collide
- mapFolDiagnostic now accepts optional metrics parameter
- adaptFOLResultToImhotepResult matches solver diagnostics to
formula results by clauseId/formulaId and passes formula-level
metrics (gap, dimensions, stacking context, overflow, etc.)
into the diagnostics output
- Previously mapFolDiagnostic always returned metrics: {} and
sourceRef: {}, making compound/quantified failures undiagnosable
for users investigating contracts
- evaluateAnd: carry left metrics on left-fail short-circuit,
carry right metrics on right fail/indeterminate
- evaluateOr: carry left metrics on left-pass short-circuit,
carry right metrics on right pass, merge both on double-fail
- evaluateNot: carry operand metrics when operand passes
(so not fails with diagnostic context), carry on indeterminate
- evaluateImplies: carry consequent metrics on fail/indeterminate
This ensures compound formula failures preserve measured geometry
context (gap, dimensions, direction) rather than presenting empty
diagnostics to users investigating layout contract violations.
- Change BUILTIN_PREDICATES[15] arity from 1 to 2 to match binary usage
in the FOL compiler and topology engine
- Update evaluator to handle both unary (does element have stacking context)
and binary (do both elements share the same stacking context) cases
- Binary comparison: both elements must have a valid stacking context
AND their stackingContextOf values must match
- Aligns predicate semantics with the legacy engine in solver/topology.ts
and the topology query layer's inStackingContext(graph, subj, ref?) API
- Extend DomainResolver.resolve() signature to accept optional BindingEnv
so that parentVar domains can be resolved with runtime variable bindings
- Pass BindingEnv through evaluateForAll/evaluateExists to resolver calls
- Add buildAncestorIndex() to precompute DOM ancestor sets from CDP data
- SelectorDomainResolver now filters descendant domains by the bound parent
when domain.parentVar is present and ancestor index is available
- Return undefined for parentVar domains when no ancestor index or env
(prevents silent fallback to global domain resolution)
- Update all test DomainResolver mocks for new resolve interface
- Add 10 unit tests covering ancestor index construction, backward compat,
descendant filtering, exclusion of non-descendants, empty descendants,
missing parentVar/env, and no-ancestor-index safety
- renderer.kind is typed on RendererDescriptor, no cast needed
- buildGeometryWorld returns proper GeometryWorld type (solver variant
differs from core variant, as any required for that cross-package cast)
- Add 4 unit tests in predicates.test.ts for new contain metrics:
inside hasClippedOverflow (with/without clipping)
clippedBy clipKind (contain:paint=1, overflow=2)
- Unskip and implement clippedBy e2e test with overflow:hidden container
in e2e-edge.test.ts (was stale skipped with 'not yet implemented')
- Add leftAlignedWith/rightAlignedWith/topAlignedWith/bottomAlignedWith
to lexer TokenKind union and keyword map
- Add to grammar.ts consumeRelation() recognized relation kinds
- Dense DSL users can now write: '.a' leftAlignedWith '.b', etc.
(previously returned parser error 'Expected relation')
- Replace makeNotImplementedPredicate stubs for leftAlignedWith,
rightAlignedWith, topAlignedWith, bottomAlignedWith with real
evaluators delegating to alignedWithPredicate with axis option
- Update logic-engine.test.ts to expect real evaluation results
instead of IMH_FEATURE_NOT_YET_IMPLEMENTED
- insidePredicate: add hasClippedOverflow=1 when reference element
clips its content (contain:paint or overflow:hidden) and the
subject overflows beyond the reference bounds
- containsPredicate: same, checking the subject (container) clips
- Reads world.clipping table to determine if the container clips
- Safe when clipping data is absent (unit test fixtures)
The proxy in buildExpectation() returned FluentRelation raw (not
proxied), so .and/.or chaining happened outside the assertion store.
The second relation was never stored — checkAll() only evaluated
the first relation (false positive).
Add ensureAndOrProxied() to override .and/.or on FluentRelation
instances so compound builders flow through the assertion store
proxy. Preserves instanceof FluentRelation checks (no JS Proxy on
the instance, just property overrides).
3 regression tests cover .and, .or, and triple-chaining.
Extracted the 629-line checkAll closure from imhotep() into a standalone
makeCheckAll() factory in check-all.ts. Handles temporal dead zone by
passing getUi() getter instead of ui value.
public.ts reduced from 3568 lines to 916 lines (-74.3%). Now contains
only: imports/re-exports, normalizeRuntime, the imhotep() factory
function, property-run internals, and entry points.
Moved the extraction pipeline, formula analysis, selector resolution glue,
CDP extraction, canonical compilation, contract building, cardinality
evaluation, FOL diagnostic mapping, compatibility reporting, and all
module-level extraction state into a dedicated extraction.ts module.
public.ts reduced from 3568 to 1533 lines (-57%). The remaining file
contains only the imhotep() entry point, property-run internals, and
component/story/fixture entry points, plus re-exports for backward
compatibility.