mcp-researchpowerpack-http 3.11.0 → 3.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp-use.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "includeInspector": false,
3
- "buildTime": "2026-04-10T04:34:47.889Z",
4
- "buildId": "5fa641655351cd15",
3
+ "buildTime": "2026-04-10T04:56:34.793Z",
4
+ "buildId": "f92a979e39981ed9",
5
5
  "entryPoint": "dist/index.js",
6
6
  "widgets": {}
7
7
  }
@@ -18,7 +18,13 @@ const webSearchOutputSchema = z.object({
18
18
  execution_time_ms: z.number().int().nonnegative().describe("Elapsed execution time in milliseconds."),
19
19
  total_unique_urls: z.number().int().nonnegative().optional().describe("Unique URL count observed across all searches."),
20
20
  consensus_url_count: z.number().int().nonnegative().optional().describe("Count of URLs that met the consensus threshold."),
21
- frequency_threshold: z.number().int().nonnegative().optional().describe("Minimum frequency required for a URL to be considered consensus.")
21
+ frequency_threshold: z.number().int().nonnegative().optional().describe("Minimum frequency required for a URL to be considered consensus."),
22
+ coverage_summary: z.array(z.object({
23
+ keyword: z.string().describe("The search keyword."),
24
+ result_count: z.number().int().nonnegative().describe("Number of results returned for this keyword."),
25
+ top_url: z.string().optional().describe("Domain of the top-ranked result for this keyword.")
26
+ })).optional().describe("Per-keyword result counts and top URLs for coverage analysis."),
27
+ low_yield_keywords: z.array(z.string()).optional().describe("Keywords that produced 0-1 results.")
22
28
  }).strict().describe("Structured metadata about the completed web search batch.")
23
29
  }).strict();
24
30
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/schemas/web-search.ts"],
4
- "sourcesContent": ["import { z } from 'zod';\n\n// Keyword schema with validation\nconst keywordSchema = z\n .string({ error: 'web-search: Keyword is required' })\n .min(1, { message: 'web-search: Keyword cannot be empty' })\n .max(500, { message: 'web-search: Keyword too long (max 500 characters)' })\n .refine(\n k => k.trim().length > 0,\n { message: 'web-search: Keyword cannot be whitespace only' }\n )\n .describe('A single Google search query (1\u2013500 chars). Each keyword runs as a separate parallel search. Use varied angles: direct topic, comparisons, \"best of\" lists, year-specific, site-specific (e.g., \"site:github.com topic\").');\n\n// Input schema for web-search tool\nconst keywordsSchema = z\n .array(keywordSchema, {\n error: 'web-search: Keywords must be an array',\n })\n .min(1, { message: 'web-search: At least 1 keyword required' })\n .max(100, { message: 'web-search: Maximum 100 keywords allowed per request' })\n .describe('Array of 1\u2013100 search keywords. RECOMMENDED: 3\u20137 for solid consensus ranking, up to 20 for thorough coverage. Each keyword runs as a separate Google search in parallel. Results are aggregated and URLs appearing in multiple searches are flagged as high-confidence consensus matches. Supply <1 and you get an error; >100 is rejected.');\n\nconst webSearchParamsShape = {\n keywords: keywordsSchema,\n};\n\nexport const webSearchParamsSchema = z.object(webSearchParamsShape).strict();\nexport type WebSearchParams = z.infer<typeof webSearchParamsSchema>;\n\nexport const webSearchOutputSchema = z.object({\n content: z\n .string()\n .describe('Formatted markdown report containing ranked URLs with consensus markers and per-query results.'),\n metadata: z.object({\n total_keywords: z\n .number()\n .int()\n .nonnegative()\n .describe('Total number of keyword queries executed.'),\n total_results: z\n .number()\n .int()\n .nonnegative()\n .describe('Total number of ranked search results included across shown queries.'),\n execution_time_ms: z\n .number()\n .int()\n .nonnegative()\n .describe('Elapsed execution time in milliseconds.'),\n total_unique_urls: z\n .number()\n .int()\n .nonnegative()\n .optional()\n .describe('Unique URL count observed across all searches.'),\n consensus_url_count: z\n .number()\n .int()\n .nonnegative()\n .optional()\n .describe('Count of URLs that met the consensus threshold.'),\n frequency_threshold: z\n .number()\n .int()\n .nonnegative()\n .optional()\n .describe('Minimum frequency required for a URL to be considered consensus.'),\n }).strict().describe('Structured metadata about the completed web search batch.'),\n}).strict();\n\nexport type WebSearchOutput = z.infer<typeof webSearchOutputSchema>;\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAGlB,MAAM,gBAAgB,EACnB,OAAO,EAAE,OAAO,kCAAkC,CAAC,EACnD,IAAI,GAAG,EAAE,SAAS,sCAAsC,CAAC,EACzD,IAAI,KAAK,EAAE,SAAS,oDAAoD,CAAC,EACzE;AAAA,EACC,OAAK,EAAE,KAAK,EAAE,SAAS;AAAA,EACvB,EAAE,SAAS,gDAAgD;AAC7D,EACC,SAAS,gOAA2N;AAGvO,MAAM,iBAAiB,EACpB,MAAM,eAAe;AAAA,EACpB,OAAO;AACT,CAAC,EACA,IAAI,GAAG,EAAE,SAAS,0CAA0C,CAAC,EAC7D,IAAI,KAAK,EAAE,SAAS,uDAAuD,CAAC,EAC5E,SAAS,uVAA6U;AAEzV,MAAM,uBAAuB;AAAA,EAC3B,UAAU;AACZ;AAEO,MAAM,wBAAwB,EAAE,OAAO,oBAAoB,EAAE,OAAO;AAGpE,MAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,SAAS,EACN,OAAO,EACP,SAAS,gGAAgG;AAAA,EAC5G,UAAU,EAAE,OAAO;AAAA,IACjB,gBAAgB,EACb,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,2CAA2C;AAAA,IACvD,eAAe,EACZ,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,sEAAsE;AAAA,IAClF,mBAAmB,EAChB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,yCAAyC;AAAA,IACrD,mBAAmB,EAChB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,gDAAgD;AAAA,IAC5D,qBAAqB,EAClB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,iDAAiD;AAAA,IAC7D,qBAAqB,EAClB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,kEAAkE;AAAA,EAChF,CAAC,EAAE,OAAO,EAAE,SAAS,2DAA2D;AAClF,CAAC,EAAE,OAAO;",
4
+ "sourcesContent": ["import { z } from 'zod';\n\n// Keyword schema with validation\nconst keywordSchema = z\n .string({ error: 'web-search: Keyword is required' })\n .min(1, { message: 'web-search: Keyword cannot be empty' })\n .max(500, { message: 'web-search: Keyword too long (max 500 characters)' })\n .refine(\n k => k.trim().length > 0,\n { message: 'web-search: Keyword cannot be whitespace only' }\n )\n .describe('A single Google search query (1\u2013500 chars). Each keyword runs as a separate parallel search. Use varied angles: direct topic, comparisons, \"best of\" lists, year-specific, site-specific (e.g., \"site:github.com topic\").');\n\n// Input schema for web-search tool\nconst keywordsSchema = z\n .array(keywordSchema, {\n error: 'web-search: Keywords must be an array',\n })\n .min(1, { message: 'web-search: At least 1 keyword required' })\n .max(100, { message: 'web-search: Maximum 100 keywords allowed per request' })\n .describe('Array of 1\u2013100 search keywords. RECOMMENDED: 3\u20137 for solid consensus ranking, up to 20 for thorough coverage. Each keyword runs as a separate Google search in parallel. Results are aggregated and URLs appearing in multiple searches are flagged as high-confidence consensus matches. Supply <1 and you get an error; >100 is rejected.');\n\nconst webSearchParamsShape = {\n keywords: keywordsSchema,\n};\n\nexport const webSearchParamsSchema = z.object(webSearchParamsShape).strict();\nexport type WebSearchParams = z.infer<typeof webSearchParamsSchema>;\n\nexport const webSearchOutputSchema = z.object({\n content: z\n .string()\n .describe('Formatted markdown report containing ranked URLs with consensus markers and per-query results.'),\n metadata: z.object({\n total_keywords: z\n .number()\n .int()\n .nonnegative()\n .describe('Total number of keyword queries executed.'),\n total_results: z\n .number()\n .int()\n .nonnegative()\n .describe('Total number of ranked search results included across shown queries.'),\n execution_time_ms: z\n .number()\n .int()\n .nonnegative()\n .describe('Elapsed execution time in milliseconds.'),\n total_unique_urls: z\n .number()\n .int()\n .nonnegative()\n .optional()\n .describe('Unique URL count observed across all searches.'),\n consensus_url_count: z\n .number()\n .int()\n .nonnegative()\n .optional()\n .describe('Count of URLs that met the consensus threshold.'),\n frequency_threshold: z\n .number()\n .int()\n .nonnegative()\n .optional()\n .describe('Minimum frequency required for a URL to be considered consensus.'),\n coverage_summary: z\n .array(z.object({\n keyword: z.string().describe('The search keyword.'),\n result_count: z.number().int().nonnegative().describe('Number of results returned for this keyword.'),\n top_url: z.string().optional().describe('Domain of the top-ranked result for this keyword.'),\n }))\n .optional()\n .describe('Per-keyword result counts and top URLs for coverage analysis.'),\n low_yield_keywords: z\n .array(z.string())\n .optional()\n .describe('Keywords that produced 0-1 results.'),\n }).strict().describe('Structured metadata about the completed web search batch.'),\n}).strict();\n\nexport type WebSearchOutput = z.infer<typeof webSearchOutputSchema>;\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAGlB,MAAM,gBAAgB,EACnB,OAAO,EAAE,OAAO,kCAAkC,CAAC,EACnD,IAAI,GAAG,EAAE,SAAS,sCAAsC,CAAC,EACzD,IAAI,KAAK,EAAE,SAAS,oDAAoD,CAAC,EACzE;AAAA,EACC,OAAK,EAAE,KAAK,EAAE,SAAS;AAAA,EACvB,EAAE,SAAS,gDAAgD;AAC7D,EACC,SAAS,gOAA2N;AAGvO,MAAM,iBAAiB,EACpB,MAAM,eAAe;AAAA,EACpB,OAAO;AACT,CAAC,EACA,IAAI,GAAG,EAAE,SAAS,0CAA0C,CAAC,EAC7D,IAAI,KAAK,EAAE,SAAS,uDAAuD,CAAC,EAC5E,SAAS,uVAA6U;AAEzV,MAAM,uBAAuB;AAAA,EAC3B,UAAU;AACZ;AAEO,MAAM,wBAAwB,EAAE,OAAO,oBAAoB,EAAE,OAAO;AAGpE,MAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,SAAS,EACN,OAAO,EACP,SAAS,gGAAgG;AAAA,EAC5G,UAAU,EAAE,OAAO;AAAA,IACjB,gBAAgB,EACb,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,2CAA2C;AAAA,IACvD,eAAe,EACZ,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,sEAAsE;AAAA,IAClF,mBAAmB,EAChB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,yCAAyC;AAAA,IACrD,mBAAmB,EAChB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,gDAAgD;AAAA,IAC5D,qBAAqB,EAClB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,iDAAiD;AAAA,IAC7D,qBAAqB,EAClB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,kEAAkE;AAAA,IAC9E,kBAAkB,EACf,MAAM,EAAE,OAAO;AAAA,MACd,SAAS,EAAE,OAAO,EAAE,SAAS,qBAAqB;AAAA,MAClD,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,8CAA8C;AAAA,MACpG,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,mDAAmD;AAAA,IAC7F,CAAC,CAAC,EACD,SAAS,EACT,SAAS,+DAA+D;AAAA,IAC3E,oBAAoB,EACjB,MAAM,EAAE,OAAO,CAAC,EAChB,SAAS,EACT,SAAS,qCAAqC;AAAA,EACnD,CAAC,EAAE,OAAO,EAAE,SAAS,2DAA2D;AAClF,CAAC,EAAE,OAAO;",
6
6
  "names": []
7
7
  }
@@ -6,12 +6,8 @@ import {
6
6
  import { SearchClient } from "../clients/search.js";
7
7
  import {
8
8
  aggregateAndRank,
9
- buildUrlLookup,
10
- lookupUrl,
11
- generateEnhancedOutput,
12
- markConsensus
9
+ generateUnifiedOutput
13
10
  } from "../utils/url-aggregator.js";
14
- import { CTR_WEIGHTS } from "../config/index.js";
15
11
  import { classifyError } from "../utils/errors.js";
16
12
  import {
17
13
  mcpLog,
@@ -25,84 +21,50 @@ import {
25
21
  toolSuccess,
26
22
  toToolResponse
27
23
  } from "./mcp-helpers.js";
28
- function getPositionScore(position) {
29
- if (position >= 1 && position <= 10) {
30
- return CTR_WEIGHTS[position] ?? 0;
31
- }
32
- return Math.max(0, 10 - (position - 10) * 0.5);
33
- }
34
24
  async function executeSearches(keywords) {
35
25
  const client = new SearchClient();
36
26
  return client.searchMultiple(keywords);
37
27
  }
38
- function processAndRankResults(response) {
28
+ function processResults(response) {
39
29
  const aggregation = aggregateAndRank(response.searches, 5);
40
- const urlLookup = buildUrlLookup(aggregation.rankedUrls);
41
30
  const consensusUrls = aggregation.rankedUrls.filter((u) => u.isConsensus);
42
- return { aggregation, urlLookup, consensusUrls };
31
+ return { aggregation, consensusUrls };
43
32
  }
44
- function buildConsensusSection(keywords, aggregation) {
45
- return generateEnhancedOutput(
33
+ function buildOutputMarkdown(keywords, aggregation, searches) {
34
+ return generateUnifiedOutput(
46
35
  aggregation.rankedUrls,
47
36
  keywords,
37
+ searches,
48
38
  aggregation.totalUniqueUrls,
49
39
  aggregation.frequencyThreshold,
50
40
  aggregation.thresholdNote
51
- ) + "\n---\n\n";
52
- }
53
- function formatSearchResultEntry(result, position, urlLookup) {
54
- const positionScore = getPositionScore(position);
55
- const rankedUrl = lookupUrl(result.link, urlLookup);
56
- const frequency = rankedUrl?.frequency ?? 1;
57
- const consensusMark = markConsensus(frequency);
58
- const consensusInfo = rankedUrl ? `${consensusMark} (${frequency} searches)` : `${consensusMark} (1 search)`;
59
- let entry = `${position}. **[${result.title}](${result.link})** \u2014 Position ${position} | Score: ${positionScore.toFixed(1)} | Consensus: ${consensusInfo}
60
- `;
61
- if (result.snippet) {
62
- entry += result.date ? ` - *${result.date}* \u2014 ${result.snippet}
63
- ` : ` - ${result.snippet}
64
- `;
65
- }
66
- entry += "\n";
67
- return entry;
41
+ );
68
42
  }
69
- function buildPerQuerySection(response, urlLookup) {
70
- let markdown = `## \u{1F4CA} Full Search Results by Query
71
-
72
- `;
73
- let totalResults = 0;
74
- response.searches.forEach((search, index) => {
75
- markdown += `### Query ${index + 1}: "${search.keyword}"
76
-
77
- `;
78
- search.results.forEach((result, resultIndex) => {
79
- markdown += formatSearchResultEntry(result, resultIndex + 1, urlLookup);
80
- totalResults++;
81
- });
82
- if (search.related && search.related.length > 0) {
83
- const relatedSuggestions = search.related.map((r) => `\`${r}\``).join(", ");
84
- markdown += `*Related:* ${relatedSuggestions}
85
-
86
- `;
43
+ function formatSearchOutput(outputMarkdown, aggregation, consensusUrlCount, executionTime, totalKeywords, searches) {
44
+ const markdown = outputMarkdown + `
45
+ ---
46
+ *${formatDuration(executionTime)} | ${aggregation.totalUniqueUrls} unique URLs | ${consensusUrlCount} consensus | threshold \u2265${aggregation.frequencyThreshold}*`;
47
+ const coverageSummary = searches.map((s) => {
48
+ let topDomain;
49
+ const topResult = s.results[0];
50
+ if (topResult) {
51
+ try {
52
+ topDomain = new URL(topResult.link).hostname.replace(/^www\./, "");
53
+ } catch {
54
+ }
87
55
  }
88
- if (index < response.searches.length - 1) markdown += `---
89
-
90
- `;
56
+ return { keyword: s.keyword, result_count: s.results.length, top_url: topDomain };
91
57
  });
92
- return { markdown, totalResults };
93
- }
94
- function formatSearchOutput(consensusSection, perQuerySection, totalResults, aggregation, consensusUrlCount, executionTime, totalKeywords) {
95
- let markdown = consensusSection + perQuerySection;
96
- markdown += `
97
- ---
98
- *${formatDuration(executionTime)} | ${aggregation.totalUniqueUrls} unique URLs | ${consensusUrlCount} consensus*`;
58
+ const lowYieldKeywords = searches.filter((s) => s.results.length <= 1).map((s) => s.keyword);
99
59
  const metadata = {
100
60
  total_keywords: totalKeywords,
101
- total_results: totalResults,
61
+ total_results: aggregation.rankedUrls.length,
102
62
  execution_time_ms: executionTime,
103
63
  total_unique_urls: aggregation.totalUniqueUrls,
104
64
  consensus_url_count: consensusUrlCount,
105
- frequency_threshold: aggregation.frequencyThreshold
65
+ frequency_threshold: aggregation.frequencyThreshold,
66
+ coverage_summary: coverageSummary,
67
+ ...lowYieldKeywords.length > 0 ? { low_yield_keywords: lowYieldKeywords } : {}
106
68
  };
107
69
  return toolSuccess(markdown, { content: markdown, metadata });
108
70
  }
@@ -136,28 +98,26 @@ async function handleWebSearch(params, reporter = NOOP_REPORTER) {
136
98
  await reporter.progress(15, 100, "Submitting search queries");
137
99
  const response = await executeSearches(params.keywords);
138
100
  await reporter.progress(50, 100, "Collected search results");
139
- const { aggregation, urlLookup, consensusUrls } = processAndRankResults(response);
101
+ const { aggregation, consensusUrls } = processResults(response);
140
102
  await reporter.log(
141
103
  "info",
142
104
  `Collected ${aggregation.totalUniqueUrls} unique URLs across ${response.totalKeywords} queries`
143
105
  );
144
- const consensusSection = buildConsensusSection(params.keywords, aggregation);
145
- const { markdown: perQuerySection, totalResults } = buildPerQuerySection(response, urlLookup);
106
+ const outputMarkdown = buildOutputMarkdown(params.keywords, aggregation, response.searches);
146
107
  await reporter.progress(80, 100, "Ranking and formatting search results");
147
108
  const executionTime = Date.now() - startTime;
148
- mcpLog("info", `Search completed: ${totalResults} results, ${aggregation.totalUniqueUrls} unique URLs, ${consensusUrls.length} consensus`, "search");
109
+ mcpLog("info", `Search completed: ${aggregation.rankedUrls.length} unique URLs, ${consensusUrls.length} consensus`, "search");
149
110
  await reporter.log(
150
111
  "info",
151
- `Search completed with ${totalResults} ranked results and ${consensusUrls.length} consensus URL(s)`
112
+ `Search completed with ${aggregation.rankedUrls.length} ranked URLs and ${consensusUrls.length} consensus`
152
113
  );
153
114
  return formatSearchOutput(
154
- consensusSection,
155
- perQuerySection,
156
- totalResults,
115
+ outputMarkdown,
157
116
  aggregation,
158
117
  consensusUrls.length,
159
118
  executionTime,
160
- response.totalKeywords
119
+ response.totalKeywords,
120
+ response.searches
161
121
  );
162
122
  } catch (error) {
163
123
  return buildWebSearchError(error, params, startTime);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/tools/search.ts"],
4
- "sourcesContent": ["/**\n * Web Search Tool Handler\n * NEVER throws - always returns structured response for graceful degradation\n */\n\nimport type { MCPServer } from 'mcp-use/server';\n\nimport { getCapabilities, getMissingEnvMessage } from '../config/index.js';\nimport {\n webSearchOutputSchema,\n webSearchParamsSchema,\n type WebSearchParams,\n type WebSearchOutput,\n} from '../schemas/web-search.js';\nimport { SearchClient } from '../clients/search.js';\nimport {\n aggregateAndRank,\n buildUrlLookup,\n lookupUrl,\n generateEnhancedOutput,\n markConsensus,\n} from '../utils/url-aggregator.js';\nimport { CTR_WEIGHTS } from '../config/index.js';\nimport { classifyError } from '../utils/errors.js';\nimport {\n mcpLog,\n formatSuccess,\n formatError,\n formatDuration,\n} from './utils.js';\nimport {\n createToolReporter,\n NOOP_REPORTER,\n toolFailure,\n toolSuccess,\n toToolResponse,\n type ToolExecutionResult,\n type ToolReporter,\n} from './mcp-helpers.js';\n\nfunction getPositionScore(position: number): number {\n if (position >= 1 && position <= 10) {\n return CTR_WEIGHTS[position] ?? 0;\n }\n return Math.max(0, 10 - (position - 10) * 0.5);\n}\n\n// --- Internal types ---\n\ninterface SearchAggregation {\n readonly rankedUrls: ReturnType<typeof aggregateAndRank>['rankedUrls'];\n readonly totalUniqueUrls: number;\n readonly frequencyThreshold: number;\n readonly thresholdNote?: string;\n}\n\ninterface SearchResponse {\n searches: Parameters<typeof aggregateAndRank>[0];\n totalKeywords: number;\n}\n\n// --- Helpers ---\n\nasync function executeSearches(keywords: string[]): Promise<SearchResponse> {\n const client = new SearchClient();\n return client.searchMultiple(keywords);\n}\n\nfunction processAndRankResults(response: SearchResponse): {\n aggregation: SearchAggregation;\n urlLookup: ReturnType<typeof buildUrlLookup>;\n consensusUrls: SearchAggregation['rankedUrls'];\n} {\n const aggregation = aggregateAndRank(response.searches, 5);\n // Build lookup from ALL ranked URLs so per-query entries can show consensus info\n const urlLookup = buildUrlLookup(aggregation.rankedUrls);\n const consensusUrls = aggregation.rankedUrls.filter(u => u.isConsensus);\n return { aggregation, urlLookup, consensusUrls };\n}\n\nfunction buildConsensusSection(\n keywords: string[],\n aggregation: SearchAggregation,\n): string {\n // Always show all ranked URLs (consensus-marked within)\n return generateEnhancedOutput(\n aggregation.rankedUrls, keywords, aggregation.totalUniqueUrls,\n aggregation.frequencyThreshold, aggregation.thresholdNote,\n ) + '\\n---\\n\\n';\n}\n\nfunction formatSearchResultEntry(\n result: { title: string; link: string; snippet?: string; date?: string },\n position: number,\n urlLookup: ReturnType<typeof buildUrlLookup>,\n): string {\n const positionScore = getPositionScore(position);\n const rankedUrl = lookupUrl(result.link, urlLookup);\n const frequency = rankedUrl?.frequency ?? 1;\n const consensusMark = markConsensus(frequency);\n const consensusInfo = rankedUrl\n ? `${consensusMark} (${frequency} searches)`\n : `${consensusMark} (1 search)`;\n\n let entry = `${position}. **[${result.title}](${result.link})** \u2014 Position ${position} | Score: ${positionScore.toFixed(1)} | Consensus: ${consensusInfo}\\n`;\n\n if (result.snippet) {\n entry += result.date\n ? ` - *${result.date}* \u2014 ${result.snippet}\\n`\n : ` - ${result.snippet}\\n`;\n }\n\n entry += '\\n';\n return entry;\n}\n\nfunction buildPerQuerySection(\n response: SearchResponse,\n urlLookup: ReturnType<typeof buildUrlLookup>,\n): { markdown: string; totalResults: number } {\n let markdown = `## \uD83D\uDCCA Full Search Results by Query\\n\\n`;\n\n let totalResults = 0;\n\n response.searches.forEach((search, index) => {\n markdown += `### Query ${index + 1}: \"${search.keyword}\"\\n\\n`;\n\n search.results.forEach((result, resultIndex) => {\n markdown += formatSearchResultEntry(result, resultIndex + 1, urlLookup);\n totalResults++;\n });\n\n if (search.related && search.related.length > 0) {\n const relatedSuggestions = search.related\n .map((r: string) => `\\`${r}\\``)\n .join(', ');\n markdown += `*Related:* ${relatedSuggestions}\\n\\n`;\n }\n\n if (index < response.searches.length - 1) markdown += `---\\n\\n`;\n });\n\n return { markdown, totalResults };\n}\n\nfunction formatSearchOutput(\n consensusSection: string,\n perQuerySection: string,\n totalResults: number,\n aggregation: SearchAggregation,\n consensusUrlCount: number,\n executionTime: number,\n totalKeywords: number,\n): ToolExecutionResult<WebSearchOutput> {\n let markdown = consensusSection + perQuerySection;\n\n markdown += `\\n---\\n*${formatDuration(executionTime)} | ${aggregation.totalUniqueUrls} unique URLs | ${consensusUrlCount} consensus*`;\n\n const metadata = {\n total_keywords: totalKeywords,\n total_results: totalResults,\n execution_time_ms: executionTime,\n total_unique_urls: aggregation.totalUniqueUrls,\n consensus_url_count: consensusUrlCount,\n frequency_threshold: aggregation.frequencyThreshold,\n };\n\n return toolSuccess(markdown, { content: markdown, metadata });\n}\n\nfunction buildWebSearchError(\n error: unknown,\n params: WebSearchParams,\n startTime: number,\n): ToolExecutionResult<WebSearchOutput> {\n const structuredError = classifyError(error);\n const executionTime = Date.now() - startTime;\n\n mcpLog('error', `web-search: ${structuredError.message}`, 'search');\n\n const errorContent = formatError({\n code: structuredError.code,\n message: structuredError.message,\n retryable: structuredError.retryable,\n toolName: 'web-search',\n howToFix: ['Verify SERPER_API_KEY is set correctly'],\n alternatives: [\n 'search-reddit(queries=[\"topic recommendations\", \"topic best practices\", \"topic vs alternatives\"]) \u2014 Reddit search uses the same API but may work; also provides community perspective',\n 'scrape-links(urls=[...any URLs you already have...], use_llm=true) \u2014 if you have URLs from prior steps, scrape them now instead of searching',\n ],\n });\n\n return toolFailure(\n `${errorContent}\\n\\nExecution time: ${formatDuration(executionTime)}\\nKeywords: ${params.keywords.length}`,\n );\n}\n\nexport async function handleWebSearch(\n params: WebSearchParams,\n reporter: ToolReporter = NOOP_REPORTER,\n): Promise<ToolExecutionResult<WebSearchOutput>> {\n const startTime = Date.now();\n\n try {\n mcpLog('info', `Searching for ${params.keywords.length} keyword(s)`, 'search');\n await reporter.log('info', `Searching for ${params.keywords.length} keyword(s)`);\n await reporter.progress(15, 100, 'Submitting search queries');\n\n const response = await executeSearches(params.keywords);\n await reporter.progress(50, 100, 'Collected search results');\n\n const { aggregation, urlLookup, consensusUrls } = processAndRankResults(response);\n await reporter.log(\n 'info',\n `Collected ${aggregation.totalUniqueUrls} unique URLs across ${response.totalKeywords} queries`,\n );\n\n const consensusSection = buildConsensusSection(params.keywords, aggregation);\n const { markdown: perQuerySection, totalResults } = buildPerQuerySection(response, urlLookup);\n await reporter.progress(80, 100, 'Ranking and formatting search results');\n\n const executionTime = Date.now() - startTime;\n mcpLog('info', `Search completed: ${totalResults} results, ${aggregation.totalUniqueUrls} unique URLs, ${consensusUrls.length} consensus`, 'search');\n await reporter.log(\n 'info',\n `Search completed with ${totalResults} ranked results and ${consensusUrls.length} consensus URL(s)`,\n );\n\n return formatSearchOutput(\n consensusSection, perQuerySection, totalResults,\n aggregation, consensusUrls.length, executionTime, response.totalKeywords,\n );\n } catch (error) {\n return buildWebSearchError(error, params, startTime);\n }\n}\n\nexport function registerWebSearchTool(server: MCPServer): void {\n server.tool(\n {\n name: 'web-search',\n title: 'Web Search',\n description:\n 'Run parallel Google searches across 1\u2013100 keywords and return CTR-weighted, consensus-ranked URLs for follow-up scraping. This is a bulk discovery tool \u2014 supply 3\u20137 keywords for solid consensus detection, or up to 100 for exhaustive coverage. Each keyword runs as a separate Google search; results are aggregated, scored by search position, and URLs appearing across multiple queries are flagged as high-confidence. Output is a ranked URL list ready to pipe into scrape-links or get-reddit-post.',\n schema: webSearchParamsSchema,\n outputSchema: webSearchOutputSchema,\n annotations: {\n readOnlyHint: true,\n idempotentHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (args, ctx) => {\n if (!getCapabilities().search) {\n return toToolResponse(toolFailure(getMissingEnvMessage('search')));\n }\n\n const reporter = createToolReporter(ctx, 'web-search');\n const result = await handleWebSearch(args, reporter);\n\n await reporter.progress(100, 100, result.isError ? 'Search failed' : 'Search complete');\n return toToolResponse(result);\n },\n );\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Web Search Tool Handler\n * NEVER throws - always returns structured response for graceful degradation\n */\n\nimport type { MCPServer } from 'mcp-use/server';\n\nimport { getCapabilities, getMissingEnvMessage } from '../config/index.js';\nimport {\n webSearchOutputSchema,\n webSearchParamsSchema,\n type WebSearchParams,\n type WebSearchOutput,\n} from '../schemas/web-search.js';\nimport { SearchClient } from '../clients/search.js';\nimport {\n aggregateAndRank,\n generateUnifiedOutput,\n} from '../utils/url-aggregator.js';\nimport { classifyError } from '../utils/errors.js';\nimport {\n mcpLog,\n formatError,\n formatDuration,\n} from './utils.js';\nimport {\n createToolReporter,\n NOOP_REPORTER,\n toolFailure,\n toolSuccess,\n toToolResponse,\n type ToolExecutionResult,\n type ToolReporter,\n} from './mcp-helpers.js';\n\n// --- Internal types ---\n\ninterface SearchAggregation {\n readonly rankedUrls: ReturnType<typeof aggregateAndRank>['rankedUrls'];\n readonly totalUniqueUrls: number;\n readonly frequencyThreshold: number;\n readonly thresholdNote?: string;\n}\n\ninterface SearchResponse {\n searches: Parameters<typeof aggregateAndRank>[0];\n totalKeywords: number;\n}\n\n// --- Helpers ---\n\nasync function executeSearches(keywords: string[]): Promise<SearchResponse> {\n const client = new SearchClient();\n return client.searchMultiple(keywords);\n}\n\nfunction processResults(response: SearchResponse): {\n aggregation: SearchAggregation;\n consensusUrls: SearchAggregation['rankedUrls'];\n} {\n const aggregation = aggregateAndRank(response.searches, 5);\n const consensusUrls = aggregation.rankedUrls.filter(u => u.isConsensus);\n return { aggregation, consensusUrls };\n}\n\nfunction buildOutputMarkdown(\n keywords: string[],\n aggregation: SearchAggregation,\n searches: SearchResponse['searches'],\n): string {\n return generateUnifiedOutput(\n aggregation.rankedUrls, keywords, searches,\n aggregation.totalUniqueUrls,\n aggregation.frequencyThreshold, aggregation.thresholdNote,\n );\n}\n\nfunction formatSearchOutput(\n outputMarkdown: string,\n aggregation: SearchAggregation,\n consensusUrlCount: number,\n executionTime: number,\n totalKeywords: number,\n searches: SearchResponse['searches'],\n): ToolExecutionResult<WebSearchOutput> {\n const markdown = outputMarkdown + `\\n---\\n*${formatDuration(executionTime)} | ${aggregation.totalUniqueUrls} unique URLs | ${consensusUrlCount} consensus | threshold \u2265${aggregation.frequencyThreshold}*`;\n\n const coverageSummary = searches.map(s => {\n let topDomain: string | undefined;\n const topResult = s.results[0];\n if (topResult) {\n try { topDomain = new URL(topResult.link).hostname.replace(/^www\\./, ''); } catch { /* ignore */ }\n }\n return { keyword: s.keyword, result_count: s.results.length, top_url: topDomain };\n });\n const lowYieldKeywords = searches\n .filter(s => s.results.length <= 1)\n .map(s => s.keyword);\n\n const metadata = {\n total_keywords: totalKeywords,\n total_results: aggregation.rankedUrls.length,\n execution_time_ms: executionTime,\n total_unique_urls: aggregation.totalUniqueUrls,\n consensus_url_count: consensusUrlCount,\n frequency_threshold: aggregation.frequencyThreshold,\n coverage_summary: coverageSummary,\n ...(lowYieldKeywords.length > 0 ? { low_yield_keywords: lowYieldKeywords } : {}),\n };\n\n return toolSuccess(markdown, { content: markdown, metadata });\n}\n\nfunction buildWebSearchError(\n error: unknown,\n params: WebSearchParams,\n startTime: number,\n): ToolExecutionResult<WebSearchOutput> {\n const structuredError = classifyError(error);\n const executionTime = Date.now() - startTime;\n\n mcpLog('error', `web-search: ${structuredError.message}`, 'search');\n\n const errorContent = formatError({\n code: structuredError.code,\n message: structuredError.message,\n retryable: structuredError.retryable,\n toolName: 'web-search',\n howToFix: ['Verify SERPER_API_KEY is set correctly'],\n alternatives: [\n 'search-reddit(queries=[\"topic recommendations\", \"topic best practices\", \"topic vs alternatives\"]) \u2014 Reddit search uses the same API but may work; also provides community perspective',\n 'scrape-links(urls=[...any URLs you already have...], use_llm=true) \u2014 if you have URLs from prior steps, scrape them now instead of searching',\n ],\n });\n\n return toolFailure(\n `${errorContent}\\n\\nExecution time: ${formatDuration(executionTime)}\\nKeywords: ${params.keywords.length}`,\n );\n}\n\nexport async function handleWebSearch(\n params: WebSearchParams,\n reporter: ToolReporter = NOOP_REPORTER,\n): Promise<ToolExecutionResult<WebSearchOutput>> {\n const startTime = Date.now();\n\n try {\n mcpLog('info', `Searching for ${params.keywords.length} keyword(s)`, 'search');\n await reporter.log('info', `Searching for ${params.keywords.length} keyword(s)`);\n await reporter.progress(15, 100, 'Submitting search queries');\n\n const response = await executeSearches(params.keywords);\n await reporter.progress(50, 100, 'Collected search results');\n\n const { aggregation, consensusUrls } = processResults(response);\n await reporter.log(\n 'info',\n `Collected ${aggregation.totalUniqueUrls} unique URLs across ${response.totalKeywords} queries`,\n );\n\n const outputMarkdown = buildOutputMarkdown(params.keywords, aggregation, response.searches);\n await reporter.progress(80, 100, 'Ranking and formatting search results');\n\n const executionTime = Date.now() - startTime;\n mcpLog('info', `Search completed: ${aggregation.rankedUrls.length} unique URLs, ${consensusUrls.length} consensus`, 'search');\n await reporter.log(\n 'info',\n `Search completed with ${aggregation.rankedUrls.length} ranked URLs and ${consensusUrls.length} consensus`,\n );\n\n return formatSearchOutput(\n outputMarkdown, aggregation, consensusUrls.length, executionTime, response.totalKeywords, response.searches,\n );\n } catch (error) {\n return buildWebSearchError(error, params, startTime);\n }\n}\n\nexport function registerWebSearchTool(server: MCPServer): void {\n server.tool(\n {\n name: 'web-search',\n title: 'Web Search',\n description:\n 'Run parallel Google searches across 1\u2013100 keywords and return CTR-weighted, consensus-ranked URLs for follow-up scraping. This is a bulk discovery tool \u2014 supply 3\u20137 keywords for solid consensus detection, or up to 100 for exhaustive coverage. Each keyword runs as a separate Google search; results are aggregated, scored by search position, and URLs appearing across multiple queries are flagged as high-confidence. Output is a ranked URL list ready to pipe into scrape-links or get-reddit-post.',\n schema: webSearchParamsSchema,\n outputSchema: webSearchOutputSchema,\n annotations: {\n readOnlyHint: true,\n idempotentHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (args, ctx) => {\n if (!getCapabilities().search) {\n return toToolResponse(toolFailure(getMissingEnvMessage('search')));\n }\n\n const reporter = createToolReporter(ctx, 'web-search');\n const result = await handleWebSearch(args, reporter);\n\n await reporter.progress(100, 100, result.isError ? 'Search failed' : 'Search complete');\n return toToolResponse(result);\n },\n );\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,6 +7,19 @@ const BEYOND_TOP10_BASE = 10;
7
7
  const DEFAULT_MIN_CONSENSUS_URLS = 5;
8
8
  const DEFAULT_REDDIT_MIN_CONSENSUS_URLS = 3;
9
9
  const HIGH_CONSENSUS_THRESHOLD = 4;
10
+ const MAX_ALT_SNIPPETS = 3;
11
+ const MAX_CONSISTENCY_PENALTY = 0.15;
12
+ const CONSISTENCY_STDDEV_SCALE = 5;
13
+ function computePositionStats(positions) {
14
+ if (positions.length <= 1) {
15
+ return { mean: positions[0] ?? 0, stdDev: 0, consistencyMultiplier: 1 };
16
+ }
17
+ const mean = positions.reduce((a, b) => a + b, 0) / positions.length;
18
+ const variance = positions.reduce((sum, p) => sum + (p - mean) ** 2, 0) / (positions.length - 1);
19
+ const stdDev = Math.sqrt(variance);
20
+ const consistencyMultiplier = 1 - MAX_CONSISTENCY_PENALTY * Math.min(stdDev / CONSISTENCY_STDDEV_SCALE, 1);
21
+ return { mean, stdDev, consistencyMultiplier };
22
+ }
10
23
  function getCtrWeight(position) {
11
24
  if (position >= 1 && position <= 10) {
12
25
  return CTR_WEIGHTS[position] ?? 0;
@@ -26,6 +39,9 @@ function aggregateResults(searches) {
26
39
  const prevBest = existing.bestPosition;
27
40
  existing.bestPosition = Math.min(existing.bestPosition, result.position);
28
41
  existing.totalScore += getCtrWeight(result.position);
42
+ if (result.snippet && existing.allSnippets.length < MAX_ALT_SNIPPETS && !existing.allSnippets.some((s) => s === result.snippet)) {
43
+ existing.allSnippets.push(result.snippet);
44
+ }
29
45
  if (result.position < prevBest) {
30
46
  existing.title = result.title;
31
47
  existing.snippet = result.snippet;
@@ -35,6 +51,7 @@ function aggregateResults(searches) {
35
51
  url: result.link,
36
52
  title: result.title,
37
53
  snippet: result.snippet,
54
+ allSnippets: result.snippet ? [result.snippet] : [],
38
55
  frequency: 1,
39
56
  positions: [result.position],
40
57
  queries: [search.keyword],
@@ -63,76 +80,108 @@ function countByFrequency(urlMap, minFrequency) {
63
80
  }
64
81
  return count;
65
82
  }
66
- function calculateWeightedScores(urls, consensusThreshold) {
83
+ function calculateWeightedScores(urls, consensusThreshold, totalQueries) {
67
84
  if (urls.length === 0) return [];
68
- const sorted = [...urls].sort((a, b) => b.totalScore - a.totalScore);
69
- const maxScore = sorted[0].totalScore;
70
- return sorted.map((url, index) => ({
85
+ const scored = urls.map((url) => {
86
+ const stats = computePositionStats(url.positions);
87
+ const compositeScore = url.totalScore * stats.consistencyMultiplier;
88
+ return { url, compositeScore, stats };
89
+ });
90
+ scored.sort((a, b) => b.compositeScore - a.compositeScore);
91
+ const maxScore = scored[0].compositeScore;
92
+ return scored.map(({ url, compositeScore, stats }, index) => ({
71
93
  url: url.url,
72
94
  title: url.title,
73
95
  snippet: url.snippet,
96
+ allSnippets: url.allSnippets,
74
97
  rank: index + 1,
75
- score: maxScore > 0 ? url.totalScore / maxScore * 100 : 0,
98
+ score: maxScore > 0 ? compositeScore / maxScore * 100 : 0,
76
99
  frequency: url.frequency,
77
100
  positions: url.positions,
78
101
  queries: url.queries,
79
102
  bestPosition: url.bestPosition,
80
- isConsensus: url.frequency >= consensusThreshold
103
+ isConsensus: url.frequency >= consensusThreshold,
104
+ coverageRatio: totalQueries > 0 ? url.frequency / totalQueries : 0,
105
+ positionStdDev: stats.stdDev,
106
+ consistencyMultiplier: stats.consistencyMultiplier
81
107
  }));
82
108
  }
83
- function markConsensus(frequency) {
84
- return frequency >= WEB_CONSENSUS_THRESHOLD ? "\u2713" : "\u2717";
109
+ function markConsensus(frequency, threshold = WEB_CONSENSUS_THRESHOLD) {
110
+ return frequency >= threshold ? "CONSENSUS" : "";
85
111
  }
86
- function generateJustification(url, rank) {
87
- const parts = [];
88
- if (url.frequency >= HIGH_CONSENSUS_THRESHOLD) {
89
- parts.push(`Appeared in ${url.frequency} different searches showing strong cross-query relevance`);
90
- } else if (url.frequency >= WEB_CONSENSUS_THRESHOLD) {
91
- parts.push(`Found across ${url.frequency} searches indicating solid topical coverage`);
92
- } else {
93
- parts.push(`Appeared in ${url.frequency} search${url.frequency > 1 ? "es" : ""}`);
94
- }
95
- if (url.bestPosition === 1) {
96
- parts.push("ranked #1 in at least one search");
97
- } else if (url.bestPosition <= 3) {
98
- parts.push(`best position was top-3 (#${url.bestPosition})`);
99
- }
100
- return parts.join(", ") + ".";
112
+ const COVERAGE_TABLE_MAX_ROWS = 20;
113
+ function consistencyLabel(stdDev, frequency) {
114
+ if (frequency <= 1) return "n/a";
115
+ if (stdDev < 1.5) return "high";
116
+ if (stdDev < 3.5) return "medium";
117
+ return "variable";
101
118
  }
102
- function generateEnhancedOutput(rankedUrls, allKeywords, totalUniqueUrls, frequencyThreshold, thresholdNote) {
119
+ function generateUnifiedOutput(rankedUrls, allKeywords, keywordResults, totalUniqueUrls, frequencyThreshold, thresholdNote) {
103
120
  const lines = [];
104
121
  const consensusCount = rankedUrls.filter((u) => u.isConsensus).length;
105
- lines.push(`## Aggregated Search Results (${allKeywords.length} Queries \u2192 ${rankedUrls.length} Unique URLs)`);
106
- lines.push("");
107
- lines.push(`Based on ${allKeywords.length} distinct searches, we found **${rankedUrls.length} unique resources** (${consensusCount} appear in multiple queries).`);
122
+ lines.push(`## Web Search Results (${allKeywords.length} queries, ${totalUniqueUrls} unique URLs)`);
108
123
  lines.push("");
109
124
  if (thresholdNote) {
110
125
  lines.push(`> ${thresholdNote}`);
111
126
  lines.push("");
112
127
  }
113
- lines.push("### \u{1F947} Ranked Resources");
114
- lines.push("");
115
128
  for (const url of rankedUrls) {
116
- const highConsensus = url.frequency >= HIGH_CONSENSUS_THRESHOLD ? " \u2B50 HIGHEST CONSENSUS" : url.isConsensus ? " \u2713 CONSENSUS" : "";
117
- lines.push(`#### #${url.rank}: ${url.title} (Score: ${url.score.toFixed(1)})${highConsensus}`);
118
- const queriesList = url.queries.map((q) => `"${q}"`).join(", ");
119
- lines.push(`- **Appeared in:** ${url.frequency} queries (${queriesList})`);
120
- lines.push(`- **Best ranking:** Position ${url.bestPosition}`);
121
- lines.push(`- **Description:** ${url.snippet}`);
122
- lines.push(`- **Why it's #${url.rank}:** ${generateJustification(url, url.rank)}`);
123
- lines.push(`- **URL:** ${url.url}`);
129
+ const consensusTag = url.frequency >= HIGH_CONSENSUS_THRESHOLD ? " CONSENSUS+++" : url.isConsensus ? " CONSENSUS" : "";
130
+ const coveragePct = Math.round(url.coverageRatio * 100);
131
+ const consistency = consistencyLabel(url.positionStdDev, url.frequency);
132
+ lines.push(`**${url.rank}. [${url.title}](${url.url})**${consensusTag}`);
133
+ lines.push(`Score: ${url.score.toFixed(1)} | Seen in: ${url.frequency}/${allKeywords.length} queries (${coveragePct}%) | Best pos: #${url.bestPosition} | Consistency: ${consistency}`);
134
+ lines.push(`Queries: ${url.queries.map((q) => `"${q}"`).join(", ")}`);
135
+ lines.push(`> ${url.snippet}`);
136
+ if (url.allSnippets.length > 1) {
137
+ const alts = url.allSnippets.filter((s) => s !== url.snippet).slice(0, 3).map((s) => s.length > 100 ? s.slice(0, 97) + "..." : s);
138
+ if (alts.length > 0) {
139
+ lines.push(`Alt: ${alts.map((s) => `"${s}"`).join(" | ")}`);
140
+ }
141
+ }
124
142
  lines.push("");
125
143
  }
126
144
  lines.push("---");
127
- lines.push("");
128
- lines.push("### \u{1F4C8} Metadata");
129
- lines.push("");
130
- lines.push(`- **Total Queries:** ${allKeywords.length} (${allKeywords.join(", ")})`);
131
- const sortedByFreq = [...rankedUrls].sort((a, b) => b.frequency - a.frequency);
132
- const urlFreqList = sortedByFreq.map((u) => `${u.url} (${u.frequency}x)`).join(", ");
133
- lines.push(`- **Unique URLs Found:** ${totalUniqueUrls} \u2014 top by frequency: ${urlFreqList}`);
134
- lines.push(`- **Consensus Threshold:** \u2265${frequencyThreshold} appearances`);
135
- lines.push("");
145
+ if (allKeywords.length <= COVERAGE_TABLE_MAX_ROWS) {
146
+ lines.push("### Keyword Coverage");
147
+ lines.push("| Keyword | Results | Top URL | Top Pos |");
148
+ lines.push("|---------|---------|---------|---------|");
149
+ for (const search of keywordResults) {
150
+ const topResult = search.results[0];
151
+ let topDomain = "";
152
+ if (topResult) {
153
+ try {
154
+ topDomain = new URL(topResult.link).hostname.replace(/^www\./, "");
155
+ } catch {
156
+ topDomain = topResult.link;
157
+ }
158
+ }
159
+ lines.push(`| "${search.keyword}" | ${search.results.length} | ${topDomain || "\u2014"} | ${topResult ? `#${topResult.position}` : "\u2014"} |`);
160
+ }
161
+ lines.push("");
162
+ } else {
163
+ const goodCount = keywordResults.filter((s) => s.results.length >= 3).length;
164
+ lines.push(`### Keyword Coverage: ${goodCount}/${allKeywords.length} keywords returned 3+ results`);
165
+ lines.push("");
166
+ }
167
+ const lowYield = keywordResults.filter((s) => s.results.length <= 1);
168
+ if (lowYield.length > 0) {
169
+ lines.push(`**Low-yield keywords** (0-1 results): ${lowYield.map((s) => `\`${s.keyword}\``).join(", ")}`);
170
+ lines.push("");
171
+ }
172
+ const allRelated = /* @__PURE__ */ new Set();
173
+ for (const search of keywordResults) {
174
+ if (search.related) {
175
+ for (const r of search.related) {
176
+ allRelated.add(r);
177
+ }
178
+ }
179
+ }
180
+ if (allRelated.size > 0) {
181
+ const related = [...allRelated].slice(0, 10);
182
+ lines.push(`**Related searches:** ${related.map((r) => `\`${r}\``).join(", ")}`);
183
+ lines.push("");
184
+ }
136
185
  return lines.join("\n");
137
186
  }
138
187
  function aggregateAndRank(searches, minConsensusUrls = DEFAULT_MIN_CONSENSUS_URLS) {
@@ -153,7 +202,7 @@ function aggregateAndRank(searches, minConsensusUrls = DEFAULT_MIN_CONSENSUS_URL
153
202
  }
154
203
  }
155
204
  const allUrls = [...urlMap.values()];
156
- const rankedUrls = calculateWeightedScores(allUrls, usedThreshold);
205
+ const rankedUrls = calculateWeightedScores(allUrls, usedThreshold, totalQueries);
157
206
  return {
158
207
  rankedUrls,
159
208
  totalUniqueUrls,
@@ -349,8 +398,8 @@ export {
349
398
  aggregateAndRank,
350
399
  aggregateAndRankReddit,
351
400
  buildUrlLookup,
352
- generateEnhancedOutput,
353
401
  generateRedditEnhancedOutput,
402
+ generateUnifiedOutput,
354
403
  lookupUrl,
355
404
  markConsensus
356
405
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/utils/url-aggregator.ts"],
4
- "sourcesContent": ["/**\n * URL Aggregator Utility\n * Aggregates search results across multiple queries, calculates CTR-weighted scores,\n * and generates consensus-based rankings.\n */\n\nimport { CTR_WEIGHTS } from '../config/index.js';\nimport type { KeywordSearchResult, RedditSearchResult } from '../clients/search.js';\n\n/** Minimum frequency for web search consensus marking */\nconst WEB_CONSENSUS_THRESHOLD = 3 as const;\n\n/** Minimum frequency for Reddit consensus marking (lower due to fewer overlapping results) */\nconst REDDIT_CONSENSUS_THRESHOLD = 2 as const;\n\n/** Minimum weight assigned to positions beyond top 10 */\nconst MIN_BEYOND_TOP10_WEIGHT = 0 as const;\n\n/** Weight decay per position beyond top 10 */\nconst BEYOND_TOP10_DECAY = 0.5 as const;\n\n/** Base position for beyond-top-10 weight calculation */\nconst BEYOND_TOP10_BASE = 10 as const;\n\n/** Default minimum consensus URLs before lowering threshold (web search) */\nconst DEFAULT_MIN_CONSENSUS_URLS = 5 as const;\n\n/** Default minimum consensus URLs before lowering threshold (Reddit) */\nconst DEFAULT_REDDIT_MIN_CONSENSUS_URLS = 3 as const;\n\n/** High consensus frequency threshold for enhanced output labeling */\nconst HIGH_CONSENSUS_THRESHOLD = 4 as const;\n\n/**\n * Aggregated URL data structure\n */\ninterface AggregatedUrl {\n readonly url: string;\n title: string;\n snippet: string;\n frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n bestPosition: number;\n totalScore: number;\n}\n\n/**\n * Ranked URL with normalized score\n */\ninterface RankedUrl {\n readonly url: string;\n readonly title: string;\n readonly snippet: string;\n readonly rank: number;\n readonly score: number;\n readonly frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n readonly bestPosition: number;\n readonly isConsensus: boolean;\n}\n\n/**\n * Aggregation result containing all processed data\n */\ninterface AggregationResult {\n readonly rankedUrls: RankedUrl[];\n readonly totalUniqueUrls: number;\n readonly totalQueries: number;\n readonly frequencyThreshold: number;\n readonly thresholdNote?: string;\n}\n\n/**\n * Get CTR weight for a position (1-10)\n * Positions beyond 10 get minimal weight\n */\nfunction getCtrWeight(position: number): number {\n if (position >= 1 && position <= 10) {\n return CTR_WEIGHTS[position] ?? 0;\n }\n // Positions beyond 10 get diminishing returns\n return Math.max(MIN_BEYOND_TOP10_WEIGHT, BEYOND_TOP10_BASE - (position - BEYOND_TOP10_BASE) * BEYOND_TOP10_DECAY);\n}\n\n/**\n * Aggregate results from multiple searches\n * Flattens all results, deduplicates by URL, and tracks frequency/positions\n */\nfunction aggregateResults(searches: KeywordSearchResult[]): Map<string, AggregatedUrl> {\n const urlMap = new Map<string, AggregatedUrl>();\n\n for (const search of searches) {\n for (const result of search.results) {\n const normalizedUrl = normalizeUrl(result.link);\n const existing = urlMap.get(normalizedUrl);\n\n if (existing) {\n existing.frequency += 1;\n existing.positions.push(result.position);\n existing.queries.push(search.keyword);\n const prevBest = existing.bestPosition;\n existing.bestPosition = Math.min(existing.bestPosition, result.position);\n existing.totalScore += getCtrWeight(result.position);\n // Keep best title/snippet (from highest ranking position)\n if (result.position < prevBest) {\n existing.title = result.title;\n existing.snippet = result.snippet;\n }\n } else {\n urlMap.set(normalizedUrl, {\n url: result.link,\n title: result.title,\n snippet: result.snippet,\n frequency: 1,\n positions: [result.position],\n queries: [search.keyword],\n bestPosition: result.position,\n totalScore: getCtrWeight(result.position),\n });\n }\n }\n }\n\n return urlMap;\n}\n\n/**\n * Normalize URL for deduplication\n * Removes trailing slashes, www prefix, and normalizes protocol\n */\nfunction normalizeUrl(url: string): string {\n try {\n const parsed = new URL(url);\n let host = parsed.hostname.replace(/^www\\./, '');\n let path = parsed.pathname.replace(/\\/$/, '') || '/';\n return `${host}${path}${parsed.search}`.toLowerCase();\n } catch {\n return url.toLowerCase().replace(/\\/$/, '');\n }\n}\n\n/**\n * Count URLs meeting a frequency threshold\n */\nfunction countByFrequency(\n urlMap: Map<string, AggregatedUrl>,\n minFrequency: number\n): number {\n let count = 0;\n for (const url of urlMap.values()) {\n if (url.frequency >= minFrequency) count++;\n }\n return count;\n}\n\n/**\n * Calculate weighted scores and normalize to 100.0\n * Returns ALL URLs sorted by score with rank assignments and consensus marking\n */\nfunction calculateWeightedScores(urls: AggregatedUrl[], consensusThreshold: number): RankedUrl[] {\n if (urls.length === 0) return [];\n\n // Sort by total score descending\n const sorted = [...urls].sort((a, b) => b.totalScore - a.totalScore);\n\n // Find max score for normalization\n const maxScore = sorted[0]!.totalScore;\n\n // Map to ranked URLs with normalized scores\n return sorted.map((url, index) => ({\n url: url.url,\n title: url.title,\n snippet: url.snippet,\n rank: index + 1,\n score: maxScore > 0 ? (url.totalScore / maxScore) * 100 : 0,\n frequency: url.frequency,\n positions: url.positions,\n queries: url.queries,\n bestPosition: url.bestPosition,\n isConsensus: url.frequency >= consensusThreshold,\n }));\n}\n\n/**\n * Mark consensus status for a URL\n * Returns \"\u2713\" if frequency >= threshold, else \"\u2717\"\n */\nexport function markConsensus(frequency: number): string {\n return frequency >= WEB_CONSENSUS_THRESHOLD ? '\u2713' : '\u2717';\n}\n\n/**\n * Generate justification for why a URL is ranked at its position\n */\nfunction generateJustification(url: RankedUrl, rank: number): string {\n const parts: string[] = [];\n \n if (url.frequency >= HIGH_CONSENSUS_THRESHOLD) {\n parts.push(`Appeared in ${url.frequency} different searches showing strong cross-query relevance`);\n } else if (url.frequency >= WEB_CONSENSUS_THRESHOLD) {\n parts.push(`Found across ${url.frequency} searches indicating solid topical coverage`);\n } else {\n parts.push(`Appeared in ${url.frequency} search${url.frequency > 1 ? 'es' : ''}`);\n }\n \n if (url.bestPosition === 1) {\n parts.push('ranked #1 in at least one search');\n } else if (url.bestPosition <= 3) {\n parts.push(`best position was top-3 (#${url.bestPosition})`);\n }\n \n return parts.join(', ') + '.';\n}\n\n/**\n * Generate enhanced narrative output for consensus URLs\n */\nexport function generateEnhancedOutput(\n rankedUrls: RankedUrl[],\n allKeywords: string[],\n totalUniqueUrls: number,\n frequencyThreshold: number,\n thresholdNote?: string\n): string {\n const lines: string[] = [];\n \n // Header\n const consensusCount = rankedUrls.filter(u => u.isConsensus).length;\n lines.push(`## Aggregated Search Results (${allKeywords.length} Queries \u2192 ${rankedUrls.length} Unique URLs)`);\n lines.push('');\n lines.push(`Based on ${allKeywords.length} distinct searches, we found **${rankedUrls.length} unique resources** (${consensusCount} appear in multiple queries).`);\n lines.push('');\n\n if (thresholdNote) {\n lines.push(`> ${thresholdNote}`);\n lines.push('');\n }\n\n // All ranked resources\n lines.push('### \uD83E\uDD47 Ranked Resources');\n lines.push('');\n\n for (const url of rankedUrls) {\n const highConsensus = url.frequency >= HIGH_CONSENSUS_THRESHOLD ? ' \u2B50 HIGHEST CONSENSUS' : url.isConsensus ? ' \u2713 CONSENSUS' : '';\n lines.push(`#### #${url.rank}: ${url.title} (Score: ${url.score.toFixed(1)})${highConsensus}`);\n \n // Appeared in queries\n const queriesList = url.queries.map(q => `\"${q}\"`).join(', ');\n lines.push(`- **Appeared in:** ${url.frequency} queries (${queriesList})`);\n \n // Best ranking\n lines.push(`- **Best ranking:** Position ${url.bestPosition}`);\n \n // Description\n lines.push(`- **Description:** ${url.snippet}`);\n \n // Justification\n lines.push(`- **Why it's #${url.rank}:** ${generateJustification(url, url.rank)}`);\n \n // URL\n lines.push(`- **URL:** ${url.url}`);\n lines.push('');\n }\n \n // Metadata section\n lines.push('---');\n lines.push('');\n lines.push('### \uD83D\uDCC8 Metadata');\n lines.push('');\n lines.push(`- **Total Queries:** ${allKeywords.length} (${allKeywords.join(', ')})`);\n \n // Sort all URLs by frequency for the unique URLs list\n const sortedByFreq = [...rankedUrls].sort((a, b) => b.frequency - a.frequency);\n const urlFreqList = sortedByFreq\n .map(u => `${u.url} (${u.frequency}x)`)\n .join(', ');\n \n lines.push(`- **Unique URLs Found:** ${totalUniqueUrls} \u2014 top by frequency: ${urlFreqList}`);\n lines.push(`- **Consensus Threshold:** \u2265${frequencyThreshold} appearances`);\n lines.push('');\n\n return lines.join('\\n');\n}\n\n/**\n * Full aggregation pipeline \u2014 returns ALL URLs ranked by CTR score.\n * Determines a consensus threshold (\u22653, \u22652, or \u22651) for labeling, but never\n * drops URLs below the threshold. Every collected URL appears in the output.\n */\nexport function aggregateAndRank(\n searches: KeywordSearchResult[],\n minConsensusUrls: number = DEFAULT_MIN_CONSENSUS_URLS\n): AggregationResult {\n const urlMap = aggregateResults(searches);\n const totalUniqueUrls = urlMap.size;\n const totalQueries = searches.length;\n\n // Determine consensus threshold for labeling (not filtering)\n const thresholds = [3, 2, 1];\n let usedThreshold = 1;\n let thresholdNote: string | undefined;\n\n for (const threshold of thresholds) {\n const count = countByFrequency(urlMap, threshold);\n if (count >= minConsensusUrls || threshold === 1) {\n usedThreshold = threshold;\n if (threshold < 3) {\n thresholdNote = `Note: Consensus threshold set to \u2265${threshold} due to result diversity.`;\n }\n break;\n }\n }\n\n // Rank ALL URLs, marking consensus based on determined threshold\n const allUrls = [...urlMap.values()];\n const rankedUrls = calculateWeightedScores(allUrls, usedThreshold);\n\n return {\n rankedUrls,\n totalUniqueUrls,\n totalQueries,\n frequencyThreshold: usedThreshold,\n thresholdNote,\n };\n}\n\n/**\n * Build URL lookup map for quick consensus checking during result formatting\n */\nexport function buildUrlLookup(rankedUrls: RankedUrl[]): Map<string, RankedUrl> {\n const lookup = new Map<string, RankedUrl>();\n \n for (const url of rankedUrls) {\n const normalized = normalizeUrl(url.url);\n lookup.set(normalized, url);\n // Also store original URL\n lookup.set(url.url.toLowerCase(), url);\n }\n\n return lookup;\n}\n\n/**\n * Look up a URL in the ranked results\n */\nexport function lookupUrl(url: string, lookup: Map<string, RankedUrl>): RankedUrl | undefined {\n const normalized = normalizeUrl(url);\n return lookup.get(normalized) || lookup.get(url.toLowerCase());\n}\n\n// ============================================================================\n// Reddit-Specific Aggregation\n// ============================================================================\n\n/**\n * Aggregated Reddit URL data structure\n */\ninterface AggregatedRedditUrl {\n readonly url: string;\n title: string;\n snippet: string;\n date?: string;\n frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n bestPosition: number;\n totalScore: number;\n}\n\n/**\n * Ranked Reddit URL with normalized score\n */\ninterface RankedRedditUrl {\n readonly url: string;\n readonly title: string;\n readonly snippet: string;\n readonly date?: string;\n readonly rank: number;\n readonly score: number;\n readonly frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n readonly bestPosition: number;\n readonly isConsensus: boolean;\n}\n\n/**\n * Reddit aggregation result\n */\ninterface RedditAggregationResult {\n readonly rankedUrls: RankedRedditUrl[];\n readonly totalUniqueUrls: number;\n readonly totalQueries: number;\n readonly frequencyThreshold: number;\n readonly thresholdNote?: string;\n}\n\n/**\n * Aggregate Reddit search results from multiple queries\n */\nfunction aggregateRedditResults(\n searches: Map<string, RedditSearchResult[]>\n): Map<string, AggregatedRedditUrl> {\n const urlMap = new Map<string, AggregatedRedditUrl>();\n\n for (const [query, results] of searches) {\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n if (!result) continue;\n const position = i + 1;\n const normalizedUrl = normalizeUrl(result.url);\n const existing = urlMap.get(normalizedUrl);\n\n if (existing) {\n existing.frequency += 1;\n existing.positions.push(position);\n existing.queries.push(query);\n const prevBest = existing.bestPosition;\n existing.bestPosition = Math.min(existing.bestPosition, position);\n existing.totalScore += getCtrWeight(position);\n // Keep best title/snippet (from highest ranking position)\n if (position < prevBest) {\n existing.title = result.title;\n existing.snippet = result.snippet;\n existing.date = result.date;\n }\n } else {\n urlMap.set(normalizedUrl, {\n url: result.url,\n title: result.title,\n snippet: result.snippet,\n date: result.date,\n frequency: 1,\n positions: [position],\n queries: [query],\n bestPosition: position,\n totalScore: getCtrWeight(position),\n });\n }\n }\n }\n\n return urlMap;\n}\n\n/**\n * Count Reddit URLs meeting a frequency threshold\n */\nfunction countRedditByFrequency(\n urlMap: Map<string, AggregatedRedditUrl>,\n minFrequency: number\n): number {\n let count = 0;\n for (const url of urlMap.values()) {\n if (url.frequency >= minFrequency) count++;\n }\n return count;\n}\n\n/**\n * Calculate weighted scores for Reddit URLs\n * Returns ALL URLs sorted by score with consensus marking\n */\nfunction calculateRedditWeightedScores(urls: AggregatedRedditUrl[], consensusThreshold: number): RankedRedditUrl[] {\n if (urls.length === 0) return [];\n\n // Sort by total score descending\n const sorted = [...urls].sort((a, b) => b.totalScore - a.totalScore);\n\n // Find max score for normalization\n const maxScore = sorted[0]!.totalScore;\n\n // Map to ranked URLs with normalized scores\n return sorted.map((url, index) => ({\n url: url.url,\n title: url.title,\n snippet: url.snippet,\n date: url.date,\n rank: index + 1,\n score: maxScore > 0 ? (url.totalScore / maxScore) * 100 : 0,\n frequency: url.frequency,\n positions: url.positions,\n queries: url.queries,\n bestPosition: url.bestPosition,\n isConsensus: url.frequency >= consensusThreshold,\n }));\n}\n\n/**\n * Full Reddit aggregation pipeline \u2014 returns ALL URLs ranked by CTR score.\n * Determines a consensus threshold for labeling, never drops URLs.\n */\nexport function aggregateAndRankReddit(\n searches: Map<string, RedditSearchResult[]>,\n minConsensusUrls: number = DEFAULT_REDDIT_MIN_CONSENSUS_URLS\n): RedditAggregationResult {\n const urlMap = aggregateRedditResults(searches);\n const totalUniqueUrls = urlMap.size;\n const totalQueries = searches.size;\n\n // Determine consensus threshold for labeling (not filtering)\n const thresholds = [2, 1];\n let usedThreshold = 1;\n let thresholdNote: string | undefined;\n\n for (const threshold of thresholds) {\n const count = countRedditByFrequency(urlMap, threshold);\n if (count >= minConsensusUrls || threshold === 1) {\n usedThreshold = threshold;\n if (threshold < 2 && totalQueries > 1) {\n thresholdNote = `Note: Consensus threshold set to \u2265${threshold} due to result diversity across queries.`;\n }\n break;\n }\n }\n\n // Rank ALL URLs, marking consensus based on determined threshold\n const allUrls = [...urlMap.values()];\n const rankedUrls = calculateRedditWeightedScores(allUrls, usedThreshold);\n\n return {\n rankedUrls,\n totalUniqueUrls,\n totalQueries,\n frequencyThreshold: usedThreshold,\n thresholdNote,\n };\n}\n\n/**\n * Generate enhanced output for Reddit aggregated results\n * Now includes both aggregated view AND per-query raw results\n */\nexport function generateRedditEnhancedOutput(\n aggregation: RedditAggregationResult,\n allQueries: string[],\n rawResults?: Map<string, RedditSearchResult[]>\n): string {\n const { rankedUrls, totalUniqueUrls, frequencyThreshold, thresholdNote } = aggregation;\n const lines: string[] = [];\n\n // Header\n lines.push(`# \uD83D\uDD0D Reddit Search Results (Aggregated from ${allQueries.length} Queries)`);\n lines.push('');\n lines.push(`**Total Unique Posts:** ${totalUniqueUrls} | **Consensus Threshold:** \u2265${frequencyThreshold} appearances`);\n lines.push('');\n\n if (thresholdNote) {\n lines.push(`> ${thresholdNote}`);\n lines.push('');\n }\n\n // Consensus section (URLs appearing in multiple queries)\n const consensusUrls = rankedUrls.filter(u => u.frequency >= frequencyThreshold && u.frequency > 1);\n if (consensusUrls.length > 0) {\n lines.push('## \u2B50 High-Consensus Posts (Multiple Queries)');\n lines.push('');\n lines.push('*These posts appeared across multiple search queries, indicating high relevance:*');\n lines.push('');\n\n for (const url of consensusUrls) {\n const dateStr = url.date ? ` \u2022 \uD83D\uDCC5 ${url.date}` : '';\n const queriesList = url.queries.map(q => `\"${q}\"`).join(', ');\n lines.push(`### #${url.rank}: ${url.title}`);\n lines.push(`**Score:** ${url.score.toFixed(1)} | **Found in:** ${url.frequency} queries (${queriesList})${dateStr}`);\n lines.push(`${url.url}`);\n lines.push(`> ${url.snippet}`);\n lines.push('');\n }\n\n lines.push('---');\n lines.push('');\n }\n\n // All results ranked by CTR score\n lines.push('## \uD83D\uDCCA All Results (CTR-Ranked)');\n lines.push('');\n\n for (const url of rankedUrls) {\n const dateStr = url.date ? ` \u2022 \uD83D\uDCC5 ${url.date}` : '';\n const consensusMarker = url.frequency > 1 ? ' \u2B50' : '';\n lines.push(`**${url.rank}. ${url.title}**${consensusMarker}${dateStr}`);\n lines.push(`${url.url}`);\n lines.push(`> ${url.snippet}`);\n if (url.frequency > 1) {\n lines.push(`_Found in ${url.frequency} queries: ${url.queries.map(q => `\"${q}\"`).join(', ')}_`);\n }\n lines.push('');\n }\n\n // Per-Query Raw Results Section (NEW)\n if (rawResults && rawResults.size > 0) {\n lines.push('---');\n lines.push('');\n lines.push('## \uD83D\uDCCB Per-Query Raw Results');\n lines.push('');\n lines.push('*Complete results for each individual query before aggregation:*');\n lines.push('');\n\n for (const [query, results] of rawResults) {\n lines.push(`### \uD83D\uDD0E Query: \"${query}\"`);\n lines.push(`**Results:** ${results.length} posts`);\n lines.push('');\n\n if (results.length === 0) {\n lines.push('_No results found for this query._');\n lines.push('');\n continue;\n }\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n if (!result) continue;\n const position = i + 1;\n const dateStr = result.date ? ` \u2022 \uD83D\uDCC5 ${result.date}` : '';\n lines.push(`${position}. **${result.title}**${dateStr}`);\n lines.push(` ${result.url}`);\n lines.push(` > ${result.snippet}`);\n lines.push('');\n }\n }\n }\n\n // Metadata\n lines.push('---');\n lines.push('');\n lines.push('### \uD83D\uDCC8 Search Metadata');\n lines.push('');\n lines.push(`- **Queries:** ${allQueries.map(q => `\"${q}\"`).join(', ')}`);\n lines.push(`- **Unique Posts Found:** ${totalUniqueUrls}`);\n lines.push(`- **High-Consensus Posts:** ${consensusUrls.length}`);\n lines.push('');\n\n return lines.join('\\n');\n}\n"],
5
- "mappings": "AAMA,SAAS,mBAAmB;AAI5B,MAAM,0BAA0B;AAGhC,MAAM,6BAA6B;AAGnC,MAAM,0BAA0B;AAGhC,MAAM,qBAAqB;AAG3B,MAAM,oBAAoB;AAG1B,MAAM,6BAA6B;AAGnC,MAAM,oCAAoC;AAG1C,MAAM,2BAA2B;AA+CjC,SAAS,aAAa,UAA0B;AAC9C,MAAI,YAAY,KAAK,YAAY,IAAI;AACnC,WAAO,YAAY,QAAQ,KAAK;AAAA,EAClC;AAEA,SAAO,KAAK,IAAI,yBAAyB,qBAAqB,WAAW,qBAAqB,kBAAkB;AAClH;AAMA,SAAS,iBAAiB,UAA6D;AACrF,QAAM,SAAS,oBAAI,IAA2B;AAE9C,aAAW,UAAU,UAAU;AAC7B,eAAW,UAAU,OAAO,SAAS;AACnC,YAAM,gBAAgB,aAAa,OAAO,IAAI;AAC9C,YAAM,WAAW,OAAO,IAAI,aAAa;AAEzC,UAAI,UAAU;AACZ,iBAAS,aAAa;AACtB,iBAAS,UAAU,KAAK,OAAO,QAAQ;AACvC,iBAAS,QAAQ,KAAK,OAAO,OAAO;AACpC,cAAM,WAAW,SAAS;AAC1B,iBAAS,eAAe,KAAK,IAAI,SAAS,cAAc,OAAO,QAAQ;AACvE,iBAAS,cAAc,aAAa,OAAO,QAAQ;AAEnD,YAAI,OAAO,WAAW,UAAU;AAC9B,mBAAS,QAAQ,OAAO;AACxB,mBAAS,UAAU,OAAO;AAAA,QAC5B;AAAA,MACF,OAAO;AACL,eAAO,IAAI,eAAe;AAAA,UACxB,KAAK,OAAO;AAAA,UACZ,OAAO,OAAO;AAAA,UACd,SAAS,OAAO;AAAA,UAChB,WAAW;AAAA,UACX,WAAW,CAAC,OAAO,QAAQ;AAAA,UAC3B,SAAS,CAAC,OAAO,OAAO;AAAA,UACxB,cAAc,OAAO;AAAA,UACrB,YAAY,aAAa,OAAO,QAAQ;AAAA,QAC1C,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAMA,SAAS,aAAa,KAAqB;AACzC,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,QAAI,OAAO,OAAO,SAAS,QAAQ,UAAU,EAAE;AAC/C,QAAI,OAAO,OAAO,SAAS,QAAQ,OAAO,EAAE,KAAK;AACjD,WAAO,GAAG,IAAI,GAAG,IAAI,GAAG,OAAO,MAAM,GAAG,YAAY;AAAA,EACtD,QAAQ;AACN,WAAO,IAAI,YAAY,EAAE,QAAQ,OAAO,EAAE;AAAA,EAC5C;AACF;AAKA,SAAS,iBACP,QACA,cACQ;AACR,MAAI,QAAQ;AACZ,aAAW,OAAO,OAAO,OAAO,GAAG;AACjC,QAAI,IAAI,aAAa,aAAc;AAAA,EACrC;AACA,SAAO;AACT;AAMA,SAAS,wBAAwB,MAAuB,oBAAyC;AAC/F,MAAI,KAAK,WAAW,EAAG,QAAO,CAAC;AAG/B,QAAM,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;AAGnE,QAAM,WAAW,OAAO,CAAC,EAAG;AAG5B,SAAO,OAAO,IAAI,CAAC,KAAK,WAAW;AAAA,IACjC,KAAK,IAAI;AAAA,IACT,OAAO,IAAI;AAAA,IACX,SAAS,IAAI;AAAA,IACb,MAAM,QAAQ;AAAA,IACd,OAAO,WAAW,IAAK,IAAI,aAAa,WAAY,MAAM;AAAA,IAC1D,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,SAAS,IAAI;AAAA,IACb,cAAc,IAAI;AAAA,IAClB,aAAa,IAAI,aAAa;AAAA,EAChC,EAAE;AACJ;AAMO,SAAS,cAAc,WAA2B;AACvD,SAAO,aAAa,0BAA0B,WAAM;AACtD;AAKA,SAAS,sBAAsB,KAAgB,MAAsB;AACnE,QAAM,QAAkB,CAAC;AAEzB,MAAI,IAAI,aAAa,0BAA0B;AAC7C,UAAM,KAAK,eAAe,IAAI,SAAS,0DAA0D;AAAA,EACnG,WAAW,IAAI,aAAa,yBAAyB;AACnD,UAAM,KAAK,gBAAgB,IAAI,SAAS,6CAA6C;AAAA,EACvF,OAAO;AACL,UAAM,KAAK,eAAe,IAAI,SAAS,UAAU,IAAI,YAAY,IAAI,OAAO,EAAE,EAAE;AAAA,EAClF;AAEA,MAAI,IAAI,iBAAiB,GAAG;AAC1B,UAAM,KAAK,kCAAkC;AAAA,EAC/C,WAAW,IAAI,gBAAgB,GAAG;AAChC,UAAM,KAAK,6BAA6B,IAAI,YAAY,GAAG;AAAA,EAC7D;AAEA,SAAO,MAAM,KAAK,IAAI,IAAI;AAC5B;AAKO,SAAS,uBACd,YACA,aACA,iBACA,oBACA,eACQ;AACR,QAAM,QAAkB,CAAC;AAGzB,QAAM,iBAAiB,WAAW,OAAO,OAAK,EAAE,WAAW,EAAE;AAC7D,QAAM,KAAK,iCAAiC,YAAY,MAAM,mBAAc,WAAW,MAAM,eAAe;AAC5G,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,YAAY,YAAY,MAAM,kCAAkC,WAAW,MAAM,wBAAwB,cAAc,+BAA+B;AACjK,QAAM,KAAK,EAAE;AAEb,MAAI,eAAe;AACjB,UAAM,KAAK,KAAK,aAAa,EAAE;AAC/B,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,QAAM,KAAK,gCAAyB;AACpC,QAAM,KAAK,EAAE;AAEb,aAAW,OAAO,YAAY;AAC5B,UAAM,gBAAgB,IAAI,aAAa,2BAA2B,8BAAyB,IAAI,cAAc,sBAAiB;AAC9H,UAAM,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,KAAK,YAAY,IAAI,MAAM,QAAQ,CAAC,CAAC,IAAI,aAAa,EAAE;AAG7F,UAAM,cAAc,IAAI,QAAQ,IAAI,OAAK,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI;AAC5D,UAAM,KAAK,sBAAsB,IAAI,SAAS,aAAa,WAAW,GAAG;AAGzE,UAAM,KAAK,gCAAgC,IAAI,YAAY,EAAE;AAG7D,UAAM,KAAK,sBAAsB,IAAI,OAAO,EAAE;AAG9C,UAAM,KAAK,iBAAiB,IAAI,IAAI,OAAO,sBAAsB,KAAK,IAAI,IAAI,CAAC,EAAE;AAGjF,UAAM,KAAK,cAAc,IAAI,GAAG,EAAE;AAClC,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,QAAM,KAAK,KAAK;AAChB,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,wBAAiB;AAC5B,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,wBAAwB,YAAY,MAAM,KAAK,YAAY,KAAK,IAAI,CAAC,GAAG;AAGnF,QAAM,eAAe,CAAC,GAAG,UAAU,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS;AAC7E,QAAM,cAAc,aACjB,IAAI,OAAK,GAAG,EAAE,GAAG,KAAK,EAAE,SAAS,IAAI,EACrC,KAAK,IAAI;AAEZ,QAAM,KAAK,4BAA4B,eAAe,6BAAwB,WAAW,EAAE;AAC3F,QAAM,KAAK,oCAA+B,kBAAkB,cAAc;AAC1E,QAAM,KAAK,EAAE;AAEb,SAAO,MAAM,KAAK,IAAI;AACxB;AAOO,SAAS,iBACd,UACA,mBAA2B,4BACR;AACnB,QAAM,SAAS,iBAAiB,QAAQ;AACxC,QAAM,kBAAkB,OAAO;AAC/B,QAAM,eAAe,SAAS;AAG9B,QAAM,aAAa,CAAC,GAAG,GAAG,CAAC;AAC3B,MAAI,gBAAgB;AACpB,MAAI;AAEJ,aAAW,aAAa,YAAY;AAClC,UAAM,QAAQ,iBAAiB,QAAQ,SAAS;AAChD,QAAI,SAAS,oBAAoB,cAAc,GAAG;AAChD,sBAAgB;AAChB,UAAI,YAAY,GAAG;AACjB,wBAAgB,0CAAqC,SAAS;AAAA,MAChE;AACA;AAAA,IACF;AAAA,EACF;AAGA,QAAM,UAAU,CAAC,GAAG,OAAO,OAAO,CAAC;AACnC,QAAM,aAAa,wBAAwB,SAAS,aAAa;AAEjE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,oBAAoB;AAAA,IACpB;AAAA,EACF;AACF;AAKO,SAAS,eAAe,YAAiD;AAC9E,QAAM,SAAS,oBAAI,IAAuB;AAE1C,aAAW,OAAO,YAAY;AAC5B,UAAM,aAAa,aAAa,IAAI,GAAG;AACvC,WAAO,IAAI,YAAY,GAAG;AAE1B,WAAO,IAAI,IAAI,IAAI,YAAY,GAAG,GAAG;AAAA,EACvC;AAEA,SAAO;AACT;AAKO,SAAS,UAAU,KAAa,QAAuD;AAC5F,QAAM,aAAa,aAAa,GAAG;AACnC,SAAO,OAAO,IAAI,UAAU,KAAK,OAAO,IAAI,IAAI,YAAY,CAAC;AAC/D;AAoDA,SAAS,uBACP,UACkC;AAClC,QAAM,SAAS,oBAAI,IAAiC;AAEpD,aAAW,CAAC,OAAO,OAAO,KAAK,UAAU;AACvC,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,YAAM,SAAS,QAAQ,CAAC;AACxB,UAAI,CAAC,OAAQ;AACb,YAAM,WAAW,IAAI;AACrB,YAAM,gBAAgB,aAAa,OAAO,GAAG;AAC7C,YAAM,WAAW,OAAO,IAAI,aAAa;AAEzC,UAAI,UAAU;AACZ,iBAAS,aAAa;AACtB,iBAAS,UAAU,KAAK,QAAQ;AAChC,iBAAS,QAAQ,KAAK,KAAK;AAC3B,cAAM,WAAW,SAAS;AAC1B,iBAAS,eAAe,KAAK,IAAI,SAAS,cAAc,QAAQ;AAChE,iBAAS,cAAc,aAAa,QAAQ;AAE5C,YAAI,WAAW,UAAU;AACvB,mBAAS,QAAQ,OAAO;AACxB,mBAAS,UAAU,OAAO;AAC1B,mBAAS,OAAO,OAAO;AAAA,QACzB;AAAA,MACF,OAAO;AACL,eAAO,IAAI,eAAe;AAAA,UACxB,KAAK,OAAO;AAAA,UACZ,OAAO,OAAO;AAAA,UACd,SAAS,OAAO;AAAA,UAChB,MAAM,OAAO;AAAA,UACb,WAAW;AAAA,UACX,WAAW,CAAC,QAAQ;AAAA,UACpB,SAAS,CAAC,KAAK;AAAA,UACf,cAAc;AAAA,UACd,YAAY,aAAa,QAAQ;AAAA,QACnC,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAKA,SAAS,uBACP,QACA,cACQ;AACR,MAAI,QAAQ;AACZ,aAAW,OAAO,OAAO,OAAO,GAAG;AACjC,QAAI,IAAI,aAAa,aAAc;AAAA,EACrC;AACA,SAAO;AACT;AAMA,SAAS,8BAA8B,MAA6B,oBAA+C;AACjH,MAAI,KAAK,WAAW,EAAG,QAAO,CAAC;AAG/B,QAAM,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;AAGnE,QAAM,WAAW,OAAO,CAAC,EAAG;AAG5B,SAAO,OAAO,IAAI,CAAC,KAAK,WAAW;AAAA,IACjC,KAAK,IAAI;AAAA,IACT,OAAO,IAAI;AAAA,IACX,SAAS,IAAI;AAAA,IACb,MAAM,IAAI;AAAA,IACV,MAAM,QAAQ;AAAA,IACd,OAAO,WAAW,IAAK,IAAI,aAAa,WAAY,MAAM;AAAA,IAC1D,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,SAAS,IAAI;AAAA,IACb,cAAc,IAAI;AAAA,IAClB,aAAa,IAAI,aAAa;AAAA,EAChC,EAAE;AACJ;AAMO,SAAS,uBACd,UACA,mBAA2B,mCACF;AACzB,QAAM,SAAS,uBAAuB,QAAQ;AAC9C,QAAM,kBAAkB,OAAO;AAC/B,QAAM,eAAe,SAAS;AAG9B,QAAM,aAAa,CAAC,GAAG,CAAC;AACxB,MAAI,gBAAgB;AACpB,MAAI;AAEJ,aAAW,aAAa,YAAY;AAClC,UAAM,QAAQ,uBAAuB,QAAQ,SAAS;AACtD,QAAI,SAAS,oBAAoB,cAAc,GAAG;AAChD,sBAAgB;AAChB,UAAI,YAAY,KAAK,eAAe,GAAG;AACrC,wBAAgB,0CAAqC,SAAS;AAAA,MAChE;AACA;AAAA,IACF;AAAA,EACF;AAGA,QAAM,UAAU,CAAC,GAAG,OAAO,OAAO,CAAC;AACnC,QAAM,aAAa,8BAA8B,SAAS,aAAa;AAEvE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,oBAAoB;AAAA,IACpB;AAAA,EACF;AACF;AAMO,SAAS,6BACd,aACA,YACA,YACQ;AACR,QAAM,EAAE,YAAY,iBAAiB,oBAAoB,cAAc,IAAI;AAC3E,QAAM,QAAkB,CAAC;AAGzB,QAAM,KAAK,sDAA+C,WAAW,MAAM,WAAW;AACtF,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,2BAA2B,eAAe,qCAAgC,kBAAkB,cAAc;AACrH,QAAM,KAAK,EAAE;AAEb,MAAI,eAAe;AACjB,UAAM,KAAK,KAAK,aAAa,EAAE;AAC/B,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,QAAM,gBAAgB,WAAW,OAAO,OAAK,EAAE,aAAa,sBAAsB,EAAE,YAAY,CAAC;AACjG,MAAI,cAAc,SAAS,GAAG;AAC5B,UAAM,KAAK,mDAA8C;AACzD,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,mFAAmF;AAC9F,UAAM,KAAK,EAAE;AAEb,eAAW,OAAO,eAAe;AAC/B,YAAM,UAAU,IAAI,OAAO,qBAAS,IAAI,IAAI,KAAK;AACjD,YAAM,cAAc,IAAI,QAAQ,IAAI,OAAK,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI;AAC5D,YAAM,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,KAAK,EAAE;AAC3C,YAAM,KAAK,cAAc,IAAI,MAAM,QAAQ,CAAC,CAAC,oBAAoB,IAAI,SAAS,aAAa,WAAW,IAAI,OAAO,EAAE;AACnH,YAAM,KAAK,GAAG,IAAI,GAAG,EAAE;AACvB,YAAM,KAAK,KAAK,IAAI,OAAO,EAAE;AAC7B,YAAM,KAAK,EAAE;AAAA,IACf;AAEA,UAAM,KAAK,KAAK;AAChB,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,QAAM,KAAK,uCAAgC;AAC3C,QAAM,KAAK,EAAE;AAEb,aAAW,OAAO,YAAY;AAC5B,UAAM,UAAU,IAAI,OAAO,qBAAS,IAAI,IAAI,KAAK;AACjD,UAAM,kBAAkB,IAAI,YAAY,IAAI,YAAO;AACnD,UAAM,KAAK,KAAK,IAAI,IAAI,KAAK,IAAI,KAAK,KAAK,eAAe,GAAG,OAAO,EAAE;AACtE,UAAM,KAAK,GAAG,IAAI,GAAG,EAAE;AACvB,UAAM,KAAK,KAAK,IAAI,OAAO,EAAE;AAC7B,QAAI,IAAI,YAAY,GAAG;AACrB,YAAM,KAAK,aAAa,IAAI,SAAS,aAAa,IAAI,QAAQ,IAAI,OAAK,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC,GAAG;AAAA,IAChG;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,MAAI,cAAc,WAAW,OAAO,GAAG;AACrC,UAAM,KAAK,KAAK;AAChB,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,oCAA6B;AACxC,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,kEAAkE;AAC7E,UAAM,KAAK,EAAE;AAEb,eAAW,CAAC,OAAO,OAAO,KAAK,YAAY;AACzC,YAAM,KAAK,yBAAkB,KAAK,GAAG;AACrC,YAAM,KAAK,gBAAgB,QAAQ,MAAM,QAAQ;AACjD,YAAM,KAAK,EAAE;AAEb,UAAI,QAAQ,WAAW,GAAG;AACxB,cAAM,KAAK,oCAAoC;AAC/C,cAAM,KAAK,EAAE;AACb;AAAA,MACF;AAEA,eAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,cAAM,SAAS,QAAQ,CAAC;AACxB,YAAI,CAAC,OAAQ;AACb,cAAM,WAAW,IAAI;AACrB,cAAM,UAAU,OAAO,OAAO,qBAAS,OAAO,IAAI,KAAK;AACvD,cAAM,KAAK,GAAG,QAAQ,OAAO,OAAO,KAAK,KAAK,OAAO,EAAE;AACvD,cAAM,KAAK,MAAM,OAAO,GAAG,EAAE;AAC7B,cAAM,KAAK,QAAQ,OAAO,OAAO,EAAE;AACnC,cAAM,KAAK,EAAE;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAGA,QAAM,KAAK,KAAK;AAChB,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,+BAAwB;AACnC,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,kBAAkB,WAAW,IAAI,OAAK,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC,EAAE;AACvE,QAAM,KAAK,6BAA6B,eAAe,EAAE;AACzD,QAAM,KAAK,+BAA+B,cAAc,MAAM,EAAE;AAChE,QAAM,KAAK,EAAE;AAEb,SAAO,MAAM,KAAK,IAAI;AACxB;",
4
+ "sourcesContent": ["/**\n * URL Aggregator Utility\n * Aggregates search results across multiple queries, calculates CTR-weighted scores,\n * and generates consensus-based rankings.\n */\n\nimport { CTR_WEIGHTS } from '../config/index.js';\nimport type { KeywordSearchResult, RedditSearchResult } from '../clients/search.js';\n\n/** Minimum frequency for web search consensus marking */\nconst WEB_CONSENSUS_THRESHOLD = 3 as const;\n\n/** Minimum frequency for Reddit consensus marking (lower due to fewer overlapping results) */\nconst REDDIT_CONSENSUS_THRESHOLD = 2 as const;\n\n/** Minimum weight assigned to positions beyond top 10 */\nconst MIN_BEYOND_TOP10_WEIGHT = 0 as const;\n\n/** Weight decay per position beyond top 10 */\nconst BEYOND_TOP10_DECAY = 0.5 as const;\n\n/** Base position for beyond-top-10 weight calculation */\nconst BEYOND_TOP10_BASE = 10 as const;\n\n/** Default minimum consensus URLs before lowering threshold (web search) */\nconst DEFAULT_MIN_CONSENSUS_URLS = 5 as const;\n\n/** Default minimum consensus URLs before lowering threshold (Reddit) */\nconst DEFAULT_REDDIT_MIN_CONSENSUS_URLS = 3 as const;\n\n/** High consensus frequency threshold for enhanced output labeling */\nconst HIGH_CONSENSUS_THRESHOLD = 4 as const;\n\n/** Maximum number of alternative snippets to retain per URL */\nconst MAX_ALT_SNIPPETS = 3 as const;\n\n/** Consistency penalty cap \u2014 bounds the impact of position variance */\nconst MAX_CONSISTENCY_PENALTY = 0.15 as const;\n\n/** Standard deviation normalizer \u2014 stdDev of 5+ gets full penalty */\nconst CONSISTENCY_STDDEV_SCALE = 5 as const;\n\n/**\n * Aggregated URL data structure\n */\ninterface AggregatedUrl {\n readonly url: string;\n title: string;\n snippet: string;\n readonly allSnippets: string[];\n frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n bestPosition: number;\n totalScore: number;\n}\n\n/**\n * Compute position statistics for consistency scoring\n */\nfunction computePositionStats(positions: number[]): { mean: number; stdDev: number; consistencyMultiplier: number } {\n if (positions.length <= 1) {\n return { mean: positions[0] ?? 0, stdDev: 0, consistencyMultiplier: 1.0 };\n }\n const mean = positions.reduce((a, b) => a + b, 0) / positions.length;\n const variance = positions.reduce((sum, p) => sum + (p - mean) ** 2, 0) / (positions.length - 1);\n const stdDev = Math.sqrt(variance);\n const consistencyMultiplier = 1.0 - MAX_CONSISTENCY_PENALTY * Math.min(stdDev / CONSISTENCY_STDDEV_SCALE, 1.0);\n return { mean, stdDev, consistencyMultiplier };\n}\n\n/**\n * Ranked URL with normalized score and enriched signals\n */\ninterface RankedUrl {\n readonly url: string;\n readonly title: string;\n readonly snippet: string;\n readonly allSnippets: string[];\n readonly rank: number;\n readonly score: number;\n readonly frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n readonly bestPosition: number;\n readonly isConsensus: boolean;\n readonly coverageRatio: number;\n readonly positionStdDev: number;\n readonly consistencyMultiplier: number;\n}\n\n/**\n * Aggregation result containing all processed data\n */\ninterface AggregationResult {\n readonly rankedUrls: RankedUrl[];\n readonly totalUniqueUrls: number;\n readonly totalQueries: number;\n readonly frequencyThreshold: number;\n readonly thresholdNote?: string;\n}\n\n/**\n * Get CTR weight for a position (1-10)\n * Positions beyond 10 get minimal weight\n */\nfunction getCtrWeight(position: number): number {\n if (position >= 1 && position <= 10) {\n return CTR_WEIGHTS[position] ?? 0;\n }\n // Positions beyond 10 get diminishing returns\n return Math.max(MIN_BEYOND_TOP10_WEIGHT, BEYOND_TOP10_BASE - (position - BEYOND_TOP10_BASE) * BEYOND_TOP10_DECAY);\n}\n\n/**\n * Aggregate results from multiple searches\n * Flattens all results, deduplicates by URL, and tracks frequency/positions\n */\nfunction aggregateResults(searches: KeywordSearchResult[]): Map<string, AggregatedUrl> {\n const urlMap = new Map<string, AggregatedUrl>();\n\n for (const search of searches) {\n for (const result of search.results) {\n const normalizedUrl = normalizeUrl(result.link);\n const existing = urlMap.get(normalizedUrl);\n\n if (existing) {\n existing.frequency += 1;\n existing.positions.push(result.position);\n existing.queries.push(search.keyword);\n const prevBest = existing.bestPosition;\n existing.bestPosition = Math.min(existing.bestPosition, result.position);\n existing.totalScore += getCtrWeight(result.position);\n // Collect distinct snippets (up to MAX_ALT_SNIPPETS)\n if (\n result.snippet &&\n existing.allSnippets.length < MAX_ALT_SNIPPETS &&\n !existing.allSnippets.some(s => s === result.snippet)\n ) {\n existing.allSnippets.push(result.snippet);\n }\n // Keep best title/snippet (from highest ranking position)\n if (result.position < prevBest) {\n existing.title = result.title;\n existing.snippet = result.snippet;\n }\n } else {\n urlMap.set(normalizedUrl, {\n url: result.link,\n title: result.title,\n snippet: result.snippet,\n allSnippets: result.snippet ? [result.snippet] : [],\n frequency: 1,\n positions: [result.position],\n queries: [search.keyword],\n bestPosition: result.position,\n totalScore: getCtrWeight(result.position),\n });\n }\n }\n }\n\n return urlMap;\n}\n\n/**\n * Normalize URL for deduplication\n * Removes trailing slashes, www prefix, and normalizes protocol\n */\nfunction normalizeUrl(url: string): string {\n try {\n const parsed = new URL(url);\n let host = parsed.hostname.replace(/^www\\./, '');\n let path = parsed.pathname.replace(/\\/$/, '') || '/';\n return `${host}${path}${parsed.search}`.toLowerCase();\n } catch {\n return url.toLowerCase().replace(/\\/$/, '');\n }\n}\n\n/**\n * Count URLs meeting a frequency threshold\n */\nfunction countByFrequency(\n urlMap: Map<string, AggregatedUrl>,\n minFrequency: number\n): number {\n let count = 0;\n for (const url of urlMap.values()) {\n if (url.frequency >= minFrequency) count++;\n }\n return count;\n}\n\n/**\n * Calculate weighted scores with consistency multiplier, normalize to 100.0.\n * Returns ALL URLs sorted by composite score with rank assignments and consensus marking.\n */\nfunction calculateWeightedScores(urls: AggregatedUrl[], consensusThreshold: number, totalQueries: number): RankedUrl[] {\n if (urls.length === 0) return [];\n\n // Compute composite scores (base CTR \u00D7 consistency multiplier)\n const scored = urls.map(url => {\n const stats = computePositionStats(url.positions);\n const compositeScore = url.totalScore * stats.consistencyMultiplier;\n return { url, compositeScore, stats };\n });\n\n // Sort by composite score descending\n scored.sort((a, b) => b.compositeScore - a.compositeScore);\n\n // Find max for normalization\n const maxScore = scored[0]!.compositeScore;\n\n // Map to ranked URLs with all signals\n return scored.map(({ url, compositeScore, stats }, index) => ({\n url: url.url,\n title: url.title,\n snippet: url.snippet,\n allSnippets: url.allSnippets,\n rank: index + 1,\n score: maxScore > 0 ? (compositeScore / maxScore) * 100 : 0,\n frequency: url.frequency,\n positions: url.positions,\n queries: url.queries,\n bestPosition: url.bestPosition,\n isConsensus: url.frequency >= consensusThreshold,\n coverageRatio: totalQueries > 0 ? url.frequency / totalQueries : 0,\n positionStdDev: stats.stdDev,\n consistencyMultiplier: stats.consistencyMultiplier,\n }));\n}\n\n/**\n * Mark consensus status for a URL against a given threshold.\n * Returns \"CONSENSUS\" if frequency >= threshold, else empty string.\n */\nexport function markConsensus(frequency: number, threshold: number = WEB_CONSENSUS_THRESHOLD): string {\n return frequency >= threshold ? 'CONSENSUS' : '';\n}\n\n/** Maximum keywords to show in the coverage table before collapsing */\nconst COVERAGE_TABLE_MAX_ROWS = 20 as const;\n\n/**\n * Consistency label based on position standard deviation\n */\nfunction consistencyLabel(stdDev: number, frequency: number): string {\n if (frequency <= 1) return 'n/a';\n if (stdDev < 1.5) return 'high';\n if (stdDev < 3.5) return 'medium';\n return 'variable';\n}\n\n/**\n * Generate a unified output where every URL appears exactly once.\n * Replaces the old generateEnhancedOutput + per-query section combo.\n */\nexport function generateUnifiedOutput(\n rankedUrls: RankedUrl[],\n allKeywords: string[],\n keywordResults: KeywordSearchResult[],\n totalUniqueUrls: number,\n frequencyThreshold: number,\n thresholdNote?: string,\n): string {\n const lines: string[] = [];\n const consensusCount = rankedUrls.filter(u => u.isConsensus).length;\n\n // Header\n lines.push(`## Web Search Results (${allKeywords.length} queries, ${totalUniqueUrls} unique URLs)`);\n lines.push('');\n if (thresholdNote) {\n lines.push(`> ${thresholdNote}`);\n lines.push('');\n }\n\n // Ranked URL list \u2014 every URL exactly once\n for (const url of rankedUrls) {\n const consensusTag = url.frequency >= HIGH_CONSENSUS_THRESHOLD\n ? ' CONSENSUS+++'\n : url.isConsensus\n ? ' CONSENSUS'\n : '';\n const coveragePct = Math.round(url.coverageRatio * 100);\n const consistency = consistencyLabel(url.positionStdDev, url.frequency);\n\n lines.push(`**${url.rank}. [${url.title}](${url.url})**${consensusTag}`);\n lines.push(`Score: ${url.score.toFixed(1)} | Seen in: ${url.frequency}/${allKeywords.length} queries (${coveragePct}%) | Best pos: #${url.bestPosition} | Consistency: ${consistency}`);\n lines.push(`Queries: ${url.queries.map(q => `\"${q}\"`).join(', ')}`);\n lines.push(`> ${url.snippet}`);\n\n // Alt snippets (if multiple distinct snippets were collected)\n if (url.allSnippets.length > 1) {\n const alts = url.allSnippets\n .filter(s => s !== url.snippet)\n .slice(0, 3)\n .map(s => s.length > 100 ? s.slice(0, 97) + '...' : s);\n if (alts.length > 0) {\n lines.push(`Alt: ${alts.map(s => `\"${s}\"`).join(' | ')}`);\n }\n }\n\n lines.push('');\n }\n\n // Keyword coverage section\n lines.push('---');\n\n if (allKeywords.length <= COVERAGE_TABLE_MAX_ROWS) {\n // Full table for \u226420 keywords\n lines.push('### Keyword Coverage');\n lines.push('| Keyword | Results | Top URL | Top Pos |');\n lines.push('|---------|---------|---------|---------|');\n\n for (const search of keywordResults) {\n const topResult = search.results[0];\n let topDomain = '';\n if (topResult) {\n try {\n topDomain = new URL(topResult.link).hostname.replace(/^www\\./, '');\n } catch {\n topDomain = topResult.link;\n }\n }\n lines.push(`| \"${search.keyword}\" | ${search.results.length} | ${topDomain || '\u2014'} | ${topResult ? `#${topResult.position}` : '\u2014'} |`);\n }\n lines.push('');\n } else {\n // Collapsed summary for >20 keywords\n const goodCount = keywordResults.filter(s => s.results.length >= 3).length;\n lines.push(`### Keyword Coverage: ${goodCount}/${allKeywords.length} keywords returned 3+ results`);\n lines.push('');\n }\n\n // Low-yield keywords\n const lowYield = keywordResults.filter(s => s.results.length <= 1);\n if (lowYield.length > 0) {\n lines.push(`**Low-yield keywords** (0-1 results): ${lowYield.map(s => `\\`${s.keyword}\\``).join(', ')}`);\n lines.push('');\n }\n\n // Related searches (merged and deduplicated)\n const allRelated = new Set<string>();\n for (const search of keywordResults) {\n if (search.related) {\n for (const r of search.related) {\n allRelated.add(r);\n }\n }\n }\n if (allRelated.size > 0) {\n const related = [...allRelated].slice(0, 10);\n lines.push(`**Related searches:** ${related.map(r => `\\`${r}\\``).join(', ')}`);\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Full aggregation pipeline \u2014 returns ALL URLs ranked by CTR score.\n * Determines a consensus threshold (\u22653, \u22652, or \u22651) for labeling, but never\n * drops URLs below the threshold. Every collected URL appears in the output.\n */\nexport function aggregateAndRank(\n searches: KeywordSearchResult[],\n minConsensusUrls: number = DEFAULT_MIN_CONSENSUS_URLS\n): AggregationResult {\n const urlMap = aggregateResults(searches);\n const totalUniqueUrls = urlMap.size;\n const totalQueries = searches.length;\n\n // Determine consensus threshold for labeling (not filtering)\n const thresholds = [3, 2, 1];\n let usedThreshold = 1;\n let thresholdNote: string | undefined;\n\n for (const threshold of thresholds) {\n const count = countByFrequency(urlMap, threshold);\n if (count >= minConsensusUrls || threshold === 1) {\n usedThreshold = threshold;\n if (threshold < 3) {\n thresholdNote = `Note: Consensus threshold set to \u2265${threshold} due to result diversity.`;\n }\n break;\n }\n }\n\n // Rank ALL URLs, marking consensus based on determined threshold\n const allUrls = [...urlMap.values()];\n const rankedUrls = calculateWeightedScores(allUrls, usedThreshold, totalQueries);\n\n return {\n rankedUrls,\n totalUniqueUrls,\n totalQueries,\n frequencyThreshold: usedThreshold,\n thresholdNote,\n };\n}\n\n/**\n * Build URL lookup map for quick consensus checking during result formatting\n */\nexport function buildUrlLookup(rankedUrls: RankedUrl[]): Map<string, RankedUrl> {\n const lookup = new Map<string, RankedUrl>();\n \n for (const url of rankedUrls) {\n const normalized = normalizeUrl(url.url);\n lookup.set(normalized, url);\n // Also store original URL\n lookup.set(url.url.toLowerCase(), url);\n }\n\n return lookup;\n}\n\n/**\n * Look up a URL in the ranked results\n */\nexport function lookupUrl(url: string, lookup: Map<string, RankedUrl>): RankedUrl | undefined {\n const normalized = normalizeUrl(url);\n return lookup.get(normalized) || lookup.get(url.toLowerCase());\n}\n\n// ============================================================================\n// Reddit-Specific Aggregation\n// ============================================================================\n\n/**\n * Aggregated Reddit URL data structure\n */\ninterface AggregatedRedditUrl {\n readonly url: string;\n title: string;\n snippet: string;\n date?: string;\n frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n bestPosition: number;\n totalScore: number;\n}\n\n/**\n * Ranked Reddit URL with normalized score\n */\ninterface RankedRedditUrl {\n readonly url: string;\n readonly title: string;\n readonly snippet: string;\n readonly date?: string;\n readonly rank: number;\n readonly score: number;\n readonly frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n readonly bestPosition: number;\n readonly isConsensus: boolean;\n}\n\n/**\n * Reddit aggregation result\n */\ninterface RedditAggregationResult {\n readonly rankedUrls: RankedRedditUrl[];\n readonly totalUniqueUrls: number;\n readonly totalQueries: number;\n readonly frequencyThreshold: number;\n readonly thresholdNote?: string;\n}\n\n/**\n * Aggregate Reddit search results from multiple queries\n */\nfunction aggregateRedditResults(\n searches: Map<string, RedditSearchResult[]>\n): Map<string, AggregatedRedditUrl> {\n const urlMap = new Map<string, AggregatedRedditUrl>();\n\n for (const [query, results] of searches) {\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n if (!result) continue;\n const position = i + 1;\n const normalizedUrl = normalizeUrl(result.url);\n const existing = urlMap.get(normalizedUrl);\n\n if (existing) {\n existing.frequency += 1;\n existing.positions.push(position);\n existing.queries.push(query);\n const prevBest = existing.bestPosition;\n existing.bestPosition = Math.min(existing.bestPosition, position);\n existing.totalScore += getCtrWeight(position);\n // Keep best title/snippet (from highest ranking position)\n if (position < prevBest) {\n existing.title = result.title;\n existing.snippet = result.snippet;\n existing.date = result.date;\n }\n } else {\n urlMap.set(normalizedUrl, {\n url: result.url,\n title: result.title,\n snippet: result.snippet,\n date: result.date,\n frequency: 1,\n positions: [position],\n queries: [query],\n bestPosition: position,\n totalScore: getCtrWeight(position),\n });\n }\n }\n }\n\n return urlMap;\n}\n\n/**\n * Count Reddit URLs meeting a frequency threshold\n */\nfunction countRedditByFrequency(\n urlMap: Map<string, AggregatedRedditUrl>,\n minFrequency: number\n): number {\n let count = 0;\n for (const url of urlMap.values()) {\n if (url.frequency >= minFrequency) count++;\n }\n return count;\n}\n\n/**\n * Calculate weighted scores for Reddit URLs\n * Returns ALL URLs sorted by score with consensus marking\n */\nfunction calculateRedditWeightedScores(urls: AggregatedRedditUrl[], consensusThreshold: number): RankedRedditUrl[] {\n if (urls.length === 0) return [];\n\n // Sort by total score descending\n const sorted = [...urls].sort((a, b) => b.totalScore - a.totalScore);\n\n // Find max score for normalization\n const maxScore = sorted[0]!.totalScore;\n\n // Map to ranked URLs with normalized scores\n return sorted.map((url, index) => ({\n url: url.url,\n title: url.title,\n snippet: url.snippet,\n date: url.date,\n rank: index + 1,\n score: maxScore > 0 ? (url.totalScore / maxScore) * 100 : 0,\n frequency: url.frequency,\n positions: url.positions,\n queries: url.queries,\n bestPosition: url.bestPosition,\n isConsensus: url.frequency >= consensusThreshold,\n }));\n}\n\n/**\n * Full Reddit aggregation pipeline \u2014 returns ALL URLs ranked by CTR score.\n * Determines a consensus threshold for labeling, never drops URLs.\n */\nexport function aggregateAndRankReddit(\n searches: Map<string, RedditSearchResult[]>,\n minConsensusUrls: number = DEFAULT_REDDIT_MIN_CONSENSUS_URLS\n): RedditAggregationResult {\n const urlMap = aggregateRedditResults(searches);\n const totalUniqueUrls = urlMap.size;\n const totalQueries = searches.size;\n\n // Determine consensus threshold for labeling (not filtering)\n const thresholds = [2, 1];\n let usedThreshold = 1;\n let thresholdNote: string | undefined;\n\n for (const threshold of thresholds) {\n const count = countRedditByFrequency(urlMap, threshold);\n if (count >= minConsensusUrls || threshold === 1) {\n usedThreshold = threshold;\n if (threshold < 2 && totalQueries > 1) {\n thresholdNote = `Note: Consensus threshold set to \u2265${threshold} due to result diversity across queries.`;\n }\n break;\n }\n }\n\n // Rank ALL URLs, marking consensus based on determined threshold\n const allUrls = [...urlMap.values()];\n const rankedUrls = calculateRedditWeightedScores(allUrls, usedThreshold);\n\n return {\n rankedUrls,\n totalUniqueUrls,\n totalQueries,\n frequencyThreshold: usedThreshold,\n thresholdNote,\n };\n}\n\n/**\n * Generate enhanced output for Reddit aggregated results\n * Now includes both aggregated view AND per-query raw results\n */\nexport function generateRedditEnhancedOutput(\n aggregation: RedditAggregationResult,\n allQueries: string[],\n rawResults?: Map<string, RedditSearchResult[]>\n): string {\n const { rankedUrls, totalUniqueUrls, frequencyThreshold, thresholdNote } = aggregation;\n const lines: string[] = [];\n\n // Header\n lines.push(`# \uD83D\uDD0D Reddit Search Results (Aggregated from ${allQueries.length} Queries)`);\n lines.push('');\n lines.push(`**Total Unique Posts:** ${totalUniqueUrls} | **Consensus Threshold:** \u2265${frequencyThreshold} appearances`);\n lines.push('');\n\n if (thresholdNote) {\n lines.push(`> ${thresholdNote}`);\n lines.push('');\n }\n\n // Consensus section (URLs appearing in multiple queries)\n const consensusUrls = rankedUrls.filter(u => u.frequency >= frequencyThreshold && u.frequency > 1);\n if (consensusUrls.length > 0) {\n lines.push('## \u2B50 High-Consensus Posts (Multiple Queries)');\n lines.push('');\n lines.push('*These posts appeared across multiple search queries, indicating high relevance:*');\n lines.push('');\n\n for (const url of consensusUrls) {\n const dateStr = url.date ? ` \u2022 \uD83D\uDCC5 ${url.date}` : '';\n const queriesList = url.queries.map(q => `\"${q}\"`).join(', ');\n lines.push(`### #${url.rank}: ${url.title}`);\n lines.push(`**Score:** ${url.score.toFixed(1)} | **Found in:** ${url.frequency} queries (${queriesList})${dateStr}`);\n lines.push(`${url.url}`);\n lines.push(`> ${url.snippet}`);\n lines.push('');\n }\n\n lines.push('---');\n lines.push('');\n }\n\n // All results ranked by CTR score\n lines.push('## \uD83D\uDCCA All Results (CTR-Ranked)');\n lines.push('');\n\n for (const url of rankedUrls) {\n const dateStr = url.date ? ` \u2022 \uD83D\uDCC5 ${url.date}` : '';\n const consensusMarker = url.frequency > 1 ? ' \u2B50' : '';\n lines.push(`**${url.rank}. ${url.title}**${consensusMarker}${dateStr}`);\n lines.push(`${url.url}`);\n lines.push(`> ${url.snippet}`);\n if (url.frequency > 1) {\n lines.push(`_Found in ${url.frequency} queries: ${url.queries.map(q => `\"${q}\"`).join(', ')}_`);\n }\n lines.push('');\n }\n\n // Per-Query Raw Results Section (NEW)\n if (rawResults && rawResults.size > 0) {\n lines.push('---');\n lines.push('');\n lines.push('## \uD83D\uDCCB Per-Query Raw Results');\n lines.push('');\n lines.push('*Complete results for each individual query before aggregation:*');\n lines.push('');\n\n for (const [query, results] of rawResults) {\n lines.push(`### \uD83D\uDD0E Query: \"${query}\"`);\n lines.push(`**Results:** ${results.length} posts`);\n lines.push('');\n\n if (results.length === 0) {\n lines.push('_No results found for this query._');\n lines.push('');\n continue;\n }\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n if (!result) continue;\n const position = i + 1;\n const dateStr = result.date ? ` \u2022 \uD83D\uDCC5 ${result.date}` : '';\n lines.push(`${position}. **${result.title}**${dateStr}`);\n lines.push(` ${result.url}`);\n lines.push(` > ${result.snippet}`);\n lines.push('');\n }\n }\n }\n\n // Metadata\n lines.push('---');\n lines.push('');\n lines.push('### \uD83D\uDCC8 Search Metadata');\n lines.push('');\n lines.push(`- **Queries:** ${allQueries.map(q => `\"${q}\"`).join(', ')}`);\n lines.push(`- **Unique Posts Found:** ${totalUniqueUrls}`);\n lines.push(`- **High-Consensus Posts:** ${consensusUrls.length}`);\n lines.push('');\n\n return lines.join('\\n');\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-researchpowerpack-http",
3
- "version": "3.11.0",
3
+ "version": "3.11.1",
4
4
  "description": "The ultimate research MCP toolkit: Reddit mining, web search with CTR aggregation, and intelligent web scraping - all in one modular package",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",