cdp-edge 1.23.2 → 1.24.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 (42) hide show
  1. package/README.md +82 -21
  2. package/bin/cdp-edge.js +10 -1
  3. package/contracts/agent-versions.json +42 -41
  4. package/contracts/types.ts +81 -0
  5. package/dist/commands/install.js +6 -1
  6. package/dist/commands/server.js +4 -4
  7. package/docs/whatsapp-ctwa.md +3 -2
  8. package/extracted-skill/tracking-events-generator/agents/database-agent.md +5 -4
  9. package/extracted-skill/tracking-events-generator/agents/fraud-detection-agent.md +0 -1
  10. package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +1 -1
  11. package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +4 -4
  12. package/extracted-skill/tracking-events-generator/agents/ml-clustering-agent.md +81 -70
  13. package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +6 -2
  14. package/extracted-skill/tracking-events-generator/cdpTrack.js +7 -0
  15. package/extracted-skill/tracking-events-generator/models/lancamento-imobiliario.md +344 -0
  16. package/extracted-skill/tracking-events-generator/route-intent-capture.js +222 -0
  17. package/package.json +9 -5
  18. package/server-edge-tracker/INSTALAR.md +5 -5
  19. package/server-edge-tracker/{index.js → index.ts} +186 -72
  20. package/server-edge-tracker/modules/{db.js → db.ts} +180 -69
  21. package/server-edge-tracker/modules/dispatch/{ga4.js → ga4.ts} +12 -10
  22. package/server-edge-tracker/modules/dispatch/meta.ts +138 -0
  23. package/server-edge-tracker/modules/dispatch/{platforms.js → platforms.ts} +58 -56
  24. package/server-edge-tracker/modules/dispatch/{tiktok.js → tiktok.ts} +22 -20
  25. package/server-edge-tracker/modules/dispatch/{whatsapp.js → whatsapp.ts} +59 -25
  26. package/server-edge-tracker/modules/{intelligence.js → intelligence.ts} +175 -60
  27. package/server-edge-tracker/modules/ml/{bidding.js → bidding.ts} +37 -35
  28. package/server-edge-tracker/modules/ml/{fraud.js → fraud.ts} +49 -56
  29. package/server-edge-tracker/modules/ml/{logistic.js → logistic.ts} +44 -19
  30. package/server-edge-tracker/modules/ml/{ltv.js → ltv.ts} +179 -83
  31. package/server-edge-tracker/modules/ml/{matchquality.js → matchquality.ts} +70 -26
  32. package/server-edge-tracker/modules/ml/segmentation.ts +407 -0
  33. package/server-edge-tracker/modules/utils.ts +186 -0
  34. package/server-edge-tracker/schema-ltv-feedback.sql +11 -0
  35. package/server-edge-tracker/types.ts +251 -0
  36. package/server-edge-tracker/wrangler.toml +24 -6
  37. package/templates/lancamento-imobiliario.md +344 -0
  38. package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
  39. package/server-edge-tracker/modules/dispatch/meta.js +0 -119
  40. package/server-edge-tracker/modules/ml/segmentation.js +0 -316
  41. package/server-edge-tracker/modules/utils.js +0 -89
  42. package/server-edge-tracker/worker.js +0 -4577
@@ -4,12 +4,73 @@
4
4
  */
5
5
 
6
6
  import { extractFeatures, predictWithWeights, loadActiveWeights } from './logistic.js';
7
+ import { Env, TrackPayload } from '../../types.js';
8
+
9
+ // ── Tipos ───────────────────────────────────────────────────────────────────────
10
+ export interface LtvResult {
11
+ score: number;
12
+ class: string;
13
+ value: number;
14
+ source?: string;
15
+ }
16
+
17
+ export interface AbVariation {
18
+ id: number;
19
+ test_id: number;
20
+ name: string;
21
+ system_prompt: string;
22
+ weight: number;
23
+ is_control: number;
24
+ total_assigned?: number;
25
+ accuracy_score?: number;
26
+ }
27
+
28
+ export interface AbTestCreate {
29
+ name: string;
30
+ description?: string;
31
+ min_sample?: number;
32
+ variations: Array<{
33
+ name: string;
34
+ system_prompt?: string;
35
+ weight?: number;
36
+ is_control?: boolean;
37
+ }>;
38
+ }
39
+
40
+ export interface AutoDecideResult {
41
+ decided: boolean;
42
+ reason?: string;
43
+ test_id?: number;
44
+ test_name?: string;
45
+ winner_id?: number;
46
+ winner_name?: string;
47
+ improvement?: string;
48
+ is_control_winner?: boolean;
49
+ winning_prompt?: string;
50
+ }
7
51
 
8
52
  // Cache key para o teste ativo (KV — evita hit no D1 a cada request /track)
9
53
  const AB_LTV_CACHE_KEY = 'ab_ltv_active_test';
10
54
 
55
+ // ── Prompt especializado para imóveis ───────────────────────────────────────
56
+ // Ativado automaticamente quando property_lat/lng estão presentes no payload.
57
+ // Override por A/B test tem prioridade sobre este prompt.
58
+ const REAL_ESTATE_PROMPT = `You are a real estate lead scoring expert for the Brazilian market.
59
+ Reply ONLY with a JSON object {"adjustment": <integer between -15 and 15>} based on the lead data.
60
+ Scoring rules (apply additively):
61
+ - distance_km < 5: +12 (lives nearby, buys fast)
62
+ - distance_km 5-15: +8
63
+ - distance_km 15-30: +3
64
+ - distance_km > 30: 0
65
+ - distance_km unknown: +3 (gave intent signal without geo)
66
+ - event = Schedule or route click: +5 (physical visit intent)
67
+ - scroll_score >= 3 AND time_level = comprador: +4 (deep engagement)
68
+ - hour_brt between 18-22 (weekday): +3 (active decision window)
69
+ - has_phone = true: +2 (reachable for follow-up)
70
+ No explanation. JSON only.`;
71
+
11
72
  // ── predictLtv — Heurística em 5 dimensões (0-100 pts) ───────────────────────
12
- export async function predictLtv(env, payload, request, customSystemPrompt = null) {
73
+ export async function predictLtv(env: Env, payload: TrackPayload, request: Request | null, customSystemPrompt: string | null = null): Promise<LtvResult> {
13
74
  // ── Tentar modelo treinado (regressão logística real) ─────────────────────
14
75
  // Se existir modelo ativo no KV/D1, usa-o em vez da heurística manual.
15
76
  // Fallback automático para heurística se modelo não disponível.
@@ -17,10 +78,10 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
17
78
  const model = await loadActiveWeights(env);
18
79
  if (model?.weights?.length) {
19
80
  const hour = new Date().getUTCHours();
20
- const country = (payload.country || request?.cf?.country || '').toUpperCase();
81
+ const country = (payload.country || (request as any)?.cf?.country || '').toUpperCase();
21
82
  const features = extractFeatures({
22
83
  utm_source: payload.utmSource,
23
- engagement_score: parseFloat(payload.engagementScore || 0),
84
+ engagement_score: parseFloat(String(payload.engagementScore || '0')),
24
85
  intention_level: payload.intentionLevel,
25
86
  days_since_lead: 0, // evento atual = recência máxima
26
87
  has_email: !!payload.email,
@@ -32,7 +93,7 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
32
93
  const score100 = predictWithWeights(model, features);
33
94
  const ltvClass = score100 >= 70 ? 'High' : score100 >= 40 ? 'Medium' : 'Low';
34
95
  const ltvMultiplier = score100 >= 70 ? 3.5 : score100 >= 40 ? 1.8 : 0.8;
35
- const productValue = payload.value ? parseFloat(payload.value) : 0;
96
+ const productValue = payload.value ? parseFloat(String(payload.value)) : 0;
36
97
  const baseValue = productValue > 0 ? productValue : 197;
37
98
  const predictedValue = Math.round(baseValue * ltvMultiplier * 100) / 100;
38
99
 
@@ -43,14 +104,14 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
43
104
  let score = 0;
44
105
 
45
106
  // 1. Engajamento browser (0–30)
46
- const engScore = parseFloat(payload.engagementScore || 0);
47
- const userScore = parseFloat(payload.userScore || 0);
107
+ const engScore = parseFloat(String(payload.engagementScore || '0'));
108
+ const userScore = parseFloat(String((payload as any).userScore || '0'));
48
109
  score += Math.min(15, Math.round((engScore / 5) * 15));
49
110
  score += Math.min(15, Math.round((userScore / 100) * 15));
50
111
 
51
112
  // 2. Origem de tráfego (0–25)
52
113
  const src = (payload.utmSource || '').toLowerCase();
53
- const utm_score_map = {
114
+ const utm_score_map: Record<string, number> = {
54
115
  facebook: 25, instagram: 25, meta: 25,
55
116
  google: 22, youtube: 22, tiktok: 20,
56
117
  email: 18, sms: 18,
@@ -60,8 +121,8 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
60
121
 
61
122
  // 3. Contexto de rede (0–15)
62
123
  const hour = new Date().getUTCHours();
63
- const country = (payload.country || request.cf?.country || '').toUpperCase();
64
- const org = String(request.cf?.asOrganization || '').toLowerCase();
124
+ const country = (payload.country || (request as any)?.cf?.country || '').toUpperCase();
125
+ const org = String((request as any).cf?.asOrganization || '').toLowerCase();
65
126
 
66
127
  const isHighConvTime = hour >= 21 || hour <= 2;
67
128
  score += isHighConvTime ? 8 : (hour >= 12 && hour <= 20 ? 4 : 1);
@@ -83,9 +144,29 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
83
144
  if (payload.phone) score += 4;
84
145
  if (payload.firstName) score += 2;
85
146
 
147
+ // 5b. Tipo de evento imobiliário (0–15) — sinais de intenção de compra física
148
+ const evType = ((payload as any).eventType || '').toLowerCase();
149
+ if (evType === 'customizeproduct') score += 15; // simulação de financiamento → intenção máxima
150
+ else if (evType === 'findlocation') score += 10; // viu mapa/localização → visita física iminente
151
+ else if (evType === 'addtowishlist') score += 8; // favoritou → interesse persistente
152
+
153
+ // 6. Proximidade ao imóvel físico (0–15) — apenas quando distância calculada
154
+ const distKm = parseFloat(String(payload.distanceKm || (payload as any).user_distance_km || '-1'));
155
+ if (distKm >= 0) {
156
+ if (distKm < 5) score += 15;
157
+ else if (distKm < 15) score += 10;
158
+ else if (distKm < 30) score += 6;
159
+ else if (distKm < 60) score += 3;
160
+ // > 60km: sem bônus — lead distante precisa de argumento diferente
161
+ } else if (payload.property_lat || (payload as any).propertyLat) {
162
+ // Coords no payload mas distância não resolvida: pequeno bônus por intenção de rota
163
+ score += 3;
164
+ }
165
+
86
166
  score = Math.min(100, score);
87
167
 
88
- let ltvClass, ltvMultiplier;
168
+ let ltvClass: string;
169
+ let ltvMultiplier: number;
89
170
  if (score >= 70) {
90
171
  ltvClass = 'High'; ltvMultiplier = 3.5;
91
172
  } else if (score >= 40) {
@@ -94,7 +175,7 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
94
175
  ltvClass = 'Low'; ltvMultiplier = 0.8;
95
176
  }
96
177
 
97
- const productValue = payload.value ? parseFloat(payload.value) : 0;
178
+ const productValue = payload.value ? parseFloat(String(payload.value)) : 0;
98
179
  const baseValue = productValue > 0 ? productValue : 197;
99
180
  const predictedValue = Math.round(baseValue * ltvMultiplier * 100) / 100;
100
181
 
@@ -102,22 +183,37 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
102
183
  let aiAdjustment = 0;
103
184
  if (env.AI && score >= 40) {
104
185
  try {
186
+ const isRealEstate = !!(payload.property_lat || (payload as any).propertyLat);
105
187
  const systemContent = customSystemPrompt ||
106
- 'You are a conversion rate expert. Reply ONLY with a JSON object {"adjustment": <number between -10 and 10>} based on the lead data provided. No explanation.';
188
+ (isRealEstate
189
+ ? REAL_ESTATE_PROMPT
190
+ : 'You are a conversion rate expert. Reply ONLY with a JSON object {"adjustment": <number between -10 and 10>} based on the lead data provided. No explanation.');
191
+
192
+ const userContext: Record<string, any> = {
193
+ utm_source: payload.utmSource,
194
+ intention: intentionLevel,
195
+ engagement: engScore,
196
+ hour_utc: hour,
197
+ country,
198
+ has_email: !!payload.email,
199
+ has_phone: !!payload.phone,
200
+ };
201
+ if (isRealEstate) {
202
+ userContext.event_type = 'real_estate_schedule';
203
+ userContext.distance_km = payload.distanceKm || (payload as any).user_distance_km || 'unknown';
204
+ userContext.distance_bucket = payload.distanceBucket || 'unknown';
205
+ userContext.scroll_score = payload.scrollScore || (payload as any).scroll_score || 0;
206
+ userContext.time_level = payload.timeLevel || (payload as any).timeLevel || 'unknown';
207
+ userContext.intent_score = payload.intentScoreNum || payload.intent_score || 'high';
208
+ userContext.hour_brt = (hour - 3 + 24) % 24; // UTC-3 aproximado
209
+ }
210
+
107
211
  const prompt = [
108
212
  { role: 'system', content: systemContent },
109
- { role: 'user', content: JSON.stringify({
110
- utm_source: payload.utmSource,
111
- intention: intentionLevel,
112
- engagement: engScore,
113
- hour_utc: hour,
114
- country,
115
- has_email: !!payload.email,
116
- has_phone: !!payload.phone,
117
- })},
213
+ { role: 'user', content: JSON.stringify(userContext) },
118
214
  ];
119
- const aiRes = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: prompt, max_tokens: 32 });
120
- const parsed = JSON.parse(aiRes.response.trim());
215
+ const aiRes = await env.AI.run('@cf/ibm-granite/granite-4.0-h-micro', { messages: prompt, max_tokens: 32 });
216
+ const parsed = JSON.parse((aiRes as any).response.trim());
121
217
  if (typeof parsed.adjustment === 'number') {
122
218
  aiAdjustment = Math.max(-10, Math.min(10, parsed.adjustment));
123
219
  }
@@ -132,13 +228,13 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
132
228
  }
133
229
 
134
230
  // ── getLtvAbVariation — busca variação ativa do A/B test ─────────────────────
135
- export async function getLtvAbVariation(env) {
231
+ export async function getLtvAbVariation(env: Env): Promise<AbVariation | null> {
136
232
  if (!env.DB) return null;
137
233
 
138
234
  try {
139
- let testData = null;
235
+ let testData: any = null;
140
236
  if (env.GEO_CACHE) {
141
- const cached = await env.GEO_CACHE.get(AB_LTV_CACHE_KEY, 'json');
237
+ const cached = await env.GEO_CACHE.get(AB_LTV_CACHE_KEY, 'json') as any;
142
238
  if (cached) testData = cached;
143
239
  }
144
240
 
@@ -163,7 +259,7 @@ export async function getLtvAbVariation(env) {
163
259
 
164
260
  if (!testData || testData.length === 0) return null;
165
261
 
166
- const totalWeight = testData.reduce((s, v) => s + (v.weight || 0.5), 0);
262
+ const totalWeight = testData.reduce((s: number, v: AbVariation) => s + (v.weight || 0.5), 0);
167
263
  let rand = Math.random() * totalWeight;
168
264
  for (const variation of testData) {
169
265
  rand -= (variation.weight || 0.5);
@@ -171,14 +267,14 @@ export async function getLtvAbVariation(env) {
171
267
  }
172
268
  return testData[testData.length - 1];
173
269
 
174
- } catch (err) {
175
- console.error('[AB-LTV] getLtvAbVariation error:', err.message);
270
+ } catch (err: any) {
271
+ console.error('[AB-LTV] getLtvAbVariation error:', err?.message || String(err));
176
272
  return null;
177
273
  }
178
274
  }
179
275
 
180
276
  // ── recordAbAssignment — registra variação usada para um lead ─────────────────
181
- export async function recordAbAssignment(env, userId, variationId, testId, predictedLtv, predictedClass, emailHash) {
277
+ export async function recordAbAssignment(env: Env, userId: string, variationId: number, testId: number, predictedLtv: number | null, predictedClass: string | null, emailHash: string | null): Promise<void> {
182
278
  if (!env.DB) return;
183
279
  try {
184
280
  await env.DB.prepare(`
@@ -187,17 +283,17 @@ export async function recordAbAssignment(env, userId, variationId, testId, predi
187
283
  `).bind(testId, variationId, userId, emailHash || null, predictedLtv || null, predictedClass || null).run();
188
284
 
189
285
  await env.DB.prepare(`UPDATE ltv_ab_variations SET total_assigned = total_assigned + 1 WHERE id = ?`).bind(variationId).run();
190
- } catch (err) {
191
- console.error('[AB-LTV] recordAbAssignment error:', err.message);
286
+ } catch (err: any) {
287
+ console.error('[AB-LTV] recordAbAssignment error:', err?.message || String(err));
192
288
  }
193
289
  }
194
290
 
195
291
  // ── POST /api/ltv/ab-test/create ─────────────────────────────────────────────
196
- export async function handleLtvAbTestCreate(env, request, headers) {
292
+ export async function handleLtvAbTestCreate(env: Env, request: Request, headers: Headers): Promise<Response> {
197
293
  if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
198
294
 
199
- let body;
200
- try { body = await request.json(); }
295
+ let body: AbTestCreate;
296
+ try { body = await request.json() as AbTestCreate; }
201
297
  catch { return new Response(JSON.stringify({ error: 'JSON inválido no body' }), { status: 400, headers }); }
202
298
 
203
299
  const { name, description, min_sample = 100, variations } = body;
@@ -208,7 +304,7 @@ export async function handleLtvAbTestCreate(env, request, headers) {
208
304
 
209
305
  const running = await env.DB.prepare(`SELECT id FROM ltv_ab_tests WHERE status = 'running' LIMIT 1`).first();
210
306
  if (running) {
211
- return new Response(JSON.stringify({ error: 'Já existe um teste em andamento.', running_test_id: running.id }), { status: 409, headers });
307
+ return new Response(JSON.stringify({ error: 'Já existe um teste em andamento.', running_test_id: (running as any).id }), { status: 409, headers });
212
308
  }
213
309
 
214
310
  try {
@@ -225,28 +321,28 @@ export async function handleLtvAbTestCreate(env, request, headers) {
225
321
  INSERT INTO ltv_ab_tests (name, description, status, min_sample, created_at) VALUES (?, ?, 'running', ?, ?)
226
322
  `).bind(name, description || null, min_sample, now).run();
227
323
 
228
- const testId = testRes.meta?.last_row_id;
324
+ const testId = (testRes as any).meta?.last_row_id;
229
325
  if (!testId) throw new Error('Falha ao criar o teste no D1');
230
326
 
231
- const createdVariations = [];
327
+ const createdVariations: Array<{ id: number; name: string; weight: number; is_control: boolean }> = [];
232
328
  for (const v of variations) {
233
329
  const vRes = await env.DB.prepare(`
234
330
  INSERT INTO ltv_ab_variations (test_id, name, system_prompt, weight, is_control, created_at) VALUES (?, ?, ?, ?, ?, ?)
235
331
  `).bind(testId, v.name, v.system_prompt, v.weight || 0.5, v.is_control ? 1 : 0, now).run();
236
- createdVariations.push({ id: vRes.meta?.last_row_id, name: v.name, weight: v.weight, is_control: !!v.is_control });
332
+ createdVariations.push({ id: (vRes as any).meta?.last_row_id, name: v.name, weight: v.weight || 0.5, is_control: !!v.is_control });
237
333
  }
238
334
 
239
335
  if (env.GEO_CACHE) await env.GEO_CACHE.delete(AB_LTV_CACHE_KEY);
240
336
 
241
337
  return new Response(JSON.stringify({ success: true, test_id: testId, name, status: 'running', min_sample, variations: createdVariations, started_at: now }), { status: 201, headers });
242
- } catch (err) {
243
- console.error('[AB-LTV] create error:', err.message);
244
- return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
338
+ } catch (err: any) {
339
+ console.error('[AB-LTV] create error:', err?.message || String(err));
340
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
245
341
  }
246
342
  }
247
343
 
248
344
  // ── GET /api/ltv/ab-test/list ─────────────────────────────────────────────────
249
- export async function handleLtvAbTestList(env, request, headers) {
345
+ export async function handleLtvAbTestList(env: Env, request: Request, headers: Headers): Promise<Response> {
250
346
  if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
251
347
 
252
348
  const url = new URL(request.url);
@@ -255,7 +351,7 @@ export async function handleLtvAbTestList(env, request, headers) {
255
351
 
256
352
  try {
257
353
  const cond = status ? 'WHERE t.status = ?' : '';
258
- const bindings = status ? [status, limit] : [limit];
354
+ const bindings: (string | number)[] = status ? [status, limit] : [limit];
259
355
 
260
356
  const tests = await env.DB.prepare(`
261
357
  SELECT t.id, t.name, t.description, t.status, t.winner_id,
@@ -271,14 +367,14 @@ export async function handleLtvAbTestList(env, request, headers) {
271
367
  `).bind(...bindings).all();
272
368
 
273
369
  return new Response(JSON.stringify({ success: true, total: (tests.results || []).length, tests: tests.results || [] }), { status: 200, headers });
274
- } catch (err) {
275
- console.error('[AB-LTV] list error:', err.message);
276
- return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
370
+ } catch (err: any) {
371
+ console.error('[AB-LTV] list error:', err?.message || String(err));
372
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
277
373
  }
278
374
  }
279
375
 
280
376
  // ── GET /api/ltv/ab-test/results ─────────────────────────────────────────────
281
- export async function handleLtvAbTestResults(env, request, headers) {
377
+ export async function handleLtvAbTestResults(env: Env, request: Request, headers: Headers): Promise<Response> {
282
378
  if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
283
379
 
284
380
  const url = new URL(request.url);
@@ -291,15 +387,15 @@ export async function handleLtvAbTestResults(env, request, headers) {
291
387
  const testRes = await env.DB.prepare(`SELECT id, name, status, min_sample, winner_id, started_at FROM ltv_ab_tests ${cond} LIMIT 1`).bind(...testBind).first();
292
388
  if (!testRes) return new Response(JSON.stringify({ error: 'Nenhum teste encontrado' }), { status: 404, headers });
293
389
 
294
- const perf = await env.DB.prepare(`SELECT * FROM v_ab_test_performance WHERE test_id = ?`).bind(testRes.id).all();
390
+ const perf = await env.DB.prepare(`SELECT * FROM v_ab_test_performance WHERE test_id = ?`).bind((testRes as any).id).all();
295
391
  const variations = perf.results || [];
296
- const ready = variations.every(v => (v.total_assigned || 0) >= testRes.min_sample);
297
- let recommendation = null;
392
+ const ready = variations.every((v: any) => (v.total_assigned || 0) >= (testRes as any).min_sample);
393
+ let recommendation: any = null;
298
394
 
299
395
  if (ready && variations.length > 0) {
300
- const best = variations.reduce((a, b) => (b.accuracy_score || 0) > (a.accuracy_score || 0) ? b : a);
301
- const control = variations.find(v => v.is_control);
302
- const improvement = control ? ((best.accuracy_score || 0) - (control.accuracy_score || 0)) * 100 : null;
396
+ const best = variations.reduce((a: any, b: any) => (Number(b.accuracy_score) || 0) > (Number(a.accuracy_score) || 0) ? b : a);
397
+ const control = variations.find((v: any) => v.is_control);
398
+ const improvement = control ? ((Number(best.accuracy_score) || 0) - (Number(control.accuracy_score) || 0)) * 100 : null;
303
399
  recommendation = {
304
400
  winner_variation_id: best.variation_id, winner_variation_name: best.variation_name,
305
401
  accuracy_score: best.accuracy_score, improvement_vs_control: improvement ? `+${improvement.toFixed(1)}%` : null,
@@ -309,20 +405,20 @@ export async function handleLtvAbTestResults(env, request, headers) {
309
405
 
310
406
  return new Response(JSON.stringify({
311
407
  success: true,
312
- test: { id: testRes.id, name: testRes.name, status: testRes.status, min_sample: testRes.min_sample, started_at: testRes.started_at, is_ready: ready },
408
+ test: { id: (testRes as any).id, name: (testRes as any).name, status: (testRes as any).status, min_sample: (testRes as any).min_sample, started_at: (testRes as any).started_at, is_ready: ready },
313
409
  variations, recommendation,
314
410
  }), { status: 200, headers });
315
- } catch (err) {
316
- console.error('[AB-LTV] results error:', err.message);
317
- return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
411
+ } catch (err: any) {
412
+ console.error('[AB-LTV] results error:', err?.message || String(err));
413
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
318
414
  }
319
415
  }
320
416
 
321
417
  // ── POST /api/ltv/ab-test/winner ──────────────────────────────────────────────
322
- export async function handleLtvAbTestWinner(env, request, headers) {
418
+ export async function handleLtvAbTestWinner(env: Env, request: Request, headers: Headers): Promise<Response> {
323
419
  if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
324
420
 
325
- let body;
421
+ let body: any;
326
422
  try { body = await request.json(); }
327
423
  catch { return new Response(JSON.stringify({ error: 'JSON inválido' }), { status: 400, headers }); }
328
424
 
@@ -339,22 +435,22 @@ export async function handleLtvAbTestWinner(env, request, headers) {
339
435
  if (env.GEO_CACHE) await env.GEO_CACHE.delete(AB_LTV_CACHE_KEY);
340
436
 
341
437
  return new Response(JSON.stringify({
342
- success: true, test_id, winner_variation_id: variation_id, winner_name: variation.name,
343
- is_control: variation.is_control === 1, winning_prompt: variation.system_prompt,
344
- message: variation.is_control === 1
438
+ success: true, test_id, winner_variation_id: variation_id, winner_name: (variation as any).name,
439
+ is_control: (variation as any).is_control === 1, winning_prompt: (variation as any).system_prompt,
440
+ message: (variation as any).is_control === 1
345
441
  ? 'O prompt original (controle) venceu. Nenhuma alteração necessária.'
346
442
  : 'Novo prompt vencedor identificado. Copie o campo winning_prompt e aplique ao predictLtv() como novo default.',
347
443
  }), { status: 200, headers });
348
- } catch (err) {
349
- console.error('[AB-LTV] winner error:', err.message);
350
- return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
444
+ } catch (err: any) {
445
+ console.error('[AB-LTV] winner error:', err?.message || String(err));
446
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
351
447
  }
352
448
  }
353
449
 
354
450
  // ── autoDecideAbWinner — declara winner automaticamente via cron ──────────────
355
451
  // Critério: todas as variações com amostra >= min_sample
356
452
  // E diferença de accuracy_score >= 5pp entre melhor e controle
357
- export async function autoDecideAbWinner(env) {
453
+ export async function autoDecideAbWinner(env: Env): Promise<AutoDecideResult> {
358
454
  if (!env.DB) return { decided: false, reason: 'no_db' };
359
455
 
360
456
  try {
@@ -368,24 +464,24 @@ export async function autoDecideAbWinner(env) {
368
464
  // Buscar performance das variações
369
465
  const perf = await env.DB.prepare(
370
466
  `SELECT * FROM v_ab_test_performance WHERE test_id = ?`
371
- ).bind(test.id).all();
467
+ ).bind((test as any).id).all();
372
468
 
373
469
  const variations = perf.results || [];
374
470
  if (variations.length < 2) return { decided: false, reason: 'insufficient_variations' };
375
471
 
376
472
  // Verificar se todas têm amostra suficiente
377
- const allReady = variations.every(v => (v.total_assigned || 0) >= test.min_sample);
473
+ const allReady = variations.every((v: any) => (v.total_assigned || 0) >= (test as any).min_sample);
378
474
  if (!allReady) {
379
- const minAssigned = Math.min(...variations.map(v => v.total_assigned || 0));
380
- return { decided: false, reason: `sample_insufficient (${minAssigned}/${test.min_sample})` };
475
+ const minAssigned = Math.min(...variations.map((v: any) => v.total_assigned || 0));
476
+ return { decided: false, reason: `sample_insufficient (${minAssigned}/${(test as any).min_sample})` };
381
477
  }
382
478
 
383
479
  // Encontrar melhor e controle
384
- const best = variations.reduce((a, b) => (b.accuracy_score || 0) > (a.accuracy_score || 0) ? b : a);
385
- const control = variations.find(v => v.is_control) || variations[0];
480
+ const best = variations.reduce((a: any, b: any) => (b.accuracy_score || 0) > (a.accuracy_score || 0) ? b : a);
481
+ const control = variations.find((v: any) => v.is_control) || variations[0];
386
482
 
387
- const bestScore = parseFloat(best.accuracy_score || 0);
388
- const controlScore = parseFloat(control.accuracy_score || 0);
483
+ const bestScore = parseFloat(String(best.accuracy_score) || '0');
484
+ const controlScore = parseFloat(String(control.accuracy_score) || '0');
389
485
  const diff = bestScore - controlScore;
390
486
 
391
487
  // Empate técnico → controle vence (determinístico)
@@ -404,17 +500,17 @@ export async function autoDecideAbWinner(env) {
404
500
 
405
501
  return {
406
502
  decided: true,
407
- test_id: test.id,
408
- test_name: test.name,
409
- winner_id: best.variation_id,
410
- winner_name: best.variation_name,
503
+ test_id: (test as any).id,
504
+ test_name: (test as any).name,
505
+ winner_id: typeof best.variation_id === 'number' ? best.variation_id : undefined,
506
+ winner_name: typeof best.variation_name === 'string' ? best.variation_name : undefined,
411
507
  improvement: `+${(diff * 100).toFixed(1)}pp`,
412
508
  is_control_winner: best.variation_id === control.variation_id,
413
- winning_prompt: best.system_prompt || null,
509
+ winning_prompt: String(best.system_prompt || ''),
414
510
  };
415
511
 
416
- } catch (err) {
417
- console.error('[AB-LTV] autoDecide error:', err.message);
418
- return { decided: false, reason: err.message };
512
+ } catch (err: any) {
513
+ console.error('[AB-LTV] autoDecide error:', err?.message || String(err));
514
+ return { decided: false, reason: err?.message || String(err) };
419
515
  }
420
516
  }