# Model · 数据结构全集 > 所有持久化数据结构。9 条核心定义,每条带 TypeBox schema(运行时校验)+ TS interface(类型推导)+ 落盘路径。Coder Agent 读这页就能写出所有数据访问代码。 ## M-1 · ProjectMeta · `data/companies/{cid}/projects/{pid}/project.json` ```ts // TypeBox schema const ProjectMeta = Type.Object({ id: Type.String({ pattern: '^p-[a-z0-9]{6}$' }), company_id: Type.String(), name: Type.String(), // 项目根 (见 arch-project-root) mode: Type.Union([ Type.Literal('sandbox'), // 默认,根在 data/.../projects/{pid}/ Type.Literal('linked-new'), // 用户指定空目录 Type.Literal('linked-existing'), // v2:复用已有 git repo ]), root_path: Type.String(), // 绝对路径,所有派生路径的源 status: Type.Union([ Type.Literal('prd_drafting'), Type.Literal('running'), Type.Literal('blocked'), Type.Literal('shipped'), Type.Literal('shipped-final'), Type.Literal('abandoned'), ]), created_at: Type.String({ format: 'date-time' }), updated_at: Type.String({ format: 'date-time' }), owner: Type.String(), token_budget: Type.Number({ default: 5_000_000 }), token_used: Type.Number({ default: 0 }), compressOldRuns: Type.Boolean({ default: false }), }); ``` - id 全局唯一,跨 company 不重复 - status 只能按状态机合法转换(见 sdd-process.html · M-process-project) - token_used > token_budget 时 LoopController 自动挂起项目 ## M-2 · units.json · `data/companies/{cid}/projects/{pid}/units.json` ```ts const Unit = Type.Object({ id: Type.String({ pattern: '^[a-z]+(-[a-z]+)*-(be|fe|mw|db)$' }), type: Type.Union([Type.Literal('be'), Type.Literal('fe'), Type.Literal('mw'), Type.Literal('db')]), status: Type.Union([ Type.Literal('pending'), Type.Literal('coding'), Type.Literal('testing'), Type.Literal('failed'), Type.Literal('passed'), Type.Literal('blocked'), ]), depends_on: Type.Array(Type.String()), retries: Type.Number({ default: 0 }), retry_budget: Type.Number({ default: 5 }), contract_hash: Type.String({ pattern: '^sha256:[a-f0-9]{16}$' }), last_run: Type.Optional(Type.String()), }); const UnitsJson = Type.Object({ project_id: Type.String(), prd_rev: Type.String(), sdd_rev: Type.String(), units: Type.Array(Unit), l2_rounds: Type.Number({ default: 0 }), l2_budget: Type.Number({ default: 3 }), }); ``` - DAG 必须无环(DocStore 写入前用拓扑排序校验) - depends_on 引用的 id 必须存在于本文件 units 中 - contract_hash = sha256(SDD 中对应 section 文本) 截前 16 位 - tests: 循环依赖 → 拒绝写入 - tests: 未知 id → 拒绝写入 - tests: 所有 status 为 passed 且 hash 与当前 SDD 匹配 → l2GateOpen 返回 true ## M-3 · prd/ 多页 PRD 站点 · `data/companies/{cid}/projects/{pid}/prd/ (目录)` ```ts // 目录布局 (PM Agent 产出,见 agents-pm.html §2.1) prd/ ├── index.html // Hero + reading guide entry ├── reading-guide.html ├── assets/ │ ├── shell.css // 复制自模板 │ ├── shell.js // 3 级菜单 + 搜索 │ └── search-index.json ├── product/ │ ├── background.html │ ├── personas.html │ ├── original-req.html │ ├── scope.html │ ├── success-metrics.html │ └── glossary.html ├── feature/ │ └── feature-tree.html ├── flow/ │ └── user-flows.html └── prototype/ └── proto-{viewName}.html // 每个 chapter HTML 内的结构 (cheerio 校验): ### ...

... ... // 可选,index.html only ... ``` - 顶级必须是 ` `,每页都有 - 每个 section 必含 ` ### ` + 至少一个 ` ` - section data-rev 单调递增(页内),**article data-rev** 取该页内最大 - 每页至少覆盖 4/8 类 HTML 信息(见 agents-pm §2.2) - data-conflict aside 仅出现在 index.html 顶部,存在时整个项目暂停等人 - 11 个必备章节缺一个 → validate_prd_site 失败 - tests: init 模式后 11 章节全在 - tests: incremental 模式后仅 affected 章节被改 - tests: 多 rev 后 history/ 含旧版归档 ## M-4 · sdd.html 结构 · `data/companies/{cid}/projects/{pid}/sdd.html` ```ts

Unit · auth-be

...功能描述... ``` POST /api/auth/login ... ``` - acceptance criteria 1 ``` - 每个 unit section 必含 contract + acceptance - data-depends 空格分隔 unit id 列表,可为空 - data-derived-from 指向产生它的 prd 版本 ## M-5 · contract.json(每 Unit) · `.../units/{unitId}/contract.json` ```ts const Contract = Type.Object({ unit_id: Type.String(), version: Type.String({ pattern: '^sha256:' }), exports: Type.Object({ http: Type.Optional(Type.Array(HttpEndpoint)), ts_types: Type.Optional(Type.String()), events: Type.Optional(Type.Array(EventSpec)), }), }); const HttpEndpoint = Type.Object({ method: Type.String(), path: Type.String(), request: Type.Optional(Type.Object({}, { additionalProperties: true })), responses:Type.Object({}, { additionalProperties: true }), }); ``` > _完整规格见 [api-contracts](api-contracts.html)。_ ## M-6 · integration/spec.json · `.../integration/spec.json` ```ts const IntegrationSpec = Type.Object({ version: Type.String(), cases: Type.Array(Type.Object({ id: Type.String(), steps: Type.Array(Type.Object({ unit: Type.String(), call: Type.String(), // "POST /api/x" body: Type.Optional(Type.Any()), headers: Type.Optional(Type.Record(Type.String(), Type.String())), expect: Type.Object({ status: Type.Number(), json: Type.Optional(Type.Any()), }), })), involves: Type.Array(Type.String()), })), }); ``` - involves 列表必须是 cases.steps.unit 的去重并集,BugTriager 用它做归因粗筛 - headers / body 中可用 `{{stepN.field}}` 占位符引用前面步骤结果 ## M-7 · runs/result.json (L1) · `.../units/{unitId}/runs/{runId}/result.json` ```ts const RunResult = Type.Object({ run_id: Type.String({ pattern: '^run-\\d{8}-\\d{4}$' }), loop: Type.Literal('L1'), unit_id: Type.String(), agent: Type.Union([Type.Literal('coder'), Type.Literal('tester')]), attempt: Type.Number(), ok: Type.Boolean(), summary: Type.Object({ total: Type.Number(), passed: Type.Number(), failed: Type.Number(), }), failures: Type.Array(Type.Object({ test: Type.String(), expected: Type.Any(), got: Type.Any(), stack: Type.String(), hint: Type.String(), })), next_action: Type.Optional(Type.String()), retry_budget_left: Type.Optional(Type.Number()), duration_ms: Type.Number(), }); ``` ## M-8 · integration/runs/result.json (L2) · `data/companies/{cid}/projects/{pid}/integration/runs/{runId}/result.json` ```ts const IntResult = Type.Object({ run_id: Type.String({ pattern: '^run-int-\\d{8}-\\d{4}$' }), ok: Type.Boolean(), failures: Type.Array(Type.Object({ case: Type.String(), failed_step: Type.Number(), got: Type.Any(), involves: Type.Array(Type.String()), stderr_excerpt: Type.String(), })), process_log: Type.String(), duration_ms: Type.Number(), }); ``` ## M-9 · triage.json · `.../integration/runs/{runId}/triage.json` ```ts const Triage = Type.Object({ run_id: Type.String(), root_cause: Type.String({ minLength: 20 }), category: Type.Union([ Type.Literal('contract_mismatch'), Type.Literal('impl_bug'), Type.Literal('spec_ambiguous'), Type.Literal('infra_flake'), ]), affected_units: Type.Array(Type.String()), proposed_sdd_patch: Type.Union([Type.Object({ target_section: Type.String(), op: Type.String(), text: Type.String(), }), Type.Null()]), confidence: Type.Number({ minimum: 0, maximum: 1 }), evidence: Type.Array(Type.String()), }); ``` - category = contract_mismatch → proposed_sdd_patch 必填 - category = impl_bug → proposed_sdd_patch 必为 null(不改 SDD,只 reset Unit) - confidence < 0.5 → LoopController 上报 L3 不自动应用 - 每条 evidence 必须在原文件中能 grep 到 ## M-10 · AgentRuntimeState · 持久化运行态 · `data/companies/{cid}/projects/{pid}/agent-state/{agent}.json` ```ts const AgentRuntimeState = Type.Object({ agent: Type.Union([Type.Literal('pm'), Type.Literal('architect'), Type.Literal('coder'), Type.Literal('tester'), Type.Literal('integrator'), Type.Literal('bug-triager')]), unit_id: Type.Optional(Type.String()), // Coder/Tester 时填 status: Type.Union([ Type.Literal('idle'), Type.Literal('running'), Type.Literal('paused'), Type.Literal('done'), Type.Literal('error') ]), call_id: Type.String(), // 单次调用唯一 id current_step: Type.Number(), // 0-based, 引用 todolist.items[i] total_steps: Type.Number(), last_input_ref: Type.String(), // 输入快照路径 last_output_ref: Type.Optional(Type.String()), session_id: Type.Optional(Type.String()), // ACP session 引用 resume_token: Type.Optional(Type.String()), // 冷启恢复用 started_at: Type.String({ format: 'date-time' }), updated_at: Type.String({ format: 'date-time' }), error: Type.Optional(Type.Object({ code: Type.String(), message: Type.String(), stack: Type.Optional(Type.String()) })), }); ``` - 每次 LoopController.callAgent 前写一次(status=running) - Agent 自己在跨越主要 checkpoint 时通过 `update_runtime_state` 工具更新 current_step - 调用结束(成功或失败)由 LoopController 写终态 status=done|error - Server SIGTERM 时所有 status=running 自动改 status=paused 并写 resume_token - Server 启动时扫所有 paused,发 SSE `resume_decision_needed` 等人决策 - tests: Agent 崩溃 → state.json status=error,error 字段有 code/message - tests: Server kill -9 → 重启后 state.json status=paused,resume_token 非空 - tests: Dashboard 能从 state.json 渲染「Architect at step 3/5」 ## M-11 · AgentMemory · 跨项目长期记忆 · `data/companies/{cid}/memory/devloop/{agent}/{topic}.md` ```ts // Markdown + frontmatter,人可读 + 机器可读 --- agent: architect topic: project-conventions project_refs: [p-731042, p-845001] last_updated: 2026-05-13T10:31:00Z relevance_score: 0.92 # 0-1, agent 自评 hits: 14 # 被注入 prompt 的次数 --- - 用户偏好 RESTful API + 完整 OpenAPI 注解 - Token 续期实测:5s 内人类可接受 - 认证方式:JWT 放 Authorization header,不用 cookie - 数据库统一用 better-sqlite3,不要 mysql/postgres ``` - 每个 Agent 启动时扫 `memory/devloop/{agent}/*.md` - 按 relevance_score × recency 排序,取前 5 条注入 system prompt - Agent 自己可通过 `record_memory(topic, content, relevance)` 工具新增 - **Dedup:同 agent + 同 topic 是 upsert(覆盖 + 累加 hits),不新建文件**。文件名 = `{topic-slug}.md`,topic-slug 由 record_memory 工具按规则计算(小写 · 空格转 -) - memory 跨项目(同 company 内)共享,**不**跨 company - frontmatter 必须含 agent / topic / last_updated 三字段 - 单文件 ≤ 8KB,超出 record_memory 自动 truncate 旧内容(保留新加 + 最相关) - **v1 注意:**如果初版 token 紧张可以关 memory 注入(top-5 改 top-0),完全跳过。memory 在 ≥ 5 个项目后才开始有积累价值。 - tests: 新 Agent 调用 → memory dir 存在则 system prompt 含 "## Memory" section - tests: 不存在该 agent dir → fallback 空 memory,不报错 - tests: record_memory 写入后下次启动能读到 ## M-12 · AgentTodolist · 单次调用的子任务清单 · `data/companies/{cid}/projects/{pid}/agent-state/{agent}-todo.json` ```ts const AgentTodolist = Type.Object({ agent: Type.String(), call_id: Type.String(), // 与 AgentRuntimeState.call_id 关联 created_at: Type.String(), items: Type.Array(Type.Object({ id: Type.String({ pattern: '^step-\\d{3}$' }), description: Type.String(), status: Type.Union([Type.Literal('todo'), Type.Literal('doing'), Type.Literal('done'), Type.Literal('failed')]), started_at: Type.Optional(Type.String()), finished_at: Type.Optional(Type.String()), output_ref: Type.Optional(Type.String()), // 产物路径 depends_on: Type.Optional(Type.Array(Type.String())), error: Type.Optional(Type.String()), })), current: Type.String(), // 当前 item.id progress_pct: Type.Number({ minimum:0, maximum:100 }), }); // 典型 Architect generate 模式的 todolist 示例: items: [ { id:'step-001', description:'读 prd.html 并解析所有 section', status:'done' }, { id:'step-002', description:'识别功能列表(共 5 个特性)', status:'done' }, { id:'step-003', description:'拆分每个特性为 be/fe Unit', status:'doing' }, { id:'step-004', description:'为每个 Unit 写 OpenAPI contract', status:'todo' }, { id:'step-005', description:'生成 integration/spec.json', status:'todo' }, ] ``` - Agent 在 system prompt 指引下,**开始时**必须先写完整 todolist(todo 状态) - 每完成一项立即调 `update_todo_item(id, status, output_ref)` 工具 - progress_pct = done 数 / total × 100 - Dashboard 从这里渲染「Architect: 3/5 拆分 OAuth 功能为 3 个 Unit」 - 失败 → status=failed,触发 retry 时 todolist 清空重写 ## M-13 · ProcessTimeline · 全流程统一日志 · `data/companies/{cid}/projects/{pid}/timeline.ndjson` ```ts // NDJSON,append-only,一行一事件,每行 ≤ 1KB const TimelineEvent = Type.Object({ ts: Type.Number(), // unix seconds source: Type.Union([ Type.Literal('system'), // LoopController / DocStore / etc Type.Literal('human'), // 人类显式动作 Type.Literal('pm-agent'), Type.Literal('architect'), Type.Literal('coder'), Type.Literal('tester'), Type.Literal('integrator'), Type.Literal('bug-triager'), ]), kind: Type.String(), // "project_created" / "prd_drafted" / etc ref: Type.Optional(Type.String()), // 关联资源,如 "prd@rev3" summary: Type.String({ maxLength: 280 }), // 人话摘要 payload: Type.Optional(Type.Object({}, { additionalProperties:true })), }); // 示例 (一行 = 一事件): {"ts":1747120731,"source":"human","kind":"project_created","summary":"yibo 创建项目'做个 TODO API'"} {"ts":1747120745,"source":"pm-agent","kind":"prd_drafted","ref":"prd@rev1","summary":"PRD rev 1 完成"} {"ts":1747120798,"source":"architect","kind":"sdd_generated","ref":"sdd@rev1","summary":"拆 3 个 Unit"} {"ts":1747120821,"source":"coder","kind":"unit_coded","ref":"unit/todo-be","summary":"todo-be 实现完成"} {"ts":1747120833,"source":"tester","kind":"unit_failed","ref":"unit/todo-be","summary":"invalid-pwd 期望 401 收 500"} ``` - **Append-only**,永不修改已有行 - 所有 state 转移、文档变更、Agent 调用、Human 动作都必须 append 一条 - 是 audit log 也是 Dashboard 时间线视图数据源 - 每条 ≤ 1KB;超长内容(如完整 stderr)放 payload.{x}_ref 引用 - 由 `timelineLogger.append(pid, event)` 写入(C-15 工具) > _与 stream.log(SSE 视角)的区别:timeline.ndjson 是**事件视角**,stream.log 是**客户端视角**。前者永远存,后者 7 天滚动。_ ## M-14 · bugs.ndjson · 跨 run 跨 unit Bug 聚合 · `data/companies/{cid}/projects/{pid}/bugs.ndjson` ```ts // NDJSON,append + 状态机更新 const Bug = Type.Object({ id: Type.String({ pattern: '^bug-\\d{8}-\\d{4}-[a-z0-9]{4}$' }), discovered_at:Type.String({ format: 'date-time' }), source: Type.Union([Type.Literal('unit_test'), Type.Literal('integration_test'), Type.Literal('triage'), Type.Literal('human')]), unit_id: Type.Optional(Type.String()), run_id: Type.Optional(Type.String()), // runs/{runId} 或 integration/runs/{runId} test_name: Type.Optional(Type.String()), category: Type.Union([Type.Literal('contract_mismatch'), Type.Literal('impl_bug'), Type.Literal('spec_ambiguous'), Type.Literal('infra_flake'), Type.Literal('unknown')]), severity: Type.Union([Type.Literal('critical'), Type.Literal('high'), Type.Literal('medium'), Type.Literal('low')]), title: Type.String({ maxLength: 120 }), detail: Type.String(), // 完整 stack/expected/got affected_units: Type.Array(Type.String()), status: Type.Union([Type.Literal('open'), Type.Literal('fixing'), Type.Literal('fixed'), Type.Literal('verified'), Type.Literal('wontfix'), Type.Literal('duplicate')]), status_history: Type.Array(Type.Object({ ts: Type.String(), from: Type.String(), to: Type.String(), by: Type.String(), // agent/system/human id note: Type.Optional(Type.String()), })), fix_refs: Type.Array(Type.String()), // 关联的 run_id / triage / commit duplicate_of: Type.Optional(Type.String()), // 另一个 bug id }); // 示例行: {"id":"bug-20260513-0731-7f3a","source":"unit_test","unit_id":"auth-be","run_id":"run-..0731", "category":"impl_bug","severity":"high","status":"fixed", "title":"login.invalid-pwd 期望 401 收 500","affected_units":["auth-be"],"fix_refs":["run-..0734"]} ``` - 每次 Tester / Integrator / BugTriager 失败 → 自动 append 1 条 bug,status=open - 同一 unit 同一 test 连续两轮失败 → 视同同一 bug(合并),不重复 append - status 转换走 P-9 状态机,每次转换 append 一条 status_history - append-only:bug 内容修改通过**新写一行 status=...** 的更新事件实现(与 timeline 同理) - 读取时取每个 bug.id 的**最新**一行作为当前状态 - tests: 单测红 → bugs.ndjson 新增 status=open 行 - tests: 下一轮 Coder 修复 → 新增 status=fixed 行 with fix_refs=[新 run_id] - tests: L2 通过 → 该 bug 状态最终 verified ## M-15 · changelog.ndjson · 版本变更流 · `data/companies/{cid}/projects/{pid}/changelog.ndjson` ```ts const ChangelogEntry = Type.Object({ ts: Type.String({ format: 'date-time' }), kind: Type.Union([ Type.Literal('prd_rev'), Type.Literal('sdd_rev'), Type.Literal('units_rev'), Type.Literal('integration_spec_rev'), ]), rev: Type.Number(), by: Type.String(), // agent or human id trigger: Type.String(), // "human_feedback" | "triage_patch" | "init" summary: Type.String({ maxLength: 280 }), diff: Type.Object({ // 影响面快照 sections_added: Type.Array(Type.String()), sections_changed: Type.Array(Type.String()), sections_removed: Type.Array(Type.String()), units_added: Type.Array(Type.String()), units_changed: Type.Array(Type.String()), units_removed: Type.Array(Type.String()), }), archive_ref: Type.String(), // history/prd-rev-{N}.html 路径 }); // 示例: {"kind":"prd_rev","rev":3,"by":"pm-agent","trigger":"human_feedback", "summary":"+ OAuth 登录,影响 auth-mw 新增","diff":{"sections_added":["#oauth"],...}} {"kind":"sdd_rev","rev":5,"by":"architect","trigger":"prd@rev3", "summary":"3 个新 Unit: oauth-be / oauth-fe / auth-mw",...} {"kind":"sdd_rev","rev":6,"by":"architect","trigger":"triage_patch", "summary":"L2 修复:auth-be 401 明确",...} ``` - 每次 DocStore.writePRD/writeSDD/writeUnits 成功后必 append 一行 - diff 字段在 PM/Architect Agent 输出后由 DocStore 自动计算(cheerio + 拓扑 diff) - archive_ref 指向归档文件,可被 Dashboard 直接打开 - 这是 history/ 目录的**线性视图**:history/ 是物,changelog 是事 ## 归档命名约定(统一目录形式) | 原产物 | 归档路径 | 形态 | | --- | --- | --- | | prd/ 整站 | `history/prd-rev-{N}/` | 目录(含整 prd/ 拷贝) | | sdd/ 整站 | `history/sdd-rev-{N}/` | 目录 | | units.json | `history/units-rev-{N}.json` | 单文件 | | integration/spec.json | `history/integration-spec-rev-{N}.json` | 单文件 | | removed unit dir | `history/removed/{unitId}-{ts}/` | 目录 + 时间戳防重 | | agent-state snapshots | `agent-state/snapshots/{call_id}-(in|out).json` | 不归到 history/(用于 retry 回放) | > _PRD/SDD 现在都是多页站点,归档单元是**整个子目录**而非单 html。所有 Agent 用 archive_doc 工具,传 kind 即可('prd'/'sdd'/'units'),路径由 DocStore 计算。_ ## 无 SQL · 无新表 整个 DevLoop 系统不引入新 SQLite 表。所有持久化通过文件系统完成。LemonBoss 既有的 `data/users.sqlite` 等不动。 > _这是有意为之的简化 — 让外部 Claude Code 复现项目时不用先想 schema migration。_