@ncukondo/search-hub 0.13.0 → 0.15.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/README.md +39 -9
- package/dist/cli/commands/check.d.ts +34 -0
- package/dist/cli/commands/check.d.ts.map +1 -0
- package/dist/cli/commands/check.js +126 -0
- package/dist/cli/commands/check.js.map +1 -0
- package/dist/cli/commands/export.d.ts +5 -3
- package/dist/cli/commands/export.d.ts.map +1 -1
- package/dist/cli/commands/export.js +0 -4
- package/dist/cli/commands/export.js.map +1 -1
- package/dist/cli/commands/query/init.d.ts.map +1 -1
- package/dist/cli/commands/query/init.js +17 -7
- package/dist/cli/commands/query/init.js.map +1 -1
- package/dist/cli/commands/query/inspect.d.ts +36 -0
- package/dist/cli/commands/query/inspect.d.ts.map +1 -0
- package/dist/cli/commands/query/inspect.js +155 -0
- package/dist/cli/commands/query/inspect.js.map +1 -0
- package/dist/cli/commands/query/translate.d.ts.map +1 -1
- package/dist/cli/commands/query/translate.js +3 -1
- package/dist/cli/commands/query/translate.js.map +1 -1
- package/dist/cli/commands/query-filter.d.ts +13 -0
- package/dist/cli/commands/query-filter.d.ts.map +1 -0
- package/dist/cli/commands/query-filter.js +149 -0
- package/dist/cli/commands/query-filter.js.map +1 -0
- package/dist/cli/commands/results.d.ts +3 -3
- package/dist/cli/commands/results.d.ts.map +1 -1
- package/dist/cli/commands/results.js +12 -3
- package/dist/cli/commands/results.js.map +1 -1
- package/dist/cli/commands/search-executor.d.ts.map +1 -1
- package/dist/cli/commands/search-executor.js +12 -7
- package/dist/cli/commands/search-executor.js.map +1 -1
- package/dist/cli/e2e-helpers.d.ts +5 -2
- package/dist/cli/e2e-helpers.d.ts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +228 -45
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/providers/arxiv/provider.d.ts +3 -3
- package/dist/providers/arxiv/provider.d.ts.map +1 -1
- package/dist/providers/arxiv/provider.js +3 -3
- package/dist/providers/arxiv/provider.js.map +1 -1
- package/dist/providers/arxiv/translator.d.ts +3 -3
- package/dist/providers/arxiv/translator.d.ts.map +1 -1
- package/dist/providers/arxiv/translator.js +6 -8
- package/dist/providers/arxiv/translator.js.map +1 -1
- package/dist/providers/base/index.d.ts +1 -1
- package/dist/providers/base/index.d.ts.map +1 -1
- package/dist/providers/base/mock-provider.d.ts +2 -2
- package/dist/providers/base/mock-provider.d.ts.map +1 -1
- package/dist/providers/base/mock-provider.js +2 -9
- package/dist/providers/base/mock-provider.js.map +1 -1
- package/dist/providers/base/provider.d.ts +3 -3
- package/dist/providers/base/provider.d.ts.map +1 -1
- package/dist/providers/base/provider.js.map +1 -1
- package/dist/providers/base/types.d.ts +4 -6
- 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 -3
- package/dist/providers/eric/provider.d.ts.map +1 -1
- package/dist/providers/eric/provider.js +3 -3
- package/dist/providers/eric/provider.js.map +1 -1
- package/dist/providers/eric/translator.d.ts +5 -5
- package/dist/providers/eric/translator.d.ts.map +1 -1
- package/dist/providers/eric/translator.js +6 -7
- package/dist/providers/eric/translator.js.map +1 -1
- package/dist/providers/pubmed/provider.d.ts +3 -3
- package/dist/providers/pubmed/provider.d.ts.map +1 -1
- package/dist/providers/pubmed/provider.js +3 -3
- package/dist/providers/pubmed/provider.js.map +1 -1
- package/dist/providers/pubmed/translator.d.ts +3 -3
- package/dist/providers/pubmed/translator.d.ts.map +1 -1
- package/dist/providers/pubmed/translator.js +4 -23
- package/dist/providers/pubmed/translator.js.map +1 -1
- package/dist/providers/scopus/provider.d.ts +3 -3
- package/dist/providers/scopus/provider.d.ts.map +1 -1
- package/dist/providers/scopus/provider.js +3 -3
- package/dist/providers/scopus/provider.js.map +1 -1
- package/dist/providers/scopus/translator.d.ts +3 -3
- package/dist/providers/scopus/translator.d.ts.map +1 -1
- package/dist/providers/scopus/translator.js +7 -9
- package/dist/providers/scopus/translator.js.map +1 -1
- package/dist/query/index.d.ts +3 -2
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/json-schema.d.ts.map +1 -1
- package/dist/query/json-schema.js +20 -11
- package/dist/query/json-schema.js.map +1 -1
- package/dist/query/mesh-lookup.d.ts.map +1 -1
- package/dist/query/mesh-lookup.js +66 -3
- package/dist/query/mesh-lookup.js.map +1 -1
- package/dist/query/resolver.d.ts +14 -0
- package/dist/query/resolver.d.ts.map +1 -0
- package/dist/query/resolver.js +61 -0
- package/dist/query/resolver.js.map +1 -0
- package/dist/query/types.d.ts +31 -11
- package/dist/query/types.d.ts.map +1 -1
- package/dist/query/validator.d.ts +659 -348
- package/dist/query/validator.d.ts.map +1 -1
- package/dist/query/validator.js +70 -30
- package/dist/query/validator.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-executor.js","sources":["../../../src/cli/commands/search-executor.ts"],"sourcesContent":["/**\n * Search executor for CLI search command.\n *\n * Handles the actual execution of searches across multiple providers,\n * including session creation, progress display, and result storage.\n */\nimport { readFile, writeFile, appendFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { createHash } from 'node:crypto';\nimport type { SearchCommandOptions, CountResult, PreviewResult } from './search.js';\nimport type { Config } from '../../config/index.js';\nimport type {\n Article,\n Provider,\n ProviderName,\n TranslatedQuery,\n} from '../../providers/base/types.js';\nimport { isProviderError } from '../../providers/base/types.js';\nimport type { QueryAST } from '../../query/types.js';\nimport { parseQueryString } from '../../query/index.js';\nimport {\n createSession,\n updateDatabaseStatus,\n updateSessionStatus,\n} from '../../session/manager.js';\nimport { MultiProviderProgress } from '../utils/progress.js';\nimport { PubMedProvider } from '../../providers/pubmed/provider.js';\nimport type { PubMedConfig } from '../../providers/pubmed/types.js';\nimport { ERICProvider } from '../../providers/eric/provider.js';\nimport { ArxivProvider } from '../../providers/arxiv/provider.js';\nimport { ScopusProvider } from '../../providers/scopus/provider.js';\nimport type { ScopusConfig } from '../../providers/scopus/types.js';\nimport { translateQuery as translatePubmed } from '../../providers/pubmed/translator.js';\nimport { translateQuery as translateEric } from '../../providers/eric/translator.js';\nimport { translateQuery as translateArxiv } from '../../providers/arxiv/translator.js';\nimport { translateQuery as translateScopus } from '../../providers/scopus/translator.js';\nimport { stringify as stringifyYaml } from 'yaml';\nimport { registerArticles, saveRegistrationRecord } from '../../integration/register.js';\nimport { buildFailureErrorMessage, buildPartialErrorMessage } from './search-utils.js';\nimport { getConfigDir } from '../../config/paths.js';\nimport type { RegistrationRecord } from '../../integration/types.js';\nimport { checkRefAvailable } from '../../integration/ref-cli.js';\nimport { convertResultsToYaml, loadResults } from '../../session/results-io.js';\n\n/**\n * Result of a search execution.\n */\nexport interface SearchExecutionResult {\n success: boolean;\n sessionId?: string;\n results?: Record<string, { hits: number; retrieved: number; error?: string; warnings?: string[] }>;\n error?: string;\n autoRegisterResult?: RegistrationRecord;\n sessionStatus: 'completed' | 'partial' | 'failed';\n}\n\n/**\n * Available providers that are implemented.\n */\nconst IMPLEMENTED_PROVIDERS: ProviderName[] = ['pubmed', 'eric', 'arxiv', 'scopus'];\n\n/**\n * Check if a provider has the required configuration (e.g., API keys).\n * Providers that require no special configuration always return true.\n */\nexport function isProviderConfigured(name: ProviderName, config: Config): boolean {\n switch (name) {\n case 'scopus':\n return !!config.providers.scopus.api_key;\n default:\n return true; // pubmed, eric, arxiv require no API key\n }\n}\n\n/**\n * Create a provider instance for the given provider name.\n */\nexport function createProviderInstance(\n name: ProviderName,\n config: Config\n): Provider | null {\n const providerConfig = config.providers[name];\n\n switch (name) {\n case 'pubmed': {\n if (!providerConfig.email) {\n const configPath = getConfigDir();\n console.warn(\n `Warning: No email configured for PubMed.\\n` +\n ` → Edit ${configPath}/config.toml and set providers.pubmed.email\\n` +\n ` → Or run: search-hub config providers.pubmed.email \"your@email.com\"`\n );\n }\n const pubmedOpts: PubMedConfig = {\n email: providerConfig.email ?? 'search-hub@example.com',\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n };\n if (providerConfig.api_key) {\n pubmedOpts.apiKey = providerConfig.api_key;\n }\n return new PubMedProvider(pubmedOpts);\n }\n case 'eric':\n return new ERICProvider({\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n });\n case 'arxiv':\n return new ArxivProvider({\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n });\n case 'scopus': {\n if (!providerConfig.api_key) {\n console.warn(\n `Warning: Scopus requires an API key. Set providers.scopus.api_key in config.\\n` +\n ` → Get an API key at https://dev.elsevier.com/\\n` +\n ` → Run: search-hub config providers.scopus.api_key \"your-key\"`\n );\n return null;\n }\n const scopusOpts: ScopusConfig = {\n apiKey: providerConfig.api_key,\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n };\n if (providerConfig.inst_token) {\n scopusOpts.instToken = providerConfig.inst_token;\n }\n return new ScopusProvider(scopusOpts);\n }\n default:\n throw new Error(`Provider '${name}' is not implemented`);\n }\n}\n\n/**\n * Translate a query AST for a specific provider.\n */\nfunction translateQueryForProvider(\n ast: QueryAST,\n provider: ProviderName\n): TranslatedQuery {\n switch (provider) {\n case 'pubmed':\n return translatePubmed(ast);\n case 'eric':\n return translateEric(ast);\n case 'arxiv':\n return translateArxiv(ast);\n case 'scopus':\n return translateScopus(ast);\n default:\n throw new Error(`No translator for provider '${provider}'`);\n }\n}\n\n/**\n * Get enabled providers from config, optionally filtered by user selection.\n */\nfunction getEnabledProviders(\n config: Config,\n requestedProviders?: ProviderName[]\n): ProviderName[] {\n const enabledInConfig = IMPLEMENTED_PROVIDERS.filter(\n (name) => config.providers[name].enabled\n );\n\n if (requestedProviders && requestedProviders.length > 0) {\n return requestedProviders.filter((p) => enabledInConfig.includes(p));\n }\n\n return enabledInConfig;\n}\n\n/**\n * Execute a search across multiple providers.\n */\nexport async function executeSearch(\n options: SearchCommandOptions,\n sessionsDir: string,\n config: Config,\n showProgress = true\n): Promise<SearchExecutionResult> {\n let ast: QueryAST | undefined;\n let queryContent: string;\n let queryFile: string;\n\n // Handle direct query mode\n if (options.directQuery && options.providers && options.providers.length === 1) {\n queryFile = 'direct-query';\n\n // For direct query, we create a minimal AST structure\n ast = {\n name: options.sessionName ?? 'direct-query',\n blocks: [\n {\n field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n overrides: {},\n };\n\n // Generate YAML safely using yaml library to handle special characters\n queryContent = stringifyYaml({\n name: ast.name,\n blocks: ast.blocks,\n filters: ast.filters,\n });\n } else if (options.queryFile) {\n // Parse query file\n try {\n queryContent = await readFile(options.queryFile, 'utf-8');\n ast = parseQueryString(queryContent);\n queryFile = options.queryFile;\n } catch (error) {\n return {\n success: false,\n sessionStatus: 'failed',\n error: `Failed to parse query file: ${error instanceof Error ? error.message : error}`,\n };\n }\n } else {\n return {\n success: false,\n sessionStatus: 'failed',\n error: 'Either queryFile or directQuery with provider is required',\n };\n }\n\n // Determine which providers to use\n let providers = getEnabledProviders(config, options.providers);\n\n // In default mode (no --db), skip unconfigured providers with a warning\n const isExplicitSelection = options.providers && options.providers.length > 0;\n if (!isExplicitSelection) {\n const skipped: ProviderName[] = [];\n providers = providers.filter((name) => {\n if (!isProviderConfigured(name, config)) {\n skipped.push(name);\n return false;\n }\n return true;\n });\n for (const name of skipped) {\n console.warn(\n `Skipping ${name}: API key not configured (use --db ${name} to force)`\n );\n }\n }\n\n if (providers.length === 0) {\n return {\n success: false,\n sessionStatus: 'failed',\n error: 'No providers enabled or selected',\n };\n }\n\n // Create query hash\n const queryHash = createHash('sha256').update(queryContent).digest('hex').slice(0, 8);\n\n // Create session\n let session;\n try {\n const sessionOpts: Parameters<typeof createSession>[0] = {\n name: options.sessionName ?? ast.name,\n queryFile,\n queryContent,\n queryHash,\n targets: providers,\n sessionsDir,\n };\n if (ast.description) {\n sessionOpts.description = ast.description;\n }\n session = await createSession(sessionOpts);\n } catch (error) {\n return {\n success: false,\n sessionStatus: 'failed',\n error: `Failed to create session: ${error instanceof Error ? error.message : error}`,\n };\n }\n\n const sessionId = session.id;\n const results: Record<string, { hits: number; retrieved: number; error?: string; warnings?: string[] }> = {};\n\n // Create progress display if enabled\n let progress: MultiProviderProgress | undefined;\n if (showProgress && process.stdout.isTTY) {\n progress = new MultiProviderProgress(providers);\n }\n\n // Execute search for each provider\n for (const providerName of providers) {\n try {\n // Create provider instance\n const provider = createProviderInstance(providerName, config);\n\n // Skip provider if it could not be created (e.g. missing configuration)\n if (provider === null) {\n const configError = `${providerName}: provider configuration incomplete. See warning above for details.`;\n results[providerName] = { hits: 0, retrieved: 0, error: configError };\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'failed',\n completedAt: new Date().toISOString(),\n error: {\n code: 'CONFIG_ERROR',\n message: configError,\n retryable: false,\n },\n },\n sessionsDir\n );\n continue;\n }\n\n // Translate query\n let translatedQuery: TranslatedQuery;\n if (options.directQuery && options.providers?.length === 1) {\n // For direct query, use the native query string directly\n translatedQuery = {\n native: options.directQuery,\n provider: providerName,\n };\n } else {\n translatedQuery = translateQueryForProvider(ast, providerName);\n }\n\n // Write translated query to session\n const queryPath = join(sessionsDir, sessionId, `${providerName}_query.txt`);\n await writeFile(queryPath, translatedQuery.native, 'utf-8');\n\n // Update database status to in_progress\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'in_progress',\n startedAt: new Date().toISOString(),\n },\n sessionsDir\n );\n\n // Prepare results file path\n const resultsPath = join(sessionsDir, sessionId, `${providerName}_results.jsonl`);\n\n // Execute search\n let retrievedCount = 0;\n let totalHits = 0;\n\n progress?.update(providerName, 0, 0, 'in_progress');\n\n const searchOptions = {\n maxResults: options.maxResults ?? config.providers[providerName].max_results,\n };\n\n for await (const article of provider.search(translatedQuery, searchOptions)) {\n retrievedCount++;\n\n // Write article to JSONL file\n await appendFile(resultsPath, JSON.stringify(article) + '\\n', 'utf-8');\n\n // Update progress (estimate total from first batch)\n if (totalHits === 0) {\n // Estimate total - this is provider-dependent, we'll use retrieved count as minimum\n totalHits = Math.max(retrievedCount * 10, 100);\n }\n progress?.update(providerName, retrievedCount, totalHits, 'in_progress');\n }\n\n // Update final totals\n totalHits = retrievedCount; // Use actual count as final total\n\n // Mark as completed\n progress?.complete(providerName);\n\n // Convert JSONL to YAML for human-readable view\n const yamlFilename = `${providerName}_results.yaml`;\n const yamlPath = join(sessionsDir, sessionId, yamlFilename);\n await convertResultsToYaml(resultsPath, yamlPath, {\n provider: providerName,\n queryName: ast.name,\n });\n\n // Update database status\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'completed',\n completedAt: new Date().toISOString(),\n totalHits,\n retrievedCount,\n files: {\n query: `${providerName}_query.txt`,\n results: `${providerName}_results.jsonl`,\n resultsYaml: yamlFilename,\n },\n },\n sessionsDir\n );\n\n // Collect warnings if provider supports them\n const providerWarnings = 'getWarnings' in provider && typeof (provider as any).getWarnings === 'function'\n ? (provider as any).getWarnings() as string[]\n : undefined;\n results[providerName] = { hits: totalHits, retrieved: retrievedCount, ...(providerWarnings && providerWarnings.length > 0 && { warnings: providerWarnings }) };\n } catch (error) {\n const errorMessage = error instanceof Error\n ? error.message\n : isProviderError(error)\n ? error.message\n : String(error);\n\n progress?.fail(providerName, errorMessage);\n\n // Update database status with error\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'failed',\n completedAt: new Date().toISOString(),\n error: {\n code: 'SEARCH_ERROR',\n message: errorMessage,\n retryable: true,\n },\n },\n sessionsDir\n );\n\n results[providerName] = { hits: 0, retrieved: 0, error: errorMessage };\n }\n }\n\n // Stop progress display\n progress?.stop();\n\n // Determine overall session status\n const anyFailed = providers.some((p) => {\n const r = results[p];\n return r && r.error !== undefined;\n });\n const anySucceeded = providers.some((p) => {\n const r = results[p];\n return r && r.retrieved > 0;\n });\n\n let sessionStatus: 'completed' | 'partial' | 'failed';\n if (!anyFailed) {\n sessionStatus = 'completed';\n } else if (anySucceeded) {\n sessionStatus = 'partial';\n } else {\n sessionStatus = 'failed';\n }\n\n // Update session status\n await updateSessionStatus(sessionId, sessionStatus, sessionsDir);\n\n if (sessionStatus === 'failed') {\n return {\n success: false,\n sessionId,\n sessionStatus,\n results,\n error: buildFailureErrorMessage(results),\n };\n }\n\n // In strict mode, partial success is treated as failure\n if (options.strict && sessionStatus === 'partial') {\n return {\n success: false,\n sessionId,\n sessionStatus,\n results,\n error: buildPartialErrorMessage(results),\n };\n }\n\n // Auto-register if enabled\n let autoRegisterResult: RegistrationRecord | undefined;\n if (\n config.integration.reference_manager.enabled &&\n config.integration.reference_manager.auto_register\n ) {\n const refAvailable = await checkRefAvailable();\n if (refAvailable) {\n // Load all articles from results files\n const allArticles = await loadArticlesFromSession(sessionsDir, sessionId, providers);\n\n if (allArticles.length > 0) {\n autoRegisterResult = await registerArticles(allArticles, {\n sessionId,\n sessionDir: join(sessionsDir, sessionId),\n withAbstracts: config.integration.reference_manager.with_abstracts,\n });\n\n // Save registration record\n await saveRegistrationRecord(join(sessionsDir, sessionId), autoRegisterResult);\n }\n }\n }\n\n const result: SearchExecutionResult = {\n success: true,\n sessionId,\n results,\n sessionStatus,\n };\n\n if (autoRegisterResult) {\n result.autoRegisterResult = autoRegisterResult;\n }\n\n return result;\n}\n\n/**\n * Execute count-only mode: get hit counts from each provider without downloading results.\n * No session is created.\n */\nexport async function executeCountOnly(\n options: SearchCommandOptions,\n config: Config\n): Promise<CountResult[]> {\n let ast: QueryAST | undefined;\n\n // Handle direct query mode\n if (options.directQuery && options.providers && options.providers.length === 1) {\n ast = {\n name: options.sessionName ?? 'direct-query',\n blocks: [\n {\n field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n overrides: {},\n };\n } else if (options.queryFile) {\n const queryContent = await readFile(options.queryFile, 'utf-8');\n ast = parseQueryString(queryContent);\n } else {\n return [];\n }\n\n // Determine which providers to use\n let providers = getEnabledProviders(config, options.providers);\n\n // In default mode (no --db), skip unconfigured providers\n const isExplicitSelection = options.providers && options.providers.length > 0;\n if (!isExplicitSelection) {\n providers = providers.filter((name) => isProviderConfigured(name, config));\n }\n\n if (providers.length === 0) {\n return [];\n }\n\n // Execute count for each provider concurrently\n const results: CountResult[] = await Promise.all(\n providers.map(async (providerName): Promise<CountResult> => {\n try {\n const provider = createProviderInstance(providerName, config);\n if (provider === null) {\n return { provider: providerName, count: 0, error: 'Provider configuration incomplete' };\n }\n\n // Translate query\n let translatedQuery: TranslatedQuery;\n if (options.directQuery && options.providers?.length === 1) {\n translatedQuery = {\n native: options.directQuery,\n provider: providerName,\n };\n } else {\n translatedQuery = translateQueryForProvider(ast!, providerName);\n }\n\n const count = await provider.count(translatedQuery);\n return { provider: providerName, count };\n } catch (error) {\n const errorMessage = error instanceof Error\n ? error.message\n : isProviderError(error)\n ? error.message\n : String(error);\n return { provider: providerName, count: 0, error: errorMessage };\n }\n })\n );\n\n return results;\n}\n\n\n/**\n * Execute preview mode: get counts and first few titles without creating a session.\n */\nexport async function executePreview(\n options: SearchCommandOptions,\n config: Config,\n maxTitles = 5\n): Promise<PreviewResult[]> {\n let ast: QueryAST | undefined;\n\n // Handle direct query mode\n if (options.directQuery && options.providers && options.providers.length === 1) {\n ast = {\n name: options.sessionName ?? 'direct-query',\n blocks: [\n {\n field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n overrides: {},\n };\n } else if (options.queryFile) {\n const queryContent = await readFile(options.queryFile, 'utf-8');\n ast = parseQueryString(queryContent);\n } else {\n return [];\n }\n\n // Determine which providers to use\n let providers = getEnabledProviders(config, options.providers);\n\n // In default mode (no --db), skip unconfigured providers\n const isExplicitSelection = options.providers && options.providers.length > 0;\n if (!isExplicitSelection) {\n providers = providers.filter((name) => isProviderConfigured(name, config));\n }\n\n if (providers.length === 0) {\n return [];\n }\n\n // Execute preview for each provider concurrently\n const results: PreviewResult[] = await Promise.all(\n providers.map(async (providerName): Promise<PreviewResult> => {\n try {\n const provider = createProviderInstance(providerName, config);\n if (provider === null) {\n return { provider: providerName, count: 0, titles: [], error: 'Provider configuration incomplete' };\n }\n\n // Translate query\n let translatedQuery: TranslatedQuery;\n if (options.directQuery && options.providers?.length === 1) {\n translatedQuery = {\n native: options.directQuery,\n provider: providerName,\n };\n } else {\n translatedQuery = translateQueryForProvider(ast!, providerName);\n }\n\n // Get count first\n const count = await provider.count(translatedQuery);\n\n // Collect first few articles for titles\n const titles: string[] = [];\n const searchOptions = { maxResults: maxTitles };\n\n for await (const article of provider.search(translatedQuery, searchOptions)) {\n titles.push(article.title);\n if (titles.length >= maxTitles) {\n break;\n }\n }\n\n return { provider: providerName, count, titles };\n } catch (error) {\n const errorMessage = error instanceof Error\n ? error.message\n : isProviderError(error)\n ? error.message\n : String(error);\n return { provider: providerName, count: 0, titles: [], error: errorMessage };\n }\n })\n );\n\n return results;\n}\n\n/**\n * Load all articles from a session's results files (YAML preferred, JSONL fallback).\n */\nasync function loadArticlesFromSession(\n sessionsDir: string,\n sessionId: string,\n providers: ProviderName[]\n): Promise<Article[]> {\n const articles: Article[] = [];\n const sessionDir = join(sessionsDir, sessionId);\n\n for (const provider of providers) {\n const providerArticles = await loadResults(sessionDir, provider);\n articles.push(...providerArticles);\n }\n\n return articles;\n}\n"],"names":["translatePubmed","translateEric","translateArxiv","translateScopus","stringifyYaml"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"search-executor.js","sources":["../../../src/cli/commands/search-executor.ts"],"sourcesContent":["/**\n * Search executor for CLI search command.\n *\n * Handles the actual execution of searches across multiple providers,\n * including session creation, progress display, and result storage.\n */\nimport { readFile, writeFile, appendFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { createHash } from 'node:crypto';\nimport type { SearchCommandOptions, CountResult, PreviewResult } from './search.js';\nimport type { Config } from '../../config/index.js';\nimport type {\n Article,\n Provider,\n ProviderName,\n TranslatedQuery,\n} from '../../providers/base/types.js';\nimport { isProviderError } from '../../providers/base/types.js';\nimport type { QueryAST } from '../../query/types.js';\nimport { parseQueryString } from '../../query/index.js';\nimport { resolveForProvider } from '../../query/resolver.js';\nimport {\n createSession,\n updateDatabaseStatus,\n updateSessionStatus,\n} from '../../session/manager.js';\nimport { MultiProviderProgress } from '../utils/progress.js';\nimport { PubMedProvider } from '../../providers/pubmed/provider.js';\nimport type { PubMedConfig } from '../../providers/pubmed/types.js';\nimport { ERICProvider } from '../../providers/eric/provider.js';\nimport { ArxivProvider } from '../../providers/arxiv/provider.js';\nimport { ScopusProvider } from '../../providers/scopus/provider.js';\nimport type { ScopusConfig } from '../../providers/scopus/types.js';\nimport { translateQuery as translatePubmed } from '../../providers/pubmed/translator.js';\nimport { translateQuery as translateEric } from '../../providers/eric/translator.js';\nimport { translateQuery as translateArxiv } from '../../providers/arxiv/translator.js';\nimport { translateQuery as translateScopus } from '../../providers/scopus/translator.js';\nimport { stringify as stringifyYaml } from 'yaml';\nimport { registerArticles, saveRegistrationRecord } from '../../integration/register.js';\nimport { buildFailureErrorMessage, buildPartialErrorMessage } from './search-utils.js';\nimport { getConfigDir } from '../../config/paths.js';\nimport type { RegistrationRecord } from '../../integration/types.js';\nimport { checkRefAvailable } from '../../integration/ref-cli.js';\nimport { convertResultsToYaml, loadResults } from '../../session/results-io.js';\n\n/**\n * Result of a search execution.\n */\nexport interface SearchExecutionResult {\n success: boolean;\n sessionId?: string;\n results?: Record<string, { hits: number; retrieved: number; error?: string; warnings?: string[] }>;\n error?: string;\n autoRegisterResult?: RegistrationRecord;\n sessionStatus: 'completed' | 'partial' | 'failed';\n}\n\n/**\n * Available providers that are implemented.\n */\nconst IMPLEMENTED_PROVIDERS: ProviderName[] = ['pubmed', 'eric', 'arxiv', 'scopus'];\n\n/**\n * Check if a provider has the required configuration (e.g., API keys).\n * Providers that require no special configuration always return true.\n */\nexport function isProviderConfigured(name: ProviderName, config: Config): boolean {\n switch (name) {\n case 'scopus':\n return !!config.providers.scopus.api_key;\n default:\n return true; // pubmed, eric, arxiv require no API key\n }\n}\n\n/**\n * Create a provider instance for the given provider name.\n */\nexport function createProviderInstance(\n name: ProviderName,\n config: Config\n): Provider | null {\n const providerConfig = config.providers[name];\n\n switch (name) {\n case 'pubmed': {\n if (!providerConfig.email) {\n const configPath = getConfigDir();\n console.warn(\n `Warning: No email configured for PubMed.\\n` +\n ` → Edit ${configPath}/config.toml and set providers.pubmed.email\\n` +\n ` → Or run: search-hub config providers.pubmed.email \"your@email.com\"`\n );\n }\n const pubmedOpts: PubMedConfig = {\n email: providerConfig.email ?? 'search-hub@example.com',\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n };\n if (providerConfig.api_key) {\n pubmedOpts.apiKey = providerConfig.api_key;\n }\n return new PubMedProvider(pubmedOpts);\n }\n case 'eric':\n return new ERICProvider({\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n });\n case 'arxiv':\n return new ArxivProvider({\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n });\n case 'scopus': {\n if (!providerConfig.api_key) {\n console.warn(\n `Warning: Scopus requires an API key. Set providers.scopus.api_key in config.\\n` +\n ` → Get an API key at https://dev.elsevier.com/\\n` +\n ` → Run: search-hub config providers.scopus.api_key \"your-key\"`\n );\n return null;\n }\n const scopusOpts: ScopusConfig = {\n apiKey: providerConfig.api_key,\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n };\n if (providerConfig.inst_token) {\n scopusOpts.instToken = providerConfig.inst_token;\n }\n return new ScopusProvider(scopusOpts);\n }\n default:\n throw new Error(`Provider '${name}' is not implemented`);\n }\n}\n\n/**\n * Translate a query AST for a specific provider.\n * Resolves provider-specific blocks/filters before translation.\n */\nfunction translateQueryForProvider(\n ast: QueryAST,\n provider: ProviderName\n): TranslatedQuery {\n const resolved = resolveForProvider(ast, provider);\n switch (provider) {\n case 'pubmed':\n return translatePubmed(resolved);\n case 'eric':\n return translateEric(resolved);\n case 'arxiv':\n return translateArxiv(resolved);\n case 'scopus':\n return translateScopus(resolved);\n default:\n throw new Error(`No translator for provider '${provider}'`);\n }\n}\n\n/**\n * Get enabled providers from config, optionally filtered by user selection.\n */\nfunction getEnabledProviders(\n config: Config,\n requestedProviders?: ProviderName[]\n): ProviderName[] {\n const enabledInConfig = IMPLEMENTED_PROVIDERS.filter(\n (name) => config.providers[name].enabled\n );\n\n if (requestedProviders && requestedProviders.length > 0) {\n return requestedProviders.filter((p) => enabledInConfig.includes(p));\n }\n\n return enabledInConfig;\n}\n\n/**\n * Execute a search across multiple providers.\n */\nexport async function executeSearch(\n options: SearchCommandOptions,\n sessionsDir: string,\n config: Config,\n showProgress = true\n): Promise<SearchExecutionResult> {\n let ast: QueryAST | undefined;\n let queryContent: string;\n let queryFile: string;\n\n // Handle direct query mode\n if (options.directQuery && options.providers && options.providers.length === 1) {\n queryFile = 'direct-query';\n\n // For direct query, we create a minimal AST structure\n ast = {\n name: options.sessionName ?? 'direct-query',\n blocks: [\n {\n id: 'direct',\n field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n providers: {},\n };\n\n // Generate YAML safely using yaml library to handle special characters\n queryContent = stringifyYaml({\n name: ast.name,\n blocks: ast.blocks,\n filters: ast.filters,\n });\n } else if (options.queryFile) {\n // Parse query file\n try {\n queryContent = await readFile(options.queryFile, 'utf-8');\n ast = parseQueryString(queryContent);\n queryFile = options.queryFile;\n } catch (error) {\n return {\n success: false,\n sessionStatus: 'failed',\n error: `Failed to parse query file: ${error instanceof Error ? error.message : error}`,\n };\n }\n } else {\n return {\n success: false,\n sessionStatus: 'failed',\n error: 'Either queryFile or directQuery with provider is required',\n };\n }\n\n // Determine which providers to use\n let providers = getEnabledProviders(config, options.providers);\n\n // In default mode (no --db), skip unconfigured providers with a warning\n const isExplicitSelection = options.providers && options.providers.length > 0;\n if (!isExplicitSelection) {\n const skipped: ProviderName[] = [];\n providers = providers.filter((name) => {\n if (!isProviderConfigured(name, config)) {\n skipped.push(name);\n return false;\n }\n return true;\n });\n for (const name of skipped) {\n console.warn(\n `Skipping ${name}: API key not configured (use --db ${name} to force)`\n );\n }\n }\n\n if (providers.length === 0) {\n return {\n success: false,\n sessionStatus: 'failed',\n error: 'No providers enabled or selected',\n };\n }\n\n // Create query hash\n const queryHash = createHash('sha256').update(queryContent).digest('hex').slice(0, 8);\n\n // Create session\n let session;\n try {\n const sessionOpts: Parameters<typeof createSession>[0] = {\n name: options.sessionName ?? ast.name,\n queryFile,\n queryContent,\n queryHash,\n targets: providers,\n sessionsDir,\n };\n if (ast.description) {\n sessionOpts.description = ast.description;\n }\n session = await createSession(sessionOpts);\n } catch (error) {\n return {\n success: false,\n sessionStatus: 'failed',\n error: `Failed to create session: ${error instanceof Error ? error.message : error}`,\n };\n }\n\n const sessionId = session.id;\n const results: Record<string, { hits: number; retrieved: number; error?: string; warnings?: string[] }> = {};\n\n // Create progress display if enabled\n let progress: MultiProviderProgress | undefined;\n if (showProgress && process.stdout.isTTY) {\n progress = new MultiProviderProgress(providers);\n }\n\n // Execute search for each provider\n for (const providerName of providers) {\n try {\n // Create provider instance\n const provider = createProviderInstance(providerName, config);\n\n // Skip provider if it could not be created (e.g. missing configuration)\n if (provider === null) {\n const configError = `${providerName}: provider configuration incomplete. See warning above for details.`;\n results[providerName] = { hits: 0, retrieved: 0, error: configError };\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'failed',\n completedAt: new Date().toISOString(),\n error: {\n code: 'CONFIG_ERROR',\n message: configError,\n retryable: false,\n },\n },\n sessionsDir\n );\n continue;\n }\n\n // Translate query\n let translatedQuery: TranslatedQuery;\n if (options.directQuery && options.providers?.length === 1) {\n // For direct query, use the native query string directly\n translatedQuery = {\n native: options.directQuery,\n provider: providerName,\n };\n } else {\n translatedQuery = translateQueryForProvider(ast, providerName);\n }\n\n // Write translated query to session\n const queryPath = join(sessionsDir, sessionId, `${providerName}_query.txt`);\n await writeFile(queryPath, translatedQuery.native, 'utf-8');\n\n // Update database status to in_progress\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'in_progress',\n startedAt: new Date().toISOString(),\n },\n sessionsDir\n );\n\n // Prepare results file path\n const resultsPath = join(sessionsDir, sessionId, `${providerName}_results.jsonl`);\n\n // Execute search\n let retrievedCount = 0;\n let totalHits = 0;\n\n progress?.update(providerName, 0, 0, 'in_progress');\n\n const searchOptions = {\n maxResults: options.maxResults ?? config.providers[providerName].max_results,\n };\n\n for await (const article of provider.search(translatedQuery, searchOptions)) {\n retrievedCount++;\n\n // Write article to JSONL file\n await appendFile(resultsPath, JSON.stringify(article) + '\\n', 'utf-8');\n\n // Update progress (estimate total from first batch)\n if (totalHits === 0) {\n // Estimate total - this is provider-dependent, we'll use retrieved count as minimum\n totalHits = Math.max(retrievedCount * 10, 100);\n }\n progress?.update(providerName, retrievedCount, totalHits, 'in_progress');\n }\n\n // Update final totals\n totalHits = retrievedCount; // Use actual count as final total\n\n // Mark as completed\n progress?.complete(providerName);\n\n // Convert JSONL to YAML for human-readable view\n const yamlFilename = `${providerName}_results.yaml`;\n const yamlPath = join(sessionsDir, sessionId, yamlFilename);\n await convertResultsToYaml(resultsPath, yamlPath, {\n provider: providerName,\n queryName: ast.name,\n });\n\n // Update database status\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'completed',\n completedAt: new Date().toISOString(),\n totalHits,\n retrievedCount,\n files: {\n query: `${providerName}_query.txt`,\n results: `${providerName}_results.jsonl`,\n resultsYaml: yamlFilename,\n },\n },\n sessionsDir\n );\n\n // Collect warnings if provider supports them\n const providerWarnings = 'getWarnings' in provider && typeof (provider as any).getWarnings === 'function'\n ? (provider as any).getWarnings() as string[]\n : undefined;\n results[providerName] = { hits: totalHits, retrieved: retrievedCount, ...(providerWarnings && providerWarnings.length > 0 && { warnings: providerWarnings }) };\n } catch (error) {\n const errorMessage = error instanceof Error\n ? error.message\n : isProviderError(error)\n ? error.message\n : String(error);\n\n progress?.fail(providerName, errorMessage);\n\n // Update database status with error\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'failed',\n completedAt: new Date().toISOString(),\n error: {\n code: 'SEARCH_ERROR',\n message: errorMessage,\n retryable: true,\n },\n },\n sessionsDir\n );\n\n results[providerName] = { hits: 0, retrieved: 0, error: errorMessage };\n }\n }\n\n // Stop progress display\n progress?.stop();\n\n // Determine overall session status\n const anyFailed = providers.some((p) => {\n const r = results[p];\n return r && r.error !== undefined;\n });\n const anySucceeded = providers.some((p) => {\n const r = results[p];\n return r && r.retrieved > 0;\n });\n\n let sessionStatus: 'completed' | 'partial' | 'failed';\n if (!anyFailed) {\n sessionStatus = 'completed';\n } else if (anySucceeded) {\n sessionStatus = 'partial';\n } else {\n sessionStatus = 'failed';\n }\n\n // Update session status\n await updateSessionStatus(sessionId, sessionStatus, sessionsDir);\n\n if (sessionStatus === 'failed') {\n return {\n success: false,\n sessionId,\n sessionStatus,\n results,\n error: buildFailureErrorMessage(results),\n };\n }\n\n // In strict mode, partial success is treated as failure\n if (options.strict && sessionStatus === 'partial') {\n return {\n success: false,\n sessionId,\n sessionStatus,\n results,\n error: buildPartialErrorMessage(results),\n };\n }\n\n // Auto-register if enabled\n let autoRegisterResult: RegistrationRecord | undefined;\n if (\n config.integration.reference_manager.enabled &&\n config.integration.reference_manager.auto_register\n ) {\n const refAvailable = await checkRefAvailable();\n if (refAvailable) {\n // Load all articles from results files\n const allArticles = await loadArticlesFromSession(sessionsDir, sessionId, providers);\n\n if (allArticles.length > 0) {\n autoRegisterResult = await registerArticles(allArticles, {\n sessionId,\n sessionDir: join(sessionsDir, sessionId),\n withAbstracts: config.integration.reference_manager.with_abstracts,\n });\n\n // Save registration record\n await saveRegistrationRecord(join(sessionsDir, sessionId), autoRegisterResult);\n }\n }\n }\n\n const result: SearchExecutionResult = {\n success: true,\n sessionId,\n results,\n sessionStatus,\n };\n\n if (autoRegisterResult) {\n result.autoRegisterResult = autoRegisterResult;\n }\n\n return result;\n}\n\n/**\n * Execute count-only mode: get hit counts from each provider without downloading results.\n * No session is created.\n */\nexport async function executeCountOnly(\n options: SearchCommandOptions,\n config: Config\n): Promise<CountResult[]> {\n let ast: QueryAST | undefined;\n\n // Handle direct query mode\n if (options.directQuery && options.providers && options.providers.length === 1) {\n ast = {\n name: options.sessionName ?? 'direct-query',\n blocks: [\n {\n id: 'direct',\n field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n providers: {},\n };\n } else if (options.queryFile) {\n const queryContent = await readFile(options.queryFile, 'utf-8');\n ast = parseQueryString(queryContent);\n } else {\n return [];\n }\n\n // Determine which providers to use\n let providers = getEnabledProviders(config, options.providers);\n\n // In default mode (no --db), skip unconfigured providers\n const isExplicitSelection = options.providers && options.providers.length > 0;\n if (!isExplicitSelection) {\n providers = providers.filter((name) => isProviderConfigured(name, config));\n }\n\n if (providers.length === 0) {\n return [];\n }\n\n // Execute count for each provider concurrently\n const results: CountResult[] = await Promise.all(\n providers.map(async (providerName): Promise<CountResult> => {\n try {\n const provider = createProviderInstance(providerName, config);\n if (provider === null) {\n return { provider: providerName, count: 0, error: 'Provider configuration incomplete' };\n }\n\n // Translate query\n let translatedQuery: TranslatedQuery;\n if (options.directQuery && options.providers?.length === 1) {\n translatedQuery = {\n native: options.directQuery,\n provider: providerName,\n };\n } else {\n translatedQuery = translateQueryForProvider(ast!, providerName);\n }\n\n const count = await provider.count(translatedQuery);\n return { provider: providerName, count };\n } catch (error) {\n const errorMessage = error instanceof Error\n ? error.message\n : isProviderError(error)\n ? error.message\n : String(error);\n return { provider: providerName, count: 0, error: errorMessage };\n }\n })\n );\n\n return results;\n}\n\n\n/**\n * Execute preview mode: get counts and first few titles without creating a session.\n */\nexport async function executePreview(\n options: SearchCommandOptions,\n config: Config,\n maxTitles = 5\n): Promise<PreviewResult[]> {\n let ast: QueryAST | undefined;\n\n // Handle direct query mode\n if (options.directQuery && options.providers && options.providers.length === 1) {\n ast = {\n name: options.sessionName ?? 'direct-query',\n blocks: [\n {\n id: 'direct',\n field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n providers: {},\n };\n } else if (options.queryFile) {\n const queryContent = await readFile(options.queryFile, 'utf-8');\n ast = parseQueryString(queryContent);\n } else {\n return [];\n }\n\n // Determine which providers to use\n let providers = getEnabledProviders(config, options.providers);\n\n // In default mode (no --db), skip unconfigured providers\n const isExplicitSelection = options.providers && options.providers.length > 0;\n if (!isExplicitSelection) {\n providers = providers.filter((name) => isProviderConfigured(name, config));\n }\n\n if (providers.length === 0) {\n return [];\n }\n\n // Execute preview for each provider concurrently\n const results: PreviewResult[] = await Promise.all(\n providers.map(async (providerName): Promise<PreviewResult> => {\n try {\n const provider = createProviderInstance(providerName, config);\n if (provider === null) {\n return { provider: providerName, count: 0, titles: [], error: 'Provider configuration incomplete' };\n }\n\n // Translate query\n let translatedQuery: TranslatedQuery;\n if (options.directQuery && options.providers?.length === 1) {\n translatedQuery = {\n native: options.directQuery,\n provider: providerName,\n };\n } else {\n translatedQuery = translateQueryForProvider(ast!, providerName);\n }\n\n // Get count first\n const count = await provider.count(translatedQuery);\n\n // Collect first few articles for titles\n const titles: string[] = [];\n const searchOptions = { maxResults: maxTitles };\n\n for await (const article of provider.search(translatedQuery, searchOptions)) {\n titles.push(article.title);\n if (titles.length >= maxTitles) {\n break;\n }\n }\n\n return { provider: providerName, count, titles };\n } catch (error) {\n const errorMessage = error instanceof Error\n ? error.message\n : isProviderError(error)\n ? error.message\n : String(error);\n return { provider: providerName, count: 0, titles: [], error: errorMessage };\n }\n })\n );\n\n return results;\n}\n\n/**\n * Load all articles from a session's results files (YAML preferred, JSONL fallback).\n */\nasync function loadArticlesFromSession(\n sessionsDir: string,\n sessionId: string,\n providers: ProviderName[]\n): Promise<Article[]> {\n const articles: Article[] = [];\n const sessionDir = join(sessionsDir, sessionId);\n\n for (const provider of providers) {\n const providerArticles = await loadResults(sessionDir, provider);\n articles.push(...providerArticles);\n }\n\n return articles;\n}\n"],"names":["translatePubmed","translateEric","translateArxiv","translateScopus","stringifyYaml"],"mappings":"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"}
|
|
@@ -115,12 +115,14 @@ export declare const queryFixtures: {
|
|
|
115
115
|
name: string;
|
|
116
116
|
description: string;
|
|
117
117
|
blocks: ({
|
|
118
|
+
id: string;
|
|
118
119
|
field: "title_abstract";
|
|
119
120
|
terms: {
|
|
120
121
|
keywords: string[];
|
|
121
122
|
};
|
|
122
123
|
operator: "AND";
|
|
123
124
|
} | {
|
|
125
|
+
id: string;
|
|
124
126
|
field: "keyword";
|
|
125
127
|
terms: {
|
|
126
128
|
keywords: string[];
|
|
@@ -132,12 +134,13 @@ export declare const queryFixtures: {
|
|
|
132
134
|
yearTo: number;
|
|
133
135
|
languages: string[];
|
|
134
136
|
};
|
|
135
|
-
|
|
137
|
+
providers: {};
|
|
136
138
|
};
|
|
137
139
|
/** Query with MeSH terms (PubMed-specific) */
|
|
138
140
|
withMesh: {
|
|
139
141
|
name: string;
|
|
140
142
|
blocks: {
|
|
143
|
+
id: string;
|
|
141
144
|
field: "title_abstract";
|
|
142
145
|
terms: {
|
|
143
146
|
keywords: string[];
|
|
@@ -146,7 +149,7 @@ export declare const queryFixtures: {
|
|
|
146
149
|
operator: "AND";
|
|
147
150
|
}[];
|
|
148
151
|
filters: {};
|
|
149
|
-
|
|
152
|
+
providers: {};
|
|
150
153
|
};
|
|
151
154
|
};
|
|
152
155
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"e2e-helpers.d.ts","sourceRoot":"","sources":["../../src/cli/e2e-helpers.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;AAE5D;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjC,GAAG,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IACtD,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACrD,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC;IAC3D,WAAW,CAAC,EAAE;QACZ,iBAAiB,CAAC,EAAE;YAClB,OAAO,CAAC,EAAE,OAAO,CAAC;YAClB,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,aAAa,CAAC,EAAE,OAAO,CAAC;YACxB,cAAc,CAAC,EAAE,OAAO,CAAC;SAC1B,CAAC;KACH,CAAC;CACH;AAMD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,mCAAmC;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,yBAAyB;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAErD;AAED;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,UAAU,CAAC,CA2C3D;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAC3B,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,UAAU,CAAC,CAkDrB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,QAAQ,EACf,QAAQ,SAAe,GACtB,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"e2e-helpers.d.ts","sourceRoot":"","sources":["../../src/cli/e2e-helpers.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuLpC;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAygFvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
|
package/dist/cli/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { VocabCache } from "../query/vocab-cache.js";
|
|
|
16
16
|
import { createEricCountValidator, createEmtreeCountValidator } from "../query/vocab-validator.js";
|
|
17
17
|
import { createProviderInstance, executePreview, executeCountOnly, executeSearch } from "./commands/search-executor.js";
|
|
18
18
|
import { translateQueryCommand, formatTranslateResult } from "./commands/query/translate.js";
|
|
19
|
+
import { inspectQueryCommand, formatInspectOutput } from "./commands/query/inspect.js";
|
|
19
20
|
import { writeQueryTemplate, generateQueryTemplate } from "./commands/query/init.js";
|
|
20
21
|
import { getSessionDetails, computeDeduplicationStats, formatSessionDetails, listSessionsForDisplay, formatSessionList } from "./commands/status.js";
|
|
21
22
|
import { parseSearchOptions, validateSearchInput, formatShortKeywordWarning, formatDryRunOutput, formatPreviewOutput, formatCountOnlyOutput } from "./commands/search.js";
|
|
@@ -25,6 +26,7 @@ import { formatVerboseProviderDetails } from "./commands/search-utils.js";
|
|
|
25
26
|
import { parseExportOptions, validateExportInput, deduplicateArticles, filterArticles, formatIds, formatJson, formatCslJson, formatJsonl } from "./commands/export.js";
|
|
26
27
|
import { computeSummary, formatSummaryJson, formatSummary } from "./commands/summary.js";
|
|
27
28
|
import { parseResultsOptions, validateResultsInput, formatResultsJson, formatResultsList } from "./commands/results.js";
|
|
29
|
+
import { filterByQuery } from "./commands/query-filter.js";
|
|
28
30
|
import { loadNotes, formatAllSessionNotes, formatNotesList, addNote, addAssessment } from "./commands/notes.js";
|
|
29
31
|
import { computeDiff, computeQueryDiff, formatDiffJson, formatDiff } from "./commands/diff.js";
|
|
30
32
|
import { validateMergeSources, mergeArticles, formatMergeJson, formatMergeOutput, createMergedSession } from "./commands/merge.js";
|
|
@@ -51,6 +53,7 @@ import { fileURLToPath } from "node:url";
|
|
|
51
53
|
import { getSessionsDir } from "./utils/sessions-dir.js";
|
|
52
54
|
import { expandPath } from "../utils/path.js";
|
|
53
55
|
import { loadSessionArticles, loadSessionQuery } from "./commands/session-utils.js";
|
|
56
|
+
import { parseIdentifierFile, checkCoverage, formatCheckResultJson, formatCheckResult } from "./commands/check.js";
|
|
54
57
|
config({ quiet: true });
|
|
55
58
|
function createProgram() {
|
|
56
59
|
const program = new Command();
|
|
@@ -60,11 +63,11 @@ function createProgram() {
|
|
|
60
63
|
Workflow:
|
|
61
64
|
1. query init → edit → validate / --dry-run Query preparation
|
|
62
65
|
2. search --preview → search Preview & execute
|
|
63
|
-
3. results / summary / diff
|
|
66
|
+
3. results / summary / diff / check Inspect & verify
|
|
64
67
|
4. review init → extract → merge → status Systematic review
|
|
65
68
|
5. register / export Output
|
|
66
69
|
|
|
67
|
-
Iterate: search
|
|
70
|
+
Iterate: search → results -q → check → diff Query refinement
|
|
68
71
|
|
|
69
72
|
Quick Start:
|
|
70
73
|
$ search-hub query init -o search.yaml # Create query template
|
|
@@ -288,6 +291,36 @@ Examples:
|
|
|
288
291
|
process.exitCode = EXIT_CODES.QUERY_ERROR;
|
|
289
292
|
}
|
|
290
293
|
});
|
|
294
|
+
queryCommand.command("inspect").description("Show how a query resolves per provider (block replacements and added filters)").argument("<file>", "path to query YAML file").option("--db <provider>", "show resolution for specific provider only").addHelpText("after", `
|
|
295
|
+
Examples:
|
|
296
|
+
$ search-hub query inspect ./diabetes-ai.yaml # All databases
|
|
297
|
+
$ search-hub query inspect ./diabetes-ai.yaml --db pubmed # PubMed only`).action(async (file, options) => {
|
|
298
|
+
const globalOpts = program.opts();
|
|
299
|
+
try {
|
|
300
|
+
const inspectOptions = options.db ? { providers: [options.db] } : {};
|
|
301
|
+
const result = await inspectQueryCommand(file, inspectOptions);
|
|
302
|
+
if (!result.success) {
|
|
303
|
+
if (!globalOpts.quiet) {
|
|
304
|
+
console.error(`✗ Failed to inspect: ${file}
|
|
305
|
+
Error: ${result.error}`);
|
|
306
|
+
}
|
|
307
|
+
process.exitCode = EXIT_CODES.QUERY_ERROR;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!globalOpts.quiet) {
|
|
311
|
+
console.log(formatInspectOutput(result.result));
|
|
312
|
+
}
|
|
313
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (!globalOpts.quiet) {
|
|
316
|
+
console.error(
|
|
317
|
+
"Error:",
|
|
318
|
+
error instanceof Error ? error.message : error
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
process.exitCode = EXIT_CODES.QUERY_ERROR;
|
|
322
|
+
}
|
|
323
|
+
});
|
|
291
324
|
queryCommand.command("init").description("Generate a template query YAML file").option("-o, --output <path>", "write to file (default: stdout)").option("--force", "overwrite existing file", false).action(async (options) => {
|
|
292
325
|
const globalOpts = program.opts();
|
|
293
326
|
try {
|
|
@@ -376,7 +409,7 @@ Query features (use "query init" to see full template):
|
|
|
376
409
|
filters: year_from, year_to, language, publication_types
|
|
377
410
|
exclude: NOT terms per block (terms.exclude)
|
|
378
411
|
mesh/eric: controlled vocabulary (terms.mesh, terms.eric)
|
|
379
|
-
|
|
412
|
+
providers: per-database block replacements and filter additions`).action(
|
|
380
413
|
async (queryFile, options) => {
|
|
381
414
|
const globalOpts = program.opts();
|
|
382
415
|
try {
|
|
@@ -695,25 +728,44 @@ Resume completed. ${execResult.resumed} provider(s) resumed.`);
|
|
|
695
728
|
}
|
|
696
729
|
}
|
|
697
730
|
);
|
|
698
|
-
program.command("export").description("Export session results to various formats").argument("<session-id>", "session ID to export").option("--format <fmt>", "output format: ids, json, jsonl, csl-json", "jsonl").option("-o, --output <path>", "output file path (default: stdout)").option("--
|
|
731
|
+
program.command("export").description("Export session results to various formats").argument("<session-id>", "session ID to export").option("--format <fmt>", "output format: ids, json, jsonl, csl-json", "jsonl").option("-o, --output <path>", "output file path (default: stdout)").option("--id-type <type>", "for ids format: doi, pmid, all").option("--no-dedup", "disable deduplication of results").option("-q, --query <expr>", "filter results with query expression").option("--filter-year <range>", "year range filter (deprecated, use -q)").option("--filter-title <keywords>", "title keyword filter (deprecated, use -q)").option("--filter-abstract <keywords>", "abstract keyword filter (deprecated, use -q)").addHelpText("after", `
|
|
699
732
|
Examples:
|
|
700
733
|
$ search-hub export SESSION_ID # JSONL to stdout
|
|
701
734
|
$ search-hub export SESSION_ID --format json # JSON to stdout
|
|
735
|
+
$ search-hub export SESSION_ID -q "year:2023" # Filter by query
|
|
736
|
+
$ search-hub export SESSION_ID -q "author:smith" --format ids # Filtered IDs
|
|
702
737
|
$ search-hub export SESSION_ID --format json -o results.json # JSON to file
|
|
703
738
|
$ search-hub export SESSION_ID --format ids --id-type doi # Export DOIs to stdout
|
|
704
|
-
$ search-hub export SESSION_ID --format csl-json -o refs.json # CSL-JSON to file
|
|
705
|
-
$ search-hub export SESSION_ID --db pubmed --format jsonl
|
|
706
739
|
$ search-hub export SESSION_ID --no-dedup # Export without deduplication
|
|
707
740
|
$ search-hub export SESSION_ID --format jsonl | jq '.title' # Pipe to jq
|
|
708
|
-
|
|
709
|
-
|
|
741
|
+
|
|
742
|
+
Query syntax:
|
|
743
|
+
Free text diabetes Search title and abstract
|
|
744
|
+
title:VALUE title:learning Title substring
|
|
745
|
+
abstract:VALUE abstract:randomized Abstract substring
|
|
746
|
+
author:VALUE author:tanaka Author name substring
|
|
747
|
+
journal:VALUE journal:lancet Journal name substring
|
|
748
|
+
year:VALUE year:2023 Exact year
|
|
749
|
+
year:FROM-TO year:2020-2024 Year range
|
|
750
|
+
doi:VALUE doi:10.1001/xxx DOI exact match
|
|
751
|
+
pmid:VALUE pmid:12345678 PMID exact match
|
|
752
|
+
source:VALUE source:pubmed Provider exact match
|
|
753
|
+
|
|
754
|
+
Multiple terms: different fields = AND, same field = OR`).action(
|
|
710
755
|
async (sessionId, options) => {
|
|
711
756
|
const globalOpts = program.opts();
|
|
712
757
|
try {
|
|
758
|
+
const hasLegacyFilter = options?.filterYear || options?.filterTitle || options?.filterAbstract;
|
|
759
|
+
if (options?.query && hasLegacyFilter) {
|
|
760
|
+
if (!globalOpts.quiet) {
|
|
761
|
+
console.error("Error: Cannot use -q/--query together with --filter-year, --filter-title, or --filter-abstract. Use -q only.");
|
|
762
|
+
}
|
|
763
|
+
process.exitCode = EXIT_CODES.SESSION_ERROR;
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
713
766
|
const exportOpts = parseExportOptions(sessionId, {
|
|
714
767
|
format: options?.format,
|
|
715
768
|
output: options?.output,
|
|
716
|
-
db: options?.db,
|
|
717
769
|
idType: options?.idType
|
|
718
770
|
});
|
|
719
771
|
const validation = validateExportInput(exportOpts);
|
|
@@ -737,7 +789,7 @@ Examples:
|
|
|
737
789
|
process.exitCode = EXIT_CODES.SESSION_ERROR;
|
|
738
790
|
return;
|
|
739
791
|
}
|
|
740
|
-
const articles = await loadSessionArticles(session, sessionId, sessionsDir
|
|
792
|
+
const articles = await loadSessionArticles(session, sessionId, sessionsDir);
|
|
741
793
|
const shouldDedup = options?.dedup !== false;
|
|
742
794
|
let exportArticles;
|
|
743
795
|
let duplicatesRemoved = 0;
|
|
@@ -748,32 +800,38 @@ Examples:
|
|
|
748
800
|
} else {
|
|
749
801
|
exportArticles = articles;
|
|
750
802
|
}
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
filter.
|
|
803
|
+
const preFilterCount = exportArticles.length;
|
|
804
|
+
let hasFilter = false;
|
|
805
|
+
if (options?.query) {
|
|
806
|
+
exportArticles = filterByQuery(exportArticles, options.query);
|
|
807
|
+
hasFilter = true;
|
|
808
|
+
} else {
|
|
809
|
+
const filter = {};
|
|
810
|
+
if (options?.filterYear) {
|
|
811
|
+
const parts = options.filterYear.split("-");
|
|
812
|
+
if (parts.length === 2) {
|
|
813
|
+
const from = parseInt(parts[0], 10);
|
|
814
|
+
const to = parseInt(parts[1], 10);
|
|
815
|
+
if (!Number.isNaN(from)) filter.yearFrom = from;
|
|
816
|
+
if (!Number.isNaN(to)) filter.yearTo = to;
|
|
817
|
+
} else if (parts.length === 1) {
|
|
818
|
+
const year = parseInt(parts[0], 10);
|
|
819
|
+
if (!Number.isNaN(year)) {
|
|
820
|
+
filter.yearFrom = year;
|
|
821
|
+
filter.yearTo = year;
|
|
822
|
+
}
|
|
764
823
|
}
|
|
765
824
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
exportArticles = filterArticles(exportArticles, filter);
|
|
825
|
+
if (options?.filterTitle) {
|
|
826
|
+
filter.titleKeywords = options.filterTitle.split(",").map((s) => s.trim()).filter(Boolean);
|
|
827
|
+
}
|
|
828
|
+
if (options?.filterAbstract) {
|
|
829
|
+
filter.abstractKeywords = options.filterAbstract.split(",").map((s) => s.trim()).filter(Boolean);
|
|
830
|
+
}
|
|
831
|
+
hasFilter = !!(filter.yearFrom !== void 0 || filter.yearTo !== void 0 || filter.titleKeywords && filter.titleKeywords.length > 0 || filter.abstractKeywords && filter.abstractKeywords.length > 0);
|
|
832
|
+
if (hasFilter) {
|
|
833
|
+
exportArticles = filterArticles(exportArticles, filter);
|
|
834
|
+
}
|
|
777
835
|
}
|
|
778
836
|
let output;
|
|
779
837
|
if (exportOpts.format === "ids") {
|
|
@@ -881,15 +939,30 @@ Examples:
|
|
|
881
939
|
}
|
|
882
940
|
}
|
|
883
941
|
);
|
|
884
|
-
program.command("results").description("List articles from a session with title, year, and journal").argument("<session-id>", "session ID to list results from").option("--limit <n>", "maximum number of results to show").option("--offset <n>", "skip first n results").option("--json", "output as JSON array").option("--fields <fields>", "fields to display (comma-separated)").option("--
|
|
942
|
+
program.command("results").description("List articles from a session with title, year, and journal").argument("<session-id>", "session ID to list results from").option("--limit <n>", "maximum number of results to show").option("--offset <n>", "skip first n results").option("--json", "output as JSON array").option("--fields <fields>", "fields to display (comma-separated)").option("-q, --query <expr>", "filter results with query expression").option("--filter-year <range>", "year range filter (deprecated, use -q)").option("--filter-title <keywords>", "title keyword filter (deprecated, use -q)").option("--filter-abstract <keywords>", "abstract keyword filter (deprecated, use -q)").option("--abstract", "show abstracts with results").option("--abstract-length <n>", "maximum abstract length in characters (default: 300)").addHelpText("after", `
|
|
885
943
|
Examples:
|
|
886
|
-
$ search-hub results SESSION_ID
|
|
887
|
-
$ search-hub results SESSION_ID --limit 20
|
|
888
|
-
$ search-hub results SESSION_ID
|
|
889
|
-
$ search-hub results SESSION_ID
|
|
890
|
-
$ search-hub results SESSION_ID
|
|
891
|
-
$ search-hub results SESSION_ID --
|
|
892
|
-
$ search-hub results SESSION_ID
|
|
944
|
+
$ search-hub results SESSION_ID # List all articles
|
|
945
|
+
$ search-hub results SESSION_ID --limit 20 # First 20 articles
|
|
946
|
+
$ search-hub results SESSION_ID -q "diabetes" # Free text filter
|
|
947
|
+
$ search-hub results SESSION_ID -q "author:smith year:2023" # Combined filter
|
|
948
|
+
$ search-hub results SESSION_ID -q "doi:10.1001/xxx" # Exact ID match
|
|
949
|
+
$ search-hub results SESSION_ID --json # JSON output
|
|
950
|
+
$ search-hub results SESSION_ID -q "source:pubmed" # Only PubMed
|
|
951
|
+
$ search-hub results SESSION_ID --abstract # Show abstracts
|
|
952
|
+
|
|
953
|
+
Query syntax:
|
|
954
|
+
Free text diabetes Search title and abstract
|
|
955
|
+
title:VALUE title:learning Title substring
|
|
956
|
+
abstract:VALUE abstract:randomized Abstract substring
|
|
957
|
+
author:VALUE author:tanaka Author name substring
|
|
958
|
+
journal:VALUE journal:lancet Journal name substring
|
|
959
|
+
year:VALUE year:2023 Exact year
|
|
960
|
+
year:FROM-TO year:2020-2024 Year range
|
|
961
|
+
doi:VALUE doi:10.1001/xxx DOI exact match
|
|
962
|
+
pmid:VALUE pmid:12345678 PMID exact match
|
|
963
|
+
source:VALUE source:pubmed Provider exact match
|
|
964
|
+
|
|
965
|
+
Multiple terms: different fields = AND, same field = OR`).action(
|
|
893
966
|
async (sessionId, options) => {
|
|
894
967
|
const globalOpts = program.opts();
|
|
895
968
|
try {
|
|
@@ -898,7 +971,7 @@ Examples:
|
|
|
898
971
|
offset: options?.offset,
|
|
899
972
|
json: options?.json,
|
|
900
973
|
fields: options?.fields,
|
|
901
|
-
|
|
974
|
+
query: options?.query,
|
|
902
975
|
filterYear: options?.filterYear,
|
|
903
976
|
filterTitle: options?.filterTitle,
|
|
904
977
|
filterAbstract: options?.filterAbstract,
|
|
@@ -926,11 +999,17 @@ Examples:
|
|
|
926
999
|
process.exitCode = EXIT_CODES.SESSION_ERROR;
|
|
927
1000
|
return;
|
|
928
1001
|
}
|
|
929
|
-
const articles = await loadSessionArticles(session, sessionId, sessionsDir
|
|
1002
|
+
const articles = await loadSessionArticles(session, sessionId, sessionsDir);
|
|
930
1003
|
const dedupResult = deduplicateArticles(articles);
|
|
931
1004
|
let displayArticles = dedupResult.articles;
|
|
932
1005
|
let filteredFrom;
|
|
933
|
-
if (resultsOpts.
|
|
1006
|
+
if (resultsOpts.query) {
|
|
1007
|
+
const preFilterCount = displayArticles.length;
|
|
1008
|
+
displayArticles = filterByQuery(displayArticles, resultsOpts.query);
|
|
1009
|
+
if (displayArticles.length !== preFilterCount) {
|
|
1010
|
+
filteredFrom = preFilterCount;
|
|
1011
|
+
}
|
|
1012
|
+
} else if (resultsOpts.filter) {
|
|
934
1013
|
const preFilterCount = displayArticles.length;
|
|
935
1014
|
displayArticles = filterArticles(displayArticles, resultsOpts.filter);
|
|
936
1015
|
if (displayArticles.length !== preFilterCount) {
|
|
@@ -1072,6 +1151,110 @@ Query Refinement Workflow:
|
|
|
1072
1151
|
}
|
|
1073
1152
|
}
|
|
1074
1153
|
);
|
|
1154
|
+
program.command("check").description("Verify coverage of known articles against session results").argument("<session-id>", "session ID to check against").option("--file <path>", "file with identifiers (one per line)").option("--doi <ids>", "comma-separated DOIs to check").option("--pmid <ids>", "comma-separated PMIDs to check").option("--json", "output as JSON").option("--missing-only", "show only missing identifiers").addHelpText("after", `
|
|
1155
|
+
Examples:
|
|
1156
|
+
$ search-hub check SESSION --file known-dois.txt # Check from file
|
|
1157
|
+
$ search-hub check SESSION --doi "10.1001/jama.2023.12345" # Check single DOI
|
|
1158
|
+
$ search-hub check SESSION --pmid "37654321,36543210" # Check PMIDs
|
|
1159
|
+
$ search-hub check SESSION --file refs.txt --json # JSON output
|
|
1160
|
+
$ search-hub check SESSION --file refs.txt --missing-only # Only missing
|
|
1161
|
+
|
|
1162
|
+
Input file format (one identifier per line):
|
|
1163
|
+
10.1001/jama.2023.12345 DOI (starts with "10.")
|
|
1164
|
+
37654321 PMID (numeric only)
|
|
1165
|
+
DOI:10.1038/s41586-023-xxxxx DOI (explicit prefix)
|
|
1166
|
+
PMID:36543210 PMID (explicit prefix)
|
|
1167
|
+
arxiv:2301.12345 arXiv ID (explicit prefix)
|
|
1168
|
+
# comment Comments and empty lines ignored`).action(
|
|
1169
|
+
async (sessionId, options) => {
|
|
1170
|
+
const globalOpts = program.opts();
|
|
1171
|
+
try {
|
|
1172
|
+
let identifiers;
|
|
1173
|
+
let source;
|
|
1174
|
+
if (options?.file) {
|
|
1175
|
+
const filePath = expandPath(options.file);
|
|
1176
|
+
let content;
|
|
1177
|
+
try {
|
|
1178
|
+
content = await readFile(filePath, "utf-8");
|
|
1179
|
+
} catch {
|
|
1180
|
+
if (!globalOpts.quiet) {
|
|
1181
|
+
console.error(`Error: File not found: ${filePath}`);
|
|
1182
|
+
}
|
|
1183
|
+
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
try {
|
|
1187
|
+
identifiers = parseIdentifierFile(content);
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
if (!globalOpts.quiet) {
|
|
1190
|
+
console.error(`Error: ${error instanceof Error ? error.message : "Failed to parse identifier file"}`);
|
|
1191
|
+
}
|
|
1192
|
+
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
source = options.file;
|
|
1196
|
+
} else if (options?.doi || options?.pmid) {
|
|
1197
|
+
const lines = [];
|
|
1198
|
+
if (options.doi) {
|
|
1199
|
+
lines.push(...options.doi.split(",").map((d) => d.trim()).filter(Boolean));
|
|
1200
|
+
}
|
|
1201
|
+
if (options.pmid) {
|
|
1202
|
+
lines.push(...options.pmid.split(",").map((p) => `PMID:${p.trim()}`).filter(Boolean));
|
|
1203
|
+
}
|
|
1204
|
+
identifiers = parseIdentifierFile(lines.join("\n"));
|
|
1205
|
+
source = "command line";
|
|
1206
|
+
} else {
|
|
1207
|
+
if (!globalOpts.quiet) {
|
|
1208
|
+
console.error("Error: Provide --file, --doi, or --pmid");
|
|
1209
|
+
}
|
|
1210
|
+
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (identifiers.length === 0) {
|
|
1214
|
+
if (!globalOpts.quiet) {
|
|
1215
|
+
console.error("Error: No identifiers found in input");
|
|
1216
|
+
}
|
|
1217
|
+
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const sessionsDir = await getSessionsDir(globalOpts);
|
|
1221
|
+
let session;
|
|
1222
|
+
try {
|
|
1223
|
+
session = await loadSession(sessionId, sessionsDir);
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
if (!globalOpts.quiet) {
|
|
1226
|
+
console.error(
|
|
1227
|
+
`Error: ${error instanceof Error ? error.message : "Failed to load session"}`
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
process.exitCode = EXIT_CODES.SESSION_ERROR;
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const articles = await loadSessionArticles(session, sessionId, sessionsDir);
|
|
1234
|
+
const result = checkCoverage(articles, identifiers);
|
|
1235
|
+
if (options?.json) {
|
|
1236
|
+
console.log(formatCheckResultJson(result, { sessionId, source }));
|
|
1237
|
+
} else {
|
|
1238
|
+
if (!globalOpts.quiet) {
|
|
1239
|
+
console.log(formatCheckResult(result, {
|
|
1240
|
+
sessionId,
|
|
1241
|
+
source,
|
|
1242
|
+
missingOnly: options?.missingOnly
|
|
1243
|
+
}));
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
if (!globalOpts.quiet) {
|
|
1249
|
+
console.error(
|
|
1250
|
+
"Error:",
|
|
1251
|
+
error instanceof Error ? error.message : error
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
);
|
|
1075
1258
|
program.command("merge").description("Merge results from multiple search sessions").argument("<session-ids...>", "two or more session IDs to merge").option("--name <string>", "name for merged session").option("--dry-run", "show what would be merged without creating session").option("--json", "output as JSON").addHelpText("after", `
|
|
1076
1259
|
Examples:
|
|
1077
1260
|
$ search-hub merge session-v4 session-v9 # Merge two sessions
|