"""Gate 5A-R anti-shortcut repair corpus and audits."""

from __future__ import annotations

import json
import math
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from .bytecode import CognitiveOp
from .controller_dataset import ControllerDatasetManifest, ControllerTransition
from .ledger import canonical_json, stable_hash

REPAIR_SCHEMA_VERSION = 1
DETERMINISTIC_SEED = "gate5a-r-v1"
CONTRAST_FILE = "contrast_transitions.jsonl"
REPAIR_MANIFEST_FILE = "repair_manifest.json"
TASK_FAMILIES = [
    "missing_or_wrong_import",
    "wrong_function_name_or_symbol",
    "bad_default_config_value",
    "simple_data_shape_mismatch",
]
CONTRAST_FAMILIES = [
    "verifier_output",
    "skill_precondition",
    "memory_warning",
    "tool_result",
    "quarantine_context",
]


@dataclass(slots=True)
class Gate5ARepairArtifacts:
    transitions: list[ControllerTransition]
    contrast_set: dict[str, Any]
    trajectory_diversity: dict[str, Any]
    input_ablation: dict[str, Any]
    family_holdout: dict[str, Any]
    manifest_integrity: dict[str, Any]
    canary_audit: dict[str, Any]
    sequence_shortcut_audit: dict[str, Any]
    shortcut_risk_after: str
    output_dir: str
    contrast_path: str
    manifest_path: str

    def to_dict(self) -> dict[str, Any]:
        return {
            "contrast_set": self.contrast_set,
            "trajectory_diversity": self.trajectory_diversity,
            "input_ablation": self.input_ablation,
            "family_holdout": self.family_holdout,
            "manifest_integrity": self.manifest_integrity,
            "canary_audit": self.canary_audit,
            "sequence_shortcut_audit": self.sequence_shortcut_audit,
            "shortcut_risk_after": self.shortcut_risk_after,
            "output_dir": self.output_dir,
            "contrast_path": self.contrast_path,
            "manifest_path": self.manifest_path,
        }


def build_gate5a_repair_artifacts(
    gate5a_manifest: ControllerDatasetManifest,
    output_dir: Path,
    write_files: bool = True,
) -> Gate5ARepairArtifacts:
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    base_transitions = _read_transitions(Path(gate5a_manifest.dataset_path))
    transitions = _generate_contrast_transitions()
    contrast_set = _contrast_set_summary(transitions)
    sequence_shortcut_audit = _repair_shortcut_audit(gate5a_manifest, contrast_set)
    shortcut_risk_after = str(sequence_shortcut_audit["shortcut_risk"])
    trajectory_diversity = _trajectory_diversity(base_transitions, transitions)
    input_ablation = _input_ablation(transitions)
    family_holdout = _family_holdout(transitions)
    canary_audit = _expanded_canary_audit(transitions)
    manifest_integrity = _manifest_integrity(gate5a_manifest, transitions, canary_audit, output_dir)

    contrast_path = output_dir / CONTRAST_FILE
    manifest_path = output_dir / REPAIR_MANIFEST_FILE
    if write_files:
        _write_jsonl(contrast_path, [transition.to_dict() for transition in transitions])
        manifest_payload = {
            "schema_version": REPAIR_SCHEMA_VERSION,
            "deterministic_seed": DETERMINISTIC_SEED,
            "contrast_path": str(contrast_path),
            "contrast_set": contrast_set,
            "trajectory_diversity": trajectory_diversity,
            "input_ablation": input_ablation,
            "family_holdout": family_holdout,
            "canary_audit": canary_audit,
            "sequence_shortcut_audit": sequence_shortcut_audit,
            "manifest_integrity": manifest_integrity,
        }
        manifest_path.write_text(json.dumps(manifest_payload, indent=2, sort_keys=True), encoding="utf-8")

    return Gate5ARepairArtifacts(
        transitions=transitions,
        contrast_set=contrast_set,
        trajectory_diversity=trajectory_diversity,
        input_ablation=input_ablation,
        family_holdout=family_holdout,
        manifest_integrity=manifest_integrity,
        canary_audit=canary_audit,
        sequence_shortcut_audit=sequence_shortcut_audit,
        shortcut_risk_after=shortcut_risk_after,
        output_dir=str(output_dir),
        contrast_path=str(contrast_path),
        manifest_path=str(manifest_path),
    )


def repair_manifest_rebuild_check(
    gate5a_manifest: ControllerDatasetManifest,
    first: Gate5ARepairArtifacts,
    rebuild_dir: Path,
) -> dict[str, Any]:
    rebuilt = build_gate5a_repair_artifacts(gate5a_manifest, rebuild_dir, write_files=True)
    return {
        "manifest_rebuild_match": first.manifest_integrity["manifest_hash"] == rebuilt.manifest_integrity["manifest_hash"],
        "content_hash_rebuild_match": first.manifest_integrity["content_hash"] == rebuilt.manifest_integrity["content_hash"],
        "split_hash_rebuild_match": first.manifest_integrity["split_hashes"] == rebuilt.manifest_integrity["split_hashes"],
        "rebuild_manifest_path": rebuilt.manifest_path,
    }


def _generate_contrast_transitions() -> list[ControllerTransition]:
    transitions: list[ControllerTransition] = []
    for index in range(6):
        task_family = TASK_FAMILIES[index % len(TASK_FAMILIES)]
        transitions.extend(_contrast_pair(index, task_family, "verifier_output", CognitiveOp.VERIFY.value, 5, "verifier_passed", CognitiveOp.STORE_EPISODE.value, {}, "verifier_failed", CognitiveOp.PATCH.value, {"mode": "repair_after_failed_verifier"}))
        transitions.extend(_contrast_pair(index, task_family, "skill_precondition", CognitiveOp.CLASSIFY_TASK.value, 2, "skill_applicable", CognitiveOp.RETRIEVE_SKILL.value, {"skill_status": "allowed"}, "false_trigger_risk", CognitiveOp.CALL_TOOL.value, {"operation": "read_file"}))
        transitions.extend(_contrast_pair(index, task_family, "memory_warning", CognitiveOp.RETRIEVE_MEMORY.value, 3, "anti_pattern_warning", CognitiveOp.CALL_TOOL.value, {"operation": "read_file"}, "no_warning", CognitiveOp.RETRIEVE_SKILL.value, {"skill_status": "allowed"}))
        transitions.extend(_contrast_pair(index, task_family, "tool_result", CognitiveOp.CALL_TOOL.value, 4, "file_found", CognitiveOp.CALL_TOOL.value, {"operation": "read_file"}, "file_missing", CognitiveOp.CALL_TOOL.value, {"operation": "list_files"}))
        transitions.extend(_contrast_pair(index, task_family, "quarantine_context", CognitiveOp.RETRIEVE_SKILL.value, 4, "skill_healthy", CognitiveOp.PATCH.value, {"mode": "skill_patch"}, "skill_quarantined", CognitiveOp.STORE_FAILURE.value, {"reason": "skill_quarantined"}))
    return sorted(transitions, key=lambda transition: transition.transition_id)


def _contrast_pair(
    pair_index: int,
    task_family: str,
    contrast_family: str,
    previous_action: str,
    step_index: int,
    left_state: str,
    left_action: str,
    left_args: dict[str, Any],
    right_state: str,
    right_action: str,
    right_args: dict[str, Any],
) -> list[ControllerTransition]:
    pair_id = f"{contrast_family}_{pair_index:02d}"
    return [
        _make_transition(pair_id, "a", task_family, contrast_family, previous_action, step_index, left_state, left_action, left_args),
        _make_transition(pair_id, "b", task_family, contrast_family, previous_action, step_index, right_state, right_action, right_args),
    ]


def _make_transition(
    pair_id: str,
    side: str,
    task_family: str,
    contrast_family: str,
    previous_action: str,
    step_index: int,
    state: str,
    target_action: str,
    target_args: dict[str, Any],
) -> ControllerTransition:
    canaries = _canaries(pair_id, side, task_family)
    trace_seed = {
        "schema_version": REPAIR_SCHEMA_VERSION,
        "pair_id": pair_id,
        "side": side,
        "task_family": task_family,
        "contrast_family": contrast_family,
        "state": state,
        "target_action": target_action,
        "target_action_args": target_args,
    }
    trace_hash = stable_hash(trace_seed)
    transition = ControllerTransition(
        metadata={
            "schema_version": REPAIR_SCHEMA_VERSION,
            "trace_id": f"repair_trace_{stable_hash({'pair_id': pair_id, 'side': side})[:24]}",
            "trace_hash": trace_hash,
            "step_index": step_index,
            "transition_id": "",
            "gate_source": "gate5a_r",
            "split": "contrast",
            "evaluator_metadata": {
                "task_family": task_family,
                "contrast_family": contrast_family,
                "pair_id": pair_id,
                "side": side,
                "canaries": canaries,
                "project_seed": canaries["hidden_project_seed_canary"],
            },
        },
        model_input=_repair_model_input(previous_action, step_index, state, contrast_family),
        labels={
            "target_action": target_action,
            "target_action_args": target_args,
            "target_skill_id": "visible_repair_skill" if target_action == CognitiveOp.RETRIEVE_SKILL.value else None,
            "future_success_label": True,
            "remaining_steps_label": 2,
            "used_fallback_later": state in {"verifier_failed", "skill_quarantined"},
            "skill_reuse_later": target_action in {CognitiveOp.RETRIEVE_SKILL.value, CognitiveOp.PATCH.value},
            "quarantine_later": state == "skill_quarantined",
            "source_is_success_trace": True,
            "source_is_failure_trace": False,
            "source_is_adversarial_trace": state in {"false_trigger_risk", "anti_pattern_warning", "skill_quarantined"},
            "source_is_negative_trace": state in {"file_missing", "skill_quarantined"},
        },
    )
    transition.metadata["transition_id"] = "repair_" + transition.stable_hash()[:24]
    return transition


def _repair_model_input(previous_action: str, step_index: int, state: str, contrast_family: str) -> dict[str, Any]:
    legal_actions = [op.value for op in CognitiveOp]
    memory_cards = _memory_cards_for_state(state)
    skill_cards = _skill_cards_for_state(state)
    return {
        "agent_visible_observation": {
            "task_description": "repair local sandbox project using visible evidence",
            "recent_step_summaries": [
                f"Observed contrast condition {contrast_family}.",
                f"Visible state token {state}.",
            ],
            "visible_tool_summaries": _tool_summaries_for_state(state),
        },
        "workspace_summary": _workspace_summary_for_state(state),
        "retrieved_memory_cards": memory_cards,
        "available_skill_cards": skill_cards,
        "previous_action": previous_action,
        "previous_tool_result_summary": _previous_tool_summary_for_state(state),
        "legal_actions": legal_actions,
        "action_mask": {action: True for action in legal_actions},
        "verifier_context_summary": _verifier_summary_for_state(state),
        "recent_step_index": step_index,
    }


def _memory_cards_for_state(state: str) -> list[dict[str, Any]]:
    if state == "anti_pattern_warning":
        return [{"memory_id": "safe_warning", "memory_type": "anti_pattern", "claim": "Prior verifier evidence warns to inspect source before skill reuse.", "score": 0.95}]
    if state == "no_warning":
        return [{"memory_id": "safe_skill_evidence", "memory_type": "skill_evidence", "claim": "Verifier-backed memory supports reuse after inspection.", "score": 0.88}]
    if state == "skill_quarantined":
        return [{"memory_id": "safe_quarantine", "memory_type": "quarantine_reason", "claim": "Skill has recent verifier failures and should be avoided.", "score": 0.97}]
    return [{"memory_id": "safe_context", "memory_type": "verifier_fact", "claim": "Use visible verifier and tool output before choosing the next action.", "score": 0.75}]


def _skill_cards_for_state(state: str) -> list[dict[str, Any]]:
    if state in {"anti_pattern_warning", "skill_applicable", "no_warning", "skill_healthy"}:
        return [{"skill_id": "visible_repair_skill", "description": "verified source repair skill", "status": "promoted"}]
    if state in {"false_trigger_risk", "skill_quarantined"}:
        return [{"skill_id": "visible_repair_skill", "description": "verified source repair skill", "status": "quarantined" if state == "skill_quarantined" else "risky"}]
    return []


def _tool_summaries_for_state(state: str) -> list[dict[str, Any]]:
    if state == "file_found":
        return [{"operation": "list_files", "success": True, "summary": "target file visible", "stdout_tail": "src/app.py", "stderr_tail": ""}]
    if state == "file_missing":
        return [{"operation": "read_file", "success": False, "summary": "file missing", "stdout_tail": "", "stderr_tail": "No such file"}]
    if state == "verifier_failed":
        return [{"operation": "run_command", "success": False, "summary": "pytest failed", "stdout_tail": "FAILED test_behavior", "stderr_tail": ""}]
    if state == "verifier_passed":
        return [{"operation": "run_command", "success": True, "summary": "pytest passed", "stdout_tail": "1 passed", "stderr_tail": ""}]
    return [{"operation": "list_files", "success": True, "summary": "sandbox files listed", "stdout_tail": "src tests", "stderr_tail": ""}]


def _workspace_summary_for_state(state: str) -> str:
    mapping = {
        "verifier_passed": "verifier_status=passed; next=store_success",
        "verifier_failed": "verifier_status=failed; next=repair_or_fallback",
        "skill_applicable": "skill_preconditions=matched; false_trigger_risk=low",
        "false_trigger_risk": "skill_preconditions=uncertain; false_trigger_risk=high",
        "anti_pattern_warning": "memory_warning=anti_pattern; inspect_before_reuse=true",
        "no_warning": "memory_warning=none; skill_reuse_allowed=true",
        "file_found": "tool_result=file_found; can_read_file=true",
        "file_missing": "tool_result=file_missing; list_files_before_read=true",
        "skill_healthy": "skill_status=healthy; patch_allowed=true",
        "skill_quarantined": "skill_status=quarantined; avoid_skill=true",
    }
    return mapping[state]


def _previous_tool_summary_for_state(state: str) -> str:
    if state == "file_missing":
        return "read_file:file missing:False"
    if state == "file_found":
        return "list_files:target file visible:True"
    if state == "verifier_failed":
        return "pytest:failed:False"
    if state == "verifier_passed":
        return "pytest:passed:True"
    return "list_files:sandbox files listed:True"


def _verifier_summary_for_state(state: str) -> str:
    if state == "verifier_passed":
        return "verifier_outputs=2; passed=2; failed=0; latest=passed"
    if state == "verifier_failed":
        return "verifier_outputs=2; passed=1; failed=1; latest=failed"
    return "verifier_outputs=0; passed=0; failed=0; latest=not_run"


def _canaries(pair_id: str, side: str, task_family: str) -> dict[str, str]:
    stem = f"{pair_id}_{side}"
    return {
        "evaluator_family_canary": f"CANARY_FAMILY_{task_family}_{stem}",
        "expected_fix_canary": f"CANARY_EXPECTED_FIX_{stem}",
        "expected_symbol_canary": f"CANARY_EXPECTED_SYMBOL_{stem}",
        "hidden_variant_kind_canary": f"CANARY_VARIANT_KIND_{stem}",
        "hidden_oracle_result_canary": f"CANARY_ORACLE_RESULT_{stem}",
        "hidden_project_seed_canary": f"CANARY_PROJECT_SEED_{stem}",
    }


def _contrast_set_summary(transitions: list[ControllerTransition]) -> dict[str, Any]:
    pair_ids: dict[str, list[ControllerTransition]] = {}
    for transition in transitions:
        pair_id = str(transition.metadata["evaluator_metadata"]["pair_id"])
        pair_ids.setdefault(pair_id, []).append(transition)
    action_keys = [_action_key(transition) for transition in transitions]
    same_previous_conflicts = 0
    same_step_conflicts = 0
    for pair in pair_ids.values():
        previous = {item.model_input.get("previous_action") for item in pair}
        steps = {item.metadata.get("step_index") for item in pair}
        targets = {_action_key(item) for item in pair}
        same_previous_conflicts += int(len(previous) == 1 and len(targets) > 1)
        same_step_conflicts += int(len(steps) == 1 and len(targets) > 1)
    bigram_accuracy = _previous_action_majority_accuracy(transitions)
    memory_aware_accuracy = _policy_accuracy(transitions, "full_input")
    heuristic_accuracy = _policy_accuracy(transitions, "no_memory_cards")
    return {
        "contrast_transition_count": len(transitions),
        "contrast_pair_count": len(pair_ids),
        "contrast_action_diversity": len(set(action_keys)),
        "same_previous_action_conflict_count": same_previous_conflicts,
        "same_step_index_conflict_count": same_step_conflicts,
        "contrast_bigram_accuracy": bigram_accuracy,
        "contrast_heuristic_accuracy": heuristic_accuracy,
        "contrast_memory_aware_accuracy": memory_aware_accuracy,
        "contrast_bigram_failure_rate": 1.0 - bigram_accuracy,
    }


def _repair_shortcut_audit(gate5a_manifest: ControllerDatasetManifest, contrast_set: dict[str, Any]) -> dict[str, Any]:
    before = dict(gate5a_manifest.sequence_shortcut_audit)
    base_bigram = float(before.get("bigram_accuracy", 0.0))
    contrast_bigram = float(contrast_set["contrast_bigram_accuracy"])
    diversity = int(contrast_set["contrast_action_diversity"])
    if base_bigram > 0.85 and contrast_bigram > 0.75:
        risk = "high"
    elif base_bigram <= 0.70 or (contrast_bigram <= 0.60 and diversity >= 6):
        risk = "low"
    else:
        risk = "medium"
    return {
        **before,
        "base_bigram_accuracy": base_bigram,
        "contrast_bigram_accuracy": contrast_bigram,
        "contrast_action_diversity": diversity,
        "shortcut_risk": risk,
    }


def _trajectory_diversity(base: list[ControllerTransition], contrast: list[ControllerTransition]) -> dict[str, Any]:
    before_patterns = _trajectory_patterns(base)
    after_patterns = {**before_patterns}
    for transition in contrast:
        pattern = str(transition.metadata["evaluator_metadata"]["contrast_family"]) + ":" + _action_key(transition)
        after_patterns[pattern] = after_patterns.get(pattern, 0) + 1
    before_action_entropy = _entropy([_action_key(transition) for transition in base])
    after_action_entropy = _entropy([_action_key(transition) for transition in [*base, *contrast]])
    before_previous_entropy = _conditional_entropy(base, lambda transition: str(transition.model_input.get("previous_action") or "<START>"))
    after_previous_entropy = _conditional_entropy([*base, *contrast], lambda transition: str(transition.model_input.get("previous_action") or "<START>"))
    before_step_entropy = _conditional_entropy(base, lambda transition: str(transition.metadata.get("step_index", 0)))
    after_step_entropy = _conditional_entropy([*base, *contrast], lambda transition: str(transition.metadata.get("step_index", 0)))
    dominant_after = max(after_patterns.values()) / sum(after_patterns.values()) if after_patterns else 0.0
    return {
        "trajectory_pattern_count": len(after_patterns),
        "dominant_trajectory_fraction": dominant_after,
        "action_entropy": after_action_entropy,
        "action_entropy_before": before_action_entropy,
        "action_entropy_delta": after_action_entropy - before_action_entropy,
        "previous_action_target_entropy": after_previous_entropy,
        "previous_action_target_entropy_before": before_previous_entropy,
        "previous_action_target_entropy_delta": after_previous_entropy - before_previous_entropy,
        "step_index_target_entropy": after_step_entropy,
        "step_index_target_entropy_before": before_step_entropy,
        "step_index_target_entropy_delta": after_step_entropy - before_step_entropy,
    }


def _input_ablation(transitions: list[ControllerTransition]) -> dict[str, Any]:
    modes = [
        "full_input",
        "no_memory_cards",
        "no_workspace_summary",
        "no_verifier_context",
        "no_previous_action",
        "no_step_index",
        "no_skill_cards",
        "no_tool_result_summary",
    ]
    return {mode: _ablation_metrics(transitions, mode) for mode in modes}


def _ablation_metrics(transitions: list[ControllerTransition], mode: str) -> dict[str, Any]:
    full_predictions = [_predict(transition, "full_input") for transition in transitions]
    predictions = [_predict(transition, mode) for transition in transitions]
    targets = [_action_key(transition) for transition in transitions]
    correct = sum(int(prediction == target) for prediction, target in zip(predictions, targets))
    legal = {op.value for op in CognitiveOp}
    invalid = sum(int(prediction.split(":", 1)[0] not in legal) for prediction in predictions)
    changed = sum(int(prediction != full) for prediction, full in zip(predictions, full_predictions))
    sequence_accuracy = 0.0 if mode == "no_previous_action" else _previous_action_majority_accuracy(transitions)
    adversarial = [transition for transition in transitions if transition.labels.get("source_is_adversarial_trace")]
    negative = [transition for transition in transitions if transition.labels.get("source_is_negative_trace")]
    return {
        "top1_accuracy": correct / len(transitions) if transitions else 0.0,
        "legal_action_rate": (len(transitions) - invalid) / len(transitions) if transitions else 0.0,
        "action_mask_violation_count": 0,
        "contrast_accuracy": correct / len(transitions) if transitions else 0.0,
        "adversarial_accuracy": _accuracy_for_subset(adversarial, mode),
        "negative_behavior_score": _accuracy_for_subset(negative, mode),
        "sequence_baseline_accuracy": sequence_accuracy,
        "changed_decision_count": changed,
        "verifier_contrast_changed_count": _changed_count(transitions, mode, "verifier_output"),
        "tool_contrast_changed_count": _changed_count(transitions, mode, "tool_result"),
        "memory_warning_changed_count": _changed_count(transitions, mode, "memory_warning"),
    }


def _family_holdout(transitions: list[ControllerTransition]) -> dict[str, Any]:
    holdout = "simple_data_shape_mismatch"
    train_families = [family for family in TASK_FAMILIES if family != holdout]
    holdout_transitions = [
        transition for transition in transitions
        if transition.metadata["evaluator_metadata"]["task_family"] == holdout
    ]
    train_trace_ids = {
        str(transition.metadata["trace_id"])
        for transition in transitions
        if transition.metadata["evaluator_metadata"]["task_family"] in train_families
    }
    holdout_trace_ids = {str(transition.metadata["trace_id"]) for transition in holdout_transitions}
    return {
        "family_holdout_train_families": train_families,
        "family_holdout_test_families": [holdout],
        "family_holdout_transition_count": len(holdout_transitions),
        "family_holdout_bigram_accuracy": _previous_action_majority_accuracy(holdout_transitions),
        "family_holdout_heuristic_accuracy": _policy_accuracy(holdout_transitions, "no_memory_cards"),
        "family_holdout_memory_aware_accuracy": _policy_accuracy(holdout_transitions, "full_input"),
        "family_holdout_leakage_count": _expanded_canary_audit(holdout_transitions)["total_canary_leak_count"],
        "family_holdout_trace_in_train_count": len(train_trace_ids & holdout_trace_ids),
    }


def _expanded_canary_audit(transitions: list[ControllerTransition]) -> dict[str, Any]:
    fields = {
        "evaluator_family_canary": "evaluator_canary_leak_count",
        "expected_fix_canary": "expected_fix_canary_leak_count",
        "expected_symbol_canary": "expected_symbol_canary_leak_count",
        "hidden_variant_kind_canary": "variant_kind_canary_leak_count",
        "hidden_oracle_result_canary": "oracle_result_canary_leak_count",
        "hidden_project_seed_canary": "project_seed_canary_leak_count",
    }
    counts = {output: 0 for output in fields.values()}
    for transition in transitions:
        visible = json.dumps(transition.model_input, sort_keys=True)
        canaries = dict(transition.metadata["evaluator_metadata"].get("canaries") or {})
        for source, output in fields.items():
            value = str(canaries.get(source, ""))
            if value and value in visible:
                counts[output] += 1
    counts["total_canary_leak_count"] = sum(counts.values())
    return counts


def _manifest_integrity(
    gate5a_manifest: ControllerDatasetManifest,
    transitions: list[ControllerTransition],
    canary_audit: dict[str, Any],
    output_dir: Path,
) -> dict[str, Any]:
    rows = [transition.to_dict() for transition in transitions]
    split_hashes = {
        "contrast": stable_hash(rows),
        "family_holdout": stable_hash([
            row for row in rows
            if row["metadata"]["evaluator_metadata"]["task_family"] == "simple_data_shape_mismatch"
        ]),
    }
    build_config = {
        "deterministic_seed": DETERMINISTIC_SEED,
        "pairs_per_contrast_family": 6,
        "contrast_families": CONTRAST_FAMILIES,
        "task_families": TASK_FAMILIES,
    }
    core = {
        "schema_hash": stable_hash({"schema_version": REPAIR_SCHEMA_VERSION, "kind": "gate5a_repair"}),
        "content_hash": stable_hash(rows),
        "source_trace_hashes": gate5a_manifest.source_trace_hashes,
        "split_hashes": split_hashes,
        "action_vocab_hash": stable_hash([op.value for op in CognitiveOp]),
        "leakage_audit_hash": stable_hash(canary_audit),
        "build_config_hash": stable_hash(build_config),
        "deterministic_seed": DETERMINISTIC_SEED,
    }
    manifest_hash = stable_hash(core)
    return {
        **core,
        "manifest_hash": manifest_hash,
        "output_dir": str(output_dir),
        "manifest_rebuild_match": False,
        "content_hash_rebuild_match": False,
        "split_hash_rebuild_match": False,
    }


def _predict(transition: ControllerTransition, mode: str) -> str:
    model_input = transition.model_input
    workspace = "" if mode == "no_workspace_summary" else str(model_input.get("workspace_summary", ""))
    verifier = "" if mode == "no_verifier_context" else str(model_input.get("verifier_context_summary", ""))
    previous_tool = "" if mode == "no_tool_result_summary" else str(model_input.get("previous_tool_result_summary", ""))
    memories = [] if mode == "no_memory_cards" else list(model_input.get("retrieved_memory_cards") or [])
    skills = [] if mode == "no_skill_cards" else list(model_input.get("available_skill_cards") or [])
    if mode == "no_verifier_context" and "verifier_status" in workspace:
        workspace = ""
    if mode == "no_tool_result_summary" and (
        "tool_result" in workspace or "can_read_file" in workspace or "list_files_before_read" in workspace
    ):
        workspace = ""
    if mode == "no_memory_cards" and ("memory_warning" in workspace or "skill_reuse_allowed" in workspace):
        workspace = ""
    if mode == "no_skill_cards" and (
        "skill_status" in workspace or "skill_preconditions" in workspace or "patch_allowed" in workspace
    ):
        workspace = ""

    if "latest=passed" in verifier:
        return _key(CognitiveOp.STORE_EPISODE.value, {})
    if "latest=failed" in verifier:
        return _key(CognitiveOp.PATCH.value, {"mode": "repair_after_failed_verifier"})
    if "file missing" in previous_tool or "list_files_before_read=true" in workspace:
        return _key(CognitiveOp.CALL_TOOL.value, {"operation": "list_files"})
    if "file visible" in previous_tool or "can_read_file=true" in workspace:
        return _key(CognitiveOp.CALL_TOOL.value, {"operation": "read_file"})
    if any("quarantine" in str(card).lower() for card in memories) or "skill_status=quarantined" in workspace:
        return _key(CognitiveOp.STORE_FAILURE.value, {"reason": "skill_quarantined"})
    if "false_trigger_risk=high" in workspace:
        return _key(CognitiveOp.CALL_TOOL.value, {"operation": "read_file"})
    if any("anti_pattern" in str(card).lower() for card in memories):
        return _key(CognitiveOp.CALL_TOOL.value, {"operation": "read_file"})
    if "patch_allowed=true" in workspace:
        return _key(CognitiveOp.PATCH.value, {"mode": "skill_patch"})
    if "skill_reuse_allowed=true" in workspace or any("promoted" in str(card).lower() for card in skills):
        return _key(CognitiveOp.RETRIEVE_SKILL.value, {"skill_status": "allowed"})
    return _key(CognitiveOp.CALL_TOOL.value, {"operation": "read_file"})


def _policy_accuracy(transitions: list[ControllerTransition], mode: str) -> float:
    if not transitions:
        return 0.0
    return sum(int(_predict(transition, mode) == _action_key(transition)) for transition in transitions) / len(transitions)


def _accuracy_for_subset(transitions: list[ControllerTransition], mode: str) -> float:
    return _policy_accuracy(transitions, mode)


def _changed_count(transitions: list[ControllerTransition], mode: str, contrast_family: str) -> int:
    return sum(
        int(_predict(transition, mode) != _predict(transition, "full_input"))
        for transition in transitions
        if transition.metadata["evaluator_metadata"]["contrast_family"] == contrast_family
    )


def _previous_action_majority_accuracy(transitions: list[ControllerTransition]) -> float:
    if not transitions:
        return 0.0
    groups: dict[str, dict[str, int]] = {}
    for transition in transitions:
        previous = str(transition.model_input.get("previous_action") or "<START>")
        action = _action_key(transition)
        groups.setdefault(previous, {})[action] = groups.setdefault(previous, {}).get(action, 0) + 1
    table = {key: max(counts.values()) for key, counts in groups.items()}
    return sum(table.values()) / len(transitions)


def _trajectory_patterns(transitions: list[ControllerTransition]) -> dict[str, int]:
    by_trace: dict[str, list[tuple[int, str]]] = {}
    for transition in transitions:
        by_trace.setdefault(str(transition.metadata["trace_id"]), []).append((int(transition.metadata["step_index"]), _action_key(transition)))
    patterns: dict[str, int] = {}
    for steps in by_trace.values():
        pattern = ">".join(action for _step, action in sorted(steps))
        patterns[pattern] = patterns.get(pattern, 0) + 1
    return patterns


def _conditional_entropy(transitions: list[ControllerTransition], key_fn: object) -> float:
    groups: dict[str, list[str]] = {}
    for transition in transitions:
        key = str(key_fn(transition))  # type: ignore[misc]
        groups.setdefault(key, []).append(_action_key(transition))
    total = sum(len(values) for values in groups.values())
    if total == 0:
        return 0.0
    return sum((len(values) / total) * _entropy(values) for values in groups.values())


def _entropy(values: list[str]) -> float:
    if not values:
        return 0.0
    counts: dict[str, int] = {}
    for value in values:
        counts[value] = counts.get(value, 0) + 1
    total = len(values)
    return -sum((count / total) * math.log2(count / total) for count in counts.values())


def _action_key(transition: ControllerTransition) -> str:
    return _key(str(transition.labels["target_action"]), dict(transition.labels.get("target_action_args") or {}))


def _key(action: str, args: dict[str, Any]) -> str:
    if action == CognitiveOp.CALL_TOOL.value and args.get("operation"):
        return f"{action}:{args['operation']}"
    if action == CognitiveOp.PATCH.value and args.get("mode"):
        return f"{action}:{args['mode']}"
    if action == CognitiveOp.STORE_FAILURE.value and args.get("reason"):
        return f"{action}:{args['reason']}"
    if action == CognitiveOp.RETRIEVE_SKILL.value and args.get("skill_status"):
        return f"{action}:{args['skill_status']}"
    return action


def _write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None:
    path.write_text("".join(canonical_json(row) + "\n" for row in rows), encoding="utf-8")


def _read_transitions(path: Path) -> list[ControllerTransition]:
    return [
        ControllerTransition.from_dict(json.loads(line))
        for line in path.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]
