@foundrynorth/compass-schema 1.0.0 → 1.0.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.
@@ -0,0 +1,1059 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * SERVICE AREA TYPES
4
+ * For home service businesses (HVAC, plumbing, electrical, etc.),
5
+ * competition is based on service area overlap rather than physical distance.
6
+ */
7
+ export type BusinessType = 'retail' | 'restaurant' | 'service_business' | 'other';
8
+ export interface ServiceArea {
9
+ zips?: string[];
10
+ cities?: string[];
11
+ namedRegions?: string[];
12
+ radiusMiles?: number;
13
+ coverageScore?: number;
14
+ }
15
+ export interface ServiceAreaOverlap {
16
+ overlapZips: string[];
17
+ overlapCities: string[];
18
+ overlapRatio: number;
19
+ competitorTier: 'primary' | 'secondary' | 'peripheral';
20
+ }
21
+ /**
22
+ * CANONICAL COMPETITOR TYPE
23
+ * Single source of truth for competitor representation across the platform.
24
+ * Use this type for:
25
+ * - Enrichment persistence
26
+ * - Prompt construction
27
+ * - UI display on competitor cards
28
+ *
29
+ * This type normalizes competitors from all sources (Google Places, DataForSEO, Perplexity)
30
+ * into a consistent shape for the entire application.
31
+ */
32
+ export interface CanonicalCompetitor {
33
+ id: string;
34
+ name: string;
35
+ domain?: string;
36
+ placeId?: string;
37
+ location?: {
38
+ lat: number;
39
+ lng: number;
40
+ city?: string;
41
+ state?: string;
42
+ address?: string;
43
+ };
44
+ category?: string;
45
+ sources: string[];
46
+ metrics?: {
47
+ searchShare?: number;
48
+ estClicks?: number;
49
+ estSpend?: number;
50
+ rating?: number;
51
+ reviewCount?: number;
52
+ };
53
+ isChain?: boolean;
54
+ chainId?: string;
55
+ chainName?: string;
56
+ chainType?: 'national' | 'regional' | 'local';
57
+ locations?: ChainLocation[];
58
+ locationCount?: number;
59
+ /**
60
+ * Selection mode for multi-location chains:
61
+ * - 'single': Only this specific location
62
+ * - 'dma': All locations in the same DMA as prospect
63
+ * - 'overlap': All locations overlapping with prospect's service area
64
+ * - 'all': All locations nationwide (legacy 'chain' mode)
65
+ */
66
+ selectionMode?: ChainSelectionMode;
67
+ selectedLocationIds?: string[];
68
+ locationsByMode?: {
69
+ dma?: number;
70
+ overlap?: number;
71
+ all: number;
72
+ };
73
+ aggregatedMetrics?: {
74
+ totalReviews?: number;
75
+ avgRating?: number;
76
+ serviceAreaUnion?: ServiceArea;
77
+ };
78
+ }
79
+ /**
80
+ * Chain selection mode for multi-location businesses
81
+ * Determines how many locations to include in analysis
82
+ */
83
+ export type ChainSelectionMode = 'single' | 'dma' | 'overlap' | 'all';
84
+ /**
85
+ * Individual location within a chain
86
+ * Used when a competitor or prospect has multiple locations in the market
87
+ */
88
+ export interface ChainLocation {
89
+ placeId: string;
90
+ name: string;
91
+ address?: string;
92
+ city?: string;
93
+ state?: string;
94
+ lat?: number;
95
+ lng?: number;
96
+ distance_km?: number;
97
+ rating?: number;
98
+ reviewCount?: number;
99
+ isPrimary?: boolean;
100
+ dmaCode?: string;
101
+ dmaName?: string;
102
+ inProspectDma?: boolean;
103
+ overlapsProspect?: boolean;
104
+ }
105
+ /**
106
+ * COMPETITOR CREATIVE TYPES
107
+ * Unified interface for ad creatives from Google Ads and Facebook Ads
108
+ * Used for UI display and media planning prompts
109
+ */
110
+ export interface CompetitorCreative {
111
+ source: 'google_ads' | 'facebook_ads';
112
+ platform: 'google' | 'facebook' | 'instagram' | 'audience_network';
113
+ accountName?: string;
114
+ accountId?: string;
115
+ headline?: string;
116
+ primaryText?: string;
117
+ description?: string;
118
+ cta?: string;
119
+ ctaType?: string;
120
+ landingUrl?: string;
121
+ imageUrl?: string;
122
+ imageUrls?: string[] | Array<{
123
+ original: string;
124
+ resized: string;
125
+ }>;
126
+ videoUrl?: string;
127
+ format?: string;
128
+ impressions?: number | null;
129
+ region?: string | null;
130
+ startDate?: string | null;
131
+ endDate?: string | null;
132
+ pageProfileUrl?: string;
133
+ pageProfilePicture?: string;
134
+ pageLikeCount?: number;
135
+ pageCategories?: string[];
136
+ adLibraryUrl?: string;
137
+ sourceUrl?: string;
138
+ raw?: unknown;
139
+ }
140
+ /**
141
+ * Facebook Ad structure (from curious_coder/facebook-ads-library-scraper)
142
+ * Extended to include all fields from the actual API response
143
+ */
144
+ export interface FacebookAdCreative {
145
+ adId?: string;
146
+ adArchiveId?: string;
147
+ adImage?: string;
148
+ adImages?: Array<{
149
+ original: string;
150
+ resized: string;
151
+ }>;
152
+ adCopy?: string;
153
+ headline?: string;
154
+ callToAction?: string;
155
+ ctaType?: string;
156
+ ctaLink?: string;
157
+ publisherPlatforms?: string[];
158
+ startDate?: string;
159
+ endDate?: string;
160
+ status?: string;
161
+ pageName?: string;
162
+ pageProfileUrl?: string;
163
+ pageProfilePicture?: string;
164
+ pageLikeCount?: number;
165
+ pageCategories?: string[];
166
+ displayFormat?: string;
167
+ adLibraryUrl?: string;
168
+ }
169
+ /**
170
+ * Summary of creatives for a competitor
171
+ */
172
+ export interface CompetitorCreativeSummary {
173
+ googleAdsCount: number;
174
+ facebookAdsCount: number;
175
+ totalCount: number;
176
+ creatives: CompetitorCreative[];
177
+ }
178
+ /**
179
+ * CRITICAL CHANGE: Known competitors are now REQUIRED (1-5)
180
+ * Discovery is OPTIONAL and defaults to OFF
181
+ */
182
+ export declare const KnownCompetitorSchema: z.ZodObject<{
183
+ name: z.ZodString;
184
+ placeId: z.ZodOptional<z.ZodString>;
185
+ notes: z.ZodOptional<z.ZodString>;
186
+ address: z.ZodOptional<z.ZodString>;
187
+ website: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
188
+ }, "strip", z.ZodTypeAny, {
189
+ name: string;
190
+ placeId?: string | undefined;
191
+ notes?: string | undefined;
192
+ address?: string | undefined;
193
+ website?: string | undefined;
194
+ }, {
195
+ name: string;
196
+ placeId?: string | undefined;
197
+ notes?: string | undefined;
198
+ address?: string | undefined;
199
+ website?: string | undefined;
200
+ }>;
201
+ export declare const AnalyzePlanInputSchema: z.ZodObject<{
202
+ name: z.ZodOptional<z.ZodString>;
203
+ placeId: z.ZodOptional<z.ZodString>;
204
+ brand: z.ZodOptional<z.ZodString>;
205
+ businessName: z.ZodOptional<z.ZodString>;
206
+ city: z.ZodOptional<z.ZodString>;
207
+ state: z.ZodOptional<z.ZodString>;
208
+ geo: z.ZodOptional<z.ZodString>;
209
+ targetGeo: z.ZodOptional<z.ZodString>;
210
+ lat: z.ZodOptional<z.ZodNumber>;
211
+ lng: z.ZodOptional<z.ZodNumber>;
212
+ chainDetected: z.ZodOptional<z.ZodBoolean>;
213
+ selectedAreas: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
214
+ businessDescription: z.ZodEffects<z.ZodDefault<z.ZodOptional<z.ZodString>>, string, string | undefined>;
215
+ url: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
216
+ knownCompetitors: z.ZodEffects<z.ZodArray<z.ZodObject<{
217
+ name: z.ZodString;
218
+ placeId: z.ZodOptional<z.ZodString>;
219
+ notes: z.ZodOptional<z.ZodString>;
220
+ address: z.ZodOptional<z.ZodString>;
221
+ website: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
222
+ }, "strip", z.ZodTypeAny, {
223
+ name: string;
224
+ placeId?: string | undefined;
225
+ notes?: string | undefined;
226
+ address?: string | undefined;
227
+ website?: string | undefined;
228
+ }, {
229
+ name: string;
230
+ placeId?: string | undefined;
231
+ notes?: string | undefined;
232
+ address?: string | undefined;
233
+ website?: string | undefined;
234
+ }>, "many">, {
235
+ name: string;
236
+ placeId?: string | undefined;
237
+ notes?: string | undefined;
238
+ address?: string | undefined;
239
+ website?: string | undefined;
240
+ }[], {
241
+ name: string;
242
+ placeId?: string | undefined;
243
+ notes?: string | undefined;
244
+ address?: string | undefined;
245
+ website?: string | undefined;
246
+ }[]>;
247
+ discoverAdditional: z.ZodDefault<z.ZodBoolean>;
248
+ maxAdditionalCompetitors: z.ZodDefault<z.ZodNumber>;
249
+ marketId: z.ZodOptional<z.ZodString>;
250
+ marketCenter: z.ZodOptional<z.ZodObject<{
251
+ lat: z.ZodNumber;
252
+ lng: z.ZodNumber;
253
+ }, "strip", z.ZodTypeAny, {
254
+ lat: number;
255
+ lng: number;
256
+ }, {
257
+ lat: number;
258
+ lng: number;
259
+ }>>;
260
+ serviceAreaConfig: z.ZodOptional<z.ZodObject<{
261
+ type: z.ZodDefault<z.ZodEnum<["physical", "hq_radius", "metro", "custom_zips"]>>;
262
+ radiusMiles: z.ZodOptional<z.ZodNumber>;
263
+ zipCodes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
264
+ }, "strip", z.ZodTypeAny, {
265
+ type: "physical" | "hq_radius" | "metro" | "custom_zips";
266
+ radiusMiles?: number | undefined;
267
+ zipCodes?: string[] | undefined;
268
+ }, {
269
+ type?: "physical" | "hq_radius" | "metro" | "custom_zips" | undefined;
270
+ radiusMiles?: number | undefined;
271
+ zipCodes?: string[] | undefined;
272
+ }>>;
273
+ prospectPlaceId: z.ZodOptional<z.ZodString>;
274
+ prospectBrandName: z.ZodOptional<z.ZodString>;
275
+ prospectChainMode: z.ZodOptional<z.ZodEnum<["single", "dma", "overlap", "all"]>>;
276
+ prospectLocationCount: z.ZodOptional<z.ZodNumber>;
277
+ prospectLocations: z.ZodOptional<z.ZodArray<z.ZodObject<{
278
+ placeId: z.ZodString;
279
+ name: z.ZodOptional<z.ZodString>;
280
+ address: z.ZodOptional<z.ZodString>;
281
+ city: z.ZodOptional<z.ZodString>;
282
+ state: z.ZodOptional<z.ZodString>;
283
+ lat: z.ZodOptional<z.ZodNumber>;
284
+ lng: z.ZodOptional<z.ZodNumber>;
285
+ }, "strip", z.ZodTypeAny, {
286
+ placeId: string;
287
+ name?: string | undefined;
288
+ address?: string | undefined;
289
+ city?: string | undefined;
290
+ state?: string | undefined;
291
+ lat?: number | undefined;
292
+ lng?: number | undefined;
293
+ }, {
294
+ placeId: string;
295
+ name?: string | undefined;
296
+ address?: string | undefined;
297
+ city?: string | undefined;
298
+ state?: string | undefined;
299
+ lat?: number | undefined;
300
+ lng?: number | undefined;
301
+ }>, "many">>;
302
+ budget: z.ZodOptional<z.ZodObject<{
303
+ min: z.ZodNumber;
304
+ max: z.ZodNumber;
305
+ }, "strip", z.ZodTypeAny, {
306
+ min: number;
307
+ max: number;
308
+ }, {
309
+ min: number;
310
+ max: number;
311
+ }>>;
312
+ planTotalBudget: z.ZodOptional<z.ZodNumber>;
313
+ planDurationMonths: z.ZodOptional<z.ZodNumber>;
314
+ goals: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
315
+ category: z.ZodOptional<z.ZodString>;
316
+ analysisDepth: z.ZodDefault<z.ZodOptional<z.ZodEnum<["standard", "deep"]>>>;
317
+ hubspotCompanyId: z.ZodOptional<z.ZodString>;
318
+ hubspotUrl: z.ZodOptional<z.ZodString>;
319
+ }, "strip", z.ZodTypeAny, {
320
+ businessDescription: string;
321
+ knownCompetitors: {
322
+ name: string;
323
+ placeId?: string | undefined;
324
+ notes?: string | undefined;
325
+ address?: string | undefined;
326
+ website?: string | undefined;
327
+ }[];
328
+ discoverAdditional: boolean;
329
+ maxAdditionalCompetitors: number;
330
+ analysisDepth: "standard" | "deep";
331
+ name?: string | undefined;
332
+ placeId?: string | undefined;
333
+ brand?: string | undefined;
334
+ businessName?: string | undefined;
335
+ city?: string | undefined;
336
+ state?: string | undefined;
337
+ geo?: string | undefined;
338
+ targetGeo?: string | undefined;
339
+ lat?: number | undefined;
340
+ lng?: number | undefined;
341
+ chainDetected?: boolean | undefined;
342
+ selectedAreas?: string[] | undefined;
343
+ url?: string | undefined;
344
+ marketId?: string | undefined;
345
+ marketCenter?: {
346
+ lat: number;
347
+ lng: number;
348
+ } | undefined;
349
+ serviceAreaConfig?: {
350
+ type: "physical" | "hq_radius" | "metro" | "custom_zips";
351
+ radiusMiles?: number | undefined;
352
+ zipCodes?: string[] | undefined;
353
+ } | undefined;
354
+ prospectPlaceId?: string | undefined;
355
+ prospectBrandName?: string | undefined;
356
+ prospectChainMode?: "single" | "dma" | "overlap" | "all" | undefined;
357
+ prospectLocationCount?: number | undefined;
358
+ prospectLocations?: {
359
+ placeId: string;
360
+ name?: string | undefined;
361
+ address?: string | undefined;
362
+ city?: string | undefined;
363
+ state?: string | undefined;
364
+ lat?: number | undefined;
365
+ lng?: number | undefined;
366
+ }[] | undefined;
367
+ budget?: {
368
+ min: number;
369
+ max: number;
370
+ } | undefined;
371
+ planTotalBudget?: number | undefined;
372
+ planDurationMonths?: number | undefined;
373
+ goals?: string[] | undefined;
374
+ category?: string | undefined;
375
+ hubspotCompanyId?: string | undefined;
376
+ hubspotUrl?: string | undefined;
377
+ }, {
378
+ knownCompetitors: {
379
+ name: string;
380
+ placeId?: string | undefined;
381
+ notes?: string | undefined;
382
+ address?: string | undefined;
383
+ website?: string | undefined;
384
+ }[];
385
+ name?: string | undefined;
386
+ placeId?: string | undefined;
387
+ brand?: string | undefined;
388
+ businessName?: string | undefined;
389
+ city?: string | undefined;
390
+ state?: string | undefined;
391
+ geo?: string | undefined;
392
+ targetGeo?: string | undefined;
393
+ lat?: number | undefined;
394
+ lng?: number | undefined;
395
+ chainDetected?: boolean | undefined;
396
+ selectedAreas?: string[] | undefined;
397
+ businessDescription?: string | undefined;
398
+ url?: string | undefined;
399
+ discoverAdditional?: boolean | undefined;
400
+ maxAdditionalCompetitors?: number | undefined;
401
+ marketId?: string | undefined;
402
+ marketCenter?: {
403
+ lat: number;
404
+ lng: number;
405
+ } | undefined;
406
+ serviceAreaConfig?: {
407
+ type?: "physical" | "hq_radius" | "metro" | "custom_zips" | undefined;
408
+ radiusMiles?: number | undefined;
409
+ zipCodes?: string[] | undefined;
410
+ } | undefined;
411
+ prospectPlaceId?: string | undefined;
412
+ prospectBrandName?: string | undefined;
413
+ prospectChainMode?: "single" | "dma" | "overlap" | "all" | undefined;
414
+ prospectLocationCount?: number | undefined;
415
+ prospectLocations?: {
416
+ placeId: string;
417
+ name?: string | undefined;
418
+ address?: string | undefined;
419
+ city?: string | undefined;
420
+ state?: string | undefined;
421
+ lat?: number | undefined;
422
+ lng?: number | undefined;
423
+ }[] | undefined;
424
+ budget?: {
425
+ min: number;
426
+ max: number;
427
+ } | undefined;
428
+ planTotalBudget?: number | undefined;
429
+ planDurationMonths?: number | undefined;
430
+ goals?: string[] | undefined;
431
+ category?: string | undefined;
432
+ analysisDepth?: "standard" | "deep" | undefined;
433
+ hubspotCompanyId?: string | undefined;
434
+ hubspotUrl?: string | undefined;
435
+ }>;
436
+ export type KnownCompetitor = z.infer<typeof KnownCompetitorSchema>;
437
+ export type AnalyzePlanInput = z.infer<typeof AnalyzePlanInputSchema>;
438
+ /**
439
+ * Validation state after processing
440
+ */
441
+ export interface ValidatedCompetitor extends KnownCompetitor {
442
+ validated: boolean | 'needs_clarification';
443
+ status: 'confirmed' | 'not_found' | 'multiple_matches';
444
+ confidence: number;
445
+ placeId?: string;
446
+ verifiedName?: string;
447
+ verifiedAddress?: string;
448
+ geometry?: {
449
+ lat: number;
450
+ lng: number;
451
+ };
452
+ category?: string;
453
+ rating?: number;
454
+ reviewCount?: number;
455
+ options?: Array<{
456
+ placeId: string;
457
+ name: string;
458
+ address: string;
459
+ distance?: number;
460
+ }>;
461
+ suggestion?: string;
462
+ source: 'user_provided' | 'ai_suggested';
463
+ originalName?: string;
464
+ businessType?: BusinessType;
465
+ serviceArea?: ServiceArea | null;
466
+ serviceAreaOverlap?: ServiceAreaOverlap | null;
467
+ }
468
+ /**
469
+ * Base business profile (minimal required fields)
470
+ */
471
+ export interface BaseBusinessProfile {
472
+ name: string;
473
+ placeId: string | null;
474
+ address: string;
475
+ category?: string;
476
+ rating?: number;
477
+ reviewCount?: number;
478
+ phone?: string;
479
+ website?: string;
480
+ seeded?: boolean;
481
+ source?: string;
482
+ validationBypassed?: boolean;
483
+ bypassedAI?: boolean;
484
+ lat?: number;
485
+ lng?: number;
486
+ }
487
+ /**
488
+ * Enrichment augmentation data (additional fields added during enrichment)
489
+ */
490
+ export interface EnrichmentAugmentation {
491
+ digitalMaturity?: number;
492
+ maturityStage?: string;
493
+ strengths?: string[];
494
+ gaps?: string[];
495
+ competitiveContext?: any;
496
+ comparativeInsights?: any;
497
+ [key: string]: any;
498
+ }
499
+ /**
500
+ * Validated business (base profile + enrichment data)
501
+ */
502
+ export interface ValidatedBusiness extends BaseBusinessProfile, Partial<EnrichmentAugmentation> {
503
+ validated: boolean;
504
+ }
505
+ /**
506
+ * Fully enriched business (all enrichment fields guaranteed)
507
+ */
508
+ export interface FullyEnrichedBusiness extends ValidatedBusiness {
509
+ digitalMaturity: number;
510
+ maturityStage: string;
511
+ strengths: string[];
512
+ gaps: string[];
513
+ }
514
+ /**
515
+ * Enrichment result structure
516
+ */
517
+ export interface EnrichedBusiness {
518
+ placeId: string;
519
+ name: string;
520
+ address: string;
521
+ city: string;
522
+ state: string;
523
+ lat: number;
524
+ lng: number;
525
+ category: string;
526
+ businessType?: BusinessType;
527
+ serviceArea?: ServiceArea | null;
528
+ serviceAreaOverlap?: ServiceAreaOverlap | null;
529
+ rating: number;
530
+ reviewCount: number;
531
+ metaAds: {
532
+ present: boolean;
533
+ activeCount: number;
534
+ totalSeen: number;
535
+ oldestAdDate: string | null;
536
+ impressionsRange: string | null;
537
+ creative?: {
538
+ themes: string[];
539
+ messaging: string[];
540
+ visualStyle: string;
541
+ offers: string[];
542
+ ctaTypes: string[];
543
+ ads?: Array<{
544
+ adId?: string;
545
+ adImage?: string;
546
+ adCopy?: string;
547
+ headline?: string;
548
+ callToAction?: string;
549
+ publisherPlatforms?: string[];
550
+ startDate?: string;
551
+ status?: string;
552
+ }>;
553
+ };
554
+ audience?: {
555
+ platforms: string[];
556
+ ageRanges: string[];
557
+ genders: string[];
558
+ interests: string[];
559
+ };
560
+ estimatedSpend?: {
561
+ monthly: number;
562
+ confidence: 'low' | 'medium' | 'high';
563
+ basis: string;
564
+ };
565
+ };
566
+ website: {
567
+ url: string | null;
568
+ accessible: boolean;
569
+ seo?: {
570
+ titleTag: string;
571
+ metaDescription: string;
572
+ h1Tags: string[];
573
+ contentWordCount: number;
574
+ keywordDensity: {
575
+ [keyword: string]: number;
576
+ };
577
+ mobileSpeed: number;
578
+ desktopSpeed: number;
579
+ coreWebVitals: boolean;
580
+ httpsEnabled: boolean;
581
+ structuredData: string[];
582
+ nabConsistency: boolean;
583
+ localKeywords: string[];
584
+ locationPages: number;
585
+ };
586
+ conversion?: {
587
+ hasBooking: boolean;
588
+ hasContactForm: boolean;
589
+ hasPhoneNumber: boolean;
590
+ hasChatWidget: boolean;
591
+ ctaCount: number;
592
+ ctaTypes: string[];
593
+ };
594
+ };
595
+ technologies: string[];
596
+ sophistication: 'basic' | 'intermediate' | 'advanced';
597
+ hasEcommerce: boolean;
598
+ hasAnalytics: boolean;
599
+ hasBlog: boolean;
600
+ mobileOptimized: boolean;
601
+ loadTime: number;
602
+ social: {
603
+ facebook?: {
604
+ url: string;
605
+ followers: number;
606
+ postsPerWeek: number;
607
+ engagementRate: number;
608
+ lastPostDate: string;
609
+ };
610
+ instagram?: {
611
+ url: string;
612
+ followers: number;
613
+ postsPerWeek: number;
614
+ engagementRate: number;
615
+ lastPostDate: string;
616
+ };
617
+ linkedin?: {
618
+ url: string;
619
+ followers: number;
620
+ };
621
+ };
622
+ googleAds?: {
623
+ active: boolean;
624
+ adCount: number;
625
+ advertiserName?: string;
626
+ lastScrapedAt?: Date;
627
+ fromCache?: boolean;
628
+ ads?: Array<{
629
+ id: string;
630
+ adType: 'image' | 'video' | 'text';
631
+ imageUrl?: string;
632
+ videoUrl?: string;
633
+ headline?: string;
634
+ description?: string;
635
+ landingPage?: string;
636
+ advertiser?: string;
637
+ format?: string;
638
+ lastShownDate?: string;
639
+ regions?: string[];
640
+ }>;
641
+ };
642
+ creatives?: CompetitorCreative[];
643
+ strategy: {
644
+ channelPresence: {
645
+ [channel: string]: {
646
+ active: boolean;
647
+ evidence: string;
648
+ investmentLevel: 'low' | 'medium' | 'high';
649
+ };
650
+ };
651
+ messaging: {
652
+ primaryThemes: string[];
653
+ positioning: string;
654
+ tone: 'professional' | 'casual' | 'playful' | 'authoritative';
655
+ };
656
+ sophistication: {
657
+ level: 'beginner' | 'intermediate' | 'advanced';
658
+ signals: string[];
659
+ investmentLevel: 'low' | 'medium' | 'high';
660
+ };
661
+ strengths: string[];
662
+ gaps: string[];
663
+ };
664
+ digitalMaturity: number;
665
+ maturityStage: 'Foundation' | 'Growth' | 'Advanced';
666
+ source: 'prospect' | 'user_provided' | 'ai_suggested';
667
+ validated: boolean;
668
+ }
669
+ /**
670
+ * Prospect context for media planning
671
+ * Contains essential business information extracted from enrichment
672
+ */
673
+ export interface PlanningProspect {
674
+ name: string;
675
+ category: string;
676
+ serviceAreaDescription: string;
677
+ websiteSummary?: string;
678
+ keyValueProps?: string[];
679
+ currentChannels?: string[];
680
+ weaknesses?: string[];
681
+ gaps?: string[];
682
+ }
683
+ /**
684
+ * Competitor context for media planning
685
+ * Summarized competitive intelligence including creative themes
686
+ */
687
+ export interface PlanningCompetitor {
688
+ name: string;
689
+ category: string;
690
+ serviceAreaDescription?: string;
691
+ adSpendLevel?: 'low' | 'medium' | 'high';
692
+ googleAdsThemes?: string[];
693
+ metaAdsThemes?: string[];
694
+ landingPagePatterns?: string[];
695
+ activeChannels?: string[];
696
+ investmentLevel?: 'low' | 'medium' | 'high';
697
+ }
698
+ /**
699
+ * Performance benchmarks for a product/channel
700
+ * Used to inform realistic KPI expectations
701
+ */
702
+ export interface PlanningBenchmark {
703
+ productCode: string;
704
+ mappedIndustry: string;
705
+ cpm?: number;
706
+ ctr?: number;
707
+ cvr?: number;
708
+ viewability?: number;
709
+ }
710
+ /**
711
+ * PlanningContext DTO
712
+ * Single structured object passed to media plan LLM
713
+ *
714
+ * This replaces scattered string concatenation with a clean JSON structure
715
+ * that the model can reference systematically when generating plans.
716
+ */
717
+ export interface PlanningContext {
718
+ prospect: PlanningProspect;
719
+ competitors: PlanningCompetitor[];
720
+ performanceBenchmarks: PlanningBenchmark[];
721
+ competitorCount: number;
722
+ advertisingIntensity: 'low' | 'medium' | 'high';
723
+ marketGaps: string[];
724
+ creativeStrategySummary?: string;
725
+ }
726
+ /**
727
+ * Local Market Score - single 0-100 number for quick comparison
728
+ * Distills the 5-pillar Local Authority Score into a sales-friendly format
729
+ */
730
+ export interface LocalMarketScore {
731
+ /** Single 0-100 score for quick comparison */
732
+ score: number;
733
+ /** Classification for sales conversations */
734
+ tier: 'Leader' | 'Competitive' | 'Emerging' | 'At Risk';
735
+ /** Color code for UI (red, orange, yellow, gray) */
736
+ tierColor: 'red' | 'orange' | 'yellow' | 'gray';
737
+ /** One-line summary for sales pitch */
738
+ summary: string;
739
+ /** Component breakdown for details */
740
+ breakdown: {
741
+ localPack: {
742
+ score: number;
743
+ maxScore: 40;
744
+ label: string;
745
+ };
746
+ reviews: {
747
+ score: number;
748
+ maxScore: 30;
749
+ label: string;
750
+ };
751
+ gbpProfile: {
752
+ score: number;
753
+ maxScore: 20;
754
+ label: string;
755
+ };
756
+ digital: {
757
+ score: number;
758
+ maxScore: 10;
759
+ label: string;
760
+ };
761
+ };
762
+ /** Calculated timestamp */
763
+ calculatedAt: string;
764
+ }
765
+ /**
766
+ * Revenue Impact - quantifies the dollar value of local search position
767
+ */
768
+ export interface RevenueImpact {
769
+ /** Monthly revenue prospect is currently capturing */
770
+ currentMonthlyRevenue: number;
771
+ /** Monthly revenue the market leader captures */
772
+ leaderMonthlyRevenue: number;
773
+ /** Monthly revenue gap (what prospect is missing) */
774
+ monthlyRevenueGap: number;
775
+ /** Annual revenue opportunity */
776
+ annualOpportunity: number;
777
+ /** Benchmarks used for calculation */
778
+ benchmarks: {
779
+ avgTicket: number;
780
+ conversionRate: number;
781
+ category: string;
782
+ };
783
+ /** Position-based CTR breakdown */
784
+ positionAnalysis: {
785
+ prospectCTR: number;
786
+ leaderCTR: number;
787
+ ctrGap: number;
788
+ };
789
+ /** Funnel metrics for transparency */
790
+ funnel: {
791
+ monthlySearchVolume: number;
792
+ prospectClicks: number;
793
+ prospectConversions: number;
794
+ leaderClicks: number;
795
+ leaderConversions: number;
796
+ };
797
+ /** Sales-ready talking points */
798
+ insights: {
799
+ headline: string;
800
+ comparison: string;
801
+ opportunity: string;
802
+ };
803
+ /** Calculation timestamp */
804
+ calculatedAt: string;
805
+ }
806
+ /**
807
+ * Single comparison point in a head-to-head analysis
808
+ */
809
+ export interface WinLossItem {
810
+ /** What's being compared */
811
+ metric: string;
812
+ /** Prospect's value/status */
813
+ prospectValue: string | number;
814
+ /** Competitor's value/status */
815
+ competitorValue: string | number;
816
+ /** Whether prospect wins this comparison */
817
+ prospectWins: boolean;
818
+ /** Impact level for prioritization */
819
+ impact: 'high' | 'medium' | 'low';
820
+ /** One-sentence insight for sales pitch */
821
+ insight: string;
822
+ /** Actionable recommendation */
823
+ action?: string;
824
+ }
825
+ /**
826
+ * Head-to-head comparison with a single competitor
827
+ */
828
+ export interface HeadToHeadComparison {
829
+ /** Competitor being compared */
830
+ competitorName: string;
831
+ /** List of wins for prospect */
832
+ wins: WinLossItem[];
833
+ /** List of losses for prospect */
834
+ losses: WinLossItem[];
835
+ /** Overall assessment */
836
+ summary: {
837
+ winCount: number;
838
+ lossCount: number;
839
+ verdict: 'prospect_leads' | 'competitor_leads' | 'close_match';
840
+ headline: string;
841
+ };
842
+ /** Priority actions based on losses */
843
+ priorityActions: string[];
844
+ /** Calculated at timestamp */
845
+ calculatedAt: string;
846
+ }
847
+ /**
848
+ * Full competitive narrative across all competitors
849
+ */
850
+ export interface CompetitiveNarrative {
851
+ /** Prospect name */
852
+ prospectName: string;
853
+ /** Head-to-head comparisons with each competitor */
854
+ headToHead: HeadToHeadComparison[];
855
+ /** Market-level summary */
856
+ marketSummary: {
857
+ /** Where prospect wins vs the market */
858
+ marketWins: string[];
859
+ /** Where prospect loses vs the market */
860
+ marketLosses: string[];
861
+ /** Overall market position */
862
+ position: 'leader' | 'competitive' | 'challenger' | 'at_risk';
863
+ /** Executive summary paragraph */
864
+ executiveSummary: string;
865
+ };
866
+ /** Top 3 actions to take */
867
+ topActions: Array<{
868
+ action: string;
869
+ impact: string;
870
+ competitor: string;
871
+ }>;
872
+ /** Calculated at timestamp */
873
+ calculatedAt: string;
874
+ }
875
+ /** Prospect enrichment from business lookup + website analysis */
876
+ export interface IntelligenceProspect extends PlanningProspect {
877
+ url?: string;
878
+ placeId?: string;
879
+ address?: string;
880
+ city?: string;
881
+ state?: string;
882
+ phone?: string;
883
+ rating?: number;
884
+ reviewCount?: number;
885
+ techStack?: string[];
886
+ digitalMaturityScore?: number;
887
+ pixelsDetected?: string[];
888
+ socialProfiles?: Record<string, string>;
889
+ /** Nielsen DMA code for market intelligence lookups */
890
+ dmaCode?: string;
891
+ }
892
+ /** Ad intelligence for a single competitor or prospect */
893
+ export interface AdIntelligenceEntry {
894
+ entityName: string;
895
+ googleAdsCount?: number;
896
+ googleAdsThemes?: string[];
897
+ metaAdsCount?: number;
898
+ metaAdsThemes?: string[];
899
+ estimatedMonthlySpend?: number;
900
+ activeChannels?: string[];
901
+ creativeExamples?: Array<{
902
+ platform: string;
903
+ headline?: string;
904
+ description?: string;
905
+ imageUrl?: string;
906
+ }>;
907
+ }
908
+ /** Pixel / tracking detection results */
909
+ export interface PixelDetectionResult {
910
+ url: string;
911
+ pixelsFound: string[];
912
+ analyticsTools: string[];
913
+ adPlatforms: string[];
914
+ tagManagers: string[];
915
+ remarketingActive: boolean;
916
+ }
917
+ /** Domain SEO overview */
918
+ export interface DomainOverviewResult {
919
+ domain: string;
920
+ organicKeywords?: number;
921
+ organicTraffic?: number;
922
+ domainAuthority?: number;
923
+ backlinks?: number;
924
+ topKeywords?: Array<{
925
+ keyword: string;
926
+ position: number;
927
+ volume: number;
928
+ }>;
929
+ }
930
+ /** SEO keyword/ranking analysis (distinct from domain-level overview) */
931
+ export interface SeoAnalysisResult {
932
+ domain: string;
933
+ totalKeywordsTracked?: number;
934
+ keywordsInTop10?: number;
935
+ keywordsInTop3?: number;
936
+ organicTraffic?: number;
937
+ topKeywords?: Array<{
938
+ keyword: string;
939
+ position: number;
940
+ volume: number;
941
+ difficulty?: number;
942
+ }>;
943
+ contentGaps?: string[];
944
+ technicalIssues?: string[];
945
+ }
946
+ /** Landing page audit results */
947
+ export interface LandingPageAuditResult {
948
+ url: string;
949
+ mobileScore?: number;
950
+ desktopScore?: number;
951
+ loadTimeMs?: number;
952
+ hasCtaAboveFold?: boolean;
953
+ hasForm?: boolean;
954
+ hasPhoneNumber?: boolean;
955
+ issues?: string[];
956
+ }
957
+ /** Social media presence data */
958
+ export interface SocialDataResult {
959
+ profiles: Array<{
960
+ platform: string;
961
+ url?: string;
962
+ followers?: number;
963
+ postsPerWeek?: number;
964
+ engagementRate?: number;
965
+ }>;
966
+ }
967
+ /** Strategy brief output */
968
+ export interface StrategyBriefResult {
969
+ executiveSummary: string;
970
+ marketPosition: string;
971
+ recommendedChannels: Array<{
972
+ channel: string;
973
+ productCode?: string;
974
+ rationale: string;
975
+ priority: 'high' | 'medium' | 'low';
976
+ }>;
977
+ competitiveGaps: string[];
978
+ targetAudience?: string;
979
+ messagingThemes?: string[];
980
+ }
981
+ /** Market context from market intelligence tables */
982
+ export interface MarketContextResult {
983
+ seasonality?: {
984
+ type: string;
985
+ score: number;
986
+ peakMonths: string[];
987
+ monthlyMultipliers: Record<string, number>;
988
+ };
989
+ benchmarks?: {
990
+ avgTicket: number;
991
+ conversionRate: number;
992
+ marketSaturationScore?: number;
993
+ yoyGrowthRate?: number;
994
+ };
995
+ marketScore?: {
996
+ overallScore: number;
997
+ timingScore: number;
998
+ competitionScore: number;
999
+ economicScore?: number;
1000
+ recommendation: string;
1001
+ rationale?: string;
1002
+ };
1003
+ competitiveLandscape?: {
1004
+ totalBusinesses?: number;
1005
+ advertisingPenetration?: number;
1006
+ marketConcentration?: string;
1007
+ opportunityScore?: number;
1008
+ };
1009
+ economicIndicators?: {
1010
+ consumerConfidenceIndex?: number;
1011
+ unemploymentRate?: number;
1012
+ medianHouseholdIncome?: number;
1013
+ economicHealthScore?: number;
1014
+ economicHealthTrend?: string;
1015
+ };
1016
+ searchVolume?: {
1017
+ monthlySearchVolume?: number;
1018
+ topKeywords?: Array<{
1019
+ keyword: string;
1020
+ volume: number;
1021
+ }>;
1022
+ yoyChangePct?: number;
1023
+ trendDirection?: string;
1024
+ };
1025
+ }
1026
+ /**
1027
+ * IntelligenceData — the single structured blob written to plans.intelligenceData.
1028
+ *
1029
+ * fn-v2 analyze-location.ts writes this after all enrichment tools complete.
1030
+ * All fields are optional — data fills in progressively as tools complete.
1031
+ */
1032
+ export interface IntelligenceData {
1033
+ /** Enriched prospect profile */
1034
+ prospect?: IntelligenceProspect;
1035
+ /** Enriched competitor profiles */
1036
+ competitors?: PlanningCompetitor[];
1037
+ /** Ad intelligence (prospect + competitors) */
1038
+ adIntelligence?: AdIntelligenceEntry[];
1039
+ /** Pixel / tracking detection for prospect site */
1040
+ pixelDetection?: PixelDetectionResult;
1041
+ /** Domain SEO overview for prospect */
1042
+ domainOverview?: DomainOverviewResult;
1043
+ /** Landing page audit for prospect */
1044
+ landingPageAudit?: LandingPageAuditResult;
1045
+ /** SEO keyword/ranking analysis */
1046
+ seoData?: SeoAnalysisResult;
1047
+ /** Social media presence */
1048
+ socialData?: SocialDataResult;
1049
+ /** Strategy brief from LLM analysis */
1050
+ strategyBrief?: StrategyBriefResult;
1051
+ /** Market intelligence (seasonality, benchmarks, scores) */
1052
+ marketContext?: MarketContextResult;
1053
+ /** Metadata */
1054
+ enrichedAt?: string;
1055
+ enrichmentVersion?: string;
1056
+ toolsCompleted?: string[];
1057
+ toolsFailed?: string[];
1058
+ }
1059
+ //# sourceMappingURL=analyzeTypes.d.ts.map