ProjectCraft / utils /versioning.py
yania-n's picture
fix: full codebase audit - 18 bugs fixed across 9 files
386a178
Raw History Blame Contribute Delete
3.31 kB
"""Plan versioning β€” save and restore named snapshots of a ProjectPlan."""
import copy
import uuid
from datetime import datetime, timezone
from core.planner import ProjectPlan
MAX_VERSIONS = 20
def save_version(state: dict, label: str = "") -> dict:
"""
Snapshot the current plan into state["versions"].
Each version: {"id": uuid, "label": str, "timestamp": ISO str, "snapshot": plan_dict}
If label is empty, auto-generate: "Version N" where N = len(versions)+1
Returns updated state dict (deep copy).
Max 20 versions kept (drop oldest if exceeded).
"""
state = copy.deepcopy(state)
versions: list = state.setdefault("versions", [])
if not label:
label = f"Version {len(versions) + 1}"
plan = state.get("plan")
if isinstance(plan, ProjectPlan):
snapshot = plan.model_dump(exclude_none=True)
elif isinstance(plan, dict):
snapshot = plan
else:
snapshot = {}
version_entry = {
"id": str(uuid.uuid4()),
"label": label,
"timestamp": datetime.now(timezone.utc).isoformat(),
"snapshot": snapshot,
}
versions.append(version_entry)
if len(versions) > MAX_VERSIONS:
versions[:] = versions[-MAX_VERSIONS:]
return state
def restore_version(version_id: str, state: dict) -> dict:
"""
Restore a saved version by ID into state["plan"].
Returns updated state dict. If version_id not found, return state unchanged.
"""
versions: list = state.get("versions", [])
match = next((v for v in versions if v["id"] == version_id), None)
if match is None:
return state
state = copy.deepcopy(state)
# snapshot is already a dict β€” use model_validate, not parse_plan_json (which expects a string)
state["plan"] = ProjectPlan.model_validate(match["snapshot"]).model_dump()
return state
def list_versions(state: dict) -> list[dict]:
"""
Return list of version summaries: [{"id", "label", "timestamp"}]
Most recent first.
"""
versions: list = state.get("versions", [])
summaries = [
{"id": v["id"], "label": v["label"], "timestamp": v["timestamp"]}
for v in versions
]
return list(reversed(summaries))
def versions_html(state: dict) -> str:
"""
Render version history as an HTML list for display in Gradio.
Each entry shows label + timestamp. Empty state shows placeholder text.
Uses dark theme colors (#111111 bg, #e8e8e8 text, #2a2a2a border).
"""
summaries = list_versions(state)
container_style = (
"background:#111111;color:#e8e8e8;border:1px solid #2a2a2a;"
"border-radius:6px;padding:12px;font-family:sans-serif;"
)
if not summaries:
return (
f'<div style="{container_style}opacity:0.5;">'
"No saved versions yet.</div>"
)
items_html = "".join(
f'<li style="padding:6px 0;border-bottom:1px solid #2a2a2a;">'
f'<strong>{v["label"]}</strong>'
f'<span style="margin-left:12px;opacity:0.6;font-size:0.85em;">{v["timestamp"]}</span>'
f"</li>"
for v in summaries
)
return (
f'<div style="{container_style}">'
f'<ul style="list-style:none;margin:0;padding:0;">{items_html}</ul>'
f"</div>"
)