@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,292 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ /**
4
+ * PID Controller for Intelligent Degradation
5
+ *
6
+ * Provides smooth throttle rate calculation (0.0-1.0) instead of binary ON/OFF.
7
+ * State is stored in KV, making the controller stateless per invocation.
8
+ *
9
+ * Key principle: PID provides smooth degradation, circuit breakers remain the emergency stop.
10
+ */
11
+
12
+ // =============================================================================
13
+ // TYPES
14
+ // =============================================================================
15
+
16
+ /**
17
+ * PID controller gains and configuration.
18
+ * Tuned for budget-based throttling where:
19
+ * - setpoint (0.70) = target 70% budget utilisation
20
+ * - Output range [0, 1] = throttle rate (0=no throttle, 1=full throttle)
21
+ */
22
+ export interface PIDConfig {
23
+ /** Proportional gain - responds to current error */
24
+ kp: number;
25
+ /** Integral gain - responds to accumulated error */
26
+ ki: number;
27
+ /** Derivative gain - responds to rate of change */
28
+ kd: number;
29
+ /** Target budget utilisation (0.0-1.0) */
30
+ setpoint: number;
31
+ /** Minimum output value (default: 0) */
32
+ outputMin: number;
33
+ /** Maximum output value (default: 1) */
34
+ outputMax: number;
35
+ /** Maximum integral accumulation to prevent windup */
36
+ integralMax: number;
37
+ }
38
+
39
+ /**
40
+ * Persisted PID state stored in KV.
41
+ * Key: STATE:PID:{feature_id}
42
+ */
43
+ export interface PIDState {
44
+ /** Accumulated error (integral term) */
45
+ integral: number;
46
+ /** Previous error for derivative calculation */
47
+ prevError: number;
48
+ /** Last update timestamp (ms) */
49
+ lastUpdate: number;
50
+ /** Current throttle rate output */
51
+ throttleRate: number;
52
+ }
53
+
54
+ /**
55
+ * Input for PID computation.
56
+ */
57
+ export interface PIDInput {
58
+ /** Current budget utilisation (0.0-1.0+, can exceed 1.0 if over budget) */
59
+ currentUsage: number;
60
+ /** Time since last update in milliseconds */
61
+ deltaTimeMs: number;
62
+ }
63
+
64
+ /**
65
+ * Output from PID computation.
66
+ */
67
+ export interface PIDOutput {
68
+ /** Computed throttle rate (0.0-1.0) */
69
+ throttleRate: number;
70
+ /** Updated state to persist */
71
+ newState: PIDState;
72
+ /** Debug info for monitoring */
73
+ debug: {
74
+ error: number;
75
+ pTerm: number;
76
+ iTerm: number;
77
+ dTerm: number;
78
+ };
79
+ }
80
+
81
+ // =============================================================================
82
+ // DEFAULT CONFIGURATION
83
+ // =============================================================================
84
+
85
+ /**
86
+ * Default PID configuration tuned for budget-based throttling.
87
+ *
88
+ * Rationale:
89
+ * - kp=0.5: Moderate response to current error (50% of error -> throttle change)
90
+ * - ki=0.1: Slow integral to avoid oscillation, correct steady-state error
91
+ * - kd=0.05: Light derivative to dampen sudden changes
92
+ * - setpoint=0.70: Target 70% budget utilisation, leaving 30% headroom
93
+ * - integralMax=2.0: Prevent integral windup during sustained over-budget
94
+ */
95
+ export const DEFAULT_PID_CONFIG: PIDConfig = {
96
+ kp: 0.5,
97
+ ki: 0.1,
98
+ kd: 0.05,
99
+ setpoint: 0.7,
100
+ outputMin: 0,
101
+ outputMax: 1,
102
+ integralMax: 2.0,
103
+ };
104
+
105
+ /**
106
+ * Create a fresh PID state (for new features or reset).
107
+ */
108
+ export function createPIDState(): PIDState {
109
+ return {
110
+ integral: 0,
111
+ prevError: 0,
112
+ lastUpdate: Date.now(),
113
+ throttleRate: 0,
114
+ };
115
+ }
116
+
117
+ // =============================================================================
118
+ // PID COMPUTATION
119
+ // =============================================================================
120
+
121
+ /**
122
+ * Compute PID output for throttle rate.
123
+ *
124
+ * This is a stateless function - pass in current state, get new state back.
125
+ * The caller is responsible for persisting state to KV.
126
+ *
127
+ * @param state - Current PID state from KV (or fresh state for new features)
128
+ * @param input - Current usage and timing information
129
+ * @param config - PID configuration (defaults provided)
130
+ * @returns New throttle rate and updated state
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * const state = await getPIDState(featureId, env);
135
+ * const input = { currentUsage: 0.85, deltaTimeMs: 60000 };
136
+ * const output = computePID(state, input);
137
+ * await savePIDState(featureId, output.newState, env);
138
+ * ```
139
+ */
140
+ export function computePID(
141
+ state: PIDState,
142
+ input: PIDInput,
143
+ config: PIDConfig = DEFAULT_PID_CONFIG
144
+ ): PIDOutput {
145
+ // Calculate error: positive = over budget, negative = under budget
146
+ // error > 0 means we need MORE throttling
147
+ const error = input.currentUsage - config.setpoint;
148
+
149
+ // Convert deltaTime to seconds for consistent gains regardless of update frequency
150
+ const dt = Math.max(input.deltaTimeMs / 1000, 0.001); // Minimum 1ms to avoid division issues
151
+
152
+ // Proportional term: immediate response to current error
153
+ const pTerm = config.kp * error;
154
+
155
+ // Integral term: accumulated error over time (with anti-windup)
156
+ let newIntegral = state.integral + error * dt;
157
+ // Clamp integral to prevent windup
158
+ newIntegral = Math.max(-config.integralMax, Math.min(config.integralMax, newIntegral));
159
+ const iTerm = config.ki * newIntegral;
160
+
161
+ // Derivative term: rate of change of error (damping)
162
+ const derivative = (error - state.prevError) / dt;
163
+ const dTerm = config.kd * derivative;
164
+
165
+ // Sum all terms
166
+ let output = pTerm + iTerm + dTerm;
167
+
168
+ // Clamp output to valid range
169
+ output = Math.max(config.outputMin, Math.min(config.outputMax, output));
170
+
171
+ // Create new state
172
+ const newState: PIDState = {
173
+ integral: newIntegral,
174
+ prevError: error,
175
+ lastUpdate: Date.now(),
176
+ throttleRate: output,
177
+ };
178
+
179
+ return {
180
+ throttleRate: output,
181
+ newState,
182
+ debug: {
183
+ error,
184
+ pTerm,
185
+ iTerm,
186
+ dTerm,
187
+ },
188
+ };
189
+ }
190
+
191
+ // =============================================================================
192
+ // KV PERSISTENCE HELPERS
193
+ // =============================================================================
194
+
195
+ /**
196
+ * KV key for PID state.
197
+ */
198
+ export function pidStateKey(featureId: string): string {
199
+ return `STATE:PID:${featureId}`;
200
+ }
201
+
202
+ /**
203
+ * KV key for throttle rate (read by SDK).
204
+ */
205
+ export function throttleRateKey(featureId: string): string {
206
+ return `CONFIG:FEATURE:${featureId}:THROTTLE_RATE`;
207
+ }
208
+
209
+ /**
210
+ * Get PID state from KV, returning fresh state if not found.
211
+ */
212
+ export async function getPIDState(featureId: string, kv: KVNamespace): Promise<PIDState> {
213
+ const key = pidStateKey(featureId);
214
+ const data = await kv.get(key, 'json');
215
+ if (data && typeof data === 'object') {
216
+ return data as PIDState;
217
+ }
218
+ return createPIDState();
219
+ }
220
+
221
+ /**
222
+ * Save PID state to KV with 24h TTL.
223
+ * Also writes throttle rate to separate key with 5min TTL for SDK consumption.
224
+ */
225
+ export async function savePIDState(
226
+ featureId: string,
227
+ state: PIDState,
228
+ kv: KVNamespace
229
+ ): Promise<void> {
230
+ const stateKey = pidStateKey(featureId);
231
+ const rateKey = throttleRateKey(featureId);
232
+
233
+ // Save state with 24h TTL (for persistence across updates)
234
+ await kv.put(stateKey, JSON.stringify(state), { expirationTtl: 86400 });
235
+
236
+ // Save throttle rate separately with 5min TTL (for SDK quick access)
237
+ // Only write if throttle rate > 0 to avoid unnecessary KV writes
238
+ if (state.throttleRate > 0.001) {
239
+ await kv.put(rateKey, state.throttleRate.toString(), { expirationTtl: 300 });
240
+ } else {
241
+ // Delete the key if throttle rate is essentially 0
242
+ await kv.delete(rateKey);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Get current throttle rate for SDK consumption.
248
+ * Returns 0 if no throttle rate is set.
249
+ */
250
+ export async function getThrottleRate(featureId: string, kv: KVNamespace): Promise<number> {
251
+ const key = throttleRateKey(featureId);
252
+ const value = await kv.get(key);
253
+ if (value) {
254
+ const rate = parseFloat(value);
255
+ return isNaN(rate) ? 0 : Math.max(0, Math.min(1, rate));
256
+ }
257
+ return 0;
258
+ }
259
+
260
+ // =============================================================================
261
+ // UTILITY FUNCTIONS
262
+ // =============================================================================
263
+
264
+ /**
265
+ * Calculate budget utilisation from current usage and limit.
266
+ *
267
+ * @param currentUsage - Current period usage value
268
+ * @param budgetLimit - Budget limit for the period
269
+ * @returns Utilisation ratio (0.0-1.0+, can exceed 1.0 if over budget)
270
+ */
271
+ export function calculateUtilisation(currentUsage: number, budgetLimit: number): number {
272
+ if (budgetLimit <= 0) return 0;
273
+ return currentUsage / budgetLimit;
274
+ }
275
+
276
+ /**
277
+ * Determine if PID update is needed based on time since last update.
278
+ *
279
+ * @param lastUpdate - Timestamp of last PID update (ms)
280
+ * @param minIntervalMs - Minimum interval between updates (default: 60s)
281
+ * @returns True if update is due
282
+ */
283
+ export function shouldUpdatePID(lastUpdate: number, minIntervalMs: number = 60_000): boolean {
284
+ return Date.now() - lastUpdate >= minIntervalMs;
285
+ }
286
+
287
+ /**
288
+ * Format throttle rate for logging (percentage with 1 decimal).
289
+ */
290
+ export function formatThrottleRate(rate: number): string {
291
+ return `${(rate * 100).toFixed(1)}%`;
292
+ }
@@ -0,0 +1,368 @@
1
+ /**
2
+ * BCU (Budget Consumption Unit) Cost Allocator
3
+ *
4
+ * Provides scarcity-weighted quota enforcement for intelligent degradation.
5
+ * BCU normalises different resource types into a single unit for budget comparison.
6
+ *
7
+ * Key difference from costs.ts:
8
+ * - costs.ts: Financial reporting (actual USD costs)
9
+ * - economics.ts: Scarcity-weighted quota enforcement (relative resource pressure)
10
+ *
11
+ * BCU weights reflect scarcity and impact, not just cost:
12
+ * - AI neurons are expensive AND scarce (weight: 100)
13
+ * - D1 writes have durability implications (weight: 10)
14
+ * - KV writes are moderately constrained (weight: 1)
15
+ * - Requests are abundant but need tracking (weight: 0.001)
16
+ */
17
+
18
+ import type { FeatureMetrics } from '@littlebearapps/platform-sdk';
19
+
20
+ // =============================================================================
21
+ // TYPES
22
+ // =============================================================================
23
+
24
+ /**
25
+ * Resource type for BCU calculation.
26
+ */
27
+ export type ResourceType =
28
+ | 'aiNeurons'
29
+ | 'aiRequests'
30
+ | 'd1Writes'
31
+ | 'd1Reads'
32
+ | 'd1RowsWritten'
33
+ | 'd1RowsRead'
34
+ | 'kvWrites'
35
+ | 'kvReads'
36
+ | 'kvDeletes'
37
+ | 'kvLists'
38
+ | 'r2ClassA'
39
+ | 'r2ClassB'
40
+ | 'doRequests'
41
+ | 'doGbSeconds'
42
+ | 'queueMessages'
43
+ | 'vectorizeQueries'
44
+ | 'vectorizeInserts'
45
+ | 'workflowInvocations'
46
+ | 'requests'
47
+ | 'cpuMs';
48
+
49
+ /**
50
+ * BCU weights per resource type.
51
+ */
52
+ export type BCUWeights = Record<ResourceType, number>;
53
+
54
+ /**
55
+ * Result of BCU calculation.
56
+ */
57
+ export interface BCUResult {
58
+ /** Total BCU value */
59
+ total: number;
60
+ /** Breakdown by resource type */
61
+ breakdown: Partial<Record<ResourceType, number>>;
62
+ /** Dominant resource (highest BCU contribution) */
63
+ dominantResource: ResourceType | null;
64
+ /** Percentage contribution of dominant resource */
65
+ dominantPercentage: number;
66
+ }
67
+
68
+ /**
69
+ * Budget state with BCU tracking.
70
+ */
71
+ export interface BCUBudgetState {
72
+ /** Current period BCU consumption */
73
+ currentBCU: number;
74
+ /** Budget limit in BCU */
75
+ limitBCU: number;
76
+ /** Utilisation ratio (0.0-1.0+) */
77
+ utilisation: number;
78
+ /** Whether budget is exceeded */
79
+ exceeded: boolean;
80
+ }
81
+
82
+ // =============================================================================
83
+ // SCARCITY WEIGHTS
84
+ // =============================================================================
85
+
86
+ /**
87
+ * Default BCU weights reflecting resource scarcity and impact.
88
+ *
89
+ * Philosophy:
90
+ * - Expensive resources that are hard to scale get high weights
91
+ * - Cheap, abundant resources get low weights
92
+ * - Writes are weighted higher than reads (durability implications)
93
+ *
94
+ * TODO: Adjust these weights based on your specific scarcity constraints.
95
+ */
96
+ export const DEFAULT_BCU_WEIGHTS: BCUWeights = {
97
+ // AI Resources - Most expensive and scarce
98
+ aiNeurons: 100, // $0.011 per 1K neurons, compute-intensive
99
+ aiRequests: 50, // Each AI call is significant
100
+
101
+ // D1 Database - Writes are expensive, reads are cheap
102
+ d1Writes: 10, // Deprecated field (use d1RowsWritten)
103
+ d1Reads: 0.01, // Deprecated field (use d1RowsRead)
104
+ d1RowsWritten: 10, // $1.00 per million, durability critical
105
+ d1RowsRead: 0.01, // $0.001 per billion, cheap
106
+
107
+ // KV - Writes constrained, reads abundant
108
+ kvWrites: 1, // $5.00 per million
109
+ kvReads: 0.1, // $0.50 per million
110
+ kvDeletes: 1, // Same cost as writes
111
+ kvLists: 1, // Same cost as writes
112
+
113
+ // R2 - Operations are relatively cheap
114
+ r2ClassA: 0.5, // $4.50 per million (PUT, POST, LIST)
115
+ r2ClassB: 0.05, // $0.36 per million (GET, HEAD)
116
+
117
+ // Durable Objects - Request-based pricing
118
+ doRequests: 0.5, // $0.15 per million
119
+ doGbSeconds: 5, // $12.50 per million GB-seconds
120
+
121
+ // Queues - Moderate pricing
122
+ queueMessages: 0.5, // $0.40 per million
123
+
124
+ // Vectorize - Query-intensive
125
+ vectorizeQueries: 1, // $0.01 per million dimensions
126
+ vectorizeInserts: 2, // Writes more expensive
127
+
128
+ // Workflows - Still in beta
129
+ workflowInvocations: 1, // Placeholder
130
+
131
+ // General compute
132
+ requests: 0.001, // Very cheap, 10M included
133
+ cpuMs: 0.01, // $0.02 per million ms
134
+ };
135
+
136
+ // =============================================================================
137
+ // BCU CALCULATION
138
+ // =============================================================================
139
+
140
+ /**
141
+ * Calculate BCU (Budget Consumption Units) from metrics.
142
+ *
143
+ * BCU provides a normalised measure of resource consumption that accounts
144
+ * for scarcity, not just cost. This enables fair quota allocation across
145
+ * features with different resource profiles.
146
+ *
147
+ * @param metrics - Feature metrics from telemetry
148
+ * @param weights - BCU weights (defaults provided)
149
+ * @returns BCU result with total, breakdown, and dominant resource
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * const metrics = { aiNeurons: 1000, d1RowsWritten: 100, requests: 50 };
154
+ * const result = calculateBCU(metrics);
155
+ * // result.total = 100000 + 1000 + 0.05 = 101000.05
156
+ * // result.dominantResource = 'aiNeurons'
157
+ * // result.dominantPercentage = 99.01
158
+ * ```
159
+ */
160
+ export function calculateBCU(
161
+ metrics: FeatureMetrics,
162
+ weights: BCUWeights = DEFAULT_BCU_WEIGHTS
163
+ ): BCUResult {
164
+ const breakdown: Partial<Record<ResourceType, number>> = {};
165
+ let total = 0;
166
+ let maxContribution = 0;
167
+ let dominantResource: ResourceType | null = null;
168
+
169
+ // Calculate BCU for each non-zero metric
170
+ const metricEntries: [ResourceType, number | undefined][] = [
171
+ ['aiNeurons', metrics.aiNeurons],
172
+ ['aiRequests', metrics.aiRequests],
173
+ ['d1Writes', metrics.d1Writes],
174
+ ['d1Reads', metrics.d1Reads],
175
+ ['d1RowsWritten', metrics.d1RowsWritten],
176
+ ['d1RowsRead', metrics.d1RowsRead],
177
+ ['kvWrites', metrics.kvWrites],
178
+ ['kvReads', metrics.kvReads],
179
+ ['kvDeletes', metrics.kvDeletes],
180
+ ['kvLists', metrics.kvLists],
181
+ ['r2ClassA', metrics.r2ClassA],
182
+ ['r2ClassB', metrics.r2ClassB],
183
+ ['doRequests', metrics.doRequests],
184
+ // Note: doGbSeconds not in FeatureMetrics - omitted
185
+ ['queueMessages', metrics.queueMessages],
186
+ ['vectorizeQueries', metrics.vectorizeQueries],
187
+ ['vectorizeInserts', metrics.vectorizeInserts],
188
+ ['workflowInvocations', metrics.workflowInvocations],
189
+ ['requests', metrics.requests],
190
+ ['cpuMs', metrics.cpuMs],
191
+ ];
192
+
193
+ for (const [resource, value] of metricEntries) {
194
+ if (value && value > 0) {
195
+ const weight = weights[resource];
196
+ const contribution = value * weight;
197
+ breakdown[resource] = contribution;
198
+ total += contribution;
199
+
200
+ if (contribution > maxContribution) {
201
+ maxContribution = contribution;
202
+ dominantResource = resource;
203
+ }
204
+ }
205
+ }
206
+
207
+ const dominantPercentage = total > 0 ? (maxContribution / total) * 100 : 0;
208
+
209
+ return {
210
+ total,
211
+ breakdown,
212
+ dominantResource,
213
+ dominantPercentage,
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Calculate BCU from raw metric values (not FeatureMetrics object).
219
+ * Useful when processing individual metric updates.
220
+ */
221
+ export function calculateBCUForResource(
222
+ resource: ResourceType,
223
+ value: number,
224
+ weights: BCUWeights = DEFAULT_BCU_WEIGHTS
225
+ ): number {
226
+ return value * weights[resource];
227
+ }
228
+
229
+ // =============================================================================
230
+ // BUDGET ENFORCEMENT
231
+ // =============================================================================
232
+
233
+ /**
234
+ * Check BCU budget state for a feature.
235
+ *
236
+ * @param currentBCU - Current BCU consumption
237
+ * @param limitBCU - Budget limit in BCU
238
+ * @returns Budget state with utilisation info
239
+ */
240
+ export function checkBCUBudget(currentBCU: number, limitBCU: number): BCUBudgetState {
241
+ const utilisation = limitBCU > 0 ? currentBCU / limitBCU : 0;
242
+ return {
243
+ currentBCU,
244
+ limitBCU,
245
+ utilisation,
246
+ exceeded: currentBCU > limitBCU,
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Convert a USD budget to BCU budget.
252
+ * Useful for setting feature budgets based on dollar allocations.
253
+ *
254
+ * This is an approximation based on the most common resource mix.
255
+ * For precise conversion, you'd need the expected resource profile.
256
+ *
257
+ * @param usdBudget - Budget in USD
258
+ * @returns Approximate BCU budget
259
+ */
260
+ export function usdToBCU(usdBudget: number): number {
261
+ // Approximation: assume average workload is 60% requests, 20% D1, 10% KV, 10% AI
262
+ // Average BCU per dollar ~= $1 buys approximately 10000 BCU in this mix
263
+ return usdBudget * 10000;
264
+ }
265
+
266
+ /**
267
+ * Convert BCU to approximate USD.
268
+ * Inverse of usdToBCU for reporting.
269
+ */
270
+ export function bcuToUSD(bcu: number): number {
271
+ return bcu / 10000;
272
+ }
273
+
274
+ // =============================================================================
275
+ // UTILITY FUNCTIONS
276
+ // =============================================================================
277
+
278
+ /**
279
+ * Get human-readable description of dominant resource.
280
+ */
281
+ export function describeDominantResource(resource: ResourceType | null): string {
282
+ if (!resource) return 'none';
283
+
284
+ const descriptions: Record<ResourceType, string> = {
285
+ aiNeurons: 'AI compute (neurons)',
286
+ aiRequests: 'AI API calls',
287
+ d1Writes: 'D1 writes (legacy)',
288
+ d1Reads: 'D1 reads (legacy)',
289
+ d1RowsWritten: 'D1 rows written',
290
+ d1RowsRead: 'D1 rows read',
291
+ kvWrites: 'KV writes',
292
+ kvReads: 'KV reads',
293
+ kvDeletes: 'KV deletes',
294
+ kvLists: 'KV list operations',
295
+ r2ClassA: 'R2 Class A ops',
296
+ r2ClassB: 'R2 Class B ops',
297
+ doRequests: 'Durable Object requests',
298
+ doGbSeconds: 'Durable Object compute',
299
+ queueMessages: 'Queue messages',
300
+ vectorizeQueries: 'Vectorize queries',
301
+ vectorizeInserts: 'Vectorize inserts',
302
+ workflowInvocations: 'Workflow invocations',
303
+ requests: 'HTTP requests',
304
+ cpuMs: 'CPU time',
305
+ };
306
+
307
+ return descriptions[resource] || resource;
308
+ }
309
+
310
+ /**
311
+ * Format BCU result for logging.
312
+ */
313
+ export function formatBCUResult(result: BCUResult): string {
314
+ const dominant = result.dominantResource
315
+ ? `${describeDominantResource(result.dominantResource)} (${result.dominantPercentage.toFixed(1)}%)`
316
+ : 'none';
317
+ return `BCU: ${result.total.toFixed(2)}, dominant: ${dominant}`;
318
+ }
319
+
320
+ /**
321
+ * Get resource-specific BCU breakdown for detailed reporting.
322
+ */
323
+ export function getTopContributors(
324
+ result: BCUResult,
325
+ topN: number = 3
326
+ ): { resource: ResourceType; bcu: number; percentage: number }[] {
327
+ const entries = Object.entries(result.breakdown) as [ResourceType, number][];
328
+ return entries
329
+ .sort((a, b) => b[1] - a[1])
330
+ .slice(0, topN)
331
+ .map(([resource, bcu]) => ({
332
+ resource,
333
+ bcu,
334
+ percentage: result.total > 0 ? (bcu / result.total) * 100 : 0,
335
+ }));
336
+ }
337
+
338
+ /**
339
+ * Combine BCU results from multiple metrics.
340
+ */
341
+ export function combineBCUResults(results: BCUResult[]): BCUResult {
342
+ const combined: Partial<Record<ResourceType, number>> = {};
343
+ let total = 0;
344
+
345
+ for (const result of results) {
346
+ for (const [resource, value] of Object.entries(result.breakdown) as [ResourceType, number][]) {
347
+ combined[resource] = (combined[resource] || 0) + value;
348
+ }
349
+ total += result.total;
350
+ }
351
+
352
+ // Find dominant resource in combined
353
+ let maxContribution = 0;
354
+ let dominantResource: ResourceType | null = null;
355
+ for (const [resource, value] of Object.entries(combined) as [ResourceType, number][]) {
356
+ if (value > maxContribution) {
357
+ maxContribution = value;
358
+ dominantResource = resource;
359
+ }
360
+ }
361
+
362
+ return {
363
+ total,
364
+ breakdown: combined,
365
+ dominantResource,
366
+ dominantPercentage: total > 0 ? (maxContribution / total) * 100 : 0,
367
+ };
368
+ }