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.
- package/README.md +192 -192
- package/dist/brain/backend.d.ts.map +1 -1
- package/dist/brain/backend.js +11 -2
- package/dist/brain/backend.js.map +1 -1
- package/dist/brain/memvidBrain.d.ts +24 -0
- package/dist/brain/memvidBrain.d.ts.map +1 -1
- package/dist/brain/memvidBrain.js +350 -93
- package/dist/brain/memvidBrain.js.map +1 -1
- package/dist/brain/memvidCompaction.test.d.ts +10 -0
- package/dist/brain/memvidCompaction.test.d.ts.map +1 -0
- package/dist/brain/memvidCompaction.test.js +71 -0
- package/dist/brain/memvidCompaction.test.js.map +1 -0
- package/dist/channel.js +3 -3
- package/dist/client.d.ts +43 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +53 -2
- package/dist/client.js.map +1 -1
- package/dist/commands/setup-shell.js +11 -11
- package/dist/employee.d.ts +0 -5
- package/dist/employee.d.ts.map +1 -1
- package/dist/employee.js +14 -1
- package/dist/employee.js.map +1 -1
- package/dist/hooks/periodicReminder.js +9 -9
- package/dist/setup.js +56 -56
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +167 -11
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/openspec-viz/diagram-generator.js +54 -54
- package/dist/tools/openspec-viz/html-generator.js +503 -503
- package/dist/tools/tools.test.js +4 -2
- package/dist/tools/tools.test.js.map +1 -1
- package/dist/worker.js +10 -10
- package/native/agenthub-memvid/agenthub-memvid.linux-x64-gnu.node +0 -0
- package/native/agenthub-memvid/agenthub-memvid.win32-x64-msvc.node +0 -0
- package/native/agenthub-memvid/index.d.ts +58 -0
- package/native/agenthub-memvid/index.js +319 -0
- package/package.json +77 -75
- package/skills/catalog.json +76 -76
- package/skills/commands/close-session.md +144 -144
- package/skills/commands/start-session.md +77 -77
- package/skills/manifest.json +50 -50
- package/skills/skills/code-brain-hook/SKILL.md +61 -61
- package/skills/skills/deploy-staging/SKILL.md +164 -164
- package/skills/skills/deploy-vps-openclaw/SKILL.md +97 -97
- package/skills/skills/file-bug/SKILL.md +75 -75
- package/skills/skills/karpathy-guidelines/SKILL.md +67 -67
- 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 & 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 & 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 & 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 & 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 & 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 & 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
|
// ---------------------------------------------------------------------------
|