File size: 5,964 Bytes
9d29c62 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | # domain/validator.py - V7.4 (DOMAIN-AWARE CONSISTENCY GATE)
"""
V7.4 ConsistencyGate: Domain-Aware Validation.
Validates by StepType — NOT by string shape.
ALGEBRAIC → sympy.sympify() (existing behaviour, unchanged)
GEOMETRY → structural validation of payload (list[str])
NUMERIC → {future expansion point}
CTO directive:
- Geometry steps must NEVER reach sympify (Category Error prevention)
- Geometry gets its own structural validator, not a "skip blind"
- expression is display-only — validation uses payload for geometry
"""
import sympy
import logging
from typing import List, Tuple
from domain.step_types import StepType, SignedStep
logger = logging.getLogger(__name__)
class ConsistencyGate:
"""
V7.4 Wall 3: Post-Execution Domain-Aware Consistency Gate.
Receives the list of SignedStep objects from the Deterministic Solver and
verifies structural and logical integrity before passing to the Renderer.
Checks performed (all domains):
1. Non-empty: at least one signed step exists.
2. Hash integrity: every step has a non-empty SHA256 hash.
3a. ALGEBRAIC: expression is SymPy-parseable (sympy.sympify).
3b. GEOMETRY: payload is a non-empty list[str] (structural validation).
"""
@staticmethod
def _validate_algebraic(step: SignedStep) -> Tuple[bool, str]:
"""
Validates an ALGEBRAIC step by attempting sympy.sympify on each
sub-expression (split on " OR " for multi-solution results).
"""
expr_str = step.expression
if not expr_str:
logger.error(f"[CONSISTENCY_GATE] Step '{step.id}' has an empty expression.")
return False, f"EMPTY_EXPRESSION:{step.id}"
parts = [p.strip() for p in expr_str.split(" OR ")]
for part in parts:
try:
sympy.sympify(part)
except Exception as e:
logger.error(
f"[CONSISTENCY_GATE] Step '{step.id}' expression not parseable: "
f"'{part}' — {e}"
)
return False, f"UNPARSEABLE_EXPRESSION:{step.id}"
return True, ""
@staticmethod
def _validate_geometry(step: SignedStep) -> Tuple[bool, str]:
"""
Validates a GEOMETRY step structurally via payload.
Geometry output (points, distances, labels) is NOT SymPy-parseable.
We verify:
- payload is a non-empty list
- every element is a string
Hash integrity (checked earlier) is the cryptographic guarantee.
"""
payload = step.payload
if not isinstance(payload, list) or len(payload) == 0:
logger.error(
f"[CONSISTENCY_GATE] Geometry step '{step.id}' has empty or non-list payload."
)
return False, f"GEOMETRY_EMPTY_PAYLOAD:{step.id}"
if not all(isinstance(p, str) for p in payload):
logger.error(
f"[CONSISTENCY_GATE] Geometry step '{step.id}' payload contains non-string items."
)
return False, f"GEOMETRY_INVALID_PAYLOAD_TYPE:{step.id}"
logger.info(
f"[CONSISTENCY_GATE] Geometry step '{step.id}' passed structural validation "
f"({len(payload)} point(s))."
)
return True, ""
@staticmethod
def validate(
signed_steps: List,
ast_registry: dict,
problem_id: str
) -> Tuple[bool, str]:
"""
Returns (True, "") if all checks pass.
Returns (False, reason) if any check fails → caller must Fail Closed.
Accepts both SignedStep objects and legacy dicts for backwards compatibility
during any partial migration.
"""
# Check 1: Non-empty output
if not signed_steps:
logger.error("[CONSISTENCY_GATE] No signed steps produced by solver.")
return False, "EMPTY_SOLVER_OUTPUT"
for step in signed_steps:
step_id = step.get("id", "?") if hasattr(step, "get") else step.get("id", "?")
# Check 2: Hash presence
h = step.get("hash", "") if hasattr(step, "get") else step.get("hash", "")
if not h or len(h) < 10:
logger.error(f"[CONSISTENCY_GATE] Step '{step_id}' is missing a valid SHA256 hash.")
return False, f"MISSING_HASH:{step_id}"
# Check 3: Domain-aware expression / payload validation
step_type = step.get("step_type") if hasattr(step, "get") else None
if step_type == StepType.GEOMETRY:
ok, reason = ConsistencyGate._validate_geometry(step)
if not ok:
return False, reason
elif step_type == StepType.ALGEBRAIC or step_type is None:
# None = legacy dict without step_type → treat as ALGEBRAIC (backwards compat)
expr_str = step.get("expression", "")
if not expr_str:
logger.error(f"[CONSISTENCY_GATE] Step '{step_id}' has an empty expression.")
return False, f"EMPTY_EXPRESSION:{step_id}"
parts = [p.strip() for p in expr_str.split(" OR ")]
for part in parts:
try:
sympy.sympify(part)
except Exception as e:
logger.error(
f"[CONSISTENCY_GATE] Step '{step_id}' expression not parseable: "
f"'{part}' — {e}"
)
return False, f"UNPARSEABLE_EXPRESSION:{step_id}"
# StepType.NUMERIC → future expansion point
# Other unknown types → pass through (hash check is the integrity guarantee)
logger.info(
f"[CONSISTENCY_GATE] ✅ {len(signed_steps)} signed steps passed all checks "
f"for problem '{problem_id}'."
)
return True, ""
|