fodda-mcp 1.38.0 → 1.46.97

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 (346) hide show
  1. package/README.md +14 -91
  2. package/dist/agents/fodda-researcher/skills.d.ts +1 -1
  3. package/dist/agents/fodda-researcher/skills.d.ts.map +1 -1
  4. package/dist/agents/fodda-researcher/skills.js +1 -1
  5. package/dist/brandTemplate.d.ts +1 -1
  6. package/dist/brandTemplate.d.ts.map +1 -1
  7. package/dist/brandTemplate.js +278 -83
  8. package/dist/brandTemplate.js.map +1 -1
  9. package/dist/catalogCache.d.ts +39 -0
  10. package/dist/catalogCache.d.ts.map +1 -1
  11. package/dist/catalogCache.js +293 -22
  12. package/dist/catalogCache.js.map +1 -1
  13. package/dist/coverageRelevance.d.ts +251 -0
  14. package/dist/coverageRelevance.d.ts.map +1 -0
  15. package/dist/coverageRelevance.js +1857 -0
  16. package/dist/coverageRelevance.js.map +1 -0
  17. package/dist/editorialFill.d.ts +5 -0
  18. package/dist/editorialFill.d.ts.map +1 -1
  19. package/dist/editorialFill.js +1 -1
  20. package/dist/editorialFill.js.map +1 -1
  21. package/dist/enrichment.d.ts +26 -0
  22. package/dist/enrichment.d.ts.map +1 -1
  23. package/dist/enrichment.js +442 -29
  24. package/dist/enrichment.js.map +1 -1
  25. package/dist/errorHandling.d.ts +35 -7
  26. package/dist/errorHandling.d.ts.map +1 -1
  27. package/dist/errorHandling.js +225 -155
  28. package/dist/errorHandling.js.map +1 -1
  29. package/dist/index.d.ts +25 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +411 -106
  32. package/dist/index.js.map +1 -1
  33. package/dist/pricingCache.d.ts.map +1 -1
  34. package/dist/pricingCache.js +55 -3
  35. package/dist/pricingCache.js.map +1 -1
  36. package/dist/queryCache.d.ts.map +1 -1
  37. package/dist/queryCache.js +2 -0
  38. package/dist/queryCache.js.map +1 -1
  39. package/dist/reportBriefing.d.ts +89 -0
  40. package/dist/reportBriefing.d.ts.map +1 -0
  41. package/dist/reportBriefing.js +662 -0
  42. package/dist/reportBriefing.js.map +1 -0
  43. package/dist/searchTemplate.d.ts.map +1 -1
  44. package/dist/searchTemplate.js +65 -53
  45. package/dist/searchTemplate.js.map +1 -1
  46. package/dist/services/graphRouter.d.ts +7 -0
  47. package/dist/services/graphRouter.d.ts.map +1 -0
  48. package/dist/services/graphRouter.js +7 -0
  49. package/dist/services/graphRouter.js.map +1 -0
  50. package/dist/services/notifications.d.ts +2 -2
  51. package/dist/services/notifications.js +3 -3
  52. package/dist/services/notifications.js.map +1 -1
  53. package/dist/sessionTracker.d.ts +42 -3
  54. package/dist/sessionTracker.d.ts.map +1 -1
  55. package/dist/sessionTracker.js +261 -14
  56. package/dist/sessionTracker.js.map +1 -1
  57. package/dist/skillClient.d.ts.map +1 -1
  58. package/dist/skillClient.js +11 -6
  59. package/dist/skillClient.js.map +1 -1
  60. package/dist/svgVisuals.d.ts +17 -19
  61. package/dist/svgVisuals.d.ts.map +1 -1
  62. package/dist/svgVisuals.js +189 -233
  63. package/dist/svgVisuals.js.map +1 -1
  64. package/dist/systemPrompt.d.ts +7 -2
  65. package/dist/systemPrompt.d.ts.map +1 -1
  66. package/dist/systemPrompt.js +153 -81
  67. package/dist/systemPrompt.js.map +1 -1
  68. package/dist/telemetry.d.ts.map +1 -1
  69. package/dist/telemetry.js +15 -0
  70. package/dist/telemetry.js.map +1 -1
  71. package/dist/test_adjacent_trends_polymorphic.d.ts +2 -0
  72. package/dist/test_adjacent_trends_polymorphic.d.ts.map +1 -0
  73. package/dist/test_adjacent_trends_polymorphic.js +154 -0
  74. package/dist/test_adjacent_trends_polymorphic.js.map +1 -0
  75. package/dist/test_book_a_call_live.d.ts +2 -0
  76. package/dist/test_book_a_call_live.d.ts.map +1 -0
  77. package/dist/test_book_a_call_live.js +161 -0
  78. package/dist/test_book_a_call_live.js.map +1 -0
  79. package/dist/test_brand_intelligence_endpoint.js +2 -2
  80. package/dist/test_byo_mcp_submission_clarity.d.ts +2 -0
  81. package/dist/test_byo_mcp_submission_clarity.d.ts.map +1 -0
  82. package/dist/test_byo_mcp_submission_clarity.js +142 -0
  83. package/dist/test_byo_mcp_submission_clarity.js.map +1 -0
  84. package/dist/test_capabilities_guidance.d.ts +2 -0
  85. package/dist/test_capabilities_guidance.d.ts.map +1 -0
  86. package/dist/test_capabilities_guidance.js +372 -0
  87. package/dist/test_capabilities_guidance.js.map +1 -0
  88. package/dist/test_client_provenance.d.ts +2 -0
  89. package/dist/test_client_provenance.d.ts.map +1 -0
  90. package/dist/test_client_provenance.js +77 -0
  91. package/dist/test_client_provenance.js.map +1 -0
  92. package/dist/test_connection_surface_cleanup.d.ts +2 -0
  93. package/dist/test_connection_surface_cleanup.d.ts.map +1 -0
  94. package/dist/test_connection_surface_cleanup.js +203 -0
  95. package/dist/test_connection_surface_cleanup.js.map +1 -0
  96. package/dist/test_consult_routing.d.ts +2 -0
  97. package/dist/test_consult_routing.d.ts.map +1 -0
  98. package/dist/test_consult_routing.js +86 -0
  99. package/dist/test_consult_routing.js.map +1 -0
  100. package/dist/test_consult_split.d.ts +2 -0
  101. package/dist/test_consult_split.d.ts.map +1 -0
  102. package/dist/test_consult_split.js +30 -0
  103. package/dist/test_consult_split.js.map +1 -0
  104. package/dist/test_coverage_relevance.d.ts +12 -0
  105. package/dist/test_coverage_relevance.d.ts.map +1 -0
  106. package/dist/test_coverage_relevance.js +162 -0
  107. package/dist/test_coverage_relevance.js.map +1 -0
  108. package/dist/test_coverage_replay.d.ts +2 -0
  109. package/dist/test_coverage_replay.d.ts.map +1 -0
  110. package/dist/test_coverage_replay.js +170 -0
  111. package/dist/test_coverage_replay.js.map +1 -0
  112. package/dist/test_dcr_and_legacy_deprecation.d.ts +2 -0
  113. package/dist/test_dcr_and_legacy_deprecation.d.ts.map +1 -0
  114. package/dist/test_dcr_and_legacy_deprecation.js +143 -0
  115. package/dist/test_dcr_and_legacy_deprecation.js.map +1 -0
  116. package/dist/test_demand_signal_logging.d.ts +2 -0
  117. package/dist/test_demand_signal_logging.d.ts.map +1 -0
  118. package/dist/test_demand_signal_logging.js +92 -0
  119. package/dist/test_demand_signal_logging.js.map +1 -0
  120. package/dist/test_earnings_activity_scope.d.ts +5 -0
  121. package/dist/test_earnings_activity_scope.d.ts.map +1 -0
  122. package/dist/test_earnings_activity_scope.js +101 -0
  123. package/dist/test_earnings_activity_scope.js.map +1 -0
  124. package/dist/test_earnings_divergence_live.d.ts +5 -0
  125. package/dist/test_earnings_divergence_live.d.ts.map +1 -0
  126. package/dist/test_earnings_divergence_live.js +135 -0
  127. package/dist/test_earnings_divergence_live.js.map +1 -0
  128. package/dist/test_earnings_hygiene_and_binding.d.ts +5 -0
  129. package/dist/test_earnings_hygiene_and_binding.d.ts.map +1 -0
  130. package/dist/test_earnings_hygiene_and_binding.js +104 -0
  131. package/dist/test_earnings_hygiene_and_binding.js.map +1 -0
  132. package/dist/test_exec_quotes_render.d.ts +2 -0
  133. package/dist/test_exec_quotes_render.d.ts.map +1 -0
  134. package/dist/test_exec_quotes_render.js +86 -0
  135. package/dist/test_exec_quotes_render.js.map +1 -0
  136. package/dist/test_expert_intro.d.ts +2 -0
  137. package/dist/test_expert_intro.d.ts.map +1 -0
  138. package/dist/test_expert_intro.js +57 -0
  139. package/dist/test_expert_intro.js.map +1 -0
  140. package/dist/test_expert_layer.d.ts +11 -0
  141. package/dist/test_expert_layer.d.ts.map +1 -0
  142. package/dist/test_expert_layer.js +356 -0
  143. package/dist/test_expert_layer.js.map +1 -0
  144. package/dist/test_financial_snapshot_render.d.ts +2 -0
  145. package/dist/test_financial_snapshot_render.d.ts.map +1 -0
  146. package/dist/test_financial_snapshot_render.js +167 -0
  147. package/dist/test_financial_snapshot_render.js.map +1 -0
  148. package/dist/test_gap_alert.d.ts +12 -0
  149. package/dist/test_gap_alert.d.ts.map +1 -0
  150. package/dist/test_gap_alert.js +51 -0
  151. package/dist/test_gap_alert.js.map +1 -0
  152. package/dist/test_grok_offering_surfaces.d.ts +2 -0
  153. package/dist/test_grok_offering_surfaces.d.ts.map +1 -0
  154. package/dist/test_grok_offering_surfaces.js +155 -0
  155. package/dist/test_grok_offering_surfaces.js.map +1 -0
  156. package/dist/test_human_agent_live.d.ts +2 -0
  157. package/dist/test_human_agent_live.d.ts.map +1 -0
  158. package/dist/test_human_agent_live.js +48 -0
  159. package/dist/test_human_agent_live.js.map +1 -0
  160. package/dist/test_identity_gap.d.ts +2 -0
  161. package/dist/test_identity_gap.d.ts.map +1 -0
  162. package/dist/test_identity_gap.js +171 -0
  163. package/dist/test_identity_gap.js.map +1 -0
  164. package/dist/test_intelligence_and_verifier.d.ts +17 -0
  165. package/dist/test_intelligence_and_verifier.d.ts.map +1 -0
  166. package/dist/test_intelligence_and_verifier.js +162 -0
  167. package/dist/test_intelligence_and_verifier.js.map +1 -0
  168. package/dist/test_latency_mcp_brief5.d.ts +12 -0
  169. package/dist/test_latency_mcp_brief5.d.ts.map +1 -0
  170. package/dist/test_latency_mcp_brief5.js +200 -0
  171. package/dist/test_latency_mcp_brief5.js.map +1 -0
  172. package/dist/test_limit_and_credit_card_capture.d.ts +21 -0
  173. package/dist/test_limit_and_credit_card_capture.d.ts.map +1 -0
  174. package/dist/test_limit_and_credit_card_capture.js +216 -0
  175. package/dist/test_limit_and_credit_card_capture.js.map +1 -0
  176. package/dist/test_list_graphs_topics.js +1 -1
  177. package/dist/test_list_graphs_topics.js.map +1 -1
  178. package/dist/test_live_deployed_mcp_search.d.ts +2 -0
  179. package/dist/test_live_deployed_mcp_search.d.ts.map +1 -0
  180. package/dist/test_live_deployed_mcp_search.js +85 -0
  181. package/dist/test_live_deployed_mcp_search.js.map +1 -0
  182. package/dist/test_live_deployed_split.d.ts +2 -0
  183. package/dist/test_live_deployed_split.d.ts.map +1 -0
  184. package/dist/test_live_deployed_split.js +72 -0
  185. package/dist/test_live_deployed_split.js.map +1 -0
  186. package/dist/test_live_endpoint_runner.js +2 -2
  187. package/dist/test_live_endpoint_runner.js.map +1 -1
  188. package/dist/test_live_mcp.js +1 -1
  189. package/dist/test_live_mcp.js.map +1 -1
  190. package/dist/test_live_neil_widget.d.ts +2 -0
  191. package/dist/test_live_neil_widget.d.ts.map +1 -0
  192. package/dist/test_live_neil_widget.js +84 -0
  193. package/dist/test_live_neil_widget.js.map +1 -0
  194. package/dist/test_longpoll_status.d.ts +2 -0
  195. package/dist/test_longpoll_status.d.ts.map +1 -0
  196. package/dist/test_longpoll_status.js +117 -0
  197. package/dist/test_longpoll_status.js.map +1 -0
  198. package/dist/test_mcp_find_expert_and_vocabulary_routing.d.ts +2 -0
  199. package/dist/test_mcp_find_expert_and_vocabulary_routing.d.ts.map +1 -0
  200. package/dist/test_mcp_find_expert_and_vocabulary_routing.js +130 -0
  201. package/dist/test_mcp_find_expert_and_vocabulary_routing.js.map +1 -0
  202. package/dist/test_next_moves.d.ts +2 -0
  203. package/dist/test_next_moves.d.ts.map +1 -0
  204. package/dist/test_next_moves.js +714 -0
  205. package/dist/test_next_moves.js.map +1 -0
  206. package/dist/test_next_moves_transcripts.d.ts +2 -0
  207. package/dist/test_next_moves_transcripts.d.ts.map +1 -0
  208. package/dist/test_next_moves_transcripts.js +495 -0
  209. package/dist/test_next_moves_transcripts.js.map +1 -0
  210. package/dist/test_on_request_caller_identity.d.ts +13 -0
  211. package/dist/test_on_request_caller_identity.d.ts.map +1 -0
  212. package/dist/test_on_request_caller_identity.js +131 -0
  213. package/dist/test_on_request_caller_identity.js.map +1 -0
  214. package/dist/test_on_request_intercept.d.ts +10 -0
  215. package/dist/test_on_request_intercept.d.ts.map +1 -0
  216. package/dist/test_on_request_intercept.js +95 -0
  217. package/dist/test_on_request_intercept.js.map +1 -0
  218. package/dist/test_onboarding_consent_and_errors.d.ts +2 -0
  219. package/dist/test_onboarding_consent_and_errors.d.ts.map +1 -0
  220. package/dist/test_onboarding_consent_and_errors.js +110 -0
  221. package/dist/test_onboarding_consent_and_errors.js.map +1 -0
  222. package/dist/test_onboarding_live_scenarios.d.ts +2 -0
  223. package/dist/test_onboarding_live_scenarios.d.ts.map +1 -0
  224. package/dist/test_onboarding_live_scenarios.js +189 -0
  225. package/dist/test_onboarding_live_scenarios.js.map +1 -0
  226. package/dist/test_referral_live.d.ts +2 -0
  227. package/dist/test_referral_live.d.ts.map +1 -0
  228. package/dist/test_referral_live.js +33 -0
  229. package/dist/test_referral_live.js.map +1 -0
  230. package/dist/test_referral_piers.d.ts +2 -0
  231. package/dist/test_referral_piers.d.ts.map +1 -0
  232. package/dist/test_referral_piers.js +33 -0
  233. package/dist/test_referral_piers.js.map +1 -0
  234. package/dist/test_report_editorial_briefing.d.ts +21 -0
  235. package/dist/test_report_editorial_briefing.d.ts.map +1 -0
  236. package/dist/test_report_editorial_briefing.js +356 -0
  237. package/dist/test_report_editorial_briefing.js.map +1 -0
  238. package/dist/test_search_graph_quality.d.ts +2 -0
  239. package/dist/test_search_graph_quality.d.ts.map +1 -0
  240. package/dist/test_search_graph_quality.js +493 -0
  241. package/dist/test_search_graph_quality.js.map +1 -0
  242. package/dist/test_service_to_service_auth.d.ts +2 -0
  243. package/dist/test_service_to_service_auth.d.ts.map +1 -0
  244. package/dist/test_service_to_service_auth.js +199 -0
  245. package/dist/test_service_to_service_auth.js.map +1 -0
  246. package/dist/test_session_next_moves_telemetry.d.ts +2 -0
  247. package/dist/test_session_next_moves_telemetry.d.ts.map +1 -0
  248. package/dist/test_session_next_moves_telemetry.js +100 -0
  249. package/dist/test_session_next_moves_telemetry.js.map +1 -0
  250. package/dist/test_sources_live.d.ts +2 -0
  251. package/dist/test_sources_live.d.ts.map +1 -0
  252. package/dist/test_sources_live.js +30 -0
  253. package/dist/test_sources_live.js.map +1 -0
  254. package/dist/test_specialist_and_analyst_tiers.d.ts +2 -0
  255. package/dist/test_specialist_and_analyst_tiers.d.ts.map +1 -0
  256. package/dist/test_specialist_and_analyst_tiers.js +155 -0
  257. package/dist/test_specialist_and_analyst_tiers.js.map +1 -0
  258. package/dist/test_v2_2_visual_hardening.d.ts +5 -0
  259. package/dist/test_v2_2_visual_hardening.d.ts.map +1 -0
  260. package/dist/test_v2_2_visual_hardening.js +333 -0
  261. package/dist/test_v2_2_visual_hardening.js.map +1 -0
  262. package/dist/test_verify_deprecate_render_spec.d.ts +2 -0
  263. package/dist/test_verify_deprecate_render_spec.d.ts.map +1 -0
  264. package/dist/test_verify_deprecate_render_spec.js +217 -0
  265. package/dist/test_verify_deprecate_render_spec.js.map +1 -0
  266. package/dist/test_verify_neil_demo.d.ts +2 -0
  267. package/dist/test_verify_neil_demo.d.ts.map +1 -0
  268. package/dist/test_verify_neil_demo.js +149 -0
  269. package/dist/test_verify_neil_demo.js.map +1 -0
  270. package/dist/test_waitable_research_and_grounded_links.d.ts +2 -0
  271. package/dist/test_waitable_research_and_grounded_links.d.ts.map +1 -0
  272. package/dist/test_waitable_research_and_grounded_links.js +129 -0
  273. package/dist/test_waitable_research_and_grounded_links.js.map +1 -0
  274. package/dist/test_widget_guards.d.ts +2 -0
  275. package/dist/test_widget_guards.d.ts.map +1 -0
  276. package/dist/test_widget_guards.js +229 -0
  277. package/dist/test_widget_guards.js.map +1 -0
  278. package/dist/test_zero_retention_alerts.d.ts +14 -0
  279. package/dist/test_zero_retention_alerts.d.ts.map +1 -0
  280. package/dist/test_zero_retention_alerts.js +76 -0
  281. package/dist/test_zero_retention_alerts.js.map +1 -0
  282. package/dist/toolHandlers.d.ts +38 -1
  283. package/dist/toolHandlers.d.ts.map +1 -1
  284. package/dist/toolHandlers.js +4290 -711
  285. package/dist/toolHandlers.js.map +1 -1
  286. package/dist/tools.d.ts +22 -1
  287. package/dist/tools.d.ts.map +1 -1
  288. package/dist/tools.js +29 -4
  289. package/dist/tools.js.map +1 -1
  290. package/dist/verify_tools_endpoint.js +19 -5
  291. package/dist/verify_tools_endpoint.js.map +1 -1
  292. package/dist/widgetShell.d.ts +2 -2
  293. package/dist/widgetShell.d.ts.map +1 -1
  294. package/dist/widgetShell.js +1 -1
  295. package/dist/widgetShell.js.map +1 -1
  296. package/package.json +1 -1
  297. package/server.json +3 -31
  298. package/dist/debug_tools.d.ts +0 -2
  299. package/dist/debug_tools.d.ts.map +0 -1
  300. package/dist/debug_tools.js +0 -19
  301. package/dist/debug_tools.js.map +0 -1
  302. package/dist/inspect_list_graphs.d.ts +0 -2
  303. package/dist/inspect_list_graphs.d.ts.map +0 -1
  304. package/dist/inspect_list_graphs.js +0 -25
  305. package/dist/inspect_list_graphs.js.map +0 -1
  306. package/dist/scratch_test.d.ts.map +0 -1
  307. package/dist/scripts/simulate_gemini.d.ts +0 -2
  308. package/dist/scripts/simulate_gemini.d.ts.map +0 -1
  309. package/dist/scripts/simulate_gemini.js +0 -100
  310. package/dist/scripts/simulate_gemini.js.map +0 -1
  311. package/dist/scripts/verify_registry.d.ts +0 -2
  312. package/dist/scripts/verify_registry.d.ts.map +0 -1
  313. package/dist/scripts/verify_registry.js +0 -77
  314. package/dist/scripts/verify_registry.js.map +0 -1
  315. package/dist/src/index.d.ts +0 -2
  316. package/dist/src/index.d.ts.map +0 -1
  317. package/dist/src/index.js +0 -332
  318. package/dist/src/index.js.map +0 -1
  319. package/dist/src/tools.d.ts +0 -12
  320. package/dist/src/tools.d.ts.map +0 -1
  321. package/dist/src/tools.js +0 -104
  322. package/dist/src/tools.js.map +0 -1
  323. package/dist/src/types.d.ts +0 -44
  324. package/dist/src/types.d.ts.map +0 -1
  325. package/dist/src/types.js +0 -2
  326. package/dist/src/types.js.map +0 -1
  327. package/dist/src/verify.d.ts +0 -2
  328. package/dist/src/verify.d.ts.map +0 -1
  329. package/dist/src/verify.js +0 -47
  330. package/dist/src/verify.js.map +0 -1
  331. package/dist/src/verify_hmac.d.ts +0 -2
  332. package/dist/src/verify_hmac.d.ts.map +0 -1
  333. package/dist/src/verify_hmac.js +0 -106
  334. package/dist/src/verify_hmac.js.map +0 -1
  335. package/dist/src/verify_tools_endpoint.d.ts +0 -2
  336. package/dist/src/verify_tools_endpoint.d.ts.map +0 -1
  337. package/dist/src/verify_tools_endpoint.js +0 -40
  338. package/dist/src/verify_tools_endpoint.js.map +0 -1
  339. package/dist/test-waverunner-response.d.ts +0 -2
  340. package/dist/test-waverunner-response.d.ts.map +0 -1
  341. package/dist/test-waverunner-response.js +0 -20
  342. package/dist/test-waverunner-response.js.map +0 -1
  343. package/dist/test_coverage_r2.d.ts +0 -2
  344. package/dist/test_coverage_r2.d.ts.map +0 -1
  345. package/dist/test_coverage_scratch.d.ts +0 -2
  346. package/dist/test_coverage_scratch.d.ts.map +0 -1
@@ -7,6 +7,100 @@
7
7
  */
8
8
  import axios from 'axios';
9
9
  import { cacheClear } from './queryCache.js';
10
+ export function stringifyField(f) {
11
+ if (!f)
12
+ return '';
13
+ if (typeof f === 'string')
14
+ return f.trim();
15
+ if (Array.isArray(f)) {
16
+ return f.map(item => {
17
+ if (typeof item === 'string')
18
+ return item;
19
+ if (item && typeof item === 'object') {
20
+ return [item.title, item.name, item.description, item.topic].filter(Boolean).join(' ');
21
+ }
22
+ return '';
23
+ }).filter(Boolean).join('. ').trim();
24
+ }
25
+ if (typeof f === 'object') {
26
+ return [f.title, f.name, f.description, f.topic].filter(Boolean).join(' ').trim();
27
+ }
28
+ return String(f);
29
+ }
30
+ export function normalizeAnalyst(a) {
31
+ const analyst_id = String(a.analyst_id || a.id || a.slug || a.name || '').trim();
32
+ const name = cleanDisplayName(String(a.name || a.analyst_id || a.id || a.slug || '').trim());
33
+ const description = stringifyField(a.description || a.bio || a.lens || a.standpoint || '');
34
+ const expert_in = stringifyField(a.expert_in || a.expertIn || a.topic || '');
35
+ const outside_their_lane = stringifyField(a.outside_their_lane || a.outsideTheirLane || a.blind_spots || a.blindSpots || '');
36
+ const topics = Array.isArray(a.topics) ? a.topics : (typeof a.topics === 'string' ? a.topics.split(',').map((s) => s.trim()) : undefined);
37
+ const rawSubType = (a.graphSubType || a.graph_sub_type || a.subType || a.type || a.kind || a.agent_type || '').toString().trim();
38
+ const cleanId = analyst_id.toLowerCase();
39
+ // ── 4-Tier Agent Classification ──
40
+ // 1. C-Suite Agents: corporate executive personas grounded in SEC/earnings
41
+ const is_c_suite = cleanId === 'brand-cmo' || cleanId === 'brand-ceo' || cleanId === 'brand-cfo' ||
42
+ /brand-(cmo|ceo|cfo)/i.test(cleanId) || /c-suite|executive/i.test(rawSubType);
43
+ // 2. Classic Agents: public-domain historical thinkers providing philosophical lenses
44
+ const is_classic = !is_c_suite && (rawSubType === 'Classic Digital Twin' ||
45
+ /classic digital twin/i.test(rawSubType) ||
46
+ a.is_classic_agent === true ||
47
+ a.category === 'classic_agent');
48
+ // 3. Human Agents: real living industry figures with consented digital twins
49
+ const is_human = !is_c_suite && !is_classic && Boolean(rawSubType === 'Digital Twin' ||
50
+ rawSubType === 'human_agent' ||
51
+ rawSubType === 'human_twin' ||
52
+ rawSubType === 'expert_twin' ||
53
+ a.is_human_agent ||
54
+ a.is_digital_twin ||
55
+ a.isVerifiedRealPerson ||
56
+ a.is_verified_real_person ||
57
+ a.type === 'human_agent' ||
58
+ a.type === 'human_twin' ||
59
+ a.kind === 'human_agent' ||
60
+ a.kind === 'human_twin' ||
61
+ a.agent_type === 'human_agent' ||
62
+ a.agent_type === 'human_twin');
63
+ let category = 'synthetic_agent';
64
+ let category_label = 'Synthetic Agent';
65
+ let twin_type = undefined;
66
+ if (is_human) {
67
+ category = 'human_agent';
68
+ category_label = 'Human Agent';
69
+ twin_type = 'Expert Digital Twin';
70
+ }
71
+ else if (is_classic) {
72
+ category = 'classic_agent';
73
+ category_label = 'Classic Agent';
74
+ twin_type = 'Classic Agent';
75
+ }
76
+ else if (is_c_suite) {
77
+ category = 'c_suite_agent';
78
+ category_label = 'C-Suite Agent';
79
+ }
80
+ const slug = a.expertSlug || a.expert_slug || a.slug || a.analyst_id || (is_human ? a.id : undefined);
81
+ const consult_tool = is_human ? 'consult_human_agent' : 'consult_analyst';
82
+ return {
83
+ ...a,
84
+ analyst_id,
85
+ name,
86
+ description,
87
+ category,
88
+ category_label,
89
+ ...(twin_type ? { twin_type } : {}),
90
+ type: category,
91
+ is_human_agent: is_human,
92
+ is_classic_agent: is_classic,
93
+ is_c_suite_agent: is_c_suite,
94
+ is_verified_real_person: is_human,
95
+ consult_tool,
96
+ slug: slug ? String(slug).trim() : undefined,
97
+ ...(expert_in ? { expert_in } : {}),
98
+ ...(outside_their_lane ? { outside_their_lane } : {}),
99
+ ...(topics ? { topics } : {}),
100
+ ...(a.what_they_offer || a.whatTheyOffer || a.askLine ? { what_they_offer: stringifyField(a.what_they_offer || a.whatTheyOffer || a.askLine) } : {}),
101
+ ...(a.example_questions || a.exampleQuestions || a.exampleQueries ? { example_questions: a.example_questions || a.exampleQuestions || a.exampleQueries } : {})
102
+ };
103
+ }
10
104
  // ---------------------------------------------------------------------------
11
105
  // Cache
12
106
  // ---------------------------------------------------------------------------
@@ -34,18 +128,103 @@ async function fetchCatalog() {
34
128
  * Fetch the analysts from the API. Requires internal auth.
35
129
  */
36
130
  async function fetchAnalysts() {
37
- const url = `${API_BASE_URL}/v1/analysts`;
38
- const internalKey = process.env.FODDA_INTERNAL_API_KEY;
131
+ const url = `${API_BASE_URL}/v1/analysts?include=unclaimed`;
132
+ const internalKey = process.env.FODDA_INTERNAL_API_KEY || process.env.FODDA_API_KEY;
39
133
  const headers = internalKey ? { 'Authorization': `Bearer ${internalKey}` } : {};
40
134
  try {
41
135
  const response = await axios.get(url, { headers, timeout: 10000 });
42
- return Array.isArray(response.data) ? response.data : (response.data.analysts || []);
136
+ const raw = Array.isArray(response.data) ? response.data : (response.data.analysts || []);
137
+ return raw.map(normalizeAnalyst);
43
138
  }
44
139
  catch (err) {
45
140
  console.error(`[catalogCache] Failed to fetch analysts: ${err.message}`);
46
141
  return [];
47
142
  }
48
143
  }
144
+ /**
145
+ * Bridges active individual analysts into the searchable graph catalog.
146
+ * Solves the "two-store indexing gap" where an analyst exists in the Analysts table
147
+ * with a backing Neo4j graph, but has no dedicated record in Airtable's Graph List.
148
+ */
149
+ function bridgeAnalystGraphs(catalog, analysts) {
150
+ if (!catalog?.graphs)
151
+ return catalog;
152
+ const existingIds = new Set(catalog.graphs.map(g => (g.graph_id || '').toLowerCase().trim()));
153
+ const bridged = [...catalog.graphs];
154
+ for (const a of analysts) {
155
+ const id = (a.analyst_id || a.id || '').toLowerCase().trim();
156
+ const status = (a.status || a.Status || '').toLowerCase().trim();
157
+ if (!id || (status && status !== 'active'))
158
+ continue;
159
+ // Skip synthetic C-suite leads that already span all graphs
160
+ if (a.is_c_suite_agent || /brand-(cmo|ceo|cfo|analyst)/i.test(id))
161
+ continue;
162
+ // Determine target graph IDs for this analyst:
163
+ // Check backingGraphs first (e.g. 'postpals-expert-graph', 'sic', '2026-macro-trend-graph')
164
+ const candidateGraphIds = new Set();
165
+ const backingList = Array.isArray(a.backingGraphs) ? a.backingGraphs : [];
166
+ for (const rawBg of backingList) {
167
+ const bg = (rawBg || '').toLowerCase().trim();
168
+ if (bg && bg !== '*' && !existingIds.has(bg)) {
169
+ candidateGraphIds.add(bg);
170
+ }
171
+ }
172
+ // If no un-registered backingGraph found, use the analyst ID itself (e.g. 'peter-abraham-bicycles-cycling')
173
+ if (candidateGraphIds.size === 0 && !existingIds.has(id) && !backingList.includes('*')) {
174
+ candidateGraphIds.add(id);
175
+ }
176
+ if (candidateGraphIds.size === 0)
177
+ continue;
178
+ const topics = Array.isArray(a.topics) ? a.topics : [];
179
+ const expertIn = typeof a.expert_in === 'string' ? a.expert_in : '';
180
+ const desc = a.description || a.what_they_offer || expertIn || `${a.name} expert knowledge graph`;
181
+ for (const targetGraphId of candidateGraphIds) {
182
+ bridged.push({
183
+ graph_id: targetGraphId,
184
+ name: a.name,
185
+ description: desc,
186
+ curator: a.name,
187
+ curator_url: '',
188
+ quality_checker_name: '',
189
+ quality_checker_title: '',
190
+ quality_checker_company: '',
191
+ update_frequency: 'Weekly',
192
+ domain: expertIn || 'Specialist Intelligence',
193
+ version: '1.0.0',
194
+ node_types: ['Trend', 'Article'],
195
+ relationship_types: ['HAS_EVIDENCE'],
196
+ graph_type: 'analyst',
197
+ status: 'live',
198
+ trend_count: 5,
199
+ evidence_count: 5,
200
+ last_synced: new Date().toISOString(),
201
+ topics: topics.length > 0 ? topics : [targetGraphId],
202
+ company: a.company || '',
203
+ headline: a.askLine || expertIn || desc,
204
+ subhead: expertIn || '',
205
+ geography: 'Global',
206
+ icon_url: '',
207
+ source_url: '',
208
+ available_as: 'API, MCP',
209
+ is_playground: false,
210
+ last_updated: new Date().toISOString(),
211
+ published_date: null,
212
+ example_queries: Array.isArray(a.example_questions) ? a.example_questions : [],
213
+ portrait_url: a.portrait_url || '',
214
+ core_tension: null,
215
+ matched_human_twin_slug: id,
216
+ expert_stance_quote: null,
217
+ cross_graph_signals: []
218
+ });
219
+ existingIds.add(targetGraphId);
220
+ }
221
+ }
222
+ return {
223
+ ...catalog,
224
+ graph_count: bridged.length,
225
+ graphs: bridged
226
+ };
227
+ }
49
228
  /**
50
229
  * Initialize the catalog cache. Call once at server startup.
51
230
  * Fetches immediately, then sets up hourly refresh.
@@ -53,8 +232,9 @@ async function fetchAnalysts() {
53
232
  export async function initCatalogCache() {
54
233
  try {
55
234
  cacheClear();
56
- cachedCatalog = await fetchCatalog();
235
+ const rawCatalog = await fetchCatalog();
57
236
  cachedAnalysts = await fetchAnalysts();
237
+ cachedCatalog = bridgeAnalystGraphs(rawCatalog, cachedAnalysts);
58
238
  lastFetchedAt = Date.now();
59
239
  rebuildSearchIndex();
60
240
  console.error(`[catalogCache] Loaded ${cachedCatalog.graph_count} graphs from catalog (search index: ${graphSearchTexts.size} entries)`);
@@ -68,8 +248,9 @@ export async function initCatalogCache() {
68
248
  clearInterval(refreshInterval);
69
249
  refreshInterval = setInterval(async () => {
70
250
  try {
71
- cachedCatalog = await fetchCatalog();
251
+ const rawCatalog = await fetchCatalog();
72
252
  cachedAnalysts = await fetchAnalysts();
253
+ cachedCatalog = bridgeAnalystGraphs(rawCatalog, cachedAnalysts);
73
254
  lastFetchedAt = Date.now();
74
255
  rebuildSearchIndex();
75
256
  console.error(`[catalogCache] Refreshed catalog — ${cachedCatalog.graph_count} graphs (search index rebuilt) and ${cachedAnalysts.length} analysts`);
@@ -79,6 +260,13 @@ export async function initCatalogCache() {
79
260
  }
80
261
  }, CATALOG_REFRESH_MS);
81
262
  }
263
+ export function setCachedCatalogForTesting(catalog, analysts) {
264
+ if (analysts)
265
+ cachedAnalysts = analysts.map(normalizeAnalyst);
266
+ cachedCatalog = bridgeAnalystGraphs(catalog, cachedAnalysts);
267
+ lastFetchedAt = Date.now();
268
+ rebuildSearchIndex();
269
+ }
82
270
  /**
83
271
  * Get the cached catalog. Returns null if not yet fetched.
84
272
  */
@@ -185,6 +373,18 @@ export function buildDisplayName(g) {
185
373
  }
186
374
  return g.name;
187
375
  }
376
+ /**
377
+ * Clean display names across MCP surfaces.
378
+ * Removes retired suffixes such as "(Classic Digital Twin)", "(Digital Twin)", etc.
379
+ */
380
+ export function cleanDisplayName(name) {
381
+ if (!name || typeof name !== 'string')
382
+ return '';
383
+ return name
384
+ .replace(/\s*[\(\[]\s*(?:Classic\s+|Expert\s+|Human\s+)?(?:Digital\s+)?Twin\s*[\)\]]/gi, '')
385
+ .replace(/\s*[\(\[]\s*(?:Classic|Human|Synthetic|C-Suite)\s+Agent\s*[\)\]]/gi, '')
386
+ .trim();
387
+ }
188
388
  /**
189
389
  * Build the GRAPH TYPES block from catalog data.
190
390
  */
@@ -585,8 +785,30 @@ const QUERY_EXPANSION_MAP = {
585
785
  // Pet & Animal Care
586
786
  pet: ['pet', 'cpg', 'food', 'wellness', 'retail'],
587
787
  pets: ['pet', 'cpg', 'food', 'wellness', 'retail'],
588
- dog: ['pet', 'cpg', 'food', 'wellness'],
589
- cat: ['pet', 'cpg', 'food', 'wellness'],
788
+ dog: ['pet', 'cpg', 'food', 'wellness', 'retail'],
789
+ dogs: ['pet', 'cpg', 'food', 'wellness', 'retail'],
790
+ cat: ['pet', 'cpg', 'food', 'wellness', 'retail'],
791
+ cats: ['pet', 'cpg', 'food', 'wellness', 'retail'],
792
+ petfood: ['pet', 'cpg', 'food', 'wellness', 'retail'],
793
+ // Marketing, Experiential & Activations
794
+ activation: ['retail', 'cpg', 'marketing', 'brand', 'experience', 'hospitality', 'culture'],
795
+ activations: ['retail', 'cpg', 'marketing', 'brand', 'experience', 'hospitality', 'culture'],
796
+ experiential: ['retail', 'cpg', 'marketing', 'brand', 'experience', 'hospitality', 'culture', 'design'],
797
+ popup: ['retail', 'cpg', 'fashion', 'experience', 'design'],
798
+ popups: ['retail', 'cpg', 'fashion', 'experience', 'design'],
799
+ sampling: ['cpg', 'retail', 'food', 'beverage', 'beauty', 'experience'],
800
+ sponsorship: ['marketing', 'brand', 'culture', 'retail', 'entertainment'],
801
+ installation: ['retail', 'design', 'art', 'experience', 'culture'],
802
+ flagship: ['retail', 'fashion', 'design', 'experience', 'brand'],
803
+ immersive: ['retail', 'experience', 'design', 'entertainment', 'technology'],
804
+ stunt: ['marketing', 'brand', 'culture', 'retail'],
805
+ showcase: ['retail', 'design', 'experience', 'marketing'],
806
+ marketing: ['retail', 'cpg', 'brand', 'strategy', 'advertising', 'culture'],
807
+ campaign: ['marketing', 'brand', 'advertising', 'retail', 'cpg'],
808
+ advertising: ['marketing', 'brand', 'media', 'retail', 'cpg'],
809
+ promo: ['marketing', 'retail', 'cpg', 'promotion'],
810
+ promotion: ['marketing', 'retail', 'cpg', 'promotion'],
811
+ hospitality: ['hospitality', 'hotel', 'dining', 'food', 'experience', 'leisure', 'culture'],
590
812
  // Tech, AI & SaaS
591
813
  ai: ['technology', 'ai', 'software', 'enterprise', 'innovation'],
592
814
  saas: ['technology', 'software', 'enterprise', 'b2b'],
@@ -616,12 +838,15 @@ function scoreClauseRelevance(clause, g) {
616
838
  const searchText = graphSearchTexts.get(g.graph_id) || buildSearchableText(g);
617
839
  const topicsText = [...(g.topics || []), ...(g.routing_keywords || []), g.name || '', g.graph_id || ''].join(' ').toLowerCase();
618
840
  const domainText = (g.domain || '').toLowerCase();
841
+ const words = new Set(searchText.split(/[^a-z0-9]+/));
842
+ const topicWords = new Set(topicsText.split(/[^a-z0-9]+/));
843
+ const domainWords = new Set(domainText.split(/[^a-z0-9]+/));
619
844
  let matchedTerms = 0;
620
845
  let highValueMatches = 0;
621
846
  for (const term of terms) {
622
- if (searchText.includes(term)) {
847
+ if (words.has(term)) {
623
848
  matchedTerms++;
624
- if (topicsText.includes(term) || domainText.includes(term)) {
849
+ if (topicWords.has(term) || domainWords.has(term)) {
625
850
  highValueMatches++;
626
851
  }
627
852
  }
@@ -642,9 +867,9 @@ function scoreClauseRelevance(clause, g) {
642
867
  let expMatches = 0;
643
868
  let expHighValueMatches = 0;
644
869
  for (const exp of expandedSet) {
645
- if (searchText.includes(exp)) {
870
+ if (words.has(exp)) {
646
871
  expMatches++;
647
- if (topicsText.includes(exp) || domainText.includes(exp)) {
872
+ if (topicWords.has(exp) || domainWords.has(exp)) {
648
873
  expHighValueMatches++;
649
874
  }
650
875
  }
@@ -679,6 +904,18 @@ function scoreClauseRelevance(clause, g) {
679
904
  finalScore = Math.min(finalScore, 0.40); // Cap generic agency floor
680
905
  }
681
906
  }
907
+ // Experiential & Activation Tactic Boost
908
+ const experientialTerms = new Set([
909
+ 'activation', 'activations', 'experiential', 'popup', 'popups', 'sampling',
910
+ 'installation', 'flagship', 'immersive', 'stunt', 'showcase'
911
+ ]);
912
+ const queryHasExperiential = terms.some(t => experientialTerms.has(t));
913
+ if (queryHasExperiential) {
914
+ const isExperienceGraph = /retail|cpg|brand|marketing|experience|culture|hospitality/i.test(g.graph_id + ' ' + (g.domain || '') + ' ' + (g.topics || []).join(' ') + ' ' + (g.routing_keywords || []).join(' '));
915
+ if (isExperienceGraph || g.graph_id === 'retail') {
916
+ finalScore += 0.20; // Experiential Format Boost
917
+ }
918
+ }
682
919
  // Generalized Narrow-Domain Mismatch Penalty
683
920
  const narrowDomains = ['beauty', 'gaming', 'automotive', 'fashion'];
684
921
  for (const dom of narrowDomains) {
@@ -716,10 +953,11 @@ export function classifyGraphTier(g) {
716
953
  const isRecurring = g.update_frequency &&
717
954
  !['One-Off', 'Coming Soon'].includes(g.update_frequency);
718
955
  // Domain graphs with recurring updates + experts with recurring updates = Living
719
- if ((g.graph_type === 'domain' || g.graph_type === 'expert') && isRecurring) {
956
+ // ('analyst' = expert graphs typed 'analyst' in the registry, e.g. Digital Twins)
957
+ if ((g.graph_type === 'domain' || g.graph_type === 'expert' || g.graph_type === 'analyst') && isRecurring) {
720
958
  return 'living';
721
959
  }
722
- if (g.graph_type === 'expert')
960
+ if (g.graph_type === 'expert' || g.graph_type === 'analyst')
723
961
  return 'static_expert';
724
962
  if (g.graph_type === 'industry report')
725
963
  return 'report';
@@ -740,8 +978,18 @@ export function classifyGraphTier(g) {
740
978
  */
741
979
  export function getRelevantGraphs(query, minGraphs = 4, maxGraphs = 15, threshold = 0.10) {
742
980
  const DEPRECATED_GRAPH_IDS = new Set(['waldo', 'psfk']);
743
- const allGraphs = getLiveGraphs().filter(g => (g.graph_type === 'domain' || g.graph_type === 'expert' || g.graph_type === 'industry report')
744
- && !DEPRECATED_GRAPH_IDS.has(g.graph_id));
981
+ // 'analyst' graphs (e.g. Digital Twin expert graphs typed 'analyst' in the
982
+ // registry) are searchable expert content — excluding them made registry-live
983
+ // graphs like jeff-longevity invisible to routing (QA 2026-08-20).
984
+ const ROUTABLE_TYPES = new Set(['domain', 'expert', 'industry report', 'analyst']);
985
+ const allLive = getLiveGraphs();
986
+ const allGraphs = allLive.filter(g => ROUTABLE_TYPES.has(g.graph_type) && !DEPRECATED_GRAPH_IDS.has(g.graph_id));
987
+ // Registry-status visibility: surface live graphs excluded from routing by
988
+ // type, so an unroutable analyst/expert graph is detectable in logs.
989
+ const typeExcluded = allLive.filter(g => !ROUTABLE_TYPES.has(g.graph_type) && g.graph_type !== 'supplemental' && g.graph_type !== 'skill');
990
+ if (typeExcluded.length > 0) {
991
+ console.error(`[graphRouter] ${typeExcluded.length} live graph(s) excluded by graph_type: ${typeExcluded.map(g => `${g.graph_id}(${g.graph_type})`).join(', ')}`);
992
+ }
745
993
  if (allGraphs.length === 0)
746
994
  return [];
747
995
  // Runtime check: skip graphs that have no synced content in Neo4j.
@@ -750,13 +998,35 @@ export function getRelevantGraphs(query, minGraphs = 4, maxGraphs = 15, threshol
750
998
  // Once the CE pipeline syncs them, they'll automatically start routing.
751
999
  const hasContent = (g) => (g.trend_count > 0) || (g.last_synced !== null && g.last_synced !== undefined);
752
1000
  const syncedGraphs = allGraphs.filter(hasContent);
753
- const skippedShells = allGraphs.length - syncedGraphs.length;
1001
+ const shellGraphs = allGraphs.filter(g => !hasContent(g));
1002
+ const skippedShells = shellGraphs.length;
754
1003
  if (skippedShells > 0) {
755
- console.error(`[graphRouter] Skipped ${skippedShells} unsynced graph shell(s) (trend_count=0, last_synced=null)`);
1004
+ console.error(`[graphRouter] Skipped ${skippedShells} unsynced graph shell(s) (trend_count=0, last_synced=null): ${shellGraphs.map(g => g.graph_id).join(', ')}`);
756
1005
  }
757
- // ── Phase 0: Direct name matching ──
1006
+ // ── Phase 0: Direct name matching & phrase routing overrides ──
758
1007
  const queryLower = query.toLowerCase();
759
1008
  const directMatchIds = new Set();
1009
+ const isCreatorQuery = /creator economy|content creator|creator monetization|creators report/i.test(queryLower);
1010
+ const isNightlifeQuery = /nightlife|going out|shared experiences|shared social experiences/i.test(queryLower);
1011
+ const eligibleGraphs = syncedGraphs.filter(g => {
1012
+ if (isCreatorQuery && g.graph_id === 'mary-shelley')
1013
+ return false;
1014
+ return true;
1015
+ });
1016
+ if (isCreatorQuery) {
1017
+ for (const g of eligibleGraphs) {
1018
+ if (g.graph_id === 'visa-creators_report-2025' || g.graph_id.includes('youtube') || g.graph_id.includes('tiktok')) {
1019
+ directMatchIds.add(g.graph_id);
1020
+ }
1021
+ }
1022
+ }
1023
+ if (isNightlifeQuery) {
1024
+ for (const g of eligibleGraphs) {
1025
+ if (g.graph_id.includes('bompasparr')) {
1026
+ directMatchIds.add(g.graph_id);
1027
+ }
1028
+ }
1029
+ }
760
1030
  const matchesWordBoundary = (text, term) => {
761
1031
  if (!term || term.length < 3)
762
1032
  return false;
@@ -771,7 +1041,7 @@ export function getRelevantGraphs(query, minGraphs = 4, maxGraphs = 15, threshol
771
1041
  const rx = new RegExp(`\\b${escaped}\\b`, 'i');
772
1042
  return rx.test(text);
773
1043
  };
774
- for (const g of syncedGraphs) {
1044
+ for (const g of eligibleGraphs) {
775
1045
  // Check graph_id (e.g., "alyson-stevens-macro", "sic")
776
1046
  if (g.graph_id.length > 3 && matchesWordBoundary(queryLower, g.graph_id)) {
777
1047
  directMatchIds.add(g.graph_id);
@@ -813,7 +1083,7 @@ export function getRelevantGraphs(query, minGraphs = 4, maxGraphs = 15, threshol
813
1083
  console.error(`[graphRouter] Phase 0 direct name matches: ${[...directMatchIds].join(', ')}`);
814
1084
  }
815
1085
  // Score ALL graph types together — domain, expert, industry report compete on merit
816
- const scored = syncedGraphs.map(g => {
1086
+ const scored = eligibleGraphs.map(g => {
817
1087
  const isDirect = directMatchIds.has(g.graph_id);
818
1088
  return {
819
1089
  graph: g,
@@ -907,11 +1177,12 @@ const EARNINGS_SECTORS = ['retail', 'technology', 'travel', 'hospitality', 'luxu
907
1177
  // (economic indicators/FRED, demographic surveys/Census, trade data, food
908
1178
  // economics + agricultural production, commodity pricing).
909
1179
  const SUPPLEMENTAL_CATEGORY_KEYWORDS = {
910
- macro: ['macro', 'economy', 'economic', 'inflation', 'gdp', 'interest rate', 'consumer confidence', 'consumer spending', 'recession', 'fred', 'cost of living', 'purchasing power', 'unemployment'],
911
- demographics: ['demographic', 'demographics', 'population', 'census', 'household income', 'age distribution', 'migration'],
1180
+ 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'],
1181
+ demographics: ['demographic', 'demographics', 'population', 'census', 'household income', 'age distribution', 'migration', 'republican', 'democrat', 'bipartisan', 'party affiliation', 'pew', 'npors'],
912
1182
  trade: ['imports', 'exports', 'trade data', 'trade flows', 'tariff', 'tariffs', 'supply chain'],
913
1183
  food_economics: ['food prices', 'food costs', 'crop', 'agriculture', 'agricultural', 'nutrition', 'nutritional'],
914
1184
  commodities: ['commodity', 'commodities', 'raw materials', 'wholesale prices'],
1185
+ science: ['science', 'scientific', 'clinical', 'pubmed', 'openalex', 'clinical trials', 'dermatology', 'peer-reviewed', 'academic', 'study', 'studies', 'journal', 'biomedical'],
915
1186
  };
916
1187
  /**
917
1188
  * Detect an earnings candidate for a query. Deterministic and conservative: