refactor: remove global registry fallbacks — factory pattern for test isolation

predicates.ts: Add populateDefaultPredicates(registry) accepting any
  PredicateRegistry. Add createDefaultPredicateRegistry() factory.
  registerDefaultPredicates() now delegates to populateDefaultPredicates
  on the global (backward compatible).

logic-engine.ts: Replace globalPredicateRegistry fallback with
  createDefaultPredicateRegistry() factory. Each evaluateLogic() call
  creates a fresh self-populated registry unless one is explicitly
  injected. No shared mutable state between evaluations.

engine.ts: Same pattern for clauses — add populateDefaultClauses(registry),
  createDefaultClauseRegistry() factory. registerDefaultClauses() now
  delegates to populateDefaultClauses on the global. evaluate() replaces
  globalClauseRegistry fallback with createDefaultClauseRegistry().

registry.ts: @deprecated tag on registerClause with migration note.

Both global registries remain for backward compatibility via the
  deprecated registerDefault*() functions, but the evaluation engines
  no longer depend on them. Every evaluation gets its own registry by
  default, so custom predicates registered by one test cannot leak
  into another. Tests using explicit registry injection are unaffected.

662 tests pass (315 DSL + 141 core + 149 solver + 57 E2E).
This commit is contained in:
John Dvorak
2026-05-22 15:44:44 -07:00
parent 6c2471052f
commit 066ef9f677
5 changed files with 109 additions and 75 deletions
+53 -34
View File
@@ -72,100 +72,119 @@ export interface EvaluationOutput {
} }
/** /**
* Register all built-in clause evaluators with the global registry. * Populate a ClauseRegistry with all built-in clause evaluators (idempotent).
* Call this once before invoking evaluate() if you want the default set.
*/ */
export function registerDefaultClauses(): void { export function populateDefaultClauses(registry: ClauseRegistry): void {
registerClause({ const SENTINEL = '__imhotep_clause_defaults__'
if (registry.getEvaluator(SENTINEL)) return
registry.register({
clauseKind: SENTINEL,
version: 1,
requiredFacts: [],
evaluate: () => ({ clauseId: '', status: 'pass' as const, truth: 'determinate' as const }),
})
registry.register({
clauseKind: 'relation.leftOf', clauseKind: 'relation.leftOf',
version: 1, version: 1,
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
evaluate: evaluateLeftOf, evaluate: evaluateLeftOf,
}); })
registerClause({ registry.register({
clauseKind: 'relation.rightOf', clauseKind: 'relation.rightOf',
version: 1, version: 1,
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
evaluate: evaluateRightOf, evaluate: evaluateRightOf,
}); })
registerClause({ registry.register({
clauseKind: 'relation.above', clauseKind: 'relation.above',
version: 1, version: 1,
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
evaluate: evaluateAbove, evaluate: evaluateAbove,
}); })
registerClause({ registry.register({
clauseKind: 'relation.below', clauseKind: 'relation.below',
version: 1, version: 1,
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
evaluate: evaluateBelow, evaluate: evaluateBelow,
}); })
registerClause({ registry.register({
clauseKind: 'relation.overlaps', clauseKind: 'relation.overlaps',
version: 1, version: 1,
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
evaluate: evaluateOverlaps, evaluate: evaluateOverlaps,
}); })
registerClause({ registry.register({
clauseKind: 'relation.inside', clauseKind: 'relation.inside',
version: 1, version: 1,
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
evaluate: evaluateInside, evaluate: evaluateInside,
}); })
registerClause({ registry.register({
clauseKind: 'alignment.alignedWith', clauseKind: 'alignment.alignedWith',
version: 1, version: 1,
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
evaluate: evaluateAlignedWith, evaluate: evaluateAlignedWith,
}); })
registerClause({ registry.register({
clauseKind: 'alignment.centeredWithin', clauseKind: 'alignment.centeredWithin',
version: 1, version: 1,
requiredFacts: ['subject.primaryBox', 'reference.primaryBox'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'],
evaluate: evaluateCenteredWithin, evaluate: evaluateCenteredWithin,
}); })
registerClause({ registry.register({
clauseKind: 'size.atLeast', clauseKind: 'size.atLeast',
version: 1, version: 1,
requiredFacts: ['subject.primaryBox'], requiredFacts: ['subject.primaryBox'],
evaluate: evaluateAtLeast, evaluate: evaluateAtLeast,
}); })
registerClause({ registry.register({
clauseKind: 'size.atMost', clauseKind: 'size.atMost',
version: 1, version: 1,
requiredFacts: ['subject.primaryBox'], requiredFacts: ['subject.primaryBox'],
evaluate: evaluateAtMost, evaluate: evaluateAtMost,
}); })
registerClause({ registry.register({
clauseKind: 'size.between', clauseKind: 'size.between',
version: 1, version: 1,
requiredFacts: ['subject.primaryBox'], requiredFacts: ['subject.primaryBox'],
evaluate: evaluateBetween, evaluate: evaluateBetween,
}); })
registerClause({ registry.register({
clauseKind: 'size.aspectRatio', clauseKind: 'size.aspectRatio',
version: 1, version: 1,
requiredFacts: ['subject.primaryBox'], requiredFacts: ['subject.primaryBox'],
evaluate: evaluateAspectRatio, evaluate: evaluateAspectRatio,
}); })
registerClause({ registry.register({
clauseKind: 'topology.clippedBy', clauseKind: 'topology.clippedBy',
version: 1, version: 1,
requiredFacts: ['subject.clipChain', 'reference.clipChain'], requiredFacts: ['subject.clipChain', 'reference.clipChain'],
evaluate: evaluateClippedBy, evaluate: evaluateClippedBy,
}); })
registerClause({ registry.register({
clauseKind: 'topology.attachedToScrollContainer', clauseKind: 'topology.attachedToScrollContainer',
version: 1, version: 1,
requiredFacts: ['topology.scrollContainerOf'], requiredFacts: ['topology.scrollContainerOf'],
evaluate: evaluateAttachedToScrollContainer, evaluate: evaluateAttachedToScrollContainer,
}); })
registerClause({ registry.register({
clauseKind: 'topology.inStackingContext', clauseKind: 'topology.inStackingContext',
version: 1, version: 1,
requiredFacts: ['topology.stackingContextOf'], requiredFacts: ['topology.stackingContextOf'],
evaluate: evaluateInStackingContext, evaluate: evaluateInStackingContext,
}); })
}
/** Create a fresh ClauseRegistry with all built-in clause evaluators pre-registered. */
export function createDefaultClauseRegistry(): ClauseRegistry {
const registry = new ClauseRegistry()
populateDefaultClauses(registry)
return registry
}
/** @deprecated Use createDefaultClauseRegistry() or explicit ClauseRegistry injection. */
export function registerDefaultClauses(): void {
populateDefaultClauses(globalClauseRegistry)
} }
/** /**
@@ -176,7 +195,7 @@ export function evaluate(
clauses: ClauseDescriptor[], clauses: ClauseDescriptor[],
options: EvaluationOptions = {}, options: EvaluationOptions = {},
): EvaluationOutput { ): EvaluationOutput {
const registry = options.registry ?? globalClauseRegistry; const registry = options.registry ?? createDefaultClauseRegistry();
// Reset per-evaluation transform caches so visual rects are recomputed // Reset per-evaluation transform caches so visual rects are recomputed
// once per subject per evaluation batch. // once per subject per evaluation batch.
clearEvaluationCache(world); clearEvaluationCache(world);
+4
View File
@@ -66,6 +66,8 @@ export {
evaluate, evaluate,
collectRequiredFacts, collectRequiredFacts,
registerDefaultClauses, registerDefaultClauses,
createDefaultClauseRegistry,
populateDefaultClauses,
type EvaluationOptions, type EvaluationOptions,
type EvaluationOutput, type EvaluationOutput,
} from './engine.js'; } from './engine.js';
@@ -78,6 +80,8 @@ export {
getPredicateDescriptor, getPredicateDescriptor,
getRequiredFactsForPredicate, getRequiredFactsForPredicate,
registerDefaultPredicates, registerDefaultPredicates,
createDefaultPredicateRegistry,
populateDefaultPredicates,
BUILTIN_PREDICATES, BUILTIN_PREDICATES,
type PredicateDescriptor, type PredicateDescriptor,
type PredicateEvaluator, type PredicateEvaluator,
+2 -2
View File
@@ -37,7 +37,7 @@ import {
getPredicateEvaluator, getPredicateEvaluator,
registerDefaultPredicates, registerDefaultPredicates,
PredicateRegistry, PredicateRegistry,
globalPredicateRegistry, createDefaultPredicateRegistry,
} from './predicates.js'; } from './predicates.js';
import { import {
@@ -906,7 +906,7 @@ export function evaluateLogic(input: LogicEngineInput): DeterministicSceneEvalua
proofs: [], proofs: [],
proofCounter: 0, proofCounter: 0,
formulaCounter: 0, formulaCounter: 0,
predicateRegistry: options.predicateRegistry ?? globalPredicateRegistry, predicateRegistry: options.predicateRegistry ?? createDefaultPredicateRegistry(),
}; };
addTrace(state, 'evaluate-logic-start'); addTrace(state, 'evaluate-logic-start');
+49 -38
View File
@@ -1200,45 +1200,56 @@ export const hasGapPredicate: PredicateEvaluator = {
/** Sentinel registered to detect if defaults were already installed. */ /** Sentinel registered to detect if defaults were already installed. */
const DEFAULT_SENTINEL = '__imhotep_defaults_registered__' const DEFAULT_SENTINEL = '__imhotep_defaults_registered__'
export function registerDefaultPredicates(): void { /** Populate a PredicateRegistry with all 33 built-in predicates (idempotent). */
if (globalPredicateRegistry.get(DEFAULT_SENTINEL)) return export function populateDefaultPredicates(registry: PredicateRegistry): void {
// Register sentinel first so partial failures don't cause infinite loops. if (registry.get(DEFAULT_SENTINEL)) return
globalPredicateRegistry.register({ registry.register({
descriptor: { name: DEFAULT_SENTINEL, arity: 0, domains: [], requiredFacts: [] }, descriptor: { name: DEFAULT_SENTINEL, arity: 0, domains: [], requiredFacts: [] },
evaluateTuple: () => ({ truth: 'indeterminate' }), evaluateTuple: () => ({ truth: 'indeterminate' }),
}) })
registerPredicate(widthPredicate); registry.register(widthPredicate)
registerPredicate(heightPredicate); registry.register(heightPredicate)
registerPredicate(abovePredicate); registry.register(abovePredicate)
registerPredicate(belowPredicate); registry.register(belowPredicate)
registerPredicate(leftOfPredicate); registry.register(leftOfPredicate)
registerPredicate(rightOfPredicate); registry.register(rightOfPredicate)
registerPredicate(insidePredicate); registry.register(insidePredicate)
registerPredicate(containsPredicate); registry.register(containsPredicate)
registerPredicate(overlapsPredicate); registry.register(overlapsPredicate)
registerPredicate(alignedWithPredicate); registry.register(alignedWithPredicate)
registerPredicate(centeredWithinPredicate); registry.register(centeredWithinPredicate)
registerPredicate(atLeastPredicate); registry.register(atLeastPredicate)
registerPredicate(atMostPredicate); registry.register(atMostPredicate)
registerPredicate(betweenPredicate); registry.register(betweenPredicate)
registerPredicate(clippedByPredicate); registry.register(clippedByPredicate)
registerPredicate(attachedToScrollContainerPredicate); registry.register(attachedToScrollContainerPredicate)
registerPredicate(escapeClippingChainOfPredicate); registry.register(escapeClippingChainOfPredicate)
registerPredicate(aspectRatioPredicate); registry.register(aspectRatioPredicate)
registerPredicate(inStackingContextPredicate); registry.register(inStackingContextPredicate)
registerPredicate(separatedFromPredicate); registry.register(separatedFromPredicate)
registerPredicate(leftAlignedWithPredicate); registry.register(leftAlignedWithPredicate)
registerPredicate(rightAlignedWithPredicate); registry.register(rightAlignedWithPredicate)
registerPredicate(topAlignedWithPredicate); registry.register(topAlignedWithPredicate)
registerPredicate(bottomAlignedWithPredicate); registry.register(bottomAlignedWithPredicate)
registerPredicate(intersectsPredicate); registry.register(intersectsPredicate)
registerPredicate(touchesPredicate); registry.register(touchesPredicate)
registerPredicate(hasGapPredicate); registry.register(hasGapPredicate)
registerPredicate(besidePredicate); registry.register(besidePredicate)
registerPredicate(nextToPredicate); registry.register(nextToPredicate)
registerPredicate(adjacentPredicate); registry.register(adjacentPredicate)
registerPredicate(touchingPredicate); registry.register(touchingPredicate)
registerPredicate(nearPredicate); registry.register(nearPredicate)
registerPredicate(underPredicate); registry.register(underPredicate)
registerPredicate(withinPredicate); registry.register(withinPredicate)
}
/** Create a fresh PredicateRegistry with all 33 built-in predicates pre-registered. */
export function createDefaultPredicateRegistry(): PredicateRegistry {
const registry = new PredicateRegistry()
populateDefaultPredicates(registry)
return registry
}
export function registerDefaultPredicates(): void {
populateDefaultPredicates(globalPredicateRegistry)
} }
+1 -1
View File
@@ -297,7 +297,7 @@ export class ClauseRegistry {
/** @deprecated Use explicit ClauseRegistry injection via EvaluationOptions.registry. */ /** @deprecated Use explicit ClauseRegistry injection via EvaluationOptions.registry. */
export const globalClauseRegistry = new ClauseRegistry(); export const globalClauseRegistry = new ClauseRegistry();
/** Register a clause family so the engine can route evaluation. */ /** Register a clause family in the global registry. @deprecated Use ClauseRegistry.register directly. */
export function registerClause(entry: ClauseEntry): void { export function registerClause(entry: ClauseEntry): void {
globalClauseRegistry.register(entry); globalClauseRegistry.register(entry);
} }