97-documentation/ns_report_publisher/platform_builder.py

from __future__ import annotations

import html
import json
import tempfile
from pathlib import Path
from typing import Any, Iterable

from .workbench_dashboard import build_summary, render_governance, render_issue_center, render_workbench, write_summary


REGISTRY_NAME = "repository-reports.json"
PLATFORM_INDEX_NAME = "platform-index.json"
PLATFORM_CSS_NAME = "platform.css"


CONTEXT_CARDS: tuple[dict[str, Any], ...] = (
    {
        "title": "Wir bauen eine lauffähige Plattform",
        "description": "Der konkrete Einstieg in die Realisierung: vom ersten End-to-End-Szenario über die P1-Baseline bis zum Capability Model.",
        "links": (
            ("SC-001", ("SC-001", "SC-001-USER-CONTEXT-MULTILINGUAL-ARTIFACT-WORKFLOW")),
            ("Architecture Baseline P1", ("BASE-003", "BASE-003-ARCHITECTURE-BASELINE-P1")),
            ("Capability Model", ("CON-014", "CON-014-CAPABILITY-MODEL")),
        ),
    },
    {
        "title": "So denken wir über die Plattform",
        "description": "Der gemeinsame semantische Artefaktbestand, fachliche Fragestellungen und der Zyklus aus Erkenntnis und kontrollierter Verbesserung.",
        "links": (
            ("Domain Theory", ("CON-026", "CON-026-NETZWERKSOLUTION-DOMAIN-THEORY")),
            ("Architecture Principles", ("PRI-001", "PRI-001-ARCHITECTURE-PRINCIPLES")),
            ("Theory Validation", ("SC-006", "SC-006-DOMAIN-THEORY-VALIDATION-SCENARIOS")),
        ),
    },
    {
        "title": "Wissen verstehen und gemeinsam bearbeiten",
        "description": "Einstieg in Meaning, Glossar, Learning Nuggets und die Fähigkeiten für verständliche sowie gemeinsame Wissensarbeit.",
        "links": (
            ("Meaning Model", ("CON-013", "CON-013-MEANING")),
            ("Glossary Architecture", ("CON-017", "CON-017-GLOSSARY-ARCHITECTURE")),
            ("Learning Nuggets", ("CON-021", "CON-021-LEARNING-NUGGET-ARCHITECTURE")),
            ("Gemeinsame Arbeit", ("CAP-011", "CAP-011-GEMEINSAME-ARBEIT-AM-ARTEFAKTMODELL-ERMOEGLICHEN")),
        ),
    },
    {
        "title": "Capabilities und Architektur verstehen",
        "description": "Von fachlichen und technischen Leistungsvermögen über die Capability-Matrix bis zu Building Blocks und Entscheidungen.",
        "links": (
            ("Capability Matrix P1", ("MAP-002", "MAP-002-CAPABILITY-MATRIX-P1")),
            ("Capability-to-Architecture Mapping", ("TRACE-003", "TRACE-003-CAPABILITY-TO-ARCHITECTURE-MAPPING-P1")),
            ("Architecture Map", ("ARCHITECTURE-MAP", "ARCHITECTURE_MAP")),
        ),
    },
    {
        "title": "Theorien und Belastungstests",
        "description": "Warum Gozintographen, Participatory Budgeting, TOGAF, ArchiMate, arc42, SAFe, Simulation und KI dieselbe Grundlage nutzen können.",
        "links": (
            ("Validierungsszenarien", ("SC-006", "SC-006-DOMAIN-THEORY-VALIDATION-SCENARIOS")),
            ("Architecture Discovery Method", ("GOV-007", "GOV-007-ARCHITECTURE-DISCOVERY-METHOD")),
        ),
    },
)


def _read_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))


def _atomic_json(path: Path, data: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
        handle.write(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
        tmp = Path(handle.name)
    tmp.replace(path)


def register_report(runtime_root: Path, entry: dict[str, Any]) -> dict[str, Any]:
    reports_root = runtime_root.resolve() / "web" / "reports"
    registry_path = reports_root / REGISTRY_NAME
    registry: dict[str, Any] = {"repositories": []}
    if registry_path.is_file():
        registry = _read_json(registry_path)
    repositories = [item for item in registry.get("repositories", []) if item.get("repositoryId") != entry["repositoryId"]]
    repositories.append(entry)
    repositories.sort(key=lambda item: (item.get("repositoryType") == "building-block", item.get("repositoryId", "")))
    registry = {"repositories": repositories}
    _atomic_json(registry_path, registry)
    return registry


def _artifact_href_map(reports_root: Path, solution: dict[str, Any]) -> dict[str, str]:
    report_root = str(solution["reportRoot"]).strip("/")
    index = _read_json(reports_root / report_root / "report-index.json")
    result: dict[str, str] = {}
    for target in index.get("artifactTargets", []):
        identifier = str(target.get("identifier", "")).strip()
        page = str(target.get("page", "")).strip("/")
        if identifier and page and identifier not in result:
            result[identifier] = f"{report_root}/{page}"
    return result


def _resolve_artifact_href(targets: dict[str, str], identifiers: Iterable[str], fallback: str) -> str:
    for identifier in identifiers:
        href = targets.get(identifier)
        if href:
            return href
    return fallback


def _render_context_card(card: dict[str, Any], targets: dict[str, str], fallback: str) -> str:
    links = []
    for label, identifiers in card["links"]:
        href = _resolve_artifact_href(targets, identifiers, fallback)
        links.append(
            f'<li><a href="{html.escape(href, quote=True)}">{html.escape(label)}</a></li>'
        )
    return "\n".join((
        '<article class="context-card">',
        f'    <h2>{html.escape(card["title"])}</h2>',
        f'    <p>{html.escape(card["description"])}</p>',
        '    <ul>',
        *(f'        {link}' for link in links),
        '    </ul>',
        '</article>',
    ))


def _repository_link(entry: dict[str, Any], css_class: str = "") -> str:
    href = f"{entry['reportRoot'].rstrip('/')}/{entry['entryPage']}"
    class_attr = f' class="{html.escape(css_class, quote=True)}"' if css_class else ""
    return f'<a{class_attr} href="{html.escape(href, quote=True)}">{html.escape(entry["displayName"])}</a>'


def _platform_css() -> str:
    return """:root { color-scheme: light dark; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
* { box-sizing: border-box; }
body { margin: 0; background: #f5f7fa; color: #202124; line-height: 1.55; }
a { color: #174ea6; text-underline-offset: .18em; }
a:hover { text-decoration-thickness: .14em; }
a:focus-visible { outline: 3px solid currentColor; outline-offset: 3px; border-radius: .2rem; }
.platform-shell { width: min(78rem, calc(100% - 2rem)); margin: 0 auto; padding: 2.2rem 0 3rem; }
.environment-marker { margin: 0; padding: .4rem 1rem; background: #f0b429; color: #241600; font-weight: 800; text-align: center; }
.hero { padding: clamp(1.5rem, 4vw, 3.2rem); border: 1px solid #d9dde3; border-radius: 1rem; background: #fff; box-shadow: 0 8px 28px rgba(31, 41, 55, .08); }
.eyebrow { margin: 0 0 .5rem; color: #5f6368; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; font-size: .8rem; }
h1 { margin: 0; font-size: clamp(2rem, 6vw, 3.8rem); line-height: 1.08; }
.hero-text { max-width: 62rem; font-size: 1.12rem; }
.vision-link { display: inline-flex; align-items: center; min-height: 2.75rem; margin-top: .6rem; padding: .65rem 1rem; border-radius: .55rem; background: #174ea6; color: #fff; font-weight: 750; text-decoration: none; }
.context-section, .repository-section, .status-section { margin-top: 2.4rem; }
.section-intro { max-width: 52rem; color: #5f6368; }
.context-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr)); gap: 1rem; }
.context-card { padding: 1.15rem 1.25rem; border: 1px solid #d9dde3; border-radius: .75rem; background: #fff; }
.context-card h2 { margin: 0 0 .45rem; font-size: 1.2rem; }
.context-card p { margin: 0 0 .75rem; color: #5f6368; }
.context-card ul { margin: 0; padding-left: 1.2rem; }
.context-card li { margin: .32rem 0; }
.status-flow { display: flex; flex-wrap: wrap; gap: .45rem; align-items: center; padding: 1rem; border-left: .35rem solid #174ea6; background: #eaf1fd; border-radius: .4rem; }
.status-flow span:not(:last-child)::after { content: "→"; margin-left: .45rem; color: #5f6368; }
.repository-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr)); gap: .8rem; }
.repository-card { display: block; min-height: 100%; padding: .95rem 1rem; border: 1px solid #d9dde3; border-radius: .6rem; background: #fff; font-weight: 700; }
.repository-card small { display: block; margin-top: .25rem; color: #5f6368; font-weight: 400; }
.workbench-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr)); gap: 1rem; }
.workbench-card { padding: 1.15rem 1.25rem; border: 1px solid #d9dde3; border-radius: .75rem; background: #fff; }
.workbench-card h3 { margin: 0 0 .6rem; }
.workbench-primary { grid-column: span 2; }
.work-list { margin: .45rem 0 .8rem; padding-left: 1.25rem; }
.work-list li { margin: .4rem 0; }
.empty-state { color: #5f6368; font-style: italic; }
.governance-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: .8rem; }
.governance-summary div, .governance-stat { padding: 1rem; border: 1px solid #d9dde3; border-radius: .65rem; background: #fff; }
.governance-stat { display: block; text-decoration: none; color: inherit; }
.governance-stat:hover { border-color: #174ea6; }
.governance-summary strong { display: block; font-size: 1.8rem; line-height: 1.1; }
.issue-section { margin-top: 2.4rem; }
.issue-item { margin: .8rem 0; padding: 1rem 1.2rem; border: 1px solid #d9dde3; border-radius: .65rem; background: #fff; }
.issue-item h3 { margin: 0 0 .25rem; font-size: 1rem; }
.issue-item p { margin: .35rem 0; }
.issue-meta { color: #5f6368; font-size: .9rem; }
.governance-summary span { display: block; margin-top: .25rem; color: #5f6368; }
@media (max-width: 48rem) { .workbench-primary { grid-column: auto; } }
.building-blocks { margin-top: 1rem; }
details { padding: .8rem 1rem; border: 1px solid #d9dde3; border-radius: .6rem; background: #fff; }
summary { cursor: pointer; font-weight: 750; }
footer { margin-top: 2.5rem; padding-top: 1rem; border-top: 1px solid #d9dde3; color: #5f6368; font-size: .9rem; }
@media (prefers-color-scheme: dark) {
  body { background: #11151b; color: #e7eaf0; }
  .hero, .context-card, .repository-card, .workbench-card, .governance-summary div, .governance-stat, .issue-item, details { background: #171c24; border-color: #465064; }
  a { color: #8ab4f8; }
  .vision-link { background: #8ab4f8; color: #10141a; }
  .eyebrow, .hero-text, .section-intro, .context-card p, .repository-card small, .empty-state, .governance-summary span, .issue-meta, footer { color: #b7beca; }
  .status-flow { background: #1d293b; border-color: #8ab4f8; }
}
@media print {
  body { background: #fff; color: #000; }
  .platform-shell { width: auto; padding: 0; }
  .hero, .context-card, .repository-card, details { box-shadow: none; break-inside: avoid; }
  .vision-link { background: none; color: #000; border: 1px solid #000; }
}
"""


def build_platform(runtime_root: Path, target_runtime: str = "") -> dict[str, Any]:
    reports_root = runtime_root.resolve() / "web" / "reports"
    registry_path = reports_root / REGISTRY_NAME
    if not registry_path.is_file():
        raise ValueError(f"Report Registry fehlt: {registry_path}")
    registry = _read_json(registry_path)
    repositories = registry.get("repositories", [])
    if not isinstance(repositories, list) or not repositories:
        raise ValueError("Report Registry enthält keine Repositorys")

    ids: set[str] = set()
    validated: list[dict[str, Any]] = []
    for entry in repositories:
        repository_id = str(entry.get("repositoryId", ""))
        if not repository_id or repository_id in ids:
            raise ValueError(f"Repositoryidentität fehlt oder ist nicht eindeutig: {repository_id}")
        ids.add(repository_id)
        report_root = reports_root / str(entry.get("reportRoot", ""))
        index_path = report_root / "report-index.json"
        if not index_path.is_file():
            raise ValueError(f"Report Index fehlt für {repository_id}: {index_path}")
        report_index = _read_json(index_path)
        pages = report_index.get("pages", [])
        if not pages:
            raise ValueError(f"Report enthält keine Seiten: {repository_id}")
        entry_page = str(entry.get("entryPage") or pages[0])
        if not (report_root / entry_page).is_file():
            raise ValueError(f"Einstiegsseite fehlt für {repository_id}: {entry_page}")
        copy = dict(entry)
        copy["entryPage"] = entry_page
        validated.append(copy)

    solution = next((x for x in validated if x["repositoryType"] == "solution-architecture"), None)
    if solution is None:
        raise ValueError("Solution Architecture ist nicht registriert")

    workspace = next((x for x in validated if x["repositoryType"] == "workspace"), None)
    workbench_summary = build_summary(reports_root, workspace, solution)
    workbench_html = render_workbench(workbench_summary)
    governance_html = render_governance(workbench_summary)

    roots = [x for x in validated if x["repositoryType"] != "building-block"]
    bbs = sorted((x for x in validated if x["repositoryType"] == "building-block"), key=lambda x: x["repositoryId"])
    solution_fallback = f"{solution['reportRoot'].rstrip('/')}/{solution['entryPage']}"
    artifact_targets = _artifact_href_map(reports_root, solution)
    vision_href = _resolve_artifact_href(
        artifact_targets,
        ("Platform_Vision", "DOCS-00-VISION-00_PLATFORM_VISION"),
        solution_fallback,
    )

    context_html = "\n".join(
        _render_context_card(card, artifact_targets, solution_fallback) for card in CONTEXT_CARDS
    )
    root_cards = "\n".join(
        f'<article>{_repository_link(entry, "repository-card")}<small>{html.escape(entry["repositoryType"])}</small></article>'
        for entry in roots
    )
    bb_items = "\n".join(
        f'<li>{_repository_link(entry)}</li>' for entry in bbs
    ) or "<li>Noch keine Building Blocks veröffentlicht.</li>"

    environment_label = {"test": "Testumgebung", "production": "Lokale Produktion"}.get(target_runtime, "")
    environment_marker = (f'<p class="environment-marker" role="status">{html.escape(environment_label)}</p>'
                          if environment_label else "")
    page = f"""<!doctype html>
<html lang="de">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Netzwerksolution – Einstieg</title>
        <link rel="stylesheet" href="{PLATFORM_CSS_NAME}">
    </head>
    <body>
        {environment_marker}
        <main class="platform-shell">
            <section class="hero" aria-labelledby="page-title">
                <p class="eyebrow">Human Context View</p>
                <h1 id="page-title">Netzwerksolution</h1>
                <p class="hero-text">Die Dokumentation beschreibt ein gemeinsames semantisches Artefaktmodell. Diese Startseite führt nicht zuerst durch Ordner, sondern durch die Fragen, mit denen Menschen an die Architektur herangehen.</p>
                <a class="vision-link" href="{html.escape(vision_href, quote=True)}">Zur Vision: Warum gibt es diese Plattform?</a>
            </section>

            <section class="context-section" aria-labelledby="context-title">
                <h2 id="context-title">Was möchtest du verstehen oder tun?</h2>
                <p class="section-intro">Jeder Einstieg führt zu vorhandenen führenden Artefakten. Die Context View erzeugt keine zweite fachliche Wahrheit.</p>
                <div class="context-grid">
{context_html}
                </div>
            </section>

            <section class="status-section" aria-labelledby="status-title">
                <h2 id="status-title">Wo stehen wir?</h2>
                <div class="status-flow" aria-label="Aktueller Architektur- und Realisierungspfad">
                    <span>Vision</span><span>Architecture Baseline P1</span><span>SC-001</span><span>erste vertikale Realisierung</span>
                </div>
            </section>

            <section class="status-section" aria-labelledby="workbench-title">
                <h2 id="workbench-title">Architecture Workbench</h2>
                <p class="section-intro">Automatisch aus den führenden Working Documents erzeugt: aktueller Stand, nächste Schritte und aktive Problemstellungen.</p>
{workbench_html}
            </section>

            <section class="status-section" aria-labelledby="governance-title">
                <h2 id="governance-title">Governance auf einen Blick</h2>
                <p class="section-intro">Zählwerte werden direkt aus <code>90-governance</code> ermittelt. Die Dokumente selbst bleiben die fachliche Quelle.</p>
{governance_html}
            </section>

            <section class="repository-section" aria-labelledby="repository-title">
                <h2 id="repository-title">Dokumentation nach Repository</h2>
                <p class="section-intro">Der klassische strukturelle Einstieg bleibt vollständig erhalten.</p>
                <div class="repository-grid">
{root_cards}
                </div>
                <details class="building-blocks">
                    <summary>Building Blocks anzeigen ({len(bbs)})</summary>
                    <ul>
                        {bb_items}
                    </ul>
                </details>
            </section>
            <footer>Automatisch aus den registrierten Repositoryreports und den semantischen Artefaktzielen der Solution Architecture erzeugt. · © miradlo · <a href="https://miradlo.com/impressum/">Impressum von miradlo</a></footer>
        </main>
    </body>
</html>
"""
    (reports_root / "index.html").write_text(page, encoding="utf-8")
    (reports_root / PLATFORM_CSS_NAME).write_text(_platform_css(), encoding="utf-8")
    write_summary(reports_root / "workbench-summary.json", workbench_summary)
    (reports_root / "workbench-issues.html").write_text(render_issue_center(workbench_summary), encoding="utf-8")
    platform_index = {
        "platform": "Netzwerksolution",
        "entryRepository": "solution-architecture",
        "entryPage": "index.html",
        "contextView": {
            "vision": vision_href,
            "cardCount": len(CONTEXT_CARDS),
            "artifactTargetCount": len(artifact_targets),
        },
        "workbench": workbench_summary,
        "repositories": validated,
    }
    _atomic_json(reports_root / PLATFORM_INDEX_NAME, platform_index)
    return platform_index