@zigrivers/scaffold 3.29.0 → 3.31.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.
Files changed (86) hide show
  1. package/content/guides/AUTHORING.md +146 -0
  2. package/content/guides/cli/index.html +1855 -0
  3. package/content/guides/cli/index.md +206 -0
  4. package/content/guides/concepts/index.html +1970 -0
  5. package/content/guides/concepts/index.md +347 -0
  6. package/content/guides/dashboard/index.html +1913 -0
  7. package/content/guides/dashboard/index.md +264 -0
  8. package/content/guides/index.html +368 -15
  9. package/content/guides/install/.diagrams/diagram-0.svg +1 -0
  10. package/content/guides/install/.diagrams/manifest.json +3 -0
  11. package/content/guides/install/index.html +1653 -0
  12. package/content/guides/install/index.md +186 -0
  13. package/content/guides/knowledge/.diagrams/diagram-0.svg +1 -0
  14. package/content/guides/knowledge/.diagrams/manifest.json +3 -0
  15. package/content/guides/knowledge/index.html +1765 -0
  16. package/content/guides/knowledge/index.md +209 -0
  17. package/content/guides/knowledge-freshness/.diagrams/diagram-0.svg +1 -0
  18. package/content/guides/knowledge-freshness/.diagrams/manifest.json +3 -0
  19. package/content/guides/knowledge-freshness/index.html +2795 -0
  20. package/content/guides/knowledge-freshness/index.md +893 -0
  21. package/content/guides/mmr/index.html +407 -36
  22. package/content/guides/mmr/index.md +39 -16
  23. package/content/guides/multi-agent/.diagrams/diagram-0.svg +1 -0
  24. package/content/guides/multi-agent/.diagrams/manifest.json +3 -0
  25. package/content/guides/multi-agent/index.html +1715 -0
  26. package/content/guides/multi-agent/index.md +243 -0
  27. package/content/guides/observability/.diagrams/diagram-0.svg +1 -0
  28. package/content/guides/observability/.diagrams/diagram-1.svg +1 -0
  29. package/content/guides/observability/.diagrams/diagram-2.svg +1 -0
  30. package/content/guides/observability/.diagrams/diagram-3.svg +1 -0
  31. package/content/guides/observability/.diagrams/manifest.json +6 -0
  32. package/content/guides/observability/index.html +3257 -0
  33. package/content/guides/observability/index.md +1097 -0
  34. package/content/guides/pipeline/.diagrams/diagram-0.svg +1 -0
  35. package/content/guides/pipeline/.diagrams/diagram-1.svg +1 -0
  36. package/content/guides/pipeline/.diagrams/manifest.json +4 -0
  37. package/content/guides/pipeline/index.html +1973 -0
  38. package/content/guides/pipeline/index.md +387 -0
  39. package/content/guides/review-workflow/.diagrams/diagram-0.svg +1 -0
  40. package/content/guides/review-workflow/.diagrams/diagram-1.svg +1 -0
  41. package/content/guides/review-workflow/.diagrams/manifest.json +4 -0
  42. package/content/guides/review-workflow/index.html +1790 -0
  43. package/content/guides/review-workflow/index.md +248 -0
  44. package/dist/guides/build.d.ts +1 -1
  45. package/dist/guides/build.d.ts.map +1 -1
  46. package/dist/guides/build.js +21 -9
  47. package/dist/guides/build.js.map +1 -1
  48. package/dist/guides/build.test.js +47 -0
  49. package/dist/guides/build.test.js.map +1 -1
  50. package/dist/guides/chrome.d.ts.map +1 -1
  51. package/dist/guides/chrome.js +83 -12
  52. package/dist/guides/chrome.js.map +1 -1
  53. package/dist/guides/dashboard-theme.css +8 -0
  54. package/dist/guides/directives-cite.test.d.ts +2 -0
  55. package/dist/guides/directives-cite.test.d.ts.map +1 -0
  56. package/dist/guides/directives-cite.test.js +26 -0
  57. package/dist/guides/directives-cite.test.js.map +1 -0
  58. package/dist/guides/directives-tabs.test.js +47 -0
  59. package/dist/guides/directives-tabs.test.js.map +1 -1
  60. package/dist/guides/directives.d.ts +1 -0
  61. package/dist/guides/directives.d.ts.map +1 -1
  62. package/dist/guides/directives.js +38 -0
  63. package/dist/guides/directives.js.map +1 -1
  64. package/dist/guides/guides.css +268 -0
  65. package/dist/guides/index-page.d.ts.map +1 -1
  66. package/dist/guides/index-page.js +41 -8
  67. package/dist/guides/index-page.js.map +1 -1
  68. package/dist/guides/links.d.ts +14 -0
  69. package/dist/guides/links.d.ts.map +1 -0
  70. package/dist/guides/links.js +56 -0
  71. package/dist/guides/links.js.map +1 -0
  72. package/dist/guides/links.test.d.ts +2 -0
  73. package/dist/guides/links.test.d.ts.map +1 -0
  74. package/dist/guides/links.test.js +72 -0
  75. package/dist/guides/links.test.js.map +1 -0
  76. package/dist/guides/render.d.ts +1 -0
  77. package/dist/guides/render.d.ts.map +1 -1
  78. package/dist/guides/render.js +1 -1
  79. package/dist/guides/render.js.map +1 -1
  80. package/dist/guides/sanitize.d.ts.map +1 -1
  81. package/dist/guides/sanitize.js +5 -0
  82. package/dist/guides/sanitize.js.map +1 -1
  83. package/dist/guides/template.d.ts.map +1 -1
  84. package/dist/guides/template.js +7 -2
  85. package/dist/guides/template.js.map +1 -1
  86. package/package.json +2 -2
@@ -0,0 +1,2795 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-chrome-version="1">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Knowledge Freshness</title>
7
+ <!-- scaffold:chrome v1 -->
8
+ <style>/* Scaffold Dashboard Theme
9
+ * All CSS for the generated pipeline dashboard.
10
+ * Embedded into HTML by scripts/generate-dashboard.sh.
11
+ * Design system reference: docs/design-system.md
12
+ *
13
+ * Aesthetic: "Precision Industrial" — Swiss-typographic control room.
14
+ * Deep navy dark mode with indigo accents, clean cool-white light mode.
15
+ */
16
+
17
+ /* ─── Design Tokens (Light Mode) ──────────────── */
18
+ :root {
19
+ /* Surface */
20
+ --bg: #f5f6fa;
21
+ --bg-card: #ffffff;
22
+ --bg-hover: #eef0f6;
23
+ --bg-inset: #e8eaf2;
24
+
25
+ /* Text */
26
+ --text: #1a1d2e;
27
+ --text-muted: #6b7294;
28
+ --text-faint: #9ba1c0;
29
+
30
+ /* Borders & Structure */
31
+ --border: #dde0ed;
32
+ --border-light: #eceef5;
33
+ --radius: 10px;
34
+ --radius-sm: 6px;
35
+
36
+ /* Accent */
37
+ --accent: #4f46e5;
38
+ --accent-hover: #4338ca;
39
+ --accent-glow: rgba(79, 70, 229, 0.10);
40
+
41
+ /* Semantic: Status */
42
+ --green: #059669;
43
+ --green-bg: #ecfdf5;
44
+ --green-border: #a7f3d0;
45
+ --blue: #2563eb;
46
+ --blue-bg: #eff6ff;
47
+ --blue-border: #bfdbfe;
48
+ --yellow: #d97706;
49
+ --yellow-bg: #fffbeb;
50
+ --yellow-border:#fde68a;
51
+ --red: #dc2626;
52
+ --red-bg: #fef2f2;
53
+ --red-border: #fecaca;
54
+ --gray: #9ca3af;
55
+ --gray-bg: #f3f4f6;
56
+ --gray-border: #e5e7eb;
57
+ --scrim: rgba(15, 17, 23, 0.45);
58
+
59
+ /* Semantic: Next Banner */
60
+ --next-bg: #eef2ff;
61
+ --next-border: #4f46e5;
62
+ --next-glow: rgba(79, 70, 229, 0.06);
63
+
64
+ /* Semantic: Progress */
65
+ --progress-bg: #e5e7eb;
66
+ --progress-h: 10px;
67
+
68
+ /* Depth */
69
+ --shadow-sm: 0 1px 2px rgba(30, 34, 60, 0.04);
70
+ --shadow: 0 1px 3px rgba(30, 34, 60, 0.07), 0 1px 2px rgba(30, 34, 60, 0.04);
71
+ --shadow-md: 0 4px 12px rgba(30, 34, 60, 0.08), 0 1px 3px rgba(30, 34, 60, 0.05);
72
+ --shadow-lg: 0 8px 24px rgba(30, 34, 60, 0.10), 0 2px 6px rgba(30, 34, 60, 0.04);
73
+
74
+ /* Spacing scale (4px base) */
75
+ --sp-1: 4px;
76
+ --sp-2: 8px;
77
+ --sp-3: 12px;
78
+ --sp-4: 16px;
79
+ --sp-5: 20px;
80
+ --sp-6: 24px;
81
+ --sp-8: 32px;
82
+ --sp-10: 40px;
83
+
84
+ /* Typography */
85
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
86
+ --font-mono: "SF Mono", "Cascadia Code", "Fira Code", "JetBrains Mono", Menlo, Consolas, monospace;
87
+ --text-xs: 0.75rem;
88
+ --text-sm: 0.8125rem;
89
+ --text-base: 0.9375rem;
90
+ --text-lg: 1.125rem;
91
+ --text-xl: 1.375rem;
92
+ --text-2xl: 1.75rem;
93
+ --lh-tight: 1.25;
94
+ --lh-normal: 1.5;
95
+ --lh-relaxed: 1.625;
96
+ --ls-tight: -0.01em;
97
+ --ls-wide: 0.025em;
98
+ --fw-normal: 400;
99
+ --fw-medium: 500;
100
+ --fw-semi: 600;
101
+ --fw-bold: 700;
102
+
103
+ /* Layout */
104
+ --max-w: 960px;
105
+ --page-pad: 24px;
106
+ }
107
+
108
+ /* ─── Design Tokens (Dark Mode) ───────────────── */
109
+ [data-theme="dark"] {
110
+ /* Surface */
111
+ --bg: #0f1117;
112
+ --bg-card: #1a1d2e;
113
+ --bg-hover: #252940;
114
+ --bg-inset: #141724;
115
+
116
+ /* Text */
117
+ --text: #e2e5f0;
118
+ --text-muted: #7c82a8;
119
+ --text-faint: #555c80;
120
+
121
+ /* Borders & Structure */
122
+ --border: #2a2f45;
123
+ --border-light: #21253a;
124
+
125
+ /* Accent */
126
+ --accent: #818cf8;
127
+ --accent-hover: #a5b4fc;
128
+ --accent-glow: rgba(129, 140, 248, 0.12);
129
+
130
+ /* Semantic: Status */
131
+ --green: #34d399;
132
+ --green-bg: rgba(6, 78, 59, 0.25);
133
+ --green-border: rgba(52, 211, 153, 0.25);
134
+ --blue: #60a5fa;
135
+ --blue-bg: rgba(30, 58, 95, 0.30);
136
+ --blue-border: rgba(96, 165, 250, 0.25);
137
+ --yellow: #fbbf24;
138
+ --yellow-bg: rgba(120, 53, 15, 0.25);
139
+ --yellow-border:rgba(251, 191, 36, 0.20);
140
+ --red: #f87171;
141
+ --red-bg: rgba(127, 29, 29, 0.25);
142
+ --red-border: rgba(248, 113, 113, 0.22);
143
+ --gray: #6b7294;
144
+ --gray-bg: #252940;
145
+ --gray-border: #363c58;
146
+ --scrim: rgba(0, 0, 0, 0.6);
147
+
148
+ /* Semantic: Next Banner */
149
+ --next-bg: rgba(30, 27, 75, 0.50);
150
+ --next-border: #818cf8;
151
+ --next-glow: rgba(129, 140, 248, 0.08);
152
+
153
+ /* Semantic: Progress */
154
+ --progress-bg: #1f2337;
155
+
156
+ /* Depth */
157
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.20);
158
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.30), 0 1px 2px rgba(0, 0, 0, 0.15);
159
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35), 0 1px 3px rgba(0, 0, 0, 0.20);
160
+ --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.40), 0 2px 6px rgba(0, 0, 0, 0.20);
161
+ }
162
+
163
+ /* ─── Theme Toggle ───────────────────────────── */
164
+ .theme-toggle {
165
+ background: var(--bg-inset);
166
+ border: 1px solid var(--border);
167
+ border-radius: var(--radius-sm);
168
+ padding: var(--sp-1) var(--sp-2);
169
+ cursor: pointer;
170
+ font-size: var(--text-base);
171
+ line-height: 1;
172
+ color: var(--text-muted);
173
+ transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
174
+ display: flex;
175
+ align-items: center;
176
+ margin-left: auto;
177
+ }
178
+
179
+ .theme-toggle:hover {
180
+ border-color: var(--accent);
181
+ color: var(--accent);
182
+ background: var(--accent-glow);
183
+ }
184
+
185
+ /* ─── Base ────────────────────────────────────── */
186
+ *, *::before, *::after {
187
+ margin: 0;
188
+ padding: 0;
189
+ box-sizing: border-box;
190
+ }
191
+
192
+ body {
193
+ font-family: var(--font-sans);
194
+ font-size: var(--text-base);
195
+ line-height: var(--lh-normal);
196
+ color: var(--text);
197
+ background: var(--bg);
198
+ -webkit-font-smoothing: antialiased;
199
+ -moz-osx-font-smoothing: grayscale;
200
+ }
201
+
202
+ /* ─── Layout ──────────────────────────────────── */
203
+ .wrap {
204
+ max-width: var(--max-w);
205
+ margin: 0 auto;
206
+ padding: var(--sp-8) var(--page-pad);
207
+ }
208
+
209
+ /* ─── Header ──────────────────────────────────── */
210
+ .header {
211
+ display: flex;
212
+ align-items: baseline;
213
+ gap: var(--sp-3);
214
+ margin-bottom: var(--sp-2);
215
+ flex-wrap: wrap;
216
+ }
217
+
218
+ h1 {
219
+ font-size: var(--text-2xl);
220
+ font-weight: var(--fw-bold);
221
+ letter-spacing: var(--ls-tight);
222
+ line-height: var(--lh-tight);
223
+ }
224
+
225
+ h2 {
226
+ font-size: var(--text-lg);
227
+ font-weight: var(--fw-semi);
228
+ letter-spacing: var(--ls-tight);
229
+ line-height: var(--lh-tight);
230
+ margin-bottom: var(--sp-3);
231
+ }
232
+
233
+ .header-meta {
234
+ font-size: var(--text-xs);
235
+ color: var(--text-faint);
236
+ margin-bottom: var(--sp-6);
237
+ letter-spacing: var(--ls-wide);
238
+ text-transform: uppercase;
239
+ }
240
+
241
+ /* ─── Badge ───────────────────────────────────── */
242
+ .badge {
243
+ display: inline-block;
244
+ padding: 2px var(--sp-2);
245
+ border-radius: 99px;
246
+ font-size: var(--text-xs);
247
+ font-weight: var(--fw-semi);
248
+ letter-spacing: var(--ls-wide);
249
+ background: var(--accent);
250
+ color: #fff;
251
+ text-transform: uppercase;
252
+ }
253
+
254
+ .badge-optional {
255
+ background: var(--yellow-bg);
256
+ color: var(--yellow);
257
+ border: 1px solid var(--yellow-border);
258
+ }
259
+
260
+ /* ─── Progress Bar ────────────────────────────── */
261
+ .progress-bar {
262
+ width: 100%;
263
+ height: var(--progress-h);
264
+ background: var(--progress-bg);
265
+ border-radius: 99px;
266
+ overflow: hidden;
267
+ margin-bottom: var(--sp-6);
268
+ display: flex;
269
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.06);
270
+ }
271
+
272
+ .progress-bar .seg-done {
273
+ background: linear-gradient(135deg, var(--green), #10b981);
274
+ box-shadow: 0 0 8px rgba(5, 150, 105, 0.3);
275
+ }
276
+
277
+ .progress-bar .seg-likely {
278
+ background: linear-gradient(135deg, var(--blue), #3b82f6);
279
+ box-shadow: 0 0 8px rgba(37, 99, 235, 0.25);
280
+ }
281
+
282
+ .progress-bar .seg-skip {
283
+ background: var(--gray);
284
+ opacity: 0.7;
285
+ }
286
+
287
+ /* ─── Summary Cards ───────────────────────────── */
288
+ .cards {
289
+ display: grid;
290
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
291
+ gap: var(--sp-3);
292
+ margin-bottom: var(--sp-6);
293
+ }
294
+
295
+ .card {
296
+ background: var(--bg-card);
297
+ border: 1px solid var(--border);
298
+ border-radius: var(--radius);
299
+ padding: var(--sp-4) var(--sp-5);
300
+ box-shadow: var(--shadow);
301
+ transition: box-shadow 0.15s ease, transform 0.15s ease;
302
+ }
303
+
304
+ .card:hover {
305
+ box-shadow: var(--shadow-md);
306
+ transform: translateY(-1px);
307
+ }
308
+
309
+ .card-num {
310
+ font-size: var(--text-2xl);
311
+ font-weight: var(--fw-bold);
312
+ font-family: var(--font-mono);
313
+ letter-spacing: var(--ls-tight);
314
+ line-height: 1;
315
+ }
316
+
317
+ .card-lbl {
318
+ font-size: var(--text-xs);
319
+ color: var(--text-muted);
320
+ margin-top: var(--sp-1);
321
+ letter-spacing: var(--ls-wide);
322
+ text-transform: uppercase;
323
+ font-weight: var(--fw-medium);
324
+ }
325
+
326
+ /* ─── What's Next Banner ──────────────────────── */
327
+ .next-banner {
328
+ background: var(--next-bg);
329
+ border: 1px solid var(--next-border);
330
+ border-left: 4px solid var(--next-border);
331
+ border-radius: var(--radius);
332
+ padding: var(--sp-5) var(--sp-6);
333
+ margin-bottom: var(--sp-6);
334
+ box-shadow: 0 0 0 1px var(--next-glow), var(--shadow);
335
+ position: relative;
336
+ overflow: hidden;
337
+ }
338
+
339
+ .next-banner::before {
340
+ content: "";
341
+ position: absolute;
342
+ top: 0;
343
+ left: 0;
344
+ width: 4px;
345
+ height: 100%;
346
+ background: var(--next-border);
347
+ animation: pulse-border 2.5s ease-in-out infinite;
348
+ }
349
+
350
+ @keyframes pulse-border {
351
+ 0%, 100% { opacity: 1; }
352
+ 50% { opacity: 0.5; }
353
+ }
354
+
355
+ .next-banner h2 {
356
+ color: var(--accent);
357
+ margin-bottom: var(--sp-1);
358
+ font-size: var(--text-base);
359
+ font-weight: var(--fw-semi);
360
+ letter-spacing: var(--ls-wide);
361
+ text-transform: uppercase;
362
+ }
363
+
364
+ .next-banner p {
365
+ color: var(--text);
366
+ font-size: var(--text-base);
367
+ }
368
+
369
+ .next-cmd {
370
+ font-family: var(--font-mono);
371
+ background: var(--bg-card);
372
+ padding: var(--sp-1) var(--sp-3);
373
+ border-radius: var(--radius-sm);
374
+ font-size: var(--text-sm);
375
+ display: inline-flex;
376
+ align-items: center;
377
+ gap: var(--sp-2);
378
+ margin-top: var(--sp-3);
379
+ border: 1px solid var(--border);
380
+ }
381
+
382
+ /* ─── Phase Headers (Collapsible) ─────────────── */
383
+ .phase {
384
+ margin-bottom: var(--sp-6);
385
+ }
386
+
387
+ .phase-hdr {
388
+ display: flex;
389
+ align-items: center;
390
+ gap: var(--sp-2);
391
+ cursor: pointer;
392
+ padding: var(--sp-2) 0;
393
+ user-select: none;
394
+ border-bottom: 2px solid var(--border);
395
+ margin-bottom: var(--sp-3);
396
+ transition: border-color 0.15s ease;
397
+ }
398
+
399
+ .phase-hdr:hover {
400
+ border-bottom-color: var(--accent);
401
+ }
402
+
403
+ .phase-hdr:hover h2 {
404
+ color: var(--accent);
405
+ }
406
+
407
+ .phase-hdr h2 {
408
+ transition: color 0.15s ease;
409
+ }
410
+
411
+ .phase-hdr .arr {
412
+ transition: transform 0.2s ease;
413
+ font-size: var(--text-xs);
414
+ color: var(--text-muted);
415
+ }
416
+
417
+ .phase-hdr.closed .arr {
418
+ transform: rotate(-90deg);
419
+ }
420
+
421
+ .phase-cnt {
422
+ font-size: var(--text-xs);
423
+ font-family: var(--font-mono);
424
+ color: var(--text-faint);
425
+ margin-left: auto;
426
+ letter-spacing: var(--ls-wide);
427
+ }
428
+
429
+ /* ─── Prompt List ─────────────────────────────── */
430
+ .plist {
431
+ display: flex;
432
+ flex-direction: column;
433
+ gap: var(--sp-2);
434
+ }
435
+
436
+ /* ─── Prompt Cards ────────────────────────────── */
437
+ .pcard {
438
+ background: var(--bg-card);
439
+ border: 1px solid var(--border);
440
+ border-radius: var(--radius);
441
+ padding: var(--sp-3) var(--sp-4);
442
+ box-shadow: var(--shadow-sm);
443
+ display: grid;
444
+ grid-template-columns: auto 1fr auto;
445
+ gap: var(--sp-2) var(--sp-3);
446
+ align-items: start;
447
+ transition: box-shadow 0.15s ease, transform 0.15s ease, border-color 0.15s ease;
448
+ }
449
+
450
+ .pcard:hover {
451
+ box-shadow: var(--shadow);
452
+ transform: translateY(-1px);
453
+ border-color: var(--accent-glow);
454
+ }
455
+
456
+ /* ─── Status Badges ──────────────────────────── */
457
+ .status-badge {
458
+ display: inline-flex;
459
+ align-items: center;
460
+ gap: var(--sp-1);
461
+ font-size: var(--text-xs);
462
+ font-weight: var(--fw-medium);
463
+ padding: 2px var(--sp-2);
464
+ border-radius: 99px;
465
+ white-space: nowrap;
466
+ flex-shrink: 0;
467
+ letter-spacing: var(--ls-wide);
468
+ line-height: var(--lh-tight);
469
+ }
470
+
471
+ .st-completed {
472
+ background: var(--green-bg);
473
+ color: var(--green);
474
+ border: 1px solid var(--green-border);
475
+ }
476
+
477
+ .st-likely-completed {
478
+ background: var(--blue-bg);
479
+ color: var(--blue);
480
+ border: 1px solid var(--blue-border);
481
+ }
482
+
483
+ .st-skipped {
484
+ background: var(--gray-bg);
485
+ color: var(--gray);
486
+ border: 1px solid var(--gray-border);
487
+ }
488
+
489
+ .st-pending {
490
+ background: var(--bg-inset);
491
+ color: var(--text-faint);
492
+ border: 1px solid var(--border);
493
+ }
494
+
495
+ /* ─── Status Legend ──────────────────────────── */
496
+ .status-legend {
497
+ display: flex;
498
+ flex-wrap: wrap;
499
+ gap: var(--sp-2);
500
+ margin-bottom: var(--sp-4);
501
+ padding: var(--sp-2) 0;
502
+ }
503
+
504
+ .status-legend .status-badge {
505
+ cursor: default;
506
+ }
507
+
508
+ /* ─── Prompt Card Inner ───────────────────────── */
509
+ .pinfo {
510
+ min-width: 0;
511
+ }
512
+
513
+ .pname {
514
+ font-weight: var(--fw-semi);
515
+ font-size: var(--text-base);
516
+ }
517
+
518
+ .pstep {
519
+ font-size: var(--text-xs);
520
+ font-family: var(--font-mono);
521
+ color: var(--text-faint);
522
+ letter-spacing: var(--ls-wide);
523
+ }
524
+
525
+ .pdesc {
526
+ font-size: var(--text-sm);
527
+ color: var(--text-muted);
528
+ margin-top: 2px;
529
+ line-height: var(--lh-relaxed);
530
+ }
531
+
532
+ .pdesc-long {
533
+ font-size: var(--text-xs);
534
+ color: var(--text-faint);
535
+ margin-top: 2px;
536
+ }
537
+
538
+ .pdeps {
539
+ font-size: var(--text-xs);
540
+ color: var(--yellow);
541
+ margin-top: var(--sp-1);
542
+ font-weight: var(--fw-medium);
543
+ }
544
+
545
+ /* ─── Copy Command Button ─────────────────────── */
546
+ .pcmd {
547
+ font-family: var(--font-mono);
548
+ font-size: var(--text-xs);
549
+ background: var(--bg-inset);
550
+ padding: 3px var(--sp-2);
551
+ border-radius: var(--radius-sm);
552
+ cursor: pointer;
553
+ border: 1px solid var(--border);
554
+ white-space: nowrap;
555
+ align-self: center;
556
+ color: var(--text-muted);
557
+ transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
558
+ letter-spacing: var(--ls-wide);
559
+ }
560
+
561
+ .pcmd:hover {
562
+ border-color: var(--accent);
563
+ color: var(--accent);
564
+ background: var(--accent-glow);
565
+ }
566
+
567
+ .pcmd.copied {
568
+ border-color: var(--green);
569
+ color: var(--green);
570
+ background: var(--green-bg);
571
+ }
572
+
573
+ /* ─── Prompt Modal ────────────────────────────── */
574
+ .modal-overlay {
575
+ position: fixed;
576
+ inset: 0;
577
+ background: rgba(0, 0, 0, 0.6);
578
+ display: flex;
579
+ align-items: center;
580
+ justify-content: center;
581
+ z-index: 1000;
582
+ padding: var(--sp-4);
583
+ }
584
+
585
+ .modal {
586
+ background: var(--bg-card);
587
+ border: 1px solid var(--border);
588
+ border-radius: var(--radius);
589
+ box-shadow: var(--shadow-lg);
590
+ max-width: 720px;
591
+ width: 100%;
592
+ max-height: 85vh;
593
+ display: flex;
594
+ flex-direction: column;
595
+ }
596
+
597
+ .modal-header {
598
+ display: flex;
599
+ align-items: center;
600
+ gap: var(--sp-3);
601
+ padding: var(--sp-4) var(--sp-5);
602
+ border-bottom: 1px solid var(--border);
603
+ flex-shrink: 0;
604
+ }
605
+
606
+ .modal-header h3 {
607
+ font-size: var(--text-lg);
608
+ font-weight: var(--fw-semi);
609
+ flex: 1;
610
+ min-width: 0;
611
+ }
612
+
613
+ .modal-close {
614
+ background: var(--bg-inset);
615
+ border: 1px solid var(--border);
616
+ border-radius: var(--radius-sm);
617
+ padding: var(--sp-1) var(--sp-2);
618
+ cursor: pointer;
619
+ font-size: var(--text-base);
620
+ color: var(--text-muted);
621
+ line-height: 1;
622
+ transition: border-color 0.15s ease, color 0.15s ease;
623
+ }
624
+
625
+ .modal-close:hover {
626
+ border-color: var(--accent);
627
+ color: var(--accent);
628
+ }
629
+
630
+ .modal-body {
631
+ padding: var(--sp-5);
632
+ overflow-y: auto;
633
+ flex: 1;
634
+ }
635
+
636
+ .modal-body pre {
637
+ font-family: var(--font-mono);
638
+ font-size: var(--text-sm);
639
+ line-height: var(--lh-relaxed);
640
+ white-space: pre-wrap;
641
+ word-break: break-word;
642
+ color: var(--text);
643
+ }
644
+
645
+ .modal-body pre .md-heading {
646
+ font-weight: var(--fw-bold);
647
+ color: var(--accent);
648
+ }
649
+
650
+ .modal-body pre .md-code {
651
+ background: var(--bg-inset);
652
+ padding: 1px 4px;
653
+ border-radius: 3px;
654
+ font-size: var(--text-xs);
655
+ }
656
+
657
+ .modal-footer {
658
+ display: flex;
659
+ gap: var(--sp-2);
660
+ padding: var(--sp-3) var(--sp-5);
661
+ border-top: 1px solid var(--border);
662
+ flex-shrink: 0;
663
+ }
664
+
665
+ .modal-copy-btn {
666
+ background: var(--accent);
667
+ color: #fff;
668
+ border: none;
669
+ border-radius: var(--radius-sm);
670
+ padding: var(--sp-2) var(--sp-4);
671
+ font-size: var(--text-sm);
672
+ font-weight: var(--fw-medium);
673
+ cursor: pointer;
674
+ transition: background 0.15s ease;
675
+ }
676
+
677
+ .modal-copy-btn:hover {
678
+ background: var(--accent-hover);
679
+ }
680
+
681
+ .modal-copy-btn.copied {
682
+ background: var(--green);
683
+ }
684
+
685
+ /* ─── Beads Task Section ─────────────────────── */
686
+ .beads-section {
687
+ margin-top: var(--sp-8);
688
+ margin-bottom: var(--sp-6);
689
+ }
690
+
691
+ .beads-filters {
692
+ display: flex;
693
+ gap: var(--sp-2);
694
+ margin-bottom: var(--sp-3);
695
+ }
696
+
697
+ .beads-filter {
698
+ background: var(--bg-inset);
699
+ border: 1px solid var(--border);
700
+ border-radius: 99px;
701
+ padding: var(--sp-1) var(--sp-3);
702
+ font-size: var(--text-xs);
703
+ font-weight: var(--fw-medium);
704
+ color: var(--text-muted);
705
+ cursor: pointer;
706
+ transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
707
+ letter-spacing: var(--ls-wide);
708
+ }
709
+
710
+ .beads-filter:hover {
711
+ border-color: var(--accent);
712
+ color: var(--accent);
713
+ }
714
+
715
+ .beads-filter.active {
716
+ background: var(--accent);
717
+ color: #fff;
718
+ border-color: var(--accent);
719
+ }
720
+
721
+ /* ─── Beads Status Badges ────────────────────── */
722
+ .st-bead-open {
723
+ background: var(--accent-glow);
724
+ color: var(--accent);
725
+ border: 1px solid var(--accent);
726
+ }
727
+
728
+ .st-bead-progress {
729
+ background: var(--blue-bg);
730
+ color: var(--blue);
731
+ border: 1px solid var(--blue-border);
732
+ }
733
+
734
+ .st-bead-blocked {
735
+ background: var(--yellow-bg);
736
+ color: var(--yellow);
737
+ border: 1px solid var(--yellow-border);
738
+ }
739
+
740
+ .st-bead-deferred {
741
+ background: var(--gray-bg);
742
+ color: var(--gray);
743
+ border: 1px solid var(--gray-border);
744
+ }
745
+
746
+ .st-bead-closed {
747
+ background: var(--green-bg);
748
+ color: var(--green);
749
+ border: 1px solid var(--green-border);
750
+ }
751
+
752
+ /* ─── Beads Filter Separator ─────────────────── */
753
+ .beads-filter-sep {
754
+ width: 1px;
755
+ background: var(--border);
756
+ align-self: stretch;
757
+ margin: 0 var(--sp-1);
758
+ }
759
+
760
+ /* ─── Beads Priority Filter ──────────────────── */
761
+ .beads-prio-filter {
762
+ background: var(--bg-inset);
763
+ border: 1px solid var(--border);
764
+ border-radius: 99px;
765
+ padding: var(--sp-1) var(--sp-3);
766
+ font-size: var(--text-xs);
767
+ font-weight: var(--fw-medium);
768
+ color: var(--text-muted);
769
+ cursor: pointer;
770
+ transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
771
+ letter-spacing: var(--ls-wide);
772
+ }
773
+
774
+ .beads-prio-filter:hover {
775
+ border-color: var(--accent);
776
+ color: var(--accent);
777
+ }
778
+
779
+ .beads-prio-filter.active {
780
+ background: var(--accent);
781
+ color: #fff;
782
+ border-color: var(--accent);
783
+ }
784
+
785
+ /* ─── Beads Task Detail Modal ────────────────── */
786
+ .bead-meta-grid {
787
+ display: grid;
788
+ grid-template-columns: 1fr 1fr;
789
+ gap: var(--sp-3);
790
+ padding: var(--sp-4) 0;
791
+ border-bottom: 1px solid var(--border-light);
792
+ }
793
+
794
+ .bead-meta-item {
795
+ display: flex;
796
+ flex-direction: column;
797
+ gap: 2px;
798
+ }
799
+
800
+ .bead-meta-label {
801
+ font-size: var(--text-xs);
802
+ color: var(--text-faint);
803
+ text-transform: uppercase;
804
+ letter-spacing: var(--ls-wide);
805
+ font-weight: var(--fw-medium);
806
+ }
807
+
808
+ .bead-meta-value {
809
+ font-size: var(--text-sm);
810
+ font-weight: var(--fw-medium);
811
+ color: var(--text);
812
+ }
813
+
814
+ .bead-description {
815
+ padding: var(--sp-4) 0;
816
+ border-bottom: 1px solid var(--border-light);
817
+ white-space: pre-wrap;
818
+ font-size: var(--text-sm);
819
+ line-height: var(--lh-relaxed);
820
+ color: var(--text-muted);
821
+ }
822
+
823
+ .bead-deps {
824
+ padding: var(--sp-4) 0;
825
+ border-bottom: 1px solid var(--border-light);
826
+ }
827
+
828
+ .bead-dep-group {
829
+ display: flex;
830
+ flex-wrap: wrap;
831
+ align-items: center;
832
+ gap: var(--sp-2);
833
+ margin-bottom: var(--sp-2);
834
+ }
835
+
836
+ .bead-dep-group:last-child {
837
+ margin-bottom: 0;
838
+ }
839
+
840
+ .bead-dep-label {
841
+ font-size: var(--text-xs);
842
+ color: var(--text-faint);
843
+ text-transform: uppercase;
844
+ letter-spacing: var(--ls-wide);
845
+ font-weight: var(--fw-medium);
846
+ min-width: 80px;
847
+ }
848
+
849
+ .bead-dep-link {
850
+ display: inline-block;
851
+ font-family: var(--font-mono);
852
+ font-size: var(--text-xs);
853
+ padding: 2px var(--sp-2);
854
+ border-radius: 99px;
855
+ background: var(--accent-glow);
856
+ color: var(--accent);
857
+ border: 1px solid var(--accent);
858
+ cursor: pointer;
859
+ transition: background 0.15s ease, color 0.15s ease;
860
+ text-decoration: none;
861
+ }
862
+
863
+ .bead-dep-link:hover {
864
+ background: var(--accent);
865
+ color: #fff;
866
+ }
867
+
868
+ .bead-timestamps {
869
+ display: flex;
870
+ flex-wrap: wrap;
871
+ gap: var(--sp-4);
872
+ padding: var(--sp-4) 0;
873
+ }
874
+
875
+ .bead-ts-item {
876
+ display: flex;
877
+ flex-direction: column;
878
+ gap: 2px;
879
+ }
880
+
881
+ .bead-ts-label {
882
+ font-size: var(--text-xs);
883
+ color: var(--text-faint);
884
+ text-transform: uppercase;
885
+ letter-spacing: var(--ls-wide);
886
+ font-weight: var(--fw-medium);
887
+ }
888
+
889
+ .bead-ts-value {
890
+ font-size: var(--text-sm);
891
+ color: var(--text-muted);
892
+ }
893
+
894
+ .bead-ts-value[title] {
895
+ border-bottom: 1px dotted var(--text-faint);
896
+ cursor: help;
897
+ }
898
+
899
+ /* ─── Standalone Commands Section ─────────────── */
900
+ .ongoing {
901
+ margin-top: var(--sp-10);
902
+ }
903
+
904
+ .ongoing h2 {
905
+ letter-spacing: var(--ls-wide);
906
+ text-transform: uppercase;
907
+ font-size: var(--text-sm);
908
+ color: var(--text-muted);
909
+ margin-bottom: var(--sp-4);
910
+ border-bottom: 2px solid var(--border);
911
+ padding-bottom: var(--sp-2);
912
+ }
913
+
914
+ /* ─── Footer ──────────────────────────────────── */
915
+ .footer {
916
+ text-align: center;
917
+ font-size: var(--text-xs);
918
+ color: var(--text-faint);
919
+ margin-top: var(--sp-10);
920
+ padding-top: var(--sp-4);
921
+ border-top: 1px solid var(--border-light);
922
+ letter-spacing: var(--ls-wide);
923
+ }
924
+
925
+ /* ─── Utilities ───────────────────────────────── */
926
+ .hidden {
927
+ display: none;
928
+ }
929
+
930
+ /* Build-observability severity + verdict tokens (Plan 4) */
931
+ :root {
932
+ --sev-p0: #dc2626; /* red 600 */
933
+ --sev-p1: #ea580c; /* orange 600 */
934
+ --sev-p2: #ca8a04; /* yellow 600 */
935
+ --sev-p3: #2563eb; /* blue 600 */
936
+ --sev-pass: #16a34a; /* green 600 */
937
+ }
938
+ [data-theme="dark"] {
939
+ --sev-p0: #f87171;
940
+ --sev-p1: #fb923c;
941
+ --sev-p2: #facc15;
942
+ --sev-p3: #60a5fa;
943
+ --sev-pass: #4ade80;
944
+ }
945
+
946
+ /* Build-observability panel layout */
947
+ .panel {
948
+ background: var(--bg-card);
949
+ border: 1px solid var(--border);
950
+ border-radius: var(--radius);
951
+ padding: var(--sp-4) var(--sp-6);
952
+ margin-bottom: var(--sp-6);
953
+ }
954
+ .panel > header {
955
+ display: flex;
956
+ align-items: center;
957
+ gap: var(--sp-3);
958
+ margin-bottom: var(--sp-4);
959
+ flex-wrap: wrap;
960
+ }
961
+ .panel > header h2 {
962
+ margin: 0;
963
+ font-size: var(--text-base);
964
+ font-weight: var(--fw-semi);
965
+ }
966
+ .panel .meta {
967
+ color: var(--text-muted);
968
+ font-size: var(--text-sm);
969
+ }
970
+ .grid { display: grid; gap: var(--sp-4); }
971
+ .grid-2 { grid-template-columns: repeat(2, 1fr); }
972
+ @media (max-width: 640px) { .grid-2 { grid-template-columns: 1fr; } }
973
+
974
+ /* Finding filters */
975
+ .finding-filters {
976
+ display: flex;
977
+ gap: var(--sp-2);
978
+ flex-wrap: wrap;
979
+ margin-bottom: var(--sp-4);
980
+ }
981
+ .finding-filters button {
982
+ padding: var(--sp-1) var(--sp-3);
983
+ border: 1px solid var(--border);
984
+ border-radius: var(--radius-sm);
985
+ background: var(--bg-inset);
986
+ color: var(--text);
987
+ font-size: var(--text-sm);
988
+ cursor: pointer;
989
+ }
990
+ .finding-filters button:hover,
991
+ .finding-filters button.active {
992
+ background: var(--accent);
993
+ border-color: var(--accent);
994
+ color: #fff;
995
+ }
996
+
997
+ /* Findings list */
998
+ .findings {
999
+ list-style: none;
1000
+ padding: 0;
1001
+ margin: 0;
1002
+ display: flex;
1003
+ flex-direction: column;
1004
+ gap: var(--sp-3);
1005
+ }
1006
+ .finding {
1007
+ background: var(--bg-inset);
1008
+ border: 1px solid var(--border-light);
1009
+ border-radius: var(--radius-sm);
1010
+ padding: var(--sp-3) var(--sp-4);
1011
+ }
1012
+ .finding header {
1013
+ display: flex;
1014
+ align-items: center;
1015
+ gap: var(--sp-2);
1016
+ flex-wrap: wrap;
1017
+ margin-bottom: var(--sp-2);
1018
+ }
1019
+ .finding-id {
1020
+ font-family: var(--font-mono, monospace);
1021
+ font-size: var(--text-xs);
1022
+ color: var(--text-muted);
1023
+ background: var(--bg-card);
1024
+ border: 1px solid var(--border);
1025
+ border-radius: 4px;
1026
+ padding: 1px var(--sp-1);
1027
+ }
1028
+ .finding .lens {
1029
+ font-size: var(--text-xs);
1030
+ color: var(--text-muted);
1031
+ }
1032
+ .finding .title {
1033
+ font-size: var(--text-sm);
1034
+ font-weight: var(--fw-semi);
1035
+ flex: 1;
1036
+ }
1037
+ .finding p { margin: 0; font-size: var(--text-sm); color: var(--text-muted); }
1038
+ .empty { color: var(--text-muted); font-size: var(--text-sm); text-align: center; padding: var(--sp-4); }
1039
+
1040
+ /* ── Mermaid diagrams ─────────────────────────────────────────────────────────
1041
+ The build renders mermaid to inline SVG via mmdc, then sanitizeSvg() +
1042
+ rehype-sanitize strip the SVG's own <script>, <foreignObject>, AND <style>
1043
+ for security. Stripping <style> means the diagram arrives unstyled (nodes
1044
+ default to a black fill). These theme-token rules restyle the SVG so nodes,
1045
+ edges, arrowheads, and labels render correctly — and follow light/dark mode.
1046
+ Authors must render with htmlLabels:false (the generator forces this) so node
1047
+ labels are native <text>/<tspan> rather than stripped <foreignObject> HTML. */
1048
+ figure.mermaid { margin: var(--sp-5) 0; text-align: center; }
1049
+ figure.mermaid svg { max-width: 100%; height: auto; }
1050
+ /* Node shapes */
1051
+ figure.mermaid svg .node rect,
1052
+ figure.mermaid svg .node circle,
1053
+ figure.mermaid svg .node ellipse,
1054
+ figure.mermaid svg .node polygon,
1055
+ figure.mermaid svg .node path {
1056
+ fill: var(--bg-inset);
1057
+ stroke: var(--border);
1058
+ stroke-width: 1px;
1059
+ }
1060
+ /* Background helper rects mermaid emits behind labels */
1061
+ figure.mermaid svg .node .label-container { fill: var(--bg-inset); stroke: var(--border); }
1062
+ figure.mermaid svg rect.background { fill: none; stroke: none; }
1063
+ /* Labels (rendered as <text>/<tspan> when htmlLabels:false) */
1064
+ figure.mermaid svg .nodeLabel,
1065
+ figure.mermaid svg .node text,
1066
+ figure.mermaid svg text.nodeLabel,
1067
+ figure.mermaid svg .label text,
1068
+ figure.mermaid svg span.nodeLabel {
1069
+ fill: var(--text);
1070
+ color: var(--text);
1071
+ font-family: var(--font-sans);
1072
+ }
1073
+ /* Edges: thin strokes, not filled blobs */
1074
+ figure.mermaid svg .edgePath path,
1075
+ figure.mermaid svg path.flowchart-link,
1076
+ figure.mermaid svg .flowchart-link {
1077
+ fill: none;
1078
+ stroke: var(--text-faint);
1079
+ stroke-width: 1.5px;
1080
+ }
1081
+ /* Arrowheads */
1082
+ figure.mermaid svg marker path,
1083
+ figure.mermaid svg .marker {
1084
+ fill: var(--text-faint);
1085
+ stroke: var(--text-faint);
1086
+ }
1087
+ figure.mermaid svg .edgeLabel,
1088
+ figure.mermaid svg .edgeLabel text { fill: var(--text-muted); color: var(--text-muted); }
1089
+
1090
+ /* ============================================================================
1091
+ * guides.css — component + layout styles for `scaffold guides` reference pages.
1092
+ *
1093
+ * Pairs with lib/dashboard-theme.css (the token source) and src/guides/chrome.ts
1094
+ * (the behavior). Styles the guide CHROME (.topbar, .layout, .rail, nav.toc,
1095
+ * .content) and the markdown DIRECTIVES (callouts, sev chips, filter-tables,
1096
+ * charts, tabs, citations) plus base prose typography.
1097
+ *
1098
+ * DESIGN SYSTEM: all COLORS come from dashboard-theme.css tokens, and spacing
1099
+ * uses the --sp-* scale wherever it maps. The few structural layout constants
1100
+ * (topbar height, rail/drawer width, chart label column, card min) are declared
1101
+ * as local custom properties below; a handful of sub-scale UI values (chip/bar
1102
+ * sizing, em-based inline-code padding) and the responsive breakpoint are
1103
+ * literal because no token expresses them. Both themes are covered because every
1104
+ * color is a token.
1105
+ * ==========================================================================*/
1106
+
1107
+ :root {
1108
+ --topbar-h: 52px; /* sticky topbar height; rail sticky offset keys off it */
1109
+ --rail-w: 260px; /* desktop TOC sidebar column */
1110
+ --drawer-w: 280px; /* mobile off-canvas TOC drawer */
1111
+ --card-min: 260px; /* index card min track width */
1112
+ --chart-label-w: 90px; /* chart row label column min */
1113
+ }
1114
+
1115
+ /* ── Base / reset on top of the token base in dashboard-theme.css ─────────── */
1116
+ .content a { color: var(--accent); text-decoration: none; }
1117
+ .content a:hover { text-decoration: underline; }
1118
+ .content strong { font-weight: var(--fw-semi); }
1119
+ .content hr { border: 0; border-top: 1px solid var(--border-light); margin: var(--sp-6) 0; }
1120
+
1121
+ /* Consistent keyboard focus for every interactive control (a11y). */
1122
+ .topbar button:focus-visible,
1123
+ .copy-btn:focus-visible,
1124
+ .tab-btn:focus-visible,
1125
+ .filter-input:focus-visible,
1126
+ nav.toc a:focus-visible,
1127
+ .guide-card:focus-visible,
1128
+ .content a:focus-visible {
1129
+ outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm);
1130
+ }
1131
+
1132
+ /* ── Topbar ────────────────────────────────────────────────────────────────*/
1133
+ .topbar {
1134
+ position: sticky; top: 0; z-index: 60; height: var(--topbar-h);
1135
+ display: flex; align-items: center; gap: var(--sp-3);
1136
+ padding: 0 var(--page-pad);
1137
+ background: var(--bg-card); border-bottom: 1px solid var(--border);
1138
+ }
1139
+ .topbar h1 {
1140
+ flex: 1; min-width: 0; margin: 0;
1141
+ font-size: var(--text-lg); font-weight: var(--fw-bold);
1142
+ letter-spacing: var(--ls-tight);
1143
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1144
+ }
1145
+ .topbar button {
1146
+ background: var(--bg-card); border: 1px solid var(--border); color: var(--text);
1147
+ border-radius: var(--radius-sm); padding: var(--sp-1) var(--sp-3); font-size: var(--text-base);
1148
+ line-height: 1; cursor: pointer; font-family: inherit;
1149
+ }
1150
+ .topbar button:hover { background: var(--bg-hover); border-color: var(--accent); }
1151
+ .nav-toggle { display: none; }
1152
+
1153
+ /* ── Layout: sticky sidebar TOC + reading-width content ──────────────────── */
1154
+ .layout {
1155
+ max-width: var(--max-w); margin: 0 auto;
1156
+ display: grid; grid-template-columns: var(--rail-w) minmax(0, 1fr);
1157
+ gap: var(--sp-8); padding: 0 var(--page-pad);
1158
+ }
1159
+ .rail {
1160
+ position: sticky; top: var(--topbar-h); align-self: start;
1161
+ height: calc(100vh - var(--topbar-h)); overflow-y: auto;
1162
+ padding: var(--sp-5) 0; border-right: 1px solid var(--border-light);
1163
+ }
1164
+ .content { min-width: 0; padding: var(--sp-6) 0 var(--sp-10); }
1165
+
1166
+ /* Backdrop behind the mobile drawer (toggled with the rail via chrome.ts). */
1167
+ .rail-backdrop { display: none; }
1168
+ /* In-drawer close button — hidden on desktop (the rail is a static sidebar). */
1169
+ .rail-close { display: none; }
1170
+
1171
+ /* ── Table of contents (scrollspy marks a.active) ────────────────────────── */
1172
+ nav.toc ul { list-style: none; margin: 0; padding: 0; }
1173
+ nav.toc li { margin: 0; }
1174
+ nav.toc a {
1175
+ display: block; padding: var(--sp-1) var(--sp-3); line-height: 1.35;
1176
+ color: var(--text-muted); font-size: var(--text-sm);
1177
+ text-decoration: none; border-left: 2px solid transparent;
1178
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
1179
+ }
1180
+ nav.toc a:hover { color: var(--text); background: var(--bg-hover); }
1181
+ nav.toc a.active {
1182
+ color: var(--accent); border-left-color: var(--accent);
1183
+ background: var(--accent-glow); font-weight: var(--fw-medium);
1184
+ }
1185
+ nav.toc li.toc-3 a { padding-left: var(--sp-6); font-size: var(--text-xs); }
1186
+
1187
+ /* ── Prose typography ──────────────────────────────────────────────────────*/
1188
+ .content h2 {
1189
+ font-size: var(--text-xl); letter-spacing: var(--ls-tight);
1190
+ margin: var(--sp-8) 0 var(--sp-3); padding-bottom: var(--sp-2);
1191
+ border-bottom: 1px solid var(--border-light); scroll-margin-top: calc(var(--topbar-h) + var(--sp-3));
1192
+ }
1193
+ .content h3 {
1194
+ font-size: var(--text-lg); margin: var(--sp-5) 0 var(--sp-2);
1195
+ scroll-margin-top: calc(var(--topbar-h) + var(--sp-3));
1196
+ }
1197
+ .content p { margin: var(--sp-3) 0; line-height: var(--lh-relaxed); }
1198
+ .content ul, .content ol { margin: var(--sp-3) 0; padding-left: var(--sp-6); }
1199
+ .content li { margin: var(--sp-1) 0; line-height: var(--lh-relaxed); }
1200
+ .content blockquote {
1201
+ margin: var(--sp-3) 0; padding: var(--sp-1) var(--sp-4);
1202
+ border-left: 3px solid var(--border); color: var(--text-muted);
1203
+ }
1204
+
1205
+ /* ── Inline code + code blocks (chrome.ts wraps <pre> in .code + .copy-btn) ──*/
1206
+ .content code {
1207
+ font-family: var(--font-mono); font-size: 0.85em;
1208
+ background: var(--bg-inset); padding: 0.12em 0.4em; border-radius: var(--radius-sm);
1209
+ }
1210
+ .content .code { position: relative; margin: var(--sp-3) 0; }
1211
+ .content .code pre {
1212
+ margin: 0; padding: var(--sp-3) var(--sp-4); overflow-x: auto;
1213
+ background: var(--bg-inset); border: 1px solid var(--border-light);
1214
+ border-radius: var(--radius-sm); font-family: var(--font-mono);
1215
+ font-size: var(--text-sm); line-height: var(--lh-relaxed);
1216
+ }
1217
+ .content .code pre code { background: none; padding: 0; font-size: inherit; }
1218
+ .copy-btn {
1219
+ position: absolute; top: var(--sp-1); right: var(--sp-1);
1220
+ background: var(--bg-card); border: 1px solid var(--border); color: var(--text-muted);
1221
+ border-radius: var(--radius-sm); font-size: var(--text-xs); padding: var(--sp-1) var(--sp-2);
1222
+ cursor: pointer; opacity: 0.85; font-family: inherit;
1223
+ }
1224
+ .copy-btn:hover { color: var(--accent); border-color: var(--accent); opacity: 1; }
1225
+
1226
+ /* ── Callouts ─ (border-color BEFORE border-left-color so the accent wins) ── */
1227
+ .callout {
1228
+ margin: var(--sp-4) 0; padding: var(--sp-3) var(--sp-4);
1229
+ border: 1px solid var(--border); border-left-width: 3px;
1230
+ border-radius: var(--radius-sm); background: var(--bg-card);
1231
+ }
1232
+ .callout > :first-child { margin-top: 0; }
1233
+ .callout > :last-child { margin-bottom: 0; }
1234
+ .callout-note, .callout-info { background: var(--blue-bg); border-color: var(--blue-border); border-left-color: var(--blue); }
1235
+ .callout-tip { background: var(--green-bg); border-color: var(--green-border); border-left-color: var(--green); }
1236
+ .callout-warning { background: var(--yellow-bg); border-color: var(--yellow-border); border-left-color: var(--yellow); }
1237
+ .callout-danger { background: var(--red-bg); border-color: var(--red-border); border-left-color: var(--red); }
1238
+
1239
+ /* ── Severity chips (:sev) — tight pill, sub-scale vertical padding ───────── */
1240
+ .sev {
1241
+ display: inline-block; font-size: var(--text-xs); font-weight: var(--fw-semi);
1242
+ padding: 1px var(--sp-2); border-radius: 999px; line-height: 1.5;
1243
+ border: 1px solid var(--border); background: var(--bg-inset); color: var(--text-muted);
1244
+ white-space: nowrap;
1245
+ }
1246
+ .sev-p0 { color: var(--sev-p0); border-color: var(--sev-p0); }
1247
+ .sev-p1 { color: var(--sev-p1); border-color: var(--sev-p1); }
1248
+ .sev-p2 { color: var(--sev-p2); border-color: var(--sev-p2); }
1249
+ .sev-p3 { color: var(--sev-p3); border-color: var(--sev-p3); }
1250
+ .sev-pass { color: var(--sev-pass); border-color: var(--sev-pass); }
1251
+
1252
+ /* ── Citations (:cite) — inline provenance refs ──────────────────────────── */
1253
+ .fp, .cite-advisory {
1254
+ font-family: var(--font-mono); font-size: 0.82em;
1255
+ padding: 0.05em 0.35em; border-radius: var(--radius-sm);
1256
+ background: var(--bg-inset); border: 1px solid var(--border-light);
1257
+ }
1258
+ .fp { color: var(--accent); }
1259
+ .cite-advisory { color: var(--text-faint); border-style: dashed; }
1260
+
1261
+ /* ── Tables + filter-tables ──────────────────────────────────────────────── */
1262
+ .content table { width: 100%; border-collapse: collapse; margin: var(--sp-3) 0; font-size: var(--text-sm); }
1263
+ .content th, .content td { text-align: left; padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border-light); vertical-align: top; }
1264
+ .content th {
1265
+ color: var(--text-muted); font-weight: var(--fw-semi); font-size: var(--text-xs);
1266
+ text-transform: uppercase; letter-spacing: var(--ls-wide); border-bottom-color: var(--border);
1267
+ }
1268
+ .content tbody tr:hover { background: var(--bg-hover); }
1269
+ .content td code { white-space: nowrap; }
1270
+ .filter-table { margin: var(--sp-4) 0; }
1271
+ .filter-input {
1272
+ width: 100%; max-width: 320px; margin-bottom: var(--sp-2);
1273
+ padding: var(--sp-2) var(--sp-3); font-family: inherit; font-size: var(--text-sm);
1274
+ color: var(--text); background: var(--bg-card);
1275
+ border: 1px solid var(--border); border-radius: var(--radius-sm);
1276
+ }
1277
+ .filter-input:focus { border-color: var(--accent); }
1278
+
1279
+ /* ── Charts (:::chart) — label + proportional bar (fill carries inline width%) */
1280
+ .chart-block { margin: var(--sp-4) 0; }
1281
+ .chart-row {
1282
+ display: grid; grid-template-columns: minmax(var(--chart-label-w), 24%) 1fr;
1283
+ align-items: center; gap: var(--sp-3); margin: var(--sp-1) 0;
1284
+ }
1285
+ .chart-label {
1286
+ font-size: var(--text-sm); color: var(--text-muted); text-align: right;
1287
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1288
+ }
1289
+ .chart-row .chart-bar {
1290
+ height: 0.9em; min-width: 2px; background: var(--accent);
1291
+ border-radius: var(--radius-sm);
1292
+ }
1293
+
1294
+ /* ── Tabs (::::tabs / :::tab) ─────────────────────────────────────────────── */
1295
+ .tabs { margin: var(--sp-4) 0; }
1296
+ .tablist { display: flex; flex-wrap: wrap; gap: var(--sp-1); border-bottom: 1px solid var(--border); margin-bottom: var(--sp-3); }
1297
+ .tab-btn {
1298
+ background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px;
1299
+ padding: var(--sp-2) var(--sp-3); color: var(--text-muted);
1300
+ font-family: inherit; font-size: var(--text-sm); font-weight: var(--fw-medium); cursor: pointer;
1301
+ }
1302
+ .tab-btn:hover { color: var(--text); }
1303
+ .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
1304
+ .tabpane { display: none; }
1305
+ .tabpane.active { display: block; }
1306
+
1307
+ /* ── Mermaid diagrams ────────────────────────────────────────────────────── */
1308
+ .content figure { margin: var(--sp-4) 0; text-align: center; }
1309
+ .content figure svg, .content > svg, .content .mermaid svg { max-width: 100%; height: auto; }
1310
+
1311
+ /* ── Index page: category card grid ──────────────────────────────────────── */
1312
+ .content .lead { color: var(--text-muted); font-size: var(--text-base); max-width: 60ch; margin-top: var(--sp-2); }
1313
+ .guide-cards {
1314
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(min(var(--card-min), 100%), 1fr));
1315
+ gap: var(--sp-4); margin: var(--sp-4) 0 var(--sp-6);
1316
+ }
1317
+ .guide-card {
1318
+ display: flex; flex-direction: column; gap: var(--sp-2);
1319
+ padding: var(--sp-4); background: var(--bg-card);
1320
+ border: 1px solid var(--border); border-radius: var(--radius);
1321
+ color: inherit; text-decoration: none;
1322
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
1323
+ }
1324
+ .guide-card:hover { border-color: var(--accent); box-shadow: var(--shadow-md); text-decoration: none; }
1325
+ .guide-card-title { font-weight: var(--fw-semi); color: var(--accent); font-size: var(--text-base); }
1326
+ .guide-card-desc { color: var(--text-muted); font-size: var(--text-sm); line-height: var(--lh-normal); }
1327
+
1328
+ /* ── Responsive: TOC becomes an off-canvas drawer (chrome.ts toggles .open) ──*/
1329
+ /* 860px is literal — media queries cannot read custom properties. Revisit it if
1330
+ --topbar-h / --rail-w / --drawer-w change (the drawer sticky offsets key off them). */
1331
+ @media (max-width: 860px) {
1332
+ .nav-toggle { display: inline-flex; align-items: center; }
1333
+ .layout { grid-template-columns: 1fr; gap: 0; }
1334
+ .rail {
1335
+ position: fixed; top: var(--topbar-h); left: 0; bottom: 0; width: var(--drawer-w); z-index: 50;
1336
+ height: auto; background: var(--bg-card); border-right: 1px solid var(--border);
1337
+ padding: var(--sp-4); box-shadow: var(--shadow-lg);
1338
+ transform: translateX(-100%); transition: transform 0.2s ease, visibility 0.2s ease;
1339
+ /* Closed drawer is off-screen AND removed from tab order / pointer events. */
1340
+ visibility: hidden; pointer-events: none;
1341
+ }
1342
+ .rail.open { transform: translateX(0); visibility: visible; pointer-events: auto; }
1343
+ .rail-close {
1344
+ display: block; margin-left: auto; margin-bottom: var(--sp-2);
1345
+ background: var(--bg-card); border: 1px solid var(--border); color: var(--text);
1346
+ border-radius: var(--radius-sm); padding: var(--sp-1) var(--sp-3); font-size: var(--text-base);
1347
+ line-height: 1; cursor: pointer; font-family: inherit;
1348
+ }
1349
+ .rail-close:hover { background: var(--bg-hover); border-color: var(--accent); }
1350
+ .rail-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1351
+ .rail-backdrop {
1352
+ display: block; position: fixed; inset: var(--topbar-h) 0 0 0;
1353
+ background: var(--scrim); z-index: 49;
1354
+ opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
1355
+ }
1356
+ .rail.open ~ .rail-backdrop { opacity: 1; pointer-events: auto; }
1357
+ }
1358
+ </style>
1359
+ <script>(function(){try{var t=localStorage.getItem('guide-theme');if(!t&&window.matchMedia&&matchMedia('(prefers-color-scheme: dark)').matches)t='dark';if(t)document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
1360
+ </head>
1361
+ <body>
1362
+ <header class="topbar">
1363
+ <button data-action="nav" class="nav-toggle" aria-label="Toggle navigation"
1364
+ aria-expanded="false" aria-controls="guide-toc">☰</button>
1365
+ <h1>Knowledge Freshness</h1>
1366
+ <button data-action="theme" class="theme-toggle" aria-label="Toggle theme">◐</button>
1367
+ </header>
1368
+ <div class="layout">
1369
+ <aside class="rail" id="guide-toc">
1370
+ <button class="rail-close" data-action="nav" aria-label="Close navigation">✕</button>
1371
+ <nav class="toc" aria-label="Table of contents"><ul><li class="toc-2"><a href="#what-this-system-does">What this system does</a></li><li class="toc-3"><a href="#how-a-gap-closes">How a gap closes</a></li><li class="toc-2"><a href="#system-map">System map</a></li><li class="toc-2"><a href="#frontmatter-signals-and-resolution">Frontmatter, signals, and resolution</a></li><li class="toc-3"><a href="#frontmatter-schema">Frontmatter schema</a></li><li class="toc-3"><a href="#cadence-model">Cadence model</a></li><li class="toc-3"><a href="#adding-a-new-entry-to-the-kb">Adding a new entry to the KB</a></li><li class="toc-3"><a href="#gap-signal-payload">Gap-signal payload</a></li><li class="toc-3"><a href="#knowledgerootresolution-shape">KnowledgeRootResolution shape</a></li><li class="toc-2"><a href="#from-candidate-to-merged-pr">From candidate to merged PR</a></li><li class="toc-3"><a href="#prefilter">Prefilter</a></li><li class="toc-3"><a href="#audit-verdicts">Audit verdicts</a></li><li class="toc-3"><a href="#pr-generation">PR generation</a></li><li class="toc-3"><a href="#mmr-corroboration-manual">MMR corroboration (manual)</a></li><li class="toc-2"><a href="#the-five-pr-gates">The five PR gates</a></li><li class="toc-2"><a href="#lens-i-gap-detection-suppression">Lens I — gap detection + suppression</a></li><li class="toc-3"><a href="#threshold-matrix">Threshold matrix</a></li><li class="toc-3"><a href="#topic-normalization">Topic normalization</a></li><li class="toc-3"><a href="#what-the-lessonsmd-scanner-sees">What the lessons.md scanner sees</a></li><li class="toc-3"><a href="#3-tier---knowledge-root-resolution">3-tier --knowledge-root resolution</a></li><li class="toc-3"><a href="#warning-policy">Warning policy</a></li><li class="toc-3"><a href="#what-a-lens-i-finding-looks-like">What a Lens I finding looks like</a></li><li class="toc-2"><a href="#the-allowlist">The allowlist</a></li><li class="toc-3"><a href="#most-cited-hosts">Most-cited hosts</a></li><li class="toc-3"><a href="#the-full-allowlist">The full allowlist</a></li><li class="toc-3"><a href="#kb-inventory">KB inventory</a></li><li class="toc-3"><a href="#how-to-expand-the-allowlist">How to expand the allowlist</a></li><li class="toc-2"><a href="#anthropic-vs-deepseek-cron-uses-deepseek">Anthropic vs DeepSeek (cron uses DeepSeek)</a></li><li class="toc-2"><a href="#every-command-that-touches-the-system">Every command that touches the system</a></li><li class="toc-3"><a href="#refresh-arm-commands">Refresh-arm commands</a></li><li class="toc-3"><a href="#gap-arm-commands">Gap-arm commands</a></li><li class="toc-3"><a href="#gate-side-subcommands-also-runnable-locally-for-triage">Gate-side subcommands (also runnable locally for triage)</a></li><li class="toc-2"><a href="#operations-cheat-sheet">Operations cheat sheet</a></li><li class="toc-3"><a href="#an-entrys-audit-failed-in-the-cron">An entry's audit failed in the cron</a></li><li class="toc-3"><a href="#lens-i-keeps-surfacing-a-topic-the-kb-already-covers">Lens I keeps surfacing a topic the KB already covers</a></li><li class="toc-3"><a href="#downstream-auto-detect-cant-find-the-kb">Downstream auto-detect can't find the KB</a></li><li class="toc-3"><a href="#yaml-knowledge-root-stops-working-after-an-upgrade">Yaml knowledge_root stops working after an upgrade</a></li><li class="toc-3"><a href="#a-source-url-fetches-in-curl-but-the-cron-rejects-it">A source URL fetches in curl but the cron rejects it</a></li><li class="toc-3"><a href="#--knowledge-root-resolves-to-a-path-you-didnt-expect">--knowledge-root resolves to a path you didn't expect</a></li><li class="toc-2"><a href="#config-reference">Config reference</a></li><li class="toc-2"><a href="#roadmap-and-known-divergences">Roadmap and known divergences</a></li><li class="toc-3"><a href="#phase-5-planned">Phase 5 (planned)</a></li><li class="toc-3"><a href="#known-divergences">Known divergences</a></li></ul></nav>
1372
+ </aside>
1373
+ <main class="content"><h2 id="what-this-system-does">What this system does</h2>
1374
+ <p>Knowledge entries under <code>content/knowledge/</code> declare a <code>volatility</code> tier and a
1375
+ list of <code>sources</code>. A daily cron prefilters at most ten entries that are <em>due</em> —
1376
+ by cadence or by a changed source hash — runs a grounded LLM audit against the
1377
+ prefetched source bodies, opens one PR per drifted entry, and gates that PR on
1378
+ five checks. In parallel, downstream agents emit <code>knowledge_gap_signal</code> events
1379
+ when they hit a topic the KB does not cover; <strong>Lens I</strong> aggregates those signals
1380
+ into P1/P2 audit findings, suppressing any topic an entry already covers.</p>
1381
+ <p>Two arms, two outcomes:</p>
1382
+ <ul>
1383
+ <li>The <strong>refresh arm</strong> chases <em>known</em> sources for drift. It ends in a PR that
1384
+ <em>updates</em> an entry.</li>
1385
+ <li>The <strong>gap arm</strong> surfaces <em>unknown</em> topics. It ends in a PR that <em>creates</em> an
1386
+ entry.</li>
1387
+ </ul>
1388
+ <p>Both terminate in a human-merged PR.</p>
1389
+
1390
+
1391
+
1392
+
1393
+
1394
+
1395
+
1396
+
1397
+
1398
+
1399
+
1400
+
1401
+
1402
+
1403
+
1404
+
1405
+
1406
+
1407
+
1408
+
1409
+
1410
+
1411
+
1412
+
1413
+
1414
+
1415
+
1416
+
1417
+
1418
+
1419
+
1420
+
1421
+
1422
+
1423
+
1424
+ <table><thead><tr><th>Surface</th><th>Value</th><th>Notes</th></tr></thead><tbody><tr><td>Volatility tiers</td><td>3</td><td><code>fast-moving</code> / <code>evolving</code> / <code>stable</code></td></tr><tr><td>Audit verdicts</td><td>4</td><td><code>current</code> / <code>minor-drift</code> / <code>major-drift</code> / <code>superseded</code></td></tr><tr><td>Daily audit ceiling</td><td>10</td><td>set by <code>--max=10</code> in the cron workflow; not a yaml knob</td></tr><tr><td>PR gates</td><td>5</td><td>4 blocking + 1 advisory</td></tr><tr><td>Signal window</td><td>90 days</td><td>rolling; drives Lens I aggregation</td></tr></tbody></table>
1425
+ <div class="callout callout-note"><p><strong>Two subsystems, one config file.</strong> Knowledge Freshness and the separate
1426
+ <a href="../observability/index.md">Build Observability</a> system both read
1427
+ <code>.scaffold/observability.yaml</code>. This guide documents Knowledge Freshness;
1428
+ Lens I is the seam where the two meet (it lives in the observability audit but
1429
+ reasons about the KB).</p></div>
1430
+ <h3 id="how-a-gap-closes">How a gap closes</h3>
1431
+ <p>The full lifecycle, end to end:</p>
1432
+ <ol>
1433
+ <li>Downstream agents emit signals; they accumulate in the rolling 90-day window.</li>
1434
+ <li>A topic's signal count and distinct-project count cross the threshold.</li>
1435
+ <li>Lens I emits a P1/P2 finding.</li>
1436
+ <li>An operator adds <code>content/knowledge/&#x3C;category>/&#x3C;slug>.md</code>.</li>
1437
+ <li>The next audit's knowledge index covers the slug and Lens I <strong>suppresses</strong> the
1438
+ bucket — the finding disappears.</li>
1439
+ </ol>
1440
+ <p>Signals are <em>not</em> purged when the entry is added. The window is rolling, so
1441
+ yesterday's signals still aggregate tomorrow; suppression filters the <em>emit</em>
1442
+ step, not the aggregation step (<span class="fp" data-path="src/observability/checks/lens-i-knowledge-gaps.ts:155">src/observability/checks/lens-i-knowledge-gaps.ts:155</span>).
1443
+ Signals only fade as they age out of the 90-day window naturally.</p>
1444
+ <h2 id="system-map">System map</h2>
1445
+ <figure class="mermaid"><svg id="my-svg" width="100%" xmlns="http://www.w3.org/2000/svg" class="flowchart" style="max-width: 951.219px; background-color: transparent;" viewBox="0 0 951.21875 940.3999633789062" role="graphics-document document">#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#000000;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#my-svg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#my-svg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#my-svg .error-icon{fill:#552222;}#my-svg .error-text{fill:#552222;stroke:#552222;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:#666;stroke:#666;}#my-svg .marker.cross{stroke:#666;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#000000;}#my-svg .cluster-label text{fill:#333;}#my-svg .cluster-label span{color:#333;}#my-svg .cluster-label span p{background-color:transparent;}#my-svg .label text,#my-svg span{fill:#000000;color:#000000;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#eee;stroke:#999;stroke-width:1px;}#my-svg .rough-node .label text,#my-svg .node .label text,#my-svg .image-shape .label,#my-svg .icon-shape .label{text-anchor:middle;}#my-svg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#my-svg .rough-node .label,#my-svg .node .label,#my-svg .image-shape .label,#my-svg .icon-shape .label{text-align:center;}#my-svg .node.clickable{cursor:pointer;}#my-svg .root .anchor path{fill:#666!important;stroke-width:0;stroke:#666;}#my-svg .arrowheadPath{fill:#333333;}#my-svg .edgePath .path{stroke:#666;stroke-width:1px;}#my-svg .flowchart-link{stroke:#666;fill:none;}#my-svg .edgeLabel{background-color:white;text-align:center;}#my-svg .edgeLabel p{background-color:white;}#my-svg .edgeLabel rect{opacity:0.5;background-color:white;fill:white;}#my-svg .labelBkg{background-color:rgba(255, 255, 255, 0.5);}#my-svg .cluster rect{fill:hsl(0, 0%, 98.9215686275%);stroke:#707070;stroke-width:1px;}#my-svg .cluster text{fill:#333;}#my-svg .cluster span{color:#333;}#my-svg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(-160, 0%, 93.3333333333%);border:1px solid #707070;border-radius:2px;pointer-events:none;z-index:100;}#my-svg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#000000;}#my-svg rect.text{fill:none;stroke-width:0;}#my-svg .icon-shape,#my-svg .image-shape{background-color:white;text-align:center;}#my-svg .icon-shape p,#my-svg .image-shape p{background-color:white;padding:2px;}#my-svg .icon-shape .label rect,#my-svg .image-shape .label rect{opacity:0.5;background-color:white;fill:white;}#my-svg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#my-svg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#my-svg .node .neo-node{stroke:#999;}#my-svg [data-look="neo"].node rect,#my-svg [data-look="neo"].cluster rect,#my-svg [data-look="neo"].node polygon{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node path{stroke:url(#my-svg-gradient);stroke-width:1px;}#my-svg [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node .neo-line path{stroke:#999;filter:none;}#my-svg [data-look="neo"].node circle{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node circle .state-start{fill:#000000;}#my-svg [data-look="neo"].icon-shape .icon{fill:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].icon-shape .icon-neo path{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}<g><marker id="my-svg_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="my-svg_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="my-svg_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"></path></marker><marker id="my-svg_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"></polygon></marker><marker id="my-svg_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="my-svg_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="my-svg_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"></circle></marker><marker id="my-svg_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"></circle></marker><marker id="my-svg_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><marker id="my-svg_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><marker id="my-svg_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"></path></marker><marker id="my-svg_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"></path></marker><g class="root"><g class="clusters"><g class="cluster" id="my-svg-gap"><rect style="" x="390.40625" y="8" width="552.8125" height="625.5999908447266"></rect><g class="cluster-label" transform="translate(636.375, 8)"><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">Gap</tspan><tspan class="text-inner-tspan"> arm</tspan></tspan></text></g></g></g><g class="cluster" id="my-svg-refresh"><rect style="" x="8" y="149.5999984741211" width="362.40625" height="782.7999877929688"></rect><g class="cluster-label" transform="translate(146.1015625, 149.5999984741211)"><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">Refresh</tspan><tspan class="text-inner-tspan"> arm</tspan></tspan></text></g></g></g></g><g class="edgePaths"><path d="M132.684,241.2L132.684,245.367C132.684,249.533,132.684,257.867,132.684,267C132.684,276.133,132.684,286.067,132.684,291.033L132.684,296" id="my-svg-L_CRON_PF_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" marker-end="url(#my-svg_flowchart-v2-pointEnd)"></path><path d="M132.684,366.6L132.684,372.233C132.684,377.867,132.684,389.133,132.684,398.267C132.684,407.4,132.684,414.4,132.684,417.9L132.684,421.4" id="my-svg-L_PF_RUN_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" marker-end="url(#my-svg_flowchart-v2-pointEnd)"></path><path d="M132.684,492L132.684,496.167C132.684,500.333,132.684,508.667,132.684,516.333C132.684,524,132.684,531,132.684,534.5L132.684,538" id="my-svg-L_RUN_APPLY_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" marker-end="url(#my-svg_flowchart-v2-pointEnd)"></path><path d="M132.684,608.6L132.684,612.767C132.684,616.933,132.684,625.267,132.684,638.45C132.684,651.633,132.684,669.667,138.932,687.163C145.18,704.66,157.676,721.62,163.925,730.1L170.173,738.58" id="my-svg-L_APPLY_GATES_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" marker-end="url(#my-svg_flowchart-v2-pointEnd)"></path><path d="M190.598,790.8L190.598,794.967C190.598,799.133,190.598,807.467,190.598,815.133C190.598,822.8,190.598,829.8,190.598,833.3L190.598,836.8" id="my-svg-L_GATES_MERGE_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" marker-end="url(#my-svg_flowchart-v2-pointEnd)"></path><path d="M536.594,99.6L536.594,103.767C536.594,107.933,536.594,116.267,536.594,124.6C536.594,132.933,536.594,141.267,536.594,148.933C536.594,156.6,536.594,163.6,536.594,167.1L536.594,170.6" id="my-svg-L_TAIL_EVENT_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" marker-end="url(#my-svg_flowchart-v2-pointEnd)"></path><path d="M536.594,241.2L536.594,245.367C536.594,249.533,536.594,257.867,536.594,267C536.594,276.133,536.594,286.067,536.594,291.033L536.594,296" id="my-svg-L_EVENT_LEDGER_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" marker-end="url(#my-svg_flowchart-v2-pointEnd)"></path><path d="M536.594,366.6L536.594,372.233C536.594,377.867,536.594,389.133,544.883,398.65C553.172,408.168,569.751,415.935,578.04,419.819L586.329,423.703" id="my-svg-L_LEDGER_LENSI_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" marker-end="url(#my-svg_flowchart-v2-pointEnd)"></path><path d="M661.023,492L661.023,496.167C661.023,500.333,661.023,508.667,661.023,516.333C661.023,524,661.023,531,661.023,534.5L661.023,538" id="my-svg-L_LENSI_FINDING_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" marker-end="url(#my-svg_flowchart-v2-pointEnd)"></path><path d="M785.453,375.4L785.453,379.567C785.453,383.733,785.453,392.067,777.164,400.117C768.875,408.168,752.296,415.935,744.007,419.819L735.718,423.703" id="my-svg-L_RESOLVER_LENSI_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" marker-end="url(#my-svg_flowchart-v2-pointEnd)"></path><path d="M661.023,608.6L661.023,612.767C661.023,616.933,661.023,625.267,592.271,638.45C523.52,651.633,386.016,669.667,311.015,687.163C236.015,704.66,223.519,721.62,217.271,730.1L211.023,738.58" id="my-svg-L_FINDING_GATES_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" marker-end="url(#my-svg_flowchart-v2-pointEnd)"></path></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" text-anchor="middle"></tspan></text></g></g><g><rect class="background" style="stroke: none"></rect></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" text-anchor="middle"></tspan></text></g></g><g><rect class="background" style="stroke: none"></rect></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" text-anchor="middle"></tspan></text></g></g><g><rect class="background" style="stroke: none"></rect></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" text-anchor="middle"></tspan></text></g></g><g><rect class="background" style="stroke: none"></rect></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" text-anchor="middle"></tspan></text></g></g><g><rect class="background" style="stroke: none"></rect></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" text-anchor="middle"></tspan></text></g></g><g><rect class="background" style="stroke: none"></rect></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" text-anchor="middle"></tspan></text></g></g><g><rect class="background" style="stroke: none"></rect></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" text-anchor="middle"></tspan></text></g></g><g><rect class="background" style="stroke: none"></rect></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" text-anchor="middle"></tspan></text></g></g><g><rect class="background" style="stroke: none"></rect></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" text-anchor="middle"></tspan></text></g></g><g><rect class="background" style="stroke: none"></rect></g><g class="edgeLabel" transform="translate(433.84693, 663.39369)"><g class="label" transform="translate(0, -28.099998474121094)"><g><rect class="background" style="" x="-95.828125" y="-1" width="191.65625" height="58.19999694824219"></rect><text y="-10.1" text-anchor="middle" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em" text-anchor="middle"><tspan class="text-inner-tspan">operator</tspan><tspan class="text-inner-tspan"> adds</tspan><tspan class="text-inner-tspan"> an</tspan><tspan class="text-inner-tspan"> entry</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="1em" text-anchor="middle"><tspan class="text-inner-tspan">whose</tspan><tspan class="text-inner-tspan"> name:</tspan><tspan class="text-inner-tspan"> matches</tspan><tspan class="text-inner-tspan"> the</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="2.1em" text-anchor="middle"><tspan class="text-inner-tspan">bucket</tspan><tspan class="text-inner-tspan"> →</tspan><tspan class="text-inner-tspan"> PR</tspan></tspan></text></g></g></g></g><g class="nodes"><g class="node default" id="my-svg-flowchart-CRON-0" transform="translate(132.68359375, 207.89999771118164)"><rect class="basic label-container" style="" x="-86.40625" y="-33.29999923706055" width="172.8125" height="66.5999984741211"></rect><g class="label" style="" transform="translate(0, -18.299999237060547)"><rect></rect><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">cron</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="1em"><tspan class="text-inner-tspan">09:00</tspan><tspan class="text-inner-tspan"> UTC</tspan><tspan class="text-inner-tspan"> daily</tspan></tspan></text></g></g></g><g class="node default" id="my-svg-flowchart-PF-1" transform="translate(132.68359375, 333.2999954223633)"><rect class="basic label-container" style="" x="-81.59375" y="-33.29999923706055" width="163.1875" height="66.5999984741211"></rect><g class="label" style="" transform="translate(0, -18.299999237060547)"><rect></rect><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">audit-prefilter</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="1em"><tspan class="text-inner-tspan">--max=10</tspan></tspan></text></g></g></g><g class="node default" id="my-svg-flowchart-RUN-3" transform="translate(132.68359375, 458.6999931335449)"><rect class="basic label-container" style="" x="-85.171875" y="-33.29999923706055" width="170.34375" height="66.5999984741211"></rect><g class="label" style="" transform="translate(0, -18.299999237060547)"><rect></rect><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">audit-run-entry</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="1em"><tspan class="text-inner-tspan">grounded</tspan><tspan class="text-inner-tspan"> LLM</tspan></tspan></text></g></g></g><g class="node default" id="my-svg-flowchart-APPLY-5" transform="translate(132.68359375, 575.299991607666)"><rect class="basic label-container" style="" x="-70.84375" y="-33.29999923706055" width="141.6875" height="66.5999984741211"></rect><g class="label" style="" transform="translate(0, -18.299999237060547)"><rect></rect><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">audit-apply</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="1em"><tspan class="text-inner-tspan">--open-pr</tspan></tspan></text></g></g></g><g class="node default" id="my-svg-flowchart-GATES-7" transform="translate(190.59765625, 766.2999877929688)"><rect class="basic label-container" style="" x="-67.1328125" y="-24.5" width="134.265625" height="49"></rect><g class="label" style="" transform="translate(0, -9.5)"><rect></rect><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">5</tspan><tspan class="text-inner-tspan"> PR</tspan><tspan class="text-inner-tspan"> gates</tspan></tspan></text></g></g></g><g class="node default" id="my-svg-flowchart-MERGE-9" transform="translate(190.59765625, 874.0999870300293)"><rect class="basic label-container" style="" x="-92.9609375" y="-33.29999923706055" width="185.921875" height="66.5999984741211"></rect><g class="label" style="" transform="translate(0, -18.299999237060547)"><rect></rect><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">human</tspan><tspan class="text-inner-tspan"> merge</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="1em"><tspan class="text-inner-tspan">→</tspan><tspan class="text-inner-tspan"> VERSION</tspan><tspan class="text-inner-tspan"> bump</tspan></tspan></text></g></g></g><g class="node default" id="my-svg-flowchart-TAIL-10" transform="translate(536.59375, 66.29999923706055)"><rect class="basic label-container" style="" x="-90.6171875" y="-33.29999923706055" width="181.234375" height="66.5999984741211"></rect><g class="label" style="" transform="translate(0, -18.299999237060547)"><rect></rect><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">gap-signal-tail</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="1em"><tspan class="text-inner-tspan">89</tspan><tspan class="text-inner-tspan"> pipeline</tspan><tspan class="text-inner-tspan"> steps</tspan></tspan></text></g></g></g><g class="node default" id="my-svg-flowchart-EVENT-11" transform="translate(536.59375, 207.89999771118164)"><rect class="basic label-container" style="" x="-111.1875" y="-33.29999923706055" width="222.375" height="66.5999984741211"></rect><g class="label" style="" transform="translate(0, -18.299999237060547)"><rect></rect><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">scaffold</tspan><tspan class="text-inner-tspan"> observe</tspan><tspan class="text-inner-tspan"> event</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="1em"><tspan class="text-inner-tspan">knowledge_gap_signal</tspan></tspan></text></g></g></g><g class="node default" id="my-svg-flowchart-LEDGER-13" transform="translate(536.59375, 333.2999954223633)"><rect class="basic label-container" style="" x="-76.09375" y="-33.29999923706055" width="152.1875" height="66.5999984741211"></rect><g class="label" style="" transform="translate(0, -18.299999237060547)"><rect></rect><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">ledger</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="1em"><tspan class="text-inner-tspan">activity.jsonl</tspan></tspan></text></g></g></g><g class="node default" id="my-svg-flowchart-LENSI-15" transform="translate(661.0234375, 458.6999931335449)"><rect class="basic label-container" style="" x="-83.65625" y="-33.29999923706055" width="167.3125" height="66.5999984741211"></rect><g class="label" style="" transform="translate(0, -18.299999237060547)"><rect></rect><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">Lens</tspan><tspan class="text-inner-tspan"> I</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="1em"><tspan class="text-inner-tspan">90-day</tspan><tspan class="text-inner-tspan"> window</tspan></tspan></text></g></g></g><g class="node default" id="my-svg-flowchart-FINDING-17" transform="translate(661.0234375, 575.299991607666)"><rect class="basic label-container" style="" x="-56.3359375" y="-33.29999923706055" width="112.671875" height="66.5999984741211"></rect><g class="label" style="" transform="translate(0, -18.299999237060547)"><rect></rect><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">finding</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="1em"><tspan class="text-inner-tspan">P1</tspan><tspan class="text-inner-tspan"> /</tspan><tspan class="text-inner-tspan"> P2</tspan></tspan></text></g></g></g><g class="node default" id="my-svg-flowchart-RESOLVER-18" transform="translate(785.453125, 333.2999954223633)"><rect class="basic label-container" style="" x="-122.765625" y="-42.099998474121094" width="245.53125" height="84.19999694824219"></rect><g class="label" style="" transform="translate(0, -27.099998474121094)"><rect></rect><g><rect class="background" style="stroke: none"></rect><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em"><tspan class="text-inner-tspan">3-tier</tspan><tspan class="text-inner-tspan"> --knowledge-root</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="1em"><tspan class="text-inner-tspan">resolver</tspan></tspan><tspan class="text-outer-tspan row" x="0" y="2.1em"><tspan class="text-inner-tspan">suppresses</tspan><tspan class="text-inner-tspan"> covered</tspan><tspan class="text-inner-tspan"> topics</tspan></tspan></text></g></g></g></g></g></g><defs></defs><defs></defs></svg></figure>
1446
+ <p>Three real hooks sit beside the two arms: the <strong>phase-audit hook</strong> (runs Lens H
1447
+ only, never Lens I), the <strong>doc-conformance MMR channel</strong> (routes Lens I findings
1448
+ into MMR), and the <strong><code>--fix</code> flow</strong> (initial + verifier + postfix audit). They
1449
+ are covered below.</p>
1450
+ <div class="callout callout-warning"><p><strong>Doc drift on MMR-in-cron.</strong> Three docs frame MMR-in-cron differently. The
1451
+ parent spec's locked decision #3 is authoritative: a native
1452
+ <code>knowledge-freshness</code> MMR channel is deferred to Phase 5. The cron today runs
1453
+ only inline gates. Two interim paths give reviewers MMR signal on a freshness
1454
+ PR: (1) the built-in <code>doc-conformance</code> MMR channel (disabled by default; enable
1455
+ with <code>mmr review --channels=doc-conformance</code>); (2) the manual <code>mmr review --diff -</code> command in <a href="#from-candidate-to-merged-pr">From candidate to merged PR</a>.</p></div>
1456
+ <h2 id="frontmatter-signals-and-resolution">Frontmatter, signals, and resolution</h2>
1457
+ <h3 id="frontmatter-schema">Frontmatter schema</h3>
1458
+ <p>Every knowledge entry's frontmatter is a Zod-validated object with four
1459
+ freshness-relevant fields. The schema is the source of truth and runs as Gate 1
1460
+ of the PR CI (<span class="fp" data-path="src/validation/knowledge-frontmatter-validator.ts:42-50">src/validation/knowledge-frontmatter-validator.ts:42-50</span>);
1461
+ runtime readers tolerate missing optional fields.</p>
1462
+
1463
+
1464
+
1465
+
1466
+
1467
+
1468
+
1469
+
1470
+
1471
+
1472
+
1473
+
1474
+
1475
+
1476
+
1477
+
1478
+
1479
+
1480
+
1481
+
1482
+
1483
+
1484
+
1485
+
1486
+
1487
+
1488
+
1489
+
1490
+
1491
+
1492
+
1493
+
1494
+
1495
+
1496
+
1497
+
1498
+
1499
+
1500
+
1501
+
1502
+
1503
+
1504
+
1505
+
1506
+
1507
+
1508
+
1509
+
1510
+
1511
+
1512
+
1513
+
1514
+
1515
+
1516
+
1517
+
1518
+
1519
+
1520
+
1521
+
1522
+
1523
+ <table><thead><tr><th>Field</th><th>Type</th><th>Default</th><th>Validation</th><th>Read by</th></tr></thead><tbody><tr><td><code>name</code></td><td>string</td><td>required</td><td>regex <code>/^[a-z][a-z0-9-]*$/</code></td><td>assembly-loader, Lens I suppression</td></tr><tr><td><code>description</code></td><td>string</td><td>required</td><td>warns if > 200 chars</td><td>assembly-loader (TOC), audit prompt</td></tr><tr><td><code>topics</code></td><td>string[]</td><td><code>[]</code></td><td>any string</td><td>assembly-loader (auto-selection)</td></tr><tr><td><code>volatility</code></td><td>enum</td><td><code>evolving</code></td><td><code>stable|evolving|fast-moving</code></td><td>prefilter cadence</td></tr><tr><td><code>last-reviewed</code></td><td>ISO date</td><td><code>null</code></td><td><code>YYYY-MM-DD</code> &#x26; real calendar date</td><td>prefilter cadence</td></tr><tr><td><code>version-pin</code></td><td>string</td><td><code>null</code></td><td>any string (e.g. <code>"OWASP Top 10 2021"</code>)</td><td>audit prompt; <code>superseded</code> verdict signals it must advance manually</td></tr><tr><td><code>sources[]</code></td><td>object[]</td><td><code>[]</code></td><td>each: <code>url</code> (SSRF-checked at fetch), <code>anchor</code> (optional, starts with <code>#</code>), <code>retrieved</code> (ISO date), <code>hash</code> (sha256)</td><td>prefilter (hash + cadence), audit runner (prefetch)</td></tr></tbody></table>
1524
+ <div class="callout callout-warning"><p><strong><code>name</code> vs. gap-topic regex.</strong> An entry <code>name</code> must start with a letter
1525
+ (<code>/^[a-z][a-z0-9-]*$/</code>), but Lens I gap <em>topics</em> allow a leading digit
1526
+ (<code>/^[a-z0-9]+(-[a-z0-9]+)*$/</code>). So a gap signalled for a topic like <code>3d-rendering</code>
1527
+ cannot be suppressed by an entry of the same name — pick a letter-leading <code>name</code>
1528
+ (and list the numeric form under <code>topics</code>) when closing such a gap.</p></div>
1529
+ <div class="callout callout-note"><p><strong>Anchor semantics.</strong> Put fragments in <code>anchor</code>, never inside <code>url</code>. The audit
1530
+ fetches <code>url + (anchor ?? '')</code> and hashes that body; the coverage check
1531
+ (<span class="fp" data-path="src/knowledge-freshness/audit-apply.ts:82-101">src/knowledge-freshness/audit-apply.ts:82-101</span>) matches the same combined
1532
+ string. Splitting prevents hash drift from spurious URL re-encodings and lets two
1533
+ sources at the same base URL with different <code>#anchor</code>s be tracked independently.</p></div>
1534
+ <h3 id="cadence-model">Cadence model</h3>
1535
+ <p>Three tiers, three windows — <strong>14 / 60 / 180</strong> days for <code>fast-moving</code> /
1536
+ <code>evolving</code> / <code>stable</code> (<span class="fp" data-path="src/knowledge-freshness/audit-prefilter.ts:5-7">src/knowledge-freshness/audit-prefilter.ts:5-7</span>).
1537
+ An entry with no <code>last-reviewed</code> always counts as due. Sources with a changed
1538
+ hash also become candidates regardless of age, but the hash check only runs for
1539
+ entries still <em>inside</em> their cadence window.</p>
1540
+ <h4>Which tier does an entry belong in?</h4>
1541
+
1542
+
1543
+
1544
+
1545
+
1546
+
1547
+
1548
+
1549
+
1550
+
1551
+
1552
+
1553
+
1554
+
1555
+
1556
+
1557
+
1558
+
1559
+
1560
+
1561
+
1562
+
1563
+
1564
+
1565
+
1566
+ <table><thead><tr><th>Provenance</th><th>Change frequency</th><th>Recommended tier</th></tr></thead><tbody><tr><td>vendor SDK / API docs</td><td>quarterly or faster</td><td><code>fast-moving</code></td></tr><tr><td>standards / RFCs, vendor docs</td><td>yearly-ish</td><td><code>evolving</code></td></tr><tr><td>canonical pattern reference</td><td>multi-year</td><td><code>stable</code></td></tr></tbody></table>
1567
+ <p>Rule of thumb: if a version bump <em>often breaks</em> downstream guidance, lean
1568
+ <code>fast-moving</code>; if drift is <em>extremely rare</em>, <code>stable</code>; otherwise <code>evolving</code>
1569
+ (the default).</p>
1570
+ <h3 id="adding-a-new-entry-to-the-kb">Adding a new entry to the KB</h3>
1571
+ <ol>
1572
+ <li><strong>Choose a category directory</strong> under <code>content/knowledge/&#x3C;category>/</code>. Many
1573
+ categories exist today (<code>backend</code>, <code>core</code>, <code>cli</code>, <code>research</code>, <code>web-app</code>,
1574
+ <code>web3</code>, …); prefer placing into an existing one. Creating a new category is a
1575
+ separate PR.</li>
1576
+ <li><strong>File name = entry slug + <code>.md</code>.</strong> The basename must match the <code>name:</code> field
1577
+ (e.g. <code>retry-with-jitter.md</code> ↔ <code>name: retry-with-jitter</code>). Lens I's
1578
+ suppression match reads <code>name:</code> only, not the filename — a mismatch silently
1579
+ breaks suppression.</li>
1580
+ <li><strong>Required frontmatter:</strong> <code>name</code>, <code>description</code>. Add <code>volatility</code> + <code>sources[]</code>
1581
+ if you want the cron to audit it — an entry with no <code>sources[]</code> is skipped by
1582
+ the prefilter (<span class="fp" data-path="src/knowledge-freshness/audit-prefilter.ts:17">src/knowledge-freshness/audit-prefilter.ts:17</span>).</li>
1583
+ <li><strong>Validate locally:</strong> <code>make validate-knowledge</code>.</li>
1584
+ <li><strong>Confirm the prefilter will pick it up.</strong> A fresh entry has no
1585
+ <code>last-reviewed</code>, so it should appear at priority 100:
1586
+ <pre><code class="language-bash">node dist/index.js knowledge-freshness audit-prefilter --max=10 \
1587
+ | jq '.[] | select(.name=="&#x3C;your-new-slug>")'
1588
+ </code></pre>
1589
+ The daily ceiling is 10, so a flood of new entries may queue past the first day.</li>
1590
+ </ol>
1591
+ <h3 id="gap-signal-payload">Gap-signal payload</h3>
1592
+ <p>A gap signal is a ledger event validated by
1593
+ <span class="fp" data-path="src/observability/engine/event-schemas.ts:191-220">src/observability/engine/event-schemas.ts:191-220</span> (payload allow-list at
1594
+ <span class="fp" data-path="src/observability/engine/event-schemas.ts:12">src/observability/engine/event-schemas.ts:12</span>):</p>
1595
+ <pre><code class="language-json">{
1596
+ "event_id": "&#x3C;uuid>",
1597
+ "worktree_id": "&#x3C;sha>",
1598
+ "actor_label": "agent | bot | …",
1599
+ "branch": "&#x3C;branch>",
1600
+ "task_id": null,
1601
+ "type": "knowledge_gap_signal",
1602
+ "ts": "&#x3C;ISO-8601>",
1603
+ "payload": {
1604
+ "topic": "&#x3C;kebab-slug>",
1605
+ "source": "agent_search",
1606
+ "project_id": "&#x3C;sha256-hex>",
1607
+ "step_name": "tech-stack",
1608
+ "agent_excerpt": "…"
1609
+ }
1610
+ }
1611
+ </code></pre>
1612
+ <p><code>topic</code> is ≤80 chars matching <code>/^[a-z0-9]+(-[a-z0-9]+)*$/</code>; <code>source</code> ∈
1613
+ {<code>agent_search</code>, <code>lessons</code>, <code>manual</code>}; <code>project_id</code> is 64-char sha256 hex (or the
1614
+ literal <code>lessons</code> when <code>source=lessons</code>); <code>step_name</code> and <code>agent_excerpt</code> (≤200
1615
+ chars) are optional.</p>
1616
+ <div class="callout callout-tip"><p><strong>Suppressing emission in tests/CI.</strong> Set <code>SCAFFOLD_GAP_SIGNAL_QUIET=1</code>. The
1617
+ assembly-time tail (<code>src/core/assembly/gap-signal-tail.ts</code>) then renders no
1618
+ emission template into the pipeline step. Default is always-on (locked decision
1619
+ #9) — catch gaps everywhere they occur.</p></div>
1620
+ <h3 id="knowledgerootresolution-shape">KnowledgeRootResolution shape</h3>
1621
+ <p>The resolver returns a three-field record that threads through the audit run
1622
+ (<span class="fp" data-path="src/observability/knowledge-index.ts:275-291">src/observability/knowledge-index.ts:275-291</span>):</p>
1623
+ <pre><code class="language-ts">export interface KnowledgeRootResolution {
1624
+ /** Validated absolute path to a knowledge directory, or null. */
1625
+ root: string | null
1626
+ /** Pre-loaded index Set, populated by the validator. Null when root is null.
1627
+ Lens I reads this directly — no re-walk. */
1628
+ index: Set&#x3C;string> | null
1629
+ /** Audit trail of what was tried. Lens I uses this to compose a precise
1630
+ warn-once message when root is null. */
1631
+ attempts: KnowledgeRootAttempt[]
1632
+ }
1633
+ </code></pre>
1634
+ <h2 id="from-candidate-to-merged-pr">From candidate to merged PR</h2>
1635
+ <p>The cron is a thin bash loop — the brains live in three CLI subcommands and a
1636
+ meta-prompt that runs a grounded LLM against pre-fetched source bodies.</p>
1637
+ <h3 id="prefilter">Prefilter</h3>
1638
+ <p>An entry becomes a candidate when (1) it has at least one source, AND (2) either
1639
+ its <code>last-reviewed</code> is older than the cadence window, OR a source's prefetched
1640
+ hash differs from the stored one. Priority orders highest-score first: unreviewed
1641
+ entries (100), then overdue entries (<code>50 + ageDays</code>, so the oldest rank highest),
1642
+ with in-window hash changes at 75; the top <code>--max</code>
1643
+ win (<span class="fp" data-path="src/knowledge-freshness/audit-prefilter.ts:14-72">src/knowledge-freshness/audit-prefilter.ts:14-72</span>):</p>
1644
+ <pre><code class="language-ts">for (const e of entries) {
1645
+ if (e.sources.length === 0) continue // no sources = no audit
1646
+ if (!e.lastReviewed) { select = true; priority = 100 }
1647
+ else if (ageDays > window) { select = true; priority = 50 + ageDays }
1648
+ else {
1649
+ // hash check — Promise.all over a small per-entry list (1-3 sources)
1650
+ if (anyHashChanged) { select = true; priority = 75 }
1651
+ }
1652
+ }
1653
+ candidates.sort((a, b) => b.priority - a.priority)
1654
+ return candidates.slice(0, max)
1655
+ </code></pre>
1656
+ <p>The hash check is a tiebreaker, not a baseline. Entries already <em>past</em> their
1657
+ window are selected immediately at priority <code>50 + ageDays</code> — no network cost.
1658
+ The hash check only runs in the <code>else</code> branch (still <em>inside</em> the window), runs
1659
+ <code>Promise.all</code> over the entry's 1–3 sources, and swallows fetch errors so a slow
1660
+ upstream doesn't crash the cron.</p>
1661
+ <h3 id="audit-verdicts">Audit verdicts</h3>
1662
+ <p>The meta-prompt at <code>content/tools/knowledge-audit-entry.md</code> instructs the LLM to
1663
+ read pre-fetched source bodies (no web tool available) and emit one of four
1664
+ verdicts. <strong>Every verdict opens a PR</strong> — the dry-run apply runs first so gates
1665
+ can inspect the proposed diff, then <code>--open-pr</code> creates the branch.</p>
1666
+
1667
+
1668
+
1669
+
1670
+
1671
+
1672
+
1673
+
1674
+
1675
+
1676
+
1677
+
1678
+
1679
+
1680
+
1681
+
1682
+
1683
+
1684
+
1685
+
1686
+
1687
+
1688
+
1689
+
1690
+
1691
+ <table><thead><tr><th>Verdict</th><th>What the PR contains</th></tr></thead><tbody><tr><td><code>current</code></td><td>Frontmatter-only: bumps <code>last-reviewed</code>, <code>sources[*].hash</code>, <code>sources[*].retrieved</code> so the entry exits the queue.</td></tr><tr><td><code>minor-drift</code></td><td>Frontmatter persistence + findings table as commentary. <code>applyVerdictToEntry</code> refuses any <code>proposed_changes</code> on this verdict (<span class="fp" data-path="src/knowledge-freshness/audit-apply.ts:54-58">src/knowledge-freshness/audit-apply.ts:54-58</span>); no body edits.</td></tr><tr><td><code>major-drift</code></td><td>Body edits land via <code>proposed_changes</code> (H2-heading-anchored splices). Gate 4 blocks if a stable entry's diff exceeds 20% churn without the override label.</td></tr><tr><td><code>superseded</code></td><td>A new edition shipped; <code>version-pin</code> must advance. <code>last-reviewed</code> does <strong>not</strong> advance (<span class="fp" data-path="src/knowledge-freshness/audit-apply.ts:103-118">src/knowledge-freshness/audit-apply.ts:103-118</span>) — only <code>hash</code>/<code>retrieved</code> update, so the entry stays due until a human re-audits. Prevents a known-stale entry from looking fresh.</td></tr></tbody></table>
1692
+ <h3 id="pr-generation">PR generation</h3>
1693
+ <p>Branch: <code>knowledge-freshness/&#x3C;entry>-&#x3C;YYYY-MM-DD></code>. <code>renderPrBody</code> renders a
1694
+ summary, the verdict fields, a findings table, the sources, and any preserve
1695
+ warnings (it does not embed the raw verdict JSON). Each candidate gets
1696
+ its own PR off <code>origin/main</code> — the cron <code>git checkout main</code> between iterations
1697
+ and restores the entry between the dry-run apply (for gates) and the final
1698
+ <code>--open-pr</code> call. PRs do not stack; failures isolate per-candidate.</p>
1699
+ <h4>VERSION bump on merge</h4>
1700
+ <p>A dedicated workflow
1701
+ (<span class="fp" data-path=".github/workflows/knowledge-freshness-version-bump.yml:16">.github/workflows/knowledge-freshness-version-bump.yml:16</span>) fires on PR
1702
+ <code>closed</code> (merged-only) when the source branch starts with <code>knowledge-freshness/</code>
1703
+ <em>or</em> the PR carries the <code>knowledge-freshness</code> label. It computes the next SemVer
1704
+ from the PR title and body, writes <code>content/knowledge/VERSION</code>, commits with the
1705
+ prefix <code>chore(knowledge):</code> (deliberately not <code>knowledge-freshness/*</code>) so the
1706
+ commit doesn't re-trigger itself, then <code>git pull --rebase</code> before pushing. Bump
1707
+ rules (<span class="fp" data-path="src/knowledge-freshness/bump-version.ts:26-45">src/knowledge-freshness/bump-version.ts:26-45</span>):</p>
1708
+
1709
+
1710
+
1711
+
1712
+
1713
+
1714
+
1715
+
1716
+
1717
+
1718
+
1719
+
1720
+
1721
+
1722
+
1723
+
1724
+
1725
+
1726
+
1727
+
1728
+
1729
+
1730
+
1731
+
1732
+
1733
+
1734
+
1735
+
1736
+
1737
+
1738
+ <table><thead><tr><th>Match</th><th>Bump</th><th>Notes</th></tr></thead><tbody><tr><td><code>BREAKING CHANGE:</code> anywhere in title, or start-of-line in body</td><td>major</td><td>Wins over every other prefix</td></tr><tr><td><code>feat(knowledge):</code> / <code>feat(knowledge-freshness):</code> title prefix</td><td>minor</td><td>Case-sensitive</td></tr><tr><td><code>chore(knowledge):</code> / <code>chore(knowledge-freshness):</code> title prefix</td><td>patch</td><td>Used by the bump commit itself</td></tr><tr><td>Anything else (including <code>fix(knowledge):</code>)</td><td>patch</td><td>Logs a <code>::notice::</code> for unrecognized prefixes</td></tr></tbody></table>
1739
+ <p>The start-of-line anchor on the BREAKING CHANGE body match (<code>/^BREAKING CHANGE:/m</code>) is deliberate — a freshness PR's body embeds an LLM-generated
1740
+ findings table whose evidence excerpts could otherwise mention "BREAKING CHANGE:"
1741
+ and trigger an accidental major bump.</p>
1742
+ <h3 id="mmr-corroboration-manual">MMR corroboration (manual)</h3>
1743
+ <p>The cron does <em>not</em> dispatch MMR today — the workflow only runs inline gates. To
1744
+ corroborate a freshness PR locally:</p>
1745
+ <pre><code class="language-bash">git diff origin/main...HEAD -- 'content/knowledge/**/*.md' \
1746
+ | mmr review --diff - --focus knowledge-freshness --sync --format json
1747
+ </code></pre>
1748
+ <p>A native <code>knowledge-freshness</code> MMR channel is the Phase 5 plan. See the
1749
+ <a href="../mmr/index.md">MMR guide</a> for the channel architecture.</p>
1750
+ <h2 id="the-five-pr-gates">The five PR gates</h2>
1751
+ <p>The cron's <code>GITHUB_TOKEN</code>-opened PRs don't fire downstream workflows, so the
1752
+ cron also runs the gate code inline (same CLI surface). Human-opened freshness
1753
+ PRs get gated by the workflow at
1754
+ <span class="fp" data-path=".github/workflows/knowledge-freshness-gates.yml:17">.github/workflows/knowledge-freshness-gates.yml:17</span>.</p>
1755
+ <div class="filter-table"><input type="text" class="filter-input" placeholder="Filter…" aria-label="Filter table rows" disabled>
1756
+
1757
+
1758
+
1759
+
1760
+
1761
+
1762
+
1763
+
1764
+
1765
+
1766
+
1767
+
1768
+
1769
+
1770
+
1771
+
1772
+
1773
+
1774
+
1775
+
1776
+
1777
+
1778
+
1779
+
1780
+
1781
+
1782
+
1783
+
1784
+
1785
+
1786
+
1787
+
1788
+
1789
+
1790
+
1791
+
1792
+
1793
+
1794
+
1795
+
1796
+
1797
+
1798
+
1799
+
1800
+
1801
+
1802
+ <table><thead><tr><th>#</th><th>Gate</th><th>What it checks</th><th>Mode</th><th>Source</th></tr></thead><tbody><tr><td>1</td><td>Frontmatter validator</td><td>Zod schema parse over every entry (excludes README). Strict calendar-date refinement; SSRF guard on source URLs.</td><td><span class="sev sev-p0">blocking</span></td><td><span class="fp" data-path="src/validation/knowledge-frontmatter-validator.ts:42-50">src/validation/knowledge-frontmatter-validator.ts:42-50</span></td></tr><tr><td>2</td><td>Source link-check</td><td>Every <code>sources[*].url</code> returns 2xx. Operates on the changed-files list via <code>--files-from</code>.</td><td><span class="sev sev-p0">blocking</span></td><td><span class="fp" data-path=".github/workflows/knowledge-freshness-gates.yml:117-123">.github/workflows/knowledge-freshness-gates.yml:117-123</span></td></tr><tr><td>3</td><td>Unsourced-claims lint</td><td>New normative claims must have a <code>sources[]</code> entry. Runs even when 1/2 failed.</td><td><span class="sev sev-p3">advisory</span></td><td><span class="fp" data-path=".github/workflows/knowledge-freshness-gates.yml:126-135">.github/workflows/knowledge-freshness-gates.yml:126-135</span></td></tr><tr><td>4</td><td>Anti-over-rewrite</td><td>Stable entries reject diffs deleting >20% of lines unless the <code>override:anti-over-rewrite</code> label is applied. Cron-opened <code>knowledge-freshness/*</code> branches only.</td><td><span class="sev sev-p1">blocking</span></td><td><span class="fp" data-path=".github/workflows/knowledge-freshness-gates.yml:137-152">.github/workflows/knowledge-freshness-gates.yml:137-152</span></td></tr><tr><td>5</td><td>Deep Guidance preserved</td><td>Literal <code>## Deep Guidance</code> heading must survive — the assembly engine pulls just that section.</td><td><span class="sev sev-p0">blocking</span></td><td><span class="fp" data-path=".github/workflows/knowledge-freshness-gates.yml:154-160">.github/workflows/knowledge-freshness-gates.yml:154-160</span></td></tr></tbody></table></div>
1803
+ <div class="callout callout-warning"><p><strong>Spec drift on the Gate 4 override.</strong> The parent spec describes the override as
1804
+ a marker in the PR <em>description</em>; the shipped mechanism
1805
+ (<span class="fp" data-path=".github/workflows/knowledge-freshness-gates.yml:148-152">.github/workflows/knowledge-freshness-gates.yml:148-152</span>) reads a
1806
+ maintainer-applied PR <em>label</em> (<code>override:anti-over-rewrite</code>) via <code>--pr-labels</code>.
1807
+ The shipped behavior is authoritative; the spec text is stale.</p></div>
1808
+ <div class="callout callout-note"><p><strong>Anti-tamper checkout (known gap).</strong> The gate workflow builds the gate code from
1809
+ HEAD, not from <code>origin/main</code>
1810
+ (<span class="fp" data-path=".github/workflows/knowledge-freshness-gates.yml:42-53">.github/workflows/knowledge-freshness-gates.yml:42-53</span>). The hardening —
1811
+ build from base, overlay only PR HEAD's <code>content/knowledge/</code> — is deferred
1812
+ because the bootstrap PR introduced the gate code itself. Risk is mitigated by
1813
+ mandatory PR review until a follow-up flips the checkout strategy.</p></div>
1814
+ <h2 id="lens-i-gap-detection-suppression">Lens I — gap detection + suppression</h2>
1815
+ <p>Lens I runs under <code>--scope=docs</code> and <code>--scope=all</code>
1816
+ (<span class="fp" data-path="src/observability/checks/lens-i-knowledge-gaps.ts:43">src/observability/checks/lens-i-knowledge-gaps.ts:43</span>). It collects
1817
+ signals from the ledger (rolling 90-day window,
1818
+ <span class="fp" data-path="src/observability/checks/lens-i-knowledge-gaps.ts:52">src/observability/checks/lens-i-knowledge-gaps.ts:52</span>) plus synthetic
1819
+ signals from <code>tasks/lessons.md</code>, buckets them by normalized topic, applies the
1820
+ threshold matrix, and suppresses buckets whose topic an entry already covers.</p>
1821
+ <div class="callout callout-note"><p><strong>Where Lens I sits in the taxonomy.</strong> "Lens" is scaffold's name for an audit
1822
+ check function inside <code>scaffold observe audit</code>. The full set is A–I; Lens I
1823
+ (<code>I-knowledge-gaps</code>) is this one. The other seven plus Lens H are documented in
1824
+ the <a href="../observability/index.md">Build Observability guide</a>.</p></div>
1825
+ <h3 id="threshold-matrix">Threshold matrix</h3>
1826
+ <p>The rules (<span class="fp" data-path="src/observability/checks/lens-i-knowledge-gaps.ts:148-149">src/observability/checks/lens-i-knowledge-gaps.ts:148-149</span>):</p>
1827
+
1828
+
1829
+
1830
+
1831
+
1832
+
1833
+
1834
+
1835
+
1836
+
1837
+
1838
+
1839
+
1840
+
1841
+
1842
+
1843
+
1844
+
1845
+
1846
+
1847
+
1848
+
1849
+
1850
+
1851
+
1852
+ <table><thead><tr><th>signal_count</th><th>distinct_projects</th><th>Severity</th></tr></thead><tbody><tr><td>≥ 5</td><td>≥ 3</td><td><span class="sev sev-p1">P1</span></td></tr><tr><td>≥ 3</td><td>≥ 2</td><td><span class="sev sev-p2">P2</span></td></tr><tr><td>below both</td><td>—</td><td>no finding</td></tr></tbody></table>
1853
+ <h3 id="topic-normalization">Topic normalization</h3>
1854
+ <p>Lens I normalizes the raw topic before bucketing, then validates the result.
1855
+ Two distinct steps: <code>normalizeTopic</code>
1856
+ (<span class="fp" data-path="src/observability/checks/lens-i-lessons-scanner.ts:32-38">src/observability/checks/lens-i-lessons-scanner.ts:32-38</span>) always
1857
+ produces a (possibly empty) string; <code>isValidTopic</code>
1858
+ (<span class="fp" data-path="src/observability/checks/lens-i-lessons-scanner.ts:114-116">src/observability/checks/lens-i-lessons-scanner.ts:114-116</span>) decides
1859
+ whether to accept it. Normalization lowercases, strips apostrophes, replaces
1860
+ every other non-slug run with a single hyphen, collapses repeats, and trims. The
1861
+ validator additionally enforces ≤ 80 chars and <code>/^[a-z0-9]+(-[a-z0-9]+)*$/</code>.
1862
+ So <code>Agent Eval Harnesses!</code> → <code>agent-eval-harnesses</code> (valid), but <code>!!!</code> → `` (rejected).</p>
1863
+ <h3 id="what-the-lessonsmd-scanner-sees">What the lessons.md scanner sees</h3>
1864
+ <p>Lens I synthesizes signals from <code>tasks/lessons.md</code> at audit time (read inline, no
1865
+ ledger writes) via two passes per non-fenced line — code-fenced blocks are
1866
+ skipped (<span class="fp" data-path="src/observability/checks/lens-i-lessons-scanner.ts:4">src/observability/checks/lens-i-lessons-scanner.ts:4</span>):</p>
1867
+ <ol>
1868
+ <li><strong>Explicit marker</strong> — <code>&#x3C;!-- gap-topic: &#x3C;slug> --></code> (slug must already be
1869
+ kebab-case; the marker regex enforces it).</li>
1870
+ <li><strong>Heuristic phrases</strong> (case-insensitive): <em>"would have helped to have a guide
1871
+ on X"</em>, <em>"missing knowledge entry for X"</em>, <em>"no knowledge entry for X"</em> /
1872
+ <em>"no kb entry for X"</em>, <em>"missing knowledge: X"</em>.</li>
1873
+ </ol>
1874
+ <p>Captured topics run through the same <code>normalizeTopic</code> / <code>isValidTopic</code>.
1875
+ Synthetic signals carry <code>project_id: "lessons"</code> and are <strong>excluded</strong> from the
1876
+ distinct-projects count by the aggregator's <code>delete('lessons')</code> rule (decision
1877
+ #6) — they corroborate but don't independently satisfy the threshold.</p>
1878
+ <h3 id="3-tier---knowledge-root-resolution">3-tier <code>--knowledge-root</code> resolution</h3>
1879
+ <p>Lens I must know where the KB lives to skip already-covered topics. The resolver
1880
+ (<code>resolveKnowledgeRoot</code> at <span class="fp" data-path="src/observability/knowledge-index.ts:326-379">src/observability/knowledge-index.ts:326-379</span>)
1881
+ tries three tiers in order:</p>
1882
+
1883
+
1884
+
1885
+
1886
+
1887
+
1888
+
1889
+
1890
+
1891
+
1892
+
1893
+
1894
+
1895
+
1896
+
1897
+
1898
+
1899
+
1900
+
1901
+
1902
+
1903
+
1904
+
1905
+
1906
+
1907
+ <table><thead><tr><th>Tier</th><th>Source</th><th>On failure</th></tr></thead><tbody><tr><td>1</td><td><code>--knowledge-root</code> CLI flag (resolved against <code>process.cwd()</code>)</td><td><strong>hard error</strong> before any lens runs (<code>KnowledgeRootCliInvalidError</code>)</td></tr><tr><td>2</td><td><code>lenses.I-knowledge-gaps.knowledge_root</code> in yaml (resolved against cwd)</td><td>soft-fail; records <code>{outcome: 'invalid', reason}</code> in the attempts trail</td></tr><tr><td>3</td><td>auto-detect — <code>findScaffoldKnowledgeRoot</code> walks parents for <code>package.json#name === '@zigrivers/scaffold'</code> (<span class="fp" data-path="src/observability/knowledge-index.ts:164-178">src/observability/knowledge-index.ts:164-178</span>)</td><td>returns <code>null</code> if no install is found</td></tr></tbody></table>
1908
+ <p>The sharp asymmetry is intentional: an operator who <em>typed</em> a <code>--knowledge-root</code>
1909
+ gets a hard error on a bad path; yaml and auto-detect soft-fail so suppression
1910
+ degrades gracefully. The most instructive case: yaml invalid + auto-detect found
1911
+ — the trail records the yaml failure <em>and</em> the auto-detect success, root is the
1912
+ auto-detect path, and a one-line stderr note points at the stale yaml. This is
1913
+ what an operator sees when <code>npm update -g @zigrivers/scaffold</code> moved the install
1914
+ out from under a pinned yaml path.</p>
1915
+ <h3 id="warning-policy">Warning policy</h3>
1916
+
1917
+
1918
+
1919
+
1920
+
1921
+
1922
+
1923
+
1924
+
1925
+
1926
+
1927
+
1928
+
1929
+
1930
+
1931
+
1932
+
1933
+
1934
+
1935
+
1936
+
1937
+
1938
+
1939
+
1940
+
1941
+ <table><thead><tr><th>Key</th><th>Status</th><th>When emitted</th></tr></thead><tbody><tr><td><code>lens-i:no-root</code></td><td>active</td><td>Lens I runs, no root resolved, lens enabled. Per-audit deduped via <code>warnedKeys: Set&#x3C;string></code>. If yaml failed validation, the message gains a clause quoting the bad path + reason.</td></tr><tr><td><code>lens-i:index-load-failed</code></td><td>reserved</td><td>Never emitted today — <code>validateKnowledgeRoot</code> exercises the loader at resolution time, foreclosing this path.</td></tr><tr><td>(none)</td><td>no-warn</td><td>Lens I disabled — resolver runs but no warning surfaces (decisions #4 / #11).</td></tr></tbody></table>
1942
+ <p><code>emitOnceForAudit</code> (<span class="fp" data-path="src/observability/knowledge-index.ts:251-259">src/observability/knowledge-index.ts:251-259</span>) reads a
1943
+ caller-provided <code>Set</code> created fresh in each <code>runAudit</code>
1944
+ (<span class="fp" data-path="src/observability/engine/api.ts:114">src/observability/engine/api.ts:114</span>), so the <code>--fix</code> flow's three
1945
+ internal audits each get their own dedup scope.</p>
1946
+ <h3 id="what-a-lens-i-finding-looks-like">What a Lens I finding looks like</h3>
1947
+ <p>A single finding excerpt from the audit sidecar (<code>docs/audits/&#x3C;id>.json</code>):</p>
1948
+ <pre><code class="language-json">{
1949
+ "id": "a3f2c1d4...",
1950
+ "lens_id": "I-knowledge-gaps",
1951
+ "severity": "P2",
1952
+ "title": "Knowledge base lacks coverage for \"agent-eval-harnesses\" — 4 signals across 2 projects",
1953
+ "source_doc": "",
1954
+ "evidence": {
1955
+ "kind": "knowledge_gap",
1956
+ "topic": "agent-eval-harnesses",
1957
+ "signal_count": 4,
1958
+ "distinct_project_count": 2,
1959
+ "distinct_projects": ["a3f2...", "1c4e..."],
1960
+ "first_seen": "2026-04-12T09:00:00Z",
1961
+ "last_seen": "2026-05-21T14:30:00Z",
1962
+ "example_excerpts": ["No knowledge entry for agent eval harnesses"]
1963
+ },
1964
+ "confidence": "medium",
1965
+ "fix_hint": {
1966
+ "kind": "edit_doc",
1967
+ "target": "content/knowledge/&#x3C;category>/agent-eval-harnesses.md",
1968
+ "prompt": "Propose a new knowledge entry for \"agent-eval-harnesses\". Evidence: 4 signals from 2 projects in the last 90 days."
1969
+ }
1970
+ }
1971
+ </code></pre>
1972
+ <div class="callout callout-warning"><p><strong>Phase audits don't trigger Lens I.</strong> The phase-boundary hook
1973
+ (<code>StateManager.markCompleted</code> → <code>runPhaseAudit</code> at
1974
+ <span class="fp" data-path="src/observability/engine/phase-audit.ts:63">src/observability/engine/phase-audit.ts:63</span>) fires only Lens H-cross-doc
1975
+ (<code>lensIds: ['H-cross-doc']</code> at <span class="fp" data-path="src/observability/engine/phase-audit.ts:77">src/observability/engine/phase-audit.ts:77</span>).
1976
+ Lens I never runs at phase boundaries. A phase-audit run that surfaces zero
1977
+ findings does <strong>not</strong> mean Lens I is happy — it means Lens I never ran. To see
1978
+ Lens I findings, invoke <code>scaffold observe audit --scope=docs</code> (or <code>--scope=all</code>)
1979
+ explicitly, or run it through <code>--fix</code>.</p></div>
1980
+ <h2 id="the-allowlist">The allowlist</h2>
1981
+ <p>Out-of-allowlist sources warn but don't block (decision #4). Bare hostnames
1982
+ match subdomains; <code>host/path</code> entries additionally require the URL path to start
1983
+ with the prefix; <code>github_repos</code> is locked to specific <code>owner/repo</code>.</p>
1984
+ <p>The off-allowlist warning is <strong>advisory</strong> and is surfaced by the
1985
+ frontmatter-validation path (<code>validateKnowledgeFile</code>), not by a gate. Gate 3
1986
+ (<code>lint-unsourced</code>) is a separate advisory check that flags nearby links not
1987
+ covered by the entry's declared <code>sources[]</code> domains. Off-allowlist sources still
1988
+ get fetched, hashed, and audited — they just warn.
1989
+ It is <strong>not</strong> a security boundary: the SSRF guard
1990
+ (<code>src/knowledge-freshness/source-url-validator.ts</code>) runs independently, so a new
1991
+ host never unlocks private-IP fetches. The editorial bar is: "would the
1992
+ maintainers want this URL to be the verbatim grounding for a P0/P1 finding?"</p>
1993
+ <h3 id="most-cited-hosts">Most-cited hosts</h3>
1994
+ <p>Counted live from every entry's <code>sources[*].url</code> at build time.</p>
1995
+
1996
+ <div class="chart-block"><div class="chart chart-bar"><div class="chart-row" aria-label="martinfowler.com: 37"><span class="chart-label">martinfowler.com</span><div class="chart-bar" style="width:100%"></div></div><div class="chart-row" aria-label="developer.mozilla.org: 24"><span class="chart-label">developer.mozilla.org</span><div class="chart-bar" style="width:65%"></div></div><div class="chart-row" aria-label="owasp.org: 17"><span class="chart-label">owasp.org</span><div class="chart-bar" style="width:46%"></div></div><div class="chart-row" aria-label="developer.android.com: 15"><span class="chart-label">developer.android.com</span><div class="chart-bar" style="width:41%"></div></div><div class="chart-row" aria-label="the-turing-way.netlify.app: 15"><span class="chart-label">the-turing-way.netlify.app</span><div class="chart-bar" style="width:41%"></div></div><div class="chart-row" aria-label="developer.apple.com: 14"><span class="chart-label">developer.apple.com</span><div class="chart-bar" style="width:38%"></div></div><div class="chart-row" aria-label="developer.chrome.com: 14"><span class="chart-label">developer.chrome.com</span><div class="chart-bar" style="width:38%"></div></div><div class="chart-row" aria-label="sre.google: 12"><span class="chart-label">sre.google</span><div class="chart-bar" style="width:32%"></div></div><div class="chart-row" aria-label="w3.org: 12"><span class="chart-label">w3.org</span><div class="chart-bar" style="width:32%"></div></div><div class="chart-row" aria-label="microservices.io: 11"><span class="chart-label">microservices.io</span><div class="chart-bar" style="width:30%"></div></div><div class="chart-row" aria-label="ethereum.org: 10"><span class="chart-label">ethereum.org</span><div class="chart-bar" style="width:27%"></div></div><div class="chart-row" aria-label="rfc-editor.org: 10"><span class="chart-label">rfc-editor.org</span><div class="chart-bar" style="width:27%"></div></div><div class="chart-row" aria-label="consensys.github.io: 9"><span class="chart-label">consensys.github.io</span><div class="chart-bar" style="width:24%"></div></div><div class="chart-row" aria-label="docs.openzeppelin.com: 9"><span class="chart-label">docs.openzeppelin.com</span><div class="chart-bar" style="width:24%"></div></div><div class="chart-row" aria-label="opentelemetry.io: 9"><span class="chart-label">opentelemetry.io</span><div class="chart-bar" style="width:24%"></div></div></div>
1997
+
1998
+
1999
+
2000
+
2001
+
2002
+
2003
+
2004
+
2005
+
2006
+
2007
+
2008
+
2009
+
2010
+
2011
+
2012
+
2013
+
2014
+
2015
+
2016
+
2017
+
2018
+
2019
+
2020
+
2021
+
2022
+
2023
+
2024
+
2025
+
2026
+
2027
+
2028
+
2029
+
2030
+
2031
+
2032
+
2033
+
2034
+
2035
+
2036
+
2037
+
2038
+
2039
+
2040
+
2041
+
2042
+
2043
+
2044
+
2045
+
2046
+
2047
+
2048
+
2049
+
2050
+
2051
+
2052
+
2053
+
2054
+
2055
+
2056
+
2057
+
2058
+
2059
+
2060
+
2061
+
2062
+
2063
+
2064
+
2065
+ <table><thead><tr><th>Host</th><th>Citations</th></tr></thead><tbody><tr><td>martinfowler.com</td><td>37</td></tr><tr><td>developer.mozilla.org</td><td>24</td></tr><tr><td>owasp.org</td><td>17</td></tr><tr><td>developer.android.com</td><td>15</td></tr><tr><td>the-turing-way.netlify.app</td><td>15</td></tr><tr><td>developer.apple.com</td><td>14</td></tr><tr><td>developer.chrome.com</td><td>14</td></tr><tr><td>sre.google</td><td>12</td></tr><tr><td>w3.org</td><td>12</td></tr><tr><td>microservices.io</td><td>11</td></tr><tr><td>ethereum.org</td><td>10</td></tr><tr><td>rfc-editor.org</td><td>10</td></tr><tr><td>consensys.github.io</td><td>9</td></tr><tr><td>docs.openzeppelin.com</td><td>9</td></tr><tr><td>opentelemetry.io</td><td>9</td></tr></tbody></table></div>
2066
+
2067
+ <h3 id="the-full-allowlist">The full allowlist</h3>
2068
+ <p>Every host plus its category, and the pinned GitHub repos.</p>
2069
+
2070
+ <p>47 allowlisted hosts and 3 GitHub repos. Out-of-list sources warn (they do not block).</p>
2071
+ <div class="filter-table"><input type="text" class="filter-input" placeholder="Filter…" aria-label="Filter table rows" disabled>
2072
+
2073
+
2074
+
2075
+
2076
+
2077
+
2078
+
2079
+
2080
+
2081
+
2082
+
2083
+
2084
+
2085
+
2086
+
2087
+
2088
+
2089
+
2090
+
2091
+
2092
+
2093
+
2094
+
2095
+
2096
+
2097
+
2098
+
2099
+
2100
+
2101
+
2102
+
2103
+
2104
+
2105
+
2106
+
2107
+
2108
+
2109
+
2110
+
2111
+
2112
+
2113
+
2114
+
2115
+
2116
+
2117
+
2118
+
2119
+
2120
+
2121
+
2122
+
2123
+
2124
+
2125
+
2126
+
2127
+
2128
+
2129
+
2130
+
2131
+
2132
+
2133
+
2134
+
2135
+
2136
+
2137
+
2138
+
2139
+
2140
+
2141
+
2142
+
2143
+
2144
+
2145
+
2146
+
2147
+
2148
+
2149
+
2150
+
2151
+
2152
+
2153
+
2154
+
2155
+
2156
+
2157
+
2158
+
2159
+
2160
+
2161
+
2162
+
2163
+
2164
+
2165
+
2166
+
2167
+
2168
+
2169
+
2170
+
2171
+
2172
+
2173
+
2174
+
2175
+
2176
+
2177
+
2178
+
2179
+
2180
+
2181
+
2182
+
2183
+
2184
+
2185
+
2186
+
2187
+
2188
+
2189
+
2190
+
2191
+
2192
+
2193
+
2194
+
2195
+
2196
+
2197
+
2198
+
2199
+
2200
+
2201
+
2202
+
2203
+
2204
+
2205
+
2206
+
2207
+
2208
+
2209
+
2210
+
2211
+
2212
+
2213
+
2214
+
2215
+
2216
+
2217
+
2218
+
2219
+
2220
+
2221
+
2222
+
2223
+
2224
+
2225
+
2226
+
2227
+
2228
+
2229
+
2230
+
2231
+
2232
+
2233
+
2234
+
2235
+
2236
+
2237
+
2238
+
2239
+
2240
+
2241
+
2242
+
2243
+
2244
+
2245
+
2246
+
2247
+
2248
+
2249
+
2250
+
2251
+
2252
+
2253
+
2254
+
2255
+
2256
+
2257
+
2258
+
2259
+
2260
+
2261
+
2262
+
2263
+
2264
+
2265
+
2266
+
2267
+
2268
+ <table><thead><tr><th>Host</th><th>Category</th></tr></thead><tbody><tr><td><code>ai.google.dev</code></td><td>ai-ml</td></tr><tr><td><code>anthropic.com</code></td><td>ai-ml</td></tr><tr><td><code>docs.wandb.ai</code></td><td>ai-ml</td></tr><tr><td><code>mlflow.org</code></td><td>ai-ml</td></tr><tr><td><code>modelcontextprotocol.io</code></td><td>ai-ml</td></tr><tr><td><code>platform.openai.com</code></td><td>ai-ml</td></tr><tr><td><code>spec.graphql.org</code></td><td>api</td></tr><tr><td><code>spec.openapis.org</code></td><td>api</td></tr><tr><td><code>developer.chrome.com</code></td><td>browser-ext</td></tr><tr><td><code>docs.aws.amazon.com</code></td><td>cloud-ops</td></tr><tr><td><code>opentelemetry.io</code></td><td>cloud-ops</td></tr><tr><td><code>sre.google</code></td><td>cloud-ops</td></tr><tr><td><code>aicpa-cima.com</code></td><td>compliance</td></tr><tr><td><code>aicpa.org</code></td><td>compliance</td></tr><tr><td><code>eur-lex.europa.eu</code></td><td>compliance</td></tr><tr><td><code>pcisecuritystandards.org</code></td><td>compliance</td></tr><tr><td><code>www.finra.org</code></td><td>compliance</td></tr><tr><td><code>www.sec.gov</code></td><td>compliance</td></tr><tr><td><code>developer.android.com</code></td><td>mobile</td></tr><tr><td><code>developer.apple.com</code></td><td>mobile</td></tr><tr><td><code>adr.github.io</code></td><td>patterns</td></tr><tr><td><code>agilealliance.org</code></td><td>patterns</td></tr><tr><td><code>conventionalcommits.org</code></td><td>patterns</td></tr><tr><td><code>google.github.io</code></td><td>patterns</td></tr><tr><td><code>martinfowler.com</code></td><td>patterns</td></tr><tr><td><code>microservices.io</code></td><td>patterns</td></tr><tr><td><code>thoughtworks.com</code></td><td>patterns</td></tr><tr><td><code>the-turing-way.netlify.app</code></td><td>research</td></tr><tr><td><code>nist.gov</code></td><td>security</td></tr><tr><td><code>openid.net</code></td><td>security</td></tr><tr><td><code>owasp.org</code></td><td>security</td></tr><tr><td><code>consensys.github.io</code></td><td>smart-contracts</td></tr><tr><td><code>docs.openzeppelin.com</code></td><td>smart-contracts</td></tr><tr><td><code>docs.safe.global</code></td><td>smart-contracts</td></tr><tr><td><code>ethereum.org</code></td><td>smart-contracts</td></tr><tr><td><code>swcregistry.io</code></td><td>smart-contracts</td></tr><tr><td><code>ietf.org/rfc</code></td><td>standards</td></tr><tr><td><code>www.iso.org</code></td><td>standards</td></tr><tr><td><code>www.rfc-editor.org</code></td><td>standards</td></tr><tr><td><code>docs.pact.io</code></td><td>testing</td></tr><tr><td><code>docs.astral.sh</code></td><td>tooling</td></tr><tr><td><code>git-scm.com</code></td><td>tooling</td></tr><tr><td><code>peps.python.org</code></td><td>tooling</td></tr><tr><td><code>www.postgresql.org</code></td><td>tooling</td></tr><tr><td><code>developer.mozilla.org</code></td><td>web-standards</td></tr><tr><td><code>tr.designtokens.org</code></td><td>web-standards</td></tr><tr><td><code>www.w3.org</code></td><td>web-standards</td></tr></tbody></table></div>
2269
+ <p><strong>GitHub repos:</strong> <code>modelcontextprotocol/specification</code>, <code>steveyegge/beads</code>, <code>joelparkerhenderson/architecture-decision-record</code></p>
2270
+
2271
+ <h3 id="kb-inventory">KB inventory</h3>
2272
+ <p>Totals over <code>content/knowledge/</code>, broken down per category.</p>
2273
+
2274
+ <p><strong>266 entries</strong> across 19 categories:</p>
2275
+
2276
+
2277
+
2278
+
2279
+
2280
+
2281
+
2282
+
2283
+
2284
+
2285
+
2286
+
2287
+
2288
+
2289
+
2290
+
2291
+
2292
+
2293
+
2294
+
2295
+
2296
+
2297
+
2298
+
2299
+
2300
+
2301
+
2302
+
2303
+
2304
+
2305
+
2306
+
2307
+
2308
+
2309
+
2310
+
2311
+
2312
+
2313
+
2314
+
2315
+
2316
+
2317
+
2318
+
2319
+
2320
+
2321
+
2322
+
2323
+
2324
+
2325
+
2326
+
2327
+
2328
+
2329
+
2330
+
2331
+
2332
+
2333
+
2334
+
2335
+
2336
+
2337
+
2338
+
2339
+
2340
+
2341
+
2342
+
2343
+
2344
+
2345
+
2346
+
2347
+
2348
+
2349
+
2350
+
2351
+
2352
+
2353
+
2354
+
2355
+
2356
+
2357
+
2358
+
2359
+
2360
+ <table><thead><tr><th>Category</th><th>Entries</th></tr></thead><tbody><tr><td>core</td><td>35</td></tr><tr><td>game</td><td>25</td></tr><tr><td>research</td><td>25</td></tr><tr><td>backend</td><td>22</td></tr><tr><td>review</td><td>20</td></tr><tr><td>web-app</td><td>17</td></tr><tr><td>web3</td><td>14</td></tr><tr><td>data-science</td><td>13</td></tr><tr><td>browser-extension</td><td>12</td></tr><tr><td>data-pipeline</td><td>12</td></tr><tr><td>library</td><td>12</td></tr><tr><td>ml</td><td>12</td></tr><tr><td>mobile-app</td><td>12</td></tr><tr><td>cli</td><td>10</td></tr><tr><td>validation</td><td>7</td></tr><tr><td>product</td><td>6</td></tr><tr><td>execution</td><td>5</td></tr><tr><td>tools</td><td>4</td></tr><tr><td>finalization</td><td>3</td></tr></tbody></table>
2361
+
2362
+ <h3 id="how-to-expand-the-allowlist">How to expand the allowlist</h3>
2363
+ <p>Adding a host is a one-line PR to
2364
+ <code>docs/knowledge-freshness/authoritative-sources.yaml</code>:</p>
2365
+ <pre><code class="language-diff"> hosts:
2366
+ - owasp.org
2367
+ + - developers.cloudflare.com
2368
+ - nist.gov
2369
+ </code></pre>
2370
+ <ol>
2371
+ <li><strong>Pick the form.</strong> Bare hostname for vendor docs whose path layout changes;
2372
+ <code>host/path</code> prefix for shared-tenancy hosts where you only trust a sub-path;
2373
+ <code>owner/repo</code> under <code>github_repos:</code> for specific GitHub repos. Skip <code>www.</code>
2374
+ (bare entries auto-match subdomains).</li>
2375
+ <li><strong>Verify the host is live</strong> — <code>curl -sI https://&#x3C;host>/&#x3C;path></code> should return
2376
+ 2xx (or a 3xx that ultimately resolves).</li>
2377
+ <li><strong>Mirror the category</strong> in <code>CATEGORY_MAP</code> in
2378
+ <code>scripts/build-freshness-reference.mjs</code> — otherwise the regenerated allowlist
2379
+ table shows the new host as <code>other</code>.</li>
2380
+ <li><strong>Open a normal PR.</strong> Allowlist additions are not a separate trust delegation;
2381
+ any maintainer can review.</li>
2382
+ </ol>
2383
+ <h2 id="anthropic-vs-deepseek-cron-uses-deepseek">Anthropic vs DeepSeek (cron uses DeepSeek)</h2>
2384
+ <p>The cron switched to DeepSeek HTTP to remove the local <code>claude</code> CLI dependency
2385
+ from CI. Local audits keep using whichever provider is configured. Precedence is
2386
+ resolved by <code>resolveProvider</code> (<span class="fp" data-path="src/knowledge-freshness/providers/index.ts:36">src/knowledge-freshness/providers/index.ts:36</span>):</p>
2387
+ <ol>
2388
+ <li><code>--provider &#x3C;name></code> — explicit flag, operator override</li>
2389
+ <li><code>KNOWLEDGE_FRESHNESS_PROVIDER</code> env var</li>
2390
+ <li>A single API key in env — inferred</li>
2391
+ <li>Both API keys present → error (ambiguous)</li>
2392
+ <li>No env, <code>claude</code> on PATH → anthropic (subprocess uses keychain)</li>
2393
+ <li>Nothing → error (no provider configured)</li>
2394
+ </ol>
2395
+ <div class="tabs"><div class="tablist" role="tablist"><button id="tab-0-0" class="tab-btn active" role="tab" data-tab="0" aria-controls="tabpane-0-0" aria-selected="true" tabindex="0">Anthropic</button><button id="tab-0-1" class="tab-btn" role="tab" data-tab="1" aria-controls="tabpane-0-1" aria-selected="false" tabindex="-1">DeepSeek</button></div><div id="tabpane-0-0" class="tabpane active" role="tabpanel" data-tab="0" aria-labelledby="tab-0-0" tabindex="0"><p>Subprocess: <code>claude -p --tools ""</code> (empty-tools disables WebFetch so the model
2396
+ can only read the prefetched bodies). <strong>Requires the <code>claude</code> CLI on PATH
2397
+ regardless of how the provider was chosen</strong> — the resolver throws
2398
+ (<span class="fp" data-path="src/knowledge-freshness/providers/index.ts:44-56">src/knowledge-freshness/providers/index.ts:44-56</span>) if anthropic is
2399
+ picked via flag, env, or API-key inference and <code>claude</code> isn't installed.
2400
+ <code>ANTHROPIC_API_KEY</code> alone is <em>not</em> sufficient. Source:
2401
+ <code>src/knowledge-freshness/providers/anthropic.ts</code>.</p></div><div id="tabpane-0-1" class="tabpane" role="tabpanel" data-tab="1" aria-labelledby="tab-0-1" tabindex="0"><p>HTTP. No subprocess; works in CI without the Claude CLI.</p><ul>
2402
+ <li><strong>Auth:</strong> requires <code>DEEPSEEK_API_KEY</code>.</li>
2403
+ <li><strong>Default model:</strong> <code>deepseek-v4-flash</code>.</li>
2404
+ <li><strong>Override:</strong> set <code>KNOWLEDGE_FRESHNESS_DEEPSEEK_MODEL</code> to <code>deepseek-v4-pro</code>.
2405
+ Other values throw at dispatcher-build time
2406
+ (<span class="fp" data-path="src/knowledge-freshness/providers/deepseek.ts:54-58">src/knowledge-freshness/providers/deepseek.ts:54-58</span>).</li>
2407
+ <li><strong>Thinking mode:</strong> hardcoded <code>thinking: { type: 'disabled' }</code>.</li>
2408
+ <li><strong>URL:</strong> hardcoded to <code>https://api.deepseek.com/chat/completions</code>;
2409
+ project-local config cannot redirect (decision #7 invariant).</li>
2410
+ </ul></div></div>
2411
+ <div class="callout callout-danger"><p><strong>Why the DeepSeek URL is hardcoded.</strong> An untrusted project's
2412
+ <code>.scaffold/observability.yaml</code> could otherwise redirect the LLM dispatcher at an
2413
+ attacker-controlled host that captures <code>DEEPSEEK_API_KEY</code> from request headers.
2414
+ Hardcoding closes that exfiltration path — the same threat model that hardcodes
2415
+ Lens H's <code>claude -p</code> command in the Build Observability audit.</p></div>
2416
+ <p>The cron wires DeepSeek explicitly
2417
+ (<span class="fp" data-path=".github/workflows/knowledge-freshness-audit.yml:70">.github/workflows/knowledge-freshness-audit.yml:70</span>):</p>
2418
+ <pre><code class="language-yaml">env:
2419
+ DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
2420
+ KNOWLEDGE_FRESHNESS_PROVIDER: deepseek
2421
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2422
+ </code></pre>
2423
+ <p>A missing <code>DEEPSEEK_API_KEY</code> fails the run loudly at preflight rather than
2424
+ silently exiting 0 with zero PRs.</p>
2425
+ <h2 id="every-command-that-touches-the-system">Every command that touches the system</h2>
2426
+ <p>All commands ship in the published CLI.</p>
2427
+ <h3 id="refresh-arm-commands">Refresh-arm commands</h3>
2428
+
2429
+
2430
+
2431
+
2432
+
2433
+
2434
+
2435
+
2436
+
2437
+
2438
+
2439
+
2440
+
2441
+
2442
+
2443
+
2444
+
2445
+
2446
+
2447
+
2448
+
2449
+
2450
+
2451
+
2452
+
2453
+ <table><thead><tr><th>Command</th><th>Purpose</th></tr></thead><tbody><tr><td><code>scaffold knowledge-freshness audit-prefilter [--max=N]</code></td><td>Walk <code>content/knowledge/</code>, apply cadence + hash check, print a JSON candidate array. <code>--max</code> default 10 (<span class="fp" data-path="src/cli/commands/knowledge-freshness-audit-prefilter.ts:18">src/cli/commands/knowledge-freshness-audit-prefilter.ts:18</span>); the CLI emits only <code>{ name, path }</code> per candidate (<span class="fp" data-path="src/cli/commands/knowledge-freshness-audit-prefilter.ts:43">src/cli/commands/knowledge-freshness-audit-prefilter.ts:43</span>).</td></tr><tr><td><code>scaffold knowledge-freshness audit-run-entry &#x3C;path></code></td><td>Pre-fetch each source through SSRF guards, dispatch the grounded audit, print verdict JSON. <code>--provider anthropic|deepseek</code> overrides env precedence.</td></tr><tr><td><code>scaffold knowledge-freshness audit-apply &#x3C;path> &#x3C;verdict.json> [--open-pr]</code></td><td>Patch frontmatter + apply <code>proposed_changes</code> by H2 heading. The wrapper re-fetches every checked URL and computes its own sha256 (<span class="fp" data-path="src/knowledge-freshness/audit-apply.ts:82-101">src/knowledge-freshness/audit-apply.ts:82-101</span>), so persisted hashes are deterministic, not the LLM's claim. Refuses to advance <code>last-reviewed</code> unless every declared source is covered.</td></tr><tr><td><code>make validate-knowledge</code></td><td>Gate 1 — runs the Zod validator over every entry (README excluded).</td></tr></tbody></table>
2454
+ <h3 id="gap-arm-commands">Gap-arm commands</h3>
2455
+
2456
+
2457
+
2458
+
2459
+
2460
+
2461
+
2462
+
2463
+
2464
+
2465
+
2466
+
2467
+
2468
+
2469
+
2470
+
2471
+
2472
+
2473
+
2474
+
2475
+
2476
+ <table><thead><tr><th>Command</th><th>Purpose</th></tr></thead><tbody><tr><td><code>scaffold observe audit --lens I-knowledge-gaps [--knowledge-root &#x3C;path>] [--fix]</code></td><td>Run the gap-detection lens against the local ledger + <code>tasks/lessons.md</code>. <code>--knowledge-root</code> overrides yaml + auto-detect for suppression. <code>--fix</code> dispatches the fix flow; the override threads through all three audits.</td></tr><tr><td><code>scaffold observe event knowledge_gap_signal --topic=&#x3C;slug> --source=&#x3C;…> --project-id=&#x3C;sha> …</code></td><td>Write one validated gap signal to the ledger. Used by the assembly-time tail and by operators backfilling synthetic signals.</td></tr><tr><td><code>scaffold observe ack &#x3C;prefix-or-id></code></td><td>Acknowledge (or reopen) a finding so it stops surfacing. Use when a Lens I topic is deliberately out of scope.</td></tr></tbody></table>
2477
+ <p>The <code>--fix</code> flow (<code>runFixFlow</code> at <span class="fp" data-path="src/observability/engine/fix-flow.ts:71">src/observability/engine/fix-flow.ts:71</span>)
2478
+ runs a three-audit loop: (1) the initial audit produces a fix plan; (2) for each
2479
+ blocking finding, dispatch a fix agent then re-audit just that finding (the
2480
+ verifier); (3) one postfix audit runs everything for the final report. The
2481
+ <code>--knowledge-root</code> override threads into all three (decision #20) so suppression
2482
+ is consistent throughout.</p>
2483
+ <h3 id="gate-side-subcommands-also-runnable-locally-for-triage">Gate-side subcommands (also runnable locally for triage)</h3>
2484
+
2485
+
2486
+
2487
+
2488
+
2489
+
2490
+
2491
+
2492
+
2493
+
2494
+
2495
+
2496
+
2497
+
2498
+
2499
+
2500
+
2501
+
2502
+
2503
+
2504
+
2505
+
2506
+
2507
+
2508
+
2509
+
2510
+
2511
+
2512
+
2513
+
2514
+
2515
+
2516
+
2517
+
2518
+
2519
+ <table><thead><tr><th>Command</th><th>Gate</th><th>Purpose</th></tr></thead><tbody><tr><td><code>knowledge-freshness link-check [&#x3C;path>] [--files-from &#x3C;json>]</code></td><td>2</td><td>HTTP-HEAD every <code>sources[*].url</code>; 2xx passes, else exit 1.</td></tr><tr><td><code>knowledge-freshness lint-unsourced [&#x3C;path>] [--files-from &#x3C;json>] [--diff &#x3C;patch>]</code></td><td>3</td><td>Heuristic scan for normative language in new lines without a <code>sources[]</code> reference. Advisory: prints findings but always exits 0.</td></tr><tr><td><code>knowledge-freshness anti-over-rewrite [--files-from &#x3C;json>] [--diff &#x3C;patch>] [--pr-labels &#x3C;csv>]</code></td><td>4</td><td>For each changed <code>stable</code> entry, compare deleted-line count to 20% of the body; exit 1 if crossed without <code>override:anti-over-rewrite</code>. The cron passes <code>--pr-labels ""</code> (it can't self-apply labels).</td></tr><tr><td><code>knowledge-freshness deep-guidance-check [&#x3C;path>] [--files-from &#x3C;json>]</code></td><td>5</td><td>Assert each changed entry still contains a <code>## Deep Guidance</code> heading (case-sensitive).</td></tr><tr><td><code>knowledge-freshness bump-version --title &#x3C;str> --body &#x3C;str></code></td><td>—</td><td>Pure-function dry-run of <code>deriveBumpKind</code> + <code>bumpSemver</code>; prints <code>bump:</code> and <code>next:</code> lines parsed by the version-bump workflow.</td></tr></tbody></table>
2520
+ <h2 id="operations-cheat-sheet">Operations cheat sheet</h2>
2521
+ <h3 id="an-entrys-audit-failed-in-the-cron">An entry's audit failed in the cron</h3>
2522
+ <p>The cron logs <code>audit failed for &#x3C;name> — moving on</code> and continues; the entry
2523
+ stays in tomorrow's queue. Causes: provider auth (key rotated), source URL now
2524
+ 404s, a fetch/HTTP error or the 5 MiB fetch-and-hash cap, dispatcher error, or
2525
+ LLM timeout. (A source body over the 96 KiB embed cap is <strong>truncated</strong> and
2526
+ flagged <code>truncated: true</code> — it does not fail the audit.) Reproduce locally:</p>
2527
+ <pre><code class="language-bash">DEEPSEEK_API_KEY=sk-… node dist/index.js knowledge-freshness \
2528
+ audit-run-entry content/knowledge/&#x3C;cat>/&#x3C;name>.md
2529
+ # read stderr to see if it's a URL issue or a provider issue
2530
+ </code></pre>
2531
+ <h3 id="lens-i-keeps-surfacing-a-topic-the-kb-already-covers">Lens I keeps surfacing a topic the KB already covers</h3>
2532
+ <p>Suppression didn't match. Either the resolver returned <code>root: null</code> (look for
2533
+ <code>[Lens I] knowledge-root not located</code> in stderr) or the entry's <code>name:</code> doesn't
2534
+ normalize to the same slug as the bucket topic — the match is exact and
2535
+ post-normalize.</p>
2536
+ <pre><code class="language-bash">scaffold observe audit --lens I-knowledge-gaps --json \
2537
+ --knowledge-root /path/to/content/knowledge \
2538
+ | jq '.findings[] | select(.lens_id=="I-knowledge-gaps")'
2539
+ grep -A1 "^---" content/knowledge/&#x3C;cat>/&#x3C;slug>.md | grep "^name:"
2540
+ </code></pre>
2541
+ <h3 id="downstream-auto-detect-cant-find-the-kb">Downstream auto-detect can't find the KB</h3>
2542
+ <p><code>findScaffoldKnowledgeRoot</code> walks parents from the CLI install's module location
2543
+ looking for <code>package.json#name === '@zigrivers/scaffold'</code>. Symlinked or
2544
+ repackaged installs may miss. Pin it via the tier-2 yaml:</p>
2545
+ <pre><code class="language-yaml">lenses:
2546
+ I-knowledge-gaps:
2547
+ knowledge_root: /opt/homebrew/lib/node_modules/@zigrivers/scaffold/content/knowledge
2548
+ </code></pre>
2549
+ <h3 id="yaml-knowledge-root-stops-working-after-an-upgrade">Yaml <code>knowledge_root</code> stops working after an upgrade</h3>
2550
+ <p>The yaml tier soft-fails and records the reason in the attempts trail; Lens I
2551
+ appends it to the warning. Validation requires all four: the path exists, is a
2552
+ directory, contains a <code>&#x3C;path>/VERSION</code> marker, and <code>loadKnowledgeIndex</code> runs
2553
+ without throwing (an empty index is OK). The usual cause after an upgrade is a
2554
+ moved install path:</p>
2555
+ <pre><code class="language-bash">find / -name VERSION -path '*content/knowledge*' 2>/dev/null
2556
+ </code></pre>
2557
+ <p>Then update <code>lenses.I-knowledge-gaps.knowledge_root</code> to the new path.</p>
2558
+ <h3 id="a-source-url-fetches-in-curl-but-the-cron-rejects-it">A source URL fetches in <code>curl</code> but the cron rejects it</h3>
2559
+ <p>The SSRF guard re-resolves the hostname at fetch time and rejects any IP in a
2560
+ non-globally-routable range (RFC1918, link-local, loopback, CGNAT, ULA,
2561
+ IPv4-mapped IPv6, …). Common cause: an internal DNS view returning a private IP
2562
+ for an outwardly-public hostname.</p>
2563
+ <pre><code class="language-bash">node -e 'require("node:dns").promises.lookup("&#x3C;host>", { all: true }).then(console.log)'
2564
+ </code></pre>
2565
+ <p>Fix: move the source to a globally-routable host, or remove it. Allowlisting does
2566
+ <strong>not</strong> bypass the SSRF guard.</p>
2567
+ <h3 id="--knowledge-root-resolves-to-a-path-you-didnt-expect"><code>--knowledge-root</code> resolves to a path you didn't expect</h3>
2568
+ <p>Auto-detect may pick a stale npm-global install. The successful-resolution path
2569
+ doesn't log its <code>attempts</code> trail today (only the failure path warns), so pin and
2570
+ compare:</p>
2571
+ <pre><code class="language-bash">scaffold observe audit --lens I-knowledge-gaps --json \
2572
+ --knowledge-root /path/you/expected/content/knowledge \
2573
+ | jq '.findings[] | select(.lens_id=="I-knowledge-gaps") | .evidence.topic'
2574
+ # compare against the unset behavior; if the lists differ, auto-detect picked a different KB
2575
+ </code></pre>
2576
+ <p>Fix: pin <code>lenses.I-knowledge-gaps.knowledge_root</code> in
2577
+ <code>.scaffold/observability.yaml</code>. A pinned yaml path takes precedence over
2578
+ auto-detect.</p>
2579
+ <h2 id="config-reference">Config reference</h2>
2580
+ <p>Everything operator-tunable lives in <code>.scaffold/observability.yaml</code>. Anything
2581
+ outside this list is hardcoded (decision #7 invariant) so an untrusted project
2582
+ can't redirect dispatch commands or LLM URLs.</p>
2583
+ <pre><code class="language-yaml">lenses:
2584
+ I-knowledge-gaps:
2585
+ knowledge_root: /path/to/content/knowledge # tier-2 resolver override
2586
+
2587
+ disabled_lenses: [I-knowledge-gaps] # opt-out
2588
+
2589
+ phase_audit:
2590
+ enabled: true # default
2591
+ timeout_s: 60
2592
+ detached: false # fire-and-forget when true
2593
+
2594
+ fix:
2595
+ dispatcher_command: "claude -p" # default
2596
+ timeout_s: 300
2597
+ per_finding_max_attempts: 3
2598
+ </code></pre>
2599
+ <div class="callout callout-warning"><p><strong>The daily audit ceiling is NOT in yaml.</strong> The parent spec's decision #8 reads
2600
+ "10 grounded audits per day; configurable via <code>.scaffold/observability.yaml</code>",
2601
+ but the yaml knob was never implemented. The ceiling is the <code>--max=10</code> flag in
2602
+ <span class="fp" data-path=".github/workflows/knowledge-freshness-audit.yml:67">.github/workflows/knowledge-freshness-audit.yml:67</span>; the CLI default at
2603
+ <span class="fp" data-path="src/cli/commands/knowledge-freshness-audit-prefilter.ts:18">src/cli/commands/knowledge-freshness-audit-prefilter.ts:18</span> is the only
2604
+ fallback. To lower it for your fork, edit the workflow — nothing in yaml will help.</p></div>
2605
+ <h2 id="roadmap-and-known-divergences">Roadmap and known divergences</h2>
2606
+ <h3 id="phase-5-planned">Phase 5 (planned)</h3>
2607
+ <ul>
2608
+ <li><strong>Native MMR <code>knowledge-freshness</code> channel</strong> — runs automatically on freshness
2609
+ PRs (today the cron dispatches no MMR).</li>
2610
+ <li><strong>Frontier scan</strong> — augment cadence/hash triggers with a model-driven check:
2611
+ "has the underlying technology meaningfully changed since <code>last-reviewed</code>?"</li>
2612
+ <li><strong>Taxonomy cross-reference</strong> — detect when two entries assert contradictory
2613
+ facts and route to a reviewer.</li>
2614
+ </ul>
2615
+ <h3 id="known-divergences">Known divergences</h3>
2616
+ <p>The reference page's own audits surfaced these doc-vs-code mismatches; the code
2617
+ is ground truth:</p>
2618
+ <ul>
2619
+ <li><strong>MMR-in-cron framing</strong> — three docs describe it differently; decision #3
2620
+ (Phase 5 deferral) is authoritative.</li>
2621
+ <li><strong>Gate 4 override</strong> — spec says PR-description marker; code reads a PR <em>label</em>.</li>
2622
+ <li><strong>Daily ceiling</strong> — spec implies a yaml knob; only the <code>--max</code> flag exists.</li>
2623
+ <li><strong><code>operations.md</code> lags</strong> — labels the native MMR channel "Phase 4" and
2624
+ describes suppression in future tense; both have shipped.</li>
2625
+ <li><strong><code>www.</code> prefix inconsistency</strong> (P3) — mixed <code>www.</code> use in the allowlist; bare
2626
+ entries already auto-match subdomains, so the prefix is redundant.</li>
2627
+ </ul></main>
2628
+ <div class="rail-backdrop" data-action="nav" aria-hidden="true"></div>
2629
+ </div>
2630
+ <script>(function(){
2631
+ var LS_KEY = 'guide-theme';
2632
+ function applyTheme(t) {
2633
+ document.documentElement.setAttribute('data-theme', t);
2634
+ }
2635
+
2636
+ document.addEventListener('DOMContentLoaded', function() {
2637
+ // ─── Theme toggle ────────────────────────────────────────────────────────
2638
+ document.querySelectorAll('[data-action="theme"]').forEach(function(btn) {
2639
+ btn.addEventListener('click', function() {
2640
+ var current = document.documentElement.getAttribute('data-theme');
2641
+ var next = current === 'dark' ? 'light' : 'dark';
2642
+ applyTheme(next);
2643
+ try { localStorage.setItem(LS_KEY, next); } catch(e) {}
2644
+ });
2645
+ });
2646
+
2647
+ // ─── Mobile nav (drawer + backdrop; aria-expanded + Escape-to-close) ──────
2648
+ function setNav(open) {
2649
+ var rail = document.querySelector('.rail');
2650
+ if (rail) rail.classList.toggle('open', open);
2651
+ var toggle = document.querySelector('.nav-toggle');
2652
+ if (toggle) toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
2653
+ // Modal-drawer focus containment: while open, make the page content inert
2654
+ // (out of tab order + a11y tree) and move focus into the drawer; on close,
2655
+ // restore content and return focus to the toggle.
2656
+ var main = document.querySelector('.content');
2657
+ if (main) main.inert = open;
2658
+ if (open) {
2659
+ var first = rail && rail.querySelector('a, button, [tabindex]:not([tabindex="-1"])');
2660
+ if (first) first.focus();
2661
+ else if (rail) { rail.setAttribute('tabindex', '-1'); rail.focus(); }
2662
+ } else if (toggle) {
2663
+ toggle.focus();
2664
+ }
2665
+ }
2666
+ // If the viewport grows past the mobile breakpoint while the drawer is open,
2667
+ // the rail becomes the desktop sidebar and the toggle hides — clear the open
2668
+ // state so .content doesn't stay inert with no way to close it.
2669
+ if (window.matchMedia) {
2670
+ var mq = window.matchMedia('(max-width: 860px)');
2671
+ var onMq = function() {
2672
+ if (mq.matches) return;
2673
+ var rail = document.querySelector('.rail');
2674
+ if (rail) rail.classList.remove('open');
2675
+ var toggle = document.querySelector('.nav-toggle');
2676
+ if (toggle) toggle.setAttribute('aria-expanded', 'false');
2677
+ var main = document.querySelector('.content');
2678
+ if (main) main.inert = false;
2679
+ };
2680
+ if (mq.addEventListener) mq.addEventListener('change', onMq);
2681
+ else if (mq.addListener) mq.addListener(onMq);
2682
+ }
2683
+ document.querySelectorAll('[data-action="nav"]').forEach(function(btn) {
2684
+ btn.addEventListener('click', function() {
2685
+ var rail = document.querySelector('.rail');
2686
+ setNav(!(rail && rail.classList.contains('open')));
2687
+ });
2688
+ });
2689
+ // Selecting a TOC link closes the drawer (so the now-active content isn't
2690
+ // left inert behind the panel) before the anchor navigation scrolls.
2691
+ var drawerRail = document.querySelector('.rail');
2692
+ if (drawerRail) {
2693
+ drawerRail.querySelectorAll('a').forEach(function(a) {
2694
+ a.addEventListener('click', function() {
2695
+ if (drawerRail.classList.contains('open')) setNav(false);
2696
+ });
2697
+ });
2698
+ }
2699
+ document.addEventListener('keydown', function(e) {
2700
+ var rail = document.querySelector('.rail');
2701
+ if (!rail || !rail.classList.contains('open')) return;
2702
+ if (e.key === 'Escape') { setNav(false); return; } // setNav restores focus to the toggle
2703
+ // Trap Tab within the open drawer (modal pattern).
2704
+ if (e.key !== 'Tab') return;
2705
+ var f = rail.querySelectorAll('a[href], button, [tabindex]:not([tabindex="-1"])');
2706
+ if (!f.length) return;
2707
+ var first = f[0], last = f[f.length - 1];
2708
+ if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
2709
+ else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
2710
+ });
2711
+
2712
+ // ─── Copy buttons ─────────────────────────────────────────────────────────
2713
+ document.querySelectorAll('pre').forEach(function(pre) {
2714
+ if (!pre.parentNode) return;
2715
+ var wrapper = document.createElement('div');
2716
+ wrapper.className = 'code';
2717
+ pre.parentNode.insertBefore(wrapper, pre);
2718
+ wrapper.appendChild(pre);
2719
+ var btn = document.createElement('button');
2720
+ btn.className = 'copy-btn';
2721
+ btn.textContent = 'Copy';
2722
+ btn.addEventListener('click', function() {
2723
+ var text = pre.textContent || '';
2724
+ if (navigator.clipboard && navigator.clipboard.writeText) {
2725
+ navigator.clipboard.writeText(text).then(function() {
2726
+ btn.textContent = 'Copied';
2727
+ setTimeout(function() { btn.textContent = 'Copy'; }, 1200);
2728
+ }, function() {
2729
+ btn.textContent = 'Copy';
2730
+ });
2731
+ }
2732
+ });
2733
+ wrapper.insertBefore(btn, pre);
2734
+ });
2735
+
2736
+ // ─── Tabs (ARIA pattern: aria-selected + roving tabindex + arrow keys) ────
2737
+ function activateTab(group, btn, focus) {
2738
+ var idx = btn.getAttribute('data-tab');
2739
+ group.querySelectorAll('.tab-btn').forEach(function(b) {
2740
+ var on = b === btn;
2741
+ b.classList.toggle('active', on);
2742
+ b.setAttribute('aria-selected', on ? 'true' : 'false');
2743
+ b.setAttribute('tabindex', on ? '0' : '-1');
2744
+ });
2745
+ group.querySelectorAll('.tabpane').forEach(function(pane) {
2746
+ pane.classList.toggle('active', pane.getAttribute('data-tab') === idx);
2747
+ });
2748
+ if (focus) btn.focus();
2749
+ }
2750
+ document.querySelectorAll('.tabs').forEach(function(group) {
2751
+ var btns = [].slice.call(group.querySelectorAll('.tab-btn'));
2752
+ btns.forEach(function(btn, i) {
2753
+ btn.addEventListener('click', function() { activateTab(group, btn, false); });
2754
+ btn.addEventListener('keydown', function(e) {
2755
+ var ni = -1;
2756
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') ni = (i + 1) % btns.length;
2757
+ else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') ni = (i - 1 + btns.length) % btns.length;
2758
+ else if (e.key === 'Home') ni = 0;
2759
+ else if (e.key === 'End') ni = btns.length - 1;
2760
+ if (ni >= 0) { e.preventDefault(); activateTab(group, btns[ni], true); }
2761
+ });
2762
+ });
2763
+ });
2764
+
2765
+ // ─── Filter tables ────────────────────────────────────────────────────────
2766
+ document.querySelectorAll('.filter-input').forEach(function(input) {
2767
+ input.addEventListener('input', function() {
2768
+ var q = input.value.toLowerCase();
2769
+ var container = input.closest('.filter-table');
2770
+ if (!container) return;
2771
+ container.querySelectorAll('tbody tr').forEach(function(row) {
2772
+ var text = (row.textContent || '').toLowerCase();
2773
+ row.style.display = text.includes(q) ? '' : 'none';
2774
+ });
2775
+ });
2776
+ });
2777
+
2778
+ // ─── Scrollspy ────────────────────────────────────────────────────────────
2779
+ if (typeof IntersectionObserver === 'undefined') return;
2780
+ var headings = document.querySelectorAll('h2[id],h3[id]');
2781
+ if (!headings.length) return;
2782
+ var observer = new IntersectionObserver(function(entries) {
2783
+ entries.forEach(function(entry) {
2784
+ if (!entry.isIntersecting) return;
2785
+ var id = entry.target.getAttribute('id');
2786
+ document.querySelectorAll('.toc a').forEach(function(a) {
2787
+ a.classList.toggle('active', a.getAttribute('href') === '#' + id);
2788
+ });
2789
+ });
2790
+ }, { rootMargin: '0px 0px -70% 0px', threshold: 0 });
2791
+ headings.forEach(function(h) { observer.observe(h); });
2792
+ });
2793
+ })();</script>
2794
+ </body>
2795
+ </html>