This commit is contained in:
2026-08-31 14:16:08 +08:00
parent b1b226593a
commit 7533b298f1
30 changed files with 6976 additions and 253 deletions
+66 -2
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import json
import re
import secrets
from copy import deepcopy
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
@@ -192,6 +193,10 @@ class WorkspaceStore:
"lifecycle": "completed",
"run_id": "",
"requirements_path": "",
"completion_checklist_path": "",
"modeling_plan_path": "",
"modeling_plan_review_path": "",
"modeling_plan_version": 0,
"agent_state_path": "",
"active_candidate_id": "",
"active_branch_id": "main",
@@ -222,6 +227,9 @@ class WorkspaceStore:
"requirements_path": "",
"source_requirements_path": "",
"completion_checklist_path": "",
"modeling_plan_path": "",
"modeling_plan_review_path": "",
"modeling_plan_version": 0,
"agent_state_path": "",
"active_candidate_id": "",
"active_branch_id": "main",
@@ -271,6 +279,15 @@ class WorkspaceStore:
write_json(self.task_path(task_id), task)
return task
def update_task_fields(self, task_id: str, fields: dict[str, Any]) -> dict[str, Any]:
"""Update task metadata without appending a synthetic revision."""
task = self.ensure_task(task_id, "")
for key, value in fields.items():
task[str(key)] = deepcopy(value)
task["updated_at"] = now_iso()
write_json(self.task_path(task_id), task)
return task
def read_task(self, task_id: str) -> dict[str, Any] | None:
task = read_json(self.task_path(task_id))
return self._migrate_task(task, self.task_path(task_id)) if isinstance(task, dict) else None
@@ -293,8 +310,8 @@ class WorkspaceStore:
return task
def finish_generation(self, task_id: str, *, lifecycle: str, failure: dict[str, Any] | None = None) -> dict[str, Any]:
if lifecycle not in {"completed", "failed"}:
raise ValueError("Generation lifecycle must be completed or failed")
if lifecycle not in {"completed", "failed", "cancelled", "waiting_review", "failed_review_service"}:
raise ValueError("Generation lifecycle must be completed, failed, cancelled, waiting_review, or failed_review_service")
task = self.ensure_task(task_id, "")
failure_path = ""
if failure:
@@ -421,6 +438,53 @@ class WorkspaceStore:
path = self.artifact_path(task_id, relative)
return path.read_text(encoding="utf-8") if path.is_file() else ""
def write_modeling_plan(self, task_id: str, markdown: str, *, version: int = 1) -> Path:
"""Persist one immutable, versioned modeling-plan document."""
text = str(markdown or "").strip()
if not text:
raise ValueError("modeling plan must not be empty")
task = self.ensure_task(task_id, "")
version = max(1, int(version))
relative = Path("plans") / f"modeling-plan-v{version}.md"
path = self.task_dir(task_id) / relative
if path.exists():
raise ValueError("modeling plan version already exists")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text + "\n", encoding="utf-8")
task["modeling_plan_path"] = relative.as_posix()
task["modeling_plan_version"] = version
task["updated_at"] = now_iso()
write_json(self.task_path(task_id), task)
return path
def read_modeling_plan(self, task_id: str) -> str:
task = self.read_task(task_id) or {}
relative = str(task.get("modeling_plan_path") or "")
if not relative:
return ""
path = self.artifact_path(task_id, relative)
return path.read_text(encoding="utf-8") if path.is_file() else ""
def write_modeling_plan_review(self, task_id: str, review: dict[str, Any], *, version: int) -> Path:
task = self.ensure_task(task_id, "")
version = max(1, int(version))
relative = Path("plans") / f"modeling-plan-v{version}.review.json"
path = self.task_dir(task_id) / relative
if path.exists():
raise ValueError("modeling plan review already exists")
path.parent.mkdir(parents=True, exist_ok=True)
write_json(path, review)
task["modeling_plan_review_path"] = relative.as_posix()
task["updated_at"] = now_iso()
write_json(self.task_path(task_id), task)
return path
def read_modeling_plan_review(self, task_id: str) -> dict[str, Any] | None:
task = self.read_task(task_id) or {}
relative = str(task.get("modeling_plan_review_path") or "")
value = read_json(self.artifact_path(task_id, relative), {}) if relative else None
return value if isinstance(value, dict) else None
def new_candidate(self, task_id: str) -> tuple[str, Path]:
task = self.ensure_task(task_id, "")
candidate_id = f"candidate_{secrets.token_hex(8)}"