orchestrata_ultimate.verification.constraints.evaluator

Reference › orchestrata_ultimate › verification › constraints › evaluator

ConstraintEvaluator is the mechanical judge of the verification subsystem. Its governing rule, quoted from its docstring, is SPEC-007 Key Principle 1: Prolog decisions are final. Every satisfaction decision is made by querying constraint_satisfied/3 in the constraints.pl rulebase through the prolog adapter; there is no Python comparison fallback. It is used by the verification agent, the checkpoint verifier and the verification engine.

A PrologEngine may be injected; otherwise _bridge builds one lazily on first use and consults the rulebase once, so constructing an evaluator is cheap. If no Prolog backend is installed, building the engine raises rather than degrading to a Python answer.

Results are VerificationResult objects from types.

At a glance

Source src/orchestrata_ultimate/verification/constraints/evaluator.py
Lines of code 232
Classes 1
Functions 2
Imports (internal) orchestrata_ultimate.core.base.traces, orchestrata_ultimate.core.paths, orchestrata_ultimate.core.prolog_adapter, orchestrata_ultimate.verification.types
Imported by orchestrata_ultimate.verification.agents.verification_agent, orchestrata_ultimate.verification.checkpoints.verifier, orchestrata_ultimate.verification.verification.engine
Tests that import it tests/test_verification.py, tests/verification/constraints/test_evaluator.py, tests/verification/test_checkpoint_verifier.py, tests/verification/test_constraints_evaluator.py, tests/verification/test_small_modules.py, tests/verification/test_verification_agents.py, tests/verification/test_verification_engine.py

Class diagram

Class diagram orchestrata_ultimate.verification.constraints.evaluator

Dependency diagram

Dependency diagram orchestrata_ultimate.verification.constraints.evaluator

API

Classes

ConstraintEvaluator

Decorators: traces(spec='035', story='US-003', reqs=['REQ-035-US-003-001', 'REQ-035-US-003-002', 'REQ-035-US-003-011']) · Line 71

Evaluates constraints against actual state via Prolog.

Method Signature Summary
__init__ __init__(prolog_bridge: PrologEngine | None = None) -> None
_bridge _bridge() -> PrologEngine Return a working Prolog engine with constraints.pl consulted.
evaluate evaluate(constraint: Constraint, context: dict[str, Any] | None = None) -> VerificationResult Evaluate a single constraint by querying constraint_satisfied/3.
evaluate_all evaluate_all(constraints: list[Constraint], context: dict[str, Any] | None = None) -> list[VerificationResult] Evaluate all constraints.
evaluate_with_dependencies evaluate_with_dependencies(constraints: list[Constraint], context: dict[str, Any] | None = None) -> list[VerificationResult] Evaluate constraints respecting dependency order.

Functions

Function Signature Summary
_escape_prolog_atom traces _escape_prolog_atom(value: str) -> str Escape a string for safe use inside a single-quoted Prolog atom.
_to_prolog_term traces _to_prolog_term(value: Any) -> str Marshal a Python value into a GROUND Prolog term literal.

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/verification/constraints/evaluator.py

# mypy: ignore-errors
"""Constraint evaluation via Prolog."""

import logging
from typing import Any

from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.core.paths import get_prolog_dir
from orchestrata_ultimate.core.prolog_adapter import PrologEngine
from orchestrata_ultimate.verification.types import (
    Constraint,
    VerificationResult,
    VerificationStatus,
)

logger = logging.getLogger(__name__)

_CONSTRAINTS_PL = "constraints.pl"


@traces(spec="035", story="US-003", reqs=["REQ-035-US-003-001"])
def _escape_prolog_atom(value: str) -> str:
    """Escape a string for safe use inside a single-quoted Prolog atom.

    Order matters: backslash MUST be escaped first, otherwise the
    backslashes introduced by the later replacements would themselves get
    re-escaped. Mirrors governance.prolog_gate._escape_prolog_atom -- each
    Prolog-marshalling module in this codebase carries its own copy rather
    than sharing a private helper across modules.
    """
    return (
        value.replace("\\", "\\\\")
        .replace("'", "\\'")
        .replace('"', '\\"')
        .replace("\n", "\\n")
    )


@traces(spec="035", story="US-003", reqs=["REQ-035-US-003-001"])
def _to_prolog_term(value: Any) -> str:
    """Marshal a Python value into a GROUND Prolog term literal.

    Only the shapes Constraint.expected/actual are documented to carry are
    supported: None, bool, int, float, str. ``bool`` is checked before
    ``int`` -- Python's bool is an int subclass, but constraints.pl treats
    true/false as distinct atoms from numbers (see truthy/1), so marshalling
    a bool as a number here would silently diverge from the rulebase.
    Anything else raises: producing a plausible-looking-but-wrong Prolog
    term for an unsupported type would be exactly the silent-fallback defect
    this evaluator exists to remove (constraint-prolog-evaluation.contract.json).
    """
    if value is None:
        return "null"
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, (int, float)):
        return repr(value)
    if isinstance(value, str):
        return f"'{_escape_prolog_atom(value)}'"
    raise TypeError(
        f"ConstraintEvaluator cannot marshal {type(value).__name__!r} to "
        "Prolog (supported: None, bool, int, float, str)"
    )


@traces(
    spec="035",
    story="US-003",
    reqs=["REQ-035-US-003-001", "REQ-035-US-003-002", "REQ-035-US-003-011"],
)
class ConstraintEvaluator:
    """Evaluates constraints against actual state via Prolog.

    SPEC-007 Key Principle #1: "Prolog decisions are final and cannot be
    overridden." Every satisfaction decision below is made by querying
    ``constraint_satisfied/3`` in prolog/constraints.pl -- there is no
    Python comparison ladder standing in as a fallback (backlog P2.10;
    plans/contracts/constraint-prolog-evaluation.contract.json).

    ``prolog_bridge`` is an optional injected ``PrologEngine`` (real
    janus-swi, e.g. the ``real_prolog`` test fixture or a production
    engine). When omitted, a real engine is constructed lazily on first
    use in :meth:`evaluate` -- dependency-injection convenience, not a
    decision fallback: every path still queries Prolog, never Python
    comparison logic. If no Prolog backend is available at all,
    constructing that engine raises (``core.prolog_adapter.PrologEngine``
    raises ``RuntimeError`` when janus-swi is not installed) rather than
    degrading to a Python-computed answer.
    """

    def __init__(self, prolog_bridge: PrologEngine | None = None) -> None:
        self._prolog = prolog_bridge
        self._rules_loaded = False

    @traces(
        spec="035", story="US-003", reqs=["REQ-035-US-003-001", "REQ-035-US-003-011"]
    )
    def _bridge(self) -> PrologEngine:
        """Return a working Prolog engine with constraints.pl consulted.

        Lazy on both counts: the engine is constructed (if not injected)
        and the rulebase is consulted only on first use, never in
        ``__init__`` -- so a bare ``ConstraintEvaluator()`` stays cheap and
        side-effect-free until a constraint is actually evaluated.
        """
        if self._prolog is None:
            self._prolog = PrologEngine()
        if not self._rules_loaded:
            self._prolog.consult(str(get_prolog_dir() / _CONSTRAINTS_PL))
            self._rules_loaded = True
        return self._prolog

    @traces(
        spec="035",
        story="US-003",
        reqs=["REQ-035-US-003-001", "REQ-035-US-003-005"],
    )
    def evaluate(
        self, constraint: Constraint, context: dict[str, Any] | None = None
    ) -> VerificationResult:
        """Evaluate a single constraint by querying constraint_satisfied/3."""
        ctx = context or {}
        actual = ctx.get(constraint.predicate)

        if actual is None:
            return VerificationResult(
                constraint_id=constraint.id,
                status=VerificationStatus.ERROR,
                expected=constraint.expected,
                actual=None,
                details=f"No value provided for '{constraint.predicate}'",
            )

        bridge = self._bridge()
        goal = (
            "constraint_satisfied("
            f"{_to_prolog_term(str(constraint.type))}, "
            f"{_to_prolog_term(constraint.expected)}, "
            f"{_to_prolog_term(actual)})"
        )
        # PrologEngine.query_once always returns a dict (janus's {'truth':
        # False} stripped down to {}) -- it is never None for an ordinary
        # failing goal, so "is not None" cannot distinguish satisfied from
        # not-met. list(bridge.query(goal)) is the established idiom for a
        # yes/no query in this codebase (governance.prolog_gate,
        # kernel.policy_evaluator): [] means the goal failed.
        satisfied = bool(list(bridge.query(goal)))

        return VerificationResult(
            constraint_id=constraint.id,
            status=(
                VerificationStatus.SATISFIED
                if satisfied
                else VerificationStatus.NOT_MET
            ),
            expected=constraint.expected,
            actual=actual,
            details=f"{'Satisfied' if satisfied else 'Not met'}: {constraint.predicate}",
        )

    @traces(
        spec="035",
        story="US-003",
        reqs=[
            "REQ-035-US-003-002",
            "REQ-035-US-003-007",
            "REQ-035-US-003-012",
            "REQ-035-US-003-020",
        ],
    )
    def evaluate_all(
        self, constraints: list[Constraint], context: dict[str, Any] | None = None
    ) -> list[VerificationResult]:
        """Evaluate all constraints."""
        return [self.evaluate(c, context) for c in constraints]

    @traces(
        spec="035",
        story="US-002",
        reqs=[
            "REQ-035-US-002-006",
            "REQ-035-US-002-013",
            "REQ-035-US-002-021",
            "REQ-035-US-003-004",
        ],
    )
    def evaluate_with_dependencies(
        self, constraints: list[Constraint], context: dict[str, Any] | None = None
    ) -> list[VerificationResult]:
        """Evaluate constraints respecting dependency order."""
        evaluated: dict[str, VerificationResult] = {}
        constraint_map = {c.id: c for c in constraints}

        @traces(
            spec="035",
            story="US-002",
            reqs=[
                "REQ-035-US-002-006",
                "REQ-035-US-002-013",
                "REQ-035-US-002-021",
                "REQ-035-US-003-004",
            ],
        )
        def eval_one(cid: str) -> VerificationResult:
            if cid in evaluated:
                return evaluated[cid]
            constraint = constraint_map.get(cid)
            if not constraint:
                result = VerificationResult(
                    constraint_id=cid,
                    status=VerificationStatus.ERROR,
                    details=f"Constraint '{cid}' not found",
                )
                evaluated[cid] = result
                return result
            for dep_id in constraint.dependencies:
                dep_result = eval_one(dep_id)
                if not dep_result.is_satisfied:
                    result = VerificationResult(
                        constraint_id=cid,
                        status=VerificationStatus.NOT_MET,
                        details=f"Dependency '{dep_id}' not satisfied",
                    )
                    evaluated[cid] = result
                    return result
            result = self.evaluate(constraint, context)
            evaluated[cid] = result
            return result

        for c in constraints:
            eval_one(c.id)
        return list(evaluated.values())