v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)
This commit is contained in:
@@ -0,0 +1,934 @@
|
||||
/**
|
||||
* Deterministic first-order logic evaluation engine.
|
||||
*
|
||||
* Evaluates Imhotep logic formulas exhaustively over a finite geometry
|
||||
* world. The engine supports:
|
||||
*
|
||||
* - Universal quantification (forall) with short-circuit on first failure
|
||||
* - Existential quantification (exists) with short-circuit on first success
|
||||
* - Boolean connectives: And, Or, Not, Implies
|
||||
* - Predicate calls against a registered predicate registry
|
||||
*
|
||||
* Invariants:
|
||||
* 1. Closed-world semantics: every domain is finite and enumerated.
|
||||
* 2. No silent defaults: missing facts produce indeterminate results.
|
||||
* 3. Rich diagnostics: every failing assignment is reported with proof ids
|
||||
* and trace events.
|
||||
* 4. Deterministic vs sampled is explicit in the result mode.
|
||||
*/
|
||||
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import type { DiagnosticCode } from 'imhotep-core'
|
||||
import type {
|
||||
GeometryWorld,
|
||||
Proof,
|
||||
Witness,
|
||||
Diagnostic,
|
||||
TraceEvent,
|
||||
} from './registry.js';
|
||||
|
||||
import type {
|
||||
PredicateEvaluator,
|
||||
PredicateResult,
|
||||
PredicateTruth,
|
||||
} from './predicates.js';
|
||||
|
||||
import {
|
||||
getPredicateEvaluator,
|
||||
registerDefaultPredicates,
|
||||
PredicateRegistry,
|
||||
globalPredicateRegistry,
|
||||
} from './predicates.js';
|
||||
|
||||
import {
|
||||
BindingEnv,
|
||||
type BindingEntry,
|
||||
type TupleBindingSpec,
|
||||
enumerateTuples,
|
||||
tupleToBindings,
|
||||
} from './bindings.js';
|
||||
|
||||
import {
|
||||
createCartesianIterator,
|
||||
type JoinSpec,
|
||||
} from './joins.js';
|
||||
|
||||
import type {
|
||||
FormulaNode,
|
||||
ForAllFormula,
|
||||
ExistsFormula,
|
||||
AndFormula,
|
||||
OrFormula,
|
||||
NotFormula,
|
||||
ImpliesFormula,
|
||||
PredicateCall,
|
||||
DomainRef,
|
||||
DomainValue,
|
||||
} from 'imhotep-core';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Evaluation Result Contract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type EvaluationMode = 'scene-determinate' | 'scene-indeterminate';
|
||||
|
||||
export interface DeterministicSceneEvaluation {
|
||||
mode: EvaluationMode;
|
||||
sceneId: string;
|
||||
passed: boolean;
|
||||
formulaResults: FormulaResult[];
|
||||
proofs: Proof[];
|
||||
diagnostics: Diagnostic[];
|
||||
trace: TraceEvent[];
|
||||
/** Failing variable assignments for diagnostics. */
|
||||
failingAssignments?: Record<string, number>[];
|
||||
}
|
||||
|
||||
export interface FormulaResult {
|
||||
formulaId: string;
|
||||
outcome: 'pass' | 'fail' | 'indeterminate';
|
||||
truth: 'determinate' | 'indeterminate';
|
||||
witness?: number[];
|
||||
metrics?: Record<string, number>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Domain Resolver
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface DomainResolver {
|
||||
resolve(domain: DomainRef): DomainValue | undefined;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Engine Options
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface LogicEngineOptions {
|
||||
skipMissingPredicates?: boolean;
|
||||
maxDiagnostics?: number;
|
||||
predicateRegistry?: PredicateRegistry;
|
||||
/** Enable detailed trace logging. Default true. Disable for production speed. */
|
||||
trace?: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal Evaluation State
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface EvalState {
|
||||
world: GeometryWorld;
|
||||
resolver: DomainResolver;
|
||||
options: LogicEngineOptions;
|
||||
diagnostics: Diagnostic[];
|
||||
trace: TraceEvent[];
|
||||
proofs: Proof[];
|
||||
proofCounter: number;
|
||||
formulaCounter: number;
|
||||
predicateRegistry: PredicateRegistry;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Proof Generation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function nextProofId(state: EvalState): string {
|
||||
return `proof_${++state.proofCounter}`;
|
||||
}
|
||||
|
||||
function nextFormulaId(state: EvalState): string {
|
||||
return `formula_${++state.formulaCounter}`;
|
||||
}
|
||||
|
||||
function makeProof(
|
||||
state: EvalState,
|
||||
formulaId: string,
|
||||
outcome: 'pass' | 'fail' | 'indeterminate',
|
||||
truth: 'determinate' | 'indeterminate',
|
||||
witness?: number[],
|
||||
usedFacts: number[] = [],
|
||||
): Proof {
|
||||
// Map indeterminate outcome to 'error' for ClauseStatus compatibility.
|
||||
const clauseOutcome = outcome === 'indeterminate' ? 'error' : outcome;
|
||||
return {
|
||||
proofId: nextProofId(state),
|
||||
clauseId: formulaId,
|
||||
outcome: clauseOutcome,
|
||||
truth,
|
||||
usedFacts,
|
||||
derivedFacts: [],
|
||||
witness: witness
|
||||
? {
|
||||
subjectId: witness[0],
|
||||
referenceId: witness[1],
|
||||
snapshotId: state.world.snapshotId,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function addTrace(
|
||||
state: EvalState,
|
||||
phase: string,
|
||||
clauseId?: string,
|
||||
payload?: Record<string, unknown>,
|
||||
): void {
|
||||
if (state.options.trace === false) return;
|
||||
state.trace.push({
|
||||
traceEventId: `trace_${state.trace.length}`,
|
||||
phase,
|
||||
at: performance.now(),
|
||||
refs: clauseId ? { clauseId } : {},
|
||||
payload: payload ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
function addDiagnostic(
|
||||
state: EvalState,
|
||||
code: DiagnosticCode,
|
||||
severity: 'error' | 'warning' | 'info',
|
||||
message: string,
|
||||
clauseId?: string,
|
||||
): void {
|
||||
const max = state.options.maxDiagnostics ?? 100;
|
||||
if (state.diagnostics.length >= max) {
|
||||
return;
|
||||
}
|
||||
state.diagnostics.push({
|
||||
code,
|
||||
severity,
|
||||
category: severity === 'error' ? 'contract-failure' : 'internal-error',
|
||||
message,
|
||||
clauseId,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Formula Evaluation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function evaluateFormula(
|
||||
formula: FormulaNode,
|
||||
env: BindingEnv,
|
||||
state: EvalState,
|
||||
): FormulaResult {
|
||||
const formulaId = nextFormulaId(state);
|
||||
if (state.options.trace !== false) {
|
||||
addTrace(state, 'evaluate-formula', formulaId, {
|
||||
kind: formula.kind,
|
||||
bindings: env.toObject(),
|
||||
});
|
||||
}
|
||||
|
||||
switch (formula.kind) {
|
||||
case 'forall':
|
||||
return evaluateForAll(formula as ForAllFormula, env, state, formulaId);
|
||||
case 'exists':
|
||||
return evaluateExists(formula as ExistsFormula, env, state, formulaId);
|
||||
case 'and':
|
||||
return evaluateAnd(formula as AndFormula, env, state, formulaId);
|
||||
case 'or':
|
||||
return evaluateOr(formula as OrFormula, env, state, formulaId);
|
||||
case 'not':
|
||||
return evaluateNot(formula as NotFormula, env, state, formulaId);
|
||||
case 'implies':
|
||||
return evaluateImplies(formula as ImpliesFormula, env, state, formulaId);
|
||||
case 'predicate':
|
||||
return evaluatePredicate(formula as PredicateCall, env, state, formulaId);
|
||||
default:
|
||||
addDiagnostic(
|
||||
state,
|
||||
'IMH_LOGIC_UNKNOWN_FORMULA_KIND',
|
||||
'error',
|
||||
`Unknown formula kind: ${(formula as FormulaNode).kind}`,
|
||||
formulaId,
|
||||
);
|
||||
return {
|
||||
formulaId,
|
||||
outcome: 'indeterminate',
|
||||
truth: 'indeterminate',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateForAll(
|
||||
formula: ForAllFormula,
|
||||
env: BindingEnv,
|
||||
state: EvalState,
|
||||
formulaId: string,
|
||||
): FormulaResult {
|
||||
addTrace(state, 'evaluate-forall-start', formulaId);
|
||||
|
||||
// Resolve domains and build join specs.
|
||||
const specs: JoinSpec[] = [];
|
||||
for (const binding of formula.bindings) {
|
||||
const domain = state.resolver.resolve(binding.domain);
|
||||
if (!domain) {
|
||||
addDiagnostic(
|
||||
state,
|
||||
'IMH_LOGIC_DOMAIN_UNRESOLVED',
|
||||
'error',
|
||||
`Domain ${binding.domain.domain} could not be resolved.`,
|
||||
formulaId,
|
||||
);
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'indeterminate',
|
||||
truth: 'indeterminate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'indeterminate', 'indeterminate'));
|
||||
return result;
|
||||
}
|
||||
|
||||
if (domain.subjectIds.length === 0) {
|
||||
// Empty domain: forall over empty set is vacuously true.
|
||||
// When ANY quantified variable has an empty domain, the entire
|
||||
// universal quantification is vacuously true (no tuples to check).
|
||||
addDiagnostic(
|
||||
state,
|
||||
'IMH_LOGIC_VACUOUS_FORALL',
|
||||
'info',
|
||||
`Forall over empty domain "${binding.domain.domain}" (selector: ${binding.domain.selector ?? 'none'}) is vacuously true.`,
|
||||
formulaId,
|
||||
);
|
||||
addTrace(state, 'evaluate-forall-empty-domain', formulaId, {
|
||||
domainId: domain.domainId,
|
||||
});
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'pass',
|
||||
truth: 'determinate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'pass', 'determinate'));
|
||||
addTrace(state, 'evaluate-forall-vacuous', formulaId);
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const varName of binding.variables) {
|
||||
specs.push({
|
||||
variableName: varName,
|
||||
domain,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (specs.length === 0) {
|
||||
// No variables or all domains empty: vacuously true.
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'pass',
|
||||
truth: 'determinate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'pass', 'determinate'));
|
||||
addTrace(state, 'evaluate-forall-vacuous', formulaId);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Enumerate all tuples exhaustively.
|
||||
const iterator = createCartesianIterator(specs);
|
||||
let failed = false;
|
||||
let indeterminate = false;
|
||||
let failingTuple: Record<string, number> | undefined;
|
||||
let failingMetrics: Record<string, number> | undefined;
|
||||
|
||||
iterator.forEach((tuple, indices) => {
|
||||
// Build extended environment.
|
||||
const entries: BindingEntry[] = [];
|
||||
for (let i = 0; i < specs.length; i++) {
|
||||
entries.push({
|
||||
variableName: specs[i].variableName,
|
||||
subjectId: tuple[i],
|
||||
});
|
||||
}
|
||||
const childEnv = env.bindTuple(entries);
|
||||
|
||||
const bodyResult = evaluateFormula(formula.body, childEnv, state);
|
||||
|
||||
if (bodyResult.outcome === 'indeterminate') {
|
||||
indeterminate = true;
|
||||
addTrace(state, 'evaluate-forall-indeterminate', formulaId, {
|
||||
tuple: Object.fromEntries(specs.map((s, i) => [s.variableName, tuple[i]])),
|
||||
bodyOutcome: bodyResult.outcome,
|
||||
});
|
||||
return false; // Short-circuit.
|
||||
}
|
||||
|
||||
if (bodyResult.outcome === 'fail') {
|
||||
failed = true;
|
||||
failingTuple = {};
|
||||
failingMetrics = bodyResult.metrics;
|
||||
for (let i = 0; i < specs.length; i++) {
|
||||
failingTuple[specs[i].variableName] = tuple[i];
|
||||
}
|
||||
addTrace(state, 'evaluate-forall-failure', formulaId, {
|
||||
failingTuple,
|
||||
bodyOutcome: bodyResult.outcome,
|
||||
});
|
||||
return false; // Short-circuit.
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
let outcome: 'pass' | 'fail' | 'indeterminate';
|
||||
if (indeterminate) {
|
||||
outcome = 'indeterminate';
|
||||
} else if (failed) {
|
||||
outcome = 'fail';
|
||||
} else {
|
||||
outcome = 'pass';
|
||||
}
|
||||
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome,
|
||||
truth: outcome === 'indeterminate' ? 'indeterminate' : 'determinate',
|
||||
metrics: failingMetrics,
|
||||
};
|
||||
|
||||
state.proofs.push(makeProof(state, formulaId, outcome, 'determinate'));
|
||||
addTrace(state, 'evaluate-forall-end', formulaId, { outcome });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function evaluateExists(
|
||||
formula: ExistsFormula,
|
||||
env: BindingEnv,
|
||||
state: EvalState,
|
||||
formulaId: string,
|
||||
): FormulaResult {
|
||||
addTrace(state, 'evaluate-exists-start', formulaId);
|
||||
|
||||
// Resolve domains and build join specs.
|
||||
const specs: JoinSpec[] = [];
|
||||
for (const binding of formula.bindings) {
|
||||
const domain = state.resolver.resolve(binding.domain);
|
||||
if (!domain) {
|
||||
addDiagnostic(
|
||||
state,
|
||||
'IMH_LOGIC_DOMAIN_UNRESOLVED',
|
||||
'error',
|
||||
`Domain ${binding.domain.domain} could not be resolved.`,
|
||||
formulaId,
|
||||
);
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'indeterminate',
|
||||
truth: 'indeterminate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'indeterminate', 'indeterminate'));
|
||||
return result;
|
||||
}
|
||||
|
||||
if (domain.subjectIds.length === 0) {
|
||||
// Empty domain: exists over empty set is false.
|
||||
// When ANY quantified variable has an empty domain, the entire
|
||||
// existential quantification is false (no witness possible).
|
||||
addDiagnostic(
|
||||
state,
|
||||
'IMH_LOGIC_EMPTY_DOMAIN_EXISTS',
|
||||
'info',
|
||||
`Exists over empty domain "${binding.domain.domain}" (selector: ${binding.domain.selector ?? 'none'}) is false.`,
|
||||
formulaId,
|
||||
);
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'fail',
|
||||
truth: 'determinate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'fail', 'determinate'));
|
||||
addTrace(state, 'evaluate-exists-empty', formulaId);
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const varName of binding.variables) {
|
||||
specs.push({
|
||||
variableName: varName,
|
||||
domain,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (specs.length === 0) {
|
||||
// No variables or all domains empty: exists is false.
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'fail',
|
||||
truth: 'determinate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'fail', 'determinate'));
|
||||
addTrace(state, 'evaluate-exists-empty', formulaId);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Enumerate tuples looking for a witness.
|
||||
const iterator = createCartesianIterator(specs);
|
||||
let found = false;
|
||||
let indeterminate = false;
|
||||
let witnessTuple: Record<string, number> | undefined;
|
||||
|
||||
iterator.forEach((tuple, indices) => {
|
||||
const entries: BindingEntry[] = [];
|
||||
for (let i = 0; i < specs.length; i++) {
|
||||
entries.push({
|
||||
variableName: specs[i].variableName,
|
||||
subjectId: tuple[i],
|
||||
});
|
||||
}
|
||||
const childEnv = env.bindTuple(entries);
|
||||
|
||||
const bodyResult = evaluateFormula(formula.body, childEnv, state);
|
||||
|
||||
if (bodyResult.outcome === 'pass') {
|
||||
found = true;
|
||||
witnessTuple = {};
|
||||
for (let i = 0; i < specs.length; i++) {
|
||||
witnessTuple[specs[i].variableName] = tuple[i];
|
||||
}
|
||||
addTrace(state, 'evaluate-exists-witness', formulaId, {
|
||||
witnessTuple,
|
||||
});
|
||||
return false; // Short-circuit.
|
||||
}
|
||||
|
||||
if (bodyResult.outcome === 'indeterminate') {
|
||||
indeterminate = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
let outcome: 'pass' | 'fail' | 'indeterminate';
|
||||
if (found) {
|
||||
outcome = 'pass';
|
||||
} else if (indeterminate) {
|
||||
outcome = 'indeterminate';
|
||||
} else {
|
||||
outcome = 'fail';
|
||||
}
|
||||
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome,
|
||||
truth: outcome === 'indeterminate' ? 'indeterminate' : 'determinate',
|
||||
witness: witnessTuple
|
||||
? Object.values(witnessTuple)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
state.proofs.push(makeProof(state, formulaId, outcome, 'determinate', result.witness));
|
||||
addTrace(state, 'evaluate-exists-end', formulaId, { outcome });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function evaluateAnd(
|
||||
formula: AndFormula,
|
||||
env: BindingEnv,
|
||||
state: EvalState,
|
||||
formulaId: string,
|
||||
): FormulaResult {
|
||||
addTrace(state, 'evaluate-and-start', formulaId);
|
||||
|
||||
const leftResult = evaluateFormula(formula.left, env, state);
|
||||
if (leftResult.outcome === 'indeterminate') {
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'indeterminate',
|
||||
truth: 'indeterminate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'indeterminate', 'indeterminate'));
|
||||
addTrace(state, 'evaluate-and-short-circuit', formulaId, { side: 'left', reason: 'indeterminate' });
|
||||
return result;
|
||||
}
|
||||
|
||||
if (leftResult.outcome === 'fail') {
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'fail',
|
||||
truth: 'determinate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'fail', 'determinate'));
|
||||
addTrace(state, 'evaluate-and-short-circuit', formulaId, { side: 'left', reason: 'fail' });
|
||||
return result;
|
||||
}
|
||||
|
||||
const rightResult = evaluateFormula(formula.right, env, state);
|
||||
let outcome: 'pass' | 'fail' | 'indeterminate';
|
||||
let truth: 'determinate' | 'indeterminate';
|
||||
|
||||
if (rightResult.outcome === 'pass') {
|
||||
outcome = 'pass';
|
||||
truth = 'determinate';
|
||||
} else if (rightResult.outcome === 'indeterminate') {
|
||||
outcome = 'indeterminate';
|
||||
truth = 'indeterminate';
|
||||
} else {
|
||||
outcome = 'fail';
|
||||
truth = 'determinate';
|
||||
}
|
||||
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome,
|
||||
truth,
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, result.outcome, result.truth));
|
||||
addTrace(state, 'evaluate-and-end', formulaId, { outcome });
|
||||
return result;
|
||||
}
|
||||
|
||||
function evaluateOr(
|
||||
formula: OrFormula,
|
||||
env: BindingEnv,
|
||||
state: EvalState,
|
||||
formulaId: string,
|
||||
): FormulaResult {
|
||||
addTrace(state, 'evaluate-or-start', formulaId);
|
||||
|
||||
const leftResult = evaluateFormula(formula.left, env, state);
|
||||
if (leftResult.outcome === 'pass') {
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'pass',
|
||||
truth: 'determinate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'pass', 'determinate'));
|
||||
addTrace(state, 'evaluate-or-short-circuit', formulaId, { side: 'left', reason: 'pass' });
|
||||
return result;
|
||||
}
|
||||
|
||||
const rightResult = evaluateFormula(formula.right, env, state);
|
||||
let outcome: 'pass' | 'fail' | 'indeterminate';
|
||||
let truth: 'determinate' | 'indeterminate';
|
||||
|
||||
if (rightResult.outcome === 'pass') {
|
||||
outcome = 'pass';
|
||||
truth = 'determinate';
|
||||
} else if (leftResult.outcome === 'indeterminate' || rightResult.outcome === 'indeterminate') {
|
||||
outcome = 'indeterminate';
|
||||
truth = 'indeterminate';
|
||||
} else {
|
||||
outcome = 'fail';
|
||||
truth = 'determinate';
|
||||
}
|
||||
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome,
|
||||
truth,
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, result.outcome, result.truth));
|
||||
addTrace(state, 'evaluate-or-end', formulaId, { outcome });
|
||||
return result;
|
||||
}
|
||||
|
||||
function evaluateNot(
|
||||
formula: NotFormula,
|
||||
env: BindingEnv,
|
||||
state: EvalState,
|
||||
formulaId: string,
|
||||
): FormulaResult {
|
||||
addTrace(state, 'evaluate-not-start', formulaId);
|
||||
|
||||
// Capture diagnostic count before evaluating operand so we can
|
||||
// suppress inner diagnostics when the not inverts a failure into a pass.
|
||||
const diagnosticCountBefore = state.diagnostics.length;
|
||||
|
||||
const operandResult = evaluateFormula(formula.operand, env, state);
|
||||
let outcome: 'pass' | 'fail' | 'indeterminate';
|
||||
let truth: 'determinate' | 'indeterminate';
|
||||
|
||||
if (operandResult.outcome === 'pass') {
|
||||
outcome = 'fail';
|
||||
truth = 'determinate';
|
||||
} else if (operandResult.outcome === 'fail') {
|
||||
outcome = 'pass';
|
||||
truth = 'determinate';
|
||||
// The inner predicate failed, but not() inverts it to a pass.
|
||||
// Remove diagnostics produced by the inner evaluation since they
|
||||
// describe a condition that the user explicitly negated.
|
||||
state.diagnostics.splice(diagnosticCountBefore, state.diagnostics.length - diagnosticCountBefore);
|
||||
} else {
|
||||
outcome = 'indeterminate';
|
||||
truth = 'indeterminate';
|
||||
}
|
||||
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome,
|
||||
truth,
|
||||
};
|
||||
|
||||
state.proofs.push(makeProof(state, formulaId, result.outcome, result.truth));
|
||||
addTrace(state, 'evaluate-not-end', formulaId, { outcome });
|
||||
return result;
|
||||
}
|
||||
|
||||
function evaluateImplies(
|
||||
formula: ImpliesFormula,
|
||||
env: BindingEnv,
|
||||
state: EvalState,
|
||||
formulaId: string,
|
||||
): FormulaResult {
|
||||
addTrace(state, 'evaluate-implies-start', formulaId);
|
||||
|
||||
const antecedentResult = evaluateFormula(formula.antecedent, env, state);
|
||||
if (antecedentResult.outcome === 'indeterminate') {
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'indeterminate',
|
||||
truth: 'indeterminate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'indeterminate', 'indeterminate'));
|
||||
addTrace(state, 'evaluate-implies-indeterminate', formulaId, { reason: 'antecedent' });
|
||||
return result;
|
||||
}
|
||||
|
||||
if (antecedentResult.outcome === 'fail') {
|
||||
// Vacuously true.
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'pass',
|
||||
truth: 'determinate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'pass', 'determinate'));
|
||||
addTrace(state, 'evaluate-implies-vacuous', formulaId);
|
||||
return result;
|
||||
}
|
||||
|
||||
const consequentResult = evaluateFormula(formula.consequent, env, state);
|
||||
let outcome: 'pass' | 'fail' | 'indeterminate';
|
||||
let truth: 'determinate' | 'indeterminate';
|
||||
|
||||
if (consequentResult.outcome === 'pass') {
|
||||
outcome = 'pass';
|
||||
truth = 'determinate';
|
||||
} else if (consequentResult.outcome === 'indeterminate') {
|
||||
outcome = 'indeterminate';
|
||||
truth = 'indeterminate';
|
||||
} else {
|
||||
outcome = 'fail';
|
||||
truth = 'determinate';
|
||||
}
|
||||
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome,
|
||||
truth,
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, result.outcome, result.truth));
|
||||
addTrace(state, 'evaluate-implies-end', formulaId, { outcome });
|
||||
return result;
|
||||
}
|
||||
|
||||
function evaluatePredicate(
|
||||
formula: PredicateCall,
|
||||
env: BindingEnv,
|
||||
state: EvalState,
|
||||
formulaId: string,
|
||||
): FormulaResult {
|
||||
addTrace(state, 'evaluate-predicate-start', formulaId, {
|
||||
predicate: formula.predicate,
|
||||
});
|
||||
|
||||
const evaluator = state.predicateRegistry.get(formula.predicate);
|
||||
if (!evaluator) {
|
||||
const msg = `No predicate evaluator registered for: ${formula.predicate}`;
|
||||
addDiagnostic(
|
||||
state,
|
||||
'IMH_LOGIC_PREDICATE_MISSING',
|
||||
'error',
|
||||
msg,
|
||||
formulaId,
|
||||
);
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'indeterminate',
|
||||
truth: 'indeterminate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'indeterminate', 'indeterminate'));
|
||||
return result;
|
||||
}
|
||||
|
||||
// Resolve arguments from bindings.
|
||||
const tuple: number[] = [];
|
||||
for (const arg of formula.args) {
|
||||
switch (arg.type) {
|
||||
case 'VariableRef': {
|
||||
const subjectId = env.lookup(arg.name);
|
||||
if (subjectId === undefined) {
|
||||
addDiagnostic(
|
||||
state,
|
||||
'IMH_LOGIC_UNBOUND_VARIABLE',
|
||||
'error',
|
||||
`Variable ${arg.name} is not bound in the current scope.`,
|
||||
formulaId,
|
||||
);
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'indeterminate',
|
||||
truth: 'indeterminate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'indeterminate', 'indeterminate'));
|
||||
return result;
|
||||
}
|
||||
tuple.push(subjectId);
|
||||
break;
|
||||
}
|
||||
case 'AccessorTerm': {
|
||||
const subjectId = env.lookup(arg.variable);
|
||||
if (subjectId === undefined) {
|
||||
addDiagnostic(
|
||||
state,
|
||||
'IMH_LOGIC_UNBOUND_VARIABLE',
|
||||
'error',
|
||||
`Variable ${arg.variable} is not bound in the current scope.`,
|
||||
formulaId,
|
||||
);
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'indeterminate',
|
||||
truth: 'indeterminate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'indeterminate', 'indeterminate'));
|
||||
return result;
|
||||
}
|
||||
// For now, accessors evaluate to the subject id itself.
|
||||
// Property access (e.g., width, height) is handled by the predicate.
|
||||
tuple.push(subjectId);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
addDiagnostic(
|
||||
state,
|
||||
'IMH_LOGIC_UNSUPPORTED_TERM',
|
||||
'error',
|
||||
`Unsupported term type: ${arg.type}`,
|
||||
formulaId,
|
||||
);
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome: 'indeterminate',
|
||||
truth: 'indeterminate',
|
||||
};
|
||||
state.proofs.push(makeProof(state, formulaId, 'indeterminate', 'indeterminate'));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const predicateResult = evaluator.evaluateTuple(state.world, tuple, (formula as any).options);
|
||||
|
||||
// Propagate predicate-level diagnostics to engine state.
|
||||
if (predicateResult.diagnostics) {
|
||||
for (const d of predicateResult.diagnostics) {
|
||||
addDiagnostic(state, d.code, d.severity, d.message, formulaId);
|
||||
}
|
||||
}
|
||||
|
||||
const outcome: 'pass' | 'fail' | 'indeterminate' =
|
||||
predicateResult.truth === 'true'
|
||||
? 'pass'
|
||||
: predicateResult.truth === 'false'
|
||||
? 'fail'
|
||||
: 'indeterminate';
|
||||
|
||||
const result: FormulaResult = {
|
||||
formulaId,
|
||||
outcome,
|
||||
truth: outcome === 'indeterminate' ? 'indeterminate' : 'determinate',
|
||||
witness: predicateResult.witness?.subjectIds,
|
||||
metrics: predicateResult.metrics,
|
||||
};
|
||||
|
||||
state.proofs.push(
|
||||
makeProof(
|
||||
state,
|
||||
formulaId,
|
||||
result.outcome,
|
||||
result.truth,
|
||||
result.witness,
|
||||
),
|
||||
);
|
||||
addTrace(state, 'evaluate-predicate-end', formulaId, {
|
||||
predicate: formula.predicate,
|
||||
outcome,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface LogicEngineInput {
|
||||
formula: FormulaNode;
|
||||
world: GeometryWorld;
|
||||
resolver: DomainResolver;
|
||||
options?: LogicEngineOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a logic formula deterministically against a geometry world.
|
||||
*
|
||||
* This is the entry point for Stream 1 deterministic scene evaluation.
|
||||
*/
|
||||
export function evaluateLogic(input: LogicEngineInput): DeterministicSceneEvaluation {
|
||||
const { formula, world, resolver, options = {} } = input;
|
||||
|
||||
const state: EvalState = {
|
||||
world,
|
||||
resolver,
|
||||
options,
|
||||
diagnostics: [],
|
||||
trace: [],
|
||||
proofs: [],
|
||||
proofCounter: 0,
|
||||
formulaCounter: 0,
|
||||
predicateRegistry: options.predicateRegistry ?? globalPredicateRegistry,
|
||||
};
|
||||
|
||||
addTrace(state, 'evaluate-logic-start');
|
||||
|
||||
const env = new BindingEnv();
|
||||
const formulaResult = evaluateFormula(formula, env, state);
|
||||
|
||||
addTrace(state, 'evaluate-logic-end');
|
||||
|
||||
const mode: EvaluationMode =
|
||||
formulaResult.outcome === 'indeterminate'
|
||||
? 'scene-indeterminate'
|
||||
: 'scene-determinate';
|
||||
|
||||
const failingAssignments: Record<string, number>[] = [];
|
||||
if (formulaResult.outcome === 'fail') {
|
||||
// Collect failing assignments from trace events.
|
||||
for (const event of state.trace) {
|
||||
if (event.payload && typeof event.payload === 'object' && 'failingTuple' in event.payload) {
|
||||
const ft = event.payload.failingTuple as Record<string, number>;
|
||||
failingAssignments.push(ft);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mode,
|
||||
sceneId: world.sceneId,
|
||||
passed: formulaResult.outcome === 'pass',
|
||||
formulaResults: [formulaResult],
|
||||
proofs: state.proofs,
|
||||
diagnostics: state.diagnostics,
|
||||
trace: state.trace,
|
||||
failingAssignments: failingAssignments.length > 0 ? failingAssignments : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: evaluate a formula with default predicates registered.
|
||||
*/
|
||||
export function evaluateLogicWithDefaults(input: LogicEngineInput): DeterministicSceneEvaluation {
|
||||
registerDefaultPredicates();
|
||||
return evaluateLogic(input);
|
||||
}
|
||||
Reference in New Issue
Block a user