diff --git a/docs/development-plan.md b/docs/development-plan.md index b1a0a2f..94cdcbf 100644 --- a/docs/development-plan.md +++ b/docs/development-plan.md @@ -460,3 +460,16 @@ U-041(回滚路径实现,含 §10 提到的 `git revert` 冲突处理)是 ## 4. 与 06-验收与测试锚点.md 的对应关系 本文档不重新定义验收标准,Unit 的 Done 标准直接引用 06 节已有的 REQ-01~14 用例(happy/boundary/failure)。如果后续 06 节的验收用例发生变更,本文档对应 Unit 的 Done 标准需要同步更新,避免两份文档各说各话。 + +--- + +## 5. PH0-PH5 之外:用户直接请求的控制台功能扩展 + +PH0-PH5(45 个 Unit)全部完成、U-042 真实自举压测进行中期间,用户直接向 Claude Code(不经过 graphlemon 自己的 Planner/Coder/Validator 闭环,原因见下)提出了两个新的控制台页面需求,是对 02-V 原有非目标"不做完整多项目管理 UI(项目列表/切换)"的明确、有意突破,不是发现遗漏后的补充。 + +**为什么不走 graphlemon 自己的自举闭环去实现这个需求**:讨论过程中确认了当前 `implement_code()`(Coder 阶段)永远用一个写死的单节点 `graph_manifest`,从不调用 `plan_work_graph()` 规划自己的 Work Graph——不管 SDD 多详细,整个实现都只交给一次 Coder 会话,没有按视图/模块拆分执行的能力(U-031 当时明确把"造一个 Coder 图规划器"排除在集成范围之外)。这个需求涉及两个新页面、6+ 个新后端接口,用一次不可拆分的 Coder 会话去做风险高、不可控,所以直接由 Claude Code 实现,不走自举闭环。 + +- **`v_project_list`(新增)**:项目列表 + 增删改查。"新建/导入项目"复用已有的 `v_project_setup`/`c_project_create` 流程——`project_scaffold.create_project()` 原本就只在目标目录下新增 `.graphlemon/`,不碰任何已有代码,"导入一个已有代码的目录"这条诉求不需要新代码。真正新增的是读/改半边:`src/project_query.py`(`list_projects`/`get_project`/`update_project`),新增 HTTP `GET /api/projects`、`GET /api/projects/:id`、`PUT /api/projects/:id`、`DELETE /api/projects/:id`(`DELETE` 语义是把 `status` 置为 `archived`,不是物理删除行——`project_id` 被 `task`/`artifact`/`bug_record` 等大量外键引用,物理删除要么违反约束要么需要级联删掉这些本该保留的审计历史,01-M 自己的 `active`/`archived` 枚举本来就是软删除的设计)。 +- **`v_goal_list`(对已有 `v_task_board`/`v_task_detail` 的扩展,非全新页面)**:搜索/筛选已有,新增排序(`task_query.list_tasks()` 加 `sort`/`order` 参数,白名单校验列名防注入);`v_task_detail` 新增"编辑目标"/"删除任务",仅在 `status='queued'` 时出现——一旦 Planner/Coder 已经基于这个 goal_text 产出过真实 artifact/work_graph_run,改写或删除就会让审计记录指向一个不存在的目标,这正是这个代码库一直在避免的不可追溯漂移,所以 `task_query.update_task_goal_text()`/`delete_task()` 都在非 `queued` 状态下拒绝(`TaskNotEditableError`,HTTP 409)。 +- **真实验证时发现并修复一个真 bug**:`delete_task()` 第一版对一个真实创建的(走 `task_create.create_task()`,不是测试自己手写的 INSERT)`queued` 任务执行 `DELETE FROM task` 时,真实抛出 `sqlite3.IntegrityError`——`task_create.create_task()` 自己在同一个事务里总会写一条 `execution_event` 行(01-M 自己的外键,没有 `ON DELETE CASCADE`),哪怕这个任务从没被 Planner/Coder/Validator 碰过,也已经至少有一条自己创建时留下的事件。补上了同事务内先删这条任务自己的 `execution_event`(`queued` 态下不可能有真正的 artifact/bug_record/work_graph_run,所以这是唯一可能挡路的外键)。这次 bug 是测试自己的 `_insert_task` 辅助函数(手写 INSERT,从不写 `execution_event`)从没暴露过的——单测通过不代表真的能用,加了一个走真实 `task_create.create_task()` 路径的回归测试锁死。 +- **验证方式**:`src/test_project_query.py`(12 个用例)、`src/test_task_query.py` 新增的排序/编辑/删除用例(含上面这条真实路径回归)、`src/test_backend_server.py` 新增两个真实 HTTP 路由测试类(`ProjectListAndCrudRouteTest`/`TaskCrudRouteTest`,真实起服务器+真实请求,不 mock)。浏览器扩展本次仍不可用,额外对着真实运行的服务器用 `curl` 走了一遍全部新增接口(含前面提到的真删除 bug 就是这一步抓到的)。474 个测试全绿。 diff --git a/frontend/app.js b/frontend/app.js index 31e396b..4b1cc1f 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -76,6 +76,7 @@ function connectLogTailSocket(nodeExecId, { onChunk, onClose }) { const routes = { "#/": { render: renderDashboard, mount: mountDashboard }, + "#/projects": { render: renderProjectList, mount: mountProjectList }, "#/tasks": { render: renderTaskBoard, mount: mountTaskBoard }, "#/artifacts": { render: renderArtifactBrowser, mount: mountArtifactBrowser }, "#/skills": { render: renderSkillLibrary, mount: mountSkillLibrary }, @@ -132,6 +133,109 @@ function render() { window.addEventListener("hashchange", render); window.addEventListener("DOMContentLoaded", render); +// ---- v_project_list (2026-08-03): project list + CRUD ---- +// 02-V originally declared "不做完整多项目管理 UI(项目列表/切换)" as an +// MVP non-goal -- this page is a deliberate, explicit scope expansion +// beyond that, requested directly by the user. Creation still goes through +// v_project_setup's existing flow (pick a folder, optionally git-init it, +// project_scaffold.create_project() already handles a folder that already +// has real code in it correctly -- it only ever adds .graphlemon/ inside, +// nothing about "importing" needs to be built separately). This page adds +// the missing read/update/archive half: list every project, rename, +// archive/reactivate, and select one as "current" to work in. + +function renderProjectList() { + return ` +
+ `; +} + +function mountProjectList() { + const rowsEl = document.getElementById("pl_rows"); + + async function refresh() { + const resp = await fetch("/api/projects"); + const projects = await resp.json(); + if (projects.length === 0) { + rowsEl.innerHTML = '