Versionen je Thema

Ein Thema hat Versionen storage/<Thema>/v<N>/ mit eigenem Lauf, Buch und
Leseposition; bestehende Ablagen wandern beim Start nach v1/. Intern ist
„Thema/vN“ der Schlüssel aller Zustandsfunktionen. API: ?v=N an allen
Themen-Endpunkten, POST /version, DELETE ?v=N. Oberfläche: Versionsliste
unter dem Thema (Klick auf Thema → Lauf der neuesten, auf Version → Buch),
Knopf „Neue Version“ mit gleichen Eingaben. make loeschen kennt V=<Nr>.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
team3
2026-10-05 17:01:40 +02:00
parent 8d28680a12
commit 1b6899638b
10 changed files with 229 additions and 107 deletions

View File

@@ -1,4 +1,5 @@
"""Zustand je Thema: storage/<name>/{thema.json,state.json,log.jsonl}. Alles im Speicher, bei Änderung auf Platte."""
"""Zustand je Themenversion. Ablage: storage/<Thema>/thema.json und storage/<Thema>/v<N>/{state.json, deutung.json, quellen/, buch/, …}.
Intern heißt eine Version „<Thema>/v<N>“ (Schlüssel); alle Funktionen mit `key` erwarten diesen Schlüssel. Alles im Speicher, bei Änderung auf Platte."""
import json
import re
import shutil
@@ -18,8 +19,56 @@ def name_ok(name: str) -> bool:
return bool(name) and name.strip() == name and "/" not in name and ".." not in name and len(name) < 120
def verzeichnis(name: str) -> Path:
return config.STORAGE / name
def verzeichnis(key: str) -> Path:
return config.STORAGE / key
def thema_von(key: str) -> str:
"""„Godot 4/v2“ → „Godot 4“; ein reiner Themenname bleibt unverändert."""
return key.rsplit("/", 1)[0] if "/" in key else key
def versionen(name: str) -> list[int]:
d = config.STORAGE / name
return sorted(int(x.name[1:]) for x in d.glob("v[0-9]*") if x.is_dir() and x.name[1:].isdigit())
def schluessel(name: str, v: int | None = None) -> str | None:
"""Schlüssel einer Version; ohne v die neueste. None, wenn Thema oder Version fehlen."""
vs = versionen(name)
if not vs or (v is not None and v not in vs):
return None
return f"{name}/v{vs[-1] if v is None else v}"
def version_anlegen(name: str) -> str:
n = (versionen(name) or [0])[-1] + 1
key = f"{name}/v{n}"
v = verzeichnis(key)
for u in ("quellen", "buch", "berichte", "arbeit"):
(v / u).mkdir(parents=True)
_schreiben(v / "state.json", leerer_zustand())
return key
def version_loeschen(key: str) -> None:
_cache.pop(key, None)
shutil.rmtree(verzeichnis(key), ignore_errors=True)
name = thema_von(key)
if not versionen(name):
shutil.rmtree(config.STORAGE / name, ignore_errors=True)
def migrieren() -> None:
"""Alte Ablage (alles direkt unter storage/<Thema>/) einmalig nach v1/ verschieben."""
for d in config.STORAGE.glob("*/"):
if not (d / "thema.json").exists() or not (d / "state.json").exists() or (d / "v1").exists():
continue
v1 = d / "v1"
v1.mkdir()
for kind in list(d.iterdir()):
if kind.name not in ("thema.json", "v1"):
shutil.move(str(kind), str(v1 / kind.name))
def _lesen(pfad: Path, standard):
@@ -37,7 +86,7 @@ def _schreiben(pfad: Path, daten) -> None:
def leerer_zustand() -> dict:
return {"status": "neu", "grund": "", "stufe": "", "deutung": None,
return {"status": "neu", "grund": "", "stufe": "", "deutung": None, "angelegt": jetzt(),
"stufen": [{"id": s, "titel": t, "status": "offen", "fertig": 0, "gesamt": 0,
"kosten_usd": 0.0, "tokens": 0, "gestartet": "", "beendet": ""} for s, t in config.STUFEN],
"kosten_usd": 0.0, "tokens": 0, "agenten": [], "ereignisse": []}
@@ -53,38 +102,36 @@ def themen() -> list[dict]:
def anlegen(name: str, hinweise: str) -> dict:
v = verzeichnis(name)
if v.exists():
d = config.STORAGE / name
if d.exists():
raise ValueError("Thema existiert schon")
for u in ("quellen", "buch", "berichte", "arbeit"):
(v / u).mkdir(parents=True)
d.mkdir(parents=True)
t = {"name": name, "titel": name, "hinweise": hinweise.strip(), "angelegt": jetzt()}
_schreiben(v / "thema.json", t)
_schreiben(v / "state.json", leerer_zustand())
_schreiben(d / "thema.json", t)
version_anlegen(name)
return t
def loeschen(name: str) -> None:
_cache.pop(name, None)
shutil.rmtree(verzeichnis(name), ignore_errors=True)
for k in [k for k in _cache if thema_von(k) == name]:
_cache.pop(k, None)
shutil.rmtree(config.STORAGE / name, ignore_errors=True)
def zuruecksetzen(name: str) -> None:
v = verzeichnis(name)
t = _lesen(v / "thema.json", None)
_cache.pop(name, None)
def zuruecksetzen(key: str) -> None:
v = verzeichnis(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"):
for f in ("deutung.json", "log.jsonl", "state.json", "position.json"):
(v / f).unlink(missing_ok=True)
_schreiben(v / "state.json", leerer_zustand())
if t:
_schreiben(v / "thema.json", t)
def thema(name: str) -> dict | None:
return _lesen(verzeichnis(name) / "thema.json", None)
def thema(key: str) -> dict | None:
"""thema.json des Themas – nimmt Schlüssel oder Themenname."""
return _lesen(config.STORAGE / thema_von(key) / "thema.json", None)
def zustand(name: str) -> dict: