fodda-mcp 1.32.1 → 1.46.31

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 (186) hide show
  1. package/README.md +14 -16
  2. package/dist/a2aHandler.d.ts +33 -3
  3. package/dist/a2aHandler.d.ts.map +1 -1
  4. package/dist/a2aHandler.js +189 -36
  5. package/dist/a2aHandler.js.map +1 -1
  6. package/dist/agentFacts.d.ts +21 -0
  7. package/dist/agentFacts.d.ts.map +1 -0
  8. package/dist/agentFacts.js +72 -0
  9. package/dist/agentFacts.js.map +1 -0
  10. package/dist/agents/fodda-researcher/index.d.ts +6 -0
  11. package/dist/agents/fodda-researcher/index.d.ts.map +1 -1
  12. package/dist/agents/fodda-researcher/index.js +18 -0
  13. package/dist/agents/fodda-researcher/index.js.map +1 -1
  14. package/dist/agents/fodda-researcher/skills.d.ts +1 -1
  15. package/dist/agents/fodda-researcher/skills.d.ts.map +1 -1
  16. package/dist/agents/fodda-researcher/skills.js +8 -3
  17. package/dist/agents/fodda-researcher/skills.js.map +1 -1
  18. package/dist/brandTemplate.d.ts +1 -1
  19. package/dist/brandTemplate.d.ts.map +1 -1
  20. package/dist/brandTemplate.js +179 -11
  21. package/dist/brandTemplate.js.map +1 -1
  22. package/dist/catalogCache.d.ts +14 -13
  23. package/dist/catalogCache.d.ts.map +1 -1
  24. package/dist/catalogCache.js +266 -67
  25. package/dist/catalogCache.js.map +1 -1
  26. package/dist/coverageRelevance.d.ts +156 -0
  27. package/dist/coverageRelevance.d.ts.map +1 -0
  28. package/dist/coverageRelevance.js +1062 -0
  29. package/dist/coverageRelevance.js.map +1 -0
  30. package/dist/deepResearch.d.ts +43 -0
  31. package/dist/deepResearch.d.ts.map +1 -0
  32. package/dist/deepResearch.js +498 -0
  33. package/dist/deepResearch.js.map +1 -0
  34. package/dist/enrichment.d.ts.map +1 -1
  35. package/dist/enrichment.js +51 -1
  36. package/dist/enrichment.js.map +1 -1
  37. package/dist/errorHandling.d.ts +1 -1
  38. package/dist/errorHandling.d.ts.map +1 -1
  39. package/dist/errorHandling.js +26 -6
  40. package/dist/errorHandling.js.map +1 -1
  41. package/dist/index.d.ts +9 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +548 -38
  44. package/dist/index.js.map +1 -1
  45. package/dist/linkedinEngine.d.ts +150 -0
  46. package/dist/linkedinEngine.d.ts.map +1 -0
  47. package/dist/linkedinEngine.js +696 -0
  48. package/dist/linkedinEngine.js.map +1 -0
  49. package/dist/oauthRegisterShim.d.ts +13 -0
  50. package/dist/oauthRegisterShim.d.ts.map +1 -0
  51. package/dist/oauthRegisterShim.js +83 -0
  52. package/dist/oauthRegisterShim.js.map +1 -0
  53. package/dist/pricingCache.d.ts +6 -8
  54. package/dist/pricingCache.d.ts.map +1 -1
  55. package/dist/pricingCache.js +69 -13
  56. package/dist/pricingCache.js.map +1 -1
  57. package/dist/queryCache.d.ts +2 -0
  58. package/dist/queryCache.d.ts.map +1 -1
  59. package/dist/queryCache.js +11 -2
  60. package/dist/queryCache.js.map +1 -1
  61. package/dist/resources.d.ts +27 -0
  62. package/dist/resources.d.ts.map +1 -0
  63. package/dist/resources.js +126 -0
  64. package/dist/resources.js.map +1 -0
  65. package/dist/sessionTracker.d.ts +24 -2
  66. package/dist/sessionTracker.d.ts.map +1 -1
  67. package/dist/sessionTracker.js +198 -4
  68. package/dist/sessionTracker.js.map +1 -1
  69. package/dist/systemPrompt.d.ts +3 -3
  70. package/dist/systemPrompt.d.ts.map +1 -1
  71. package/dist/systemPrompt.js +81 -61
  72. package/dist/systemPrompt.js.map +1 -1
  73. package/dist/telemetry.d.ts +43 -0
  74. package/dist/telemetry.d.ts.map +1 -0
  75. package/dist/telemetry.js +101 -0
  76. package/dist/telemetry.js.map +1 -0
  77. package/dist/test_analyst.js +2 -2
  78. package/dist/test_analyst.js.map +1 -1
  79. package/dist/test_book_a_call_live.d.ts +2 -0
  80. package/dist/test_book_a_call_live.d.ts.map +1 -0
  81. package/dist/test_book_a_call_live.js +148 -0
  82. package/dist/test_book_a_call_live.js.map +1 -0
  83. package/dist/test_brand_intelligence_endpoint.d.ts +2 -0
  84. package/dist/test_brand_intelligence_endpoint.d.ts.map +1 -0
  85. package/dist/test_brand_intelligence_endpoint.js +20 -0
  86. package/dist/test_brand_intelligence_endpoint.js.map +1 -0
  87. package/dist/test_coverage_relevance.d.ts +12 -0
  88. package/dist/test_coverage_relevance.d.ts.map +1 -0
  89. package/dist/test_coverage_relevance.js +121 -0
  90. package/dist/test_coverage_relevance.js.map +1 -0
  91. package/dist/test_dcr_and_legacy_deprecation.d.ts +2 -0
  92. package/dist/test_dcr_and_legacy_deprecation.d.ts.map +1 -0
  93. package/dist/test_dcr_and_legacy_deprecation.js +148 -0
  94. package/dist/test_dcr_and_legacy_deprecation.js.map +1 -0
  95. package/dist/test_financial_snapshot_render.d.ts +2 -0
  96. package/dist/test_financial_snapshot_render.d.ts.map +1 -0
  97. package/dist/test_financial_snapshot_render.js +167 -0
  98. package/dist/test_financial_snapshot_render.js.map +1 -0
  99. package/dist/test_gap_alert.d.ts +12 -0
  100. package/dist/test_gap_alert.d.ts.map +1 -0
  101. package/dist/test_gap_alert.js +43 -0
  102. package/dist/test_gap_alert.js.map +1 -0
  103. package/dist/test_list_graphs_topics.d.ts +2 -0
  104. package/dist/test_list_graphs_topics.d.ts.map +1 -0
  105. package/dist/test_list_graphs_topics.js +26 -0
  106. package/dist/test_list_graphs_topics.js.map +1 -0
  107. package/dist/test_live_endpoint_runner.d.ts +2 -0
  108. package/dist/test_live_endpoint_runner.d.ts.map +1 -0
  109. package/dist/test_live_endpoint_runner.js +65 -0
  110. package/dist/test_live_endpoint_runner.js.map +1 -0
  111. package/dist/test_local_list_graphs.d.ts +2 -0
  112. package/dist/test_local_list_graphs.d.ts.map +1 -0
  113. package/dist/test_local_list_graphs.js +53 -0
  114. package/dist/test_local_list_graphs.js.map +1 -0
  115. package/dist/test_next_moves.d.ts +2 -0
  116. package/dist/test_next_moves.d.ts.map +1 -0
  117. package/dist/test_next_moves.js +349 -0
  118. package/dist/test_next_moves.js.map +1 -0
  119. package/dist/test_next_moves_transcripts.d.ts +2 -0
  120. package/dist/test_next_moves_transcripts.d.ts.map +1 -0
  121. package/dist/test_next_moves_transcripts.js +413 -0
  122. package/dist/test_next_moves_transcripts.js.map +1 -0
  123. package/dist/test_nike_cmo_alias.d.ts +2 -0
  124. package/dist/test_nike_cmo_alias.d.ts.map +1 -0
  125. package/dist/test_nike_cmo_alias.js +76 -0
  126. package/dist/test_nike_cmo_alias.js.map +1 -0
  127. package/dist/test_session_next_moves_telemetry.d.ts +2 -0
  128. package/dist/test_session_next_moves_telemetry.d.ts.map +1 -0
  129. package/dist/test_session_next_moves_telemetry.js +100 -0
  130. package/dist/test_session_next_moves_telemetry.js.map +1 -0
  131. package/dist/test_token_resolution.d.ts +2 -0
  132. package/dist/test_token_resolution.d.ts.map +1 -0
  133. package/dist/test_token_resolution.js +82 -0
  134. package/dist/test_token_resolution.js.map +1 -0
  135. package/dist/toolHandlers.d.ts +7 -3
  136. package/dist/toolHandlers.d.ts.map +1 -1
  137. package/dist/toolHandlers.js +1678 -364
  138. package/dist/toolHandlers.js.map +1 -1
  139. package/dist/tools.d.ts +7 -2
  140. package/dist/tools.d.ts.map +1 -1
  141. package/dist/tools.js +12 -3
  142. package/dist/tools.js.map +1 -1
  143. package/dist/types.d.ts +9 -46
  144. package/dist/types.d.ts.map +1 -1
  145. package/dist/types.js +6 -0
  146. package/dist/types.js.map +1 -1
  147. package/dist/verify_tools_endpoint.js +4 -3
  148. package/dist/verify_tools_endpoint.js.map +1 -1
  149. package/package.json +5 -2
  150. package/server.json +16 -3
  151. package/dist/scripts/simulate_gemini.d.ts +0 -2
  152. package/dist/scripts/simulate_gemini.d.ts.map +0 -1
  153. package/dist/scripts/simulate_gemini.js +0 -100
  154. package/dist/scripts/simulate_gemini.js.map +0 -1
  155. package/dist/scripts/verify_registry.d.ts +0 -2
  156. package/dist/scripts/verify_registry.d.ts.map +0 -1
  157. package/dist/scripts/verify_registry.js +0 -77
  158. package/dist/scripts/verify_registry.js.map +0 -1
  159. package/dist/src/index.d.ts +0 -2
  160. package/dist/src/index.d.ts.map +0 -1
  161. package/dist/src/index.js +0 -332
  162. package/dist/src/index.js.map +0 -1
  163. package/dist/src/tools.d.ts +0 -12
  164. package/dist/src/tools.d.ts.map +0 -1
  165. package/dist/src/tools.js +0 -104
  166. package/dist/src/tools.js.map +0 -1
  167. package/dist/src/types.d.ts +0 -44
  168. package/dist/src/types.d.ts.map +0 -1
  169. package/dist/src/types.js +0 -2
  170. package/dist/src/types.js.map +0 -1
  171. package/dist/src/verify.d.ts +0 -2
  172. package/dist/src/verify.d.ts.map +0 -1
  173. package/dist/src/verify.js +0 -47
  174. package/dist/src/verify.js.map +0 -1
  175. package/dist/src/verify_hmac.d.ts +0 -2
  176. package/dist/src/verify_hmac.d.ts.map +0 -1
  177. package/dist/src/verify_hmac.js +0 -106
  178. package/dist/src/verify_hmac.js.map +0 -1
  179. package/dist/src/verify_tools_endpoint.d.ts +0 -2
  180. package/dist/src/verify_tools_endpoint.d.ts.map +0 -1
  181. package/dist/src/verify_tools_endpoint.js +0 -40
  182. package/dist/src/verify_tools_endpoint.js.map +0 -1
  183. package/dist/test-waverunner-response.d.ts +0 -2
  184. package/dist/test-waverunner-response.d.ts.map +0 -1
  185. package/dist/test-waverunner-response.js +0 -20
  186. package/dist/test-waverunner-response.js.map +0 -1
@@ -48,12 +48,23 @@ export interface CatalogAnalyst {
48
48
  analyst_id: string;
49
49
  name: string;
50
50
  description: string;
51
+ type?: string;
52
+ agent_type?: string;
53
+ kind?: string;
54
+ is_digital_twin?: boolean;
55
+ is_human_agent?: boolean;
56
+ consult_tool?: string;
57
+ price?: string;
58
+ offerings?: any[];
59
+ status?: string;
60
+ Status?: string;
51
61
  }
52
62
  /**
53
63
  * Initialize the catalog cache. Call once at server startup.
54
64
  * Fetches immediately, then sets up hourly refresh.
55
65
  */
56
66
  export declare function initCatalogCache(): Promise<void>;
67
+ export declare function setCachedCatalogForTesting(catalog: CatalogResponse, analysts?: CatalogAnalyst[]): void;
57
68
  /**
58
69
  * Get the cached catalog. Returns null if not yet fetched.
59
70
  */
@@ -66,9 +77,7 @@ export declare function getAnalysts(): CatalogAnalyst[];
66
77
  * Get all graphs from the cached catalog.
67
78
  */
68
79
  export declare function getGraphs(): CatalogGraph[];
69
- /**
70
- * Get only graphs with status === 'live'.
71
- */
80
+ export declare function getGraphDetails(graphId: string): CatalogGraph | undefined;
72
81
  export declare function getLiveGraphs(): CatalogGraph[];
73
82
  /**
74
83
  * Get domain/curated graphs (graph_type === 'domain').
@@ -138,16 +147,6 @@ export declare function buildCuratedOnlyToolsBlock(): string;
138
147
  * to inject into the system prompt, replacing the hardcoded blocks.
139
148
  */
140
149
  export declare function buildDynamicPromptSections(): string | null;
141
- /**
142
- * Score how relevant a graph is to a query.
143
- * Returns a 0-1 score based on keyword overlap between query terms and graph metadata.
144
- *
145
- * Scoring strategy:
146
- * - Each query term that appears in the graph's searchable text adds to the score
147
- * - Matches in topics/routing_keywords are weighted higher (2x) than description matches
148
- * - Living graphs get a modest freshness boost (+0.05)
149
- * - Brand-name queries (single capitalized word) always include domain graphs
150
- */
151
150
  export declare function scoreGraphRelevance(query: string, g: CatalogGraph): number;
152
151
  /**
153
152
  * Classify a graph into its operational tier based on type + update cadence.
@@ -159,6 +158,7 @@ export interface GraphRelevanceResult {
159
158
  graph: CatalogGraph;
160
159
  score: number;
161
160
  graphTier: 'living' | 'static_expert' | 'report' | 'supplemental' | 'skill';
161
+ isDirectMatch: boolean;
162
162
  }
163
163
  /**
164
164
  * Get the graphs most relevant to a query, using 2-step routing:
@@ -194,6 +194,7 @@ export type SourceCandidate = {
194
194
  score: number;
195
195
  reason: string;
196
196
  };
197
+ export declare function flushSourcePlanCache(): void;
197
198
  /**
198
199
  * Unified source selector: graph candidates from getRelevantGraphs()
199
200
  * (UNCHANGED — same scoring, tier diversity, freshness bonus, same order),
@@ -1 +1 @@
1
- {"version":3,"file":"catalogCache.d.ts","sourceRoot":"","sources":["../src/catalogCache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"catalogCache.d.ts","sourceRoot":"","sources":["../src/catalogCache.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,7 @@
6
6
  * and is the same endpoint the website and app use.
7
7
  */
8
8
  import axios from 'axios';
9
+ import { cacheClear } from './queryCache.js';
9
10
  // ---------------------------------------------------------------------------
10
11
  // Cache
11
12
  // ---------------------------------------------------------------------------
@@ -51,6 +52,7 @@ async function fetchAnalysts() {
51
52
  */
52
53
  export async function initCatalogCache() {
53
54
  try {
55
+ cacheClear();
54
56
  cachedCatalog = await fetchCatalog();
55
57
  cachedAnalysts = await fetchAnalysts();
56
58
  lastFetchedAt = Date.now();
@@ -77,6 +79,13 @@ export async function initCatalogCache() {
77
79
  }
78
80
  }, CATALOG_REFRESH_MS);
79
81
  }
82
+ export function setCachedCatalogForTesting(catalog, analysts) {
83
+ cachedCatalog = catalog;
84
+ if (analysts)
85
+ cachedAnalysts = analysts;
86
+ lastFetchedAt = Date.now();
87
+ rebuildSearchIndex();
88
+ }
80
89
  /**
81
90
  * Get the cached catalog. Returns null if not yet fetched.
82
91
  */
@@ -93,11 +102,11 @@ export function getAnalysts() {
93
102
  * Get all graphs from the cached catalog.
94
103
  */
95
104
  export function getGraphs() {
96
- return cachedCatalog?.graphs ?? [];
105
+ return cachedCatalog?.graphs || [];
106
+ }
107
+ export function getGraphDetails(graphId) {
108
+ return getGraphs().find(g => g.graph_id === graphId);
97
109
  }
98
- /**
99
- * Get only graphs with status === 'live'.
100
- */
101
110
  export function getLiveGraphs() {
102
111
  return getGraphs().filter(g => g.status === 'live');
103
112
  }
@@ -152,20 +161,7 @@ export function getDomainGraphIds() {
152
161
  * Generates: - graphId "retail" → "PSFK's Retail Graph"
153
162
  */
154
163
  export function buildGraphNamingBlock() {
155
- const graphs = getLiveGraphs();
156
- if (graphs.length === 0)
157
- return ''; // fallback — will use hardcoded
158
- const lines = [];
159
- lines.push('GRAPH NAMING: Never call results "the Fodda graph." Fodda is the platform — the knowledge graphs are created by named experts. Always attribute to the expert:');
160
- for (const g of graphs) {
161
- const displayName = buildDisplayName(g);
162
- const domainHint = g.domain ? ` (${g.domain})` : '';
163
- lines.push(`- graphId "${g.graph_id}" → "${displayName}"${domainHint}`);
164
- }
165
- lines.push('Example: "PSFK\'s Retail Graph identifies Retailer-Operated Value-Recovery Programs as a top signal (score: 100)" — NOT "the Fodda graph shows..."');
166
- lines.push('Fodda is the delivery mechanism. The experts are the authority.');
167
- lines.push('When presenting results from community Pattern Graphs, use the creator\'s name in attribution (e.g., "According to [creator]\'s [Graph Name] Pattern Graph on Fodda...").');
168
- return lines.join('\n');
164
+ return 'GRAPH NAMING: Never call results "the Fodda graph." Fodda is the platform — knowledge graphs are created by named experts. Always attribute each graph to its named expert; call `list_graphs` for graph names, curators, and domain details. Example: "PSFK\'s Retail Graph identifies Retailer-Operated Value-Recovery Programs as a top signal (score: 100)" — NOT "the Fodda graph shows..."';
169
165
  }
170
166
  /**
171
167
  * Build a human-friendly display name for a graph.
@@ -517,6 +513,7 @@ let graphSearchTexts = new Map();
517
513
  */
518
514
  function rebuildSearchIndex() {
519
515
  graphSearchTexts.clear();
516
+ flushSourcePlanCache();
520
517
  for (const g of getGraphs()) {
521
518
  graphSearchTexts.set(g.graph_id, buildSearchableText(g));
522
519
  }
@@ -531,47 +528,187 @@ function rebuildSearchIndex() {
531
528
  * - Living graphs get a modest freshness boost (+0.05)
532
529
  * - Brand-name queries (single capitalized word) always include domain graphs
533
530
  */
534
- export function scoreGraphRelevance(query, g) {
535
- const queryLower = query.toLowerCase();
536
- // Stopwords that shouldn't contribute to routing relevance
531
+ const QUERY_EXPANSION_MAP = {
532
+ // Wine & Beverages
533
+ wine: ['beverage', 'drink', 'food', 'hospitality', 'dining', 'nightlife', 'leisure', 'entertainment', 'luxury', 'home', 'appliances', 'kitchen', 'dining', 'cpg', 'retail'],
534
+ beverage: ['beverage', 'food', 'drink', 'cpg', 'retail', 'hospitality', 'dining', 'nightlife'],
535
+ beverages: ['beverage', 'food', 'drink', 'cpg', 'retail', 'hospitality', 'dining', 'nightlife'],
536
+ drinks: ['beverage', 'drink', 'food', 'cpg', 'retail', 'nightlife', 'hospitality'],
537
+ drink: ['beverage', 'drink', 'food', 'cpg', 'retail', 'nightlife', 'hospitality'],
538
+ mocktail: ['beverage', 'drink', 'food', 'hospitality', 'dining', 'nightlife', 'wellness'],
539
+ mocktails: ['beverage', 'drink', 'food', 'hospitality', 'dining', 'nightlife', 'wellness'],
540
+ vin: ['beverage', 'food', 'hospitality', 'dining'],
541
+ sommelier: ['beverage', 'food', 'hospitality', 'dining', 'nightlife'],
542
+ beer: ['beverage', 'drink', 'food', 'hospitality', 'nightlife', 'cpg'],
543
+ spirits: ['beverage', 'drink', 'food', 'hospitality', 'nightlife', 'luxury', 'cpg'],
544
+ cocktail: ['beverage', 'drink', 'food', 'hospitality', 'dining', 'nightlife'],
545
+ barware: ['beverage', 'home', 'appliances', 'dining', 'nightlife', 'tableware', 'retail'],
546
+ glassware: ['home', 'dining', 'beverage', 'design', 'nightlife', 'tableware', 'barware', 'retail'],
547
+ stemware: ['home', 'dining', 'beverage', 'design', 'luxury', 'tableware', 'barware', 'retail'],
548
+ decanter: ['home', 'dining', 'beverage', 'luxury', 'design', 'barware', 'tableware'],
549
+ fridge: ['appliances', 'home', 'kitchen', 'electronics', 'consumer-electronics', 'retail'],
550
+ fridges: ['appliances', 'home', 'kitchen', 'electronics', 'consumer-electronics', 'retail'],
551
+ refrigerator: ['appliances', 'home', 'kitchen', 'electronics', 'consumer-electronics', 'retail'],
552
+ cooler: ['appliances', 'home', 'kitchen', 'electronics', 'beverage', 'cooling', 'retail'],
553
+ coolers: ['appliances', 'home', 'kitchen', 'electronics', 'beverage', 'cooling', 'retail'],
554
+ cellar: ['home', 'luxury', 'architecture', 'design', 'interior', 'wine'],
555
+ cabinet: ['furniture', 'home', 'interior', 'design', 'decor', 'storage'],
556
+ furniture: ['home', 'design', 'living', 'decor', 'interior', 'retail'],
557
+ racking: ['home', 'design', 'storage'],
558
+ accessories: ['consumer-goods', 'home', 'retail', 'products', 'design'],
559
+ // Food & Dining
560
+ restaurant: ['food', 'beverage', 'dining', 'hospitality', 'nightlife'],
561
+ dining: ['food', 'beverage', 'hospitality', 'restaurant', 'dining-trends', 'nightlife', 'home'],
562
+ kitchen: ['home', 'appliances', 'food', 'design'],
563
+ // Home & Living
564
+ lighting: ['home', 'design', 'decor'],
565
+ decor: ['home', 'design', 'living'],
566
+ appliance: ['appliances', 'home', 'kitchen', 'consumer-electronics', 'retail'],
567
+ appliances: ['appliances', 'home', 'kitchen', 'consumer-electronics', 'retail'],
568
+ // Fashion & Apparel
569
+ sneakers: ['fashion', 'apparel', 'footwear', 'streetwear', 'retail'],
570
+ footwear: ['fashion', 'apparel', 'retail'],
571
+ clothing: ['fashion', 'apparel', 'retail'],
572
+ apparel: ['fashion', 'retail'],
573
+ // Beauty & Skincare
574
+ skincare: ['beauty', 'wellness', 'cpg', 'retail'],
575
+ cosmetics: ['beauty', 'cpg', 'retail'],
576
+ fragrance: ['beauty', 'luxury', 'cpg', 'retail'],
577
+ // Tech & Electronics
578
+ smartphone: ['technology', 'electronics', 'consumer-electronics', 'retail'],
579
+ gadget: ['technology', 'electronics', 'consumer-electronics'],
580
+ // Luxury & Travel
581
+ watch: ['luxury', 'fashion', 'retail'],
582
+ watches: ['luxury', 'fashion', 'retail'],
583
+ jewelry: ['luxury', 'fashion', 'retail'],
584
+ // Automotive & Mobility
585
+ ev: ['automotive', 'mobility', 'transportation', 'energy', 'sustainability', 'technology'],
586
+ electric: ['automotive', 'mobility', 'energy', 'sustainability', 'technology'],
587
+ automotive: ['automotive', 'mobility', 'transportation', 'manufacturing'],
588
+ car: ['automotive', 'mobility', 'transportation'],
589
+ cars: ['automotive', 'mobility', 'transportation'],
590
+ vehicle: ['automotive', 'mobility', 'transportation'],
591
+ vehicles: ['automotive', 'mobility', 'transportation'],
592
+ // Pet & Animal Care
593
+ pet: ['pet', 'cpg', 'food', 'wellness', 'retail'],
594
+ pets: ['pet', 'cpg', 'food', 'wellness', 'retail'],
595
+ dog: ['pet', 'cpg', 'food', 'wellness'],
596
+ cat: ['pet', 'cpg', 'food', 'wellness'],
597
+ // Tech, AI & SaaS
598
+ ai: ['technology', 'ai', 'software', 'enterprise', 'innovation'],
599
+ saas: ['technology', 'software', 'enterprise', 'b2b'],
600
+ software: ['technology', 'software', 'enterprise', 'b2b'],
601
+ cloud: ['technology', 'enterprise', 'software'],
602
+ cybersecurity: ['technology', 'enterprise', 'security'],
603
+ };
604
+ function scoreClauseRelevance(clause, g) {
605
+ const clauseLower = clause.toLowerCase().trim();
606
+ if (!clauseLower)
607
+ return 0;
537
608
  const stopWords = new Set([
538
609
  'trend', 'trends', 'consumer', 'consumers', 'report', 'reports',
539
610
  'industry', 'data', 'future', 'insight', 'insights', 'analysis',
540
- 'what', 'how', 'why', 'who', 'when', 'where', 'are', 'the', 'and', 'for', 'with'
611
+ 'what', 'how', 'why', 'who', 'when', 'where', 'are', 'the', 'and', 'for', 'with',
612
+ 'run', 'fodda', 'deep', 'research', 'project', 'about', 'session', 'briefing',
613
+ 'brief', 'study', 'overview', 'summary', 'deck', 'slides', 'presentation', 'topic',
614
+ '2024', '2025', '2026', '2027', '2030', 'year', 'years', 'strategic', 'breakdown',
615
+ 'give', 'provide', 'show', 'tell', 'key'
541
616
  ]);
542
- const queryTerms = queryLower
617
+ const terms = clauseLower
543
618
  .split(/\s+/)
544
- .filter(t => t.length > 2) // Skip very short words (a, in, of, etc.)
545
- .map(t => t.replace(/[^a-z0-9]/g, '')) // Strip punctuation
619
+ .map(t => t.replace(/[^a-z0-9]/g, ''))
546
620
  .filter(t => t.length > 2 && !stopWords.has(t));
547
- if (queryTerms.length === 0)
621
+ if (terms.length === 0)
548
622
  return 0;
549
623
  const searchText = graphSearchTexts.get(g.graph_id) || buildSearchableText(g);
550
- const topicsText = [...(g.topics || []), ...(g.routing_keywords || [])].join(' ').toLowerCase();
624
+ const topicsText = [...(g.topics || []), ...(g.routing_keywords || []), g.name || '', g.graph_id || ''].join(' ').toLowerCase();
551
625
  const domainText = (g.domain || '').toLowerCase();
552
626
  let matchedTerms = 0;
553
627
  let highValueMatches = 0;
554
- for (const term of queryTerms) {
628
+ for (const term of terms) {
555
629
  if (searchText.includes(term)) {
556
630
  matchedTerms++;
557
- // High-value match: the term appears in topics, routing_keywords, or domain
558
631
  if (topicsText.includes(term) || domainText.includes(term)) {
559
632
  highValueMatches++;
560
633
  }
561
634
  }
562
635
  }
563
- if (matchedTerms === 0)
564
- return 0;
565
- // Base score: fraction of query terms that match
566
- let score = matchedTerms / queryTerms.length;
567
- // Boost for high-value matches (topics/domain/routing)
568
- score += (highValueMatches / queryTerms.length) * 0.3;
569
- // Living graphs (any type with recurring updates) get a modest freshness boost
570
- const isLiving = g.update_frequency && !['One-Off', 'Coming Soon'].includes(g.update_frequency);
571
- if (isLiving) {
572
- score += 0.05; // Freshness tiebreaker, not an inclusion floor
636
+ let directScore = 0;
637
+ if (matchedTerms > 0) {
638
+ directScore = (matchedTerms / terms.length) * 0.6 + (highValueMatches / terms.length) * 0.5;
639
+ }
640
+ // Category Expansion Score
641
+ let expansionScore = 0;
642
+ const expandedSet = new Set();
643
+ for (const t of terms) {
644
+ if (QUERY_EXPANSION_MAP[t]) {
645
+ QUERY_EXPANSION_MAP[t].forEach(e => expandedSet.add(e));
646
+ }
647
+ }
648
+ if (expandedSet.size > 0) {
649
+ let expMatches = 0;
650
+ let expHighValueMatches = 0;
651
+ for (const exp of expandedSet) {
652
+ if (searchText.includes(exp)) {
653
+ expMatches++;
654
+ if (topicsText.includes(exp) || domainText.includes(exp)) {
655
+ expHighValueMatches++;
656
+ }
657
+ }
658
+ }
659
+ if (expMatches > 0) {
660
+ expansionScore = 0.20 + 0.20 * Math.min(expMatches / 3, 1.0) + 0.15 * Math.min(expHighValueMatches / 2, 1.0);
661
+ }
662
+ }
663
+ let finalScore = Math.max(directScore, expansionScore);
664
+ // Flagship retail graph anchor boost
665
+ if (g.graph_id === 'retail') {
666
+ finalScore += 0.30;
573
667
  }
574
- return Math.min(score, 1.0);
668
+ // Topic Specialist Boost: if graph's explicit topics, name or routing keywords match query terms directly
669
+ if (highValueMatches > 0) {
670
+ finalScore += 0.10;
671
+ }
672
+ // Specialist Category Boost & Generic Agency Cap
673
+ const specialistProductTerms = new Set([
674
+ 'fridge', 'fridges', 'refrigerator', 'cooler', 'coolers', 'cellar', 'cabinet',
675
+ 'furniture', 'glassware', 'stemware', 'decanter', 'appliance', 'appliances',
676
+ 'barware', 'tableware', 'racking', 'kitchen', 'lighting', 'decor'
677
+ ]);
678
+ const queryHasSpecialistProduct = terms.some(t => specialistProductTerms.has(t));
679
+ if (queryHasSpecialistProduct) {
680
+ const isGenericAgency = /bcg|dentsu|ecdb|kpmg|postpals|greenhouse|michaels|agency|consulting/i.test(g.graph_id + ' ' + (g.domain || ''));
681
+ const isSpecialistGraph = !isGenericAgency && /home|living|interior|decor|appliances?|kitchen|dining|beverage|food|hospitality|leisure/i.test(g.graph_id + ' ' + (g.domain || '') + ' ' + (g.topics || []).join(' ') + ' ' + (g.routing_keywords || []).join(' '));
682
+ if (isSpecialistGraph || g.graph_id === 'retail') {
683
+ finalScore += 0.20; // Specialist Category Boost
684
+ }
685
+ else if (isGenericAgency && directScore === 0) {
686
+ finalScore = Math.min(finalScore, 0.40); // Cap generic agency floor
687
+ }
688
+ }
689
+ // Generalized Narrow-Domain Mismatch Penalty
690
+ const narrowDomains = ['beauty', 'gaming', 'automotive', 'fashion'];
691
+ for (const dom of narrowDomains) {
692
+ const isNarrowGraph = (g.graph_id + (g.domain || '')).toLowerCase().includes(dom);
693
+ const queryHasDom = terms.some(t => t.includes(dom) || (QUERY_EXPANSION_MAP[t] && QUERY_EXPANSION_MAP[t].includes(dom)));
694
+ if (isNarrowGraph && !queryHasDom) {
695
+ finalScore -= 0.30;
696
+ break;
697
+ }
698
+ }
699
+ return Math.max(0, Math.min(finalScore, 1.0));
700
+ }
701
+ export function scoreGraphRelevance(query, g) {
702
+ const clauses = query
703
+ .split(/[,;\.\n]| and | with | for | about /i)
704
+ .map(c => c.trim())
705
+ .filter(c => c.length > 2);
706
+ if (clauses.length === 0)
707
+ return scoreClauseRelevance(query, g);
708
+ const scores = clauses.map(c => scoreClauseRelevance(c, g));
709
+ const maxScore = Math.max(...scores);
710
+ const avgScore = scores.reduce((a, b) => a + b, 0) / scores.length;
711
+ return Math.min((maxScore * 0.7) + (avgScore * 0.3), 1.0);
575
712
  }
576
713
  /**
577
714
  * Classify a graph into its operational tier based on type + update cadence.
@@ -586,10 +723,11 @@ export function classifyGraphTier(g) {
586
723
  const isRecurring = g.update_frequency &&
587
724
  !['One-Off', 'Coming Soon'].includes(g.update_frequency);
588
725
  // Domain graphs with recurring updates + experts with recurring updates = Living
589
- if ((g.graph_type === 'domain' || g.graph_type === 'expert') && isRecurring) {
726
+ // ('analyst' = expert graphs typed 'analyst' in the registry, e.g. Digital Twins)
727
+ if ((g.graph_type === 'domain' || g.graph_type === 'expert' || g.graph_type === 'analyst') && isRecurring) {
590
728
  return 'living';
591
729
  }
592
- if (g.graph_type === 'expert')
730
+ if (g.graph_type === 'expert' || g.graph_type === 'analyst')
593
731
  return 'static_expert';
594
732
  if (g.graph_type === 'industry report')
595
733
  return 'report';
@@ -610,8 +748,18 @@ export function classifyGraphTier(g) {
610
748
  */
611
749
  export function getRelevantGraphs(query, minGraphs = 4, maxGraphs = 15, threshold = 0.10) {
612
750
  const DEPRECATED_GRAPH_IDS = new Set(['waldo', 'psfk']);
613
- const allGraphs = getLiveGraphs().filter(g => (g.graph_type === 'domain' || g.graph_type === 'expert' || g.graph_type === 'industry report')
614
- && !DEPRECATED_GRAPH_IDS.has(g.graph_id));
751
+ // 'analyst' graphs (e.g. Digital Twin expert graphs typed 'analyst' in the
752
+ // registry) are searchable expert content — excluding them made registry-live
753
+ // graphs like jeff-longevity invisible to routing (QA 2026-08-20).
754
+ const ROUTABLE_TYPES = new Set(['domain', 'expert', 'industry report', 'analyst']);
755
+ const allLive = getLiveGraphs();
756
+ const allGraphs = allLive.filter(g => ROUTABLE_TYPES.has(g.graph_type) && !DEPRECATED_GRAPH_IDS.has(g.graph_id));
757
+ // Registry-status visibility: surface live graphs excluded from routing by
758
+ // type, so an unroutable analyst/expert graph is detectable in logs.
759
+ const typeExcluded = allLive.filter(g => !ROUTABLE_TYPES.has(g.graph_type) && g.graph_type !== 'supplemental' && g.graph_type !== 'skill');
760
+ if (typeExcluded.length > 0) {
761
+ console.error(`[graphRouter] ${typeExcluded.length} live graph(s) excluded by graph_type: ${typeExcluded.map(g => `${g.graph_id}(${g.graph_type})`).join(', ')}`);
762
+ }
615
763
  if (allGraphs.length === 0)
616
764
  return [];
617
765
  // Runtime check: skip graphs that have no synced content in Neo4j.
@@ -620,45 +768,62 @@ export function getRelevantGraphs(query, minGraphs = 4, maxGraphs = 15, threshol
620
768
  // Once the CE pipeline syncs them, they'll automatically start routing.
621
769
  const hasContent = (g) => (g.trend_count > 0) || (g.last_synced !== null && g.last_synced !== undefined);
622
770
  const syncedGraphs = allGraphs.filter(hasContent);
623
- const skippedShells = allGraphs.length - syncedGraphs.length;
771
+ const shellGraphs = allGraphs.filter(g => !hasContent(g));
772
+ const skippedShells = shellGraphs.length;
624
773
  if (skippedShells > 0) {
625
- console.error(`[graphRouter] Skipped ${skippedShells} unsynced graph shell(s) (trend_count=0, last_synced=null)`);
774
+ console.error(`[graphRouter] Skipped ${skippedShells} unsynced graph shell(s) (trend_count=0, last_synced=null): ${shellGraphs.map(g => g.graph_id).join(', ')}`);
626
775
  }
627
776
  // ── Phase 0: Direct name matching ──
628
- // When users explicitly name a graph, curator, or company (e.g. "Pull from
629
- // TBWA/Alyson Stevens Macro, SIC (Ben Dietz), Marieke Neleman..."), keyword
630
- // scoring dilutes these names across 100+ query terms, dropping them below
631
- // the threshold. Direct matches get automatic inclusion at score 1.0.
632
777
  const queryLower = query.toLowerCase();
633
778
  const directMatchIds = new Set();
779
+ const matchesWordBoundary = (text, term) => {
780
+ if (!term || term.length < 3)
781
+ return false;
782
+ // Ignore generic topic/format words from forcing Phase 0 direct inclusion
783
+ const genericNames = new Set([
784
+ 'trends', 'report', 'graph', 'data', '2026', 'state', 'outlook',
785
+ 'beauty', 'food', 'home', 'retail', 'fashion', 'travel', 'design'
786
+ ]);
787
+ if (genericNames.has(term.toLowerCase()))
788
+ return false;
789
+ const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
790
+ const rx = new RegExp(`\\b${escaped}\\b`, 'i');
791
+ return rx.test(text);
792
+ };
634
793
  for (const g of syncedGraphs) {
635
794
  // Check graph_id (e.g., "alyson-stevens-macro", "sic")
636
- if (queryLower.includes(g.graph_id)) {
795
+ if (g.graph_id.length > 3 && matchesWordBoundary(queryLower, g.graph_id)) {
637
796
  directMatchIds.add(g.graph_id);
638
797
  continue;
639
798
  }
640
799
  // Check curator full name (e.g., "Alyson Stevens", "Ben Dietz")
641
- const curatorLower = (g.curator || '').toLowerCase();
642
- if (curatorLower && curatorLower.length > 3 && queryLower.includes(curatorLower)) {
800
+ const curatorLower = (g.curator || '').trim().toLowerCase();
801
+ if (curatorLower && curatorLower.length > 4 && matchesWordBoundary(queryLower, curatorLower)) {
643
802
  directMatchIds.add(g.graph_id);
644
803
  continue;
645
804
  }
646
805
  // Check curator last name (>3 chars to avoid false positives like "AI", "NIQ")
647
806
  const curatorParts = curatorLower.split(/\s+/);
648
807
  const curatorLastName = curatorParts[curatorParts.length - 1];
649
- if (curatorLastName && curatorLastName.length > 3 && queryLower.includes(curatorLastName)) {
808
+ if (curatorLastName && curatorLastName.length > 3 && matchesWordBoundary(queryLower, curatorLastName)) {
650
809
  directMatchIds.add(g.graph_id);
651
810
  continue;
652
811
  }
653
812
  // Check company name (e.g., "TBWA", "Delta", "Havas", "Kantar")
654
- const companyLower = (g.company || '').toLowerCase();
655
- if (companyLower && companyLower.length > 2 && queryLower.includes(companyLower)) {
813
+ // Skip generic publisher/consultancy names so mentioning a firm doesn't force all its graphs
814
+ const companyLower = (g.company || '').trim().toLowerCase();
815
+ const genericPublishers = new Set([
816
+ 'mckinsey', 'bcg', 'deloitte', 'kpmg', 'forrester', 'gartner',
817
+ 'pwc', 'ey', 'capgemini', 'bain', 'niq', 'mintel', 'dentsu',
818
+ 'accenture', 'youtube', 'google', 'microsoft'
819
+ ]);
820
+ if (companyLower && companyLower.length > 3 && !genericPublishers.has(companyLower) && matchesWordBoundary(queryLower, companyLower)) {
656
821
  directMatchIds.add(g.graph_id);
657
822
  continue;
658
823
  }
659
824
  // Check graph name (e.g., "SIC", "Connection Index")
660
- const nameLower = (g.name || '').toLowerCase();
661
- if (nameLower && nameLower.length > 3 && queryLower.includes(nameLower)) {
825
+ const nameLower = (g.name || '').trim().toLowerCase();
826
+ if (nameLower && nameLower.length > 3 && matchesWordBoundary(queryLower, nameLower)) {
662
827
  directMatchIds.add(g.graph_id);
663
828
  continue;
664
829
  }
@@ -667,11 +832,15 @@ export function getRelevantGraphs(query, minGraphs = 4, maxGraphs = 15, threshol
667
832
  console.error(`[graphRouter] Phase 0 direct name matches: ${[...directMatchIds].join(', ')}`);
668
833
  }
669
834
  // Score ALL graph types together — domain, expert, industry report compete on merit
670
- const scored = syncedGraphs.map(g => ({
671
- graph: g,
672
- score: directMatchIds.has(g.graph_id) ? 1.0 : scoreGraphRelevance(query, g),
673
- graphTier: classifyGraphTier(g),
674
- }));
835
+ const scored = syncedGraphs.map(g => {
836
+ const isDirect = directMatchIds.has(g.graph_id);
837
+ return {
838
+ graph: g,
839
+ score: isDirect ? 1.0 : scoreGraphRelevance(query, g),
840
+ graphTier: classifyGraphTier(g),
841
+ isDirectMatch: isDirect,
842
+ };
843
+ });
675
844
  // Sort by score descending
676
845
  scored.sort((a, b) => b.score - a.score);
677
846
  // Take all above threshold, respecting min/max bounds
@@ -700,8 +869,22 @@ export function getRelevantGraphs(query, minGraphs = 4, maxGraphs = 15, threshol
700
869
  results.push(best);
701
870
  }
702
871
  };
872
+ ensureTierPresent('living');
703
873
  ensureTierPresent('report');
704
874
  ensureTierPresent('static_expert');
875
+ const stopWords = new Set([
876
+ 'trend', 'trends', 'consumer', 'consumers', 'report', 'reports',
877
+ 'industry', 'data', 'future', 'insight', 'insights', 'analysis',
878
+ 'what', 'how', 'why', 'who', 'when', 'where', 'are', 'the', 'and', 'for', 'with',
879
+ 'run', 'fodda', 'deep', 'research', 'project', 'about', 'session', 'briefing',
880
+ 'brief', 'study', 'overview', 'summary', 'deck', 'slides', 'presentation', 'topic',
881
+ '2024', '2025', '2026', '2027', '2030', 'year', 'years', 'strategic', 'breakdown',
882
+ 'give', 'provide', 'show', 'tell', 'key'
883
+ ]);
884
+ const debugTokens = query.toLowerCase().split(/\s+/).map(t => t.replace(/[^a-z0-9]/g, '')).filter(t => t.length > 2 && !stopWords.has(t));
885
+ const debugExpansions = new Set();
886
+ debugTokens.forEach(t => (QUERY_EXPANSION_MAP[t] || []).forEach(e => debugExpansions.add(e)));
887
+ console.error(`[graphRouter] Extracted tokens: [${debugTokens.join(', ')}] | Fired expansions: [${Array.from(debugExpansions).join(', ')}]`);
705
888
  console.error(`[graphRouter] Query: "${query.substring(0, 60)}..." → ${results.length}/${syncedGraphs.length} synced graphs selected (${skippedShells} shells skipped): ${results.map(r => `${r.graph.graph_id}(${r.score.toFixed(2)},${r.graphTier})`).join(', ')}`);
706
889
  return results;
707
890
  }
@@ -743,11 +926,12 @@ const EARNINGS_SECTORS = ['retail', 'technology', 'travel', 'hospitality', 'luxu
743
926
  // (economic indicators/FRED, demographic surveys/Census, trade data, food
744
927
  // economics + agricultural production, commodity pricing).
745
928
  const SUPPLEMENTAL_CATEGORY_KEYWORDS = {
746
- macro: ['macro', 'economy', 'economic', 'inflation', 'gdp', 'interest rate', 'consumer confidence', 'consumer spending', 'recession', 'fred', 'cost of living', 'purchasing power', 'unemployment'],
747
- demographics: ['demographic', 'demographics', 'population', 'census', 'household income', 'age distribution', 'migration'],
929
+ macro: ['macro', 'economy', 'economic', 'inflation', 'gdp', 'interest rate', 'consumer confidence', 'consumer sentiment', 'michigan sentiment', 'umcsent', 'consumer spending', 'recession', 'fred', 'cost of living', 'purchasing power', 'unemployment'],
930
+ demographics: ['demographic', 'demographics', 'population', 'census', 'household income', 'age distribution', 'migration', 'republican', 'democrat', 'bipartisan', 'party affiliation', 'pew', 'npors'],
748
931
  trade: ['imports', 'exports', 'trade data', 'trade flows', 'tariff', 'tariffs', 'supply chain'],
749
932
  food_economics: ['food prices', 'food costs', 'crop', 'agriculture', 'agricultural', 'nutrition', 'nutritional'],
750
933
  commodities: ['commodity', 'commodities', 'raw materials', 'wholesale prices'],
934
+ science: ['science', 'scientific', 'clinical', 'pubmed', 'openalex', 'clinical trials', 'dermatology', 'peer-reviewed', 'academic', 'study', 'studies', 'journal', 'biomedical'],
751
935
  };
752
936
  /**
753
937
  * Detect an earnings candidate for a query. Deterministic and conservative:
@@ -810,19 +994,33 @@ function detectSupplementalCandidates(query) {
810
994
  }
811
995
  return candidates;
812
996
  }
997
+ const sourcePlanCache = new Map();
998
+ const SOURCE_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
999
+ export function flushSourcePlanCache() {
1000
+ sourcePlanCache.clear();
1001
+ console.error('[catalogCache] Source plan cache flushed');
1002
+ }
813
1003
  /**
814
1004
  * Unified source selector: graph candidates from getRelevantGraphs()
815
1005
  * (UNCHANGED — same scoring, tier diversity, freshness bonus, same order),
816
1006
  * plus additive earnings/supplemental candidates appended after.
817
1007
  */
818
1008
  export function getRelevantSources(query, opts) {
819
- const graphResults = getRelevantGraphs(query, opts?.minGraphs, opts?.maxGraphs, opts?.threshold);
1009
+ const minGraphs = opts?.minGraphs ?? 2;
1010
+ const maxGraphs = opts?.maxGraphs ?? 15;
1011
+ const threshold = opts?.threshold ?? 0.10;
1012
+ const cacheKey = `v1.35.2:${query.trim().toLowerCase()}:${minGraphs}:${maxGraphs}:${threshold}`;
1013
+ const cached = sourcePlanCache.get(cacheKey);
1014
+ if (cached && (Date.now() - cached.timestamp < SOURCE_CACHE_TTL_MS)) {
1015
+ return cached.result;
1016
+ }
1017
+ const graphResults = getRelevantGraphs(query, minGraphs, maxGraphs, threshold);
820
1018
  const candidates = graphResults.map(r => ({
821
1019
  kind: 'graph',
822
1020
  graphId: r.graph.graph_id,
823
1021
  score: r.score,
824
1022
  graphTier: r.graphTier,
825
- reason: r.score >= 1.0 ? 'named directly in query' : `topic match (score ${r.score.toFixed(2)}, ${r.graphTier})`,
1023
+ reason: r.isDirectMatch ? 'named directly in query' : `topic match (score ${r.score.toFixed(2)}, ${r.graphTier})`,
826
1024
  }));
827
1025
  const earnings = detectEarningsCandidate(query);
828
1026
  if (earnings)
@@ -832,6 +1030,7 @@ export function getRelevantSources(query, opts) {
832
1030
  if (extras.length > 0) {
833
1031
  console.error(`[sourceRouter] Extra candidates appended: ${extras.map(c => c.kind === 'earnings' ? `earnings(${c.ticker || c.brand || c.sector || 'search'})` : `supplemental(${c.category})`).join(', ')}`);
834
1032
  }
1033
+ sourcePlanCache.set(cacheKey, { result: candidates, timestamp: Date.now() });
835
1034
  return candidates;
836
1035
  }
837
1036
  //# sourceMappingURL=catalogCache.js.map