@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.
- package/README.md +98 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +36 -6
- package/dist/prompts.d.ts +14 -2
- package/dist/prompts.js +29 -7
- package/dist/templates.js +78 -0
- package/package.json +3 -2
- package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
- package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
- package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
- package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
- package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
- package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
- package/templates/full/workers/pattern-discovery.ts +661 -0
- package/templates/full/workers/platform-alert-router.ts +1809 -0
- package/templates/full/workers/platform-notifications.ts +424 -0
- package/templates/full/workers/platform-search.ts +480 -0
- package/templates/full/workers/platform-settings.ts +436 -0
- package/templates/shared/workers/lib/analytics-engine.ts +357 -0
- package/templates/shared/workers/lib/billing.ts +293 -0
- package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
- package/templates/shared/workers/lib/control.ts +292 -0
- package/templates/shared/workers/lib/economics.ts +368 -0
- package/templates/shared/workers/lib/metrics.ts +103 -0
- package/templates/shared/workers/lib/platform-settings.ts +407 -0
- package/templates/shared/workers/lib/shared/allowances.ts +333 -0
- package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
- package/templates/shared/workers/lib/shared/types.ts +58 -0
- package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
- package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
- package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
- package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
- package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
- package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
- package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
- package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
- package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
- package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
- package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
- package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
- package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
- package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
- package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
- package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
- package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
- package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
- package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
- package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
- package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
- package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
- package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
- package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
- package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
- package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
- package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
- package/templates/shared/workers/platform-usage.ts +1915 -0
- package/templates/standard/workers/error-collector.ts +2670 -0
- package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
- package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
- package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
- package/templates/standard/workers/lib/error-collector/github.ts +329 -0
- package/templates/standard/workers/lib/error-collector/types.ts +262 -0
- package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
- package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
- package/templates/standard/workers/platform-sentinel.ts +1744 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow Evaluation for Self-Tuning Pattern System
|
|
3
|
+
*
|
|
4
|
+
* Evaluates patterns in shadow mode for auto-promotion to approved,
|
|
5
|
+
* and approved patterns for auto-demotion to stale.
|
|
6
|
+
*
|
|
7
|
+
* @module workers/lib/pattern-discovery/shadow-evaluation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { D1Database, KVNamespace } from '@cloudflare/workers-types';
|
|
11
|
+
import type {
|
|
12
|
+
PatternSuggestion,
|
|
13
|
+
ShadowEvaluationResult,
|
|
14
|
+
ShadowEvaluationConfig,
|
|
15
|
+
PatternRule,
|
|
16
|
+
} from './types';
|
|
17
|
+
import { logAuditEvent, refreshDynamicPatternsCache, aggregatePatternEvidence } from './storage';
|
|
18
|
+
import { generateReviewContext } from './ai-prompt';
|
|
19
|
+
import { compilePattern, backtestPattern, storeBacktestResult } from './validation';
|
|
20
|
+
import type { Logger } from '@littlebearapps/platform-sdk';
|
|
21
|
+
|
|
22
|
+
/** Default evaluation configuration */
|
|
23
|
+
export const DEFAULT_EVALUATION_CONFIG: ShadowEvaluationConfig = {
|
|
24
|
+
minMatchesForPromotion: 5,
|
|
25
|
+
minSpreadDaysForPromotion: 3,
|
|
26
|
+
maxMatchRateForPromotion: 0.8,
|
|
27
|
+
shadowPeriodDays: 7,
|
|
28
|
+
staleDaysThreshold: 30,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Confidence-based shadow period multipliers */
|
|
32
|
+
const CONFIDENCE_SHADOW_PERIODS: Record<string, number> = {
|
|
33
|
+
high: 3, // >=90% confidence: 3 days
|
|
34
|
+
medium: 7, // 70-89%: 7 days
|
|
35
|
+
low: 14, // 50-69%: 14 days
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get shadow period based on confidence score
|
|
40
|
+
*/
|
|
41
|
+
export function getShadowPeriodDays(confidenceScore: number): number {
|
|
42
|
+
if (confidenceScore >= 0.9) return CONFIDENCE_SHADOW_PERIODS.high;
|
|
43
|
+
if (confidenceScore >= 0.7) return CONFIDENCE_SHADOW_PERIODS.medium;
|
|
44
|
+
return CONFIDENCE_SHADOW_PERIODS.low;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Move a pending pattern into shadow mode
|
|
49
|
+
*/
|
|
50
|
+
export async function enterShadowMode(
|
|
51
|
+
db: D1Database,
|
|
52
|
+
patternId: string,
|
|
53
|
+
log: Logger
|
|
54
|
+
): Promise<boolean> {
|
|
55
|
+
const now = Math.floor(Date.now() / 1000);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Get the pattern to determine shadow period
|
|
59
|
+
const pattern = await db
|
|
60
|
+
.prepare('SELECT confidence_score FROM transient_pattern_suggestions WHERE id = ?')
|
|
61
|
+
.bind(patternId)
|
|
62
|
+
.first<{ confidence_score: number }>();
|
|
63
|
+
|
|
64
|
+
if (!pattern) {
|
|
65
|
+
log.warn('Pattern not found for shadow mode', { patternId });
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const shadowDays = getShadowPeriodDays(pattern.confidence_score || 0.5);
|
|
70
|
+
const shadowEnd = now + shadowDays * 24 * 60 * 60;
|
|
71
|
+
|
|
72
|
+
await db
|
|
73
|
+
.prepare(
|
|
74
|
+
`
|
|
75
|
+
UPDATE transient_pattern_suggestions
|
|
76
|
+
SET status = 'shadow',
|
|
77
|
+
shadow_mode_start = ?,
|
|
78
|
+
shadow_mode_end = ?,
|
|
79
|
+
shadow_mode_matches = 0,
|
|
80
|
+
shadow_match_days = '[]',
|
|
81
|
+
updated_at = unixepoch()
|
|
82
|
+
WHERE id = ? AND status = 'pending'
|
|
83
|
+
`
|
|
84
|
+
)
|
|
85
|
+
.bind(now, shadowEnd, patternId)
|
|
86
|
+
.run();
|
|
87
|
+
|
|
88
|
+
await logAuditEvent(db, patternId, 'shadow-started', 'system:shadow-evaluator',
|
|
89
|
+
`Entered shadow mode for ${shadowDays} days`, {
|
|
90
|
+
shadowDays,
|
|
91
|
+
confidenceScore: pattern.confidence_score,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
log.info('Pattern entered shadow mode', { patternId, shadowDays });
|
|
95
|
+
return true;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
log.error('Failed to enter shadow mode', error, { patternId });
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Record a shadow match for a pattern
|
|
104
|
+
*/
|
|
105
|
+
export async function recordShadowMatch(
|
|
106
|
+
db: D1Database,
|
|
107
|
+
patternId: string
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
110
|
+
|
|
111
|
+
// Get current shadow_match_days
|
|
112
|
+
const current = await db
|
|
113
|
+
.prepare('SELECT shadow_match_days FROM transient_pattern_suggestions WHERE id = ?')
|
|
114
|
+
.bind(patternId)
|
|
115
|
+
.first<{ shadow_match_days: string | null }>();
|
|
116
|
+
|
|
117
|
+
const existingDays: string[] = current?.shadow_match_days
|
|
118
|
+
? JSON.parse(current.shadow_match_days)
|
|
119
|
+
: [];
|
|
120
|
+
|
|
121
|
+
// Add today if not already present
|
|
122
|
+
if (!existingDays.includes(today)) {
|
|
123
|
+
existingDays.push(today);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await db
|
|
127
|
+
.prepare(
|
|
128
|
+
`
|
|
129
|
+
UPDATE transient_pattern_suggestions
|
|
130
|
+
SET shadow_mode_matches = shadow_mode_matches + 1,
|
|
131
|
+
shadow_match_days = ?,
|
|
132
|
+
last_matched_at = unixepoch(),
|
|
133
|
+
updated_at = unixepoch()
|
|
134
|
+
WHERE id = ?
|
|
135
|
+
`
|
|
136
|
+
)
|
|
137
|
+
.bind(JSON.stringify(existingDays), patternId)
|
|
138
|
+
.run();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Evaluate a single shadow pattern for promotion
|
|
143
|
+
*/
|
|
144
|
+
export async function evaluateShadowPattern(
|
|
145
|
+
db: D1Database,
|
|
146
|
+
pattern: PatternSuggestion,
|
|
147
|
+
config: ShadowEvaluationConfig,
|
|
148
|
+
log: Logger
|
|
149
|
+
): Promise<ShadowEvaluationResult> {
|
|
150
|
+
const now = Math.floor(Date.now() / 1000);
|
|
151
|
+
|
|
152
|
+
// Check if shadow period has ended
|
|
153
|
+
const shadowEnded = pattern.shadowModeEnd && now >= pattern.shadowModeEnd;
|
|
154
|
+
const shadowDays = pattern.shadowModeStart
|
|
155
|
+
? Math.floor((now - pattern.shadowModeStart) / (24 * 60 * 60))
|
|
156
|
+
: 0;
|
|
157
|
+
|
|
158
|
+
const matchSpreadDays = pattern.shadowMatchDays?.length || 0;
|
|
159
|
+
const shadowMatches = pattern.shadowModeMatches || 0;
|
|
160
|
+
|
|
161
|
+
// Calculate current match rate via backtest
|
|
162
|
+
const rule: PatternRule = {
|
|
163
|
+
type: pattern.patternType,
|
|
164
|
+
value: pattern.patternValue,
|
|
165
|
+
category: pattern.category,
|
|
166
|
+
scope: pattern.scope as PatternRule['scope'],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const backtestResult = await backtestPattern(pattern.id, rule, db, log, 7);
|
|
170
|
+
|
|
171
|
+
// Decision logic
|
|
172
|
+
let recommendation: 'promote' | 'demote' | 'continue' = 'continue';
|
|
173
|
+
let reasoning = '';
|
|
174
|
+
|
|
175
|
+
if (!shadowEnded) {
|
|
176
|
+
reasoning = `Shadow period ongoing (${shadowDays}/${config.shadowPeriodDays} days)`;
|
|
177
|
+
} else if (backtestResult.overMatching) {
|
|
178
|
+
recommendation = 'demote';
|
|
179
|
+
reasoning = `Over-matching: ${(backtestResult.matchRate * 100).toFixed(1)}% > ${config.maxMatchRateForPromotion * 100}%`;
|
|
180
|
+
} else if (shadowMatches < config.minMatchesForPromotion) {
|
|
181
|
+
recommendation = 'demote';
|
|
182
|
+
reasoning = `Insufficient matches: ${shadowMatches} < ${config.minMatchesForPromotion}`;
|
|
183
|
+
} else if (matchSpreadDays < config.minSpreadDaysForPromotion) {
|
|
184
|
+
recommendation = 'demote';
|
|
185
|
+
reasoning = `Insufficient spread: ${matchSpreadDays} days < ${config.minSpreadDaysForPromotion}`;
|
|
186
|
+
} else {
|
|
187
|
+
recommendation = 'promote';
|
|
188
|
+
reasoning = `Met criteria: ${shadowMatches} matches across ${matchSpreadDays} days, ${(backtestResult.matchRate * 100).toFixed(1)}% match rate`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
log.info('Shadow evaluation result', {
|
|
192
|
+
patternId: pattern.id,
|
|
193
|
+
recommendation,
|
|
194
|
+
shadowMatches,
|
|
195
|
+
matchSpreadDays,
|
|
196
|
+
matchRate: backtestResult.matchRate,
|
|
197
|
+
reasoning,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
patternId: pattern.id,
|
|
202
|
+
shadowMatchCount: shadowMatches,
|
|
203
|
+
shadowDays,
|
|
204
|
+
matchSpreadDays,
|
|
205
|
+
currentMatchRate: backtestResult.matchRate,
|
|
206
|
+
recommendation,
|
|
207
|
+
reasoning,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Environment needed for AI context generation */
|
|
212
|
+
export interface AIContextEnv {
|
|
213
|
+
CLOUDFLARE_ACCOUNT_ID: string;
|
|
214
|
+
PLATFORM_AI_GATEWAY_KEY: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Mark a shadow pattern as ready for human review (no auto-promotion)
|
|
219
|
+
*
|
|
220
|
+
* Instead of auto-promoting patterns, we aggregate evidence and store it
|
|
221
|
+
* as review_context for human decision-making. Optionally generates AI explainer.
|
|
222
|
+
*/
|
|
223
|
+
export async function markReadyForReview(
|
|
224
|
+
db: D1Database,
|
|
225
|
+
patternId: string,
|
|
226
|
+
evaluation: ShadowEvaluationResult,
|
|
227
|
+
log: Logger,
|
|
228
|
+
env?: AIContextEnv
|
|
229
|
+
): Promise<boolean> {
|
|
230
|
+
try {
|
|
231
|
+
// Get pattern details for AI context
|
|
232
|
+
const patternDetails = await db
|
|
233
|
+
.prepare(`
|
|
234
|
+
SELECT pattern_type, pattern_value, category, confidence_score, ai_reasoning
|
|
235
|
+
FROM transient_pattern_suggestions WHERE id = ?
|
|
236
|
+
`)
|
|
237
|
+
.bind(patternId)
|
|
238
|
+
.first<{
|
|
239
|
+
pattern_type: string;
|
|
240
|
+
pattern_value: string;
|
|
241
|
+
category: string;
|
|
242
|
+
confidence_score: number;
|
|
243
|
+
ai_reasoning: string | null;
|
|
244
|
+
}>();
|
|
245
|
+
|
|
246
|
+
if (!patternDetails) {
|
|
247
|
+
log.warn('Pattern not found for review context', { patternId });
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Aggregate evidence from pattern_match_evidence table
|
|
252
|
+
const evidence = await aggregatePatternEvidence(db, patternId);
|
|
253
|
+
|
|
254
|
+
// Try to generate AI explainer if env is provided
|
|
255
|
+
let aiExplainer: Awaited<ReturnType<typeof generateReviewContext>> = null;
|
|
256
|
+
if (env) {
|
|
257
|
+
aiExplainer = await generateReviewContext(
|
|
258
|
+
{
|
|
259
|
+
patternType: patternDetails.pattern_type,
|
|
260
|
+
patternValue: patternDetails.pattern_value,
|
|
261
|
+
category: patternDetails.category,
|
|
262
|
+
confidenceScore: patternDetails.confidence_score,
|
|
263
|
+
aiReasoning: patternDetails.ai_reasoning ?? undefined,
|
|
264
|
+
},
|
|
265
|
+
evidence,
|
|
266
|
+
env,
|
|
267
|
+
log
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Build review context JSON for human review
|
|
272
|
+
const reviewContext = {
|
|
273
|
+
...evidence,
|
|
274
|
+
evaluatedAt: Math.floor(Date.now() / 1000),
|
|
275
|
+
recommendation: aiExplainer?.recommendation ?? ('likely-approve' as const),
|
|
276
|
+
reasoning: evaluation.reasoning,
|
|
277
|
+
shadowMatchCount: evaluation.shadowMatchCount,
|
|
278
|
+
matchSpreadDays: evaluation.matchSpreadDays,
|
|
279
|
+
matchRate: evaluation.currentMatchRate,
|
|
280
|
+
// AI-generated explainer fields
|
|
281
|
+
aiExplainer: aiExplainer?.summary ?? null,
|
|
282
|
+
whatItCatches: aiExplainer?.whatItCatches ?? null,
|
|
283
|
+
whyTransient: aiExplainer?.whyTransient ?? null,
|
|
284
|
+
affectedAreas: aiExplainer?.affectedAreas ?? null,
|
|
285
|
+
concerns: aiExplainer?.concerns ?? [],
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Keep status as 'shadow' but add review_context
|
|
289
|
+
// This signals the pattern is ready for human review
|
|
290
|
+
await db
|
|
291
|
+
.prepare(
|
|
292
|
+
`
|
|
293
|
+
UPDATE transient_pattern_suggestions
|
|
294
|
+
SET review_context = ?,
|
|
295
|
+
updated_at = unixepoch()
|
|
296
|
+
WHERE id = ? AND status = 'shadow'
|
|
297
|
+
`
|
|
298
|
+
)
|
|
299
|
+
.bind(JSON.stringify(reviewContext), patternId)
|
|
300
|
+
.run();
|
|
301
|
+
|
|
302
|
+
await logAuditEvent(db, patternId, 'ready-for-review', 'system:shadow-evaluator',
|
|
303
|
+
'Evidence collected, awaiting human review', {
|
|
304
|
+
totalMatches: evidence.totalMatches,
|
|
305
|
+
distinctDays: evidence.distinctDays,
|
|
306
|
+
hasAiExplainer: !!aiExplainer,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
log.info('Pattern marked ready for review', {
|
|
310
|
+
patternId,
|
|
311
|
+
totalMatches: evidence.totalMatches,
|
|
312
|
+
distinctDays: evidence.distinctDays,
|
|
313
|
+
projects: Object.keys(evidence.matchesByProject).length,
|
|
314
|
+
hasAiExplainer: !!aiExplainer,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return true;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
log.error('Failed to mark pattern ready for review', error, { patternId });
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @deprecated Use markReadyForReview instead - patterns should not auto-promote
|
|
326
|
+
* Kept for reference during migration
|
|
327
|
+
*/
|
|
328
|
+
export async function autoPromotePattern(
|
|
329
|
+
db: D1Database,
|
|
330
|
+
kv: KVNamespace,
|
|
331
|
+
patternId: string,
|
|
332
|
+
evaluation: ShadowEvaluationResult,
|
|
333
|
+
log: Logger
|
|
334
|
+
): Promise<boolean> {
|
|
335
|
+
log.warn('autoPromotePattern is deprecated - use markReadyForReview instead', { patternId });
|
|
336
|
+
// Redirect to markReadyForReview to preserve existing API
|
|
337
|
+
return markReadyForReview(db, patternId, evaluation, log);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Auto-demote a shadow pattern (failed evaluation)
|
|
342
|
+
*/
|
|
343
|
+
export async function autoDemoteShadowPattern(
|
|
344
|
+
db: D1Database,
|
|
345
|
+
patternId: string,
|
|
346
|
+
evaluation: ShadowEvaluationResult,
|
|
347
|
+
log: Logger
|
|
348
|
+
): Promise<boolean> {
|
|
349
|
+
const now = Math.floor(Date.now() / 1000);
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
await db
|
|
353
|
+
.prepare(
|
|
354
|
+
`
|
|
355
|
+
UPDATE transient_pattern_suggestions
|
|
356
|
+
SET status = 'rejected',
|
|
357
|
+
shadow_mode_end = ?,
|
|
358
|
+
rejection_reason = ?,
|
|
359
|
+
reviewed_by = 'system:auto-demote',
|
|
360
|
+
reviewed_at = ?,
|
|
361
|
+
updated_at = unixepoch()
|
|
362
|
+
WHERE id = ? AND status = 'shadow'
|
|
363
|
+
`
|
|
364
|
+
)
|
|
365
|
+
.bind(now, evaluation.reasoning, now, patternId)
|
|
366
|
+
.run();
|
|
367
|
+
|
|
368
|
+
await logAuditEvent(db, patternId, 'auto-demoted', 'system:shadow-evaluator',
|
|
369
|
+
evaluation.reasoning, {
|
|
370
|
+
shadowMatchCount: evaluation.shadowMatchCount,
|
|
371
|
+
matchSpreadDays: evaluation.matchSpreadDays,
|
|
372
|
+
matchRate: evaluation.currentMatchRate,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
log.info('Shadow pattern auto-demoted', { patternId, reasoning: evaluation.reasoning });
|
|
376
|
+
return true;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
log.error('Failed to auto-demote shadow pattern', error, { patternId });
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Check approved patterns for staleness
|
|
385
|
+
*/
|
|
386
|
+
export async function checkForStalePatterns(
|
|
387
|
+
db: D1Database,
|
|
388
|
+
kv: KVNamespace,
|
|
389
|
+
config: ShadowEvaluationConfig,
|
|
390
|
+
log: Logger
|
|
391
|
+
): Promise<{ demoted: number; checked: number }> {
|
|
392
|
+
const staleCutoff = Math.floor(Date.now() / 1000) - config.staleDaysThreshold * 24 * 60 * 60;
|
|
393
|
+
|
|
394
|
+
// Find approved, non-protected patterns with no recent matches
|
|
395
|
+
const result = await db
|
|
396
|
+
.prepare(
|
|
397
|
+
`
|
|
398
|
+
SELECT id, pattern_value, category, last_matched_at, match_count
|
|
399
|
+
FROM transient_pattern_suggestions
|
|
400
|
+
WHERE status = 'approved'
|
|
401
|
+
AND is_protected = 0
|
|
402
|
+
AND (last_matched_at IS NULL OR last_matched_at < ?)
|
|
403
|
+
AND match_count > 0
|
|
404
|
+
`
|
|
405
|
+
)
|
|
406
|
+
.bind(staleCutoff)
|
|
407
|
+
.all<{ id: string; pattern_value: string; category: string; last_matched_at: number | null; match_count: number }>();
|
|
408
|
+
|
|
409
|
+
let demoted = 0;
|
|
410
|
+
|
|
411
|
+
for (const pattern of result.results) {
|
|
412
|
+
const now = Math.floor(Date.now() / 1000);
|
|
413
|
+
|
|
414
|
+
await db
|
|
415
|
+
.prepare(
|
|
416
|
+
`
|
|
417
|
+
UPDATE transient_pattern_suggestions
|
|
418
|
+
SET status = 'stale',
|
|
419
|
+
disabled_at = ?,
|
|
420
|
+
updated_at = unixepoch()
|
|
421
|
+
WHERE id = ?
|
|
422
|
+
`
|
|
423
|
+
)
|
|
424
|
+
.bind(now, pattern.id)
|
|
425
|
+
.run();
|
|
426
|
+
|
|
427
|
+
const daysSinceMatch = pattern.last_matched_at
|
|
428
|
+
? Math.floor((now - pattern.last_matched_at) / (24 * 60 * 60))
|
|
429
|
+
: 'never';
|
|
430
|
+
|
|
431
|
+
await logAuditEvent(db, pattern.id, 'auto-demoted', 'system:stale-detector',
|
|
432
|
+
`No matches in ${config.staleDaysThreshold} days`, {
|
|
433
|
+
lastMatchedAt: pattern.last_matched_at,
|
|
434
|
+
daysSinceMatch,
|
|
435
|
+
totalMatches: pattern.match_count,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
log.info('Pattern marked stale', {
|
|
439
|
+
patternId: pattern.id,
|
|
440
|
+
category: pattern.category,
|
|
441
|
+
daysSinceMatch,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
demoted++;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Refresh KV if any patterns were demoted
|
|
448
|
+
if (demoted > 0) {
|
|
449
|
+
await refreshDynamicPatternsCache(db, kv, log);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return { demoted, checked: result.results.length };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Get patterns in shadow mode that need evaluation
|
|
457
|
+
*/
|
|
458
|
+
export async function getShadowPatternsForEvaluation(
|
|
459
|
+
db: D1Database
|
|
460
|
+
): Promise<PatternSuggestion[]> {
|
|
461
|
+
const now = Math.floor(Date.now() / 1000);
|
|
462
|
+
|
|
463
|
+
const result = await db
|
|
464
|
+
.prepare(
|
|
465
|
+
`
|
|
466
|
+
SELECT
|
|
467
|
+
id, pattern_type as patternType, pattern_value as patternValue,
|
|
468
|
+
category, scope, confidence_score as confidenceScore,
|
|
469
|
+
sample_messages as sampleMessages, ai_reasoning as aiReasoning,
|
|
470
|
+
cluster_id as clusterId, status,
|
|
471
|
+
reviewed_by as reviewedBy, reviewed_at as reviewedAt,
|
|
472
|
+
rejection_reason as rejectionReason,
|
|
473
|
+
backtest_match_count as backtestMatchCount,
|
|
474
|
+
backtest_total_errors as backtestTotalErrors,
|
|
475
|
+
backtest_match_rate as backtestMatchRate,
|
|
476
|
+
shadow_mode_start as shadowModeStart,
|
|
477
|
+
shadow_mode_end as shadowModeEnd,
|
|
478
|
+
shadow_mode_matches as shadowModeMatches,
|
|
479
|
+
shadow_match_days as shadowMatchDays,
|
|
480
|
+
enabled_at as enabledAt, disabled_at as disabledAt,
|
|
481
|
+
last_matched_at as lastMatchedAt, match_count as matchCount,
|
|
482
|
+
is_protected as isProtected, source, original_regex as originalRegex,
|
|
483
|
+
created_at as createdAt, updated_at as updatedAt
|
|
484
|
+
FROM transient_pattern_suggestions
|
|
485
|
+
WHERE status = 'shadow'
|
|
486
|
+
AND shadow_mode_end <= ?
|
|
487
|
+
ORDER BY created_at ASC
|
|
488
|
+
`
|
|
489
|
+
)
|
|
490
|
+
.bind(now)
|
|
491
|
+
.all<PatternSuggestion & { sampleMessages: string; shadowMatchDays: string }>();
|
|
492
|
+
|
|
493
|
+
return result.results.map((r) => ({
|
|
494
|
+
...r,
|
|
495
|
+
sampleMessages: JSON.parse(r.sampleMessages || '[]'),
|
|
496
|
+
shadowMatchDays: r.shadowMatchDays ? JSON.parse(r.shadowMatchDays) : [],
|
|
497
|
+
isProtected: Boolean(r.isProtected),
|
|
498
|
+
}));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Auto-enter shadow mode for pending patterns older than 24 hours
|
|
503
|
+
*/
|
|
504
|
+
export async function autoEnterShadowForOldPending(
|
|
505
|
+
db: D1Database,
|
|
506
|
+
log: Logger
|
|
507
|
+
): Promise<number> {
|
|
508
|
+
const cutoff = Math.floor(Date.now() / 1000) - 24 * 60 * 60; // 24 hours ago
|
|
509
|
+
|
|
510
|
+
const result = await db
|
|
511
|
+
.prepare(
|
|
512
|
+
`
|
|
513
|
+
SELECT id FROM transient_pattern_suggestions
|
|
514
|
+
WHERE status = 'pending'
|
|
515
|
+
AND created_at < ?
|
|
516
|
+
AND confidence_score >= 0.5
|
|
517
|
+
`
|
|
518
|
+
)
|
|
519
|
+
.bind(cutoff)
|
|
520
|
+
.all<{ id: string }>();
|
|
521
|
+
|
|
522
|
+
let entered = 0;
|
|
523
|
+
|
|
524
|
+
for (const pattern of result.results) {
|
|
525
|
+
const success = await enterShadowMode(db, pattern.id, log);
|
|
526
|
+
if (success) entered++;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (entered > 0) {
|
|
530
|
+
log.info('Auto-entered shadow mode for pending patterns', { count: entered });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return entered;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Run full shadow evaluation cycle
|
|
538
|
+
*
|
|
539
|
+
* Note: Patterns that meet promotion criteria are marked as "ready for review"
|
|
540
|
+
* rather than auto-promoted. Human approval is required.
|
|
541
|
+
*
|
|
542
|
+
* @param env - Optional env for AI context generation. If provided, generates AI explainers.
|
|
543
|
+
*/
|
|
544
|
+
export async function runShadowEvaluationCycle(
|
|
545
|
+
db: D1Database,
|
|
546
|
+
kv: KVNamespace,
|
|
547
|
+
log: Logger,
|
|
548
|
+
config: ShadowEvaluationConfig = DEFAULT_EVALUATION_CONFIG,
|
|
549
|
+
env?: AIContextEnv
|
|
550
|
+
): Promise<{
|
|
551
|
+
evaluated: number;
|
|
552
|
+
readyForReview: number;
|
|
553
|
+
demoted: number;
|
|
554
|
+
enteredShadow: number;
|
|
555
|
+
staleDetected: number;
|
|
556
|
+
/** @deprecated Use readyForReview instead */
|
|
557
|
+
promoted?: number;
|
|
558
|
+
}> {
|
|
559
|
+
log.info('Starting shadow evaluation cycle', { hasAIEnv: !!env });
|
|
560
|
+
|
|
561
|
+
// Step 1: Auto-enter shadow for old pending patterns
|
|
562
|
+
const enteredShadow = await autoEnterShadowForOldPending(db, log);
|
|
563
|
+
|
|
564
|
+
// Step 2: Evaluate shadow patterns ready for decision
|
|
565
|
+
const shadowPatterns = await getShadowPatternsForEvaluation(db);
|
|
566
|
+
let readyForReview = 0;
|
|
567
|
+
let demoted = 0;
|
|
568
|
+
|
|
569
|
+
for (const pattern of shadowPatterns) {
|
|
570
|
+
const evaluation = await evaluateShadowPattern(db, pattern, config, log);
|
|
571
|
+
|
|
572
|
+
if (evaluation.recommendation === 'promote') {
|
|
573
|
+
// Mark as ready for human review instead of auto-promoting
|
|
574
|
+
// Pass env to enable AI explainer generation
|
|
575
|
+
const success = await markReadyForReview(db, pattern.id, evaluation, log, env);
|
|
576
|
+
if (success) readyForReview++;
|
|
577
|
+
} else if (evaluation.recommendation === 'demote') {
|
|
578
|
+
const success = await autoDemoteShadowPattern(db, pattern.id, evaluation, log);
|
|
579
|
+
if (success) demoted++;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Step 3: Check for stale patterns (weekly - only run on Sundays)
|
|
584
|
+
let staleDetected = 0;
|
|
585
|
+
const dayOfWeek = new Date().getDay();
|
|
586
|
+
if (dayOfWeek === 0) { // Sunday
|
|
587
|
+
const staleResult = await checkForStalePatterns(db, kv, config, log);
|
|
588
|
+
staleDetected = staleResult.demoted;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const result = {
|
|
592
|
+
evaluated: shadowPatterns.length,
|
|
593
|
+
readyForReview,
|
|
594
|
+
demoted,
|
|
595
|
+
enteredShadow,
|
|
596
|
+
staleDetected,
|
|
597
|
+
// Backwards compatibility
|
|
598
|
+
promoted: readyForReview,
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
log.info('Shadow evaluation cycle complete', result);
|
|
602
|
+
return result;
|
|
603
|
+
}
|