Download ProjectIntakeTower/verifier/generate_understanding_site.py from smlflg/AgentProjectMinenraeumer: direct link, hf CLI and curl.
- Browser
- Download file 69.9 kB
-
https://huggingface.co/smlflg/AgentProjectMinenraeumer/resolve/main/ProjectIntakeTower/verifier/generate_understanding_site.py
- Command line
-
hf download hf://smlflg/AgentProjectMinenraeumer/ProjectIntakeTower/verifier/generate_understanding_site.py
-
curl -L -o generate_understanding_site.py https://huggingface.co/smlflg/AgentProjectMinenraeumer/resolve/main/ProjectIntakeTower/verifier/generate_understanding_site.py
69.9 kB
| #!/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"<strong>\1</strong>", | |
| re.sub(r"`([^`]+)`", r"<code>\1</code>", 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}>") | |
| 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("<pre>" + escape("\n".join(code_lines), quote=True) + "</pre>") | |
| 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("<hr>") | |
| continue | |
| heading = re.match(r"^(#{1,4})\s+(.+)$", line) | |
| if heading: | |
| flush_list() | |
| level = min(len(heading.group(1)) + 1, 5) | |
| html.append(f"<h{level}>{markdown_inline(heading.group(2))}</h{level}>") | |
| continue | |
| bullet = re.match(r"^\s*[-*]\s+(.+)$", line) | |
| if bullet: | |
| open_list("ul") | |
| html.append("<li>" + markdown_inline(bullet.group(1)) + "</li>") | |
| continue | |
| ordered = re.match(r"^\s*\d+\.\s+(.+)$", line) | |
| if ordered: | |
| open_list("ol") | |
| html.append("<li>" + markdown_inline(ordered.group(1)) + "</li>") | |
| continue | |
| if not line.strip(): | |
| flush_list() | |
| continue | |
| flush_list() | |
| html.append("<p>" + markdown_inline(line) + "</p>") | |
| 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'<div class="metric"><strong>{markdown_inline(item["value"])}</strong><span>{markdown_inline(item["label"])}</span></div>' | |
| for item in model["metrics"] | |
| ) | |
| workstreams_html = "\n".join( | |
| f'''<article class="stream {escape(stream["id"].lower(), quote=True)}"> | |
| <h3>{markdown_inline(stream["title"])}</h3> | |
| <p>{markdown_inline(stream["claim"])}</p> | |
| <span>{markdown_inline(stream["meaning"])}</span> | |
| </article>''' | |
| for stream in data["workstreams"] | |
| ) | |
| families_html = "\n".join( | |
| f'''<article class="family"> | |
| <div class="family-head"> | |
| <h3>{markdown_inline(section["title"])}</h3> | |
| {f'<span class="status {status_class(extract_status(section["body"]))}">{markdown_inline(extract_status(section["body"]))}</span>' if extract_status(section["body"]) else ''} | |
| </div> | |
| <div class="markdown">{render_markdown_fragment(section["body"])}</div> | |
| </article>''' | |
| for section in model["families"] | |
| ) | |
| decisions_html = "\n".join( | |
| f'''<article class="decision"> | |
| <h3>{markdown_inline(section["title"])}</h3> | |
| <div class="markdown">{render_markdown_fragment(section["body"])}</div> | |
| </article>''' | |
| for section in model["decisions"] | |
| ) | |
| page = r'''<!doctype html> | |
| <html lang="de"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Project Portfolio Map V1</title> | |
| <style> | |
| :root { | |
| --bg: #f6f7f9; | |
| --surface: #ffffff; | |
| --surface-2: #eef2f5; | |
| --text: #17202a; | |
| --muted: #627081; | |
| --line: #d7dee7; | |
| --cyan: #007a8a; | |
| --green: #257a4d; | |
| --amber: #9a6400; | |
| --red: #b42318; | |
| --violet: #6750a4; | |
| --ink: #101828; | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| letter-spacing: 0; | |
| } | |
| a { color: var(--cyan); text-decoration: none; } | |
| a:hover { text-decoration: underline; } | |
| .topbar { | |
| position: sticky; | |
| top: 0; | |
| z-index: 2; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 12px 24px; | |
| border-bottom: 1px solid var(--line); | |
| background: rgba(251, 252, 253, .96); | |
| backdrop-filter: blur(10px); | |
| } | |
| .topbar strong { font-size: 14px; } | |
| .actions { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| justify-content: flex-end; | |
| } | |
| .actions a { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| background: var(--surface); | |
| padding: 7px 10px; | |
| font-size: 13px; | |
| color: var(--text); | |
| } | |
| header, main { | |
| max-width: 1180px; | |
| margin: 0 auto; | |
| padding: 24px; | |
| } | |
| header { | |
| display: grid; | |
| gap: 18px; | |
| padding-top: 34px; | |
| } | |
| h1 { | |
| margin: 0; | |
| max-width: 840px; | |
| font-size: 44px; | |
| line-height: 1.05; | |
| letter-spacing: 0; | |
| } | |
| .lead { | |
| max-width: 840px; | |
| color: var(--muted); | |
| font-size: 18px; | |
| line-height: 1.55; | |
| margin: 0; | |
| } | |
| .metrics { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); | |
| gap: 10px; | |
| } | |
| .metric, .stream, .family, .decision, .panel { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| background: var(--surface); | |
| } | |
| .metric { | |
| min-height: 82px; | |
| padding: 14px; | |
| display: grid; | |
| align-content: start; | |
| gap: 8px; | |
| } | |
| .metric strong { | |
| font-size: 26px; | |
| line-height: 1; | |
| color: var(--ink); | |
| overflow-wrap: anywhere; | |
| } | |
| .metric span { | |
| color: var(--muted); | |
| font-size: 13px; | |
| line-height: 1.35; | |
| } | |
| main { | |
| display: grid; | |
| gap: 24px; | |
| padding-bottom: 56px; | |
| } | |
| section { | |
| display: grid; | |
| gap: 12px; | |
| } | |
| section > h2 { | |
| margin: 0; | |
| font-size: 28px; | |
| line-height: 1.15; | |
| letter-spacing: 0; | |
| } | |
| .two-col, .family-grid, .decision-grid, .stream-grid { | |
| display: grid; | |
| gap: 12px; | |
| } | |
| .two-col { grid-template-columns: repeat(2, minmax(0, 1fr)); } | |
| .stream-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } | |
| .family-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; } | |
| .decision-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; } | |
| .panel, .stream, .family, .decision { | |
| padding: 16px; | |
| min-width: 0; | |
| } | |
| .stream { | |
| display: grid; | |
| gap: 8px; | |
| align-content: start; | |
| } | |
| .stream h3, .family h3, .decision h3 { | |
| margin: 0; | |
| color: var(--ink); | |
| font-size: 18px; | |
| line-height: 1.25; | |
| } | |
| .stream p { | |
| margin: 0; | |
| color: var(--text); | |
| line-height: 1.5; | |
| font-weight: 650; | |
| } | |
| .stream span { | |
| color: var(--muted); | |
| font-size: 14px; | |
| line-height: 1.5; | |
| } | |
| .stream.core { border-color: #add8bf; } | |
| .stream.safety { border-color: #c8d9df; } | |
| .stream.raw { border-color: #e8cc91; } | |
| .family-head { | |
| display: flex; | |
| gap: 10px; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| margin-bottom: 10px; | |
| } | |
| .status { | |
| border: 1px solid var(--line); | |
| border-radius: 999px; | |
| padding: 3px 8px; | |
| font-size: 11px; | |
| white-space: nowrap; | |
| background: #fbfcfd; | |
| } | |
| .status.human { color: var(--amber); border-color: #e8cc91; } | |
| .status.stabilize { color: var(--red); border-color: #f2b8b5; } | |
| .status.merge { color: var(--violet); border-color: #c9bee8; } | |
| .status.archive { color: var(--muted); } | |
| .status.secondary { color: var(--green); border-color: #add8bf; } | |
| .markdown { | |
| color: var(--muted); | |
| line-height: 1.58; | |
| font-size: 14px; | |
| overflow-wrap: anywhere; | |
| } | |
| .markdown p { margin: .45em 0; } | |
| .markdown ul, .markdown ol { margin: .45em 0 .8em; padding-left: 20px; } | |
| .markdown li { margin: .2em 0; } | |
| .markdown h2, .markdown h3, .markdown h4, .markdown h5 { | |
| color: var(--ink); | |
| line-height: 1.25; | |
| margin: 1em 0 .4em; | |
| } | |
| .markdown code { | |
| background: var(--surface-2); | |
| color: var(--ink); | |
| padding: 1px 4px; | |
| border-radius: 4px; | |
| font-size: .95em; | |
| } | |
| .markdown pre { | |
| white-space: pre-wrap; | |
| overflow-wrap: anywhere; | |
| background: #fbfcfd; | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| padding: 12px; | |
| color: var(--ink); | |
| font-size: 13px; | |
| } | |
| .markdown hr { | |
| border: 0; | |
| border-top: 1px solid var(--line); | |
| margin: 16px 0; | |
| } | |
| details.panel summary { | |
| cursor: pointer; | |
| color: var(--ink); | |
| font-weight: 700; | |
| } | |
| @media (max-width: 820px) { | |
| .topbar { position: static; align-items: flex-start; flex-direction: column; padding: 12px 16px; } | |
| header, main { padding-left: 16px; padding-right: 16px; } | |
| h1 { font-size: 34px; } | |
| .lead { font-size: 16px; } | |
| .two-col, .family-grid, .decision-grid, .stream-grid { grid-template-columns: 1fr; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav class="topbar"> | |
| <strong>ProjectIntakeTower / Portfolio Map</strong> | |
| <div class="actions"> | |
| <a href="index.html">Understanding Site</a> | |
| <a href="index.html#doc=ProjectIntakeTower%2FPROJECT_PORTFOLIO_MAP_V1.md">Im Dokument-Reader</a> | |
| <a href="../PROJECT_PORTFOLIO_MAP_V1.md">Original Markdown</a> | |
| </div> | |
| </nav> | |
| <header> | |
| <div> | |
| <h1>Project Portfolio Map V1</h1> | |
| <p class="lead">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.</p> | |
| </div> | |
| <div class="metrics">__METRICS__</div> | |
| </header> | |
| <main> | |
| <section> | |
| <h2>Warum diese Karte existiert</h2> | |
| <article class="panel markdown">__PURPOSE__</article> | |
| </section> | |
| <section> | |
| <h2>Mentale Trennung</h2> | |
| <div class="stream-grid">__WORKSTREAMS__</div> | |
| </section> | |
| <section> | |
| <h2>Projektfamilien</h2> | |
| <div class="family-grid">__FAMILIES__</div> | |
| </section> | |
| <section> | |
| <h2>Entscheidungen</h2> | |
| <div class="decision-grid">__DECISIONS__</div> | |
| </section> | |
| <section> | |
| <h2>Heute nicht anfassen</h2> | |
| <article class="panel markdown">__IGNORE_TODAY__</article> | |
| </section> | |
| <section> | |
| <h2>Naechste konkrete Aktion</h2> | |
| <div class="two-col"> | |
| <article class="panel markdown">__NEXT_ACTION__</article> | |
| <article class="panel markdown">__VERIFIER__</article> | |
| </div> | |
| </section> | |
| <details class="panel"> | |
| <summary>Volltext der Markdown-Quelle</summary> | |
| <div class="markdown">__FULL_MARKDOWN__</div> | |
| </details> | |
| </main> | |
| </body> | |
| </html> | |
| ''' | |
| 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'''<!doctype html> | |
| <html lang="de"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>ProjectIntakeTower verstehen</title> | |
| <style> | |
| :root { | |
| --bg: #f6f7f9; | |
| --surface: #ffffff; | |
| --surface-2: #eef2f5; | |
| --text: #17202a; | |
| --muted: #627081; | |
| --line: #d7dee7; | |
| --cyan: #007a8a; | |
| --green: #257a4d; | |
| --amber: #9a6400; | |
| --red: #b42318; | |
| --violet: #6750a4; | |
| --ink: #101828; | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| letter-spacing: 0; | |
| } | |
| button, input, select { font: inherit; } | |
| a { color: var(--cyan); text-decoration: none; } | |
| a:hover { text-decoration: underline; } | |
| .app { | |
| min-height: 100vh; | |
| display: grid; | |
| grid-template-columns: minmax(260px, 330px) minmax(0, 1fr); | |
| } | |
| aside { | |
| position: sticky; | |
| top: 0; | |
| height: 100vh; | |
| overflow: auto; | |
| border-right: 1px solid var(--line); | |
| background: #fbfcfd; | |
| padding: 20px; | |
| } | |
| main { | |
| min-width: 0; | |
| padding: 24px; | |
| } | |
| .brand { | |
| display: grid; | |
| gap: 6px; | |
| margin-bottom: 18px; | |
| } | |
| .brand h1 { | |
| margin: 0; | |
| font-size: 22px; | |
| line-height: 1.15; | |
| letter-spacing: 0; | |
| } | |
| .brand p { | |
| margin: 0; | |
| color: var(--muted); | |
| font-size: 13px; | |
| line-height: 1.45; | |
| } | |
| .nav { | |
| display: grid; | |
| gap: 8px; | |
| margin: 18px 0; | |
| } | |
| .nav button, .nav a, .chip, .doc-row, .gain-action { | |
| border: 1px solid var(--line); | |
| background: var(--surface); | |
| color: var(--text); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| } | |
| .nav button, .nav a { | |
| text-align: left; | |
| padding: 10px 12px; | |
| font-size: 14px; | |
| line-height: 1.25; | |
| } | |
| .nav a { | |
| display: block; | |
| text-decoration: none; | |
| } | |
| .nav button.active { | |
| border-color: var(--cyan); | |
| box-shadow: inset 3px 0 0 var(--cyan); | |
| } | |
| .mini-stats { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 8px; | |
| margin: 18px 0; | |
| } | |
| .metric { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| background: var(--surface); | |
| padding: 10px; | |
| min-height: 68px; | |
| } | |
| .metric strong { | |
| display: block; | |
| font-size: 22px; | |
| line-height: 1; | |
| } | |
| .metric span { | |
| display: block; | |
| margin-top: 6px; | |
| color: var(--muted); | |
| font-size: 12px; | |
| } | |
| .hero { | |
| display: grid; | |
| grid-template-columns: minmax(0, 1.2fr) minmax(260px, .8fr); | |
| gap: 18px; | |
| align-items: stretch; | |
| margin-bottom: 18px; | |
| } | |
| .panel, .gain, .reader, .doc-list, .project-table, .timeline, .visual-panel { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| background: var(--surface); | |
| } | |
| .panel { | |
| padding: 22px; | |
| } | |
| .panel h2, .section h2 { | |
| margin: 0 0 10px; | |
| font-size: 28px; | |
| line-height: 1.15; | |
| letter-spacing: 0; | |
| } | |
| .panel p, .section p { | |
| color: var(--muted); | |
| line-height: 1.6; | |
| margin: 0; | |
| } | |
| .pattern { | |
| margin-top: 18px; | |
| padding: 12px; | |
| border: 1px solid #c8d9df; | |
| border-radius: 8px; | |
| background: #eef8fa; | |
| color: #0f5662; | |
| font-weight: 700; | |
| word-break: break-word; | |
| } | |
| .overview-gains { | |
| display: grid; | |
| gap: 8px; | |
| margin-top: 16px; | |
| border-top: 1px solid var(--line); | |
| padding-top: 14px; | |
| } | |
| .overview-gain { | |
| display: grid; | |
| grid-template-columns: 150px minmax(0, 1fr); | |
| gap: 10px; | |
| font-size: 13px; | |
| line-height: 1.45; | |
| } | |
| .overview-gain strong { | |
| color: var(--ink); | |
| } | |
| .overview-gain span { | |
| color: var(--muted); | |
| } | |
| .split-cards { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| gap: 10px; | |
| margin-top: 16px; | |
| } | |
| .split-card { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| padding: 12px; | |
| background: #fbfcfd; | |
| display: grid; | |
| gap: 8px; | |
| align-content: start; | |
| } | |
| .split-card strong { | |
| font-size: 14px; | |
| color: var(--ink); | |
| } | |
| .split-card span { | |
| color: var(--muted); | |
| font-size: 13px; | |
| line-height: 1.45; | |
| } | |
| .split-card.core { border-color: #add8bf; } | |
| .split-card.safety { border-color: #c8d9df; } | |
| .split-card.raw { border-color: #e8cc91; } | |
| .visual-panel { | |
| overflow: hidden; | |
| min-height: 260px; | |
| display: grid; | |
| align-content: start; | |
| } | |
| .visual-panel img { | |
| width: 100%; | |
| display: block; | |
| aspect-ratio: 4 / 3; | |
| object-fit: cover; | |
| object-position: top; | |
| border-bottom: 1px solid var(--line); | |
| } | |
| .visual-panel div { | |
| padding: 14px; | |
| color: var(--muted); | |
| font-size: 13px; | |
| line-height: 1.5; | |
| } | |
| .section { | |
| margin: 22px 0; | |
| } | |
| .gains { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| gap: 12px; | |
| } | |
| .gain { | |
| padding: 16px; | |
| display: grid; | |
| gap: 12px; | |
| align-content: start; | |
| } | |
| .gain h3 { | |
| margin: 0; | |
| font-size: 18px; | |
| } | |
| .gain p { | |
| margin: 0; | |
| color: var(--muted); | |
| line-height: 1.55; | |
| font-size: 14px; | |
| } | |
| .gain ul { | |
| margin: 0; | |
| padding-left: 18px; | |
| color: var(--muted); | |
| font-size: 13px; | |
| line-height: 1.5; | |
| } | |
| .gain-actions { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| } | |
| .gain-action { | |
| padding: 6px 8px; | |
| font-size: 12px; | |
| color: var(--cyan); | |
| } | |
| .timeline { | |
| padding: 14px; | |
| display: grid; | |
| gap: 8px; | |
| } | |
| .stream-note { | |
| margin: 0 0 12px; | |
| padding: 12px; | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| background: #fbfcfd; | |
| color: var(--muted); | |
| line-height: 1.55; | |
| } | |
| .digest-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 12px; | |
| align-items: start; | |
| } | |
| .digest-card { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| background: var(--surface); | |
| overflow: hidden; | |
| } | |
| .digest-card h3 { | |
| margin: 0; | |
| padding: 12px 14px; | |
| border-bottom: 1px solid var(--line); | |
| font-size: 16px; | |
| background: #fbfcfd; | |
| } | |
| .digest-list { | |
| display: grid; | |
| } | |
| .digest-row { | |
| display: grid; | |
| gap: 4px; | |
| padding: 10px 14px; | |
| border-bottom: 1px solid var(--line); | |
| } | |
| .digest-row:last-child { | |
| border-bottom: 0; | |
| } | |
| .digest-row strong { | |
| font-size: 13px; | |
| line-height: 1.3; | |
| } | |
| .digest-row span { | |
| color: var(--muted); | |
| font-size: 12px; | |
| line-height: 1.35; | |
| overflow-wrap: anywhere; | |
| } | |
| .digest-card.wide { | |
| grid-column: 1 / -1; | |
| } | |
| .cluster-members { | |
| color: var(--muted); | |
| font-size: 12px; | |
| line-height: 1.45; | |
| } | |
| .step { | |
| display: grid; | |
| grid-template-columns: 34px minmax(0, 1fr); | |
| gap: 10px; | |
| align-items: start; | |
| } | |
| .step-index { | |
| width: 28px; | |
| height: 28px; | |
| border-radius: 50%; | |
| background: var(--ink); | |
| color: white; | |
| display: grid; | |
| place-items: center; | |
| font-size: 12px; | |
| font-weight: 700; | |
| } | |
| .step button { | |
| width: 100%; | |
| border: 1px solid var(--line); | |
| background: var(--surface-2); | |
| border-radius: 8px; | |
| padding: 9px 10px; | |
| text-align: left; | |
| cursor: pointer; | |
| color: var(--text); | |
| } | |
| .workspace { | |
| display: grid; | |
| grid-template-columns: minmax(280px, 420px) minmax(0, 1fr); | |
| gap: 14px; | |
| align-items: start; | |
| } | |
| .toolbar { | |
| display: grid; | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .toolbar input, .toolbar select { | |
| width: 100%; | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| padding: 10px 12px; | |
| background: var(--surface); | |
| color: var(--text); | |
| } | |
| .chips { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| } | |
| .chip { | |
| padding: 6px 9px; | |
| font-size: 12px; | |
| } | |
| .chip.active { | |
| border-color: var(--cyan); | |
| color: var(--cyan); | |
| font-weight: 700; | |
| } | |
| .doc-list { | |
| overflow: hidden; | |
| } | |
| .doc-list-header { | |
| padding: 10px 12px; | |
| border-bottom: 1px solid var(--line); | |
| color: var(--muted); | |
| font-size: 12px; | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 8px; | |
| } | |
| .doc-results { | |
| max-height: 72vh; | |
| overflow: auto; | |
| } | |
| .doc-row { | |
| width: 100%; | |
| display: grid; | |
| gap: 4px; | |
| text-align: left; | |
| padding: 10px 12px; | |
| border: 0; | |
| border-bottom: 1px solid var(--line); | |
| border-radius: 0; | |
| } | |
| .doc-row:hover, .doc-row.active { | |
| background: #eef8fa; | |
| } | |
| .doc-row strong { | |
| font-size: 13px; | |
| line-height: 1.3; | |
| } | |
| .doc-row span { | |
| color: var(--muted); | |
| font-size: 11px; | |
| line-height: 1.35; | |
| overflow-wrap: anywhere; | |
| } | |
| .reader { | |
| min-width: 0; | |
| overflow: hidden; | |
| } | |
| .reader-head { | |
| display: grid; | |
| gap: 8px; | |
| padding: 14px 16px; | |
| border-bottom: 1px solid var(--line); | |
| } | |
| .reader-title { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 12px; | |
| align-items: start; | |
| } | |
| .reader-title h3 { | |
| margin: 0; | |
| font-size: 18px; | |
| line-height: 1.25; | |
| } | |
| .reader-meta { | |
| color: var(--muted); | |
| font-size: 12px; | |
| overflow-wrap: anywhere; | |
| } | |
| .reader-body { | |
| padding: 18px; | |
| max-height: 72vh; | |
| overflow: auto; | |
| line-height: 1.6; | |
| } | |
| .reader-body pre { | |
| white-space: pre-wrap; | |
| overflow-wrap: anywhere; | |
| margin: 0; | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; | |
| font-size: 13px; | |
| } | |
| .reader-body h1, .reader-body h2, .reader-body h3 { | |
| line-height: 1.25; | |
| margin: 1em 0 .4em; | |
| } | |
| .reader-body p { | |
| margin: .45em 0; | |
| } | |
| .reader-body code { | |
| background: #eef2f5; | |
| padding: 1px 4px; | |
| border-radius: 4px; | |
| font-size: .95em; | |
| } | |
| .reader-body table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 13px; | |
| } | |
| .reader-body th, .reader-body td { | |
| border: 1px solid var(--line); | |
| padding: 6px; | |
| vertical-align: top; | |
| } | |
| .project-table { | |
| overflow: auto; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| th, td { | |
| padding: 10px; | |
| border-bottom: 1px solid var(--line); | |
| text-align: left; | |
| vertical-align: top; | |
| font-size: 13px; | |
| } | |
| th { | |
| color: var(--muted); | |
| background: #fbfcfd; | |
| position: sticky; | |
| top: 0; | |
| } | |
| .badge { | |
| display: inline-block; | |
| padding: 2px 6px; | |
| border-radius: 999px; | |
| border: 1px solid var(--line); | |
| color: var(--muted); | |
| font-size: 11px; | |
| margin: 0 3px 3px 0; | |
| white-space: nowrap; | |
| } | |
| .badge.urgent { color: var(--red); border-color: #f2b8b5; } | |
| .badge.keep { color: var(--green); border-color: #add8bf; } | |
| .badge.merge { color: var(--violet); border-color: #c9bee8; } | |
| .badge.human { color: var(--amber); border-color: #e8cc91; } | |
| .empty { | |
| color: var(--muted); | |
| padding: 18px; | |
| text-align: center; | |
| } | |
| @media (max-width: 980px) { | |
| .app { grid-template-columns: 1fr; } | |
| aside { position: static; height: auto; } | |
| .nav { grid-template-columns: repeat(2, minmax(0, 1fr)); } | |
| .nav button { padding: 8px 10px; } | |
| .mini-stats { display: none; } | |
| .hero, .workspace, .gains, .split-cards, .digest-grid { grid-template-columns: 1fr; } | |
| .digest-card.wide { grid-column: auto; } | |
| main { padding: 16px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <aside> | |
| <div class="brand"> | |
| <h1>ProjectIntakeTower verstehen</h1> | |
| <p id="generatedAt"></p> | |
| </div> | |
| <div class="nav"> | |
| <button data-section="overview" class="active">Ueberblick</button> | |
| <a href="portfolio-map.html">Portfolio Map</a> | |
| <button data-section="digest">Verdichtung</button> | |
| <button data-section="core">Core</button> | |
| <button data-section="safety">Safety</button> | |
| <button data-section="recommended">Map + 4</button> | |
| <button data-section="documents">Dokumente</button> | |
| <button data-section="projects">Projekte & Gates</button> | |
| </div> | |
| <div class="mini-stats" id="miniStats"></div> | |
| </aside> | |
| <main> | |
| <section class="hero" data-view="overview"> | |
| <div class="panel"> | |
| <h2>Was gestern greifbar wurde</h2> | |
| <p id="overviewText"></p> | |
| <div class="pattern" id="pattern"></div> | |
| <div class="split-cards" id="workstreamCards"></div> | |
| </div> | |
| <div class="visual-panel"> | |
| <img id="visualAsset" alt="Project learning map"> | |
| <div>Die vorhandene Lern-Grafik bleibt als visuelle Landkarte erhalten. Die neue Website macht darunter jedes Textartefakt direkt aufrufbar.</div> | |
| </div> | |
| </section> | |
| <section class="section" data-view="core"> | |
| <h2>Core: echte Projektlandkarte</h2> | |
| <p>Das ist der Teil, der wirklich Wert erzeugt hat: Inventar, Triage, Top-10-Actions, Job-Contracts, Verifier, Visual Summary und Owner Digest.</p> | |
| <div class="timeline" id="coreDocs"></div> | |
| </section> | |
| <section class="section" data-view="digest"> | |
| <h2>Verdichtung: worin der Mehrwert liegt</h2> | |
| <p>Nicht die einzelnen <code>jobs/pending/*.json</code> sind der Hauptwert, sondern diese zusammengezogene Projektkarte.</p> | |
| <div class="digest-grid" id="digestGrid"></div> | |
| </section> | |
| <section class="section" data-view="safety"> | |
| <h2>Safety: Kaskadenkontrolle</h2> | |
| <p>Das ist nicht normale Produktarbeit, sondern Betriebsunfall-Management. Es bleibt wertvoll, aber es gehoert in eine andere mentale Schublade.</p> | |
| <div class="timeline" id="safetyDocs"></div> | |
| <div class="stream-note">Safety-Pattern: Chaos Power Mode -> Stop-Loss -> Containment -> Morning Resume.</div> | |
| <div class="gains" id="gains"></div> | |
| </section> | |
| <section class="section" data-view="recommended"> | |
| <h2>Portfolio-Map plus 4 Stuetzen</h2> | |
| <p>Nicht mit <code>triage/HUMAN_DECISION_REQUIRED.md</code> starten. Lies zuerst die Portfolio-Map, dann die vier Dokumente, die sie tragen.</p> | |
| <div class="timeline" id="readingPath"></div> | |
| </section> | |
| <section class="section" data-view="documents"> | |
| <h2>Dokumente</h2> | |
| <div class="workspace"> | |
| <div> | |
| <div class="toolbar"> | |
| <input id="searchInput" type="search" placeholder="Suche in allen Textdokumenten"> | |
| <select id="groupSelect"></select> | |
| <div class="chips" id="quickChips"></div> | |
| </div> | |
| <div class="doc-list"> | |
| <div class="doc-list-header"><span id="resultCount"></span><span id="activeFilter"></span></div> | |
| <div class="doc-results" id="docResults"></div> | |
| </div> | |
| </div> | |
| <article class="reader"> | |
| <div class="reader-head"> | |
| <div class="reader-title"> | |
| <h3 id="readerTitle"></h3> | |
| <a id="readerLink" href="" target="_blank" rel="noreferrer">Original</a> | |
| </div> | |
| <div class="reader-meta" id="readerMeta"></div> | |
| </div> | |
| <div class="reader-body" id="readerBody"></div> | |
| </article> | |
| </div> | |
| </section> | |
| <section class="section" data-view="projects"> | |
| <h2>Projekte & Human Gates</h2> | |
| <p>Die Tabelle zeigt die wichtigsten Projektknoten aus der vorhandenen Priorisierung. Externe Projektdateien werden hier nur referenziert.</p> | |
| <div class="project-table"> | |
| <table> | |
| <thead><tr><th>Projekt</th><th>Score</th><th>Status</th><th>Warum</th></tr></thead> | |
| <tbody id="projectRows"></tbody> | |
| </table> | |
| </div> | |
| </section> | |
| </main> | |
| </div> | |
| <script src="site-data.js"></script> | |
| <script> | |
| const data = window.PROJECT_UNDERSTANDING_DATA; | |
| const byPath = new Map(data.documents.map(doc => [doc.path, doc])); | |
| let currentGroup = "all"; | |
| let currentStream = "all"; | |
| let currentQuery = ""; | |
| let activeDoc = data.documents[0]; | |
| const escapeHtml = value => String(value ?? "").replace(/[&<>"']/g, ch => ({ | |
| "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" | |
| }[ch])); | |
| function compactPath(path) { | |
| return path.replace("ProjectIntakeTower/", "PIT/"); | |
| } | |
| function formatBytes(bytes) { | |
| if (bytes > 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + " MB"; | |
| if (bytes > 1024) return Math.round(bytes / 1024) + " KB"; | |
| return bytes + " B"; | |
| } | |
| function showSection(name) { | |
| document.querySelectorAll("[data-view]").forEach(el => { | |
| el.style.display = el.dataset.view === name ? "" : "none"; | |
| }); | |
| document.querySelectorAll(".nav button").forEach(btn => { | |
| btn.classList.toggle("active", btn.dataset.section === name); | |
| }); | |
| } | |
| function renderMarkdown(content) { | |
| const lines = String(content || "").split(/\r?\n/); | |
| let html = ""; | |
| let inCode = false; | |
| let code = []; | |
| let inList = false; | |
| let table = []; | |
| const flushList = () => { | |
| if (inList) { html += "</ul>"; inList = false; } | |
| }; | |
| const flushTable = () => { | |
| if (!table.length) return; | |
| html += "<table><tbody>"; | |
| for (const row of table) { | |
| const cells = row.split("|").slice(1, -1); | |
| if (!cells.length) continue; | |
| const isSep = cells.every(cell => /^:?-{3,}:?$/.test(cell.trim())); | |
| if (isSep) continue; | |
| html += "<tr>" + cells.map(cell => "<td>" + inlineMarkdown(cell.trim()) + "</td>").join("") + "</tr>"; | |
| } | |
| html += "</tbody></table>"; | |
| table = []; | |
| }; | |
| const flushCode = () => { | |
| if (inCode) { | |
| html += "<pre>" + escapeHtml(code.join("\n")) + "</pre>"; | |
| code = []; | |
| inCode = false; | |
| } | |
| }; | |
| for (const line of lines) { | |
| if (line.startsWith("```")) { | |
| if (inCode) flushCode(); | |
| else { flushList(); flushTable(); inCode = true; code = []; } | |
| continue; | |
| } | |
| if (inCode) { code.push(line); continue; } | |
| if (/^\|.*\|$/.test(line)) { | |
| flushList(); | |
| table.push(line); | |
| continue; | |
| } | |
| flushTable(); | |
| const heading = line.match(/^(#{1,4})\s+(.*)$/); | |
| if (heading) { | |
| flushList(); | |
| const level = Math.min(heading[1].length + 1, 4); | |
| html += `<h${level}>${inlineMarkdown(heading[2])}</h${level}>`; | |
| continue; | |
| } | |
| const bullet = line.match(/^\s*[-*]\s+(.*)$/); | |
| if (bullet) { | |
| if (!inList) { html += "<ul>"; inList = true; } | |
| html += "<li>" + inlineMarkdown(bullet[1]) + "</li>"; | |
| continue; | |
| } | |
| if (!line.trim()) { | |
| flushList(); | |
| html += ""; | |
| continue; | |
| } | |
| flushList(); | |
| html += "<p>" + inlineMarkdown(line) + "</p>"; | |
| } | |
| flushCode(); | |
| flushList(); | |
| flushTable(); | |
| return html; | |
| } | |
| function inlineMarkdown(text) { | |
| return escapeHtml(text) | |
| .replace(/`([^`]+)`/g, "<code>$1</code>") | |
| .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"); | |
| } | |
| function renderReader(doc) { | |
| activeDoc = doc; | |
| document.getElementById("readerTitle").textContent = doc.title; | |
| document.getElementById("readerMeta").textContent = | |
| `${doc.path} - ${doc.workstream} - ${doc.group} - ${doc.kind} - ${formatBytes(doc.size)}`; | |
| const link = document.getElementById("readerLink"); | |
| if (doc.href) { | |
| link.style.display = ""; | |
| link.href = doc.href; | |
| } else { | |
| link.style.display = "none"; | |
| } | |
| const body = document.getElementById("readerBody"); | |
| if (doc.kind === "md" || doc.kind === "generated") { | |
| body.innerHTML = renderMarkdown(doc.content); | |
| } else { | |
| body.innerHTML = "<pre>" + escapeHtml(doc.content) + "</pre>"; | |
| } | |
| document.querySelectorAll(".doc-row").forEach(row => row.classList.toggle("active", row.dataset.path === doc.path)); | |
| history.replaceState(null, "", "#doc=" + encodeURIComponent(doc.path)); | |
| } | |
| function scoreDoc(doc, query) { | |
| if (!query) return doc.priority; | |
| const q = query.toLowerCase(); | |
| const title = doc.title.toLowerCase(); | |
| const path = doc.path.toLowerCase(); | |
| const content = doc.content.toLowerCase(); | |
| let score = doc.priority; | |
| if (title.includes(q)) score += 800; | |
| if (path.includes(q)) score += 500; | |
| if (content.includes(q)) score += 250; | |
| return score; | |
| } | |
| function filterDocuments() { | |
| const query = currentQuery.trim().toLowerCase(); | |
| let docs = data.documents.filter(doc => | |
| (currentGroup === "all" || doc.group === currentGroup) && | |
| (currentStream === "all" || doc.workstream === currentStream) | |
| ); | |
| if (query) { | |
| docs = docs.filter(doc => | |
| doc.title.toLowerCase().includes(query) || | |
| doc.path.toLowerCase().includes(query) || | |
| doc.content.toLowerCase().includes(query) | |
| ); | |
| } | |
| docs.sort((a, b) => scoreDoc(b, query) - scoreDoc(a, query) || a.path.localeCompare(b.path)); | |
| return docs; | |
| } | |
| function renderDocList() { | |
| const docs = filterDocuments(); | |
| const list = document.getElementById("docResults"); | |
| document.getElementById("resultCount").textContent = `${docs.length} Treffer`; | |
| document.getElementById("activeFilter").textContent = | |
| `${currentStream === "all" ? "alle Streams" : currentStream} - ${currentGroup === "all" ? "alle Gruppen" : currentGroup}`; | |
| list.innerHTML = docs.map(doc => ` | |
| <button class="doc-row ${activeDoc && activeDoc.path === doc.path ? "active" : ""}" data-path="${escapeHtml(doc.path)}"> | |
| <strong>${escapeHtml(doc.title)}</strong> | |
| <span>${escapeHtml(compactPath(doc.path))}</span> | |
| <span>${escapeHtml(doc.workstream)} - ${escapeHtml(doc.group)} - ${escapeHtml(doc.kind)} - ${formatBytes(doc.size)}</span> | |
| </button> | |
| `).join("") || '<div class="empty">Keine Treffer</div>'; | |
| list.querySelectorAll(".doc-row").forEach(row => { | |
| row.addEventListener("click", () => { | |
| const doc = byPath.get(row.dataset.path); | |
| if (doc) renderReader(doc); | |
| }); | |
| }); | |
| } | |
| function renderStats() { | |
| const c = data.counts; | |
| const metrics = [ | |
| ["Textdokumente", c.documents], | |
| ["Projekte", c.total_projects], | |
| ["Core", c.coreDocuments], | |
| ["Safety", c.safetyDocuments], | |
| ["Raw", c.rawDocuments], | |
| ["Urgent", c.URGENT], | |
| ]; | |
| document.getElementById("miniStats").innerHTML = metrics.map(([label, value]) => | |
| `<div class="metric"><strong>${escapeHtml(value ?? "-")}</strong><span>${escapeHtml(label)}</span></div>` | |
| ).join(""); | |
| } | |
| function renderGains() { | |
| document.getElementById("gains").innerHTML = data.gains.map(gain => ` | |
| <article class="gain"> | |
| <h3>${escapeHtml(gain.title)}</h3> | |
| <p><strong>${escapeHtml(gain.claim)}</strong></p> | |
| <p>${escapeHtml(gain.why)}</p> | |
| <ul>${gain.reuseIdeas.map(item => `<li>${escapeHtml(item)}</li>`).join("")}</ul> | |
| <div class="gain-actions"> | |
| ${gain.evidencePaths.map(path => `<button class="gain-action" data-open="${escapeHtml(path)}">${escapeHtml(compactPath(path))}</button>`).join("")} | |
| </div> | |
| </article> | |
| `).join(""); | |
| document.querySelectorAll(".gain-action").forEach(btn => { | |
| btn.addEventListener("click", () => { | |
| openDocPath(btn.dataset.open); | |
| showSection("documents"); | |
| }); | |
| }); | |
| } | |
| function renderWorkstreams() { | |
| document.getElementById("workstreamCards").innerHTML = data.workstreams.map(stream => ` | |
| <article class="split-card ${escapeHtml(stream.id.toLowerCase())}"> | |
| <strong>${escapeHtml(stream.title)}</strong> | |
| <span>${escapeHtml(stream.claim)}</span> | |
| <span>${escapeHtml(stream.meaning)}</span> | |
| </article> | |
| `).join(""); | |
| } | |
| function renderDocTimeline(targetId, paths) { | |
| document.getElementById(targetId).innerHTML = paths.map((path, index) => { | |
| const doc = byPath.get(path); | |
| const title = doc ? doc.title : path; | |
| return `<div class="step"><div class="step-index">${index + 1}</div><button data-open="${escapeHtml(path)}"><strong>${escapeHtml(title)}</strong><br><span>${escapeHtml(compactPath(path))}</span></button></div>`; | |
| }).join(""); | |
| document.querySelectorAll(`#${targetId} button`).forEach(btn => { | |
| btn.addEventListener("click", () => { | |
| openDocPath(btn.dataset.open); | |
| showSection("documents"); | |
| }); | |
| }); | |
| } | |
| function renderReadingPath() { | |
| renderDocTimeline("readingPath", data.recommendedReading); | |
| renderDocTimeline("coreDocs", data.coreDocuments); | |
| renderDocTimeline("safetyDocs", data.safetyDocuments); | |
| } | |
| function renderProjectDigestRows(items, mode) { | |
| return items.map(item => { | |
| const cats = (item.categories || []).join(", "); | |
| const stats = `score ${item.score ?? "-"} | dirty ${item.dirty_count ?? 0} | todo ${item.todo_fixme_count ?? 0}`; | |
| const detail = mode === "agent" && item.suggested_run | |
| ? `${item.suggested_run}${item.human_gate ? " | human gate" : ""}` | |
| : (item.reasons || []).slice(0, 2).join(" | "); | |
| return `<div class="digest-row"> | |
| <strong>${escapeHtml(item.name || item.path)}</strong> | |
| <span>${escapeHtml(compactPath(item.path || ""))}</span> | |
| <span>${escapeHtml(stats)}${cats ? " | " + escapeHtml(cats) : ""}</span> | |
| <span>${escapeHtml(detail || "")}</span> | |
| </div>`; | |
| }).join("") || '<div class="empty">Keine Treffer</div>'; | |
| } | |
| function renderClusters(clusters) { | |
| return clusters.map(cluster => ` | |
| <div class="digest-row"> | |
| <strong>${escapeHtml(cluster.name)} (${cluster.count})</strong> | |
| <span class="cluster-members">${cluster.members.map(m => escapeHtml(m.name || m.path)).join(", ")}</span> | |
| </div> | |
| `).join("") || '<div class="empty">Keine Cluster</div>'; | |
| } | |
| function renderDigest() { | |
| const c = data.condensation; | |
| document.getElementById("digestGrid").innerHTML = ` | |
| <article class="digest-card"> | |
| <h3>Wirklich aktiv</h3> | |
| <div class="digest-list">${renderProjectDigestRows(c.activeProjects)}</div> | |
| </article> | |
| <article class="digest-card"> | |
| <h3>Alte Energie</h3> | |
| <div class="digest-list">${renderProjectDigestRows(c.oldEnergy)}</div> | |
| </article> | |
| <article class="digest-card"> | |
| <h3>Gefaehrliche dirty states</h3> | |
| <div class="digest-list">${renderProjectDigestRows(c.dirtyDanger)}</div> | |
| </article> | |
| <article class="digest-card"> | |
| <h3>Gehoert zusammen</h3> | |
| <div class="digest-list">${renderClusters(c.clusters)}</div> | |
| </article> | |
| <article class="digest-card wide"> | |
| <h3>Naechster echter Agentenlauf</h3> | |
| <div class="digest-list">${renderProjectDigestRows(c.nextAgentRuns, "agent")}</div> | |
| </article> | |
| `; | |
| } | |
| function badgeClass(cat) { | |
| const lower = String(cat).toLowerCase(); | |
| if (lower.includes("urgent")) return "urgent"; | |
| if (lower.includes("keep")) return "keep"; | |
| if (lower.includes("merge")) return "merge"; | |
| if (lower.includes("human")) return "human"; | |
| return ""; | |
| } | |
| function renderProjects() { | |
| document.getElementById("projectRows").innerHTML = data.projects.slice(0, 80).map(project => ` | |
| <tr> | |
| <td><strong>${escapeHtml(project.name || project.path)}</strong><br><span class="reader-meta">${escapeHtml(project.path || "")}</span></td> | |
| <td>${escapeHtml(project.score ?? "-")}</td> | |
| <td>${(project.categories || []).map(cat => `<span class="badge ${badgeClass(cat)}">${escapeHtml(cat)}</span>`).join("")}</td> | |
| <td>${(project.reasons || project.score_reasons || []).slice(0, 3).map(reason => escapeHtml(reason)).join("<br>")}</td> | |
| </tr> | |
| `).join(""); | |
| } | |
| function renderFilters() { | |
| const groups = Array.from(new Set(data.documents.map(doc => doc.group))).sort(); | |
| document.getElementById("groupSelect").innerHTML = | |
| '<option value="all">Alle Gruppen</option>' + groups.map(group => `<option value="${escapeHtml(group)}">${escapeHtml(group)}</option>`).join(""); | |
| document.getElementById("quickChips").innerHTML = ["Core", "Safety", "Raw", "Guide"].map(stream => | |
| `<button class="chip" data-stream="${escapeHtml(stream)}">${escapeHtml(stream)}</button>` | |
| ).join(""); | |
| document.getElementById("groupSelect").addEventListener("change", event => { | |
| currentGroup = event.target.value; | |
| updateChips(); | |
| renderDocList(); | |
| }); | |
| document.querySelectorAll(".chip").forEach(chip => { | |
| chip.addEventListener("click", () => { | |
| currentStream = currentStream === chip.dataset.stream ? "all" : chip.dataset.stream; | |
| updateChips(); | |
| renderDocList(); | |
| }); | |
| }); | |
| document.getElementById("searchInput").addEventListener("input", event => { | |
| currentQuery = event.target.value; | |
| renderDocList(); | |
| }); | |
| } | |
| function updateChips() { | |
| document.querySelectorAll(".chip").forEach(chip => chip.classList.toggle("active", chip.dataset.stream === currentStream)); | |
| } | |
| function openDocPath(path) { | |
| const doc = byPath.get(path); | |
| if (doc) { | |
| renderReader(doc); | |
| renderDocList(); | |
| } | |
| } | |
| function init() { | |
| document.getElementById("generatedAt").textContent = `Generiert: ${data.generatedAt}`; | |
| document.getElementById("overviewText").textContent = data.narrativeSections.map(s => s.body).join(" "); | |
| document.getElementById("pattern").textContent = data.pattern; | |
| document.getElementById("visualAsset").src = data.visualAsset; | |
| document.querySelectorAll(".nav button").forEach(btn => btn.addEventListener("click", () => showSection(btn.dataset.section))); | |
| renderStats(); | |
| renderWorkstreams(); | |
| renderDigest(); | |
| renderGains(); | |
| renderReadingPath(); | |
| renderFilters(); | |
| renderProjects(); | |
| const hashDoc = decodeURIComponent((location.hash.match(/doc=([^&]+)/) || [])[1] || ""); | |
| const hasHashDoc = Boolean(hashDoc && byPath.has(hashDoc)); | |
| activeDoc = byPath.get(hashDoc) || byPath.get("__generated__/DREI_GEWINNE.md") || data.documents[0]; | |
| renderDocList(); | |
| renderReader(activeDoc); | |
| showSection(hasHashDoc ? "documents" : "overview"); | |
| } | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |
| ''', | |
| 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() | |