@contractspec/lib.example-shared-ui 0.0.0-canary-20260113170453
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/.turbo/turbo-build$colon$bundle.log +9 -0
- package/.turbo/turbo-build.log +11 -0
- package/CHANGELOG.md +34 -0
- package/dist/index.mjs +3121 -0
- package/package.json +43 -0
- package/src/EvolutionDashboard.tsx +480 -0
- package/src/EvolutionSidebar.tsx +282 -0
- package/src/LocalDataIndicator.tsx +39 -0
- package/src/MarkdownView.tsx +389 -0
- package/src/OverlayContextProvider.tsx +341 -0
- package/src/PersonalizationInsights.tsx +293 -0
- package/src/SaveToStudioButton.tsx +64 -0
- package/src/SpecEditorPanel.tsx +165 -0
- package/src/TemplateShell.tsx +63 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useBehaviorTracking.ts +327 -0
- package/src/hooks/useEvolution.ts +501 -0
- package/src/hooks/useRegistryTemplates.ts +49 -0
- package/src/hooks/useSpecContent.ts +243 -0
- package/src/hooks/useWorkflowComposer.ts +670 -0
- package/src/index.ts +15 -0
- package/src/lib/component-registry.tsx +64 -0
- package/src/lib/runtime-context.tsx +54 -0
- package/src/lib/types.ts +84 -0
- package/src/overlay-types.ts +25 -0
- package/src/utils/fetchPresentationData.ts +48 -0
- package/src/utils/generateSpecFromTemplate.ts +458 -0
- package/src/utils/index.ts +2 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import type { TemplateId } from '../lib/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Types from @contractspec/lib.evolution (replicated to avoid SSR issues)
|
|
8
|
+
*/
|
|
9
|
+
export type AnomalySeverity = 'low' | 'medium' | 'high';
|
|
10
|
+
export type SuggestionStatus = 'pending' | 'approved' | 'rejected';
|
|
11
|
+
|
|
12
|
+
export interface OperationCoordinate {
|
|
13
|
+
name: string;
|
|
14
|
+
version: string;
|
|
15
|
+
tenantId?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OperationMetricSample {
|
|
19
|
+
operation: OperationCoordinate;
|
|
20
|
+
durationMs: number;
|
|
21
|
+
success: boolean;
|
|
22
|
+
timestamp: Date;
|
|
23
|
+
payloadSizeBytes?: number;
|
|
24
|
+
errorCode?: string;
|
|
25
|
+
errorMessage?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SpecUsageStats {
|
|
29
|
+
operation: OperationCoordinate;
|
|
30
|
+
totalCalls: number;
|
|
31
|
+
successRate: number;
|
|
32
|
+
errorRate: number;
|
|
33
|
+
averageLatencyMs: number;
|
|
34
|
+
p95LatencyMs: number;
|
|
35
|
+
p99LatencyMs: number;
|
|
36
|
+
maxLatencyMs: number;
|
|
37
|
+
lastSeenAt: Date;
|
|
38
|
+
windowStart: Date;
|
|
39
|
+
windowEnd: Date;
|
|
40
|
+
topErrors: Record<string, number>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SpecAnomaly {
|
|
44
|
+
operation: OperationCoordinate;
|
|
45
|
+
severity: AnomalySeverity;
|
|
46
|
+
metric: 'latency' | 'error-rate' | 'throughput' | 'policy' | 'schema';
|
|
47
|
+
description: string;
|
|
48
|
+
detectedAt: Date;
|
|
49
|
+
threshold?: number;
|
|
50
|
+
observedValue?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface IntentPattern {
|
|
54
|
+
id: string;
|
|
55
|
+
type:
|
|
56
|
+
| 'latency-regression'
|
|
57
|
+
| 'error-spike'
|
|
58
|
+
| 'missing-operation'
|
|
59
|
+
| 'chained-intent'
|
|
60
|
+
| 'throughput-drop'
|
|
61
|
+
| 'schema-mismatch';
|
|
62
|
+
description: string;
|
|
63
|
+
operation?: OperationCoordinate;
|
|
64
|
+
confidence: { score: number; sampleSize: number };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SpecSuggestion {
|
|
68
|
+
id: string;
|
|
69
|
+
intent: IntentPattern;
|
|
70
|
+
target?: OperationCoordinate;
|
|
71
|
+
proposal: {
|
|
72
|
+
summary: string;
|
|
73
|
+
rationale: string;
|
|
74
|
+
changeType: 'new-spec' | 'revision' | 'policy-update' | 'schema-update';
|
|
75
|
+
recommendedActions?: string[];
|
|
76
|
+
};
|
|
77
|
+
confidence: number;
|
|
78
|
+
createdAt: Date;
|
|
79
|
+
createdBy: string;
|
|
80
|
+
status: SuggestionStatus;
|
|
81
|
+
priority: 'low' | 'medium' | 'high';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface OptimizationHint {
|
|
85
|
+
operation: OperationCoordinate;
|
|
86
|
+
category: 'schema' | 'policy' | 'performance' | 'error-handling';
|
|
87
|
+
summary: string;
|
|
88
|
+
justification: string;
|
|
89
|
+
recommendedActions: string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Hook return type
|
|
94
|
+
*/
|
|
95
|
+
export interface UseEvolutionReturn {
|
|
96
|
+
/** Usage statistics from analyzed metrics */
|
|
97
|
+
usageStats: SpecUsageStats[];
|
|
98
|
+
/** Detected anomalies */
|
|
99
|
+
anomalies: SpecAnomaly[];
|
|
100
|
+
/** AI-generated suggestions */
|
|
101
|
+
suggestions: SpecSuggestion[];
|
|
102
|
+
/** Optimization hints */
|
|
103
|
+
hints: OptimizationHint[];
|
|
104
|
+
/** Whether data is loading */
|
|
105
|
+
loading: boolean;
|
|
106
|
+
/** Track a new operation metric */
|
|
107
|
+
trackOperation: (
|
|
108
|
+
operationName: string,
|
|
109
|
+
durationMs: number,
|
|
110
|
+
success: boolean,
|
|
111
|
+
errorCode?: string
|
|
112
|
+
) => void;
|
|
113
|
+
/** Analyze tracked operations */
|
|
114
|
+
analyzeUsage: () => void;
|
|
115
|
+
/** Generate suggestions from anomalies */
|
|
116
|
+
generateSuggestions: () => Promise<void>;
|
|
117
|
+
/** Approve a suggestion */
|
|
118
|
+
approveSuggestion: (id: string, notes?: string) => void;
|
|
119
|
+
/** Reject a suggestion */
|
|
120
|
+
rejectSuggestion: (id: string, notes?: string) => void;
|
|
121
|
+
/** Clear all data */
|
|
122
|
+
clear: () => void;
|
|
123
|
+
/** Total tracked operations count */
|
|
124
|
+
operationCount: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Storage key for evolution data persistence
|
|
129
|
+
*/
|
|
130
|
+
const EVOLUTION_STORAGE_KEY = 'contractspec-evolution-data';
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Hook for AI-powered spec evolution analysis and suggestions.
|
|
134
|
+
* Tracks sandbox operations, detects anomalies, and generates improvement suggestions.
|
|
135
|
+
*/
|
|
136
|
+
export function useEvolution(templateId: TemplateId): UseEvolutionReturn {
|
|
137
|
+
const [usageStats, setUsageStats] = useState<SpecUsageStats[]>([]);
|
|
138
|
+
const [anomalies, setAnomalies] = useState<SpecAnomaly[]>([]);
|
|
139
|
+
const [suggestions, setSuggestions] = useState<SpecSuggestion[]>([]);
|
|
140
|
+
const [hints, setHints] = useState<OptimizationHint[]>([]);
|
|
141
|
+
const [loading, setLoading] = useState(false);
|
|
142
|
+
|
|
143
|
+
// Use ref to avoid re-renders on every track
|
|
144
|
+
const metricsRef = useRef<OperationMetricSample[]>([]);
|
|
145
|
+
const [operationCount, setOperationCount] = useState(0);
|
|
146
|
+
|
|
147
|
+
// Load persisted data on mount
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
try {
|
|
150
|
+
const stored = localStorage.getItem(
|
|
151
|
+
`${EVOLUTION_STORAGE_KEY}-${templateId}`
|
|
152
|
+
);
|
|
153
|
+
if (stored) {
|
|
154
|
+
const data = JSON.parse(stored) as {
|
|
155
|
+
suggestions: (Omit<SpecSuggestion, 'createdAt'> & {
|
|
156
|
+
createdAt: string;
|
|
157
|
+
})[];
|
|
158
|
+
};
|
|
159
|
+
setSuggestions(
|
|
160
|
+
data.suggestions.map((s) => ({
|
|
161
|
+
...s,
|
|
162
|
+
createdAt: new Date(s.createdAt),
|
|
163
|
+
}))
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// Ignore storage errors
|
|
168
|
+
}
|
|
169
|
+
}, [templateId]);
|
|
170
|
+
|
|
171
|
+
// Persist suggestions when they change
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
try {
|
|
174
|
+
localStorage.setItem(
|
|
175
|
+
`${EVOLUTION_STORAGE_KEY}-${templateId}`,
|
|
176
|
+
JSON.stringify({ suggestions })
|
|
177
|
+
);
|
|
178
|
+
} catch {
|
|
179
|
+
// Ignore storage errors
|
|
180
|
+
}
|
|
181
|
+
}, [suggestions, templateId]);
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Track a new operation metric
|
|
185
|
+
*/
|
|
186
|
+
const trackOperation = useCallback(
|
|
187
|
+
(
|
|
188
|
+
operationName: string,
|
|
189
|
+
durationMs: number,
|
|
190
|
+
success: boolean,
|
|
191
|
+
errorCode?: string
|
|
192
|
+
) => {
|
|
193
|
+
const sample: OperationMetricSample = {
|
|
194
|
+
operation: {
|
|
195
|
+
name: operationName,
|
|
196
|
+
version: '1.0.0',
|
|
197
|
+
tenantId: 'sandbox',
|
|
198
|
+
},
|
|
199
|
+
durationMs,
|
|
200
|
+
success,
|
|
201
|
+
timestamp: new Date(),
|
|
202
|
+
errorCode,
|
|
203
|
+
};
|
|
204
|
+
metricsRef.current.push(sample);
|
|
205
|
+
setOperationCount((prev) => prev + 1);
|
|
206
|
+
},
|
|
207
|
+
[]
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Analyze tracked operations to generate usage stats and anomalies
|
|
212
|
+
*/
|
|
213
|
+
const analyzeUsage = useCallback(() => {
|
|
214
|
+
const samples = metricsRef.current;
|
|
215
|
+
if (samples.length < 5) return;
|
|
216
|
+
|
|
217
|
+
// Group samples by operation
|
|
218
|
+
const groups = new Map<string, OperationMetricSample[]>();
|
|
219
|
+
for (const sample of samples) {
|
|
220
|
+
const key = `${sample.operation.name}.v${sample.operation.version}`;
|
|
221
|
+
const arr = groups.get(key) ?? [];
|
|
222
|
+
arr.push(sample);
|
|
223
|
+
groups.set(key, arr);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Calculate stats for each operation
|
|
227
|
+
const stats: SpecUsageStats[] = [];
|
|
228
|
+
const detectedAnomalies: SpecAnomaly[] = [];
|
|
229
|
+
|
|
230
|
+
groups.forEach((opSamples) => {
|
|
231
|
+
if (opSamples.length < 3) return;
|
|
232
|
+
|
|
233
|
+
const durations = opSamples
|
|
234
|
+
.map((s) => s.durationMs)
|
|
235
|
+
.sort((a, b) => a - b);
|
|
236
|
+
const errors = opSamples.filter((s) => !s.success);
|
|
237
|
+
const totalCalls = opSamples.length;
|
|
238
|
+
const errorRate = errors.length / totalCalls;
|
|
239
|
+
const averageLatencyMs =
|
|
240
|
+
durations.reduce((sum, value) => sum + value, 0) / totalCalls;
|
|
241
|
+
|
|
242
|
+
const timestamps = opSamples.map((s) => s.timestamp.getTime());
|
|
243
|
+
const firstSample = opSamples[0];
|
|
244
|
+
if (!firstSample) return;
|
|
245
|
+
|
|
246
|
+
const stat: SpecUsageStats = {
|
|
247
|
+
operation: firstSample.operation,
|
|
248
|
+
totalCalls,
|
|
249
|
+
successRate: 1 - errorRate,
|
|
250
|
+
errorRate,
|
|
251
|
+
averageLatencyMs,
|
|
252
|
+
p95LatencyMs: percentile(durations, 0.95),
|
|
253
|
+
p99LatencyMs: percentile(durations, 0.99),
|
|
254
|
+
maxLatencyMs: Math.max(...durations),
|
|
255
|
+
lastSeenAt: new Date(Math.max(...timestamps)),
|
|
256
|
+
windowStart: new Date(Math.min(...timestamps)),
|
|
257
|
+
windowEnd: new Date(Math.max(...timestamps)),
|
|
258
|
+
topErrors: errors.reduce<Record<string, number>>((acc, s) => {
|
|
259
|
+
if (s.errorCode) {
|
|
260
|
+
acc[s.errorCode] = (acc[s.errorCode] ?? 0) + 1;
|
|
261
|
+
}
|
|
262
|
+
return acc;
|
|
263
|
+
}, {}),
|
|
264
|
+
};
|
|
265
|
+
stats.push(stat);
|
|
266
|
+
|
|
267
|
+
// Detect anomalies
|
|
268
|
+
if (errorRate > 0.1) {
|
|
269
|
+
detectedAnomalies.push({
|
|
270
|
+
operation: stat.operation,
|
|
271
|
+
severity:
|
|
272
|
+
errorRate > 0.3 ? 'high' : errorRate > 0.2 ? 'medium' : 'low',
|
|
273
|
+
metric: 'error-rate',
|
|
274
|
+
description: `Error rate ${(errorRate * 100).toFixed(1)}% exceeds threshold`,
|
|
275
|
+
detectedAt: new Date(),
|
|
276
|
+
threshold: 0.1,
|
|
277
|
+
observedValue: errorRate,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (stat.p99LatencyMs > 500) {
|
|
282
|
+
detectedAnomalies.push({
|
|
283
|
+
operation: stat.operation,
|
|
284
|
+
severity:
|
|
285
|
+
stat.p99LatencyMs > 1000
|
|
286
|
+
? 'high'
|
|
287
|
+
: stat.p99LatencyMs > 750
|
|
288
|
+
? 'medium'
|
|
289
|
+
: 'low',
|
|
290
|
+
metric: 'latency',
|
|
291
|
+
description: `P99 latency ${stat.p99LatencyMs.toFixed(0)}ms exceeds threshold`,
|
|
292
|
+
detectedAt: new Date(),
|
|
293
|
+
threshold: 500,
|
|
294
|
+
observedValue: stat.p99LatencyMs,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
setUsageStats(stats);
|
|
300
|
+
setAnomalies(detectedAnomalies);
|
|
301
|
+
|
|
302
|
+
// Generate hints
|
|
303
|
+
const newHints: OptimizationHint[] = detectedAnomalies.map((anomaly) => ({
|
|
304
|
+
operation: anomaly.operation,
|
|
305
|
+
category: anomaly.metric === 'latency' ? 'performance' : 'error-handling',
|
|
306
|
+
summary:
|
|
307
|
+
anomaly.metric === 'latency'
|
|
308
|
+
? 'Latency regression detected'
|
|
309
|
+
: 'Error spike detected',
|
|
310
|
+
justification: anomaly.description,
|
|
311
|
+
recommendedActions:
|
|
312
|
+
anomaly.metric === 'latency'
|
|
313
|
+
? [
|
|
314
|
+
'Add caching layer',
|
|
315
|
+
'Optimize database queries',
|
|
316
|
+
'Consider pagination',
|
|
317
|
+
]
|
|
318
|
+
: [
|
|
319
|
+
'Add retry logic',
|
|
320
|
+
'Improve error handling',
|
|
321
|
+
'Add circuit breaker',
|
|
322
|
+
],
|
|
323
|
+
}));
|
|
324
|
+
setHints(newHints);
|
|
325
|
+
}, []);
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Generate AI suggestions from anomalies (mock for sandbox)
|
|
329
|
+
*/
|
|
330
|
+
const generateSuggestions = useCallback(async () => {
|
|
331
|
+
if (anomalies.length === 0) return;
|
|
332
|
+
|
|
333
|
+
setLoading(true);
|
|
334
|
+
// Simulate AI generation delay
|
|
335
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
336
|
+
|
|
337
|
+
const newSuggestions: SpecSuggestion[] = anomalies.map((anomaly) => ({
|
|
338
|
+
id: `suggestion-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
339
|
+
intent: {
|
|
340
|
+
id: `intent-${anomaly.operation.name}`,
|
|
341
|
+
type:
|
|
342
|
+
anomaly.metric === 'latency'
|
|
343
|
+
? 'latency-regression'
|
|
344
|
+
: anomaly.metric === 'error-rate'
|
|
345
|
+
? 'error-spike'
|
|
346
|
+
: 'throughput-drop',
|
|
347
|
+
description: anomaly.description,
|
|
348
|
+
operation: anomaly.operation,
|
|
349
|
+
confidence: {
|
|
350
|
+
score:
|
|
351
|
+
anomaly.severity === 'high'
|
|
352
|
+
? 0.9
|
|
353
|
+
: anomaly.severity === 'medium'
|
|
354
|
+
? 0.7
|
|
355
|
+
: 0.5,
|
|
356
|
+
sampleSize:
|
|
357
|
+
usageStats.find((s) => s.operation.name === anomaly.operation.name)
|
|
358
|
+
?.totalCalls ?? 0,
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
target: anomaly.operation,
|
|
362
|
+
proposal: {
|
|
363
|
+
summary: generateSuggestionSummary(anomaly),
|
|
364
|
+
rationale: generateSuggestionRationale(anomaly),
|
|
365
|
+
changeType:
|
|
366
|
+
anomaly.metric === 'error-rate' ? 'policy-update' : 'revision',
|
|
367
|
+
recommendedActions: generateRecommendedActions(anomaly),
|
|
368
|
+
},
|
|
369
|
+
confidence:
|
|
370
|
+
anomaly.severity === 'high'
|
|
371
|
+
? 0.85
|
|
372
|
+
: anomaly.severity === 'medium'
|
|
373
|
+
? 0.7
|
|
374
|
+
: 0.55,
|
|
375
|
+
createdAt: new Date(),
|
|
376
|
+
createdBy: 'ai-evolution-agent',
|
|
377
|
+
status: 'pending',
|
|
378
|
+
priority: anomaly.severity,
|
|
379
|
+
}));
|
|
380
|
+
|
|
381
|
+
setSuggestions((prev) => [...prev, ...newSuggestions]);
|
|
382
|
+
setLoading(false);
|
|
383
|
+
}, [anomalies, usageStats]);
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Approve a suggestion
|
|
387
|
+
*/
|
|
388
|
+
const approveSuggestion = useCallback((id: string, _notes?: string) => {
|
|
389
|
+
setSuggestions((prev) =>
|
|
390
|
+
prev.map((s) => (s.id === id ? { ...s, status: 'approved' as const } : s))
|
|
391
|
+
);
|
|
392
|
+
}, []);
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Reject a suggestion
|
|
396
|
+
*/
|
|
397
|
+
const rejectSuggestion = useCallback((id: string, _notes?: string) => {
|
|
398
|
+
setSuggestions((prev) =>
|
|
399
|
+
prev.map((s) => (s.id === id ? { ...s, status: 'rejected' as const } : s))
|
|
400
|
+
);
|
|
401
|
+
}, []);
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Clear all evolution data
|
|
405
|
+
*/
|
|
406
|
+
const clear = useCallback(() => {
|
|
407
|
+
metricsRef.current = [];
|
|
408
|
+
setOperationCount(0);
|
|
409
|
+
setUsageStats([]);
|
|
410
|
+
setAnomalies([]);
|
|
411
|
+
setSuggestions([]);
|
|
412
|
+
setHints([]);
|
|
413
|
+
localStorage.removeItem(`${EVOLUTION_STORAGE_KEY}-${templateId}`);
|
|
414
|
+
}, [templateId]);
|
|
415
|
+
|
|
416
|
+
return useMemo(
|
|
417
|
+
() => ({
|
|
418
|
+
usageStats,
|
|
419
|
+
anomalies,
|
|
420
|
+
suggestions,
|
|
421
|
+
hints,
|
|
422
|
+
loading,
|
|
423
|
+
trackOperation,
|
|
424
|
+
analyzeUsage,
|
|
425
|
+
generateSuggestions,
|
|
426
|
+
approveSuggestion,
|
|
427
|
+
rejectSuggestion,
|
|
428
|
+
clear,
|
|
429
|
+
operationCount,
|
|
430
|
+
}),
|
|
431
|
+
[
|
|
432
|
+
usageStats,
|
|
433
|
+
anomalies,
|
|
434
|
+
suggestions,
|
|
435
|
+
hints,
|
|
436
|
+
loading,
|
|
437
|
+
trackOperation,
|
|
438
|
+
analyzeUsage,
|
|
439
|
+
generateSuggestions,
|
|
440
|
+
approveSuggestion,
|
|
441
|
+
rejectSuggestion,
|
|
442
|
+
clear,
|
|
443
|
+
operationCount,
|
|
444
|
+
]
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Utility functions for generating mock AI content
|
|
450
|
+
*/
|
|
451
|
+
function percentile(values: number[], p: number): number {
|
|
452
|
+
if (!values.length) return 0;
|
|
453
|
+
if (values.length === 1) return values[0] ?? 0;
|
|
454
|
+
const idx = Math.min(values.length - 1, Math.floor(p * values.length));
|
|
455
|
+
return values[idx] ?? 0;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function generateSuggestionSummary(anomaly: SpecAnomaly): string {
|
|
459
|
+
if (anomaly.metric === 'latency') {
|
|
460
|
+
return `Add caching and pagination to ${anomaly.operation.name} to reduce latency`;
|
|
461
|
+
}
|
|
462
|
+
if (anomaly.metric === 'error-rate') {
|
|
463
|
+
return `Add retry policy and circuit breaker to ${anomaly.operation.name}`;
|
|
464
|
+
}
|
|
465
|
+
return `Optimize ${anomaly.operation.name} for improved throughput`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function generateSuggestionRationale(anomaly: SpecAnomaly): string {
|
|
469
|
+
if (anomaly.metric === 'latency') {
|
|
470
|
+
return `The operation ${anomaly.operation.name} is experiencing P99 latency of ${anomaly.observedValue?.toFixed(0)}ms, which is above the recommended threshold of ${anomaly.threshold}ms. This can impact user experience and downstream operations.`;
|
|
471
|
+
}
|
|
472
|
+
if (anomaly.metric === 'error-rate') {
|
|
473
|
+
return `The error rate for ${anomaly.operation.name} is ${((anomaly.observedValue ?? 0) * 100).toFixed(1)}%, indicating potential issues with input validation, external dependencies, or resource limits.`;
|
|
474
|
+
}
|
|
475
|
+
return `Throughput for ${anomaly.operation.name} has dropped significantly, suggesting potential bottlenecks or reduced demand that should be investigated.`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function generateRecommendedActions(anomaly: SpecAnomaly): string[] {
|
|
479
|
+
if (anomaly.metric === 'latency') {
|
|
480
|
+
return [
|
|
481
|
+
'Add response caching for frequently accessed data',
|
|
482
|
+
'Implement pagination for large result sets',
|
|
483
|
+
'Optimize database queries with proper indexing',
|
|
484
|
+
'Consider adding a GraphQL DataLoader for batching',
|
|
485
|
+
];
|
|
486
|
+
}
|
|
487
|
+
if (anomaly.metric === 'error-rate') {
|
|
488
|
+
return [
|
|
489
|
+
'Add input validation at the contract level',
|
|
490
|
+
'Implement retry policy with exponential backoff',
|
|
491
|
+
'Add circuit breaker for external dependencies',
|
|
492
|
+
'Enhance error logging for better debugging',
|
|
493
|
+
];
|
|
494
|
+
}
|
|
495
|
+
return [
|
|
496
|
+
'Review resource allocation and scaling policies',
|
|
497
|
+
'Check for upstream routing or load balancer issues',
|
|
498
|
+
'Validate feature flag configurations',
|
|
499
|
+
'Monitor dependency health metrics',
|
|
500
|
+
];
|
|
501
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
export interface RegistryTemplate {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
tags: string[];
|
|
8
|
+
source: 'registry';
|
|
9
|
+
registryUrl?: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useRegistryTemplates() {
|
|
13
|
+
return useQuery({
|
|
14
|
+
queryKey: ['registryTemplates'] as const,
|
|
15
|
+
queryFn: async (): Promise<RegistryTemplate[]> => {
|
|
16
|
+
const registryUrl =
|
|
17
|
+
process.env.NEXT_PUBLIC_CONTRACTSPEC_REGISTRY_URL ?? '';
|
|
18
|
+
if (!registryUrl) return [];
|
|
19
|
+
const res = await fetch(
|
|
20
|
+
`${registryUrl.replace(/\/$/, '')}/r/contractspec.json`,
|
|
21
|
+
{
|
|
22
|
+
method: 'GET',
|
|
23
|
+
headers: { Accept: 'application/json' },
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
if (!res.ok) return [];
|
|
27
|
+
const json = (await res.json()) as {
|
|
28
|
+
items?: {
|
|
29
|
+
name: string;
|
|
30
|
+
type: string;
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
meta?: { tags?: string[] };
|
|
34
|
+
}[];
|
|
35
|
+
};
|
|
36
|
+
const items = json.items ?? [];
|
|
37
|
+
return items
|
|
38
|
+
.filter((i) => i.type === 'contractspec:template')
|
|
39
|
+
.map((i) => ({
|
|
40
|
+
id: i.name,
|
|
41
|
+
name: i.title ?? i.name,
|
|
42
|
+
description: i.description,
|
|
43
|
+
tags: i.meta?.tags ?? [],
|
|
44
|
+
source: 'registry' as const,
|
|
45
|
+
registryUrl,
|
|
46
|
+
}));
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|