97-documentation/publish-semantic-landing.py

#!/usr/bin/env python3
"""Render a deterministic, meaning-first HTML landing page from an explicit JSON model."""
import argparse
import html
import importlib.util
import json
import re
import shutil

PUBLIC_FOOTER = '<footer>© miradlo · <a href="https://miradlo.com/impressum/">Impressum von miradlo</a></footer>'
from pathlib import Path


PLACEHOLDERS = {"", "-", "n/a", "null", "none", "t0", "undefined"}
CODE_SUFFIXES = {".py", ".go", ".js", ".ts", ".json", ".sh"}
MAX_CODE_BYTES = 100 * 1024


def display_title(item, section, findings):
    """Resolve a visible link label without silently exposing technical names.

    ``displayTitle`` is the explicit, German publication title.  A source H1
    may be used only when its language is declared as German; filename- and
    identifier-derived labels are intentionally not fallbacks.
    """
    title = item.get("displayTitle")
    origin = "displayTitle"
    if not title:
        title = item.get("heading")
        origin = "heading"
        if title and item.get("headingLanguage") != "de":
            findings.append({"severity": "ERROR", "code": "NON_GERMAN_FALLBACK", "section": section,
                             "href": item.get("href"), "message": "H1-Fallback ist nicht als deutsch deklariert."})
    if not title:
        findings.append({"severity": "ERROR", "code": "MISSING_DISPLAY_TITLE", "section": section,
                         "href": item.get("href"), "message": "Kein deutscher Anzeige-Titel vorhanden."})
        return None
    if not isinstance(title, str) or title.strip().casefold() in PLACEHOLDERS:
        findings.append({"severity": "ERROR", "code": "PLACEHOLDER_LINK_TEXT", "section": section,
                         "href": item.get("href"), "message": "Anzeige-Titel ist leer oder ein Platzhalter."})
        return None
    if item.get("displayTitleLanguage", "de") != "de":
        findings.append({"severity": "ERROR", "code": "NON_GERMAN_DISPLAY_TITLE", "section": section,
                         "href": item.get("href"), "message": "Anzeige-Titel ist nicht als deutsch deklariert."})
        return None
    if origin == "heading":
        findings.append({"severity": "INFO", "code": "HEADING_FALLBACK_USED", "section": section,
                         "href": item.get("href"), "message": "Deutscher H1 wurde als kontrollierter Fallback verwendet."})
    return title.strip()


def links(items, section, findings):
    rendered = []
    for item in items:
        href = item.get("href")
        if not href:
            findings.append({"severity": "ERROR", "code": "MISSING_LINK_TARGET", "section": section,
                             "href": None, "message": "Linkziel fehlt."})
            continue
        title = display_title(item, section, findings)
        if title:
            rendered.append('<li><a href="{}">{}</a></li>'.format(html.escape(href, quote=True), html.escape(title)))
    return "".join(rendered)


def link_cards(items, section, findings, css_class):
    """Render existing canonical links as readable cards, never as new truth."""
    rendered = []
    for item in items:
        href = item.get("href")
        if not href:
            findings.append({"severity": "ERROR", "code": "MISSING_LINK_TARGET", "section": section,
                             "href": None, "message": "Linkziel fehlt."})
            continue
        title = display_title(item, section, findings)
        if not title:
            continue
        description = item.get("description", "")
        rendered.append(
            '<a class="{}" href="{}"><strong>{}</strong>{}</a>'.format(
                css_class,
                html.escape(href, quote=True),
                html.escape(title),
                '<span>{}</span>'.format(html.escape(description)) if description else "",
            )
        )
    return "\n".join(rendered)


def semantic_landing_map(landing, findings):
    """Render explicit landing nodes and feedback edges, never list positions."""
    nodes = landing.get("nodes", [])
    edges = landing.get("feedback", [])
    required_ids = {"vision", "capability", "building-block", "code", "runtime-evidence"}
    node_ids = {item.get("id") for item in nodes}
    if node_ids != required_ids:
        raise ValueError("semanticLanding benötigt genau Vision, Capability, Building Block, Code und Runtime & Evidence.")
    by_id, rendered_nodes = {}, []
    layer_y = {"meaning": 28, "artifact": 94, "context-time": 160}
    for index, item in enumerate(nodes):
        identifier, label, layer, href = item.get("id"), item.get("label"), item.get("layer"), item.get("href")
        title = display_title(item, "semanticLanding", findings)
        if not isinstance(identifier, str) or not re.fullmatch(r"[a-z-]+", identifier):
            raise ValueError("semanticLanding-Knoten benötigt eine sichere ID.")
        if not isinstance(label, str) or not label or layer not in layer_y or not href or not title:
            raise ValueError("semanticLanding-Knoten benötigt Label, Ebene, Ziel und deutschen Anzeige-Titel.")
        x, y = 18 + index * 178, layer_y[layer]
        by_id[identifier] = {"x": x, "y": y, "href": href, "label": label, "title": title}
        rendered_nodes.append('<a class="map-node-link" href="{href}" aria-label="{label}: {title}"><rect x="{x}" y="{y}" width="150" height="52" rx="10"/><text x="{tx}" y="{label_y}">{label}</text><text class="map-detail" x="{tx}" y="{title_y}">{title}</text></a>'.format(href=html.escape(href, quote=True), label=html.escape(label), title=html.escape(title), x=x, y=y, tx=x + 75, label_y=y + 21, title_y=y + 39))
    sequence = ["vision", "capability", "building-block", "code", "runtime-evidence"]
    arrows = "".join('<path class="map-arrow" d="M{sx} {sy} L{tx} {ty}" marker-end="url(#arrow)"/>'.format(sx=by_id[left]["x"] + 150, sy=by_id[left]["y"] + 26, tx=by_id[right]["x"] - 4, ty=by_id[right]["y"] + 26) for left, right in zip(sequence, sequence[1:]))
    rendered_edges, fallback_edges = [], []
    for edge in edges:
        source, target, label = edge.get("from"), edge.get("to"), edge.get("label")
        if source not in by_id or target not in by_id:
            raise ValueError("semanticLanding-Feedback verweist auf einen unbekannten Knoten.")
        if not isinstance(label, str) or not label:
            raise ValueError("semanticLanding-Feedback benötigt einen deutschen Namen.")
        start, end = by_id[source], by_id[target]
        href = edge.get("href", end["href"])
        if not isinstance(href, str) or not href:
            raise ValueError("semanticLanding-Feedback benötigt ein Ziel.")
        # The long Code → Capability return must run below Building Block,
        # never through its text or click area.
        control_y = 246 if source == "runtime-evidence" or (source == "code" and target == "capability") else max(start["y"], end["y"]) + 74
        rendered_edges.append('<a class="map-feedback-link" href="{href}" aria-label="{label}: {source} zu {target}"><path class="map-feedback" d="M{sx} {sy} Q{cx} {cy} {tx} {ty}" marker-end="url(#arrow)"/></a>'.format(href=html.escape(href, quote=True), label=html.escape(label), source=html.escape(start["label"]), target=html.escape(end["label"]), sx=start["x"] + 75, sy=start["y"] + 52, cx=(start["x"] + end["x"]) // 2 + 75, cy=control_y, tx=end["x"] + 75, ty=end["y"] + 52))
        fallback_edges.append('<li><a href="{href}">{label}: {source} → {target}</a></li>'.format(href=html.escape(href, quote=True), label=html.escape(label), source=html.escape(start["label"]), target=html.escape(end["label"])))
    fallback_nodes = ''.join('<li><a href="{href}"><strong>{label}</strong>: {title}</a></li>'.format(href=html.escape(item["href"], quote=True), label=html.escape(item["label"]), title=html.escape(item["title"])) for item in by_id.values())
    return '\n'.join([
        '<section class="section semantic-map-section" id="semantische-landkarte"><h2>Von der Vision zur überprüfbaren Wirkung</h2><p class="section-intro">Meaning begründet, Artefakte realisieren und Runtime & Evidence zeigen Wirkung im Kontext. Jede Rückkopplung führt nachvollziehbar zur passenden fachlichen Ebene zurück.</p>',
        '<svg class="semantic-map" viewBox="0 0 910 270" role="img" aria-labelledby="map-title map-desc"><title id="map-title">Semantische Landkarte</title><desc id="map-desc">Klickbare Ebenen für Meaning, Artifact und Kontext/Zeit. Der Vorwärtsweg führt von Vision bis Runtime. Explizite Rückkopplungen führen zu den jeweils betroffenen Ebenen zurück.</desc><defs><marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z"/></marker></defs>{arrows}{nodes}{edges}</svg>'.format(arrows=arrows, nodes=''.join(rendered_nodes), edges=''.join(rendered_edges)),
        '<nav class="semantic-map-fallback" aria-label="Textalternative zur semantischen Landkarte"><h3>Einstiege</h3><ol>{}</ol><h3>Rückkopplungen</h3><ul>{}</ul></nav></section>'.format(fallback_nodes, ''.join(fallback_edges)),
    ])


def legacy_semantic_landing_map(items, pdca_items, findings):
    """Temporary read compatibility for already deployed Runtime input models.

    New runtime models must use ``semanticLanding``. This fallback prevents an
    Engineering-Tools-only import from breaking the automatic report rebuild
    before the corresponding Runtime input model is delivered.
    """
    findings.append({"severity": "INFO", "code": "LEGACY_SEMANTIC_LANDING_MODEL", "section": "semanticLanding",
                     "href": None, "message": "Runtime-Eingabemodell enthält noch keine explizite semanticLanding-Struktur."})
    labels = ["Vision", "Capability", "Building Block", "Code", "Runtime & Evidence"]
    nodes = []
    for index, label in enumerate(labels):
        item = items[index] if index < len(items) else None
        if item:
            href, title = item.get("href", ""), display_title(item, "semanticLanding", findings)
        else:
            href, title = "#technische-details", label
        if href and title:
            nodes.append((18 + index * 178, label, title, href))
    svg_nodes = "".join('<a class="map-node-link" href="{href}" aria-label="{label}: {title}"><rect x="{x}" y="54" width="150" height="62" rx="10"/><text x="{tx}" y="79">{label}</text><text class="map-detail" x="{tx}" y="99">{title}</text></a>'.format(href=html.escape(href, quote=True), label=html.escape(label), title=html.escape(title), x=x, tx=x + 75) for x, label, title, href in nodes)
    arrows = "".join('<path class="map-arrow" d="M{} 85 H{}" marker-end="url(#arrow)"/>'.format(x + 150, x + 174) for x, *_ in nodes[:-1])
    feedback_href = pdca_items[2].get("href") if len(pdca_items) > 2 else "#pdca"
    fallback = ''.join('<li><a href="{}"><strong>{}</strong>: {}</a></li>'.format(html.escape(href, quote=True), html.escape(label), html.escape(title)) for _, label, title, href in nodes)
    return '\n'.join([
        '<section class="section semantic-map-section" id="semantische-landkarte"><h2>Von der Vision zur überprüfbaren Wirkung</h2><p class="section-intro">Die explizite Landing-Page-Struktur wird mit dem nächsten Runtime-Eingabemodell aktiviert.</p>',
        '<svg class="semantic-map" viewBox="0 0 910 190" role="img" aria-labelledby="map-title map-desc"><title id="map-title">Semantische Landkarte</title><desc id="map-desc">Übergangsansicht der klickbaren Kette von Vision bis Runtime und Evidence.</desc><defs><marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z"/></marker></defs>{arrows}{nodes}<a class="map-feedback-link" href="{href}" aria-label="Feedback zu PDCA öffnen"><path class="map-feedback" d="M840 122 V164 H90 V126" marker-end="url(#arrow)"/></a></svg>'.format(arrows=arrows, nodes=svg_nodes, href=html.escape(feedback_href, quote=True)),
        '<nav class="semantic-map-fallback" aria-label="Textalternative zur semantischen Landkarte"><h3>Textalternative</h3><ol>{}</ol></nav></section>'.format(fallback),
    ])


def vision_cards(items):
    cards = []
    for item in items:
        identifier = item.get("id", "")
        title = item.get("title", "")
        summary = item.get("summary", "")
        relation = item.get("relation", "")
        if not re.fullmatch(r"[A-Za-z0-9._-]+", identifier):
            raise ValueError("Visionskarte benötigt eine sichere ID.")
        if not title or not summary or not relation:
            raise ValueError("Visionskarte benötigt Titel, Kurzbeschreibung und Beziehung.")
        cards.append(
            '<a class="vision-card" href="visions/{id}.html"><span class="relation">{relation}</span><strong>{title}</strong><span>{summary}</span><em>Details und Quelle öffnen</em></a>'.format(
                id=html.escape(identifier, quote=True), relation=html.escape(relation),
                title=html.escape(title), summary=html.escape(summary),
            )
        )
    return "\n".join(cards)


def vision_detail(item):
    identifier = item["id"]
    source_href = detail_source_href(item.get("sourceHref"))
    source_title = item.get("sourceTitle", "Kanonische Quelle")
    if not source_href:
        raise ValueError("Visionsdetail benötigt eine kanonische Quelle.")
    related = item.get("relatedTo", "")
    relation = item["relation"]
    return "\n".join([
        "<!doctype html>", '<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">',
        "<title>{}</title><style>body{{font-family:system-ui,sans-serif;line-height:1.6;margin:0;background:#f5f7fb;color:#1b2638}}main{{width:min(48rem,calc(100% - 2rem));margin:0 auto;padding:2rem 0 4rem}}a{{color:#075a9c;text-underline-offset:.18em}}.back{{font-weight:700}}article{{margin-top:1rem;padding:1.5rem;border:1px solid #d5dde7;border-radius:.9rem;background:#fff}}.relation{{color:#a04408;font-weight:750;text-transform:uppercase;font-size:.82rem;letter-spacing:.05em}}.note{{padding:1rem;border-left:.4rem solid #e36b20;background:#fff7ef}}@media(prefers-color-scheme:dark){{body{{background:#101820;color:#e6edf5}}article{{background:#18232f;border-color:#405469}}a{{color:#8cc8ff}}.note{{background:#2b241c}}}}</style></head><body><main>".format(html.escape(item["title"])),
        '<p><a class="back" href="../index.html">← Zurück zur Visionslandkarte</a></p>',
        '<article><p class="relation">{}</p><h1>{}</h1><p>{}</p><h2>Einordnung</h2><p>{}</p>{}</article>'.format(
            html.escape(relation), html.escape(item["title"]), html.escape(item["summary"]),
            html.escape(item.get("detail", item["summary"])),
            '<p><a href="{}">{} öffnen</a></p>'.format(html.escape(source_href, quote=True), html.escape(source_title)),
        ),
        '<p class="note">{}</p>'.format(html.escape(related)) if related else "",
        PUBLIC_FOOTER + "</main></body></html>", "",
    ])


def knowledge_cards(items):
    cards = []
    for item in items:
        identifier = item.get("id", "")
        kind = item.get("kind", "")
        title = item.get("title", "")
        preview = item.get("preview", "")
        if not re.fullmatch(r"[A-Za-z0-9._-]+", identifier) or not kind or not title or not preview:
            raise ValueError("Wissensverweis benötigt ID, Typ, Titel und Vorschau.")
        cards.append('<a class="knowledge-ref" data-knowledge-kind="{kind}" data-knowledge-id="{id}" href="wissen/{id}.html" aria-describedby="preview-{id}"><span class="knowledge-kind">{kind}</span><strong>{title}</strong><span class="knowledge-preview" id="preview-{id}" role="tooltip">{preview}</span><em>Vorschau lesen · Detail öffnen</em></a>'.format(id=html.escape(identifier, quote=True), kind=html.escape(kind, quote=True), title=html.escape(title), preview=html.escape(preview)))
    return "\n".join(cards)


def detail_source_href(source_href):
    """Project a landing-page source link into its one-level-deeper detail page."""
    if not source_href or source_href.startswith(("#", "/", "http://", "https://")):
        return source_href
    # Older Runtime input already expressed the detail-page projection.  Keep
    # it readable during overlay imports while new input stays relative to the
    # reports entrypoint.
    if source_href.startswith("../"):
        return source_href
    return "../" + source_href

def knowledge_detail(item):
    source_href = detail_source_href(item.get("sourceHref"))
    if not source_href:
        raise ValueError("Wissensdetail benötigt eine kanonische Quelle.")
    return "\n".join([
        "<!doctype html>", '<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">',
        "<title>{}</title><style>body{{font-family:system-ui,sans-serif;line-height:1.6;margin:0;background:#f5f7fb;color:#1b2638}}main{{width:min(48rem,calc(100% - 2rem));margin:0 auto;padding:2rem 0 4rem}}a{{color:#075a9c;text-underline-offset:.18em}}article{{margin-top:1rem;padding:1.5rem;border:1px solid #d5dde7;border-radius:.9rem;background:#fff}}.kind{{color:#a04408;font-weight:750;text-transform:uppercase;font-size:.82rem;letter-spacing:.05em}}@media(prefers-color-scheme:dark){{body{{background:#101820;color:#e6edf5}}article{{background:#18232f;border-color:#405469}}a{{color:#8cc8ff}}}}</style></head><body><main>".format(html.escape(item["title"])),
        '<p><a href="../index.html">← Zurück zur Wissensübersicht</a></p>',
        '<article><p class="kind">{}</p><h1>{}</h1><p>{}</p><h2>Einordnung</h2><p>{}</p><p><a href="{}">{} öffnen</a></p></article>'.format(html.escape(item["kind"]), html.escape(item["title"]), html.escape(item["preview"]), html.escape(item.get("detail", item["preview"])), html.escape(source_href, quote=True), html.escape(item.get("sourceTitle", "Kanonische Quelle"))),
        PUBLIC_FOOTER + "</main></body></html>", "",
    ])

def context_cards(items):
    cards=[]
    for item in items:
        identifier=item.get("id", "")
        if not re.fullmatch(r"[A-Za-z0-9._-]+", identifier) or not item.get("title") or not item.get("summary"):
            raise ValueError("Kontextwirkung benötigt sichere ID, Titel und Kurzbeschreibung.")
        cards.append('<a class="context-card" data-context-impact-id="{}" href="context/{}.html"><strong>{}</strong><span>{}</span><em>Kontext und PDCA öffnen</em></a>'.format(html.escape(identifier,quote=True),html.escape(identifier,quote=True),html.escape(item["title"]),html.escape(item["summary"])))
    return "\n".join(cards)

def context_detail(item):
    references=item.get("references", [])
    if not references:
        raise ValueError("Kontextwirkung benötigt direkte Referenzen.")
    links="".join('<li><a href="{}">{}</a></li>'.format(html.escape(reference["href"],quote=True),html.escape(reference["title"])) for reference in references)
    return "\n".join(["<!doctype html>",'<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>{}</title></head><body><main>'.format(html.escape(item["title"])), '<p><a href="../index.html">← Zurück zur Veröffentlichung</a></p>', '<h1>{}</h1><p>{}</p><h2>Kontext und PDCA</h2><dl><dt>Plan</dt><dd>{}</dd><dt>Do</dt><dd>{}</dd><dt>Check</dt><dd>{}</dd><dt>Act</dt><dd>{}</dd></dl><h2>Direkt betroffene Wissenselemente</h2><ul>{}</ul>'.format(html.escape(item["title"]),html.escape(item["narrative"]),html.escape(item["pdca"]["plan"]),html.escape(item["pdca"]["do"]),html.escape(item["pdca"]["check"]),html.escape(item["pdca"]["act"]),links),PUBLIC_FOOTER,"</main></body></html>",""])

def regression_cards(items):
    return "".join('<a class="context-card" data-regression-id="{}" href="regressions/{}.html"><strong>{}</strong><span>{}: {}</span><em>Test, Ergebnis und Rückführung öffnen</em></a>'.format(html.escape(item["id"],quote=True),html.escape(item["id"],quote=True),html.escape(item["title"]),html.escape(item["status"]),html.escape(item["verifiedSubject"])) for item in items)

def regression_detail(item):
    links="".join('<li><a href="{}">{}</a></li>'.format(html.escape(x["href"],quote=True),html.escape(x["title"])) for x in item["references"])
    return "\n".join(["<!doctype html>",'<html lang="de"><head><meta charset="utf-8"><title>{}</title></head><body><main>'.format(html.escape(item["title"])), '<p><a href="../index.html">← Zurück zur Veröffentlichung</a></p>', '<h1>{}</h1><dl><dt>Ergebnis</dt><dd>{}</dd><dt>Geprüfter Bezug</dt><dd>{}</dd><dt>Rückführung</dt><dd>{}</dd></dl><ul>{}</ul></main></body></html>'.format(html.escape(item["title"]),html.escape(item["status"]),html.escape(item["verifiedSubject"]),html.escape(item["feedback"]),links),""])

def code_view(reference):
    path = reference.get("path", "")
    content = reference.get("content", "")
    artifact_id = reference.get("id", "")
    if not artifact_id or not re.fullmatch(r"[A-Za-z0-9._-]+", artifact_id):
        raise ValueError("Code-Referenz benötigt eine sichere Artefakt-ID.")
    if not isinstance(path, str) or path.startswith(("/", "~")) or ".." in Path(path).parts or Path(path).suffix not in CODE_SUFFIXES:
        raise ValueError("Code-Referenz ist nicht veröffentlichungsfähig.")
    if not isinstance(content, str) or len(content.encode("utf-8")) > MAX_CODE_BYTES:
        raise ValueError("Code-Inhalt fehlt oder überschreitet die Größenbegrenzung.")
    if any(marker in path.lower() for marker in (".env", "secret", "credential", "password")):
        raise ValueError("Sensible Code-Referenz ist nicht veröffentlichungsfähig.")
    language = reference.get("language") or Path(path).suffix[1:]
    revision = reference.get("revision", "unbekannt")
    back = reference.get("backLink", "../index.html")
    role = reference.get("role", "Ausführbarer Bestandteil der Veröffentlichung")
    stable_location = reference.get("stableLocation", path)
    verified_by = reference.get("verifiedBy", "Kein zugeordneter Testnachweis")
    context_impact = reference.get("contextImpact", "Kontextwirkung ist im zugehörigen Nachweis beschrieben.")
    return "\n".join(["<!doctype html>", '<html lang="de"><head><meta charset="utf-8">',
        f"<title>{html.escape(path)}</title></head><body><main>",
        f"<p><a href=\"{html.escape(back, quote=True)}\">Zurück zur Veröffentlichung</a></p>",
        f"<h1>{html.escape(path)}</h1><dl><dt>Artefakt</dt><dd>{html.escape(artifact_id)}</dd><dt>Rolle</dt><dd>{html.escape(role)}</dd><dt>Stabile Stelle</dt><dd>{html.escape(stable_location)}</dd><dt>Sprache</dt><dd>{html.escape(language)}</dd><dt>Revision/Herkunft</dt><dd>{html.escape(revision)}</dd><dt>Geprüft durch</dt><dd>{html.escape(verified_by)}</dd><dt>Kontextwirkung</dt><dd>{html.escape(context_impact)}</dd></dl>",
        f"<pre><code class=\"language-{html.escape(language, quote=True)}\">{html.escape(content)}</code></pre>", "</main></body></html>", ""])


def test_view(reference):
    return "\n".join(["<!doctype html>", '<html lang="de"><head><meta charset="utf-8">',
        f"<title>{html.escape(reference['path'])}</title></head><body><main>",
        '<p><a href="../index.html">Zurück zur Veröffentlichung</a></p>',
        f"<h1>{html.escape(reference['title'])}</h1><dl><dt>Testfall</dt><dd>{html.escape(reference['id'])}</dd><dt>Testebene</dt><dd>{html.escape(reference['level'])}</dd><dt>Geprüfter Bezug</dt><dd>{html.escape(reference['verifiedSubject'])}</dd><dt>Evidenz</dt><dd>{html.escape(reference['evidence'])}</dd></dl>",
        f"<p><strong>Pfad:</strong> {html.escape(reference['path'])}</p><pre><code>{html.escape(reference['content'])}</code></pre>", "</main></body></html>", ""])

def collect_code_references(sources):
    references=[]
    for name, root in sources.items():
        for path in sorted(root.rglob("*")):
            if not path.is_file() or path.suffix not in CODE_SUFFIXES: continue
            relative=path.relative_to(root).as_posix()
            if any(part in {"__pycache__", ".git"} for part in path.parts) or any(x in relative.lower() for x in (".env", "secret", "credential", "password")): continue
            if path.stat().st_size > MAX_CODE_BYTES: continue
            artifact_id=(name+"-"+relative.replace("/","-").replace(".","-")).replace("_","-")
            references.append({"id":artifact_id,"path":f"{name}:{relative}","language":path.suffix[1:],"revision":"target snapshot","backLink":"../index.html","content":path.read_text(encoding="utf-8",errors="replace")})
    return references


def collect_test_references(sources):
    references = []
    for name, root in sources.items():
        for path in sorted(root.rglob("*")):
            if not path.is_file() or path.suffix not in CODE_SUFFIXES or path.stat().st_size > MAX_CODE_BYTES:
                continue
            relative = path.relative_to(root).as_posix()
            if any(part in {"__pycache__", ".git"} for part in path.parts):
                continue
            artifact_id = (name + "-" + relative.replace("/", "-").replace(".", "-")).replace("_", "-")
            references.append({"id": artifact_id, "path": f"{name}:{relative}", "title": f"Testfall: {relative}", "level": "Automatisierter Regressionstest", "verifiedSubject": "Aktuelle veröffentlichte Systemwelt", "evidence": "Ausführung im SE-0068-Regressionstest", "content": path.read_text(encoding="utf-8", errors="replace")})
    return references


def public_navigation(presentation):
    if not presentation:
        return ""
    return ('<nav class="public-navigation" aria-label="Öffentliche Seiten">'
            '<a href="elevator-pitch.html">Elevator Pitch</a>'
            '<a href="ueber-mich.html">Über mich</a></nav>')


def render(model, target_runtime="", public_presentation=None):
    required = ("title", "elevatorPitch", "navigation", "glossaryHint", "relationshipHint", "technicalReports", "knowledgeStructure", "pdca")
    missing = [key for key in required if not model.get(key)]
    if missing:
        raise ValueError("Pflichtfelder fehlen: " + ", ".join(missing))
    findings = []
    navigation = links(model["navigation"], "navigation", findings)
    reports = links(model["technicalReports"], "technicalReports", findings)
    structure = link_cards(model["knowledgeStructure"], "knowledgeStructure", findings, "structure-card")
    stages = link_cards(model["pdca"]["stages"], "pdca", findings, "pdca-card")
    semantic_map = (semantic_landing_map(model["semanticLanding"], findings)
                    if model.get("semanticLanding")
                    else legacy_semantic_landing_map(model["knowledgeStructure"], model["pdca"]["stages"], findings))
    visions = vision_cards(model.get("visionMap", []))
    knowledge = knowledge_cards(model.get("knowledgeReferences", []))
    contexts = context_cards(model.get("contextImpacts", []))
    regressions = regression_cards(model.get("regressionEvidence", []))
    help_cards = link_cards(model.get("help", []), "help", findings, "help-card")
    levels = link_cards(model.get("knowledgeLevels", model["navigation"]), "knowledgeLevels", findings, "structure-card")
    code_links = "".join('<li><a data-code-artifact="{}" href="code/{}.html">{}</a> — {}</li>'.format(html.escape(item["id"], quote=True), html.escape(item["id"], quote=True), html.escape(item["path"]), html.escape(item.get("role", "Codeansicht"))) for item in model.get("codeReferences", []))
    test_links = "".join('<li><a data-test-artifact="{}" href="tests/{}.html">{}</a> — {}</li>'.format(html.escape(item["id"], quote=True), html.escape(item["id"], quote=True), html.escape(item["title"]), html.escape(item["verifiedSubject"])) for item in model.get("testReferences", []))
    blocking = [finding for finding in findings if finding["severity"] == "ERROR"]
    if blocking:
        raise ValueError("Nicht publizierbare Linktexte: " + ", ".join(finding["code"] for finding in blocking))
    css = """
:root { color-scheme: light dark; font-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; color: #1b2638; background: #f5f7fb; }
* { box-sizing: border-box; }
body { margin: 0; line-height: 1.6; }
a { color: #075a9c; text-underline-offset: .18em; }
a:focus-visible { outline: 3px solid #e36b20; outline-offset: 3px; border-radius: .2rem; }
.shell { width: min(74rem, calc(100% - 2rem)); margin: 0 auto; padding: 2rem 0 4rem; }
.environment-marker { margin: 0; padding: .4rem 1rem; background: #f0b429; color: #241600; font-weight: 800; text-align: center; }
.public-navigation { display:flex; flex-wrap:wrap; gap:.7rem; margin:1rem 0 0; }.public-navigation a { padding:.55rem .8rem; border:1px solid #075a9c; border-radius:.5rem; background:#fff; font-weight:750; }
.hero { padding: clamp(1.5rem, 5vw, 4rem); border-radius: 1.2rem; color: #fff; background: linear-gradient(128deg, #123d63, #0a6d86); box-shadow: 0 1rem 2.5rem rgba(13, 49, 76, .2); }
.eyebrow { margin: 0 0 .5rem; font-size: .82rem; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; color: #b9e8ee; }
h1 { max-width: 18ch; margin: 0; font-size: clamp(2.25rem, 7vw, 4.8rem); line-height: 1.04; }
h2 { margin: 0 0 .65rem; font-size: clamp(1.35rem, 3vw, 2rem); line-height: 1.2; }
.lead { max-width: 54rem; margin: 1.25rem 0 0; font-size: clamp(1.08rem, 2vw, 1.3rem); }
.hero-note { margin: 1.25rem 0 0; font-weight: 650; color: #e3f6f6; }
.section { margin-top: 2.5rem; padding: clamp(1.1rem, 3vw, 2rem); border: 1px solid #d5dde7; border-radius: .9rem; background: #fff; box-shadow: 0 .4rem 1.3rem rgba(26, 44, 70, .06); }
.section-intro { max-width: 52rem; margin: 0 0 1.2rem; color: #46576b; }
.structure { display: grid; grid-template-columns: repeat(5, minmax(10rem, 1fr)); gap: .7rem; }
.semantic-map { width:100%; height:auto; overflow:visible; }.map-node-link rect{fill:#f3fbfa;stroke:#0a6d86;stroke-width:1.5}.map-node-link:hover rect,.map-node-link:focus rect{fill:#dff4f1;stroke-width:3}.map-node-link text{fill:#075a9c;font-size:14px;font-weight:750;text-anchor:middle}.map-node-link .map-detail{fill:#46576b;font-size:10px;font-weight:500}.map-arrow,.map-feedback{fill:none;stroke:#e36b20;stroke-width:2}.map-feedback-link text{fill:#a04408;font-size:12px;font-weight:750;text-anchor:middle}.semantic-map-fallback{margin-top:1rem;padding:1rem;border-left:.35rem solid #0a6d86;background:#f3fbfa}.semantic-map-fallback h3{margin-top:0}.help-card{display:block;padding:1rem;border:1px solid #0a6d86;border-radius:.7rem;background:#f3fbfa;color:inherit;text-decoration:none}.help-card strong,.help-card span{display:block}.help-card strong{color:#075a9c}.help-card span{margin-top:.4rem;color:#46576b}
.structure-card, .pdca-card { display: block; min-height: 100%; padding: 1rem; border: 1px solid #cbd8e5; border-radius: .7rem; background: #f8fbff; text-decoration: none; color: inherit; }
.structure-card:hover, .pdca-card:hover { border-color: #075a9c; box-shadow: 0 .3rem .9rem rgba(7, 90, 156, .14); }
.structure-card strong, .pdca-card strong { display: block; color: #075a9c; }
.structure-card span, .pdca-card span { display: block; margin-top: .45rem; font-size: .93rem; color: #46576b; }
.pdca { border-left: .55rem solid #e36b20; }
.pdca-flow { display: grid; grid-template-columns: repeat(4, minmax(10rem, 1fr)); gap: .7rem; }
.pdca-card { background: #fffaf4; }
.pdca-card strong::before { content: \"↻ \"; color: #bf4b05; }
.vision-map { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: .8rem; }
.vision-card { display: block; min-height: 100%; padding: 1rem; border: 1px solid #b9d1d7; border-radius: .7rem; background: #f3fbfa; color: inherit; text-decoration: none; }
.vision-card:hover { border-color: #0a6d86; box-shadow: 0 .3rem .9rem rgba(10, 109, 134, .14); }
.vision-card strong, .vision-card span, .vision-card em { display: block; }.vision-card strong { margin:.25rem 0; color:#075a9c; }.vision-card span { color:#46576b; }.vision-card em { margin-top:.7rem; color:#075a9c; font-style:normal; font-weight:700; font-size:.9rem; }.vision-card .relation { color:#a04408; font-weight:750; font-size:.78rem; letter-spacing:.05em; text-transform:uppercase; }
.knowledge-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(15rem,1fr)); gap:.8rem; }.knowledge-ref { display:block; min-height:100%; padding:1rem; border:1px solid #b9d1d7; border-radius:.7rem; background:#f7fbff; color:inherit; text-decoration:none; }.knowledge-ref:hover,.knowledge-ref:focus-visible { border-color:#075a9c; box-shadow:0 .3rem .9rem rgba(7,90,156,.14); }.knowledge-ref strong,.knowledge-ref em,.knowledge-ref span { display:block; }.knowledge-ref strong{margin:.25rem 0;color:#075a9c}.knowledge-kind{color:#a04408;font-size:.78rem;font-weight:750;letter-spacing:.05em;text-transform:uppercase}.knowledge-preview{max-height:0;margin-top:0;overflow:hidden;opacity:0;color:#46576b;transition:max-height .18s ease,margin .18s ease,opacity .18s ease}.knowledge-ref:hover .knowledge-preview,.knowledge-ref:focus-visible .knowledge-preview{max-height:10rem;margin-top:.45rem;opacity:1}.knowledge-ref em{margin-top:.7rem;color:#075a9c;font-size:.9rem;font-style:normal;font-weight:700}
.context-card { display:block; padding:1rem; border:1px solid #e5c4a5; border-radius:.7rem; background:#fffaf4; color:inherit; text-decoration:none; }.context-card strong,.context-card span,.context-card em { display:block; }.context-card strong,.context-card em { color:#a04408; }.context-card span { margin:.45rem 0; color:#46576b; }
.entry-list { columns: 2 18rem; padding-left: 1.2rem; margin: 0; }
.entry-list li { margin: .55rem 0; break-inside: avoid; }
.quiet { background: #f3f8fa; }
details { margin-top: 1rem; padding: .9rem 1rem; border: 1px solid #d5dde7; border-radius: .65rem; background: #f8fbff; }
summary { cursor: pointer; font-weight: 750; color: #123d63; }
details ul { margin-bottom: 0; }
.code-list { columns: 2 23rem; overflow-wrap: anywhere; }
footer { margin-top: 2rem; color: #53657a; font-size: .92rem; text-align: center; }
@media (max-width: 56rem) { .structure, .pdca-flow { grid-template-columns: 1fr; } .entry-list, .code-list { columns: 1; } }
@media (max-width: 56rem) { .semantic-map { display:none; } }
@media (prefers-color-scheme: dark) { :root, body { background: #101820; color: #e6edf5; } .section, .structure-card, .pdca-card, .vision-card, .knowledge-ref, details { background: #18232f; border-color: #405469; } .quiet { background: #13252b; } .section-intro, .structure-card span, .pdca-card span, .vision-card span, .knowledge-preview, footer { color: #bdc9d7; } a, .structure-card strong, .pdca-card strong, .vision-card strong, .vision-card em, .knowledge-ref strong, .knowledge-ref em, summary { color: #8cc8ff; } .pdca-card { background: #2b241c; } }
"""
    return "\n".join([
        "<!doctype html>", '<html lang="de">', "<head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>{}</title><style>{}</style></head>".format(html.escape(model["title"]), css),
        "<body>{}<main class=\"shell\">".format(
            "<p class=\"environment-marker\" role=\"status\">{}</p>".format(
                html.escape({"test": "Testumgebung", "production": "Lokale Produktion"}.get(target_runtime, ""))
            ) if target_runtime in {"test", "production"} else ""
        ),
        "<header class=\"hero\" id=\"elevator-pitch\"><p class=\"eyebrow\">Wissen gemeinsam weiterentwickeln</p><h1>{}</h1><p class=\"lead\">{}</p><p class=\"hero-note\">Menschen arbeiten kontinuierlich an diesem Wissen, präzisieren es gemeinsam und führen Feedback als nachvollziehbare Verbesserung zurück.</p></header>".format(html.escape(model["title"]), html.escape(model["elevatorPitch"])),
        public_navigation(public_presentation),
        semantic_map,
        "<section class=\"section\" id=\"hilfe\"><h2>Hilfe zur Wissensbasis</h2><p class=\"section-intro\">Begriffe, Lernimpulse und Verweise helfen direkt im Lesefluss. Die Hilfe erklärt Vorschau, Klick, Tastatur und die Rückkopplung von Beobachtungen.</p><div class=\"knowledge-grid\">{}</div></section>".format(help_cards) if help_cards else "",
        "<nav class=\"section\" aria-label=\"Die Ebenen des Wissens\"><h2>Die Ebenen des Wissens</h2><p class=\"section-intro\">Jede Ebene besitzt eine eigene Verantwortung und einen führenden Einstieg.</p><div class=\"structure\">{}</div></nav>".format(levels),
        "<footer>Automatisch aus vorhandenen fachlichen Quellen, Runtime-Evidenz und Engineering-Tools-Code materialisiert. · © miradlo · <a href=\"https://miradlo.com/impressum/\">Impressum von miradlo</a></footer></main></body></html>", "",
    ]), findings


def materialize_public_presentation(presentation, output_root):
    """Publish the approved public pages and their explicitly provided image."""
    if not presentation:
        return []
    assets = output_root / "assets"
    assets.mkdir(parents=True, exist_ok=True)
    image_name = presentation["about"]["image"]
    source_image = Path(__file__).parent / "assets" / image_name
    if not source_image.is_file():
        raise ValueError("Öffentliches Bild fehlt: " + str(source_image))
    shutil.copy2(source_image, assets / image_name)
    style = '<style>body{font-family:system-ui,sans-serif;line-height:1.65;margin:0;background:#f5f7fb;color:#1b2638}main{width:min(52rem,calc(100% - 2rem));margin:0 auto;padding:2rem 0 4rem}article{background:#fff;border:1px solid #d5dde7;border-radius:.9rem;padding:clamp(1.2rem,4vw,2.4rem)}a{color:#075a9c}.portrait{width:100%;height:auto;border-radius:.7rem}.caption{color:#46576b;font-size:.93rem}</style>'
    pages = []
    for filename, item, image in (("elevator-pitch.html", presentation["elevatorPitch"], presentation["elevatorPitch"].get("image", "")), ("ueber-mich.html", presentation["about"], image_name)):
        paragraphs = "".join("<p>{}</p>".format(html.escape(value)) for value in item["paragraphs"])
        links = "".join('<li><a href="{}">{}</a></li>'.format(html.escape(link["href"], quote=True), html.escape(link["label"])) for link in item.get("links", []))
        links = "<ul>" + links + "</ul>" if links else ""
        caption = item.get("imageCaption", "")
        if image and image != image_name:
            source = Path(__file__).parent / "assets" / image
            if not source.is_file():
                raise ValueError("Öffentliche Grafik fehlt: " + str(source))
            shutil.copy2(source, assets / image)
        figure = (f'<figure><img class="portrait" src="assets/{html.escape(image, quote=True)}" alt="{html.escape(caption, quote=True)}"><figcaption class="caption">{html.escape(caption)}</figcaption></figure>' if image else "")
        page = f'<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>{html.escape(item["title"])}</title>{style}</head><body><main><p><a href="index.html">← Zur Wissenslandkarte</a></p><article><h1>{html.escape(item["title"])}</h1>{figure}{paragraphs}{links}</article><footer>© miradlo · <a href="https://miradlo.com/impressum/">Impressum von miradlo</a></footer></main></body></html>'
        target = output_root / filename
        target.write_text(page, encoding="utf-8")
        pages.append(target)
    return pages


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("input", type=Path)
    parser.add_argument("output", type=Path)
    parser.add_argument("--report", type=Path)
    parser.add_argument("--repository", action="append", default=[],
                        help="Freigegebener Prüfrooot als name=path")
    parser.add_argument("--link-report", type=Path, required=True)
    parser.add_argument("--link-markdown-report", type=Path, required=True)
    parser.add_argument("--timestamp")
    parser.add_argument("--target-runtime", default="")
    parser.add_argument("--code-view-directory", type=Path, required=True)
    parser.add_argument("--code-source", action="append", default=[], help="Freigegebene Codequelle als name=path")
    parser.add_argument("--test-source", action="append", default=[], help="Freigegebene Testquelle als name=path")
    args = parser.parse_args()
    model = json.loads(args.input.read_text(encoding="utf-8"))
    code_sources={}
    for item in args.code_source:
        name, separator, path=item.partition("=")
        if not separator: parser.error("--code-source benötigt name=path")
        code_sources[name]=Path(path).resolve()
    if code_sources:
        model["codeReferences"]=collect_code_references(code_sources)
    test_sources = {}
    for item in args.test_source:
        name, separator, path = item.partition("=")
        if not separator: parser.error("--test-source benötigt name=path")
        test_sources[name] = Path(path).resolve()
    if test_sources:
        model["testReferences"] = collect_test_references(test_sources)
    for reference in model.get("codeReferences", []):
        reference.update(model.get("codeEvidence", {}).get(reference["path"], {}))
    for reference in model.get("testReferences", []):
        reference.update(model.get("testEvidence", {}).get(reference["path"], {}))
    presentation_path = Path(__file__).parent / "public-presentation.json"
    presentation = json.loads(presentation_path.read_text(encoding="utf-8")) if presentation_path.is_file() else None
    page, findings = render(model, args.target_runtime, presentation)
    public_pages = materialize_public_presentation(presentation, args.output.parent)
    for vision in model.get("visionMap", []):
        target = args.output.parent / "visions" / (vision["id"] + ".html")
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(vision_detail(vision), encoding="utf-8")
    for reference in model.get("knowledgeReferences", []):
        target = args.output.parent / "wissen" / (reference["id"] + ".html")
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(knowledge_detail(reference), encoding="utf-8")
    for context in model.get("contextImpacts", []):
        target = args.output.parent / "context" / (context["id"] + ".html")
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(context_detail(context), encoding="utf-8")
    for regression in model.get("regressionEvidence", []):
        target=args.output.parent / "regressions" / (regression["id"]+".html")
        target.parent.mkdir(parents=True,exist_ok=True); target.write_text(regression_detail(regression),encoding="utf-8")
    for reference in model.get("codeReferences", []):
        code_page = code_view(reference)
        target = args.code_view_directory / (reference["id"] + ".html")
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(code_page, encoding="utf-8")
    for reference in model.get("testReferences", []):
        target = args.output.parent / "tests" / (reference["id"] + ".html")
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(test_view(reference), encoding="utf-8")
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(page, encoding="utf-8")
    validator_path = Path(__file__).parents[1] / "40-validation" / "validate-links.py"
    spec = importlib.util.spec_from_file_location("link_checker", validator_path)
    checker = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(checker)
    roots = {}
    for item in args.repository:
        name, separator, path = item.partition("=")
        if not separator:
            parser.error("--repository benötigt name=path")
        roots[name] = Path(path).resolve()
    published = [args.output.resolve(), *[path.resolve() for path in public_pages]] + [x.resolve() for x in args.code_view_directory.glob("*.html")] + [x.resolve() for x in (args.output.parent / "tests").glob("*.html")] + [x.resolve() for x in (args.output.parent / "visions").glob("*.html")] + [x.resolve() for x in (args.output.parent / "wissen").glob("*.html")] + [x.resolve() for x in (args.output.parent / "context").glob("*.html")] + [x.resolve() for x in (args.output.parent / "regressions").glob("*.html")]
    link_result = checker.validate(roots, published, timestamp=args.timestamp, work_item=model.get("workItem", "SE-0059E"))
    args.link_report.parent.mkdir(parents=True, exist_ok=True)
    args.link_report.write_text(json.dumps(link_result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    args.link_markdown_report.parent.mkdir(parents=True, exist_ok=True)
    args.link_markdown_report.write_text(checker.markdown_report(link_result), encoding="utf-8")
    if link_result["status"] != "PASSED":
        errors = [item for item in link_result["findings"] if item.get("severity") == "ERROR"]
        details = "; ".join(
            f"{item.get('sourceReference', item.get('sourceArtifact', '?'))} -> "
            f"{item.get('expectedTarget', item.get('target', '?'))} "
            f"({item.get('errorClass', 'LINK_ERROR')})"
            for item in errors[:10]
        )
        raise SystemExit(
            "Linkprüfung fehlgeschlagen; Veröffentlichung nicht erfolgreich. "
            f"Befunde={len(errors)}. {details}"
        )
    report = {"schemaVersion": "2.0", "workItem": model.get("workItem", "SE-0065"), "status": "PASSED", "output": str(args.output), "meaningBeforeNavigation": page.index("elevator-pitch") < page.index("aria-label"), "semanticMapBeforeNavigation": page.index("semantische-landkarte") < page.index("aria-label"), "helpBeforeNavigation": page.index("hilfe") < page.index("aria-label"), "technicalReportsReachable": bool(model["technicalReports"]), "linkValidation": {"status": link_result["status"], "jsonReport": str(args.link_report), "markdownReport": str(args.link_markdown_report)}, "contentTruthAssessed": False, "findings": findings}
    if args.report:
        args.report.parent.mkdir(parents=True, exist_ok=True)
        args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(json.dumps(report, ensure_ascii=False))


if __name__ == "__main__":
    main()