diff --git a/API.md b/API.md index 5ae7f76..47ae955 100644 --- a/API.md +++ b/API.md @@ -12,13 +12,14 @@ Ohne `v2` gilt die neueste Version. ## Themen und Versionen -- `GET /api/themen` → `[{slug, name, titel, hinweise, spezialisierung, angelegt, status, laeuft, stufe, versionen: [{v, status, laeuft, stufe, angelegt, kosten_usd}]}]` +- `GET /api/themen` → `[{slug, name, thema, spezialisierung, titel, hinweise, angelegt, status, laeuft, stufe, versionen: [{v, status, laeuft, stufe, angelegt, kosten_usd}]}]` – `status/laeuft/stufe` gelten für die neueste Version; `status`: `neu | laeuft | pausiert | fertig | fehler` - `POST /api/themen` `{name, hinweise, spezialisierung}` → Thema wie oben (legt `v1` an) - – `hinweise`: wie das Thema zu verstehen ist (fügt hinzu); `spezialisierung`: Grenze des Umfangs, nur was dafür gebraucht wird (nimmt weg) + – `name`: Hauptthema; `spezialisierung`: grenzt es ein, nur was dafür gebraucht wird; die Bezeichnung (`name` in der Antwort, Ordner) wird „Thema – Spezialisierung“ + – `hinweise`: wie das Thema zu verstehen ist; gelten je Version und sind bei einer neuen Version änderbar - `GET /api/themen/{slug}` → Thema wie oben - `DELETE /api/themen/{slug}` – ganzes Thema -- `POST /api/themen/{slug}/versionen` `{hinweise?, spezialisierung?}` → `{"v": N}` – nächste Version ohne Start; fehlende Felder übernimmt sie von der neuesten Version +- `POST /api/themen/{slug}/versionen` `{hinweise?}` → `{"v": N}` – nächste Version ohne Start; ohne Angabe übernimmt sie die Hinweise der neuesten Version - `DELETE /api/themen/{slug}/v{N}` – nur diese Version - `POST /api/themen/{slug}/v{N}/start` – startet oder setzt fort (resümiert an der offenen Stufe) - `POST /api/themen/{slug}/v{N}/pause` – bricht laufende Agenten ab; Neustart mit `start` @@ -38,7 +39,7 @@ Alte Zustände ohne diese Stufe bekommen sie beim Laden nachgetragen. ```json { - "slug": "godot-4", "name": "Godot 4", "version": 1, "titel": "Godot 4", "hinweise": "…", "spezialisierung": "", // Eingaben dieser Version + "slug": "godot-4", "name": "Godot 4", "version": 1, "thema": "Godot 4", "spezialisierung": "", "titel": "Godot 4", "hinweise": "…", // hinweise: die dieser Version "status": "laeuft", "grund": "", "stufe": "schreiben", "angelegt": "2026-09-28T10:00:00Z", "deutung": {"titel": "Godot 4", "untertitel": "…", "deutung": "…", "buchtyp": "werkzeug|wissen", "zielgruppe": "…"}, "stufen": [ @@ -92,9 +93,9 @@ Solange ein Lauf läuft, enthält `buch` die bereits geschriebenen Kapitel (Stat ``` storage// - thema.json {name, slug, titel, hinweise, spezialisierung, angelegt} + thema.json {name, thema, spezialisierung, slug, titel, hinweise, angelegt} – name = „Thema – Spezialisierung“ v1/, v2/, … je Version: - state.json Zustand wie oben (ohne Buch), mit hinweise und spezialisierung der Version + state.json Zustand wie oben (ohne Buch), mit den Hinweisen der Version deutung.json vom Recherche-Agenten position.json Leseposition quellen/ QUELLEN.md + geklonte/geladene Quellen diff --git a/README.md b/README.md index 3b9a5c3..5b9fc69 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # buchwerk -Aus einem Thema (plus optionalen Hinweisen und einer optionalen Spezialisierung, die den Umfang begrenzt) entsteht ein deutschsprachiges Sachbuch. Ein Python-Backend +Aus einem Thema (plus optionaler Spezialisierung, die es eingrenzt, und optionalen Hinweisen) entsteht ein deutschsprachiges Sachbuch. Ein Python-Backend steuert Agenten des Claude Agent SDK (Opus 5.5): Recherche und Deutung, Gliederung, Kapitel parallel schreiben, jedes Kapitel durch einen unabhängigen Agenten prüfen, Querverweise abgleichen, Vorwort, Montage (Markdown, README, PDF). Das Buch wird im Browser als Leseansicht gezeigt. diff --git a/backend/main.py b/backend/main.py index 79d2ca6..72d7fdd 100644 --- a/backend/main.py +++ b/backend/main.py @@ -34,7 +34,6 @@ class ThemaNeu(BaseModel): class VersionNeu(BaseModel): hinweise: str | None = None # None: wie die bisherige Version - spezialisierung: str | None = None class Position(BaseModel): @@ -73,7 +72,8 @@ def _thema_info(t: dict) -> dict | None: if not vs: return None neueste = vs[-1] - return {"slug": t["slug"], "name": t["name"], "titel": t["titel"], "hinweise": t.get("hinweise", ""), "spezialisierung": t.get("spezialisierung", ""), + return {"slug": t["slug"], "name": t["name"], "thema": t.get("thema") or t["name"], "spezialisierung": t.get("spezialisierung", ""), + "titel": t["titel"], "hinweise": t.get("hinweise", ""), "angelegt": t.get("angelegt", ""), "status": neueste["status"], "laeuft": neueste["laeuft"], "stufe": neueste["stufe"], "versionen": vs} @@ -86,7 +86,7 @@ async def themen(): @app.post("/api/themen") async def thema_anlegen(body: ThemaNeu): name = body.name.strip() - if not state.name_ok(name): + if not state.name_ok(name) or not state.name_ok(state.bezeichnung(name, body.spezialisierung.strip())): raise HTTPException(400, "Ungültiger Name") try: return _thema_info(state.anlegen(name, body.hinweise, body.spezialisierung)) @@ -110,9 +110,9 @@ async def thema_loeschen(slug: str): @app.post("/api/themen/{slug}/versionen") async def version_anlegen(slug: str, body: VersionNeu | None = None): - """Neue Version: frischer Lauf ohne Wissen über die Vorgänger; Hinweise und Spezialisierung wahlweise geändert.""" + """Neue Version: frischer Lauf ohne Wissen über die Vorgänger; Hinweise wahlweise geändert.""" b = body or VersionNeu() - key = state.version_anlegen(_thema(slug)["name"], b.hinweise, b.spezialisierung) + key = state.version_anlegen(_thema(slug)["name"], b.hinweise) return {"v": int(key.rsplit("/v", 1)[1])} @@ -121,8 +121,8 @@ async def zustand(slug: str, n: int): key = _key(slug, n) t = _thema(slug) st = state.zustand(key) - h, sp = state.eingaben(key) - return {"slug": slug, "name": t["name"], "version": n, "titel": t["titel"], **st, "hinweise": h, "spezialisierung": sp, + return {"slug": slug, "name": t["name"], "thema": t.get("thema") or t["name"], "version": n, "titel": t["titel"], **st, + "hinweise": state.hinweise_von(key), "spezialisierung": state.spezialisierung_von(key), "deutung": st.get("deutung") or state.deutung(key), "ereignisse": st["ereignisse"][-100:]} diff --git a/backend/pipeline.py b/backend/pipeline.py index b791400..4aa9f75 100644 --- a/backend/pipeline.py +++ b/backend/pipeline.py @@ -73,9 +73,9 @@ def _offen(name: str) -> None: def _thema(name: str) -> tuple[str, str, str]: - """Titel, Hinweise, Spezialisierung – die Eingaben gelten je Version.""" + """Hauptthema (ohne Spezialisierung), Hinweise der Version, Spezialisierung des Themas.""" t = state.thema(name) or {} - return (t.get("titel") or name, *state.eingaben(name)) + return t.get("thema") or state.thema_von(name), state.hinweise_von(name), state.spezialisierung_von(name) def _kapitel(name: str) -> list[dict]: diff --git a/backend/state.py b/backend/state.py index 266dc44..4e3224f 100644 --- a/backend/state.py +++ b/backend/state.py @@ -55,32 +55,32 @@ def name_von_slug(sl: str) -> str | None: return next((t["name"] for t in themen() if t.get("slug") == sl), None) -def version_anlegen(name: str, hinweise: str | None = None, spezialisierung: str | None = None) -> str: - """Neue Version; ohne Angabe übernimmt sie Hinweise und Spezialisierung der neuesten Version (bzw. des Themas).""" +def version_anlegen(name: str, hinweise: str | None = None) -> str: + """Neue Version; ohne Angabe übernimmt sie die Hinweise der neuesten Version (bzw. des Themas).""" vs = versionen(name) - alt_h, alt_s = eingaben(f"{name}/v{vs[-1]}") if vs else _eingaben_thema(name) + alt = hinweise_von(f"{name}/v{vs[-1]}") if vs else (thema(name) or {}).get("hinweise", "") key = f"{name}/v{(vs or [0])[-1] + 1}" v = verzeichnis(key) for u in ("quellen", "buch", "berichte", "arbeit"): (v / u).mkdir(parents=True) - st = leerer_zustand() - st["hinweise"] = (alt_h if hinweise is None else hinweise).strip() - st["spezialisierung"] = (alt_s if spezialisierung is None else spezialisierung).strip() - _schreiben(v / "state.json", st) + _schreiben(v / "state.json", {**leerer_zustand(), "hinweise": (alt if hinweise is None else hinweise).strip()}) return key -def _eingaben_thema(name: str) -> tuple[str, str]: - t = thema(name) or {} - return t.get("hinweise", ""), t.get("spezialisierung", "") - - -def eingaben(key: str) -> tuple[str, str]: - """Hinweise und Spezialisierung einer Version; alte Versionen ohne eigene Eingaben nehmen die des Themas.""" +def hinweise_von(key: str) -> str: + """Hinweise gelten je Version; alte Versionen ohne eigene nehmen die des Themas.""" st = zustand(key) - if "hinweise" in st: - return st["hinweise"], st.get("spezialisierung", "") - return _eingaben_thema(key) + return st["hinweise"] if "hinweise" in st else (thema(key) or {}).get("hinweise", "") + + +def spezialisierung_von(key: str) -> str: + """Die Spezialisierung gehört zum Thema: eine andere Spezialisierung ist ein anderes Buch.""" + return (thema(key) or {}).get("spezialisierung", "") + + +def bezeichnung(name: str, spezialisierung: str) -> str: + """Ordner- und Anzeigename: „Game Design – digitale Sammelkartenspiele“.""" + return f"{name} – {spezialisierung}" if spezialisierung else name def version_loeschen(key: str) -> None: @@ -138,13 +138,17 @@ def themen() -> list[dict]: def anlegen(name: str, hinweise: str, spezialisierung: str = "") -> dict: - d = config.STORAGE / name + """name ist das Hauptthema („Game Design“), die Spezialisierung grenzt es ein; beides zusammen bildet die Bezeichnung.""" + spezialisierung = spezialisierung.strip() + bez = bezeichnung(name, spezialisierung) + d = config.STORAGE / bez if d.exists(): raise ValueError("Thema existiert schon") d.mkdir(parents=True) - t = {"name": name, "slug": slug_frei(name), "titel": name, "hinweise": hinweise.strip(), "spezialisierung": spezialisierung.strip(), "angelegt": jetzt()} + t = {"name": bez, "thema": name, "spezialisierung": spezialisierung, "slug": slug_frei(bez), "titel": bez, + "hinweise": hinweise.strip(), "angelegt": jetzt()} _schreiben(d / "thema.json", t) - version_anlegen(name) + version_anlegen(bez) return t @@ -156,14 +160,14 @@ def loeschen(name: str) -> None: def zuruecksetzen(key: str) -> None: v = verzeichnis(key) - h, s = eingaben(key) + h = hinweise_von(key) _cache.pop(key, None) for u in ("quellen", "buch", "berichte", "arbeit"): shutil.rmtree(v / u, ignore_errors=True) (v / u).mkdir() for f in ("deutung.json", "log.jsonl", "state.json", "position.json"): (v / f).unlink(missing_ok=True) - _schreiben(v / "state.json", {**leerer_zustand(), "hinweise": h, "spezialisierung": s}) + _schreiben(v / "state.json", {**leerer_zustand(), "hinweise": h}) def thema(key: str) -> dict | None: diff --git a/frontend/src/App.vue b/frontend/src/App.vue index e542346..0e02d37 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -172,7 +172,7 @@ function tokens(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? M
- +
diff --git a/frontend/src/components/Lauf.vue b/frontend/src/components/Lauf.vue index 7e08812..bd8e2f4 100644 --- a/frontend/src/components/Lauf.vue +++ b/frontend/src/components/Lauf.vue @@ -36,12 +36,11 @@ const gruppen = computed(() => { const ereignisse = computed(() => (props.state?.ereignisse || []).slice(-100).reverse()) function reset() { if (confirm('Lauf zurücksetzen? Alles außer dem Thema wird gelöscht.')) emit('reset') } -// Neue Version: Formular mit den Eingaben dieser Version vorbelegt; Thema bleibt, Hinweise und Spezialisierung sind änderbar. +// Neue Version: Formular mit den Hinweisen dieser Version vorbelegt; Thema und Spezialisierung bleiben. const neuOffen = ref(false) const neuHinweise = ref('') -const neuSpezialisierung = ref('') -function neuOeffnen() { neuHinweise.value = props.state.hinweise || ''; neuSpezialisierung.value = props.state.spezialisierung || ''; neuOffen.value = true } -function neuStarten() { neuOffen.value = false; emit('neue-version', { hinweise: neuHinweise.value.trim(), spezialisierung: neuSpezialisierung.value.trim() }) } +function neuOeffnen() { neuHinweise.value = props.state.hinweise || ''; neuOffen.value = true } +function neuStarten() { neuOffen.value = false; emit('neue-version', { hinweise: neuHinweise.value.trim() }) }