AgentProjectMinenraeumer / ProjectIntakeTower /verifier /generate_understanding_site.py
smlflg's picture
Initial public upload from Projekte/AgentProjectMinenraeumer (part 9)
b67a1e9 verified
Raw History Blame Contribute Delete
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 => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;"
}[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()