outcome-cli 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.
Files changed (113) hide show
  1. package/README.md +261 -0
  2. package/package.json +95 -0
  3. package/src/agents/README.md +139 -0
  4. package/src/agents/adapters/anthropic.adapter.ts +166 -0
  5. package/src/agents/adapters/dalle.adapter.ts +145 -0
  6. package/src/agents/adapters/gemini.adapter.ts +134 -0
  7. package/src/agents/adapters/imagen.adapter.ts +106 -0
  8. package/src/agents/adapters/nano-banana.adapter.ts +129 -0
  9. package/src/agents/adapters/openai.adapter.ts +165 -0
  10. package/src/agents/adapters/veo.adapter.ts +130 -0
  11. package/src/agents/agent.schema.property.test.ts +379 -0
  12. package/src/agents/agent.schema.test.ts +148 -0
  13. package/src/agents/agent.schema.ts +263 -0
  14. package/src/agents/index.ts +60 -0
  15. package/src/agents/registered-agent.schema.ts +356 -0
  16. package/src/agents/registry.ts +97 -0
  17. package/src/agents/tournament-configs.property.test.ts +266 -0
  18. package/src/cli/README.md +145 -0
  19. package/src/cli/commands/define.ts +79 -0
  20. package/src/cli/commands/list.ts +46 -0
  21. package/src/cli/commands/logs.ts +83 -0
  22. package/src/cli/commands/run.ts +416 -0
  23. package/src/cli/commands/verify.ts +110 -0
  24. package/src/cli/index.ts +81 -0
  25. package/src/config/README.md +128 -0
  26. package/src/config/env.ts +262 -0
  27. package/src/config/index.ts +19 -0
  28. package/src/eval/README.md +318 -0
  29. package/src/eval/ai-judge.test.ts +435 -0
  30. package/src/eval/ai-judge.ts +368 -0
  31. package/src/eval/code-validators.ts +414 -0
  32. package/src/eval/evaluateOutcome.property.test.ts +1174 -0
  33. package/src/eval/evaluateOutcome.ts +591 -0
  34. package/src/eval/immigration-validators.ts +122 -0
  35. package/src/eval/index.ts +90 -0
  36. package/src/eval/judge-cache.ts +402 -0
  37. package/src/eval/tournament-validators.property.test.ts +439 -0
  38. package/src/eval/validators.property.test.ts +1118 -0
  39. package/src/eval/validators.ts +1199 -0
  40. package/src/eval/weighted-scorer.ts +285 -0
  41. package/src/index.ts +17 -0
  42. package/src/league/README.md +188 -0
  43. package/src/league/health-check.ts +353 -0
  44. package/src/league/index.ts +93 -0
  45. package/src/league/killAgent.ts +151 -0
  46. package/src/league/league.test.ts +1151 -0
  47. package/src/league/runLeague.ts +843 -0
  48. package/src/league/scoreAgent.ts +175 -0
  49. package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
  50. package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
  51. package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
  52. package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
  53. package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
  54. package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
  55. package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
  56. package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
  57. package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
  58. package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
  59. package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
  60. package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
  61. package/src/modules/omnibridge/api/.gitkeep +1 -0
  62. package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
  63. package/src/modules/omnibridge/auth/.gitkeep +1 -0
  64. package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
  65. package/src/modules/omnibridge/auth/session-vault.ts +577 -0
  66. package/src/modules/omnibridge/core/.gitkeep +1 -0
  67. package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
  68. package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
  69. package/src/modules/omnibridge/core/types.ts +610 -0
  70. package/src/modules/omnibridge/execution/.gitkeep +1 -0
  71. package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
  72. package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
  73. package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
  74. package/src/modules/omnibridge/index.ts +212 -0
  75. package/src/modules/omnibridge/omnibridge.ts +510 -0
  76. package/src/modules/omnibridge/verification/.gitkeep +1 -0
  77. package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
  78. package/src/outcomes/README.md +75 -0
  79. package/src/outcomes/acquire-pilot-customer.ts +297 -0
  80. package/src/outcomes/code-delivery-outcomes.ts +89 -0
  81. package/src/outcomes/code-outcomes.ts +256 -0
  82. package/src/outcomes/code_review_battle.test.ts +135 -0
  83. package/src/outcomes/code_review_battle.ts +135 -0
  84. package/src/outcomes/cold_email_battle.ts +97 -0
  85. package/src/outcomes/content_creation_battle.ts +160 -0
  86. package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
  87. package/src/outcomes/index.ts +107 -0
  88. package/src/outcomes/lead_gen_battle.test.ts +113 -0
  89. package/src/outcomes/lead_gen_battle.ts +99 -0
  90. package/src/outcomes/outcome.schema.property.test.ts +229 -0
  91. package/src/outcomes/outcome.schema.ts +187 -0
  92. package/src/outcomes/qualified_sales_interest.ts +118 -0
  93. package/src/outcomes/swarm_planner.property.test.ts +370 -0
  94. package/src/outcomes/swarm_planner.ts +96 -0
  95. package/src/outcomes/web_extraction.ts +234 -0
  96. package/src/runtime/README.md +220 -0
  97. package/src/runtime/agentRunner.test.ts +341 -0
  98. package/src/runtime/agentRunner.ts +746 -0
  99. package/src/runtime/claudeAdapter.ts +232 -0
  100. package/src/runtime/costTracker.ts +123 -0
  101. package/src/runtime/index.ts +34 -0
  102. package/src/runtime/modelAdapter.property.test.ts +305 -0
  103. package/src/runtime/modelAdapter.ts +144 -0
  104. package/src/runtime/openaiAdapter.ts +235 -0
  105. package/src/utils/README.md +122 -0
  106. package/src/utils/command-runner.ts +134 -0
  107. package/src/utils/cost-guard.ts +379 -0
  108. package/src/utils/errors.test.ts +290 -0
  109. package/src/utils/errors.ts +442 -0
  110. package/src/utils/index.ts +37 -0
  111. package/src/utils/logger.test.ts +361 -0
  112. package/src/utils/logger.ts +419 -0
  113. package/src/utils/output-parsers.ts +216 -0
@@ -0,0 +1,122 @@
1
+ export interface ValidationResult {
2
+ valid: boolean;
3
+ errors: string[];
4
+ }
5
+
6
+ export type ArtifactContent = Record<string, unknown>;
7
+
8
+ export function validateI983RequiredFields(
9
+ content: ArtifactContent,
10
+ params: { requiredFields: string[] }
11
+ ): ValidationResult {
12
+ const errors: string[] = [];
13
+ const extracted = (content.extractedFormData || content) as Record<string, unknown>;
14
+
15
+ for (const field of params.requiredFields) {
16
+ const value = extracted[field];
17
+ if (!value || (typeof value === 'string' && value.trim() === '')) {
18
+ const fieldName = field.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
19
+ errors.push(`Missing required field: ${fieldName}`);
20
+ }
21
+ }
22
+
23
+ return { valid: errors.length === 0, errors };
24
+ }
25
+
26
+ export function validateOPTDateRange(
27
+ content: ArtifactContent,
28
+ params: { field: string; minDaysFromNow: number; maxDaysFromNow: number }
29
+ ): ValidationResult {
30
+ const extracted = (content.extractedFormData || content) as Record<string, unknown>;
31
+ const dateStr = extracted[params.field];
32
+
33
+ if (!dateStr) {
34
+ return {
35
+ valid: false,
36
+ errors: [`Missing date field: ${params.field}`]
37
+ };
38
+ }
39
+
40
+ const date = new Date(String(dateStr));
41
+ const now = new Date();
42
+ now.setHours(0, 0, 0, 0);
43
+ date.setHours(0, 0, 0, 0);
44
+
45
+ const daysDiff = Math.floor((date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
46
+
47
+ if (isNaN(date.getTime())) {
48
+ return {
49
+ valid: false,
50
+ errors: [`Invalid date format for ${params.field}. Please use MM/DD/YYYY format.`]
51
+ };
52
+ }
53
+
54
+ if (daysDiff < params.minDaysFromNow) {
55
+ return {
56
+ valid: false,
57
+ errors: [`${params.field} must be at least ${params.minDaysFromNow} days from today. Current date is ${daysDiff} days from today.`]
58
+ };
59
+ }
60
+
61
+ if (daysDiff > params.maxDaysFromNow) {
62
+ return {
63
+ valid: false,
64
+ errors: [`${params.field} must be within ${params.maxDaysFromNow} days from today. Current date is ${daysDiff} days from today.`]
65
+ };
66
+ }
67
+
68
+ return { valid: true, errors: [] };
69
+ }
70
+
71
+ export function validateEVerifyFormat(
72
+ content: ArtifactContent,
73
+ params: { field: string }
74
+ ): ValidationResult {
75
+ const extracted = (content.extractedFormData || content) as Record<string, unknown>;
76
+ const value = extracted[params.field];
77
+
78
+ if (!value) {
79
+ return {
80
+ valid: false,
81
+ errors: ['Missing E-Verify number. Your employer must be registered with E-Verify.']
82
+ };
83
+ }
84
+
85
+ const eVerifyRegex = /^\d{8}$/;
86
+ const cleanedValue = String(value).replace(/\D/g, '');
87
+
88
+ if (!eVerifyRegex.test(cleanedValue)) {
89
+ return {
90
+ valid: false,
91
+ errors: [`E-Verify number must be exactly 8 digits (example: 12345678). Current value: ${value}`]
92
+ };
93
+ }
94
+
95
+ return { valid: true, errors: [] };
96
+ }
97
+
98
+ export function validateTrainingDescriptionLength(
99
+ content: ArtifactContent,
100
+ params: { field: string; minWords: number }
101
+ ): ValidationResult {
102
+ const extracted = (content.extractedFormData || content) as Record<string, unknown>;
103
+ const description = extracted[params.field];
104
+
105
+ if (!description) {
106
+ return {
107
+ valid: false,
108
+ errors: ['Training description is required. Please provide a detailed training plan.']
109
+ };
110
+ }
111
+
112
+ const wordCount = String(description).trim().split(/\s+/).filter(word => word.length > 0).length;
113
+
114
+ if (wordCount < params.minWords) {
115
+ return {
116
+ valid: false,
117
+ errors: [`Training description must be at least ${params.minWords} words. Currently ${wordCount} words. Add more details about learning objectives, tasks, and outcomes.`]
118
+ };
119
+ }
120
+
121
+ return { valid: true, errors: [] };
122
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Evaluation System
3
+ *
4
+ * Binary evaluation of agent artifacts against outcome success criteria.
5
+ * Now includes SWE-bench style code evaluation for engineering tasks.
6
+ * Also includes weighted scoring for granular evaluation.
7
+ *
8
+ * @module eval
9
+ */
10
+
11
+ // Sales/Email validators (original)
12
+ export {
13
+ type ValidationResult,
14
+ validateBuyingIntent,
15
+ validateCompanySize,
16
+ validateRole,
17
+ validateMessageLength,
18
+ validateEmail,
19
+ // Code Review validators (Tournament Seed Bounties)
20
+ validateSecurityIssue,
21
+ validatePerformanceIssue,
22
+ validateNoiseFreeness,
23
+ validateComplexityReduction,
24
+ validateExpertReview,
25
+ // Lead Gen validators (Tournament Seed Bounties)
26
+ validateLinkedIn,
27
+ validateLeadGenPrecision,
28
+ } from './validators.js';
29
+
30
+ // Core evaluation
31
+ export {
32
+ type CriterionResult,
33
+ type ArtifactContent,
34
+ type AgentArtifact,
35
+ type EvaluationResult,
36
+ evaluateOutcome,
37
+ } from './evaluateOutcome.js';
38
+
39
+ // SWE-bench style code validators
40
+ export {
41
+ type CodeArtifactContent,
42
+ type TestCase,
43
+ validateCodeSyntax,
44
+ validateCodeStructure,
45
+ validateTestCases,
46
+ validatePatchRelevance,
47
+ validateCodeQuality,
48
+ evaluateSWEBenchStyle,
49
+ } from './code-validators.js';
50
+
51
+ // Weighted scoring system
52
+ export {
53
+ type WeightedValidationResult,
54
+ type WeightedCriterion,
55
+ type CriterionEvaluationResult,
56
+ type ScoringResult,
57
+ type WeightedLeaderboardEntry,
58
+ validateWeights,
59
+ calculateWeightedScore,
60
+ calculateEfficiency,
61
+ rankLeaderboardEntries,
62
+ createLeaderboardEntry,
63
+ } from './weighted-scorer.js';
64
+
65
+ // AI-powered evaluation (AI Judge)
66
+ export {
67
+ type JudgeConfig,
68
+ AIJudgeError,
69
+ hashArtifact,
70
+ evaluateWithAIJudge,
71
+ validateJudgeConfig,
72
+ } from './ai-judge.js';
73
+
74
+ // Judge result caching
75
+ export {
76
+ type JudgeModel,
77
+ type JudgeResult,
78
+ type JudgeCache,
79
+ type CacheEntry,
80
+ type CacheStats,
81
+ type RedisClient,
82
+ InMemoryJudgeCache,
83
+ KVJudgeCache,
84
+ RedisJudgeCache,
85
+ getJudgeCache,
86
+ setJudgeCache,
87
+ resetJudgeCache,
88
+ createKVCache,
89
+ createRedisCache,
90
+ } from './judge-cache.js';
@@ -0,0 +1,402 @@
1
+ /**
2
+ * AI Judge Result Caching
3
+ *
4
+ * Provides caching for AI judge evaluation results to ensure idempotence.
5
+ * Identical artifacts evaluated with the same rubric return cached scores.
6
+ *
7
+ * Supports multiple cache backends:
8
+ * - In-memory (default, for development/testing)
9
+ * - KV (Cloudflare Workers KV)
10
+ * - Redis (for production deployments)
11
+ *
12
+ * @module eval/judge-cache
13
+ * @see Requirements 10.4
14
+ */
15
+
16
+ /**
17
+ * Supported AI judge models.
18
+ */
19
+ export type JudgeModel = 'gpt-4o' | 'claude-opus';
20
+
21
+ /**
22
+ * Result from an AI judge evaluation.
23
+ * Defined here to avoid circular dependency with ai-judge.ts.
24
+ */
25
+ export interface JudgeResult {
26
+ /** Numeric score from 0 to maxScore */
27
+ score: number;
28
+ /** Normalized score from 0.0 to 1.0 */
29
+ normalizedScore: number;
30
+ /** Detailed reasoning for the score */
31
+ reasoning: string;
32
+ /** Key highlights or notable aspects of the artifact */
33
+ highlights: string[];
34
+ /** The model that performed the evaluation */
35
+ model: JudgeModel;
36
+ /** Whether this result was retrieved from cache */
37
+ cached: boolean;
38
+ /** Timestamp of evaluation */
39
+ evaluatedAt: string;
40
+ }
41
+
42
+ /**
43
+ * Cache entry with metadata.
44
+ */
45
+ export interface CacheEntry {
46
+ /** The cached judge result */
47
+ result: JudgeResult;
48
+ /** Timestamp when the entry was created */
49
+ createdAt: string;
50
+ /** Number of times this entry has been accessed */
51
+ accessCount: number;
52
+ }
53
+
54
+ /**
55
+ * Interface for judge result cache implementations.
56
+ *
57
+ * @see Requirements 10.4
58
+ */
59
+ export interface JudgeCache {
60
+ /**
61
+ * Retrieves a cached judge result by key.
62
+ *
63
+ * @param key - Cache key (hash of artifact + rubric)
64
+ * @returns Cached JudgeResult or null if not found
65
+ */
66
+ get(key: string): Promise<JudgeResult | null>;
67
+
68
+ /**
69
+ * Stores a judge result in the cache.
70
+ *
71
+ * @param key - Cache key (hash of artifact + rubric)
72
+ * @param result - Judge result to cache
73
+ * @param ttlSeconds - Optional TTL in seconds (default: 7 days)
74
+ */
75
+ set(key: string, result: JudgeResult, ttlSeconds?: number): Promise<void>;
76
+
77
+ /**
78
+ * Removes a cached result.
79
+ *
80
+ * @param key - Cache key to remove
81
+ * @returns true if entry was removed, false if not found
82
+ */
83
+ delete(key: string): Promise<boolean>;
84
+
85
+ /**
86
+ * Clears all cached results.
87
+ */
88
+ clear(): Promise<void>;
89
+
90
+ /**
91
+ * Gets cache statistics.
92
+ */
93
+ stats(): Promise<CacheStats>;
94
+ }
95
+
96
+ /**
97
+ * Cache statistics.
98
+ */
99
+ export interface CacheStats {
100
+ /** Number of entries in the cache */
101
+ size: number;
102
+ /** Number of cache hits */
103
+ hits: number;
104
+ /** Number of cache misses */
105
+ misses: number;
106
+ /** Hit rate (hits / (hits + misses)) */
107
+ hitRate: number;
108
+ }
109
+
110
+ /**
111
+ * In-memory cache implementation for development and testing.
112
+ *
113
+ * @see Requirements 10.4
114
+ */
115
+ export class InMemoryJudgeCache implements JudgeCache {
116
+ private cache: Map<string, CacheEntry> = new Map();
117
+ private hits = 0;
118
+ private misses = 0;
119
+ private defaultTtlSeconds: number;
120
+
121
+ constructor(defaultTtlSeconds: number = 7 * 24 * 60 * 60) {
122
+ this.defaultTtlSeconds = defaultTtlSeconds;
123
+ }
124
+
125
+ async get(key: string): Promise<JudgeResult | null> {
126
+ const entry = this.cache.get(key);
127
+
128
+ if (!entry) {
129
+ this.misses++;
130
+ return null;
131
+ }
132
+
133
+ // Check if entry has expired
134
+ const createdAt = new Date(entry.createdAt).getTime();
135
+ const now = Date.now();
136
+ const ageSeconds = (now - createdAt) / 1000;
137
+
138
+ if (ageSeconds > this.defaultTtlSeconds) {
139
+ this.cache.delete(key);
140
+ this.misses++;
141
+ return null;
142
+ }
143
+
144
+ // Update access count
145
+ entry.accessCount++;
146
+ this.hits++;
147
+
148
+ return entry.result;
149
+ }
150
+
151
+ async set(key: string, result: JudgeResult, _ttlSeconds?: number): Promise<void> {
152
+ const entry: CacheEntry = {
153
+ result,
154
+ createdAt: new Date().toISOString(),
155
+ accessCount: 0,
156
+ };
157
+ this.cache.set(key, entry);
158
+ }
159
+
160
+ async delete(key: string): Promise<boolean> {
161
+ return this.cache.delete(key);
162
+ }
163
+
164
+ async clear(): Promise<void> {
165
+ this.cache.clear();
166
+ this.hits = 0;
167
+ this.misses = 0;
168
+ }
169
+
170
+ async stats(): Promise<CacheStats> {
171
+ const total = this.hits + this.misses;
172
+ return {
173
+ size: this.cache.size,
174
+ hits: this.hits,
175
+ misses: this.misses,
176
+ hitRate: total > 0 ? this.hits / total : 0,
177
+ };
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Cloudflare Workers KV cache implementation.
183
+ *
184
+ * @see Requirements 10.4
185
+ */
186
+ export class KVJudgeCache implements JudgeCache {
187
+ private kv: KVNamespace;
188
+ private prefix: string;
189
+ private hits = 0;
190
+ private misses = 0;
191
+ private defaultTtlSeconds: number;
192
+
193
+ constructor(
194
+ kv: KVNamespace,
195
+ prefix: string = 'judge:',
196
+ defaultTtlSeconds: number = 7 * 24 * 60 * 60
197
+ ) {
198
+ this.kv = kv;
199
+ this.prefix = prefix;
200
+ this.defaultTtlSeconds = defaultTtlSeconds;
201
+ }
202
+
203
+ private getKey(key: string): string {
204
+ return `${this.prefix}${key}`;
205
+ }
206
+
207
+ async get(key: string): Promise<JudgeResult | null> {
208
+ const fullKey = this.getKey(key);
209
+ const data = await this.kv.get(fullKey, 'json');
210
+
211
+ if (!data) {
212
+ this.misses++;
213
+ return null;
214
+ }
215
+
216
+ this.hits++;
217
+ return data as JudgeResult;
218
+ }
219
+
220
+ async set(key: string, result: JudgeResult, ttlSeconds?: number): Promise<void> {
221
+ const fullKey = this.getKey(key);
222
+ const ttl = ttlSeconds ?? this.defaultTtlSeconds;
223
+
224
+ await this.kv.put(fullKey, JSON.stringify(result), {
225
+ expirationTtl: ttl,
226
+ });
227
+ }
228
+
229
+ async delete(key: string): Promise<boolean> {
230
+ const fullKey = this.getKey(key);
231
+ await this.kv.delete(fullKey);
232
+ return true; // KV doesn't return whether key existed
233
+ }
234
+
235
+ async clear(): Promise<void> {
236
+ // KV doesn't support bulk delete, so we list and delete
237
+ const list = await this.kv.list({ prefix: this.prefix });
238
+ await Promise.all(list.keys.map((k) => this.kv.delete(k.name)));
239
+ this.hits = 0;
240
+ this.misses = 0;
241
+ }
242
+
243
+ async stats(): Promise<CacheStats> {
244
+ const list = await this.kv.list({ prefix: this.prefix });
245
+ const total = this.hits + this.misses;
246
+ return {
247
+ size: list.keys.length,
248
+ hits: this.hits,
249
+ misses: this.misses,
250
+ hitRate: total > 0 ? this.hits / total : 0,
251
+ };
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Redis cache implementation for production deployments.
257
+ *
258
+ * @see Requirements 10.4
259
+ */
260
+ export class RedisJudgeCache implements JudgeCache {
261
+ private redis: RedisClient;
262
+ private prefix: string;
263
+ private hits = 0;
264
+ private misses = 0;
265
+ private defaultTtlSeconds: number;
266
+
267
+ constructor(
268
+ redis: RedisClient,
269
+ prefix: string = 'judge:',
270
+ defaultTtlSeconds: number = 7 * 24 * 60 * 60
271
+ ) {
272
+ this.redis = redis;
273
+ this.prefix = prefix;
274
+ this.defaultTtlSeconds = defaultTtlSeconds;
275
+ }
276
+
277
+ private getKey(key: string): string {
278
+ return `${this.prefix}${key}`;
279
+ }
280
+
281
+ async get(key: string): Promise<JudgeResult | null> {
282
+ const fullKey = this.getKey(key);
283
+ const data = await this.redis.get(fullKey);
284
+
285
+ if (!data) {
286
+ this.misses++;
287
+ return null;
288
+ }
289
+
290
+ this.hits++;
291
+ return JSON.parse(data) as JudgeResult;
292
+ }
293
+
294
+ async set(key: string, result: JudgeResult, ttlSeconds?: number): Promise<void> {
295
+ const fullKey = this.getKey(key);
296
+ const ttl = ttlSeconds ?? this.defaultTtlSeconds;
297
+
298
+ await this.redis.setex(fullKey, ttl, JSON.stringify(result));
299
+ }
300
+
301
+ async delete(key: string): Promise<boolean> {
302
+ const fullKey = this.getKey(key);
303
+ const deleted = await this.redis.del(fullKey);
304
+ return deleted > 0;
305
+ }
306
+
307
+ async clear(): Promise<void> {
308
+ const keys = await this.redis.keys(`${this.prefix}*`);
309
+ if (keys.length > 0) {
310
+ await this.redis.del(...keys);
311
+ }
312
+ this.hits = 0;
313
+ this.misses = 0;
314
+ }
315
+
316
+ async stats(): Promise<CacheStats> {
317
+ const keys = await this.redis.keys(`${this.prefix}*`);
318
+ const total = this.hits + this.misses;
319
+ return {
320
+ size: keys.length,
321
+ hits: this.hits,
322
+ misses: this.misses,
323
+ hitRate: total > 0 ? this.hits / total : 0,
324
+ };
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Minimal Redis client interface for type safety.
330
+ * Compatible with ioredis and node-redis.
331
+ */
332
+ export interface RedisClient {
333
+ get(key: string): Promise<string | null>;
334
+ setex(key: string, seconds: number, value: string): Promise<string>;
335
+ del(...keys: string[]): Promise<number>;
336
+ keys(pattern: string): Promise<string[]>;
337
+ }
338
+
339
+ /**
340
+ * Cloudflare Workers KV namespace interface.
341
+ */
342
+ interface KVNamespace {
343
+ get(key: string, type: 'json'): Promise<unknown>;
344
+ put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;
345
+ delete(key: string): Promise<void>;
346
+ list(options?: { prefix?: string }): Promise<{ keys: Array<{ name: string }> }>;
347
+ }
348
+
349
+ // Global cache instance
350
+ let globalCache: JudgeCache | null = null;
351
+
352
+ /**
353
+ * Gets the global judge cache instance.
354
+ * Creates an in-memory cache if not already initialized.
355
+ *
356
+ * @returns The global JudgeCache instance
357
+ */
358
+ export function getJudgeCache(): JudgeCache {
359
+ if (!globalCache) {
360
+ globalCache = new InMemoryJudgeCache();
361
+ }
362
+ return globalCache;
363
+ }
364
+
365
+ /**
366
+ * Sets the global judge cache instance.
367
+ * Use this to configure a KV or Redis cache in production.
368
+ *
369
+ * @param cache - The cache instance to use globally
370
+ */
371
+ export function setJudgeCache(cache: JudgeCache): void {
372
+ globalCache = cache;
373
+ }
374
+
375
+ /**
376
+ * Resets the global cache (primarily for testing).
377
+ */
378
+ export function resetJudgeCache(): void {
379
+ globalCache = null;
380
+ }
381
+
382
+ /**
383
+ * Creates a KV-backed judge cache.
384
+ *
385
+ * @param kv - Cloudflare Workers KV namespace
386
+ * @param prefix - Key prefix (default: 'judge:')
387
+ * @returns KVJudgeCache instance
388
+ */
389
+ export function createKVCache(kv: KVNamespace, prefix?: string): JudgeCache {
390
+ return new KVJudgeCache(kv, prefix);
391
+ }
392
+
393
+ /**
394
+ * Creates a Redis-backed judge cache.
395
+ *
396
+ * @param redis - Redis client instance
397
+ * @param prefix - Key prefix (default: 'judge:')
398
+ * @returns RedisJudgeCache instance
399
+ */
400
+ export function createRedisCache(redis: RedisClient, prefix?: string): JudgeCache {
401
+ return new RedisJudgeCache(redis, prefix);
402
+ }