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