@anchorlock/cli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1985 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <script>
7
+ (function () {
8
+ try {
9
+ var t = localStorage.getItem("anchorlock-theme");
10
+ if (!t) t = matchMedia("(prefers-color-scheme: dark)").matches ? "night" : "day";
11
+ document.documentElement.setAttribute("data-theme", t);
12
+ } catch (e) {
13
+ document.documentElement.setAttribute("data-theme", "day");
14
+ }
15
+ })();
16
+ </script>
17
+ <title>AnchorLock - AI Coding 时,Agent 越多,越需要同一块锚</title>
18
+ <link rel="preload" as="font" type="font/woff2" crossorigin href="https://cdn.jsdelivr.net/fontsource/fonts/geist-sans@5.2.5/latin-400-normal.woff2" />
19
+ <link rel="preload" as="font" type="font/woff2" crossorigin href="https://cdn.jsdelivr.net/fontsource/fonts/geist-sans@5.2.5/latin-600-normal.woff2" />
20
+ <meta name="description" content="前端/客户端对照后端钉死的 OpenAPI 收紧字段名。契约来自发版说明书,不是编码 Agent 根据需求编的名单。写偏了,合并就停。" />
21
+ <style>
22
+ @font-face {
23
+ font-family: "Geist";
24
+ font-style: normal;
25
+ font-weight: 400;
26
+ font-display: optional;
27
+ src: url("https://cdn.jsdelivr.net/fontsource/fonts/geist-sans@5.2.5/latin-400-normal.woff2") format("woff2");
28
+ }
29
+ @font-face {
30
+ font-family: "Geist";
31
+ font-style: normal;
32
+ font-weight: 500;
33
+ font-display: optional;
34
+ src: url("https://cdn.jsdelivr.net/fontsource/fonts/geist-sans@5.2.5/latin-500-normal.woff2") format("woff2");
35
+ }
36
+ @font-face {
37
+ font-family: "Geist";
38
+ font-style: normal;
39
+ font-weight: 600;
40
+ font-display: optional;
41
+ src: url("https://cdn.jsdelivr.net/fontsource/fonts/geist-sans@5.2.5/latin-600-normal.woff2") format("woff2");
42
+ }
43
+ @font-face {
44
+ font-family: "Geist Mono";
45
+ font-style: normal;
46
+ font-weight: 400;
47
+ font-display: optional;
48
+ src: url("https://cdn.jsdelivr.net/fontsource/fonts/geist-mono@5.2.5/latin-400-normal.woff2") format("woff2");
49
+ }
50
+ @font-face {
51
+ font-family: "Geist Mono";
52
+ font-style: normal;
53
+ font-weight: 500;
54
+ font-display: optional;
55
+ src: url("https://cdn.jsdelivr.net/fontsource/fonts/geist-mono@5.2.5/latin-500-normal.woff2") format("woff2");
56
+ }
57
+
58
+ :root,
59
+ html[data-theme="day"] {
60
+ color-scheme: light;
61
+ --bg: #f9f8f8;
62
+ --bg-elev: rgba(255, 255, 255, 0.88);
63
+ --text: #2c3138;
64
+ --text-secondary: #6a717a;
65
+ --text-muted: #8b919a;
66
+ --accent: #4176e6;
67
+ --accent-hover: #2f62d0;
68
+ --accent-soft: rgba(65, 118, 230, 0.09);
69
+ --anchor: #1a2740;
70
+ --title: #2e343c;
71
+ --claim: #5a616b;
72
+ --border: rgba(32, 38, 48, 0.09);
73
+ --border-strong: rgba(32, 38, 48, 0.15);
74
+ --success: #4176e6;
75
+ --danger: hsl(8, 18%, 48%);
76
+ --fail-soft: rgba(122, 86, 82, 0.08);
77
+ --ink: #15181f;
78
+ --nav-bg: rgba(255, 255, 255, 0.72);
79
+ --fog: rgba(249, 248, 248, 0.5);
80
+ --flow-box: rgba(255, 255, 255, 0.8);
81
+ --th: rgba(246, 247, 249, 0.95);
82
+ --grid: rgba(32, 38, 48, 0.04);
83
+ --shadow: 0 10px 36px rgba(32, 38, 48, 0.06);
84
+ --code: #eef3fa;
85
+ --code-muted: #8a96a6;
86
+ --code-comment: #aab3c0;
87
+ --code-cmd: #c5d8ff;
88
+ --code-ok: #8fd4c0;
89
+ --code-fail: #f0b8b2;
90
+ --term-bar: #1b1f28;
91
+ --term-line: #8a93a0;
92
+ --wash-a: rgba(186, 204, 236, 0.42);
93
+ --wash-b: rgba(214, 222, 236, 0.5);
94
+ --wash-c: rgba(249, 248, 248, 0.4);
95
+ --plot-ring: rgba(21, 36, 67, 0.18);
96
+ --plot-tick: rgba(65, 118, 230, 0.55);
97
+ --plot-hex: #152443;
98
+ --plot-hold: #4176e6;
99
+ --plot-drift: rgba(21, 36, 67, 0.42);
100
+ --plot-glow: #4176e6;
101
+ --plot-bloom: 0.14;
102
+ --plot-label: rgba(21, 36, 67, 0.45);
103
+ --plot-text-drift: #4a5568;
104
+ --plot-text-hold: #2f62d0;
105
+ --plot-text-lock: #152443;
106
+ --plot-locked: hsl(162, 28%, 36%);
107
+ --plot-text-locked: hsl(162, 26%, 32%);
108
+ --radius: 8px;
109
+ --font: "Geist", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", "Microsoft YaHei", sans-serif;
110
+ --mono: "Geist Mono", "SF Mono", ui-monospace, monospace;
111
+ --max-w: 1120px;
112
+ }
113
+ html[data-theme="night"] {
114
+ color-scheme: dark;
115
+ --bg: #0c0d10;
116
+ --bg-elev: rgba(20, 22, 28, 0.78);
117
+ --text: #e6eaef;
118
+ --text-secondary: #c2c8d0;
119
+ --text-muted: #8b929c;
120
+ --accent: #6b8ff0;
121
+ --accent-hover: #8aa6f4;
122
+ --accent-soft: rgba(107, 143, 240, 0.14);
123
+ --anchor: #e8ebf0;
124
+ --title: #f3f4f7;
125
+ --claim: #c5cbd4;
126
+ --border: rgba(255, 255, 255, 0.08);
127
+ --border-strong: rgba(255, 255, 255, 0.14);
128
+ --success: #6b8ff0;
129
+ --danger: hsl(8, 26%, 76%);
130
+ --fail-soft: rgba(196, 140, 136, 0.1);
131
+ --ink: #08090c;
132
+ --nav-bg: rgba(12, 13, 16, 0.62);
133
+ --fog: rgba(12, 13, 16, 0.5);
134
+ --flow-box: rgba(28, 31, 38, 0.72);
135
+ --th: rgba(32, 35, 44, 0.9);
136
+ --grid: rgba(238, 240, 243, 0.04);
137
+ --shadow: 0 12px 40px rgba(0, 0, 0, 0.28);
138
+ --code: #f4f7fc;
139
+ --code-muted: #9aa3b0;
140
+ --code-comment: #b8c1cd;
141
+ --code-cmd: #d4e2ff;
142
+ --code-ok: #9ee0ce;
143
+ --code-fail: #f3c4be;
144
+ --term-bar: #14171e;
145
+ --term-line: #9aa3b0;
146
+ --wash-a: rgba(45, 62, 110, 0.28);
147
+ --wash-b: rgba(20, 28, 48, 0.4);
148
+ --wash-c: rgba(12, 13, 16, 0.5);
149
+ --plot-ring: rgba(210, 220, 238, 0.42);
150
+ --plot-tick: rgba(140, 175, 255, 0.78);
151
+ --plot-hex: #e8eef8;
152
+ --plot-hold: #8eb0f7;
153
+ --plot-drift: rgba(210, 220, 235, 0.78);
154
+ --plot-glow: #8eb0f7;
155
+ --plot-label: rgba(220, 228, 242, 0.82);
156
+ --plot-text-drift: #c9d1dc;
157
+ --plot-text-hold: #b4c8fa;
158
+ --plot-text-lock: #eef1f6;
159
+ --plot-locked: hsl(162, 34%, 62%);
160
+ --plot-text-locked: hsl(162, 30%, 74%);
161
+ --plot-bloom: 0.3;
162
+ }
163
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
164
+ html { scroll-behavior: smooth; -webkit-font-smoothing: antialiased; background: var(--bg); transition: background-color .28s ease, color .28s ease; }
165
+ body {
166
+ font-family: var(--font);
167
+ background: var(--bg);
168
+ color: var(--text);
169
+ line-height: 1.6;
170
+ font-size: 15px;
171
+ }
172
+ .ambient {
173
+ position: fixed; inset: 0; pointer-events: none; z-index: 0; overflow: hidden;
174
+ background:
175
+ radial-gradient(ellipse 78% 58% at 72% 8%, var(--wash-a), transparent 62%),
176
+ radial-gradient(ellipse 48% 42% at 8% 92%, var(--wash-b), transparent 55%),
177
+ var(--bg);
178
+ }
179
+ .ambient::before { display: none; }
180
+ .ambient::after { display: none; }
181
+ .scan { display: none; }
182
+ .plasma {
183
+ position: absolute; inset: -16%; width: 132%; height: 132%;
184
+ display: none;
185
+ filter: blur(72px) saturate(0.88);
186
+ transform: translateZ(0);
187
+ }
188
+ html.motion-ok .plasma.is-on { display: block; opacity: 0.78; }
189
+ .ambient.has-plasma { background: var(--bg); }
190
+ .wrap { position: relative; z-index: 1; max-width: var(--max-w); margin: 0 auto; padding: 0 28px; }
191
+
192
+ nav {
193
+ position: sticky; top: 0; z-index: 100;
194
+ backdrop-filter: blur(20px) saturate(120%);
195
+ -webkit-backdrop-filter: blur(20px) saturate(120%);
196
+ background: var(--nav-bg);
197
+ border-bottom: 1px solid var(--border);
198
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45);
199
+ }
200
+ html[data-theme="night"] nav {
201
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
202
+ border-bottom-color: rgba(255, 255, 255, 0.06);
203
+ }
204
+ .nav-inner { display: flex; align-items: center; justify-content: space-between; height: 56px; gap: 16px; }
205
+ .nav-end { display: flex; align-items: center; gap: 10px; }
206
+ .theme-switch {
207
+ display: grid; grid-template-columns: 1fr 1fr; align-items: center;
208
+ position: relative;
209
+ border: 1px solid var(--border-strong);
210
+ background: color-mix(in srgb, var(--text) 6%, var(--bg));
211
+ padding: 2px;
212
+ font-family: var(--mono);
213
+ border-radius: var(--radius);
214
+ }
215
+ .theme-switch::before {
216
+ content: "";
217
+ position: absolute;
218
+ top: 2px; bottom: 2px; left: 2px;
219
+ width: calc(50% - 2px);
220
+ background: var(--accent);
221
+ border-radius: 6px;
222
+ transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
223
+ z-index: 0;
224
+ }
225
+ html[data-theme="night"] .theme-switch::before { transform: translateX(100%); }
226
+ .theme-switch button {
227
+ appearance: none; border: 0; background: transparent;
228
+ color: var(--text-muted);
229
+ font: 500 10px/1 var(--mono);
230
+ letter-spacing: 0.1em;
231
+ padding: 6px 9px;
232
+ cursor: pointer;
233
+ position: relative;
234
+ z-index: 1;
235
+ transition: color 0.25s ease;
236
+ }
237
+ .theme-switch button[aria-pressed="true"] {
238
+ background: transparent;
239
+ color: #fff;
240
+ }
241
+ .logo {
242
+ display: flex; align-items: center; gap: 10px;
243
+ text-decoration: none; color: var(--title);
244
+ font-family: var(--mono); font-size: 13px; font-weight: 500; letter-spacing: 0.02em;
245
+ }
246
+ .logo-icon {
247
+ width: 22px; height: 22px; border-radius: 6px;
248
+ background: var(--accent); display: flex; align-items: center; justify-content: center;
249
+ transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1);
250
+ }
251
+ .logo:hover .logo-icon { transform: translateY(1px); }
252
+ .logo-icon svg { width: 12px; height: 12px; }
253
+ .nav-links { display: flex; gap: 22px; list-style: none; }
254
+ .nav-links a {
255
+ position: relative;
256
+ color: var(--text); text-decoration: none;
257
+ font-size: 13px; font-weight: 500; opacity: 0.78;
258
+ transition: opacity 0.25s ease;
259
+ }
260
+ .nav-links a::after {
261
+ content: "";
262
+ position: absolute; left: 0; right: 0; bottom: -5px;
263
+ height: 1px;
264
+ background: var(--accent);
265
+ transform: scaleX(0);
266
+ transform-origin: left;
267
+ opacity: 0.7;
268
+ transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
269
+ }
270
+ .nav-links a:hover { opacity: 1; }
271
+ .nav-links a:hover::after { transform: scaleX(1); }
272
+
273
+ .btn {
274
+ display: inline-flex; align-items: center; gap: 8px;
275
+ position: relative;
276
+ overflow: hidden;
277
+ padding: 11px 18px; border-radius: var(--radius);
278
+ font-size: 14px; font-weight: 500; text-decoration: none;
279
+ border: none; cursor: pointer; font-family: var(--font);
280
+ transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1), background 0.2s ease, border-color 0.25s ease, box-shadow 0.25s ease, color 0.2s ease;
281
+ }
282
+ .btn:active { transform: scale(0.98); }
283
+ .btn-primary {
284
+ background: var(--accent); color: #fff;
285
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
286
+ }
287
+ .btn-primary::before,
288
+ .btn-primary::after { display: none; }
289
+ .btn-primary:hover { background: var(--accent-hover); }
290
+ .btn-ghost {
291
+ background: color-mix(in srgb, var(--bg-elev) 70%, transparent);
292
+ color: var(--text);
293
+ border: 1px solid var(--border-strong);
294
+ backdrop-filter: blur(10px);
295
+ -webkit-backdrop-filter: blur(10px);
296
+ }
297
+ .btn-ghost:hover {
298
+ border-color: color-mix(in srgb, var(--accent) 42%, transparent);
299
+ color: var(--anchor);
300
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), var(--shadow);
301
+ }
302
+ html[data-theme="night"] .btn-primary {
303
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
304
+ }
305
+ html[data-theme="night"] .btn-ghost {
306
+ background: transparent;
307
+ border-color: rgba(255, 255, 255, 0.16);
308
+ }
309
+ html[data-theme="night"] .btn-ghost:hover {
310
+ background: rgba(255, 255, 255, 0.04);
311
+ box-shadow: none;
312
+ }
313
+ .btn-sm { padding: 6px 12px; font-size: 12px; }
314
+
315
+ .hero {
316
+ position: relative;
317
+ min-height: calc(100dvh - 56px);
318
+ display: flex; align-items: center;
319
+ padding: 24px 0 56px;
320
+ overflow: hidden;
321
+ }
322
+ .hero .wrap { max-width: 1180px; }
323
+ .hero-grid {
324
+ display: grid;
325
+ grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
326
+ gap: 40px 56px;
327
+ align-items: center;
328
+ width: 100%;
329
+ }
330
+ .hero h1 {
331
+ font-size: clamp(3.35rem, 6.4vw, 5.2rem);
332
+ font-weight: 600;
333
+ letter-spacing: -0.045em;
334
+ line-height: 1;
335
+ margin: 0 0 22px;
336
+ color: var(--title);
337
+ }
338
+ .hero-stack {
339
+ display: inline-block;
340
+ max-width: 100%;
341
+ vertical-align: top;
342
+ }
343
+ .hero-claim {
344
+ font-size: clamp(1.18rem, 2.05vw, 1.48rem);
345
+ font-weight: 600;
346
+ letter-spacing: -0.022em;
347
+ line-height: 1.4;
348
+ color: var(--claim);
349
+ margin: 0 0 28px;
350
+ max-width: none;
351
+ white-space: nowrap;
352
+ }
353
+ .hero-lead {
354
+ width: 0;
355
+ min-width: 100%;
356
+ margin-bottom: 40px;
357
+ font-size: 15px; color: var(--text-secondary); line-height: 1.8;
358
+ }
359
+ .hero-lead strong { color: var(--anchor); font-weight: 600; }
360
+ .hero-actions { display: flex; gap: 12px; flex-wrap: wrap; }
361
+ .hero-visual {
362
+ position: relative;
363
+ margin: 0;
364
+ width: 100%;
365
+ max-width: none;
366
+ aspect-ratio: 6 / 5;
367
+ background: none;
368
+ border: none;
369
+ overflow: visible;
370
+ pointer-events: none;
371
+ }
372
+ .hero-visual svg { display: block; width: 100%; height: 100%; }
373
+ .kicker {
374
+ font-family: var(--mono); font-size: 11px; font-weight: 500;
375
+ letter-spacing: 0.12em; color: var(--text-muted); margin-bottom: 18px;
376
+ }
377
+ .hero-visual text {
378
+ font-size: 11px;
379
+ font-weight: 500;
380
+ }
381
+ .glow-drift { filter: none; }
382
+ .glow-hold { filter: drop-shadow(0 0 5px color-mix(in srgb, var(--plot-glow) 40%, transparent)); }
383
+ .glow-lock { filter: drop-shadow(0 0 7px color-mix(in srgb, var(--plot-glow) 32%, transparent)); }
384
+ html[data-theme="day"] .glow-drift,
385
+ html[data-theme="day"] .glow-hold,
386
+ html[data-theme="day"] .glow-lock { filter: none; }
387
+ html[data-theme="day"] .halo-pulse { opacity: 0.4; }
388
+ html[data-theme="day"] .scan-beam { opacity: 0.4; }
389
+ html[data-theme="day"] .sonar-ring { stroke-opacity: 0.55; }
390
+ .halo-pulse {
391
+ transform-box: fill-box;
392
+ transform-origin: center;
393
+ animation: halo 2.2s ease-out infinite;
394
+ }
395
+ .scan-beam { animation: scan 5.2s linear infinite; }
396
+ .sonar-ring {
397
+ transform-box: view-box;
398
+ transform-origin: 240px 160px;
399
+ fill: none;
400
+ stroke: var(--plot-hold);
401
+ stroke-width: 0.65;
402
+ animation: sonar 4.4s cubic-bezier(0.12, 0.6, 0.28, 1) infinite;
403
+ }
404
+ .sonar-b { animation-delay: 1.45s; stroke: var(--plot-glow); }
405
+ .sonar-c { animation-delay: 2.9s; }
406
+ html[data-theme="day"] .hero-visual text { filter: none; }
407
+ html[data-theme="night"] .hero-visual text { filter: none; }
408
+ html[data-theme="night"] .sonar-ring { stroke-opacity: 0.9; }
409
+ html[data-theme="night"] .halo-pulse { opacity: 0.72; }
410
+ html[data-theme="night"] .hero-visual .ring-spin { opacity: 0.42; }
411
+ html[data-theme="night"] .hero-visual .ring-spin-rev { opacity: 0.3; }
412
+ html[data-theme="night"] .hero-visual .sweep line { opacity: 0.72; }
413
+ html[data-theme="night"] .hero-visual .sweep path { opacity: 0.9; }
414
+ html[data-theme="night"] .hero-visual .graticule line { opacity: 0.32; }
415
+ html[data-theme="night"] .hero-visual .graticule > circle { opacity: 0.55; }
416
+ html[data-theme="night"] .hero-visual .graticule g { opacity: 0.85; }
417
+ html[data-theme="night"] .glow-hold {
418
+ filter: drop-shadow(0 0 6px color-mix(in srgb, var(--plot-glow) 55%, transparent));
419
+ }
420
+ html[data-theme="night"] .glow-lock {
421
+ filter: drop-shadow(0 0 9px color-mix(in srgb, var(--plot-glow) 48%, transparent));
422
+ }
423
+
424
+ .ring-spin, .ring-spin-rev {
425
+ transform-box: fill-box;
426
+ transform-origin: center;
427
+ }
428
+ .ring-spin { animation: spin 48s linear infinite; }
429
+ .ring-spin-rev { animation: spin 36s linear infinite reverse; }
430
+ .sweep {
431
+ transform-box: view-box;
432
+ transform-origin: 240px 160px;
433
+ }
434
+ .hero-visual, .cast-photo { --beat: 16s; }
435
+ html.motion-ok .drift-a,
436
+ html.motion-ok .drift-b,
437
+ html.motion-ok .drift-c,
438
+ html.motion-ok .drift-d {
439
+ animation: none;
440
+ }
441
+ .anchor-drop {
442
+ animation: drop-anchor var(--beat) cubic-bezier(0.22, 1, 0.36, 1) infinite;
443
+ }
444
+ html.motion-ok .anchor-drop {
445
+ animation: none;
446
+ opacity: 0;
447
+ }
448
+ .tethers { stroke-dasharray: 2 10; }
449
+ .tethers.is-taut { stroke-dasharray: none; }
450
+ .lock-meta {
451
+ animation: lock-meta var(--beat) cubic-bezier(0.22, 1, 0.36, 1) infinite;
452
+ }
453
+ .chain-hold, .chain-name {
454
+ animation: chain-a var(--beat) cubic-bezier(0.22, 1, 0.36, 1) infinite;
455
+ }
456
+ .chain-draw-1, .chain-draw-2 {
457
+ stroke-dasharray: 44;
458
+ }
459
+ .chain-draw-1 { animation: chain-draw-a var(--beat) cubic-bezier(0.22, 1, 0.36, 1) infinite; }
460
+ .chain-draw-2 { animation: chain-draw-b var(--beat) cubic-bezier(0.22, 1, 0.36, 1) infinite; }
461
+ .chain-locked {
462
+ animation: chain-b var(--beat) cubic-bezier(0.22, 1, 0.36, 1) infinite;
463
+ }
464
+ .bind-drop {
465
+ animation: drop-anchor var(--beat) cubic-bezier(0.22, 1, 0.36, 1) infinite;
466
+ transform-box: fill-box;
467
+ transform-origin: 180px 118px;
468
+ }
469
+ .cast-line {
470
+ stroke-dasharray: 50;
471
+ animation: cast-draw var(--beat) cubic-bezier(0.22, 1, 0.36, 1) infinite;
472
+ }
473
+ .cast-check {
474
+ animation: taut-line var(--beat) cubic-bezier(0.22, 1, 0.36, 1) infinite;
475
+ }
476
+ .node-pulse { animation: pulse 3.2s ease-in-out infinite; }
477
+ .sweep { animation: spin 24s linear infinite; }
478
+
479
+ @keyframes spin { to { transform: rotate(360deg); } }
480
+ @keyframes sonar {
481
+ 0% { transform: scale(0.28); opacity: 0.42; }
482
+ 55% { opacity: 0.14; }
483
+ 100% { transform: scale(1.92); opacity: 0; }
484
+ }
485
+ @keyframes dash { to { stroke-dashoffset: -48; } }
486
+ @keyframes pulse {
487
+ 0%, 100% { opacity: 0.55; }
488
+ 50% { opacity: 1; }
489
+ }
490
+ @keyframes halo {
491
+ 0% { transform: scale(1); opacity: 0.55; }
492
+ 100% { transform: scale(2.9); opacity: 0; }
493
+ }
494
+ @keyframes scan {
495
+ 0% { transform: translateY(-30%); }
496
+ 100% { transform: translateY(380%); }
497
+ }
498
+ @keyframes drift {
499
+ 0%, 100% { transform: translate(0, 0); }
500
+ 25% { transform: translate(5px, -7px); }
501
+ 50% { transform: translate(1px, -2px); }
502
+ 75% { transform: translate(-4px, 4px); }
503
+ }
504
+ @keyframes drop-anchor {
505
+ 0%, 14% { transform: translateY(-38px); opacity: 0; }
506
+ 24% { transform: translateY(3px); opacity: 1; }
507
+ 30%, 80% { transform: translateY(0); opacity: 1; }
508
+ 90%, 100% { transform: translateY(-28px); opacity: 0; }
509
+ }
510
+ @keyframes slack-line {
511
+ 0%, 22% { opacity: 0.55; stroke-dasharray: 2 11; }
512
+ 38%, 78% { opacity: 0.08; stroke-dasharray: 2 11; }
513
+ 90%, 100% { opacity: 0.55; stroke-dasharray: 2 11; }
514
+ }
515
+ @keyframes taut-line {
516
+ 0%, 28% { opacity: 0; }
517
+ 42%, 78% { opacity: 0.88; }
518
+ 90%, 100% { opacity: 0; }
519
+ }
520
+ @keyframes lock-meta {
521
+ 0%, 20% { opacity: 0; }
522
+ 32%, 80% { opacity: 0.72; }
523
+ 90%, 100% { opacity: 0; }
524
+ }
525
+ @keyframes chain-a {
526
+ 0%, 44% { opacity: 0.18; }
527
+ 54%, 80% { opacity: 1; }
528
+ 90%, 100% { opacity: 0.18; }
529
+ }
530
+ @keyframes chain-b {
531
+ 0%, 56% { opacity: 0; }
532
+ 66%, 80% { opacity: 1; }
533
+ 90%, 100% { opacity: 0; }
534
+ }
535
+ @keyframes chain-draw-a {
536
+ 0%, 46% { stroke-dashoffset: 44; opacity: 0.2; }
537
+ 56%, 80% { stroke-dashoffset: 0; opacity: 1; }
538
+ 90%, 100% { stroke-dashoffset: 44; opacity: 0.2; }
539
+ }
540
+ @keyframes chain-draw-b {
541
+ 0%, 56% { stroke-dashoffset: 44; opacity: 0.2; }
542
+ 66%, 80% { stroke-dashoffset: 0; opacity: 1; }
543
+ 90%, 100% { stroke-dashoffset: 44; opacity: 0.2; }
544
+ }
545
+ @keyframes cast-draw {
546
+ 0%, 22% { stroke-dashoffset: 50; opacity: 0.2; }
547
+ 40%, 78% { stroke-dashoffset: 0; opacity: 0.9; }
548
+ 90%, 100% { stroke-dashoffset: 50; opacity: 0.2; }
549
+ }
550
+ @keyframes drift-cycle {
551
+ 0% { transform: translate(0, 0); }
552
+ 8% { transform: translate(5px, -7px); }
553
+ 16% { transform: translate(-3px, 4px); }
554
+ 24% { transform: translate(4px, -3px); }
555
+ 36%, 80% { transform: translate(0, 0); }
556
+ 90% { transform: translate(2px, -2px); }
557
+ 100% { transform: translate(0, 0); }
558
+ }
559
+ @keyframes sheen {
560
+ to { transform: translateX(460%) skewX(-18deg); }
561
+ }
562
+
563
+ .spec-strip {
564
+ display: grid; grid-template-columns: repeat(3, 1fr);
565
+ border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
566
+ background:
567
+ linear-gradient(90deg, var(--accent-soft), transparent 42%),
568
+ var(--bg-elev);
569
+ }
570
+ .spec-strip article { padding: 18px 22px; border-right: 1px solid var(--border); }
571
+ .spec-strip article:last-child { border-right: none; }
572
+ .spec-k { font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; color: var(--text-muted); margin-bottom: 6px; }
573
+ .spec-v { font-size: 15px; font-weight: 600; color: var(--anchor); letter-spacing: -0.02em; }
574
+ .spec-v.danger { color: var(--danger); }
575
+ .spec-v.ok { color: var(--accent); }
576
+
577
+ section { padding: 80px 0; }
578
+ .section-title {
579
+ font-size: clamp(1.45rem, 2.4vw, 1.85rem);
580
+ font-weight: 500; letter-spacing: -0.032em;
581
+ line-height: 1.25; margin-bottom: 12px; color: var(--anchor);
582
+ }
583
+ .section-desc { color: var(--text-secondary); max-width: 36rem; margin-bottom: 32px; font-size: 15px; }
584
+ .section-desc code, .compare-card code, .compose-card code, .path-note code, .who-card code, .start-callout code, .hero-lead code {
585
+ font-family: var(--mono); font-size: 12px; color: var(--accent);
586
+ }
587
+
588
+ .ledger {
589
+ position: relative;
590
+ border: 1px solid var(--border-strong);
591
+ background: var(--bg-elev);
592
+ backdrop-filter: blur(18px);
593
+ -webkit-backdrop-filter: blur(18px);
594
+ border-radius: var(--radius);
595
+ overflow: hidden;
596
+ box-shadow: var(--shadow);
597
+ }
598
+ .ledger::after,
599
+ .compare::after,
600
+ .path-pick::after,
601
+ .terminal::after,
602
+ .who-card::after {
603
+ content: "";
604
+ position: absolute; inset: 0;
605
+ border-radius: inherit;
606
+ pointer-events: none;
607
+ background: radial-gradient(240px circle at var(--mx, 50%) var(--my, 0%), color-mix(in srgb, var(--accent) 12%, transparent), transparent 58%);
608
+ opacity: 0;
609
+ transition: opacity 0.45s ease;
610
+ z-index: 1;
611
+ }
612
+ .ledger:hover::after,
613
+ .compare:hover::after,
614
+ .path-pick:hover::after,
615
+ .terminal:hover::after,
616
+ .who-card:hover::after { opacity: 1; }
617
+ .ledger-head {
618
+ display: flex; justify-content: space-between; align-items: baseline;
619
+ padding: 14px 18px; border-bottom: 1px solid var(--border);
620
+ font-family: var(--mono); font-size: 11px;
621
+ color: color-mix(in srgb, var(--text) 55%, var(--text-muted));
622
+ letter-spacing: 0.04em;
623
+ }
624
+ .ledger-row {
625
+ display: grid; grid-template-columns: 140px 1fr 1fr;
626
+ gap: 16px; padding: 16px 18px;
627
+ border-bottom: 1px solid var(--border);
628
+ }
629
+ .ledger-row:last-child { border-bottom: none; }
630
+ .ledger-who { font-family: var(--mono); font-size: 11px; font-weight: 500; color: var(--accent); letter-spacing: 0.04em; }
631
+ .ledger-row p { font-size: 13px; color: color-mix(in srgb, var(--text) 50%, var(--text-secondary)); margin: 0; }
632
+ .ledger-row code { font-family: var(--mono); font-size: 12px; color: var(--danger); }
633
+ .split-code {
634
+ display: grid; grid-template-columns: 1fr 1fr;
635
+ border-top: 1px solid var(--border);
636
+ }
637
+ .hero-panel { min-width: 0; }
638
+ .hero-panel + .hero-panel { border-left: 1px solid var(--border); }
639
+ .hero-panel-head {
640
+ display: flex; justify-content: space-between;
641
+ padding: 10px 16px; font-size: 12px; font-weight: 600;
642
+ border-bottom: 1px solid var(--border);
643
+ }
644
+ .hero-panel.bad .hero-panel-head { color: var(--danger); background: var(--fail-soft); }
645
+ .hero-panel.good .hero-panel-head { color: var(--accent); background: var(--accent-soft); }
646
+ .hero-panel-head .file { font-family: var(--mono); font-weight: 400; font-size: 11px; color: color-mix(in srgb, var(--text) 45%, var(--text-muted)); }
647
+ .hero-panel pre {
648
+ font-family: var(--mono); font-size: 13px; line-height: 1.75;
649
+ padding: 16px 18px; margin: 0; color: var(--code); background: var(--ink);
650
+ }
651
+ .hero-panel pre .x { color: var(--danger); font-weight: 500; }
652
+ .hero-panel pre .ok { color: var(--success); font-weight: 500; }
653
+ .hero-panel pre .c { color: var(--code-comment); }
654
+ .hero-one-liner { margin-top: 16px; font-size: 14px; color: var(--text-secondary); }
655
+ .hero-one-liner strong { color: var(--anchor); }
656
+
657
+ .compare { display: grid; grid-template-columns: 1fr 1fr; position: relative; border: 1px solid var(--border-strong); border-radius: var(--radius); overflow: hidden; background: var(--bg-elev); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); box-shadow: var(--shadow); }
658
+ .compare-card { padding: 28px 26px; }
659
+ .compare-card + .compare-card { border-left: 1px solid var(--border); }
660
+ .compare-card.highlight { background: var(--accent-soft); }
661
+ .compare-tag {
662
+ display: inline-block; font-family: var(--mono); font-size: 10px; font-weight: 500;
663
+ letter-spacing: 0.06em; padding: 2px 0; margin-bottom: 12px;
664
+ }
665
+ .compare-tag.ok { color: var(--accent); }
666
+ .compare-tag.bad { color: var(--text-secondary); }
667
+ .compare-tag code { font-family: var(--mono); font-size: 10px; color: inherit; }
668
+ .compare-card h3 { font-size: 16px; font-weight: 600; margin-bottom: 10px; letter-spacing: -0.02em; color: var(--anchor); }
669
+ .compare-card p { font-size: 14px; color: var(--text-secondary); }
670
+
671
+ .compose-list { display: grid; gap: 0; border-top: 1px solid var(--border); }
672
+ .compose-card {
673
+ display: grid; grid-template-columns: 56px 1fr;
674
+ gap: 18px; padding: 22px 0;
675
+ border-bottom: 1px solid var(--border);
676
+ background: transparent; box-shadow: none;
677
+ }
678
+ .compose-num { font-family: var(--mono); font-size: 13px; font-weight: 500; color: var(--accent); padding-top: 2px; }
679
+ .compose-card h3 { font-size: 16px; font-weight: 600; color: var(--anchor); margin-bottom: 6px; letter-spacing: -0.02em; }
680
+ .compose-card p { font-size: 14px; color: var(--text-secondary); line-height: 1.6; }
681
+ .second-round-title { margin: 36px 0 8px; font-size: 18px; font-weight: 600; color: var(--anchor); }
682
+
683
+ .code-block {
684
+ background: var(--ink); border-radius: var(--radius);
685
+ padding: 22px 24px; overflow-x: auto;
686
+ border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
687
+ box-shadow: var(--shadow);
688
+ }
689
+ .code-block pre { font-family: var(--mono); font-size: 13.5px; line-height: 1.8; color: var(--code); font-weight: 500; }
690
+ .code-block .comment { color: var(--code-comment); font-weight: 400; }
691
+ .code-block .cmd { color: var(--code-cmd); font-weight: 600; }
692
+ .code-block .out { color: var(--code-ok); font-weight: 600; }
693
+ .code-block .err { color: var(--code-fail); font-weight: 600; }
694
+
695
+ .terminal {
696
+ position: relative;
697
+ background: var(--ink); border: 1px solid color-mix(in srgb, var(--accent) 18%, transparent);
698
+ border-radius: 8px; overflow: hidden;
699
+ box-shadow: var(--shadow);
700
+ }
701
+ .terminal-bar {
702
+ display: flex; align-items: center; gap: 7px;
703
+ padding: 10px 14px; background: var(--term-bar); border-bottom: 1px solid color-mix(in srgb, var(--code) 12%, transparent);
704
+ }
705
+ .terminal-dot { width: 10px; height: 10px; border-radius: 50%; }
706
+ .terminal-dot.r { background: #b88986; }
707
+ .terminal-dot.y { background: #c2ab78; }
708
+ .terminal-dot.g { background: #7a9a86; }
709
+ .terminal-title { flex: 1; text-align: center; font-size: 11px; color: var(--term-line); font-family: var(--mono); }
710
+ .terminal-body {
711
+ position: relative;
712
+ padding: 20px 22px 20px 52px; font-family: var(--mono);
713
+ font-size: 13.5px; line-height: 1.85; color: var(--code);
714
+ counter-reset: tline;
715
+ }
716
+ .terminal-body > div {
717
+ position: relative;
718
+ counter-increment: tline;
719
+ }
720
+ .terminal-body > div::before {
721
+ content: counter(tline);
722
+ position: absolute; left: -36px; width: 24px; text-align: right;
723
+ color: var(--term-line); font-size: 11px;
724
+ }
725
+ .terminal-body .violation { color: var(--code-fail); font-weight: 600; }
726
+ .terminal-body .pass { color: var(--code-ok); font-weight: 600; }
727
+ .terminal-body .dim { color: var(--code-comment); font-weight: 400; }
728
+ .caret {
729
+ display: inline-block;
730
+ width: 8px; height: 1.05em;
731
+ margin-left: 3px;
732
+ background: var(--accent);
733
+ box-shadow: 0 0 12px color-mix(in srgb, var(--accent) 50%, transparent);
734
+ vertical-align: -2px;
735
+ animation: caret 1.05s steps(1) infinite;
736
+ }
737
+ @keyframes caret {
738
+ 0%, 49% { opacity: 1; }
739
+ 50%, 100% { opacity: 0; }
740
+ }
741
+
742
+ .cast-grid {
743
+ display: grid; grid-template-columns: 0.9fr 1.1fr; gap: 28px; align-items: stretch;
744
+ }
745
+ .cast-photo {
746
+ margin: 0;
747
+ min-height: 280px;
748
+ background: none;
749
+ border: none;
750
+ overflow: visible;
751
+ mask-image: radial-gradient(ellipse 70% 65% at 50% 50%, #000 32%, transparent 78%);
752
+ -webkit-mask-image: radial-gradient(ellipse 70% 65% at 50% 50%, #000 32%, transparent 78%);
753
+ }
754
+ .cast-photo svg { display: block; width: 100%; height: 100%; min-height: 280px; }
755
+ .cast-stack { display: grid; gap: 0; border-top: 1px solid var(--border); }
756
+ .cast-item { padding: 22px 0; border-bottom: 1px solid var(--border); }
757
+ .cast-item h3 { font-size: 16px; font-weight: 600; margin-bottom: 8px; color: var(--anchor); }
758
+ .cast-item p { font-size: 14px; color: var(--text-secondary); }
759
+ .cast-item .compare-tag { margin-bottom: 8px; }
760
+
761
+ .mini-table { width: 100%; border-collapse: collapse; font-size: 13px; background: var(--bg-elev); backdrop-filter: blur(18px); border: 1px solid var(--border-strong); border-radius: var(--radius); overflow: hidden; }
762
+ .mini-table th, .mini-table td { padding: 12px 16px; text-align: left; border-bottom: 1px solid var(--border); vertical-align: top; }
763
+ .mini-table th { background: var(--th); font-weight: 600; width: 28%; }
764
+ .mini-table td { color: var(--text-secondary); }
765
+ .mini-table code { font-family: var(--mono); font-size: 11px; color: var(--accent); }
766
+ .start-callout {
767
+ margin-bottom: 28px; padding: 14px 16px;
768
+ background: var(--bg-elev); border: 1px solid var(--border-strong);
769
+ border-left: 2px solid var(--accent); border-radius: var(--radius);
770
+ font-size: 13px; color: var(--text-secondary);
771
+ }
772
+ .path-fork { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 40px; }
773
+ .path-fork .path-pick.span { grid-column: 1 / -1; }
774
+ .path-pick {
775
+ display: block; text-decoration: none; color: inherit;
776
+ position: relative; overflow: hidden;
777
+ background: var(--bg-elev); backdrop-filter: blur(18px);
778
+ border: 1px solid var(--border-strong);
779
+ border-radius: var(--radius); padding: 22px 20px;
780
+ transition: border-color 0.28s ease, transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.28s ease;
781
+ }
782
+ .path-pick:hover {
783
+ border-color: color-mix(in srgb, var(--accent) 40%, transparent);
784
+ transform: translateY(-1px);
785
+ box-shadow: var(--shadow);
786
+ }
787
+ .path-pick.rec { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 32%, transparent); }
788
+ .path-pick-kicker { font-family: var(--mono); font-size: 11px; font-weight: 500; color: var(--accent); margin-bottom: 8px; }
789
+ .path-pick h3 { font-size: 17px; font-weight: 600; color: var(--anchor); margin-bottom: 8px; letter-spacing: -0.02em; }
790
+ .path-pick p { font-size: 13px; color: var(--text-secondary); line-height: 1.6; margin: 0 0 14px; }
791
+ .path-pick-go { font-size: 13px; font-weight: 600; color: var(--accent); }
792
+ main > section, .path-how { scroll-margin-top: 80px; }
793
+ .path-how-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
794
+ .path-how-head h3 { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.02em; color: var(--anchor); }
795
+ .path-mark { font-family: var(--mono); font-size: 12px; color: var(--accent); }
796
+ .path-how > .section-desc { margin-bottom: 20px; }
797
+ .who-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; }
798
+ .who-card { position: relative; overflow: hidden; background: var(--bg-elev); backdrop-filter: blur(18px); border: 1px solid var(--border-strong); border-radius: var(--radius); padding: 16px 18px; }
799
+ .who-card h4 { font-size: 13px; font-weight: 600; color: var(--anchor); margin-bottom: 8px; }
800
+ .who-card ol { margin: 0; padding-left: 18px; font-size: 13px; color: var(--text-secondary); line-height: 1.65; }
801
+ .who-card li { margin-bottom: 6px; }
802
+ .who-card li:last-child { margin-bottom: 0; }
803
+ .path-note { font-size: 13px; color: var(--text-secondary); margin: 14px 0 0; }
804
+ .flow { background: var(--bg-elev); backdrop-filter: blur(18px); border: 1px solid var(--border-strong); border-radius: var(--radius); padding: 22px 20px 18px; margin: 0 0 20px; }
805
+ .flow-caption { font-size: 13px; color: var(--text-secondary); margin: 16px 0 0; line-height: 1.6; }
806
+ .flow-caption strong { color: var(--anchor); }
807
+ .flow-caption code { font-family: var(--mono); font-size: 12px; color: var(--anchor); }
808
+ .flow-board { display: grid; grid-template-columns: 1fr auto 1fr; gap: 10px 14px; align-items: stretch; }
809
+ .flow-lane { font-family: var(--mono); font-size: 11px; font-weight: 500; color: var(--accent); margin-bottom: 10px; }
810
+ .flow-box { background: var(--flow-box); border: 1px solid var(--border-strong); border-radius: 4px; padding: 10px 12px; font-size: 13px; color: var(--text); line-height: 1.45; }
811
+ .flow-box small { display: block; font-size: 11px; color: var(--text-muted); margin-top: 4px; font-family: var(--mono); }
812
+ .flow-box.file { border-color: color-mix(in srgb, var(--accent) 35%, transparent); background: var(--accent-soft); font-weight: 600; color: var(--anchor); }
813
+ .flow-box.ok { border-color: color-mix(in srgb, var(--plot-locked) 40%, transparent); background: color-mix(in srgb, var(--plot-locked) 10%, transparent); }
814
+ .flow-box.warn { border-color: color-mix(in srgb, var(--danger) 35%, transparent); background: var(--fail-soft); color: var(--text-secondary); }
815
+ .flow-stack { display: flex; flex-direction: column; gap: 8px; }
816
+ .flow-join { display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 88px; color: var(--text-muted); font-size: 11px; font-weight: 600; text-align: center; gap: 6px; }
817
+ .flow-join span { line-height: 1.35; }
818
+ .flow-join::before, .flow-join::after { content: ""; width: 1px; flex: 1; background: color-mix(in srgb, var(--accent) 40%, transparent); min-height: 12px; }
819
+ .flow-arrow { text-align: center; color: var(--accent); font-size: 14px; line-height: 1; font-weight: 600; }
820
+ .flow-split { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
821
+ .fork-board { display: grid; grid-template-columns: 1fr auto 1fr; gap: 12px; align-items: start; margin-bottom: 28px; }
822
+ .fork-q { grid-column: 1 / -1; text-align: center; }
823
+ .fork-q .flow-box { display: inline-block; padding: 12px 22px; font-size: 15px; font-weight: 600; }
824
+ .fork-arm { text-align: center; }
825
+ .fork-arm .flow-arrow { color: var(--text-muted); font-size: 13px; font-weight: 600; margin-bottom: 8px; }
826
+ .fork-mid { width: 1px; }
827
+
828
+ footer { border-top: 1px solid var(--border); padding: 28px 0; }
829
+ .footer-inner { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 16px; }
830
+ .footer-copy { font-family: var(--mono); font-size: 12px; color: var(--text-muted); }
831
+ .footer-links { display: flex; gap: 20px; list-style: none; }
832
+ .footer-links a { font-size: 13px; color: var(--text-secondary); text-decoration: none; }
833
+ .footer-links a:hover { color: var(--accent); }
834
+
835
+ @keyframes rise {
836
+ from { opacity: 0; transform: translateY(12px); }
837
+ to { opacity: 1; transform: none; }
838
+ }
839
+ .hero-copy { animation: rise 0.8s cubic-bezier(0.22, 1, 0.36, 1) both; }
840
+ .hero-visual { animation: rise 1s 0.1s cubic-bezier(0.22, 1, 0.36, 1) both; }
841
+ html.motion-ok main section:not(.hero) .wrap {
842
+ animation: rise 0.75s cubic-bezier(0.22, 1, 0.36, 1) both;
843
+ }
844
+
845
+ @media (prefers-reduced-motion: reduce) {
846
+ .hero-copy, .hero-visual,
847
+ .ring-spin, .ring-spin-rev, .sweep, .tether, .node-pulse, .halo-pulse, .scan-beam, .sonar-ring,
848
+ .drift-a, .drift-b, .drift-c, .drift-d, .caret, .btn-primary:hover::before,
849
+ .anchor-drop, .tether-slack, .tether-taut, .lock-meta, .chain-hold, .chain-name, .chain-locked,
850
+ .chain-draw-1, .chain-draw-2, .bind-drop, .cast-line, .cast-check { animation: none; }
851
+ .anchor-drop, .bind-drop, .tether-taut, .cast-check, .lock-meta, .chain-hold, .chain-name, .chain-locked { opacity: 1; transform: none; }
852
+ .tether-slack { opacity: 0.12; }
853
+ .chain-draw-1, .chain-draw-2, .cast-line { stroke-dashoffset: 0; opacity: 1; }
854
+ .theme-switch::before, .nav-links a::after, .btn, .path-pick, .logo-icon { transition: none; }
855
+ main section:not(.hero) .wrap { opacity: 1; transform: none; transition: none; }
856
+ .plasma { display: none !important; }
857
+ html { scroll-behavior: auto; }
858
+ }
859
+ @media (prefers-reduced-transparency: reduce) {
860
+ nav { background: var(--nav-bg); backdrop-filter: none; }
861
+ }
862
+ @media (max-width: 768px) {
863
+ .nav-links { display: none; }
864
+ .hero-grid, .split-code, .compare, .cast-grid, .path-fork, .who-grid, .spec-strip,
865
+ .flow-board, .fork-board, .flow-split, .ledger-row { grid-template-columns: 1fr; }
866
+ .hero { min-height: auto; padding: 28px 0 36px; }
867
+ .hero-claim { white-space: normal; }
868
+ .hero-stack { display: block; }
869
+ .hero-lead { width: auto; min-width: 0; }
870
+ .hero-visual { width: 100%; aspect-ratio: 6 / 5; }
871
+ .compare-card + .compare-card, .hero-panel + .hero-panel { border-left: none; border-top: 1px solid var(--border); }
872
+ .spec-strip article { border-right: none; border-bottom: 1px solid var(--border); }
873
+ .flow-join { flex-direction: row; min-height: 0; min-width: 0; padding: 4px 0; }
874
+ .flow-join::before, .flow-join::after { width: 24px; height: 1px; flex: 1; min-height: 0; }
875
+ .fork-mid { display: none; }
876
+ }
877
+ </style>
878
+ </head>
879
+ <body>
880
+ <div class="ambient" aria-hidden="true">
881
+ <canvas class="plasma"></canvas>
882
+ </div>
883
+
884
+ <nav>
885
+ <div class="wrap nav-inner">
886
+ <a class="logo" href="#">
887
+ <span class="logo-icon">
888
+ <svg viewBox="0 0 16 16" fill="none"><circle cx="8" cy="4" r="2" fill="white"/><path d="M8 6V14M5 11L8 14L11 11" stroke="white" stroke-width="1.5" stroke-linecap="round"/></svg>
889
+ </span>
890
+ AnchorLock
891
+ </a>
892
+ <ul class="nav-links">
893
+ <li><a href="#why">为什么需要</a></li>
894
+ <li><a href="#demo">看红变绿</a></li>
895
+ <li><a href="#cast">怎么卡住</a></li>
896
+ <li><a href="#install">安装</a></li>
897
+ <li><a href="#start">使用</a></li>
898
+ <li><a href="#agents">多 Agent</a></li>
899
+ <li><a href="docs/">文档</a></li>
900
+ </ul>
901
+ <div class="nav-end">
902
+ <div class="theme-switch" role="group" aria-label="显示模式">
903
+ <button type="button" data-theme-set="day" aria-pressed="true">DAY</button>
904
+ <button type="button" data-theme-set="night" aria-pressed="false">NIGHT</button>
905
+ </div>
906
+ <a class="btn btn-primary btn-sm" href="#demo">看红变绿</a>
907
+ </div>
908
+ </div>
909
+ </nav>
910
+
911
+ <main>
912
+ <section class="hero" id="see">
913
+ <div class="wrap hero-grid">
914
+ <div class="hero-copy">
915
+ <h1>AnchorLock</h1>
916
+ <div class="hero-stack">
917
+ <p class="hero-claim">AI Coding 时,Agent 越多,越需要同一块锚</p>
918
+ <p class="hero-lead">
919
+ 前端、客户端、测试可以分头推进,但字段名必须对齐后端钉死的那份 OpenAPI。
920
+ AnchorLock 把名字钉死:名单是 <code>order_no</code>,写成 <code>orderId</code>,<strong>合并就停</strong>。
921
+ </p>
922
+ </div>
923
+ <div class="hero-actions">
924
+ <a class="btn btn-primary" href="#demo">看红变绿</a>
925
+ <a class="btn btn-ghost" href="#install">安装到我的仓</a>
926
+ </div>
927
+ </div>
928
+ <figure class="hero-visual" aria-label="产品、前端、后端、测试四个 Agent 系在同一锁定点">
929
+ <svg viewBox="0 0 480 400" role="img" shape-rendering="geometricPrecision">
930
+ <title>数字化锚:PM / FE / BE / QA 锁定到 order_no</title>
931
+ <defs>
932
+ <radialGradient id="glow" cx="50%" cy="50%" r="50%">
933
+ <stop offset="0%" stop-color="var(--plot-glow)" stop-opacity="var(--plot-bloom)"/>
934
+ <stop offset="100%" stop-color="var(--plot-glow)" stop-opacity="0"/>
935
+ </radialGradient>
936
+ <linearGradient id="sweep-fade" x1="240" y1="160" x2="310" y2="28" gradientUnits="userSpaceOnUse">
937
+ <stop offset="0%" stop-color="var(--plot-glow)" stop-opacity="0"/>
938
+ <stop offset="100%" stop-color="var(--plot-glow)" stop-opacity="0.42"/>
939
+ </linearGradient>
940
+ <marker id="chain-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto">
941
+ <path d="M0 0L10 5L0 10Z" fill="var(--plot-glow)"/>
942
+ </marker>
943
+ <marker id="lock-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto">
944
+ <path d="M0 0L10 5L0 10Z" fill="var(--plot-locked)"/>
945
+ </marker>
946
+ </defs>
947
+ <circle cx="240" cy="160" r="96" fill="url(#glow)"/>
948
+ <g class="graticule" fill="none" stroke="var(--plot-ring)">
949
+ <line x1="66" y1="160" x2="414" y2="160" stroke-width="0.4" opacity="0.13"/>
950
+ <line x1="240" y1="14" x2="240" y2="306" stroke-width="0.4" opacity="0.13"/>
951
+ <circle cx="240" cy="160" r="70" stroke-width="0.45" opacity="0.3"/>
952
+ <circle class="ring-spin" cx="240" cy="160" r="110" stroke-width="0.55" opacity="0.18" stroke-dasharray="1.2 27.6"/>
953
+ <circle class="ring-spin-rev" cx="240" cy="160" r="146" stroke-width="0.35" opacity="0.12" stroke-dasharray="1 37.2"/>
954
+ <g fill="var(--plot-label)" stroke="none" font-family="ui-monospace, monospace" font-size="7" letter-spacing="0.12em" opacity="0.55">
955
+ <text x="240" y="8" text-anchor="middle">000</text>
956
+ <text x="398" y="163">090</text>
957
+ <text x="240" y="316" text-anchor="middle">180</text>
958
+ <text x="66" y="163" text-anchor="end">270</text>
959
+ </g>
960
+ </g>
961
+ <circle cx="240" cy="160" r="44" fill="none" stroke="var(--plot-ring)" stroke-width="0.55" opacity="0.5"/>
962
+ <circle class="sonar-ring sonar-a" cx="240" cy="160" r="78"/>
963
+ <circle class="sonar-ring sonar-b" cx="240" cy="160" r="78"/>
964
+ <circle class="sonar-ring sonar-c" cx="240" cy="160" r="78"/>
965
+ <g class="sweep">
966
+ <path d="M240 160 L258 16 A148 148 0 0 0 222 16 Z" fill="url(#sweep-fade)" opacity="0.55"/>
967
+ <line x1="240" y1="160" x2="240" y2="14" stroke="var(--plot-hold)" stroke-width="0.65" stroke-linecap="round" opacity="0.45"/>
968
+ </g>
969
+ <circle class="ping-drop" cx="240" cy="160" r="18" fill="none" stroke="var(--plot-hold)" stroke-width="0.85" opacity="0"/>
970
+ <circle class="ping-drop-2" cx="240" cy="160" r="18" fill="none" stroke="var(--plot-glow)" stroke-width="0.55" opacity="0"/>
971
+ <circle class="ping-lock" cx="240" cy="160" r="18" fill="none" stroke="var(--plot-locked)" stroke-width="0.85" opacity="0"/>
972
+ <circle class="ping-lock-2" cx="240" cy="160" r="18" fill="none" stroke="var(--plot-locked)" stroke-width="0.55" opacity="0"/>
973
+ <g class="mode-line" font-family="ui-monospace, monospace">
974
+ <text x="20" y="24" fill="var(--plot-label)" font-size="8" letter-spacing="0.16em">MOORING SEQ</text>
975
+ <text class="mode-value" x="20" y="40" fill="var(--plot-text-locked)" font-size="10" letter-spacing="0.08em">LOCKED · order_no</text>
976
+ </g>
977
+ <g class="tethers" fill="none" stroke="var(--plot-glow)" stroke-width="0.7" stroke-linecap="round" stroke-dasharray="2 10">
978
+ <path data-tether="0" d="M240 160 C240 110 240 70 240 48"/>
979
+ <path data-tether="1" d="M240 160 C190 160 130 160 88 160"/>
980
+ <path data-tether="2" d="M240 160 C300 140 360 120 400 108"/>
981
+ <path data-tether="3" d="M240 160 C280 210 340 250 372 268"/>
982
+ </g>
983
+ <g transform="translate(240 160)">
984
+ <g class="anchor-drop glow-lock" fill="none" stroke="var(--plot-hex)" stroke-width="0.9">
985
+ <circle class="halo-pulse" r="18" stroke="var(--plot-glow)" stroke-width="0.6"/>
986
+ <polygon points="0,-16 14,-8 14,8 0,16 -14,8 -14,-8"/>
987
+ <circle r="3.2" fill="var(--plot-glow)" stroke="none" class="node-pulse"/>
988
+ <path d="M0 16 V30 M-7 24 L0 32 L7 24" stroke="var(--plot-hold)" stroke-width="0.9"/>
989
+ </g>
990
+ </g>
991
+ <g class="drift-c" data-node="0">
992
+ <path d="M240 38V34M240 58V62M230 48H226M250 48H254" stroke="var(--plot-drift)" stroke-width="0.6" opacity="0.55" fill="none"/>
993
+ <circle class="glow-drift" cx="240" cy="48" r="7" fill="none" stroke="var(--plot-drift)" stroke-width="0.7"/>
994
+ <circle cx="240" cy="48" r="1.8" fill="var(--plot-drift)"/>
995
+ <text x="252" y="44" fill="var(--plot-text-drift)" font-family="ui-monospace, monospace">PM · 订单编号</text>
996
+ </g>
997
+ <g class="drift-a" data-node="1">
998
+ <path d="M88 150V146M88 170V174M78 160H74M98 160H102" stroke="var(--plot-drift)" stroke-width="0.6" opacity="0.55" fill="none"/>
999
+ <circle class="glow-drift" cx="88" cy="160" r="7" fill="none" stroke="var(--plot-drift)" stroke-width="0.7"/>
1000
+ <circle cx="88" cy="160" r="1.8" fill="var(--plot-drift)"/>
1001
+ <text x="88" y="144" text-anchor="middle" fill="var(--plot-text-drift)" font-family="ui-monospace, monospace">FE · orderId</text>
1002
+ </g>
1003
+ <g class="drift-d" data-node="2">
1004
+ <path d="M400 97V93M400 119V123M389 108H385M411 108H415" stroke="var(--plot-hold)" stroke-width="0.6" opacity="0.6" fill="none"/>
1005
+ <circle class="halo-pulse" cx="400" cy="108" r="8" fill="none" stroke="var(--plot-hold)" stroke-width="0.6"/>
1006
+ <circle class="glow-hold" cx="400" cy="108" r="3.4" fill="var(--plot-hold)"/>
1007
+ <text x="318" y="100" fill="var(--plot-text-hold)" font-family="ui-monospace, monospace">BE · order_no</text>
1008
+ </g>
1009
+ <g class="drift-b" data-node="3">
1010
+ <path d="M372 258V254M372 278V282M362 268H358M382 268H386" stroke="var(--plot-drift)" stroke-width="0.6" opacity="0.55" fill="none"/>
1011
+ <circle class="glow-drift" cx="372" cy="268" r="7" fill="none" stroke="var(--plot-drift)" stroke-width="0.7"/>
1012
+ <circle cx="372" cy="268" r="1.8" fill="var(--plot-drift)"/>
1013
+ <text x="276" y="286" fill="var(--plot-text-drift)" font-family="ui-monospace, monospace">QA · 「单号」</text>
1014
+ </g>
1015
+ <g class="glow-chain" font-family="ui-monospace, monospace">
1016
+ <text class="chain-hold" x="86" y="352" fill="var(--plot-text-lock)">HOLD</text>
1017
+ <path class="chain-draw-1" d="M132 348H176" fill="none" stroke="var(--plot-glow)" stroke-width="0.75" marker-end="url(#chain-arrow)"/>
1018
+ <text class="chain-name" x="186" y="352" fill="var(--plot-text-lock)">order_no</text>
1019
+ <path class="chain-draw-2" d="M292 348H336" fill="none" stroke="var(--plot-locked)" stroke-width="0.75" marker-end="url(#lock-arrow)"/>
1020
+ <text class="chain-locked" x="346" y="352" fill="var(--plot-text-locked)">LOCKED</text>
1021
+ </g>
1022
+ </svg>
1023
+ </figure>
1024
+ </div>
1025
+ </section>
1026
+
1027
+ <div class="spec-strip" aria-label="问题、闸门、结果">
1028
+ <article>
1029
+ <div class="spec-k">01 NAME</div>
1030
+ <div class="spec-v danger">各造一套名</div>
1031
+ </article>
1032
+ <article>
1033
+ <div class="spec-k">02 GATE</div>
1034
+ <div class="spec-v">合入时核对</div>
1035
+ </article>
1036
+ <article>
1037
+ <div class="spec-k">03 HOLD</div>
1038
+ <div class="spec-v ok">写偏即停</div>
1039
+ </article>
1040
+ </div>
1041
+
1042
+ <section id="why">
1043
+ <div class="wrap">
1044
+ <h2 class="section-title">每个 Agent 写的名字,单独看都对</h2>
1045
+ <p class="section-desc">
1046
+ 你让它用下划线,这次听了;换一个对话、换一个同事的 Cursor,它又换成同义词。
1047
+ Review 盯不过来,编译器不管,prompt 挡不住下一次。
1048
+ </p>
1049
+ <div class="ledger">
1050
+ <div class="ledger-head"><span>SHARED INTENT</span><span>订单号 · 支付流水</span></div>
1051
+ <div class="ledger-row">
1052
+ <div class="ledger-who">FE AGENT</div>
1053
+ <p>按前端习惯写成 <code>orderId</code>、<code>paymentId</code>。页面看着没问题。</p>
1054
+ </div>
1055
+ <div class="ledger-row">
1056
+ <div class="ledger-who">BE AGENT</div>
1057
+ <p>按库表习惯写成 <code>order_no</code>、<code>trade_no</code>。接口文档也这样。</p>
1058
+ </div>
1059
+ <div class="ledger-row">
1060
+ <div class="ledger-who">QA AGENT</div>
1061
+ <p>fixture 里直接用中文 key <code>「单号」</code>、<code>「支付单号」</code>。用例本地也能过。</p>
1062
+ </div>
1063
+ <div class="split-code">
1064
+ <div class="hero-panel bad">
1065
+ <div class="hero-panel-head"><span>合进去之后</span><span class="file">drift</span></div>
1066
+ <pre><span class="c">// 同一个意思,四套名字</span>
1067
+ 产品写的 「订单编号」
1068
+ 前端 POST { <span class="x">orderId</span>: "A10086" }
1069
+ 后端在等 { <span class="ok">order_no</span>: ... }
1070
+ 测试断言 { <span class="x">「单号」</span>: "A10086" }</pre>
1071
+ </div>
1072
+ <div class="hero-panel good">
1073
+ <div class="hero-panel-head"><span>锚咬住之后</span><span class="file">hold</span></div>
1074
+ <pre><span class="c">// 同一个意思,系在这一个名字上</span>
1075
+ { "id": "<span class="ok">order_no</span>" }
1076
+ { "id": "<span class="ok">trade_no</span>" }
1077
+
1078
+ <span class="c">// 出现 orderId / order_sn / 「单号」→ 进不了主干</span></pre>
1079
+ </div>
1080
+ </div>
1081
+ </div>
1082
+ <p class="hero-one-liner">多角色可以分头写,契约必须同一块锚。写偏了,合并就停。</p>
1083
+ <div class="compare" style="margin-top:28px">
1084
+ <div class="compare-card">
1085
+ <span class="compare-tag ok">你已经有的</span>
1086
+ <h3>口头约定、接口文档、改 prompt</h3>
1087
+ <p>人可以对齐一次。AI 第二天还能再发明一套近义词。文档不会在合并时失败。</p>
1088
+ </div>
1089
+ <div class="compare-card highlight">
1090
+ <span class="compare-tag bad">缺的那一下</span>
1091
+ <h3>合代码时查:这个名字在不在名单里</h3>
1092
+ <p>名单里是 order_no,代码里写了 orderId 或 order_sn → 这条合并失败。没有「先合进去再说」。</p>
1093
+ </div>
1094
+ </div>
1095
+ <div class="hero-actions" style="margin-top:24px">
1096
+ <a class="btn btn-primary" href="#demo">看这条怎么失败</a>
1097
+ </div>
1098
+ </div>
1099
+ </section>
1100
+
1101
+ <section id="demo">
1102
+ <div class="wrap">
1103
+ <p class="kicker">LIVE CHECK</p>
1104
+ <h2 class="section-title">名单是 order_no,代码写成 orderId → 失败</h2>
1105
+ <p class="section-desc">还是上面那个订单号。红了改代码、不要改契约。把 FAIL 贴给编码 Agent,再跑同一条。</p>
1106
+ <div class="terminal">
1107
+ <div class="terminal-bar">
1108
+ <span class="terminal-dot r"></span>
1109
+ <span class="terminal-dot y"></span>
1110
+ <span class="terminal-dot g"></span>
1111
+ <span class="terminal-title">npx @anchorlock/cli demo</span>
1112
+ </div>
1113
+ <div class="terminal-body">
1114
+ <div class="dim"># 契约是 order_no;代码写成了 orderId / order_sn</div>
1115
+ <div class="dim">$ npx @anchorlock/cli demo</div>
1116
+ <div class="violation">FAIL field-name gate (2 issues)</div>
1117
+ <div class="violation"> × "orderId" should be "order_no" (src/order.js)</div>
1118
+ <div class="violation"> × "order_sn" should be "order_no" (src/order.js)</div>
1119
+ <div class="dim">next 改代码对齐上面的规范名,不要改 pin / OpenAPI。</div>
1120
+ <div class="dim"> 把本段 FAIL 贴给编码 Agent;改完再跑同一条 check。</div>
1121
+ <br>
1122
+ <div class="dim"># 改代码,不改契约。再跑同一条</div>
1123
+ <div class="pass">PASS field-name gate (2 names · 1 files)</div>
1124
+ <div class="pass">demo ok: 碰撞拦住 → 对齐通过</div>
1125
+ <div class="dim">$ <span class="caret" aria-hidden="true"></span></div>
1126
+ </div>
1127
+ </div>
1128
+ <div class="hero-actions" style="margin-top:24px">
1129
+ <a class="btn btn-primary" href="#install">这就是我要的,先装上</a>
1130
+ <a class="btn btn-ghost" href="#cast">它凭什么能停</a>
1131
+ </div>
1132
+ </div>
1133
+ </section>
1134
+
1135
+ <section id="cast">
1136
+ <div class="wrap">
1137
+ <h2 class="section-title">两下钉同一块锚</h2>
1138
+ <p class="section-desc">
1139
+ 船是这次要合进去的代码。锚是那一个机器名:只许 <code>order_no</code>。
1140
+ 没有锚,风会把每条船吹去不同的港。有锚不收紧,锚在水里,船照样漂。
1141
+ 这两下不是同义词:<strong>抛锚</strong>是写盘前的 MCP,<strong>收紧</strong>是合并前的 CLI,各对应一条命令。
1142
+ </p>
1143
+ <table class="mini-table" aria-label="抛锚和收紧分别是哪条命令">
1144
+ <tr><th></th><th>何时</th><th>命令(记住这一列)</th></tr>
1145
+ <tr><td><strong>抛锚</strong></td><td>Agent 正在写文件</td><td>SemanticAnchor MCP:<code>bind</code> / <code>verify_usage</code>(吃 <code>constraints.json</code>)</td></tr>
1146
+ <tr><td><strong>收紧</strong></td><td>合进主干之前</td><td>本 CLI:<code>anchor check --contract pin.json --src src --strict --require-hash</code></td></tr>
1147
+ </table>
1148
+ <p class="path-note" style="margin-bottom:28px"><code>anchor constraints --from</code> 不是抛锚,只是从 OpenAPI / pin 投影出抛锚要吃的那份 <code>constraints.json</code>。</p>
1149
+ <div class="cast-grid">
1150
+ <figure class="cast-photo" aria-label="抛锚与收紧的两层锁定">
1151
+ <svg viewBox="0 0 360 320" role="img">
1152
+ <title>写盘前 bind,合并前 check,同一锁定点</title>
1153
+ <g fill="none" stroke="var(--plot-ring)" stroke-width="0.7">
1154
+ <circle class="ring-spin" cx="180" cy="118" r="54" stroke-dasharray="1.5 9"/>
1155
+ <circle cx="180" cy="118" r="28"/>
1156
+ </g>
1157
+ <g class="bind-drop">
1158
+ <circle cx="180" cy="118" r="3.4" fill="var(--plot-glow)" class="node-pulse"/>
1159
+ <text x="156" y="78" fill="var(--plot-hex)" font-size="9" font-family="ui-monospace, monospace">BIND</text>
1160
+ </g>
1161
+ <path class="cast-line" d="M180 146 V196" fill="none" stroke="var(--plot-glow)" stroke-width="0.75"/>
1162
+ <g class="cast-check" fill="none" stroke="var(--plot-hold)" stroke-width="0.65">
1163
+ <circle class="ring-spin-rev" cx="180" cy="228" r="46" stroke-dasharray="2 8"/>
1164
+ <circle cx="180" cy="228" r="22"/>
1165
+ <circle cx="180" cy="228" r="3.4" fill="var(--plot-hold)" stroke="none"/>
1166
+ <text x="152" y="288" fill="var(--plot-hold)" font-size="9" font-family="ui-monospace, monospace">CHECK</text>
1167
+ </g>
1168
+ </svg>
1169
+ </figure>
1170
+ <div class="cast-stack">
1171
+ <div class="cast-item">
1172
+ <span class="compare-tag ok">抛锚 · SemanticAnchor <code>bind</code> / <code>verify_usage</code></span>
1173
+ <h3>写盘前:先对照契约再落盘</h3>
1174
+ <p>Agent 要写文件时,编辑器里的 SemanticAnchor 跑 <code>bind</code> / <code>verify_usage</code>。名字不对当场改。这不是 <code>anchor check</code>,本 CLI 只负责准备它要吃的 <code>constraints.json</code>。</p>
1175
+ </div>
1176
+ <div class="cast-item">
1177
+ <span class="compare-tag ok">收紧 · <code>anchor check</code></span>
1178
+ <h3>合并前:再扫一遍源码</h3>
1179
+ <p>合进主干前跑 <code>anchor check --contract pin.json --src src --strict --require-hash</code>。抛锚漏了、<code>bind</code> 没绑、有人用手改飘了,卡在这里进不了主干。本机和 CI 挂同一条。</p>
1180
+ </div>
1181
+ </div>
1182
+ </div>
1183
+ <div class="hero-actions" style="margin-top:24px">
1184
+ <a class="btn btn-primary" href="#install">装进仓</a>
1185
+ </div>
1186
+ </div>
1187
+ </section>
1188
+
1189
+ <section id="install">
1190
+ <div class="wrap">
1191
+ <p class="kicker">QUICK START</p>
1192
+ <h2 class="section-title">安装。一次就够。</h2>
1193
+ <p class="section-desc">
1194
+ 需要 Node 18 +。本 CLI 做的是<strong>收紧</strong>:<code>anchor check</code> 只查字段名。写盘前的<strong>抛锚</strong>是 SemanticAnchor 的 <code>bind</code> / <code>verify_usage</code>,吃同一份 <code>constraints.json</code>。
1195
+ </p>
1196
+ <div class="compose-list">
1197
+ <div class="compose-card">
1198
+ <div class="compose-num">01</div>
1199
+ <div>
1200
+ <h3>装进消费仓</h3>
1201
+ <p>在<strong>前端 / 客户端 / 测试仓</strong>执行 <code>npm i -D @anchorlock/cli</code>。不要拿它去扫同一个 Java 后端仓里的方法和 SQL。全公司钉同一份发版 OpenAPI,不要每个仓各编一份名单。</p>
1202
+ </div>
1203
+ </div>
1204
+ <div class="compose-card">
1205
+ <div class="compose-num">02</div>
1206
+ <div>
1207
+ <h3>一次 <code>init</code></h3>
1208
+ <p>只装 Agent 协议,<strong>不写字段名单</strong>。名单来自 pin 住的 OpenAPI,或一处从代码起草后由<strong>人</strong>确认。</p>
1209
+ </div>
1210
+ </div>
1211
+ </div>
1212
+ <div class="code-block" style="margin-top:28px">
1213
+ <pre><span class="comment"># 装进你要锁的那个仓 · 只做一次</span>
1214
+ <span class="cmd">npm i -D @anchorlock/cli</span>
1215
+ <span class="cmd">npx anchor init</span></pre>
1216
+ </div>
1217
+ <div class="hero-actions" style="margin-top:24px">
1218
+ <a class="btn btn-primary" href="#path-a">去钉 OpenAPI</a>
1219
+ </div>
1220
+ </div>
1221
+ </section>
1222
+
1223
+ <section id="start">
1224
+ <div class="wrap">
1225
+ <p class="kicker">HOOK INTO YOUR REPO</p>
1226
+ <h2 class="section-title">使用。对照钉死的说明书。</h2>
1227
+ <p class="section-desc">
1228
+ 契约必须有仓外权威。编码 Agent 根据需求自己写名单,等于没锁。有 OpenAPI 就 pin;没有就一处起草,人确认一次。
1229
+ </p>
1230
+ <div class="compose-list">
1231
+ <div class="compose-card">
1232
+ <div class="compose-num">01</div>
1233
+ <div>
1234
+ <h3>Pin 发版 OpenAPI</h3>
1235
+ <p>后端构建导出的 <code>openapi.json</code> 挂在 tag 上。消费仓只提交 <code>contracts/*.pin.json</code>(url + hash),不 clone 后端、不对着 <code>/v3/api-docs</code>。</p>
1236
+ </div>
1237
+ </div>
1238
+ <div class="compose-card">
1239
+ <div class="compose-num">02</div>
1240
+ <div>
1241
+ <h3>合主干:<code>anchor check --strict</code></h3>
1242
+ <p>红了改代码对齐说明书,不改 pin。把 FAIL 贴给编码 Agent。函数名(<code>lookupMine()</code>)不算字段。不要用它扫本仓 Java 的 getter 和 SQL 列名。</p>
1243
+ </div>
1244
+ </div>
1245
+ </div>
1246
+ <div class="code-block" style="margin-top:28px">
1247
+ <pre><span class="comment"># 消费仓 · 对照钉死的说明书</span>
1248
+ <span class="cmd">npx anchor pin --from</span> dist/openapi.json <span class="cmd">--url</span> https://…/openapi.json <span class="cmd">--tag</span> v1.4.0
1249
+ <span class="cmd">npx anchor check --contract</span> contracts/order.pin.json <span class="cmd">--src</span> src <span class="cmd">--strict --require-hash</span></pre>
1250
+ </div>
1251
+ <p class="path-note">不要从工单 / PRD 让 Agent 撰写 <code>constraints.json</code>。没有 OpenAPI 时,用下面的路 B:一处 <code>init --from-code</code>,人确认后再 check。</p>
1252
+
1253
+ <h2 class="section-title" style="margin-top:56px">有没有现成的接口文档?</h2>
1254
+ <p class="section-desc">
1255
+ 全公司只锁定一份机器名。有 OpenAPI 走路 A。没有则一处起草、人确认;其它仓只 check 那一份。
1256
+ </p>
1257
+
1258
+ <figure class="flow" aria-label="两条路怎么分">
1259
+ <div class="fork-board">
1260
+ <div class="fork-q"><div class="flow-box file">你已经有 OpenAPI 吗?</div></div>
1261
+ <div class="fork-arm">
1262
+ <div class="flow-arrow">有 → 路 A</div>
1263
+ <div class="flow-stack">
1264
+ <div class="flow-box">构建导出 <strong>openapi.json</strong></div>
1265
+ <div class="flow-box"><strong>anchor pin</strong> 写出指纹</div>
1266
+ <div class="flow-box ok">收紧 · <code>anchor check</code><small>要抛锚先投影再 bind</small></div>
1267
+ </div>
1268
+ </div>
1269
+ <div class="fork-mid"></div>
1270
+ <div class="fork-arm">
1271
+ <div class="flow-arrow">没有 → 路 B</div>
1272
+ <div class="flow-stack">
1273
+ <div class="flow-box">一处 <strong>init --from-code</strong></div>
1274
+ <div class="flow-box">自动起草 <strong>constraints.json</strong></div>
1275
+ <div class="flow-box ok">人确认后:收紧走 check,抛锚走 bind</div>
1276
+ </div>
1277
+ </div>
1278
+ </div>
1279
+ <figcaption class="flow-caption">中间没有第三条路。不要「前端走 B、后端走 A」——两边对的必须是同一份契约。</figcaption>
1280
+ </figure>
1281
+
1282
+ <div class="path-fork">
1283
+ <a class="path-pick rec" href="#path-a">
1284
+ <div class="path-pick-kicker">PATH A</div>
1285
+ <h3>有 OpenAPI</h3>
1286
+ <p>后端已经有接口说明书。消费仓钉住这次发版挂出来的那一份,不要再手写字段清单。</p>
1287
+ <span class="path-pick-go">看路 A 怎么做 →</span>
1288
+ </a>
1289
+ <a class="path-pick" href="#path-b">
1290
+ <div class="path-pick-kicker">PATH B</div>
1291
+ <h3>没有 OpenAPI</h3>
1292
+ <p>一处 <code>init --from-code</code> 起草,<strong>人确认</strong> ids。其它仓只 check 这一份。不要让 Agent 按需求编名单。</p>
1293
+ <span class="path-pick-go">看路 B 怎么做 →</span>
1294
+ </a>
1295
+ <a class="path-pick span" href="#agents">
1296
+ <div class="path-pick-kicker">场景 · 不是 PATH C</div>
1297
+ <h3>多个 Agent 同时写</h3>
1298
+ <p>Cursor / Claude / 前端和测试分头开,仍走路 A 或路 B。共享同一份 pin,各自改代码,CI 收紧。不要每人编一份名单。</p>
1299
+ <span class="path-pick-go">看怎么分工 →</span>
1300
+ </a>
1301
+ </div>
1302
+
1303
+ <div class="path-how" id="path-a">
1304
+ <div class="path-how-head">
1305
+ <span class="path-mark">A</span>
1306
+ <h3>有接口文档:钉住发版说明书</h3>
1307
+ </div>
1308
+ <p class="section-desc">OpenAPI 由后端<strong>构建导出</strong>(Springdoc / Nest / FastAPI…),不要手写。前端不去 clone 后端仓,也不对着线上最新的 <code>/v3/api-docs</code> 扫。</p>
1309
+ <div class="compare" style="margin-bottom:24px">
1310
+ <div class="compare-card">
1311
+ <span class="compare-tag bad">不要这样</span>
1312
+ <h3>每个仓自己抄一份名单</h3>
1313
+ <p>前端抄出 orderId,后端抄出 order_no。两边 check 都绿,联调还是炸。路 A 不要各 pin 各的。</p>
1314
+ </div>
1315
+ <div class="compare-card highlight">
1316
+ <span class="compare-tag ok">要这样</span>
1317
+ <h3>消费仓都指向同一份发版说明书</h3>
1318
+ <p>后端打 tag,把说明书挂到发版页。前端只提交 pin:url 填下载链接。check 自己按 url 拉文件。不要拿 check 去扫后端自己的实现代码。</p>
1319
+ </div>
1320
+ </div>
1321
+ <figure class="flow" aria-label="路 A 流向">
1322
+ <div class="flow-board">
1323
+ <div>
1324
+ <div class="flow-lane">后端仓</div>
1325
+ <div class="flow-stack">
1326
+ <div class="flow-box">构建导出说明书<small>框架抽出 openapi.json · 不手写</small></div>
1327
+ <div class="flow-arrow">↓</div>
1328
+ <div class="flow-box">打 tag,CI 挂到发版页<small>git tag v1.4.0</small></div>
1329
+ <div class="flow-arrow">↓</div>
1330
+ <div class="flow-box file">anchor pin 写出指纹<small>pin.json · 不用手抄 hash</small></div>
1331
+ </div>
1332
+ </div>
1333
+ <div class="flow-join"><span>https 下载<br>不是 clone</span></div>
1334
+ <div>
1335
+ <div class="flow-lane">前端仓</div>
1336
+ <div class="flow-stack">
1337
+ <div class="flow-box file">只提交 pin.json<small>url + hash + version</small></div>
1338
+ <div class="flow-arrow">↓ 同一份说明书</div>
1339
+ <div class="flow-split">
1340
+ <div class="flow-box ok">收紧<small>check --src src --strict --require-hash</small></div>
1341
+ <div class="flow-box ok">抛锚<small>constraints --from → SA bind / verify_usage</small></div>
1342
+ </div>
1343
+ </div>
1344
+ </div>
1345
+ </div>
1346
+ <figcaption class="flow-caption">左边产出<strong>一份钉死的说明书</strong>,右边只记「去哪下、指纹是多少」。收紧:<code>anchor check --contract pin.json --src src --strict --require-hash</code>,自己按 url 拉文件。抛锚:先 <code>anchor constraints --from</code> 投影成 <code>constraints.json</code>,再交给 SemanticAnchor 的 <code>bind</code> / <code>verify_usage</code>。</figcaption>
1347
+ </figure>
1348
+ <div class="who-grid">
1349
+ <div class="who-card">
1350
+ <h4>后端仓做什么</h4>
1351
+ <ol>
1352
+ <li>构建导出 <code>openapi.json</code>(框架插件,不手写)</li>
1353
+ <li>打 <strong>tag</strong>,CI 把文件挂到这次 Release</li>
1354
+ <li>打开发版页,复制「下载附件」的 https</li>
1355
+ <li><code>anchor pin --from dist/openapi.json --url … --tag v1.4.0 -o dist/order.pin.json</code>,把这份 pin 发给前端</li>
1356
+ </ol>
1357
+ </div>
1358
+ <div class="who-card">
1359
+ <h4>前端仓做什么</h4>
1360
+ <ol>
1361
+ <li>把收到的 <code>order.pin.json</code> 放进 <code>contracts/</code> 并提交(不要手抄 hash,也不要把整份 OpenAPI 提交进来)</li>
1362
+ <li>收紧(本机和 CI 同一条):<code>anchor check --contract contracts/order.pin.json --src src --strict --require-hash</code></li>
1363
+ <li>红了:把 FAIL 贴给编码 Agent,改代码对齐说明书,不要改 pin</li>
1364
+ <li>抛锚:先 <code>anchor constraints --from contracts/order.pin.json -o constraints.json</code>,再把 SemanticAnchor 指向它(MCP <code>bind</code> / <code>verify_usage</code>)</li>
1365
+ </ol>
1366
+ </div>
1367
+ </div>
1368
+ <p class="section-desc" style="margin-top:8px">pin 只记住说明书在哪。<code>check</code> 才是闸:拿那份说明书对照<strong>你仓库里的代码</strong>。字段名对不上,这条命令失败,合不进主干。</p>
1369
+ <div class="compose-list" style="margin:8px 0 24px">
1370
+ <div class="compose-card">
1371
+ <div class="compose-num">01</div>
1372
+ <div>
1373
+ <h3>找到那一版说明书</h3>
1374
+ <p><code>--contract contracts/order.pin.json</code>:读 pin 里的下载地址和指纹,拉下 <code>v1.4.0</code> 的 <code>openapi.json</code>。hash 对不上就停,不拿错文件继续扫。</p>
1375
+ </div>
1376
+ </div>
1377
+ <div class="compose-card">
1378
+ <div class="compose-num">02</div>
1379
+ <div>
1380
+ <h3>抽出说明书上的字段名</h3>
1381
+ <p>从 OpenAPI 的 schema 属性名得到这份契约认的名字(例如 <code>order_no</code>)。这就是「对」的那一份,前端不另编名单。</p>
1382
+ </div>
1383
+ </div>
1384
+ <div class="compose-card">
1385
+ <div class="compose-num">03</div>
1386
+ <div>
1387
+ <h3>扫 <code>src/</code>,对不上就红</h3>
1388
+ <p><code>--src src --strict --require-hash</code> 只看你的业务代码。写了 <code>orderId</code> 而说明书是 <code>order_no</code> → 退出码非 0。把 FAIL 贴给 Agent,改代码,不改 pin。</p>
1389
+ </div>
1390
+ </div>
1391
+ </div>
1392
+ <div class="code-block">
1393
+ <pre><span class="comment"># ── 后端仓 ── 有 dist/openapi.json。打 tag,算出指纹,把 pin 发给前端</span>
1394
+ <span class="cmd">tag=</span>"v1.4.0"
1395
+ <span class="cmd">git tag</span> "$tag"
1396
+ <span class="cmd">git push origin</span> "refs/tags/$tag"
1397
+ <span class="comment"># CI 把 openapi.json 挂到这次发版页,复制「下载附件」的 https</span>
1398
+ <span class="cmd">anchor pin --from</span> dist/openapi.json \
1399
+ <span class="cmd">--url</span> "https://git.example.com/group/order-api/-/releases/v1.4.0/downloads/openapi.json" \
1400
+ <span class="cmd">--tag</span> "$tag" <span class="cmd">-o</span> dist/order.pin.json
1401
+ <span class="comment"># 把 dist/order.pin.json 交给前端(发版附件或拷贝)。前端不要自己再 pin</span>
1402
+
1403
+ <span class="comment"># 收紧 · 本机和 CI 同一条。红了改代码,把 FAIL 贴给 Agent,不要改 pin</span>
1404
+ <span class="comment"># git add contracts/order.pin.json &amp;&amp; git commit</span>
1405
+ <span class="cmd">anchor check --contract</span> contracts/order.pin.json <span class="cmd">--src</span> src <span class="cmd">--strict --require-hash</span>
1406
+
1407
+ <span class="comment"># 抛锚前先投影。真正的抛锚是 SemanticAnchor 的 bind / verify_usage(不是这条 CLI)</span>
1408
+ <span class="cmd">anchor constraints --from</span> contracts/order.pin.json <span class="cmd">-o</span> constraints.json</pre>
1409
+ </div>
1410
+ <p class="path-note">pin 相当于契约的 lockfile:记下「去哪下、指纹是多少」。前端提交它,是为了让本机和 CI 都锁在同一版说明书上,而不是 submodule 后端仓、也不是对着线上 <code>/v3/api-docs</code> 扫。</p>
1411
+
1412
+ <h4 class="second-round-title">第二回合:这次需求要加字段</h4>
1413
+ <p class="section-desc">字段范围由<strong>后端发版</strong>推进,不由你和 Agent 的对话推进。需求说要加「优惠券抵扣」,不等于可以往契约里塞一个 <code>couponAmount</code>——先让后端发版,说明书上有了,前端才认。pin 是<strong>整份换</strong>,不是往里面加字段。</p>
1414
+ <div class="code-block">
1415
+ <pre><span class="comment"># 后端发了 v1.5.0 之后,前端换整份。url 里的 tag 跟着换,不用手改</span>
1416
+ <span class="cmd">npx anchor repin --from</span> ../order-api/dist/openapi.json <span class="cmd">--tag</span> v1.5.0 <span class="cmd">--change</span> PROJ-123
1417
+
1418
+ <span class="comment"># 看清楚换了什么再提交</span>
1419
+ <span class="cmd">git diff</span> contracts/order.pin.json
1420
+ <span class="cmd">git add</span> contracts/order.pin.json
1421
+
1422
+ <span class="comment"># 改业务代码用新字段,跑同一条 check</span>
1423
+ <span class="cmd">npx anchor check --contract</span> contracts/order.pin.json <span class="cmd">--src</span> src <span class="cmd">--strict --require-hash</span>
1424
+ <span class="out"> pin v1.5.0 · change PROJ-123</span></pre>
1425
+ </div>
1426
+ <table class="mini-table" aria-label="pin 的版本管理靠什么">
1427
+ <tr><th>你想知道的</th><th>记在哪</th></tr>
1428
+ <tr><td>现在锁在哪一版</td><td><code>pin.json</code> 的 <code>version</code> + <code>hash</code>,每次 <code>check</code> 都打出来</td></tr>
1429
+ <tr><td>为什么换的这一版</td><td><code>pin.json</code> 的 <code>change</code>(需求号)+ 这次 commit</td></tr>
1430
+ <tr><td>一共换过几次</td><td><code>git log contracts/order.pin.json</code></td></tr>
1431
+ </table>
1432
+ <p class="path-note"><strong>编码 Agent 不许自己 repin。</strong>换版由「后端发布了新版本」触发,不由「check 红了想换绿」触发。红了只能改代码。</p>
1433
+ </div>
1434
+
1435
+ <div class="path-how" id="path-b">
1436
+ <div class="path-how-head">
1437
+ <span class="path-mark">B</span>
1438
+ <h3>没有 OpenAPI:从代码起草,人确认一次</h3>
1439
+ </div>
1440
+ <p class="section-desc">没有接口文档,就不要手抄字段清单。命令扫现有源码,把驼峰/下划线收成一份 <code>constraints.json</code>。人只确认,不从空白填起。</p>
1441
+ <figure class="flow" aria-label="路 B 流向">
1442
+ <div class="flow-board">
1443
+ <div>
1444
+ <div class="flow-lane">只扫一次</div>
1445
+ <div class="flow-stack">
1446
+ <div class="flow-box">现有源码<small>orderId / order_sn / 「单号」…</small></div>
1447
+ <div class="flow-arrow">↓ 自动</div>
1448
+ <div class="flow-box file">起草 constraints.json<small>anchor init --from-code &lt;一个模块&gt;</small></div>
1449
+ <div class="flow-arrow">↓ 人确认</div>
1450
+ <div class="flow-box">删掉不该锁的名字<small>函数名、框架词</small></div>
1451
+ </div>
1452
+ </div>
1453
+ <div class="flow-join"><span>同一份<br>不要各扫各的</span></div>
1454
+ <div>
1455
+ <div class="flow-lane">各仓只查</div>
1456
+ <div class="flow-stack">
1457
+ <div class="flow-split">
1458
+ <div class="flow-box ok">收紧<small>anchor check --contract constraints.json --src src</small></div>
1459
+ <div class="flow-box ok">抛锚<small>SA_CONSTRAINTS + bind / verify_usage</small></div>
1460
+ </div>
1461
+ <div class="flow-box warn">不要再 init<small>各仓各扫一份 = 没锁</small></div>
1462
+ </div>
1463
+ </div>
1464
+ </div>
1465
+ <figcaption class="flow-caption">左边<strong>自动起草</strong>驼峰/下划线。<code>orderId</code> 会收成 <code>order_id</code>,不会猜成 <code>order_no</code>。人把 <code>id</code> 改成要锁的名字,近义词写进 aliases。右边都指这一份。</figcaption>
1466
+ </figure>
1467
+ <div class="who-grid">
1468
+ <div class="who-card">
1469
+ <h4>谁来扫(只做一次)</h4>
1470
+ <ol>
1471
+ <li><code>anchor init --from-code src</code> 自动写出 <code>constraints.json</code></li>
1472
+ <li>打开文件:删掉函数名、框架词、一次性变量</li>
1473
+ <li>判据:<strong>留</strong>会跨进程传的名字(HTTP JSON key、SQL 列名、MQ payload、存储字段);<strong>删</strong>函数名、局部变量、类型名、框架词,以及只在一个文件出现过的名字。拿不准就删</li>
1474
+ <li>确认推荐的 snake_case;近义词不会自动合并,要把 <code>id</code> 改成 <code>order_no</code>,并把 <code>orderId</code> / <code>order_sn</code> 写进 <code>aliases</code></li>
1475
+ <li>有枚举再补 <code>values</code>。不要另起一份 txt</li>
1476
+ </ol>
1477
+ </div>
1478
+ <div class="who-card">
1479
+ <h4>其它仓怎么用</h4>
1480
+ <ol>
1481
+ <li>拿到<strong>同一份</strong>已确认的文件,不要再 <code>init</code></li>
1482
+ <li>收紧:<code>anchor check --contract constraints.json --src src</code></li>
1483
+ <li>抛锚:<code>SA_CONSTRAINTS=./constraints.json</code>,SemanticAnchor 跑 <code>bind</code> / <code>verify_usage</code></li>
1484
+ <li>新字段先改这一份,再写代码。各仓各扫各的,等于没锁</li>
1485
+ </ol>
1486
+ </div>
1487
+ </div>
1488
+ <div class="code-block">
1489
+ <pre><span class="comment"># 只在一处执行。不要前端扫一次、后端再扫一次</span>
1490
+ <span class="cmd">anchor init --from-code</span> src
1491
+ <span class="comment"># 自动写出 constraints.json。人只删不该锁的,并把 id 改成要锁的名字</span>
1492
+ {
1493
+ "id": "order_no",
1494
+ "aliases": ["orderId", "order_sn", "订单编号", "单号"]
1495
+ }
1496
+
1497
+ <span class="cmd">anchor check --contract</span> constraints.json <span class="cmd">--src</span> src
1498
+ <span class="comment"># 上面是收紧。抛锚:SA_CONSTRAINTS=./constraints.json ,再跑 SemanticAnchor bind / verify_usage</span></pre>
1499
+ </div>
1500
+ <p class="path-note">连代码都没有、要从零发明字段时,才用 <code>anchor constraints --init</code> 出空白模板。那是例外,不是路 B 的主路径。</p>
1501
+ </div>
1502
+
1503
+ <div class="path-how" id="shapes">
1504
+ <div class="path-how-head">
1505
+ <span class="path-mark">文件</span>
1506
+ <h3>这两份文件长什么样</h3>
1507
+ </div>
1508
+ <p class="section-desc">下面是<strong>对公转账</strong>里最容易各写一套的五个名字。这是切片,不是整份支付说明书。<code>check</code> 只读字段名。消费仓提交的是 <code>pin.json</code>,不是整份 OpenAPI。</p>
1509
+ <div class="who-grid">
1510
+ <div class="who-card">
1511
+ <h4>openapi.json · 路 A 的权威</h4>
1512
+ <p style="font-size:13px;color:var(--text-secondary);margin:0 0 10px">框架导出。规范名在 schema 的 <code>properties</code> 上:流水号、付款账号、收款账号、金额、币种。</p>
1513
+ </div>
1514
+ <div class="who-card">
1515
+ <h4>constraints.json · 路 B / 抛锚吃的名单</h4>
1516
+ <p style="font-size:13px;color:var(--text-secondary);margin:0 0 10px"><code>id</code> 是规范名。前端习惯的 <code>fromAccount</code>、<code>amount</code>,核心库的 <code>payer_acct</code>,都写进 <code>aliases</code>。路 A 用投影,不要手写。</p>
1517
+ </div>
1518
+ </div>
1519
+ <div class="split-code" style="margin-bottom:16px">
1520
+ <div class="hero-panel">
1521
+ <div class="hero-panel-head"><span>openapi.json</span><span class="file">POST /payments/transfers</span></div>
1522
+ <pre>{
1523
+ "openapi": "3.0.3",
1524
+ "info": { "title": "payment-api", "version": "1.4.0" },
1525
+ "paths": {
1526
+ "/payments/transfers": {
1527
+ "post": {
1528
+ "operationId": "createTransfer",
1529
+ "requestBody": {
1530
+ "required": true,
1531
+ "content": {
1532
+ "application/json": {
1533
+ "schema": { "$ref": "#/components/schemas/TransferReq" }
1534
+ }
1535
+ }
1536
+ },
1537
+ "responses": {
1538
+ "200": {
1539
+ "content": {
1540
+ "application/json": {
1541
+ "schema": { "$ref": "#/components/schemas/TransferRes" }
1542
+ }
1543
+ }
1544
+ }
1545
+ }
1546
+ }
1547
+ }
1548
+ },
1549
+ "components": {
1550
+ "schemas": {
1551
+ "TransferReq": {
1552
+ "type": "object",
1553
+ "required": ["payer_account", "payee_account", "payment_amount", "payment_currency"],
1554
+ "properties": {
1555
+ "payer_account": { "type": "string", "description": "付款账号" },
1556
+ "payee_account": { "type": "string", "description": "收款账号" },
1557
+ "payment_amount": { "type": "string", "description": "转账金额,十进制字符串" },
1558
+ "payment_currency": { "type": "string", "description": "币种,ISO 4217" }
1559
+ }
1560
+ },
1561
+ "TransferRes": {
1562
+ "type": "object",
1563
+ "properties": {
1564
+ "payment_id": { "type": "string", "description": "支付流水号" },
1565
+ "payment_status": { "type": "string" }
1566
+ }
1567
+ }
1568
+ }
1569
+ }
1570
+ }</pre>
1571
+ </div>
1572
+ <div class="hero-panel">
1573
+ <div class="hero-panel-head"><span>constraints.json</span><span class="file">同一笔转账</span></div>
1574
+ <pre>{
1575
+ "schemaVersion": "1",
1576
+ "units": [
1577
+ {
1578
+ "id": "payment_id",
1579
+ "type": "string",
1580
+ "aliases": ["paymentId", "payId", "bank_serial_no", "支付流水号"]
1581
+ },
1582
+ {
1583
+ "id": "payer_account",
1584
+ "type": "string",
1585
+ "aliases": ["fromAccount", "payerAcct", "payer_acct", "付款账号"]
1586
+ },
1587
+ {
1588
+ "id": "payee_account",
1589
+ "type": "string",
1590
+ "aliases": ["toAccount", "payeeAcct", "payee_acct", "收款账号"]
1591
+ },
1592
+ {
1593
+ "id": "payment_amount",
1594
+ "type": "string",
1595
+ "aliases": ["amount", "txnAmt", "pay_amt", "转账金额"]
1596
+ },
1597
+ {
1598
+ "id": "payment_currency",
1599
+ "type": "string",
1600
+ "aliases": ["currency", "ccy", "币种"]
1601
+ }
1602
+ ]
1603
+ }</pre>
1604
+ </div>
1605
+ </div>
1606
+ <p class="path-note">前端写成 <code>fromAccount</code> / <code>amount</code>,或测试断言写成 <code>"付款账号"</code> → 红,应改成 <code>payer_account</code> / <code>payment_amount</code>。路 A 的消费仓只提交 pin,例如 <code>{ "url": "https://…/v1.4.0/openapi.json", "hash": "sha256:…", "version": "v1.4.0" }</code>。</p>
1607
+ </div>
1608
+
1609
+ <div class="path-how" id="ci">
1610
+ <div class="path-how-head">
1611
+ <span class="path-mark">CI</span>
1612
+ <h3>挂到合并检查</h3>
1613
+ </div>
1614
+ <p class="section-desc">和跑测试同一位置。失败就不能合。这里挂的是<strong>收紧</strong>:<code>anchor check</code>。抛锚(<code>bind</code> / <code>verify_usage</code>)发生在编辑器写盘时,不替代这一条。</p>
1615
+ <div class="code-block">
1616
+ <pre><span class="comment"># 收紧 · 路 A(CI 挂这条,合主干前再拦一次)</span>
1617
+ <span class="cmd">npx anchor check --contract</span> contracts/order.pin.json <span class="cmd">--src</span> src <span class="cmd">--strict --require-hash</span>
1618
+ <span class="comment"># 收紧 · 路 B</span>
1619
+ <span class="cmd">npx anchor check --contract</span> constraints.json <span class="cmd">--src</span> src</pre>
1620
+ </div>
1621
+ </div>
1622
+
1623
+ <h2 class="section-title">这条 CLI 不做抛锚</h2>
1624
+ <p class="section-desc">抛锚和收紧必须分开记。本包装的是收紧;抛锚是另一个工具,吃同一份契约。</p>
1625
+ <table class="mini-table">
1626
+ <tr><th>你想做的</th><th>那一步叫</th><th>对应命令</th></tr>
1627
+ <tr><td>合代码前发现 orderId 写飘了</td><td>收紧</td><td><code>anchor check --contract pin.json --src src --strict --require-hash</code></td></tr>
1628
+ <tr><td>AI 正在写文件时先对照契约</td><td>抛锚</td><td>SemanticAnchor MCP <code>bind</code> / <code>verify_usage</code></td></tr>
1629
+ <tr><td>有 OpenAPI,要给抛锚准备文件</td><td>投影(不是抛锚)</td><td><code>anchor constraints --from pin.json -o constraints.json</code></td></tr>
1630
+ <tr><td>从需求文档编出整份接口包</td><td>进阶</td><td>@anchorlock/core,不在这一页</td></tr>
1631
+ </table>
1632
+ </div>
1633
+ </section>
1634
+
1635
+ <section id="agents">
1636
+ <div class="wrap">
1637
+ <p class="kicker">SEVERAL AGENTS · NOT PATH C</p>
1638
+ <h2 class="section-title">多个 Agent 同时写:不是第三条路</h2>
1639
+ <p class="section-desc">Cursor、Claude、Copilot 并行,或前端 / 后端 / 测试各开一个 Agent,<strong>仍走路 A</strong>(没有说明书则路 B)。协作增加的是写代码的人,不是契约的份数。共享 <code>contracts/order.pin.json</code>,CI 只认同一条 check。</p>
1640
+ <div class="who-grid">
1641
+ <div class="who-card">
1642
+ <h4>人 / 后端 Agent</h4>
1643
+ <ol>
1644
+ <li>人:第一次 <code>pin</code>;后端发新 tag 后 <code>repin --change &lt;需求号&gt;</code></li>
1645
+ <li>后端 Agent:实现接口、导出 OpenAPI、打 tag</li>
1646
+ <li>不要在消费仓里扫后端自己的 Java,也不要手写名单当权威</li>
1647
+ </ol>
1648
+ </div>
1649
+ <div class="who-card">
1650
+ <h4>前端 / 测试 Agent</h4>
1651
+ <ol>
1652
+ <li>用说明书上的名字写代码。红了把整段 FAIL 贴给<strong>写飘的那个</strong> Agent</li>
1653
+ <li>不许 <code>repin</code>、改 pin、按这次对话另编名单</li>
1654
+ <li>跨仓顺序:后端发版 → 人 <code>repin</code> → 消费仓 Agent 才用新字段。测试仓和前端仓 pin 同一个 tag</li>
1655
+ </ol>
1656
+ </div>
1657
+ </div>
1658
+ <div class="code-block">
1659
+ <pre><span class="comment"># 每个 Agent、每个消费仓,同一条。不是每人一份名单</span>
1660
+ <span class="cmd">npx anchor check --contract</span> contracts/order.pin.json <span class="cmd">--src</span> src <span class="cmd">--strict --require-hash</span>
1661
+ <span class="out"> pin v1.5.0 · change PROJ-123</span>
1662
+ <span class="comment"># 开工先看这一行是不是同一版。冲突在 git,不在契约</span></pre>
1663
+ </div>
1664
+ </div>
1665
+ </section>
1666
+ </main>
1667
+
1668
+ <footer>
1669
+ <div class="wrap footer-inner">
1670
+ <span class="footer-copy">ANCHORLOCK · HOLD THE CONTRACT</span>
1671
+ <ul class="footer-links">
1672
+ <li><a href="#cast">抛锚与收紧</a></li>
1673
+ <li><a href="#install">安装</a></li>
1674
+ <li><a href="#start">接到项目</a></li>
1675
+ <li><a href="#agents">多 Agent</a></li>
1676
+ <li><a href="docs/">文档</a></li>
1677
+ <li><a href="https://www.npmjs.com/package/@anchorlock/cli">npm</a></li>
1678
+ <li><a href="https://www.npmjs.com/package/@anchorlock/core">core</a></li>
1679
+ </ul>
1680
+ </div>
1681
+ </footer>
1682
+ <script>
1683
+ (function () {
1684
+ var root = document.documentElement;
1685
+ var plasmaTheme = null;
1686
+ function themePalette(theme) {
1687
+ if (theme === "night") {
1688
+ return {
1689
+ paper: [0.047, 0.051, 0.063],
1690
+ blue: [0.176, 0.243, 0.431],
1691
+ foam: [0.90, 0.92, 0.96],
1692
+ deep: [0.420, 0.561, 0.941]
1693
+ };
1694
+ }
1695
+ return {
1696
+ paper: [0.976, 0.973, 0.973],
1697
+ blue: [0.545, 0.655, 0.878],
1698
+ foam: [1.0, 1.0, 1.0],
1699
+ deep: [0.196, 0.396, 0.847]
1700
+ };
1701
+ }
1702
+ function apply(theme) {
1703
+ root.setAttribute("data-theme", theme);
1704
+ try { localStorage.setItem("anchorlock-theme", theme); } catch (e) {}
1705
+ document.querySelectorAll("[data-theme-set]").forEach(function (btn) {
1706
+ btn.setAttribute("aria-pressed", btn.getAttribute("data-theme-set") === theme ? "true" : "false");
1707
+ });
1708
+ if (plasmaTheme) plasmaTheme(theme);
1709
+ }
1710
+ apply(root.getAttribute("data-theme") || "day");
1711
+ document.querySelectorAll("[data-theme-set]").forEach(function (btn) {
1712
+ btn.addEventListener("click", function () {
1713
+ apply(btn.getAttribute("data-theme-set"));
1714
+ });
1715
+ });
1716
+
1717
+ var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1718
+ function straightenMooring() {
1719
+ var svg = document.querySelector(".hero-visual svg");
1720
+ if (!svg) return;
1721
+ var rest = [[240, 48], [88, 160], [400, 108], [372, 268]];
1722
+ svg.querySelectorAll("[data-tether]").forEach(function (path, i) {
1723
+ var n = rest[i];
1724
+ if (n) path.setAttribute("d", "M240 160L" + n[0] + " " + n[1]);
1725
+ });
1726
+ var tethers = svg.querySelector(".tethers");
1727
+ if (tethers) {
1728
+ tethers.removeAttribute("stroke-dasharray");
1729
+ tethers.setAttribute("stroke-width", "0.85");
1730
+ }
1731
+ }
1732
+ if (reduce) {
1733
+ straightenMooring();
1734
+ return;
1735
+ }
1736
+ root.classList.add("motion-ok");
1737
+ var t0 = performance.now();
1738
+
1739
+ (function startPlasma() {
1740
+ var canvas = document.querySelector(".plasma");
1741
+ if (!canvas) return;
1742
+ var gl = canvas.getContext("webgl", { alpha: false, antialias: false, premultipliedAlpha: false });
1743
+ if (!gl) return;
1744
+
1745
+ function compile(type, src) {
1746
+ var sh = gl.createShader(type);
1747
+ gl.shaderSource(sh, src);
1748
+ gl.compileShader(sh);
1749
+ if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) return null;
1750
+ return sh;
1751
+ }
1752
+
1753
+ var vs = compile(gl.VERTEX_SHADER,
1754
+ "attribute vec2 a_pos; void main(){ gl_Position = vec4(a_pos,0.0,1.0); }");
1755
+ var fs = compile(gl.FRAGMENT_SHADER, [
1756
+ "precision mediump float;",
1757
+ "uniform float u_time;",
1758
+ "uniform vec2 u_resolution;",
1759
+ "uniform vec3 u_paper;",
1760
+ "uniform vec3 u_blue;",
1761
+ "uniform vec3 u_foam;",
1762
+ "uniform vec3 u_deep;",
1763
+ "void main() {",
1764
+ " vec2 uv = gl_FragCoord.xy / u_resolution;",
1765
+ " float tt = u_time * 0.4;",
1766
+ " float v = sin(uv.x*10.0 + tt) + sin(uv.y*10.0 + tt*1.3) + sin((uv.x+uv.y)*8.0 + tt*0.7);",
1767
+ " v = v / 3.0;",
1768
+ " float r = 0.5 + 0.5 * sin(v * 3.14);",
1769
+ " float g = 0.5 + 0.5 * sin(v * 3.14 + 2.0);",
1770
+ " float b = 0.5 + 0.5 * sin(v * 3.14 + 4.0);",
1771
+ " vec3 col = mix(u_paper, u_blue, r);",
1772
+ " col = mix(col, u_foam, g * 0.42);",
1773
+ " col = mix(col, u_deep, b * 0.14);",
1774
+ " gl_FragColor = vec4(col, 1.0);",
1775
+ "}"
1776
+ ].join("\n"));
1777
+ if (!vs || !fs) return;
1778
+
1779
+ var prog = gl.createProgram();
1780
+ gl.attachShader(prog, vs);
1781
+ gl.attachShader(prog, fs);
1782
+ gl.linkProgram(prog);
1783
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return;
1784
+ gl.useProgram(prog);
1785
+
1786
+ var buf = gl.createBuffer();
1787
+ gl.bindBuffer(gl.ARRAY_BUFFER, buf);
1788
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
1789
+ var loc = gl.getAttribLocation(prog, "a_pos");
1790
+ gl.enableVertexAttribArray(loc);
1791
+ gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
1792
+
1793
+ var uTime = gl.getUniformLocation(prog, "u_time");
1794
+ var uRes = gl.getUniformLocation(prog, "u_resolution");
1795
+ var uPaper = gl.getUniformLocation(prog, "u_paper");
1796
+ var uBlue = gl.getUniformLocation(prog, "u_blue");
1797
+ var uFoam = gl.getUniformLocation(prog, "u_foam");
1798
+ var uDeep = gl.getUniformLocation(prog, "u_deep");
1799
+
1800
+ function setPalette(theme) {
1801
+ var p = themePalette(theme);
1802
+ gl.useProgram(prog);
1803
+ gl.uniform3fv(uPaper, p.paper);
1804
+ gl.uniform3fv(uBlue, p.blue);
1805
+ gl.uniform3fv(uFoam, p.foam);
1806
+ gl.uniform3fv(uDeep, p.deep);
1807
+ }
1808
+ plasmaTheme = setPalette;
1809
+ setPalette(root.getAttribute("data-theme") || "day");
1810
+
1811
+ function resize() {
1812
+ var dpr = Math.min(window.devicePixelRatio || 1, 1.5) * 0.42;
1813
+ var w = Math.max(2, Math.floor(window.innerWidth * dpr));
1814
+ var h = Math.max(2, Math.floor(window.innerHeight * dpr));
1815
+ if (canvas.width === w && canvas.height === h) return;
1816
+ canvas.width = w;
1817
+ canvas.height = h;
1818
+ gl.viewport(0, 0, w, h);
1819
+ gl.uniform2f(uRes, w, h);
1820
+ }
1821
+ resize();
1822
+ window.addEventListener("resize", resize);
1823
+
1824
+ canvas.classList.add("is-on");
1825
+ if (canvas.parentElement) canvas.parentElement.classList.add("has-plasma");
1826
+ var running = true;
1827
+ document.addEventListener("visibilitychange", function () {
1828
+ running = document.visibilityState !== "hidden";
1829
+ });
1830
+ function frame(now) {
1831
+ if (running) {
1832
+ gl.uniform1f(uTime, (now - t0) / 1000);
1833
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
1834
+ }
1835
+ requestAnimationFrame(frame);
1836
+ }
1837
+ requestAnimationFrame(frame);
1838
+ })();
1839
+
1840
+ function bindSpot(el) {
1841
+ el.addEventListener("pointermove", function (e) {
1842
+ var r = el.getBoundingClientRect();
1843
+ el.style.setProperty("--mx", (((e.clientX - r.left) / r.width) * 100).toFixed(2) + "%");
1844
+ el.style.setProperty("--my", (((e.clientY - r.top) / r.height) * 100).toFixed(2) + "%");
1845
+ });
1846
+ }
1847
+ document.querySelectorAll(".btn-primary, .ledger, .compare, .path-pick, .terminal, .who-card").forEach(bindSpot);
1848
+
1849
+ (function moor() {
1850
+ var svg = document.querySelector(".hero-visual svg");
1851
+ if (!svg) return;
1852
+ var paths = svg.querySelectorAll("[data-tether]");
1853
+ var nodes = [
1854
+ { el: svg.querySelector('[data-node="0"]'), x: 240, y: 48, phase: 0.2 },
1855
+ { el: svg.querySelector('[data-node="1"]'), x: 88, y: 160, phase: 1.1 },
1856
+ { el: svg.querySelector('[data-node="2"]'), x: 400, y: 108, phase: 2.4 },
1857
+ { el: svg.querySelector('[data-node="3"]'), x: 372, y: 268, phase: 3.3 }
1858
+ ];
1859
+ var drop = svg.querySelector(".anchor-drop");
1860
+ var tethers = svg.querySelector(".tethers");
1861
+ var modeValue = svg.querySelector(".mode-value");
1862
+ var pingDrop = svg.querySelector(".ping-drop");
1863
+ var pingDrop2 = svg.querySelector(".ping-drop-2");
1864
+ var pingLock = svg.querySelector(".ping-lock");
1865
+ var pingLock2 = svg.querySelector(".ping-lock-2");
1866
+ var CX = 240, CY = 160, BEAT = 16000;
1867
+ var prevP = 0, pingDropT = -1, pingLockT = -1, modeText = "";
1868
+
1869
+ function modeFor(p) {
1870
+ if (p < 0.14) return ["DRIFT", "var(--plot-text-drift)"];
1871
+ if (p < 0.32) return ["ANCHOR DOWN", "var(--plot-text-hold)"];
1872
+ if (p < 0.46) return ["TAKING UP SLACK", "var(--plot-text-hold)"];
1873
+ if (p < 0.82) return ["LOCKED · order_no", "var(--plot-text-locked)"];
1874
+ return ["RELEASE", "var(--plot-text-drift)"];
1875
+ }
1876
+ function renderPing(el, startT, now, dur, maxR, peak) {
1877
+ if (!el) return;
1878
+ if (startT < 0) { el.setAttribute("opacity", "0"); return; }
1879
+ var q = (now - startT) / dur;
1880
+ if (q < 0 || q >= 1) { el.setAttribute("opacity", "0"); return; }
1881
+ var e = 1 - (1 - q) * (1 - q);
1882
+ el.setAttribute("r", (18 + (maxR - 18) * e).toFixed(1));
1883
+ el.setAttribute("opacity", ((peak || 0.88) * (1 - q)).toFixed(3));
1884
+ }
1885
+
1886
+ function clamp(x, a, b) { return Math.max(a, Math.min(b, x)); }
1887
+ function ease(t) { t = clamp(t, 0, 1); return t * t * (3 - 2 * t); }
1888
+ function tautK(p) {
1889
+ if (p < 0.32) return 0;
1890
+ if (p < 0.46) return ease((p - 0.32) / 0.14);
1891
+ if (p < 0.82) return 1;
1892
+ return 1 - ease((p - 0.82) / 0.18);
1893
+ }
1894
+ function dropY(p) {
1895
+ if (p < 0.14) return { y: -38, o: 0 };
1896
+ if (p < 0.24) {
1897
+ var k = ease((p - 0.14) / 0.1);
1898
+ return { y: -38 + 41 * k, o: k };
1899
+ }
1900
+ if (p < 0.3) {
1901
+ var k2 = ease((p - 0.24) / 0.06);
1902
+ return { y: 3 - 3 * k2, o: 1 };
1903
+ }
1904
+ if (p < 0.8) return { y: 0, o: 1 };
1905
+ var k3 = ease((p - 0.8) / 0.2);
1906
+ return { y: -28 * k3, o: 1 - k3 };
1907
+ }
1908
+ function pathD(x0, y0, x1, y1, sea, phase, amp, k) {
1909
+ if (k >= 1) return "M" + x0 + " " + y0 + "L" + x1 + " " + y1;
1910
+ var dx = x1 - x0, dy = y1 - y0;
1911
+ var len = Math.hypot(dx, dy) || 1;
1912
+ var nx = -dy / len, ny = dx / len;
1913
+ var w = Math.sin(sea + phase) * amp * (1 - k);
1914
+ var w2 = Math.cos(sea * 0.71 + phase * 1.35) * amp * 0.62 * (1 - k);
1915
+ if (k > 0.72) {
1916
+ return "M" + x0 + " " + y0 + "L" + x1 + " " + y1;
1917
+ }
1918
+ return "M" + x0 + " " + y0 +
1919
+ "C" + (x0 + dx * 0.32 + nx * w) + " " + (y0 + dy * 0.32 + ny * w) + " " +
1920
+ (x0 + dx * 0.68 + nx * w2) + " " + (y0 + dy * 0.68 + ny * w2) + " " +
1921
+ x1 + " " + y1;
1922
+ }
1923
+
1924
+ function frame(now) {
1925
+ var p = ((now - t0) % BEAT) / BEAT;
1926
+ var k = tautK(p);
1927
+ var d = dropY(p);
1928
+ var sea = now / 900;
1929
+ if (drop) {
1930
+ drop.style.transform = "translateY(" + d.y + "px)";
1931
+ drop.style.opacity = String(d.o);
1932
+ }
1933
+ nodes.forEach(function (n, i) {
1934
+ if (!n.el) return;
1935
+ var ox = (1 - k) * (Math.sin(sea * 0.92 + n.phase) * 6.5 + Math.sin(sea * 1.55 + n.phase) * 2);
1936
+ var oy = (1 - k) * (Math.cos(sea * 0.78 + n.phase * 0.9) * 5.5);
1937
+ n.el.style.transform = "translate(" + ox + "px," + oy + "px)";
1938
+ n._x = n.x + ox;
1939
+ n._y = n.y + oy;
1940
+ });
1941
+ var ax = CX, ay = CY + d.y;
1942
+ paths.forEach(function (path, i) {
1943
+ var n = nodes[i];
1944
+ if (!n) return;
1945
+ path.setAttribute("d", pathD(ax, ay, n._x, n._y, sea, n.phase, 18, k));
1946
+ });
1947
+ if (tethers) {
1948
+ tethers.classList.toggle("is-taut", k > 0.72);
1949
+ tethers.style.strokeDasharray = k > 0.72 ? "none" : "2 " + (2 + 9 * (1 - k)).toFixed(1);
1950
+ tethers.style.strokeWidth = (0.7 + 0.2 * k).toFixed(2);
1951
+ }
1952
+ if (prevP < 0.24 && p >= 0.24) pingDropT = now;
1953
+ if (prevP < 0.46 && p >= 0.46) pingLockT = now;
1954
+ prevP = p;
1955
+ renderPing(pingDrop, pingDropT, now, 1800, 128, 0.55);
1956
+ renderPing(pingDrop2, pingDropT + 280, now, 1800, 128, 0.35);
1957
+ renderPing(pingLock, pingLockT, now, 2000, 152, 0.55);
1958
+ renderPing(pingLock2, pingLockT + 320, now, 2000, 152, 0.35);
1959
+ if (modeValue) {
1960
+ var m = modeFor(p);
1961
+ if (m[0] !== modeText) {
1962
+ modeText = m[0];
1963
+ modeValue.textContent = m[0];
1964
+ modeValue.setAttribute("fill", m[1]);
1965
+ }
1966
+ }
1967
+ requestAnimationFrame(frame);
1968
+ }
1969
+ requestAnimationFrame(frame);
1970
+ })();
1971
+
1972
+ if ("IntersectionObserver" in window) {
1973
+ var io = new IntersectionObserver(function (entries) {
1974
+ entries.forEach(function (entry) {
1975
+ if (entry.isIntersecting) entry.target.classList.add("is-in");
1976
+ });
1977
+ }, { threshold: 0, rootMargin: "80px 0px 0px 0px" });
1978
+ document.querySelectorAll("main section:not(.hero)").forEach(function (sec) { io.observe(sec); });
1979
+ } else {
1980
+ document.querySelectorAll("main section:not(.hero)").forEach(function (sec) { sec.classList.add("is-in"); });
1981
+ }
1982
+ })();
1983
+ </script>
1984
+ </body>
1985
+ </html>