@ncukondo/search-hub 0.18.0 → 0.20.0
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/cli/commands/register.d.ts +1 -1
- package/dist/cli/commands/register.js.map +1 -1
- package/dist/cli/commands/related.d.ts +66 -0
- package/dist/cli/commands/related.d.ts.map +1 -0
- package/dist/cli/commands/related.js +161 -0
- package/dist/cli/commands/related.js.map +1 -0
- package/dist/cli/commands/review/extract.d.ts.map +1 -1
- package/dist/cli/commands/review/extract.js +15 -5
- package/dist/cli/commands/review/extract.js.map +1 -1
- package/dist/cli/commands/review/finalize.js +6 -6
- package/dist/cli/commands/review/finalize.js.map +1 -1
- package/dist/cli/commands/review/init.d.ts +2 -3
- package/dist/cli/commands/review/init.d.ts.map +1 -1
- package/dist/cli/commands/review/init.js +1 -0
- package/dist/cli/commands/review/init.js.map +1 -1
- package/dist/cli/commands/review/list.d.ts +1 -1
- package/dist/cli/commands/review/list.d.ts.map +1 -1
- package/dist/cli/commands/review/list.js.map +1 -1
- package/dist/cli/commands/review/next-steps.d.ts +4 -1
- package/dist/cli/commands/review/next-steps.d.ts.map +1 -1
- package/dist/cli/commands/review/next-steps.js +53 -19
- package/dist/cli/commands/review/next-steps.js.map +1 -1
- package/dist/cli/commands/review/schema.d.ts +8 -0
- package/dist/cli/commands/review/schema.d.ts.map +1 -1
- package/dist/cli/commands/review/schema.js +3 -0
- package/dist/cli/commands/review/schema.js.map +1 -1
- package/dist/cli/commands/review/status.d.ts +5 -3
- package/dist/cli/commands/review/status.d.ts.map +1 -1
- package/dist/cli/commands/review/status.js +16 -14
- package/dist/cli/commands/review/status.js.map +1 -1
- package/dist/cli/commands/review/types.d.ts +7 -6
- package/dist/cli/commands/review/types.d.ts.map +1 -1
- package/dist/cli/commands/review/types.js +6 -9
- package/dist/cli/commands/review/types.js.map +1 -1
- package/dist/cli/commands/search-executor.d.ts.map +1 -1
- package/dist/cli/commands/search-executor.js +3 -2
- package/dist/cli/commands/search-executor.js.map +1 -1
- package/dist/cli/commands/search.d.ts +2 -0
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/search.js +3 -0
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +131 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/suggestions/rules.d.ts.map +1 -1
- package/dist/cli/suggestions/rules.js +19 -3
- package/dist/cli/suggestions/rules.js.map +1 -1
- package/dist/providers/arxiv/provider.d.ts.map +1 -1
- package/dist/providers/arxiv/provider.js +7 -4
- package/dist/providers/arxiv/provider.js.map +1 -1
- package/dist/providers/base/types.d.ts +8 -0
- package/dist/providers/base/types.d.ts.map +1 -1
- package/dist/providers/base/types.js.map +1 -1
- package/dist/providers/eric/provider.d.ts +3 -0
- package/dist/providers/eric/provider.d.ts.map +1 -1
- package/dist/providers/eric/provider.js +11 -0
- package/dist/providers/eric/provider.js.map +1 -1
- package/dist/providers/pubmed/client.d.ts +15 -1
- package/dist/providers/pubmed/client.d.ts.map +1 -1
- package/dist/providers/pubmed/client.js +64 -1
- package/dist/providers/pubmed/client.js.map +1 -1
- package/dist/providers/pubmed/index.d.ts +2 -2
- package/dist/providers/pubmed/index.d.ts.map +1 -1
- package/dist/providers/pubmed/parser.d.ts +8 -1
- package/dist/providers/pubmed/parser.d.ts.map +1 -1
- package/dist/providers/pubmed/parser.js +23 -1
- package/dist/providers/pubmed/parser.js.map +1 -1
- package/dist/providers/pubmed/provider.d.ts.map +1 -1
- package/dist/providers/pubmed/provider.js +8 -2
- package/dist/providers/pubmed/provider.js.map +1 -1
- package/dist/providers/pubmed/types.d.ts +29 -0
- package/dist/providers/pubmed/types.d.ts.map +1 -1
- package/dist/providers/scopus/client.d.ts +2 -0
- package/dist/providers/scopus/client.d.ts.map +1 -1
- package/dist/providers/scopus/client.js +3 -0
- package/dist/providers/scopus/client.js.map +1 -1
- package/dist/providers/scopus/provider.d.ts.map +1 -1
- package/dist/providers/scopus/provider.js +7 -1
- package/dist/providers/scopus/provider.js.map +1 -1
- package/dist/session/types.d.ts +13 -1
- package/dist/session/types.d.ts.map +1 -1
- package/dist/session/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/cli/suggestions/rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAkB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/cli/suggestions/rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAkB,MAAM,YAAY,CAAC;AAsetF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,IAAI,CAM7E"}
|
|
@@ -203,6 +203,18 @@ const mergeRule = (ctx) => {
|
|
|
203
203
|
seeAlso: []
|
|
204
204
|
};
|
|
205
205
|
};
|
|
206
|
+
const relatedRule = (ctx) => {
|
|
207
|
+
if (ctx.command !== "related") return null;
|
|
208
|
+
const sid = ctx.sessionId ?? "<session-id>";
|
|
209
|
+
return {
|
|
210
|
+
next: [
|
|
211
|
+
{ command: `search-hub results ${sid}`, description: "View related articles" },
|
|
212
|
+
{ command: `search-hub review init ${sid}`, description: "Screen related articles" },
|
|
213
|
+
{ command: `search-hub export ${sid}`, description: "Export results" }
|
|
214
|
+
],
|
|
215
|
+
seeAlso: []
|
|
216
|
+
};
|
|
217
|
+
};
|
|
206
218
|
const reviewInitRule = (ctx) => {
|
|
207
219
|
if (ctx.command !== "review init") return null;
|
|
208
220
|
const sid = ctx.sessionId ?? "<session-id>";
|
|
@@ -222,7 +234,8 @@ const reviewStatusRule = (ctx) => {
|
|
|
222
234
|
if (!rs) return null;
|
|
223
235
|
return generateReviewNextSteps({
|
|
224
236
|
sessionId: ctx.sessionId ?? "<session-id>",
|
|
225
|
-
statusResult: rs
|
|
237
|
+
statusResult: rs,
|
|
238
|
+
...rs.mode && { mode: rs.mode }
|
|
226
239
|
});
|
|
227
240
|
};
|
|
228
241
|
const reviewListRule = (ctx) => {
|
|
@@ -281,7 +294,8 @@ const reviewMergeRule = (ctx) => {
|
|
|
281
294
|
}
|
|
282
295
|
return generateReviewNextSteps({
|
|
283
296
|
sessionId: ctx.sessionId ?? "<session-id>",
|
|
284
|
-
statusResult: rs
|
|
297
|
+
statusResult: rs,
|
|
298
|
+
...rs.mode && { mode: rs.mode }
|
|
285
299
|
});
|
|
286
300
|
};
|
|
287
301
|
const reviewFinalizeRule = (ctx) => {
|
|
@@ -290,7 +304,8 @@ const reviewFinalizeRule = (ctx) => {
|
|
|
290
304
|
if (!rs) return null;
|
|
291
305
|
return generateReviewNextSteps({
|
|
292
306
|
sessionId: ctx.sessionId ?? "<session-id>",
|
|
293
|
-
statusResult: rs
|
|
307
|
+
statusResult: rs,
|
|
308
|
+
...rs.mode && { mode: rs.mode }
|
|
294
309
|
});
|
|
295
310
|
};
|
|
296
311
|
const reviewExportRule = (ctx) => {
|
|
@@ -376,6 +391,7 @@ const rules = [
|
|
|
376
391
|
summaryRule,
|
|
377
392
|
diffRule,
|
|
378
393
|
mergeRule,
|
|
394
|
+
relatedRule,
|
|
379
395
|
// Phase 4
|
|
380
396
|
reviewInitRule,
|
|
381
397
|
reviewStatusRule,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.js","sources":["../../../src/cli/suggestions/rules.ts"],"sourcesContent":["import type { SuggestionContext, SuggestionResult, SuggestionRule } from './types.js';\nimport { computeBatchContinuation, generateReviewNextSteps } from '../commands/review/next-steps.js';\n\n// Phase 1: Query Preparation rules\n\nconst queryInitRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query init') return null;\n const file = ctx.outputFile ?? 'query.yaml';\n return {\n next: [{ command: `$EDITOR ${file}`, description: 'Edit your query' }],\n seeAlso: [],\n };\n};\n\nconst queryValidateRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query validate') return null;\n const file = ctx.queryFile ?? '<query-file>';\n\n if (ctx.validationSuccess === false) {\n const next = [{ command: `$EDITOR ${file}`, description: 'Fix errors and re-validate' }];\n\n if (ctx.hasSchemaLink === false) {\n return {\n next,\n or: {\n label: 'Or create a new query from the template',\n items: [{ command: 'search-hub query init -o query.yaml', description: '' }],\n },\n seeAlso: [],\n };\n }\n\n return { next, seeAlso: [] };\n }\n\n const next = [\n { command: `search-hub search ${file} --dry-run`, description: 'Check DB translations' },\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n ];\n\n if (ctx.hasSchemaLink === false) {\n return {\n tip: 'Tip: Start from a template to get $schema support and usage examples:\\n'\n + ' search-hub query init -o query.yaml',\n next,\n seeAlso: [],\n };\n }\n\n return { next, seeAlso: [] };\n};\n\nconst queryTranslateRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query translate') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n { command: `search-hub search ${file}`, description: 'Execute search' },\n ],\n seeAlso: [],\n };\n};\n\n// Phase 2: Search Execution rules\n\n/**\n * Build search completion suggestions based on session status.\n * Shared by search, search --query, and resume commands.\n */\nfunction searchCompletionSuggestion(ctx: SuggestionContext): SuggestionResult | null {\n const sid = ctx.sessionId ?? '<session-id>';\n\n switch (ctx.sessionStatus) {\n case 'completed': {\n const seeAlso: SuggestionResult['seeAlso'] = [];\n if (ctx.sessionCount !== undefined && ctx.sessionCount > 1) {\n seeAlso.push({\n command: `search-hub diff <other-session> ${sid}`,\n description: 'Compare with another query version',\n });\n }\n return {\n next: [{ command: `search-hub results ${sid}`, description: 'View results' }],\n seeAlso,\n };\n }\n case 'partial':\n return {\n next: [{ command: `search-hub resume ${sid}`, description: 'Retry failed databases' }],\n seeAlso: [],\n };\n case 'failed':\n return {\n next: [\n { command: `search-hub resume ${sid} --retry-failed`, description: 'Retry all databases' },\n { command: `search-hub status ${sid}`, description: 'View error details' },\n ],\n seeAlso: [],\n };\n default:\n return null;\n }\n}\n\nconst searchDryRunRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --dry-run') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n { command: `search-hub search ${file}`, description: 'Execute search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchPreviewRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --preview') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query assess ${file} --verdict <verdict>`, description: 'Record assessment' },\n { command: `search-hub search ${file}`, description: 'Execute full search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchCountOnlyRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --count-only') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query assess ${file} --verdict <verdict>`, description: 'Record assessment' },\n { command: `search-hub search ${file}`, description: 'Execute full search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchDirectQueryRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --query') return null;\n const base = searchCompletionSuggestion(ctx);\n if (base === null) return null;\n return {\n next: base.next,\n seeAlso: [\n ...base.seeAlso,\n { command: 'search-hub query init -o my-search.yaml', description: 'Save as YAML for reproducibility' },\n ],\n };\n};\n\nconst searchRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search') return null;\n return searchCompletionSuggestion(ctx);\n};\n\nconst resumeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'resume') return null;\n return searchCompletionSuggestion(ctx);\n};\n\n// Phase 3: Result Analysis rules\n\nconst statusRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'status') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n\n switch (ctx.sessionStatus) {\n case 'completed':\n return {\n next: [{ command: `search-hub results ${sid}`, description: 'View results' }],\n seeAlso: [],\n };\n case 'partial':\n return {\n next: [{ command: `search-hub resume ${sid}`, description: 'Resume search' }],\n seeAlso: [],\n };\n case 'failed':\n return {\n next: [{ command: `search-hub resume ${sid} --retry-failed`, description: 'Retry all databases' }],\n seeAlso: [],\n };\n default:\n return null;\n }\n};\n\n/**\n * Suggestion for results/summary commands - conditional based on reviews.yaml existence.\n */\nfunction resultReviewSuggestion(ctx: SuggestionContext): SuggestionResult {\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === true) {\n return {\n next: [{ command: `search-hub review status --session ${sid}`, description: 'Check review progress' }],\n seeAlso: [],\n };\n }\n return {\n next: [{ command: `search-hub review init --session ${sid}`, description: 'Start systematic review' }],\n seeAlso: [],\n };\n}\n\nconst resultsRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'results') return null;\n return resultReviewSuggestion(ctx);\n};\n\nconst summaryRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'summary') return null;\n return resultReviewSuggestion(ctx);\n};\n\nconst diffRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'diff') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n const seeAlso: SuggestionResult['seeAlso'] = [];\n\n // Suggest merge when both sessions have unique articles\n if (ctx.diffAddedCount !== undefined && ctx.diffAddedCount > 0 &&\n ctx.diffRemovedCount !== undefined && ctx.diffRemovedCount > 0) {\n const sid1 = ctx.diffSession1Id ?? '<session-id-1>';\n seeAlso.push({\n command: `search-hub merge ${sid1} ${sid}`,\n description: 'Combine results from both sessions',\n });\n }\n\n seeAlso.push({ command: `search-hub results ${sid}`, description: 'View detailed results' });\n\n return { next: [], seeAlso };\n};\n\nconst mergeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'merge') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n { command: `search-hub results ${sid}`, description: 'View merged results' },\n { command: `search-hub summary ${sid}`, description: 'View merge statistics' },\n ],\n seeAlso: [],\n };\n};\n\n// Phase 4: Review Workflow rules\n\nconst reviewInitRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review init') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n {\n command: `search-hub review extract --session ${sid} --basis title --name title-screening`,\n description: 'Start title screening',\n },\n ],\n seeAlso: [],\n };\n};\n\nconst reviewStatusRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review status') return null;\n const rs = ctx.reviewStatus;\n if (!rs) return null;\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n });\n};\n\nconst reviewListRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review list') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review extract --session ${sid} --name <name>`,\n description: 'Extract subset for review',\n },\n ],\n };\n};\n\nconst reviewExtractRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review extract') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n const name = ctx.extractName ?? '<name>';\n const result: SuggestionResult = {\n next: [\n {\n command: `search-hub review merge --session ${sid} --name ${name}`,\n description: 'Merge review results',\n },\n ],\n seeAlso: [],\n };\n\n // Batch continuation: suggest next batch if --limit was used with remaining articles\n if (\n ctx.extractLimit !== undefined &&\n ctx.extractedCount !== undefined &&\n ctx.totalMatching !== undefined\n ) {\n const batch = computeBatchContinuation({\n sessionId: sid,\n extractName: name !== '<name>' ? name : undefined,\n extractedCount: ctx.extractedCount,\n totalMatching: ctx.totalMatching,\n limit: ctx.extractLimit,\n offset: ctx.extractOffset,\n });\n if (batch) result.seeAlso.push(batch);\n }\n\n return result;\n};\n\nconst reviewMergeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review merge') return null;\n const rs = ctx.reviewStatus;\n if (!rs) {\n // Fallback: suggest status check when reviewStatus not available\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n {\n command: `search-hub review status --session ${sid}`,\n description: 'Check progress',\n },\n ],\n seeAlso: [],\n };\n }\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n });\n};\n\nconst reviewFinalizeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review finalize') return null;\n const rs = ctx.reviewStatus;\n if (!rs) return null;\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n });\n};\n\nconst reviewExportRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review export') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub register ${sid} --reviewed`,\n description: 'Register with reference-manager',\n },\n ],\n };\n};\n\n// Phase 5: Registration & Export rules\n\nconst exportRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'export') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === false) {\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review init --session ${sid}`,\n description: 'Start review workflow',\n },\n ],\n };\n }\n return { next: [], seeAlso: [] };\n};\n\nconst registerRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'register') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === false) {\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review init --session ${sid}`,\n description: 'Start systematic review',\n },\n ],\n };\n }\n // Terminal state: no suggestions\n return null;\n};\n\nconst queryAssessRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query assess') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query log ${file}`, description: 'View iteration history' },\n ],\n seeAlso: [\n { command: `$EDITOR ${file}`, description: 'Edit query and re-run count' },\n ],\n };\n};\n\nconst notesRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'notes add' && ctx.command !== 'notes assess') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [{ command: `search-hub notes list ${sid}`, description: 'View notes' }],\n };\n};\n\n/**\n * All suggestion rules in evaluation order.\n */\nconst rules: SuggestionRule[] = [\n // Phase 1\n queryInitRule,\n queryValidateRule,\n queryTranslateRule,\n // Phase 2\n searchDryRunRule,\n searchPreviewRule,\n searchCountOnlyRule,\n searchDirectQueryRule,\n searchRule,\n resumeRule,\n // Phase 3\n statusRule,\n resultsRule,\n summaryRule,\n diffRule,\n mergeRule,\n // Phase 4\n reviewInitRule,\n reviewStatusRule,\n reviewListRule,\n reviewExtractRule,\n reviewMergeRule,\n reviewFinalizeRule,\n reviewExportRule,\n // Query iteration\n queryAssessRule,\n // Phase 5\n exportRule,\n registerRule,\n notesRule,\n];\n\n/**\n * Evaluate suggestion rules for the given context.\n * Returns the first matching rule's result, or null if no rules match.\n */\nexport function getSuggestion(ctx: SuggestionContext): SuggestionResult | null {\n for (const rule of rules) {\n const result = rule(ctx);\n if (result !== null) return result;\n }\n return null;\n}\n"],"names":["next"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rules.js","sources":["../../../src/cli/suggestions/rules.ts"],"sourcesContent":["import type { SuggestionContext, SuggestionResult, SuggestionRule } from './types.js';\nimport { computeBatchContinuation, generateReviewNextSteps } from '../commands/review/next-steps.js';\n\n// Phase 1: Query Preparation rules\n\nconst queryInitRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query init') return null;\n const file = ctx.outputFile ?? 'query.yaml';\n return {\n next: [{ command: `$EDITOR ${file}`, description: 'Edit your query' }],\n seeAlso: [],\n };\n};\n\nconst queryValidateRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query validate') return null;\n const file = ctx.queryFile ?? '<query-file>';\n\n if (ctx.validationSuccess === false) {\n const next = [{ command: `$EDITOR ${file}`, description: 'Fix errors and re-validate' }];\n\n if (ctx.hasSchemaLink === false) {\n return {\n next,\n or: {\n label: 'Or create a new query from the template',\n items: [{ command: 'search-hub query init -o query.yaml', description: '' }],\n },\n seeAlso: [],\n };\n }\n\n return { next, seeAlso: [] };\n }\n\n const next = [\n { command: `search-hub search ${file} --dry-run`, description: 'Check DB translations' },\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n ];\n\n if (ctx.hasSchemaLink === false) {\n return {\n tip: 'Tip: Start from a template to get $schema support and usage examples:\\n'\n + ' search-hub query init -o query.yaml',\n next,\n seeAlso: [],\n };\n }\n\n return { next, seeAlso: [] };\n};\n\nconst queryTranslateRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query translate') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n { command: `search-hub search ${file}`, description: 'Execute search' },\n ],\n seeAlso: [],\n };\n};\n\n// Phase 2: Search Execution rules\n\n/**\n * Build search completion suggestions based on session status.\n * Shared by search, search --query, and resume commands.\n */\nfunction searchCompletionSuggestion(ctx: SuggestionContext): SuggestionResult | null {\n const sid = ctx.sessionId ?? '<session-id>';\n\n switch (ctx.sessionStatus) {\n case 'completed': {\n const seeAlso: SuggestionResult['seeAlso'] = [];\n if (ctx.sessionCount !== undefined && ctx.sessionCount > 1) {\n seeAlso.push({\n command: `search-hub diff <other-session> ${sid}`,\n description: 'Compare with another query version',\n });\n }\n return {\n next: [{ command: `search-hub results ${sid}`, description: 'View results' }],\n seeAlso,\n };\n }\n case 'partial':\n return {\n next: [{ command: `search-hub resume ${sid}`, description: 'Retry failed databases' }],\n seeAlso: [],\n };\n case 'failed':\n return {\n next: [\n { command: `search-hub resume ${sid} --retry-failed`, description: 'Retry all databases' },\n { command: `search-hub status ${sid}`, description: 'View error details' },\n ],\n seeAlso: [],\n };\n default:\n return null;\n }\n}\n\nconst searchDryRunRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --dry-run') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n { command: `search-hub search ${file}`, description: 'Execute search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchPreviewRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --preview') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query assess ${file} --verdict <verdict>`, description: 'Record assessment' },\n { command: `search-hub search ${file}`, description: 'Execute full search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchCountOnlyRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --count-only') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query assess ${file} --verdict <verdict>`, description: 'Record assessment' },\n { command: `search-hub search ${file}`, description: 'Execute full search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchDirectQueryRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --query') return null;\n const base = searchCompletionSuggestion(ctx);\n if (base === null) return null;\n return {\n next: base.next,\n seeAlso: [\n ...base.seeAlso,\n { command: 'search-hub query init -o my-search.yaml', description: 'Save as YAML for reproducibility' },\n ],\n };\n};\n\nconst searchRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search') return null;\n return searchCompletionSuggestion(ctx);\n};\n\nconst resumeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'resume') return null;\n return searchCompletionSuggestion(ctx);\n};\n\n// Phase 3: Result Analysis rules\n\nconst statusRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'status') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n\n switch (ctx.sessionStatus) {\n case 'completed':\n return {\n next: [{ command: `search-hub results ${sid}`, description: 'View results' }],\n seeAlso: [],\n };\n case 'partial':\n return {\n next: [{ command: `search-hub resume ${sid}`, description: 'Resume search' }],\n seeAlso: [],\n };\n case 'failed':\n return {\n next: [{ command: `search-hub resume ${sid} --retry-failed`, description: 'Retry all databases' }],\n seeAlso: [],\n };\n default:\n return null;\n }\n};\n\n/**\n * Suggestion for results/summary commands - conditional based on reviews.yaml existence.\n */\nfunction resultReviewSuggestion(ctx: SuggestionContext): SuggestionResult {\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === true) {\n return {\n next: [{ command: `search-hub review status --session ${sid}`, description: 'Check review progress' }],\n seeAlso: [],\n };\n }\n return {\n next: [{ command: `search-hub review init --session ${sid}`, description: 'Start systematic review' }],\n seeAlso: [],\n };\n}\n\nconst resultsRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'results') return null;\n return resultReviewSuggestion(ctx);\n};\n\nconst summaryRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'summary') return null;\n return resultReviewSuggestion(ctx);\n};\n\nconst diffRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'diff') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n const seeAlso: SuggestionResult['seeAlso'] = [];\n\n // Suggest merge when both sessions have unique articles\n if (ctx.diffAddedCount !== undefined && ctx.diffAddedCount > 0 &&\n ctx.diffRemovedCount !== undefined && ctx.diffRemovedCount > 0) {\n const sid1 = ctx.diffSession1Id ?? '<session-id-1>';\n seeAlso.push({\n command: `search-hub merge ${sid1} ${sid}`,\n description: 'Combine results from both sessions',\n });\n }\n\n seeAlso.push({ command: `search-hub results ${sid}`, description: 'View detailed results' });\n\n return { next: [], seeAlso };\n};\n\nconst mergeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'merge') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n { command: `search-hub results ${sid}`, description: 'View merged results' },\n { command: `search-hub summary ${sid}`, description: 'View merge statistics' },\n ],\n seeAlso: [],\n };\n};\n\nconst relatedRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'related') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n { command: `search-hub results ${sid}`, description: 'View related articles' },\n { command: `search-hub review init ${sid}`, description: 'Screen related articles' },\n { command: `search-hub export ${sid}`, description: 'Export results' },\n ],\n seeAlso: [],\n };\n};\n\n// Phase 4: Review Workflow rules\n\nconst reviewInitRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review init') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n {\n command: `search-hub review extract --session ${sid} --basis title --name title-screening`,\n description: 'Start title screening',\n },\n ],\n seeAlso: [],\n };\n};\n\nconst reviewStatusRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review status') return null;\n const rs = ctx.reviewStatus;\n if (!rs) return null;\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewListRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review list') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review extract --session ${sid} --name <name>`,\n description: 'Extract subset for review',\n },\n ],\n };\n};\n\nconst reviewExtractRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review extract') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n const name = ctx.extractName ?? '<name>';\n const result: SuggestionResult = {\n next: [\n {\n command: `search-hub review merge --session ${sid} --name ${name}`,\n description: 'Merge review results',\n },\n ],\n seeAlso: [],\n };\n\n // Batch continuation: suggest next batch if --limit was used with remaining articles\n if (\n ctx.extractLimit !== undefined &&\n ctx.extractedCount !== undefined &&\n ctx.totalMatching !== undefined\n ) {\n const batch = computeBatchContinuation({\n sessionId: sid,\n extractName: name !== '<name>' ? name : undefined,\n extractedCount: ctx.extractedCount,\n totalMatching: ctx.totalMatching,\n limit: ctx.extractLimit,\n offset: ctx.extractOffset,\n });\n if (batch) result.seeAlso.push(batch);\n }\n\n return result;\n};\n\nconst reviewMergeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review merge') return null;\n const rs = ctx.reviewStatus;\n if (!rs) {\n // Fallback: suggest status check when reviewStatus not available\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n {\n command: `search-hub review status --session ${sid}`,\n description: 'Check progress',\n },\n ],\n seeAlso: [],\n };\n }\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewFinalizeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review finalize') return null;\n const rs = ctx.reviewStatus;\n if (!rs) return null;\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewExportRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review export') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub register ${sid} --reviewed`,\n description: 'Register with reference-manager',\n },\n ],\n };\n};\n\n// Phase 5: Registration & Export rules\n\nconst exportRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'export') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === false) {\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review init --session ${sid}`,\n description: 'Start review workflow',\n },\n ],\n };\n }\n return { next: [], seeAlso: [] };\n};\n\nconst registerRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'register') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === false) {\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review init --session ${sid}`,\n description: 'Start systematic review',\n },\n ],\n };\n }\n // Terminal state: no suggestions\n return null;\n};\n\nconst queryAssessRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query assess') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query log ${file}`, description: 'View iteration history' },\n ],\n seeAlso: [\n { command: `$EDITOR ${file}`, description: 'Edit query and re-run count' },\n ],\n };\n};\n\nconst notesRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'notes add' && ctx.command !== 'notes assess') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [{ command: `search-hub notes list ${sid}`, description: 'View notes' }],\n };\n};\n\n/**\n * All suggestion rules in evaluation order.\n */\nconst rules: SuggestionRule[] = [\n // Phase 1\n queryInitRule,\n queryValidateRule,\n queryTranslateRule,\n // Phase 2\n searchDryRunRule,\n searchPreviewRule,\n searchCountOnlyRule,\n searchDirectQueryRule,\n searchRule,\n resumeRule,\n // Phase 3\n statusRule,\n resultsRule,\n summaryRule,\n diffRule,\n mergeRule,\n relatedRule,\n // Phase 4\n reviewInitRule,\n reviewStatusRule,\n reviewListRule,\n reviewExtractRule,\n reviewMergeRule,\n reviewFinalizeRule,\n reviewExportRule,\n // Query iteration\n queryAssessRule,\n // Phase 5\n exportRule,\n registerRule,\n notesRule,\n];\n\n/**\n * Evaluate suggestion rules for the given context.\n * Returns the first matching rule's result, or null if no rules match.\n */\nexport function getSuggestion(ctx: SuggestionContext): SuggestionResult | null {\n for (const rule of rules) {\n const result = rule(ctx);\n if (result !== null) return result;\n }\n return null;\n}\n"],"names":["next"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/arxiv/provider.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,aAAa,
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/arxiv/provider.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,aAAa,EAEb,WAAW,EACX,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACrB,MAAM,kBAAkB,CAAC;AAI1B,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,YAAY,CAAC;AAUlE;;GAEG;AACH,qBAAa,aAAc,SAAQ,YAAY;IAC7C,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAU;IAEjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwB;IAGpD,OAAO,CAAC,YAAY,CAA4B;gBAEpC,MAAM,GAAE,WAAgB;IAqBpC;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,eAAe;IAItD;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAOpD;;OAEG;IACI,MAAM,CAAC,KAAK,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,aAAa,CAAC,OAAO,CAAC;IA4DtF;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAWrD;;OAEG;IACH,cAAc,IAAI,WAAW,GAAG,IAAI;IAIpC;;OAEG;IACI,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,aAAa,CAAC,OAAO,CAAC;IAsD/D;;;OAGG;IACG,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAItE"}
|
|
@@ -3,6 +3,9 @@ import { ArxivClient } from "./client.js";
|
|
|
3
3
|
import { translateQuery } from "./translator.js";
|
|
4
4
|
import { DEFAULT_ARXIV_CONFIG } from "./types.js";
|
|
5
5
|
const DEFAULT_PAGE_SIZE = 100;
|
|
6
|
+
function mapSortField(sort) {
|
|
7
|
+
return sort === "date" ? "submittedDate" : "relevance";
|
|
8
|
+
}
|
|
6
9
|
class ArxivProvider extends BaseProvider {
|
|
7
10
|
name = "arxiv";
|
|
8
11
|
client;
|
|
@@ -53,14 +56,14 @@ class ArxivProvider extends BaseProvider {
|
|
|
53
56
|
let offset = 0;
|
|
54
57
|
let retrievedCount = 0;
|
|
55
58
|
let totalResults = 0;
|
|
59
|
+
const arxivSortBy = options?.sort ? mapSortField(options.sort) : void 0;
|
|
56
60
|
while (retrievedCount < maxResults) {
|
|
57
61
|
const searchOptions = {
|
|
58
62
|
start: offset,
|
|
59
|
-
maxResults: Math.min(pageSize, maxResults - retrievedCount)
|
|
63
|
+
maxResults: Math.min(pageSize, maxResults - retrievedCount),
|
|
64
|
+
...options?.signal && { signal: options.signal },
|
|
65
|
+
...arxivSortBy && { sortBy: arxivSortBy }
|
|
60
66
|
};
|
|
61
|
-
if (options?.signal) {
|
|
62
|
-
searchOptions.signal = options.signal;
|
|
63
|
-
}
|
|
64
67
|
const response = await this.withRetry(
|
|
65
68
|
() => this.client.search(query.native, searchOptions)
|
|
66
69
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sources":["../../../src/providers/arxiv/provider.ts"],"sourcesContent":["/**\n * arXiv Provider\n *\n * Provider implementation for searching the arXiv preprint server.\n * Supports physics, mathematics, computer science, quantitative biology,\n * and related fields.\n */\n\nimport { BaseProvider } from '../base/provider.js';\nimport type {\n Article,\n TranslatedQuery,\n SearchOptions,\n SearchState,\n SearchResumeResult,\n ResolvedAST,\n ConnectionTestResult,\n} from '../base/types.js';\nimport { ArxivClient } from './client.js';\nimport { translateQuery } from './translator.js';\nimport type { ArxivConfig, ArxivProviderState } from './types.js';\nimport { DEFAULT_ARXIV_CONFIG } from './types.js';\n\nconst DEFAULT_PAGE_SIZE = 100;\n\n/**\n * arXiv provider for searching preprints.\n */\nexport class ArxivProvider extends BaseProvider {\n readonly name = 'arxiv' as const;\n\n private readonly client: ArxivClient;\n private readonly arxivConfig: Required<ArxivConfig>;\n\n // State tracking for session resume\n private currentState: SearchState | null = null;\n\n constructor(config: ArxivConfig = {}) {\n super({\n rateLimit: config.rateLimit ?? DEFAULT_ARXIV_CONFIG.rateLimit,\n timeout: config.timeout ?? DEFAULT_ARXIV_CONFIG.timeout,\n retries: config.retries ?? DEFAULT_ARXIV_CONFIG.retries,\n initialBackoff: config.initialBackoff ?? DEFAULT_ARXIV_CONFIG.initialBackoff,\n maxBackoff: config.maxBackoff ?? DEFAULT_ARXIV_CONFIG.maxBackoff,\n });\n\n this.arxivConfig = {\n ...DEFAULT_ARXIV_CONFIG,\n ...config,\n };\n\n this.client = new ArxivClient({\n baseUrl: this.arxivConfig.baseUrl,\n minRequestInterval: Math.ceil(1000 / this.arxivConfig.rateLimit), // Convert rate to interval\n timeout: this.arxivConfig.timeout,\n });\n }\n\n /**\n * Translate ResolvedAST to arXiv-native query syntax.\n */\n translateQuery(resolved: ResolvedAST): TranslatedQuery {\n return translateQuery(resolved);\n }\n\n /**\n * Get total hit count for a query without downloading results.\n * Uses a minimal search with maxResults=1 to get the total count from response metadata.\n */\n async count(query: TranslatedQuery): Promise<number> {\n const response = await this.withRetry(() =>\n this.client.search(query.native, { start: 0, maxResults: 1 })\n );\n return response.totalResults;\n }\n\n /**\n * Search arXiv and yield articles as async iterable.\n */\n async *search(query: TranslatedQuery, options?: SearchOptions): AsyncIterable<Article> {\n const maxResults = options?.maxResults ?? this.arxivConfig.maxResults;\n const pageSize = options?.pageSize ?? DEFAULT_PAGE_SIZE;\n\n let offset = 0;\n let retrievedCount = 0;\n let totalResults = 0;\n\n while (retrievedCount < maxResults) {\n const searchOptions: { start: number; maxResults: number; signal?: AbortSignal } = {\n start: offset,\n maxResults: Math.min(pageSize, maxResults - retrievedCount),\n };\n if (options?.signal) {\n searchOptions.signal = options.signal;\n }\n\n const response = await this.withRetry(() =>\n this.client.search(query.native, searchOptions)\n );\n\n totalResults = response.totalResults;\n\n // Update state for session resume\n this.currentState = {\n ...this.createBaseState(query, totalResults, retrievedCount),\n providerState: { offset } as ArxivProviderState,\n };\n\n // Yield articles from this page\n for (const entry of response.entries) {\n if (retrievedCount >= maxResults) {\n break;\n }\n yield entry;\n retrievedCount++;\n }\n\n // Update state after yielding\n if (this.currentState) {\n this.currentState.retrievedCount = retrievedCount;\n this.currentState.providerState = {\n offset: offset + response.entries.length,\n } as ArxivProviderState;\n }\n\n // Check if we've retrieved all available results\n if (response.entries.length === 0 || offset + response.entries.length >= totalResults) {\n break;\n }\n\n offset += response.entries.length;\n }\n\n // Clear state when search completes\n this.currentState = null;\n }\n\n /**\n * Test connection to arXiv API.\n */\n async testConnection(): Promise<ConnectionTestResult> {\n try {\n // Make a minimal search request\n await this.client.search('ti:test', { start: 0, maxResults: 1 });\n return { ok: true };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return { ok: false, error: message };\n }\n }\n\n /**\n * Get current search state for session persistence.\n */\n getSearchState(): SearchState | null {\n return this.currentState;\n }\n\n /**\n * Resume search from saved state.\n */\n async *resumeSearch(state: SearchState): AsyncIterable<Article> {\n const providerState = state.providerState as ArxivProviderState | undefined;\n const startOffset = providerState?.offset ?? 0;\n\n const maxResults = state.totalResults - state.retrievedCount;\n const pageSize = DEFAULT_PAGE_SIZE;\n\n let offset = startOffset;\n let retrievedCount = 0;\n\n while (retrievedCount < maxResults) {\n const response = await this.withRetry(() =>\n this.client.search(state.query.native, {\n start: offset,\n maxResults: Math.min(pageSize, maxResults - retrievedCount),\n })\n );\n\n // Update state\n this.currentState = {\n ...state,\n retrievedCount: state.retrievedCount + retrievedCount,\n lastUpdated: new Date(),\n providerState: { offset },\n };\n\n // Yield articles\n for (const entry of response.entries) {\n if (retrievedCount >= maxResults) {\n break;\n }\n yield entry;\n retrievedCount++;\n }\n\n // Update state after yielding\n if (this.currentState) {\n this.currentState.retrievedCount = state.retrievedCount + retrievedCount;\n this.currentState.providerState = {\n offset: offset + response.entries.length,\n } as ArxivProviderState;\n }\n\n // Check if done\n if (response.entries.length === 0 || offset + response.entries.length >= state.totalResults) {\n break;\n }\n\n offset += response.entries.length;\n }\n\n this.currentState = null;\n }\n\n /**\n * Validate if saved state is still valid.\n * arXiv uses offset-based pagination, so state is always valid.\n */\n async validateState(_state: SearchState): Promise<SearchResumeResult> {\n // Offset-based pagination doesn't expire\n return { valid: true };\n }\n}\n"],"names":[],"mappings":";;;;AAuBA,MAAM,oBAAoB;AAKnB,MAAM,sBAAsB,aAAa;AAAA,EACrC,OAAO;AAAA,EAEC;AAAA,EACA;AAAA;AAAA,EAGT,eAAmC;AAAA,EAE3C,YAAY,SAAsB,IAAI;AACpC,UAAM;AAAA,MACJ,WAAW,OAAO,aAAa,qBAAqB;AAAA,MACpD,SAAS,OAAO,WAAW,qBAAqB;AAAA,MAChD,SAAS,OAAO,WAAW,qBAAqB;AAAA,MAChD,gBAAgB,OAAO,kBAAkB,qBAAqB;AAAA,MAC9D,YAAY,OAAO,cAAc,qBAAqB;AAAA,IAAA,CACvD;AAED,SAAK,cAAc;AAAA,MACjB,GAAG;AAAA,MACH,GAAG;AAAA,IAAA;AAGL,SAAK,SAAS,IAAI,YAAY;AAAA,MAC5B,SAAS,KAAK,YAAY;AAAA,MAC1B,oBAAoB,KAAK,KAAK,MAAO,KAAK,YAAY,SAAS;AAAA;AAAA,MAC/D,SAAS,KAAK,YAAY;AAAA,IAAA,CAC3B;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,UAAwC;AACrD,WAAO,eAAe,QAAQ;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,MAAM,OAAyC;AACnD,UAAM,WAAW,MAAM,KAAK;AAAA,MAAU,MACpC,KAAK,OAAO,OAAO,MAAM,QAAQ,EAAE,OAAO,GAAG,YAAY,EAAA,CAAG;AAAA,IAAA;AAE9D,WAAO,SAAS;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,OAAO,OAAwB,SAAiD;AACrF,UAAM,aAAa,SAAS,cAAc,KAAK,YAAY;AAC3D,UAAM,WAAW,SAAS,YAAY;AAEtC,QAAI,SAAS;AACb,QAAI,iBAAiB;AACrB,QAAI,eAAe;AAEnB,WAAO,iBAAiB,YAAY;AAClC,YAAM,gBAA6E;AAAA,QACjF,OAAO;AAAA,QACP,YAAY,KAAK,IAAI,UAAU,aAAa,cAAc;AAAA,MAAA;AAE5D,UAAI,SAAS,QAAQ;AACnB,sBAAc,SAAS,QAAQ;AAAA,MACjC;AAEA,YAAM,WAAW,MAAM,KAAK;AAAA,QAAU,MACpC,KAAK,OAAO,OAAO,MAAM,QAAQ,aAAa;AAAA,MAAA;AAGhD,qBAAe,SAAS;AAGxB,WAAK,eAAe;AAAA,QAClB,GAAG,KAAK,gBAAgB,OAAO,cAAc,cAAc;AAAA,QAC3D,eAAe,EAAE,OAAA;AAAA,MAAO;AAI1B,iBAAW,SAAS,SAAS,SAAS;AACpC,YAAI,kBAAkB,YAAY;AAChC;AAAA,QACF;AACA,cAAM;AACN;AAAA,MACF;AAGA,UAAI,KAAK,cAAc;AACrB,aAAK,aAAa,iBAAiB;AACnC,aAAK,aAAa,gBAAgB;AAAA,UAChC,QAAQ,SAAS,SAAS,QAAQ;AAAA,QAAA;AAAA,MAEtC;AAGA,UAAI,SAAS,QAAQ,WAAW,KAAK,SAAS,SAAS,QAAQ,UAAU,cAAc;AACrF;AAAA,MACF;AAEA,gBAAU,SAAS,QAAQ;AAAA,IAC7B;AAGA,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,iBAAgD;AACpD,QAAI;AAEF,YAAM,KAAK,OAAO,OAAO,WAAW,EAAE,OAAO,GAAG,YAAY,GAAG;AAC/D,aAAO,EAAE,IAAI,KAAA;AAAA,IACf,SAAS,OAAO;AACd,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,aAAO,EAAE,IAAI,OAAO,OAAO,QAAA;AAAA,IAC7B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAqC;AACnC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,aAAa,OAA4C;AAC9D,UAAM,gBAAgB,MAAM;AAC5B,UAAM,cAAc,eAAe,UAAU;AAE7C,UAAM,aAAa,MAAM,eAAe,MAAM;AAC9C,UAAM,WAAW;AAEjB,QAAI,SAAS;AACb,QAAI,iBAAiB;AAErB,WAAO,iBAAiB,YAAY;AAClC,YAAM,WAAW,MAAM,KAAK;AAAA,QAAU,MACpC,KAAK,OAAO,OAAO,MAAM,MAAM,QAAQ;AAAA,UACrC,OAAO;AAAA,UACP,YAAY,KAAK,IAAI,UAAU,aAAa,cAAc;AAAA,QAAA,CAC3D;AAAA,MAAA;AAIH,WAAK,eAAe;AAAA,QAClB,GAAG;AAAA,QACH,gBAAgB,MAAM,iBAAiB;AAAA,QACvC,iCAAiB,KAAA;AAAA,QACjB,eAAe,EAAE,OAAA;AAAA,MAAO;AAI1B,iBAAW,SAAS,SAAS,SAAS;AACpC,YAAI,kBAAkB,YAAY;AAChC;AAAA,QACF;AACA,cAAM;AACN;AAAA,MACF;AAGA,UAAI,KAAK,cAAc;AACrB,aAAK,aAAa,iBAAiB,MAAM,iBAAiB;AAC1D,aAAK,aAAa,gBAAgB;AAAA,UAChC,QAAQ,SAAS,SAAS,QAAQ;AAAA,QAAA;AAAA,MAEtC;AAGA,UAAI,SAAS,QAAQ,WAAW,KAAK,SAAS,SAAS,QAAQ,UAAU,MAAM,cAAc;AAC3F;AAAA,MACF;AAEA,gBAAU,SAAS,QAAQ;AAAA,IAC7B;AAEA,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAc,QAAkD;AAEpE,WAAO,EAAE,OAAO,KAAA;AAAA,EAClB;AACF;"}
|
|
1
|
+
{"version":3,"file":"provider.js","sources":["../../../src/providers/arxiv/provider.ts"],"sourcesContent":["/**\n * arXiv Provider\n *\n * Provider implementation for searching the arXiv preprint server.\n * Supports physics, mathematics, computer science, quantitative biology,\n * and related fields.\n */\n\nimport { BaseProvider } from '../base/provider.js';\nimport type {\n Article,\n TranslatedQuery,\n SearchOptions,\n SortField,\n SearchState,\n SearchResumeResult,\n ResolvedAST,\n ConnectionTestResult,\n} from '../base/types.js';\nimport { ArxivClient } from './client.js';\nimport type { ArxivSearchOptions } from './client.js';\nimport { translateQuery } from './translator.js';\nimport type { ArxivConfig, ArxivProviderState } from './types.js';\nimport { DEFAULT_ARXIV_CONFIG } from './types.js';\n\nconst DEFAULT_PAGE_SIZE = 100;\n\n/** Map base SortField to arXiv sortBy parameter */\nfunction mapSortField(sort: SortField): ArxivSearchOptions['sortBy'] {\n return sort === 'date' ? 'submittedDate' : 'relevance';\n}\n\n/**\n * arXiv provider for searching preprints.\n */\nexport class ArxivProvider extends BaseProvider {\n readonly name = 'arxiv' as const;\n\n private readonly client: ArxivClient;\n private readonly arxivConfig: Required<ArxivConfig>;\n\n // State tracking for session resume\n private currentState: SearchState | null = null;\n\n constructor(config: ArxivConfig = {}) {\n super({\n rateLimit: config.rateLimit ?? DEFAULT_ARXIV_CONFIG.rateLimit,\n timeout: config.timeout ?? DEFAULT_ARXIV_CONFIG.timeout,\n retries: config.retries ?? DEFAULT_ARXIV_CONFIG.retries,\n initialBackoff: config.initialBackoff ?? DEFAULT_ARXIV_CONFIG.initialBackoff,\n maxBackoff: config.maxBackoff ?? DEFAULT_ARXIV_CONFIG.maxBackoff,\n });\n\n this.arxivConfig = {\n ...DEFAULT_ARXIV_CONFIG,\n ...config,\n };\n\n this.client = new ArxivClient({\n baseUrl: this.arxivConfig.baseUrl,\n minRequestInterval: Math.ceil(1000 / this.arxivConfig.rateLimit), // Convert rate to interval\n timeout: this.arxivConfig.timeout,\n });\n }\n\n /**\n * Translate ResolvedAST to arXiv-native query syntax.\n */\n translateQuery(resolved: ResolvedAST): TranslatedQuery {\n return translateQuery(resolved);\n }\n\n /**\n * Get total hit count for a query without downloading results.\n * Uses a minimal search with maxResults=1 to get the total count from response metadata.\n */\n async count(query: TranslatedQuery): Promise<number> {\n const response = await this.withRetry(() =>\n this.client.search(query.native, { start: 0, maxResults: 1 })\n );\n return response.totalResults;\n }\n\n /**\n * Search arXiv and yield articles as async iterable.\n */\n async *search(query: TranslatedQuery, options?: SearchOptions): AsyncIterable<Article> {\n const maxResults = options?.maxResults ?? this.arxivConfig.maxResults;\n const pageSize = options?.pageSize ?? DEFAULT_PAGE_SIZE;\n\n let offset = 0;\n let retrievedCount = 0;\n let totalResults = 0;\n\n // Map sort field to arXiv-specific parameter\n const arxivSortBy = options?.sort ? mapSortField(options.sort) : undefined;\n\n while (retrievedCount < maxResults) {\n const searchOptions: ArxivSearchOptions = {\n start: offset,\n maxResults: Math.min(pageSize, maxResults - retrievedCount),\n ...(options?.signal && { signal: options.signal }),\n ...(arxivSortBy && { sortBy: arxivSortBy }),\n };\n\n const response = await this.withRetry(() =>\n this.client.search(query.native, searchOptions)\n );\n\n totalResults = response.totalResults;\n\n // Update state for session resume\n this.currentState = {\n ...this.createBaseState(query, totalResults, retrievedCount),\n providerState: { offset } as ArxivProviderState,\n };\n\n // Yield articles from this page\n for (const entry of response.entries) {\n if (retrievedCount >= maxResults) {\n break;\n }\n yield entry;\n retrievedCount++;\n }\n\n // Update state after yielding\n if (this.currentState) {\n this.currentState.retrievedCount = retrievedCount;\n this.currentState.providerState = {\n offset: offset + response.entries.length,\n } as ArxivProviderState;\n }\n\n // Check if we've retrieved all available results\n if (response.entries.length === 0 || offset + response.entries.length >= totalResults) {\n break;\n }\n\n offset += response.entries.length;\n }\n\n // Clear state when search completes\n this.currentState = null;\n }\n\n /**\n * Test connection to arXiv API.\n */\n async testConnection(): Promise<ConnectionTestResult> {\n try {\n // Make a minimal search request\n await this.client.search('ti:test', { start: 0, maxResults: 1 });\n return { ok: true };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return { ok: false, error: message };\n }\n }\n\n /**\n * Get current search state for session persistence.\n */\n getSearchState(): SearchState | null {\n return this.currentState;\n }\n\n /**\n * Resume search from saved state.\n */\n async *resumeSearch(state: SearchState): AsyncIterable<Article> {\n const providerState = state.providerState as ArxivProviderState | undefined;\n const startOffset = providerState?.offset ?? 0;\n\n const maxResults = state.totalResults - state.retrievedCount;\n const pageSize = DEFAULT_PAGE_SIZE;\n\n let offset = startOffset;\n let retrievedCount = 0;\n\n while (retrievedCount < maxResults) {\n const response = await this.withRetry(() =>\n this.client.search(state.query.native, {\n start: offset,\n maxResults: Math.min(pageSize, maxResults - retrievedCount),\n })\n );\n\n // Update state\n this.currentState = {\n ...state,\n retrievedCount: state.retrievedCount + retrievedCount,\n lastUpdated: new Date(),\n providerState: { offset },\n };\n\n // Yield articles\n for (const entry of response.entries) {\n if (retrievedCount >= maxResults) {\n break;\n }\n yield entry;\n retrievedCount++;\n }\n\n // Update state after yielding\n if (this.currentState) {\n this.currentState.retrievedCount = state.retrievedCount + retrievedCount;\n this.currentState.providerState = {\n offset: offset + response.entries.length,\n } as ArxivProviderState;\n }\n\n // Check if done\n if (response.entries.length === 0 || offset + response.entries.length >= state.totalResults) {\n break;\n }\n\n offset += response.entries.length;\n }\n\n this.currentState = null;\n }\n\n /**\n * Validate if saved state is still valid.\n * arXiv uses offset-based pagination, so state is always valid.\n */\n async validateState(_state: SearchState): Promise<SearchResumeResult> {\n // Offset-based pagination doesn't expire\n return { valid: true };\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -53,6 +53,7 @@ export interface TranslatedQuery {
|
|
|
53
53
|
/**
|
|
54
54
|
* Search options for controlling query execution.
|
|
55
55
|
*/
|
|
56
|
+
export type SortField = 'relevance' | 'date';
|
|
56
57
|
export interface SearchOptions {
|
|
57
58
|
/** Maximum number of results to retrieve */
|
|
58
59
|
maxResults?: number;
|
|
@@ -65,6 +66,8 @@ export interface SearchOptions {
|
|
|
65
66
|
};
|
|
66
67
|
/** AbortSignal for cancellation */
|
|
67
68
|
signal?: AbortSignal;
|
|
69
|
+
/** Sort order for results */
|
|
70
|
+
sort?: SortField;
|
|
68
71
|
}
|
|
69
72
|
/**
|
|
70
73
|
* Result of a connection test.
|
|
@@ -100,6 +103,11 @@ export interface Provider {
|
|
|
100
103
|
* Does not throw.
|
|
101
104
|
*/
|
|
102
105
|
testConnection(): Promise<ConnectionTestResult>;
|
|
106
|
+
/**
|
|
107
|
+
* Get warnings from the most recent search (e.g. unsupported sort options).
|
|
108
|
+
* Optional — providers that don't produce warnings need not implement this.
|
|
109
|
+
*/
|
|
110
|
+
getWarnings?(): string[];
|
|
103
111
|
}
|
|
104
112
|
/**
|
|
105
113
|
* Error codes used by providers.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/providers/base/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGlE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;AAEtC;;GAEG;AACH,MAAM,MAAM,YAAY,GACpB,QAAQ,GACR,MAAM,GACN,OAAO,GACP,QAAQ,GACR,KAAK,GACL,QAAQ,CAAC;AAEb;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uBAAuB;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,OAAO;IAEtB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAGhB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IAGpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,QAAQ,EAAE,YAAY,CAAC;IACvB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,SAAS,CAAC,EAAE;QACV,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/providers/base/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sources":["../../../src/providers/base/types.ts"],"sourcesContent":["/**\n * Provider base types for the search-hub CLI tool.\n * These types define the common interface for all database providers.\n */\n\nimport type { QueryAST, ResolvedAST } from '../../query/types.js';\n\n// Re-export QueryAST and ResolvedAST for convenience\nexport type { QueryAST, ResolvedAST };\n\n/**\n * Supported provider names.\n */\nexport type ProviderName =\n | 'pubmed'\n | 'eric'\n | 'arxiv'\n | 'scopus'\n | 'wos'\n | 'embase';\n\n/**\n * Author information.\n */\nexport interface Author {\n /** Last name (required) */\n family: string;\n /** First name */\n given?: string;\n /** Institutional affiliation */\n affiliation?: string;\n /** ORCID identifier */\n orcid?: string;\n}\n\n/**\n * Represents a single search result from any database.\n * At least one identifier (doi, pmid, arxivId, scopusId, ericId) is required.\n */\nexport interface Article {\n // Identifiers (at least one required)\n doi?: string;\n pmid?: string;\n arxivId?: string;\n scopusId?: string;\n ericId?: string;\n\n // Required fields\n title: string;\n authors: Author[];\n source: ProviderName;\n retrievedAt: string; // ISO 8601 format\n\n // Optional fields\n abstract?: string;\n publicationDate?: string;\n journal?: string;\n volume?: string;\n issue?: string;\n pages?: string;\n rawResponse?: unknown;\n}\n\n/**\n * Result of translating a query to database-native syntax.\n */\nexport interface TranslatedQuery {\n /** Database-native query string */\n native: string;\n /** Provider that produced this translation */\n provider: ProviderName;\n /** Warnings about unsupported controlled vocabulary */\n warnings?: string[];\n}\n\n/**\n * Search options for controlling query execution.\n */\nexport interface SearchOptions {\n /** Maximum number of results to retrieve */\n maxResults?: number;\n /** Number of results per page/request */\n pageSize?: number;\n /** Date range filter */\n dateRange?: {\n start: string;\n end: string;\n };\n /** AbortSignal for cancellation */\n signal?: AbortSignal;\n}\n\n/**\n * Result of a connection test.\n */\nexport interface ConnectionTestResult {\n /** Whether the connection succeeded */\n ok: boolean;\n /** Error message if the connection failed */\n error?: string;\n}\n\n/**\n * Core provider interface that all database providers must implement.\n */\nexport interface Provider {\n /** Provider name identifier */\n readonly name: ProviderName;\n\n /**\n * Execute search and return results as async iterable (streaming).\n */\n search(\n query: TranslatedQuery,\n options?: SearchOptions\n ): AsyncIterable<Article>;\n\n /**\n * Get total hit count for a query without downloading results.\n * Used for count-only mode during query refinement.\n */\n count(query: TranslatedQuery): Promise<number>;\n\n /**\n * Convert ResolvedAST to database-native syntax.\n */\n translateQuery(resolved: ResolvedAST): TranslatedQuery;\n\n /**\n * Verify API access and credentials.\n * Returns { ok: true } on success, { ok: false, error: string } on failure.\n * Does not throw.\n */\n testConnection(): Promise<ConnectionTestResult>;\n}\n\n/**\n * Error codes used by providers.\n */\nexport type ProviderErrorCode =\n | 'PROVIDER_NOT_AVAILABLE'\n | 'API_KEY_MISSING'\n | 'API_KEY_INVALID'\n | 'ACCESS_DENIED'\n | 'RATE_LIMIT_EXCEEDED'\n | 'NETWORK_ERROR'\n | 'PARSE_ERROR'\n | 'QUERY_ERROR'\n | 'SERVER_ERROR'\n | 'TIMEOUT';\n\n/**\n * Base error type for provider errors.\n */\nexport interface ProviderError {\n code: ProviderErrorCode;\n message: string;\n provider: ProviderName;\n retryable: boolean;\n cause?: unknown;\n}\n\n/**\n * Rate limit exceeded error with retry information.\n */\nexport interface RateLimitError extends ProviderError {\n code: 'RATE_LIMIT_EXCEEDED';\n /** Time to wait before retrying (in milliseconds) */\n retryAfter?: number;\n}\n\n/**\n * Authentication/authorization error.\n */\nexport interface AuthError extends ProviderError {\n code: 'API_KEY_MISSING' | 'API_KEY_INVALID' | 'ACCESS_DENIED';\n}\n\n/**\n * Create a provider error.\n */\nexport function createProviderError(\n code: ProviderErrorCode,\n message: string,\n provider: ProviderName,\n options?: { retryable?: boolean; cause?: unknown }\n): ProviderError {\n return {\n code,\n message,\n provider,\n retryable: options?.retryable ?? false,\n cause: options?.cause,\n };\n}\n\n/**\n * Check if an error is a provider error.\n */\nexport function isProviderError(error: unknown): error is ProviderError {\n if (typeof error !== 'object' || error === null) {\n return false;\n }\n const e = error as Record<string, unknown>;\n return (\n typeof e['code'] === 'string' &&\n typeof e['message'] === 'string' &&\n typeof e['provider'] === 'string' &&\n typeof e['retryable'] === 'boolean'\n );\n}\n\n/**\n * Check if an error is a rate limit error.\n */\nexport function isRateLimitError(error: unknown): error is RateLimitError {\n return isProviderError(error) && error.code === 'RATE_LIMIT_EXCEEDED';\n}\n\n/**\n * Check if an error is an auth error.\n */\nexport function isAuthError(error: unknown): error is AuthError {\n return (\n isProviderError(error) &&\n (error.code === 'API_KEY_MISSING' || error.code === 'API_KEY_INVALID' || error.code === 'ACCESS_DENIED')\n );\n}\n\n/**\n * Represents the current state of a search for session persistence.\n * Used to resume searches after interruption or application restart.\n */\nexport interface SearchState {\n /** Provider that produced this state */\n provider: ProviderName;\n /** The query being executed */\n query: TranslatedQuery;\n /** Total number of results available */\n totalResults: number;\n /** Number of results retrieved so far */\n retrievedCount: number;\n /** When the state was last updated */\n lastUpdated: Date;\n /** Provider-specific state (e.g., PubMed webenv/querykey, or offset for other providers) */\n providerState?: unknown;\n}\n\n/**\n * Result of validating a search state for resume.\n */\nexport interface SearchResumeResult {\n /** Whether the state is valid for resuming */\n valid: boolean;\n /** Reason if the state is invalid */\n reason?: string;\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sources":["../../../src/providers/base/types.ts"],"sourcesContent":["/**\n * Provider base types for the search-hub CLI tool.\n * These types define the common interface for all database providers.\n */\n\nimport type { QueryAST, ResolvedAST } from '../../query/types.js';\n\n// Re-export QueryAST and ResolvedAST for convenience\nexport type { QueryAST, ResolvedAST };\n\n/**\n * Supported provider names.\n */\nexport type ProviderName =\n | 'pubmed'\n | 'eric'\n | 'arxiv'\n | 'scopus'\n | 'wos'\n | 'embase';\n\n/**\n * Author information.\n */\nexport interface Author {\n /** Last name (required) */\n family: string;\n /** First name */\n given?: string;\n /** Institutional affiliation */\n affiliation?: string;\n /** ORCID identifier */\n orcid?: string;\n}\n\n/**\n * Represents a single search result from any database.\n * At least one identifier (doi, pmid, arxivId, scopusId, ericId) is required.\n */\nexport interface Article {\n // Identifiers (at least one required)\n doi?: string;\n pmid?: string;\n arxivId?: string;\n scopusId?: string;\n ericId?: string;\n\n // Required fields\n title: string;\n authors: Author[];\n source: ProviderName;\n retrievedAt: string; // ISO 8601 format\n\n // Optional fields\n abstract?: string;\n publicationDate?: string;\n journal?: string;\n volume?: string;\n issue?: string;\n pages?: string;\n rawResponse?: unknown;\n}\n\n/**\n * Result of translating a query to database-native syntax.\n */\nexport interface TranslatedQuery {\n /** Database-native query string */\n native: string;\n /** Provider that produced this translation */\n provider: ProviderName;\n /** Warnings about unsupported controlled vocabulary */\n warnings?: string[];\n}\n\n/**\n * Search options for controlling query execution.\n */\nexport type SortField = 'relevance' | 'date';\n\nexport interface SearchOptions {\n /** Maximum number of results to retrieve */\n maxResults?: number;\n /** Number of results per page/request */\n pageSize?: number;\n /** Date range filter */\n dateRange?: {\n start: string;\n end: string;\n };\n /** AbortSignal for cancellation */\n signal?: AbortSignal;\n /** Sort order for results */\n sort?: SortField;\n}\n\n/**\n * Result of a connection test.\n */\nexport interface ConnectionTestResult {\n /** Whether the connection succeeded */\n ok: boolean;\n /** Error message if the connection failed */\n error?: string;\n}\n\n/**\n * Core provider interface that all database providers must implement.\n */\nexport interface Provider {\n /** Provider name identifier */\n readonly name: ProviderName;\n\n /**\n * Execute search and return results as async iterable (streaming).\n */\n search(\n query: TranslatedQuery,\n options?: SearchOptions\n ): AsyncIterable<Article>;\n\n /**\n * Get total hit count for a query without downloading results.\n * Used for count-only mode during query refinement.\n */\n count(query: TranslatedQuery): Promise<number>;\n\n /**\n * Convert ResolvedAST to database-native syntax.\n */\n translateQuery(resolved: ResolvedAST): TranslatedQuery;\n\n /**\n * Verify API access and credentials.\n * Returns { ok: true } on success, { ok: false, error: string } on failure.\n * Does not throw.\n */\n testConnection(): Promise<ConnectionTestResult>;\n\n /**\n * Get warnings from the most recent search (e.g. unsupported sort options).\n * Optional — providers that don't produce warnings need not implement this.\n */\n getWarnings?(): string[];\n}\n\n/**\n * Error codes used by providers.\n */\nexport type ProviderErrorCode =\n | 'PROVIDER_NOT_AVAILABLE'\n | 'API_KEY_MISSING'\n | 'API_KEY_INVALID'\n | 'ACCESS_DENIED'\n | 'RATE_LIMIT_EXCEEDED'\n | 'NETWORK_ERROR'\n | 'PARSE_ERROR'\n | 'QUERY_ERROR'\n | 'SERVER_ERROR'\n | 'TIMEOUT';\n\n/**\n * Base error type for provider errors.\n */\nexport interface ProviderError {\n code: ProviderErrorCode;\n message: string;\n provider: ProviderName;\n retryable: boolean;\n cause?: unknown;\n}\n\n/**\n * Rate limit exceeded error with retry information.\n */\nexport interface RateLimitError extends ProviderError {\n code: 'RATE_LIMIT_EXCEEDED';\n /** Time to wait before retrying (in milliseconds) */\n retryAfter?: number;\n}\n\n/**\n * Authentication/authorization error.\n */\nexport interface AuthError extends ProviderError {\n code: 'API_KEY_MISSING' | 'API_KEY_INVALID' | 'ACCESS_DENIED';\n}\n\n/**\n * Create a provider error.\n */\nexport function createProviderError(\n code: ProviderErrorCode,\n message: string,\n provider: ProviderName,\n options?: { retryable?: boolean; cause?: unknown }\n): ProviderError {\n return {\n code,\n message,\n provider,\n retryable: options?.retryable ?? false,\n cause: options?.cause,\n };\n}\n\n/**\n * Check if an error is a provider error.\n */\nexport function isProviderError(error: unknown): error is ProviderError {\n if (typeof error !== 'object' || error === null) {\n return false;\n }\n const e = error as Record<string, unknown>;\n return (\n typeof e['code'] === 'string' &&\n typeof e['message'] === 'string' &&\n typeof e['provider'] === 'string' &&\n typeof e['retryable'] === 'boolean'\n );\n}\n\n/**\n * Check if an error is a rate limit error.\n */\nexport function isRateLimitError(error: unknown): error is RateLimitError {\n return isProviderError(error) && error.code === 'RATE_LIMIT_EXCEEDED';\n}\n\n/**\n * Check if an error is an auth error.\n */\nexport function isAuthError(error: unknown): error is AuthError {\n return (\n isProviderError(error) &&\n (error.code === 'API_KEY_MISSING' || error.code === 'API_KEY_INVALID' || error.code === 'ACCESS_DENIED')\n );\n}\n\n/**\n * Represents the current state of a search for session persistence.\n * Used to resume searches after interruption or application restart.\n */\nexport interface SearchState {\n /** Provider that produced this state */\n provider: ProviderName;\n /** The query being executed */\n query: TranslatedQuery;\n /** Total number of results available */\n totalResults: number;\n /** Number of results retrieved so far */\n retrievedCount: number;\n /** When the state was last updated */\n lastUpdated: Date;\n /** Provider-specific state (e.g., PubMed webenv/querykey, or offset for other providers) */\n providerState?: unknown;\n}\n\n/**\n * Result of validating a search state for resume.\n */\nexport interface SearchResumeResult {\n /** Whether the state is valid for resuming */\n valid: boolean;\n /** Reason if the state is invalid */\n reason?: string;\n}\n"],"names":[],"mappings":"AA+LO,SAAS,oBACd,MACA,SACA,UACA,SACe;AACf,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,SAAS,aAAa;AAAA,IACjC,OAAO,SAAS;AAAA,EAAA;AAEpB;AAKO,SAAS,gBAAgB,OAAwC;AACtE,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,WAAO;AAAA,EACT;AACA,QAAM,IAAI;AACV,SACE,OAAO,EAAE,MAAM,MAAM,YACrB,OAAO,EAAE,SAAS,MAAM,YACxB,OAAO,EAAE,UAAU,MAAM,YACzB,OAAO,EAAE,WAAW,MAAM;AAE9B;AAKO,SAAS,iBAAiB,OAAyC;AACxE,SAAO,gBAAgB,KAAK,KAAK,MAAM,SAAS;AAClD;AAKO,SAAS,YAAY,OAAoC;AAC9D,SACE,gBAAgB,KAAK,MACpB,MAAM,SAAS,qBAAqB,MAAM,SAAS,qBAAqB,MAAM,SAAS;AAE5F;"}
|
|
@@ -28,6 +28,8 @@ export declare class ERICProvider extends BaseProvider {
|
|
|
28
28
|
private currentOffset;
|
|
29
29
|
private currentTotalResults;
|
|
30
30
|
private currentRetrievedCount;
|
|
31
|
+
/** Warnings from the most recent search */
|
|
32
|
+
private searchWarnings;
|
|
31
33
|
constructor(config?: ERICProviderOptions);
|
|
32
34
|
/**
|
|
33
35
|
* Translate a ResolvedAST to ERIC-native query syntax.
|
|
@@ -42,6 +44,7 @@ export declare class ERICProvider extends BaseProvider {
|
|
|
42
44
|
* Execute search and return results as async iterable (streaming).
|
|
43
45
|
*/
|
|
44
46
|
search(query: TranslatedQuery, options?: SearchOptions): AsyncIterable<Article>;
|
|
47
|
+
getWarnings(): string[];
|
|
45
48
|
/**
|
|
46
49
|
* Verify API access.
|
|
47
50
|
* Returns false on failure (doesn't throw).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/eric/provider.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAA2B,MAAM,kBAAkB,CAAC;AACzE,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,aAAa,EACb,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,oBAAoB,EACrB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAc,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,SAAS,CAAC;AAQ7D;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC/E;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,UAAU;IACrD,yDAAyD;IACzD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;GAGG;AACH,qBAAa,YAAa,SAAQ,YAAY;IAC5C,QAAQ,CAAC,IAAI,EAAG,MAAM,CAAU;IAEhC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAGlC,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,qBAAqB,CAAK;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/eric/provider.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAA2B,MAAM,kBAAkB,CAAC;AACzE,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,aAAa,EACb,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,oBAAoB,EACrB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAc,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,SAAS,CAAC;AAQ7D;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC/E;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,UAAU;IACrD,yDAAyD;IACzD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;GAGG;AACH,qBAAa,YAAa,SAAQ,YAAY;IAC5C,QAAQ,CAAC,IAAI,EAAG,MAAM,CAAU;IAEhC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAGlC,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,qBAAqB,CAAK;IAElC,2CAA2C;IAC3C,OAAO,CAAC,cAAc,CAAgB;gBAE1B,MAAM,GAAE,mBAAwB;IAsB5C;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,eAAe;IAItD;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAQpD;;OAEG;IACI,MAAM,CACX,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE,aAAkB,GAC1B,aAAa,CAAC,OAAO,CAAC;IAkEzB,WAAW,IAAI,MAAM,EAAE;IAIvB;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAarD;;OAEG;IACH,cAAc,IAAI,WAAW,GAAG,IAAI;IAoBpC;;OAEG;IACI,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,aAAa,CAAC,OAAO,CAAC;IAkD/D;;;OAGG;IACG,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAKtE"}
|
|
@@ -12,6 +12,8 @@ class ERICProvider extends BaseProvider {
|
|
|
12
12
|
currentOffset = 0;
|
|
13
13
|
currentTotalResults = 0;
|
|
14
14
|
currentRetrievedCount = 0;
|
|
15
|
+
/** Warnings from the most recent search */
|
|
16
|
+
searchWarnings = [];
|
|
15
17
|
constructor(config = {}) {
|
|
16
18
|
const baseConfig = {
|
|
17
19
|
rateLimit: config.rateLimit ?? 5,
|
|
@@ -53,6 +55,12 @@ class ERICProvider extends BaseProvider {
|
|
|
53
55
|
async *search(query, options = {}) {
|
|
54
56
|
const maxResults = options.maxResults ?? Number.MAX_SAFE_INTEGER;
|
|
55
57
|
const pageSize = options.pageSize ?? this.pageSize;
|
|
58
|
+
this.searchWarnings = [];
|
|
59
|
+
if (options.sort) {
|
|
60
|
+
this.searchWarnings.push(
|
|
61
|
+
`ERIC does not support sort option '${options.sort}'; using default order`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
56
64
|
this.currentQuery = query;
|
|
57
65
|
this.currentOffset = 0;
|
|
58
66
|
this.currentTotalResults = 0;
|
|
@@ -90,6 +98,9 @@ class ERICProvider extends BaseProvider {
|
|
|
90
98
|
}
|
|
91
99
|
}
|
|
92
100
|
}
|
|
101
|
+
getWarnings() {
|
|
102
|
+
return this.searchWarnings;
|
|
103
|
+
}
|
|
93
104
|
/**
|
|
94
105
|
* Verify API access.
|
|
95
106
|
* Returns false on failure (doesn't throw).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sources":["../../../src/providers/eric/provider.ts"],"sourcesContent":["/**\n * ERIC Provider implementation.\n * Provides search functionality for the ERIC education database.\n */\n\nimport { BaseProvider, type BaseProviderConfig } from '../base/provider';\nimport type {\n Article,\n TranslatedQuery,\n SearchOptions,\n ResolvedAST,\n SearchState,\n SearchResumeResult,\n ConnectionTestResult,\n} from '../base/types';\nimport { ERICClient, type ERICSearchOptions } from './client';\nimport type { ERICSearchResult } from './parser';\nimport { translateQuery } from './translator';\nimport type { ERICConfig, ERICProviderState } from './types';\n\n/** Default page size for ERIC searches */\nconst DEFAULT_PAGE_SIZE = 100;\n\n/** ERIC API base URL for connection test */\nconst ERIC_API_BASE_URL = 'https://api.ies.ed.gov/eric/';\n\n/**\n * Interface for ERIC client (for dependency injection in tests).\n */\nexport interface IERICClient {\n search(query: string, options?: ERICSearchOptions): Promise<ERICSearchResult>;\n}\n\n/**\n * Extended configuration for ERIC provider with optional client injection.\n */\nexport interface ERICProviderOptions extends ERICConfig {\n /** Optional client for dependency injection (testing) */\n client?: IERICClient;\n}\n\n/**\n * ERIC database provider.\n * Implements the Provider interface for searching ERIC.\n */\nexport class ERICProvider extends BaseProvider {\n readonly name = 'eric' as const;\n\n private readonly client: IERICClient;\n private readonly pageSize: number;\n\n // Current search state for session persistence\n private currentQuery: TranslatedQuery | null = null;\n private currentOffset = 0;\n private currentTotalResults = 0;\n private currentRetrievedCount = 0;\n\n constructor(config: ERICProviderOptions = {}) {\n // Set default rate limit for ERIC (5 req/s recommended)\n const baseConfig: BaseProviderConfig = {\n rateLimit: config.rateLimit ?? 5,\n timeout: config.timeout ?? 30000,\n retries: config.retries ?? 3,\n };\n if (config.initialBackoff !== undefined) {\n baseConfig.initialBackoff = config.initialBackoff;\n }\n if (config.maxBackoff !== undefined) {\n baseConfig.maxBackoff = config.maxBackoff;\n }\n super(baseConfig);\n\n this.pageSize = config.maxResultsPerPage ?? DEFAULT_PAGE_SIZE;\n // Allow client injection for testing\n this.client = config.client ?? new ERICClient({\n timeout: this.config.timeout,\n });\n }\n\n /**\n * Translate a ResolvedAST to ERIC-native query syntax.\n */\n translateQuery(resolved: ResolvedAST): TranslatedQuery {\n return translateQuery(resolved);\n }\n\n /**\n * Get total hit count for a query without downloading results.\n * Uses a minimal search with rows=0 to get only the total count.\n */\n async count(query: TranslatedQuery): Promise<number> {\n await this.rateLimiter.acquire();\n const result = await this.withRetry(() =>\n this.client.search(query.native, { start: 0, rows: 0 })\n );\n return result.totalResults;\n }\n\n /**\n * Execute search and return results as async iterable (streaming).\n */\n async *search(\n query: TranslatedQuery,\n options: SearchOptions = {}\n ): AsyncIterable<Article> {\n const maxResults = options.maxResults ?? Number.MAX_SAFE_INTEGER;\n const pageSize = options.pageSize ?? this.pageSize;\n\n // Initialize search state\n this.currentQuery = query;\n this.currentOffset = 0;\n this.currentTotalResults = 0;\n this.currentRetrievedCount = 0;\n\n let retrieved = 0;\n\n while (retrieved < maxResults) {\n // Wait for rate limiter\n await this.rateLimiter.acquire();\n\n // Execute search with retry\n const searchOptions: ERICSearchOptions = {\n start: this.currentOffset,\n rows: Math.min(pageSize, maxResults - retrieved),\n };\n if (options.signal) {\n searchOptions.signal = options.signal;\n }\n const result = await this.withRetry(() =>\n this.client.search(query.native, searchOptions)\n );\n\n // Update total on first page\n if (this.currentOffset === 0) {\n this.currentTotalResults = result.totalResults;\n }\n\n // No more results\n if (result.documents.length === 0) {\n break;\n }\n\n // Yield documents\n for (const doc of result.documents) {\n if (retrieved >= maxResults) {\n break;\n }\n yield doc;\n retrieved++;\n this.currentRetrievedCount = retrieved;\n }\n\n // Update offset for next page\n this.currentOffset += result.documents.length;\n\n // Check if we've retrieved all results\n if (this.currentOffset >= this.currentTotalResults) {\n break;\n }\n }\n }\n\n /**\n * Verify API access.\n * Returns false on failure (doesn't throw).\n */\n async testConnection(): Promise<ConnectionTestResult> {\n try {\n const response = await fetch(`${ERIC_API_BASE_URL}?search=test&format=json&rows=1`);\n if (!response.ok) {\n return { ok: false, error: `ERIC API returned HTTP ${response.status}` };\n }\n return { ok: true };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return { ok: false, error: message };\n }\n }\n\n /**\n * Get the current search state for session persistence.\n */\n getSearchState(): SearchState | null {\n if (!this.currentQuery) {\n return null;\n }\n\n const providerState: ERICProviderState = {\n offset: this.currentOffset,\n pageSize: this.pageSize,\n };\n\n return {\n ...this.createBaseState(\n this.currentQuery,\n this.currentTotalResults,\n this.currentRetrievedCount\n ),\n providerState,\n };\n }\n\n /**\n * Resume a search from a saved state.\n */\n async *resumeSearch(state: SearchState): AsyncIterable<Article> {\n const providerState = state.providerState as ERICProviderState | undefined;\n if (!providerState) {\n throw new Error('Invalid state: missing providerState');\n }\n\n // Restore state\n this.currentQuery = state.query;\n this.currentOffset = providerState.offset;\n this.currentTotalResults = state.totalResults;\n this.currentRetrievedCount = state.retrievedCount;\n\n const maxResults = this.currentTotalResults - this.currentRetrievedCount;\n const pageSize = providerState.pageSize ?? this.pageSize;\n let retrieved = 0;\n\n // Continue from saved offset\n while (retrieved < maxResults) {\n await this.rateLimiter.acquire();\n\n const searchOptions: ERICSearchOptions = {\n start: this.currentOffset,\n rows: Math.min(pageSize, maxResults - retrieved),\n };\n\n const result = await this.withRetry(() =>\n this.client.search(state.query.native, searchOptions)\n );\n\n if (result.documents.length === 0) {\n break;\n }\n\n for (const doc of result.documents) {\n if (retrieved >= maxResults) {\n break;\n }\n yield doc;\n retrieved++;\n this.currentRetrievedCount++;\n }\n\n this.currentOffset += result.documents.length;\n\n if (this.currentOffset >= this.currentTotalResults) {\n break;\n }\n }\n }\n\n /**\n * Validate if a saved state is still valid for resuming.\n * ERIC uses offset-based pagination, so state is always valid.\n */\n async validateState(_state: SearchState): Promise<SearchResumeResult> {\n // ERIC uses offset-based pagination with no server-side state\n // The state is always valid for resuming\n return { valid: true };\n }\n}\n"],"names":[],"mappings":";;;AAqBA,MAAM,oBAAoB;AAG1B,MAAM,oBAAoB;AAqBnB,MAAM,qBAAqB,aAAa;AAAA,EACpC,OAAO;AAAA,EAEC;AAAA,EACA;AAAA;AAAA,EAGT,eAAuC;AAAA,EACvC,gBAAgB;AAAA,EAChB,sBAAsB;AAAA,EACtB,wBAAwB;AAAA,EAEhC,YAAY,SAA8B,IAAI;AAE5C,UAAM,aAAiC;AAAA,MACrC,WAAW,OAAO,aAAa;AAAA,MAC/B,SAAS,OAAO,WAAW;AAAA,MAC3B,SAAS,OAAO,WAAW;AAAA,IAAA;AAE7B,QAAI,OAAO,mBAAmB,QAAW;AACvC,iBAAW,iBAAiB,OAAO;AAAA,IACrC;AACA,QAAI,OAAO,eAAe,QAAW;AACnC,iBAAW,aAAa,OAAO;AAAA,IACjC;AACA,UAAM,UAAU;AAEhB,SAAK,WAAW,OAAO,qBAAqB;AAE5C,SAAK,SAAS,OAAO,UAAU,IAAI,WAAW;AAAA,MAC5C,SAAS,KAAK,OAAO;AAAA,IAAA,CACtB;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,UAAwC;AACrD,WAAO,eAAe,QAAQ;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,MAAM,OAAyC;AACnD,UAAM,KAAK,YAAY,QAAA;AACvB,UAAM,SAAS,MAAM,KAAK;AAAA,MAAU,MAClC,KAAK,OAAO,OAAO,MAAM,QAAQ,EAAE,OAAO,GAAG,MAAM,EAAA,CAAG;AAAA,IAAA;AAExD,WAAO,OAAO;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,OACL,OACA,UAAyB,IACD;AACxB,UAAM,aAAa,QAAQ,cAAc,OAAO;AAChD,UAAM,WAAW,QAAQ,YAAY,KAAK;AAG1C,SAAK,eAAe;AACpB,SAAK,gBAAgB;AACrB,SAAK,sBAAsB;AAC3B,SAAK,wBAAwB;AAE7B,QAAI,YAAY;AAEhB,WAAO,YAAY,YAAY;AAE7B,YAAM,KAAK,YAAY,QAAA;AAGvB,YAAM,gBAAmC;AAAA,QACvC,OAAO,KAAK;AAAA,QACZ,MAAM,KAAK,IAAI,UAAU,aAAa,SAAS;AAAA,MAAA;AAEjD,UAAI,QAAQ,QAAQ;AAClB,sBAAc,SAAS,QAAQ;AAAA,MACjC;AACA,YAAM,SAAS,MAAM,KAAK;AAAA,QAAU,MAClC,KAAK,OAAO,OAAO,MAAM,QAAQ,aAAa;AAAA,MAAA;AAIhD,UAAI,KAAK,kBAAkB,GAAG;AAC5B,aAAK,sBAAsB,OAAO;AAAA,MACpC;AAGA,UAAI,OAAO,UAAU,WAAW,GAAG;AACjC;AAAA,MACF;AAGA,iBAAW,OAAO,OAAO,WAAW;AAClC,YAAI,aAAa,YAAY;AAC3B;AAAA,QACF;AACA,cAAM;AACN;AACA,aAAK,wBAAwB;AAAA,MAC/B;AAGA,WAAK,iBAAiB,OAAO,UAAU;AAGvC,UAAI,KAAK,iBAAiB,KAAK,qBAAqB;AAClD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,iBAAgD;AACpD,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,GAAG,iBAAiB,iCAAiC;AAClF,UAAI,CAAC,SAAS,IAAI;AAChB,eAAO,EAAE,IAAI,OAAO,OAAO,0BAA0B,SAAS,MAAM,GAAA;AAAA,MACtE;AACA,aAAO,EAAE,IAAI,KAAA;AAAA,IACf,SAAS,OAAO;AACd,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,aAAO,EAAE,IAAI,OAAO,OAAO,QAAA;AAAA,IAC7B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAqC;AACnC,QAAI,CAAC,KAAK,cAAc;AACtB,aAAO;AAAA,IACT;AAEA,UAAM,gBAAmC;AAAA,MACvC,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK;AAAA,IAAA;AAGjB,WAAO;AAAA,MACL,GAAG,KAAK;AAAA,QACN,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK;AAAA,MAAA;AAAA,MAEP;AAAA,IAAA;AAAA,EAEJ;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,aAAa,OAA4C;AAC9D,UAAM,gBAAgB,MAAM;AAC5B,QAAI,CAAC,eAAe;AAClB,YAAM,IAAI,MAAM,sCAAsC;AAAA,IACxD;AAGA,SAAK,eAAe,MAAM;AAC1B,SAAK,gBAAgB,cAAc;AACnC,SAAK,sBAAsB,MAAM;AACjC,SAAK,wBAAwB,MAAM;AAEnC,UAAM,aAAa,KAAK,sBAAsB,KAAK;AACnD,UAAM,WAAW,cAAc,YAAY,KAAK;AAChD,QAAI,YAAY;AAGhB,WAAO,YAAY,YAAY;AAC7B,YAAM,KAAK,YAAY,QAAA;AAEvB,YAAM,gBAAmC;AAAA,QACvC,OAAO,KAAK;AAAA,QACZ,MAAM,KAAK,IAAI,UAAU,aAAa,SAAS;AAAA,MAAA;AAGjD,YAAM,SAAS,MAAM,KAAK;AAAA,QAAU,MAClC,KAAK,OAAO,OAAO,MAAM,MAAM,QAAQ,aAAa;AAAA,MAAA;AAGtD,UAAI,OAAO,UAAU,WAAW,GAAG;AACjC;AAAA,MACF;AAEA,iBAAW,OAAO,OAAO,WAAW;AAClC,YAAI,aAAa,YAAY;AAC3B;AAAA,QACF;AACA,cAAM;AACN;AACA,aAAK;AAAA,MACP;AAEA,WAAK,iBAAiB,OAAO,UAAU;AAEvC,UAAI,KAAK,iBAAiB,KAAK,qBAAqB;AAClD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAc,QAAkD;AAGpE,WAAO,EAAE,OAAO,KAAA;AAAA,EAClB;AACF;"}
|
|
1
|
+
{"version":3,"file":"provider.js","sources":["../../../src/providers/eric/provider.ts"],"sourcesContent":["/**\n * ERIC Provider implementation.\n * Provides search functionality for the ERIC education database.\n */\n\nimport { BaseProvider, type BaseProviderConfig } from '../base/provider';\nimport type {\n Article,\n TranslatedQuery,\n SearchOptions,\n ResolvedAST,\n SearchState,\n SearchResumeResult,\n ConnectionTestResult,\n} from '../base/types';\nimport { ERICClient, type ERICSearchOptions } from './client';\nimport type { ERICSearchResult } from './parser';\nimport { translateQuery } from './translator';\nimport type { ERICConfig, ERICProviderState } from './types';\n\n/** Default page size for ERIC searches */\nconst DEFAULT_PAGE_SIZE = 100;\n\n/** ERIC API base URL for connection test */\nconst ERIC_API_BASE_URL = 'https://api.ies.ed.gov/eric/';\n\n/**\n * Interface for ERIC client (for dependency injection in tests).\n */\nexport interface IERICClient {\n search(query: string, options?: ERICSearchOptions): Promise<ERICSearchResult>;\n}\n\n/**\n * Extended configuration for ERIC provider with optional client injection.\n */\nexport interface ERICProviderOptions extends ERICConfig {\n /** Optional client for dependency injection (testing) */\n client?: IERICClient;\n}\n\n/**\n * ERIC database provider.\n * Implements the Provider interface for searching ERIC.\n */\nexport class ERICProvider extends BaseProvider {\n readonly name = 'eric' as const;\n\n private readonly client: IERICClient;\n private readonly pageSize: number;\n\n // Current search state for session persistence\n private currentQuery: TranslatedQuery | null = null;\n private currentOffset = 0;\n private currentTotalResults = 0;\n private currentRetrievedCount = 0;\n\n /** Warnings from the most recent search */\n private searchWarnings: string[] = [];\n\n constructor(config: ERICProviderOptions = {}) {\n // Set default rate limit for ERIC (5 req/s recommended)\n const baseConfig: BaseProviderConfig = {\n rateLimit: config.rateLimit ?? 5,\n timeout: config.timeout ?? 30000,\n retries: config.retries ?? 3,\n };\n if (config.initialBackoff !== undefined) {\n baseConfig.initialBackoff = config.initialBackoff;\n }\n if (config.maxBackoff !== undefined) {\n baseConfig.maxBackoff = config.maxBackoff;\n }\n super(baseConfig);\n\n this.pageSize = config.maxResultsPerPage ?? DEFAULT_PAGE_SIZE;\n // Allow client injection for testing\n this.client = config.client ?? new ERICClient({\n timeout: this.config.timeout,\n });\n }\n\n /**\n * Translate a ResolvedAST to ERIC-native query syntax.\n */\n translateQuery(resolved: ResolvedAST): TranslatedQuery {\n return translateQuery(resolved);\n }\n\n /**\n * Get total hit count for a query without downloading results.\n * Uses a minimal search with rows=0 to get only the total count.\n */\n async count(query: TranslatedQuery): Promise<number> {\n await this.rateLimiter.acquire();\n const result = await this.withRetry(() =>\n this.client.search(query.native, { start: 0, rows: 0 })\n );\n return result.totalResults;\n }\n\n /**\n * Execute search and return results as async iterable (streaming).\n */\n async *search(\n query: TranslatedQuery,\n options: SearchOptions = {}\n ): AsyncIterable<Article> {\n const maxResults = options.maxResults ?? Number.MAX_SAFE_INTEGER;\n const pageSize = options.pageSize ?? this.pageSize;\n\n // ERIC does not support sort — emit warning if specified\n this.searchWarnings = [];\n if (options.sort) {\n this.searchWarnings.push(\n `ERIC does not support sort option '${options.sort}'; using default order`\n );\n }\n\n // Initialize search state\n this.currentQuery = query;\n this.currentOffset = 0;\n this.currentTotalResults = 0;\n this.currentRetrievedCount = 0;\n\n let retrieved = 0;\n\n while (retrieved < maxResults) {\n // Wait for rate limiter\n await this.rateLimiter.acquire();\n\n // Execute search with retry\n const searchOptions: ERICSearchOptions = {\n start: this.currentOffset,\n rows: Math.min(pageSize, maxResults - retrieved),\n };\n if (options.signal) {\n searchOptions.signal = options.signal;\n }\n const result = await this.withRetry(() =>\n this.client.search(query.native, searchOptions)\n );\n\n // Update total on first page\n if (this.currentOffset === 0) {\n this.currentTotalResults = result.totalResults;\n }\n\n // No more results\n if (result.documents.length === 0) {\n break;\n }\n\n // Yield documents\n for (const doc of result.documents) {\n if (retrieved >= maxResults) {\n break;\n }\n yield doc;\n retrieved++;\n this.currentRetrievedCount = retrieved;\n }\n\n // Update offset for next page\n this.currentOffset += result.documents.length;\n\n // Check if we've retrieved all results\n if (this.currentOffset >= this.currentTotalResults) {\n break;\n }\n }\n }\n\n getWarnings(): string[] {\n return this.searchWarnings;\n }\n\n /**\n * Verify API access.\n * Returns false on failure (doesn't throw).\n */\n async testConnection(): Promise<ConnectionTestResult> {\n try {\n const response = await fetch(`${ERIC_API_BASE_URL}?search=test&format=json&rows=1`);\n if (!response.ok) {\n return { ok: false, error: `ERIC API returned HTTP ${response.status}` };\n }\n return { ok: true };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return { ok: false, error: message };\n }\n }\n\n /**\n * Get the current search state for session persistence.\n */\n getSearchState(): SearchState | null {\n if (!this.currentQuery) {\n return null;\n }\n\n const providerState: ERICProviderState = {\n offset: this.currentOffset,\n pageSize: this.pageSize,\n };\n\n return {\n ...this.createBaseState(\n this.currentQuery,\n this.currentTotalResults,\n this.currentRetrievedCount\n ),\n providerState,\n };\n }\n\n /**\n * Resume a search from a saved state.\n */\n async *resumeSearch(state: SearchState): AsyncIterable<Article> {\n const providerState = state.providerState as ERICProviderState | undefined;\n if (!providerState) {\n throw new Error('Invalid state: missing providerState');\n }\n\n // Restore state\n this.currentQuery = state.query;\n this.currentOffset = providerState.offset;\n this.currentTotalResults = state.totalResults;\n this.currentRetrievedCount = state.retrievedCount;\n\n const maxResults = this.currentTotalResults - this.currentRetrievedCount;\n const pageSize = providerState.pageSize ?? this.pageSize;\n let retrieved = 0;\n\n // Continue from saved offset\n while (retrieved < maxResults) {\n await this.rateLimiter.acquire();\n\n const searchOptions: ERICSearchOptions = {\n start: this.currentOffset,\n rows: Math.min(pageSize, maxResults - retrieved),\n };\n\n const result = await this.withRetry(() =>\n this.client.search(state.query.native, searchOptions)\n );\n\n if (result.documents.length === 0) {\n break;\n }\n\n for (const doc of result.documents) {\n if (retrieved >= maxResults) {\n break;\n }\n yield doc;\n retrieved++;\n this.currentRetrievedCount++;\n }\n\n this.currentOffset += result.documents.length;\n\n if (this.currentOffset >= this.currentTotalResults) {\n break;\n }\n }\n }\n\n /**\n * Validate if a saved state is still valid for resuming.\n * ERIC uses offset-based pagination, so state is always valid.\n */\n async validateState(_state: SearchState): Promise<SearchResumeResult> {\n // ERIC uses offset-based pagination with no server-side state\n // The state is always valid for resuming\n return { valid: true };\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RateLimiter } from '../base/index.js';
|
|
2
|
-
import { ESearchResponse, PubMedArticle, PubMedConfig } from './types.js';
|
|
2
|
+
import { ELinkOptions, ELinkResponse, RelatedArticle, ESearchResponse, PubMedArticle, PubMedConfig } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Options for esearch API call.
|
|
5
5
|
*/
|
|
@@ -10,6 +10,8 @@ export interface SearchOptions {
|
|
|
10
10
|
retmax?: number;
|
|
11
11
|
/** Use history server for large result sets */
|
|
12
12
|
useHistory?: boolean;
|
|
13
|
+
/** Sort parameter for esearch (e.g. 'relevance', 'pub_date') */
|
|
14
|
+
sort?: string;
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* Options for history-based fetch.
|
|
@@ -48,6 +50,18 @@ export declare class PubMedClient {
|
|
|
48
50
|
* Fetch articles using history server (webenv/querykey).
|
|
49
51
|
*/
|
|
50
52
|
fetchFromHistory(options: HistoryFetchOptions): Promise<PubMedArticle[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Find related articles using ELink API with neighbor_score.
|
|
55
|
+
*/
|
|
56
|
+
findRelated(options: ELinkOptions): Promise<ELinkResponse[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Find related articles with deduplication across multiple seeds.
|
|
59
|
+
*
|
|
60
|
+
* Merges related articles from all seeds, keeps highest score for duplicates,
|
|
61
|
+
* excludes seed PMIDs from results, sorts by score descending, and truncates
|
|
62
|
+
* to maxResults.
|
|
63
|
+
*/
|
|
64
|
+
findRelatedMerged(options: ELinkOptions): Promise<RelatedArticle[]>;
|
|
51
65
|
/**
|
|
52
66
|
* Fetch with error handling for HTTP responses.
|
|
53
67
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/pubmed/client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAuB,MAAM,kBAAkB,CAAC;AAGpE,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/pubmed/client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAuB,MAAM,kBAAkB,CAAC;AAGpE,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI5H;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,mCAAmC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,sBAAsB;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;gBAE9B,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW;IAK1D;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,eAAe,CAAC;IAsClF;;;OAGG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwBjD;;OAEG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IA2BtD;;OAEG;IACG,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IA0B9E;;OAEG;IACG,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAwClE;;;;;;OAMG;IACG,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IA8BzE;;OAEG;YACW,sBAAsB;IA2CpC;;OAEG;IACH,OAAO,CAAC,WAAW;CAQpB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createProviderError } from "../base/types.js";
|
|
2
2
|
import "../base/mock-provider.js";
|
|
3
|
-
import { parseESearchResponse, parseEFetchResponse } from "./parser.js";
|
|
3
|
+
import { parseESearchResponse, parseEFetchResponse, parseELinkResponse } from "./parser.js";
|
|
4
4
|
const BASE_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
|
|
5
5
|
class PubMedClient {
|
|
6
6
|
config;
|
|
@@ -32,6 +32,9 @@ class PubMedClient {
|
|
|
32
32
|
if (options.useHistory) {
|
|
33
33
|
params.set("usehistory", "y");
|
|
34
34
|
}
|
|
35
|
+
if (options.sort) {
|
|
36
|
+
params.set("sort", options.sort);
|
|
37
|
+
}
|
|
35
38
|
const url = `${BASE_URL}/esearch.fcgi?${params.toString()}`;
|
|
36
39
|
const response = await this.fetchWithErrorHandling(url);
|
|
37
40
|
const xml = await response.text();
|
|
@@ -109,6 +112,66 @@ class PubMedClient {
|
|
|
109
112
|
this.rateLimiter.resetBackoff();
|
|
110
113
|
return parseEFetchResponse(xml).articles;
|
|
111
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Find related articles using ELink API with neighbor_score.
|
|
117
|
+
*/
|
|
118
|
+
async findRelated(options) {
|
|
119
|
+
await this.rateLimiter.acquire();
|
|
120
|
+
const params = new URLSearchParams({
|
|
121
|
+
dbfrom: "pubmed",
|
|
122
|
+
db: "pubmed",
|
|
123
|
+
cmd: "neighbor_score",
|
|
124
|
+
retmode: "xml",
|
|
125
|
+
email: this.config.email
|
|
126
|
+
});
|
|
127
|
+
for (const id of options.ids) {
|
|
128
|
+
params.append("id", id);
|
|
129
|
+
}
|
|
130
|
+
if (this.config.apiKey) {
|
|
131
|
+
params.set("api_key", this.config.apiKey);
|
|
132
|
+
}
|
|
133
|
+
if (options.term) {
|
|
134
|
+
params.set("term", options.term);
|
|
135
|
+
}
|
|
136
|
+
const url = `${BASE_URL}/elink.fcgi?${params.toString()}`;
|
|
137
|
+
const response = await this.fetchWithErrorHandling(url);
|
|
138
|
+
const xml = await response.text();
|
|
139
|
+
this.rateLimiter.resetBackoff();
|
|
140
|
+
const results = parseELinkResponse(xml);
|
|
141
|
+
if (options.maxResults !== void 0) {
|
|
142
|
+
for (const result of results) {
|
|
143
|
+
result.relatedIds = result.relatedIds.slice(0, options.maxResults);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Find related articles with deduplication across multiple seeds.
|
|
150
|
+
*
|
|
151
|
+
* Merges related articles from all seeds, keeps highest score for duplicates,
|
|
152
|
+
* excludes seed PMIDs from results, sorts by score descending, and truncates
|
|
153
|
+
* to maxResults.
|
|
154
|
+
*/
|
|
155
|
+
async findRelatedMerged(options) {
|
|
156
|
+
const { maxResults, ...findRelatedOptions } = options;
|
|
157
|
+
const responses = await this.findRelated(findRelatedOptions);
|
|
158
|
+
const seedSet = new Set(options.ids);
|
|
159
|
+
const scoreMap = /* @__PURE__ */ new Map();
|
|
160
|
+
for (const response of responses) {
|
|
161
|
+
for (const related of response.relatedIds) {
|
|
162
|
+
if (seedSet.has(related.id)) continue;
|
|
163
|
+
const existing = scoreMap.get(related.id);
|
|
164
|
+
if (existing === void 0 || related.score > existing) {
|
|
165
|
+
scoreMap.set(related.id, related.score);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const merged = Array.from(scoreMap.entries()).map(([id, score]) => ({ id, score })).sort((a, b) => b.score - a.score);
|
|
170
|
+
if (maxResults !== void 0) {
|
|
171
|
+
return merged.slice(0, maxResults);
|
|
172
|
+
}
|
|
173
|
+
return merged;
|
|
174
|
+
}
|
|
112
175
|
/**
|
|
113
176
|
* Fetch with error handling for HTTP responses.
|
|
114
177
|
*/
|