#!/usr/bin/env python3 import json import re from datetime import datetime from html import escape from pathlib import Path ROOT = Path(__file__).resolve().parents[1] WORKSPACE = ROOT.parent OUT = ROOT / "understanding_site" DATA_JS = OUT / "site-data.js" INDEX = OUT / "index.html" PORTFOLIO_HTML = OUT / "portfolio-map.html" RESULT = OUT / "result.json" TEXT_EXTENSIONS = { ".md", ".json", ".jsonl", ".yaml", ".yml", ".html", ".txt", ".sh", ".py", } RECOMMENDED_READING = [ "ProjectIntakeTower/PROJECT_PORTFOLIO_MAP_V1.md", "ProjectIntakeTower/visual_summary/project_learning_graphic.html", "ProjectIntakeTower/OWNER_DIGEST.md", "ProjectIntakeTower/triage/TOP_10_NEXT_ACTIONS.md", "ProjectIntakeTower/NIGHT_STOP.md", ] STOP_LAYER_PATHS = [ "ProjectIntakeTower/PANIC_QUOTA_STOPLOSS.md", "CONTROL_ROOM.md", "ProjectIntakeTower/NIGHT_STOP.md", "ProjectIntakeTower/jobs/running/STOP_NOW.md", "ProjectIntakeTower/MORNING_RESUME.md", ] CORE_ANCHOR_PATHS = [ "ProjectIntakeTower/PROJECT_PORTFOLIO_MAP_V1.md", "ProjectIntakeTower/visual_summary/project_learning_graphic.html", "ProjectIntakeTower/OWNER_DIGEST.md", "ProjectIntakeTower/triage/TOP_10_NEXT_ACTIONS.md", "ProjectIntakeTower/jobs/pending/", "ProjectIntakeTower/verifier/", "ProjectIntakeTower/visual_summary/", "ProjectIntakeTower/inventory/projects.jsonl", "ProjectIntakeTower/inventory/project_cards/", ] RAW_MASS_PATHS = [ "ProjectIntakeTower/triage/HUMAN_DECISION_REQUIRED.md", "ProjectIntakeTower/triage/BACKLOG.md", "ProjectIntakeTower/inventory/evidence/", "ProjectIntakeTower/inventory/raw_scan.jsonl", ] def rel_workspace(path): return path.resolve().relative_to(WORKSPACE.resolve()).as_posix() def load_json(path, fallback): try: return json.loads(path.read_text(encoding="utf-8")) except Exception: return fallback def is_probably_text(path): if path.suffix.lower() in TEXT_EXTENSIONS: return True try: chunk = path.read_bytes()[:4096] except OSError: return False if b"\x00" in chunk: return False try: chunk.decode("utf-8") except UnicodeDecodeError: return False return True def title_from_content(path, content): if path.endswith(".md"): match = re.search(r"^#\s+(.+)$", content, re.M) if match: return match.group(1).strip() name = Path(path).name return name.replace("_", " ").replace("-", " ") def kind_for(path): suffix = Path(path).suffix.lower().lstrip(".") return suffix or "text" def group_for(path): if path == "CONTROL_ROOM.md": return "Control Layer" if path.startswith("__generated__/"): return "Guide" if "PANIC" in path or "NIGHT_STOP" in path or "MORNING_RESUME" in path or "STOP_NOW" in path: return "Control Layer" if "/triage/" in path: return "Triage" if "/jobs/" in path: return "Jobs" if "/inventory/" in path: return "Inventory" if "/github/" in path: return "GitHub Drafts" if "/verifier/" in path or "/scripts/" in path: return "Pipeline" if "/visual_summary/" in path or "/explainers/" in path: return "Visual Explainability" if "/consolidation_decision/" in path or "CONSOLIDATION" in path: return "Consolidation" if "/archive_plan/" in path: return "Archive" if "/small_fix/" in path or "/test_generation/" in path or "/verifier_creation/" in path or "/doc_reality_sync/" in path: return "Generated Work" return "Root" def workstream_for(path): if path.startswith("__generated__/"): return "Guide" if path in RAW_MASS_PATHS or any(path.startswith(prefix) for prefix in RAW_MASS_PATHS if prefix.endswith("/")): return "Raw" if path in STOP_LAYER_PATHS: return "Safety" if path == "CONTROL_ROOM.md": return "Safety" if "PANIC" in path or "NIGHT_STOP" in path or "MORNING_RESUME" in path or "STOP_NOW" in path: return "Safety" if "/consolidation_decision/" in path or "CONSOLIDATION_DECISION" in path: return "Safety" if "/doc_reality_sync/" in path or "/small_fix/" in path or "/test_generation/" in path or "/verifier_creation/" in path: return "Raw" if path in CORE_ANCHOR_PATHS or any(path.startswith(prefix) for prefix in CORE_ANCHOR_PATHS if prefix.endswith("/")): return "Core" if "/triage/" in path or "/jobs/" in path or "/verifier/" in path or "/scripts/" in path or "/github/issue_drafts/" in path: return "Core" if "/inventory/projects.jsonl" in path or "/inventory/project_cards/" in path: return "Core" if "/visual_summary/" in path: return "Core" return "Raw" def priority_for(path): if path.startswith("__generated__/"): return 1000 if path in RECOMMENDED_READING: return 980 - (RECOMMENDED_READING.index(path) * 10) if path in STOP_LAYER_PATHS: return 900 if workstream_for(path) == "Core": return 760 if workstream_for(path) == "Safety": return 740 if "/triage/" in path: return 680 if "/jobs/running/" in path or "/jobs/done/" in path: return 660 if "/consolidation_decision/" in path or "CONSOLIDATION" in path: return 640 if "/inventory/project_cards/" in path: return 420 if "/inventory/evidence/" in path: return 360 return 300 def original_href(path): if path.startswith("__generated__/"): return "" if path == "CONTROL_ROOM.md": return "../../CONTROL_ROOM.md" prefix = "ProjectIntakeTower/" if path.startswith(prefix): return "../" + path[len(prefix) :] return "" def collect_documents(): files = [WORKSPACE / "CONTROL_ROOM.md"] for path in ROOT.rglob("*"): if not path.is_file(): continue rel = rel_workspace(path) if rel.startswith("ProjectIntakeTower/understanding_site/"): continue if path.suffix.lower() in {".png", ".mp4", ".pyc"}: continue files.append(path) docs = [] seen = set() for path in files: if not path.exists() or not is_probably_text(path): continue rel = rel_workspace(path) if rel in seen: continue seen.add(rel) try: content = path.read_text(encoding="utf-8", errors="replace") except OSError: continue stat = path.stat() docs.append( { "path": rel, "title": title_from_content(rel, content), "kind": kind_for(rel), "group": group_for(rel), "workstream": workstream_for(rel), "size": stat.st_size, "mtime": datetime.fromtimestamp(stat.st_mtime).astimezone().isoformat(timespec="seconds"), "priority": priority_for(rel), "href": original_href(rel), "content": content, } ) docs.extend(generated_documents()) docs.sort(key=lambda d: (-d["priority"], d["group"], d["path"])) return docs def generated_documents(): gains_md = """# Safety-Pattern und drei verwertbare Gewinne Diese Gewinne gehoeren nicht zur normalen Produktarbeit von ProjectIntakeTower/Core. Sie gehoeren zur Safety-Seite: Kaskadenbewaeltigung, Stop-Loss, Containment und Wiederaufnahme. ## 1. Technischer Gewinn Der Lauf war ein echter Agentic-Stress-Test. Der Owner-Befund lautet: ca. 45 Mio. Codex-Token, plus Hermes/OpenCode/Codex-Prozessspuren. Das ist Trainingsmaterial fuer Agent Engineering, weil der Stress nicht abstrakt war: er hat konkrete Artefakte, Jobs, Triage-Listen und Kontrollreaktionen erzeugt. Evidenz: `CONTROL_ROOM.md`, `ProjectIntakeTower/PANIC_QUOTA_STOPLOSS.md`, `ProjectIntakeTower/NIGHT_STOP.md`. ## 2. System-Gewinn Das System hat eigene Stop-Layer produziert: `PANIC_QUOTA_STOPLOSS.md`, `CONTROL_ROOM.md`, `NIGHT_STOP.md`, `STOP_NOW.md`. Das ist wertvoller als viele gebaute Features, weil es zeigt, dass der Stack Kontrollmechanismen entwickeln kann, sobald die Orchestrierung zu breit wird. ## 3. Produkt-Gewinn Daraus wird ein wiederverwendbares Pattern: `Chaos Power Mode -> Stop-Loss -> Containment -> Morning Resume`. Das kann als Skill, Artikel, Vortrag, Blogpost oder Agent-Operating-Manual wiederverwendet werden. """ reading_md = """# Gefuehrte Lesereihenfolge Nicht `triage/HUMAN_DECISION_REQUIRED.md` zuerst lesen. Das ist Rohmasse. 1. `PROJECT_PORTFOLIO_MAP_V1.md`: Verwertbare Ernte und Arbeitsrahmen. 2. `visual_summary/project_learning_graphic.html`: Visuelle Karte. 3. `OWNER_DIGEST.md`: Komprimierter Owner-Blick. 4. `triage/TOP_10_NEXT_ACTIONS.md`: Priorisierte naechste Handlungen. 5. `NIGHT_STOP.md`: Safety-Grenze und Restart-Regeln. Danach mental trennen: - `ProjectIntakeTower/Core` = echte Projektlandkarte. - `ProjectIntakeTower/Safety` = Kaskadenkontrolle. """ now_md = """# Was jetzt zu tun ist Der naechste Anker ist `ProjectIntakeTower/Core`, nicht ein neues Source-Projekt und nicht die gesamte Rohmasse. Arbeitsregel: erst die vier empfohlenen Dokumente lesen, dann Core und Safety bewusst getrennt halten. Default naechster Arbeitsblock: Core-Karte verstehen, Safety-Pattern als eigenes Betriebswissen markieren, dann maximal einen bounded Job aus `jobs/pending/` auswaehlen. """ now = datetime.now().astimezone().isoformat(timespec="seconds") generated = [] for rel, title, content in [ ("__generated__/DREI_GEWINNE.md", "Drei verwertbare Gewinne", gains_md), ("__generated__/LESEPFAD.md", "Gefuehrte Lesereihenfolge", reading_md), ("__generated__/JETZT_TUN.md", "Was jetzt zu tun ist", now_md), ]: generated.append( { "path": rel, "title": title, "kind": "generated", "group": "Guide", "workstream": "Guide", "size": len(content.encode("utf-8")), "mtime": now, "priority": priority_for(rel), "href": "", "content": content, } ) return generated def summarize_jobs(): jobs_root = ROOT / "jobs" out = {} for state in ["pending", "done", "running", "failed"]: folder = jobs_root / state entries = [] if folder.exists(): for path in sorted(folder.rglob("*")): if not path.is_file(): continue rel = rel_workspace(path) item = {"path": rel, "name": path.name, "size": path.stat().st_size} if path.suffix == ".json": data = load_json(path, {}) item["job_id"] = data.get("job_id") or data.get("id") item["mode"] = data.get("mode") or data.get("job_mode") item["project_path"] = data.get("project_path") item["labels"] = data.get("labels") or data.get("tags") or [] entries.append(item) out[state] = entries return out def load_all_projects(): pri = load_json(ROOT / "triage" / "PROJECT_PRIORITIZATION.json", {}) projects = pri.get("projects") if not isinstance(projects, list): by_path = {} for rows in pri.get("categories", {}).values(): if not isinstance(rows, list): continue for row in rows: path = row.get("path") if path and path not in by_path: by_path[path] = row projects = list(by_path.values()) projects.sort(key=lambda p: p.get("score", 0), reverse=True) return projects def load_projects(): return load_all_projects()[:120] def gain_cards(): return [ { "title": "Technischer Gewinn", "claim": "Echter Agentic-Stress-Test: ca. 45 Mio. Codex-Token als Owner-/Usage-Befund, plus belegte Hermes/OpenCode/Codex-Spuren.", "why": "Das ist Trainingsmaterial fuer Agent Engineering: nicht als Theorie, sondern als reale Ueberlastung mit Artefakten, Verifiern und Jobs.", "evidencePaths": [ "CONTROL_ROOM.md", "ProjectIntakeTower/PANIC_QUOTA_STOPLOSS.md", "ProjectIntakeTower/NIGHT_STOP.md", ], "reuseIdeas": ["Agent Engineering Case Study", "Stress-Test Fixture", "Token-Burn Postmortem"], }, { "title": "System-Gewinn", "claim": "Der Stack hat selbst Stop-Layer produziert: Panic Stop-Loss, Control Room, Night Stop und STOP_NOW.", "why": "Das ist ein Kontrollmechanismus, nicht nur Output. Das System konnte seine eigene Expansion begrenzen.", "evidencePaths": STOP_LAYER_PATHS, "reuseIdeas": ["Stop-Loss Skill", "Operator Policy", "Containment Checklist"], }, { "title": "Produkt-Gewinn", "claim": "Das wiederverwendbare Pattern lautet: Chaos Power Mode -> Stop-Loss -> Containment -> Morning Resume.", "why": "Aus dem Chaos wird ein Produktbaustein: Skill, Artikel, Vortrag, Blogpost oder Agent-Operating-Manual.", "evidencePaths": [ "ProjectIntakeTower/PANIC_QUOTA_STOPLOSS.md", "CONTROL_ROOM.md", "ProjectIntakeTower/NIGHT_STOP.md", "ProjectIntakeTower/MORNING_RESUME.md", ], "reuseIdeas": ["Operating Manual", "Talk Outline", "Blogpost Series"], }, ] def workstream_cards(): return [ { "id": "Core", "title": "ProjectIntakeTower/Core", "claim": "Echte Arbeit: Projektinventar, Triage, Top-10-Actions, Job-Contracts, Verifier, Visual Summary und Owner Digest.", "meaning": "Das ist der 'Was habe ich eigentlich auf meinem System?'-Teil.", }, { "id": "Safety", "title": "ProjectIntakeTower/Safety", "claim": "Kaskadenkontrolle: Panic Stop-Loss, Control Room, Night Stop, STOP_NOW, Morning Resume und Safety-nahe Consolidator-Artefakte.", "meaning": "Das ist Betriebsunfall-Management. Wertvoll, aber anders wertvoll als Produktarbeit.", }, { "id": "Raw", "title": "Rohmasse", "claim": "Vollstaendige, lange Artefakte wie HUMAN_DECISION_REQUIRED, Backlog, Evidence-JSONs und Generated Reports.", "meaning": "Durchsuchbar, aber nicht zuerst lesen.", }, ] def top_paths(docs, workstream, limit=12): return [doc["path"] for doc in docs if doc["workstream"] == workstream and not doc["path"].startswith("__generated__/")][:limit] def project_reasons(project): return project.get("reasons") or project.get("score_reasons") or [] def compact_project(project): return { "name": project.get("name") or Path(project.get("path", "")).name, "path": project.get("path", ""), "score": project.get("score", 0), "dirty_count": project.get("dirty_count", 0), "branch_count": project.get("branch_count", 0), "todo_fixme_count": project.get("todo_fixme_count", 0), "plan_hit_count": project.get("plan_hit_count", 0), "agent_artifact_count": project.get("agent_artifact_count", 0), "duplicate_cluster": project.get("duplicate_cluster"), "themes": project.get("themes", []), "categories": project.get("categories", []), "tech": project.get("tech"), "reasons": project_reasons(project)[:4], } def next_agent_run_label(project): categories = set(project.get("categories", [])) if "MERGE" in categories: return "bounded consolidation review, not feature work" if project.get("dirty_count", 0) >= 10: return "dirty-state stabilization audit before any build" if project.get("todo_fixme_count", 0) >= 20: return "focused verifier/test pass" if project.get("agent_artifact_count", 0): return "artifact verification and closure" return "small read-only audit, then decide" def build_condensation(projects): active = [ p for p in projects if "KEEP" in p.get("categories", []) and "ARCHIVE" not in p.get("categories", []) and "PAUSE" not in p.get("categories", []) ][:14] old_energy = sorted( [ p for p in projects if "PAUSE" in p.get("categories", []) or "ARCHIVE" in p.get("categories", []) or ( "TOKEN_BURN_CANDIDATE" in p.get("categories", []) and "KEEP" not in p.get("categories", []) ) ], key=lambda p: (p.get("score", 0), p.get("plan_hit_count", 0), p.get("todo_fixme_count", 0)), reverse=True, )[:14] dirty_danger = sorted( [p for p in projects if p.get("dirty_count", 0) > 0], key=lambda p: (p.get("dirty_count", 0), p.get("score", 0)), reverse=True, )[:14] cluster_map = {} for p in projects: duplicate = p.get("duplicate_cluster") if duplicate: cluster_map.setdefault(f"duplicate:{duplicate}", []).append(p) for theme in p.get("themes", []) or []: cluster_map.setdefault(f"theme:{theme}", []).append(p) clusters = [] for key, members in cluster_map.items(): if len(members) < 2: continue members = sorted(members, key=lambda p: p.get("score", 0), reverse=True) clusters.append( { "name": key, "count": len(members), "top_score": members[0].get("score", 0), "members": [compact_project(p) for p in members[:8]], } ) clusters.sort(key=lambda c: (c["count"], c["top_score"]), reverse=True) next_agent = [] for p in projects: categories = set(p.get("categories", [])) if "ARCHIVE" in categories or "PAUSE" in categories: continue if "KEEP" not in categories and "URGENT" not in categories: continue if p.get("score", 0) < 45 and p.get("dirty_count", 0) < 10: continue item = compact_project(p) item["suggested_run"] = next_agent_run_label(p) item["human_gate"] = "HUMAN_DECISION_REQUIRED" in categories or p.get("dirty_count", 0) > 0 next_agent.append(item) next_agent = next_agent[:12] return { "questions": [ "Welche Projekte sind wirklich aktiv?", "Welche sind nur alte Energie?", "Welche haben gefaehrliche dirty states?", "Welche gehoeren zusammen?", "Wo lohnt sich ein echter naechster Agentenlauf?", ], "activeProjects": [compact_project(p) for p in active], "oldEnergy": [compact_project(p) for p in old_energy], "dirtyDanger": [compact_project(p) for p in dirty_danger], "clusters": clusters[:10], "nextAgentRuns": next_agent, } def markdown_inline(text): return re.sub( r"\*\*([^*]+)\*\*", r"\1", re.sub(r"`([^`]+)`", r"\1", escape(str(text), quote=True)), ) def render_markdown_fragment(content): lines = str(content or "").splitlines() html = [] list_type = None in_code = False code_lines = [] def flush_list(): nonlocal list_type if list_type: html.append(f"") list_type = None def open_list(tag): nonlocal list_type if list_type == tag: return flush_list() html.append(f"<{tag}>") list_type = tag def flush_code(): nonlocal in_code, code_lines if in_code: html.append("
" + escape("\n".join(code_lines), quote=True) + "
") code_lines = [] in_code = False for line in lines: if line.startswith("```"): if in_code: flush_code() else: flush_list() in_code = True code_lines = [] continue if in_code: code_lines.append(line) continue if re.match(r"^\s*---+\s*$", line): flush_list() html.append("
") continue heading = re.match(r"^(#{1,4})\s+(.+)$", line) if heading: flush_list() level = min(len(heading.group(1)) + 1, 5) html.append(f"{markdown_inline(heading.group(2))}") continue bullet = re.match(r"^\s*[-*]\s+(.+)$", line) if bullet: open_list("ul") html.append("
  • " + markdown_inline(bullet.group(1)) + "
  • ") continue ordered = re.match(r"^\s*\d+\.\s+(.+)$", line) if ordered: open_list("ol") html.append("
  • " + markdown_inline(ordered.group(1)) + "
  • ") continue if not line.strip(): flush_list() continue flush_list() html.append("

    " + markdown_inline(line) + "

    ") flush_code() flush_list() return "\n".join(html) def extract_h2_section(content, heading): pattern = rf"^##\s+{re.escape(heading)}\s*\n(.*?)(?=^##\s+|\Z)" match = re.search(pattern, content, re.M | re.S) return match.group(1).strip() if match else "" def extract_h2_from_prefix(content, prefix): match = re.search(rf"^##\s+{re.escape(prefix)}.*$", content, re.M) return content[match.start():].strip() if match else "" def extract_h3_sections(content): matches = list(re.finditer(r"^###\s+(.+)$", content, re.M)) sections = [] for index, match in enumerate(matches): start = match.end() end = matches[index + 1].start() if index + 1 < len(matches) else len(content) sections.append({"title": match.group(1).strip(), "body": content[start:end].strip()}) return sections def extract_status(content): match = re.search(r"Status:\s*\n\s*([A-Z_]+)\.?", content) return match.group(1) if match else "" def status_class(status): lower = status.lower() if "stabilize" in lower: return "stabilize" if "human" in lower: return "human" if "archive" in lower: return "archive" if "merge" in lower: return "merge" return "secondary" def portfolio_metrics(assumptions, counts): metrics = [] for line in assumptions.splitlines(): match = re.match(r"^-\s+([^:]+):\s+(.+)$", line.strip()) if match: metrics.append({"label": match.group(1), "value": match.group(2)}) if metrics: return metrics[:8] return [ {"label": "Projekt-/Artefaktcluster", "value": counts.get("total_projects", "-")}, {"label": "Core-Dokumente", "value": counts.get("coreDocuments", "-")}, {"label": "Safety-Dokumente", "value": counts.get("safetyDocuments", "-")}, {"label": "Rohdokumente", "value": counts.get("rawDocuments", "-")}, ] def build_portfolio_model(data): doc = next((item for item in data["documents"] if item["path"] == data["portfolioMap"]), None) content = doc["content"] if doc else "" purpose = extract_h2_section(content, "Zweck") assumptions = extract_h2_section(content, "Was ich jetzt als wahr annehme") families = extract_h3_sections(extract_h2_section(content, "Meine wichtigsten Projektfamilien")) decisions = extract_h3_sections(extract_h2_section(content, "Meine Top-Entscheidungen, aber nicht alle heute")) ignore_today = extract_h2_section(content, "Was ich ab jetzt ignoriere") next_action = extract_h2_section(content, "Nächste konkrete Aktion") verifier = extract_h2_from_prefix(content, "Verifier-Ergebnis") return { "doc": doc, "content": content, "purpose": purpose, "assumptions": assumptions, "families": families, "decisions": decisions, "ignore_today": ignore_today, "next_action": next_action, "verifier": verifier, "metrics": portfolio_metrics(assumptions, data["counts"]), } def build_data(): summary = load_json(ROOT / "triage" / "PROJECT_PRIORITIZATION_SUMMARY.json", {}) docs = collect_documents() projects = load_all_projects() workstream_counts = {} for doc in docs: workstream_counts[doc["workstream"]] = workstream_counts.get(doc["workstream"], 0) + 1 return { "generatedAt": datetime.now().astimezone().isoformat(timespec="seconds"), "title": "ProjectIntakeTower Understanding Site", "counts": { **summary, "documents": len(docs), "textBytes": sum(d["size"] for d in docs if not d["path"].startswith("__generated__/")), "coreDocuments": workstream_counts.get("Core", 0), "safetyDocuments": workstream_counts.get("Safety", 0), "rawDocuments": workstream_counts.get("Raw", 0), }, "pattern": "Core = echte Projektlandkarte | Safety = Kaskadenkontrolle | Raw = nicht zuerst lesen", "safetyPattern": "Chaos Power Mode -> Stop-Loss -> Containment -> Morning Resume", "gains": gain_cards(), "workstreams": workstream_cards(), "portfolioMap": "ProjectIntakeTower/PROJECT_PORTFOLIO_MAP_V1.md", "portfolioMapHtml": "portfolio-map.html", "recommendedReading": RECOMMENDED_READING, "readingPath": RECOMMENDED_READING, "coreDocuments": top_paths(docs, "Core"), "safetyDocuments": top_paths(docs, "Safety"), "rawDocuments": top_paths(docs, "Raw"), "stopLayers": STOP_LAYER_PATHS, "projects": projects[:120], "condensation": build_condensation(projects), "jobs": summarize_jobs(), "documents": docs, "visualAsset": "../visual_summary/project_learning_graphic.png", "narrativeSections": [ { "title": "Was passiert ist", "body": "PROJECT_PORTFOLIO_MAP_V1.md ist jetzt die verwertbare Ernte aus ProjectIntakeTower. Die Website trennt weiterhin echte Projektlandkarte, Safety-Kaskadenkontrolle und Rohmasse.", }, { "title": "Was jetzt wichtig ist", "body": "Der Mehrwert liegt in der Verdichtung: wirklich aktive Projekte, alte Energie, dirty risks, Zusammenhaenge und naechste echte Agentenlaeufe. Lies zuerst die Portfolio-Map und die vier stuetzenden Dokumente.", }, { "title": "Was nicht passieren darf", "body": "Nicht mit HUMAN_DECISION_REQUIRED.md starten. Das Dokument bleibt auffindbar, ist aber Rohmasse und kein guter Einstieg.", }, ], } def write_site_data(data): OUT.mkdir(parents=True, exist_ok=True) payload = json.dumps(data, ensure_ascii=False, separators=(",", ":")) DATA_JS.write_text("window.PROJECT_UNDERSTANDING_DATA = " + payload + ";\n", encoding="utf-8") def write_portfolio_map(data): model = build_portfolio_model(data) doc = model["doc"] or {} metrics_html = "\n".join( f'
    {markdown_inline(item["value"])}{markdown_inline(item["label"])}
    ' for item in model["metrics"] ) workstreams_html = "\n".join( f'''

    {markdown_inline(stream["title"])}

    {markdown_inline(stream["claim"])}

    {markdown_inline(stream["meaning"])}
    ''' for stream in data["workstreams"] ) families_html = "\n".join( f'''

    {markdown_inline(section["title"])}

    {f'{markdown_inline(extract_status(section["body"]))}' if extract_status(section["body"]) else ''}
    {render_markdown_fragment(section["body"])}
    ''' for section in model["families"] ) decisions_html = "\n".join( f'''

    {markdown_inline(section["title"])}

    {render_markdown_fragment(section["body"])}
    ''' for section in model["decisions"] ) page = r''' Project Portfolio Map V1

    Project Portfolio Map V1

    Die Markdown-Datei bleibt die Quelle. Diese Seite macht daraus eine greifbare Arbeitskarte: Was ist Core-Arbeit, was ist Safety, welche Projektfamilien gibt es, und welche Entscheidung kommt als naechstes.

    __METRICS__

    Warum diese Karte existiert

    __PURPOSE__

    Mentale Trennung

    __WORKSTREAMS__

    Projektfamilien

    __FAMILIES__

    Entscheidungen

    __DECISIONS__

    Heute nicht anfassen

    __IGNORE_TODAY__

    Naechste konkrete Aktion

    __NEXT_ACTION__
    __VERIFIER__
    Volltext der Markdown-Quelle
    __FULL_MARKDOWN__
    ''' replacements = { "__METRICS__": metrics_html, "__WORKSTREAMS__": workstreams_html, "__FAMILIES__": families_html, "__DECISIONS__": decisions_html, "__PURPOSE__": render_markdown_fragment(model["purpose"]), "__IGNORE_TODAY__": render_markdown_fragment(model["ignore_today"]), "__NEXT_ACTION__": render_markdown_fragment(model["next_action"]), "__VERIFIER__": render_markdown_fragment(model["verifier"]), "__FULL_MARKDOWN__": render_markdown_fragment(model["content"]), } for placeholder, value in replacements.items(): page = page.replace(placeholder, value) PORTFOLIO_HTML.write_text(page, encoding="utf-8") def write_index(): INDEX.write_text( r''' ProjectIntakeTower verstehen

    Was gestern greifbar wurde

    Project learning map
    Die vorhandene Lern-Grafik bleibt als visuelle Landkarte erhalten. Die neue Website macht darunter jedes Textartefakt direkt aufrufbar.

    Core: echte Projektlandkarte

    Das ist der Teil, der wirklich Wert erzeugt hat: Inventar, Triage, Top-10-Actions, Job-Contracts, Verifier, Visual Summary und Owner Digest.

    Verdichtung: worin der Mehrwert liegt

    Nicht die einzelnen jobs/pending/*.json sind der Hauptwert, sondern diese zusammengezogene Projektkarte.

    Safety: Kaskadenkontrolle

    Das ist nicht normale Produktarbeit, sondern Betriebsunfall-Management. Es bleibt wertvoll, aber es gehoert in eine andere mentale Schublade.

    Safety-Pattern: Chaos Power Mode -> Stop-Loss -> Containment -> Morning Resume.

    Portfolio-Map plus 4 Stuetzen

    Nicht mit triage/HUMAN_DECISION_REQUIRED.md starten. Lies zuerst die Portfolio-Map, dann die vier Dokumente, die sie tragen.

    Dokumente

    Projekte & Human Gates

    Die Tabelle zeigt die wichtigsten Projektknoten aus der vorhandenen Priorisierung. Externe Projektdateien werden hier nur referenziert.

    ProjektScoreStatusWarum
    ''', encoding="utf-8", ) def main(): data = build_data() write_site_data(data) write_index() write_portfolio_map(data) result = { "status": "succeeded", "index": rel_workspace(INDEX), "portfolio_map_html": rel_workspace(PORTFOLIO_HTML), "data": rel_workspace(DATA_JS), "documents": len(data["documents"]), "text_bytes": data["counts"]["textBytes"], "core_documents": data["counts"]["coreDocuments"], "safety_documents": data["counts"]["safetyDocuments"], "raw_documents": data["counts"]["rawDocuments"], "portfolio_map": data["portfolioMap"], "recommended_reading": data["recommendedReading"], "gains": [gain["title"] for gain in data["gains"]], } RESULT.write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8") print(json.dumps(result, indent=2, ensure_ascii=False)) if __name__ == "__main__": main()