"""Two cognitive minds, one model: the fusion opinion layer (experimental). Mind 1 (analyst, persona 1): conservative and focal - what does the record say? Mind 2 (skeptic, persona 2): adversarial - what is the weakest link, what else explains the same record? Each mind runs its OWN scratchpad pass (separate prompt + decoding) and writes to its OWN memory pool (persona-tagged helix strands). The fusion gate combines them: AGREE -> shared verdict; confidence raised to the higher of the two RULE -> deterministic spine wins when it resolves (cannot hallucinate) CONFLICT -> calibrated OPINION, not just abstention: lean toward the side with the better value citation, else "conflict/LOW"; ALWAYS state the discrepancy and what would settle it. The output is an OPINION: position + evidence + discrepancy + open questions. Composition is deterministic (suit logic); the Spock voice is generated by the model from the opinion as context. Usage (library): from research.fusion import run_two_pass, fuse, opinion_text """ import re import json from pathlib import Path from research.helix import rungs, normalize from research.decision import load_table, calibrated_prob def _cited(rep): c = rep.get("cited") if isinstance(c, list): return [str(v) for v in c][:5] if c: return [str(c)] return [v for v in rungs(rep.get("reasoning", ""))][:5] def _gaps(reasoning): out = [] if not reasoning: return out for s in re.split(r"(?<=[.!?])\s+", reasoning.replace("\n", " ")): low = s.lower() if any(k in low for k in ("missing", "what would settle", "what would change", "what is needed", "not in the record", "no record")): out.append(s.strip()) return out[:4] def _calibrated_merge(a_conf, s_conf, table_path): """Merge two confidence labels using calibrated reliability from the table. Returns (p_mean, bucket) where p_mean is the mean calibrated probability and bucket is the display bucket (HIGH/MEDIUM/LOW/cannot assess). """ table = load_table(table_path) p_a = calibrated_prob(a_conf, table, unknown=0.0) p_s = calibrated_prob(s_conf, table, unknown=0.0) p_mean = (p_a + p_s) / 2.0 if (p_a > 0 or p_s > 0) else 0.0 # Map to display bucket if p_mean >= 0.66: return p_mean, "HIGH" if p_mean >= 0.40: return p_mean, "MEDIUM" if p_mean > 0.0: return p_mean, "LOW" return p_mean, "cannot assess" def fuse(analyst, skeptic, rule=None, sources=(), max_gaps=4, calibration_table=None): """Fuse two minds (+ optional rule spine) into one calibrated opinion. Args: analyst: analyst report dict with verdict, confidence, reasoning skeptic: skeptic report dict with verdict, confidence, reasoning rule: optional rule spine result dict sources: optional list of sources max_gaps: max open questions to include calibration_table: path to calibration summary JSON (e.g., logs/calib_summary_dpo3_200.json) """ a_v = normalize(analyst.get("verdict", "")) s_v = normalize(skeptic.get("verdict", "")) a_conf = (analyst.get("confidence") or "LOW").upper() s_conf = (skeptic.get("confidence") or "LOW").upper() gaps = (_gaps(analyst.get("reasoning", "")) + _gaps(skeptic.get("reasoning", "")))[:max_gaps] if rule and rule.get("verdict") in ("supports", "refutes", "not enough information"): verdict, conf, basis = rule["verdict"], rule.get("confidence", "HIGH"), "rule" pos = ("the record deterministically " + ("supports" if verdict == "supports" else "contradicts" if verdict == "refutes" else "does not settle") + " the claim") elif a_v and a_v == s_v: # Both minds agree - use calibrated merge instead of naive confidence raise if calibration_table: p_mean, conf = _calibrated_merge(a_conf, s_conf, calibration_table) else: # Fallback: naive confidence raise (but mark as uncalibrated) conf = "HIGH" if "HIGH" in (a_conf, s_conf) else "MEDIUM" verdict, basis = a_v, "agreed" pos = "both minds reach the same verdict" elif a_v and s_v: cite_a, cite_s = bool(_cited(analyst)), bool(_cited(skeptic)) if cite_a != cite_s: lean, side = (analyst, "analyst") if cite_a else (skeptic, "skeptic") verdict, conf, basis = f"leaning: {lean['verdict']}", "MEDIUM", f"leaning-{side}" pos = f"the minds conflict, but the {side} mind cites record values" else: verdict, conf, basis = "conflict", "LOW", "conflict" pos = "the two minds conflict on the same record" else: verdict, conf, basis = "not enough information", "LOW", "insufficient" pos = "neither mind can reach a verdict from the record" discrepancy = "" if basis in ("conflict", "leaning-analyst", "leaning-skeptic"): discrepancy = (skeptic.get("reasoning") or "")[:220] return { "verdict": verdict, "confidence": conf, "basis": basis, "position": pos, "discrepancy": discrepancy, "cited": _cited(analyst)[:4] + [v for v in _cited(skeptic) if v not in _cited(analyst)][:2], "open_questions": gaps, "sources": list(sources)[:6], "minds": {"analyst": analyst.get("verdict", ""), "skeptic": skeptic.get("verdict", "")}, } def opinion_text(op): """Turn a fused opinion into a spoken, calibrating statement (suit-composed).""" v = op["verdict"] conf = op["confidence"] line = f"My assessment: {op['position']}. Confidence: {conf}." if op.get("discrepancy"): line += f" Discrepancy noted: {op['discrepancy']}" if op.get("cited"): line += " Cited values: " + ", ".join(str(c) for c in op["cited"][:4]) + "." if op.get("open_questions"): line += " Open: " + "; ".join(op["open_questions"][:3]) + "." if op.get("sources"): line += " Sources: " + ", ".join(str(s) for s in op["sources"][:4]) + "." return line def run_two_pass(model, tok, doc, memory=None, persona_ids=(1, 2), max_scratch=90, max_reason=50): """Mind 1 (analyst) then Mind 2 (skeptic): separate scratchpads, own memory pool.""" from research.structured import analyst_report a = analyst_report(model, tok, doc, persona_id=persona_ids[0], max_scratch=max_scratch, max_reason=max_reason) s = analyst_report(model, tok, doc, persona_id=persona_ids[1], max_scratch=max_scratch, max_reason=max_reason) if memory is not None: for rep, mind in ((a, "analyst"), (s, "skeptic")): memory.write(doc, "", rep.get("verdict", ""), rep.get("confidence", ""), rep.get("reasoning", ""), agreed=True, mind=mind) return a, s