@littlebearapps/create-platform 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +98 -0
  2. package/dist/index.d.ts +6 -1
  3. package/dist/index.js +36 -6
  4. package/dist/prompts.d.ts +14 -2
  5. package/dist/prompts.js +29 -7
  6. package/dist/templates.js +78 -0
  7. package/package.json +3 -2
  8. package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
  9. package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
  10. package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
  11. package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
  12. package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
  13. package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
  14. package/templates/full/workers/pattern-discovery.ts +661 -0
  15. package/templates/full/workers/platform-alert-router.ts +1809 -0
  16. package/templates/full/workers/platform-notifications.ts +424 -0
  17. package/templates/full/workers/platform-search.ts +480 -0
  18. package/templates/full/workers/platform-settings.ts +436 -0
  19. package/templates/shared/workers/lib/analytics-engine.ts +357 -0
  20. package/templates/shared/workers/lib/billing.ts +293 -0
  21. package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
  22. package/templates/shared/workers/lib/control.ts +292 -0
  23. package/templates/shared/workers/lib/economics.ts +368 -0
  24. package/templates/shared/workers/lib/metrics.ts +103 -0
  25. package/templates/shared/workers/lib/platform-settings.ts +407 -0
  26. package/templates/shared/workers/lib/shared/allowances.ts +333 -0
  27. package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
  28. package/templates/shared/workers/lib/shared/types.ts +58 -0
  29. package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
  30. package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
  31. package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
  32. package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
  33. package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
  34. package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
  35. package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
  36. package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
  37. package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
  38. package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
  39. package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
  40. package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
  41. package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
  42. package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
  43. package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
  44. package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
  45. package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
  46. package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
  47. package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
  48. package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
  49. package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
  50. package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
  51. package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
  52. package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
  53. package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
  54. package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
  55. package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
  56. package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
  57. package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
  58. package/templates/shared/workers/platform-usage.ts +1915 -0
  59. package/templates/standard/workers/error-collector.ts +2670 -0
  60. package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
  61. package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
  62. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
  63. package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
  64. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
  65. package/templates/standard/workers/lib/error-collector/github.ts +329 -0
  66. package/templates/standard/workers/lib/error-collector/types.ts +262 -0
  67. package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
  68. package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
  69. package/templates/standard/workers/platform-sentinel.ts +1744 -0
@@ -0,0 +1,644 @@
1
+ /**
2
+ * AI Prompt for Pattern Suggestion
3
+ *
4
+ * Uses DeepSeek via AI Gateway to analyse error clusters
5
+ * and suggest transient error patterns.
6
+ *
7
+ * @module workers/lib/pattern-discovery/ai-prompt
8
+ */
9
+
10
+ import type { ErrorCluster, AISuggestionResponse, PatternType } from './types';
11
+ import type { Logger } from '@littlebearapps/platform-sdk';
12
+ import type { AggregatedPatternEvidence } from './storage';
13
+
14
+ /** DeepSeek AI Gateway URL */
15
+ const DEEPSEEK_GATEWAY_URL = 'https://gateway.ai.cloudflare.com/v1';
16
+
17
+ /** Maximum tokens for AI response */
18
+ const MAX_TOKENS = 1500;
19
+
20
+ /** Timeout for AI API calls (25s — well within Worker's 30s scheduled limit) */
21
+ const AI_FETCH_TIMEOUT_MS = 25_000;
22
+
23
+ /**
24
+ * Build the prompt for pattern suggestion
25
+ */
26
+ function buildPrompt(clusters: ErrorCluster[], sampleMessages: Map<string, string[]>): string {
27
+ const clusterDescriptions = clusters
28
+ .map((cluster, i) => {
29
+ const samples = sampleMessages.get(cluster.id) || [cluster.representativeMessage];
30
+ return `
31
+ ### Cluster ${i + 1}
32
+ - **Occurrences**: ${cluster.occurrenceCount}
33
+ - **Unique fingerprints**: ${cluster.uniqueFingerprints}
34
+ - **Scripts**: ${cluster.scripts.join(', ')}
35
+ - **Sample messages**:
36
+ ${samples.map((s, j) => ` ${j + 1}. "${s}"`).join('\n')}
37
+ `;
38
+ })
39
+ .join('\n');
40
+
41
+ return `You are analysing error messages from Cloudflare Workers to identify TRANSIENT errors.
42
+
43
+ ## Definition of Transient Errors
44
+ Transient errors are expected operational issues that:
45
+ - Self-resolve over time (quota resets, rate limits lift, services recover)
46
+ - Are caused by external factors (API limits, network issues, deployments)
47
+ - Should NOT create duplicate GitHub issues
48
+
49
+ ## Common Transient Categories
50
+ - \`quota-exhausted\`: API quotas, daily limits
51
+ - \`rate-limited\`: Rate limiting, 429 errors
52
+ - \`timeout\`: Request/connection timeouts
53
+ - \`service-unavailable\`: 502/503 errors
54
+ - \`connection-error\`: ECONNREFUSED, ETIMEDOUT, ECONNRESET
55
+ - \`deployment-related\`: Durable Object resets, code updates
56
+
57
+ ## Error Clusters to Analyse
58
+ ${clusterDescriptions}
59
+
60
+ ## Your Task
61
+ For each cluster that represents a TRANSIENT error, suggest a pattern to match it.
62
+
63
+ **IMPORTANT**: Use the safest pattern type possible:
64
+ 1. \`contains\` - Match if message contains specific tokens (SAFEST)
65
+ 2. \`startsWith\` - Match if message starts with prefix
66
+ 3. \`statusCode\` - Match HTTP status codes (e.g., "429", "503")
67
+ 4. \`regex\` - Only if the above won't work (AVOID if possible)
68
+
69
+ If a cluster is NOT transient (actual bugs, logic errors), mark confidence as 0.
70
+
71
+ ## Response Format (JSON only)
72
+ {
73
+ "patterns": [
74
+ {
75
+ "patternType": "contains",
76
+ "patternValue": "quota exceeded",
77
+ "category": "quota-exhausted",
78
+ "confidence": 0.9,
79
+ "reasoning": "Error mentions quota exceeded, typical API rate limit",
80
+ "positiveExamples": ["quota exceeded for API", "daily quota exceeded"],
81
+ "negativeExamples": ["quota configuration error"]
82
+ }
83
+ ],
84
+ "summary": "Found 2 transient patterns (quota, rate-limit), 1 cluster appears to be a real bug"
85
+ }
86
+
87
+ IMPORTANT: Your response must be valid JSON. Do not include any text outside the JSON object.`;
88
+ }
89
+
90
+ /**
91
+ * Validate AI response matches expected schema
92
+ */
93
+ function validateResponse(data: unknown): AISuggestionResponse | null {
94
+ if (!data || typeof data !== 'object') return null;
95
+
96
+ const obj = data as Record<string, unknown>;
97
+ if (!Array.isArray(obj.patterns)) return null;
98
+ if (typeof obj.summary !== 'string') return null;
99
+
100
+ const validTypes: PatternType[] = ['contains', 'startsWith', 'statusCode', 'regex'];
101
+
102
+ for (const pattern of obj.patterns) {
103
+ if (!pattern || typeof pattern !== 'object') return null;
104
+ const p = pattern as Record<string, unknown>;
105
+
106
+ if (!validTypes.includes(p.patternType as PatternType)) return null;
107
+ if (typeof p.patternValue !== 'string') return null;
108
+ if (typeof p.category !== 'string') return null;
109
+ if (typeof p.confidence !== 'number') return null;
110
+ if (typeof p.reasoning !== 'string') return null;
111
+ if (!Array.isArray(p.positiveExamples)) return null;
112
+ if (!Array.isArray(p.negativeExamples)) return null;
113
+ }
114
+
115
+ return obj as unknown as AISuggestionResponse;
116
+ }
117
+
118
+ /**
119
+ * Call DeepSeek to analyse clusters and suggest patterns
120
+ */
121
+ /** Static pattern evaluation request */
122
+ export interface StaticPatternInput {
123
+ pattern: string; // The regex pattern as a string
124
+ category: string;
125
+ index: number;
126
+ }
127
+
128
+ /** AI evaluation response for static patterns */
129
+ export interface StaticPatternEvaluation {
130
+ evaluations: Array<{
131
+ index: number;
132
+ category: string;
133
+ verdict: 'keep-static' | 'migrate-dynamic' | 'merge' | 'deprecate';
134
+ convertedType?: PatternType;
135
+ convertedValue?: string;
136
+ reasoning: string;
137
+ confidenceScore: number;
138
+ }>;
139
+ summary: string;
140
+ }
141
+
142
+ /**
143
+ * Build prompt for evaluating static patterns
144
+ */
145
+ function buildStaticEvaluationPrompt(patterns: StaticPatternInput[]): string {
146
+ const patternList = patterns
147
+ .map(
148
+ (p) => `${p.index}. Category: "${p.category}"
149
+ Regex: \`${p.pattern}\``
150
+ )
151
+ .join('\n\n');
152
+
153
+ return `You are evaluating HARDCODED transient error patterns to determine if they should be migrated to a dynamic pattern system.
154
+
155
+ ## Current Static Patterns
156
+ These patterns are compiled into production code. They detect transient errors (quota, rate limits, timeouts, etc.) that should NOT create duplicate GitHub issues.
157
+
158
+ ${patternList}
159
+
160
+ ## Your Task
161
+ For each pattern, evaluate whether it should:
162
+ 1. **keep-static** - Keep as hardcoded (core infrastructure patterns that rarely change)
163
+ 2. **migrate-dynamic** - Convert to dynamic DSL for better visibility/management
164
+ 3. **merge** - Can be merged with another pattern
165
+ 4. **deprecate** - Pattern is too broad, outdated, or problematic
166
+
167
+ If recommending migration, convert the regex to our safer DSL:
168
+ - \`contains\` - Match if message contains tokens (PREFERRED)
169
+ - \`startsWith\` - Match if message starts with prefix
170
+ - \`statusCode\` - Match HTTP status codes
171
+ - \`regex\` - Only if truly necessary
172
+
173
+ ## Response Format (JSON only)
174
+ {
175
+ "evaluations": [
176
+ {
177
+ "index": 1,
178
+ "category": "quota-exhausted",
179
+ "verdict": "migrate-dynamic",
180
+ "convertedType": "contains",
181
+ "convertedValue": "quota exceeded",
182
+ "reasoning": "Can be expressed safely with contains, benefits from visibility in dashboard",
183
+ "confidenceScore": 0.9
184
+ },
185
+ {
186
+ "index": 2,
187
+ "category": "rate-limited",
188
+ "verdict": "keep-static",
189
+ "reasoning": "Core infrastructure pattern, simple regex, low maintenance risk",
190
+ "confidenceScore": 0.85
191
+ }
192
+ ],
193
+ "summary": "Recommend migrating 5 patterns, keeping 10 static, merging 2, deprecating 1"
194
+ }
195
+
196
+ IMPORTANT: Respond with valid JSON only. No text outside the JSON object.`;
197
+ }
198
+
199
+ /**
200
+ * Validate static evaluation response
201
+ */
202
+ function validateStaticEvaluationResponse(data: unknown): StaticPatternEvaluation | null {
203
+ if (!data || typeof data !== 'object') return null;
204
+
205
+ const obj = data as Record<string, unknown>;
206
+ if (!Array.isArray(obj.evaluations)) return null;
207
+ if (typeof obj.summary !== 'string') return null;
208
+
209
+ const validVerdicts = ['keep-static', 'migrate-dynamic', 'merge', 'deprecate'];
210
+ const validTypes: PatternType[] = ['contains', 'startsWith', 'statusCode', 'regex'];
211
+
212
+ for (const evaluation of obj.evaluations) {
213
+ if (!evaluation || typeof evaluation !== 'object') return null;
214
+ const e = evaluation as Record<string, unknown>;
215
+
216
+ if (typeof e.index !== 'number') return null;
217
+ if (typeof e.category !== 'string') return null;
218
+ if (!validVerdicts.includes(e.verdict as string)) return null;
219
+ if (typeof e.reasoning !== 'string') return null;
220
+ if (typeof e.confidenceScore !== 'number') return null;
221
+
222
+ // If migrating, must have converted type/value
223
+ if (e.verdict === 'migrate-dynamic') {
224
+ if (!validTypes.includes(e.convertedType as PatternType)) return null;
225
+ if (typeof e.convertedValue !== 'string') return null;
226
+ }
227
+ }
228
+
229
+ return obj as unknown as StaticPatternEvaluation;
230
+ }
231
+
232
+ /**
233
+ * Call DeepSeek to evaluate static patterns for potential migration
234
+ */
235
+ export async function evaluateStaticPatterns(
236
+ patterns: StaticPatternInput[],
237
+ env: { CLOUDFLARE_ACCOUNT_ID: string; PLATFORM_AI_GATEWAY_KEY: string },
238
+ log: Logger
239
+ ): Promise<StaticPatternEvaluation | null> {
240
+ if (patterns.length === 0) {
241
+ log.info('No patterns to evaluate');
242
+ return { evaluations: [], summary: 'No patterns provided' };
243
+ }
244
+
245
+ const prompt = buildStaticEvaluationPrompt(patterns);
246
+
247
+ const controller = new AbortController();
248
+ const timeoutId = setTimeout(() => controller.abort(), AI_FETCH_TIMEOUT_MS);
249
+
250
+ try {
251
+ const response = await fetch(
252
+ `${DEEPSEEK_GATEWAY_URL}/${env.CLOUDFLARE_ACCOUNT_ID}/platform/deepseek/chat/completions`,
253
+ {
254
+ method: 'POST',
255
+ headers: {
256
+ 'cf-aig-authorization': `Bearer ${env.PLATFORM_AI_GATEWAY_KEY}`,
257
+ 'Content-Type': 'application/json',
258
+ },
259
+ body: JSON.stringify({
260
+ model: 'deepseek-chat',
261
+ messages: [
262
+ {
263
+ role: 'system',
264
+ content:
265
+ 'You are an expert at evaluating error patterns for production systems. Respond with valid JSON only.',
266
+ },
267
+ { role: 'user', content: prompt },
268
+ ],
269
+ temperature: 0.1,
270
+ max_tokens: 3000, // Larger for evaluating many patterns
271
+ response_format: { type: 'json_object' },
272
+ }),
273
+ signal: controller.signal,
274
+ }
275
+ );
276
+
277
+ if (!response.ok) {
278
+ const errorBody = await response.text().catch(() => 'Unknown error');
279
+ log.error('DeepSeek API error', new Error(`HTTP ${response.status}`), {
280
+ status: response.status,
281
+ errorBody: errorBody.slice(0, 500),
282
+ });
283
+ return null;
284
+ }
285
+
286
+ const data = (await response.json()) as {
287
+ choices?: Array<{ message?: { content?: string } }>;
288
+ };
289
+
290
+ const content = data.choices?.[0]?.message?.content;
291
+ if (!content) {
292
+ log.error('Empty response from DeepSeek');
293
+ return null;
294
+ }
295
+
296
+ // Parse JSON response
297
+ let parsed: unknown;
298
+ try {
299
+ parsed = JSON.parse(content);
300
+ } catch {
301
+ log.error('Invalid JSON from DeepSeek', new Error('Parse failed'), {
302
+ content: content.slice(0, 500),
303
+ });
304
+ return null;
305
+ }
306
+
307
+ // Validate response structure
308
+ const validated = validateStaticEvaluationResponse(parsed);
309
+ if (!validated) {
310
+ log.error('Response failed validation', new Error('Schema mismatch'), {
311
+ parsed: JSON.stringify(parsed).slice(0, 500),
312
+ });
313
+ return null;
314
+ }
315
+
316
+ log.info('Static pattern evaluation complete', {
317
+ patternsEvaluated: validated.evaluations.length,
318
+ summary: validated.summary.slice(0, 100),
319
+ });
320
+
321
+ return validated;
322
+ } catch (error) {
323
+ log.error('DeepSeek request failed', error);
324
+ return null;
325
+ } finally {
326
+ clearTimeout(timeoutId);
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Call DeepSeek to analyse clusters and suggest patterns
332
+ */
333
+ export async function suggestPatterns(
334
+ clusters: ErrorCluster[],
335
+ sampleMessages: Map<string, string[]>,
336
+ env: { CLOUDFLARE_ACCOUNT_ID: string; PLATFORM_AI_GATEWAY_KEY: string },
337
+ log: Logger
338
+ ): Promise<AISuggestionResponse | null> {
339
+ if (clusters.length === 0) {
340
+ log.info('No clusters to analyse');
341
+ return { patterns: [], summary: 'No clusters provided' };
342
+ }
343
+
344
+ const prompt = buildPrompt(clusters, sampleMessages);
345
+
346
+ const controller = new AbortController();
347
+ const timeoutId = setTimeout(() => controller.abort(), AI_FETCH_TIMEOUT_MS);
348
+
349
+ try {
350
+ const response = await fetch(
351
+ `${DEEPSEEK_GATEWAY_URL}/${env.CLOUDFLARE_ACCOUNT_ID}/platform/deepseek/chat/completions`,
352
+ {
353
+ method: 'POST',
354
+ headers: {
355
+ 'cf-aig-authorization': `Bearer ${env.PLATFORM_AI_GATEWAY_KEY}`,
356
+ 'Content-Type': 'application/json',
357
+ },
358
+ body: JSON.stringify({
359
+ model: 'deepseek-chat',
360
+ messages: [
361
+ {
362
+ role: 'system',
363
+ content:
364
+ 'You are an expert at identifying transient vs permanent errors in production systems. Respond with valid JSON only.',
365
+ },
366
+ { role: 'user', content: prompt },
367
+ ],
368
+ temperature: 0.1,
369
+ max_tokens: MAX_TOKENS,
370
+ response_format: { type: 'json_object' },
371
+ }),
372
+ signal: controller.signal,
373
+ }
374
+ );
375
+
376
+ if (!response.ok) {
377
+ const errorBody = await response.text().catch(() => 'Unknown error');
378
+ log.error('DeepSeek API error', new Error(`HTTP ${response.status}`), {
379
+ status: response.status,
380
+ errorBody: errorBody.slice(0, 500),
381
+ });
382
+ return null;
383
+ }
384
+
385
+ const data = (await response.json()) as {
386
+ choices?: Array<{ message?: { content?: string } }>;
387
+ };
388
+
389
+ const content = data.choices?.[0]?.message?.content;
390
+ if (!content) {
391
+ log.error('Empty response from DeepSeek');
392
+ return null;
393
+ }
394
+
395
+ // Parse JSON response
396
+ let parsed: unknown;
397
+ try {
398
+ parsed = JSON.parse(content);
399
+ } catch {
400
+ log.error('Invalid JSON from DeepSeek', new Error('Parse failed'), {
401
+ content: content.slice(0, 500),
402
+ });
403
+ return null;
404
+ }
405
+
406
+ // Validate response structure
407
+ const validated = validateResponse(parsed);
408
+ if (!validated) {
409
+ log.error('Response failed validation', new Error('Schema mismatch'), {
410
+ parsed: JSON.stringify(parsed).slice(0, 500),
411
+ });
412
+ return null;
413
+ }
414
+
415
+ log.info('DeepSeek analysis complete', {
416
+ patternsFound: validated.patterns.length,
417
+ summary: validated.summary.slice(0, 100),
418
+ });
419
+
420
+ return validated;
421
+ } catch (error) {
422
+ log.error('DeepSeek request failed', error);
423
+ return null;
424
+ } finally {
425
+ clearTimeout(timeoutId);
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Review context explainer response from AI
431
+ */
432
+ export interface PatternReviewExplainer {
433
+ whatItCatches: string;
434
+ whyTransient: string;
435
+ affectedAreas: string;
436
+ recommendation: 'likely-approve' | 'needs-investigation' | 'likely-reject';
437
+ concerns: string[];
438
+ summary: string;
439
+ }
440
+
441
+ /**
442
+ * Build prompt for generating pattern review context
443
+ */
444
+ function buildReviewContextPrompt(
445
+ pattern: {
446
+ patternType: string;
447
+ patternValue: string;
448
+ category: string;
449
+ confidenceScore: number;
450
+ aiReasoning?: string;
451
+ },
452
+ evidence: AggregatedPatternEvidence
453
+ ): string {
454
+ const projectsList = Object.entries(evidence.matchesByProject)
455
+ .map(([project, count]) => `- ${project}: ${count} matches`)
456
+ .join('\n');
457
+
458
+ const scriptsList = Object.entries(evidence.matchesByScript)
459
+ .map(([script, count]) => `- ${script}: ${count} matches`)
460
+ .join('\n');
461
+
462
+ const samplesList = evidence.sampleMessages.slice(0, 5)
463
+ .map((msg, i) => `${i + 1}. "${msg}"`)
464
+ .join('\n');
465
+
466
+ return `You are helping a platform admin review a transient error pattern for approval.
467
+
468
+ ## Pattern Details
469
+ - **Type**: ${pattern.patternType}
470
+ - **Value**: "${pattern.patternValue}"
471
+ - **Category**: ${pattern.category}
472
+ - **Initial AI Confidence**: ${Math.round(pattern.confidenceScore * 100)}%
473
+ ${pattern.aiReasoning ? `- **Original Reasoning**: ${pattern.aiReasoning}` : ''}
474
+
475
+ ## Match Evidence (collected over shadow period)
476
+ - **Total Matches**: ${evidence.totalMatches}
477
+ - **Distinct Days**: ${evidence.distinctDays}
478
+ - **First Match**: ${evidence.firstMatchAt ? new Date(evidence.firstMatchAt * 1000).toISOString() : 'N/A'}
479
+ - **Last Match**: ${evidence.lastMatchAt ? new Date(evidence.lastMatchAt * 1000).toISOString() : 'N/A'}
480
+
481
+ ### Matches by Project
482
+ ${projectsList || 'None recorded'}
483
+
484
+ ### Matches by Worker Script
485
+ ${scriptsList || 'None recorded'}
486
+
487
+ ### Sample Error Messages
488
+ ${samplesList || 'None available'}
489
+
490
+ ## Your Task
491
+ Generate a review context to help the admin decide whether to approve this pattern.
492
+
493
+ Consider:
494
+ 1. Does this pattern clearly catch transient errors (quota, rate limits, timeouts)?
495
+ 2. Is there a risk of over-matching (catching real bugs as transient)?
496
+ 3. Is there a risk of under-matching (missing similar errors)?
497
+ 4. Which projects/workers are most affected?
498
+
499
+ ## Response Format (JSON only)
500
+ {
501
+ "whatItCatches": "Brief 1-2 sentence description of what errors this pattern catches",
502
+ "whyTransient": "Brief explanation of why these errors are transient and self-resolving",
503
+ "affectedAreas": "Summary of which projects and workers are most affected",
504
+ "recommendation": "likely-approve" | "needs-investigation" | "likely-reject",
505
+ "concerns": ["List of any concerns about approving this pattern"],
506
+ "summary": "One paragraph summary for the admin dashboard"
507
+ }
508
+
509
+ IMPORTANT: Respond with valid JSON only.`;
510
+ }
511
+
512
+ /**
513
+ * Validate review context response
514
+ */
515
+ function validateReviewContextResponse(data: unknown): PatternReviewExplainer | null {
516
+ if (!data || typeof data !== 'object') return null;
517
+
518
+ const obj = data as Record<string, unknown>;
519
+ if (typeof obj.whatItCatches !== 'string') return null;
520
+ if (typeof obj.whyTransient !== 'string') return null;
521
+ if (typeof obj.affectedAreas !== 'string') return null;
522
+ if (!['likely-approve', 'needs-investigation', 'likely-reject'].includes(obj.recommendation as string)) return null;
523
+ if (!Array.isArray(obj.concerns)) return null;
524
+ if (typeof obj.summary !== 'string') return null;
525
+
526
+ return obj as unknown as PatternReviewExplainer;
527
+ }
528
+
529
+ /**
530
+ * Generate AI review context for a pattern ready for human review
531
+ */
532
+ export async function generateReviewContext(
533
+ pattern: {
534
+ patternType: string;
535
+ patternValue: string;
536
+ category: string;
537
+ confidenceScore: number;
538
+ aiReasoning?: string;
539
+ },
540
+ evidence: AggregatedPatternEvidence,
541
+ env: { CLOUDFLARE_ACCOUNT_ID: string; PLATFORM_AI_GATEWAY_KEY: string },
542
+ log: Logger
543
+ ): Promise<PatternReviewExplainer | null> {
544
+ // If no matches, generate a simple context without AI
545
+ if (evidence.totalMatches === 0) {
546
+ return {
547
+ whatItCatches: `Matches ${pattern.patternType} patterns containing "${pattern.patternValue}"`,
548
+ whyTransient: 'No real-world matches recorded during shadow period',
549
+ affectedAreas: 'No data available',
550
+ recommendation: 'needs-investigation',
551
+ concerns: ['No matches recorded - pattern may be too specific or not yet triggered'],
552
+ summary: 'This pattern has not matched any errors during the shadow evaluation period. Consider extending observation or reviewing the pattern logic.',
553
+ };
554
+ }
555
+
556
+ const prompt = buildReviewContextPrompt(pattern, evidence);
557
+
558
+ const controller = new AbortController();
559
+ const timeoutId = setTimeout(() => controller.abort(), AI_FETCH_TIMEOUT_MS);
560
+
561
+ try {
562
+ const response = await fetch(
563
+ `${DEEPSEEK_GATEWAY_URL}/${env.CLOUDFLARE_ACCOUNT_ID}/platform/deepseek/chat/completions`,
564
+ {
565
+ method: 'POST',
566
+ headers: {
567
+ 'cf-aig-authorization': `Bearer ${env.PLATFORM_AI_GATEWAY_KEY}`,
568
+ 'Content-Type': 'application/json',
569
+ },
570
+ body: JSON.stringify({
571
+ model: 'deepseek-chat',
572
+ messages: [
573
+ {
574
+ role: 'system',
575
+ content: 'You are an expert at evaluating transient error patterns for production systems. Help the admin understand this pattern clearly and concisely. Respond with valid JSON only.',
576
+ },
577
+ { role: 'user', content: prompt },
578
+ ],
579
+ temperature: 0.2,
580
+ max_tokens: 800,
581
+ response_format: { type: 'json_object' },
582
+ }),
583
+ signal: controller.signal,
584
+ }
585
+ );
586
+
587
+ if (!response.ok) {
588
+ const errorBody = await response.text().catch(() => 'Unknown error');
589
+ log.error('DeepSeek API error (review context)', new Error(`HTTP ${response.status}`), {
590
+ status: response.status,
591
+ errorBody: errorBody.slice(0, 500),
592
+ });
593
+ // Return a fallback context
594
+ return {
595
+ whatItCatches: `Matches "${pattern.patternValue}" errors in the ${pattern.category} category`,
596
+ whyTransient: 'AI analysis unavailable - review manually',
597
+ affectedAreas: `${Object.keys(evidence.matchesByProject).length} projects, ${Object.keys(evidence.matchesByScript).length} workers`,
598
+ recommendation: 'needs-investigation',
599
+ concerns: ['AI context generation failed - manual review recommended'],
600
+ summary: `Pattern matched ${evidence.totalMatches} times across ${evidence.distinctDays} days. Please review evidence manually.`,
601
+ };
602
+ }
603
+
604
+ const data = (await response.json()) as {
605
+ choices?: Array<{ message?: { content?: string } }>;
606
+ };
607
+
608
+ const content = data.choices?.[0]?.message?.content;
609
+ if (!content) {
610
+ log.error('Empty response from DeepSeek (review context)');
611
+ return null;
612
+ }
613
+
614
+ let parsed: unknown;
615
+ try {
616
+ parsed = JSON.parse(content);
617
+ } catch {
618
+ log.error('Invalid JSON from DeepSeek (review context)', new Error('Parse failed'), {
619
+ content: content.slice(0, 500),
620
+ });
621
+ return null;
622
+ }
623
+
624
+ const validated = validateReviewContextResponse(parsed);
625
+ if (!validated) {
626
+ log.error('Review context response failed validation', new Error('Schema mismatch'), {
627
+ parsed: JSON.stringify(parsed).slice(0, 500),
628
+ });
629
+ return null;
630
+ }
631
+
632
+ log.info('Generated review context', {
633
+ patternValue: pattern.patternValue,
634
+ recommendation: validated.recommendation,
635
+ });
636
+
637
+ return validated;
638
+ } catch (error) {
639
+ log.error('Failed to generate review context', error);
640
+ return null;
641
+ } finally {
642
+ clearTimeout(timeoutId);
643
+ }
644
+ }