fodda-mcp 1.46.31 → 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.
- package/README.md +14 -94
- package/dist/agents/fodda-researcher/skills.d.ts +1 -1
- package/dist/agents/fodda-researcher/skills.d.ts.map +1 -1
- package/dist/agents/fodda-researcher/skills.js +1 -1
- package/dist/brandTemplate.d.ts +1 -1
- package/dist/brandTemplate.d.ts.map +1 -1
- package/dist/brandTemplate.js +162 -74
- package/dist/brandTemplate.js.map +1 -1
- package/dist/catalogCache.d.ts +29 -1
- package/dist/catalogCache.d.ts.map +1 -1
- package/dist/catalogCache.js +267 -16
- package/dist/catalogCache.js.map +1 -1
- package/dist/coverageRelevance.d.ts +97 -2
- package/dist/coverageRelevance.d.ts.map +1 -1
- package/dist/coverageRelevance.js +959 -164
- package/dist/coverageRelevance.js.map +1 -1
- package/dist/editorialFill.d.ts +5 -0
- package/dist/editorialFill.d.ts.map +1 -1
- package/dist/editorialFill.js +1 -1
- package/dist/editorialFill.js.map +1 -1
- package/dist/enrichment.d.ts +26 -0
- package/dist/enrichment.d.ts.map +1 -1
- package/dist/enrichment.js +394 -31
- package/dist/enrichment.js.map +1 -1
- package/dist/errorHandling.d.ts +35 -7
- package/dist/errorHandling.d.ts.map +1 -1
- package/dist/errorHandling.js +225 -155
- package/dist/errorHandling.js.map +1 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +351 -143
- package/dist/index.js.map +1 -1
- package/dist/jobs/jlens_quarterly_sweep.d.ts +62 -0
- package/dist/jobs/jlens_quarterly_sweep.d.ts.map +1 -0
- package/dist/jobs/jlens_quarterly_sweep.js +160 -0
- package/dist/jobs/jlens_quarterly_sweep.js.map +1 -0
- package/dist/pricingCache.d.ts.map +1 -1
- package/dist/pricingCache.js +41 -1
- package/dist/pricingCache.js.map +1 -1
- package/dist/queryCache.d.ts.map +1 -1
- package/dist/queryCache.js +2 -0
- package/dist/queryCache.js.map +1 -1
- package/dist/reportBriefing.d.ts +89 -0
- package/dist/reportBriefing.d.ts.map +1 -0
- package/dist/reportBriefing.js +662 -0
- package/dist/reportBriefing.js.map +1 -0
- package/dist/searchTemplate.d.ts.map +1 -1
- package/dist/searchTemplate.js +65 -53
- package/dist/searchTemplate.js.map +1 -1
- package/dist/services/graphRouter.d.ts +7 -0
- package/dist/services/graphRouter.d.ts.map +1 -0
- package/dist/services/graphRouter.js +7 -0
- package/dist/services/graphRouter.js.map +1 -0
- package/dist/services/notifications.d.ts +49 -0
- package/dist/services/notifications.d.ts.map +1 -0
- package/dist/services/notifications.js +206 -0
- package/dist/services/notifications.js.map +1 -0
- package/dist/sessionTracker.d.ts +19 -2
- package/dist/sessionTracker.d.ts.map +1 -1
- package/dist/sessionTracker.js +69 -16
- package/dist/sessionTracker.js.map +1 -1
- package/dist/skillClient.d.ts.map +1 -1
- package/dist/skillClient.js +11 -6
- package/dist/skillClient.js.map +1 -1
- package/dist/svgVisuals.d.ts +17 -19
- package/dist/svgVisuals.d.ts.map +1 -1
- package/dist/svgVisuals.js +189 -233
- package/dist/svgVisuals.js.map +1 -1
- package/dist/systemPrompt.d.ts +7 -2
- package/dist/systemPrompt.d.ts.map +1 -1
- package/dist/systemPrompt.js +110 -68
- package/dist/systemPrompt.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +15 -0
- package/dist/telemetry.js.map +1 -1
- package/dist/test_adjacent_trends_polymorphic.d.ts +2 -0
- package/dist/test_adjacent_trends_polymorphic.d.ts.map +1 -0
- package/dist/test_adjacent_trends_polymorphic.js +154 -0
- package/dist/test_adjacent_trends_polymorphic.js.map +1 -0
- package/dist/test_book_a_call_live.js +18 -5
- package/dist/test_book_a_call_live.js.map +1 -1
- package/dist/test_byo_mcp_submission_clarity.d.ts +2 -0
- package/dist/test_byo_mcp_submission_clarity.d.ts.map +1 -0
- package/dist/test_byo_mcp_submission_clarity.js +142 -0
- package/dist/test_byo_mcp_submission_clarity.js.map +1 -0
- package/dist/test_capabilities_guidance.d.ts +2 -0
- package/dist/test_capabilities_guidance.d.ts.map +1 -0
- package/dist/test_capabilities_guidance.js +372 -0
- package/dist/test_capabilities_guidance.js.map +1 -0
- package/dist/test_client_provenance.d.ts +2 -0
- package/dist/test_client_provenance.d.ts.map +1 -0
- package/dist/test_client_provenance.js +77 -0
- package/dist/test_client_provenance.js.map +1 -0
- package/dist/test_connection_surface_cleanup.d.ts +2 -0
- package/dist/test_connection_surface_cleanup.d.ts.map +1 -0
- package/dist/test_connection_surface_cleanup.js +203 -0
- package/dist/test_connection_surface_cleanup.js.map +1 -0
- package/dist/test_consult_routing.d.ts +2 -0
- package/dist/test_consult_routing.d.ts.map +1 -0
- package/dist/test_consult_routing.js +86 -0
- package/dist/test_consult_routing.js.map +1 -0
- package/dist/test_consult_split.d.ts +2 -0
- package/dist/test_consult_split.d.ts.map +1 -0
- package/dist/test_consult_split.js +30 -0
- package/dist/test_consult_split.js.map +1 -0
- package/dist/test_coverage_relevance.js +111 -70
- package/dist/test_coverage_relevance.js.map +1 -1
- package/dist/test_coverage_replay.d.ts +2 -0
- package/dist/test_coverage_replay.d.ts.map +1 -0
- package/dist/test_coverage_replay.js +170 -0
- package/dist/test_coverage_replay.js.map +1 -0
- package/dist/test_dcr_and_legacy_deprecation.js +45 -50
- package/dist/test_dcr_and_legacy_deprecation.js.map +1 -1
- package/dist/test_demand_signal_logging.d.ts +2 -0
- package/dist/test_demand_signal_logging.d.ts.map +1 -0
- package/dist/test_demand_signal_logging.js +92 -0
- package/dist/test_demand_signal_logging.js.map +1 -0
- package/dist/test_earnings_activity_scope.d.ts +5 -0
- package/dist/test_earnings_activity_scope.d.ts.map +1 -0
- package/dist/test_earnings_activity_scope.js +101 -0
- package/dist/test_earnings_activity_scope.js.map +1 -0
- package/dist/test_earnings_divergence_live.d.ts +5 -0
- package/dist/test_earnings_divergence_live.d.ts.map +1 -0
- package/dist/test_earnings_divergence_live.js +135 -0
- package/dist/test_earnings_divergence_live.js.map +1 -0
- package/dist/test_earnings_hygiene_and_binding.d.ts +5 -0
- package/dist/test_earnings_hygiene_and_binding.d.ts.map +1 -0
- package/dist/test_earnings_hygiene_and_binding.js +104 -0
- package/dist/test_earnings_hygiene_and_binding.js.map +1 -0
- package/dist/test_exec_quotes_render.d.ts +2 -0
- package/dist/test_exec_quotes_render.d.ts.map +1 -0
- package/dist/test_exec_quotes_render.js +86 -0
- package/dist/test_exec_quotes_render.js.map +1 -0
- package/dist/test_expert_intro.d.ts +2 -0
- package/dist/test_expert_intro.d.ts.map +1 -0
- package/dist/test_expert_intro.js +57 -0
- package/dist/test_expert_intro.js.map +1 -0
- package/dist/test_expert_layer.d.ts +11 -0
- package/dist/test_expert_layer.d.ts.map +1 -0
- package/dist/test_expert_layer.js +356 -0
- package/dist/test_expert_layer.js.map +1 -0
- package/dist/test_gap_alert.js +8 -0
- package/dist/test_gap_alert.js.map +1 -1
- package/dist/test_grok_offering_surfaces.d.ts +2 -0
- package/dist/test_grok_offering_surfaces.d.ts.map +1 -0
- package/dist/test_grok_offering_surfaces.js +155 -0
- package/dist/test_grok_offering_surfaces.js.map +1 -0
- package/dist/test_human_agent_live.d.ts +2 -0
- package/dist/test_human_agent_live.d.ts.map +1 -0
- package/dist/test_human_agent_live.js +48 -0
- package/dist/test_human_agent_live.js.map +1 -0
- package/dist/test_identity_gap.d.ts +2 -0
- package/dist/test_identity_gap.d.ts.map +1 -0
- package/dist/test_identity_gap.js +171 -0
- package/dist/test_identity_gap.js.map +1 -0
- package/dist/test_intelligence_and_verifier.d.ts +17 -0
- package/dist/test_intelligence_and_verifier.d.ts.map +1 -0
- package/dist/test_intelligence_and_verifier.js +162 -0
- package/dist/test_intelligence_and_verifier.js.map +1 -0
- package/dist/test_latency_mcp_brief5.d.ts +12 -0
- package/dist/test_latency_mcp_brief5.d.ts.map +1 -0
- package/dist/test_latency_mcp_brief5.js +200 -0
- package/dist/test_latency_mcp_brief5.js.map +1 -0
- package/dist/test_limit_and_credit_card_capture.d.ts +21 -0
- package/dist/test_limit_and_credit_card_capture.d.ts.map +1 -0
- package/dist/test_limit_and_credit_card_capture.js +216 -0
- package/dist/test_limit_and_credit_card_capture.js.map +1 -0
- package/dist/test_list_graphs_topics.js +1 -1
- package/dist/test_list_graphs_topics.js.map +1 -1
- package/dist/test_live_deployed_mcp_search.d.ts +2 -0
- package/dist/test_live_deployed_mcp_search.d.ts.map +1 -0
- package/dist/test_live_deployed_mcp_search.js +85 -0
- package/dist/test_live_deployed_mcp_search.js.map +1 -0
- package/dist/test_live_deployed_split.d.ts +2 -0
- package/dist/test_live_deployed_split.d.ts.map +1 -0
- package/dist/test_live_deployed_split.js +72 -0
- package/dist/test_live_deployed_split.js.map +1 -0
- package/dist/test_live_endpoint_runner.js +2 -2
- package/dist/test_live_endpoint_runner.js.map +1 -1
- package/dist/test_live_mcp.js +1 -1
- package/dist/test_live_mcp.js.map +1 -1
- package/dist/test_live_neil_widget.d.ts +2 -0
- package/dist/test_live_neil_widget.d.ts.map +1 -0
- package/dist/test_live_neil_widget.js +84 -0
- package/dist/test_live_neil_widget.js.map +1 -0
- package/dist/test_longpoll_status.d.ts +2 -0
- package/dist/test_longpoll_status.d.ts.map +1 -0
- package/dist/test_longpoll_status.js +117 -0
- package/dist/test_longpoll_status.js.map +1 -0
- package/dist/test_mcp_find_expert_and_vocabulary_routing.d.ts +2 -0
- package/dist/test_mcp_find_expert_and_vocabulary_routing.d.ts.map +1 -0
- package/dist/test_mcp_find_expert_and_vocabulary_routing.js +130 -0
- package/dist/test_mcp_find_expert_and_vocabulary_routing.js.map +1 -0
- package/dist/test_next_moves.js +644 -279
- package/dist/test_next_moves.js.map +1 -1
- package/dist/test_next_moves_transcripts.js +122 -40
- package/dist/test_next_moves_transcripts.js.map +1 -1
- package/dist/test_on_request_caller_identity.d.ts +13 -0
- package/dist/test_on_request_caller_identity.d.ts.map +1 -0
- package/dist/test_on_request_caller_identity.js +131 -0
- package/dist/test_on_request_caller_identity.js.map +1 -0
- package/dist/test_on_request_intercept.d.ts +10 -0
- package/dist/test_on_request_intercept.d.ts.map +1 -0
- package/dist/test_on_request_intercept.js +95 -0
- package/dist/test_on_request_intercept.js.map +1 -0
- package/dist/test_onboarding_consent_and_errors.d.ts +2 -0
- package/dist/test_onboarding_consent_and_errors.d.ts.map +1 -0
- package/dist/test_onboarding_consent_and_errors.js +110 -0
- package/dist/test_onboarding_consent_and_errors.js.map +1 -0
- package/dist/test_onboarding_live_scenarios.d.ts +2 -0
- package/dist/test_onboarding_live_scenarios.d.ts.map +1 -0
- package/dist/test_onboarding_live_scenarios.js +189 -0
- package/dist/test_onboarding_live_scenarios.js.map +1 -0
- package/dist/test_referral_live.d.ts +2 -0
- package/dist/test_referral_live.d.ts.map +1 -0
- package/dist/test_referral_live.js +33 -0
- package/dist/test_referral_live.js.map +1 -0
- package/dist/test_referral_piers.d.ts +2 -0
- package/dist/test_referral_piers.d.ts.map +1 -0
- package/dist/test_referral_piers.js +33 -0
- package/dist/test_referral_piers.js.map +1 -0
- package/dist/test_report_editorial_briefing.d.ts +21 -0
- package/dist/test_report_editorial_briefing.d.ts.map +1 -0
- package/dist/test_report_editorial_briefing.js +356 -0
- package/dist/test_report_editorial_briefing.js.map +1 -0
- package/dist/test_search_graph_quality.d.ts +2 -0
- package/dist/test_search_graph_quality.d.ts.map +1 -0
- package/dist/test_search_graph_quality.js +493 -0
- package/dist/test_search_graph_quality.js.map +1 -0
- package/dist/test_service_to_service_auth.d.ts +2 -0
- package/dist/test_service_to_service_auth.d.ts.map +1 -0
- package/dist/test_service_to_service_auth.js +199 -0
- package/dist/test_service_to_service_auth.js.map +1 -0
- package/dist/test_sources_live.d.ts +2 -0
- package/dist/test_sources_live.d.ts.map +1 -0
- package/dist/test_sources_live.js +30 -0
- package/dist/test_sources_live.js.map +1 -0
- package/dist/test_specialist_and_analyst_tiers.d.ts +2 -0
- package/dist/test_specialist_and_analyst_tiers.d.ts.map +1 -0
- package/dist/test_specialist_and_analyst_tiers.js +155 -0
- package/dist/test_specialist_and_analyst_tiers.js.map +1 -0
- package/dist/test_v2_2_visual_hardening.d.ts +5 -0
- package/dist/test_v2_2_visual_hardening.d.ts.map +1 -0
- package/dist/test_v2_2_visual_hardening.js +333 -0
- package/dist/test_v2_2_visual_hardening.js.map +1 -0
- package/dist/test_verify_deprecate_render_spec.d.ts +2 -0
- package/dist/test_verify_deprecate_render_spec.d.ts.map +1 -0
- package/dist/test_verify_deprecate_render_spec.js +217 -0
- package/dist/test_verify_deprecate_render_spec.js.map +1 -0
- package/dist/test_verify_neil_demo.d.ts +2 -0
- package/dist/test_verify_neil_demo.d.ts.map +1 -0
- package/dist/test_verify_neil_demo.js +149 -0
- package/dist/test_verify_neil_demo.js.map +1 -0
- package/dist/test_waitable_research_and_grounded_links.d.ts +2 -0
- package/dist/test_waitable_research_and_grounded_links.d.ts.map +1 -0
- package/dist/test_waitable_research_and_grounded_links.js +129 -0
- package/dist/test_waitable_research_and_grounded_links.js.map +1 -0
- package/dist/test_widget_guards.d.ts +2 -0
- package/dist/test_widget_guards.d.ts.map +1 -0
- package/dist/test_widget_guards.js +229 -0
- package/dist/test_widget_guards.js.map +1 -0
- package/dist/test_zero_retention_alerts.d.ts +14 -0
- package/dist/test_zero_retention_alerts.d.ts.map +1 -0
- package/dist/test_zero_retention_alerts.js +76 -0
- package/dist/test_zero_retention_alerts.js.map +1 -0
- package/dist/toolHandlers.d.ts +38 -1
- package/dist/toolHandlers.d.ts.map +1 -1
- package/dist/toolHandlers.js +3856 -938
- package/dist/toolHandlers.js.map +1 -1
- package/dist/tools.d.ts +20 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +25 -4
- package/dist/tools.js.map +1 -1
- package/dist/verify_discoverability.d.ts +2 -0
- package/dist/verify_discoverability.d.ts.map +1 -0
- package/dist/verify_discoverability.js +53 -0
- package/dist/verify_discoverability.js.map +1 -0
- package/dist/verify_tools_endpoint.js +16 -3
- package/dist/verify_tools_endpoint.js.map +1 -1
- package/dist/widgetShell.d.ts +2 -2
- package/dist/widgetShell.d.ts.map +1 -1
- package/dist/widgetShell.js +1 -1
- package/dist/widgetShell.js.map +1 -1
- package/package.json +1 -1
- package/server.json +4 -32
- package/dist/oauthRegisterShim.d.ts +0 -13
- package/dist/oauthRegisterShim.d.ts.map +0 -1
- package/dist/oauthRegisterShim.js +0 -83
- package/dist/oauthRegisterShim.js.map +0 -1
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
* emerging trends"), relevance can't be judged and `evaluated: false` is
|
|
32
32
|
* returned — callers keep the legacy count-only behavior.
|
|
33
33
|
*/
|
|
34
|
-
import { getGraphs, getLiveGraphs, buildDisplayName, getRelevantGraphs, getAnalysts } from './catalogCache.js';
|
|
34
|
+
import { getGraphs, getLiveGraphs, buildDisplayName, cleanDisplayName, getRelevantGraphs, getAnalysts } from './catalogCache.js';
|
|
35
|
+
import { LUXURY_KEYWORDS, LUXURY_BRANDS, MASS_BUDGET_KEYWORDS, MASS_BUDGET_BRANDS } from './enrichment.js';
|
|
35
36
|
/** Nominal on-topic relevance-score scale per graph tier (observed in QA:
|
|
36
37
|
* domain composites ~2.0, report vector scores ~0.8; expert sits between). */
|
|
37
38
|
export const TIER_NOMINAL_SCORE = {
|
|
@@ -72,13 +73,13 @@ export function specificQueryTokens(query) {
|
|
|
72
73
|
return [...new Set(query.toLowerCase().split(/[^a-z0-9]+/)
|
|
73
74
|
.filter(t => t.length >= 2 && !GENERIC_QUERY_TOKENS.has(t)))];
|
|
74
75
|
}
|
|
75
|
-
function rowScore(row) {
|
|
76
|
+
export function rowScore(row) {
|
|
76
77
|
return row.relevance_score || row.semantic_score || row._score || row.score || 0;
|
|
77
78
|
}
|
|
78
|
-
function rowGraphId(row) {
|
|
79
|
+
export function rowGraphId(row) {
|
|
79
80
|
return row._use_this_graphId || row.graphId || row.graph_id || row.psfk_graph_slug || undefined;
|
|
80
81
|
}
|
|
81
|
-
function resolveRowTier(row, searchedGraphs, catalog) {
|
|
82
|
+
export function resolveRowTier(row, searchedGraphs, catalog) {
|
|
82
83
|
const gid = rowGraphId(row);
|
|
83
84
|
let type;
|
|
84
85
|
if (gid) {
|
|
@@ -125,6 +126,247 @@ export function rowMatchesQueryTokens(row, tokens, catalog) {
|
|
|
125
126
|
}
|
|
126
127
|
return false;
|
|
127
128
|
}
|
|
129
|
+
export function stemToken(token) {
|
|
130
|
+
let s = token.toLowerCase().trim();
|
|
131
|
+
if (s.endsWith('ies') && s.length > 4) {
|
|
132
|
+
s = s.slice(0, -3) + 'y';
|
|
133
|
+
}
|
|
134
|
+
else if (s.endsWith('es') && s.length > 4) {
|
|
135
|
+
s = s.slice(0, -2);
|
|
136
|
+
}
|
|
137
|
+
else if (s.endsWith('s') && !s.endsWith('ss') && s.length > 3) {
|
|
138
|
+
s = s.slice(0, -1);
|
|
139
|
+
}
|
|
140
|
+
return s;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Direct token match tier: checks if specific query tokens match directly
|
|
144
|
+
* against the row's core trend name, sectors, or trend slug/categories.
|
|
145
|
+
* Applies singular/plural folding and stem matching (e.g. "collectible" matches "Collectibles", "trading cards" matches "cards").
|
|
146
|
+
* (Used to rank direct niche matches above semantically adjacent mega-trends).
|
|
147
|
+
*/
|
|
148
|
+
export function rowHasDirectTokenMatch(row, tokens) {
|
|
149
|
+
if (!tokens || tokens.length === 0 || !row)
|
|
150
|
+
return false;
|
|
151
|
+
const parts = [
|
|
152
|
+
row.trendName, row.name, row.label, row.title,
|
|
153
|
+
row.trendSlug, row.slug,
|
|
154
|
+
row.sectorNames, row.sectors, row.sector,
|
|
155
|
+
row.categoryNames, row.categories, row.category,
|
|
156
|
+
];
|
|
157
|
+
for (const arr of [row.sectors, row.sectorNames, row.categories, row.categoryNames, row.topics]) {
|
|
158
|
+
if (Array.isArray(arr))
|
|
159
|
+
parts.push(...arr);
|
|
160
|
+
}
|
|
161
|
+
const words = new Set(parts.filter((x) => typeof x === 'string').join(' ').toLowerCase().split(/[^a-z0-9]+/));
|
|
162
|
+
const stemmedWords = new Set();
|
|
163
|
+
for (const w of words) {
|
|
164
|
+
stemmedWords.add(stemToken(w));
|
|
165
|
+
}
|
|
166
|
+
for (const t of tokens) {
|
|
167
|
+
if (words.has(t))
|
|
168
|
+
return true;
|
|
169
|
+
const stemT = stemToken(t);
|
|
170
|
+
if (stemmedWords.has(stemT))
|
|
171
|
+
return true;
|
|
172
|
+
if (t.length >= 4) {
|
|
173
|
+
for (const w of words) {
|
|
174
|
+
if (w.startsWith(t) || (w.length >= 4 && t.startsWith(w)))
|
|
175
|
+
return true;
|
|
176
|
+
const stemW = stemToken(w);
|
|
177
|
+
if (stemW.length >= 4 && stemT.length >= 4) {
|
|
178
|
+
if (stemW.startsWith(stemT) || stemT.startsWith(stemW))
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Row-level brand guard: returns true if the row is NOT a mega-trend brand roster.
|
|
188
|
+
* Excludes rows with brand_count > 30 (or brandNames.length >= 10 when brand_count is absent).
|
|
189
|
+
*/
|
|
190
|
+
export function isRowBrandEligible(row) {
|
|
191
|
+
if (!row)
|
|
192
|
+
return false;
|
|
193
|
+
const brandCount = typeof row.brand_count === 'number'
|
|
194
|
+
? row.brand_count
|
|
195
|
+
: (typeof row.brandCount === 'number' ? row.brandCount : undefined);
|
|
196
|
+
if (brandCount !== undefined) {
|
|
197
|
+
if (brandCount > 30)
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
const rawNames = Array.isArray(row.brandNames)
|
|
202
|
+
? row.brandNames
|
|
203
|
+
: (Array.isArray(row.brands) ? row.brands : undefined);
|
|
204
|
+
if (rawNames && rawNames.length >= 10) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Evaluates whether a result row matches category/market tier qualifiers in the query
|
|
212
|
+
* (e.g. "luxury" vs "budget"). Returns a positive score boost for matching tiers,
|
|
213
|
+
* or a penalty for conflicting tiers.
|
|
214
|
+
*/
|
|
215
|
+
export function computeTierFit(row, query) {
|
|
216
|
+
if (!query || !row)
|
|
217
|
+
return 0;
|
|
218
|
+
const qLower = query.toLowerCase();
|
|
219
|
+
const isLuxuryQuery = [...LUXURY_KEYWORDS].some(k => qLower.includes(k));
|
|
220
|
+
const isBudgetQuery = [...MASS_BUDGET_KEYWORDS].some(k => qLower.includes(k));
|
|
221
|
+
const title = (row.trendName || row.title || row.label || row.name || '').toLowerCase();
|
|
222
|
+
const summary = (row.summary || row.description || row.trendDescription || '').toLowerCase();
|
|
223
|
+
let boost = 0;
|
|
224
|
+
// 1. Format alignment with query (e.g. pop-up / activation / experiential)
|
|
225
|
+
if (/\bpop-?ups?\b/i.test(qLower)) {
|
|
226
|
+
if (/\bpop-?ups?\b/i.test(title))
|
|
227
|
+
boost += 0.40;
|
|
228
|
+
else if (/\bpop-?ups?\b/i.test(summary))
|
|
229
|
+
boost += 0.15;
|
|
230
|
+
}
|
|
231
|
+
if (/\b(experiential|experiences?)\b/i.test(qLower)) {
|
|
232
|
+
if (/\b(experiential|experiences?)\b/i.test(title))
|
|
233
|
+
boost += 0.20;
|
|
234
|
+
}
|
|
235
|
+
if (/\bactivations?\b/i.test(qLower)) {
|
|
236
|
+
if (/\bactivations?\b/i.test(title))
|
|
237
|
+
boost += 0.15;
|
|
238
|
+
}
|
|
239
|
+
// 2. Luxury / Market Tier matching
|
|
240
|
+
if (isLuxuryQuery) {
|
|
241
|
+
// Title-level luxury match gets highest priority
|
|
242
|
+
const titleHasLuxWord = [...LUXURY_KEYWORDS].some(k => title.includes(k));
|
|
243
|
+
const titleHasLuxBrand = [...LUXURY_BRANDS].some(b => title.includes(b));
|
|
244
|
+
if (titleHasLuxWord || titleHasLuxBrand)
|
|
245
|
+
boost += 0.40;
|
|
246
|
+
// Summary-level luxury match
|
|
247
|
+
const summaryHasLux = [...LUXURY_KEYWORDS].some(k => summary.includes(k)) ||
|
|
248
|
+
[...LUXURY_BRANDS].some(b => summary.includes(b));
|
|
249
|
+
if (summaryHasLux)
|
|
250
|
+
boost += 0.25;
|
|
251
|
+
// Evidence-level luxury match
|
|
252
|
+
if (Array.isArray(row.evidence) && row.evidence.length > 0) {
|
|
253
|
+
const hasLuxEvidence = row.evidence.some((e) => {
|
|
254
|
+
const txt = `${e.title || ''} ${e.summary || ''}`.toLowerCase();
|
|
255
|
+
return [...LUXURY_BRANDS].some(b => txt.includes(b)) || [...LUXURY_KEYWORDS].some(k => txt.includes(k));
|
|
256
|
+
});
|
|
257
|
+
if (hasLuxEvidence)
|
|
258
|
+
boost += 0.35;
|
|
259
|
+
boost += 0.20; // Having verified on-topic evidence is a positive signal
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
boost -= 0.20; // Having 0 valid evidence items penalizes the trend
|
|
263
|
+
}
|
|
264
|
+
// Mass-market penalty
|
|
265
|
+
const titleHasMass = [...MASS_BUDGET_BRANDS].some(b => title.includes(b));
|
|
266
|
+
const summaryHasMass = [...MASS_BUDGET_BRANDS].some(b => summary.includes(b));
|
|
267
|
+
if (titleHasMass || summaryHasMass)
|
|
268
|
+
boost -= 0.40;
|
|
269
|
+
}
|
|
270
|
+
else if (isBudgetQuery) {
|
|
271
|
+
const titleHasMass = [...MASS_BUDGET_BRANDS].some(b => title.includes(b)) ||
|
|
272
|
+
[...MASS_BUDGET_KEYWORDS].some(k => title.includes(k));
|
|
273
|
+
if (titleHasMass)
|
|
274
|
+
boost += 0.40;
|
|
275
|
+
}
|
|
276
|
+
return boost;
|
|
277
|
+
}
|
|
278
|
+
/** Normalize brand string for deduplication / comparison. */
|
|
279
|
+
export function normalizeBrandKey(name) {
|
|
280
|
+
return name.toLowerCase().replace(/[^\w\s]/g, '').trim().replace(/\s+/g, ' ');
|
|
281
|
+
}
|
|
282
|
+
/** Build standard publisher / curator exclusion set. */
|
|
283
|
+
export function buildPublisherExclusionSet(searchedGraphs = [], catalog = [], rows = []) {
|
|
284
|
+
const publisherTokens = new Set([
|
|
285
|
+
'dentsu', 'dentsu creative', 'havas', 'psfk', 'nielseniq', 'niq',
|
|
286
|
+
'wpp', 'omnicom', 'ipg', 'publicis', 'accenture', 'deloitte',
|
|
287
|
+
'mckinsey', 'bcg', 'bain', 'gartner', 'forrester', 'kantar', 'jwt',
|
|
288
|
+
'ogilvy', 'edelman', 'mccann', 'bbdo', 'tbwa', 'vml', 'leo burnett'
|
|
289
|
+
]);
|
|
290
|
+
for (const sg of searchedGraphs) {
|
|
291
|
+
const gMeta = typeof sg === 'string' ? catalog.find(x => x.graph_id === sg) : sg;
|
|
292
|
+
if (gMeta) {
|
|
293
|
+
if (gMeta.curator)
|
|
294
|
+
publisherTokens.add(gMeta.curator.toLowerCase().trim());
|
|
295
|
+
if (gMeta.company)
|
|
296
|
+
publisherTokens.add(gMeta.company.toLowerCase().trim());
|
|
297
|
+
if (gMeta.name)
|
|
298
|
+
publisherTokens.add(gMeta.name.toLowerCase().trim());
|
|
299
|
+
for (const word of `${gMeta.curator || ''} ${gMeta.company || ''} ${gMeta.name || ''}`.toLowerCase().split(/[\s,.-]+/)) {
|
|
300
|
+
if (word.length > 2)
|
|
301
|
+
publisherTokens.add(word);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const r of rows) {
|
|
306
|
+
if (r && r.source_label) {
|
|
307
|
+
const match = String(r.source_label).match(/\(([^)]+)\)/);
|
|
308
|
+
if (match && match[1]) {
|
|
309
|
+
const org = match[1].toLowerCase().trim();
|
|
310
|
+
publisherTokens.add(org);
|
|
311
|
+
for (const w of org.split(/[\s,.-]+/)) {
|
|
312
|
+
if (w.length > 2)
|
|
313
|
+
publisherTokens.add(w);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return publisherTokens;
|
|
319
|
+
}
|
|
320
|
+
/** Test whether a brand name matches publisher / curator tokens. */
|
|
321
|
+
export function isPublisherBrand(brand, publisherTokens) {
|
|
322
|
+
const clean = brand.trim();
|
|
323
|
+
const norm = normalizeBrandKey(clean);
|
|
324
|
+
if (!norm)
|
|
325
|
+
return true;
|
|
326
|
+
if (publisherTokens.has(norm) || publisherTokens.has(clean.toLowerCase())) {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
return [...publisherTokens].some(pt => norm === pt || norm.startsWith(`${pt} `) || norm.endsWith(` ${pt}`));
|
|
330
|
+
}
|
|
331
|
+
/** Extract clean, guarded brand names for a single row up to limit. */
|
|
332
|
+
export function extractCleanRowBrands(row, publisherTokens, limit = 4) {
|
|
333
|
+
if (!isRowBrandEligible(row))
|
|
334
|
+
return [];
|
|
335
|
+
const rawList = [];
|
|
336
|
+
if (Array.isArray(row.brandNames))
|
|
337
|
+
rawList.push(...row.brandNames);
|
|
338
|
+
else if (typeof row.brandNames === 'string')
|
|
339
|
+
rawList.push(...row.brandNames.split('|'));
|
|
340
|
+
if (Array.isArray(row.brands))
|
|
341
|
+
rawList.push(...row.brands);
|
|
342
|
+
if (typeof row.brand === 'string' && row.brand)
|
|
343
|
+
rawList.push(row.brand);
|
|
344
|
+
if (typeof row.Brand === 'string' && row.Brand)
|
|
345
|
+
rawList.push(...row.Brand.split('|'));
|
|
346
|
+
else if (Array.isArray(row.Brand))
|
|
347
|
+
rawList.push(...row.Brand);
|
|
348
|
+
if (typeof row.company === 'string' && row.company)
|
|
349
|
+
rawList.push(row.company);
|
|
350
|
+
if (Array.isArray(row.entities?.brands))
|
|
351
|
+
rawList.push(...row.entities.brands);
|
|
352
|
+
const seen = new Set();
|
|
353
|
+
const result = [];
|
|
354
|
+
for (const b of rawList) {
|
|
355
|
+
if (typeof b === 'string' && b.trim().length > 1) {
|
|
356
|
+
const clean = b.trim();
|
|
357
|
+
const norm = normalizeBrandKey(clean);
|
|
358
|
+
if (!norm || seen.has(norm))
|
|
359
|
+
continue;
|
|
360
|
+
if (publisherTokens && isPublisherBrand(clean, publisherTokens))
|
|
361
|
+
continue;
|
|
362
|
+
seen.add(norm);
|
|
363
|
+
result.push(clean);
|
|
364
|
+
if (result.length >= limit)
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
128
370
|
/**
|
|
129
371
|
* Count rows that are on-topic for the query. `catalog` is injectable for
|
|
130
372
|
* tests; defaults to the live graph catalog.
|
|
@@ -177,7 +419,7 @@ export function isDemandShaped(query) {
|
|
|
177
419
|
];
|
|
178
420
|
return demandKeywords.some(pat => pat.test(q));
|
|
179
421
|
}
|
|
180
|
-
export function addCoverageAnnotation(data, query, searchedGraphs, limit, skipEvidenceCheck = false, catalog = getGraphs(), options) {
|
|
422
|
+
export async function addCoverageAnnotation(data, query, searchedGraphs, limit, skipEvidenceCheck = false, catalog = getGraphs(), options) {
|
|
181
423
|
if (!data || typeof data !== 'object')
|
|
182
424
|
return data;
|
|
183
425
|
let normalizedData = data;
|
|
@@ -245,8 +487,17 @@ export function addCoverageAnnotation(data, query, searchedGraphs, limit, skipEv
|
|
|
245
487
|
if (limit !== undefined && limit < 3 && resultCount === limit) {
|
|
246
488
|
isThinCount = false;
|
|
247
489
|
}
|
|
490
|
+
// Evidence check guard: only evaluate evidence thinness if rows actually contain
|
|
491
|
+
// evidence arrays AND evidence was not skipped or opted out.
|
|
492
|
+
// If resultCount >= 3 and query did not request inline evidence (or evidence was stripped),
|
|
493
|
+
// missing inline evidence must NOT flag healthy results as 'thin'.
|
|
494
|
+
const hasEvidenceArrays = rows.some((r) => (Array.isArray(r.evidence) && r.evidence.length > 0) ||
|
|
495
|
+
(Array.isArray(r.evidence_items) && r.evidence_items.length > 0) ||
|
|
496
|
+
(Array.isArray(r.evidenceItems) && r.evidenceItems.length > 0) ||
|
|
497
|
+
(Array.isArray(r.trendEvidence) && r.trendEvidence.length > 0));
|
|
248
498
|
let isThinEvidence = false;
|
|
249
|
-
|
|
499
|
+
const skipEvidence = skipEvidenceCheck || options?.include_evidence === false || !hasEvidenceArrays;
|
|
500
|
+
if (!skipEvidence && resultCount >= 3) {
|
|
250
501
|
isThinEvidence = rows.every((r) => {
|
|
251
502
|
const count = Array.isArray(r.evidence)
|
|
252
503
|
? r.evidence.length
|
|
@@ -276,6 +527,20 @@ export function addCoverageAnnotation(data, query, searchedGraphs, limit, skipEv
|
|
|
276
527
|
if (isThinCount || isThinEvidence || isThinRelevance) {
|
|
277
528
|
status = 'thin';
|
|
278
529
|
}
|
|
530
|
+
// ── Respect Backend dataStatus / on_topic_total ──
|
|
531
|
+
// Fodda API evaluates vector similarity and relevance scores directly in the graph layer.
|
|
532
|
+
// When normalizedData.dataStatus === 'TREND_MATCH' or (on_topic_total >= 3),
|
|
533
|
+
// the backend has already confirmed that the returned trends are genuine on-topic matches.
|
|
534
|
+
// MCP's secondary heuristics should not downgrade a confirmed TREND_MATCH to 'thin'.
|
|
535
|
+
const backendOnTopicTotal = normalizedData.on_topic_total ?? options?.onTopicTotal;
|
|
536
|
+
const isBackendTrendMatch = normalizedData.dataStatus === 'TREND_MATCH' ||
|
|
537
|
+
(typeof backendOnTopicTotal === 'number' && backendOnTopicTotal >= 3);
|
|
538
|
+
if (isBackendTrendMatch && resultCount >= 3) {
|
|
539
|
+
status = 'ok';
|
|
540
|
+
if (onTopicCount !== undefined && onTopicCount < 3) {
|
|
541
|
+
onTopicCount = Math.min(resultCount, Math.max(3, typeof backendOnTopicTotal === 'number' ? backendOnTopicTotal : 3));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
279
544
|
}
|
|
280
545
|
const isDemand = isDemandShaped(query);
|
|
281
546
|
let suggestedAction = undefined;
|
|
@@ -339,10 +604,79 @@ export function addCoverageAnnotation(data, query, searchedGraphs, limit, skipEv
|
|
|
339
604
|
],
|
|
340
605
|
};
|
|
341
606
|
}
|
|
342
|
-
normalizedData.next_moves = generateNextMoves(rows, query, searchedGraphs, status, onTopicCount, suggestedAction, catalog, options?.analysts || getAnalysts(), options);
|
|
607
|
+
normalizedData.next_moves = await generateNextMoves(rows, query, searchedGraphs, status, onTopicCount, suggestedAction, catalog, options?.analysts || getAnalysts(), options);
|
|
343
608
|
console.error(`[coverage] status: ${status}, results: ${resultCount}/${limit || 'default'}${onTopicCount !== undefined ? `, on-topic: ${onTopicCount}` : ''}, query: "${query}"`);
|
|
344
609
|
return normalizedData;
|
|
345
610
|
}
|
|
611
|
+
const suggestCache = new Map();
|
|
612
|
+
const SUGGEST_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
613
|
+
const MAX_SUGGEST_CACHE_SIZE = 500;
|
|
614
|
+
export function clearSuggestCacheForTesting() {
|
|
615
|
+
suggestCache.clear();
|
|
616
|
+
}
|
|
617
|
+
function setSuggestCacheEntry(key, sources) {
|
|
618
|
+
const now = Date.now();
|
|
619
|
+
if (suggestCache.size >= MAX_SUGGEST_CACHE_SIZE) {
|
|
620
|
+
for (const [k, v] of suggestCache.entries()) {
|
|
621
|
+
if (now - v.timestamp >= SUGGEST_CACHE_TTL_MS) {
|
|
622
|
+
suggestCache.delete(k);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (suggestCache.size >= MAX_SUGGEST_CACHE_SIZE) {
|
|
627
|
+
const oldestKey = suggestCache.keys().next().value;
|
|
628
|
+
if (oldestKey)
|
|
629
|
+
suggestCache.delete(oldestKey);
|
|
630
|
+
}
|
|
631
|
+
suggestCache.set(key, { timestamp: now, sources });
|
|
632
|
+
}
|
|
633
|
+
export function truncateAtWordBoundary(str, maxLength) {
|
|
634
|
+
if (!str || str.length <= maxLength)
|
|
635
|
+
return str;
|
|
636
|
+
const sub = str.slice(0, maxLength);
|
|
637
|
+
const lastSpace = sub.lastIndexOf(' ');
|
|
638
|
+
if (lastSpace > 10) {
|
|
639
|
+
return sub.slice(0, lastSpace).trim();
|
|
640
|
+
}
|
|
641
|
+
return sub.trim();
|
|
642
|
+
}
|
|
643
|
+
export async function fetchSupplementalSuggest(query, options) {
|
|
644
|
+
const normQuery = query.toLowerCase().trim();
|
|
645
|
+
const sessionId = options?.sessionId || options?.userId || 'default';
|
|
646
|
+
const cacheKey = `${sessionId}:${normQuery}`;
|
|
647
|
+
const cached = suggestCache.get(cacheKey);
|
|
648
|
+
if (cached && (Date.now() - cached.timestamp) < SUGGEST_CACHE_TTL_MS) {
|
|
649
|
+
return cached.sources;
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
let suggestPromise = null;
|
|
653
|
+
if (options?.suggestPromise) {
|
|
654
|
+
suggestPromise = options.suggestPromise;
|
|
655
|
+
}
|
|
656
|
+
else if (options?.suggestFn) {
|
|
657
|
+
suggestPromise = options.suggestFn(query);
|
|
658
|
+
}
|
|
659
|
+
else if (options?.foddaRequest && options?.apiKey) {
|
|
660
|
+
suggestPromise = options.foddaRequest('GET', `/v1/supplemental/suggest?query=${encodeURIComponent(query)}`, options.apiKey, options.userId || '');
|
|
661
|
+
}
|
|
662
|
+
if (!suggestPromise) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
let timer;
|
|
666
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
667
|
+
timer = setTimeout(() => reject(new Error('suggest timeout (3000ms)')), 3000);
|
|
668
|
+
});
|
|
669
|
+
const data = await Promise.race([suggestPromise, timeoutPromise]);
|
|
670
|
+
clearTimeout(timer);
|
|
671
|
+
const sources = Array.isArray(data) ? data : (Array.isArray(data?.sources) ? data.sources : []);
|
|
672
|
+
setSuggestCacheEntry(cacheKey, sources);
|
|
673
|
+
return sources;
|
|
674
|
+
}
|
|
675
|
+
catch (err) {
|
|
676
|
+
console.error(`[next_moves] suggest fetch failed: ${err.message}`);
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
346
680
|
/**
|
|
347
681
|
* Clean and truncate a single theme candidate to <= 3 words, lowercase.
|
|
348
682
|
*/
|
|
@@ -394,7 +728,288 @@ export function formatThemePhrase(rawCandidates, fallback = 'emerging signals')
|
|
|
394
728
|
}
|
|
395
729
|
return `${cleanedThemes[0]} and ${cleanedThemes[1]}`;
|
|
396
730
|
}
|
|
397
|
-
|
|
731
|
+
/**
|
|
732
|
+
* Cross-cutting or generic tech/strategy terms that appear across multiple domains.
|
|
733
|
+
* When evaluating domain fit, matching solely on these words does NOT constitute
|
|
734
|
+
* genuine domain overlap if the query contains a specific vertical or domain term.
|
|
735
|
+
*/
|
|
736
|
+
export const CROSS_CUTTING_MODIFIERS = new Set([
|
|
737
|
+
'ai', 'artificial intelligence', 'genai', 'tech', 'technology', 'trends', 'trend',
|
|
738
|
+
'insights', 'insight', 'strategy', 'strategic', 'systems', 'system',
|
|
739
|
+
'innovation', 'innovations', 'future', 'futures', 'management', 'data',
|
|
740
|
+
'analytics', 'research', 'market', 'markets', 'consumer', 'consumers',
|
|
741
|
+
'culture', 'experience', 'experiences', 'overview', 'summary', 'report',
|
|
742
|
+
'industry', 'growth', 'landscape', 'benchmarks', 'benchmark'
|
|
743
|
+
]);
|
|
744
|
+
/**
|
|
745
|
+
* Score and rank candidate experts for a query.
|
|
746
|
+
* Reuses the shared scoring spine: domain overlap, blind-spot exclusion,
|
|
747
|
+
* self-recommendation suppression, and honest failure.
|
|
748
|
+
*/
|
|
749
|
+
export function findCandidateExperts(query, options) {
|
|
750
|
+
const rawAnalysts = options?.analysts || getAnalysts();
|
|
751
|
+
const activeAnalysts = rawAnalysts.filter(a => {
|
|
752
|
+
const st = (a.status || a.Status || '').toLowerCase().trim();
|
|
753
|
+
return !st || st === 'active' || st === 'unclaimed' || st === 'on_request' || st === 'on request';
|
|
754
|
+
});
|
|
755
|
+
const isCurrentAnalyst = (a) => {
|
|
756
|
+
const excludeId = options?.currentAnalystId || options?.excludeAnalystId;
|
|
757
|
+
if (!excludeId)
|
|
758
|
+
return false;
|
|
759
|
+
const cur = excludeId.toLowerCase().trim();
|
|
760
|
+
return (a.analyst_id && a.analyst_id.toLowerCase().trim() === cur) ||
|
|
761
|
+
(a.name && a.name.toLowerCase().trim() === cur);
|
|
762
|
+
};
|
|
763
|
+
const qLower = (query || '').toLowerCase().trim();
|
|
764
|
+
const queryTokens = specificQueryTokens(query);
|
|
765
|
+
const allQueryWords = qLower.split(/[^a-z0-9]+/).filter((w) => w.length >= 3 && !GENERIC_QUERY_TOKENS.has(w));
|
|
766
|
+
const queryDomainWords = allQueryWords.filter(w => !CROSS_CUTTING_MODIFIERS.has(w));
|
|
767
|
+
const isBrandOrCorporateQuery = Boolean(options?.isBrandTracker ||
|
|
768
|
+
/brand|financial|earnings|corporate|cmo|ceo|cfo|executive|revenue|margin|quarterly|investor|q[1-4]|balance sheet/i.test(qLower));
|
|
769
|
+
const isPhilosophicalQuery = /art\b|aesthetic|philosophy|philosophical|criticism|craftsmanship|harlem renaissance|aesthetic theory|ethics of art|ruskin|locke\b/i.test(qLower);
|
|
770
|
+
function getCategory(a) {
|
|
771
|
+
if (a.category)
|
|
772
|
+
return a.category;
|
|
773
|
+
if (a.is_human_agent || a.is_verified_real_person)
|
|
774
|
+
return 'human_agent';
|
|
775
|
+
if (a.is_c_suite_agent || /brand-(cmo|ceo|cfo)/i.test(a.analyst_id || ''))
|
|
776
|
+
return 'c_suite_agent';
|
|
777
|
+
if (a.is_classic_agent)
|
|
778
|
+
return 'classic_agent';
|
|
779
|
+
const sub = (a.graphSubType || a.graph_sub_type || a.type || a.kind || '').toString().toLowerCase();
|
|
780
|
+
if (sub.includes('classic'))
|
|
781
|
+
return 'classic_agent';
|
|
782
|
+
if (sub.includes('digital twin') || sub.includes('human'))
|
|
783
|
+
return 'human_agent';
|
|
784
|
+
if (sub.includes('c-suite') || sub.includes('executive'))
|
|
785
|
+
return 'c_suite_agent';
|
|
786
|
+
return 'synthetic_agent';
|
|
787
|
+
}
|
|
788
|
+
const searchedGraphs = options?.searchedGraphs || (options?.graphs ? options.graphs : getRelevantGraphs(query, 4, 15, 0.10));
|
|
789
|
+
const rowsByGraph = options?.rowsByGraph || new Map();
|
|
790
|
+
const scoredAnalysts = [];
|
|
791
|
+
for (const a of activeAnalysts) {
|
|
792
|
+
if (isCurrentAnalyst(a))
|
|
793
|
+
continue;
|
|
794
|
+
let baseScore = 0;
|
|
795
|
+
let directMatch = false;
|
|
796
|
+
let hasExplicitMatch = false;
|
|
797
|
+
const category = getCategory(a);
|
|
798
|
+
// Extract expert name and slug parts for direct identification and blindspot immunity
|
|
799
|
+
const aName = (a.name || '').toLowerCase().trim();
|
|
800
|
+
const aSlug = (a.analyst_id || '').toLowerCase().trim();
|
|
801
|
+
const nameParts = new Set([
|
|
802
|
+
...aName.split(/\s+/).filter((p) => p.length >= 3),
|
|
803
|
+
...aSlug.split(/[-_]+/).filter((p) => p.length >= 3)
|
|
804
|
+
]);
|
|
805
|
+
// 1. Negative signal: Blind spot / outside_their_lane
|
|
806
|
+
const rawBlindSpots = [
|
|
807
|
+
...(Array.isArray(a.outside_their_lane) ? a.outside_their_lane : (typeof a.outside_their_lane === 'string' ? [a.outside_their_lane] : [])),
|
|
808
|
+
...(Array.isArray(a.blind_spots) ? a.blind_spots : (typeof a.blind_spots === 'string' ? [a.blind_spots] : [])),
|
|
809
|
+
].join(' ').toLowerCase();
|
|
810
|
+
let hasBlindSpot = false;
|
|
811
|
+
if (rawBlindSpots.length > 0) {
|
|
812
|
+
for (const t of queryTokens) {
|
|
813
|
+
if (nameParts.has(t))
|
|
814
|
+
continue; // Never disqualify an expert for matching their own name or slug!
|
|
815
|
+
if (t.length >= 3 && rawBlindSpots.includes(t)) {
|
|
816
|
+
hasBlindSpot = true;
|
|
817
|
+
baseScore -= 20;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
for (const w of allQueryWords) {
|
|
821
|
+
if (nameParts.has(w))
|
|
822
|
+
continue; // Never disqualify an expert for matching their own name or slug!
|
|
823
|
+
if (w.length >= 4 && rawBlindSpots.includes(w)) {
|
|
824
|
+
hasBlindSpot = true;
|
|
825
|
+
baseScore -= 10;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// 2. Positive signal: expert_in & what_they_offer
|
|
830
|
+
const expertIn = [
|
|
831
|
+
...(Array.isArray(a.expert_in) ? a.expert_in : (typeof a.expert_in === 'string' ? [a.expert_in] : [])),
|
|
832
|
+
...(Array.isArray(a.what_they_offer) ? a.what_they_offer : (typeof a.what_they_offer === 'string' ? [a.what_they_offer] : [])),
|
|
833
|
+
].join(' ').toLowerCase();
|
|
834
|
+
if (expertIn.length > 0) {
|
|
835
|
+
if (qLower.length > 5 && expertIn.includes(qLower)) {
|
|
836
|
+
baseScore += 8;
|
|
837
|
+
hasExplicitMatch = true;
|
|
838
|
+
}
|
|
839
|
+
for (const t of queryTokens) {
|
|
840
|
+
if (expertIn.includes(t)) {
|
|
841
|
+
const isDomainToken = queryDomainWords.length === 0 || queryDomainWords.includes(t) || !CROSS_CUTTING_MODIFIERS.has(t);
|
|
842
|
+
baseScore += isDomainToken ? 4 : 1.5;
|
|
843
|
+
if (t.length >= 4 && isDomainToken)
|
|
844
|
+
hasExplicitMatch = true;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
for (const w of allQueryWords) {
|
|
848
|
+
if (expertIn.includes(w)) {
|
|
849
|
+
const isDomainWord = queryDomainWords.length === 0 || queryDomainWords.includes(w) || !CROSS_CUTTING_MODIFIERS.has(w);
|
|
850
|
+
baseScore += isDomainWord ? 1.5 : 0.5;
|
|
851
|
+
if (w.length >= 4 && isDomainWord)
|
|
852
|
+
hasExplicitMatch = true;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
// 2b. Positive signal: direct name or analyst_id match (for lookup/booking/hiring intent)
|
|
857
|
+
if (qLower.includes(aName) && aName.length >= 4) {
|
|
858
|
+
baseScore += 15;
|
|
859
|
+
hasExplicitMatch = true;
|
|
860
|
+
directMatch = true;
|
|
861
|
+
}
|
|
862
|
+
else if (aSlug && (qLower.includes(aSlug) || aSlug.includes(qLower))) {
|
|
863
|
+
baseScore += 15;
|
|
864
|
+
hasExplicitMatch = true;
|
|
865
|
+
directMatch = true;
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
for (const part of nameParts) {
|
|
869
|
+
if (queryTokens.includes(part) || allQueryWords.includes(part)) {
|
|
870
|
+
baseScore += 10;
|
|
871
|
+
hasExplicitMatch = true;
|
|
872
|
+
directMatch = true;
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// 3. Positive signal: topics
|
|
878
|
+
const aTopics = (Array.isArray(a.topics) ? a.topics : []).map((t) => (typeof t === 'string' ? t.toLowerCase() : '')).filter(Boolean);
|
|
879
|
+
for (const tp of aTopics) {
|
|
880
|
+
if (qLower.includes(tp) || tp.includes(qLower)) {
|
|
881
|
+
baseScore += 5;
|
|
882
|
+
hasExplicitMatch = true;
|
|
883
|
+
}
|
|
884
|
+
for (const t of queryTokens) {
|
|
885
|
+
if (tp.includes(t) || t.includes(tp)) {
|
|
886
|
+
const isDomainToken = queryDomainWords.length === 0 || queryDomainWords.includes(t) || !CROSS_CUTTING_MODIFIERS.has(t);
|
|
887
|
+
baseScore += isDomainToken ? 3 : 1;
|
|
888
|
+
if (t.length >= 3 && tp.length >= 3 && isDomainToken)
|
|
889
|
+
hasExplicitMatch = true;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
for (const w of allQueryWords) {
|
|
893
|
+
if (tp.includes(w) || w.includes(tp)) {
|
|
894
|
+
const isDomainWord = queryDomainWords.length === 0 || queryDomainWords.includes(w) || !CROSS_CUTTING_MODIFIERS.has(w);
|
|
895
|
+
baseScore += isDomainWord ? 1.5 : 0.5;
|
|
896
|
+
if (w.length >= 3 && tp.length >= 3 && isDomainWord)
|
|
897
|
+
hasExplicitMatch = true;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// 4. Positive signal: description (supporting context)
|
|
902
|
+
const desc = (typeof a.description === 'string' ? a.description : '').toLowerCase();
|
|
903
|
+
if (desc.length > 0) {
|
|
904
|
+
if (qLower.length > 5 && desc.includes(qLower)) {
|
|
905
|
+
baseScore += 4;
|
|
906
|
+
}
|
|
907
|
+
for (const t of queryTokens) {
|
|
908
|
+
if (desc.includes(t)) {
|
|
909
|
+
baseScore += 2;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
for (const w of allQueryWords) {
|
|
913
|
+
if (desc.includes(w)) {
|
|
914
|
+
baseScore += 1;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
// 5. Positive signal: Searched graphs & graph relevance scores
|
|
919
|
+
for (const g of searchedGraphs) {
|
|
920
|
+
const gid = (typeof g === 'string' ? g : (g.graph_id || g.id || '')).toLowerCase().trim();
|
|
921
|
+
const gCurator = (typeof g === 'object' && g.curator ? g.curator : '').toLowerCase().trim();
|
|
922
|
+
const isOwner = (gid && (gid === aSlug || gid.includes(aSlug) || aSlug.includes(gid))) ||
|
|
923
|
+
(gCurator && (gCurator === aName || gCurator.includes(aName) || aName.includes(gCurator)));
|
|
924
|
+
if (isOwner) {
|
|
925
|
+
// If graph has a router relevance score, use it directly.
|
|
926
|
+
// If unscored (e.g. broadcast or unranked search), award positive score ONLY if
|
|
927
|
+
// this graph actually returned matching results in rowsByGraph.
|
|
928
|
+
const rowsForGraph = rowsByGraph.get(gid) || (aSlug ? rowsByGraph.get(aSlug) : null) || [];
|
|
929
|
+
const relScore = (typeof g === 'object' && typeof g.relevanceScore === 'number')
|
|
930
|
+
? g.relevanceScore
|
|
931
|
+
: (rowsForGraph.length > 0 ? 0.8 : 0);
|
|
932
|
+
if (relScore > 0) {
|
|
933
|
+
baseScore += relScore * 5.0;
|
|
934
|
+
hasExplicitMatch = true;
|
|
935
|
+
if (relScore >= 0.5)
|
|
936
|
+
directMatch = true;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
let score = baseScore;
|
|
941
|
+
// ── Tier Intent Weighting ──
|
|
942
|
+
// Require base token relevance score >= 6 before applying any status preference boost
|
|
943
|
+
// so irrelevant profiles (sam-horn, taxtalk-online) are excluded on local fallback.
|
|
944
|
+
if (baseScore >= 6) {
|
|
945
|
+
if (category === 'c_suite_agent') {
|
|
946
|
+
if (isBrandOrCorporateQuery) {
|
|
947
|
+
score += 6.0;
|
|
948
|
+
}
|
|
949
|
+
else {
|
|
950
|
+
score -= 5.0;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
else if (category === 'classic_agent') {
|
|
954
|
+
if (isPhilosophicalQuery) {
|
|
955
|
+
score += 5.0;
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
score -= 10.0; // Classic thinkers must never outrank living practitioners on contemporary queries
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
else if (category === 'human_agent' || a.is_verified_real_person) {
|
|
962
|
+
score += 3.0; // Prioritize living human practitioners over synthetic agents
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
scoredAnalysts.push({ analyst: a, score, hasBlindSpot, hasExplicitMatch, directMatch });
|
|
966
|
+
}
|
|
967
|
+
// Require minimum threshold score (>= 6.0), explicit domain/topic match, and no disqualifying blind spot match
|
|
968
|
+
const MIN_EXPERT_FIT_SCORE = 6.0;
|
|
969
|
+
const validCandidates = scoredAnalysts
|
|
970
|
+
.filter(s => s.score >= MIN_EXPERT_FIT_SCORE && (s.directMatch || (s.hasExplicitMatch && !s.hasBlindSpot)))
|
|
971
|
+
.sort((a, b) => b.score - a.score);
|
|
972
|
+
const maxLimit = Math.min(Math.max(options?.limit || 3, 1), 3);
|
|
973
|
+
const topCandidates = validCandidates.slice(0, maxLimit);
|
|
974
|
+
const status = options?.status;
|
|
975
|
+
return topCandidates.map(c => {
|
|
976
|
+
const matchedAnalyst = c.analyst;
|
|
977
|
+
let lane = '';
|
|
978
|
+
const expertInStr = Array.isArray(matchedAnalyst.expert_in)
|
|
979
|
+
? matchedAnalyst.expert_in.filter(Boolean).join(', ')
|
|
980
|
+
: (typeof matchedAnalyst.expert_in === 'string' ? matchedAnalyst.expert_in : '');
|
|
981
|
+
if (expertInStr.trim().length > 0) {
|
|
982
|
+
lane = truncateAtWordBoundary(expertInStr.trim(), 60);
|
|
983
|
+
}
|
|
984
|
+
else if (matchedAnalyst.description && matchedAnalyst.description.trim().length > 0) {
|
|
985
|
+
const firstClause = (matchedAnalyst.description.split(/[,.;]/)[0] || '').trim();
|
|
986
|
+
lane = truncateAtWordBoundary(firstClause, 60);
|
|
987
|
+
}
|
|
988
|
+
else if (matchedAnalyst.what_they_offer && typeof matchedAnalyst.what_they_offer === 'string' && matchedAnalyst.what_they_offer.trim().length > 0) {
|
|
989
|
+
lane = truncateAtWordBoundary(matchedAnalyst.what_they_offer.trim(), 60);
|
|
990
|
+
}
|
|
991
|
+
const reason = (status === 'thin' || status === 'empty')
|
|
992
|
+
? (lane && lane.length > 3 ? `closest expert lane for ${lane}` : 'closest expert lane for this topic')
|
|
993
|
+
: (lane && lane.length > 3 ? `covers ${lane} directly` : 'covers this domain directly');
|
|
994
|
+
const cat = matchedAnalyst.category || getCategory(matchedAnalyst);
|
|
995
|
+
const statusRaw = (matchedAnalyst.status || matchedAnalyst.Status || '').toLowerCase().trim();
|
|
996
|
+
const isOnRequest = statusRaw === 'unclaimed' || statusRaw === 'on request' || statusRaw === 'on_request';
|
|
997
|
+
const expertStatus = isOnRequest ? 'on_request' : 'active';
|
|
998
|
+
const consult_tool = (matchedAnalyst.consult_tool === 'consult_human_agent' || matchedAnalyst.consult_tool === 'consult_analyst')
|
|
999
|
+
? matchedAnalyst.consult_tool
|
|
1000
|
+
: (cat === 'human_agent' ? 'consult_human_agent' : 'consult_analyst');
|
|
1001
|
+
return {
|
|
1002
|
+
analyst_id: matchedAnalyst.analyst_id,
|
|
1003
|
+
display_name: cleanDisplayName(matchedAnalyst.name),
|
|
1004
|
+
category: cat,
|
|
1005
|
+
status: expertStatus,
|
|
1006
|
+
consult_tool,
|
|
1007
|
+
reason,
|
|
1008
|
+
out_of_lane: false
|
|
1009
|
+
};
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
export async function generateNextMoves(rows, query, searchedGraphs, status, onTopicCount, suggestedAction, catalog = getGraphs(), analysts = getAnalysts(), options) {
|
|
398
1013
|
const nextMoves = {
|
|
399
1014
|
scope_prompt: true,
|
|
400
1015
|
presentation: 'internal',
|
|
@@ -530,97 +1145,102 @@ export function generateNextMoves(rows, query, searchedGraphs, status, onTopicCo
|
|
|
530
1145
|
}
|
|
531
1146
|
}
|
|
532
1147
|
else {
|
|
533
|
-
// Empty brand footprint:
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
1148
|
+
// Empty brand footprint: try to find relevant unsearched domain graph if score >= 0.10
|
|
1149
|
+
let relevantCandidates = [];
|
|
1150
|
+
try {
|
|
1151
|
+
relevantCandidates = getRelevantGraphs(query);
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
relevantCandidates = [];
|
|
1155
|
+
}
|
|
1156
|
+
const unsearchedDomain = relevantCandidates.find(cand => cand.graph && !searchedGraphIds.has(cand.graph.graph_id) && (cand.score ?? 0) >= 0.10)?.graph;
|
|
1157
|
+
const primarySearched = searchedGraphs[0];
|
|
1158
|
+
const primaryGid = typeof primarySearched === 'string' ? primarySearched : primarySearched?.graph_id;
|
|
1159
|
+
const primaryMeta = catalog.find(g => g.graph_id === primaryGid);
|
|
1160
|
+
const primaryDisplay = primaryMeta ? buildDisplayName(primaryMeta) : (primaryGid || 'the graph');
|
|
1161
|
+
if (unsearchedDomain) {
|
|
1162
|
+
const topDisplay = buildDisplayName(unsearchedDomain);
|
|
538
1163
|
nextMoves.thread = {
|
|
539
1164
|
kind: 'honest_thin',
|
|
540
|
-
graph_id:
|
|
541
|
-
graph_display:
|
|
1165
|
+
graph_id: primaryGid || unsearchedDomain.graph_id,
|
|
1166
|
+
graph_display: primaryDisplay,
|
|
542
1167
|
adjacent: {
|
|
543
|
-
graph_id:
|
|
1168
|
+
graph_id: unsearchedDomain.graph_id,
|
|
544
1169
|
graph_display: topDisplay,
|
|
545
|
-
reason:
|
|
1170
|
+
reason: unsearchedDomain.headline || unsearchedDomain.domain || unsearchedDomain.name,
|
|
546
1171
|
}
|
|
547
1172
|
};
|
|
548
1173
|
}
|
|
1174
|
+
else {
|
|
1175
|
+
nextMoves.thread = {
|
|
1176
|
+
kind: 'honest_thin',
|
|
1177
|
+
graph_id: primaryGid,
|
|
1178
|
+
graph_display: primaryDisplay,
|
|
1179
|
+
text: `That's what Fodda holds on this right now.`,
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
549
1182
|
}
|
|
550
1183
|
}
|
|
551
|
-
else
|
|
552
|
-
const
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
else if (status === 'thin' || status === 'empty') {
|
|
563
|
-
// honest_thin ONLY when status is thin or empty
|
|
564
|
-
let relevantCandidates = [];
|
|
565
|
-
try {
|
|
566
|
-
relevantCandidates = getRelevantGraphs(query);
|
|
567
|
-
}
|
|
568
|
-
catch {
|
|
569
|
-
relevantCandidates = [];
|
|
570
|
-
}
|
|
571
|
-
const candidateGraphs = relevantCandidates.map(c => c.graph).filter(Boolean);
|
|
572
|
-
if (candidateGraphs.length === 0 && catalog?.length > 0) {
|
|
573
|
-
candidateGraphs.push(...catalog);
|
|
574
|
-
}
|
|
575
|
-
const closest = candidateGraphs.find(g => !searchedGraphIds.has(g.graph_id)) || candidateGraphs[0];
|
|
576
|
-
if (closest) {
|
|
577
|
-
const closestDisplay = buildDisplayName(closest);
|
|
578
|
-
const primarySearched = searchedGraphs[0];
|
|
579
|
-
const primaryGid = typeof primarySearched === 'string' ? primarySearched : primarySearched?.graph_id;
|
|
580
|
-
const primaryMeta = catalog.find(g => g.graph_id === primaryGid);
|
|
581
|
-
const primaryDisplay = primaryMeta ? buildDisplayName(primaryMeta) : (primaryGid || closestDisplay);
|
|
1184
|
+
else {
|
|
1185
|
+
const bestMeta = bestGraphId ? catalog.find(g => g.graph_id === bestGraphId) : undefined;
|
|
1186
|
+
const trendCount = bestMeta?.trend_count;
|
|
1187
|
+
const gOnTopicTotal = bestGraphId
|
|
1188
|
+
? (rowsByGraph.get(bestGraphId)?.[0]?.on_topic_total ?? options?.onTopicTotal ?? (maxRemainder + rows.length))
|
|
1189
|
+
: undefined;
|
|
1190
|
+
const isSmallReportOrExhausted = (typeof trendCount === 'number' && trendCount > 0 && trendCount <= 15) ||
|
|
1191
|
+
(typeof trendCount === 'number' && trendCount > 0 && typeof gOnTopicTotal === 'number' && gOnTopicTotal >= 0.6 * trendCount);
|
|
1192
|
+
const canEmitMoreInGraph = maxRemainder > 0 && bestGraphId && !isSmallReportOrExhausted;
|
|
1193
|
+
if (canEmitMoreInGraph && bestGraphId) {
|
|
1194
|
+
const display = bestMeta ? buildDisplayName(bestMeta) : bestGraphId;
|
|
582
1195
|
nextMoves.thread = {
|
|
583
|
-
kind: '
|
|
584
|
-
graph_id:
|
|
585
|
-
graph_display:
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
graph_display: closestDisplay,
|
|
589
|
-
reason: closest.headline || closest.domain || closest.name,
|
|
590
|
-
}
|
|
1196
|
+
kind: 'more_in_graph',
|
|
1197
|
+
graph_id: bestGraphId,
|
|
1198
|
+
graph_display: display,
|
|
1199
|
+
remaining_count: maxRemainder,
|
|
1200
|
+
theme: bestGraphTheme,
|
|
591
1201
|
};
|
|
592
1202
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
const unsearched = relevantCandidates.find(cand => cand.graph && !searchedGraphIds.has(cand.graph.graph_id) && (cand.score ?? 0) >= 0.10);
|
|
605
|
-
if (unsearched) {
|
|
606
|
-
const adjG = unsearched.graph;
|
|
607
|
-
const adjDisplay = buildDisplayName(adjG);
|
|
1203
|
+
else {
|
|
1204
|
+
// Fall through to unsearched adjacent_room or honest_thin
|
|
1205
|
+
let relevantCandidates = [];
|
|
1206
|
+
try {
|
|
1207
|
+
relevantCandidates = getRelevantGraphs(query);
|
|
1208
|
+
}
|
|
1209
|
+
catch {
|
|
1210
|
+
relevantCandidates = [];
|
|
1211
|
+
}
|
|
1212
|
+
const unsearchedCandidate = relevantCandidates.find(cand => cand.graph && !searchedGraphIds.has(cand.graph.graph_id) && (cand.score ?? 0) >= 0.10);
|
|
608
1213
|
const primarySearched = searchedGraphs[0];
|
|
609
1214
|
const primaryGid = typeof primarySearched === 'string' ? primarySearched : primarySearched?.graph_id;
|
|
610
1215
|
const primaryMeta = catalog.find(g => g.graph_id === primaryGid);
|
|
611
|
-
const primaryDisplay = primaryMeta ? buildDisplayName(primaryMeta) : (primaryGid ||
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
graph_id: adjG.graph_id,
|
|
618
|
-
graph_display:
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1216
|
+
const primaryDisplay = primaryMeta ? buildDisplayName(primaryMeta) : (primaryGid || 'the graph');
|
|
1217
|
+
if (unsearchedCandidate) {
|
|
1218
|
+
const adjG = unsearchedCandidate.graph;
|
|
1219
|
+
const adjDisplay = buildDisplayName(adjG);
|
|
1220
|
+
nextMoves.thread = {
|
|
1221
|
+
kind: (status === 'thin' || status === 'empty' || isSmallReportOrExhausted) ? 'honest_thin' : 'adjacent_room',
|
|
1222
|
+
graph_id: primaryGid || adjG.graph_id,
|
|
1223
|
+
graph_display: primaryDisplay,
|
|
1224
|
+
adjacent: {
|
|
1225
|
+
graph_id: adjG.graph_id,
|
|
1226
|
+
graph_display: adjDisplay,
|
|
1227
|
+
reason: adjG.headline || adjG.domain || adjG.description || adjG.name,
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
else if (status === 'thin' || status === 'empty' || isSmallReportOrExhausted) {
|
|
1232
|
+
// Honest thin without hallucinated adjacent graph: never fabricate catalog[0] or blind unsearched fallback
|
|
1233
|
+
nextMoves.thread = {
|
|
1234
|
+
kind: 'honest_thin',
|
|
1235
|
+
graph_id: primaryGid,
|
|
1236
|
+
graph_display: primaryDisplay,
|
|
1237
|
+
text: `That's what Fodda holds on this right now.`,
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
nextMoves.thread = undefined;
|
|
1242
|
+
}
|
|
622
1243
|
}
|
|
623
|
-
// If no unsearched relevant room, thread is undefined (drop the line, no fabrication)
|
|
624
1244
|
}
|
|
625
1245
|
// ── 2. Specific (Go specific: brands, statistics_source, expert) ──
|
|
626
1246
|
const specific = {};
|
|
@@ -635,39 +1255,61 @@ export function generateNextMoves(rows, query, searchedGraphs, status, onTopicCo
|
|
|
635
1255
|
else {
|
|
636
1256
|
const tokens = specificQueryTokens(query);
|
|
637
1257
|
const onTopicRows = rows.filter(r => rowMatchesQueryTokens(r, tokens, catalog) || (rowScore(r) >= 0.75 * (TIER_NOMINAL_SCORE[resolveRowTier(r, searchedGraphs, catalog)] ?? 0.8)));
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
brandList
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1258
|
+
// If no on-topic rows exist, leave specific.brands undefined (do not fall back to off-topic noise rows)
|
|
1259
|
+
if (onTopicRows.length > 0) {
|
|
1260
|
+
const publisherTokens = buildPublisherExclusionSet(searchedGraphs, catalog, onTopicRows);
|
|
1261
|
+
const brandCounts = new Map();
|
|
1262
|
+
for (const r of onTopicRows) {
|
|
1263
|
+
if (!isRowBrandEligible(r))
|
|
1264
|
+
continue;
|
|
1265
|
+
const brandList = [];
|
|
1266
|
+
if (Array.isArray(r.brandNames))
|
|
1267
|
+
brandList.push(...r.brandNames);
|
|
1268
|
+
else if (typeof r.brandNames === 'string')
|
|
1269
|
+
brandList.push(...r.brandNames.split('|'));
|
|
1270
|
+
if (Array.isArray(r.brands))
|
|
1271
|
+
brandList.push(...r.brands);
|
|
1272
|
+
if (typeof r.brand === 'string' && r.brand)
|
|
1273
|
+
brandList.push(r.brand);
|
|
1274
|
+
if (typeof r.Brand === 'string' && r.Brand)
|
|
1275
|
+
brandList.push(...r.Brand.split('|'));
|
|
1276
|
+
else if (Array.isArray(r.Brand))
|
|
1277
|
+
brandList.push(...r.Brand);
|
|
1278
|
+
if (typeof r.company === 'string' && r.company)
|
|
1279
|
+
brandList.push(r.company);
|
|
1280
|
+
if (Array.isArray(r.entities?.brands))
|
|
1281
|
+
brandList.push(...r.entities.brands);
|
|
1282
|
+
for (const b of brandList) {
|
|
1283
|
+
if (typeof b === 'string' && b.trim().length > 1) {
|
|
1284
|
+
const clean = b.trim();
|
|
1285
|
+
const norm = normalizeBrandKey(clean);
|
|
1286
|
+
if (!norm)
|
|
1287
|
+
continue;
|
|
1288
|
+
if (isPublisherBrand(clean, publisherTokens))
|
|
1289
|
+
continue;
|
|
1290
|
+
const existing = brandCounts.get(norm);
|
|
1291
|
+
if (existing) {
|
|
1292
|
+
existing.count++;
|
|
1293
|
+
}
|
|
1294
|
+
else {
|
|
1295
|
+
brandCounts.set(norm, { count: 1, displayName: clean });
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
656
1298
|
}
|
|
657
1299
|
}
|
|
658
|
-
|
|
659
|
-
if (brandCounts.size > 0) {
|
|
660
|
-
const sortedBrands = [...brandCounts.entries()]
|
|
661
|
-
.filter(([b]) => {
|
|
662
|
-
const bLow = b.toLowerCase();
|
|
1300
|
+
if (brandCounts.size > 0) {
|
|
663
1301
|
const qLow = query.toLowerCase();
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1302
|
+
const sortedBrands = [...brandCounts.values()]
|
|
1303
|
+
.filter(b => {
|
|
1304
|
+
const bLow = b.displayName.toLowerCase();
|
|
1305
|
+
return bLow !== qLow && !qLow.includes(bLow) && !bLow.includes(qLow);
|
|
1306
|
+
})
|
|
1307
|
+
.sort((a, b) => b.count - a.count)
|
|
1308
|
+
.map(b => b.displayName)
|
|
1309
|
+
.slice(0, 2);
|
|
1310
|
+
if (sortedBrands.length > 0) {
|
|
1311
|
+
specific.brands = sortedBrands;
|
|
1312
|
+
}
|
|
671
1313
|
}
|
|
672
1314
|
}
|
|
673
1315
|
}
|
|
@@ -699,65 +1341,155 @@ export function generateNextMoves(rows, query, searchedGraphs, status, onTopicCo
|
|
|
699
1341
|
else {
|
|
700
1342
|
const qLower = query.toLowerCase();
|
|
701
1343
|
const isStatOrMarketShaped = suggestedAction !== undefined ||
|
|
1344
|
+
status === 'thin' ||
|
|
1345
|
+
status === 'empty' ||
|
|
702
1346
|
isDemandShaped(query) ||
|
|
703
1347
|
options?.knownBrand !== undefined ||
|
|
704
1348
|
(specific.brands && specific.brands.length > 0) ||
|
|
705
1349
|
/(?:market|spend|sales|growth|size|volume|rate|adoption|share|stats?|numbers?|forecast|demographics?|economic|inflation|pricing|consumer|retail|cpg|beauty|fashion|auto|tech|work|travel|food|drink|beverage|culture|trends?|brand|performance|activity)/i.test(qLower);
|
|
706
1350
|
if (isStatOrMarketShaped) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1351
|
+
let suggestHandled = false;
|
|
1352
|
+
if (options?.foddaRequest || options?.suggestFn || options?.suggestPromise) {
|
|
1353
|
+
const sources = await fetchSupplementalSuggest(query, options);
|
|
1354
|
+
if (sources && sources.length > 0) {
|
|
1355
|
+
const validSources = sources.filter(s => s && s.returns_draft !== true && s.name);
|
|
1356
|
+
if (validSources.length > 0) {
|
|
1357
|
+
const topSources = validSources.slice(0, 2);
|
|
1358
|
+
if (topSources.length === 2 && topSources[0] && topSources[1]) {
|
|
1359
|
+
specific.statistics_source = `${topSources[0].name} and ${topSources[1].name}`;
|
|
1360
|
+
}
|
|
1361
|
+
else if (topSources[0]) {
|
|
1362
|
+
specific.statistics_source = topSources[0].name;
|
|
1363
|
+
}
|
|
1364
|
+
suggestHandled = true;
|
|
1365
|
+
options?.sessionTracker?.recordSuggestPath?.('suggest');
|
|
1366
|
+
console.error(`[next_moves] statistics_source from suggest: "${specific.statistics_source}"`);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
721
1369
|
}
|
|
722
|
-
|
|
723
|
-
|
|
1370
|
+
if (!suggestHandled) {
|
|
1371
|
+
options?.sessionTracker?.recordSuggestPath?.('regex-fallback');
|
|
1372
|
+
console.error(`[next_moves] statistics_source from regex-fallback`);
|
|
1373
|
+
if (/(?:retail|spend|sales|commerce|shopping|store|grocery|cpg|consumer)/i.test(qLower)) {
|
|
1374
|
+
specific.statistics_source = 'Census retail trade and spending data';
|
|
1375
|
+
}
|
|
1376
|
+
else if (/(?:search|demand|interest|popular|google|volume|buzz|social)/i.test(qLower)) {
|
|
1377
|
+
specific.statistics_source = 'Google Trends search volume and breakout queries';
|
|
1378
|
+
}
|
|
1379
|
+
else if (/(?:employment|labor|job|wage|worker|workplace|talent|hiring)/i.test(qLower)) {
|
|
1380
|
+
specific.statistics_source = 'BLS labor and employment metrics';
|
|
1381
|
+
}
|
|
1382
|
+
else if (/(?:economic|inflation|cpi|interest\s+rate|gdp|macro|fed|recession)/i.test(qLower)) {
|
|
1383
|
+
specific.statistics_source = 'FRED macroeconomic series';
|
|
1384
|
+
}
|
|
1385
|
+
else if (/(?:beauty|fashion|apparel|luxury|sport|wellness|food|beverage)/i.test(qLower)) {
|
|
1386
|
+
specific.statistics_source = 'Census and Google Trends market demand data';
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
specific.statistics_source = 'Census and FRED market statistics';
|
|
1390
|
+
}
|
|
724
1391
|
}
|
|
725
1392
|
}
|
|
726
1393
|
}
|
|
727
|
-
// Expert:
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
(a.name && a.name.toLowerCase().trim() === cur);
|
|
738
|
-
};
|
|
739
|
-
const matchedAnalyst = activeAnalysts.find(a => {
|
|
740
|
-
if (isCurrentAnalyst(a))
|
|
741
|
-
return false;
|
|
742
|
-
const aSlug = (a.analyst_id || '').toLowerCase().trim();
|
|
743
|
-
if (searchedGraphIds.has(aSlug))
|
|
744
|
-
return true;
|
|
745
|
-
const aName = (a.name || '').toLowerCase();
|
|
746
|
-
const aDesc = (a.description || '').toLowerCase();
|
|
747
|
-
if (tokens.some(t => aName.includes(t) || aDesc.includes(t)))
|
|
748
|
-
return true;
|
|
749
|
-
return false;
|
|
1394
|
+
// Expert: scored lane-fit picker over active analysts using shared findCandidateExperts
|
|
1395
|
+
const candidateExperts = findCandidateExperts(query, {
|
|
1396
|
+
analysts,
|
|
1397
|
+
graphs: catalog,
|
|
1398
|
+
limit: 1,
|
|
1399
|
+
currentAnalystId: options?.currentAnalystId,
|
|
1400
|
+
searchedGraphs,
|
|
1401
|
+
rowsByGraph,
|
|
1402
|
+
status,
|
|
1403
|
+
isBrandTracker: options?.isBrandTracker
|
|
750
1404
|
});
|
|
751
|
-
if (
|
|
752
|
-
specific.expert =
|
|
753
|
-
analyst_id: matchedAnalyst.analyst_id,
|
|
754
|
-
display_name: matchedAnalyst.name,
|
|
755
|
-
reason: matchedAnalyst.description ? `covers ${matchedAnalyst.description.slice(0, 50).trim()} directly` : 'covers this domain directly',
|
|
756
|
-
};
|
|
1405
|
+
if (candidateExperts.length > 0 && candidateExperts[0]) {
|
|
1406
|
+
specific.expert = candidateExperts[0];
|
|
757
1407
|
}
|
|
758
1408
|
if (Object.keys(specific).length > 0) {
|
|
759
1409
|
nextMoves.specific = specific;
|
|
760
1410
|
}
|
|
1411
|
+
nextMoves.scope = options?.knownBrand
|
|
1412
|
+
? `Want this cut to ${options.knownBrand} specifically?`
|
|
1413
|
+
: `If you tell me the brand or brief you're working on, I'll cut this to that.`;
|
|
1414
|
+
const cleanTopic = options?.knownBrand || options?.brandDisplayName || nextMoves.thread?.theme || query.trim();
|
|
1415
|
+
const isOnRequestExpert = specific.expert?.status === 'on_request';
|
|
1416
|
+
const isExpertHuman = specific.expert?.category === 'human_agent' || specific.expert?.consult_tool === 'consult_human_agent';
|
|
1417
|
+
let expertTargetTool = 'find_expert';
|
|
1418
|
+
let expertName = 'Consult specialist';
|
|
1419
|
+
let expertDescription = 'Get practitioner judgment and domain depth on this topic.';
|
|
1420
|
+
let expertReason = 'Consult a domain specialist to interpret qualitative nuances.';
|
|
1421
|
+
let expertPrompt = `Who should I consult about ${cleanTopic}?`;
|
|
1422
|
+
let expertParams = { query: cleanTopic };
|
|
1423
|
+
if (specific.expert) {
|
|
1424
|
+
if (isOnRequestExpert) {
|
|
1425
|
+
expertTargetTool = 'request_expert_intro';
|
|
1426
|
+
expertName = `Request intro to ${specific.expert.display_name}`;
|
|
1427
|
+
expertDescription = 'Request an advisory introduction or consultation with this prospective Human Agent.';
|
|
1428
|
+
expertReason = `Connect with ${specific.expert.display_name} for human advisory or consultation.`;
|
|
1429
|
+
expertPrompt = `Request an introduction or advisory consultation with ${specific.expert.display_name} regarding ${cleanTopic}.`;
|
|
1430
|
+
expertParams = { expert_id: specific.expert.analyst_id, query: cleanTopic };
|
|
1431
|
+
}
|
|
1432
|
+
else if (isExpertHuman) {
|
|
1433
|
+
expertTargetTool = 'consult_human_agent';
|
|
1434
|
+
expertName = `Consult ${specific.expert.display_name}`;
|
|
1435
|
+
expertDescription = 'Consult this verified Human Agent twin for practitioner depth and proprietary frameworks.';
|
|
1436
|
+
expertReason = `Consult ${specific.expert.display_name} for authoritative interpretation and practitioner depth.`;
|
|
1437
|
+
expertPrompt = `Ask ${specific.expert.display_name} about ${cleanTopic}`;
|
|
1438
|
+
expertParams = { analyst_id: specific.expert.analyst_id, query: `How should we evaluate the strategic opportunity and risks in ${cleanTopic}?` };
|
|
1439
|
+
}
|
|
1440
|
+
else {
|
|
1441
|
+
expertTargetTool = specific.expert.consult_tool || 'consult_analyst';
|
|
1442
|
+
expertName = `Consult ${specific.expert.display_name}`;
|
|
1443
|
+
expertDescription = 'Consult this specialist analyst for domain intelligence.';
|
|
1444
|
+
expertReason = `Consult ${specific.expert.display_name} for authoritative interpretation and practitioner depth.`;
|
|
1445
|
+
expertPrompt = `Ask ${specific.expert.display_name} about ${cleanTopic}`;
|
|
1446
|
+
expertParams = { analyst_id: specific.expert.analyst_id, query: cleanTopic };
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
const actions = [
|
|
1450
|
+
{
|
|
1451
|
+
action: 'pressure_test',
|
|
1452
|
+
name: 'Pressure-test thesis',
|
|
1453
|
+
description: 'Evaluate assertions against counter-evidence and executive divergence.',
|
|
1454
|
+
reason: 'Evaluate whether findings rely on untested assumptions',
|
|
1455
|
+
target_tool: 'verify_market_claim',
|
|
1456
|
+
tool_display_name: 'Market Claim Verifier',
|
|
1457
|
+
suggested_prompt: `Pressure-test whether the market momentum around ${cleanTopic} holds up against counter-evidence.`,
|
|
1458
|
+
suggested_parameters: { claim: `Pressure-test whether ${cleanTopic} strategy and market momentum holds up` },
|
|
1459
|
+
available: true,
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
action: 'ask_expert',
|
|
1463
|
+
name: expertName,
|
|
1464
|
+
description: expertDescription,
|
|
1465
|
+
reason: expertReason,
|
|
1466
|
+
target_tool: expertTargetTool,
|
|
1467
|
+
suggested_prompt: expertPrompt,
|
|
1468
|
+
suggested_parameters: expertParams,
|
|
1469
|
+
available: true,
|
|
1470
|
+
},
|
|
1471
|
+
{
|
|
1472
|
+
action: 'create_brief',
|
|
1473
|
+
name: 'Commission executive brief',
|
|
1474
|
+
description: 'Turn this intelligence into a finished strategic deliverable.',
|
|
1475
|
+
reason: `Package verified evidence, quantitative statistics, and executive quotes on ${cleanTopic} into a deliverable.`,
|
|
1476
|
+
target_tool: 'request_deliverable',
|
|
1477
|
+
suggested_prompt: `Commission an executive brief on ${cleanTopic}`,
|
|
1478
|
+
suggested_parameters: { skill_slug: 'research_brief', brief: `Synthesize verified findings, market metrics, and strategic implications for ${cleanTopic}` },
|
|
1479
|
+
available: true,
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
action: 'track_topic',
|
|
1483
|
+
name: 'Track topic shifts',
|
|
1484
|
+
description: 'Establish automated weekly monitoring on this subject.',
|
|
1485
|
+
reason: `Detect newly emerging signals, shifts in executive tone, and trend momentum on ${cleanTopic} weekly.`,
|
|
1486
|
+
target_tool: 'manage_scheduled_reports',
|
|
1487
|
+
suggested_prompt: `Set up weekly tracking on ${cleanTopic}`,
|
|
1488
|
+
suggested_parameters: { action: 'create', topic: cleanTopic, cadence: 'weekly' },
|
|
1489
|
+
available: true,
|
|
1490
|
+
},
|
|
1491
|
+
];
|
|
1492
|
+
nextMoves.actions = actions;
|
|
761
1493
|
return nextMoves;
|
|
762
1494
|
}
|
|
763
1495
|
/**
|
|
@@ -828,13 +1560,16 @@ export function generateConsultNextMoves(result, query, analystId, options, cata
|
|
|
828
1560
|
}
|
|
829
1561
|
}
|
|
830
1562
|
else {
|
|
831
|
-
// Next angle token check per §2.A.5: must share >=1 content token (>=3 chars) with sources_used or
|
|
1563
|
+
// Next angle token check per §2.A.5: must share >=1 content token (>=3 chars) with sources_used, uncited_themes, query, or expert topics
|
|
832
1564
|
let nextAngleValid = false;
|
|
833
1565
|
if (typeof nextAngleRaw === 'string' && nextAngleRaw.trim().length > 0) {
|
|
834
1566
|
const angleTokens = specificQueryTokens(nextAngleRaw);
|
|
835
|
-
const sourceTitles = (result?.sources_used || [])
|
|
1567
|
+
const sourceTitles = (result?.sources_used || [])
|
|
1568
|
+
.filter((s) => s.origin !== 'profile' && !(s.title || '').includes('Official Profile'))
|
|
1569
|
+
.map((s) => (s.title || s.name || '').toLowerCase());
|
|
836
1570
|
const themeTexts = uncitedThemes.map((t) => t.toLowerCase());
|
|
837
|
-
const
|
|
1571
|
+
const expertTopics = Array.isArray(matchedAnalyst?.topics) ? matchedAnalyst.topics.join(' ').toLowerCase() : '';
|
|
1572
|
+
const groundingCorpus = [...sourceTitles, ...themeTexts, expertTopics, query.toLowerCase()].join(' ');
|
|
838
1573
|
const hasSharedToken = angleTokens.some(tok => tok.length >= 3 && groundingCorpus.includes(tok));
|
|
839
1574
|
if (hasSharedToken || (sourceTitles.length === 0 && themeTexts.length === 0)) {
|
|
840
1575
|
nextAngleValid = true;
|
|
@@ -905,7 +1640,8 @@ export function generateConsultNextMoves(result, query, analystId, options, cata
|
|
|
905
1640
|
if (matchedAnalyst.slug)
|
|
906
1641
|
expertOwnIds.add(String(matchedAnalyst.slug).toLowerCase());
|
|
907
1642
|
}
|
|
908
|
-
const SHELF_RELEVANCE_FLOOR = 0.
|
|
1643
|
+
const SHELF_RELEVANCE_FLOOR = 0.50;
|
|
1644
|
+
const queryTokens = specificQueryTokens(query);
|
|
909
1645
|
const shelfCandidateGraphs = [];
|
|
910
1646
|
for (const cand of relevantCandidates) {
|
|
911
1647
|
if ((cand.score ?? 0) < SHELF_RELEVANCE_FLOOR)
|
|
@@ -920,6 +1656,26 @@ export function generateConsultNextMoves(result, query, analystId, options, cata
|
|
|
920
1656
|
continue;
|
|
921
1657
|
if (g.name && g.name.toLowerCase() === expertDisplayName.toLowerCase())
|
|
922
1658
|
continue;
|
|
1659
|
+
// Shelf on an expert consult must only suggest published research/trend graphs (report, domain),
|
|
1660
|
+
// never other individual expert agents or classic historical figures.
|
|
1661
|
+
const gType = (g.graph_type || '').toLowerCase();
|
|
1662
|
+
if (gType === 'expert' || gType === 'analyst' || cand.graphTier === 'static_expert' || cand.graphTier === 'living') {
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
// Must match at least 2 distinct domain tokens from the query, or have a direct topic match
|
|
1666
|
+
let tokenMatchCount = 0;
|
|
1667
|
+
if (queryTokens.length > 0) {
|
|
1668
|
+
const graphText = `${g.name || ''} ${g.domain || ''} ${(Array.isArray(g.topics) ? g.topics : []).join(' ')} ${g.headline || ''} ${g.one_liner || ''} ${g.description || ''}`.toLowerCase();
|
|
1669
|
+
const words = new Set(graphText.split(/[^a-z0-9]+/));
|
|
1670
|
+
for (const t of queryTokens) {
|
|
1671
|
+
if (t.length >= 3 && words.has(t)) {
|
|
1672
|
+
tokenMatchCount++;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
const minMatchesNeeded = queryTokens.length >= 2 ? 2 : 1;
|
|
1677
|
+
if (tokenMatchCount < minMatchesNeeded)
|
|
1678
|
+
continue;
|
|
923
1679
|
if (shelfCandidateGraphs.length < 2 && !shelfCandidateGraphs.some(sg => sg.graph_id === g.graph_id)) {
|
|
924
1680
|
shelfCandidateGraphs.push(g);
|
|
925
1681
|
}
|
|
@@ -948,8 +1704,12 @@ export function generateConsultNextMoves(result, query, analystId, options, cata
|
|
|
948
1704
|
shelfSentence += `.`;
|
|
949
1705
|
}
|
|
950
1706
|
}
|
|
1707
|
+
else if (hasDeliverableOffering) {
|
|
1708
|
+
shelfSentence = `${expertDisplayName} takes scoped briefs on this if you need a deliverable.`;
|
|
1709
|
+
nextMoves.shelf = undefined;
|
|
1710
|
+
}
|
|
951
1711
|
else {
|
|
952
|
-
// Empty candidate list — omit sentence 2 per Brief 1.46.29
|
|
1712
|
+
// Empty candidate list and no deliverable offerings — omit sentence 2 per Brief 1.46.29
|
|
953
1713
|
shelfSentence = '';
|
|
954
1714
|
nextMoves.shelf = undefined;
|
|
955
1715
|
}
|
|
@@ -964,7 +1724,7 @@ export function generateConsultNextMoves(result, query, analystId, options, cata
|
|
|
964
1724
|
const topRef = result.referrals[0];
|
|
965
1725
|
specific.expert = {
|
|
966
1726
|
analyst_id: topRef.id || topRef.analyst_id || topRef.slug,
|
|
967
|
-
display_name: topRef.name || topRef.curator,
|
|
1727
|
+
display_name: cleanDisplayName(topRef.name || topRef.curator),
|
|
968
1728
|
reason: topRef.reason || 'related expertise',
|
|
969
1729
|
};
|
|
970
1730
|
}
|
|
@@ -979,11 +1739,46 @@ export function generateConsultNextMoves(result, query, analystId, options, cata
|
|
|
979
1739
|
else {
|
|
980
1740
|
scopeSentence = `If you tell me the brand or brief you're working on, I'll cut this to that.`;
|
|
981
1741
|
}
|
|
1742
|
+
nextMoves.scope = scopeSentence;
|
|
982
1743
|
nextMoves.consult_envelope = {
|
|
983
1744
|
thread_line: threadSentence,
|
|
984
1745
|
shelf_line: shelfSentence || undefined,
|
|
985
1746
|
scope_line: scopeSentence,
|
|
986
1747
|
};
|
|
1748
|
+
const consultActions = [
|
|
1749
|
+
{
|
|
1750
|
+
action: 'pressure_test',
|
|
1751
|
+
name: 'Pressure-test perspective',
|
|
1752
|
+
description: "Verify expert assertions and claims against broader market data.",
|
|
1753
|
+
reason: `Verify ${expertDisplayName}'s perspective against empirical market evidence and executive divergence.`,
|
|
1754
|
+
target_tool: 'verify_market_claim',
|
|
1755
|
+
tool_display_name: 'Market Claim Verifier',
|
|
1756
|
+
suggested_prompt: `Pressure-test ${expertDisplayName}'s perspective on ${query.trim()}`,
|
|
1757
|
+
suggested_parameters: { claim: `Pressure-test ${expertDisplayName}'s thesis and assertions on ${query.trim()}` },
|
|
1758
|
+
available: true,
|
|
1759
|
+
},
|
|
1760
|
+
{
|
|
1761
|
+
action: 'create_brief',
|
|
1762
|
+
name: 'Commission deliverable',
|
|
1763
|
+
description: `Commission a finished deliverable from ${expertDisplayName}.`,
|
|
1764
|
+
reason: `Turn ${expertDisplayName}'s insights and framework into a formatted deliverable.`,
|
|
1765
|
+
target_tool: 'request_deliverable',
|
|
1766
|
+
suggested_prompt: `Commission a strategic briefing from ${expertDisplayName} on ${query.trim()}`,
|
|
1767
|
+
suggested_parameters: { analyst_id: cleanAnalystId || expertGraphId, skill_slug: 'research_brief', brief: `Strategic brief incorporating ${expertDisplayName}'s framework on ${query.trim()}` },
|
|
1768
|
+
available: true,
|
|
1769
|
+
},
|
|
1770
|
+
{
|
|
1771
|
+
action: 'track_topic',
|
|
1772
|
+
name: 'Track topic shifts',
|
|
1773
|
+
description: 'Establish automated weekly monitoring on this subject.',
|
|
1774
|
+
reason: `Monitor new signals and developments in ${expertDisplayName}'s coverage area.`,
|
|
1775
|
+
target_tool: 'manage_scheduled_reports',
|
|
1776
|
+
suggested_prompt: `Set up weekly tracking on ${query.trim()}`,
|
|
1777
|
+
suggested_parameters: { action: 'create', topic: query.trim(), cadence: 'weekly' },
|
|
1778
|
+
available: true,
|
|
1779
|
+
},
|
|
1780
|
+
];
|
|
1781
|
+
nextMoves.actions = consultActions;
|
|
987
1782
|
return nextMoves;
|
|
988
1783
|
}
|
|
989
1784
|
/**
|