"""
fw_server/server.py — MCP server exposing the DUSUN.md framework engine.

Registered tools (callable from VS Code Copilot via @dusun/fw-engine):
  1. run_lens            — Execute one of the 7 lenses and return a LensResult.
  2. run_bileshke        — Run the full composite pipeline (all 7 lenses → QualityReport).
  3. check_kavaid        — Evaluate all 8 kavaid constraints.
  4. validate_stage      — Single-call /dusun stage gate (AX4 enforcement).
  5. verify_chains       — Verify inference chains from the Inference Graph (§IG).
  6. run_kaskad          — Query the Generative Cascade Engine (AX23, T7, T8, AX63).
  7. framework_summary   — Aggregate framework_summary() from all modules.
  8. hcp_ingest          — Ingest context text into the Holographic Context Protocol.
  9. hcp_query           — Query HCP for relevant chunks.
 10. hcp_diagnostics     — Return HCP diagnostic state.
 11. hcp_create_workflow  — Create a multi-step workflow chain (E2).
 12. hcp_advance_workflow — Advance a workflow by one stage (E2).
 13. hcp_export_state    — Export full HCP state as JSON checkpoint (E3).
 14. hcp_import_state    — Import a previously exported HCP state checkpoint (E3).
 15. hcp_sync_memory_bank — Sync HCP context to memory-bank/ markdown files.
 16. calibrate_source_texts — Run ground-truth calibration pipeline (BS-21/22).
 17. dusun             — Universal framework analysis (T1 Boot).
 18. sentinel_scan      — Scan text for epistemic failure signals.
 19. sentinel_report    — Report a confirmed epistemic incident.
 20. sentinel_status    — Return sentinel diagnostics and statistics.

Design constraints:
  KV₇:  Every tool call is stateless (fresh adapter instances), EXCEPT HCP
         which is classified as AX27 (transparent vessel) and may persist
         across calls within a session.
  AX56: No tool ever returns grade = "Hakkalyakîn".
  AX57: Every response carries a transparency/disclosure block.
  T6/KV₄: Composite scores are always < 1.0.
  AX52: Multiplicative gate — zero in any dimension collapses the whole.

Usage:
  python -m fw_server.server        # starts stdio MCP server
"""

from __future__ import annotations

import atexit
import json
import logging
import os
import sys
import traceback
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any, Dict, List, Optional

from mcp.server.fastmcp import Context, FastMCP

from fw_server.adapters import ADAPTER_MAP, run_lens
from fw_server.memory_bank import sync_memory_bank
from fw_server.persistence import (
    get_persistence_info,
    get_sentinel_persistence_info,
    get_session_persistence_info,
    restore_hcp_state,
    restore_sentinel_state,
    restore_session_state,
    save_hcp_state,
    save_sentinel_state,
    save_session_state,
)

logger = logging.getLogger("fw_server")
from fw_server.grade_map import grade_to_string, score_to_grade
from fw_server.session import get_ledger
from fw_server.context import build_framework_context
from fw_server.tracer import trace, record, init_tracing, shutdown_tracing
from fw_server.types import LENS_IDS, VALID_GRADES
from hcp.types import SourceMetadata, WorkflowStage


# ── Coercion helper (accepts both native types from MCP and JSON strings
#    from direct Python callers / tests) ──────────────────────────────────

def _coerce(value: Any, target_type: type, default: Any = None) -> Any:
    """Coerce *value* to *target_type*, accepting JSON strings for backward compat.

    FastMCP/Pydantic delivers native Python types over the MCP wire.
    Direct callers (tests) may still pass JSON strings.
    """
    if value is None or value == "":
        return default
    if isinstance(value, target_type):
        return value
    if isinstance(value, str):
        parsed = json.loads(value)
        if not isinstance(parsed, target_type):
            raise TypeError(
                f"Expected {target_type.__name__} after JSON parse, "
                f"got {type(parsed).__name__}"
            )
        return parsed
    raise TypeError(
        f"Expected {target_type.__name__} or JSON string, "
        f"got {type(value).__name__}"
    )


# ── State directory (workspace-scoped persistence) ──

@trace
def _get_state_dir() -> Optional[Path]:
    """Return the workspace state directory for HCP persistence.

    Uses FW_STATE_DIR environment variable (set by VS Code mcp.json
    to ${workspaceFolder}/.hcp_state).  Returns None if not set,
    disabling auto-save/restore.
    """
    env = os.environ.get("FW_STATE_DIR")
    if not env:
        return None
    return Path(env)


# ── HCP instance (AX27 — transparent vessel, persists across calls) ──

_hcp_protocol = None  # lazily initialised


@trace
def _get_hcp():
    """Get or create the singleton HCPProtocol instance.

    HCP is classified as AX27 (transparent vessel): it mediates context
    retrieval but doesn't originate meaning.  It persists across calls
    because context ingestion is expensive, and the seed (computed once
    via KV₇-compliant independent process) doesn't leak into lens
    computations.
    """
    global _hcp_protocol
    if _hcp_protocol is None:
        from hcp.protocol import HCPProtocol
        _hcp_protocol = HCPProtocol()
    return _hcp_protocol


# ── SC-3 content-aware tool suggestion engine ────────────────────────

import re as _re

# Patterns that indicate specific claim types in text content
_SC3_PATTERNS = [
    {
        "claim_type": "axiom_reference",
        "regex": _re.compile(r"\bAX\d{1,2}\b", _re.IGNORECASE),
        "tool": "run_single_lens",
        "call_hint": 'run_single_lens(lens_id="fol", params={"text": "..."})',
        "reason": "Text references axioms — FOL lens can verify consistency",
    },
    {
        "claim_type": "theorem_reference",
        "regex": _re.compile(r"\bT\d{1,2}\b"),
        "tool": "run_single_lens",
        "call_hint": 'run_single_lens(lens_id="fol", params={"text": "..."})',
        "reason": "Text references theorems — FOL lens can verify derivation",
    },
    {
        "claim_type": "inference_chain",
        "regex": _re.compile(r"\bC[1-7]\b"),
        "tool": "verify_chains",
        "call_hint": 'verify_chains(chain_id="C?", text="...")',
        "reason": "Text references inference chains — verify_chains traces them",
    },
    {
        "claim_type": "kavaid_reference",
        "regex": _re.compile(r"\bKV[₁₂₃₄₅₆₇₈1-8]\b"),
        "tool": "check_kavaid",
        "call_hint": "check_kavaid(composite_score=..., latife_bits=[...])",
        "reason": "Text references kavaid — check_kavaid evaluates all 8",
    },
    {
        "claim_type": "convergence_claim",
        "regex": _re.compile(r"convergence|composite.?score|bileshke|yakınlaşma", _re.IGNORECASE),
        "tool": "run_bileshke_pipeline",
        "call_hint": "run_bileshke_pipeline(lens_params={...})",
        "reason": "Text mentions convergence/composite — compute actual score",
    },
    {
        "claim_type": "holographic_claim",
        "regex": _re.compile(r"holograph|B0[1-9]|B[12][0-9]|KV₆|KV6|seed.?omni|part.?whole", _re.IGNORECASE),
        "tool": "run_single_lens",
        "call_hint": 'run_single_lens(lens_id="holografik", params={})',
        "reason": "Text mentions holographic patterns — check B01-B22 dimensions",
    },
    {
        "claim_type": "structural_pattern",
        "regex": _re.compile(
            r"three.?phase|actuali[sz]ation|integration.?prerequisite|"
            r"multiplicative.?gate|transparent.?vessel|"
            r"unreachable.?supremum|structural.?incompleteness",
            _re.IGNORECASE,
        ),
        "tool": "validate_stage",
        "call_hint": 'validate_stage(stage="...", summary="...")',
        "reason": "Text asserts structural patterns — validate_stage can gate-check",
    },
    {
        "claim_type": "grade_claim",
        "regex": _re.compile(r"Tasavvur|Tasdik|İlmelyakîn|Ilmelyakin|Hakkalyakîn|epistemic.?grade", _re.IGNORECASE),
        "tool": "run_bileshke_pipeline",
        "call_hint": "run_bileshke_pipeline(lens_params={...})",
        "reason": "Text claims an epistemic grade — compute from bileshke score",
    },
]


@trace
def _suggest_tools_for_content(texts: List[str]) -> List[Dict[str, str]]:
    """Scan texts for claim-type patterns and return SC-3 tool suggestions.

    Returns a deduplicated list of {claim_type, tool, call_hint, reason}
    dicts, one per matched claim type.
    """
    combined = " ".join(texts)
    seen: set = set()
    suggestions: List[Dict[str, str]] = []
    for pat in _SC3_PATTERNS:
        if pat["claim_type"] not in seen and pat["regex"].search(combined):
            seen.add(pat["claim_type"])
            suggestions.append({
                "claim_type": pat["claim_type"],
                "tool": pat["tool"],
                "call_hint": pat["call_hint"],
                "reason": pat["reason"],
            })
    return suggestions


# ── Create the MCP server ──


@asynccontextmanager
async def _lifespan(server: FastMCP) -> AsyncIterator[None]:
    """Lifespan hook: auto-restore on startup, auto-save on shutdown.

    AX5 (Integration Prerequisite): this is the Hayat layer that
    integrates HCP persistence across VS Code sessions.
    """
    state_dir = _get_state_dir()
    protocol = _get_hcp()
    init_tracing()

    # ── Startup: auto-restore ──
    if state_dir is not None:
        restored = restore_hcp_state(state_dir, protocol)
        if restored:
            logger.info("HCP context restored — %d chunks", len(protocol._chunks))
        # Restore session ledger (independent of HCP — KV₇)
        ledger = get_ledger()
        session_restored = restore_session_state(state_dir, ledger)
        if session_restored:
            logger.info(
                "Session ledger restored — %d prior calls",
                ledger.call_count,
            )
        # Restore sentinel state (independent of HCP + session — KV₇)
        try:
            from sentinel.engine import get_sentinel as _boot_sentinel
            _sentinel = _boot_sentinel()
            sentinel_restored = restore_sentinel_state(state_dir, _sentinel)
            if sentinel_restored:
                logger.info("Sentinel state restored")
        except Exception:
            logger.debug("Sentinel restore skipped", exc_info=True)
    else:
        logger.info("FW_STATE_DIR not set — persistence disabled")

    # Register atexit as backup (in case lifespan finally doesn't fire)
    def _atexit_save():
        sd = _get_state_dir()
        if sd is not None:
            save_hcp_state(sd, _get_hcp())
            save_session_state(sd, get_ledger())
            try:
                from sentinel.engine import get_sentinel as _exit_sentinel
                save_sentinel_state(sd, _exit_sentinel())
            except Exception:
                pass

    atexit.register(_atexit_save)

    try:
        yield
    finally:
        # ── Shutdown: auto-save ──
        if state_dir is not None:
            save_hcp_state(state_dir, protocol)
            save_session_state(state_dir, get_ledger())
            try:
                from sentinel.engine import get_sentinel as _shutdown_sentinel
                save_sentinel_state(state_dir, _shutdown_sentinel())
            except Exception:
                logger.debug("Sentinel save on shutdown skipped", exc_info=True)
            logger.info("HCP + session + sentinel state saved on shutdown")
            # Memory bank sync (best-effort, don't block shutdown)
            try:
                sync_memory_bank(state_dir, protocol, ledger=get_ledger())
                logger.info("Memory bank synced on shutdown")
            except Exception:
                logger.warning("Memory bank sync failed on shutdown", exc_info=True)
        shutdown_tracing()
        atexit.unregister(_atexit_save)


mcp = FastMCP(
    "fw-engine",
    instructions=(
        "DUSUN.md Framework Engine — exposes the 7-lens analytical apparatus, "
        "bileshke composite engine, kavaid constraints, inference chains, and "
        "Holographic Context Protocol as MCP tools."
    ),
    lifespan=_lifespan,
    warn_on_duplicate_tools=True,
)


# =====================================================================
# Tool 1: run_lens
# =====================================================================

@mcp.tool()
@trace
def run_single_lens(
    lens_id: str,
    params: Optional[dict] = None,
    hints: Optional[dict] = None,
) -> str:
    """Execute one of the 7 formal lenses and return a LensResult.

    Args:
        lens_id: One of "Ontoloji", "Mereoloji", "FOL", "Bayes",
                 "OyunTeorisi", "KategoriTeorisi", "Topoloji + Holografik".
        params: Dict of lens-specific parameters (optional; JSON string also accepted).
                Examples:
                  FOL: {"text": "AX1 states that..."}
                  Ontoloji: {"kavramlar": [{"name": "tree", "hakikat": [{"isim": "Muhyi", "derece": 0.7}], "mahiyet": "shadow", "dual_class": "harfi"}]}
        hints: Dict of LLM-extracted structural hints for this lens
                (optional). Uses the same per-lens hint schemas as
                dusun()'s hint parameters. Merged into params as
                "_hints" key before the adapter runs.

    Returns:
        JSON string with score, grade, checks, and detail.
    """
    try:
        parsed_params = _coerce(params, dict, {})
        if hints:
            parsed_params["_hints"] = _coerce(hints, dict)
        result = run_lens(lens_id, parsed_params)
        return json.dumps(result.to_dict(), ensure_ascii=False, indent=2)
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 2: run_bileshke
# =====================================================================

@mcp.tool()
@trace
def run_bileshke_pipeline(
    lens_params: Optional[dict] = None,
    weights: Optional[dict] = None,
    ortam_bits: Optional[list] = None,
) -> str:
    """Run the full composite pipeline: all 7 lenses → bileshke → QualityReport.

    Computes the bileshke (composite) convergence score across all 7 formal
    lenses.  The composite is structurally bounded: 0 < composite < 1.0
    (T6/KV₄ — the map participates in truth without being identical to it).

    Args:
        lens_params: Dict mapping lens_id to params dict (JSON string also accepted).
                     Example: {"FOL": {"text": "AX1..."}, "Ontoloji": {}}
                     If omitted, all lenses run with empty params.
        weights: Dict mapping lens_id to float weight (JSON string also accepted).
                 Must sum to 1.0. If omitted, equal weights (1/7 each).
        ortam_bits: List of 3 ints (0 or 1) for ortam coverage (JSON string also accepted).
                    Example: [1, 1, 1] for full coverage.

    Returns:
        JSON string with composite_score, lens_results, kavaid_checks,
        coverage vectors, warnings, and AX57 disclosure.
        The ``_framework_context`` field contains enforcement directives
        you MUST follow in your response.

    Enforcement (MANDATORY — read before generating your response):
        - The composite_score MUST stay below 1.0.  If it reaches >= 0.95,
          this is a KV₄ violation — report it as error, NEVER as success.
        - The grade derived from composite_score is the CEILING — you
          MUST NOT claim a higher grade in your disclosure.
        - Coverage of 6/7 is the structural maximum (T17 — Ahfâ is
          permanently unmapped). NEVER claim 7/7 coverage.
        - The ``_framework_context.directives`` list contains specific
          constraints. You MUST honour every directive listed there.
    """
    try:
        from bileshke import (
            run_bileshke as _run_bileshke,
            OrtamVektor,
            LensId,
            detect_kv4_warning,
        )
        from bileshke.quality import compute_q3_kavaid, ALL_KAVAID

        # Parse inputs
        all_lens_params: Dict[str, Dict] = _coerce(lens_params, dict, {})
        parsed_weights: Optional[Dict[str, float]] = _coerce(weights, dict)
        parsed_ortam: Optional[List[int]] = _coerce(ortam_bits, list)

        # Run each lens through its adapter (KV₇: each fresh)
        lens_results = []
        errors: Dict[str, str] = {}
        for lid in LENS_IDS:
            p = all_lens_params.get(lid, {})
            try:
                lr = run_lens(lid, p)
                lens_results.append(lr)
            except Exception as e:
                errors[lid] = str(e)

        # Convert string weights to LensId weights if provided
        engine_weights = None
        if parsed_weights:
            engine_weights = {}
            for lid_str, w in parsed_weights.items():
                for lid_enum in LensId:
                    if lid_enum.value == lid_str:
                        engine_weights[lid_enum] = w
                        break

        # Ortam
        ortam = None
        if parsed_ortam and len(parsed_ortam) == 3:
            ortam = OrtamVektor(bits=tuple(parsed_ortam))

        # Run bileshke
        report = _run_bileshke(lens_results, engine_weights, ortam)

        # Compute kavaid (Q-3)
        kavaid_result = compute_q3_kavaid(
            composite_score=report.composite_score,
            latife=report.latife_vektor,
        )

        # Build response
        response = report.to_dict()
        response["kavaid_q3"] = kavaid_result
        if errors:
            response["lens_errors"] = errors

        # AX57 disclosure
        response["ax57_disclosure"] = {
            "composite_score": report.composite_score,
            "grade": grade_to_string(score_to_grade(report.composite_score)),
            "completeness": report.completeness,
            "latife_gate": report.latife_gate,
            "ortam_gate": report.ortam_gate,
            "practical_gate": report.practical_gate,
            "kv4_status": "PASS" if report.composite_score < 0.95 else "FAIL — ≥0.95",
            "kv7_status": "PASS — each lens ran in fresh adapter",
            "ax56_status": "PASS — Hakkalyakîn never claimed",
        }

        # ── Session ledger recording (Channel 3 — fire-and-forget) ───
        try:
            _bl_grade = grade_to_string(score_to_grade(report.composite_score))
            get_ledger().record_call(
                "run_bileshke_pipeline",
                grade=_bl_grade,
                composite_score=report.composite_score,
            )
        except Exception:
            pass  # KV₃: ledger failure never propagates

        # ── Channel 2: enriched tool return ──────────────────────────
        try:
            _bl_ctx_grade = grade_to_string(score_to_grade(report.composite_score))
            response["_framework_context"] = build_framework_context(
                "run_bileshke_pipeline",
                grade=_bl_ctx_grade,
                composite_score=report.composite_score,
                session_context=get_ledger().get_cumulative_context(),
                cascade_status=get_ledger().get_cascade_status(),
                cascade_forward=get_ledger().get_cascade_forward(),
            )
        except Exception:
            pass  # KV₃: context enrichment failure never propagates

        return json.dumps(response, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 3: check_kavaid
# =====================================================================

@mcp.tool()
@trace
def check_kavaid(
    composite_score: float = 0.0,
    latife_bits: Optional[list] = None,
    kavaid_overrides: Optional[dict] = None,
    lens_scores: Optional[dict] = None,
) -> str:
    """Evaluate all 8 kavaid constraints and return pass/fail for each.

    The 8 kavaid are formal boundary conditions (DUSUN.md §5.4) that
    EVERY analysis MUST satisfy.  A single kavaid failure is a structural
    violation — it cannot be offset by high scores elsewhere (AX52,
    multiplicative gate).

    Args:
        composite_score: The bileshke composite score (for KV₄ check).
        latife_bits: List of 7 ints (0 or 1) (JSON string also accepted).
        kavaid_overrides: Dict of manual overrides (JSON string also accepted)
                          (e.g. {"KV1": true, "KV2": false}).
        lens_scores: Dict of lens_id->score (JSON string also accepted).
                     When provided, enables computational verification of
                     KV₁ (Ontoloji>0), KV₆ (Holografik>0), KV₈ (Ontoloji>0).
                     Without this, those kavaid are structural-only (always True).

    Returns:
        JSON with per-kavaid pass/fail, descriptions, all_pass flag,
        and KV₄ warning if applicable.
        The ``_framework_context`` field contains enforcement directives
        you MUST follow in your response.

    Enforcement (MANDATORY — read before citing kavaid compliance):
        - If ``all_pass`` is false, you MUST report the failing kavaid
          in your AX57 disclosure. NEVER hide kavaid violations.
        - You MUST NOT cite a passing grade when kavaid violations exist —
          any failure constrains the maximum achievable grade.
        - KV₇ (independence) failure means convergence is meaningless —
          you MUST disclose this if KV₇ fails.
        - The ``_framework_context.directives`` list contains specific
          constraints. You MUST honour every directive listed there.
    """
    try:
        from bileshke.quality import (
            compute_q3_kavaid,
            ALL_KAVAID,
            KAVAID_DESCRIPTIONS,
        )
        from bileshke.types import LatifeVektor
        from bileshke.engine import detect_kv4_warning

        latife = None
        if latife_bits:
            bits = _coerce(latife_bits, list)
            if len(bits) == 7:
                latife = LatifeVektor(bits=tuple(bits))

        overrides = _coerce(kavaid_overrides, dict)
        scores = _coerce(lens_scores, dict) if lens_scores else None

        result = compute_q3_kavaid(
            kavaid_checks=overrides,
            composite_score=composite_score,
            latife=latife,
            lens_scores=scores,
        )

        # Add descriptions
        result["descriptions"] = KAVAID_DESCRIPTIONS

        # KV₄ explicit check
        kv4_warn = detect_kv4_warning(composite_score)
        if kv4_warn:
            result["kv4_warning"] = kv4_warn

        # ── Session ledger recording (Channel 3 — fire-and-forget) ───
        try:
            _kv_pass = sum(
                1 for k, v in result.get("checks", {}).items()
                if isinstance(v, bool) and v
            )
            get_ledger().record_call(
                "check_kavaid",
                composite_score=composite_score,
                kavaid_pass_count=_kv_pass,
            )
            # Record individual kavaid failures
            for kv_id, kv_val in result.get("checks", {}).items():
                if kv_val is False:
                    get_ledger().record_kavaid_failure(kv_id)
        except Exception:
            pass  # KV₃: ledger failure never propagates

        # ── Channel 2: enriched tool return ──────────────────────────
        try:
            _kv_ctx_pass = sum(
                1 for k, v in result.get("checks", {}).items()
                if isinstance(v, bool) and v
            )
            result["_framework_context"] = build_framework_context(
                "check_kavaid",
                composite_score=composite_score,
                kavaid_pass_count=_kv_ctx_pass,
                session_context=get_ledger().get_cumulative_context(),
                cascade_status=get_ledger().get_cascade_status(),
                cascade_forward=get_ledger().get_cascade_forward(),
            )
        except Exception:
            pass  # KV₃: context enrichment failure never propagates

        return json.dumps(result, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 4: validate_stage  (AX4 enforcement gate for /dusun pipeline)
# =====================================================================

# Stage → most-relevant lens IDs
_STAGE_LENS_MAP: Dict[str, List[str]] = {
    "preflight": ["Ontoloji"],
    "1": ["Ontoloji"],                                   # Output Translation
    "2": ["FOL"],                                         # Foundational Commitments
    "3": ["Ontoloji", "Mereoloji", "Topoloji + Holografik"],  # Ontological Framework
    "4": ["Bayes"],                                       # Epistemic Architecture
    "5": ["FOL", "Topoloji + Holografik"],                # Quality Framework + Kavaid
    "6": ["Topoloji + Holografik"],                       # Structural Incompleteness
    "7": ["FOL"],                                         # dusun-router.instructions.md
    "8": ["FOL"],                                         # Inference Graph
    "9":  list(LENS_IDS),                                 # alias for 9A
    "9a": list(LENS_IDS),                                 # Actualize (full)
    "9b": list(LENS_IDS),                                 # Synthesize (full)
}


@mcp.tool()
@trace
def validate_stage(
    stage: str,
    summary: str = "",
    content: Optional[str] = None,
) -> str:
    """Single-call stage gate: runs bileshke + kavaid + anomaly detection.

    This is the AX4 (Kudret) enforcement mechanism.  Every /dusun pipeline
    stage MUST call this tool before marking ✅.  It combines
    run_bileshke_pipeline and check_kavaid into one call, adds
    stage-aware anomaly detection, and returns a BINDING gate verdict.

    Gate verdicts and their consequences:
      PASS  — stage may proceed.
      WARN  — stage may proceed but anomalies MUST be disclosed.
      FAIL  — stage MUST NOT be marked complete; remediation required.

    Prescription 1 (AX4): mechanical enforcement — one tool, not three.
    Prescription 2 (AX5): feedback integration — anomaly flags change
      what the next stage should do.
    Prescription 3 (Feedback loop): anomalous scores trigger
      explicit remediation hints.

    Args:
        stage: Pipeline stage identifier.  One of:
               "preflight", "1" through "8", "9", "9a", "9b".
               Determines which lenses are most relevant for
               anomaly detection.
        summary: Brief description of what was analysed/built.
               When content is not provided, the summary is used
               as fallback text for content-aware lenses (FIX-01).
        content: Optional source text / reasoning to feed into
               content-aware lenses.  When provided, ALL lenses
               receive {"text": content}.  When omitted (the
               common case during /dusun pipeline runs), the summary
               is used as a fallback so lenses can still produce
               meaningful scores rather than baseline zero.

    Returns:
        JSON with gate verdict (PASS/WARN/FAIL), composite_score,
        kavaid pass/fail, relevant-lens scores, anomaly flags,
        remediation hints, and compact AX57 disclosure.
        The ``_framework_context`` field contains enforcement directives
        you MUST follow in your response.

    Enforcement (MANDATORY — read before marking any stage):
        - You MUST call validate_stage at EVERY /dusun pipeline stage
          before marking it ✅.  Skipping is a framework violation.
        - The gate verdict is BINDING — you MUST NOT override FAIL
          or ignore WARN anomalies. NEVER skip disclosure.
        - The grade from composite_score is the CEILING — you MUST NOT
          claim a higher grade. Grade inflation is a violation.
        - Anomalies MUST be disclosed in your response — hiding them
          is a transparency violation (AX57).
        - The ``_framework_context.directives`` list contains specific
          constraints. You MUST honour every directive listed there.
    """
    try:
        from bileshke import run_bileshke as _run_bileshke
        from bileshke.quality import compute_q3_kavaid, ALL_KAVAID
        from bileshke.engine import detect_kv4_warning as kv4_check

        stage_key = stage.strip().lower()

        # ── 0.  Derive content when the LLM omits it (FIX-01) ───────
        # The LLM almost never passes ``content`` during /dusun pipeline
        # runs, so lenses receive {} and return 0.0.  The code must
        # actively feed text so it "changes the flow of information
        # and energy" (AX4 — Kudret).  Derivation order:
        #   1. Explicit content (as-is)
        #   2. summary (always available from the LLM)
        effective_content = content or summary or None

        # ── 1.  Run all 7 lenses (KV₇: each fresh) ──────────────────
        lens_results = []
        lens_scores: Dict[str, float] = {}
        errors: Dict[str, str] = {}
        for lid in LENS_IDS:
            try:
                # Content-aware dispatch: ALL lenses receive text when
                # effective_content is available (Fix 4 upgraded by FIX-01).
                if effective_content:
                    params = {"text": effective_content}
                else:
                    params = {}
                lr = run_lens(lid, params)
                lens_results.append(lr)
                lens_scores[lid] = lr.score
            except Exception as e:
                errors[lid] = str(e)
                lens_scores[lid] = -1.0  # sentinel

        # ── 2.  Bileshke composite ───────────────────────────────────
        report = _run_bileshke(lens_results, None, None)
        composite = report.composite_score

        # ── 3.  Kavaid (Q-3) ─────────────────────────────────────────
        kavaid_result = compute_q3_kavaid(
            composite_score=composite,
            latife=report.latife_vektor,
            lens_scores=lens_scores,
        )
        kavaid_pass_count = sum(
            1 for k, v in kavaid_result.get("checks", {}).items()
            if isinstance(v, bool) and v
        )

        # ── 4.  Stage-aware anomaly detection ────────────────────────
        relevant = _STAGE_LENS_MAP.get(stage_key, list(LENS_IDS))
        anomalies: List[str] = []
        relevant_scores: Dict[str, float] = {}
        for lid in relevant:
            sc = lens_scores.get(lid, -1.0)
            relevant_scores[lid] = sc
            if sc == 0.0:
                anomalies.append(
                    f"ZERO-SCORE: {lid} returned 0.0 — expected "
                    f"non-zero for stage {stage}. Check if {lid}-domain "
                    f"content was actually provided."
                )
            elif sc < 0:
                anomalies.append(f"ERROR: {lid} failed — {errors.get(lid, '?')}")

        # KV₄ check
        kv4_warn = kv4_check(composite)
        if kv4_warn:
            anomalies.append(f"KV4-BREACH: composite={composite:.4f} — {kv4_warn}")

        # Kavaid failures (checks are nested inside kavaid_result["checks"])
        kavaid_checks = kavaid_result.get("checks", {})
        for kv_id in ALL_KAVAID:
            val = kavaid_checks.get(kv_id)
            if val is False:
                anomalies.append(f"KAVAID-FAIL: {kv_id} did not pass")

        # ── 5.  Gate verdict ─────────────────────────────────────────
        if any(a.startswith("KV4-BREACH") or a.startswith("ERROR") for a in anomalies):
            gate = "FAIL"
        elif anomalies:
            gate = "WARN"
        else:
            gate = "PASS"

        # ── 6.  Remediation hints (Prescription 3: feedback loop) ────
        remediation: List[str] = []
        if gate != "PASS":
            for a in anomalies:
                if "ZERO-SCORE" in a:
                    lid_name = a.split(":")[1].strip().split(" ")[0]
                    remediation.append(
                        f"Feed {lid_name}-specific content into the lens. "
                        f"For FOL: provide axiom text. For Ontoloji: "
                        f"provide kavram dicts. For Holografik: ensure "
                        f"holographic seed data is available."
                    )
                elif "ERROR" in a:
                    remediation.append(
                        "Lens crashed — check module installation and imports."
                    )
                elif "KV4-BREACH" in a:
                    remediation.append(
                        "Composite ≥ 0.95 — this is structurally impossible "
                        "(T6/KV₄). Likely a bug in weight configuration."
                    )
                elif "KAVAID-FAIL" in a:
                    remediation.append(
                        "Review the failing kavaid constraint and ensure "
                        "the analysis honours its boundary condition."
                    )

        # ── 7.  Compact AX57 disclosure ──────────────────────────────
        grade_str = grade_to_string(score_to_grade(composite))
        ax57 = (
            f"DUSUN: stage={stage} | composite={composite:.3f} | "
            f"kavaid={kavaid_pass_count}/8 | "
            f"gate={gate} | grade={grade_str}"
        )

        response = {
            "gate": gate,
            "composite_score": round(composite, 4),
            "kavaid": kavaid_result,
            "kavaid_pass_count": kavaid_pass_count,
            "relevant_lens_scores": relevant_scores,
            "all_lens_scores": lens_scores,
            "anomalies": anomalies,
            "remediation": remediation,
            "ax57_compact": ax57,
            "stage": stage,
            "summary": summary,
            "completeness": report.completeness,
            "latife_gate": report.latife_gate,
            "ortam_gate": report.ortam_gate,
        }
        if errors:
            response["lens_errors"] = errors

        # ── 8.  Auto-ingest gate result into HCP (Step 4: workflow
        #        enforcement — every stage gate is remembered) ────────
        try:
            protocol = _get_hcp()
            ingest_text = (
                f"[GATE stage={stage}] verdict={gate} "
                f"composite={composite:.4f} kavaid={kavaid_pass_count}/8 "
                f"grade={grade_str}"
            )
            if summary:
                ingest_text += f" | summary: {summary}"
            if anomalies:
                ingest_text += f" | anomalies: {'; '.join(anomalies)}"
            protocol.ingest(ingest_text, source_metadata=SourceMetadata(
                tool_name="validate_stage",
                content_type_hint="gate_result",
            ))
            response["hcp_ingested"] = True
        except Exception:
            response["hcp_ingested"] = False

        # ── Session ledger recording (Channel 3 — fire-and-forget) ───
        try:
            get_ledger().record_call(
                "validate_stage",
                grade=grade_str,
                gate=gate,
                composite_score=composite,
                kavaid_pass_count=kavaid_pass_count,
                anomalies=anomalies,
                extra={"stage": stage},
            )
            # Record individual kavaid failures into ledger
            for kv_id in ALL_KAVAID:
                if kavaid_checks.get(kv_id) is False:
                    get_ledger().record_kavaid_failure(kv_id)
        except Exception:
            pass  # KV₃: ledger failure never propagates

        # ── Channel 2: enriched tool return ──────────────────────────
        try:
            response["_framework_context"] = build_framework_context(
                "validate_stage",
                grade=grade_str,
                gate=gate,
                composite_score=composite,
                kavaid_pass_count=kavaid_pass_count,
                anomalies=anomalies,
                session_context=get_ledger().get_cumulative_context(),
                cascade_status=get_ledger().get_cascade_status(),
                cascade_forward=get_ledger().get_cascade_forward(),
            )
        except Exception:
            pass  # KV₃: context enrichment failure never propagates

        return json.dumps(response, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 5: verify_chains
# =====================================================================

@mcp.tool()
@trace
def verify_chains(
    chain_id: Optional[str] = None,
    text: Optional[str] = None,
) -> str:
    """Verify inference chains from the DUSUN.md Inference Graph (§IG).

    Args:
        chain_id: Optional — one of "C1"–"C7". If omitted, all chains verified.
        text: Optional — source text to scan for axiom/theorem/kavaid references.

    Returns:
        JSON with per-chain verification results, hub nodes,
        and (if text provided) extracted axiom references.
    """
    try:
        from fol_formalizasyon import (
            verify_all_chains,
            verify_inference_chain,
            extract_axiom_refs,
            INFERENCE_CHAINS,
            HUB_NODES,
        )

        response: Dict[str, Any] = {}

        # Chain verification (fol_formalizasyon — axiom-level)
        if chain_id:
            valid, missing = verify_inference_chain(chain_id)
            response["chains"] = {
                chain_id: {"valid": valid, "missing": missing}
            }
        else:
            all_results = verify_all_chains()
            response["chains"] = {
                cid: {"valid": v, "missing": m}
                for cid, (v, m) in all_results.items()
            }

        response["all_valid"] = all(
            v["valid"] for v in response["chains"].values()
        )
        response["hub_nodes"] = dict(HUB_NODES)

        # Axiom ref extraction
        if text:
            refs = extract_axiom_refs(text)
            response["axiom_refs"] = refs
            response["total_refs"] = sum(len(v) for v in refs.values())

        # Kaskad structural verification (AX23 DAG-level)
        try:
            from kaskad import (
                build_framework_graph,
                verify_all as kaskad_verify_all,
                is_fully_verified as kaskad_is_fully_verified,
            )

            dag = build_framework_graph()
            checks = kaskad_verify_all(dag)
            report = dag.generate_report()

            response["kaskad_verification"] = {
                "checks": [
                    {
                        "name": c.check_name,
                        "passed": c.passed,
                        "message": c.message,
                    }
                    for c in checks
                ],
                "all_pass": kaskad_is_fully_verified(checks),
                "graph_stats": {
                    "nodes": report.total_nodes,
                    "edges": report.total_edges,
                    "chains": report.chain_count,
                    "max_depth": report.max_depth,
                    "structural_completeness": round(
                        report.structural_completeness, 4
                    ),
                },
                "bridge_nodes": report.bridge_nodes,
                "tesanud_count": sum(
                    1 for t in report.tesanud_nodes if t.is_tesanud
                ),
            }
        except Exception as e:
            response["kaskad_verification"] = {"error": str(e)}

        return json.dumps(response, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 6: run_kaskad  (Generative Cascade Engine — AX23, T7, T8, AX63)
# =====================================================================

@mcp.tool()
@trace
def run_kaskad(
    action: str = "report",
    seeds: Optional[list] = None,
    chain_id: Optional[str] = None,
    max_hops: int = 2,
    lens_scores: Optional[dict] = None,
) -> str:
    """Query the Generative Cascade Engine (AX23 DAG, T8 prediction, tesanüd).

    The kaskad module materialises the DUSUN.md Inference Graph (§IG.2)
    as a computable, self-referential DAG.  This tool exposes its core
    operations.

    Args:
        action: Operation to perform. One of:
            "report"     — Full cascade report (nodes, edges, chains, hubs,
                           bridges, tesanüd, structural_completeness).
            "predict"    — T8 Neighborliness: given seed nodes, predict
                           which neighbors become detectable (requires seeds).
            "propagate"  — Cascade propagation from seed nodes (requires seeds).
            "hubs"       — Hub & bridge analysis (§IG.3 cross-chain points).
            "tesanud"    — Tesanüd detection (AX63 super-additive convergence).
            "chain"      — Analyse a specific chain (requires chain_id, C1–C7).
            "verify"     — Run all 7 structural verification checks.
            "diagnostic" — Self-diagnostic (AX23 applied reflexively).
        seeds: List of node IDs to seed predictions/propagation (JSON string also accepted).
               Example: ["AX24", "AX25"]
        chain_id: Chain identifier for chain-specific analysis.
                  One of "C1"–"C7", "C_QF" (Quality Framework).
        max_hops: Maximum hops for propagation/prediction (default: 2).

    Returns:
        JSON with action-specific results and AX57 disclosure.
    """
    try:
        from kaskad import (
            build_framework_graph,
            verify_all as kaskad_verify_all,
            is_fully_verified as kaskad_is_fully_verified,
            ChainId,
            activate_edges,
            predict_neighbors_dynamic,
        )

        dag = build_framework_graph()

        if action == "report":
            report = dag.generate_report()
            return json.dumps({
                "action": "report",
                "total_nodes": report.total_nodes,
                "total_edges": report.total_edges,
                "chain_count": report.chain_count,
                "max_depth": report.max_depth,
                "structural_completeness": round(
                    report.structural_completeness, 4
                ),
                "hub_nodes": [
                    {
                        "node": h.node_id,
                        "in_degree": h.in_degree,
                        "out_degree": h.out_degree,
                        "chains": [c.value for c in h.chains],
                    }
                    for h in report.hub_nodes
                ],
                "bridge_nodes": report.bridge_nodes,
                "tesanud_points": [
                    {
                        "node": t.node_id,
                        "is_tesanud": t.is_tesanud,
                        "chains": [c.value for c in t.converging_chains],
                        "chain_count": t.chain_count,
                        "composition_mode": t.composition_mode,
                    }
                    for t in report.tesanud_nodes
                    if t.is_tesanud
                ],
                "chain_integrity": {
                    chain.value: {
                        "nodes": ci.node_count,
                        "edges": ci.edge_count,
                        "acyclic": ci.is_acyclic,
                        "connected": ci.is_connected,
                        "depth": ci.max_depth,
                    }
                    for chain, ci in report.chain_integrity.items()
                },
                "ax57_disclosure": {
                    "structural_completeness": round(
                        report.structural_completeness, 4
                    ),
                    "kv4_status": (
                        "PASS"
                        if report.structural_completeness < 0.95
                        else "WARNING — approaching 0.95"
                    ),
                },
            }, ensure_ascii=False, indent=2)

        elif action == "predict":
            if not seeds:
                return json.dumps({
                    "error": "seeds required for 'predict' action.",
                    "hint": 'Provide seeds as JSON list, e.g. \'["AX24", "AX25"]\'',
                }, ensure_ascii=False, indent=2)
            seed_list = _coerce(seeds, list, [])
            _ls = _coerce(lens_scores, dict, {}) if lens_scores else {}
            if _ls:
                prediction = predict_neighbors_dynamic(
                    dag, frozenset(seed_list), _ls, max_hops=max_hops,
                )
            else:
                prediction = dag.predict_neighbors(
                    frozenset(seed_list), max_hops=max_hops
                )
            result_dict: dict = {
                "action": "predict",
                "seeds": seed_list,
                "max_hops": max_hops,
                "detected": sorted(prediction.detected),
                "predicted": sorted(prediction.predicted),
                "confidence": round(prediction.confidence, 4),
                "chain_coverage": {
                    c.value: round(v, 4)
                    for c, v in prediction.chain_coverage.items()
                },
                "t8_note": "T8: Detecting one Name predicts detectability of cascade neighbors.",
            }
            if _ls:
                dynamic_edges = activate_edges(dag, set(), _ls)
                active_count = sum(
                    1 for de in dynamic_edges.values()
                    if de.activation > de.MIN_ACTIVATION
                )
                result_dict["dynamic_activation"] = {
                    "total_edges": len(dynamic_edges),
                    "active_edges": active_count,
                    "lens_scores_used": True,
                }
            return json.dumps(result_dict, ensure_ascii=False, indent=2)

        elif action == "propagate":
            if not seeds:
                return json.dumps({
                    "error": "seeds required for 'propagate' action.",
                }, ensure_ascii=False, indent=2)
            seed_list = _coerce(seeds, list, [])
            reached = dag.propagate(
                seed_list, max_hops=max_hops if max_hops > 0 else -1
            )
            return json.dumps({
                "action": "propagate",
                "seeds": seed_list,
                "max_hops": max_hops,
                "reached": [
                    {"node": nid, "distance": dist}
                    for nid, dist in reached
                ],
                "total_reached": len(reached),
                "t7_note": "T7: Creation exists because Beauty desires mirrors — cascade propagates outward.",
            }, ensure_ascii=False, indent=2)

        elif action == "hubs":
            hubs = dag.analyze_hubs()
            bridges = dag.find_bridges()
            return json.dumps({
                "action": "hubs",
                "hubs": [
                    {
                        "node": h.node_id,
                        "in_degree": h.in_degree,
                        "out_degree": h.out_degree,
                        "chains": [c.value for c in h.chains],
                        "is_hub": h.in_degree + h.out_degree >= 3,
                    }
                    for h in hubs
                ],
                "bridges": bridges,
                "ig3_note": "§IG.3: Hub nodes have highest in-degree across chains (KV₇ tesanüd).",
            }, ensure_ascii=False, indent=2)

        elif action == "tesanud":
            tesanud_results = dag.analyze_tesanud()
            return json.dumps({
                "action": "tesanud",
                "results": [
                    {
                        "node": t.node_id,
                        "is_tesanud": t.is_tesanud,
                        "converging_chains": [
                            c.value for c in t.converging_chains
                        ],
                        "chain_count": t.chain_count,
                        "composition_mode": t.composition_mode,
                    }
                    for t in tesanud_results
                ],
                "tesanud_count": sum(
                    1 for t in tesanud_results if t.is_tesanud
                ),
                "ax63_note": (
                    "AX63: Affirmations compose super-additively (tesanüd); "
                    "denials remain isolated (infirâdî)."
                ),
            }, ensure_ascii=False, indent=2)

        elif action == "chain":
            if not chain_id:
                return json.dumps({
                    "error": "chain_id required for 'chain' action.",
                    "valid_chains": [c.value for c in ChainId],
                }, ensure_ascii=False, indent=2)
            # Map chain_id string to ChainId enum
            target = None
            for c in ChainId:
                if c.value == chain_id:
                    target = c
                    break
            if target is None:
                return json.dumps({
                    "error": f"Unknown chain_id: {chain_id}",
                    "valid_chains": [c.value for c in ChainId],
                }, ensure_ascii=False, indent=2)
            ci = dag.analyze_chain(target)
            nodes = dag.nodes_in_chain(target)
            return json.dumps({
                "action": "chain",
                "chain_id": chain_id,
                "nodes": sorted(n.id for n in nodes),
                "node_count": ci.node_count,
                "edge_count": ci.edge_count,
                "is_acyclic": ci.is_acyclic,
                "is_connected": ci.is_connected,
                "max_depth": ci.max_depth,
            }, ensure_ascii=False, indent=2)

        elif action == "verify":
            checks = kaskad_verify_all(dag)
            return json.dumps({
                "action": "verify",
                "checks": [
                    {
                        "name": c.check_name,
                        "passed": c.passed,
                        "message": c.message,
                    }
                    for c in checks
                ],
                "all_pass": kaskad_is_fully_verified(checks),
                "checks_run": len(checks),
            }, ensure_ascii=False, indent=2)

        elif action == "diagnostic":
            diag = dag.self_diagnostic()
            return json.dumps({
                "action": "diagnostic",
                "self_diagnostic": diag,
                "note": "AX23 applied reflexively — the engine validates its own cascade structure.",
            }, ensure_ascii=False, indent=2)

        else:
            return json.dumps({
                "error": f"Unknown action: {action}",
                "valid_actions": [
                    "report", "predict", "propagate", "hubs",
                    "tesanud", "chain", "verify", "diagnostic",
                ],
            }, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 7: framework_summary
# =====================================================================

# Hayat layer component markers and paths (relative to fw_server/data/)
_HAYAT_COMPONENTS = {
    "field_instructions": {
        "file": "dusun-field.instructions.md",
        "markers": ["applyTo: '**'", "shaping_directives", "grade_ceiling"],
        "role": "Always-on field effect — Copilot follows computed shaping directives",
    },
    "dusun_prompt": {
        "file": "dusun.prompt.md",
        "markers": ["/dusun", "shaping_directives", "Universal Epistemic Analysis"],
        "role": "Entry point prompt — invokes dusun() universal substrate",
    },
    "self_check_prompt": {
        "file": "dusun-check.prompt.md",
        "markers": ["/self-check", "Framework Engagement Verification", "shaping_directives"],
        "role": "Manual diagnostic — on-demand spot-check of substrate health",
    },
}


@trace
def _check_hayat_layer() -> Dict[str, Any]:
    """Check the 3 Hayat layer components (AX5 — Integration Prerequisite).

    Returns a dict with status, per-component health, and integration summary.
    Uses filesystem checks on fw_server/data/ (the shipped data bundle).

    v3.0 components: field_instructions, dusun_prompt, self_check_prompt.
    """
    data_dir = Path(__file__).resolve().parent / "data"
    components: Dict[str, Any] = {}
    all_present = True
    always_on = False
    shaping_refs_found = 0

    for comp_id, spec in _HAYAT_COMPONENTS.items():
        filepath = data_dir / spec["file"]
        present = filepath.is_file()
        markers_found: List[str] = []

        if present:
            try:
                content = filepath.read_text(encoding="utf-8")
                markers_found = [m for m in spec["markers"] if m in content]
            except Exception:
                markers_found = []

        # Component-specific health checks
        if comp_id == "field_instructions":
            always_on = "applyTo: '**'" in markers_found
        # Count how many components reference shaping_directives
        if "shaping_directives" in markers_found:
            shaping_refs_found += 1

        healthy = present and len(markers_found) == len(spec["markers"])
        if not healthy:
            all_present = False

        components[comp_id] = {
            "present": present,
            "healthy": healthy,
            "markers_expected": len(spec["markers"]),
            "markers_found": len(markers_found),
            "role": spec["role"],
            "path": str(filepath.relative_to(data_dir.parent.parent))
            if present else None,
        }

    # Overall Hayat status (AX5: ¬Hayat → Inert)
    # Active requires: all files present, field_instructions always-on,
    # and all 3 components reference shaping_directives.
    if all_present and always_on and shaping_refs_found == 3:
        status = "active"
    elif any(c["present"] for c in components.values()):
        status = "degraded"
    else:
        status = "inactive"

    return {
        "status": status,
        "axiom": "AX5 — ¬Hayat → ∀s' ∈ S_Sifat\\{Hayat}: Inert(s')",
        "components": components,
        "integration_health": {
            "all_components_present": all_present,
            "always_on_confirmed": always_on,
            "shaping_refs_found": shaping_refs_found,
            "shaping_refs_expected": 3,
        },
    }


@mcp.tool()
@trace
def get_framework_summary() -> str:
    """Aggregate framework_summary() from all modules and report Hayat layer status.

    Returns metadata about each module (lens info, axiom counts,
    capabilities, max scores) PLUS the Hayat integration layer health
    (AX5 — framework-engagement, self-critique gate, self-check prompt).

    The Hayat layer is the integration prerequisite that activates all
    other attributes into living, coordinated action.  Without it, the
    9 module summaries are inert data (AX5: ¬Hayat → Inert).

    Useful for Stage 5 (Quality Framework) and Stage 9B (Synthesis).
    """
    try:
        summaries: Dict[str, Any] = {}

        # kavram_sozlugu (Lens 1 — Ontoloji, AX17–AX22, KV₁, KV₈)
        try:
            from kavram_sozlugu import framework_summary as kavram_summary
            summaries["kavram_sozlugu"] = kavram_summary()
        except Exception as e:
            summaries["kavram_sozlugu"] = {"error": str(e)}

        # mereoloji (Lens 2 — Mereoloji, AX5, AX29, AX37, KV₆)
        try:
            from mereoloji import framework_summary as mereo_summary
            summaries["mereoloji"] = mereo_summary()
        except Exception as e:
            summaries["mereoloji"] = {"error": str(e)}

        # bileshke
        try:
            from bileshke.quality import framework_summary as bil_summary
            summaries["bileshke"] = bil_summary()
        except Exception as e:
            summaries["bileshke"] = {"error": str(e)}

        # fol_formalizasyon
        try:
            from fol_formalizasyon import framework_summary as fol_summary
            summaries["fol_formalizasyon"] = fol_summary()
        except Exception as e:
            summaries["fol_formalizasyon"] = {"error": str(e)}

        # bayes_analiz
        try:
            from bayes_analiz import framework_summary as bayes_summary
            summaries["bayes_analiz"] = bayes_summary()
        except Exception as e:
            summaries["bayes_analiz"] = {"error": str(e)}

        # oyun_teorisi
        try:
            from oyun_teorisi import framework_summary as oyun_summary
            summaries["oyun_teorisi"] = oyun_summary()
        except Exception as e:
            summaries["oyun_teorisi"] = {"error": str(e)}

        # kategori_teorisi
        try:
            from kategori_teorisi import framework_summary as kat_summary
            summaries["kategori_teorisi"] = kat_summary()
        except Exception as e:
            summaries["kategori_teorisi"] = {"error": str(e)}

        # holografik
        try:
            from holografik import framework_summary as holo_summary
            summaries["holografik"] = holo_summary()
        except Exception as e:
            summaries["holografik"] = {"error": str(e)}

        # kaskad (Generative Cascade Engine — AX23, T7, T8, AX63)
        try:
            from kaskad import framework_summary as kaskad_summary
            summaries["kaskad"] = kaskad_summary()
        except Exception as e:
            summaries["kaskad"] = {"error": str(e)}

        # ── Hayat Layer (AX5 — Integration Prerequisite) ──────────
        hayat = _check_hayat_layer()
        summaries["hayat_layer"] = hayat

        # ── System Integration (AX52 — Multiplicative Gate) ──────
        healthy_modules = sum(
            1 for k, v in summaries.items()
            if k not in ("hayat_layer",) and "error" not in v
        )
        total_modules = sum(
            1 for k in summaries if k not in ("hayat_layer",)
        )
        summaries["system_integration"] = {
            "total_modules": total_modules,
            "healthy_modules": healthy_modules,
            "hayat_active": hayat["status"] == "active",
            "coverage": "6/7 (T17 — Ahfâ permanently unmapped)",
            "max_grade": "İlmelyakîn (AX56)",
            "ax52_gate": (
                "PASS" if healthy_modules == total_modules
                and hayat["status"] == "active"
                else "FAIL — zero dimension detected"
            ),
        }

        # ── SC-3 Tool Guide (claim-type → tool map) ─────────────
        # Makes SC-3 ("Could fw-engine tools have grounded a claim?")
        # actionable by providing a concrete decision matrix.
        summaries["sc3_tool_guide"] = {
            "purpose": (
                "SC-3 decision matrix: when your response makes a claim "
                "of the type below, the listed tool can ground it with "
                "computed evidence instead of unverified assertion."
            ),
            "claim_to_tool": [
                {
                    "claim_type": "structural_pattern",
                    "description": "Claims about system structure, architecture, design patterns",
                    "tool": "validate_stage",
                    "call_example": 'validate_stage(stage="3", summary="<structural claim>")',
                    "grounds": "Runs all 7 lenses + kavaid → PASS/WARN/FAIL verdict",
                },
                {
                    "claim_type": "convergence_or_quality",
                    "description": "Claims about quality, completeness, convergence scores",
                    "tool": "run_bileshke_pipeline",
                    "call_example": "run_bileshke_pipeline(lens_params={...})",
                    "grounds": "Computes composite score in [0,1) with per-lens breakdown",
                },
                {
                    "claim_type": "constraint_satisfaction",
                    "description": "Claims that constraints/kavaid are satisfied or violated",
                    "tool": "check_kavaid",
                    "call_example": "check_kavaid(composite_score=X, latife_bits=[...])",
                    "grounds": "Evaluates all 8 kavaid with structural auto-checks",
                },
                {
                    "claim_type": "axiom_or_formal_reference",
                    "description": "Claims referencing specific axioms (AX1-AX66) or theorems (T1-T18)",
                    "tool": "run_single_lens",
                    "call_example": 'run_single_lens(lens_id="fol", params={"text": "<claim text>"})',
                    "grounds": "Extracts axiom references and checks formal consistency",
                },
                {
                    "claim_type": "inference_chain",
                    "description": "Claims about causal chains, cascades, or inference paths (C1-C7)",
                    "tool": "verify_chains",
                    "call_example": 'verify_chains(chain_id="C1", text="<claim text>")',
                    "grounds": "Traces chain through FOL axiom references",
                },
                {
                    "claim_type": "part_whole_or_holographic",
                    "description": "Claims about holographic structure, part-whole, fractal self-similarity",
                    "tool": "run_single_lens",
                    "call_example": 'run_single_lens(lens_id="holografik", params={})',
                    "grounds": "Checks B01-B22 holographic dimensions and KV₆ seed omnipresence",
                },
                {
                    "claim_type": "epistemic_grade",
                    "description": "Claims about certainty level (Tasavvur/Tasdik/İlmelyakîn)",
                    "tool": "run_bileshke_pipeline",
                    "call_example": "run_bileshke_pipeline(lens_params={...})",
                    "grounds": "Grade is computed from composite score via grade_map (AX56 ceiling enforced)",
                },
                {
                    "claim_type": "system_health",
                    "description": "Claims about module availability, framework status, coverage",
                    "tool": "get_framework_summary",
                    "call_example": "get_framework_summary()",
                    "grounds": "Aggregates all 9 module summaries + Hayat layer + AX52 gate",
                },
            ],
            "rule": (
                "If your response makes a claim matching any type above "
                "and you did NOT call the corresponding tool, SC-3 FAILS. "
                "Either call the tool to ground the claim, or explicitly "
                "note the claim is ungrounded (epistemic hedging per AX57)."
            ),
        }

        return json.dumps(summaries, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 8: hcp_ingest
# =====================================================================

@mcp.tool()
@trace
def hcp_ingest(
    text: str,
    source_metadata: Optional[dict] = None,
    append: bool = True,
) -> str:
    """Ingest context text into the Holographic Context Protocol.

    The HCP seed is computed once and broadcast to every chunk (AX37, KV₆).
    This is classified as AX27 — the protocol is a transparent vessel that
    persists across calls within a session.

    Args:
        text: Raw context text to chunk and process.
        source_metadata: Optional dict with ingestion hints (JSON string also accepted).
            Accepted keys:
              tool_name (str) — originating tool/plugin name.
              content_type_hint (str) — expected ContentType, e.g. "theological".
              hint_confidence (float) — confidence in the hint [0-1].
              tags (object) — arbitrary string key-value pairs.
        append: If True (default), new chunks are ADDED to existing
            context.  If False, all existing chunks are replaced.

    Returns:
        JSON with seed diagnostics and chunk count.
    """
    try:
        protocol = _get_hcp()

        meta = None
        if source_metadata is not None:
            try:
                meta_dict = _coerce(source_metadata, dict)
                if not isinstance(meta_dict, dict):
                    return json.dumps({
                        "error": "source_metadata must be a JSON object {...}, not a plain string or array.",
                    }, ensure_ascii=False, indent=2)
                meta = SourceMetadata.from_dict(meta_dict)
            except (json.JSONDecodeError, TypeError, KeyError) as exc:
                return json.dumps({
                    "error": f"Invalid source_metadata JSON: {exc}",
                }, ensure_ascii=False, indent=2)

        seed = protocol.ingest(text, source_metadata=meta, append=append)

        return json.dumps({
            "action": "ingest",
            "n_chunks": seed.n_chunks,
            "coverage_ratio": round(seed.coverage_ratio, 3),
            "n_global_keywords": len(seed.global_keywords),
            "has_structure": seed.has_structure,
            "diagnostics": protocol.diagnostics(),
        }, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 9: hcp_query
# =====================================================================

@mcp.tool()
@trace
def hcp_query(
    question: str,
    keywords: Optional[list] = None,
    top_k: int = 5,
) -> str:
    """Query the HCP for relevant context chunks.

    Requires prior hcp_ingest call. Uses holographic attention
    (seed-modulated) to rank chunks by relevance.

    Args:
        question: Query text.
        keywords: List of keyword strings (optional; JSON string also accepted; auto-extracted if omitted).
        top_k: Number of top results to return (default: 5).

    Returns:
        JSON list of {content, position, attention_weight, local_importance}.
    """
    try:
        protocol = _get_hcp()
        if not protocol.is_ready:
            return json.dumps({
                "error": "HCP not ready — call hcp_ingest first.",
            }, ensure_ascii=False, indent=2)

        kw_tuple = None
        if keywords:
            kw_list = _coerce(keywords, list, [])
            kw_tuple = tuple(kw_list)

        results = protocol.query(question, keywords=kw_tuple, top_k=top_k)

        output = []
        for sc, weight in results:
            output.append({
                "content": sc.chunk.content[:500],  # truncate for MCP
                "position": sc.chunk.position,
                "attention_weight": round(weight, 4),
                "local_importance": round(sc.local_importance, 4),
                "keyword_overlap": list(sc.keyword_overlap),
            })

        # ── SC-3 cognitive layer: scan returned chunks for claim
        #    patterns and suggest grounding tools (Step 6) ──────
        chunk_texts = [item["content"] for item in output]
        sc3_suggestions = _suggest_tools_for_content(
            [question] + chunk_texts
        )

        response: Dict[str, Any] = {
            "action": "query",
            "question": question,
            "n_results": len(output),
            "results": output,
        }
        if sc3_suggestions:
            response["sc3_tool_suggestions"] = sc3_suggestions
            response["sc3_note"] = (
                "The returned content contains claim patterns that can be "
                "grounded via the tools above. Per SC-3, either call the "
                "suggested tool or hedge the claim as ungrounded (AX57)."
            )

        return json.dumps(response, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 10: hcp_diagnostics
# =====================================================================

@mcp.tool()
@trace
def hcp_diagnostics() -> str:
    """Return HCP diagnostic state (AX57 — transparency).

    Returns:
        JSON with status, chunk count, type distribution,
        coverage ratio, structural link info, persistence status,
        and session ledger persistence status.
    """
    try:
        protocol = _get_hcp()
        diag = protocol.diagnostics()
        diag["persistence"] = get_persistence_info(_get_state_dir())
        diag["session_persistence"] = get_session_persistence_info(_get_state_dir())
        # ── Session ledger summary (AX57) ──
        ledger = get_ledger()
        diag["session_summary"] = {
            "call_count": ledger.call_count,
            "grade_ceiling": ledger.get_grade_ceiling(),
            "last_gate": ledger.get_last_gate(),
            "anomaly_count": len(ledger.get_anomaly_history()),
        }
        # ── Tracing diagnostics (AX57 — transparency) ──
        from fw_server.tracer import Tracer
        tracer_inst = Tracer.get()
        diag["tracing"] = {
            "active": tracer_inst is not None,
            "FW_TRACE_DIR_env": os.environ.get("FW_TRACE_DIR", "<NOT SET>"),
            "session_id": tracer_inst._config.session_id if tracer_inst else None,
            "trace_dir": str(tracer_inst._config.trace_dir) if tracer_inst else None,
        }
        return json.dumps(
            diag,
            ensure_ascii=False,
            indent=2,
        )
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 11: hcp_create_workflow
# =====================================================================

@mcp.tool()
@trace
def hcp_create_workflow(
    stages: list,
    workflow_id: Optional[str] = None,
) -> str:
    """Create a multi-step workflow chain (E2 — typed contracts).

    Each stage defines a typed contract with expected input/output
    and an optional prompt template.  The workflow advances one stage
    at a time via hcp_advance_workflow.

    Args:
        stages: List of stage dicts (JSON string also accepted).  Each dict must have
                a "name" key; optional keys: "expected_input",
                "expected_output", "prompt_template", "metadata".
        workflow_id: Optional custom workflow ID (auto-generated if omitted).

    Returns:
        JSON with action, workflow_id, n_stages, and current_stage.
    """
    try:
        protocol = _get_hcp()
        stage_dicts = _coerce(stages, list)
        if not isinstance(stage_dicts, list) or len(stage_dicts) == 0:
            return json.dumps({
                "error": "stages must be a non-empty JSON array."
            }, ensure_ascii=False, indent=2)

        stage_objects: List[WorkflowStage] = []
        for sd in stage_dicts:
            stage_objects.append(WorkflowStage.from_dict(sd))

        state = protocol.create_workflow(stage_objects, workflow_id=workflow_id)

        return json.dumps({
            "action": "create_workflow",
            "workflow_id": state.workflow_id,
            "n_stages": len(state.stages),
            "current_stage": state.current_stage.name if state.current_stage else None,
        }, ensure_ascii=False, indent=2)

    except (json.JSONDecodeError, TypeError, KeyError) as e:
        return json.dumps({
            "error": f"Invalid stages JSON: {e}",
        }, ensure_ascii=False, indent=2)
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 12: hcp_advance_workflow
# =====================================================================

@mcp.tool()
@trace
def hcp_advance_workflow(
    workflow_id: str,
    output: str,
    success: bool = True,
    error: Optional[str] = None,
) -> str:
    """Advance a workflow by one stage (E2 — step execution).

    Records the result for the current stage and advances the index.

    Args:
        workflow_id: The workflow ID returned by hcp_create_workflow.
        output: The output produced for the current stage.
        success: Whether the stage succeeded (default: True).
        error: Optional error message if success is False.

    Returns:
        JSON with action, workflow_id, completed_stage, next_stage,
        and is_complete flag.
    """
    try:
        protocol = _get_hcp()
        state = protocol.get_workflow(workflow_id)

        current_name = state.current_stage.name if state.current_stage else None
        protocol.advance_workflow(workflow_id, output, success=success, error=error)

        next_stage = state.current_stage
        return json.dumps({
            "action": "advance_workflow",
            "workflow_id": workflow_id,
            "completed_stage": current_name,
            "next_stage": next_stage.name if next_stage else None,
            "is_complete": state.is_complete,
            "n_results": len(state.results),
        }, ensure_ascii=False, indent=2)

    except KeyError:
        return json.dumps({
            "error": f"Workflow '{workflow_id}' not found.",
        }, ensure_ascii=False, indent=2)
    except RuntimeError as e:
        return json.dumps({
            "error": str(e),
        }, ensure_ascii=False, indent=2)
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 13: hcp_export_state
# =====================================================================

@mcp.tool()
@trace
def hcp_export_state() -> str:
    """Export the full HCP state as a JSON checkpoint (E3).

    Captures all ingested chunks, computed seed, seeded chunks,
    workflows, and config.  The exported JSON can be restored later
    via hcp_import_state.

    Per T14 (Ne Ayn Ne Gayr): the snapshot participates in the
    protocol's truth without being identical to it.

    Returns:
        JSON string containing the full HCP state with schema_version.
    """
    try:
        protocol = _get_hcp()
        state = protocol.export_state()
        return json.dumps(state, ensure_ascii=False, indent=2)
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 14: hcp_import_state
# =====================================================================

@mcp.tool()
@trace
def hcp_import_state(state_json: Any) -> str:
    """Import a previously exported HCP state checkpoint (E3).

    Restores the protocol to the exact state captured by
    hcp_export_state.  Overwrites any current state.

    Args:
        state_json: Dict from a previous hcp_export_state call (JSON string also accepted).

    Returns:
        JSON confirmation with action, schema_version, n_chunks, and is_ready.
    """
    try:
        protocol = _get_hcp()
        state = _coerce(state_json, dict)
        protocol.import_state(state)
        return json.dumps({
            "action": "import_state",
            "schema_version": state.get("schema_version"),
            "n_chunks": len(protocol._chunks),
            "is_ready": protocol.is_ready,
        }, ensure_ascii=False, indent=2)
    except json.JSONDecodeError as e:
        return json.dumps({
            "error": f"Invalid JSON: {e}",
        }, ensure_ascii=False, indent=2)
    except ValueError as e:
        return json.dumps({
            "error": str(e),
        }, ensure_ascii=False, indent=2)
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


@mcp.tool()
@trace
def hcp_sync_memory_bank(session_note: str = "") -> str:
    """Sync HCP context to memory-bank/ markdown files.

    Exports the current HCP state (chunks, seed, workflows) to
    human-readable markdown files in the workspace's memory-bank/
    directory.  Files written: activeContext.md, techContext.md,
    sessionLog.md, progress.md.

    Called automatically on shutdown, but can also be invoked
    manually to snapshot the current state mid-session.

    Args:
        session_note: Optional note to append to the session log entry.

    Returns:
        JSON with files_written, timestamp, and n_chunks.
    """
    try:
        state_dir = _get_state_dir()
        if state_dir is None:
            return json.dumps({
                "error": "FW_STATE_DIR not set — persistence disabled. "
                         "Add FW_STATE_DIR to .vscode/mcp.json env block.",
            }, ensure_ascii=False, indent=2)

        protocol = _get_hcp()
        result = sync_memory_bank(
            state_dir, protocol,
            session_note=session_note,
            ledger=get_ledger(),
        )
        return json.dumps(result, ensure_ascii=False, indent=2)
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 16: calibrate_source_texts — Ground-truth calibration (BS-21/22)
# =====================================================================

@mcp.tool()
@trace
def calibrate_source_texts() -> str:
    """Run the ground-truth calibration pipeline on kaynak_metinler/ texts.

    Addresses BS-21 (Empty kaynak_metinler) and BS-22 (No ground truth).
    Loads curated source texts, runs each through the dusun() pipeline,
    and compares results against expected ranges.

    Returns JSON with:
      - per-text calibration (lens scores, composite, path, grade, patterns)
      - aggregate accuracy metrics (lens, grade, pattern, composite, path)
      - recommended parameter adjustments

    Constraints:
      AX64: Source texts are read-only (immutability verified).
      AX65: Every text paired with axiom references.
      KV₄:  0 < composite < 1.
    """
    try:
        from fw_server.calibration import calibrate_pipeline
        result = calibrate_pipeline()
        return json.dumps(result.to_dict(), ensure_ascii=False, indent=2)
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# ── Hint parameter mapping ──────────────────────────────────────────

_HINT_PARAM_TO_LENS: Dict[str, str] = {
    "bayes_hints": "Bayes",
    "mereology_hints": "Mereoloji",
    "fol_hints": "FOL",
    "oyun_hints": "OyunTeorisi",
    "kategori_hints": "KategoriTeorisi",
    "holografik_hints": "Topoloji + Holografik",
    "ontoloji_hints": "Ontoloji",
}


def _collect_hints(**hint_kwargs: Any) -> Dict[str, dict]:
    """Collect non-None hint parameters into a lens_id → hints mapping."""
    hints: Dict[str, dict] = {}
    for param_name, lens_id in _HINT_PARAM_TO_LENS.items():
        value = hint_kwargs.get(param_name)
        if value is not None:
            coerced = _coerce(value, dict)
            if coerced:
                hints[lens_id] = coerced
    return hints


# =====================================================================
# Tool 17: dusun — Universal Neural Substrate (AX5)
# =====================================================================

@mcp.tool(name="dusun")
@trace
async def dusun_tool(
    text: str,
    ctx: Optional[Context] = None,
    context: Optional[str] = None,
    mode: str = "analyze",
    bayes_hints: Optional[dict] = None,
    mereology_hints: Optional[dict] = None,
    fol_hints: Optional[dict] = None,
    oyun_hints: Optional[dict] = None,
    kategori_hints: Optional[dict] = None,
    holografik_hints: Optional[dict] = None,
    ontoloji_hints: Optional[dict] = None,
) -> str:
    """Universal framework analysis — fires ALL relevant lenses automatically.

    This is the T1 (Boot) neuron and the PRIMARY entry point for every
    P1–P4 interaction.  You MUST call dusun() BEFORE generating any
    substantive response to ensure structural analysis is active.

    The name 'düşün' means 'think' in Turkish.

    Algorithm:
      1. Detect framework-relevant vocabulary in input
      2. Auto-classify to P0/P1/P2/P3/P4 (dusun-router.instructions.md §1.1)
      3. Build fire plan — which lenses to run based on path + vocabulary
      4. Execute fire plan — run lenses + bileshke + kavaid
      5. Detect applicable structural patterns (DUSUN.md §8.3)
      6. Produce translation hints for non-P4 content (§8.2)
      7. Compile AX57 transparency disclosure

    Args:
        text: The user's input text to analyze.
        context: Optional additional context (code, prior conversation).
        mode: "analyze" (default) or "validate" (future — stub).
        bayes_hints: Optional Bayesian structure extracted from the text.
            Provide when the text discusses probability, prior beliefs,
            hypothesis testing, or evidence-based reasoning.
            Schema: {"hypotheses": [{"name": str, "prior": float}],
                     "evidence": [{"name": str, "likelihood": float,
                                   "likelihood_complement": float}]}
        mereology_hints: Optional part-whole structure from the text.
            Provide when the text discusses parts, wholes, composition,
            subsystems, or hierarchical structure.
            Schema: {"whole": str, "parts": [{"name": str, "telos": str}],
                     "relations": [{"whole": str, "part": str}]}
        fol_hints: Optional formal logic structure from the text.
            Provide when the text contains logical arguments, axiom
            references (AX/T/KV patterns), premises, or conclusions.
            Schema: {"axiom_refs": [str], "premises": [str],
                     "conclusion": str}
        oyun_hints: Optional game-theoretic structure from the text.
            Provide when the text discusses strategies, players,
            payoffs, cooperation/defection, or nefs-makam dynamics.
            Schema: {"players": [{"name": str, "makam": str}],
                     "strategies": [{"player": str, "name": str,
                       "type": "worldly"|"alignment"|"mixed"}],
                     "payoffs": [{"player": str, "my_strategy": str,
                       "opponent_strategy": str, "payoff": float}]}
        kategori_hints: Optional categorical structure from the text.
            Provide when the text discusses morphisms, functors,
            categories, natural transformations, or commutativity.
            Schema: {"objects": [str], "morphisms": [{"name": str,
                     "source": str, "target": str}],
                     "functor_kind": str}
        holografik_hints: Optional holographic structure from the text.
            Provide when the text discusses fractal patterns,
            self-similarity, part-whole isomorphisms, seed vectors,
            Besmele dimensions, or holographic encoding.
            Schema: {"dimensions_active": [int],
                     "fidelity_pairs": [{"source": str,
                       "derivative": str, "fidelity": float}],
                     "seed_descriptions": [{"name": str, "level": str}]}
        ontoloji_hints: Optional ontological structure from the text.
            Provide when the text discusses Divine Names (Esma),
            concepts (kavramlar), dual classifications (harfi/ismi),
            or hakikat/mahiyet distinctions.
            Schema: {"concepts": [{"name": str,
                     "names_referenced": [str],
                     "dual_class": "harfi"|"ismi",
                     "is_living": bool}]}

    Returns:
        JSON with path, composite_score, grade, lens_scores, kavaid,
        vocabulary_detected, fire_plan, patterns_applied,
        translation_hints, anomalies, ax57_compact, and boot_seed.
        The ``_framework_context`` field contains enforcement directives
        you MUST follow in your response.

    Enforcement (MANDATORY — read before generating your response):
        - You MUST call dusun() before generating ANY P1–P4 response.
        - The returned ``grade`` is the CEILING — you MUST NOT claim a
          higher grade in your AX57 disclosure. Grade inflation is a
          framework violation.
        - If ``composite_score`` >= 0.95, this is an ERROR per KV₄/T6.
          You MUST flag it as error, NEVER present it as success.
        - You MUST use the ``patterns_applied`` list in your response —
          do not invent different patterns.
        - The ``_framework_context.directives`` list contains specific
          constraints. You MUST honour every directive listed there.
        - If this tool is unavailable, your maximum grade drops to
          Tasavvur and all structural claims MUST be marked [UNGROUNDED].
    """
    try:
        from math import prod as _prod

        from mcp_server.engine.lens_runner import run_lens_analysis
        from mcp_server.engine.synthesizer import run_synthesis
        from mcp_server.models import LensName, LENS_TO_TURKISH
        from mcp.types import SamplingMessage, TextContent
        from fw_server.dusun import (
            _detect_vocabulary,
            _auto_classify,
            _build_fire_plan,
            _detect_patterns,
            _build_translation_hints,
            _build_ax57,
            _build_shaping_directives,
            _build_dynamic_boot_seed,
        )

        _TURKISH_TO_LENS = {v: k for k, v in LENS_TO_TURKISH.items()}

        combined = f"{text}\n{context}" if context else text

        # ── Step 1-3: Routing (vocabulary → path → fire plan) ────────
        vocab = _detect_vocabulary(combined)
        path = _auto_classify(combined, vocab)
        fire_plan = _build_fire_plan(path, vocab)

        # ── Fallback: no MCP context → old regex engine ──────────────
        if ctx is None:
            from fw_server.dusun import dusun as _dusun
            result = _dusun(text=text, context=context, mode=mode)
            result["engine"] = "fw_server (regex fallback — no MCP context)"
        else:
            # ── Sampling function from MCP host ──────────────────────
            async def _host_sample(sys_prompt: str, usr_prompt: str) -> str:
                res = await ctx.session.create_message(
                    messages=[SamplingMessage(
                        role="user",
                        content=TextContent(type="text", text=usr_prompt),
                    )],
                    system_prompt=sys_prompt,
                    max_tokens=4096,
                    include_context="thisServer",
                )
                return res.content.text if hasattr(res.content, "text") else str(res.content)

            # ── Auto-extract hints via sampling (if no explicit hints) ──
            extracted_hints: Dict[str, dict] = {}
            try:
                from fw_server.sampling_extractor import extract_structured_hints

                class _SampleCtx:
                    """Adapter: exposes .sample() for sampling_extractor."""
                    async def sample(self, prompt: str) -> str:
                        return await _host_sample(
                            "Extract structured analysis from the given text.",
                            prompt,
                        )

                extracted_hints = await extract_structured_hints(
                    _SampleCtx(), combined, fire_plan,
                )
            except Exception:
                pass  # KV₃: extraction failure degrades to no hints

            # ── Step 4: Run each lens via new LLM-powered engine ─────
            text_id = "user_input"
            analyses = []
            lens_scores: Dict[str, float] = {}
            lens_errors: Dict[str, str] = {}
            anomalies: List[str] = []

            total = len(fire_plan)
            for idx, turkish_id in enumerate(fire_plan):
                lens = _TURKISH_TO_LENS.get(turkish_id)
                if lens is None:
                    continue
                if ctx is not None:
                    await ctx.report_progress(idx, total)
                try:
                    analysis = await run_lens_analysis(
                        _host_sample, combined, text_id, lens,
                    )
                    analyses.append(analysis)
                    avg_conf = (
                        sum(f.confidence for f in analysis.findings)
                        / max(len(analysis.findings), 1)
                    )
                    lens_scores[turkish_id] = round(avg_conf, 4)
                except Exception as e:
                    lens_errors[turkish_id] = str(e)
                    lens_scores[turkish_id] = 0.0
                    anomalies.append(f"LENS-ERROR: {turkish_id} — {e}")

            # Final progress notification
            if ctx is not None:
                await ctx.report_progress(total, total)

            # Composite: geometric mean (AX52 multiplicative gate)
            pos_scores = [s for s in lens_scores.values() if s > 0]
            if pos_scores:
                composite = round(
                    _prod(pos_scores) ** (1.0 / len(pos_scores)), 4,
                )
                composite = min(composite, 0.9499)  # KV₄/T6
            else:
                composite = 0.0

            grade_str = grade_to_string(score_to_grade(composite))

            # ── Kavaid (structural constraint checks) ────────────────
            kavaid_checks = {
                "KV1": True,                                   # harfî default
                "KV2": True,                                   # privation ontology
                "KV3": True,                                   # observer = şart-ı âdî
                "KV4": composite < 0.95,                       # map ≠ territory
                "KV5": len(analyses) > 0,                      # faithful map
                "KV6": any(a.lens == LensName.TOPOLOGICAL for a in analyses),
                "KV7": True,                                   # independent lenses
                "KV8": any(a.lens == LensName.ONTOLOGICAL for a in analyses),
            }
            kavaid_pass_count = sum(1 for v in kavaid_checks.values() if v)

            if not kavaid_checks["KV4"]:
                anomalies.append(
                    f"KV4-BREACH: composite={composite:.4f} — exceeds 0.95"
                )

            # ── Step 5+: Downstream framework enforcement ────────────
            patterns = _detect_patterns(path, vocab, lens_scores)
            trans_hints = _build_translation_hints(path, patterns)
            kavaid_passed = [k for k, v in kavaid_checks.items() if v]
            ax57 = _build_ax57(
                path=path, patterns=patterns, kavaid_ids=kavaid_passed,
                grade=grade_str, composite=composite,
            )
            boot_seed = _build_dynamic_boot_seed(
                path=path, patterns=patterns, anomalies=anomalies,
            )

            # Shaping directives
            sentinel_signal = None
            if path != "P0":
                try:
                    from sentinel.engine import get_sentinel as _get_sentinel
                    _sentinel = _get_sentinel()
                    _ledger_ref = get_ledger()
                    sentinel_signal = _sentinel.proactive_scan(
                        ai_response=ax57,
                        session_ledger=_ledger_ref,
                        turn_number=_ledger_ref.call_count,
                    )
                except Exception:
                    pass

            shaping = _build_shaping_directives(
                path=path, grade=grade_str, composite_score=composite,
                patterns=patterns, anomalies=anomalies,
                kavaid={"checks": kavaid_checks},
                kavaid_pass_count=kavaid_pass_count,
                cross_validation={},
                sentinel_signal=(
                    sentinel_signal.to_dict() if sentinel_signal else None
                ),
                lens_scores=lens_scores,
            )

            # ── Synthesis across lenses ──────────────────────────────
            synthesis_data = None
            if len(analyses) >= 2:
                try:
                    synthesis = await run_synthesis(
                        _host_sample, text_id, analyses,
                    )
                    synthesis_data = {
                        "overall_interpretation": synthesis.overall_interpretation,
                        "connections": [
                            {
                                "from": c.from_lens.value,
                                "to": c.to_lens.value,
                                "relationship": c.relationship,
                                "strength": c.strength,
                            }
                            for c in synthesis.connections
                        ],
                        "meta_score": synthesis.meta_score,
                    }
                except Exception:
                    pass  # KV₃: synthesis failure doesn't block result

            # ── HCP ingestion (best-effort) ──────────────────────────
            hcp_ingested = False
            if path != "P0":
                try:
                    from hcp.types import SourceMetadata
                    protocol = _get_hcp()
                    ingest_text = (
                        f"[DUSUN path={path}] composite={composite:.4f} "
                        f"kavaid={kavaid_pass_count}/8 grade={grade_str} "
                        f"patterns={','.join(patterns[:3])}"
                    )
                    if text and len(text) <= 500:
                        ingest_text += f" | input: {text}"
                    protocol.ingest(ingest_text, source_metadata=SourceMetadata(
                        tool_name="dusun",
                        content_type_hint="analysis_result",
                    ))
                    hcp_ingested = True
                except Exception:
                    pass

            # ── Build result (compatible format) ─────────────────────
            result = {
                "path": path,
                "mode": mode,
                "composite_score": composite,
                "grade": grade_str,
                "lens_scores": lens_scores,
                "lens_analyses": {
                    a.lens.value: {
                        "summary": a.summary,
                        "findings": [
                            {
                                "claim": f.claim,
                                "evidence": f.evidence,
                                "confidence": f.confidence,
                                "formal_notation": f.formal_notation,
                            }
                            for f in a.findings
                        ],
                        "vocabulary_used": a.vocabulary_used,
                        "raw_prose": a.raw_prose,
                    }
                    for a in analyses
                },
                "synthesis": synthesis_data,
                "kavaid": {"checks": kavaid_checks},
                "kavaid_pass_count": kavaid_pass_count,
                "vocabulary_detected": vocab,
                "fire_plan": fire_plan,
                "patterns_applied": patterns,
                "translation_hints": trans_hints,
                "anomalies": anomalies,
                "ax57_compact": ax57,
                "boot_seed": boot_seed,
                "shaping_directives": shaping,
                "hcp_ingested": hcp_ingested,
                "sentinel_signal": (
                    sentinel_signal.to_dict() if sentinel_signal else None
                ),
                "engine": "mcp_server (LLM-powered via sampling)",
            }

            if lens_errors:
                result["lens_errors"] = lens_errors

        # ── Session ledger recording (Channel 3 — fire-and-forget) ───
        try:
            get_ledger().record_call(
                "dusun",
                grade=result.get("grade"),
                composite_score=result.get("composite_score"),
                kavaid_pass_count=result.get("kavaid_pass_count"),
                anomalies=result.get("anomalies", []),
            )
        except Exception:
            pass  # KV₃: ledger failure never propagates

        # ── Channel 2: enriched tool return ──────────────────────────
        try:
            result["_framework_context"] = build_framework_context(
                "dusun",
                grade=result.get("grade"),
                composite_score=result.get("composite_score"),
                kavaid_pass_count=result.get("kavaid_pass_count"),
                anomalies=result.get("anomalies", []),
                session_context=get_ledger().get_cumulative_context(),
                cascade_status=get_ledger().get_cascade_status(),
                cascade_forward=get_ledger().get_cascade_forward(),
            )
        except Exception:
            pass  # KV₃: context enrichment failure never propagates

        return json.dumps(result, ensure_ascii=False, indent=2)
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 18: sentinel_scan — Scan text for epistemic failure signals
# =====================================================================

@mcp.tool()
@trace
def sentinel_scan(
    text: str = "",
) -> str:
    """Scan *text* for epistemic failure signals across all three detection arms.

    Arms:
      - Reactive: regex patterns matching known failure phrases
      - Proactive: session-aware heuristics (hedging density, sycophancy, etc.)
      - Structural: generalised pattern rules derived from confirmed incidents

    Returns a list of SentinelSignal dicts (may be empty if no signals detected).
    Fire-and-forget — sentinel failure never propagates (KV₃).
    """
    try:
        from sentinel.engine import get_sentinel
        from fw_server.types import SentinelResponse

        sentinel = get_sentinel()
        ledger = get_ledger()
        signal = sentinel.scan(
            user_msg=text,
            ai_response="",
            session_ledger=ledger,
            turn_number=ledger.call_count,
        )
        resp = SentinelResponse(
            action="scan",
            signal=signal.to_dict() if signal else None,
            stats=sentinel.get_stats(),
        )
        return json.dumps(resp.to_dict(), ensure_ascii=False, indent=2)
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 19: sentinel_report — Report a confirmed epistemic incident
# =====================================================================

@mcp.tool()
@trace
def sentinel_report(
    taxonomy_id: str = "",
    description: str = "",
    context_snippet: str = "",
) -> str:
    """Report a confirmed epistemic failure incident.

    *taxonomy_id*: The failure taxonomy ID (e.g. "CD-1", "FM-3").
                   If empty, defaults to "FM-1" (general failure).
    *description*: User's description of what went wrong.
    *context_snippet*: Optional snippet of the problematic output.

    The incident is stored in the cross-session registry, used for
    pattern generalisation, and fed to the co-articulation engine
    for vocabulary evolution.

    Returns the created Incident with its ID.
    """
    try:
        from sentinel.engine import get_sentinel

        sentinel = get_sentinel()
        tid = taxonomy_id.strip() or "FM-1"
        incident = sentinel.report_incident(
            taxonomy_id=tid,
            user_description=description,
            context_snippet=context_snippet,
        )
        from fw_server.types import SentinelResponse
        resp = SentinelResponse(
            action="report",
            incident=incident,
            stats=sentinel.get_stats(),
        )
        return json.dumps(resp.to_dict(), ensure_ascii=False, indent=2)
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 20: sentinel_status — Return sentinel diagnostics and stats
# =====================================================================

@mcp.tool()
@trace
def sentinel_status() -> str:
    """Return Epistemic Sentinel diagnostics: incident count, pattern rules,
    co-articulation vocabulary size, picker readiness, and persistence info.

    No side-effects — purely diagnostic.
    """
    try:
        from sentinel.engine import get_sentinel

        sentinel = get_sentinel()
        stats = sentinel.get_stats()

        # Add persistence info
        state_dir = _get_state_dir()
        stats["persistence"] = get_sentinel_persistence_info(state_dir)

        from fw_server.types import SentinelResponse
        resp = SentinelResponse(
            action="status",
            stats=stats,
        )
        return json.dumps(resp.to_dict(), ensure_ascii=False, indent=2)
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# RESOURCES
# =====================================================================

@mcp.resource("fw://kavaid")
def resource_kavaid_list() -> str:
    """List all 8 kavaid constraints with their definitions."""
    from fw_server.types import KAVAID_DEFINITIONS
    return json.dumps(KAVAID_DEFINITIONS, ensure_ascii=False, indent=2)


@mcp.resource("fw://kavaid/{kv_id}")
def resource_kavaid(kv_id: str) -> str:
    """Get a specific kavaid constraint definition and current status."""
    from fw_server.types import KAVAID_DEFINITIONS
    kv_id_upper = kv_id.upper()
    if kv_id_upper not in KAVAID_DEFINITIONS:
        return json.dumps({"error": f"Unknown kavaid: {kv_id}"})
    return json.dumps({
        "id": kv_id_upper,
        **KAVAID_DEFINITIONS[kv_id_upper],
    }, ensure_ascii=False, indent=2)


@mcp.resource("fw://boot_seed")
def resource_boot_seed() -> str:
    """Return the current dynamic boot seed (holographic compression)."""
    from fw_server.dusun import _build_dynamic_boot_seed
    return _build_dynamic_boot_seed(path="P0", patterns=[], anomalies=[])


@mcp.resource("fw://session")
def resource_session() -> str:
    """Return current session ledger state (grade ceiling, call count, anomalies)."""
    try:
        ledger = get_ledger()
        return json.dumps(ledger.get_cumulative_context(), ensure_ascii=False, indent=2)
    except Exception:
        return json.dumps({"status": "no active session"})


@mcp.resource("fw://lenses")
def resource_lens_list() -> str:
    """List all 7 analytical lenses with their IDs."""
    from fw_server.types import LENS_IDS
    return json.dumps({"lenses": list(LENS_IDS)}, indent=2)


# =====================================================================
# PROMPT WORKFLOWS
# =====================================================================

@mcp.prompt()
def dusun_analysis(text: str) -> str:
    """Guide a complete dusun analysis workflow.

    Orchestrates: dusun() → read directives → structured response.
    """
    return (
        "# Dusun Analysis Workflow\n\n"
        "Execute in order:\n\n"
        f'1. Call `dusun(text="{text[:100]}...")`\n'
        "2. Read the `shaping_directives` from the result\n"
        "3. Follow `path_guidance` to structure your response\n"
        "4. Apply each `pattern_directives` entry\n"
        "5. Address any `anomaly_directives`\n"
        "6. Close with `[Grade: X | Path: PY | Composite: Z]`\n"
    )


@mcp.prompt()
def kavaid_audit() -> str:
    """Guide a kavaid constraint audit workflow."""
    return (
        "# Kavaid Audit Workflow\n\n"
        "1. Read `fw://kavaid` to see all 8 constraints\n"
        "2. Call `check_kavaid(params={})` with your current analysis\n"
        "3. For each FAIL, call `validate_stage(...)` to diagnose\n"
        "4. Report pass/fail summary with remediation for failures\n"
    )


@mcp.prompt()
def sentinel_workflow(text: str) -> str:
    """Guide an epistemic sentinel scan workflow."""
    return (
        "# Sentinel Scan Workflow\n\n"
        f'1. Call `sentinel_scan(text="{text[:100]}...")`\n'
        "2. Review detected patterns across all three arms\n"
        "3. Call `sentinel_report()` for formatted analysis\n"
        "4. Call `sentinel_status()` for diagnostic stats\n"
        "5. If incidents found, address each in your response\n"
    )


# =====================================================================
# Entry point
# =====================================================================

_MCP_CONFIG = {
    "servers": {
        "fw-engine": {
            "command": "fw-engine",
            "type": "stdio",
            "env": {
                "FW_STATE_DIR": "${workspaceFolder}/.hcp_state"
            }
        }
    }
}

# Framework files deployed by --init.
# Keys = destination relative to cwd, values = filename inside fw_server/data/
_FRAMEWORK_FILES = {
    ".github/prompts/dusun.prompt.md": "dusun.prompt.md",
    ".github/prompts/dusun-check.prompt.md": "dusun-check.prompt.md",
    ".github/instructions/dusun-field.instructions.md": "dusun-field.instructions.md",
    "DUSUN.md": "DUSUN.md",
}

# Legacy → current path mapping for upgrade migration.
# When upgrading from ≤v2.5.x, old-named files must be replaced.
_LEGACY_RENAMES: dict[str, str] = {
    ".github/prompts/fw.prompt.md": ".github/prompts/dusun.prompt.md",
    ".github/prompts/fw-tools.prompt.md": ".github/prompts/dusun.prompt.md",
    ".github/prompts/fw-check.prompt.md": ".github/prompts/dusun-check.prompt.md",
    ".github/prompts/dusun-tools.prompt.md": ".github/prompts/dusun.prompt.md",
    ".github/instructions/fw-router.instructions.md": ".github/instructions/dusun-field.instructions.md",
    ".github/instructions/fw-engagement.instructions.md": ".github/instructions/dusun-field.instructions.md",
    ".github/instructions/dusun-router.instructions.md": ".github/instructions/dusun-field.instructions.md",
    ".github/instructions/dusun-engagement.instructions.md": ".github/instructions/dusun-field.instructions.md",
}


def _migrate_legacy_files(cwd: Path) -> list[str]:
    """Rename old fw-* framework files to dusun-* equivalents.

    If an old file exists and the new destination does NOT exist, rename it.
    If BOTH exist, delete the old one (user already has the new version).
    Returns a list of human-readable migration actions taken.
    """
    actions: list[str] = []
    for old_rel, new_rel in _LEGACY_RENAMES.items():
        old_path = cwd / old_rel
        new_path = cwd / new_rel
        if not old_path.exists():
            continue
        try:
            if new_path.exists():
                # Both exist — remove stale old copy
                old_path.unlink()
                actions.append(f"removed stale {old_rel}")
            else:
                # Rename old → new
                new_path.parent.mkdir(parents=True, exist_ok=True)
                old_path.rename(new_path)
                actions.append(f"migrated {old_rel} → {new_rel}")
        except OSError as exc:
            print(f"WARNING: legacy migration failed for {old_rel} — {exc}",
                  file=sys.stderr)
    return actions


def _deploy_framework_files(*, verbose: bool = True) -> tuple[list[str], list[str]]:
    """Copy missing framework files from fw_server/data/ into the workspace.

    Migrates old fw-* file names to dusun-* equivalents first (upgrade path).
    Returns (created, skipped) lists of relative paths.
    Never overwrites existing files.  All output goes to *stderr*
    so it is safe to call even when stdout is the MCP stdio transport.
    """
    cwd = Path.cwd()
    data_dir = Path(__file__).resolve().parent / "data"
    created: list[str] = []
    skipped: list[str] = []

    # Phase 0: migrate legacy file names (fw-* → dusun-*)
    migrations = _migrate_legacy_files(cwd)
    if migrations and verbose:
        print("Legacy file migration:", file=sys.stderr)
        for action in migrations:
            print(f"  {action}", file=sys.stderr)

    for dest_rel, src_name in _FRAMEWORK_FILES.items():
        dest = cwd / dest_rel
        if dest.exists():
            skipped.append(dest_rel)
            continue
        src = data_dir / src_name
        if not src.exists():
            print(f"WARNING: bundled file missing — {src}", file=sys.stderr)
            continue
        try:
            dest.parent.mkdir(parents=True, exist_ok=True)
            dest.write_bytes(src.read_bytes())
            created.append(dest_rel)
        except OSError as exc:
            print(f"WARNING: could not deploy {dest_rel} — {exc}",
                  file=sys.stderr)

    if verbose and created:
        print("[fw-engine] Auto-deployed framework files:", file=sys.stderr)
        for f in created:
            print(f"  {f}", file=sys.stderr)

    return created, skipped


def _init_vscode():
    """Bootstrap a workspace: .vscode/mcp.json + framework files for /dusun."""
    cwd = Path.cwd()
    created: list[str] = []
    skipped: list[str] = []

    # 1. .vscode/mcp.json
    vscode_dir = cwd / ".vscode"
    config_path = vscode_dir / "mcp.json"
    if config_path.exists():
        skipped.append(str(config_path.relative_to(cwd)))
    else:
        vscode_dir.mkdir(exist_ok=True)
        config_path.write_text(
            json.dumps(_MCP_CONFIG, indent=2) + "\n", encoding="utf-8"
        )
        created.append(str(config_path.relative_to(cwd)))

    # 2. Framework files (prompts, instructions, DUSUN.md)
    fw_created, fw_skipped = _deploy_framework_files(verbose=False)
    created.extend(fw_created)
    skipped.extend(fw_skipped)

    # 3. Report (stdout is fine here — --init is interactive)
    if created:
        print("Created:")
        for f in created:
            print(f"  {f}")
    if skipped:
        print("Already exists (skipped):")
        for f in skipped:
            print(f"  {f}")
    if created:
        print("\nReload VS Code to activate fw-engine MCP tools and /dusun pipeline.")
    else:
        print("Everything already in place. Nothing to do.")


_HELP_TEXT = """\
dusun — Systemic Reasoning MCP Server (fw-engine)

  Quick Start (2 commands):
    1. cd your-project
    2. fw-engine --init     # creates .vscode/mcp.json + framework files
    3. Reload VS Code       # tools + /dusun command appear automatically

  Sub-commands:
    fw-engine               Start the MCP server on stdio (VS Code calls this)
    fw-engine --init        Bootstrap workspace files for VS Code
    fw-engine doctor        Run framework health checks

  After --init, the /dusun slash command and all 20 MCP tools are available
  in Copilot Chat. Framework files auto-repair on every server start.

  https://github.com/kaantahti/dusun
"""


def main():
    """Start the MCP server on stdio, or run sub-commands (--init, doctor)."""
    if "--init" in sys.argv:
        _init_vscode()
        return
    if "doctor" in sys.argv:
        from fw_doctor.__main__ import main as doctor_main
        # Strip 'doctor' so argparse in fw_doctor sees only its own flags
        sys.argv = [a for a in sys.argv if a != "doctor"]
        doctor_main()
        return

    # If stdin is a TTY (user typed `fw-engine` in a terminal), show help
    # instead of starting the stdio server (which would hang indefinitely).
    if sys.stdin.isatty():
        print(_HELP_TEXT)
        return

    # Auto-deploy missing framework files on every server start.
    # VS Code sets CWD to the workspace root, so this is safe.
    # Output goes to stderr (stdout = MCP stdio transport).
    try:
        _deploy_framework_files(verbose=True)
    except Exception:
        pass  # Never let scaffolding failure prevent server start

    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
