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 > 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 <pre data-contract> 文本) 截前 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"><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">></span>
<span class="k"><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">></span>
<span class="k"><h3></span>...<span class="k"></h3></span>
<span class="k"><p></span>...<span class="k"></p></span>
<span class="k"></section></span>
<span class="k"><aside</span> <span class="n">data-conflict</span><span class="k">></span>...<span class="k"></aside></span> <span class="c">// 可选,index.html only</span>
<span class="k"><footer</span> <span class="n">data-changelog</span><span class="k">></span>...<span class="k"></footer></span>
<span class="k"></article></span></pre>
<ul class="contract">
<li>顶级必须是 <code><article data-doc="prd"></code>,每页都有</li>
<li>每个 section 必含 <code><h3></code> + 至少一个 <code><p></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"><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">></span>
<span class="k"><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">></span>
<span class="k"><h3></span><span class="v">Unit · auth-be</span><span class="k"></h3></span>
<span class="k"><p></span><span class="v">...功能描述...</span><span class="k"></p></span>
<span class="k"><pre</span> <span class="n">data-contract</span>=<span class="s">"openapi"</span><span class="k">></span>
<span class="v">POST /api/auth/login ...</span>
<span class="k"></pre></span>
<span class="k"><ul</span> <span class="n">data-acceptance</span><span class="k">></span>
<span class="k"><li></span><span class="v">acceptance criteria 1</span><span class="k"></li></span>
<span class="k"></ul></span>
<span class="k"></section></span>
<span class="k"></article></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 < 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>