@littlebearapps/platform-admin-sdk 1.0.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 +112 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +89 -0
- package/dist/prompts.d.ts +27 -0
- package/dist/prompts.js +80 -0
- package/dist/scaffold.d.ts +5 -0
- package/dist/scaffold.js +65 -0
- package/dist/templates.d.ts +16 -0
- package/dist/templates.js +131 -0
- package/package.json +46 -0
- package/templates/full/migrations/006_pattern_discovery.sql +199 -0
- package/templates/full/migrations/007_notifications_search.sql +127 -0
- 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/full/wrangler.alert-router.jsonc.hbs +34 -0
- package/templates/full/wrangler.notifications.jsonc.hbs +23 -0
- package/templates/full/wrangler.pattern-discovery.jsonc.hbs +33 -0
- package/templates/full/wrangler.search.jsonc.hbs +16 -0
- package/templates/full/wrangler.settings.jsonc.hbs +23 -0
- package/templates/shared/README.md.hbs +69 -0
- package/templates/shared/config/budgets.yaml.hbs +72 -0
- package/templates/shared/config/services.yaml.hbs +45 -0
- package/templates/shared/migrations/001_core_tables.sql +117 -0
- package/templates/shared/migrations/002_usage_warehouse.sql +830 -0
- package/templates/shared/migrations/003_feature_tracking.sql +250 -0
- package/templates/shared/migrations/004_settings_alerts.sql +452 -0
- package/templates/shared/migrations/seed.sql.hbs +4 -0
- package/templates/shared/package.json.hbs +21 -0
- package/templates/shared/scripts/sync-config.ts +242 -0
- package/templates/shared/tsconfig.json +12 -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/shared/wrangler.usage.jsonc.hbs +58 -0
- package/templates/standard/migrations/005_error_collection.sql +162 -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
- package/templates/standard/wrangler.error-collector.jsonc.hbs +44 -0
- package/templates/standard/wrangler.sentinel.jsonc.hbs +45 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for Automated Transient Error Pattern Discovery
|
|
3
|
+
* @module workers/lib/pattern-discovery/types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Pattern types supported by the DSL (ordered by safety) */
|
|
7
|
+
export type PatternType = 'contains' | 'startsWith' | 'statusCode' | 'regex';
|
|
8
|
+
|
|
9
|
+
/** Scope for pattern application */
|
|
10
|
+
export type PatternScope = 'global' | `service:${string}` | `upstream:${string}`;
|
|
11
|
+
|
|
12
|
+
/** Pattern suggestion status - expanded for self-tuning */
|
|
13
|
+
export type SuggestionStatus = 'pending' | 'shadow' | 'approved' | 'stale' | 'rejected' | 'disabled';
|
|
14
|
+
|
|
15
|
+
/** Pattern source tracking */
|
|
16
|
+
export type PatternSource = 'ai-discovered' | 'static-import' | 'manual';
|
|
17
|
+
|
|
18
|
+
/** Audit log action types - expanded for self-tuning */
|
|
19
|
+
export type AuditAction =
|
|
20
|
+
| 'created'
|
|
21
|
+
| 'approved'
|
|
22
|
+
| 'rejected'
|
|
23
|
+
| 'enabled'
|
|
24
|
+
| 'disabled'
|
|
25
|
+
| 'auto-disabled'
|
|
26
|
+
| 'backtest-passed'
|
|
27
|
+
| 'backtest-failed'
|
|
28
|
+
| 'shadow-started'
|
|
29
|
+
| 'shadow-completed'
|
|
30
|
+
| 'expired'
|
|
31
|
+
// Self-tuning actions
|
|
32
|
+
| 'auto-promoted'
|
|
33
|
+
| 'auto-demoted'
|
|
34
|
+
| 'ready-for-review'
|
|
35
|
+
| 'reactivated'
|
|
36
|
+
| 'imported';
|
|
37
|
+
|
|
38
|
+
/** Pattern rule using the constrained DSL */
|
|
39
|
+
export interface PatternRule {
|
|
40
|
+
type: PatternType;
|
|
41
|
+
value: string; // Token(s) for contains, prefix for startsWith, code for statusCode, regex string for regex
|
|
42
|
+
category: string;
|
|
43
|
+
scope: PatternScope;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Pattern suggestion from AI analysis */
|
|
47
|
+
export interface PatternSuggestion {
|
|
48
|
+
id: string;
|
|
49
|
+
patternType: PatternType;
|
|
50
|
+
patternValue: string;
|
|
51
|
+
category: string;
|
|
52
|
+
scope: PatternScope;
|
|
53
|
+
confidenceScore: number;
|
|
54
|
+
sampleMessages: string[];
|
|
55
|
+
aiReasoning: string;
|
|
56
|
+
clusterId: string | null;
|
|
57
|
+
status: SuggestionStatus;
|
|
58
|
+
reviewedBy: string | null;
|
|
59
|
+
reviewedAt: number | null;
|
|
60
|
+
rejectionReason: string | null;
|
|
61
|
+
backtestMatchCount: number | null;
|
|
62
|
+
backtestTotalErrors: number | null;
|
|
63
|
+
backtestMatchRate: number | null;
|
|
64
|
+
// Shadow mode tracking
|
|
65
|
+
shadowModeStart: number | null;
|
|
66
|
+
shadowModeEnd: number | null;
|
|
67
|
+
shadowModeMatches: number;
|
|
68
|
+
shadowMatchDays: string[] | null; // Unique days with matches
|
|
69
|
+
// Lifecycle
|
|
70
|
+
enabledAt: number | null;
|
|
71
|
+
disabledAt: number | null;
|
|
72
|
+
lastMatchedAt: number | null;
|
|
73
|
+
matchCount: number;
|
|
74
|
+
// Self-tuning fields
|
|
75
|
+
isProtected: boolean;
|
|
76
|
+
source: PatternSource;
|
|
77
|
+
originalRegex: string | null;
|
|
78
|
+
// Timestamps
|
|
79
|
+
createdAt: number;
|
|
80
|
+
updatedAt: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Shadow evaluation result for auto-promotion decision */
|
|
84
|
+
export interface ShadowEvaluationResult {
|
|
85
|
+
patternId: string;
|
|
86
|
+
shadowMatchCount: number;
|
|
87
|
+
shadowDays: number;
|
|
88
|
+
matchSpreadDays: number; // How many unique days had matches
|
|
89
|
+
currentMatchRate: number;
|
|
90
|
+
recommendation: 'promote' | 'demote' | 'continue';
|
|
91
|
+
reasoning: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Configuration for shadow evaluation thresholds */
|
|
95
|
+
export interface ShadowEvaluationConfig {
|
|
96
|
+
minMatchesForPromotion: number; // Default: 5
|
|
97
|
+
minSpreadDaysForPromotion: number; // Default: 3
|
|
98
|
+
maxMatchRateForPromotion: number; // Default: 0.8 (80%)
|
|
99
|
+
shadowPeriodDays: number; // Default: 7
|
|
100
|
+
staleDaysThreshold: number; // Default: 30
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Error cluster for grouping similar unclassified errors */
|
|
104
|
+
export interface ErrorCluster {
|
|
105
|
+
id: string;
|
|
106
|
+
clusterHash: string;
|
|
107
|
+
representativeMessage: string;
|
|
108
|
+
occurrenceCount: number;
|
|
109
|
+
uniqueFingerprints: number;
|
|
110
|
+
firstSeenAt: number;
|
|
111
|
+
lastSeenAt: number;
|
|
112
|
+
scripts: string[];
|
|
113
|
+
status: 'pending' | 'processing' | 'suggested' | 'ignored';
|
|
114
|
+
suggestionId: string | null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** AI suggestion response from DeepSeek */
|
|
118
|
+
export interface AISuggestionResponse {
|
|
119
|
+
patterns: Array<{
|
|
120
|
+
patternType: PatternType;
|
|
121
|
+
patternValue: string;
|
|
122
|
+
category: string;
|
|
123
|
+
confidence: number;
|
|
124
|
+
reasoning: string;
|
|
125
|
+
positiveExamples: string[];
|
|
126
|
+
negativeExamples: string[];
|
|
127
|
+
}>;
|
|
128
|
+
summary: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Backtest result for a pattern */
|
|
132
|
+
export interface BacktestResult {
|
|
133
|
+
patternId: string;
|
|
134
|
+
matchCount: number;
|
|
135
|
+
totalErrors: number;
|
|
136
|
+
matchRate: number;
|
|
137
|
+
matchedFingerprints: string[];
|
|
138
|
+
overMatching: boolean; // true if > 80% match rate
|
|
139
|
+
runAt: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Unclassified error from D1 for clustering */
|
|
143
|
+
export interface UnclassifiedError {
|
|
144
|
+
fingerprint: string;
|
|
145
|
+
scriptName: string;
|
|
146
|
+
normalizedMessage: string;
|
|
147
|
+
occurrenceCount: number;
|
|
148
|
+
lastSeenAt: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Discovery run result */
|
|
152
|
+
export interface DiscoveryResult {
|
|
153
|
+
runId: string;
|
|
154
|
+
runAt: number;
|
|
155
|
+
clustersFound: number;
|
|
156
|
+
clustersProcessed: number;
|
|
157
|
+
suggestionsCreated: number;
|
|
158
|
+
errors: string[];
|
|
159
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Validation and Safety Checks
|
|
3
|
+
*
|
|
4
|
+
* Validates AI-generated patterns for safety (ReDoS prevention)
|
|
5
|
+
* and correctness (backtest against historical data).
|
|
6
|
+
*
|
|
7
|
+
* @module workers/lib/pattern-discovery/validation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { D1Database } from '@cloudflare/workers-types';
|
|
11
|
+
import type { PatternType, PatternRule, BacktestResult } from './types';
|
|
12
|
+
import type { Logger } from '@littlebearapps/platform-consumer-sdk';
|
|
13
|
+
|
|
14
|
+
/** Maximum regex execution time in ms */
|
|
15
|
+
const MAX_REGEX_EXEC_TIME_MS = 10;
|
|
16
|
+
|
|
17
|
+
/** Match rate threshold for over-matching detection */
|
|
18
|
+
const OVER_MATCHING_THRESHOLD = 0.8;
|
|
19
|
+
|
|
20
|
+
/** Dangerous regex patterns to reject */
|
|
21
|
+
const DANGEROUS_PATTERNS = [
|
|
22
|
+
/\(\.\*\)\+/, // (.*)+
|
|
23
|
+
/\(\.\+\)\+/, // (.+)+
|
|
24
|
+
/\([^)]*\|[^)]*\)\+/, // (a|b)+ nested alternation with quantifier
|
|
25
|
+
/\(\?<[!=]/, // Lookbehind (not supported in all engines)
|
|
26
|
+
/\\1/, // Backreferences
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Maximum allowed regex length */
|
|
30
|
+
const MAX_REGEX_LENGTH = 200;
|
|
31
|
+
|
|
32
|
+
/** Maximum allowed alternations in regex */
|
|
33
|
+
const MAX_ALTERNATIONS = 10;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate a pattern for safety
|
|
37
|
+
* Returns error message if invalid, null if valid
|
|
38
|
+
*/
|
|
39
|
+
export function validatePatternSafety(
|
|
40
|
+
patternType: PatternType,
|
|
41
|
+
patternValue: string
|
|
42
|
+
): string | null {
|
|
43
|
+
// Non-regex patterns are always safe
|
|
44
|
+
if (patternType !== 'regex') {
|
|
45
|
+
// Basic validation for other types
|
|
46
|
+
if (!patternValue || patternValue.length === 0) {
|
|
47
|
+
return 'Pattern value cannot be empty';
|
|
48
|
+
}
|
|
49
|
+
if (patternType === 'statusCode' && !/^\d{3}$/.test(patternValue)) {
|
|
50
|
+
return 'Status code must be 3 digits';
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Regex validation
|
|
56
|
+
if (patternValue.length > MAX_REGEX_LENGTH) {
|
|
57
|
+
return `Regex too long (max ${MAX_REGEX_LENGTH} chars)`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for dangerous patterns
|
|
61
|
+
for (const dangerous of DANGEROUS_PATTERNS) {
|
|
62
|
+
if (dangerous.test(patternValue)) {
|
|
63
|
+
return `Regex contains dangerous pattern: ${dangerous.source}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check alternation count
|
|
68
|
+
const alternations = (patternValue.match(/\|/g) || []).length;
|
|
69
|
+
if (alternations > MAX_ALTERNATIONS) {
|
|
70
|
+
return `Too many alternations (max ${MAX_ALTERNATIONS})`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Try to compile the regex
|
|
74
|
+
try {
|
|
75
|
+
new RegExp(patternValue, 'i');
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return `Invalid regex: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Performance test with adversarial input
|
|
81
|
+
const testResult = testRegexPerformance(patternValue);
|
|
82
|
+
if (testResult !== null) {
|
|
83
|
+
return testResult;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Test regex performance with adversarial input
|
|
91
|
+
* Returns error message if too slow, null if OK
|
|
92
|
+
*/
|
|
93
|
+
function testRegexPerformance(pattern: string): string | null {
|
|
94
|
+
const regex = new RegExp(pattern, 'i');
|
|
95
|
+
|
|
96
|
+
// Test with various adversarial inputs
|
|
97
|
+
const adversarialInputs = [
|
|
98
|
+
'a'.repeat(100),
|
|
99
|
+
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!',
|
|
100
|
+
' '.repeat(100),
|
|
101
|
+
'x'.repeat(50) + 'y'.repeat(50),
|
|
102
|
+
'0'.repeat(100),
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
for (const input of adversarialInputs) {
|
|
106
|
+
const start = performance.now();
|
|
107
|
+
try {
|
|
108
|
+
regex.test(input);
|
|
109
|
+
} catch {
|
|
110
|
+
return 'Regex execution failed';
|
|
111
|
+
}
|
|
112
|
+
const elapsed = performance.now() - start;
|
|
113
|
+
|
|
114
|
+
if (elapsed > MAX_REGEX_EXEC_TIME_MS) {
|
|
115
|
+
return `Regex too slow (${elapsed.toFixed(1)}ms > ${MAX_REGEX_EXEC_TIME_MS}ms)`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Compile a pattern rule to a test function
|
|
124
|
+
*/
|
|
125
|
+
export function compilePattern(rule: PatternRule): (message: string) => boolean {
|
|
126
|
+
switch (rule.type) {
|
|
127
|
+
case 'contains': {
|
|
128
|
+
const tokens = rule.value.toLowerCase().split(/\s+/);
|
|
129
|
+
return (message: string) => {
|
|
130
|
+
const lower = message.toLowerCase();
|
|
131
|
+
return tokens.every((token) => lower.includes(token));
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case 'startsWith': {
|
|
136
|
+
const prefix = rule.value.toLowerCase();
|
|
137
|
+
return (message: string) => message.toLowerCase().startsWith(prefix);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case 'statusCode': {
|
|
141
|
+
const code = rule.value;
|
|
142
|
+
const codePattern = new RegExp(`\\b${code}\\b`);
|
|
143
|
+
return (message: string) => codePattern.test(message);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case 'regex': {
|
|
147
|
+
const regex = new RegExp(rule.value, 'i');
|
|
148
|
+
return (message: string) => regex.test(message);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Run backtest against historical error data
|
|
155
|
+
*/
|
|
156
|
+
export async function backtestPattern(
|
|
157
|
+
patternId: string,
|
|
158
|
+
rule: PatternRule,
|
|
159
|
+
db: D1Database,
|
|
160
|
+
log: Logger,
|
|
161
|
+
daysBack: number = 7
|
|
162
|
+
): Promise<BacktestResult> {
|
|
163
|
+
const cutoff = Math.floor(Date.now() / 1000) - daysBack * 24 * 60 * 60;
|
|
164
|
+
|
|
165
|
+
// Get recent errors
|
|
166
|
+
const result = await db
|
|
167
|
+
.prepare(
|
|
168
|
+
`
|
|
169
|
+
SELECT fingerprint, normalized_message as message
|
|
170
|
+
FROM error_occurrences
|
|
171
|
+
WHERE last_seen_at >= ?
|
|
172
|
+
LIMIT 10000
|
|
173
|
+
`
|
|
174
|
+
)
|
|
175
|
+
.bind(cutoff)
|
|
176
|
+
.all<{ fingerprint: string; message: string | null }>();
|
|
177
|
+
|
|
178
|
+
const errors = result.results;
|
|
179
|
+
const testFn = compilePattern(rule);
|
|
180
|
+
|
|
181
|
+
const matchedFingerprints: string[] = [];
|
|
182
|
+
let matchCount = 0;
|
|
183
|
+
|
|
184
|
+
for (const error of errors) {
|
|
185
|
+
if (error.message && testFn(error.message)) {
|
|
186
|
+
matchCount++;
|
|
187
|
+
if (matchedFingerprints.length < 100) {
|
|
188
|
+
matchedFingerprints.push(error.fingerprint);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const totalErrors = errors.length;
|
|
194
|
+
const matchRate = totalErrors > 0 ? matchCount / totalErrors : 0;
|
|
195
|
+
const overMatching = matchRate > OVER_MATCHING_THRESHOLD;
|
|
196
|
+
|
|
197
|
+
log.info('Backtest complete', {
|
|
198
|
+
patternId,
|
|
199
|
+
matchCount,
|
|
200
|
+
totalErrors,
|
|
201
|
+
matchRate: (matchRate * 100).toFixed(1) + '%',
|
|
202
|
+
overMatching,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
patternId,
|
|
207
|
+
matchCount,
|
|
208
|
+
totalErrors,
|
|
209
|
+
matchRate,
|
|
210
|
+
matchedFingerprints,
|
|
211
|
+
overMatching,
|
|
212
|
+
runAt: Math.floor(Date.now() / 1000),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Store backtest result in D1
|
|
218
|
+
*/
|
|
219
|
+
export async function storeBacktestResult(
|
|
220
|
+
db: D1Database,
|
|
221
|
+
result: BacktestResult,
|
|
222
|
+
log: Logger
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
try {
|
|
225
|
+
await db
|
|
226
|
+
.prepare(
|
|
227
|
+
`
|
|
228
|
+
UPDATE transient_pattern_suggestions
|
|
229
|
+
SET
|
|
230
|
+
backtest_match_count = ?,
|
|
231
|
+
backtest_total_errors = ?,
|
|
232
|
+
backtest_match_rate = ?,
|
|
233
|
+
backtest_run_at = ?,
|
|
234
|
+
updated_at = unixepoch()
|
|
235
|
+
WHERE id = ?
|
|
236
|
+
`
|
|
237
|
+
)
|
|
238
|
+
.bind(
|
|
239
|
+
result.matchCount,
|
|
240
|
+
result.totalErrors,
|
|
241
|
+
result.matchRate,
|
|
242
|
+
result.runAt,
|
|
243
|
+
result.patternId
|
|
244
|
+
)
|
|
245
|
+
.run();
|
|
246
|
+
|
|
247
|
+
// Log audit event
|
|
248
|
+
const action = result.overMatching ? 'backtest-failed' : 'backtest-passed';
|
|
249
|
+
await db
|
|
250
|
+
.prepare(
|
|
251
|
+
`
|
|
252
|
+
INSERT INTO pattern_audit_log (id, pattern_id, action, actor, reason, metadata)
|
|
253
|
+
VALUES (?, ?, ?, 'system:backtest', ?, ?)
|
|
254
|
+
`
|
|
255
|
+
)
|
|
256
|
+
.bind(
|
|
257
|
+
`audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
258
|
+
result.patternId,
|
|
259
|
+
action,
|
|
260
|
+
result.overMatching
|
|
261
|
+
? `Over-matching: ${(result.matchRate * 100).toFixed(1)}% match rate`
|
|
262
|
+
: `Passed: ${(result.matchRate * 100).toFixed(1)}% match rate`,
|
|
263
|
+
JSON.stringify({
|
|
264
|
+
matchCount: result.matchCount,
|
|
265
|
+
totalErrors: result.totalErrors,
|
|
266
|
+
matchRate: result.matchRate,
|
|
267
|
+
})
|
|
268
|
+
)
|
|
269
|
+
.run();
|
|
270
|
+
|
|
271
|
+
log.info('Stored backtest result', {
|
|
272
|
+
patternId: result.patternId,
|
|
273
|
+
action,
|
|
274
|
+
});
|
|
275
|
+
} catch (error) {
|
|
276
|
+
log.error('Failed to store backtest result', error, { patternId: result.patternId });
|
|
277
|
+
}
|
|
278
|
+
}
|