"""PR-2: deterministic proposal bridge from proof-gated derived CLOSE facts. Collects realized derived facts (member/subset + TRANSITIVE_PREDICATES) that carry a Derivation with verdict="entailed" and are SPECULATIVE. Emits reviewable proposal-only artifacts (source="derived_close_fact") to a dedicated sink. - Default-off via RuntimeConfig.review_derived_close_proposals - Review-gated, proposal-only (never ratification or corpus mutation) - Deterministic dedupe by stable key (predicate + args + derivation + structure_key) - Skips non-derived, malformed, unsupported, non-entailed safely - Best-effort; failures do not corrupt state or set did_work on engine checkpoint Intended to be called from idle_tick *after* consolidation (Step D) so that newly derived facts can immediately surface as proposals. """ from __future__ import annotations import json from hashlib import sha256 from pathlib import Path from typing import Any from generate.meaning_graph.relational import TRANSITIVE_PREDICATES from generate.realize import RealizedRecord, recall_realized #: Dedicated sink for derived CLOSE proposals (distinct from comprehension-failures #: to keep families clean while still reviewable by the same HITL tooling). DEFAULT_SINK = ( Path(__file__).resolve().parents[3] / "teaching" / "proposals" / "derived_close_facts" ) def _stable_dedupe_key(rec: RealizedRecord) -> str: """Stable dedupe key for a derived record. Includes derivation provenance so that a re-derived fact with different proof chain is treated as distinct if premises differ.""" if rec.derivation is None: return "" key = ( rec.relation_predicate, rec.relation_arguments[0] if len(rec.relation_arguments) > 0 else "", rec.relation_arguments[1] if len(rec.relation_arguments) > 1 else "", rec.derivation.rule, tuple(rec.derivation.premise_structure_keys), rec.structure_key, ) return sha256(repr(key).encode("utf-8")).hexdigest() def _is_eligible(rec: RealizedRecord) -> bool: if not rec.derived or rec.derivation is None: return False if rec.derivation.verdict != "entailed": return False if getattr(rec, "epistemic_status", None) != "speculative": return False p = rec.relation_predicate if p not in {"member", "subset"} and p not in TRANSITIVE_PREDICATES: return False if len(getattr(rec, "relation_arguments", ())) != 2: return False return True def emit_derived_close_proposals( ctx: "SessionContext", *, sink: Path | None = None, max_emissions: int = 100, ) -> dict[str, int]: """Scan realized facts for eligible derived CLOSE conclusions and emit proposal artifacts. Returns counts: considered, eligible, emitted, duplicate, skipped Artifacts are written only for first sighting (deduped by stable key). Order is deterministic (sorted by (predicate, subject, object)). Metric invariant (post-collection, pre-write): considered == eligible + skipped Write failures (I/O etc.) also increment skipped (best-effort). """ sink = sink or DEFAULT_SINK sink.mkdir(parents=True, exist_ok=True) considered = 0 eligible = 0 emitted = 0 duplicate = 0 skipped = 0 candidates: list[tuple[str, str, str, RealizedRecord]] = [] # member and subset first (from PR-1 is-a), then the relational transitive for p in ["member", "subset"] + sorted(TRANSITIVE_PREDICATES): for rec in recall_realized(ctx, predicate=p): considered += 1 if _is_eligible(rec): eligible += 1 candidates.append( (p, rec.relation_arguments[0], rec.relation_arguments[1], rec) ) else: skipped += 1 # deterministic global order candidates.sort(key=lambda t: (t[0], t[1], t[2])) for p, subj, obj, rec in candidates: if emitted >= max_emissions: break dkey = _stable_dedupe_key(rec) path = sink / f"{dkey}.json" if path.exists(): duplicate += 1 continue try: artifact: dict[str, Any] = { "source": "derived_close_fact", "predicate": rec.relation_predicate, "subject": subj, "object": obj, "relation_arguments": list(rec.relation_arguments), "derivation": { "rule": rec.derivation.rule, "verdict": rec.derivation.verdict, "premise_structure_keys": list(rec.derivation.premise_structure_keys), }, "epistemic_status": rec.epistemic_status, "structure_key": rec.structure_key, "dedupe_key": dkey, "status": "proposal_only", "requires_review": True, "mounted": False, } path.write_text( json.dumps(artifact, indent=2, sort_keys=True), encoding="utf-8" ) emitted += 1 except Exception: skipped += 1 continue return { "considered": considered, "eligible": eligible, "emitted": emitted, "duplicate": duplicate, "skipped": skipped, } __all__ = ["emit_derived_close_proposals", "DEFAULT_SINK"]