arch-components.html 12 KB
<!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> &lt; <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>++ &gt;= <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>&lt;article data-doc="prd" data-rev&gt;</code>,至少 1 <code>&lt;section&gt;</code></td><td>cheerio</td></tr>
            <tr><td>sdd.html</td><td>每 <code>&lt;section data-unit-id&gt;</code> 含 <code>&lt;h3&gt;</code> + <code>&lt;pre data-contract&gt;</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>