@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,278 @@
1
+ /**
2
+ * Error Clustering for Pattern Discovery
3
+ *
4
+ * Groups similar unclassified errors to reduce AI API costs
5
+ * and improve suggestion quality.
6
+ *
7
+ * @module workers/lib/pattern-discovery/clustering
8
+ */
9
+
10
+ import type { D1Database } from '@cloudflare/workers-types';
11
+ import type { UnclassifiedError, ErrorCluster } from './types';
12
+ import type { Logger } from '@littlebearapps/platform-sdk';
13
+
14
+ /** Minimum occurrences to consider for clustering */
15
+ const MIN_OCCURRENCE_COUNT = 3;
16
+
17
+ /** Maximum clusters to process per run */
18
+ const MAX_CLUSTERS_PER_RUN = 20;
19
+
20
+ /** Maximum samples per cluster to send to AI */
21
+ export const MAX_SAMPLES_PER_CLUSTER = 5;
22
+
23
+ /**
24
+ * Query unclassified errors from D1
25
+ * Returns errors without a category that have occurred multiple times
26
+ *
27
+ * Uses COALESCE to check both last_exception_message and normalized_message,
28
+ * allowing pattern discovery to work for:
29
+ * - Exceptions (have last_exception_message)
30
+ * - Soft errors (have normalized_message from console.error logs)
31
+ * - Workflow failures (have normalized_message from logs)
32
+ */
33
+ export async function queryUnclassifiedErrors(
34
+ db: D1Database,
35
+ log: Logger
36
+ ): Promise<UnclassifiedError[]> {
37
+ try {
38
+ // Query high-frequency errors that haven't been resolved
39
+ // Uses COALESCE to get message from either exception or normalized logs
40
+ // Errors with transient categories (tracked in fingerprint) are already handled
41
+ const result = await db
42
+ .prepare(
43
+ `
44
+ SELECT
45
+ fingerprint,
46
+ script_name as scriptName,
47
+ COALESCE(last_exception_message, normalized_message, '') as normalizedMessage,
48
+ occurrence_count as occurrenceCount,
49
+ last_seen_at as lastSeenAt
50
+ FROM error_occurrences
51
+ WHERE status = 'open'
52
+ AND occurrence_count >= ?
53
+ AND error_category IS NULL
54
+ AND (
55
+ (last_exception_message IS NOT NULL AND last_exception_message != '')
56
+ OR (normalized_message IS NOT NULL AND normalized_message != '')
57
+ )
58
+ ORDER BY occurrence_count DESC
59
+ LIMIT 500
60
+ `
61
+ )
62
+ .bind(MIN_OCCURRENCE_COUNT)
63
+ .all<UnclassifiedError>();
64
+
65
+ log.info('Queried unclassified errors', {
66
+ count: result.results.length,
67
+ minOccurrences: MIN_OCCURRENCE_COUNT,
68
+ });
69
+
70
+ return result.results;
71
+ } catch (error) {
72
+ log.error('Failed to query unclassified errors', error);
73
+ return [];
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Simple hash function for clustering
79
+ * Normalizes message and creates a stable hash for grouping
80
+ */
81
+ function hashMessage(message: string): string {
82
+ // Further normalize: lowercase, collapse whitespace, remove numbers
83
+ const normalized = message
84
+ .toLowerCase()
85
+ .replace(/\s+/g, ' ')
86
+ .replace(/\d+/g, 'N')
87
+ .replace(/[a-f0-9]{8,}/gi, 'HASH') // Remove hex strings
88
+ .trim();
89
+
90
+ // Simple string hash
91
+ let hash = 0;
92
+ for (let i = 0; i < normalized.length; i++) {
93
+ const char = normalized.charCodeAt(i);
94
+ hash = (hash << 5) - hash + char;
95
+ hash = hash & hash; // Convert to 32bit integer
96
+ }
97
+ return hash.toString(16);
98
+ }
99
+
100
+ /**
101
+ * Cluster errors by normalized message similarity
102
+ * Uses exact match on further-normalized messages as first pass
103
+ */
104
+ export function clusterErrors(errors: UnclassifiedError[]): Map<string, UnclassifiedError[]> {
105
+ const clusters = new Map<string, UnclassifiedError[]>();
106
+
107
+ for (const error of errors) {
108
+ const hash = hashMessage(error.normalizedMessage);
109
+ const existing = clusters.get(hash) || [];
110
+ existing.push(error);
111
+ clusters.set(hash, existing);
112
+ }
113
+
114
+ return clusters;
115
+ }
116
+
117
+ /**
118
+ * Convert clustered errors to ErrorCluster objects
119
+ * Filters to clusters with sufficient volume
120
+ */
121
+ export function buildClusterObjects(
122
+ clusteredErrors: Map<string, UnclassifiedError[]>
123
+ ): ErrorCluster[] {
124
+ const clusters: ErrorCluster[] = [];
125
+
126
+ for (const [hash, errors] of clusteredErrors) {
127
+ // Sum occurrences across all errors in cluster
128
+ const totalOccurrences = errors.reduce((sum, e) => sum + e.occurrenceCount, 0);
129
+
130
+ // Skip small clusters
131
+ if (totalOccurrences < MIN_OCCURRENCE_COUNT * 2) {
132
+ continue;
133
+ }
134
+
135
+ // Find representative message (most common or first)
136
+ const representative = errors.reduce((best, e) =>
137
+ e.occurrenceCount > best.occurrenceCount ? e : best
138
+ );
139
+
140
+ // Collect unique scripts
141
+ const scripts = [...new Set(errors.map((e) => e.scriptName))];
142
+
143
+ // Find time range
144
+ const firstSeen = Math.min(...errors.map((e) => e.lastSeenAt));
145
+ const lastSeen = Math.max(...errors.map((e) => e.lastSeenAt));
146
+
147
+ clusters.push({
148
+ id: `cluster-${hash}-${Date.now()}`,
149
+ clusterHash: hash,
150
+ representativeMessage: representative.normalizedMessage,
151
+ occurrenceCount: totalOccurrences,
152
+ uniqueFingerprints: errors.length,
153
+ firstSeenAt: firstSeen,
154
+ lastSeenAt: lastSeen,
155
+ scripts,
156
+ status: 'pending',
157
+ suggestionId: null,
158
+ });
159
+ }
160
+
161
+ // Sort by occurrence count descending
162
+ clusters.sort((a, b) => b.occurrenceCount - a.occurrenceCount);
163
+
164
+ // Limit to max clusters per run
165
+ return clusters.slice(0, MAX_CLUSTERS_PER_RUN);
166
+ }
167
+
168
+ /**
169
+ * Get sample messages for a cluster
170
+ * Returns diverse samples for AI analysis
171
+ */
172
+ export function getSampleMessages(
173
+ errors: UnclassifiedError[],
174
+ maxSamples: number = MAX_SAMPLES_PER_CLUSTER
175
+ ): string[] {
176
+ // Get unique messages (some may be duplicates with different fingerprints)
177
+ const uniqueMessages = [...new Set(errors.map((e) => e.normalizedMessage))];
178
+
179
+ // Return up to maxSamples
180
+ return uniqueMessages.slice(0, maxSamples);
181
+ }
182
+
183
+ /**
184
+ * Store clusters in D1 for tracking
185
+ */
186
+ export async function storeClusters(
187
+ db: D1Database,
188
+ clusters: ErrorCluster[],
189
+ log: Logger
190
+ ): Promise<void> {
191
+ for (const cluster of clusters) {
192
+ try {
193
+ await db
194
+ .prepare(
195
+ `
196
+ INSERT INTO error_clusters (
197
+ id, cluster_hash, representative_message,
198
+ occurrence_count, unique_fingerprints,
199
+ first_seen_at, last_seen_at, scripts, status
200
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
201
+ ON CONFLICT (cluster_hash) DO UPDATE SET
202
+ occurrence_count = excluded.occurrence_count,
203
+ unique_fingerprints = excluded.unique_fingerprints,
204
+ last_seen_at = excluded.last_seen_at,
205
+ scripts = excluded.scripts,
206
+ updated_at = unixepoch()
207
+ `
208
+ )
209
+ .bind(
210
+ cluster.id,
211
+ cluster.clusterHash,
212
+ cluster.representativeMessage.slice(0, 500),
213
+ cluster.occurrenceCount,
214
+ cluster.uniqueFingerprints,
215
+ cluster.firstSeenAt,
216
+ cluster.lastSeenAt,
217
+ JSON.stringify(cluster.scripts),
218
+ cluster.status
219
+ )
220
+ .run();
221
+ } catch (error) {
222
+ log.warn('Failed to store cluster', error, { clusterId: cluster.id });
223
+ }
224
+ }
225
+
226
+ log.info('Stored clusters', { count: clusters.length });
227
+ }
228
+
229
+ /**
230
+ * Get pending clusters for AI analysis
231
+ */
232
+ export async function getPendingClusters(
233
+ db: D1Database,
234
+ limit: number = 10
235
+ ): Promise<ErrorCluster[]> {
236
+ const result = await db
237
+ .prepare(
238
+ `
239
+ SELECT
240
+ id, cluster_hash as clusterHash, representative_message as representativeMessage,
241
+ occurrence_count as occurrenceCount, unique_fingerprints as uniqueFingerprints,
242
+ first_seen_at as firstSeenAt, last_seen_at as lastSeenAt,
243
+ scripts, status, suggestion_id as suggestionId
244
+ FROM error_clusters
245
+ WHERE status = 'pending'
246
+ ORDER BY occurrence_count DESC
247
+ LIMIT ?
248
+ `
249
+ )
250
+ .bind(limit)
251
+ .all<ErrorCluster & { scripts: string }>();
252
+
253
+ return result.results.map((r) => ({
254
+ ...r,
255
+ scripts: JSON.parse(r.scripts || '[]'),
256
+ }));
257
+ }
258
+
259
+ /**
260
+ * Update cluster status
261
+ */
262
+ export async function updateClusterStatus(
263
+ db: D1Database,
264
+ clusterId: string,
265
+ status: ErrorCluster['status'],
266
+ suggestionId?: string
267
+ ): Promise<void> {
268
+ await db
269
+ .prepare(
270
+ `
271
+ UPDATE error_clusters
272
+ SET status = ?, suggestion_id = ?, updated_at = unixepoch()
273
+ WHERE id = ?
274
+ `
275
+ )
276
+ .bind(status, suggestionId || null, clusterId)
277
+ .run();
278
+ }