sdd-model.html 46 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>SDD · Model · LemonBoss PRD</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,500;9..144,700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="assets/shell.css" />
</head>
<body data-page="sdd-model.html">
<div class="shell">
  <aside id="sidebar"></aside>
  <main class="page">
    <header class="page-head">
      <div class="crumb"><span class="pill">03 · SDD</span><span class="sep">/</span><span>M · Model</span></div>
      <h1>Model · 数据结构全集</h1>
      <p class="deck">所有持久化数据结构。9 条核心定义,每条带 TypeBox schema(运行时校验)+ TS interface(类型推导)+ 落盘路径。Coder Agent 读这页就能写出所有数据访问代码。</p>
    </header>
    <article class="body">

      <section data-def-name="ProjectMeta" data-mvcp="model">
        <h2>M-1 · ProjectMeta</h2>
        <code class="path mono" style="color:var(--ink-soft)">data/companies/{cid}/projects/{pid}/project.json</code>
<pre class="code shape"><span class="c">// TypeBox schema</span>
<span class="k">const</span> ProjectMeta = Type.<span class="n">Object</span>(<span class="p">{</span>
  id:          Type.<span class="n">String</span>(<span class="p">{</span> pattern: <span class="s">'^p-[a-z0-9]{6}$'</span> <span class="p">}</span>),
  company_id:  Type.<span class="n">String</span>(),
  name:        Type.<span class="n">String</span>(),
  status:      Type.<span class="n">Union</span>(<span class="p">[</span>
    Type.<span class="n">Literal</span>(<span class="s">'prd_drafting'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'running'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'blocked'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'shipped'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'shipped-final'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'abandoned'</span>),
  <span class="p">]</span>),
  created_at:  Type.<span class="n">String</span>(<span class="p">{</span> format: <span class="s">'date-time'</span> <span class="p">}</span>),
  updated_at:  Type.<span class="n">String</span>(<span class="p">{</span> format: <span class="s">'date-time'</span> <span class="p">}</span>),
  owner:       Type.<span class="n">String</span>(),
  token_budget:        Type.<span class="n">Number</span>(<span class="p">{</span> default: <span class="v">5_000_000</span> <span class="p">}</span>),
  token_used:          Type.<span class="n">Number</span>(<span class="p">{</span> default: <span class="v">0</span> <span class="p">}</span>),
  compressOldRuns:     Type.<span class="n">Boolean</span>(<span class="p">{</span> default: <span class="v">false</span> <span class="p">}</span>),
<span class="p">}</span>);</pre>
        <ul class="contract">
          <li>id 全局唯一,跨 company 不重复</li>
          <li>status 只能按状态机合法转换(见 sdd-process.html · M-process-project)</li>
          <li>token_used &gt; token_budget 时 LoopController 自动挂起项目</li>
        </ul>
      </section>

      <section data-def-name="UnitsJson" data-mvcp="model">
        <h2>M-2 · units.json</h2>
        <code class="path mono" style="color:var(--ink-soft)">data/companies/{cid}/projects/{pid}/units.json</code>
<pre class="code shape"><span class="k">const</span> Unit = Type.<span class="n">Object</span>(<span class="p">{</span>
  id:            Type.<span class="n">String</span>(<span class="p">{</span> pattern: <span class="s">'^[a-z]+(-[a-z]+)*-(be|fe|mw|db)$'</span> <span class="p">}</span>),
  type:          Type.<span class="n">Union</span>(<span class="p">[</span>Type.<span class="n">Literal</span>(<span class="s">'be'</span>), Type.<span class="n">Literal</span>(<span class="s">'fe'</span>), Type.<span class="n">Literal</span>(<span class="s">'mw'</span>), Type.<span class="n">Literal</span>(<span class="s">'db'</span>)<span class="p">]</span>),
  status:        Type.<span class="n">Union</span>(<span class="p">[</span>
    Type.<span class="n">Literal</span>(<span class="s">'pending'</span>), Type.<span class="n">Literal</span>(<span class="s">'coding'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'testing'</span>), Type.<span class="n">Literal</span>(<span class="s">'failed'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'passed'</span>), Type.<span class="n">Literal</span>(<span class="s">'blocked'</span>),
  <span class="p">]</span>),
  depends_on:    Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
  retries:       Type.<span class="n">Number</span>(<span class="p">{</span> default: <span class="v">0</span> <span class="p">}</span>),
  retry_budget:  Type.<span class="n">Number</span>(<span class="p">{</span> default: <span class="v">5</span> <span class="p">}</span>),
  contract_hash: Type.<span class="n">String</span>(<span class="p">{</span> pattern: <span class="s">'^sha256:[a-f0-9]{16}$'</span> <span class="p">}</span>),
  last_run:      Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),
<span class="p">}</span>);
<span class="k">const</span> UnitsJson = Type.<span class="n">Object</span>(<span class="p">{</span>
  project_id:    Type.<span class="n">String</span>(),
  prd_rev:       Type.<span class="n">String</span>(),
  sdd_rev:       Type.<span class="n">String</span>(),
  units:         Type.<span class="n">Array</span>(Unit),
  l2_rounds:     Type.<span class="n">Number</span>(<span class="p">{</span> default: <span class="v">0</span> <span class="p">}</span>),
  l2_budget:     Type.<span class="n">Number</span>(<span class="p">{</span> default: <span class="v">3</span> <span class="p">}</span>),
<span class="p">}</span>);</pre>
        <ul class="contract">
          <li>DAG 必须无环(DocStore 写入前用拓扑排序校验)</li>
          <li>depends_on 引用的 id 必须存在于本文件 units 中</li>
          <li>contract_hash = sha256(SDD 中对应 section &lt;pre data-contract&gt; 文本) 截前 16 位</li>
        </ul>
        <ul class="tests">
          <li>循环依赖 → 拒绝写入</li>
          <li>未知 id → 拒绝写入</li>
          <li>所有 status 为 passed 且 hash 与当前 SDD 匹配 → l2GateOpen 返回 true</li>
        </ul>
      </section>

      <section data-def-name="PRD-Site" data-mvcp="model">
        <h2>M-3 · prd/ 多页 PRD 站点</h2>
        <code class="path mono" style="color:var(--ink-soft)">data/companies/{cid}/projects/{pid}/prd/ (目录)</code>
<pre class="code shape"><span class="c">// 目录布局 (PM Agent 产出,见 agents-pm.html §2.1)</span>
prd/
├── index.html              <span class="c">// Hero + reading guide entry</span>
├── reading-guide.html
├── assets/
│   ├── shell.css           <span class="c">// 复制自模板</span>
│   ├── shell.js            <span class="c">// 3 级菜单 + 搜索</span>
│   └── 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

<span class="c">// 每个 chapter HTML 内的结构 (cheerio 校验):</span>
<span class="k">&lt;article</span> <span class="n">data-doc</span>=<span class="s">"prd"</span> <span class="n">data-rev</span>=<span class="s">"N"</span><span class="k">&gt;</span>
  <span class="k">&lt;section</span> <span class="n">data-rev</span>=<span class="s">"M"</span> <span class="n">data-origin</span>=<span class="s">"{user@iso}"</span><span class="k">&gt;</span>
    <span class="k">&lt;h3&gt;</span>...<span class="k">&lt;/h3&gt;</span>
    <span class="k">&lt;p&gt;</span>...<span class="k">&lt;/p&gt;</span>
  <span class="k">&lt;/section&gt;</span>
  <span class="k">&lt;aside</span> <span class="n">data-conflict</span><span class="k">&gt;</span>...<span class="k">&lt;/aside&gt;</span>  <span class="c">// 可选,index.html only</span>
  <span class="k">&lt;footer</span> <span class="n">data-changelog</span><span class="k">&gt;</span>...<span class="k">&lt;/footer&gt;</span>
<span class="k">&lt;/article&gt;</span></pre>
        <ul class="contract">
          <li>顶级必须是 <code>&lt;article data-doc="prd"&gt;</code>,每页都有</li>
          <li>每个 section 必含 <code>&lt;h3&gt;</code> + 至少一个 <code>&lt;p&gt;</code></li>
          <li>section data-rev 单调递增(页内),<b>article data-rev</b> 取该页内最大</li>
          <li>每页至少覆盖 4/8 类 HTML 信息(见 agents-pm §2.2)</li>
          <li>data-conflict aside 仅出现在 index.html 顶部,存在时整个项目暂停等人</li>
          <li>11 个必备章节缺一个 → validate_prd_site 失败</li>
        </ul>
        <ul class="tests">
          <li>init 模式后 11 章节全在</li>
          <li>incremental 模式后仅 affected 章节被改</li>
          <li>多 rev 后 history/ 含旧版归档</li>
        </ul>
      </section>

      <section data-def-name="SDD-HTML" data-mvcp="model">
        <h2>M-4 · sdd.html 结构</h2>
        <code class="path mono" style="color:var(--ink-soft)">data/companies/{cid}/projects/{pid}/sdd.html</code>
<pre class="code shape"><span class="k">&lt;article</span> <span class="n">data-doc</span>=<span class="s">"sdd"</span> <span class="n">data-derived-from</span>=<span class="s">"prd@revN"</span><span class="k">&gt;</span>
  <span class="k">&lt;section</span>
    <span class="n">data-unit-id</span>=<span class="s">"auth-be"</span>
    <span class="n">data-type</span>=<span class="s">"be"</span>
    <span class="n">data-depends</span>=<span class="s">""</span><span class="k">&gt;</span>
    <span class="k">&lt;h3&gt;</span><span class="v">Unit · auth-be</span><span class="k">&lt;/h3&gt;</span>
    <span class="k">&lt;p&gt;</span><span class="v">...功能描述...</span><span class="k">&lt;/p&gt;</span>
    <span class="k">&lt;pre</span> <span class="n">data-contract</span>=<span class="s">"openapi"</span><span class="k">&gt;</span>
      <span class="v">POST /api/auth/login ...</span>
    <span class="k">&lt;/pre&gt;</span>
    <span class="k">&lt;ul</span> <span class="n">data-acceptance</span><span class="k">&gt;</span>
      <span class="k">&lt;li&gt;</span><span class="v">acceptance criteria 1</span><span class="k">&lt;/li&gt;</span>
    <span class="k">&lt;/ul&gt;</span>
  <span class="k">&lt;/section&gt;</span>
<span class="k">&lt;/article&gt;</span></pre>
        <ul class="contract">
          <li>每个 unit section 必含 contract + acceptance</li>
          <li>data-depends 空格分隔 unit id 列表,可为空</li>
          <li>data-derived-from 指向产生它的 prd 版本</li>
        </ul>
      </section>

      <section data-def-name="ContractJson" data-mvcp="model">
        <h2>M-5 · contract.json(每 Unit)</h2>
        <code class="path mono" style="color:var(--ink-soft)">.../units/{unitId}/contract.json</code>
<pre class="code shape"><span class="k">const</span> Contract = Type.<span class="n">Object</span>(<span class="p">{</span>
  unit_id:  Type.<span class="n">String</span>(),
  version:  Type.<span class="n">String</span>(<span class="p">{</span> pattern: <span class="s">'^sha256:'</span> <span class="p">}</span>),
  exports:  Type.<span class="n">Object</span>(<span class="p">{</span>
    http:     Type.<span class="n">Optional</span>(Type.<span class="n">Array</span>(HttpEndpoint)),
    ts_types: Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),
    events:   Type.<span class="n">Optional</span>(Type.<span class="n">Array</span>(EventSpec)),
  <span class="p">}</span>),
<span class="p">}</span>);

<span class="k">const</span> HttpEndpoint = Type.<span class="n">Object</span>(<span class="p">{</span>
  method:   Type.<span class="n">String</span>(),
  path:     Type.<span class="n">String</span>(),
  request:  Type.<span class="n">Optional</span>(Type.<span class="n">Object</span>(<span class="p">{}</span>, <span class="p">{</span> additionalProperties: <span class="v">true</span> <span class="p">}</span>)),
  responses:Type.<span class="n">Object</span>(<span class="p">{}</span>, <span class="p">{</span> additionalProperties: <span class="v">true</span> <span class="p">}</span>),
<span class="p">}</span>);</pre>
        <p class="sig">完整规格见 <a href="api-contracts.html">api-contracts</a>。</p>
      </section>

      <section data-def-name="IntegrationSpec" data-mvcp="model">
        <h2>M-6 · integration/spec.json</h2>
        <code class="path mono" style="color:var(--ink-soft)">.../integration/spec.json</code>
<pre class="code shape"><span class="k">const</span> IntegrationSpec = Type.<span class="n">Object</span>(<span class="p">{</span>
  version: Type.<span class="n">String</span>(),
  cases:   Type.<span class="n">Array</span>(Type.<span class="n">Object</span>(<span class="p">{</span>
    id:       Type.<span class="n">String</span>(),
    steps:    Type.<span class="n">Array</span>(Type.<span class="n">Object</span>(<span class="p">{</span>
      unit:    Type.<span class="n">String</span>(),
      call:    Type.<span class="n">String</span>(),      <span class="c">// "POST /api/x"</span>
      body:    Type.<span class="n">Optional</span>(Type.<span class="n">Any</span>()),
      headers: Type.<span class="n">Optional</span>(Type.<span class="n">Record</span>(Type.<span class="n">String</span>(), Type.<span class="n">String</span>())),
      expect:  Type.<span class="n">Object</span>(<span class="p">{</span>
        status: Type.<span class="n">Number</span>(),
        json:   Type.<span class="n">Optional</span>(Type.<span class="n">Any</span>()),
      <span class="p">}</span>),
    <span class="p">}</span>)),
    involves: Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
  <span class="p">}</span>)),
<span class="p">}</span>);</pre>
        <ul class="contract">
          <li>involves 列表必须是 cases.steps.unit 的去重并集,BugTriager 用它做归因粗筛</li>
          <li>headers / body 中可用 <code>{{stepN.field}}</code> 占位符引用前面步骤结果</li>
        </ul>
      </section>

      <section data-def-name="RunResult" data-mvcp="model">
        <h2>M-7 · runs/result.json (L1)</h2>
        <code class="path mono" style="color:var(--ink-soft)">.../units/{unitId}/runs/{runId}/result.json</code>
<pre class="code shape"><span class="k">const</span> RunResult = Type.<span class="n">Object</span>(<span class="p">{</span>
  run_id:    Type.<span class="n">String</span>(<span class="p">{</span> pattern: <span class="s">'^run-\\d{8}-\\d{4}$'</span> <span class="p">}</span>),
  loop:      Type.<span class="n">Literal</span>(<span class="s">'L1'</span>),
  unit_id:   Type.<span class="n">String</span>(),
  agent:     Type.<span class="n">Union</span>(<span class="p">[</span>Type.<span class="n">Literal</span>(<span class="s">'coder'</span>), Type.<span class="n">Literal</span>(<span class="s">'tester'</span>)<span class="p">]</span>),
  attempt:   Type.<span class="n">Number</span>(),
  ok:        Type.<span class="n">Boolean</span>(),
  summary:   Type.<span class="n">Object</span>(<span class="p">{</span>
    total:  Type.<span class="n">Number</span>(),
    passed: Type.<span class="n">Number</span>(),
    failed: Type.<span class="n">Number</span>(),
  <span class="p">}</span>),
  failures:  Type.<span class="n">Array</span>(Type.<span class="n">Object</span>(<span class="p">{</span>
    test:     Type.<span class="n">String</span>(),
    expected: Type.<span class="n">Any</span>(),
    got:      Type.<span class="n">Any</span>(),
    stack:    Type.<span class="n">String</span>(),
    hint:     Type.<span class="n">String</span>(),
  <span class="p">}</span>)),
  next_action:       Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),
  retry_budget_left: Type.<span class="n">Optional</span>(Type.<span class="n">Number</span>()),
  duration_ms:       Type.<span class="n">Number</span>(),
<span class="p">}</span>);</pre>
      </section>

      <section data-def-name="IntegrationResult" data-mvcp="model">
        <h2>M-8 · integration/runs/result.json (L2)</h2>
        <code class="path mono" style="color:var(--ink-soft)">data/companies/{cid}/projects/{pid}/integration/runs/{runId}/result.json</code>
<pre class="code shape"><span class="k">const</span> IntResult = Type.<span class="n">Object</span>(<span class="p">{</span>
  run_id:    Type.<span class="n">String</span>(<span class="p">{</span> pattern: <span class="s">'^run-int-\\d{8}-\\d{4}$'</span> <span class="p">}</span>),
  ok:        Type.<span class="n">Boolean</span>(),
  failures:  Type.<span class="n">Array</span>(Type.<span class="n">Object</span>(<span class="p">{</span>
    case:        Type.<span class="n">String</span>(),
    failed_step: Type.<span class="n">Number</span>(),
    got:         Type.<span class="n">Any</span>(),
    involves:    Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
    stderr_excerpt: Type.<span class="n">String</span>(),
  <span class="p">}</span>)),
  process_log: Type.<span class="n">String</span>(),
  duration_ms: Type.<span class="n">Number</span>(),
<span class="p">}</span>);</pre>
      </section>

      <section data-def-name="TriageJson" data-mvcp="model">
        <h2>M-9 · triage.json</h2>
        <code class="path mono" style="color:var(--ink-soft)">.../integration/runs/{runId}/triage.json</code>
<pre class="code shape"><span class="k">const</span> Triage = Type.<span class="n">Object</span>(<span class="p">{</span>
  run_id:          Type.<span class="n">String</span>(),
  root_cause:      Type.<span class="n">String</span>(<span class="p">{</span> minLength: <span class="v">20</span> <span class="p">}</span>),
  category:        Type.<span class="n">Union</span>(<span class="p">[</span>
    Type.<span class="n">Literal</span>(<span class="s">'contract_mismatch'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'impl_bug'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'spec_ambiguous'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'infra_flake'</span>),
  <span class="p">]</span>),
  affected_units:  Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
  proposed_sdd_patch: Type.<span class="n">Union</span>(<span class="p">[</span>Type.<span class="n">Object</span>(<span class="p">{</span>
    target_section: Type.<span class="n">String</span>(),
    op:             Type.<span class="n">String</span>(),
    text:           Type.<span class="n">String</span>(),
  <span class="p">}</span>), Type.<span class="n">Null</span>()<span class="p">]</span>),
  confidence:      Type.<span class="n">Number</span>(<span class="p">{</span> minimum: <span class="v">0</span>, maximum: <span class="v">1</span> <span class="p">}</span>),
  evidence:        Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
<span class="p">}</span>);</pre>
        <ul class="contract">
          <li>category = contract_mismatch → proposed_sdd_patch 必填</li>
          <li>category = impl_bug → proposed_sdd_patch 必为 null(不改 SDD,只 reset Unit)</li>
          <li>confidence &lt; 0.5 → LoopController 上报 L3 不自动应用</li>
          <li>每条 evidence 必须在原文件中能 grep 到</li>
        </ul>
      </section>

      <section data-def-name="AgentRuntimeState" data-mvcp="model">
        <h2>M-10 · AgentRuntimeState · 持久化运行态</h2>
        <code class="path mono" style="color:var(--ink-soft)">data/companies/{cid}/projects/{pid}/agent-state/{agent}.json</code>
<pre class="code shape"><span class="k">const</span> AgentRuntimeState = Type.<span class="n">Object</span>(<span class="p">{</span>
  agent:          Type.<span class="n">Union</span>(<span class="p">[</span>Type.<span class="n">Literal</span>(<span class="s">'pm'</span>), Type.<span class="n">Literal</span>(<span class="s">'architect'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'coder'</span>), Type.<span class="n">Literal</span>(<span class="s">'tester'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'integrator'</span>), Type.<span class="n">Literal</span>(<span class="s">'bug-triager'</span>)<span class="p">]</span>),
  unit_id:        Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),   <span class="c">// Coder/Tester 时填</span>
  status:         Type.<span class="n">Union</span>(<span class="p">[</span>
    Type.<span class="n">Literal</span>(<span class="s">'idle'</span>), Type.<span class="n">Literal</span>(<span class="s">'running'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'paused'</span>), Type.<span class="n">Literal</span>(<span class="s">'done'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'error'</span>)
  <span class="p">]</span>),
  call_id:        Type.<span class="n">String</span>(),                <span class="c">// 单次调用唯一 id</span>
  current_step:   Type.<span class="n">Number</span>(),                <span class="c">// 0-based, 引用 todolist.items[i]</span>
  total_steps:    Type.<span class="n">Number</span>(),
  last_input_ref: Type.<span class="n">String</span>(),                <span class="c">// 输入快照路径</span>
  last_output_ref: Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),
  session_id:     Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()), <span class="c">// ACP session 引用</span>
  resume_token:   Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()), <span class="c">// 冷启恢复用</span>
  started_at:     Type.<span class="n">String</span>(<span class="p">{</span> format: <span class="s">'date-time'</span> <span class="p">}</span>),
  updated_at:     Type.<span class="n">String</span>(<span class="p">{</span> format: <span class="s">'date-time'</span> <span class="p">}</span>),
  error:          Type.<span class="n">Optional</span>(Type.<span class="n">Object</span>(<span class="p">{</span>
    code:    Type.<span class="n">String</span>(),
    message: Type.<span class="n">String</span>(),
    stack:   Type.<span class="n">Optional</span>(Type.<span class="n">String</span>())
  <span class="p">}</span>)),
<span class="p">}</span>);</pre>
        <ul class="contract">
          <li>每次 LoopController.callAgent 前写一次(status=running)</li>
          <li>Agent 自己在跨越主要 checkpoint 时通过 <code>update_runtime_state</code> 工具更新 current_step</li>
          <li>调用结束(成功或失败)由 LoopController 写终态 status=done|error</li>
          <li>Server SIGTERM 时所有 status=running 自动改 status=paused 并写 resume_token</li>
          <li>Server 启动时扫所有 paused,发 SSE <code>resume_decision_needed</code> 等人决策</li>
        </ul>
        <ul class="tests">
          <li>Agent 崩溃 → state.json status=error,error 字段有 code/message</li>
          <li>Server kill -9 → 重启后 state.json status=paused,resume_token 非空</li>
          <li>Dashboard 能从 state.json 渲染「Architect at step 3/5」</li>
        </ul>
      </section>

      <section data-def-name="AgentMemory" data-mvcp="model">
        <h2>M-11 · AgentMemory · 跨项目长期记忆</h2>
        <code class="path mono" style="color:var(--ink-soft)">data/companies/{cid}/memory/devloop/{agent}/{topic}.md</code>
<pre class="code shape"><span class="c">// Markdown + frontmatter,人可读 + 机器可读</span>
---
agent: architect
topic: project-conventions
project_refs: [p-731042, p-845001]
last_updated: 2026-05-13T10:31:00Z
relevance_score: 0.92      <span class="c"># 0-1, agent 自评</span>
hits: 14                    <span class="c"># 被注入 prompt 的次数</span>
---

<span class="v">- 用户偏好 RESTful API + 完整 OpenAPI 注解</span>
<span class="v">- Token 续期实测:5s 内人类可接受</span>
<span class="v">- 认证方式:JWT 放 Authorization header,不用 cookie</span>
<span class="v">- 数据库统一用 better-sqlite3,不要 mysql/postgres</span></pre>
        <ul class="contract">
          <li>每个 Agent 启动时扫 <code>memory/devloop/{agent}/*.md</code></li>
          <li>按 relevance_score × recency 排序,取前 5 条注入 system prompt</li>
          <li>Agent 自己可通过 <code>record_memory(topic, content, relevance)</code> 工具新增</li>
          <li><b>Dedup:同 agent + 同 topic 是 upsert(覆盖 + 累加 hits),不新建文件</b>。文件名 = <code>{topic-slug}.md</code>,topic-slug 由 record_memory 工具按规则计算(小写 · 空格转 -)</li>
          <li>memory 跨项目(同 company 内)共享,<b>不</b>跨 company</li>
          <li>frontmatter 必须含 agent / topic / last_updated 三字段</li>
          <li>单文件 ≤ 8KB,超出 record_memory 自动 truncate 旧内容(保留新加 + 最相关)</li>
          <li><b>v1 注意:</b>如果初版 token 紧张可以关 memory 注入(top-5 改 top-0),完全跳过。memory 在 ≥ 5 个项目后才开始有积累价值。</li>
        </ul>
        <ul class="tests">
          <li>新 Agent 调用 → memory dir 存在则 system prompt 含 "## Memory" section</li>
          <li>不存在该 agent dir → fallback 空 memory,不报错</li>
          <li>record_memory 写入后下次启动能读到</li>
        </ul>
      </section>

      <section data-def-name="AgentTodolist" data-mvcp="model">
        <h2>M-12 · AgentTodolist · 单次调用的子任务清单</h2>
        <code class="path mono" style="color:var(--ink-soft)">data/companies/{cid}/projects/{pid}/agent-state/{agent}-todo.json</code>
<pre class="code shape"><span class="k">const</span> AgentTodolist = Type.<span class="n">Object</span>(<span class="p">{</span>
  agent:        Type.<span class="n">String</span>(),
  call_id:      Type.<span class="n">String</span>(),       <span class="c">// 与 AgentRuntimeState.call_id 关联</span>
  created_at:   Type.<span class="n">String</span>(),
  items:        Type.<span class="n">Array</span>(Type.<span class="n">Object</span>(<span class="p">{</span>
    id:           Type.<span class="n">String</span>(<span class="p">{</span> pattern: <span class="s">'^step-\\d{3}$'</span> <span class="p">}</span>),
    description:  Type.<span class="n">String</span>(),
    status:       Type.<span class="n">Union</span>(<span class="p">[</span>Type.<span class="n">Literal</span>(<span class="s">'todo'</span>), Type.<span class="n">Literal</span>(<span class="s">'doing'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'done'</span>), Type.<span class="n">Literal</span>(<span class="s">'failed'</span>)<span class="p">]</span>),
    started_at:   Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),
    finished_at:  Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),
    output_ref:   Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),    <span class="c">// 产物路径</span>
    depends_on:   Type.<span class="n">Optional</span>(Type.<span class="n">Array</span>(Type.<span class="n">String</span>())),
    error:        Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),
  <span class="p">}</span>)),
  current:      Type.<span class="n">String</span>(),       <span class="c">// 当前 item.id</span>
  progress_pct: Type.<span class="n">Number</span>(<span class="p">{</span> minimum:<span class="v">0</span>, maximum:<span class="v">100</span> <span class="p">}</span>),
<span class="p">}</span>);

<span class="c">// 典型 Architect generate 模式的 todolist 示例:</span>
items: <span class="p">[</span>
  <span class="p">{</span> id:<span class="s">'step-001'</span>, description:<span class="s">'读 prd.html 并解析所有 section'</span>, status:<span class="s">'done'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-002'</span>, description:<span class="s">'识别功能列表(共 5 个特性)'</span>, status:<span class="s">'done'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-003'</span>, description:<span class="s">'拆分每个特性为 be/fe Unit'</span>, status:<span class="s">'doing'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-004'</span>, description:<span class="s">'为每个 Unit 写 OpenAPI contract'</span>, status:<span class="s">'todo'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-005'</span>, description:<span class="s">'生成 integration/spec.json'</span>, status:<span class="s">'todo'</span> <span class="p">}</span>,
<span class="p">]</span></pre>
        <ul class="contract">
          <li>Agent 在 system prompt 指引下,<b>开始时</b>必须先写完整 todolist(todo 状态)</li>
          <li>每完成一项立即调 <code>update_todo_item(id, status, output_ref)</code> 工具</li>
          <li>progress_pct = done 数 / total × 100</li>
          <li>Dashboard 从这里渲染「Architect: 3/5 拆分 OAuth 功能为 3 个 Unit」</li>
          <li>失败 → status=failed,触发 retry 时 todolist 清空重写</li>
        </ul>
      </section>

      <section data-def-name="ProcessTimeline" data-mvcp="model">
        <h2>M-13 · ProcessTimeline · 全流程统一日志</h2>
        <code class="path mono" style="color:var(--ink-soft)">data/companies/{cid}/projects/{pid}/timeline.ndjson</code>
<pre class="code shape"><span class="c">// NDJSON,append-only,一行一事件,每行 ≤ 1KB</span>
<span class="k">const</span> TimelineEvent = Type.<span class="n">Object</span>(<span class="p">{</span>
  ts:       Type.<span class="n">Number</span>(),         <span class="c">// unix seconds</span>
  source:   Type.<span class="n">Union</span>(<span class="p">[</span>
    Type.<span class="n">Literal</span>(<span class="s">'system'</span>),       <span class="c">// LoopController / DocStore / etc</span>
    Type.<span class="n">Literal</span>(<span class="s">'human'</span>),        <span class="c">// 人类显式动作</span>
    Type.<span class="n">Literal</span>(<span class="s">'pm-agent'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'architect'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'coder'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'tester'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'integrator'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'bug-triager'</span>),
  <span class="p">]</span>),
  kind:     Type.<span class="n">String</span>(),         <span class="c">// "project_created" / "prd_drafted" / etc</span>
  ref:      Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()), <span class="c">// 关联资源,如 "prd@rev3"</span>
  summary:  Type.<span class="n">String</span>(<span class="p">{</span> maxLength: <span class="v">280</span> <span class="p">}</span>),  <span class="c">// 人话摘要</span>
  payload:  Type.<span class="n">Optional</span>(Type.<span class="n">Object</span>(<span class="p">{}</span>, <span class="p">{</span> additionalProperties:<span class="v">true</span> <span class="p">}</span>)),
<span class="p">}</span>);

<span class="c">// 示例 (一行 = 一事件):</span>
<span class="p">{</span><span class="n">"ts"</span>:<span class="v">1747120731</span>,<span class="n">"source"</span>:<span class="s">"human"</span>,<span class="n">"kind"</span>:<span class="s">"project_created"</span>,<span class="n">"summary"</span>:<span class="s">"yibo 创建项目'做个 TODO API'"</span><span class="p">}</span>
<span class="p">{</span><span class="n">"ts"</span>:<span class="v">1747120745</span>,<span class="n">"source"</span>:<span class="s">"pm-agent"</span>,<span class="n">"kind"</span>:<span class="s">"prd_drafted"</span>,<span class="n">"ref"</span>:<span class="s">"prd@rev1"</span>,<span class="n">"summary"</span>:<span class="s">"PRD rev 1 完成"</span><span class="p">}</span>
<span class="p">{</span><span class="n">"ts"</span>:<span class="v">1747120798</span>,<span class="n">"source"</span>:<span class="s">"architect"</span>,<span class="n">"kind"</span>:<span class="s">"sdd_generated"</span>,<span class="n">"ref"</span>:<span class="s">"sdd@rev1"</span>,<span class="n">"summary"</span>:<span class="s">"拆 3 个 Unit"</span><span class="p">}</span>
<span class="p">{</span><span class="n">"ts"</span>:<span class="v">1747120821</span>,<span class="n">"source"</span>:<span class="s">"coder"</span>,<span class="n">"kind"</span>:<span class="s">"unit_coded"</span>,<span class="n">"ref"</span>:<span class="s">"unit/todo-be"</span>,<span class="n">"summary"</span>:<span class="s">"todo-be 实现完成"</span><span class="p">}</span>
<span class="p">{</span><span class="n">"ts"</span>:<span class="v">1747120833</span>,<span class="n">"source"</span>:<span class="s">"tester"</span>,<span class="n">"kind"</span>:<span class="s">"unit_failed"</span>,<span class="n">"ref"</span>:<span class="s">"unit/todo-be"</span>,<span class="n">"summary"</span>:<span class="s">"invalid-pwd 期望 401 收 500"</span><span class="p">}</span></pre>
        <ul class="contract">
          <li><b>Append-only</b>,永不修改已有行</li>
          <li>所有 state 转移、文档变更、Agent 调用、Human 动作都必须 append 一条</li>
          <li>是 audit log 也是 Dashboard 时间线视图数据源</li>
          <li>每条 ≤ 1KB;超长内容(如完整 stderr)放 payload.{x}_ref 引用</li>
          <li>由 <code>timelineLogger.append(pid, event)</code> 写入(C-15 工具)</li>
        </ul>
        <p class="sig">与 stream.log(SSE 视角)的区别:timeline.ndjson 是<b>事件视角</b>,stream.log 是<b>客户端视角</b>。前者永远存,后者 7 天滚动。</p>
      </section>

      <section data-def-name="BugsNdjson" data-mvcp="model">
        <h2>M-14 · bugs.ndjson · 跨 run 跨 unit Bug 聚合</h2>
        <code class="path mono" style="color:var(--ink-soft)">data/companies/{cid}/projects/{pid}/bugs.ndjson</code>
<pre class="code shape"><span class="c">// NDJSON,append + 状态机更新</span>
<span class="k">const</span> Bug = Type.<span class="n">Object</span>(<span class="p">{</span>
  id:           Type.<span class="n">String</span>(<span class="p">{</span> pattern: <span class="s">'^bug-\\d{8}-\\d{4}-[a-z0-9]{4}$'</span> <span class="p">}</span>),
  discovered_at:Type.<span class="n">String</span>(<span class="p">{</span> format: <span class="s">'date-time'</span> <span class="p">}</span>),
  source:       Type.<span class="n">Union</span>(<span class="p">[</span>Type.<span class="n">Literal</span>(<span class="s">'unit_test'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'integration_test'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'triage'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'human'</span>)<span class="p">]</span>),
  unit_id:      Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),
  run_id:       Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),    <span class="c">// runs/{runId} 或 integration/runs/{runId}</span>
  test_name:    Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),
  category:     Type.<span class="n">Union</span>(<span class="p">[</span>Type.<span class="n">Literal</span>(<span class="s">'contract_mismatch'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'impl_bug'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'spec_ambiguous'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'infra_flake'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'unknown'</span>)<span class="p">]</span>),
  severity:     Type.<span class="n">Union</span>(<span class="p">[</span>Type.<span class="n">Literal</span>(<span class="s">'critical'</span>), Type.<span class="n">Literal</span>(<span class="s">'high'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'medium'</span>), Type.<span class="n">Literal</span>(<span class="s">'low'</span>)<span class="p">]</span>),
  title:        Type.<span class="n">String</span>(<span class="p">{</span> maxLength: <span class="v">120</span> <span class="p">}</span>),
  detail:       Type.<span class="n">String</span>(),                <span class="c">// 完整 stack/expected/got</span>
  affected_units: Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
  status:       Type.<span class="n">Union</span>(<span class="p">[</span>Type.<span class="n">Literal</span>(<span class="s">'open'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'fixing'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'fixed'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'verified'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'wontfix'</span>),
                              Type.<span class="n">Literal</span>(<span class="s">'duplicate'</span>)<span class="p">]</span>),
  status_history: Type.<span class="n">Array</span>(Type.<span class="n">Object</span>(<span class="p">{</span>
    ts: Type.<span class="n">String</span>(),
    from: Type.<span class="n">String</span>(), to: Type.<span class="n">String</span>(),
    by: Type.<span class="n">String</span>(),                     <span class="c">// agent/system/human id</span>
    note: Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),
  <span class="p">}</span>)),
  fix_refs:     Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),    <span class="c">// 关联的 run_id / triage / commit</span>
  duplicate_of: Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),  <span class="c">// 另一个 bug id</span>
<span class="p">}</span>);

<span class="c">// 示例行:</span>
<span class="p">{</span><span class="n">"id"</span>:<span class="s">"bug-20260513-0731-7f3a"</span>,<span class="n">"source"</span>:<span class="s">"unit_test"</span>,<span class="n">"unit_id"</span>:<span class="s">"auth-be"</span>,<span class="n">"run_id"</span>:<span class="s">"run-..0731"</span>,
 <span class="n">"category"</span>:<span class="s">"impl_bug"</span>,<span class="n">"severity"</span>:<span class="s">"high"</span>,<span class="n">"status"</span>:<span class="s">"fixed"</span>,
 <span class="n">"title"</span>:<span class="s">"login.invalid-pwd 期望 401 收 500"</span>,<span class="n">"affected_units"</span>:<span class="p">[</span><span class="s">"auth-be"</span><span class="p">]</span>,<span class="n">"fix_refs"</span>:<span class="p">[</span><span class="s">"run-..0734"</span><span class="p">]</span><span class="p">}</span></pre>
        <ul class="contract">
          <li>每次 Tester / Integrator / BugTriager 失败 → 自动 append 1 条 bug,status=open</li>
          <li>同一 unit 同一 test 连续两轮失败 → 视同同一 bug(合并),不重复 append</li>
          <li>status 转换走 P-9 状态机,每次转换 append 一条 status_history</li>
          <li>append-only:bug 内容修改通过<b>新写一行 status=...</b> 的更新事件实现(与 timeline 同理)</li>
          <li>读取时取每个 bug.id 的<b>最新</b>一行作为当前状态</li>
        </ul>
        <ul class="tests">
          <li>单测红 → bugs.ndjson 新增 status=open 行</li>
          <li>下一轮 Coder 修复 → 新增 status=fixed 行 with fix_refs=[新 run_id]</li>
          <li>L2 通过 → 该 bug 状态最终 verified</li>
        </ul>
      </section>

      <section data-def-name="ChangelogNdjson" data-mvcp="model">
        <h2>M-15 · changelog.ndjson · 版本变更流</h2>
        <code class="path mono" style="color:var(--ink-soft)">data/companies/{cid}/projects/{pid}/changelog.ndjson</code>
<pre class="code shape"><span class="k">const</span> ChangelogEntry = Type.<span class="n">Object</span>(<span class="p">{</span>
  ts:       Type.<span class="n">String</span>(<span class="p">{</span> format: <span class="s">'date-time'</span> <span class="p">}</span>),
  kind:     Type.<span class="n">Union</span>(<span class="p">[</span>
    Type.<span class="n">Literal</span>(<span class="s">'prd_rev'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'sdd_rev'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'units_rev'</span>),
    Type.<span class="n">Literal</span>(<span class="s">'integration_spec_rev'</span>),
  <span class="p">]</span>),
  rev:      Type.<span class="n">Number</span>(),
  by:       Type.<span class="n">String</span>(),                <span class="c">// agent or human id</span>
  trigger:  Type.<span class="n">String</span>(),                <span class="c">// "human_feedback" | "triage_patch" | "init"</span>
  summary:  Type.<span class="n">String</span>(<span class="p">{</span> maxLength: <span class="v">280</span> <span class="p">}</span>),
  diff:     Type.<span class="n">Object</span>(<span class="p">{</span>                <span class="c">// 影响面快照</span>
    sections_added:   Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
    sections_changed: Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
    sections_removed: Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
    units_added:      Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
    units_changed:    Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
    units_removed:    Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
  <span class="p">}</span>),
  archive_ref: Type.<span class="n">String</span>(),             <span class="c">// history/prd-rev-{N}.html 路径</span>
<span class="p">}</span>);

<span class="c">// 示例:</span>
<span class="p">{</span><span class="n">"kind"</span>:<span class="s">"prd_rev"</span>,<span class="n">"rev"</span>:<span class="v">3</span>,<span class="n">"by"</span>:<span class="s">"pm-agent"</span>,<span class="n">"trigger"</span>:<span class="s">"human_feedback"</span>,
 <span class="n">"summary"</span>:<span class="s">"+ OAuth 登录,影响 auth-mw 新增"</span>,<span class="n">"diff"</span>:<span class="p">{</span><span class="n">"sections_added"</span>:<span class="p">[</span><span class="s">"#oauth"</span><span class="p">]</span>,...<span class="p">}</span><span class="p">}</span>
<span class="p">{</span><span class="n">"kind"</span>:<span class="s">"sdd_rev"</span>,<span class="n">"rev"</span>:<span class="v">5</span>,<span class="n">"by"</span>:<span class="s">"architect"</span>,<span class="n">"trigger"</span>:<span class="s">"prd@rev3"</span>,
 <span class="n">"summary"</span>:<span class="s">"3 个新 Unit: oauth-be / oauth-fe / auth-mw"</span>,...<span class="p">}</span>
<span class="p">{</span><span class="n">"kind"</span>:<span class="s">"sdd_rev"</span>,<span class="n">"rev"</span>:<span class="v">6</span>,<span class="n">"by"</span>:<span class="s">"architect"</span>,<span class="n">"trigger"</span>:<span class="s">"triage_patch"</span>,
 <span class="n">"summary"</span>:<span class="s">"L2 修复:auth-be 401 明确"</span>,...<span class="p">}</span></pre>
        <ul class="contract">
          <li>每次 DocStore.writePRD/writeSDD/writeUnits 成功后必 append 一行</li>
          <li>diff 字段在 PM/Architect Agent 输出后由 DocStore 自动计算(cheerio + 拓扑 diff)</li>
          <li>archive_ref 指向归档文件,可被 Dashboard 直接打开</li>
          <li>这是 history/ 目录的<b>线性视图</b>:history/ 是物,changelog 是事</li>
        </ul>
      </section>

      <section>
        <h2>归档命名约定(统一目录形式)</h2>
        <table class="dense">
          <thead><tr><th>原产物</th><th>归档路径</th><th>形态</th></tr></thead>
          <tbody>
            <tr><td>prd/ 整站</td><td><code>history/prd-rev-{N}/</code></td><td>目录(含整 prd/ 拷贝)</td></tr>
            <tr><td>sdd/ 整站</td><td><code>history/sdd-rev-{N}/</code></td><td>目录</td></tr>
            <tr><td>units.json</td><td><code>history/units-rev-{N}.json</code></td><td>单文件</td></tr>
            <tr><td>integration/spec.json</td><td><code>history/integration-spec-rev-{N}.json</code></td><td>单文件</td></tr>
            <tr><td>removed unit dir</td><td><code>history/removed/{unitId}-{ts}/</code></td><td>目录 + 时间戳防重</td></tr>
            <tr><td>agent-state snapshots</td><td><code>agent-state/snapshots/{call_id}-(in|out).json</code></td><td>不归到 history/(用于 retry 回放)</td></tr>
          </tbody>
        </table>
        <p class="sig">PRD/SDD 现在都是多页站点,归档单元是<b>整个子目录</b>而非单 html。所有 Agent 用 archive_doc 工具,传 kind 即可('prd'/'sdd'/'units'),路径由 DocStore 计算。</p>
      </section>

      <section>
        <h2>无 SQL · 无新表</h2>
        <p>整个 DevLoop 系统不引入新 SQLite 表。所有持久化通过文件系统完成。LemonBoss 既有的 <code>data/users.sqlite</code> 等不动。</p>
        <p class="sig">这是有意为之的简化 — 让外部 Claude Code 复现项目时不用先想 schema migration。</p>
      </section>

    </article>
    <nav class="prev-next"></nav>
    <footer class="site"><div><b>Section</b>03 · SDD</div><div><b>Page</b>sdd-model.html</div></footer>
  </main>
</div>
<script src="assets/shell.js"></script>
</body>
</html>