arch-components.html
12 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
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>三个新组件 · 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="arch-components.html">
<div class="shell">
<aside id="sidebar"></aside>
<main class="page">
<header class="page-head">
<div class="crumb"><span class="pill">04 · ARCHITECTURE</span><span class="sep">/</span><span>三个新组件</span></div>
<h1>三个新组件</h1>
<p class="deck">整套改造的核心只有三个新对象:<b>LoopController</b>、<b>DocStore</b>、<b>UnitRegistry</b>。它们是新增 1500 行代码的主要载体。</p>
</header>
<article class="body">
<section>
<h2>① LoopController</h2>
<p>路径:<code>server/src/core/loop-controller.js</code> · ~180 行</p>
<p>编排 L1 / L2 / L3 三层状态机。所有 Agent 调用统一从这里出。它本身无状态,所有状态从 UnitRegistry / DocStore 读。</p>
<h3>关键方法</h3>
<table class="dense">
<thead><tr><th>方法</th><th>调用时机</th><th>返回</th></tr></thead>
<tbody>
<tr><td><code>runProject(pid)</code></td><td>启动 / 续跑项目</td><td><code>'shipped' | 'blocked' | 'l2_exhausted'</code></td></tr>
<tr><td><code>runUnit(unit)</code></td><td>L1 内部</td><td>UnitResult</td></tr>
<tr><td><code>onHumanFeedback(pid, req)</code></td><td>L3 入口(HTTP)</td><td>void · 异步触发 runProject</td></tr>
<tr><td><code>onL2Failure(pid, runId)</code></td><td>Integrator 返回 fail 后</td><td>void</td></tr>
<tr><td><code>getState(pid)</code></td><td>Dashboard 轮询</td><td>ProjectState 快照</td></tr>
</tbody>
</table>
<h3>主循环骨架</h3>
<pre class="code"><span class="k">async function</span> <span class="n">runProject</span>(<span class="v">pid</span>) <span class="p">{</span>
<span class="k">while</span> (<span class="v">true</span>) <span class="p">{</span>
<span class="c">// L1: pending → coding → testing → passed</span>
<span class="k">const</span> waves = <span class="n">scheduler</span>.<span class="n">getExecutionGroups</span>(<span class="n">unitRegistry</span>.<span class="n">pending</span>(pid));
<span class="k">for</span> (<span class="k">const</span> w <span class="k">of</span> waves) <span class="k">await</span> <span class="n">Promise.all</span>(w.<span class="n">map</span>(<span class="n">runUnit</span>));
<span class="k">if</span> (<span class="n">unitRegistry</span>.<span class="n">anyBlocked</span>(pid)) <span class="k">return</span> <span class="s">'blocked'</span>;
<span class="c">// L2 gate</span>
<span class="k">if</span> (!<span class="n">l2GateOpen</span>(pid)) <span class="k">continue</span>;
<span class="k">const</span> r = <span class="k">await</span> <span class="n">agentManager</span>.<span class="n">callAgent</span>(<span class="s">'integrator'</span>, <span class="p">{</span> pid <span class="p">}</span>);
<span class="k">if</span> (r.<span class="n">ok</span>) <span class="k">return</span> <span class="s">'shipped'</span>;
<span class="c">// L2 failed → triage → patch SDD → reset units</span>
<span class="k">const</span> tri = <span class="k">await</span> <span class="n">agentManager</span>.<span class="n">callAgent</span>(<span class="s">'bug-triager'</span>, r.<span class="n">failure</span>);
<span class="k">if</span> (tri.<span class="n">confidence</span> < <span class="v">0.5</span>) <span class="p">{</span>
<span class="n">sse</span>.<span class="n">emit</span>(pid, <span class="s">'human_intervention_needed'</span>, tri); <span class="k">return</span> <span class="s">'l2_blocked'</span>;
<span class="p">}</span>
<span class="k">await</span> <span class="n">agentManager</span>.<span class="n">callAgent</span>(<span class="s">'architect'</span>, <span class="p">{</span> mode: <span class="s">'patch'</span>, patch: tri.<span class="n">proposed_sdd_patch</span> <span class="p">}</span>);
<span class="n">unitRegistry</span>.<span class="n">reset</span>(pid, tri.<span class="n">affected_units</span>);
<span class="k">if</span> (<span class="n">project</span>.<span class="n">l2Rounds</span>++ >= <span class="v">3</span>) <span class="k">return</span> <span class="s">'l2_exhausted'</span>;
<span class="p">}</span>
<span class="p">}</span></pre>
</section>
<section>
<h2>② DocStore</h2>
<p>路径:<code>server/services/doc-store.js</code> · ~140 行</p>
<p>负责 prd.html / sdd.html / units.json / integration/spec.json 的读写、归档、校验。所有写操作走它,没有 Agent 能直接 Write 这些文件。</p>
<h3>API 面</h3>
<pre class="code"><span class="k">module.exports</span> = <span class="p">{</span>
<span class="c">// 读</span>
<span class="n">readPRD</span>(pid), <span class="n">readSDD</span>(pid), <span class="n">readUnits</span>(pid), <span class="n">readIntegrationSpec</span>(pid),
<span class="c">// 写(自动归档旧版)</span>
<span class="n">writePRD</span>(pid, html),
<span class="n">writeSDD</span>(pid, html),
<span class="n">writeUnits</span>(pid, json),
<span class="n">writeIntegrationSpec</span>(pid, json),
<span class="c">// 校验</span>
<span class="n">validateHTML</span>(html), <span class="c">// 用 cheerio + 必含 section</span>
<span class="n">validateUnitsSchema</span>(json), <span class="c">// 用 typebox</span>
<span class="c">// diff / 归档</span>
<span class="n">diffPRD</span>(pid, fromRev, toRev),
<span class="n">archive</span>(pid, kind, oldHtml), <span class="c">// → history/{kind}-rev-N.html</span>
<span class="p">}</span>;</pre>
<h3>HTML 校验规则</h3>
<table class="dense">
<thead><tr><th>文档</th><th>必含元素</th><th>校验工具</th></tr></thead>
<tbody>
<tr><td>prd.html</td><td>顶级 <code><article data-doc="prd" data-rev></code>,至少 1 <code><section></code></td><td>cheerio</td></tr>
<tr><td>sdd.html</td><td>每 <code><section data-unit-id></code> 含 <code><h3></code> + <code><pre data-contract></code></td><td>cheerio</td></tr>
<tr><td>units.json</td><td>schema 校验 + DAG 无环</td><td>typebox + 拓扑排序</td></tr>
<tr><td>integration/spec.json</td><td>每 case 的 involves 都在 units.json 中存在</td><td>自写</td></tr>
</tbody>
</table>
</section>
<section>
<h2>③ UnitRegistry</h2>
<p>路径:<code>server/services/unit-registry.js</code> · ~120 行</p>
<p>Unit 状态机的 source of truth。状态转换都走它,不允许 Agent 自己改 units.json 的 status 字段。</p>
<h3>API 面</h3>
<pre class="code"><span class="k">module.exports</span> = <span class="p">{</span>
<span class="n">list</span>(pid), <span class="c">// 全部 unit + 状态快照</span>
<span class="n">pending</span>(pid), <span class="n">passed</span>(pid), <span class="n">blocked</span>(pid),
<span class="n">l2GateOpen</span>(pid), <span class="c">// 全 passed 且 hash 一致</span>
<span class="c">// 状态转换</span>
<span class="n">markCoding</span>(pid, unitId),
<span class="n">markTesting</span>(pid, unitId),
<span class="n">pass</span>(pid, unitId, contractHash),
<span class="n">fail</span>(pid, unitId, reason),
<span class="n">block</span>(pid, unitId, reason),
<span class="n">reset</span>(pid, unitIds), <span class="c">// L2 修复后</span>
<span class="c">// diff (用于 L3 增量)</span>
<span class="n">diffWithSDD</span>(pid), <span class="c">// → { added, changed, removed }</span>
<span class="p">}</span>;</pre>
<h3>状态转换合法性</h3>
<table class="dense">
<thead><tr><th>from \ to</th><th>pending</th><th>coding</th><th>testing</th><th>failed</th><th>passed</th><th>blocked</th></tr></thead>
<tbody>
<tr><th>pending</th><td>—</td><td>✓</td><td></td><td></td><td></td><td></td></tr>
<tr><th>coding</th><td></td><td>—</td><td>✓</td><td></td><td></td><td></td></tr>
<tr><th>testing</th><td></td><td></td><td>—</td><td>✓</td><td>✓</td><td></td></tr>
<tr><th>failed</th><td></td><td>✓</td><td></td><td>—</td><td></td><td>✓</td></tr>
<tr><th>passed</th><td>✓ (L2 reset)</td><td></td><td></td><td></td><td>—</td><td></td></tr>
<tr><th>blocked</th><td>✓ (人类干预)</td><td></td><td></td><td></td><td></td><td>—</td></tr>
</tbody>
</table>
<p class="sig">非法转换 → 抛异常 + SSE 错误事件,便于排查。</p>
</section>
<section>
<h2>三者协作</h2>
<svg viewBox="0 0 1100 280" width="100%" role="img">
<defs><marker id="arrC" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 Z" fill="#0E0E10"/></marker></defs>
<g font-family="JetBrains Mono" font-size="12">
<rect x="440" y="40" width="240" height="60" rx="6" fill="#FCE7E2" stroke="#C7341A" stroke-width="2"/>
<text x="560" y="70" text-anchor="middle" font-weight="700">LoopController</text>
<text x="560" y="88" text-anchor="middle" font-size="10" fill="#3B3B40">编排所有 Loop</text>
<rect x="100" y="180" width="220" height="60" rx="6" fill="#FCE7E2" stroke="#C7341A"/>
<text x="210" y="210" text-anchor="middle" font-weight="700">DocStore</text>
<text x="210" y="228" text-anchor="middle" font-size="10" fill="#3B3B40">PRD · SDD · units.json IO</text>
<rect x="780" y="180" width="220" height="60" rx="6" fill="#FCE7E2" stroke="#C7341A"/>
<text x="890" y="210" text-anchor="middle" font-weight="700">UnitRegistry</text>
<text x="890" y="228" text-anchor="middle" font-size="10" fill="#3B3B40">Unit 状态机</text>
<g stroke="#0E0E10" fill="none" marker-end="url(#arrC)">
<line x1="500" y1="100" x2="240" y2="175"/>
<line x1="620" y1="100" x2="860" y2="175"/>
<line x1="320" y1="210" x2="775" y2="210"/>
</g>
<text x="350" y="160" font-size="10" fill="#3B3B40">read/write docs</text>
<text x="720" y="160" font-size="10" fill="#3B3B40">update unit state</text>
<text x="510" y="200" font-size="10" fill="#3B3B40">UnitRegistry reads units.json via DocStore</text>
</g>
</svg>
</section>
<section>
<h2>外部依赖</h2>
<table class="dense">
<thead><tr><th>包</th><th>用途</th><th>是否新增</th></tr></thead>
<tbody>
<tr><td><code>cheerio</code></td><td>解析 / 校验 HTML</td><td>新增 (~120KB)</td></tr>
<tr><td><code>@sinclair/typebox</code></td><td>JSON schema 校验</td><td>已存在</td></tr>
<tr><td><code>better-sqlite3</code></td><td>—</td><td>已存在(不需要新表)</td></tr>
</tbody>
</table>
<p class="sig">不引入新数据库;项目状态全部落文件。Dashboard 用 SSE 推送,不需要轮询 DB。</p>
</section>
</article>
<nav class="prev-next"></nav>
<footer class="site"><div><b>Section</b>04 · Architecture</div><div><b>Page</b>arch-components.html</div></footer>
</main>
</div>
<script src="assets/shell.js"></script>
</body>
</html>