sdd-overview.html 12.6 KB
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>SDD MVCP 总览 · 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-overview.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>MVCP 总览</span></div>
      <h1>SDD · MVCP 总览</h1>
      <p class="deck">SDD(System Design Document)用 <b>MVCP 四象限</b>组织:<b>M</b>odel · <b>V</b>iew · <b>C</b>ontrol · <b>P</b>rocess。每个象限是一篇独立页,每个定义带 <b>name / path / shape / contract / tests</b> 五字段。Claude Code 用 cheerio 即可机械抽取。</p>
    </header>
    <article class="body">

      <section>
        <h2>为什么是 MVCP 而不是 MVC</h2>
        <p>传统 MVC 三分对一个有大量「长进程 / 异步循环」的系统不够用。DevLoop 系统里:</p>
        <ul>
          <li>L1/L2/L3 是<b>长时运行的状态机</b></li>
          <li>每个 Agent 是<b>长寿子进程</b>(ACP session 复用)</li>
          <li>SSE Broker 是<b>持续推送通道</b></li>
        </ul>
        <p>这些都不属于 Controller(即时响应请求),也不属于 Model(数据)。给它们一个独立象限 <b>Process</b> 才有归处。</p>
      </section>

      <section>
        <h2>四象限</h2>
        <svg viewBox="0 0 1100 480" width="100%" role="img" aria-label="MVCP 四象限">
          <defs>
            <linearGradient id="mg" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#0E8C5A" stop-opacity=".12"/><stop offset="100%" stop-color="#0E8C5A" stop-opacity=".02"/></linearGradient>
            <linearGradient id="vg" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#6E48C3" stop-opacity=".12"/><stop offset="100%" stop-color="#6E48C3" stop-opacity=".02"/></linearGradient>
            <linearGradient id="cg" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#C77C2C" stop-opacity=".12"/><stop offset="100%" stop-color="#C77C2C" stop-opacity=".02"/></linearGradient>
            <linearGradient id="pg" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#C7341A" stop-opacity=".12"/><stop offset="100%" stop-color="#C7341A" stop-opacity=".02"/></linearGradient>
          </defs>
          <g font-family="JetBrains Mono" font-size="12" fill="#0E0E10">
            <rect x="40" y="40"  width="480" height="200" rx="8" fill="url(#mg)" stroke="#0E8C5A"/>
            <text x="60" y="68" fill="#0E8C5A" font-weight="700" font-size="14">M · MODEL</text>
            <text x="60" y="90"  font-size="11">数据结构 · 文档 schema · 落盘格式</text>
            <text x="60" y="115" font-size="11">• prd.html · sdd.html 结构</text>
            <text x="60" y="135" font-size="11">• units.json · project.json · contract.json</text>
            <text x="60" y="155" font-size="11">• runs/result.json · triage.json</text>
            <text x="60" y="175" font-size="11">• Unit 六态机字典</text>
            <text x="60" y="195" font-size="11">• 全部 TypeBox schema</text>
            <text x="60" y="220" fill="#0E8C5A" font-size="11" font-weight="700">→ sdd-model.html</text>

            <rect x="540" y="40" width="480" height="200" rx="8" fill="url(#vg)" stroke="#6E48C3"/>
            <text x="560" y="68" fill="#6E48C3" font-weight="700" font-size="14">V · VIEW</text>
            <text x="560" y="90"  font-size="11">UI 组件 · 路由 · i18n</text>
            <text x="560" y="115" font-size="11">• DevLoopDashboard.vue 组件树</text>
            <text x="560" y="135" font-size="11">• 7 个子组件 props/slots/emits</text>
            <text x="560" y="155" font-size="11">• router 路由表</text>
            <text x="560" y="175" font-size="11">• i18n keys 全清单</text>
            <text x="560" y="195" font-size="11">• 设计 token 引用</text>
            <text x="560" y="220" fill="#6E48C3" font-size="11" font-weight="700">→ sdd-view.html</text>

            <rect x="40" y="260" width="480" height="200" rx="8" fill="url(#cg)" stroke="#C77C2C"/>
            <text x="60" y="288" fill="#C77C2C" font-weight="700" font-size="14">C · CONTROL</text>
            <text x="60" y="310" font-size="11">即时响应:API · 服务方法 · 路由 handler</text>
            <text x="60" y="335" font-size="11">• REST 端点全签名(7 条)</text>
            <text x="60" y="355" font-size="11">• LoopController 公共方法</text>
            <text x="60" y="375" font-size="11">• DocStore / UnitRegistry / SSEBroker 方法</text>
            <text x="60" y="395" font-size="11">• Agent callAgent 调用约定</text>
            <text x="60" y="415" font-size="11">• 中间件(auth · jsonGuard · sanitizer)</text>
            <text x="60" y="440" fill="#C77C2C" font-size="11" font-weight="700">→ sdd-control.html</text>

            <rect x="540" y="260" width="480" height="200" rx="8" fill="url(#pg)" stroke="#C7341A"/>
            <text x="560" y="288" fill="#C7341A" font-weight="700" font-size="14">P · PROCESS</text>
            <text x="560" y="310" font-size="11">长进程:状态机 · Loop · 后台任务</text>
            <text x="560" y="335" font-size="11">• L1 / L2 / L3 状态机详尽转换</text>
            <text x="560" y="355" font-size="11">• Agent 生命周期 (启动/复用/回收)</text>
            <text x="560" y="375" font-size="11">• SSE 订阅 / 心跳 / 重连</text>
            <text x="560" y="395" font-size="11">• retryWithReflection 算法</text>
            <text x="560" y="415" font-size="11">• 清理 / 归档 / 预算守护</text>
            <text x="560" y="440" fill="#C7341A" font-size="11" font-weight="700">→ sdd-process.html</text>
          </g>
        </svg>
      </section>

      <section>
        <h2>每个定义的「五字段标准」</h2>
        <p>所有 MVCP 页里的每条定义都强制遵循下表结构,便于 Claude Code 用同一套抽取器处理:</p>
        <table>
          <thead><tr><th>字段</th><th>类型</th><th>含义</th><th>抽取选择器</th></tr></thead>
          <tbody>
            <tr><td><b>name</b></td><td>string · 必填</td><td>唯一标识符 · 命名空间.对象名</td><td><code>section[data-def-name]</code></td></tr>
            <tr><td><b>path</b></td><td>string · 必填</td><td>预期落盘路径或文件位置</td><td><code>[data-def-name] code.path</code></td></tr>
            <tr><td><b>shape</b></td><td>code · 必填</td><td>TypeBox / TS interface / Vue defineProps / SQL DDL</td><td><code>[data-def-name] pre.shape</code></td></tr>
            <tr><td><b>contract</b></td><td>list · 选填</td><td>前置 / 后置条件 / 不变式</td><td><code>[data-def-name] ul.contract li</code></td></tr>
            <tr><td><b>tests</b></td><td>list · 选填</td><td>验收用例(Tester Agent 直接转 vitest)</td><td><code>[data-def-name] ul.tests li</code></td></tr>
          </tbody>
        </table>

        <h3>HTML 模板示例</h3>
        <pre class="code"><span class="k">&lt;section</span> <span class="n">data-def-name</span>=<span class="s">"DocStore.writePRD"</span> <span class="n">data-mvcp</span>=<span class="s">"control"</span><span class="k">&gt;</span>
  <span class="k">&lt;h3&gt;</span><span class="v">DocStore.writePRD(pid, html)</span><span class="k">&lt;/h3&gt;</span>
  <span class="k">&lt;code</span> <span class="n">class</span>=<span class="s">"path"</span><span class="k">&gt;</span><span class="v">server/services/doc-store.js</span><span class="k">&lt;/code&gt;</span>

  <span class="k">&lt;pre</span> <span class="n">class</span>=<span class="s">"shape"</span><span class="k">&gt;</span><span class="v">
async function writePRD(pid: string, html: string): Promise&lt;{rev: number}&gt;
  </span><span class="k">&lt;/pre&gt;</span>

  <span class="k">&lt;ul</span> <span class="n">class</span>=<span class="s">"contract"</span><span class="k">&gt;</span>
    <span class="k">&lt;li&gt;</span><span class="v">前置:html 必须包含顶级 article[data-doc="prd"]</span><span class="k">&lt;/li&gt;</span>
    <span class="k">&lt;li&gt;</span><span class="v">后置:旧版本归档到 history/prd-rev-{rev}.html</span><span class="k">&lt;/li&gt;</span>
  <span class="k">&lt;/ul&gt;</span>

  <span class="k">&lt;ul</span> <span class="n">class</span>=<span class="s">"tests"</span><span class="k">&gt;</span>
    <span class="k">&lt;li&gt;</span><span class="v">写入后 readPRD 能读回完全一致内容</span><span class="k">&lt;/li&gt;</span>
    <span class="k">&lt;li&gt;</span><span class="v">非法 HTML 抛 ValidationError</span><span class="k">&lt;/li&gt;</span>
  <span class="k">&lt;/ul&gt;</span>
<span class="k">&lt;/section&gt;</span></pre>
        <p class="sig">本套文档因美观需要不完全遵守这个最严格的 HTML 模板,但每条定义至少含 name + path + shape。完全机械化版会在 P1 实施时由 Architect Agent 产出。</p>
      </section>

      <section>
        <h2>抽取流程</h2>
        <pre class="code"><span class="c">// Claude Code 抽取 build-manifest 的伪代码</span>
<span class="k">const</span> manifest = <span class="p">{</span> model: <span class="p">[]</span>, view: <span class="p">[]</span>, control: <span class="p">[]</span>, process: <span class="p">[]</span> <span class="p">}</span>;
<span class="k">for</span> (<span class="k">const</span> mvcp <span class="k">of</span> <span class="p">[</span><span class="s">'model'</span>, <span class="s">'view'</span>, <span class="s">'control'</span>, <span class="s">'process'</span><span class="p">]</span>) <span class="p">{</span>
  <span class="k">const</span> html = <span class="k">await</span> <span class="n">fetch</span>(<span class="s">`sdd-</span><span class="v">${mvcp}</span><span class="s">.html`</span>).<span class="n">then</span>(r =&gt; r.<span class="n">text</span>());
  <span class="k">const</span> $ = cheerio.<span class="n">load</span>(html);
  $(<span class="s">'[data-def-name]'</span>).<span class="n">each</span>((_, el) =&gt; <span class="p">{</span>
    manifest[mvcp].<span class="n">push</span>(<span class="p">{</span>
      name:     $(el).<span class="n">attr</span>(<span class="s">'data-def-name'</span>),
      path:     $(el).<span class="n">find</span>(<span class="s">'code.path'</span>).<span class="n">first</span>().<span class="n">text</span>(),
      shape:    $(el).<span class="n">find</span>(<span class="s">'pre.shape'</span>).<span class="n">text</span>(),
      contract: $(el).<span class="n">find</span>(<span class="s">'ul.contract li'</span>).<span class="n">map</span>((_,li) =&gt; $(li).<span class="n">text</span>()).<span class="n">get</span>(),
      tests:    $(el).<span class="n">find</span>(<span class="s">'ul.tests li'</span>).<span class="n">map</span>((_,li) =&gt; $(li).<span class="n">text</span>()).<span class="n">get</span>(),
    <span class="p">}</span>);
  <span class="p">}</span>);
<span class="p">}</span>
<span class="n">fs</span>.<span class="n">writeFileSync</span>(<span class="s">'build-manifest.json'</span>, <span class="n">JSON.stringify</span>(manifest, <span class="v">null</span>, <span class="v">2</span>));</pre>
        <p>结果是一份 <code>build-manifest.json</code>,能直接喂给 Architect Agent 生成 units.json + Unit spec 文件。</p>
      </section>

      <section>
        <h2>四象限对应 Agent</h2>
        <table class="dense">
          <thead><tr><th>象限</th><th>谁负责写</th><th>谁负责消费</th></tr></thead>
          <tbody>
            <tr><td>Model</td><td>Architect</td><td>Coder(读 schema 生成数据访问代码)· DocStore(自身实现)</td></tr>
            <tr><td>View</td><td>Architect</td><td>Coder(fe 类 Unit 直接对应)</td></tr>
            <tr><td>Control</td><td>Architect</td><td>Coder(be 类 Unit)· Integrator(跑端点)</td></tr>
            <tr><td>Process</td><td>Architect</td><td>所有 Agent 都要读懂 Process 才知道自己在生命周期哪一步</td></tr>
          </tbody>
        </table>
      </section>

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