#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Consolidation Stages (extracted per Law 7 from consolidation_daemon.py)."""
import hashlib
import subprocess
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent.parent.parent
STATE_DIR = ROOT_DIR / "state"


import json
import logging
import re
from pulse.core.brain_utils import (
    HIPPOCAMPUS_DIR, PATTERNS_DIR, LESSONS_DIR, FAILS_DIR, EVIDENCE_METRICS_DIR,
    LESSONS_JSONL, FAILS_JSONL, PATTERNS_JSONL,
    load_json, load_json_dict, save_json, text_similarity,
    read_jsonl_entries, now_iso as _now_iso,
)
from pulse.core.brain_io import _acquire

# Map MD files to JSONL counterparts for fast-path reads
_MD_TO_JSONL = {
    LESSONS_DIR / "auto_lessons.md": LESSONS_JSONL,
    PATTERNS_DIR / "auto_patterns.md": PATTERNS_JSONL,
    FAILS_DIR / "auto_fails.md": FAILS_JSONL,
}


def _get_entries_for_file(md_file):
    """JSONL-first loader: returns list of {title, body, conf, ...} dicts.

    Falls back to MD parsing if JSONL is empty/missing."""
    jsonl_path = _MD_TO_JSONL.get(md_file)
    if jsonl_path:
        rows = read_jsonl_entries(jsonl_path)
        if rows:
            entries = []
            for r in rows:
                title = (r.get("title") or r.get("content", ""))[:200]
                conf = r.get("confidence", 0.5)
                sal = r.get("salience", 2.0)
                domain = r.get("domain", "general")
                ts = (r.get("timestamp") or "")[:10]
                source = r.get("agent", "unknown")
                cc = r.get("consolidation_count", 1)
                entries.append({"title": title, "conf": conf, "salience": sal,
                                "domain": domain, "date": ts, "source": source,
                                "consolidation_count": cc, "fingerprint": r.get("fingerprint", ""),
                                "_from_jsonl": True})
            return entries
    # No MD fallback — JSONL is sole source of truth
    return []

# === Bounded hash tracking to prevent re-processing across cycles ===

PROCESSED_HASHES_FILE = STATE_DIR / "consolidation_hashes.json"
MAX_TRACKED_HASHES = 5000  # Bounded to prevent infinite growth (~200KB max)


def _load_processed_hashes() -> dict[str, None]:
    """Load processed hashes as an insertion-ordered dict."""
    if PROCESSED_HASHES_FILE.exists():
        try:
            data = json.loads(PROCESSED_HASHES_FILE.read_text(encoding="utf-8"))
            return {h: None for h in data.get("hashes", [])}
        except Exception:
            pass
    return {}


def _save_processed_hashes(hashes: dict[str, None]):
    """Save bounded hashes (keeps most recent MAX_TRACKED_HASHES via LRU eviction)."""
    # Evict oldest if over limit (dict preserves insertion order in Python 3.7+)
    while len(hashes) > MAX_TRACKED_HASHES:
        oldest = next(iter(hashes))
        del hashes[oldest]
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    with _acquire(PROCESSED_HASHES_FILE):
        PROCESSED_HASHES_FILE.write_text(
            json.dumps({"hashes": list(hashes.keys()), "count": len(hashes),
                         "updated": datetime.now(timezone.utc).isoformat()}),
            encoding="utf-8"
        )


def _entry_hash(text: str) -> str:
    """Hash first 80 chars for cross-cycle tracking."""
    return hashlib.md5(text[:80].lower().strip().encode()).hexdigest()
from .consolidation_helpers import (
    extract_event_sequences, mine_cross_session_patterns,
    register_discovered_procedures,
    promote_cross_patterns_to_schemas,
    promote_sequences_to_schemas,
)

DEDUP_SCRIPT = SCRIPT_DIR.parent / "synthesis" / "pulse_dedup.py"

def stage_dedup(verbose=False):
    """Run existing dedup logic as subprocess."""
    if not DEDUP_SCRIPT.exists():
        return {"status": "skipped", "reason": "dedup script missing"}
    try:
        result = subprocess.run(
            [sys.executable, str(DEDUP_SCRIPT)],
            capture_output=True, text=True, timeout=120,
        )
        lines = result.stdout.strip().splitlines()
        if verbose:
            for line in lines:
                print(f"  [DEDUP] {line}")
        merged = 0
        for line in lines:
            if "items merged:" in line.lower():
                try:
                    merged = int(line.split(":")[-1].strip())
                except (ValueError, IndexError):
                    logging.debug("Suppressed exception in consolidation_stages", exc_info=True)
        return {"status": "ok", "merged": merged}
    except Exception as e:
        return {"status": "error", "error": str(e)}

def stage_pattern_mining(verbose=False):
    """Stage 2: Mine patterns from past 7 days."""
    sequences = extract_event_sequences(days=7, verbose=verbose)
    cross_patterns = mine_cross_session_patterns(days=7, verbose=verbose)
    return {
        "status": "ok",
        "sequences": sequences,
        "cross_patterns": cross_patterns,
        "total_found": len(sequences) + len(cross_patterns),
    }

def stage_schema_promotion(cross_patterns, verbose=False):
    """Promote high-confidence cross-session patterns to schemas.json."""
    return promote_cross_patterns_to_schemas(cross_patterns, verbose=verbose)

def stage_schema_promotion_from_sequences(sequences, verbose=False):
    """Promote event sequences with 5+ occurrences to schemas.json."""
    return promote_sequences_to_schemas(sequences, verbose=verbose)

def stage_ltd_decay(verbose=False):
    """Flag items using type-aware half-lives (Dimension D) — Long-Term Depression."""
    from pulse.core.brain_utils import RETRIEVAL_METRICS, EVIDENCE_SESSIONS_DIR
    from pulse.core.temporal import get_half_life, _days_since
    import hashlib
    metrics = load_json_dict(RETRIEVAL_METRICS)
    total_queries = metrics.get("total_queries", 0)
    if total_queries < 10:
        if verbose:
            print(f"  [LTD] Skipped — only {total_queries} queries (need 10+)")
        return {"status": "skipped", "reason": f"only {total_queries} queries", "flagged": 0}
    retrieved_keys = set(metrics.get("items", {}).keys())
    session_file = EVIDENCE_SESSIONS_DIR / "current_session.json"
    items = load_json(session_file)
    if not isinstance(items, list):
        return {"status": "skipped", "reason": "no session data", "flagged": 0}
    flagged = 0
    for item in items:
        if item.get("_ltd_flagged"):
            continue
        ts_str = item.get("timestamp", "")
        if not ts_str:
            continue
        # Dimension D: type-aware cutoff instead of flat 30 days
        ktype = item.get("type", "lesson")
        hl = get_half_life(ktype)
        age = _days_since(ts_str)
        if age < hl:  # Not yet past half-life — too young to flag
            continue
        desc = item.get("description", "")
        if not desc or desc == "[historical - no source text]":
            continue
        key = hashlib.md5(desc[:60].lower().encode("utf-8")).hexdigest()[:12]
        if key not in retrieved_keys:
            item["_ltd_flagged"] = True
            item["_ltd_flagged_at"] = _now_iso()
            flagged += 1
    if flagged > 0:
        # Layer 3: Re-sign modified items after LTD flagging
        from pulse.core.pulse_crypto import sign_item
        items = [sign_item(it, it.get("_agent_id", "consolidation"))
                 if isinstance(it, dict) else it for it in items]
        save_json(session_file, items)
    if verbose:
        print(f"  [LTD] Flagged {flagged} unretrieved items (30+ days old)")
    return {"status": "ok", "flagged": flagged}

def stage_graph_update(verbose=False):
    """Stage 4: Rebuild knowledge graph from current brain state."""
    try:
        from pulse.graph.knowledge_graph import incremental_update
        result = incremental_update(verbose=verbose)
        node_count = result.get("metadata", {}).get("node_count", 0)
        edge_count = result.get("metadata", {}).get("edge_count", 0)
        if verbose:
            print(f"  [GRAPH] Updated: {node_count} nodes, {edge_count} edges")
        return {"status": "ok", "nodes": node_count, "edges": edge_count}
    except ImportError:
        if verbose:
            print("  [GRAPH] knowledge_graph.py not available")
        return {"status": "skipped", "reason": "knowledge_graph not available"}
    except Exception as e:
        if verbose:
            print(f"  [GRAPH] Error: {e}")
        return {"status": "error", "error": str(e)}


def stage_reconsolidation(verbose=False):
    """Stage 2d: Auto-reconsolidation — merge similar entries (similarity > 0.6).

    Uses bounded hash tracking to skip entries already processed in previous cycles.
    """
    processed = _load_processed_hashes()
    merged_total = 0
    new_hashes = 0
    for md_file in [LESSONS_DIR / "auto_lessons.md", PATTERNS_DIR / "auto_patterns.md",
                    FAILS_DIR / "auto_fails.md"]:
        raw_entries = _get_entries_for_file(md_file)
        if len(raw_entries) < 2:
            continue
        # Enrich with hash/merge tracking
        entries = []
        for e in raw_entries:
            ehash = _entry_hash(e["title"])
            e.update({"merged": False, "hash": ehash, "seen": ehash in processed})
            entries.append(e)
        # Find similar pairs and merge (only process entries not yet seen)
        for i in range(len(entries)):
            if entries[i]["merged"] or entries[i]["seen"]:
                continue
            for j in range(i + 1, len(entries)):
                if entries[j]["merged"]:
                    continue
                sim = text_similarity(entries[i]["title"][:80], entries[j]["title"][:80])
                if sim >= 0.6:
                    if entries[j]["conf"] > entries[i]["conf"]:
                        i, j = j, i
                    # Dimension E: Bayesian update instead of flat +0.05
                    from pulse.core.uncertainty import bayesian_update
                    old_conf = max(entries[i]["conf"], entries[j]["conf"])
                    old_ec = entries[i].get("evidence_count", 1)
                    new_conf, new_ec = bayesian_update(old_conf, old_ec, True)
                    entries[i]["conf"] = new_conf
                    entries[i]["evidence_count"] = new_ec
                    entries[i]["consolidation_count"] = entries[i].get("consolidation_count", 1) + 1
                    # Dimension D: reset decay clock on merge (reinforcement)
                    from pulse.core.temporal import reinforce
                    reinforce(entries[i])
                    # For MD fallback, also update body
                    if not entries[i].get("_from_jsonl") and entries[i].get("body"):
                        cc_match = re.search(r"(\*\*Consolidation Count:\*\*\s*)(\d+)", entries[i]["body"])
                        if cc_match:
                            new_cc = int(cc_match.group(2)) + 1
                            entries[i]["body"] = re.sub(
                                r"\*\*Consolidation Count:\*\*\s*\d+",
                                f"**Consolidation Count:** {new_cc}", entries[i]["body"])
                    entries[j]["merged"] = True
                    merged_total += 1
                    # Dimension C: Provenance — record reconsolidation merge
                    try:
                        from pulse.brain.provenance import append_provenance
                        fp_i = hashlib.md5(entries[i]["title"][:60].lower().encode()).hexdigest()[:12]
                        fp_j = hashlib.md5(entries[j]["title"][:60].lower().encode()).hexdigest()[:12]
                        append_provenance(fp_i, {
                            "stage": "consolidation",
                            "action": "reconsolidation_merge",
                            "merged_with": fp_j,
                        })
                    except Exception:
                        pass  # Never crash the pipeline
        for e in entries:
            if not e["merged"] and not e["seen"]:
                processed[e["hash"]] = None
                new_hashes += 1
        # Dimension B: track which agents' items survived consolidation
        try:
            from pulse.admin.audit.agent_trust import _load_trust, _save_trust
            trust_data = _load_trust()
            agents_dict = trust_data.setdefault("agents", {})
            for e in entries:
                src = e.get("source", "")
                if not src or src == "unknown":
                    continue
                rec = agents_dict.setdefault(src, {})
                if not e["merged"]:
                    rec["survived_consolidation"] = rec.get("survived_consolidation", 0) + 1
                rec["contributions"] = rec.get("contributions", 0) + 1
            _save_trust(trust_data)
        except Exception:
            pass  # Never crash consolidation for trust tracking
        # Write back to JSONL (remove merged entries by fingerprint)
        if any(e["merged"] for e in entries):
            jsonl_path = _MD_TO_JSONL.get(md_file)
            if jsonl_path:
                from pulse.brain.save_writers import rewrite_jsonl
                all_rows = read_jsonl_entries(jsonl_path)
                merged_fps = {e.get("fingerprint") for e in entries if e["merged"] and e.get("fingerprint")}
                surviving = [r for r in all_rows if r.get("fingerprint") not in merged_fps]
                # Update surviving entries with new confidence/consolidation_count
                surv_by_fp = {r.get("fingerprint"): r for r in surviving}
                for e in entries:
                    if not e["merged"] and e.get("fingerprint") in surv_by_fp:
                        r = surv_by_fp[e["fingerprint"]]
                        r["confidence"] = e["conf"]
                        r["consolidation_count"] = e.get("consolidation_count", 1)
                        r.setdefault("evidence_count", 1)
                        if e.get("evidence_count"):
                            r["evidence_count"] = e["evidence_count"]
                rewrite_jsonl(jsonl_path, surviving)
    _save_processed_hashes(processed)
    if verbose:
        print(f"  [RECONSOLIDATION] Merged {merged_total} similar entries, tracked {new_hashes} new hashes")
    return {"status": "ok", "merged": merged_total, "new_hashes": new_hashes}


def stage_metacognitive_audit(verbose=False):
    """Stage 5: Brain quality audit — flag vague/unused/contradicted items, then auto-fix."""
    _ACTIONABLE = re.compile(
        r"(?:use|fix|add|remove|change|avoid|always|never|check|run|set|switch|"
        r"replace|install|update|create|delete|move|ensure|implement|deploy)", re.IGNORECASE
    )
    report = {"vague": 0, "no_verb": 0, "unused_14d": 0, "contradicted": 0,
              "total_audited": 0, "timestamp": _now_iso()}

    # Load retrieval metrics
    retrieval_file = EVIDENCE_METRICS_DIR / "retrieval_metrics.json"
    retrieval_data = load_json_dict(retrieval_file)
    retrieved_keys = set(retrieval_data.get("items", {}).keys())
    cutoff_14d = datetime.now(timezone.utc) - timedelta(days=14)

    # Load contradiction log
    contradiction_file = Path(HIPPOCAMPUS_DIR).parent / "state" / "contradiction_log.json"
    contradiction_data = load_json_dict(contradiction_file)
    repeat_counts = contradiction_data.get("repeat_counts", {})

    for md_file in [LESSONS_DIR / "auto_lessons.md", PATTERNS_DIR / "auto_patterns.md",
                    FAILS_DIR / "auto_fails.md"]:
        entries = _get_entries_for_file(md_file)
        for e in entries:
            title = e["title"]
            if not title:
                continue
            report["total_audited"] += 1
            if len(title) < 30:
                report["vague"] += 1
            if not _ACTIONABLE.search(title):
                report["no_verb"] += 1
            import hashlib
            key = hashlib.md5(title[:60].lower().encode("utf-8")).hexdigest()[:12]
            item_date_str = e.get("date", "")
            if item_date_str and key not in retrieved_keys:
                try:
                    item_date = datetime.fromisoformat(item_date_str[:10] + "T00:00:00+00:00")
                    if item_date < cutoff_14d:
                        report["unused_14d"] += 1
                except (ValueError, TypeError):
                    logging.debug("Suppressed exception in consolidation_stages", exc_info=True)
            title_key = title[:60].lower()
            if repeat_counts.get(title_key, 0) >= 3:
                report["contradicted"] += 1

    # Write report
    report_file = EVIDENCE_METRICS_DIR / "metacognition_report.json"
    report_file.parent.mkdir(parents=True, exist_ok=True)

    # Auto-fix: reduce confidence on flagged items, archive below 0.2
    fixes = _apply_metacognitive_fixes(retrieved_keys, repeat_counts, cutoff_14d, verbose)
    report["fixes_applied"] = fixes
    save_json(report_file, report)

    if verbose:
        print(f"  [METACOGNITION] Audited {report['total_audited']} items: "
              f"{report['vague']} vague, {report['no_verb']} no-verb, "
              f"{report['unused_14d']} unused, {report['contradicted']} contradicted")
    return report


def stage_hierarchical_prune(verbose=False):
    """Stage 6: Miller's Law enforcement — max 5 items per section, 200 entries per file.

    Two-level hierarchy:
      Level 1 (file): Keep top 200 entries by quality (handled by pulse_prune.py)
      Level 2 (section): Keep top 5 list items per ## section (Miller's Law 4±1)
    """
    from pulse.core.brain_utils import load_config
    config = load_config()
    max_chunk = config.get("validation", {}).get("max_chunk_size", 5)
    min_conf = config.get("pruning", {}).get("min_confidence_to_keep", 0.3)

    # Level 1: File-level pruning (reuse existing pulse_prune logic)
    from pulse.admin.brain_ops.pulse_prune import prune_file, BRAIN_FILES
    for bf in BRAIN_FILES:
        try:
            prune_file(bf)
        except Exception as e:
            if verbose:
                print(f"  [PRUNE-L1] Error pruning {bf.name}: {e}")

    # Level 2: JSONL domain-level quality pruning (replaces MD Miller's Law)
    trimmed_total = 0
    from collections import defaultdict
    for md_file in [LESSONS_DIR / "auto_lessons.md", PATTERNS_DIR / "auto_patterns.md",
                    FAILS_DIR / "auto_fails.md"]:
        jsonl_path = _MD_TO_JSONL.get(md_file)
        if not jsonl_path:
            continue
        rows = read_jsonl_entries(jsonl_path)
        if not rows:
            continue
        # Group by domain, keep top entries per domain sorted by quality
        by_domain = defaultdict(list)
        for r in rows:
            by_domain[r.get("domain", "general")].append(r)
        domain_cap = max_chunk * 40  # 200 per domain — generous cap
        surviving = []
        for domain, items in by_domain.items():
            items.sort(key=lambda x: x.get("salience", 0) * x.get("confidence", 0.5), reverse=True)
            if len(items) > domain_cap:
                trimmed_total += len(items) - domain_cap
                items = items[:domain_cap]
            surviving.extend(items)
        if trimmed_total > 0:
            from pulse.brain.save_writers import rewrite_jsonl
            rewrite_jsonl(jsonl_path, surviving)

    if verbose:
        print(f"  [PRUNE] Hierarchical: {trimmed_total} over-limit items trimmed")
    return {"status": "ok", "trimmed": trimmed_total}


def stage_cross_domain_transfer(verbose=False):
    """Stage 2e: Detect patterns appearing across 2+ evidence domains."""
    from pulse.core.brain_analysis import detect_cross_domain_patterns
    evidence_dir = HIPPOCAMPUS_DIR / "Evidence"
    transfers = detect_cross_domain_patterns(evidence_dir)
    if not transfers:
        if verbose:
            print("  [TRANSFER] No cross-domain patterns found")
        return {"status": "ok", "transfers": 0, "promoted": 0}
    # Promote universal patterns (3+ domains) to schemas
    schemas_file = PATTERNS_DIR / "schemas.json"
    schemas_data = load_json_dict(schemas_file)
    schemas = schemas_data.get("schemas", [])
    existing = {s.get("name", "").lower() for s in schemas}
    promoted = 0
    for t in transfers:
        if not t.get("universal"):
            continue
        name = f"Cross-domain: {t['pattern'][:50]}"
        if name.lower() in existing:
            continue
        schemas.append({
            "name": name, "confidence": 0.65, "evidence_count": t["count"],
            "domains": t["domains"], "type": "cross_domain_transfer",
            "created": _now_iso(),
        })
        existing.add(name.lower())
        promoted += 1
        if verbose:
            print(f"  [TRANSFER] {name} ({t['count']} domains)")
    if promoted:
        schemas_data["schemas"] = schemas
        schemas_data["last_updated"] = _now_iso()
        save_json(schemas_file, schemas_data)
    if verbose:
        print(f"  [TRANSFER] {len(transfers)} cross-domain, {promoted} promoted")
    return {"status": "ok", "transfers": len(transfers), "promoted": promoted}


def stage_promote_to_patterns(verbose=False):
    """Promote high-confidence lessons/failures to patterns.

    Lessons/failures with confidence >= 0.7 containing prescriptive language
    (always/never/must/should/avoid) are operational patterns. Copies them
    to auto_patterns.md via append_knowledge_md (dedup handles duplicates).
    """
    from pulse.brain.save_writers import append_knowledge_md
    _PRESCRIPTIVE = re.compile(
        r"\b(?:always|never|must|should|avoid|ensure|require|prevent|"
        r"don't|do not|critical|important)\b", re.IGNORECASE
    )
    promoted = 0
    patterns_file = PATTERNS_DIR / "auto_patterns.md"

    for md_file in [LESSONS_DIR / "auto_lessons.md", FAILS_DIR / "auto_fails.md"]:
        entries = _get_entries_for_file(md_file)
        for e in entries:
            title = e["title"]
            conf = e["conf"]
            if conf < 0.7:
                continue
            if not _PRESCRIPTIVE.search(title):
                continue
            salience = e.get("salience", 2.0)
            domain = e.get("domain", "general")
            ts = e.get("date", "") or _now_iso()[:10]
            source = e.get("source", "consolidation")
            append_knowledge_md(
                patterns_file, title, source, ts, domain, conf, salience
            )
            promoted += 1
            # Dimension C: Provenance — record promotion stage
            try:
                from pulse.brain.provenance import append_provenance
                fp = hashlib.md5(title[:60].lower().encode()).hexdigest()[:12]
                append_provenance(fp, {
                    "stage": "promotion",
                    "from_file": md_file.name,
                    "to_file": "auto_patterns.md",
                    "agent": source,
                })
            except Exception:
                pass  # Never crash the pipeline

    if verbose:
        print(f"  [PROMOTE] {promoted} high-confidence items promoted to patterns")
    return {"status": "ok", "promoted": promoted}


def _apply_metacognitive_fixes(retrieved_keys, repeat_counts, cutoff_14d, verbose=False):
    """Reduce confidence on vague/unused/contradicted items. Archive items below 0.2.

    Reads from JSONL when available (for analysis), but write-back still targets MD.
    """
    import hashlib
    fixes = {"confidence_reduced": 0, "archived": 0}
    for md_file in [LESSONS_DIR / "auto_lessons.md", PATTERNS_DIR / "auto_patterns.md",
                    FAILS_DIR / "auto_fails.md"]:
        if not md_file.exists():
            continue

        # --- JSONL-first: pre-compute penalties from structured data ---
        jsonl_entries = _get_entries_for_file(md_file)
        penalty_by_title_key = {}  # title_hash -> (penalty, conf)
        if jsonl_entries and jsonl_entries[0].get("_from_jsonl"):
            for e in jsonl_entries:
                title = e["title"]
                if not title:
                    continue
                conf = e.get("conf", 0.5)
                penalty = 0.0
                if len(title) < 30:
                    penalty += 0.1
                key = hashlib.md5(title[:60].lower().encode("utf-8")).hexdigest()[:12]
                item_date_str = e.get("date", "")
                if item_date_str and key not in retrieved_keys:
                    try:
                        if datetime.fromisoformat(item_date_str[:10] + "T00:00:00+00:00") < cutoff_14d:
                            penalty += 0.15
                    except (ValueError, TypeError):
                        logging.debug("Suppressed exception in consolidation_stages", exc_info=True)
                if repeat_counts.get(title[:60].lower(), 0) >= 3:
                    penalty += 0.2
                title_key = hashlib.md5(title[:80].lower().encode("utf-8")).hexdigest()
                penalty_by_title_key[title_key] = (penalty, conf)

        # --- JSONL write-back: apply penalties directly to entries ---
        if penalty_by_title_key:
            jsonl_path = _MD_TO_JSONL.get(md_file)
            if jsonl_path:
                all_rows = read_jsonl_entries(jsonl_path)
                surviving = []
                for r in all_rows:
                    title = (r.get("title") or r.get("content", ""))[:200]
                    title_key = hashlib.md5(title[:80].lower().encode("utf-8")).hexdigest()
                    if title_key in penalty_by_title_key:
                        penalty, _ = penalty_by_title_key[title_key]
                        if penalty > 0:
                            new_conf = max(0.0, round(r.get("confidence", 0.5) - penalty, 2))
                            r["confidence"] = new_conf
                            fixes["confidence_reduced"] += 1
                            if new_conf < 0.2:
                                fixes["archived"] += 1
                                continue  # Drop from JSONL
                    surviving.append(r)
                from pulse.brain.save_writers import rewrite_jsonl
                rewrite_jsonl(jsonl_path, surviving)
    if verbose:
        print(f"  [METACOGNITION] Fixes: {fixes['confidence_reduced']} reduced, {fixes['archived']} archived")
    return fixes


def stage_truth_verification(verbose=False):
    """Stage 8: Dimension A — scan for contradictions and apply penalties."""
    from pulse.admin.audit.truth_verifier import scan_contradictions, apply_penalties
    scan = scan_contradictions(verbose=verbose)
    penalized = apply_penalties(verbose=verbose)
    return {"status": "ok", "new_contradictions": scan["new"],
            "pending": scan["pending"], "penalized": penalized}