Spezialisierung gehört zum Thema, Hinweise zur Version

Thema „Game Design“ plus Spezialisierung „digitale Sammelkartenspiele“
wird zur Bezeichnung „Game Design – digitale Sammelkartenspiele“
(Ordner, Liste). Die Prompts bekommen das Hauptthema und die
Spezialisierung getrennt. „Neue Version“ ändert nur noch die Hinweise.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
team3
2026-10-12 15:42:44 +02:00
parent d2da9920a3
commit 4c989bc93c
7 changed files with 50 additions and 47 deletions

13
API.md
View File

@@ -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>/
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

View File

@@ -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.

View File

@@ -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:]}

View File

@@ -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]:

View File

@@ -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:

View File

@@ -172,7 +172,7 @@ function tokens(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? M
<div class="neu">
<input v-model="neuName" placeholder="Thema, z. B. Godot 4" @keyup.enter="anlegen" />
<input v-model="neuHinweise" placeholder="Hinweise (optional), z. B. für Entwickler, ab Version 4.6" @keyup.enter="anlegen" />
<input v-model="neuSpezialisierung" placeholder="Spezialisierung (optional), z. B. Kartenspiel gegen KI" title="Begrenzt den Umfang: nur, was dafür gebraucht wird" @keyup.enter="anlegen" />
<input v-model="neuSpezialisierung" placeholder="Spezialisierung (optional), z. B. digitale Sammelkartenspiele" title="Grenzt das Thema ein; Bezeichnung wird „Thema – Spezialisierung“" @keyup.enter="anlegen" />
<button @click="anlegen">Anlegen</button>
</div>
</aside>

View File

@@ -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() }) }
</script>
<template>
@@ -50,7 +49,7 @@ function neuStarten() { neuOffen.value = false; emit('neue-version', { hinweise:
<button v-if="!laeuft && !fertig" @click="emit('start')">{{ begonnen ? 'Fortsetzen' : 'Buch erzeugen' }}</button>
<button v-if="laeuft" class="sekundaer" @click="emit('pause')">Pause</button>
<button v-if="!laeuft && begonnen && !fertig" class="sekundaer" @click="reset">Zurücksetzen</button>
<button v-if="!laeuft && begonnen" class="sekundaer" title="Gleiches Thema, kompletter neuer Lauf; Hinweise und Spezialisierung änderbar" @click="neuOeffnen">Neue Version</button>
<button v-if="!laeuft && begonnen" class="sekundaer" title="Gleiches Thema, kompletter neuer Lauf; Hinweise änderbar" @click="neuOeffnen">Neue Version</button>
<span class="spacer"></span>
<span class="badge" :class="statusFarbe[state.status]">{{ statusText[state.status] || state.status }}<template v-if="state.stufe && laeuft"> · {{ state.stufe }}</template></span>
<span class="badge">{{ usd(state.kosten_usd) }}</span>
@@ -61,14 +60,13 @@ function neuStarten() { neuOffen.value = false; emit('neue-version', { hinweise:
<div v-if="neuOffen" class="neu-version">
<div class="titel">Neue Version von „{{ state.name }}“</div>
<input v-model="neuHinweise" placeholder="Hinweise (optional), z. B. für Entwickler, ab Version 4.6" @keyup.enter="neuStarten" />
<input v-model="neuSpezialisierung" placeholder="Spezialisierung (optional), z. B. Kartenspiel gegen KI" title="Begrenzt den Umfang: nur, was dafür gebraucht wird" @keyup.enter="neuStarten" />
<div class="knoepfe"><button @click="neuStarten">Buch erzeugen</button><button class="sekundaer" @click="neuOffen = false">Abbrechen</button></div>
</div>
<div class="eingaben">
<b>Thema</b><span>{{ state.name }}</span>
<b>Hinweise</b><span>{{ state.hinweise || '–' }}</span>
<b>Thema</b><span>{{ state.thema }}</span>
<b>Spezialisierung</b><span>{{ state.spezialisierung || '–' }}</span>
<b>Hinweise</b><span>{{ state.hinweise || '–' }}</span>
</div>
<div v-if="state.deutung" class="deutung">