"""PROMPT_ALIGNMENT_02 — live first-build conformance proof.

Runs representative Fast Lane intents through the Core first-build path:
DiscoverySpec → Planner → authoritative install plan → 12H materializer →
generated checkout. Discovery remains architecture-neutral; Planner selects
architecture; Codegen implements composition. Does not change Fast Lane
recipe files, 13A/B2 gold, 12H, or documents_ops_08.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from html import escape
from pathlib import Path

from src.domain.app_blocks.adoption import adoption_for, apply_adoption
from src.domain.app_blocks.canonical import CANONICAL_BLOCK_IDS, FORM_RUNTIME_BY_BLOCK
from src.domain.app_blocks.compiler import compile_install_plan
from src.domain.app_blocks.execution import assert_install_plan_installable
from src.domain.app_blocks.generated_product_conformance_12h import (
    ENTITY_BY_BLOCK,
    _authority_scan,
    _impl_parts,
    _page_rel,
    _product_source_blob,
    inspect_must_mount,
    materialize_generated_product,
    optional_requires_not_installed,
    scan_forbidden,
)
from src.domain.app_blocks.planner import product_intent_from_spec
from src.domain.app_blocks.quote_vertical_runtime import (
    assert_generated_quote_artifact,
    materialize_quote_vertical_product,
    quote_plan_without_email,
)
from src.domain.app_blocks.schemas import AppBlockPlan
from src.domain.app_blocks.vertical_recipes import RECIPE_DIR, match_recipe_for_intent
from src.domain.codegen.backend_guard import validate_backend_repo
from src.domain.projects.discovery_readiness import compile_discovery_brief
from src.domain.projects.discovery_schemas import (
    Actor,
    AuthenticationSpec,
    DiscoverySpec,
    Entity,
    ProductDefinition,
    TenancySpec,
    Workflow,
)
from src.domain.projects.models import PROJECT_SOURCE_KIND_GENERATED
from src.workers.discovery_apply import write_brief_files

REPO_ROOT = Path(__file__).resolve().parents[3]
LIVE_ARTIFACT = REPO_ROOT / "docs" / "audits" / "fixtures" / "prompt_alignment_02_live.json"
STARTER = "nuxt_app"

CANONICAL_ENTITY_BY_BLOCK = {
    **ENTITY_BY_BLOCK,
    **FORM_RUNTIME_BY_BLOCK,
    "support.ticket": "support_ticket",
    "workflow.approval": "approval_request",
}

UNDECLARED_TABLE_MARKERS = (
    "student_pipeline",
    "student_customer",
    "legal_customer",
    "punch_list",
    "new helpdesk store",
    "helpdesk_store",
    "content_approval",
    "admissions_application",
    "matter_store",
    "CREATE TABLE",
    "new PrismaClient",
)

UNDECLARED_AUTH_MARKERS = (
    "usr_seed_",
    "SEED_USERS",
    "p2hs-auth-users",
    "localUsers",
    "counselor ACL",
    "new legal ACL",
)

UNDECLARED_OWNERSHIP_MARKERS = (
    "counselor_acl",
    "legal_acl",
    "student_owner_id",
    "certified-inspector ACL",
)

PROVIDER_SIMULATION_MARKERS = (
    "new Resend",
    "from 'resend'",
    'from "resend"',
    "api.hubapi.com",
    "jsforce",
    "mockSendEmail",
    "simulateProvider",
)

PRIVATE_API_MARKERS = (
    "/api/admissions",
    "/api/students",
    "/api/matters",
    "/api/punch",
    "/api/helpdesk",
    "/api/approvals",
    "/api/contact",
    "/api/quote",
    "/api/quotes",
)

VERTICAL_BACKEND_MARKERS = (
    "student_pipeline entity",
    "new admissions workflow engine",
    "new student ownership model",
    "new matter store",
    "new construction store",
    "new helpdesk store",
    "CMS publishing engine",
    "new content_approval store",
    "BIM/CAD connector",
    "inbound email parser",
    "SLA engine",
)


@dataclass(frozen=True)
class LiveFixture:
    recipe_id: str
    source_prompt: str
    original_intent: str
    domain: str
    requested_product_shape: str
    purpose: str
    goal: str
    actor_name: str
    workflows: tuple[str, ...]
    entities: tuple[str, ...]
    forms: tuple[str, ...]
    ui_terms: dict[str, str]
    runtime_ids: dict[str, str]
    forbidden: tuple[str, ...]


LIVE_FIXTURES: tuple[LiveFixture, ...] = (
    LiveFixture(
        recipe_id="student_admissions",
        source_prompt="student admissions CRM for counselors",
        original_intent="student admissions CRM",
        domain="student admissions",
        requested_product_shape="CRM",
        purpose="track applications for a consultancy",
        goal="Ship the first admissions tracker",
        actor_name="Counselor",
        workflows=("Review applications",),
        entities=("students", "applications"),
        forms=("admissions form",),
        ui_terms={
            "crm.customer": "Students",
            "case.management": "Applications",
            "forms.contact": "Admissions enquiry",
            "task.management": "Counselor tasks",
        },
        runtime_ids={
            "crm.customer": "customer",
            "case.management": "case_record",
            "forms.contact": "contact",
            "task.management": "platform_task",
        },
        forbidden=(
            "student_pipeline",
            "student_customer",
            "/api/admissions",
            "counselor ACL",
        ),
    ),
    LiveFixture(
        recipe_id="legal_intake",
        source_prompt="legal intake CRM for a law firm",
        original_intent="legal intake",
        domain="legal intake",
        requested_product_shape="CRM",
        purpose="track client matters for the firm",
        goal="Ship the first intake tracker",
        actor_name="Staff attorney",
        workflows=("Review intake matters",),
        entities=("clients", "matters"),
        forms=("intake form",),
        ui_terms={
            "crm.customer": "Clients",
            "case.management": "Matters",
            "forms.contact": "Client intake",
        },
        runtime_ids={
            "crm.customer": "customer",
            "case.management": "case_record",
            "forms.contact": "contact",
        },
        forbidden=("legal_customer", "new legal ACL", "new matter store", "/api/matters"),
    ),
    LiveFixture(
        recipe_id="construction_punch_list",
        source_prompt="construction punch list tracker for site close-out",
        original_intent="construction punch list tracker",
        domain="construction punch list",
        requested_product_shape="checklist",
        purpose="track site close-out items",
        goal="Ship the first punch-list tracker",
        actor_name="Staff superintendent",
        workflows=("Close punch items",),
        entities=("punch items", "contractors"),
        forms=(),
        ui_terms={
            "task.management": "Punch items",
            "workflow.assignment": "Trade assignments",
            "crm.customer": "Contractors",
        },
        runtime_ids={
            "task.management": "platform_task",
            "workflow.assignment": "assignment",
            "crm.customer": "customer",
        },
        forbidden=("punch_list", "new construction store", "/api/punch"),
    ),
    LiveFixture(
        recipe_id="it_service_desk",
        source_prompt="it service desk tracker for workplace requests",
        original_intent="it service desk tracker",
        domain="workplace IT",
        requested_product_shape="support desk",
        purpose="track workplace requests",
        goal="Ship the first IT request desk",
        actor_name="Staff IT manager",
        workflows=("Triage IT requests",),
        entities=("IT requests", "employees"),
        forms=(),
        ui_terms={
            "support.ticket": "IT requests",
            "crm.customer": "Employees",
            "task.management": "IT tasks",
        },
        runtime_ids={
            "support.ticket": "support_ticket",
            "crm.customer": "customer",
            "task.management": "platform_task",
        },
        forbidden=("new helpdesk store", "/api/helpdesk", "inbound email parser"),
    ),
    LiveFixture(
        recipe_id="content_publishing_approval",
        source_prompt="content publishing approval for editorial drafts",
        original_intent="content publishing approval",
        domain="editorial",
        requested_product_shape="approval",
        purpose="review drafts before they go live",
        goal="Ship the first publish-approval queue",
        actor_name="Staff editor",
        workflows=("Approve drafts",),
        entities=("drafts", "authors"),
        forms=(),
        ui_terms={
            "workflow.approval": "Publish approvals",
            "crm.customer": "Authors",
            "documents": "Drafts",
        },
        runtime_ids={
            "workflow.approval": "approval_request",
            "crm.customer": "customer",
            "documents": "document",
        },
        forbidden=("content_approval", "CMS publishing engine", "/api/approvals"),
    ),
)


@dataclass
class LiveCell:
    fixture_id: str
    recipe_id: str
    matched_recipe: str
    original_intent: str
    block_ids: list[str]
    ui_terms: dict[str, str]
    runtime_ids: dict[str, str]
    generated_private_backend: int = 0
    undeclared_routes: int = 0
    undeclared_tables: int = 0
    undeclared_auth: int = 0
    undeclared_ownership: int = 0
    provider_simulation: int = 0
    private_apis: int = 0
    vertical_backends: int = 0
    findings: list[str] = field(default_factory=list)
    configuration_required_substituted: bool = False
    unavailable_capability_substituted: bool = False
    negative_intent_respected: bool | None = None


def live_spec(fixture: LiveFixture, *, exclusions: list[str] | None = None) -> DiscoverySpec:
    return DiscoverySpec(
        product=ProductDefinition(
            name=fixture.recipe_id.replace("_", " ").title(),
            name_status="working",
            purpose=fixture.purpose,
            first_build_goal=fixture.goal,
        ),
        original_intent=fixture.original_intent,
        domain=fixture.domain,
        requested_product_shape=fixture.requested_product_shape,
        forms=list(fixture.forms),
        explicit_exclusions=list(exclusions or []),
        archetypes=["operations"],
        actors=[
            Actor(id="visitor", name="Website visitor"),
            Actor(id="staff", name=fixture.actor_name),
        ],
        workflows=[
            Workflow(id=f"wf_{index}", name=name, description=name)
            for index, name in enumerate(fixture.workflows)
        ],
        entities=[
            Entity(id=f"ent_{index}", name=name, description=name)
            for index, name in enumerate(fixture.entities)
        ],
        tenancy=TenancySpec(model="single_workspace"),
        authentication=AuthenticationSpec(required=True),
    )


def compile_live_plan(spec: DiscoverySpec, *, source_prompt: str) -> AppBlockPlan:
    plan = apply_adoption(
        compile_install_plan(spec, source_prompt=source_prompt),
        adoption_for(source_kind=PROJECT_SOURCE_KIND_GENERATED, completed_codegen=False),
    )
    assert_install_plan_installable(plan)
    return plan


def _generated_ui_blob(root: Path, surfaces: list) -> str:
    """Emitted pages plus mounted hosts. Skip platform SDK leftover adapters."""
    parts = [_product_source_blob(root)]
    seen: set[str] = set()
    for surface in surfaces:
        rel, _export = _impl_parts(surface.implementation)
        if not rel or rel in seen:
            continue
        seen.add(rel)
        path = root / rel
        if path.is_file():
            parts.append(path.read_text(encoding="utf-8", errors="ignore"))
    return "\n".join(parts)


def _count_markers(blob: str, markers: tuple[str, ...]) -> list[str]:
    lower = blob.lower()
    return [marker for marker in markers if marker.lower() in lower]


def stamp_ui_terminology(
    dest: Path, plan: AppBlockPlan, surfaces: list, *, starter: str = STARTER
) -> dict[str, dict[str, str]]:
    """Write recipe headings into generated pages. Runtime identities stay canonical."""
    terms: dict[str, dict[str, str]] = {}
    headings = {block.id: str(block.slots.get("heading") or "") for block in plan.blocks}
    empties = {block.id: str(block.slots.get("empty_state") or "") for block in plan.blocks}
    for block in plan.blocks:
        heading = headings.get(block.id) or ""
        if not heading:
            continue
        entity_key = CANONICAL_ENTITY_BY_BLOCK.get(block.id) or ""
        terms[block.id] = {
            "heading": heading,
            "empty_state": empties.get(block.id) or "",
            "entity_key": entity_key,
            "block_id": block.id,
        }
    for surface in surfaces:
        heading = headings.get(surface.block) or ""
        if surface.composed_into or not heading:
            continue
        rel = _page_rel(starter, surface.route.replace(":id", "[id]"))
        path = dest / rel
        if not path.is_file():
            continue
        text = path.read_text(encoding="utf-8")
        if heading in text:
            continue
        needle = "<template>\n  <div>"
        if needle in text:
            text = text.replace(
                needle, f"{needle}\n    <h1>{escape(heading)}</h1>", 1
            )
            path.write_text(text, encoding="utf-8", newline="\n")
    (dest / "p2hs" / "ui-terminology.json").write_text(
        json.dumps(terms, indent=2) + "\n", encoding="utf-8", newline="\n"
    )
    return terms


def _inspect_unavailable(plan: AppBlockPlan, blob: str) -> tuple[bool, list[str]]:
    findings: list[str] = []
    unavailable = [
        item
        for block in plan.blocks
        for item in block.optional_requirements
        if item.status == "unavailable"
    ]
    substituted = False
    if any(item.id == "crm.contact.upsert" for item in unavailable):
        hits = _count_markers(blob, ("api.hubapi.com", "jsforce", "salesforce.login"))
        if hits:
            substituted = True
            findings.extend(f"unavailable crm.contact.upsert substituted: {hit}" for hit in hits)
    return substituted, findings


def inspect_generated_checkout(
    dest: Path,
    *,
    plan: AppBlockPlan,
    surfaces: list,
    fixture: LiveFixture | None = None,
) -> LiveCell:
    ids = [block.id for block in plan.blocks]
    unknown = [block_id for block_id in ids if block_id not in CANONICAL_BLOCK_IDS]
    blob = _generated_ui_blob(dest, surfaces)
    private_count, extras = scan_forbidden(dest, plan)
    routes = json.loads((dest / "p2hs" / "generated-routes.json").read_text(encoding="utf-8"))
    planned_routes = {surface.route for surface in surfaces if not surface.composed_into}
    undeclared = [
        route
        for route in routes
        if route not in planned_routes and route.replace("[id]", ":id") not in planned_routes
    ]
    table_hits = _count_markers(blob, UNDECLARED_TABLE_MARKERS)
    auth_hits = _count_markers(blob, UNDECLARED_AUTH_MARKERS)
    ownership_hits = _count_markers(blob, UNDECLARED_OWNERSHIP_MARKERS)
    provider_hits = _count_markers(blob, PROVIDER_SIMULATION_MARKERS)
    api_hits = _count_markers(blob, PRIVATE_API_MARKERS)
    vertical_hits = _count_markers(blob, VERTICAL_BACKEND_MARKERS)
    if fixture is not None:
        table_hits.extend(_count_markers(blob, fixture.forbidden))
        table_hits = list(dict.fromkeys(table_hits))
    terms = json.loads((dest / "p2hs" / "ui-terminology.json").read_text(encoding="utf-8"))
    runtime_ids = {
        block_id: CANONICAL_ENTITY_BY_BLOCK[block_id]
        for block_id in ids
        if block_id in CANONICAL_ENTITY_BY_BLOCK
    }
    ui_terms = {
        block_id: str(terms[block_id]["heading"])
        for block_id in terms
        if terms[block_id].get("heading")
    }
    substituted, unavailable_findings = _inspect_unavailable(plan, blob)
    findings = (
        list(unknown) + extras + unavailable_findings + _authority_scan(dest, plan, surfaces)
    )
    if private_count:
        findings.append(f"forbidden_markers={private_count}")
    if undeclared:
        findings.append("undeclared_routes=" + ",".join(undeclared))
    findings.extend(
        table_hits + auth_hits + ownership_hits + provider_hits + api_hits + vertical_hits
    )
    return LiveCell(
        fixture_id=fixture.recipe_id if fixture else dest.name,
        recipe_id=fixture.recipe_id if fixture else "",
        matched_recipe=fixture.recipe_id if fixture else "",
        original_intent=fixture.original_intent if fixture else "",
        block_ids=ids,
        ui_terms=ui_terms,
        runtime_ids=runtime_ids,
        generated_private_backend=private_count + len(vertical_hits) + len(api_hits),
        undeclared_routes=len(undeclared),
        undeclared_tables=len(table_hits),
        undeclared_auth=len(auth_hits),
        undeclared_ownership=len(ownership_hits),
        provider_simulation=len(provider_hits),
        private_apis=len(api_hits),
        vertical_backends=len(vertical_hits),
        findings=findings,
        unavailable_capability_substituted=substituted,
    )


def materialize_live_app(
    dest: Path, fixture: LiveFixture, *, exclusions: list[str] | None = None
) -> tuple[AppBlockPlan, LiveCell]:
    dest.mkdir(parents=True, exist_ok=True)
    spec = live_spec(fixture, exclusions=exclusions)
    intent = product_intent_from_spec(spec, source_prompt=fixture.source_prompt)
    recipe = match_recipe_for_intent(intent)
    if recipe is None or recipe.id != fixture.recipe_id:
        raise AssertionError(
            f"planner missed {fixture.recipe_id}: matched "
            f"{None if recipe is None else recipe.id}"
        )
    brief = compile_discovery_brief(
        spec,
        {"messages": [{"role": "user", "content": fixture.source_prompt}]},
        product_name=spec.product.name or fixture.recipe_id,
        summary=spec.product.first_build_goal,
    )
    write_brief_files(
        dest,
        {"specification": brief, "spec": spec},
        source_prompt=fixture.source_prompt,
    )
    plan = compile_live_plan(spec, source_prompt=fixture.source_prompt)
    if not optional_requires_not_installed(plan):
        raise AssertionError(f"{fixture.recipe_id} installed an optional App Block")
    surfaces = materialize_generated_product(dest, plan=plan, starter=STARTER)
    stamp_ui_terminology(dest, plan, surfaces)
    validate_backend_repo(str(dest))
    mount = inspect_must_mount(dest, surfaces, STARTER)
    cell = inspect_generated_checkout(dest, plan=plan, surfaces=surfaces, fixture=fixture)
    cell.matched_recipe = recipe.id
    hosted_missing = [
        f"{surface.block}#{surface.slot}"
        for surface in surfaces
        if surface.implementation
        and f"{surface.block}#{surface.slot}" in mount["missing_must_surfaces"]
    ]
    if hosted_missing:
        cell.findings.append("missing_must=" + ",".join(hosted_missing))
    if fixture.original_intent not in brief:
        cell.findings.append("original_intent missing from brief")
    if "architecture authority" not in brief:
        cell.findings.append("brief lost composition authority")
    for block_id, heading in fixture.ui_terms.items():
        if block_id not in {item.id for item in plan.blocks}:
            continue
        actual = cell.ui_terms.get(block_id)
        if actual != heading:
            cell.findings.append(f"ui term {block_id}={actual!r} expected {heading!r}")
        entity_key = cell.runtime_ids.get(block_id)
        expected_key = fixture.runtime_ids.get(block_id)
        if expected_key and entity_key != expected_key:
            cell.findings.append(
                f"runtime id {block_id}={entity_key!r} expected {expected_key!r}"
            )
        if heading == entity_key:
            cell.findings.append(f"UI heading collapsed onto runtime id {entity_key}")
    return plan, cell


def prove_negative_intent(dest: Path) -> LiveCell:
    fixture = LIVE_FIXTURES[0]
    plan, cell = materialize_live_app(
        dest, fixture, exclusions=["do not add documents"]
    )
    ids = {block.id for block in plan.blocks}
    cell.negative_intent_respected = "documents" not in ids and "case.management" in ids
    if not cell.negative_intent_respected:
        cell.findings.append(
            "negative intent failed: documents="
            f"{'documents' in ids} case.management={'case.management' in ids}"
        )
    return cell


def prove_configuration_required(dest: Path) -> LiveCell:
    dest.mkdir(parents=True, exist_ok=True)
    plan = quote_plan_without_email()
    materialize_quote_vertical_product(dest, plan=plan)
    validate_backend_repo(str(dest))
    report = assert_generated_quote_artifact(dest)
    blob = _product_source_blob(dest)
    if (dest / "components").is_dir():
        blob += "\n" + "\n".join(
            path.read_text(encoding="utf-8", errors="ignore")
            for path in (dest / "components").rglob("*.vue")
        )
    provider_hits = _count_markers(blob, PROVIDER_SIMULATION_MARKERS)
    substituted = bool(provider_hits) or report["email_status"] != "configuration_required"
    settings = (dest / "pages" / "admin" / "settings.vue").read_text(encoding="utf-8")
    surfaced = "Setup required" in settings and "configuration_required" in blob
    findings = []
    if substituted:
        findings.append("configuration_required email was substituted")
    if not surfaced:
        findings.append("configuration_required email was not surfaced")
    return LiveCell(
        fixture_id="quote_configuration_required",
        recipe_id="forms.quote_request",
        matched_recipe="forms.quote_request",
        original_intent="request a quote",
        block_ids=[block.id for block in plan.blocks],
        ui_terms={"forms.quote_request": str(plan.blocks[0].slots.get("heading") or "")},
        runtime_ids={"forms.quote_request": "quote_request"},
        provider_simulation=len(provider_hits),
        findings=findings,
        configuration_required_substituted=substituted,
    )


def _yes(flag: bool) -> str:
    return "yes" if flag else "no"


def _no(flag: bool) -> str:
    return "no" if not flag else "yes"


def build_exit_block(cells: list[LiveCell]) -> dict[str, str | int]:
    live = [cell for cell in cells if cell.fixture_id in {item.recipe_id for item in LIVE_FIXTURES}]
    negative = next(cell for cell in cells if cell.fixture_id == "student_admissions_negative")
    config = next(cell for cell in cells if cell.fixture_id == "quote_configuration_required")
    findings = [item for cell in cells for item in cell.findings]
    ui_ok = all(
        all(
            cell.ui_terms.get(block_id) == heading
            for block_id, heading in fixture.ui_terms.items()
            if block_id in cell.block_ids
        )
        for fixture, cell in zip(LIVE_FIXTURES, live, strict=True)
    )
    runtime_ok = all(
        all(
            cell.runtime_ids.get(block_id) == entity_key
            for block_id, entity_key in fixture.runtime_ids.items()
            if block_id in cell.block_ids
        )
        for fixture, cell in zip(LIVE_FIXTURES, live, strict=True)
    )
    planner_ok = all(cell.matched_recipe == cell.recipe_id for cell in live)
    intent_ok = all(cell.original_intent for cell in live) and not any(
        "original_intent missing" in item for item in findings
    )
    codegen_ok = not findings and ui_ok and runtime_ok
    private = sum(cell.generated_private_backend for cell in live)
    routes = sum(cell.undeclared_routes for cell in cells)
    tables = sum(cell.undeclared_tables for cell in live)
    auth = sum(cell.undeclared_auth for cell in cells)
    ownership = sum(cell.undeclared_ownership for cell in cells)
    providers = sum(cell.provider_simulation for cell in cells)
    proven = (
        not findings
        and private == 0
        and routes == 0
        and tables == 0
        and auth == 0
        and ownership == 0
        and providers == 0
        and ui_ok
        and runtime_ok
        and planner_ok
        and intent_ok
        and negative.negative_intent_respected is True
        and config.configuration_required_substituted is False
        and all(cell.unavailable_capability_substituted is False for cell in live)
    )
    return {
        "prompt_alignment_live_proven": _yes(proven),
        "discovery_intent_fidelity_proven": _yes(intent_ok),
        "planner_authority_preserved": _yes(planner_ok),
        "codegen_composition_authority_preserved": _yes(codegen_ok),
        "generated_private_backend": private,
        "undeclared_routes": routes,
        "undeclared_tables": tables,
        "undeclared_auth": auth,
        "undeclared_ownership": ownership,
        "provider_simulation": providers,
        "vertical_ui_terms_preserved": _yes(ui_ok),
        "canonical_runtime_ids_preserved": _yes(runtime_ok),
        "negative_intent_respected": _yes(bool(negative.negative_intent_respected)),
        "configuration_required_substituted": _no(config.configuration_required_substituted),
        "unavailable_capability_substituted": _no(
            any(cell.unavailable_capability_substituted for cell in live)
        ),
    }


def prove_live_first_build(base: str | Path) -> dict:
    base = Path(base)
    cells: list[LiveCell] = []
    for fixture in LIVE_FIXTURES:
        _plan, cell = materialize_live_app(base / fixture.recipe_id, fixture)
        cells.append(cell)
    negative = prove_negative_intent(base / "student_admissions_negative")
    negative.fixture_id = "student_admissions_negative"
    cells.append(negative)
    cells.append(prove_configuration_required(base / "quote_configuration_required"))
    exit_block = build_exit_block(cells)
    return {
        "phase": "PROMPT_ALIGNMENT_02",
        "starter": STARTER,
        "documents_ops_08_waited": False,
        "recipe_files_unchanged": sorted(path.name for path in RECIPE_DIR.glob("*.yaml")),
        "cells": [
            {
                "fixture_id": cell.fixture_id,
                "recipe_id": cell.recipe_id,
                "matched_recipe": cell.matched_recipe,
                "block_ids": cell.block_ids,
                "ui_terms": cell.ui_terms,
                "runtime_ids": cell.runtime_ids,
                "findings": cell.findings,
                "generated_private_backend": cell.generated_private_backend,
                "undeclared_routes": cell.undeclared_routes,
                "undeclared_tables": cell.undeclared_tables,
                "undeclared_auth": cell.undeclared_auth,
                "undeclared_ownership": cell.undeclared_ownership,
                "provider_simulation": cell.provider_simulation,
                "configuration_required_substituted": cell.configuration_required_substituted,
                "unavailable_capability_substituted": cell.unavailable_capability_substituted,
                "negative_intent_respected": cell.negative_intent_respected,
            }
            for cell in cells
        ],
        "exit": exit_block,
    }


def write_live_artifact(report: dict, path: Path | None = None) -> Path:
    path = path or LIVE_ARTIFACT
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8", newline="\n")
    return path
