api-contracts.html
12.9 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
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Unit Contract 规范 · 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="api-contracts.html">
<div class="shell">
<aside id="sidebar"></aside>
<main class="page">
<header class="page-head">
<div class="crumb"><span class="pill">06 · API</span><span class="sep">/</span><span>Unit Contract 规范</span></div>
<h1>Unit Contract 规范</h1>
<p class="deck">每个 Unit 暴露给「外部」的契约协议。Architect 在 SDD 中规定,Coder 实现后产出 <code>contract.json</code>。两者必须一致;DocStore 校验。Contract 是跨 Unit 协同的唯一接口。</p>
</header>
<article class="body">
<section>
<h2>为什么需要 Contract</h2>
<p>没有 Contract 时,downstream Unit(如 auth-fe)只能<b>读 upstream Unit 的源码</b>(如 auth-be 的 server.js)。这会引发:</p>
<ul>
<li>实现细节耦合,任何重构都连环失败</li>
<li>Coder Agent 沙箱必须开放跨目录读权限(不安全)</li>
<li>contract_hash 无法计算 → L2 gate 失效</li>
</ul>
<p>有 Contract 后,downstream 只读 upstream 的 <code>contract.json</code>。这是<b>跨 Unit 唯一允许的读路径</b>。</p>
</section>
<section>
<h2>Contract Schema(TypeBox)</h2>
<pre class="code"><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>()), <span class="c">// 相对路径 ./types.d.ts</span>
events: Type.<span class="n">Optional</span>(Type.<span class="n">Array</span>(EventSpec)),
cli: Type.<span class="n">Optional</span>(Type.<span class="n">Array</span>(CliSpec)),
<span class="p">}</span>),
consumes: Type.<span class="n">Optional</span>(Type.<span class="n">Array</span>(Type.<span class="n">String</span>())), <span class="c">// 上游 Unit id</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">Union</span>(<span class="p">[</span>Type.<span class="n">Literal</span>(<span class="s">'GET'</span>), Type.<span class="n">Literal</span>(<span class="s">'POST'</span>),
Type.<span class="n">Literal</span>(<span class="s">'PUT'</span>), Type.<span class="n">Literal</span>(<span class="s">'DELETE'</span>),
Type.<span class="n">Literal</span>(<span class="s">'PATCH'</span>)<span class="p">]</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">Record</span>(Type.<span class="n">String</span>(), <span class="c">// "200" | "401" | ...</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>)),
auth: Type.<span class="n">Optional</span>(Type.<span class="n">Union</span>(<span class="p">[</span>
Type.<span class="n">Literal</span>(<span class="s">'none'</span>), Type.<span class="n">Literal</span>(<span class="s">'jwt'</span>), Type.<span class="n">Literal</span>(<span class="s">'apikey'</span>)
<span class="p">]</span>)),
<span class="p">}</span>);
<span class="k">const</span> EventSpec = Type.<span class="n">Object</span>(<span class="p">{</span>
name: Type.<span class="n">String</span>(), <span class="c">// "unit.state"</span>
payload: Type.<span class="n">Object</span>(<span class="p">{}</span>, <span class="p">{</span> additionalProperties: <span class="v">true</span> <span class="p">}</span>),
channel: Type.<span class="n">Union</span>(<span class="p">[</span>Type.<span class="n">Literal</span>(<span class="s">'sse'</span>), Type.<span class="n">Literal</span>(<span class="s">'ws'</span>)<span class="p">]</span>),
<span class="p">}</span>);
<span class="k">const</span> CliSpec = Type.<span class="n">Object</span>(<span class="p">{</span>
cmd: Type.<span class="n">String</span>(), <span class="c">// "auto-dev"</span>
args: Type.<span class="n">Array</span>(Type.<span class="n">Object</span>(<span class="p">{</span> name: Type.<span class="n">String</span>(), required: Type.<span class="n">Boolean</span>() <span class="p">}</span>)),
<span class="p">}</span>);</pre>
</section>
<section>
<h2>完整示例 · auth-be</h2>
<pre class="code"><span class="p">{</span>
<span class="n">"unit_id"</span>: <span class="s">"auth-be"</span>,
<span class="n">"version"</span>: <span class="s">"sha256:7f3a..."</span>,
<span class="n">"exports"</span>: <span class="p">{</span>
<span class="n">"http"</span>: <span class="p">[</span>
<span class="p">{</span>
<span class="n">"method"</span>: <span class="s">"POST"</span>,
<span class="n">"path"</span>: <span class="s">"/api/auth/login"</span>,
<span class="n">"request"</span>: <span class="p">{</span> <span class="n">"email"</span>: <span class="s">"string"</span>, <span class="n">"password"</span>: <span class="s">"string"</span> <span class="p">}</span>,
<span class="n">"responses"</span>: <span class="p">{</span>
<span class="n">"200"</span>: <span class="p">{</span> <span class="n">"token"</span>: <span class="s">"string"</span>, <span class="n">"user_id"</span>: <span class="s">"string"</span> <span class="p">}</span>,
<span class="n">"401"</span>: <span class="p">{</span> <span class="n">"error"</span>: <span class="s">"\"invalid_credentials\""</span> <span class="p">}</span>
<span class="p">}</span>,
<span class="n">"auth"</span>: <span class="s">"none"</span>
<span class="p">}</span>,
<span class="p">{</span>
<span class="n">"method"</span>: <span class="s">"POST"</span>,
<span class="n">"path"</span>: <span class="s">"/api/auth/register"</span>,
<span class="n">"request"</span>: <span class="p">{</span> <span class="n">"email"</span>: <span class="s">"string"</span>, <span class="n">"password"</span>: <span class="s">"string"</span> <span class="p">}</span>,
<span class="n">"responses"</span>: <span class="p">{</span>
<span class="n">"201"</span>: <span class="p">{</span> <span class="n">"user_id"</span>: <span class="s">"string"</span> <span class="p">}</span>,
<span class="n">"409"</span>: <span class="p">{</span> <span class="n">"error"</span>: <span class="s">"\"email_exists\""</span> <span class="p">}</span>
<span class="p">}</span>,
<span class="n">"auth"</span>: <span class="s">"none"</span>
<span class="p">}</span>
<span class="p">]</span>,
<span class="n">"ts_types"</span>: <span class="s">"./types.d.ts"</span>
<span class="p">}</span>,
<span class="n">"consumes"</span>: <span class="p">[]</span>
<span class="p">}</span></pre>
</section>
<section>
<h2>contract_hash 计算</h2>
<pre class="code"><span class="k">function</span> <span class="n">hashContract</span>(contract) <span class="p">{</span>
<span class="c">// 1. 序列化 exports 字段(不包含 version 自身)</span>
<span class="k">const</span> normalized = canonicalJSON(contract.<span class="n">exports</span>);
<span class="c">// 2. sha256 完整 hex</span>
<span class="k">const</span> full = crypto.<span class="n">createHash</span>(<span class="s">'sha256'</span>).<span class="n">update</span>(normalized).<span class="n">digest</span>(<span class="s">'hex'</span>);
<span class="c">// 3. 截前 16 位(加 'sha256:' 前缀)</span>
<span class="k">return</span> <span class="s">`sha256:</span><span class="v">${full.slice(0, 16)}</span><span class="s">`</span>;
<span class="p">}</span>
<span class="c">// canonicalJSON:key 按字典序排列,无空白,统一引号</span>
<span class="c">// 保证同一 exports 在任意机器上 hash 完全相同</span></pre>
<p class="sig">截 16 位的概率碰撞计算 = 2^-64,对单项目内 ≤ 100 个 Unit 安全。</p>
</section>
<section>
<h2>SDD ↔ contract.json 一致性</h2>
<p>Architect 在 SDD 写的 <code><pre data-contract></code> 内容必须与 Coder 写的 contract.json 一致。校验流程:</p>
<ol>
<li>Architect 写 SDD 时把 contract 文本固化在 <code><pre></code></li>
<li>Coder 实现完成后写 contract.json,<b>必须包含 SDD 描述的全部 endpoints</b></li>
<li>DocStore 计算两者 hash 对比 — 不一致 → Coder 视为 fail,触发 retry</li>
<li>反过来 Coder 不许新增 SDD 未声明的 endpoint(沙箱拒绝)</li>
</ol>
</section>
<section>
<h2>contract 变化触发的连锁</h2>
<svg viewBox="0 0 1100 220" width="100%" role="img">
<defs><marker id="arrCT" 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="11" fill="#0E0E10">
<rect x="20" y="80" width="180" height="50" rx="6" fill="#fff" stroke="#0E0E10"/>
<text x="110" y="105" text-anchor="middle">Architect 改 SDD</text>
<text x="110" y="120" text-anchor="middle" font-size="9"><pre data-contract> 内容变</text>
<rect x="240" y="80" width="180" height="50" rx="6" fill="#fff" stroke="#0E0E10"/>
<text x="330" y="105" text-anchor="middle">新 contract_hash</text>
<text x="330" y="120" text-anchor="middle" font-size="9">DocStore 计算</text>
<rect x="460" y="80" width="180" height="50" rx="6" fill="#FFF6CC" stroke="#8a6b00"/>
<text x="550" y="105" text-anchor="middle">UnitRegistry diff</text>
<text x="550" y="120" text-anchor="middle" font-size="9">该 Unit changed</text>
<rect x="680" y="80" width="180" height="50" rx="6" fill="#E2F2EA" stroke="#0E8C5A"/>
<text x="770" y="105" text-anchor="middle">reset 该 Unit</text>
<text x="770" y="120" text-anchor="middle" font-size="9">→ pending</text>
<rect x="900" y="80" width="180" height="50" rx="6" fill="#E2F2EA" stroke="#0E8C5A"/>
<text x="990" y="105" text-anchor="middle">L1 重跑</text>
<text x="990" y="120" text-anchor="middle" font-size="9">Coder 看到新契约</text>
<g stroke="#0E0E10" fill="none" marker-end="url(#arrCT)">
<line x1="200" y1="105" x2="235" y2="105"/>
<line x1="420" y1="105" x2="455" y2="105"/>
<line x1="640" y1="105" x2="675" y2="105"/>
<line x1="860" y1="105" x2="895" y2="105"/>
</g>
</g>
</svg>
</section>
<section>
<h2>跨 Unit 引用约定</h2>
<p>auth-fe 想用 auth-be 的契约,<b>不</b>读 auth-be 的代码。流程:</p>
<pre class="code"><span class="c">// auth-fe 的 Coder Agent 在 system prompt 指引下:</span>
<span class="k">const</span> contract = JSON.<span class="n">parse</span>(fs.<span class="n">readFileSync</span>(<span class="s">'../auth-be/contract.json'</span>));
<span class="k">const</span> endpoint = contract.exports.<span class="n">http</span>.<span class="n">find</span>(e => e.<span class="n">path</span> === <span class="s">'/api/auth/login'</span>);
<span class="c">// → 写 fetch 调用基于 endpoint.responses 推导出 200 / 401 分支</span></pre>
<p>沙箱白名单允许 <code>../{otherUnit}/contract.json</code> 只读,<b>不</b>允许读 <code>../{otherUnit}/code/</code>。</p>
</section>
</article>
<nav class="prev-next"></nav>
<footer class="site"><div><b>Section</b>06 · API</div><div><b>Page</b>api-contracts.html</div></footer>
</main>
</div>
<script src="assets/shell.js"></script>
</body>
</html>