thevoidforge 21.0.11 → 21.0.13

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 (108) hide show
  1. package/dist/.claude/commands/ai.md +69 -0
  2. package/dist/.claude/commands/architect.md +121 -0
  3. package/dist/.claude/commands/assemble.md +201 -0
  4. package/dist/.claude/commands/assess.md +75 -0
  5. package/dist/.claude/commands/blueprint.md +135 -0
  6. package/dist/.claude/commands/build.md +116 -0
  7. package/dist/.claude/commands/campaign.md +201 -0
  8. package/dist/.claude/commands/cultivation.md +166 -0
  9. package/dist/.claude/commands/current.md +128 -0
  10. package/dist/.claude/commands/dangerroom.md +74 -0
  11. package/dist/.claude/commands/debrief.md +178 -0
  12. package/dist/.claude/commands/deploy.md +99 -0
  13. package/dist/.claude/commands/devops.md +143 -0
  14. package/dist/.claude/commands/gauntlet.md +140 -0
  15. package/dist/.claude/commands/git.md +104 -0
  16. package/dist/.claude/commands/grow.md +146 -0
  17. package/dist/.claude/commands/imagine.md +126 -0
  18. package/dist/.claude/commands/portfolio.md +50 -0
  19. package/dist/.claude/commands/prd.md +113 -0
  20. package/dist/.claude/commands/qa.md +107 -0
  21. package/dist/.claude/commands/review.md +151 -0
  22. package/dist/.claude/commands/security.md +100 -0
  23. package/dist/.claude/commands/test.md +96 -0
  24. package/dist/.claude/commands/thumper.md +116 -0
  25. package/dist/.claude/commands/treasury.md +100 -0
  26. package/dist/.claude/commands/ux.md +118 -0
  27. package/dist/.claude/commands/vault.md +189 -0
  28. package/dist/.claude/commands/void.md +108 -0
  29. package/dist/CHANGELOG.md +1918 -0
  30. package/dist/CLAUDE.md +250 -0
  31. package/dist/HOLOCRON.md +856 -0
  32. package/dist/VERSION.md +123 -0
  33. package/dist/docs/NAMING_REGISTRY.md +478 -0
  34. package/dist/docs/methods/AI_INTELLIGENCE.md +276 -0
  35. package/dist/docs/methods/ASSEMBLER.md +142 -0
  36. package/dist/docs/methods/BACKEND_ENGINEER.md +165 -0
  37. package/dist/docs/methods/BUILD_JOURNAL.md +185 -0
  38. package/dist/docs/methods/BUILD_PROTOCOL.md +426 -0
  39. package/dist/docs/methods/CAMPAIGN.md +568 -0
  40. package/dist/docs/methods/CONTEXT_MANAGEMENT.md +189 -0
  41. package/dist/docs/methods/DEEP_CURRENT.md +184 -0
  42. package/dist/docs/methods/DEVOPS_ENGINEER.md +295 -0
  43. package/dist/docs/methods/FIELD_MEDIC.md +261 -0
  44. package/dist/docs/methods/FORGE_ARTIST.md +108 -0
  45. package/dist/docs/methods/FORGE_KEEPER.md +268 -0
  46. package/dist/docs/methods/GAUNTLET.md +344 -0
  47. package/dist/docs/methods/GROWTH_STRATEGIST.md +466 -0
  48. package/dist/docs/methods/HEARTBEAT.md +168 -0
  49. package/dist/docs/methods/MCP_INTEGRATION.md +139 -0
  50. package/dist/docs/methods/MUSTER.md +148 -0
  51. package/dist/docs/methods/PRD_GENERATOR.md +186 -0
  52. package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +250 -0
  53. package/dist/docs/methods/QA_ENGINEER.md +337 -0
  54. package/dist/docs/methods/RELEASE_MANAGER.md +145 -0
  55. package/dist/docs/methods/SECURITY_AUDITOR.md +320 -0
  56. package/dist/docs/methods/SUB_AGENTS.md +335 -0
  57. package/dist/docs/methods/SYSTEMS_ARCHITECT.md +171 -0
  58. package/dist/docs/methods/TESTING.md +359 -0
  59. package/dist/docs/methods/THUMPER.md +175 -0
  60. package/dist/docs/methods/TIME_VAULT.md +120 -0
  61. package/dist/docs/methods/TREASURY.md +184 -0
  62. package/dist/docs/methods/TROUBLESHOOTING.md +265 -0
  63. package/dist/docs/patterns/README.md +52 -0
  64. package/dist/docs/patterns/ad-billing-adapter.ts +537 -0
  65. package/dist/docs/patterns/ad-platform-adapter.ts +421 -0
  66. package/dist/docs/patterns/ai-classifier.ts +195 -0
  67. package/dist/docs/patterns/ai-eval.ts +272 -0
  68. package/dist/docs/patterns/ai-orchestrator.ts +341 -0
  69. package/dist/docs/patterns/ai-router.ts +194 -0
  70. package/dist/docs/patterns/ai-tool-schema.ts +237 -0
  71. package/dist/docs/patterns/api-route.ts +241 -0
  72. package/dist/docs/patterns/backtest-engine.ts +499 -0
  73. package/dist/docs/patterns/browser-review.ts +292 -0
  74. package/dist/docs/patterns/combobox.tsx +300 -0
  75. package/dist/docs/patterns/component.tsx +262 -0
  76. package/dist/docs/patterns/daemon-process.ts +338 -0
  77. package/dist/docs/patterns/data-pipeline.ts +297 -0
  78. package/dist/docs/patterns/database-migration.ts +466 -0
  79. package/dist/docs/patterns/e2e-test.ts +629 -0
  80. package/dist/docs/patterns/error-handling.ts +312 -0
  81. package/dist/docs/patterns/execution-safety.ts +601 -0
  82. package/dist/docs/patterns/financial-transaction.ts +342 -0
  83. package/dist/docs/patterns/funding-plan.ts +462 -0
  84. package/dist/docs/patterns/game-entity.ts +137 -0
  85. package/dist/docs/patterns/game-loop.ts +113 -0
  86. package/dist/docs/patterns/game-state.ts +143 -0
  87. package/dist/docs/patterns/job-queue.ts +225 -0
  88. package/dist/docs/patterns/kongo-integration.ts +164 -0
  89. package/dist/docs/patterns/middleware.ts +363 -0
  90. package/dist/docs/patterns/mobile-screen.tsx +139 -0
  91. package/dist/docs/patterns/mobile-service.ts +167 -0
  92. package/dist/docs/patterns/multi-tenant.ts +382 -0
  93. package/dist/docs/patterns/oauth-token-lifecycle.ts +223 -0
  94. package/dist/docs/patterns/outbound-rate-limiter.ts +260 -0
  95. package/dist/docs/patterns/prompt-template.ts +195 -0
  96. package/dist/docs/patterns/revenue-source-adapter.ts +311 -0
  97. package/dist/docs/patterns/service.ts +224 -0
  98. package/dist/docs/patterns/sse-endpoint.ts +118 -0
  99. package/dist/docs/patterns/stablecoin-adapter.ts +511 -0
  100. package/dist/docs/patterns/third-party-script.ts +68 -0
  101. package/dist/scripts/thumper/gom-jabbar.sh +241 -0
  102. package/dist/scripts/thumper/relay.sh +610 -0
  103. package/dist/scripts/thumper/scan.sh +359 -0
  104. package/dist/scripts/thumper/thumper.sh +190 -0
  105. package/dist/scripts/thumper/water-rings.sh +76 -0
  106. package/dist/wizard/ui/index.html +1 -1
  107. package/package.json +1 -1
  108. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Pattern: Ad Platform Adapter (Split Interface)
3
+ *
4
+ * Key principles:
5
+ * - Separate interactive setup (browser OAuth) from runtime operations (daemon)
6
+ * - AdPlatformSetup runs in CLI/Danger Room (interactive, user-present)
7
+ * - AdPlatformAdapter runs in heartbeat daemon (non-interactive, autonomous)
8
+ * - All amounts in integer cents (Cents branded type) — never float
9
+ * - Rate limiting per-platform with token bucket
10
+ * - Errors normalized to common PlatformError format
11
+ * - Idempotency keys on all write operations (WAL pattern per ADR-3)
12
+ *
13
+ * Agents: Breeze (platform relations), Wax (paid ads), Dockson (treasury)
14
+ *
15
+ * PRD Reference: §9.5, §9.19.10, §9.20.4
16
+ *
17
+ * Authorization Guard (§9.20.4):
18
+ * Daemon Tier 1 jobs receive ReadOnlyAdapter (pause, read operations only).
19
+ * Authenticated external commands receive full AdPlatformAdapter.
20
+ * See financial-transaction.ts for the Cents branded type.
21
+ */
22
+
23
+ // ── Branded Financial Types (§9.17) ───────────────────
24
+
25
+ type Cents = number & { readonly __brand: 'Cents' };
26
+ type Percentage = number & { readonly __brand: 'Percentage' };
27
+ type Ratio = number & { readonly __brand: 'Ratio' };
28
+
29
+ function toCents(dollars: number): Cents {
30
+ return Math.round(dollars * 100) as Cents;
31
+ }
32
+
33
+ function toDollars(cents: Cents): number {
34
+ return cents / 100;
35
+ }
36
+
37
+ // ── Platform Types ────────────────────────────────────
38
+
39
+ type AdPlatform = 'meta' | 'google' | 'tiktok' | 'linkedin' | 'twitter' | 'reddit' | 'snap';
40
+
41
+ interface OAuthTokens {
42
+ accessToken: string;
43
+ refreshToken: string;
44
+ expiresAt: string; // ISO 8601
45
+ platform: AdPlatform;
46
+ scopes: string[];
47
+ }
48
+
49
+ interface ConnectionStatus {
50
+ connected: boolean;
51
+ accountId?: string;
52
+ accountName?: string;
53
+ currency?: string; // ISO 4217 — must be 'USD' per ADR-6
54
+ error?: string;
55
+ }
56
+
57
+ // ── Error Normalization ───────────────────────────────
58
+
59
+ interface PlatformError {
60
+ platform: AdPlatform;
61
+ code: 'RATE_LIMITED' | 'AUTH_EXPIRED' | 'BUDGET_EXCEEDED' | 'CREATIVE_REJECTED' | 'UNKNOWN';
62
+ originalCode: number;
63
+ message: string;
64
+ retryable: boolean;
65
+ retryAfter?: number; // seconds
66
+ }
67
+
68
+ // ── Campaign Types ────────────────────────────────────
69
+
70
+ interface CampaignConfig {
71
+ name: string;
72
+ platform: AdPlatform;
73
+ objective: 'awareness' | 'traffic' | 'conversions';
74
+ dailyBudget: Cents;
75
+ targeting: {
76
+ audiences: string[];
77
+ locations: string[];
78
+ ageRange?: [number, number];
79
+ interests?: string[];
80
+ };
81
+ creative: {
82
+ headlines: string[];
83
+ descriptions: string[];
84
+ callToAction: string;
85
+ landingUrl: string;
86
+ imageUrls?: string[];
87
+ };
88
+ testGroupId?: string;
89
+ testVariant?: string;
90
+ schedule?: {
91
+ startDate?: string;
92
+ endDate?: string;
93
+ };
94
+ idempotencyKey: string; // UUID, per ADR-3
95
+ complianceStatus: 'passed' | 'pending';
96
+ }
97
+
98
+ interface CampaignResult {
99
+ externalId: string;
100
+ platform: AdPlatform;
101
+ status: 'created' | 'pending_review';
102
+ dashboardUrl: string;
103
+ }
104
+
105
+ interface CampaignUpdate {
106
+ name?: string;
107
+ dailyBudget?: Cents;
108
+ targeting?: Partial<CampaignConfig['targeting']>;
109
+ schedule?: CampaignConfig['schedule'];
110
+ }
111
+
112
+ interface CreativeConfig {
113
+ headlines?: string[];
114
+ descriptions?: string[];
115
+ callToAction?: string;
116
+ landingUrl?: string;
117
+ imageUrls?: string[];
118
+ }
119
+
120
+ interface SpendReport {
121
+ platform: AdPlatform;
122
+ dateRange: { start: string; end: string };
123
+ totalSpend: Cents;
124
+ campaigns: Array<{
125
+ externalId: string;
126
+ spend: Cents;
127
+ impressions: number;
128
+ clicks: number;
129
+ conversions: number;
130
+ }>;
131
+ }
132
+
133
+ interface PerformanceMetrics {
134
+ campaignId: string;
135
+ impressions: number;
136
+ clicks: number;
137
+ conversions: number;
138
+ spend: Cents;
139
+ ctr: Percentage;
140
+ cpc: Cents;
141
+ roas: Ratio;
142
+ }
143
+
144
+ interface InsightData {
145
+ campaignId: string;
146
+ metrics: Record<string, number>;
147
+ recommendations?: string[];
148
+ }
149
+
150
+ // ── Interactive Setup Interface ───────────────────────
151
+ // Runs in CLI or Danger Room — requires user interaction (browser OAuth)
152
+
153
+ interface AdPlatformSetup {
154
+ /** Interactive OAuth flow — opens browser for authorization */
155
+ authenticate(): Promise<OAuthTokens>;
156
+
157
+ /** Verify the connection works and return account info */
158
+ verifyConnection(tokens: OAuthTokens): Promise<ConnectionStatus>;
159
+
160
+ /** Detect account currency for ADR-6 enforcement */
161
+ detectCurrency(tokens: OAuthTokens): Promise<string>;
162
+ }
163
+
164
+ // ── Runtime Adapter Interface ─────────────────────────
165
+ // Runs in the heartbeat daemon — non-interactive, autonomous
166
+
167
+ interface AdPlatformAdapter {
168
+ // Token management
169
+ refreshToken(token: OAuthTokens): Promise<OAuthTokens>;
170
+
171
+ // Campaign CRUD (requires FullAdapter auth — see Authorization Guard)
172
+ createCampaign(config: CampaignConfig): Promise<CampaignResult>;
173
+ updateCampaign(id: string, changes: CampaignUpdate): Promise<void>;
174
+ pauseCampaign(id: string): Promise<void>;
175
+ resumeCampaign(id: string): Promise<void>;
176
+ deleteCampaign(id: string): Promise<void>;
177
+
178
+ // Budget and creative (requires FullAdapter auth)
179
+ updateBudget(id: string, dailyBudget: Cents): Promise<void>;
180
+ updateCreative(id: string, creative: CreativeConfig): Promise<void>;
181
+
182
+ // Reporting (read-only — available to ReadOnlyAdapter)
183
+ getSpend(dateRange: { start: string; end: string }): Promise<SpendReport>;
184
+ getPerformance(campaignId: string): Promise<PerformanceMetrics>;
185
+ getInsights(campaignId: string, metrics: string[]): Promise<InsightData>;
186
+
187
+ // Webhooks (deferred to remote mode per ADR-5)
188
+ handleWebhook?(payload: unknown): Promise<{ processed: boolean }>;
189
+ verifyWebhookSignature?(payload: Buffer, signature: string): boolean;
190
+ }
191
+
192
+ // ── Authorization Guard (§9.20.4) ─────────────────────
193
+ // Daemon Tier 1 jobs receive this restricted interface
194
+
195
+ type ReadOnlyAdapter = Pick<AdPlatformAdapter,
196
+ 'pauseCampaign' | // Protective — stopping spend is always safe
197
+ 'getSpend' |
198
+ 'getPerformance' |
199
+ 'getInsights' |
200
+ 'refreshToken'
201
+ >;
202
+
203
+ // Authenticated external commands receive the full AdPlatformAdapter
204
+ //
205
+ // ── Adapter Caching Rule (field report #258) ────────────
206
+ // Stateful adapters (in-memory campaign store, e.g. sandbox) MUST be cached
207
+ // per platform — one instance per platform key. Creating a new instance per
208
+ // call loses state between operations (campaigns created become invisible).
209
+ // Stateless adapters (HTTP clients calling external APIs) can be created per
210
+ // call since they hold no local state. Use a module-level Map<string, Adapter>
211
+ // for caching when the adapter's constructor initializes mutable collections.
212
+
213
+ // ── Reference Implementation: Meta Marketing API ──────
214
+
215
+ class MetaAdapter implements AdPlatformSetup, AdPlatformAdapter {
216
+ private readonly baseUrl = 'https://graph.facebook.com/v19.0';
217
+ private rateLimiter: TokenBucketLimiter;
218
+
219
+ constructor(private adAccountId: string) {
220
+ // Meta: 200 calls/hr/ad account (sliding window)
221
+ this.rateLimiter = new TokenBucketLimiter({ capacity: 200, refillRate: 200 / 3600 });
222
+ }
223
+
224
+ // ── Setup (interactive) ──────────
225
+
226
+ async authenticate(): Promise<OAuthTokens> {
227
+ // 1. Open browser to Facebook Login
228
+ // 2. User authorizes → callback with short-lived token
229
+ // 3. Exchange for long-lived token (60 days)
230
+ // 4. Return OAuthTokens
231
+ throw new Error('Interactive OAuth — implement per platform');
232
+ }
233
+
234
+ async verifyConnection(tokens: OAuthTokens): Promise<ConnectionStatus> {
235
+ const res = await this.apiCall('GET', `/act_${this.adAccountId}`, tokens, {
236
+ fields: 'name,currency,account_status'
237
+ });
238
+ return {
239
+ connected: true,
240
+ accountId: this.adAccountId,
241
+ accountName: res.name as string | undefined,
242
+ currency: res.currency as string | undefined,
243
+ };
244
+ }
245
+
246
+ async detectCurrency(tokens: OAuthTokens): Promise<string> {
247
+ const status = await this.verifyConnection(tokens);
248
+ return status.currency ?? 'USD';
249
+ }
250
+
251
+ // ── Runtime (daemon) ─────────────
252
+
253
+ async refreshToken(token: OAuthTokens): Promise<OAuthTokens> {
254
+ // Meta long-lived tokens: exchange at 80% of 60-day TTL
255
+ const res = await this.apiCall('GET', '/oauth/access_token', token, {
256
+ grant_type: 'fb_exchange_token',
257
+ fb_exchange_token: token.accessToken,
258
+ });
259
+ return { ...token, accessToken: res.access_token as string, expiresAt: res.expires_at as string };
260
+ }
261
+
262
+ async createCampaign(config: CampaignConfig): Promise<CampaignResult> {
263
+ await this.rateLimiter.acquire();
264
+ const res = await this.apiCall('POST', `/act_${this.adAccountId}/campaigns`, undefined, {
265
+ name: config.name,
266
+ objective: this.mapObjective(config.objective),
267
+ status: 'PAUSED', // Create paused, activate after ad set + ad creation
268
+ special_ad_categories: [],
269
+ });
270
+ return {
271
+ externalId: res.id as string,
272
+ platform: 'meta',
273
+ status: 'created',
274
+ dashboardUrl: `https://www.facebook.com/adsmanager/manage/campaigns?act=${this.adAccountId}&campaign_ids=${res.id}`,
275
+ };
276
+ }
277
+
278
+ async pauseCampaign(id: string): Promise<void> {
279
+ await this.rateLimiter.acquire();
280
+ await this.apiCall('POST', `/${id}`, undefined, { status: 'PAUSED' });
281
+ }
282
+
283
+ async resumeCampaign(id: string): Promise<void> {
284
+ await this.rateLimiter.acquire();
285
+ await this.apiCall('POST', `/${id}`, undefined, { status: 'ACTIVE' });
286
+ }
287
+
288
+ async deleteCampaign(id: string): Promise<void> {
289
+ await this.rateLimiter.acquire();
290
+ await this.apiCall('DELETE', `/${id}`, undefined, {});
291
+ }
292
+
293
+ async updateCampaign(id: string, changes: CampaignUpdate): Promise<void> {
294
+ await this.rateLimiter.acquire();
295
+ const params: Record<string, unknown> = {};
296
+ if (changes.name) params.name = changes.name;
297
+ // Budget changes go through ad set, not campaign on Meta
298
+ await this.apiCall('POST', `/${id}`, undefined, params);
299
+ }
300
+
301
+ async updateBudget(id: string, dailyBudget: Cents): Promise<void> {
302
+ await this.rateLimiter.acquire();
303
+ // Meta budgets are in the account's currency smallest unit (cents for USD)
304
+ await this.apiCall('POST', `/${id}`, undefined, { daily_budget: dailyBudget });
305
+ }
306
+
307
+ async updateCreative(id: string, creative: CreativeConfig): Promise<void> {
308
+ await this.rateLimiter.acquire();
309
+ // Creative updates go through the ad object, not campaign
310
+ throw new Error('Creative update requires ad-level API call — implement per ad structure');
311
+ }
312
+
313
+ async getSpend(dateRange: { start: string; end: string }): Promise<SpendReport> {
314
+ await this.rateLimiter.acquire();
315
+ const res = await this.apiCall('GET', `/act_${this.adAccountId}/insights`, undefined, {
316
+ fields: 'campaign_id,spend,impressions,clicks,conversions',
317
+ time_range: JSON.stringify({ since: dateRange.start, until: dateRange.end }),
318
+ level: 'campaign',
319
+ });
320
+ const resData = res.data as Array<Record<string, string>>;
321
+ return {
322
+ platform: 'meta',
323
+ dateRange,
324
+ totalSpend: toCents(resData.reduce((sum: number, r: Record<string, string>) => sum + parseFloat(r.spend), 0)),
325
+ campaigns: resData.map((r: Record<string, string>) => ({
326
+ externalId: r.campaign_id,
327
+ spend: toCents(parseFloat(r.spend)),
328
+ impressions: parseInt(r.impressions),
329
+ clicks: parseInt(r.clicks),
330
+ conversions: parseInt(r.conversions || '0'),
331
+ })),
332
+ };
333
+ }
334
+
335
+ async getPerformance(campaignId: string): Promise<PerformanceMetrics> {
336
+ await this.rateLimiter.acquire();
337
+ const res = await this.apiCall('GET', `/${campaignId}/insights`, undefined, {
338
+ fields: 'impressions,clicks,conversions,spend,ctr,cpc',
339
+ });
340
+ const d = (res.data as Array<Record<string, string>>)[0];
341
+ const spend = toCents(parseFloat(d.spend));
342
+ const conversions = parseInt(d.conversions || '0');
343
+ const revenue = 0 as Cents; // Revenue comes from Stripe, not the ad platform
344
+ return {
345
+ campaignId,
346
+ impressions: parseInt(d.impressions),
347
+ clicks: parseInt(d.clicks),
348
+ conversions,
349
+ spend,
350
+ ctr: parseFloat(d.ctr) as Percentage,
351
+ cpc: toCents(parseFloat(d.cpc)),
352
+ roas: (revenue > 0 ? toDollars(revenue) / toDollars(spend) : 0) as Ratio,
353
+ };
354
+ }
355
+
356
+ async getInsights(campaignId: string, metrics: string[]): Promise<InsightData> {
357
+ await this.rateLimiter.acquire();
358
+ const res = await this.apiCall('GET', `/${campaignId}/insights`, undefined, {
359
+ fields: metrics.join(','),
360
+ });
361
+ return { campaignId, metrics: (res.data as Array<Record<string, number>>)[0] ?? {} };
362
+ }
363
+
364
+ // ── Helpers ──────────────────────
365
+
366
+ private mapObjective(obj: CampaignConfig['objective']): string {
367
+ const map = { awareness: 'OUTCOME_AWARENESS', traffic: 'OUTCOME_TRAFFIC', conversions: 'OUTCOME_SALES' };
368
+ return map[obj];
369
+ }
370
+
371
+ private async apiCall(
372
+ method: string, path: string, tokens: OAuthTokens | undefined, params: Record<string, unknown>
373
+ ): Promise<Record<string, unknown>> {
374
+ // Sanitize platform response data per §9.19.16
375
+ // All string fields: strip HTML tags, escape <>&"', truncate to 500 chars
376
+ // Use idempotency key for POST/DELETE per ADR-3
377
+ throw new Error('HTTP implementation — use node:https, no SDK dependencies');
378
+ }
379
+ }
380
+
381
+ // ── Token Bucket Rate Limiter ─────────────────────────
382
+
383
+ class TokenBucketLimiter {
384
+ private tokens: number;
385
+ private lastRefill: number;
386
+ private readonly capacity: number;
387
+ private readonly refillRate: number; // tokens per second
388
+
389
+ constructor(opts: { capacity: number; refillRate: number }) {
390
+ this.capacity = opts.capacity;
391
+ this.refillRate = opts.refillRate;
392
+ this.tokens = opts.capacity;
393
+ this.lastRefill = Date.now();
394
+ }
395
+
396
+ async acquire(): Promise<void> {
397
+ this.refill();
398
+ if (this.tokens < 1) {
399
+ const waitMs = (1 / this.refillRate) * 1000;
400
+ await new Promise(resolve => setTimeout(resolve, waitMs));
401
+ this.refill();
402
+ }
403
+ this.tokens -= 1;
404
+ }
405
+
406
+ private refill(): void {
407
+ const now = Date.now();
408
+ const elapsed = (now - this.lastRefill) / 1000;
409
+ this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
410
+ this.lastRefill = now;
411
+ }
412
+ }
413
+
414
+ export type {
415
+ AdPlatformSetup, AdPlatformAdapter, ReadOnlyAdapter,
416
+ CampaignConfig, CampaignResult, CampaignUpdate, CreativeConfig,
417
+ SpendReport, PerformanceMetrics, InsightData,
418
+ OAuthTokens, ConnectionStatus, PlatformError,
419
+ Cents, Percentage, Ratio, AdPlatform,
420
+ };
421
+ export { toCents, toDollars, MetaAdapter, TokenBucketLimiter };
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Pattern: AI Classifier
3
+ *
4
+ * Key principles:
5
+ * - Every classification has a confidence score — never act on low confidence blindly
6
+ * - Confidence-based routing: high → auto-act, medium → soft-act, low → human review
7
+ * - Fallback chain: primary model → fallback model → rule-based → human queue
8
+ * - Log every classification for eval and drift detection
9
+ * - Never expose raw model confidence to users — map to business thresholds
10
+ *
11
+ * Agents: Picard (routing architecture), Stark (service layer), Batman (edge cases)
12
+ *
13
+ * Provider note: Primary examples use Anthropic SDK. OpenAI adaptation noted inline.
14
+ */
15
+
16
+ import Anthropic from '@anthropic-ai/sdk'
17
+ import { z } from 'zod'
18
+
19
+ // --- Core types ---
20
+
21
+ export interface ClassificationResult<T extends string> {
22
+ label: T
23
+ confidence: number // 0.0 - 1.0
24
+ reasoning: string // Model's explanation — logged, not shown to users
25
+ model: string // Which model produced this result
26
+ latencyMs: number
27
+ }
28
+
29
+ // Thresholds are business decisions, not model tuning knobs.
30
+ // Adjust based on eval results (see ai-eval.ts), not gut feeling.
31
+ export const CONFIDENCE_THRESHOLD = {
32
+ AUTO_ACT: 0.85, // High confidence — act without human review
33
+ SOFT_ACT: 0.60, // Medium — act but flag for async review
34
+ REJECT: 0.60, // Below this — route to human queue
35
+ } as const
36
+
37
+ // --- Single-label classifier ---
38
+
39
+ const ClassifierOutputSchema = z.object({
40
+ label: z.string(),
41
+ confidence: z.number().min(0).max(1),
42
+ reasoning: z.string(),
43
+ })
44
+
45
+ export async function classify<T extends string>(
46
+ client: Anthropic,
47
+ text: string,
48
+ labels: readonly T[],
49
+ systemPrompt: string
50
+ ): Promise<ClassificationResult<T>> {
51
+ const start = Date.now()
52
+
53
+ const response = await client.messages.create({
54
+ model: 'claude-sonnet-4-20250514',
55
+ max_tokens: 256,
56
+ system: [
57
+ systemPrompt,
58
+ `Valid labels: ${labels.join(', ')}`,
59
+ 'Respond with JSON: { "label": "<label>", "confidence": <0.0-1.0>, "reasoning": "<why>" }',
60
+ ].join('\n'),
61
+ messages: [{ role: 'user', content: text }],
62
+ })
63
+
64
+ const content = response.content[0]
65
+ if (content.type !== 'text') throw new Error('Expected text response')
66
+
67
+ const parsed = ClassifierOutputSchema.parse(JSON.parse(content.text))
68
+
69
+ // Validate label is one of the allowed values
70
+ if (!labels.includes(parsed.label as T)) {
71
+ throw new Error(`Model returned invalid label: ${parsed.label}`)
72
+ }
73
+
74
+ return {
75
+ label: parsed.label as T,
76
+ confidence: parsed.confidence,
77
+ reasoning: parsed.reasoning,
78
+ model: 'claude-sonnet-4-20250514',
79
+ latencyMs: Date.now() - start,
80
+ }
81
+ }
82
+
83
+ // OpenAI adaptation:
84
+ // const response = await openai.chat.completions.create({
85
+ // model: 'gpt-4o-mini', // Cheaper model for classification
86
+ // messages: [...],
87
+ // response_format: { type: 'json_object' },
88
+ // })
89
+ // Parse response.choices[0].message.content the same way.
90
+
91
+ // --- Confidence-based routing ---
92
+
93
+ type RouteAction = 'auto_act' | 'soft_act' | 'human_review'
94
+
95
+ export function routeByConfidence<T extends string>(
96
+ result: ClassificationResult<T>
97
+ ): RouteAction {
98
+ if (result.confidence >= CONFIDENCE_THRESHOLD.AUTO_ACT) return 'auto_act'
99
+ if (result.confidence >= CONFIDENCE_THRESHOLD.SOFT_ACT) return 'soft_act'
100
+ return 'human_review'
101
+ }
102
+
103
+ // Usage:
104
+ // const result = await classify(client, ticket.body, ['billing', 'technical', 'general'], prompt)
105
+ // const action = routeByConfidence(result)
106
+ // if (action === 'auto_act') await autoRoute(ticket, result.label)
107
+ // else if (action === 'soft_act') await autoRoute(ticket, result.label, { flagForReview: true })
108
+ // else await queueForHuman(ticket, result)
109
+
110
+ // --- Fallback chain ---
111
+
112
+ interface ClassifierProvider<T extends string> {
113
+ name: string
114
+ classify: (text: string, labels: readonly T[]) => Promise<ClassificationResult<T>>
115
+ }
116
+
117
+ /**
118
+ * Try classifiers in order. First one to return high-confidence wins.
119
+ * If all fail or return low confidence, returns the best result with 'human_review' routing.
120
+ */
121
+ export async function classifyWithFallback<T extends string>(
122
+ text: string,
123
+ labels: readonly T[],
124
+ providers: ClassifierProvider<T>[]
125
+ ): Promise<ClassificationResult<T> & { action: RouteAction }> {
126
+ let bestResult: ClassificationResult<T> | null = null
127
+
128
+ for (const provider of providers) {
129
+ try {
130
+ const result = await provider.classify(text, labels)
131
+
132
+ // Keep track of best result across all providers
133
+ if (!bestResult || result.confidence > bestResult.confidence) {
134
+ bestResult = result
135
+ }
136
+
137
+ // If confidence is high enough to act, stop the chain
138
+ if (result.confidence >= CONFIDENCE_THRESHOLD.SOFT_ACT) {
139
+ return { ...result, action: routeByConfidence(result) }
140
+ }
141
+
142
+ // Low confidence — try next provider in chain
143
+ } catch (error) {
144
+ // Provider failed — log and try next
145
+ console.error(`[Classifier] ${provider.name} failed:`, {
146
+ error: error instanceof Error ? error.message : 'Unknown',
147
+ text: text.slice(0, 100), // Truncate for logging — never log full PII
148
+ })
149
+ }
150
+ }
151
+
152
+ // All providers exhausted — return best result or default to human review
153
+ if (bestResult) {
154
+ return { ...bestResult, action: 'human_review' }
155
+ }
156
+
157
+ // Complete failure — nothing classified, must go to human
158
+ return {
159
+ label: labels[0], // Default label — human will override
160
+ confidence: 0,
161
+ reasoning: 'All classifier providers failed',
162
+ model: 'fallback',
163
+ latencyMs: 0,
164
+ action: 'human_review',
165
+ }
166
+ }
167
+
168
+ // Example fallback chain setup:
169
+ // const providers: ClassifierProvider<TicketCategory>[] = [
170
+ // { name: 'anthropic', classify: (text, labels) => classify(anthropicClient, text, labels, prompt) },
171
+ // { name: 'openai', classify: (text, labels) => classifyOpenAI(openaiClient, text, labels) },
172
+ // { name: 'rules', classify: (text, labels) => ruleBasedClassifier(text, labels) },
173
+ // ]
174
+ // const result = await classifyWithFallback(ticket.body, categories, providers)
175
+
176
+ /**
177
+ * Framework adaptations:
178
+ *
179
+ * Express:
180
+ * - Classification endpoint: POST /api/classify with Zod validation on input
181
+ * - Confidence thresholds in environment config, not hardcoded
182
+ * - Log classifications to structured logging (Winston/Pino) for eval pipelines
183
+ *
184
+ * FastAPI:
185
+ * - classify() → async def classify() with Pydantic ClassificationResult model
186
+ * - Fallback chain: same pattern, use httpx.AsyncClient for provider calls
187
+ * - Route confidence thresholds via Settings (pydantic-settings)
188
+ * - Background classification: FastAPI BackgroundTasks for non-blocking
189
+ *
190
+ * Django:
191
+ * - Classification logic in services.py — never in views
192
+ * - Store ClassificationResult in a ClassificationLog model for audit trail
193
+ * - Thresholds in django.conf.settings or per-tenant config
194
+ * - Async classification: Celery task with result stored in DB
195
+ */