@ipation/specbridge 1.2.0 → 1.2.1

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/CHANGELOG.md CHANGED
@@ -7,7 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [1.2.0] - 2024-02-03 - Phase 4: Analytics & Insights
10
+ ## [1.2.1] - 2026-02-03
11
+
12
+ ### Fixed
13
+ - **Dashboard Chart.js rendering**: Fixed chart initialization timing issue by adding 100ms delay to ensure canvas element is fully mounted before rendering
14
+ - **Build process**: Configured tsup to automatically copy dashboard static files (`src/dashboard/public/`) to `dist/public/` during build
15
+ - **Chart enhancements**: Added better error handling, formatted date labels, tooltips, and improved visual styling for compliance trend chart
16
+
17
+ ### Changed
18
+ - **Build output**: Dashboard static files now automatically included in build output without manual intervention
19
+ - **.gitignore**: Added `.playwright-mcp/` and `.specbridge/reports/history/` to ignore test and runtime artifacts
20
+
21
+ ## [1.2.0] - 2026-02-03
22
+
23
+ ### Phase 4: Analytics & Insights
11
24
 
12
25
  ### Added
13
26
 
package/README.md CHANGED
@@ -16,6 +16,8 @@ SpecBridge creates a living integration layer between design intent and implemen
16
16
  - **Verification Engine** - Continuously verifies code compliance at multiple levels
17
17
  - **Propagation Engine** - Analyzes impact when architectural decisions change
18
18
  - **Compliance Reporting** - Provides dashboards and tracks conformity over time
19
+ - **Analytics & Insights** - AI-generated insights, drift detection, and trend analysis
20
+ - **Interactive Dashboard** - Real-time compliance monitoring with visual charts
19
21
  - **Agent Interface** - Exposes decisions to code generation agents (Copilot, Claude, etc.)
20
22
 
21
23
  ## Installation
@@ -92,7 +94,31 @@ specbridge report
92
94
  specbridge report --format markdown --save
93
95
  ```
94
96
 
95
- ### 6. Integrate with AI agents
97
+ Track compliance trends over time:
98
+ ```bash
99
+ specbridge report --trend --days 30
100
+ specbridge report --drift
101
+ ```
102
+
103
+ ### 6. Analyze compliance with AI insights
104
+
105
+ ```bash
106
+ specbridge analytics
107
+ specbridge analytics --insights
108
+ specbridge analytics auth-001
109
+ ```
110
+
111
+ Get AI-generated insights about compliance trends, violations, and decision impact.
112
+
113
+ ### 7. Launch interactive dashboard
114
+
115
+ ```bash
116
+ specbridge dashboard
117
+ ```
118
+
119
+ Open your browser to view real-time compliance metrics, trend charts, and insights.
120
+
121
+ ### 8. Integrate with AI agents
96
122
 
97
123
  ```bash
98
124
  specbridge context src/api/auth.ts
@@ -222,6 +248,11 @@ verification:
222
248
  | `specbridge decision create <id>` | Create new decision |
223
249
  | `specbridge decision validate` | Validate decision files |
224
250
  | `specbridge report` | Generate compliance report |
251
+ | `specbridge report --trend` | Show compliance trends over time |
252
+ | `specbridge report --drift` | Analyze drift since last report |
253
+ | `specbridge analytics` | Analyze compliance with AI insights |
254
+ | `specbridge analytics <id>` | Analyze specific decision |
255
+ | `specbridge dashboard` | Launch interactive web dashboard |
225
256
  | `specbridge hook install` | Install git hooks |
226
257
  | `specbridge hook run` | Run verification (for hooks) |
227
258
  | `specbridge context <file>` | Generate agent context |
@@ -0,0 +1,551 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SpecBridge Compliance Dashboard</title>
7
+ <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
8
+ <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
9
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0"></script>
11
+ <style>
12
+ * {
13
+ margin: 0;
14
+ padding: 0;
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ body {
19
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
20
+ -webkit-font-smoothing: antialiased;
21
+ -moz-osx-font-smoothing: grayscale;
22
+ background: #f5f7fa;
23
+ color: #2c3e50;
24
+ }
25
+
26
+ .header {
27
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
28
+ color: white;
29
+ padding: 2rem;
30
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
31
+ }
32
+
33
+ .header h1 {
34
+ font-size: 2rem;
35
+ margin-bottom: 0.5rem;
36
+ }
37
+
38
+ .header p {
39
+ opacity: 0.9;
40
+ font-size: 1rem;
41
+ }
42
+
43
+ .container {
44
+ max-width: 1400px;
45
+ margin: 0 auto;
46
+ padding: 2rem;
47
+ }
48
+
49
+ .loading {
50
+ text-align: center;
51
+ padding: 4rem;
52
+ font-size: 1.2rem;
53
+ color: #666;
54
+ }
55
+
56
+ .error {
57
+ background: #fee;
58
+ border: 1px solid #fcc;
59
+ border-radius: 8px;
60
+ padding: 1rem;
61
+ margin: 1rem 0;
62
+ color: #c33;
63
+ }
64
+
65
+ .grid {
66
+ display: grid;
67
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
68
+ gap: 1.5rem;
69
+ margin-bottom: 2rem;
70
+ }
71
+
72
+ .card {
73
+ background: white;
74
+ border-radius: 12px;
75
+ padding: 1.5rem;
76
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
77
+ transition: transform 0.2s, box-shadow 0.2s;
78
+ }
79
+
80
+ .card:hover {
81
+ transform: translateY(-2px);
82
+ box-shadow: 0 4px 16px rgba(0,0,0,0.12);
83
+ }
84
+
85
+ .card h2 {
86
+ font-size: 1.5rem;
87
+ margin-bottom: 1rem;
88
+ color: #2c3e50;
89
+ }
90
+
91
+ .metric {
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: space-between;
95
+ margin-bottom: 0.75rem;
96
+ }
97
+
98
+ .metric-label {
99
+ font-size: 0.9rem;
100
+ color: #666;
101
+ }
102
+
103
+ .metric-value {
104
+ font-size: 1.5rem;
105
+ font-weight: bold;
106
+ }
107
+
108
+ .metric-value.success {
109
+ color: #27ae60;
110
+ }
111
+
112
+ .metric-value.warning {
113
+ color: #f39c12;
114
+ }
115
+
116
+ .metric-value.danger {
117
+ color: #e74c3c;
118
+ }
119
+
120
+ .compliance-score {
121
+ text-align: center;
122
+ padding: 2rem;
123
+ }
124
+
125
+ .compliance-score .score {
126
+ font-size: 4rem;
127
+ font-weight: bold;
128
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
129
+ -webkit-background-clip: text;
130
+ -webkit-text-fill-color: transparent;
131
+ background-clip: text;
132
+ }
133
+
134
+ .compliance-score .label {
135
+ font-size: 0.9rem;
136
+ color: #666;
137
+ text-transform: uppercase;
138
+ letter-spacing: 1px;
139
+ }
140
+
141
+ .trend-indicator {
142
+ display: inline-flex;
143
+ align-items: center;
144
+ gap: 0.25rem;
145
+ padding: 0.25rem 0.75rem;
146
+ border-radius: 12px;
147
+ font-size: 0.85rem;
148
+ font-weight: 600;
149
+ }
150
+
151
+ .trend-indicator.up {
152
+ background: #d4edda;
153
+ color: #155724;
154
+ }
155
+
156
+ .trend-indicator.down {
157
+ background: #f8d7da;
158
+ color: #721c24;
159
+ }
160
+
161
+ .trend-indicator.stable {
162
+ background: #fff3cd;
163
+ color: #856404;
164
+ }
165
+
166
+ .chart-container {
167
+ position: relative;
168
+ height: 300px;
169
+ margin-top: 1rem;
170
+ }
171
+
172
+ .decision-list {
173
+ list-style: none;
174
+ }
175
+
176
+ .decision-item {
177
+ padding: 0.75rem;
178
+ border-bottom: 1px solid #eee;
179
+ display: flex;
180
+ justify-content: space-between;
181
+ align-items: center;
182
+ }
183
+
184
+ .decision-item:last-child {
185
+ border-bottom: none;
186
+ }
187
+
188
+ .decision-name {
189
+ font-weight: 500;
190
+ }
191
+
192
+ .decision-compliance {
193
+ font-weight: bold;
194
+ }
195
+
196
+ .violation-badge {
197
+ display: inline-block;
198
+ padding: 0.25rem 0.5rem;
199
+ border-radius: 4px;
200
+ font-size: 0.75rem;
201
+ font-weight: 600;
202
+ text-transform: uppercase;
203
+ }
204
+
205
+ .violation-badge.critical {
206
+ background: #fee;
207
+ color: #c33;
208
+ }
209
+
210
+ .violation-badge.high {
211
+ background: #fff3cd;
212
+ color: #856404;
213
+ }
214
+
215
+ .violation-badge.medium {
216
+ background: #d1ecf1;
217
+ color: #0c5460;
218
+ }
219
+
220
+ .violation-badge.low {
221
+ background: #e2e3e5;
222
+ color: #383d41;
223
+ }
224
+
225
+ .insights {
226
+ margin-top: 1rem;
227
+ }
228
+
229
+ .insight {
230
+ padding: 0.75rem;
231
+ border-left: 4px solid #ccc;
232
+ margin-bottom: 0.75rem;
233
+ background: #f9f9f9;
234
+ }
235
+
236
+ .insight.warning {
237
+ border-color: #f39c12;
238
+ background: #fff9e6;
239
+ }
240
+
241
+ .insight.success {
242
+ border-color: #27ae60;
243
+ background: #e8f8f0;
244
+ }
245
+
246
+ .insight.info {
247
+ border-color: #3498db;
248
+ background: #e8f4fc;
249
+ }
250
+
251
+ .insight-message {
252
+ font-weight: 500;
253
+ margin-bottom: 0.25rem;
254
+ }
255
+
256
+ .insight-details {
257
+ font-size: 0.85rem;
258
+ color: #666;
259
+ }
260
+
261
+ .timestamp {
262
+ text-align: center;
263
+ color: #999;
264
+ font-size: 0.85rem;
265
+ margin-top: 2rem;
266
+ padding: 1rem;
267
+ }
268
+ </style>
269
+ </head>
270
+ <body>
271
+ <div id="root"></div>
272
+
273
+ <script type="text/babel">
274
+ const { useState, useEffect } = React;
275
+
276
+ function Dashboard() {
277
+ const [loading, setLoading] = useState(true);
278
+ const [error, setError] = useState(null);
279
+ const [report, setReport] = useState(null);
280
+ const [history, setHistory] = useState([]);
281
+ const [analytics, setAnalytics] = useState(null);
282
+
283
+ useEffect(() => {
284
+ loadData();
285
+ }, []);
286
+
287
+ async function loadData() {
288
+ try {
289
+ setLoading(true);
290
+ setError(null);
291
+
292
+ // Load latest report
293
+ const reportRes = await fetch('/api/report/latest');
294
+ if (!reportRes.ok) throw new Error('Failed to load report');
295
+ const reportData = await reportRes.json();
296
+ setReport(reportData);
297
+
298
+ // Load history for chart
299
+ const historyRes = await fetch('/api/report/history?days=30');
300
+ if (historyRes.ok) {
301
+ const historyData = await historyRes.json();
302
+ setHistory(historyData);
303
+ }
304
+
305
+ // Load analytics
306
+ const analyticsRes = await fetch('/api/analytics/summary?days=90');
307
+ if (analyticsRes.ok) {
308
+ const analyticsData = await analyticsRes.json();
309
+ setAnalytics(analyticsData);
310
+ }
311
+
312
+ setLoading(false);
313
+ } catch (err) {
314
+ setError(err.message);
315
+ setLoading(false);
316
+ }
317
+ }
318
+
319
+ useEffect(() => {
320
+ if (history.length > 0) {
321
+ // Small delay to ensure canvas is fully mounted
322
+ const timer = setTimeout(() => {
323
+ renderChart();
324
+ }, 100);
325
+ return () => clearTimeout(timer);
326
+ }
327
+ }, [history]);
328
+
329
+ function renderChart() {
330
+ const ctx = document.getElementById('complianceChart');
331
+ if (!ctx) {
332
+ console.warn('Chart canvas not found');
333
+ return;
334
+ }
335
+
336
+ try {
337
+ // Destroy existing chart
338
+ const existing = Chart.getChart(ctx);
339
+ if (existing) {
340
+ existing.destroy();
341
+ }
342
+
343
+ // Sort history by date
344
+ const sorted = [...history].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
345
+
346
+ // Format dates for better readability
347
+ const formatDate = (timestamp) => {
348
+ const date = new Date(timestamp);
349
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
350
+ };
351
+
352
+ new Chart(ctx, {
353
+ type: 'line',
354
+ data: {
355
+ labels: sorted.map(h => formatDate(h.timestamp)),
356
+ datasets: [{
357
+ label: 'Compliance %',
358
+ data: sorted.map(h => h.report.summary.compliance),
359
+ borderColor: '#667eea',
360
+ backgroundColor: 'rgba(102, 126, 234, 0.1)',
361
+ tension: 0.4,
362
+ fill: true,
363
+ pointRadius: 4,
364
+ pointHoverRadius: 6,
365
+ }]
366
+ },
367
+ options: {
368
+ responsive: true,
369
+ maintainAspectRatio: false,
370
+ plugins: {
371
+ legend: {
372
+ display: false,
373
+ },
374
+ tooltip: {
375
+ callbacks: {
376
+ label: (context) => `Compliance: ${context.parsed.y}%`
377
+ }
378
+ }
379
+ },
380
+ scales: {
381
+ y: {
382
+ beginAtZero: true,
383
+ max: 100,
384
+ ticks: {
385
+ callback: (value) => value + '%'
386
+ }
387
+ },
388
+ x: {
389
+ ticks: {
390
+ maxRotation: 45,
391
+ minRotation: 45
392
+ }
393
+ }
394
+ }
395
+ }
396
+ });
397
+ } catch (error) {
398
+ console.error('Failed to render chart:', error);
399
+ }
400
+ }
401
+
402
+ if (loading) {
403
+ return <div className="loading">Loading dashboard data...</div>;
404
+ }
405
+
406
+ if (error) {
407
+ return (
408
+ <div className="container">
409
+ <div className="error">
410
+ <strong>Error:</strong> {error}
411
+ </div>
412
+ </div>
413
+ );
414
+ }
415
+
416
+ if (!report) {
417
+ return (
418
+ <div className="container">
419
+ <div className="error">No report data available. Run `specbridge report` to generate a report.</div>
420
+ </div>
421
+ );
422
+ }
423
+
424
+ return (
425
+ <>
426
+ <div className="header">
427
+ <h1>📊 SpecBridge Compliance Dashboard</h1>
428
+ <p>{report.project} - {new Date(report.timestamp).toLocaleString()}</p>
429
+ </div>
430
+
431
+ <div className="container">
432
+ {/* Summary Cards */}
433
+ <div className="grid">
434
+ <div className="card">
435
+ <div className="compliance-score">
436
+ <div className="score">{report.summary.compliance}%</div>
437
+ <div className="label">Overall Compliance</div>
438
+ {analytics && (
439
+ <div style={{ marginTop: '1rem' }}>
440
+ <span className={`trend-indicator ${analytics.overallTrend}`}>
441
+ {analytics.overallTrend === 'up' ? '📈' : analytics.overallTrend === 'down' ? '📉' : '➡️'}
442
+ {' '}
443
+ {analytics.overallTrend.toUpperCase()}
444
+ </span>
445
+ </div>
446
+ )}
447
+ </div>
448
+ </div>
449
+
450
+ <div className="card">
451
+ <h2>📋 Decisions</h2>
452
+ <div className="metric">
453
+ <span className="metric-label">Total Decisions</span>
454
+ <span className="metric-value">{report.summary.totalDecisions}</span>
455
+ </div>
456
+ <div className="metric">
457
+ <span className="metric-label">Active Decisions</span>
458
+ <span className="metric-value success">{report.summary.activeDecisions}</span>
459
+ </div>
460
+ <div className="metric">
461
+ <span className="metric-label">Total Constraints</span>
462
+ <span className="metric-value">{report.summary.totalConstraints}</span>
463
+ </div>
464
+ </div>
465
+
466
+ <div className="card">
467
+ <h2>⚠️ Violations</h2>
468
+ <div className="metric">
469
+ <span className="metric-label">Critical</span>
470
+ <span className={`metric-value ${report.summary.violations.critical > 0 ? 'danger' : 'success'}`}>
471
+ {report.summary.violations.critical}
472
+ </span>
473
+ </div>
474
+ <div className="metric">
475
+ <span className="metric-label">High</span>
476
+ <span className={`metric-value ${report.summary.violations.high > 0 ? 'warning' : 'success'}`}>
477
+ {report.summary.violations.high}
478
+ </span>
479
+ </div>
480
+ <div className="metric">
481
+ <span className="metric-label">Medium</span>
482
+ <span className="metric-value">{report.summary.violations.medium}</span>
483
+ </div>
484
+ <div className="metric">
485
+ <span className="metric-label">Low</span>
486
+ <span className="metric-value">{report.summary.violations.low}</span>
487
+ </div>
488
+ </div>
489
+ </div>
490
+
491
+ {/* Compliance Trend Chart */}
492
+ {history.length > 1 && (
493
+ <div className="card">
494
+ <h2>📈 Compliance Trend (30 Days)</h2>
495
+ <div className="chart-container">
496
+ <canvas id="complianceChart"></canvas>
497
+ </div>
498
+ </div>
499
+ )}
500
+
501
+ {/* Decision Breakdown */}
502
+ <div className="card">
503
+ <h2>🎯 Decision Compliance</h2>
504
+ <ul className="decision-list">
505
+ {report.byDecision.map(decision => (
506
+ <li key={decision.decisionId} className="decision-item">
507
+ <div>
508
+ <div className="decision-name">{decision.title}</div>
509
+ <div style={{ marginTop: '0.25rem' }}>
510
+ {decision.violations > 0 && (
511
+ <span className="violation-badge medium">
512
+ {decision.violations} violation{decision.violations !== 1 ? 's' : ''}
513
+ </span>
514
+ )}
515
+ </div>
516
+ </div>
517
+ <div className={`decision-compliance ${decision.compliance === 100 ? 'success' : decision.compliance < 50 ? 'danger' : 'warning'}`}>
518
+ {decision.compliance}%
519
+ </div>
520
+ </li>
521
+ ))}
522
+ </ul>
523
+ </div>
524
+
525
+ {/* Insights */}
526
+ {analytics && analytics.insights && analytics.insights.length > 0 && (
527
+ <div className="card">
528
+ <h2>💡 Insights</h2>
529
+ <div className="insights">
530
+ {analytics.insights.map((insight, i) => (
531
+ <div key={i} className={`insight ${insight.type}`}>
532
+ <div className="insight-message">{insight.message}</div>
533
+ {insight.details && <div className="insight-details">{insight.details}</div>}
534
+ </div>
535
+ ))}
536
+ </div>
537
+ </div>
538
+ )}
539
+
540
+ <div className="timestamp">
541
+ Last updated: {new Date(report.timestamp).toLocaleString()}
542
+ </div>
543
+ </div>
544
+ </>
545
+ );
546
+ }
547
+
548
+ ReactDOM.render(<Dashboard />, document.getElementById('root'));
549
+ </script>
550
+ </body>
551
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ipation/specbridge",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Architecture Decision Runtime - Transform architectural decisions into executable, verifiable constraints",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",