#!/usr/bin/env python3
"""CARBON readiness, evidence graph, failure intelligence, project memory, and dogfood benchmark."""

from __future__ import annotations

import argparse
import datetime as dt
import hashlib
import html
import json
import os
import re
import shutil
import tempfile
from collections import Counter
from pathlib import Path
from typing import Any

from carbon_brand import REPORT_FAVICON
from carbon_evidence import analyze_page
from carbon_quality_confidence import assess_quality_confidence
from carbon_report_visuals import REPORT_VISUAL_CSS, render_coverage_heatmap, render_decision_hero, render_evidence_spine, render_risk_matrix


SCHEMA = "carbon.intelligence/v1"
DOCTOR_SCHEMA = "carbon.readiness-doctor/v1"
GRAPH_SCHEMA = "carbon.evidence-graph/v1"
FAILURE_SCHEMA = "carbon.failure-clusters/v1"
MEMORY_SCHEMA = "carbon.project-memory/v1"
BENCHMARK_SCHEMA = "carbon.seeded-defect-benchmark/v1"
REPORT_SCHEMA = "carbon.intelligence-report/v1"
IGNORED = {
    ".git", ".carbon", ".confidence", ".idea", ".vscode", "node_modules",
    "vendor", "dist", "build", "coverage", ".next", ".nuxt", ".venv",
    "venv", "__pycache__", "target", "Pods", "output",
}
SOURCE_EXTENSIONS = {
    ".js", ".jsx", ".ts", ".tsx", ".py", ".rb", ".go", ".java",
    ".kt", ".swift", ".cs", ".php", ".rs", ".html", ".vue", ".svelte",
}
TEST_TOKENS = {"test", "tests", "spec", "specs", "e2e", "playwright", "cypress", "selenium"}
DEPENDENCY_FILES = {
    "package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb",
    "requirements.txt", "pyproject.toml", "poetry.lock", "uv.lock", "Gemfile.lock",
    "go.mod", "go.sum", "Cargo.toml", "Cargo.lock", "pom.xml", "build.gradle",
}
SCHEMA_TOKENS = {"schema", "schemas", "migration", "migrations", "openapi", "swagger", "prisma", "sql"}
CONFIG_NAMES = {"dockerfile", "compose.yaml", "docker-compose.yml", "vercel.json", "netlify.toml", "wrangler.toml"}
STOPWORDS = {
    "the", "and", "for", "with", "from", "that", "this", "into", "when", "then",
    "test", "failed", "failure", "error", "page", "result", "actual", "expected",
    "carbon", "run", "was", "were", "has", "have", "not", "but", "its", "their",
}
SEVERITY_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3, "unknown": 4}
FRESHNESS_MAX_DAYS = 30


def utc_now() -> str:
    return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def safe_slug(value: str, fallback: str = "item") -> str:
    slug = re.sub(r"[^a-z0-9]+", "-", str(value).lower()).strip("-")
    return slug[:80] or fallback


def stable_id(prefix: str, *parts: str) -> str:
    digest = hashlib.sha256("\0".join(parts).encode()).hexdigest()[:12].upper()
    return f"{prefix}-{digest}"


def read_json(path: Path, fallback: Any = None) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return fallback


def atomic_json(path: Path, value: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
        json.dump(value, handle, indent=2, ensure_ascii=False)
        handle.write("\n")
        temporary = Path(handle.name)
    os.replace(temporary, path)


def paths(root: Path) -> dict[str, Path]:
    base = root / ".carbon" / "intelligence"
    return {
        "base": base,
        "doctor": base / "readiness.json",
        "graph": base / "evidence-graph.json",
        "failures": base / "failure-clusters.json",
        "memory": base / "project-memory.json",
        "benchmark": base / "seeded-benchmark.json",
        "report": base / "report.html",
    }


def repository_files(root: Path, limit: int = 20_000) -> list[Path]:
    result: list[Path] = []
    for current, dirs, files in os.walk(root):
        dirs[:] = sorted(name for name in dirs if name not in IGNORED and not name.startswith(".cache"))
        for name in sorted(files):
            path = Path(current) / name
            if not path.is_symlink():
                result.append(path)
            if len(result) >= limit:
                return result
    return result


def file_digest(root: Path, files: list[Path]) -> str:
    digest = hashlib.sha256()
    for path in sorted(files):
        relative = path.relative_to(root).as_posix()
        digest.update(relative.encode())
        try:
            stat = path.stat()
            digest.update(str(stat.st_size).encode())
            if stat.st_size <= 2_000_000:
                digest.update(path.read_bytes())
            else:
                digest.update(str(stat.st_mtime_ns).encode())
        except OSError:
            digest.update(b"unreadable")
    return digest.hexdigest()


def project_fingerprints(root: Path) -> dict[str, Any]:
    files = repository_files(root)
    buckets: dict[str, list[Path]] = {name: [] for name in ("source", "tests", "dependencies", "schemas", "config")}
    for path in files:
        relative = path.relative_to(root)
        lower_parts = {part.lower() for part in relative.parts}
        name = path.name.lower()
        if path.suffix.lower() in SOURCE_EXTENSIONS:
            buckets["source"].append(path)
        if lower_parts & TEST_TOKENS or any(token in name for token in (".test.", ".spec.", "_test.", "test_", "-test.", "-test-", "contract-test", "smoke-test")):
            buckets["tests"].append(path)
        if path.name in DEPENDENCY_FILES or name in {value.lower() for value in DEPENDENCY_FILES}:
            buckets["dependencies"].append(path)
        if lower_parts & SCHEMA_TOKENS or any(token in name for token in ("openapi", "swagger", "schema", "migration")) or path.suffix.lower() == ".sql":
            buckets["schemas"].append(path)
        if name in CONFIG_NAMES or name.startswith(".env.example") or relative.parts[:1] in {(".github",), ("config",)}:
            buckets["config"].append(path)
    return {
        "files": len(files),
        "counts": {name: len(values) for name, values in buckets.items()},
        "digests": {name: file_digest(root, values) for name, values in buckets.items()},
    }


def latest_run(root: Path) -> dict[str, Any] | None:
    rows: list[tuple[str, dict[str, Any]]] = []
    for path in (root / ".carbon" / "runs").glob("*/run.json") if (root / ".carbon" / "runs").is_dir() else []:
        value = read_json(path, {})
        if isinstance(value, dict):
            value["_runDir"] = str(path.parent)
            rows.append((str(value.get("updatedAt") or value.get("createdAt") or ""), value))
    return max(rows, key=lambda item: item[0], default=("", None))[1]


def parse_time(value: Any) -> dt.datetime | None:
    try:
        parsed = dt.datetime.fromisoformat(str(value).replace("Z", "+00:00"))
        return parsed if parsed.tzinfo else parsed.replace(tzinfo=dt.timezone.utc)
    except (TypeError, ValueError):
        return None


def refresh_memory(root: Path, persist: bool = True) -> dict[str, Any]:
    artifact = paths(root)["memory"]
    prior = read_json(artifact, {})
    current = project_fingerprints(root)
    previous_digests = prior.get("current", {}).get("digests", {}) if isinstance(prior, dict) else {}
    changed = [name for name, digest in current["digests"].items() if previous_digests and previous_digests.get(name) != digest]
    invalidation_map = {
        "source": ["product model", "test-to-code lineage", "unreplayed source-derived candidates"],
        "tests": ["test inventory", "suite-readiness claims", "benchmark comparison"],
        "dependencies": ["execution-environment evidence", "compatibility assumptions"],
        "schemas": ["API contracts", "data boundaries", "schema-derived candidates"],
        "config": ["target configuration", "environment and deployment assumptions"],
    }
    invalidations = [
        {
            "id": stable_id("INVALIDATION", name, current["digests"][name]),
            "cause": f"{name} fingerprint changed",
            "invalidates": invalidation_map[name],
            "action": f"Refresh {', '.join(invalidation_map[name])} before relying on prior evidence.",
        }
        for name in changed
    ]
    latest = latest_run(root)
    observed_at = parse_time((latest or {}).get("updatedAt") or (latest or {}).get("finishedAt"))
    evidence_age_days = None
    if observed_at:
        evidence_age_days = max(0, (dt.datetime.now(dt.timezone.utc) - observed_at).days)
    if not prior:
        freshness = "initialized"
    elif changed:
        freshness = "stale"
    elif evidence_age_days is not None and evidence_age_days > FRESHNESS_MAX_DAYS:
        freshness = "aging"
        invalidations.append({
            "id": stable_id("INVALIDATION", "age", str(evidence_age_days)),
            "cause": f"Latest run evidence is {evidence_age_days} days old",
            "invalidates": ["current release-confidence inference"],
            "action": "Run the smallest current-change validation before making a release decision.",
        })
    else:
        freshness = "current"
    history = list(prior.get("history", [])) if isinstance(prior, dict) else []
    if prior.get("current") and prior.get("current") != current:
        history.append({"capturedAt": prior.get("updatedAt"), **prior["current"]})
    memory = {
        "schema": MEMORY_SCHEMA,
        "projectRoot": str(root),
        "freshness": freshness,
        "freshnessMaxDays": FRESHNESS_MAX_DAYS,
        "current": current,
        "changedDomains": changed,
        "invalidations": invalidations,
        "latestRunId": (latest or {}).get("runId"),
        "latestEvidenceAgeDays": evidence_age_days,
        "history": history[-20:],
        "updatedAt": utc_now(),
        "rule": "Prior project knowledge is advisory until its source, schema, dependency, configuration, and evidence fingerprints are current.",
    }
    if persist:
        atomic_json(artifact, memory)
    return memory


def check(identifier: str, label: str, score: int, evidence: list[str], gaps: list[str], action: str, command: str) -> dict[str, Any]:
    score = max(0, min(100, score))
    status = "ready" if score >= 80 and not gaps else ("guarded" if score >= 50 else "not-ready")
    return {
        "id": identifier, "label": label, "score": score, "status": status,
        "evidence": evidence, "gaps": gaps, "action": action, "command": command,
    }


def readiness_doctor(root: Path, persist: bool = True) -> dict[str, Any]:
    memory = refresh_memory(root, persist=persist)
    fingerprint = memory["current"]
    carbon = root / ".carbon"
    model = read_json(carbon / "framework" / "product-model.json", {})
    coverage = read_json(carbon / "framework" / "coverage-map.json", {})
    suite = read_json(carbon / "framework" / "regression-suite.json", {})
    latest = latest_run(root)
    evidence_manifest = read_json(Path(latest["_runDir"]) / "evidence" / "manifest.json", {}) if latest else {}
    confidence_state = {"targetRoot": str(root), **(latest or {})}
    confidence = assess_quality_confidence(confidence_state, evidence_manifest, coverage.get("gaps", []))
    human_context = confidence.get("humanDecisionContext", {})
    files = fingerprint["files"]
    source_count = fingerprint["counts"]["source"]
    test_count = fingerprint["counts"]["tests"]
    model_entities = len(model.get("surfaces", [])) + len(model.get("apis", [])) + len(model.get("flows", []))
    regression_count = sum(1 for item in suite.get("tests", []) if item.get("lifecycle") == "regression")
    repro_count = sum(1 for item in (latest or {}).get("findings", []) if item.get("reproPath"))
    checks = [
        check(
            "project-surface", "Project surface understood",
            0 if files == 0 else (90 if model_entities else 55),
            [f"{files} files, {source_count} source files", f"{model_entities} modeled product/API/flow entities"],
            (["Project contains no inspectable files."] if files == 0 else (["Product model has not been generated."] if not model_entities else [])),
            "Generate or refresh the project model.", "/carbon test this project --size quick",
        ),
        check(
            "execution", "Local execution ready",
            85 if model.get("surfaces") or model.get("apis") else (55 if source_count else 10),
            [f"{len(model.get('surfaces', []))} UI/AI surfaces", f"{len(model.get('apis', []))} APIs"],
            ([] if model.get("surfaces") or model.get("apis") else ["No executable UI or API target is modeled."]),
            "Resolve a local target and adapter, or keep the pass review-only.", "/carbon-browser inspect the primary local target",
        ),
        check(
            "test-assets", "Test assets reusable",
            90 if regression_count else (65 if test_count or suite.get("tests") else 25),
            [f"{test_count} repository test files", f"{len(suite.get('tests', []))} candidates, {regression_count} trusted regressions"],
            ([] if regression_count else ["No explicitly promoted repeatable regression exists."]),
            "Bake valuable existing or generated cases into repeatable regression evidence.", "/carbon-tests review and bake the highest-value candidates",
        ),
        check(
            "evidence", "Evidence and reproducibility ready",
            90 if latest and confidence["coverage"]["evidenceBacked"] else (55 if latest else 15),
            [f"Latest run: {(latest or {}).get('runId') or 'none'}", f"Evidence-backed results: {confidence['coverage']['evidenceBacked']}", f"Portable repros: {repro_count}"],
            ([] if latest and confidence["coverage"]["evidenceBacked"] else ["No current evidence-backed execution result is available."]),
            "Execute the smallest decisive scope and preserve actual results plus artifacts.", "/carbon test the current change --size quick",
        ),
        check(
            "memory", "Project memory current",
            90 if memory["freshness"] == "current" else (70 if memory["freshness"] == "initialized" else 35),
            [f"Freshness: {memory['freshness']}", f"Changed domains: {', '.join(memory['changedDomains']) or 'none'}"],
            [item["cause"] for item in memory["invalidations"]],
            "Refresh every invalidated model, mapping, and evidence claim before relying on it.", "/carbon refresh stale project knowledge and test the affected scope",
        ),
        check(
            "confidence", "Shipping confidence explainable",
            85 if confidence["confidenceInQuality"]["band"] in {"high", "moderate"} else (50 if latest else 10),
            [f"Overall band: {confidence['confidenceInQuality']['band']}", f"Dimensions assessed: {sum(1 for value in confidence['dimensions'].values() if value['band'] != 'not-assessed')} of {len(confidence['dimensions'])}"],
            confidence["coverageMissing"][:6],
            "Close the largest dimension-level evidence gaps, then reassess.", "/carbon-confidence assess multidimensional release confidence",
        ),
        check(
            "human-context", "Human decision context explicit",
            90 if human_context.get("status") == "recorded" else (60 if human_context.get("status") == "pending" else 45),
            [f"Status: {human_context.get('status', 'not-recorded')}", f"Answered: {human_context.get('answered', 0)}", f"Open P0: {human_context.get('openP0', 0)}"],
            ([] if human_context.get("status") == "recorded" else [str(human_context.get("qualifier") or "Human decision context is not recorded.")]),
            "Ask only the smallest questions that can change the decision; retain them until a person answers.", "/carbon-human identify the highest-value human decision context for this release",
        ),
        check(
            "safety", "Authority and recovery bounded",
            85 if carbon.is_dir() else 60,
            ["Destructive, paid, public, and release actions remain approval-gated", "Local .carbon evidence is project-scoped"],
            ([] if carbon.is_dir() else ["No durable CARBON workspace exists yet."]),
            "Initialize durable state and preserve explicit approval boundaries.", "/carbon initialize this project safely",
        ),
    ]
    overall = round(sum(item["score"] for item in checks) / len(checks))
    blockers = [gap for item in checks if item["status"] == "not-ready" for gap in item["gaps"]]
    doctor = {
        "schema": DOCTOR_SCHEMA,
        "projectRoot": str(root),
        "overallScore": overall,
        "status": "ready" if overall >= 80 and not blockers else ("guarded" if overall >= 50 else "not-ready"),
        "checks": checks,
        "blockers": blockers,
        "confidence": confidence,
        "memoryFreshness": memory["freshness"],
        "recommendedStart": min(checks, key=lambda item: item["score"]),
        "oneCommand": "/carbon",
        "oneCommandBehavior": "Inspect readiness, refresh memory and evidence lineage, cluster failures, run the packaged dogfood benchmark, explain priorities, then build the smallest adaptive plan.",
        "generatedAt": utc_now(),
    }
    if persist:
        atomic_json(paths(root)["doctor"], doctor)
    return doctor


def add_node(nodes: dict[str, dict[str, Any]], identifier: str, kind: str, label: str, **data: Any) -> str:
    nodes[identifier] = {"id": identifier, "kind": kind, "label": label, **data}
    return identifier


def add_edge(edges: list[dict[str, Any]], source: str, target: str, relation: str, **data: Any) -> None:
    edges.append({"source": source, "target": target, "relation": relation, **data})


def evidence_graph(root: Path, persist: bool = True) -> dict[str, Any]:
    carbon = root / ".carbon"
    model = read_json(carbon / "framework" / "product-model.json", {})
    suite = read_json(carbon / "framework" / "regression-suite.json", {})
    memory = refresh_memory(root, persist=persist)
    nodes: dict[str, dict[str, Any]] = {}
    edges: list[dict[str, Any]] = []
    project_id = add_node(nodes, "PROJECT", "project", root.name, path=str(root), freshness=memory["freshness"])
    human = read_json(root / ".carbon" / "human" / "decision-context.json", {})
    if isinstance(human, dict) and human.get("schema") == "carbon.human-decision-context/v1":
        for question in [row for row in human.get("questions", []) if isinstance(row, dict)]:
            identifier = str(question.get("id") or stable_id("HUMAN", str(question)))
            add_node(nodes, identifier, "human-question", str(question.get("question") or identifier), status=question.get("status"), priority=question.get("priority"), category=question.get("category"))
            add_edge(edges, project_id, identifier, "needs-human-context")
            if question.get("status") == "answered":
                answer_id = stable_id("HUMAN-ANSWER", identifier, str(question.get("answeredAt") or ""))
                add_node(nodes, answer_id, "human-answer", f"Answer · {identifier}", answeredAt=question.get("answeredAt"), answeredBy=question.get("answeredBy"))
                add_edge(edges, identifier, answer_id, "answered-by-human")
    entity_lookup: dict[str, dict[str, Any]] = {}
    for kind, values in (("surface", model.get("surfaces", [])), ("api", model.get("apis", [])), ("flow", model.get("flows", []))):
        for entity in values:
            identifier = str(entity.get("id") or stable_id(kind.upper(), str(entity)))
            entity_lookup[identifier] = entity
            add_node(nodes, identifier, kind, str(entity.get("name") or entity.get("operationId") or entity.get("path") or identifier), source=entity.get("source"), risks=entity.get("risks", []))
            add_edge(edges, project_id, identifier, "contains")
    tests = {str(item.get("id")): item for item in suite.get("tests", []) if isinstance(item, dict) and item.get("id")}
    for identifier, test in tests.items():
        add_node(nodes, identifier, "test", str(test.get("title") or identifier), lifecycle=test.get("lifecycle"), priority=test.get("priority"), reason=test.get("reason"), staleReason=test.get("staleReason"))
        add_edge(edges, project_id, identifier, "plans")
        for entity_id in list(test.get("surfaceIds") or []) + list(test.get("flowIds") or []):
            if entity_id in nodes:
                add_edge(edges, entity_id, identifier, "covered-by", why=test.get("reason"))
        api_operation = str(test.get("apiOperation") or "")
        if api_operation:
            for entity_id, entity in entity_lookup.items():
                if str(entity.get("operationId") or "") == api_operation or entity_id == api_operation:
                    add_edge(edges, entity_id, identifier, "covered-by", why=test.get("reason"))
        for provenance in test.get("provenance", [])[:20]:
            provenance_text = str(provenance)
            provenance_id = stable_id("SOURCE", provenance_text)
            add_node(nodes, provenance_id, "source", provenance_text, path=provenance_text)
            add_edge(edges, provenance_id, identifier, "justifies")
    runs_root = carbon / "runs"
    for run_path in sorted(runs_root.glob("*/run.json")) if runs_root.is_dir() else []:
        run = read_json(run_path, {})
        if not isinstance(run, dict):
            continue
        run_id = str(run.get("runId") or run_path.parent.name)
        add_node(nodes, run_id, "run", run_id, status=run.get("status"), revision=run.get("revision"), updatedAt=run.get("updatedAt"))
        add_edge(edges, project_id, run_id, "executed")
        for result in run.get("tests", []):
            test_id = str(result.get("id") or "")
            if test_id not in nodes:
                add_node(nodes, test_id, "test", str(result.get("title") or test_id), priority=result.get("priority"), reason=result.get("reason"))
            result_id = stable_id("RESULT", run_id, test_id)
            add_node(nodes, result_id, "result", f"{test_id} · {result.get('status')}", status=result.get("status"), actual=result.get("actual"), evidence=result.get("evidence", []))
            add_edge(edges, test_id, result_id, "produced")
            add_edge(edges, result_id, run_id, "observed-in")
            for handle in result.get("evidence", [])[:20]:
                evidence_id = stable_id("EVIDENCE", run_id, str(handle))
                add_node(nodes, evidence_id, "evidence", str(handle), runId=run_id)
                add_edge(edges, result_id, evidence_id, "supported-by")
        for finding in run.get("findings", []):
            finding_id = f"{run_id}:{finding.get('id') or stable_id('FINDING', str(finding))}"
            add_node(nodes, finding_id, "finding", str(finding.get("title") or finding_id), severity=finding.get("severity"), reproPath=finding.get("reproPath"))
            add_edge(edges, run_id, finding_id, "found")
            for test_id in finding.get("testIds", []):
                if str(test_id) in nodes:
                    add_edge(edges, str(test_id), finding_id, "exposed")
    graph = {
        "schema": GRAPH_SCHEMA,
        "projectRoot": str(root),
        "nodes": list(nodes.values()),
        "edges": edges,
        "summary": {"nodes": len(nodes), "edges": len(edges), "byKind": dict(Counter(node["kind"] for node in nodes.values()))},
        "freshness": memory["freshness"],
        "invalidations": memory["invalidations"],
        "generatedAt": utc_now(),
    }
    if persist:
        atomic_json(paths(root)["graph"], graph)
    return graph


def why_test(root: Path, test_id: str) -> dict[str, Any]:
    graph = evidence_graph(root, persist=True)
    nodes = {node["id"]: node for node in graph["nodes"]}
    if test_id not in nodes or nodes[test_id]["kind"] != "test":
        raise ValueError(f"Unknown test id: {test_id}")
    incoming = [edge for edge in graph["edges"] if edge["target"] == test_id]
    outgoing = [edge for edge in graph["edges"] if edge["source"] == test_id]
    sources = [nodes[edge["source"]] for edge in incoming if edge["source"] in nodes and edge["relation"] in {"justifies", "covered-by"}]
    results = [nodes[edge["target"]] for edge in outgoing if edge["target"] in nodes and edge["relation"] == "produced"]
    test = nodes[test_id]
    return {
        "schema": "carbon.why-test/v1",
        "test": test,
        "why": test.get("reason") or "No explicit rationale is recorded; keep this candidate under review.",
        "mappedProductAndRisk": [node for node in sources if node["kind"] in {"surface", "api", "flow"}],
        "sourceProvenance": [node for node in sources if node["kind"] == "source"],
        "observedResults": results,
        "lineage": incoming + outgoing,
        "freshness": graph["freshness"],
        "invalidations": graph["invalidations"],
        "decisionRule": "Run this test when its mapped risk matters and its source lineage is current; do not keep it solely because it already exists.",
    }


def tokens(value: str) -> set[str]:
    return {
        token for token in re.findall(r"[a-z0-9]{3,}", value.lower())
        if token not in STOPWORDS and not token.isdigit()
    }


def classify_failure(text: str, category: str = "") -> str:
    structured = {
        "security": "security",
        "privacy": "security",
        "accessibility": "accessibility",
        "performance": "performance",
        "data": "data",
        "storage": "data",
        "network": "reliability",
        "console": "product-defect",
    }.get(category.strip().lower())
    if structured:
        return structured
    rules = [
        ("security", ("auth", "permission", "tenant", "leak", "injection", "secret", "csrf", "xss")),
        ("accessibility", ("accessibility", "aria", "keyboard", "contrast", "screen reader", "label")),
        ("performance", ("performance", "latency", "slow", "timeout", "lcp", "memory", "load")),
        ("test-definition", ("selector", "oracle", "assertion", "test definition", "fixture missing", "expected text")),
        ("infrastructure", ("browser launch", "driver", "dns", "connection refused", "port", "worker crash", "ci runner")),
        ("environment", ("credential", "configuration", "environment", "service unavailable", "dependency unavailable")),
        ("data", ("schema", "migration", "database", "serialization", "corrupt", "duplicate record")),
        ("reliability", ("race", "flaky", "intermittent", "retry", "idempotency", "deadlock")),
    ]
    lower = text.lower()
    for label, needles in rules:
        if any(needle in lower for needle in needles):
            return label
    return "product-defect"


def jaccard(left: set[str], right: set[str]) -> float:
    return len(left & right) / max(1, len(left | right))


def failure_records(root: Path) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    runs_root = root / ".carbon" / "runs"
    for path in sorted(runs_root.glob("*/run.json")) if runs_root.is_dir() else []:
        run = read_json(path, {})
        run_id = str(run.get("runId") or path.parent.name)
        for finding in run.get("findings", []):
            text = " ".join(str(finding.get(key) or "") for key in ("title", "detail", "expected", "actual", "evidence"))
            rows.append({
                "id": f"{run_id}:{finding.get('id') or stable_id('F', text)}", "runId": run_id,
                "testIds": list(finding.get("testIds") or []), "title": finding.get("title") or "Finding",
                "detail": finding.get("detail") or "", "severity": str(finding.get("severity") or "unknown").lower(),
                "category": str(finding.get("category") or "").lower(),
                "classification": classify_failure(text, str(finding.get("category") or "")), "reproPath": finding.get("reproPath"),
                "text": text, "tokens": tokens(text), "source": "finding",
            })
        for test in run.get("tests", []):
            if str(test.get("status")) != "failed":
                continue
            if any(str(test.get("id")) in row["testIds"] for row in rows if row["runId"] == run_id):
                continue
            text = " ".join(str(test.get(key) or "") for key in ("title", "reason", "actual", "error", "oracle"))
            rows.append({
                "id": f"{run_id}:{test.get('id')}", "runId": run_id, "testIds": [str(test.get("id"))],
                "title": test.get("title") or test.get("id"), "detail": test.get("actual") or test.get("error") or "",
                "severity": "high" if str(test.get("priority")) in {"P0", "P1"} else "medium",
                "classification": classify_failure(text), "reproPath": None,
                "text": text, "tokens": tokens(text), "source": "failed-test",
            })
    return rows


def cluster_failures(root: Path, persist: bool = True) -> dict[str, Any]:
    records = failure_records(root)
    clusters: list[dict[str, Any]] = []
    for record in records:
        best: tuple[float, dict[str, Any] | None] = (0.0, None)
        for cluster in clusters:
            score = jaccard(record["tokens"], set(cluster["signatureTokens"]))
            if record["classification"] == cluster["classification"]:
                score += 0.18
            if set(record["testIds"]) & set(cluster["testIds"]):
                score += 0.35
            if score > best[0]:
                best = (score, cluster)
        if best[1] is not None and best[0] >= 0.48:
            cluster = best[1]
            cluster["records"].append({key: value for key, value in record.items() if key not in {"tokens", "text"}})
            cluster["signatureTokens"] = sorted(set(cluster["signatureTokens"]) | record["tokens"])[:40]
            cluster["runIds"] = sorted(set(cluster["runIds"]) | {record["runId"]})
            cluster["testIds"] = sorted(set(cluster["testIds"]) | set(record["testIds"]))
            cluster["reproPaths"] = sorted(set(cluster["reproPaths"]) | ({str(record["reproPath"])} if record["reproPath"] else set()))
            if SEVERITY_RANK.get(record["severity"], 4) < SEVERITY_RANK.get(cluster["severity"], 4):
                cluster["severity"] = record["severity"]
        else:
            clusters.append({
                "id": stable_id("CLUSTER", record["classification"], record["title"]),
                "title": record["title"], "classification": record["classification"],
                "severity": record["severity"], "signatureTokens": sorted(record["tokens"])[:40],
                "runIds": [record["runId"]], "testIds": sorted(set(record["testIds"])),
                "reproPaths": [str(record["reproPath"])] if record["reproPath"] else [],
                "records": [{key: value for key, value in record.items() if key not in {"tokens", "text"}}],
            })
    for cluster in clusters:
        occurrence_count = len(cluster["records"])
        cluster["occurrences"] = occurrence_count
        cluster["reproducibility"] = "repeatable" if len(cluster["runIds"]) >= 2 else ("portable-repro" if cluster["reproPaths"] else "observed-once")
        cluster["likelyRootCause"] = {
            "test-definition": "The test definition, selector, setup, or oracle may be wrong.",
            "infrastructure": "Execution infrastructure or browser/runtime availability is the leading shared signal.",
            "environment": "Configuration, credentials, dependency state, or environment drift is the leading shared signal.",
            "security": "A shared trust-boundary or authorization behavior needs investigation.",
            "accessibility": "A shared semantic or interaction accessibility defect is indicated.",
            "performance": "A shared latency, resource, or load boundary is indicated.",
            "data": "A shared schema, persistence, or state-integrity boundary is indicated.",
            "reliability": "A shared timing, concurrency, retry, or recovery behavior is indicated.",
            "product-defect": "The available evidence points to product behavior, but root cause remains a hypothesis.",
        }[cluster["classification"]]
        cluster["nextAction"] = "Reproduce the smallest representative occurrence, compare artifacts across the cluster, then confirm or revise the classification before fixing."
    clusters.sort(key=lambda value: (SEVERITY_RANK.get(value["severity"], 4), -value["occurrences"], value["title"]))
    result = {
        "schema": FAILURE_SCHEMA, "projectRoot": str(root), "records": len(records),
        "clusters": clusters, "summary": {
            "clusters": len(clusters), "repeatable": sum(1 for item in clusters if item["reproducibility"] == "repeatable"),
            "withPortableRepro": sum(1 for item in clusters if item["reproPaths"]),
            "byClassification": dict(Counter(item["classification"] for item in clusters)),
        },
        "generatedAt": utc_now(),
        "boundary": "Clustering and classification are investigation hypotheses. Reproduction and artifact comparison are required before assigning root cause.",
    }
    if persist:
        atomic_json(paths(root)["failures"], result)
    return result


def benchmark_fixture_root() -> Path:
    return Path(__file__).resolve().parents[1] / "benchmarks" / "seeded-defects"


def run_seeded_benchmark(root: Path, persist: bool = True) -> dict[str, Any]:
    fixture = benchmark_fixture_root()
    manifest = read_json(fixture / "manifest.json", {})
    if manifest.get("schema") != "carbon.seeded-defect-fixture/v1":
        raise ValueError("Packaged seeded-defect fixture is missing or invalid.")
    run_id = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    run_dir = root / ".carbon" / "benchmarks" / "runs" / run_id
    artifact_dir = run_dir / "artifacts"
    artifact_dir.mkdir(parents=True, exist_ok=True)
    artifacts = []
    for index, definition in enumerate(manifest["artifacts"], 1):
        source = fixture / definition["file"]
        destination = artifact_dir / source.name
        shutil.copy2(source, destination)
        artifacts.append({
            "id": f"ART-{index:03d}", "kind": definition["kind"], "label": definition["file"],
            "path": destination.relative_to(run_dir).as_posix(), "bytes": destination.stat().st_size,
            "redactions": 0, "truncated": False,
        })
    page = {
        "id": "PAGE-SEEDED", "url": "https://seeded.carbon.invalid/checkout", "title": "Seeded checkout",
        "artifacts": artifacts, "testIds": ["BENCH-SEEDED"], "visualReviews": manifest.get("visualReviews", []),
        "personaFeedback": [], "explorationObservations": [],
    }
    state = {"tests": [{"id": "BENCH-SEEDED", "status": "failed"}]}
    issues = analyze_page(run_dir, page, state)
    expected = set(manifest["expectedIssueTitles"])
    detected = {str(issue.get("title")) for issue in issues}
    true_positive = sorted(expected & detected)
    missed = sorted(expected - detected)
    unexpected = sorted(detected - expected)
    precision = len(true_positive) / max(1, len(detected))
    recall = len(true_positive) / max(1, len(expected))
    f1 = 2 * precision * recall / max(0.00001, precision + recall)
    result = {
        "schema": BENCHMARK_SCHEMA, "fixtureVersion": manifest.get("version"),
        "runId": run_id, "status": "passed" if not missed and recall == 1 else "failed",
        "expected": len(expected), "detected": len(detected), "truePositive": len(true_positive),
        "precision": round(precision, 4), "recall": round(recall, 4), "f1": round(f1, 4),
        "detectedSeeds": true_positive, "missedSeeds": missed, "unexpectedSignals": unexpected,
        "issues": issues, "runDir": str(run_dir), "generatedAt": utc_now(),
        "boundary": "This deterministic packaged fixture dogfoods Carbon artifact analysis. It does not estimate detection rates on arbitrary real products.",
    }
    atomic_json(run_dir / "result.json", result)
    if persist:
        atomic_json(paths(root)["benchmark"], result)
    return result


def esc(value: Any) -> str:
    return html.escape(str(value or ""))


def render_report(root: Path, doctor: dict[str, Any] | None = None, graph: dict[str, Any] | None = None, failures: dict[str, Any] | None = None, memory: dict[str, Any] | None = None, benchmark: dict[str, Any] | None = None) -> Path:
    artifact_paths = paths(root)
    doctor = doctor or read_json(artifact_paths["doctor"], {}) or readiness_doctor(root)
    graph = graph or read_json(artifact_paths["graph"], {}) or evidence_graph(root)
    failures = failures or read_json(artifact_paths["failures"], {}) or cluster_failures(root)
    memory = memory or read_json(artifact_paths["memory"], {}) or refresh_memory(root)
    benchmark = benchmark or read_json(artifact_paths["benchmark"], {}) or run_seeded_benchmark(root)
    dimensions = doctor.get("confidence", {}).get("dimensions", {})
    checks_html = "".join(
        f'''<article class="record check" data-kind="readiness" data-status="{esc(item['status'])}" data-search="{esc((item['label']+' '+' '.join(item['gaps'])).lower())}"><div class="record-head"><span class="score">{item['score']}</span><div><small>{esc(item['status'])}</small><h3>{esc(item['label'])}</h3></div></div><p>{esc(item['action'])}</p><ul>{''.join(f'<li>{esc(gap)}</li>' for gap in item['gaps']) or '<li>No material gap recorded.</li>'}</ul><code>{esc(item['command'])}</code></article>'''
        for item in doctor.get("checks", [])
    )
    dimensions_html = "".join(
        f'''<article class="record dimension" data-kind="dimension" data-status="{esc(value['band'])}" data-search="{esc((value['label']+' '+value['basis']).lower())}"><div class="record-head"><span class="score">{esc(value['score'] if value['score'] is not None else '—')}</span><div><small>{esc(value['band'])}</small><h3>{esc(value['label'])}</h3></div></div><p>{esc(value['basis'])}</p><div class="mini">{value['conclusive']} conclusive · {value['evidenceBacked']} evidence-backed · {value['failed']} failed</div></article>'''
        for _, value in dimensions.items()
    )
    clusters_html = "".join(
        f'''<article class="record cluster" data-kind="failure" data-status="{esc(item['classification'])}" data-search="{esc((item['title']+' '+item['classification']+' '+' '.join(item['signatureTokens'])).lower())}"><div class="record-head"><span class="severity severity-{esc(item['severity'])}">{esc(item['severity'])}</span><div><small>{esc(item['classification'])} · {item['occurrences']} occurrence(s)</small><h3>{esc(item['title'])}</h3></div></div><p>{esc(item['likelyRootCause'])}</p><div class="mini">{esc(item['reproducibility'])} · runs {esc(', '.join(item['runIds']))}</div><details><summary>Evidence and next action</summary><p>{esc(item['nextAction'])}</p><code>{esc(', '.join(item['reproPaths']) or 'No portable repro yet')}</code></details></article>'''
        for item in failures.get("clusters", [])
    ) or '<p class="empty">No failed tests or findings are available to cluster.</p>'
    graph_nodes = graph.get("nodes", [])[:300]
    graph_html = "".join(
        f'''<article class="graph-node" data-kind="graph" data-status="{esc(node['kind'])}" data-search="{esc((node['label']+' '+node['kind']).lower())}"><span>{esc(node['kind'])}</span><strong>{esc(node['label'])}</strong><small>{esc(node['id'])}</small></article>'''
        for node in graph_nodes
    )
    invalidations_html = "".join(
        f'''<article class="record invalidation" data-kind="memory" data-status="stale" data-search="{esc((item['cause']+' '+item['action']).lower())}"><div class="record-head"><span class="severity severity-high">stale</span><div><small>invalidation</small><h3>{esc(item['cause'])}</h3></div></div><p>{esc(item['action'])}</p><div class="mini">Invalidates: {esc(', '.join(item['invalidates']))}</div></article>'''
        for item in memory.get("invalidations", [])
    ) or '<p class="empty">No current memory invalidations.</p>'
    benchmark_rows = "".join(f'<li><span>{esc(item)}</span><strong>detected</strong></li>' for item in benchmark.get("detectedSeeds", []))
    readiness_status = str(doctor.get("status") or "unknown")
    assessed_dimensions = [value for value in dimensions.values() if value.get("score") is not None]
    decision_label = (
        "MORE EVIDENCE" if not assessed_dimensions
        else "SHIP" if readiness_status == "ready"
        else "CANARY" if readiness_status == "guarded"
        else "HOLD" if readiness_status == "not-ready"
        else "MORE EVIDENCE"
    )
    decision_tone = "risk" if decision_label in {"HOLD", "MORE EVIDENCE"} else "neutral"
    weakest = min(dimensions.values(), key=lambda item: item.get("score") if item.get("score") is not None else -1, default={})
    heatmap_html = render_coverage_heatmap([
        {"label": value.get("label") or key, "value": value.get("band") or "—", "detail": f"{value.get('conclusive', 0)} conclusive · {value.get('failed', 0)} failed", "state": "risk" if value.get("failed") or value.get("score") is None else "good" if value.get("score", 0) >= 60 else "unknown"}
        for key, value in dimensions.items()
    ], "Confidence dimensions", "Evidence strength by dimension; striped cells remain inconclusive.")
    matrix_html = render_risk_matrix([
        {"label": value.get("label") or key, "risk": min(100, 20 + 25 * int(value.get("failed", 0)) + (20 if value.get("score") is None else max(0, 50 - int(value.get("score", 0))))), "evidence": min(100, 100 * int(value.get("evidenceBacked", 0)) / max(1, int(value.get("tests", 0))))}
        for key, value in dimensions.items()
    ], note="Derived from current mapped evidence and failures; not a probability of correctness.")
    spine_html = render_evidence_spine([
        {"value": "Decide whether the project is ready to validate and ship"},
        {"value": f"{failures.get('summary', {}).get('clusters', 0)} failure cluster(s)", "tone": "risk" if failures.get('summary', {}).get('clusters', 0) else "neutral"},
        {"value": f"{graph.get('summary', {}).get('nodes', 0)} evidence graph nodes"},
        {"value": decision_label, "tone": "risk" if decision_tone == "risk" else "neutral"},
        {"value": weakest.get("action") or weakest.get("basis") or "Close the weakest confidence dimension"},
    ])
    document = f'''<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><link rel="icon" href="{REPORT_FAVICON}"><title>CARBON intelligence · {esc(root.name)}</title>
<style>
:root{{--bg:#000;--panel:#070707;--soft:#0d0d0d;--line:#292929;--line-bright:#555;--text:#fff;--muted:#999;--muted-strong:#d4d4d0;--risk:#ff9f43;--risk-line:rgba(255,159,67,.48);--good:#fff;--bad:#ff7d7d}}*{{box-sizing:border-box}}html{{overflow-x:hidden;-webkit-text-size-adjust:100%;text-size-adjust:100%}}body{{min-height:100vh;min-height:100svh;margin:0;background:var(--bg);color:var(--text);font:15px/1.5 Inter,system-ui,sans-serif}}main{{width:min(1240px,calc(100% - 28px));margin:auto;padding:max(24px,env(safe-area-inset-top)) env(safe-area-inset-right) calc(80px + env(safe-area-inset-bottom)) env(safe-area-inset-left)}}header{{display:flex;justify-content:space-between;align-items:center;gap:16px;border-bottom:1px solid var(--line);padding:10px 0 24px}}.brand{{display:flex;gap:12px;align-items:center;min-width:0}}.brand svg{{width:38px;height:38px;flex:0 0 auto}}.brand strong{{font-size:24px;letter-spacing:.08em}}.brand span,small,.mini,.empty{{color:var(--muted)}}h1{{font-size:clamp(44px,8vw,88px);line-height:.9;letter-spacing:-.06em;margin:45px 0 18px;max-width:900px}}.lede{{max-width:800px;color:#bbb;font-size:19px}}.hero-metrics{{display:grid;grid-template-columns:repeat(5,1fr);margin:35px 0;border:1px solid var(--line)}}.metric{{min-width:0;padding:20px;border-right:1px solid var(--line);overflow-wrap:anywhere}}.metric:last-child{{border:0}}.metric strong{{display:block;font-size:32px}}.toolbar{{position:sticky;top:0;z-index:4;display:grid;grid-template-columns:minmax(0,1fr) 190px auto auto;gap:8px;padding:12px 0;background:#000}}input,select,button{{min-width:0;min-height:44px;border:1px solid var(--line);background:#090909;color:#fff;padding:11px 13px;font:inherit;touch-action:manipulation}}button{{cursor:pointer}}button:hover,button.active{{border-color:#fff}}.tabs{{display:flex;flex-wrap:wrap;gap:7px;margin:15px 0 25px}}section[data-panel]{{display:none}}section[data-panel].active{{display:block}}.section-head{{display:flex;justify-content:space-between;align-items:end;gap:16px;border-bottom:1px solid var(--line);padding:28px 0 14px;margin-bottom:16px}}.section-head h2{{font-size:30px;margin:0}}.section-head span{{overflow-wrap:anywhere}}.grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(290px,100%),1fr));gap:1px;background:var(--line);border:1px solid var(--line)}}.record{{min-width:0;background:var(--panel);padding:22px;min-height:210px;overflow-wrap:anywhere}}.record-head{{display:flex;gap:15px;align-items:flex-start}}.record h3{{margin:3px 0 10px;font-size:19px}}.score{{display:grid;place-items:center;width:54px;height:54px;border:1px solid #555;font-size:20px;font-weight:800}}.severity{{padding:5px 8px;border:1px solid var(--line);font-size:11px;text-transform:uppercase}}.severity-critical,.severity-high{{border-color:#7b2d2d;color:var(--bad)}}.record code{{display:block;margin-top:14px;padding:10px;background:#000;color:#ddd;overflow-wrap:anywhere}}details{{margin-top:15px}}summary{{min-height:44px;cursor:pointer;color:#ddd;touch-action:manipulation}}.graph{{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(190px,100%),1fr));gap:8px}}.graph-node{{display:flex;min-width:0;flex-direction:column;min-height:125px;padding:16px;border:1px solid var(--line);background:var(--panel);overflow-wrap:anywhere}}.graph-node span{{color:var(--risk);font-size:10px;text-transform:uppercase;letter-spacing:.1em}}.graph-node strong{{margin:12px 0}}.benchmark{{display:grid;grid-template-columns:.7fr 1.3fr;gap:20px}}.benchmark-score{{display:grid;place-items:center;min-height:320px;border:1px solid var(--line);background:var(--panel)}}.benchmark-score strong{{font-size:90px;letter-spacing:-.08em}}.seed-list{{margin:0;padding:0;list-style:none;border-top:1px solid var(--line)}}.seed-list li{{display:flex;justify-content:space-between;gap:12px;padding:13px 0;border-bottom:1px solid var(--line)}}.seed-list strong{{color:var(--good)}}[hidden]{{display:none!important}}@media(max-width:760px){{body{{font-size:16px}}.hero-metrics{{grid-template-columns:1fr 1fr}}.metric{{border-bottom:1px solid var(--line)}}.toolbar{{grid-template-columns:1fr 1fr}}.toolbar input{{grid-column:1/-1}}.benchmark{{grid-template-columns:1fr}}}}@media(max-width:420px){{main{{width:min(100% - 20px,1240px);padding-top:max(12px,env(safe-area-inset-top))}}header{{align-items:flex-start;flex-direction:column}}header small{{overflow-wrap:anywhere}}h1{{font-size:clamp(38px,13vw,52px)}}.hero-metrics{{grid-template-columns:1fr 1fr}}.toolbar{{position:static;grid-template-columns:1fr}}.toolbar input{{grid-column:auto}}.tabs{{display:grid;grid-template-columns:1fr 1fr}}.tabs button{{width:100%}}.section-head{{align-items:flex-start;flex-direction:column}}.record{{padding:18px}}.record-head{{flex-direction:column}}.benchmark-score{{min-height:230px}}.benchmark-score strong{{font-size:70px}}.seed-list li{{align-items:flex-start;flex-direction:column}}}}
.brand>div{{display:grid;gap:2px}}.brand span{{display:block;font-size:10px;letter-spacing:.08em}}
{REPORT_VISUAL_CSS}
</style></head><body><main>
<header><div class="brand"><svg viewBox="0 0 256 256" aria-hidden="true"><rect width="256" height="256" fill="#000"/><path fill="#fff" d="M56 34h112l48 48-48 48V96l-16-16H94c-15 0-22 7-22 22v52c0 15 7 22 22 22h104v46H56c-21 0-32-11-32-32V66c0-21 11-32 32-32Z"/></svg><div><strong>CARBON</strong><span>INTELLIGENCE</span></div></div><small>{esc(utc_now())}</small></header>
{render_decision_hero(decision_label, 'Readiness and evidence point to the next release move.', f"Project readiness is {readiness_status}; the evidence graph contains {graph.get('summary',{}).get('nodes',0)} nodes and {failures.get('summary',{}).get('clusters',0)} clustered failure(s).", 'Readiness checks, multidimensional evidence, failure clusters, freshness, and the packaged dogfood benchmark.', decision_tone)}
{spine_html}
<h1>Know what the evidence actually supports.</h1><p class="lede">One adaptive view of readiness, evidence lineage, clustered failures, freshness, benchmark health, and multidimensional shipping confidence.</p>
<div class="hero-metrics"><div class="metric"><span>Readiness</span><strong>{doctor.get('overallScore',0)}</strong><small>{esc(doctor.get('status'))}</small></div><div class="metric"><span>Graph</span><strong>{graph.get('summary',{}).get('nodes',0)}</strong><small>nodes</small></div><div class="metric"><span>Failure clusters</span><strong>{failures.get('summary',{}).get('clusters',0)}</strong><small>{failures.get('records',0)} records</small></div><div class="metric"><span>Memory</span><strong>{esc(memory.get('freshness','unknown'))}</strong><small>{len(memory.get('invalidations',[]))} invalidations</small></div><div class="metric"><span>Dogfood recall</span><strong>{round(benchmark.get('recall',0)*100)}%</strong><small>packaged seeded defects</small></div></div>
{heatmap_html}
{matrix_html}
<div class="toolbar"><input id="search" type="search" placeholder="Search risks, tests, failures, evidence…"><select id="status"><option value="">All states</option><option>ready</option><option>guarded</option><option>not-ready</option><option>high</option><option>low</option><option>stale</option><option>product-defect</option><option>security</option><option>accessibility</option><option>performance</option></select><button id="expand">Expand details</button><button id="collapse">Collapse details</button></div>
<nav class="tabs"><button class="active" data-tab="readiness">Readiness</button><button data-tab="confidence">Confidence</button><button data-tab="graph">Evidence graph</button><button data-tab="failures">Failures</button><button data-tab="memory">Memory</button><button data-tab="benchmark">Dogfood benchmark</button></nav>
<section data-panel="readiness" class="active"><div class="section-head"><h2>Readiness doctor</h2><span>{esc(doctor.get('oneCommandBehavior'))}</span></div><div class="grid">{checks_html}</div></section>
<section data-panel="confidence"><div class="section-head"><h2>Multidimensional confidence</h2><span>Evidence strength, never probability</span></div><div class="grid">{dimensions_html}</div></section>
<section data-panel="graph"><div class="section-head"><h2>Evidence graph</h2><span>{graph.get('summary',{}).get('edges',0)} typed edges · use why-test for full lineage</span></div><div class="graph">{graph_html}</div></section>
<section data-panel="failures"><div class="section-head"><h2>Failure clusters</h2><span>Classification remains a hypothesis until reproduced</span></div><div class="grid">{clusters_html}</div></section>
<section data-panel="memory"><div class="section-head"><h2>Freshness and invalidation</h2><span>{esc(memory.get('rule'))}</span></div><div class="grid">{invalidations_html}</div></section>
<section data-panel="benchmark"><div class="section-head"><h2>Seeded-defect dogfood benchmark</h2><span>{esc(benchmark.get('boundary'))}</span></div><div class="benchmark"><div class="benchmark-score"><div><strong>{round(benchmark.get('f1',0)*100)}</strong><p>F1 · precision {round(benchmark.get('precision',0)*100)}% · recall {round(benchmark.get('recall',0)*100)}%</p></div></div><ul class="seed-list">{benchmark_rows}</ul></div></section>
</main><script>
const tabs=[...document.querySelectorAll('[data-tab]')],panels=[...document.querySelectorAll('[data-panel]')],search=document.querySelector('#search'),status=document.querySelector('#status');
function show(name){{tabs.forEach(x=>x.classList.toggle('active',x.dataset.tab===name));panels.forEach(x=>x.classList.toggle('active',x.dataset.panel===name));apply()}}tabs.forEach(x=>x.onclick=()=>show(x.dataset.tab));
function apply(){{const q=search.value.toLowerCase().trim(),s=status.value;document.querySelectorAll('.record,.graph-node').forEach(x=>{{const okQ=!q||(x.dataset.search||'').includes(q),okS=!s||(x.dataset.status||'')===s;x.hidden=!(okQ&&okS)}})}}search.oninput=apply;status.onchange=apply;
document.querySelector('#expand').onclick=()=>document.querySelectorAll('details').forEach(x=>x.open=true);document.querySelector('#collapse').onclick=()=>document.querySelectorAll('details').forEach(x=>x.open=false);
</script></body></html>'''
    artifact_paths["report"].parent.mkdir(parents=True, exist_ok=True)
    with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=artifact_paths["report"].parent, delete=False) as handle:
        handle.write(document)
        temporary = Path(handle.name)
    os.replace(temporary, artifact_paths["report"])
    return artifact_paths["report"]


def run_all(root: Path) -> dict[str, Any]:
    doctor = readiness_doctor(root)
    memory = refresh_memory(root)
    graph = evidence_graph(root)
    failures = cluster_failures(root)
    benchmark = run_seeded_benchmark(root)
    report = render_report(root, doctor, graph, failures, memory, benchmark)
    return {
        "schema": SCHEMA, "projectRoot": str(root), "status": doctor["status"],
        "readiness": doctor, "memory": memory, "graphSummary": graph["summary"],
        "failureSummary": failures["summary"], "benchmark": benchmark,
        "reportPath": str(report), "reportUrl": report.resolve().as_uri(),
        "chatLink": f"[Open CARBON Intelligence]({report.resolve()})",
        "next": "Use why-test for a selected case, then build the smallest adaptive plan that closes the lowest readiness and confidence dimensions.",
    }


def root_path(value: str) -> Path:
    root = Path(value).expanduser().resolve()
    if not root.is_dir():
        raise ValueError("root must be an existing project directory")
    return root


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    sub = parser.add_subparsers(dest="action", required=True)
    for name in ("all", "doctor", "graph", "failures", "memory", "benchmark", "report"):
        command = sub.add_parser(name)
        command.add_argument("--root", default=".")
    why = sub.add_parser("why-test")
    why.add_argument("--root", default=".")
    why.add_argument("--test-id", required=True)
    args = parser.parse_args()
    root = root_path(args.root)
    if args.action == "all":
        value = run_all(root)
    elif args.action == "doctor":
        value = readiness_doctor(root)
    elif args.action == "graph":
        value = evidence_graph(root)
    elif args.action == "why-test":
        value = why_test(root, args.test_id)
    elif args.action == "failures":
        value = cluster_failures(root)
    elif args.action == "memory":
        value = refresh_memory(root)
    elif args.action == "benchmark":
        value = run_seeded_benchmark(root)
    else:
        report = render_report(root)
        value = {"schema": REPORT_SCHEMA, "reportPath": str(report), "reportUrl": report.resolve().as_uri(), "chatLink": f"[Open CARBON Intelligence]({report.resolve()})"}
    print(json.dumps(value, indent=2, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except (OSError, ValueError) as error:
        print(json.dumps({"error": str(error)}), file=os.sys.stderr)
        raise SystemExit(2)
