@easyoref/shared 1.21.1

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/src/schemas.ts ADDED
@@ -0,0 +1,712 @@
1
+ /**
2
+ * Zod validation schemas for agent state, types, and store.
3
+ * Replaces TypeScript interfaces with runtime validation.
4
+ */
5
+
6
+ import { z } from "zod";
7
+
8
+ // ─────────────────────────────────────────────────────────
9
+ // Alert & Phase types
10
+ // ─────────────────────────────────────────────────────────
11
+
12
+ export const AlertTypeSchema = z.enum([
13
+ "early_warning",
14
+ "red_alert",
15
+ "resolved",
16
+ ]);
17
+ export type AlertType = z.infer<typeof AlertTypeSchema>;
18
+
19
+ export const QualitativeCountSchema = z.discriminatedUnion("type", [
20
+ z.object({ type: z.literal("all") }),
21
+ z.object({ type: z.literal("most") }),
22
+ z.object({ type: z.literal("many") }),
23
+ z.object({ type: z.literal("few") }),
24
+ z.object({ type: z.literal("exists") }),
25
+ z.object({ type: z.literal("none") }),
26
+ z.object({ type: z.literal("more_than"), value: z.number() }),
27
+ z.object({ type: z.literal("less_than"), value: z.number() }),
28
+ ]);
29
+ export type QualitativeCount = z.infer<typeof QualitativeCountSchema>;
30
+
31
+ // ─────────────────────────────────────────────────────────
32
+ // Source Message Types (input from Telegram channels)
33
+ // ─────────────────────────────────────────────────────────
34
+
35
+ export const SourceTypeSchema = z.enum([
36
+ "telegram_channel",
37
+ "web_scrape",
38
+ "manual",
39
+ ]);
40
+ export type SourceType = z.infer<typeof SourceTypeSchema>;
41
+
42
+ export const BaseSourceMessageSchema = z.object({
43
+ channelId: z.string().min(1),
44
+ sourceType: SourceTypeSchema,
45
+ timestamp: z.number().int().min(0),
46
+ text: z.string().min(1),
47
+ sourceUrl: z.url().optional(),
48
+ });
49
+ export type BaseSourceMessage = z.infer<typeof BaseSourceMessageSchema>;
50
+
51
+ export const NewsMessageSchema = BaseSourceMessageSchema.extend({
52
+ sourceType: z.literal("telegram_channel"),
53
+ grammyMessageId: z.number().optional(),
54
+ });
55
+ export type NewsMessage = z.infer<typeof NewsMessageSchema>;
56
+
57
+ // ─────────────────────────────────────────────────────────
58
+ // Channel Types (source vs target)
59
+ // ─────────────────────────────────────────────────────────
60
+
61
+ export const NewsChannelSchema = z.object({
62
+ channelId: z.string().min(1),
63
+ channelName: z.string(),
64
+ language: z.string().min(2).max(5),
65
+ region: z.string().optional(),
66
+ });
67
+ export type NewsChannel = z.infer<typeof NewsChannelSchema>;
68
+
69
+ export const TargetGroupSchema = z.object({
70
+ chatId: z.string().min(1),
71
+ groupName: z.string(),
72
+ subscribedRegions: z.array(z.string()),
73
+ });
74
+ export type TargetGroup = z.infer<typeof TargetGroupSchema>;
75
+
76
+ // ─────────────────────────────────────────────────────────
77
+ // Channel tracking (pre-graph structure)
78
+ // ─────────────────────────────────────────────────────────
79
+
80
+ export const NewsChannelWithUpdatesSchema = z.object({
81
+ channel: z.string().min(1),
82
+ processedMessages: z
83
+ .array(NewsMessageSchema)
84
+ .default([])
85
+ .describe("Already processed messages"),
86
+ unprocessedMessages: z
87
+ .array(NewsMessageSchema)
88
+ .default([])
89
+ .describe("New messages pending processing"),
90
+ });
91
+ export type NewsChannelWithUpdates = z.infer<
92
+ typeof NewsChannelWithUpdatesSchema
93
+ >;
94
+
95
+ export const ChannelTrackingSchema = z.object({
96
+ trackStartTimestamp: z.number().int().min(0),
97
+ lastUpdateTimestamp: z.number().int().min(0),
98
+ channelsWithUpdates: z.array(NewsChannelWithUpdatesSchema).default([]),
99
+ });
100
+ export type ChannelTracking = z.infer<typeof ChannelTrackingSchema>;
101
+
102
+ // ─────────────────────────────────────────────────────────
103
+ // Pre-filter (deterministic, zero tokens)
104
+ // ─────────────────────────────────────────────────────────
105
+
106
+ export const RelevanceCheckSchema = z.object({
107
+ channel: z.string().min(1),
108
+ text: z.string().min(1),
109
+ ts: z.number().int().min(0),
110
+ relevant: z
111
+ .boolean()
112
+ .describe("true if post passed keyword/region pre-filter"),
113
+ });
114
+ export type RelevanceCheck = z.infer<typeof RelevanceCheckSchema>;
115
+
116
+ // ─────────────────────────────────────────────────────────
117
+ // LLM filter (cheap pre-filter)
118
+ // ─────────────────────────────────────────────────────────
119
+
120
+ export const FilterOutputSchema = z.object({
121
+ relevantChannels: z
122
+ .array(z.string())
123
+ .describe("Channels with important intel"),
124
+ });
125
+ export type FilterOutput = z.infer<typeof FilterOutputSchema>;
126
+
127
+ // ─────────────────────────────────────────────────────────
128
+ // Insight Types (discriminated union for structured extraction)
129
+ // ─────────────────────────────────────────────────────────
130
+
131
+ export const InsightKindSchema = z.enum([
132
+ "rocket_impact",
133
+ "rocket_interception",
134
+ "location",
135
+ "casualty",
136
+ "injury",
137
+ "eta_minutes",
138
+ "cassette_munition",
139
+ ]);
140
+ export type InsightKind = z.infer<typeof InsightKindSchema>;
141
+
142
+ export const InsightSchema = z.discriminatedUnion("kind", [
143
+ z.object({
144
+ kind: z.literal("rocket_impact"),
145
+ value: z.number().int().min(0),
146
+ }),
147
+ z.object({
148
+ kind: z.literal("rocket_interception"),
149
+ value: z.number().int().min(0),
150
+ }),
151
+ z.object({ kind: z.literal("location"), value: z.string().min(1) }),
152
+ z.object({ kind: z.literal("casualty"), value: z.number().int().min(0) }),
153
+ z.object({ kind: z.literal("injury"), value: z.number().int().min(0) }),
154
+ z.object({ kind: z.literal("eta_minutes"), value: z.number().int().min(0) }),
155
+ z.object({ kind: z.literal("cassette_munition"), value: z.boolean() }),
156
+ ]);
157
+ export type Insight = z.infer<typeof InsightSchema>;
158
+
159
+ // ─────────────────────────────────────────────────────────
160
+ // Confidence Matrix — insight verification thresholds
161
+ // ─────────────────────────────────────────────────────────
162
+
163
+ export type ClarifyNeed = "needs_clarify" | "uncertain" | "verified";
164
+
165
+ export interface InsightConfidenceThresholds {
166
+ needsClarify: number;
167
+ uncertain: number;
168
+ verified: number;
169
+ }
170
+
171
+ export const CONFIDENCE_MATRIX: Record<
172
+ InsightKind,
173
+ InsightConfidenceThresholds
174
+ > = {
175
+ rocket_impact: { needsClarify: 0.4, uncertain: 0.5, verified: 0.6 },
176
+ rocket_interception: { needsClarify: 0.4, uncertain: 0.5, verified: 0.6 },
177
+ location: { needsClarify: 0.4, uncertain: 0.7, verified: 0.8 },
178
+ eta_minutes: { needsClarify: 0.4, uncertain: 0.5, verified: 0.55 },
179
+ cassette_munition: { needsClarify: 0.4, uncertain: 0.5, verified: 0.7 },
180
+ casualty: { needsClarify: 0.85, uncertain: 0.9, verified: 0.95 },
181
+ injury: { needsClarify: 0.85, uncertain: 0.9, verified: 0.95 },
182
+ };
183
+
184
+ export function getClarifyNeed(
185
+ insightKind: InsightKind,
186
+ confidence: number,
187
+ ): ClarifyNeed {
188
+ const thresholds = CONFIDENCE_MATRIX[insightKind];
189
+ if (!thresholds) return "uncertain";
190
+ if (confidence < thresholds.needsClarify) return "needs_clarify";
191
+ if (confidence < thresholds.uncertain) return "uncertain";
192
+ return "verified";
193
+ }
194
+
195
+ // ─────────────────────────────────────────────────────────
196
+ // LLM extraction (single call per post)
197
+ // ─────────────────────────────────────────────────────────
198
+
199
+ export const ExtractionResultSchema = z.object({
200
+ channel: z.string().min(1).describe("Source Telegram channel name"),
201
+ regionRelevance: z
202
+ .number()
203
+ .min(0)
204
+ .max(1)
205
+ .describe(
206
+ "V1: region relevance (0-1) — does post mention our alert region?",
207
+ ),
208
+ sourceTrust: z
209
+ .number()
210
+ .min(0)
211
+ .max(1)
212
+ .describe("V2: source trust (0-1) — factual reporting vs rumors/panic?"),
213
+ countryOrigin: z.string().optional().describe("Extracted data"),
214
+ rocketCount: z.number().int().min(0).optional(),
215
+ isCassette: z.boolean().optional(),
216
+ intercepted: z
217
+ .number()
218
+ .int()
219
+ .min(0)
220
+ .optional()
221
+ .describe("Rocket breakdown: intercepted by Iron Dome"),
222
+ interceptedQual: QualitativeCountSchema.optional().describe(
223
+ "Qualitative descriptor when no exact number is stated (undefined if exact number given)",
224
+ ),
225
+ seaImpact: z
226
+ .number()
227
+ .int()
228
+ .min(0)
229
+ .optional()
230
+ .describe("Rocket breakdown: fell in sea/empty area"),
231
+ seaImpactQual: QualitativeCountSchema.optional(),
232
+ openAreaImpact: z
233
+ .number()
234
+ .int()
235
+ .min(0)
236
+ .optional()
237
+ .describe("Rocket breakdown: hit open/populated ground"),
238
+ openAreaImpactQual: QualitativeCountSchema.optional(),
239
+ hitsConfirmed: z.number().int().min(0).optional(),
240
+ hitLocation: z
241
+ .string()
242
+ .optional()
243
+ .describe("Region where impact occurred (in UI language)"),
244
+ hitType: z.enum(["direct", "shrapnel"]).optional().describe("Type of impact"),
245
+ hitDetail: z
246
+ .string()
247
+ .optional()
248
+ .describe(
249
+ 'Impact detail: where/how (e.g. "open area", "building", "sea", "no damage"). In UI language.',
250
+ ),
251
+ casualties: z
252
+ .number()
253
+ .int()
254
+ .min(0)
255
+ .optional()
256
+ .describe(
257
+ "Casualties reported (injured/killed) — primarily resolved phase",
258
+ ),
259
+ injuries: z.number().int().min(0).optional(),
260
+ injuriesCause: z
261
+ .enum(["rocket", "rushing_to_shelter"])
262
+ .optional()
263
+ .describe(
264
+ "Cause of injuries: rocket fragment/direct hit vs panic/rushing to shelter",
265
+ ),
266
+ etaRefinedMinutes: z.number().int().min(0).optional(),
267
+ rocketDetail: z
268
+ .string()
269
+ .optional()
270
+ .describe(
271
+ 'Verbatim per-region rocket breakdown (e.g. "2 center, 3 north")',
272
+ ),
273
+ tone: z
274
+ .enum(["calm", "neutral", "alarmist"])
275
+ .describe('V3: tone — "calm"|"neutral"|"alarmist"'),
276
+ confidence: z
277
+ .number()
278
+ .min(0)
279
+ .max(1)
280
+ .describe("Overall extraction confidence (0-1)"),
281
+ timeRelevance: z
282
+ .number()
283
+ .min(0)
284
+ .max(1)
285
+ .describe(
286
+ "Time relevance (0-1) — does this post discuss the CURRENT attack? LLM sets: 0 = clearly about a previous/different event, 1 = current event. Post-filter rejects posts with timeRelevance < 0.5.",
287
+ ),
288
+ });
289
+ export type ExtractionResult = z.infer<typeof ExtractionResultSchema>;
290
+
291
+ // ─────────────────────────────────────────────────────────
292
+ // Post-filter (deterministic, zero tokens)
293
+ // ─────────────────────────────────────────────────────────
294
+
295
+ export const ValidatedExtractionSchema = ExtractionResultSchema.extend({
296
+ valid: z.boolean().describe("Passed all three validators?"),
297
+ rejectReason: z.string().optional().describe("Reason if rejected"),
298
+ messageUrl: z
299
+ .url()
300
+ .optional()
301
+ .describe("Link to original Telegram post (from ChannelPost.messageUrl)"),
302
+ });
303
+ export type ValidatedExtraction = z.infer<typeof ValidatedExtractionSchema>;
304
+
305
+ // ─────────────────────────────────────────────────────────
306
+ // Cited sources & voting
307
+ // ─────────────────────────────────────────────────────────
308
+
309
+ export const CitedSourceSchema = z.object({
310
+ index: z.number().int().min(1).describe("1-based citation index"),
311
+ channel: z.string().min(1),
312
+ messageUrl: z.url().optional(),
313
+ });
314
+ export type CitedSource = z.infer<typeof CitedSourceSchema>;
315
+
316
+ export const CountryOriginSchema = z.object({
317
+ name: z.string().min(1),
318
+ citations: z.array(z.number().int().min(1)),
319
+ });
320
+
321
+ export const VotedResultSchema = z.object({
322
+ etaRefinedMinutes: z
323
+ .number()
324
+ .int()
325
+ .min(0)
326
+ .optional()
327
+ .describe("ETA in minutes (highest-confidence source)"),
328
+ etaCitations: z
329
+ .array(z.number().int().min(1))
330
+ .default([])
331
+ .describe("Citation indices that provided ETA"),
332
+
333
+ countryOrigins: z
334
+ .array(CountryOriginSchema)
335
+ .default([])
336
+ .describe("Unique origin countries with per-country citation indices"),
337
+
338
+ rocketCountMin: z
339
+ .number()
340
+ .int()
341
+ .min(0)
342
+ .optional()
343
+ .describe("Rocket count range across sources (min == max → exact)"),
344
+ rocketCountMax: z.number().int().min(0).optional(),
345
+ rocketCitations: z.array(z.number().int().min(1)).default([]),
346
+ rocketConfidence: z
347
+ .number()
348
+ .min(0)
349
+ .max(1)
350
+ .default(0)
351
+ .describe(
352
+ "Avg weighted confidence of sources reporting rocket count (for uncertainty marker)",
353
+ ),
354
+ rocketDetail: z
355
+ .string()
356
+ .optional()
357
+ .describe(
358
+ "Verbatim per-region rocket breakdown if sources split by region",
359
+ ),
360
+
361
+ isCassette: z.boolean().optional(),
362
+ isCassetteConfidence: z
363
+ .number()
364
+ .min(0)
365
+ .max(1)
366
+ .default(0)
367
+ .describe(
368
+ "Avg weighted confidence of sources confirming cassette munitions",
369
+ ),
370
+
371
+ intercepted: z
372
+ .number()
373
+ .int()
374
+ .min(0)
375
+ .optional()
376
+ .describe(
377
+ "Rocket breakdown (median values; undefined if no sources reported)",
378
+ ),
379
+ interceptedQual: QualitativeCountSchema.optional(),
380
+ interceptedConfidence: z
381
+ .number()
382
+ .min(0)
383
+ .max(1)
384
+ .default(0)
385
+ .describe("Avg weighted confidence of sources reporting intercepted count"),
386
+ seaImpact: z.number().int().min(0).optional(),
387
+ seaImpactQual: QualitativeCountSchema.optional(),
388
+ seaConfidence: z.number().min(0).max(1).default(0),
389
+ openAreaImpact: z.number().int().min(0).optional(),
390
+ openAreaImpactQual: QualitativeCountSchema.optional(),
391
+ openAreaConfidence: z.number().min(0).max(1).default(0),
392
+
393
+ hitsConfirmed: z.number().int().min(0).optional(),
394
+ hitsCitations: z
395
+ .array(z.number().int().min(1))
396
+ .default([])
397
+ .describe("Citation indices that provided hits data"),
398
+ hitsConfidence: z
399
+ .number()
400
+ .min(0)
401
+ .max(1)
402
+ .default(0)
403
+ .describe("Avg weighted confidence of sources reporting confirmed hits"),
404
+ hitLocation: z
405
+ .string()
406
+ .optional()
407
+ .describe(
408
+ "Region where impact occurred (in UI language, from highest-confidence source)",
409
+ ),
410
+ hitType: z
411
+ .enum(["direct", "shrapnel"])
412
+ .optional()
413
+ .describe("Type of impact: direct or shrapnel/debris"),
414
+ hitDetail: z
415
+ .string()
416
+ .optional()
417
+ .describe(
418
+ 'Impact detail: where/how (e.g. "на открытой местности", "здание", "в море")',
419
+ ),
420
+ noImpacts: z
421
+ .boolean()
422
+ .default(false)
423
+ .describe('Sources explicitly confirm NO impacts ("прилетов нет")'),
424
+ noImpactsCitations: z.array(z.number().int().min(1)).default([]),
425
+ interceptedCitations: z
426
+ .array(z.number().int().min(1))
427
+ .default([])
428
+ .describe("Citation indices for intercepted data"),
429
+
430
+ casualties: z.number().int().min(0).optional(),
431
+ casualtiesCitations: z.array(z.number().int().min(1)).default([]),
432
+ casualtiesConfidence: z.number().min(0).max(1).default(0),
433
+
434
+ injuries: z.number().int().min(0).optional(),
435
+ injuriesCause: z.enum(["rocket", "rushing_to_shelter"]).optional(),
436
+ injuriesCitations: z.array(z.number().int().min(1)).default([]),
437
+ injuriesConfidence: z.number().min(0).max(1).default(0),
438
+
439
+ confidence: z.number().min(0).max(1).default(0),
440
+ sourcesCount: z.number().int().min(0).default(0),
441
+ citedSources: z
442
+ .array(CitedSourceSchema)
443
+ .default([])
444
+ .describe("All valid sources, ordered by citation index"),
445
+ });
446
+ export type VotedResult = z.infer<typeof VotedResultSchema>;
447
+
448
+ // ─────────────────────────────────────────────────────────
449
+ // Simplified Vote Result (using Insight discriminatedUnion)
450
+ // ─────────────────────────────────────────────────────────
451
+
452
+ export const ConsensusInsightSchema = z.object({
453
+ insight: InsightSchema,
454
+ validExtractions: z
455
+ .array(z.lazy(() => ValidatedExtractionSchema))
456
+ .default([]),
457
+ avgConfidence: z.number().min(0).max(1),
458
+ sourcesCount: z.number().int().min(1),
459
+ });
460
+ export type ConsensusInsight = z.infer<typeof ConsensusInsightSchema>;
461
+
462
+ export const VoteResultSchemaV2 = z.object({
463
+ insights: z.array(ConsensusInsightSchema).default([]),
464
+ needsClarify: z.array(z.lazy(() => ExtractionResultSchema)).default([]),
465
+ timestamp: z.number().int().min(0),
466
+ });
467
+ export type VoteResultV2 = z.infer<typeof VoteResultSchemaV2>;
468
+
469
+ // Alias for backward compatibility
470
+ export const VoteResultSchema = VotedResultSchema;
471
+ export type VoteResult = VotedResult;
472
+
473
+ // ─────────────────────────────────────────────────────────
474
+ // Enrichment data (cross-phase persistence)
475
+ // ─────────────────────────────────────────────────────────
476
+
477
+ export const InlineCiteSchema = z.object({
478
+ url: z.url(),
479
+ channel: z.string().min(1),
480
+ });
481
+ export type InlineCite = z.infer<typeof InlineCiteSchema>;
482
+
483
+ export const EnrichmentDataSchema = z.object({
484
+ origin: z
485
+ .string()
486
+ .optional()
487
+ .describe("Origin country (from early_warning or red_alert)"),
488
+ originCites: z.array(InlineCiteSchema).default([]),
489
+ etaAbsolute: z
490
+ .string()
491
+ .optional()
492
+ .describe('ETA absolute time string (e.g. "~17:42")'),
493
+ etaCites: z.array(InlineCiteSchema).default([]),
494
+ rocketCount: z
495
+ .string()
496
+ .optional()
497
+ .describe('Rocket count display string (e.g. "~5–7")'),
498
+ rocketCites: z.array(InlineCiteSchema).default([]),
499
+ isCassette: z.boolean().optional().describe("Is cassette munitions"),
500
+ intercepted: z
501
+ .string()
502
+ .optional()
503
+ .describe('Interception data display string (e.g. "3", "большинство")'),
504
+ interceptedCites: z.array(InlineCiteSchema).default([]),
505
+ seaImpact: z.string().optional().describe("Sea impact display string"),
506
+ openAreaImpact: z
507
+ .string()
508
+ .optional()
509
+ .describe("Open area impact display string"),
510
+ hitsConfirmed: z.string().optional().describe("Confirmed hits on structures"),
511
+ hitsCites: z.array(InlineCiteSchema).default([]),
512
+ hitLocation: z
513
+ .string()
514
+ .optional()
515
+ .describe("Region where impact occurred (in UI language)"),
516
+ hitType: z
517
+ .string()
518
+ .optional()
519
+ .describe('Type of impact: "direct" | "shrapnel"'),
520
+ hitDetail: z
521
+ .string()
522
+ .optional()
523
+ .describe(
524
+ 'Impact detail: where/how (e.g. "на открытой местности", "здание")',
525
+ ),
526
+ noImpacts: z
527
+ .boolean()
528
+ .default(false)
529
+ .describe('Sources explicitly confirm NO impacts ("прилетов нет")'),
530
+ noImpactsCites: z.array(InlineCiteSchema).default([]),
531
+ rocketDetail: z
532
+ .string()
533
+ .optional()
534
+ .describe("Verbatim per-region rocket breakdown"),
535
+ casualties: z
536
+ .string()
537
+ .optional()
538
+ .describe("Casualties / injuries (from resolved)"),
539
+ casualtiesCites: z.array(InlineCiteSchema).default([]),
540
+ injuries: z.string().optional(),
541
+ injuriesCause: z
542
+ .enum(["rocket", "rushing_to_shelter"])
543
+ .optional()
544
+ .describe(
545
+ "Cause display string — set only if injuries came from rushing to shelter",
546
+ ),
547
+ injuriesCites: z.array(InlineCiteSchema).default([]),
548
+ earlyWarningTime: z
549
+ .string()
550
+ .optional()
551
+ .describe(
552
+ 'Time early_warning was received (for red_alert: "Раннее: было в HH:MM")',
553
+ ),
554
+ lastEditHash: z
555
+ .string()
556
+ .optional()
557
+ .describe(
558
+ 'Hash of last enriched text to detect "message not modified" before sending',
559
+ ),
560
+ });
561
+ export type EnrichmentData = z.infer<typeof EnrichmentDataSchema>;
562
+
563
+ // ─────────────────────────────────────────────────────────
564
+ // Chat & Alert metadata (store)
565
+ // ─────────────────────────────────────────────────────────
566
+
567
+ export const TelegramMessageSchema = z.object({
568
+ chatId: z.string().min(1),
569
+ messageId: z.number().int().min(1),
570
+ isCaption: z.boolean(),
571
+ });
572
+ export type TelegramMessage = z.infer<typeof TelegramMessageSchema>;
573
+
574
+ export const AlertMetaSchema = z.object({
575
+ alertId: z.string().min(1),
576
+ messageId: z.number().int().min(1),
577
+ chatId: z.string().min(1),
578
+ isCaption: z.boolean(),
579
+ alertTs: z.number().int().min(0),
580
+ alertType: AlertTypeSchema,
581
+ alertAreas: z.array(z.string().min(1)),
582
+ currentText: z.string().min(1),
583
+ });
584
+ export type AlertMeta = z.infer<typeof AlertMetaSchema>;
585
+
586
+ export const ChannelPostSchema = z.object({
587
+ channel: z.string().min(1),
588
+ text: z.string().min(1),
589
+ ts: z.number().int().min(0),
590
+ messageUrl: z.url().optional(),
591
+ });
592
+ export type ChannelPost = z.infer<typeof ChannelPostSchema>;
593
+
594
+ export const ActiveSessionSchema = z.object({
595
+ sessionId: z.string().min(1),
596
+ sessionStartTs: z.number().int().min(0),
597
+ phase: AlertTypeSchema,
598
+ phaseStartTs: z.number().int().min(0),
599
+ latestAlertId: z.string().min(1),
600
+ latestMessageId: z.number().int().min(1),
601
+ latestAlertTs: z.number().int().min(0),
602
+ chatId: z.string().min(1),
603
+ isCaption: z.boolean(),
604
+ currentText: z.string().min(1),
605
+ baseText: z.string().min(1),
606
+ alertAreas: z.array(z.string().min(1)),
607
+ telegramMessages: z.array(TelegramMessageSchema).optional(),
608
+ });
609
+ export type ActiveSession = z.infer<typeof ActiveSessionSchema>;
610
+
611
+ // ─────────────────────────────────────────────────────────
612
+ // Extract & Clarify contexts
613
+ // ─────────────────────────────────────────────────────────
614
+
615
+ export const ExtractContextSchema = z.object({
616
+ alertTs: z.number().int().min(0),
617
+ alertType: AlertTypeSchema,
618
+ alertAreas: z.array(z.string().min(1)),
619
+ alertId: z.string().min(1),
620
+ language: z.string().min(1),
621
+ existingEnrichment: EnrichmentDataSchema.optional(),
622
+ });
623
+ export type ExtractContext = z.infer<typeof ExtractContextSchema>;
624
+
625
+ export const ClarifyInputSchema = z.object({
626
+ alertId: z.string().min(1),
627
+ alertAreas: z.array(z.string().min(1)),
628
+ alertType: z.string().min(1),
629
+ alertTs: z.number().int().min(0),
630
+ messageId: z.number().int().min(1),
631
+ currentText: z.string().min(1),
632
+ extractions: z.array(ValidatedExtractionSchema),
633
+ votedResult: VotedResultSchema,
634
+ });
635
+ export type ClarifyInput = z.infer<typeof ClarifyInputSchema>;
636
+
637
+ export const ClarifyOutputSchema = z.object({
638
+ newPosts: z.array(ChannelPostSchema),
639
+ newExtractions: z.array(ValidatedExtractionSchema),
640
+ toolCallCount: z.number().int().min(0),
641
+ clarified: z.boolean(),
642
+ });
643
+ export type ClarifyOutput = z.infer<typeof ClarifyOutputSchema>;
644
+
645
+ // ─────────────────────────────────────────────────────────
646
+ // Graph & enrichment input
647
+ // ─────────────────────────────────────────────────────────
648
+
649
+ export const RunEnrichmentInputSchema = z.object({
650
+ alertId: z.string().min(1),
651
+ alertTs: z.number().int().min(0),
652
+ alertType: AlertTypeSchema,
653
+ alertAreas: z.array(z.string().min(1)),
654
+ chatId: z.string().min(1),
655
+ messageId: z.number().int().min(1),
656
+ isCaption: z.boolean(),
657
+ currentText: z.string().min(1),
658
+ telegramMessages: z.array(TelegramMessageSchema).default([]),
659
+ monitoringLabel: z.string().optional(),
660
+ });
661
+ export type RunEnrichmentInput = z.infer<typeof RunEnrichmentInputSchema>;
662
+
663
+ // ─────────────────────────────────────────────────────────
664
+ // Config types
665
+ // ─────────────────────────────────────────────────────────
666
+
667
+ export type AlertTypeConfig = "early" | "red_alert" | "resolved";
668
+
669
+ export const GifModeSchema = z.enum(["funny_cats", "none"]);
670
+ export type GifMode = z.infer<typeof GifModeSchema>;
671
+
672
+ // ─────────────────────────────────────────────────────────
673
+ // Helper functions
674
+ // ─────────────────────────────────────────────────────────
675
+
676
+ /** Create empty enrichment data */
677
+ export function createEmptyEnrichmentData(): EnrichmentData {
678
+ return EnrichmentDataSchema.parse({
679
+ originCites: [],
680
+ etaCites: [],
681
+ rocketCites: [],
682
+ interceptedCites: [],
683
+ hitsCites: [],
684
+ noImpacts: false,
685
+ noImpactsCites: [],
686
+ casualtiesCites: [],
687
+ injuriesCites: [],
688
+ });
689
+ }
690
+
691
+ export const emptyEnrichmentData = createEmptyEnrichmentData();
692
+
693
+ /** Validate and parse JSON string */
694
+ export function parseJSON<T extends z.ZodSchema>(
695
+ schema: T,
696
+ json: string,
697
+ ): z.infer<T> {
698
+ const parsed = JSON.parse(json);
699
+ return schema.parse(parsed);
700
+ }
701
+
702
+ /** Safely validate data, returning error string instead of throwing */
703
+ export function validateSafe<T extends z.ZodSchema>(
704
+ schema: T,
705
+ data: unknown,
706
+ ): { ok: true; data: z.infer<T> } | { ok: false; error: string } {
707
+ const result = schema.safeParse(data);
708
+ if (result.success) {
709
+ return { ok: true, data: result.data };
710
+ }
711
+ return { ok: false, error: result.error.message };
712
+ }