agenthub-multiagent-mcp 1.49.0 → 1.51.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 (47) hide show
  1. package/README.md +192 -192
  2. package/dist/brain/backend.d.ts.map +1 -1
  3. package/dist/brain/backend.js +11 -2
  4. package/dist/brain/backend.js.map +1 -1
  5. package/dist/brain/memvidBrain.d.ts +24 -0
  6. package/dist/brain/memvidBrain.d.ts.map +1 -1
  7. package/dist/brain/memvidBrain.js +350 -93
  8. package/dist/brain/memvidBrain.js.map +1 -1
  9. package/dist/brain/memvidCompaction.test.d.ts +10 -0
  10. package/dist/brain/memvidCompaction.test.d.ts.map +1 -0
  11. package/dist/brain/memvidCompaction.test.js +71 -0
  12. package/dist/brain/memvidCompaction.test.js.map +1 -0
  13. package/dist/channel.js +3 -3
  14. package/dist/client.d.ts +43 -0
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +53 -2
  17. package/dist/client.js.map +1 -1
  18. package/dist/commands/setup-shell.js +11 -11
  19. package/dist/employee.d.ts +0 -5
  20. package/dist/employee.d.ts.map +1 -1
  21. package/dist/employee.js +14 -1
  22. package/dist/employee.js.map +1 -1
  23. package/dist/hooks/periodicReminder.js +9 -9
  24. package/dist/setup.js +56 -56
  25. package/dist/tools/index.d.ts.map +1 -1
  26. package/dist/tools/index.js +167 -11
  27. package/dist/tools/index.js.map +1 -1
  28. package/dist/tools/openspec-viz/diagram-generator.js +54 -54
  29. package/dist/tools/openspec-viz/html-generator.js +503 -503
  30. package/dist/tools/tools.test.js +4 -2
  31. package/dist/tools/tools.test.js.map +1 -1
  32. package/dist/worker.js +10 -10
  33. package/native/agenthub-memvid/agenthub-memvid.linux-x64-gnu.node +0 -0
  34. package/native/agenthub-memvid/agenthub-memvid.win32-x64-msvc.node +0 -0
  35. package/native/agenthub-memvid/index.d.ts +58 -0
  36. package/native/agenthub-memvid/index.js +319 -0
  37. package/package.json +77 -75
  38. package/skills/catalog.json +76 -76
  39. package/skills/commands/close-session.md +144 -144
  40. package/skills/commands/start-session.md +77 -77
  41. package/skills/manifest.json +50 -50
  42. package/skills/skills/code-brain-hook/SKILL.md +61 -61
  43. package/skills/skills/deploy-staging/SKILL.md +164 -164
  44. package/skills/skills/deploy-vps-openclaw/SKILL.md +97 -97
  45. package/skills/skills/file-bug/SKILL.md +75 -75
  46. package/skills/skills/karpathy-guidelines/SKILL.md +67 -67
  47. package/skills/skills/multiagent-brainstorm/SKILL.md +142 -142
@@ -12,29 +12,29 @@ import { humanizeChangeId } from "./parser.js";
12
12
  * Exported so agents using `openspec_parse` can include consistent styling
13
13
  * when generating custom HTML with Claude's intelligence.
14
14
  */
15
- export const OPENSPEC_BASE_CSS = `
16
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
17
- *{margin:0;padding:0;box-sizing:border-box}
18
- body{font-family:'Inter',sans-serif;background:#FAF8F3;color:#1a2332;line-height:1.6}
19
- a{color:#4A7C59;text-decoration:none}
20
- .container{max-width:1100px;margin:0 auto;padding:40px 24px}
21
- .hero{background:#1a2332;color:#fff;padding:48px 0;border-bottom:4px solid #FF8F00}
22
- .hero .container{display:flex;flex-direction:column;gap:16px}
23
- .hero h1{font-size:32px;font-weight:700}
24
- section{margin-bottom:40px}
25
- section h2{font-size:22px;font-weight:700;color:#1a2332;border-left:4px solid #4A7C59;padding-left:12px;margin-bottom:16px}
26
- .card{background:#fff;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.06);padding:20px;margin-bottom:16px}
27
- table{width:100%;border-collapse:collapse;font-size:13px}
28
- th{background:#E8F0EB;color:#4A7C59;padding:8px 12px;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:1px}
29
- td{padding:8px 12px;border-bottom:1px solid #f0ede6}
30
- .field-name{font-family:'JetBrains Mono',monospace;font-weight:500;color:#1a2332}
31
- .field-type{font-family:'JetBrains Mono',monospace;color:#FF8F00;font-size:12px}
32
- .method-badge{display:inline-block;padding:2px 8px;border-radius:3px;font-size:11px;font-weight:700;font-family:'JetBrains Mono',monospace;min-width:54px;text-align:center}
33
- .method-badge.GET{background:#E8F0EB;color:#4A7C59}
34
- .method-badge.POST{background:#E3F2FD;color:#1565C0}
35
- .method-badge.PATCH{background:#FFF3E0;color:#E65100}
36
- .method-badge.PUT{background:#FFF3E0;color:#E65100}
37
- .method-badge.DELETE{background:#FFEBEE;color:#C62828}
15
+ export const OPENSPEC_BASE_CSS = `
16
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
17
+ *{margin:0;padding:0;box-sizing:border-box}
18
+ body{font-family:'Inter',sans-serif;background:#FAF8F3;color:#1a2332;line-height:1.6}
19
+ a{color:#4A7C59;text-decoration:none}
20
+ .container{max-width:1100px;margin:0 auto;padding:40px 24px}
21
+ .hero{background:#1a2332;color:#fff;padding:48px 0;border-bottom:4px solid #FF8F00}
22
+ .hero .container{display:flex;flex-direction:column;gap:16px}
23
+ .hero h1{font-size:32px;font-weight:700}
24
+ section{margin-bottom:40px}
25
+ section h2{font-size:22px;font-weight:700;color:#1a2332;border-left:4px solid #4A7C59;padding-left:12px;margin-bottom:16px}
26
+ .card{background:#fff;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.06);padding:20px;margin-bottom:16px}
27
+ table{width:100%;border-collapse:collapse;font-size:13px}
28
+ th{background:#E8F0EB;color:#4A7C59;padding:8px 12px;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:1px}
29
+ td{padding:8px 12px;border-bottom:1px solid #f0ede6}
30
+ .field-name{font-family:'JetBrains Mono',monospace;font-weight:500;color:#1a2332}
31
+ .field-type{font-family:'JetBrains Mono',monospace;color:#FF8F00;font-size:12px}
32
+ .method-badge{display:inline-block;padding:2px 8px;border-radius:3px;font-size:11px;font-weight:700;font-family:'JetBrains Mono',monospace;min-width:54px;text-align:center}
33
+ .method-badge.GET{background:#E8F0EB;color:#4A7C59}
34
+ .method-badge.POST{background:#E3F2FD;color:#1565C0}
35
+ .method-badge.PATCH{background:#FFF3E0;color:#E65100}
36
+ .method-badge.PUT{background:#FFF3E0;color:#E65100}
37
+ .method-badge.DELETE{background:#FFEBEE;color:#C62828}
38
38
  `;
39
39
  export async function generateHTML(parsed, outputDir) {
40
40
  if (!fs.existsSync(outputDir)) {
@@ -89,363 +89,363 @@ function buildHTML(p) {
89
89
  const date = new Date(p.generatedAt).toLocaleDateString("en-US", {
90
90
  year: "numeric", month: "long", day: "numeric",
91
91
  });
92
- return `<!DOCTYPE html>
93
- <html lang="en">
94
- <head>
95
- <meta charset="UTF-8">
96
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
97
- <title>${esc(title)} — OpenSpec Viewer</title>
98
- <style>
99
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
100
- *{margin:0;padding:0;box-sizing:border-box}
101
- body{font-family:'Inter',sans-serif;background:#FAF8F3;color:#1a2332;line-height:1.6}
102
- a{color:#4A7C59;text-decoration:none}
103
- .container{max-width:1100px;margin:0 auto;padding:40px 24px}
104
-
105
- /* Hero */
106
- .hero{background:#1a2332;color:#fff;padding:48px 0;border-bottom:4px solid #FF8F00}
107
- .hero .container{display:flex;flex-direction:column;gap:16px}
108
- .hero h1{font-size:32px;font-weight:700}
109
- .hero .subtitle{color:#a0b0c0;font-size:16px}
110
- .hero .meta{display:flex;gap:32px;flex-wrap:wrap;margin-top:8px}
111
- .hero .meta-item{display:flex;flex-direction:column;gap:2px}
112
- .hero .meta-item .label{color:#7a8a9a;font-size:11px;text-transform:uppercase;letter-spacing:1px}
113
- .hero .meta-item .value{color:#FF8F00;font-size:14px;font-weight:600}
114
- .stats{display:flex;gap:24px;margin-top:16px}
115
- .stat{background:rgba(255,255,255,0.08);border-radius:8px;padding:12px 20px;text-align:center}
116
- .stat .num{font-size:24px;font-weight:700;color:#4A7C59}
117
- .stat .lbl{font-size:11px;color:#a0b0c0;text-transform:uppercase}
118
-
119
- /* Mode badge */
120
- .mode-badge{display:inline-block;padding:4px 12px;border-radius:4px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px}
121
- .mode-badge.change{background:#4A7C59;color:#fff}
122
- .mode-badge.spec{background:#1565C0;color:#fff}
123
- .mode-badge.diff{background:#FF8F00;color:#fff}
124
- .mode-badge.project{background:#7B1FA2;color:#fff}
125
-
126
- /* Search */
127
- .search-bar{margin:24px 0;position:relative}
128
- .search-bar input{width:100%;padding:12px 16px 12px 44px;border:2px solid #e0ddd5;border-radius:8px;font-size:14px;background:#fff;outline:none;font-family:'Inter',sans-serif}
129
- .search-bar input:focus{border-color:#4A7C59}
130
- .search-bar svg{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#7a8a9a}
131
-
132
- /* Gaps banner */
133
- .gaps-banner{background:#FFF3E0;border:1px solid #FFB74D;border-radius:8px;padding:16px 20px;margin-bottom:32px}
134
- .gaps-banner h3{color:#E65100;font-size:14px;margin-bottom:8px}
135
- .gaps-banner ul{list-style:none;padding:0}
136
- .gaps-banner li{color:#BF360C;font-size:13px;padding:4px 0;padding-left:20px;position:relative}
137
- .gaps-banner li::before{content:'⚠';position:absolute;left:0}
138
-
139
- /* Section */
140
- section{margin-bottom:40px}
141
- section h2{font-size:22px;font-weight:700;color:#1a2332;border-left:4px solid #4A7C59;padding-left:12px;margin-bottom:16px}
142
- section h2.orange-accent{border-left-color:#FF8F00}
143
-
144
- /* Cards */
145
- .card{background:#fff;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.06);padding:20px;margin-bottom:16px}
146
- .card h3{font-size:16px;color:#4A7C59;margin-bottom:8px}
147
- .card p{font-size:14px;color:#4a5568}
148
-
149
- /* Decision cards */
150
- .decision-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(480px,1fr));gap:16px}
151
- .dec-card{background:#fff;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.06);padding:20px}
152
- .dec-card h3{font-size:15px;color:#4A7C59;margin-bottom:8px;border-bottom:2px solid #E8F0EB;padding-bottom:8px}
153
- .dec-row{display:flex;gap:8px;margin-bottom:6px;align-items:flex-start}
154
- .dec-label{padding:2px 8px;border-radius:3px;font-size:10px;font-weight:700;white-space:nowrap;flex-shrink:0}
155
- .dec-label.primary{background:#1a2332;color:#FF8F00}
156
- .dec-label.alt{background:#f5f0e8;color:#7a8a9a}
157
- .dec-text{font-size:13px;color:#4a5568;line-height:1.5}
158
-
159
- /* Data Model */
160
- .model-card{background:#fff;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.06);padding:20px;overflow-x:auto}
161
- .model-card h3{font-size:16px;font-family:'JetBrains Mono',monospace;color:#4A7C59;margin-bottom:12px}
162
- table{width:100%;border-collapse:collapse;font-size:13px}
163
- th{background:#E8F0EB;color:#4A7C59;padding:8px 12px;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:1px}
164
- td{padding:8px 12px;border-bottom:1px solid #f0ede6}
165
- .field-name{font-family:'JetBrains Mono',monospace;font-weight:500;color:#1a2332}
166
- .field-type{font-family:'JetBrains Mono',monospace;color:#FF8F00;font-size:12px}
167
- .req-badge{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600}
168
- .req-badge.yes{background:#E8F0EB;color:#4A7C59}
169
- .req-badge.no{background:#f5f0e8;color:#7a8a9a}
170
- .idx-list{margin-top:12px;display:flex;gap:8px;flex-wrap:wrap}
171
- .idx-tag{background:#1a2332;color:#FF8F00;font-family:'JetBrains Mono',monospace;font-size:11px;padding:4px 8px;border-radius:4px}
172
-
173
- /* API Routes */
174
- .route-table{overflow-x:auto}
175
- .method-badge{display:inline-block;padding:2px 8px;border-radius:3px;font-size:11px;font-weight:700;font-family:'JetBrains Mono',monospace;min-width:54px;text-align:center}
176
- .method-badge.GET{background:#E8F0EB;color:#4A7C59}
177
- .method-badge.POST{background:#E3F2FD;color:#1565C0}
178
- .method-badge.PATCH{background:#FFF3E0;color:#E65100}
179
- .method-badge.PUT{background:#FFF3E0;color:#E65100}
180
- .method-badge.DELETE{background:#FFEBEE;color:#C62828}
181
- .auth-badge{font-size:11px;padding:2px 8px;border-radius:3px}
182
- .auth-badge.public{background:#FFF3E0;color:#FF8F00}
183
- .auth-badge.auth{background:#E8F0EB;color:#4A7C59}
184
- .route-path{font-family:'JetBrains Mono',monospace;font-size:13px;color:#1a2332}
185
-
186
- /* Tasks */
187
- .task-group{margin-bottom:24px}
188
- .task-group h3{font-size:15px;color:#1a2332;margin-bottom:8px;cursor:pointer;display:flex;align-items:center;gap:8px}
189
- .task-group h3::before{content:'▸';color:#4A7C59;font-size:12px;transition:transform 0.2s}
190
- .task-group.open h3::before{transform:rotate(90deg)}
191
- .progress-bar{height:6px;background:#f0ede6;border-radius:3px;overflow:hidden;margin-bottom:8px}
192
- .progress-fill{height:100%;background:#4A7C59;border-radius:3px;transition:width 0.3s}
193
- .progress-text{font-size:11px;color:#7a8a9a;margin-bottom:8px}
194
- .task-list{list-style:none;padding:0;display:none}
195
- .task-group.open .task-list{display:block}
196
- .task-item{display:flex;align-items:flex-start;gap:8px;padding:6px 0;font-size:13px}
197
- .task-item input[type=checkbox]{margin-top:3px;accent-color:#4A7C59}
198
- .task-item.done{color:#7a8a9a;text-decoration:line-through}
199
-
200
- /* Requirements accordion */
201
- .req-card{background:#fff;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.06);margin-bottom:12px;overflow:hidden}
202
- .req-header{padding:16px 20px;cursor:pointer;display:flex;align-items:center;gap:12px;border-left:4px solid #4A7C59}
203
- .req-header:hover{background:#f9f8f5}
204
- .req-header h3{font-size:15px;color:#1a2332;flex:1}
205
- .req-header .arrow{color:#7a8a9a;font-size:12px;transition:transform 0.2s}
206
- .req-card.open .req-header .arrow{transform:rotate(90deg)}
207
- .req-body{display:none;padding:0 20px 16px 20px;border-top:1px solid #f0ede6}
208
- .req-card.open .req-body{display:block}
209
- .req-desc{font-size:13px;color:#4a5568;margin:12px 0}
210
- .scenario{margin:12px 0;padding:12px;background:#f9f8f5;border-radius:6px}
211
- .scenario h4{font-size:13px;color:#1a2332;margin-bottom:8px}
212
- .step{display:flex;gap:8px;padding:3px 0;font-size:13px}
213
- .step-kw{font-weight:600;min-width:50px;flex-shrink:0}
214
- .step-kw.WHEN{color:#FF8F00}
215
- .step-kw.THEN{color:#4A7C59}
216
- .step-kw.AND{color:#7a8a9a}
217
- .step-kw.GIVEN{color:#1565C0}
218
-
219
- /* Operation badges (diff mode) */
220
- .op-badge{padding:2px 8px;border-radius:3px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px}
221
- .op-badge.ADDED{background:#E8F0EB;color:#2E7D32}
222
- .op-badge.MODIFIED{background:#FFF3E0;color:#E65100}
223
- .op-badge.REMOVED{background:#FFEBEE;color:#C62828}
224
- .op-badge.RENAMED{background:#E3F2FD;color:#1565C0}
225
- .req-header.ADDED{border-left-color:#4A7C59;background:rgba(74,124,89,0.04)}
226
- .req-header.MODIFIED{border-left-color:#FF8F00;background:rgba(255,143,0,0.04)}
227
- .req-header.REMOVED{border-left-color:#C62828;background:rgba(198,40,40,0.04)}
228
-
229
- /* Risk cards */
230
- .risk-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
231
- .risk-card{padding:16px;border-radius:8px;border-left:3px solid #FF8F00}
232
- .risk-card.question{border-left-color:#4A7C59}
233
- .risk-card h4{font-size:14px;color:#1a2332;margin-bottom:4px}
234
- .risk-card p{font-size:13px;color:#4a5568}
235
-
236
- /* Expand/Collapse All */
237
- .toggle-all{display:flex;gap:8px;margin-bottom:16px}
238
- .toggle-btn{padding:6px 14px;border:1px solid #e0ddd5;border-radius:6px;background:#fff;font-size:12px;color:#4a5568;cursor:pointer;font-family:'Inter',sans-serif}
239
- .toggle-btn:hover{background:#f0ede6}
240
-
241
- /* Changes Overview (project mode) */
242
- .changes-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px}
243
- .change-card{background:#fff;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.06);padding:20px;border-left:4px solid #7B1FA2}
244
- .change-card h3{font-size:15px;color:#1a2332;margin-bottom:8px}
245
- .change-card .change-id{font-family:'JetBrains Mono',monospace;font-size:11px;color:#7a8a9a;margin-bottom:8px}
246
- .change-stats{display:flex;gap:16px;flex-wrap:wrap}
247
- .change-stat{font-size:12px;color:#4a5568}
248
- .change-stat strong{color:#4A7C59}
249
-
250
- /* Footer */
251
- .footer{text-align:center;color:#7a8a9a;font-size:12px;margin-top:40px;padding-top:20px;border-top:1px solid #e0ddd5}
252
-
253
- /* Print */
254
- @media print{
255
- body{background:#fff}
256
- .hero{background:#1a2332;-webkit-print-color-adjust:exact;print-color-adjust:exact}
257
- .search-bar{display:none}
258
- .req-body,.task-list{display:block!important}
259
- }
260
-
261
- /* Responsive */
262
- @media(max-width:768px){
263
- .decision-grid{grid-template-columns:1fr}
264
- .risk-grid{grid-template-columns:1fr}
265
- .hero .meta{gap:16px}
266
- .stats{flex-wrap:wrap}
267
- }
268
- </style>
269
- </head>
270
- <body>
271
-
272
- <!-- Hero -->
273
- <div class="hero">
274
- <div class="container">
275
- <span class="mode-badge ${p.mode}">${p.mode.toUpperCase()}</span>
276
- <h1>${esc(title)}</h1>
277
- <p class="subtitle">${esc(capNames)}</p>
278
- <div class="meta">
279
- <div class="meta-item"><span class="label">Change ID</span><span class="value">${esc(p.changeId)}</span></div>
280
- <div class="meta-item"><span class="label">Mode</span><span class="value">${p.mode}</span></div>
281
- <div class="meta-item"><span class="label">Generated</span><span class="value">${date}</span></div>
282
- </div>
283
- <div class="stats">
284
- ${p.mode === "project" && p.changes ? `<div class="stat"><div class="num">${p.changes.length}</div><div class="lbl">Changes</div></div>` : ""}
285
- <div class="stat"><div class="num">${p.specs.length}</div><div class="lbl">Specs</div></div>
286
- <div class="stat"><div class="num">${totalReqs}</div><div class="lbl">Requirements</div></div>
287
- <div class="stat"><div class="num">${totalScenarios}</div><div class="lbl">Scenarios</div></div>
288
- ${totalTasks > 0 ? `<div class="stat"><div class="num">${doneTasks}/${totalTasks}</div><div class="lbl">Tasks Done</div></div>` : ""}
289
- ${p.gaps.length > 0 ? `<div class="stat"><div class="num" style="color:#E65100">${p.gaps.length}</div><div class="lbl">Gaps</div></div>` : ""}
290
- </div>
291
- </div>
292
- </div>
293
-
294
- <div class="container">
295
-
296
- <!-- Search -->
297
- <div class="search-bar">
298
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
299
- <input type="text" id="search" placeholder="Search requirements, scenarios, decisions..." oninput="filterContent(this.value)">
300
- </div>
301
-
302
- ${p.gaps.length > 0 ? buildGapsSection(p) : ""}
303
- ${p.mode === "project" && p.changes?.length ? buildChangesOverviewSection(p) : ""}
304
- ${p.proposal ? buildProposalSection(p) : ""}
305
- ${p.design?.context ? buildContextSection(p) : ""}
306
- ${(p.design?.decisions.length || 0) > 0 ? buildDecisionsSection(p) : ""}
307
- ${p.design?.dataModel || (p.dataModels?.length || 0) > 0 ? buildDataModelSection(p) : ""}
308
- ${(p.design?.apiRoutes?.length || 0) > 0 ? buildApiRoutesSection(p) : ""}
309
- ${(p.design?.frontendPages?.length || 0) > 0 ? buildFrontendSection(p) : ""}
310
- ${(p.tasks?.length || 0) > 0 ? buildTasksSection(p) : ""}
311
-
312
- <div class="toggle-all">
313
- <button class="toggle-btn" onclick="toggleAll(true)">Expand All</button>
314
- <button class="toggle-btn" onclick="toggleAll(false)">Collapse All</button>
315
- </div>
316
-
317
- ${p.specs.length > 0 ? buildRequirementsSection(p) : ""}
318
- ${(p.design?.risks?.length || 0) > 0 || (p.design?.openQuestions?.length || 0) > 0 ? buildRisksSection(p) : ""}
319
-
320
- <div class="footer">
321
- Generated by OpenSpec Visualizer · AgentHub · ${date}
322
- </div>
323
-
324
- </div>
325
-
326
- <script>
327
- function filterContent(q) {
328
- q = q.toLowerCase();
329
- document.querySelectorAll('.req-card').forEach(el => {
330
- const text = el.textContent.toLowerCase();
331
- el.style.display = text.includes(q) ? '' : 'none';
332
- });
333
- document.querySelectorAll('.dec-card').forEach(el => {
334
- const text = el.textContent.toLowerCase();
335
- el.style.display = text.includes(q) ? '' : 'none';
336
- });
337
- document.querySelectorAll('.task-group').forEach(el => {
338
- const text = el.textContent.toLowerCase();
339
- el.style.display = text.includes(q) ? '' : 'none';
340
- });
341
- document.querySelectorAll('.change-card').forEach(el => {
342
- const text = el.textContent.toLowerCase();
343
- el.style.display = text.includes(q) ? '' : 'none';
344
- });
345
- }
346
-
347
- function toggleAll(open) {
348
- document.querySelectorAll('.req-card').forEach(el => {
349
- open ? el.classList.add('open') : el.classList.remove('open');
350
- });
351
- document.querySelectorAll('.task-group').forEach(el => {
352
- open ? el.classList.add('open') : el.classList.remove('open');
353
- });
354
- }
355
-
356
- document.querySelectorAll('.req-header').forEach(h => {
357
- h.addEventListener('click', () => h.parentElement.classList.toggle('open'));
358
- });
359
-
360
- document.querySelectorAll('.task-group h3').forEach(h => {
361
- h.addEventListener('click', () => h.parentElement.classList.toggle('open'));
362
- });
363
- </script>
364
- </body>
92
+ return `<!DOCTYPE html>
93
+ <html lang="en">
94
+ <head>
95
+ <meta charset="UTF-8">
96
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
97
+ <title>${esc(title)} — OpenSpec Viewer</title>
98
+ <style>
99
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
100
+ *{margin:0;padding:0;box-sizing:border-box}
101
+ body{font-family:'Inter',sans-serif;background:#FAF8F3;color:#1a2332;line-height:1.6}
102
+ a{color:#4A7C59;text-decoration:none}
103
+ .container{max-width:1100px;margin:0 auto;padding:40px 24px}
104
+
105
+ /* Hero */
106
+ .hero{background:#1a2332;color:#fff;padding:48px 0;border-bottom:4px solid #FF8F00}
107
+ .hero .container{display:flex;flex-direction:column;gap:16px}
108
+ .hero h1{font-size:32px;font-weight:700}
109
+ .hero .subtitle{color:#a0b0c0;font-size:16px}
110
+ .hero .meta{display:flex;gap:32px;flex-wrap:wrap;margin-top:8px}
111
+ .hero .meta-item{display:flex;flex-direction:column;gap:2px}
112
+ .hero .meta-item .label{color:#7a8a9a;font-size:11px;text-transform:uppercase;letter-spacing:1px}
113
+ .hero .meta-item .value{color:#FF8F00;font-size:14px;font-weight:600}
114
+ .stats{display:flex;gap:24px;margin-top:16px}
115
+ .stat{background:rgba(255,255,255,0.08);border-radius:8px;padding:12px 20px;text-align:center}
116
+ .stat .num{font-size:24px;font-weight:700;color:#4A7C59}
117
+ .stat .lbl{font-size:11px;color:#a0b0c0;text-transform:uppercase}
118
+
119
+ /* Mode badge */
120
+ .mode-badge{display:inline-block;padding:4px 12px;border-radius:4px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px}
121
+ .mode-badge.change{background:#4A7C59;color:#fff}
122
+ .mode-badge.spec{background:#1565C0;color:#fff}
123
+ .mode-badge.diff{background:#FF8F00;color:#fff}
124
+ .mode-badge.project{background:#7B1FA2;color:#fff}
125
+
126
+ /* Search */
127
+ .search-bar{margin:24px 0;position:relative}
128
+ .search-bar input{width:100%;padding:12px 16px 12px 44px;border:2px solid #e0ddd5;border-radius:8px;font-size:14px;background:#fff;outline:none;font-family:'Inter',sans-serif}
129
+ .search-bar input:focus{border-color:#4A7C59}
130
+ .search-bar svg{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#7a8a9a}
131
+
132
+ /* Gaps banner */
133
+ .gaps-banner{background:#FFF3E0;border:1px solid #FFB74D;border-radius:8px;padding:16px 20px;margin-bottom:32px}
134
+ .gaps-banner h3{color:#E65100;font-size:14px;margin-bottom:8px}
135
+ .gaps-banner ul{list-style:none;padding:0}
136
+ .gaps-banner li{color:#BF360C;font-size:13px;padding:4px 0;padding-left:20px;position:relative}
137
+ .gaps-banner li::before{content:'⚠';position:absolute;left:0}
138
+
139
+ /* Section */
140
+ section{margin-bottom:40px}
141
+ section h2{font-size:22px;font-weight:700;color:#1a2332;border-left:4px solid #4A7C59;padding-left:12px;margin-bottom:16px}
142
+ section h2.orange-accent{border-left-color:#FF8F00}
143
+
144
+ /* Cards */
145
+ .card{background:#fff;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.06);padding:20px;margin-bottom:16px}
146
+ .card h3{font-size:16px;color:#4A7C59;margin-bottom:8px}
147
+ .card p{font-size:14px;color:#4a5568}
148
+
149
+ /* Decision cards */
150
+ .decision-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(480px,1fr));gap:16px}
151
+ .dec-card{background:#fff;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.06);padding:20px}
152
+ .dec-card h3{font-size:15px;color:#4A7C59;margin-bottom:8px;border-bottom:2px solid #E8F0EB;padding-bottom:8px}
153
+ .dec-row{display:flex;gap:8px;margin-bottom:6px;align-items:flex-start}
154
+ .dec-label{padding:2px 8px;border-radius:3px;font-size:10px;font-weight:700;white-space:nowrap;flex-shrink:0}
155
+ .dec-label.primary{background:#1a2332;color:#FF8F00}
156
+ .dec-label.alt{background:#f5f0e8;color:#7a8a9a}
157
+ .dec-text{font-size:13px;color:#4a5568;line-height:1.5}
158
+
159
+ /* Data Model */
160
+ .model-card{background:#fff;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.06);padding:20px;overflow-x:auto}
161
+ .model-card h3{font-size:16px;font-family:'JetBrains Mono',monospace;color:#4A7C59;margin-bottom:12px}
162
+ table{width:100%;border-collapse:collapse;font-size:13px}
163
+ th{background:#E8F0EB;color:#4A7C59;padding:8px 12px;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:1px}
164
+ td{padding:8px 12px;border-bottom:1px solid #f0ede6}
165
+ .field-name{font-family:'JetBrains Mono',monospace;font-weight:500;color:#1a2332}
166
+ .field-type{font-family:'JetBrains Mono',monospace;color:#FF8F00;font-size:12px}
167
+ .req-badge{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600}
168
+ .req-badge.yes{background:#E8F0EB;color:#4A7C59}
169
+ .req-badge.no{background:#f5f0e8;color:#7a8a9a}
170
+ .idx-list{margin-top:12px;display:flex;gap:8px;flex-wrap:wrap}
171
+ .idx-tag{background:#1a2332;color:#FF8F00;font-family:'JetBrains Mono',monospace;font-size:11px;padding:4px 8px;border-radius:4px}
172
+
173
+ /* API Routes */
174
+ .route-table{overflow-x:auto}
175
+ .method-badge{display:inline-block;padding:2px 8px;border-radius:3px;font-size:11px;font-weight:700;font-family:'JetBrains Mono',monospace;min-width:54px;text-align:center}
176
+ .method-badge.GET{background:#E8F0EB;color:#4A7C59}
177
+ .method-badge.POST{background:#E3F2FD;color:#1565C0}
178
+ .method-badge.PATCH{background:#FFF3E0;color:#E65100}
179
+ .method-badge.PUT{background:#FFF3E0;color:#E65100}
180
+ .method-badge.DELETE{background:#FFEBEE;color:#C62828}
181
+ .auth-badge{font-size:11px;padding:2px 8px;border-radius:3px}
182
+ .auth-badge.public{background:#FFF3E0;color:#FF8F00}
183
+ .auth-badge.auth{background:#E8F0EB;color:#4A7C59}
184
+ .route-path{font-family:'JetBrains Mono',monospace;font-size:13px;color:#1a2332}
185
+
186
+ /* Tasks */
187
+ .task-group{margin-bottom:24px}
188
+ .task-group h3{font-size:15px;color:#1a2332;margin-bottom:8px;cursor:pointer;display:flex;align-items:center;gap:8px}
189
+ .task-group h3::before{content:'▸';color:#4A7C59;font-size:12px;transition:transform 0.2s}
190
+ .task-group.open h3::before{transform:rotate(90deg)}
191
+ .progress-bar{height:6px;background:#f0ede6;border-radius:3px;overflow:hidden;margin-bottom:8px}
192
+ .progress-fill{height:100%;background:#4A7C59;border-radius:3px;transition:width 0.3s}
193
+ .progress-text{font-size:11px;color:#7a8a9a;margin-bottom:8px}
194
+ .task-list{list-style:none;padding:0;display:none}
195
+ .task-group.open .task-list{display:block}
196
+ .task-item{display:flex;align-items:flex-start;gap:8px;padding:6px 0;font-size:13px}
197
+ .task-item input[type=checkbox]{margin-top:3px;accent-color:#4A7C59}
198
+ .task-item.done{color:#7a8a9a;text-decoration:line-through}
199
+
200
+ /* Requirements accordion */
201
+ .req-card{background:#fff;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.06);margin-bottom:12px;overflow:hidden}
202
+ .req-header{padding:16px 20px;cursor:pointer;display:flex;align-items:center;gap:12px;border-left:4px solid #4A7C59}
203
+ .req-header:hover{background:#f9f8f5}
204
+ .req-header h3{font-size:15px;color:#1a2332;flex:1}
205
+ .req-header .arrow{color:#7a8a9a;font-size:12px;transition:transform 0.2s}
206
+ .req-card.open .req-header .arrow{transform:rotate(90deg)}
207
+ .req-body{display:none;padding:0 20px 16px 20px;border-top:1px solid #f0ede6}
208
+ .req-card.open .req-body{display:block}
209
+ .req-desc{font-size:13px;color:#4a5568;margin:12px 0}
210
+ .scenario{margin:12px 0;padding:12px;background:#f9f8f5;border-radius:6px}
211
+ .scenario h4{font-size:13px;color:#1a2332;margin-bottom:8px}
212
+ .step{display:flex;gap:8px;padding:3px 0;font-size:13px}
213
+ .step-kw{font-weight:600;min-width:50px;flex-shrink:0}
214
+ .step-kw.WHEN{color:#FF8F00}
215
+ .step-kw.THEN{color:#4A7C59}
216
+ .step-kw.AND{color:#7a8a9a}
217
+ .step-kw.GIVEN{color:#1565C0}
218
+
219
+ /* Operation badges (diff mode) */
220
+ .op-badge{padding:2px 8px;border-radius:3px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px}
221
+ .op-badge.ADDED{background:#E8F0EB;color:#2E7D32}
222
+ .op-badge.MODIFIED{background:#FFF3E0;color:#E65100}
223
+ .op-badge.REMOVED{background:#FFEBEE;color:#C62828}
224
+ .op-badge.RENAMED{background:#E3F2FD;color:#1565C0}
225
+ .req-header.ADDED{border-left-color:#4A7C59;background:rgba(74,124,89,0.04)}
226
+ .req-header.MODIFIED{border-left-color:#FF8F00;background:rgba(255,143,0,0.04)}
227
+ .req-header.REMOVED{border-left-color:#C62828;background:rgba(198,40,40,0.04)}
228
+
229
+ /* Risk cards */
230
+ .risk-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
231
+ .risk-card{padding:16px;border-radius:8px;border-left:3px solid #FF8F00}
232
+ .risk-card.question{border-left-color:#4A7C59}
233
+ .risk-card h4{font-size:14px;color:#1a2332;margin-bottom:4px}
234
+ .risk-card p{font-size:13px;color:#4a5568}
235
+
236
+ /* Expand/Collapse All */
237
+ .toggle-all{display:flex;gap:8px;margin-bottom:16px}
238
+ .toggle-btn{padding:6px 14px;border:1px solid #e0ddd5;border-radius:6px;background:#fff;font-size:12px;color:#4a5568;cursor:pointer;font-family:'Inter',sans-serif}
239
+ .toggle-btn:hover{background:#f0ede6}
240
+
241
+ /* Changes Overview (project mode) */
242
+ .changes-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px}
243
+ .change-card{background:#fff;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.06);padding:20px;border-left:4px solid #7B1FA2}
244
+ .change-card h3{font-size:15px;color:#1a2332;margin-bottom:8px}
245
+ .change-card .change-id{font-family:'JetBrains Mono',monospace;font-size:11px;color:#7a8a9a;margin-bottom:8px}
246
+ .change-stats{display:flex;gap:16px;flex-wrap:wrap}
247
+ .change-stat{font-size:12px;color:#4a5568}
248
+ .change-stat strong{color:#4A7C59}
249
+
250
+ /* Footer */
251
+ .footer{text-align:center;color:#7a8a9a;font-size:12px;margin-top:40px;padding-top:20px;border-top:1px solid #e0ddd5}
252
+
253
+ /* Print */
254
+ @media print{
255
+ body{background:#fff}
256
+ .hero{background:#1a2332;-webkit-print-color-adjust:exact;print-color-adjust:exact}
257
+ .search-bar{display:none}
258
+ .req-body,.task-list{display:block!important}
259
+ }
260
+
261
+ /* Responsive */
262
+ @media(max-width:768px){
263
+ .decision-grid{grid-template-columns:1fr}
264
+ .risk-grid{grid-template-columns:1fr}
265
+ .hero .meta{gap:16px}
266
+ .stats{flex-wrap:wrap}
267
+ }
268
+ </style>
269
+ </head>
270
+ <body>
271
+
272
+ <!-- Hero -->
273
+ <div class="hero">
274
+ <div class="container">
275
+ <span class="mode-badge ${p.mode}">${p.mode.toUpperCase()}</span>
276
+ <h1>${esc(title)}</h1>
277
+ <p class="subtitle">${esc(capNames)}</p>
278
+ <div class="meta">
279
+ <div class="meta-item"><span class="label">Change ID</span><span class="value">${esc(p.changeId)}</span></div>
280
+ <div class="meta-item"><span class="label">Mode</span><span class="value">${p.mode}</span></div>
281
+ <div class="meta-item"><span class="label">Generated</span><span class="value">${date}</span></div>
282
+ </div>
283
+ <div class="stats">
284
+ ${p.mode === "project" && p.changes ? `<div class="stat"><div class="num">${p.changes.length}</div><div class="lbl">Changes</div></div>` : ""}
285
+ <div class="stat"><div class="num">${p.specs.length}</div><div class="lbl">Specs</div></div>
286
+ <div class="stat"><div class="num">${totalReqs}</div><div class="lbl">Requirements</div></div>
287
+ <div class="stat"><div class="num">${totalScenarios}</div><div class="lbl">Scenarios</div></div>
288
+ ${totalTasks > 0 ? `<div class="stat"><div class="num">${doneTasks}/${totalTasks}</div><div class="lbl">Tasks Done</div></div>` : ""}
289
+ ${p.gaps.length > 0 ? `<div class="stat"><div class="num" style="color:#E65100">${p.gaps.length}</div><div class="lbl">Gaps</div></div>` : ""}
290
+ </div>
291
+ </div>
292
+ </div>
293
+
294
+ <div class="container">
295
+
296
+ <!-- Search -->
297
+ <div class="search-bar">
298
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
299
+ <input type="text" id="search" placeholder="Search requirements, scenarios, decisions..." oninput="filterContent(this.value)">
300
+ </div>
301
+
302
+ ${p.gaps.length > 0 ? buildGapsSection(p) : ""}
303
+ ${p.mode === "project" && p.changes?.length ? buildChangesOverviewSection(p) : ""}
304
+ ${p.proposal ? buildProposalSection(p) : ""}
305
+ ${p.design?.context ? buildContextSection(p) : ""}
306
+ ${(p.design?.decisions.length || 0) > 0 ? buildDecisionsSection(p) : ""}
307
+ ${p.design?.dataModel || (p.dataModels?.length || 0) > 0 ? buildDataModelSection(p) : ""}
308
+ ${(p.design?.apiRoutes?.length || 0) > 0 ? buildApiRoutesSection(p) : ""}
309
+ ${(p.design?.frontendPages?.length || 0) > 0 ? buildFrontendSection(p) : ""}
310
+ ${(p.tasks?.length || 0) > 0 ? buildTasksSection(p) : ""}
311
+
312
+ <div class="toggle-all">
313
+ <button class="toggle-btn" onclick="toggleAll(true)">Expand All</button>
314
+ <button class="toggle-btn" onclick="toggleAll(false)">Collapse All</button>
315
+ </div>
316
+
317
+ ${p.specs.length > 0 ? buildRequirementsSection(p) : ""}
318
+ ${(p.design?.risks?.length || 0) > 0 || (p.design?.openQuestions?.length || 0) > 0 ? buildRisksSection(p) : ""}
319
+
320
+ <div class="footer">
321
+ Generated by OpenSpec Visualizer · AgentHub · ${date}
322
+ </div>
323
+
324
+ </div>
325
+
326
+ <script>
327
+ function filterContent(q) {
328
+ q = q.toLowerCase();
329
+ document.querySelectorAll('.req-card').forEach(el => {
330
+ const text = el.textContent.toLowerCase();
331
+ el.style.display = text.includes(q) ? '' : 'none';
332
+ });
333
+ document.querySelectorAll('.dec-card').forEach(el => {
334
+ const text = el.textContent.toLowerCase();
335
+ el.style.display = text.includes(q) ? '' : 'none';
336
+ });
337
+ document.querySelectorAll('.task-group').forEach(el => {
338
+ const text = el.textContent.toLowerCase();
339
+ el.style.display = text.includes(q) ? '' : 'none';
340
+ });
341
+ document.querySelectorAll('.change-card').forEach(el => {
342
+ const text = el.textContent.toLowerCase();
343
+ el.style.display = text.includes(q) ? '' : 'none';
344
+ });
345
+ }
346
+
347
+ function toggleAll(open) {
348
+ document.querySelectorAll('.req-card').forEach(el => {
349
+ open ? el.classList.add('open') : el.classList.remove('open');
350
+ });
351
+ document.querySelectorAll('.task-group').forEach(el => {
352
+ open ? el.classList.add('open') : el.classList.remove('open');
353
+ });
354
+ }
355
+
356
+ document.querySelectorAll('.req-header').forEach(h => {
357
+ h.addEventListener('click', () => h.parentElement.classList.toggle('open'));
358
+ });
359
+
360
+ document.querySelectorAll('.task-group h3').forEach(h => {
361
+ h.addEventListener('click', () => h.parentElement.classList.toggle('open'));
362
+ });
363
+ </script>
364
+ </body>
365
365
  </html>`;
366
366
  }
367
367
  // ---------------------------------------------------------------------------
368
368
  // Section builders
369
369
  // ---------------------------------------------------------------------------
370
370
  function buildGapsSection(p) {
371
- return `
372
- <div class="gaps-banner">
373
- <h3>Gaps Detected</h3>
374
- <ul>${p.gaps.map((g) => `<li>${esc(g)}</li>`).join("")}</ul>
371
+ return `
372
+ <div class="gaps-banner">
373
+ <h3>Gaps Detected</h3>
374
+ <ul>${p.gaps.map((g) => `<li>${esc(g)}</li>`).join("")}</ul>
375
375
  </div>`;
376
376
  }
377
377
  function buildProposalSection(p) {
378
378
  const pr = p.proposal;
379
- return `
380
- <section data-section="proposal">
381
- <h2>Proposal</h2>
382
- <div class="card">
383
- <h3>Why</h3>
384
- <p>${renderMd(pr.why)}</p>
385
- </div>
386
- ${pr.whatChanges.length > 0 ? `
387
- <div class="card">
388
- <h3>What Changes</h3>
389
- <ul style="padding-left:20px;font-size:14px;color:#4a5568">
390
- ${pr.whatChanges.map((c) => `<li${c.breaking ? ' style="color:#C62828;font-weight:600"' : ""}>${renderMd(c.text)}</li>`).join("")}
391
- </ul>
392
- </div>` : ""}
393
- ${pr.impact.length > 0 ? `
394
- <div class="card">
395
- <h3>Impact</h3>
396
- <ul style="padding-left:20px;font-size:14px;color:#4a5568">
397
- ${pr.impact.map((i) => `<li>${renderMd(i)}</li>`).join("")}
398
- </ul>
399
- </div>` : ""}
379
+ return `
380
+ <section data-section="proposal">
381
+ <h2>Proposal</h2>
382
+ <div class="card">
383
+ <h3>Why</h3>
384
+ <p>${renderMd(pr.why)}</p>
385
+ </div>
386
+ ${pr.whatChanges.length > 0 ? `
387
+ <div class="card">
388
+ <h3>What Changes</h3>
389
+ <ul style="padding-left:20px;font-size:14px;color:#4a5568">
390
+ ${pr.whatChanges.map((c) => `<li${c.breaking ? ' style="color:#C62828;font-weight:600"' : ""}>${renderMd(c.text)}</li>`).join("")}
391
+ </ul>
392
+ </div>` : ""}
393
+ ${pr.impact.length > 0 ? `
394
+ <div class="card">
395
+ <h3>Impact</h3>
396
+ <ul style="padding-left:20px;font-size:14px;color:#4a5568">
397
+ ${pr.impact.map((i) => `<li>${renderMd(i)}</li>`).join("")}
398
+ </ul>
399
+ </div>` : ""}
400
400
  </section>`;
401
401
  }
402
402
  function buildContextSection(p) {
403
- return `
404
- <section data-section="context">
405
- <h2>Context</h2>
406
- <div class="card">
407
- <p>${renderMd(p.design.context).replace(/\n/g, "<br>")}</p>
408
- </div>
409
- ${p.design.goals ? `
410
- <div class="card">
411
- <h3>Goals</h3>
412
- <ul style="padding-left:20px;font-size:14px;color:#4a5568">
413
- ${p.design.goals.map((g) => `<li>${renderMd(g)}</li>`).join("")}
414
- </ul>
415
- </div>` : ""}
416
- ${p.design.nonGoals ? `
417
- <div class="card">
418
- <h3>Non-Goals</h3>
419
- <ul style="padding-left:20px;font-size:14px;color:#7a8a9a">
420
- ${p.design.nonGoals.map((g) => `<li>${renderMd(g)}</li>`).join("")}
421
- </ul>
422
- </div>` : ""}
403
+ return `
404
+ <section data-section="context">
405
+ <h2>Context</h2>
406
+ <div class="card">
407
+ <p>${renderMd(p.design.context).replace(/\n/g, "<br>")}</p>
408
+ </div>
409
+ ${p.design.goals ? `
410
+ <div class="card">
411
+ <h3>Goals</h3>
412
+ <ul style="padding-left:20px;font-size:14px;color:#4a5568">
413
+ ${p.design.goals.map((g) => `<li>${renderMd(g)}</li>`).join("")}
414
+ </ul>
415
+ </div>` : ""}
416
+ ${p.design.nonGoals ? `
417
+ <div class="card">
418
+ <h3>Non-Goals</h3>
419
+ <ul style="padding-left:20px;font-size:14px;color:#7a8a9a">
420
+ ${p.design.nonGoals.map((g) => `<li>${renderMd(g)}</li>`).join("")}
421
+ </ul>
422
+ </div>` : ""}
423
423
  </section>`;
424
424
  }
425
425
  function buildDecisionsSection(p) {
426
- return `
427
- <section data-section="decisions">
428
- <h2>Key Design Decisions</h2>
429
- <div class="decision-grid">
430
- ${p.design.decisions.map((d) => `
431
- <div class="dec-card searchable">
432
- <h3>${esc(d.title)}</h3>
433
- <div class="dec-row">
434
- <span class="dec-label primary">DECISION</span>
435
- <span class="dec-text">${esc(d.decision)}</span>
436
- </div>
437
- ${d.alternative ? `
438
- <div class="dec-row">
439
- <span class="dec-label alt">REJECTED</span>
440
- <span class="dec-text">${esc(d.alternative)}</span>
441
- </div>` : ""}
442
- ${d.tradeoff ? `
443
- <div class="dec-row">
444
- <span class="dec-label alt">TRADE-OFF</span>
445
- <span class="dec-text">${esc(d.tradeoff)}</span>
446
- </div>` : ""}
447
- </div>`).join("")}
448
- </div>
426
+ return `
427
+ <section data-section="decisions">
428
+ <h2>Key Design Decisions</h2>
429
+ <div class="decision-grid">
430
+ ${p.design.decisions.map((d) => `
431
+ <div class="dec-card searchable">
432
+ <h3>${esc(d.title)}</h3>
433
+ <div class="dec-row">
434
+ <span class="dec-label primary">DECISION</span>
435
+ <span class="dec-text">${esc(d.decision)}</span>
436
+ </div>
437
+ ${d.alternative ? `
438
+ <div class="dec-row">
439
+ <span class="dec-label alt">REJECTED</span>
440
+ <span class="dec-text">${esc(d.alternative)}</span>
441
+ </div>` : ""}
442
+ ${d.tradeoff ? `
443
+ <div class="dec-row">
444
+ <span class="dec-label alt">TRADE-OFF</span>
445
+ <span class="dec-text">${esc(d.tradeoff)}</span>
446
+ </div>` : ""}
447
+ </div>`).join("")}
448
+ </div>
449
449
  </section>`;
450
450
  }
451
451
  function buildDataModelSection(p) {
@@ -462,178 +462,178 @@ function buildDataModelSection(p) {
462
462
  }
463
463
  if (models.length === 0)
464
464
  return "";
465
- return `
466
- <section data-section="data-model">
467
- <h2>Data Model${models.length > 1 ? "s" : ""}</h2>
468
- ${models.map((dm) => `
469
- <div class="model-card" style="margin-bottom:16px">
470
- <h3>${esc(dm.name)}</h3>
471
- <table>
472
- <thead><tr><th>Field</th><th>Type</th><th>Required</th><th>Description</th></tr></thead>
473
- <tbody>
474
- ${dm.fields.map((f) => `
475
- <tr>
476
- <td class="field-name">${esc(f.name)}</td>
477
- <td class="field-type">${esc(f.type)}</td>
478
- <td><span class="req-badge ${f.required ? "yes" : "no"}">${f.required ? "Yes" : "No"}</span></td>
479
- <td style="font-size:13px;color:#4a5568">${esc(f.description)}</td>
480
- </tr>`).join("")}
481
- </tbody>
482
- </table>
483
- ${dm.indexes && dm.indexes.length > 0 ? `
484
- <div class="idx-list">
485
- <span style="font-size:12px;color:#7a8a9a;margin-right:4px">Indexes:</span>
486
- ${dm.indexes.map((idx) => `<span class="idx-tag">${esc(idx)}</span>`).join("")}
487
- </div>` : ""}
488
- </div>`).join("")}
465
+ return `
466
+ <section data-section="data-model">
467
+ <h2>Data Model${models.length > 1 ? "s" : ""}</h2>
468
+ ${models.map((dm) => `
469
+ <div class="model-card" style="margin-bottom:16px">
470
+ <h3>${esc(dm.name)}</h3>
471
+ <table>
472
+ <thead><tr><th>Field</th><th>Type</th><th>Required</th><th>Description</th></tr></thead>
473
+ <tbody>
474
+ ${dm.fields.map((f) => `
475
+ <tr>
476
+ <td class="field-name">${esc(f.name)}</td>
477
+ <td class="field-type">${esc(f.type)}</td>
478
+ <td><span class="req-badge ${f.required ? "yes" : "no"}">${f.required ? "Yes" : "No"}</span></td>
479
+ <td style="font-size:13px;color:#4a5568">${esc(f.description)}</td>
480
+ </tr>`).join("")}
481
+ </tbody>
482
+ </table>
483
+ ${dm.indexes && dm.indexes.length > 0 ? `
484
+ <div class="idx-list">
485
+ <span style="font-size:12px;color:#7a8a9a;margin-right:4px">Indexes:</span>
486
+ ${dm.indexes.map((idx) => `<span class="idx-tag">${esc(idx)}</span>`).join("")}
487
+ </div>` : ""}
488
+ </div>`).join("")}
489
489
  </section>`;
490
490
  }
491
491
  function buildApiRoutesSection(p) {
492
- return `
493
- <section data-section="api-routes">
494
- <h2 class="orange-accent">API Routes</h2>
495
- <div class="model-card route-table">
496
- <table>
497
- <thead><tr><th>Method</th><th>Path</th><th>Auth</th><th>Description</th></tr></thead>
498
- <tbody>
492
+ return `
493
+ <section data-section="api-routes">
494
+ <h2 class="orange-accent">API Routes</h2>
495
+ <div class="model-card route-table">
496
+ <table>
497
+ <thead><tr><th>Method</th><th>Path</th><th>Auth</th><th>Description</th></tr></thead>
498
+ <tbody>
499
499
  ${p.design.apiRoutes.map((r) => {
500
500
  const isPublic = /public/i.test(r.auth);
501
- return `
502
- <tr>
503
- <td><span class="method-badge ${r.method.toUpperCase()}">${r.method.toUpperCase()}</span></td>
504
- <td class="route-path">${esc(r.path)}</td>
505
- <td><span class="auth-badge ${isPublic ? "public" : "auth"}">${esc(r.auth)}</span></td>
506
- <td style="font-size:13px;color:#4a5568">${esc(r.description)}</td>
501
+ return `
502
+ <tr>
503
+ <td><span class="method-badge ${r.method.toUpperCase()}">${r.method.toUpperCase()}</span></td>
504
+ <td class="route-path">${esc(r.path)}</td>
505
+ <td><span class="auth-badge ${isPublic ? "public" : "auth"}">${esc(r.auth)}</span></td>
506
+ <td style="font-size:13px;color:#4a5568">${esc(r.description)}</td>
507
507
  </tr>`;
508
- }).join("")}
509
- </tbody>
510
- </table>
511
- </div>
508
+ }).join("")}
509
+ </tbody>
510
+ </table>
511
+ </div>
512
512
  </section>`;
513
513
  }
514
514
  function buildFrontendSection(p) {
515
- return `
516
- <section data-section="frontend">
517
- <h2>Frontend Pages</h2>
518
- <div class="model-card route-table">
519
- <table>
520
- <thead><tr><th>Route</th><th>Auth</th><th>Description</th></tr></thead>
521
- <tbody>
522
- ${p.design.frontendPages.map((pg) => `
523
- <tr>
524
- <td class="route-path">${esc(pg.route)}</td>
525
- <td><span class="auth-badge ${pg.auth ? "auth" : "public"}">${pg.auth ? "Authenticated" : "Public"}</span></td>
526
- <td style="font-size:13px;color:#4a5568">${esc(pg.description)}</td>
527
- </tr>`).join("")}
528
- </tbody>
529
- </table>
530
- </div>
515
+ return `
516
+ <section data-section="frontend">
517
+ <h2>Frontend Pages</h2>
518
+ <div class="model-card route-table">
519
+ <table>
520
+ <thead><tr><th>Route</th><th>Auth</th><th>Description</th></tr></thead>
521
+ <tbody>
522
+ ${p.design.frontendPages.map((pg) => `
523
+ <tr>
524
+ <td class="route-path">${esc(pg.route)}</td>
525
+ <td><span class="auth-badge ${pg.auth ? "auth" : "public"}">${pg.auth ? "Authenticated" : "Public"}</span></td>
526
+ <td style="font-size:13px;color:#4a5568">${esc(pg.description)}</td>
527
+ </tr>`).join("")}
528
+ </tbody>
529
+ </table>
530
+ </div>
531
531
  </section>`;
532
532
  }
533
533
  function buildTasksSection(p) {
534
- return `
535
- <section data-section="tasks">
536
- <h2>Implementation Tasks</h2>
534
+ return `
535
+ <section data-section="tasks">
536
+ <h2>Implementation Tasks</h2>
537
537
  ${p.tasks.map((g) => {
538
538
  const done = g.tasks.filter((t) => t.done).length;
539
539
  const total = g.tasks.length;
540
540
  const pct = total > 0 ? Math.round((done / total) * 100) : 0;
541
- return `
542
- <div class="task-group card">
543
- <h3>${esc(g.title)}</h3>
544
- <div class="progress-bar"><div class="progress-fill" style="width:${pct}%"></div></div>
545
- <div class="progress-text">${done}/${total} completed (${pct}%)</div>
546
- <ul class="task-list">
547
- ${g.tasks.map((t) => `
548
- <li class="task-item${t.done ? " done" : ""}">
549
- <input type="checkbox" ${t.done ? "checked" : ""} disabled>
550
- <span>${esc(t.text)}</span>
551
- </li>`).join("")}
552
- </ul>
541
+ return `
542
+ <div class="task-group card">
543
+ <h3>${esc(g.title)}</h3>
544
+ <div class="progress-bar"><div class="progress-fill" style="width:${pct}%"></div></div>
545
+ <div class="progress-text">${done}/${total} completed (${pct}%)</div>
546
+ <ul class="task-list">
547
+ ${g.tasks.map((t) => `
548
+ <li class="task-item${t.done ? " done" : ""}">
549
+ <input type="checkbox" ${t.done ? "checked" : ""} disabled>
550
+ <span>${esc(t.text)}</span>
551
+ </li>`).join("")}
552
+ </ul>
553
553
  </div>`;
554
- }).join("")}
554
+ }).join("")}
555
555
  </section>`;
556
556
  }
557
557
  function buildRequirementsSection(p) {
558
- return `
559
- <section data-section="requirements">
560
- <h2>Requirements &amp; Scenarios</h2>
561
- ${p.specs.map((spec) => `
562
- <div style="margin-bottom:24px">
563
- <h3 style="font-size:16px;color:#4A7C59;margin-bottom:12px;font-family:'JetBrains Mono',monospace">${esc(spec.capability)}</h3>
564
- ${spec.requirements.map((req) => buildRequirementCard(req)).join("")}
565
- </div>`).join("")}
558
+ return `
559
+ <section data-section="requirements">
560
+ <h2>Requirements &amp; Scenarios</h2>
561
+ ${p.specs.map((spec) => `
562
+ <div style="margin-bottom:24px">
563
+ <h3 style="font-size:16px;color:#4A7C59;margin-bottom:12px;font-family:'JetBrains Mono',monospace">${esc(spec.capability)}</h3>
564
+ ${spec.requirements.map((req) => buildRequirementCard(req)).join("")}
565
+ </div>`).join("")}
566
566
  </section>`;
567
567
  }
568
568
  function buildRequirementCard(req) {
569
569
  const opClass = req.operation ? ` ${req.operation}` : "";
570
- return `
571
- <div class="req-card">
572
- <div class="req-header${opClass}">
573
- ${req.operation ? `<span class="op-badge ${req.operation}">${req.operation}</span>` : ""}
574
- <h3>${esc(req.name)}</h3>
575
- <span style="font-size:11px;color:#7a8a9a">${req.scenarios.length} scenario${req.scenarios.length !== 1 ? "s" : ""}</span>
576
- <span class="arrow">▸</span>
577
- </div>
578
- <div class="req-body">
579
- <div class="req-desc">${esc(req.description).replace(/\n/g, "<br>")}</div>
580
- ${req.scenarios.map((sc) => `
581
- <div class="scenario">
582
- <h4>${esc(sc.name)}</h4>
583
- ${sc.steps.map((st) => `
584
- <div class="step">
585
- <span class="step-kw ${st.keyword}">${st.keyword}</span>
586
- <span>${esc(st.text)}</span>
587
- </div>`).join("")}
588
- </div>`).join("")}
589
- ${req.scenarios.length === 0 ? '<p style="color:#E65100;font-size:13px">⚠ No scenarios defined for this requirement</p>' : ""}
590
- </div>
570
+ return `
571
+ <div class="req-card">
572
+ <div class="req-header${opClass}">
573
+ ${req.operation ? `<span class="op-badge ${req.operation}">${req.operation}</span>` : ""}
574
+ <h3>${esc(req.name)}</h3>
575
+ <span style="font-size:11px;color:#7a8a9a">${req.scenarios.length} scenario${req.scenarios.length !== 1 ? "s" : ""}</span>
576
+ <span class="arrow">▸</span>
577
+ </div>
578
+ <div class="req-body">
579
+ <div class="req-desc">${esc(req.description).replace(/\n/g, "<br>")}</div>
580
+ ${req.scenarios.map((sc) => `
581
+ <div class="scenario">
582
+ <h4>${esc(sc.name)}</h4>
583
+ ${sc.steps.map((st) => `
584
+ <div class="step">
585
+ <span class="step-kw ${st.keyword}">${st.keyword}</span>
586
+ <span>${esc(st.text)}</span>
587
+ </div>`).join("")}
588
+ </div>`).join("")}
589
+ ${req.scenarios.length === 0 ? '<p style="color:#E65100;font-size:13px">⚠ No scenarios defined for this requirement</p>' : ""}
590
+ </div>
591
591
  </div>`;
592
592
  }
593
593
  function buildChangesOverviewSection(p) {
594
594
  const changes = p.changes || [];
595
- return `
596
- <section data-section="changes">
597
- <h2>Change Proposals</h2>
598
- <div class="changes-grid">
599
- ${changes.map((c) => `
600
- <div class="change-card searchable">
601
- <div class="change-id">${esc(c.id)}</div>
602
- <h3>${esc(c.title)}</h3>
603
- <div class="change-stats">
604
- <span class="change-stat"><strong>${c.requirementCount}</strong> requirements</span>
605
- <span class="change-stat"><strong>${c.scenarioCount}</strong> scenarios</span>
606
- ${c.taskCount > 0 ? `<span class="change-stat"><strong>${c.tasksDone}/${c.taskCount}</strong> tasks</span>` : ""}
607
- </div>
608
- </div>`).join("")}
609
- </div>
595
+ return `
596
+ <section data-section="changes">
597
+ <h2>Change Proposals</h2>
598
+ <div class="changes-grid">
599
+ ${changes.map((c) => `
600
+ <div class="change-card searchable">
601
+ <div class="change-id">${esc(c.id)}</div>
602
+ <h3>${esc(c.title)}</h3>
603
+ <div class="change-stats">
604
+ <span class="change-stat"><strong>${c.requirementCount}</strong> requirements</span>
605
+ <span class="change-stat"><strong>${c.scenarioCount}</strong> scenarios</span>
606
+ ${c.taskCount > 0 ? `<span class="change-stat"><strong>${c.tasksDone}/${c.taskCount}</strong> tasks</span>` : ""}
607
+ </div>
608
+ </div>`).join("")}
609
+ </div>
610
610
  </section>`;
611
611
  }
612
612
  function buildRisksSection(p) {
613
613
  const risks = p.design?.risks || [];
614
614
  const questions = p.design?.openQuestions || [];
615
- return `
616
- <section data-section="risks">
617
- <h2 class="orange-accent">Risks &amp; Resolved Questions</h2>
618
- <div class="risk-grid">
619
- ${risks.length > 0 ? `
620
- <div>
621
- <h3 style="font-size:14px;color:#E65100;margin-bottom:12px">Risks &amp; Mitigations</h3>
622
- ${risks.map((r) => `
623
- <div class="risk-card card">
624
- <h4>${renderMd(r.title)}</h4>
625
- <p>${renderMd(r.mitigation)}</p>
626
- </div>`).join("")}
627
- </div>` : ""}
628
- ${questions.length > 0 ? `
629
- <div>
630
- <h3 style="font-size:14px;color:#4A7C59;margin-bottom:12px">Resolved Questions</h3>
631
- ${questions.map((q) => `
632
- <div class="risk-card question card">
633
- <p>${renderMd(q)}</p>
634
- </div>`).join("")}
635
- </div>` : ""}
636
- </div>
615
+ return `
616
+ <section data-section="risks">
617
+ <h2 class="orange-accent">Risks &amp; Resolved Questions</h2>
618
+ <div class="risk-grid">
619
+ ${risks.length > 0 ? `
620
+ <div>
621
+ <h3 style="font-size:14px;color:#E65100;margin-bottom:12px">Risks &amp; Mitigations</h3>
622
+ ${risks.map((r) => `
623
+ <div class="risk-card card">
624
+ <h4>${renderMd(r.title)}</h4>
625
+ <p>${renderMd(r.mitigation)}</p>
626
+ </div>`).join("")}
627
+ </div>` : ""}
628
+ ${questions.length > 0 ? `
629
+ <div>
630
+ <h3 style="font-size:14px;color:#4A7C59;margin-bottom:12px">Resolved Questions</h3>
631
+ ${questions.map((q) => `
632
+ <div class="risk-card question card">
633
+ <p>${renderMd(q)}</p>
634
+ </div>`).join("")}
635
+ </div>` : ""}
636
+ </div>
637
637
  </section>`;
638
638
  }
639
639
  // ---------------------------------------------------------------------------