|
|
from __future__ import annotations |
|
|
|
|
|
from dataclasses import dataclass |
|
|
from typing import Any, Dict, List, Optional, Tuple |
|
|
|
|
|
|
|
|
@dataclass |
|
|
class Explanation: |
|
|
summary: str |
|
|
correspondence: List[str] |
|
|
missing_assumptions: List[str] |
|
|
notes: List[str] |
|
|
evidence_refs: List[str] |
|
|
repair_suggestion: Optional[List[str]] = None |
|
|
|
|
|
def _normalize_formula(formula: str) -> str: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return formula.strip().replace(" ", "").replace("→", "->") |
|
|
|
|
|
def _kb_get(kb: Dict[str, Any], path: List[str], default=None): |
|
|
cur: Any = kb |
|
|
for p in path: |
|
|
if not isinstance(cur, dict) or p not in cur: |
|
|
return default |
|
|
cur = cur[p] |
|
|
return cur |
|
|
|
|
|
|
|
|
def explain_formula( |
|
|
*, |
|
|
formula: str, |
|
|
assumptions: List[str], |
|
|
verdict: str, |
|
|
kb: Dict[str, Any], |
|
|
) -> Explanation: |
|
|
""" |
|
|
Phase 7.5: |
|
|
- If the formula matches a known schema, attach correspondence axioms from KB. |
|
|
- If invalid, mention missing assumptions when known. |
|
|
""" |
|
|
f = _normalize_formula(formula) |
|
|
corr = _kb_get(kb, ["correspondence"], {}) or {} |
|
|
schema_map = (corr.get("schemas") or {}) |
|
|
|
|
|
|
|
|
norm_schema_map = {k.replace(" ", ""): v for k, v in schema_map.items()} |
|
|
|
|
|
axioms = (corr.get("axioms") or {}) |
|
|
missing_hints = (corr.get("missing_assumption_hints") or {}) |
|
|
norm_missing_hints = {k.replace(" ", ""): v for k, v in missing_hints.items()} |
|
|
|
|
|
notes: List[str] = [] |
|
|
correspondence_ids: List[str] = [] |
|
|
missing: List[str] = [] |
|
|
|
|
|
|
|
|
schema_entry = norm_schema_map.get(f) |
|
|
if schema_entry: |
|
|
supports = schema_entry.get("supports") or [] |
|
|
for ax_id in supports: |
|
|
if ax_id in axioms: |
|
|
correspondence_ids.append(ax_id) |
|
|
notes.append(schema_entry.get("explain") or "Matches a known modal axiom schema.") |
|
|
else: |
|
|
notes.append("No direct correspondence schema match in knowledge DB.") |
|
|
|
|
|
|
|
|
hinted = norm_missing_hints.get(f) or [] |
|
|
if verdict == "invalid" and hinted: |
|
|
|
|
|
for need in hinted: |
|
|
if need not in assumptions: |
|
|
missing.append(need) |
|
|
|
|
|
|
|
|
if verdict == "valid": |
|
|
if correspondence_ids: |
|
|
|
|
|
ax0 = axioms.get(correspondence_ids[0], {}) |
|
|
reqs = ax0.get("requires") or [] |
|
|
ok = all(r in assumptions for r in reqs) |
|
|
if ok: |
|
|
summary = f"Valid: under assumptions {assumptions}, this matches {correspondence_ids[0]} ({ax0.get('schema')})." |
|
|
else: |
|
|
summary = f"Valid (by model search): matches {correspondence_ids[0]} but required assumptions {reqs} are not fully present in Spec." |
|
|
else: |
|
|
summary = f"Valid: no counterexample found under assumptions {assumptions}." |
|
|
elif verdict == "invalid": |
|
|
if missing: |
|
|
summary = f"Invalid: counterexample exists; typically this schema needs {missing}." |
|
|
else: |
|
|
summary = "Invalid: counterexample exists under the given assumptions." |
|
|
else: |
|
|
summary = "Unknown: insufficient evidence." |
|
|
|
|
|
|
|
|
refs: List[str] = [] |
|
|
for ax_id in correspondence_ids: |
|
|
ax = axioms.get(ax_id, {}) or {} |
|
|
for req in (ax.get("requires") or []): |
|
|
if isinstance(req, str) and req.startswith("assume:"): |
|
|
refs.append(req) |
|
|
refs.extend(missing) |
|
|
refs = sorted(set(refs)) |
|
|
|
|
|
return Explanation( |
|
|
summary=summary, |
|
|
correspondence=correspondence_ids, |
|
|
missing_assumptions=missing, |
|
|
notes=notes, |
|
|
evidence_refs=refs, |
|
|
repair_suggestion=missing if verdict == "invalid" and missing else None |
|
|
) |