@ncukondo/search-hub 0.23.0 → 0.24.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.
Files changed (77) hide show
  1. package/dist/cli/commands/fulltext/check.d.ts +4 -0
  2. package/dist/cli/commands/fulltext/check.d.ts.map +1 -1
  3. package/dist/cli/commands/fulltext/check.js +41 -4
  4. package/dist/cli/commands/fulltext/check.js.map +1 -1
  5. package/dist/cli/commands/fulltext/index.d.ts.map +1 -1
  6. package/dist/cli/commands/fulltext/index.js +10 -0
  7. package/dist/cli/commands/fulltext/index.js.map +1 -1
  8. package/dist/cli/commands/fulltext/verify-pmcid.d.ts +28 -0
  9. package/dist/cli/commands/fulltext/verify-pmcid.d.ts.map +1 -0
  10. package/dist/cli/commands/fulltext/verify-pmcid.js +51 -0
  11. package/dist/cli/commands/fulltext/verify-pmcid.js.map +1 -0
  12. package/dist/cli/commands/query/init.d.ts +0 -1
  13. package/dist/cli/commands/query/init.d.ts.map +1 -1
  14. package/dist/cli/commands/query/init.js +2 -3
  15. package/dist/cli/commands/query/init.js.map +1 -1
  16. package/dist/cli/commands/query/resolve.d.ts.map +1 -1
  17. package/dist/cli/commands/query/resolve.js +5 -2
  18. package/dist/cli/commands/query/resolve.js.map +1 -1
  19. package/dist/cli/commands/register.d.ts +4 -0
  20. package/dist/cli/commands/register.d.ts.map +1 -1
  21. package/dist/cli/commands/register.js +62 -13
  22. package/dist/cli/commands/register.js.map +1 -1
  23. package/dist/cli/commands/upgrade.d.ts +28 -0
  24. package/dist/cli/commands/upgrade.d.ts.map +1 -0
  25. package/dist/cli/commands/upgrade.js +96 -0
  26. package/dist/cli/commands/upgrade.js.map +1 -0
  27. package/dist/cli/index.d.ts.map +1 -1
  28. package/dist/cli/index.js +12 -3
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/cli/suggestions/rules.js +1 -1
  31. package/dist/cli/suggestions/rules.js.map +1 -1
  32. package/dist/cli/utils/argv.d.ts +34 -0
  33. package/dist/cli/utils/argv.d.ts.map +1 -0
  34. package/dist/cli/utils/argv.js +63 -0
  35. package/dist/cli/utils/argv.js.map +1 -0
  36. package/dist/config/index.d.ts +1 -1
  37. package/dist/config/index.d.ts.map +1 -1
  38. package/dist/config/paths.d.ts +1 -1
  39. package/dist/config/paths.d.ts.map +1 -1
  40. package/dist/config/paths.js +6 -2
  41. package/dist/config/paths.js.map +1 -1
  42. package/dist/index.js +2 -2
  43. package/dist/integration/csl-json.d.ts +10 -0
  44. package/dist/integration/csl-json.d.ts.map +1 -1
  45. package/dist/integration/csl-json.js +12 -0
  46. package/dist/integration/csl-json.js.map +1 -1
  47. package/dist/integration/register.d.ts.map +1 -1
  48. package/dist/integration/register.js +1 -1
  49. package/dist/integration/register.js.map +1 -1
  50. package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js +25 -7
  51. package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js.map +1 -1
  52. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/index.js +67 -25
  53. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/index.js.map +1 -1
  54. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/pmc.js +3 -3
  55. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/pmc.js.map +1 -1
  56. package/dist/package.json.js +1 -1
  57. package/dist/upgrade/apply-binary.d.ts +37 -0
  58. package/dist/upgrade/apply-binary.d.ts.map +1 -0
  59. package/dist/upgrade/apply-binary.js +200 -0
  60. package/dist/upgrade/apply-binary.js.map +1 -0
  61. package/dist/upgrade/apply-npm.d.ts +23 -0
  62. package/dist/upgrade/apply-npm.d.ts.map +1 -0
  63. package/dist/upgrade/apply-npm.js +96 -0
  64. package/dist/upgrade/apply-npm.js.map +1 -0
  65. package/dist/upgrade/check.d.ts +21 -0
  66. package/dist/upgrade/check.d.ts.map +1 -0
  67. package/dist/upgrade/check.js +91 -0
  68. package/dist/upgrade/check.js.map +1 -0
  69. package/dist/upgrade/detect.d.ts +16 -0
  70. package/dist/upgrade/detect.d.ts.map +1 -0
  71. package/dist/upgrade/detect.js +65 -0
  72. package/dist/upgrade/detect.js.map +1 -0
  73. package/dist/upgrade/notifier.d.ts +26 -0
  74. package/dist/upgrade/notifier.d.ts.map +1 -0
  75. package/dist/upgrade/notifier.js +111 -0
  76. package/dist/upgrade/notifier.js.map +1 -0
  77. package/package.json +2 -2
@@ -7,6 +7,10 @@ export interface FulltextCheckOptions {
7
7
  export interface FulltextCheckArticleResult {
8
8
  doi?: string;
9
9
  pmid?: string;
10
+ /** PMCID discovered during OA check and verified against the article */
11
+ pmcid?: string;
12
+ /** PMCID discovered during OA check but rejected because its metadata did not match */
13
+ rejectedPmcid?: string;
10
14
  title: string;
11
15
  oaStatus: OAStatus;
12
16
  locationCount: number;
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/check.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAkC,KAAK,eAAe,EAAyB,KAAK,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAMzI,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,eAAe,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,QAAQ,EAAE,0BAA0B,EAAE,CAAC;CACxC;AAoHD;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAiC9B"}
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/check.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAkC,KAAK,eAAe,EAAyB,KAAK,QAAQ,EAAmB,MAAM,6BAA6B,CAAC;AAO1J,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,eAAe,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,QAAQ,EAAE,0BAA0B,EAAE,CAAC;CACxC;AAiLD;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAiC9B"}
@@ -5,6 +5,7 @@ import { discoverOA } from "../../../node_modules/@ncukondo/academic-fulltext/di
5
5
  import { loadMeta, saveMeta } from "../../../node_modules/@ncukondo/academic-fulltext/dist/meta.js";
6
6
  import "../../../node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js";
7
7
  import "node:crypto";
8
+ import { verifyPmcid } from "./verify-pmcid.js";
8
9
  const DEFAULT_CONCURRENCY = 3;
9
10
  async function loadIncludedArticles(sessionDir) {
10
11
  const reviewsPath = join(sessionDir, ".internal", "reviews.yaml");
@@ -37,26 +38,62 @@ async function findArticleDir(sessionDir, article) {
37
38
  }
38
39
  return null;
39
40
  }
41
+ async function validateDiscoveredPmcid(article, config, discoveredPmcid, oaStatus, locations) {
42
+ const hasPmcLocations = locations.some((loc) => loc.source === "pmc");
43
+ if (!discoveredPmcid || !hasPmcLocations) {
44
+ return { oaStatus, locations };
45
+ }
46
+ const verifyArticle = {
47
+ title: article.title
48
+ };
49
+ if (article.doi) verifyArticle.doi = article.doi;
50
+ if (article.pmid) verifyArticle.pmid = article.pmid;
51
+ const verifyOptions = {};
52
+ if (config.ncbiEmail) verifyOptions.ncbiEmail = config.ncbiEmail;
53
+ if (config.ncbiTool) verifyOptions.ncbiTool = config.ncbiTool;
54
+ const verification = await verifyPmcid(discoveredPmcid, verifyArticle, verifyOptions);
55
+ if (verification === "match") {
56
+ return { oaStatus, locations, verifiedPmcid: discoveredPmcid };
57
+ }
58
+ if (verification === "mismatch") {
59
+ const filtered = locations.filter((loc) => loc.source !== "pmc");
60
+ const status = filtered.length === 0 && oaStatus === "open" ? "unknown" : oaStatus;
61
+ return { oaStatus: status, locations: filtered, rejectedPmcid: discoveredPmcid };
62
+ }
63
+ return { oaStatus, locations };
64
+ }
40
65
  async function processArticle(article, sessionDir, config) {
41
66
  const discoveryArticle = {};
42
67
  if (article.doi) discoveryArticle.doi = article.doi;
43
68
  if (article.pmid) discoveryArticle.pmid = article.pmid;
44
69
  if (article.arxivId) discoveryArticle.arxivId = article.arxivId;
45
70
  const discoveryResult = await discoverOA(discoveryArticle, config);
71
+ const validated = await validateDiscoveredPmcid(
72
+ article,
73
+ config,
74
+ discoveryResult.discoveredIds.pmcid,
75
+ discoveryResult.oaStatus,
76
+ discoveryResult.locations
77
+ );
46
78
  const articleResult = {
47
79
  title: article.title,
48
- oaStatus: discoveryResult.oaStatus,
49
- locationCount: discoveryResult.locations.length
80
+ oaStatus: validated.oaStatus,
81
+ locationCount: validated.locations.length
50
82
  };
51
83
  if (article.doi) articleResult.doi = article.doi;
52
84
  if (article.pmid) articleResult.pmid = article.pmid;
85
+ if (validated.verifiedPmcid) articleResult.pmcid = validated.verifiedPmcid;
86
+ if (validated.rejectedPmcid) articleResult.rejectedPmcid = validated.rejectedPmcid;
53
87
  const dirName = await findArticleDir(sessionDir, article);
54
88
  if (dirName) {
55
89
  try {
56
90
  const metaPath = join(sessionDir, "fulltext", dirName, "meta.json");
57
91
  const meta = await loadMeta(metaPath);
58
- meta.oaStatus = discoveryResult.oaStatus;
59
- meta.oaLocations = discoveryResult.locations;
92
+ meta.oaStatus = validated.oaStatus;
93
+ meta.oaLocations = validated.locations;
94
+ if (validated.verifiedPmcid && !meta.pmcid) {
95
+ meta.pmcid = validated.verifiedPmcid;
96
+ }
60
97
  meta.checkedAt = (/* @__PURE__ */ new Date()).toISOString();
61
98
  await saveMeta(metaPath, meta);
62
99
  } catch {
@@ -1 +1 @@
1
- {"version":3,"file":"check.js","sources":["../../../../src/cli/commands/fulltext/check.ts"],"sourcesContent":["/**\n * Fulltext check command.\n * Checks OA availability for included articles in a session.\n */\n\nimport { readFile, readdir, access } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport { discoverOA, loadMeta, saveMeta, type DiscoveryConfig, type DiscoveryArticle, type OAStatus } from '@ncukondo/academic-fulltext';\nimport type { ReviewFile, ArticleEntry } from '../review/types';\n\n/** Default concurrency for parallel article processing */\nconst DEFAULT_CONCURRENCY = 3;\n\nexport interface FulltextCheckOptions {\n sessionDir: string;\n config: DiscoveryConfig;\n concurrency?: number;\n}\n\nexport interface FulltextCheckArticleResult {\n doi?: string;\n pmid?: string;\n title: string;\n oaStatus: OAStatus;\n locationCount: number;\n}\n\nexport interface FulltextCheckResult {\n summary: {\n total: number;\n open: number;\n closed: number;\n unknown: number;\n };\n articles: FulltextCheckArticleResult[];\n}\n\n/**\n * Load included articles from reviews.yaml\n */\nasync function loadIncludedArticles(sessionDir: string): Promise<ArticleEntry[]> {\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(content) as ReviewFile;\n return reviewFile.articles.filter((a) => a.finalDecision === 'include');\n}\n\n/**\n * Try to find a meta.json matching an article in the fulltext directory.\n * Returns the dirName if found, null otherwise.\n */\nasync function findArticleDir(\n sessionDir: string,\n article: ArticleEntry\n): Promise<string | null> {\n const fulltextDir = join(sessionDir, 'fulltext');\n try {\n await access(fulltextDir);\n } catch {\n return null;\n }\n\n let entries: string[];\n try {\n const dirEntries = await readdir(fulltextDir);\n entries = dirEntries.map(String);\n } catch {\n return null;\n }\n\n for (const entry of entries) {\n try {\n const metaPath = join(fulltextDir, entry, 'meta.json');\n const meta = await loadMeta(metaPath);\n // Match by DOI or PMID\n if (article.doi && meta.doi === article.doi) return entry;\n if (article.pmid && meta.pmid === article.pmid) return entry;\n } catch {\n // Skip entries without valid meta.json\n }\n }\n return null;\n}\n\n/**\n * Process a single article: run OA discovery and optionally update meta.json.\n */\nasync function processArticle(\n article: ArticleEntry,\n sessionDir: string,\n config: DiscoveryConfig\n): Promise<FulltextCheckArticleResult> {\n const discoveryArticle: DiscoveryArticle = {};\n if (article.doi) discoveryArticle.doi = article.doi;\n if (article.pmid) discoveryArticle.pmid = article.pmid;\n if (article.arxivId) discoveryArticle.arxivId = article.arxivId;\n const discoveryResult = await discoverOA(discoveryArticle, config);\n\n const articleResult: FulltextCheckArticleResult = {\n title: article.title,\n oaStatus: discoveryResult.oaStatus,\n locationCount: discoveryResult.locations.length,\n };\n if (article.doi) articleResult.doi = article.doi;\n if (article.pmid) articleResult.pmid = article.pmid;\n\n // Try to update meta.json if a fulltext directory exists for this article\n const dirName = await findArticleDir(sessionDir, article);\n if (dirName) {\n try {\n const metaPath = join(sessionDir, 'fulltext', dirName, 'meta.json');\n const meta = await loadMeta(metaPath);\n meta.oaStatus = discoveryResult.oaStatus;\n meta.oaLocations = discoveryResult.locations;\n meta.checkedAt = new Date().toISOString();\n await saveMeta(metaPath, meta);\n } catch {\n // Meta update is best-effort\n }\n }\n\n return articleResult;\n}\n\n/**\n * Run async tasks with a concurrency limit.\n */\nasync function runWithConcurrency<T>(\n tasks: Array<() => Promise<T>>,\n concurrency: number\n): Promise<PromiseSettledResult<T>[]> {\n const results: PromiseSettledResult<T>[] = new Array(tasks.length);\n let nextIndex = 0;\n\n async function worker(): Promise<void> {\n while (nextIndex < tasks.length) {\n const index = nextIndex++;\n try {\n const value = await tasks[index]!();\n results[index] = { status: 'fulfilled', value };\n } catch (reason) {\n results[index] = { status: 'rejected', reason };\n }\n }\n }\n\n const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker());\n await Promise.all(workers);\n return results;\n}\n\n/**\n * Execute the fulltext check command.\n * Checks OA availability for all included articles in a session,\n * processing articles in parallel with a concurrency limit.\n */\nexport async function executeFulltextCheck(\n options: FulltextCheckOptions\n): Promise<FulltextCheckResult> {\n const { sessionDir, config, concurrency = DEFAULT_CONCURRENCY } = options;\n\n // Load included articles\n const articles = await loadIncludedArticles(sessionDir);\n\n const summary = { total: articles.length, open: 0, closed: 0, unknown: 0 };\n\n const tasks = articles.map(\n (article) => () => processArticle(article, sessionDir, config)\n );\n\n const settled = await runWithConcurrency(tasks, concurrency);\n\n const results: FulltextCheckArticleResult[] = [];\n for (const result of settled) {\n if (result.status === 'fulfilled') {\n results.push(result.value);\n switch (result.value.oaStatus) {\n case 'open':\n summary.open++;\n break;\n case 'closed':\n summary.closed++;\n break;\n case 'unknown':\n summary.unknown++;\n break;\n }\n }\n }\n\n return { summary, articles: results };\n}\n"],"names":["parseYaml"],"mappings":"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"}
1
+ {"version":3,"file":"check.js","sources":["../../../../src/cli/commands/fulltext/check.ts"],"sourcesContent":["/**\n * Fulltext check command.\n * Checks OA availability for included articles in a session.\n */\n\nimport { readFile, readdir, access } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport { discoverOA, loadMeta, saveMeta, type DiscoveryConfig, type DiscoveryArticle, type OAStatus, type OALocation } from '@ncukondo/academic-fulltext';\nimport type { ReviewFile, ArticleEntry } from '../review/types';\nimport { verifyPmcid } from './verify-pmcid';\n\n/** Default concurrency for parallel article processing */\nconst DEFAULT_CONCURRENCY = 3;\n\nexport interface FulltextCheckOptions {\n sessionDir: string;\n config: DiscoveryConfig;\n concurrency?: number;\n}\n\nexport interface FulltextCheckArticleResult {\n doi?: string;\n pmid?: string;\n /** PMCID discovered during OA check and verified against the article */\n pmcid?: string;\n /** PMCID discovered during OA check but rejected because its metadata did not match */\n rejectedPmcid?: string;\n title: string;\n oaStatus: OAStatus;\n locationCount: number;\n}\n\nexport interface FulltextCheckResult {\n summary: {\n total: number;\n open: number;\n closed: number;\n unknown: number;\n };\n articles: FulltextCheckArticleResult[];\n}\n\n/**\n * Load included articles from reviews.yaml\n */\nasync function loadIncludedArticles(sessionDir: string): Promise<ArticleEntry[]> {\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(content) as ReviewFile;\n return reviewFile.articles.filter((a) => a.finalDecision === 'include');\n}\n\n/**\n * Try to find a meta.json matching an article in the fulltext directory.\n * Returns the dirName if found, null otherwise.\n */\nasync function findArticleDir(\n sessionDir: string,\n article: ArticleEntry\n): Promise<string | null> {\n const fulltextDir = join(sessionDir, 'fulltext');\n try {\n await access(fulltextDir);\n } catch {\n return null;\n }\n\n let entries: string[];\n try {\n const dirEntries = await readdir(fulltextDir);\n entries = dirEntries.map(String);\n } catch {\n return null;\n }\n\n for (const entry of entries) {\n try {\n const metaPath = join(fulltextDir, entry, 'meta.json');\n const meta = await loadMeta(metaPath);\n // Match by DOI or PMID\n if (article.doi && meta.doi === article.doi) return entry;\n if (article.pmid && meta.pmid === article.pmid) return entry;\n } catch {\n // Skip entries without valid meta.json\n }\n }\n return null;\n}\n\n/** Outcome of validating a discovered PMCID against the article. */\ninterface ValidatedDiscovery {\n oaStatus: OAStatus;\n locations: OALocation[];\n verifiedPmcid?: string;\n rejectedPmcid?: string;\n}\n\n/**\n * Validate a PMCID discovered during OA discovery against the article's\n * own metadata. On mismatch, drop all PMC locations so an unrelated\n * paper's fulltext is never fetched (issue #146).\n */\nasync function validateDiscoveredPmcid(\n article: ArticleEntry,\n config: DiscoveryConfig,\n discoveredPmcid: string | undefined,\n oaStatus: OAStatus,\n locations: OALocation[]\n): Promise<ValidatedDiscovery> {\n const hasPmcLocations = locations.some((loc) => loc.source === 'pmc');\n if (!discoveredPmcid || !hasPmcLocations) {\n return { oaStatus, locations };\n }\n\n const verifyArticle: { doi?: string; pmid?: string; title?: string } = {\n title: article.title,\n };\n if (article.doi) verifyArticle.doi = article.doi;\n if (article.pmid) verifyArticle.pmid = article.pmid;\n const verifyOptions: { ncbiEmail?: string; ncbiTool?: string } = {};\n if (config.ncbiEmail) verifyOptions.ncbiEmail = config.ncbiEmail;\n if (config.ncbiTool) verifyOptions.ncbiTool = config.ncbiTool;\n\n const verification = await verifyPmcid(discoveredPmcid, verifyArticle, verifyOptions);\n\n if (verification === 'match') {\n return { oaStatus, locations, verifiedPmcid: discoveredPmcid };\n }\n if (verification === 'mismatch') {\n const filtered = locations.filter((loc) => loc.source !== 'pmc');\n const status = filtered.length === 0 && oaStatus === 'open' ? 'unknown' : oaStatus;\n return { oaStatus: status, locations: filtered, rejectedPmcid: discoveredPmcid };\n }\n // Unverified: keep locations but do not vouch for the PMCID\n return { oaStatus, locations };\n}\n\n/**\n * Process a single article: run OA discovery and optionally update meta.json.\n */\nasync function processArticle(\n article: ArticleEntry,\n sessionDir: string,\n config: DiscoveryConfig\n): Promise<FulltextCheckArticleResult> {\n const discoveryArticle: DiscoveryArticle = {};\n if (article.doi) discoveryArticle.doi = article.doi;\n if (article.pmid) discoveryArticle.pmid = article.pmid;\n if (article.arxivId) discoveryArticle.arxivId = article.arxivId;\n const discoveryResult = await discoverOA(discoveryArticle, config);\n\n const validated = await validateDiscoveredPmcid(\n article,\n config,\n discoveryResult.discoveredIds.pmcid,\n discoveryResult.oaStatus,\n discoveryResult.locations\n );\n\n const articleResult: FulltextCheckArticleResult = {\n title: article.title,\n oaStatus: validated.oaStatus,\n locationCount: validated.locations.length,\n };\n if (article.doi) articleResult.doi = article.doi;\n if (article.pmid) articleResult.pmid = article.pmid;\n if (validated.verifiedPmcid) articleResult.pmcid = validated.verifiedPmcid;\n if (validated.rejectedPmcid) articleResult.rejectedPmcid = validated.rejectedPmcid;\n\n // Try to update meta.json if a fulltext directory exists for this article\n const dirName = await findArticleDir(sessionDir, article);\n if (dirName) {\n try {\n const metaPath = join(sessionDir, 'fulltext', dirName, 'meta.json');\n const meta = await loadMeta(metaPath);\n meta.oaStatus = validated.oaStatus;\n meta.oaLocations = validated.locations;\n if (validated.verifiedPmcid && !meta.pmcid) {\n meta.pmcid = validated.verifiedPmcid;\n }\n meta.checkedAt = new Date().toISOString();\n await saveMeta(metaPath, meta);\n } catch {\n // Meta update is best-effort\n }\n }\n\n return articleResult;\n}\n\n/**\n * Run async tasks with a concurrency limit.\n */\nasync function runWithConcurrency<T>(\n tasks: Array<() => Promise<T>>,\n concurrency: number\n): Promise<PromiseSettledResult<T>[]> {\n const results: PromiseSettledResult<T>[] = new Array(tasks.length);\n let nextIndex = 0;\n\n async function worker(): Promise<void> {\n while (nextIndex < tasks.length) {\n const index = nextIndex++;\n try {\n const value = await tasks[index]!();\n results[index] = { status: 'fulfilled', value };\n } catch (reason) {\n results[index] = { status: 'rejected', reason };\n }\n }\n }\n\n const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker());\n await Promise.all(workers);\n return results;\n}\n\n/**\n * Execute the fulltext check command.\n * Checks OA availability for all included articles in a session,\n * processing articles in parallel with a concurrency limit.\n */\nexport async function executeFulltextCheck(\n options: FulltextCheckOptions\n): Promise<FulltextCheckResult> {\n const { sessionDir, config, concurrency = DEFAULT_CONCURRENCY } = options;\n\n // Load included articles\n const articles = await loadIncludedArticles(sessionDir);\n\n const summary = { total: articles.length, open: 0, closed: 0, unknown: 0 };\n\n const tasks = articles.map(\n (article) => () => processArticle(article, sessionDir, config)\n );\n\n const settled = await runWithConcurrency(tasks, concurrency);\n\n const results: FulltextCheckArticleResult[] = [];\n for (const result of settled) {\n if (result.status === 'fulfilled') {\n results.push(result.value);\n switch (result.value.oaStatus) {\n case 'open':\n summary.open++;\n break;\n case 'closed':\n summary.closed++;\n break;\n case 'unknown':\n summary.unknown++;\n break;\n }\n }\n }\n\n return { summary, articles: results };\n}\n"],"names":["parseYaml"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAMpD,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,MAAM,CAAC,GACvD,IAAI,CAkVN"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAMpD,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,MAAM,CAAC,GACvD,IAAI,CA2VN"}
@@ -183,6 +183,16 @@ OA Status Summary:`);
183
183
  console.log(` Closed Access: ${result.summary.closed}`);
184
184
  console.log(` Unknown: ${result.summary.unknown}`);
185
185
  console.log(` Total: ${result.summary.total}`);
186
+ const rejected = result.articles.filter((a) => a.rejectedPmcid);
187
+ if (rejected.length > 0) {
188
+ console.log(`
189
+ Warnings:`);
190
+ for (const article of rejected) {
191
+ console.log(
192
+ ` Discovered ${article.rejectedPmcid} does not match "${article.title}" — PMC locations dropped.`
193
+ );
194
+ }
195
+ }
186
196
  if (result.summary.open > 0) {
187
197
  console.log(`
188
198
  Run \`fulltext fetch\` to download available OA articles.`);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/cli/commands/fulltext/index.ts"],"sourcesContent":["/**\n * Fulltext command group registration.\n */\n\nimport type { Command } from 'commander';\nimport { executeFulltextInit } from './init.js';\nimport { executeFulltextSync } from './sync.js';\nimport { executeFulltextConvert } from './convert.js';\nimport { executeFulltextCheck } from './check.js';\nimport { executeFulltextFetch } from './fetch.js';\nimport { executeFulltextAttach } from './attach.js';\nimport { executeFulltextStatus, type FulltextStatusResult } from './status.js';\nimport { executeFulltextPending, type PendingArticle } from './pending.js';\nimport { formatInitOutput, formatSyncOutput } from './format.js';\nimport type { GlobalOptions } from '../../index.js';\nimport { EXIT_CODES } from '../../exit-codes.js';\nimport { sessionExists } from '../../../session/manager.js';\nimport { loadConfig } from '../../../config/index.js';\nimport { join } from 'node:path';\n\nexport function registerFulltextCommands(\n program: Command,\n getSessionsDir: (opts: GlobalOptions) => Promise<string>,\n): void {\n const fulltextCommand = program\n .command('fulltext')\n .description('Fulltext management: retrieval, conversion, attachment')\n .addHelpText('after', `\nExamples:\n $ search-hub fulltext init SESSION_ID # Create directories for included articles\n $ search-hub fulltext init SESSION_ID --dry-run # Preview what would be created\n $ search-hub fulltext sync SESSION_ID # Detect and register added files\n $ search-hub fulltext sync SESSION_ID --dry-run # Preview what would be synced\n $ search-hub fulltext convert SESSION_ID # Convert PMC XML to Markdown\n $ search-hub fulltext check --session SESSION_ID # Check OA availability\n $ search-hub fulltext fetch SESSION_ID # Download available OA articles\n $ search-hub fulltext fetch SESSION_ID --dry-run # Preview what would be downloaded\n $ search-hub fulltext fetch SESSION_ID --source pmc # Download from specific sources\n $ search-hub fulltext attach SESSION_ID # Attach fulltexts to ref entries\n $ search-hub fulltext attach SESSION_ID --dry-run # Preview what would be attached\n $ search-hub fulltext status SESSION_ID # Show fulltext retrieval status\n $ search-hub fulltext pending SESSION_ID # List articles needing download`);\n\n fulltextCommand\n .command('init')\n .description('Create directories for included articles with meta.json and README')\n .argument('<session-id>', 'session ID')\n .option('--dry-run', 'show what would be created without creating', false)\n .action(async (sessionId: string, options: { dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n const result = await executeFulltextInit({\n sessionId,\n sessionsDir,\n dryRun: options.dryRun,\n });\n\n if (!globalOpts.quiet) {\n console.log(formatInitOutput(result));\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error(\n 'Error:',\n error instanceof Error ? error.message : error,\n );\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('sync')\n .description('Detect and register manually added fulltext files')\n .argument('<session-id>', 'session ID')\n .option('--dry-run', 'show what would be synced without modifying', false)\n .action(async (sessionId: string, options: { dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n const result = await executeFulltextSync({\n sessionId,\n sessionsDir,\n dryRun: options.dryRun,\n });\n\n if (!globalOpts.quiet) {\n console.log(formatSyncOutput(result));\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error(\n 'Error:',\n error instanceof Error ? error.message : error,\n );\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('convert')\n .description('Convert PMC XML files to Markdown')\n .argument('<session-id>', 'session ID')\n .option('--article <dir>', 'convert specific article directory')\n .action(async (sessionId: string, options?: { article?: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n if (!(await sessionExists(sessionId, sessionsDir))) {\n if (!globalOpts.quiet) {\n console.error(`Error: session '${sessionId}' not found`);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n return;\n }\n\n const convertOpts: Parameters<typeof executeFulltextConvert>[0] = { sessionId };\n if (options?.article) convertOpts.article = options.article;\n const result = await executeFulltextConvert(convertOpts, sessionsDir);\n\n if (!globalOpts.quiet) {\n console.log(`Converted: ${result.converted} Skipped: ${result.skipped} Failed: ${result.failed}`);\n for (const article of result.articles) {\n const icon = article.status === 'converted' ? '+' : article.status === 'skipped' ? '-' : '!';\n console.log(` [${icon}] ${article.dirName}: ${article.title}`);\n }\n }\n\n process.exitCode = result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.SESSION_ERROR;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('attach')\n .description('Attach fulltext files to reference-manager entries')\n .argument('<session-id>', 'session ID')\n .option('--dry-run', 'show what would be attached without attaching', false)\n .action(async (sessionId: string, options: { dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n if (!(await sessionExists(sessionId, sessionsDir))) {\n if (!globalOpts.quiet) {\n console.error(`Error: session '${sessionId}' not found`);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n return;\n }\n\n const sessionDir = join(sessionsDir, sessionId);\n\n const result = await executeFulltextAttach({\n sessionDir,\n dryRun: options.dryRun,\n });\n\n if (!globalOpts.quiet) {\n const prefix = options.dryRun ? 'Would attach' : 'Attached';\n console.log(`\\nFulltext ${prefix.toLowerCase()}:`);\n if (result.summary.attached > 0) {\n const totalFiles = result.attached.reduce((sum, a) => sum + a.files.length, 0);\n console.log(` ✓ ${result.summary.attached} articles (${totalFiles} files)`);\n for (const item of result.attached) {\n console.log(` ${item.refId}: ${item.files.join(', ')}`);\n }\n }\n if (result.summary.skipped > 0) {\n console.log(` ⚠ ${result.summary.skipped} skipped`);\n }\n if (result.summary.failed > 0) {\n console.log(` ✗ ${result.summary.failed} failed`);\n for (const item of result.failed) {\n console.log(` ${item.dirName}: ${item.reason}`);\n }\n }\n if (result.summary.total === 0) {\n console.log(' No fulltext directories found.');\n }\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error(\n 'Error:',\n error instanceof Error ? error.message : error,\n );\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('check')\n .description('Check Open Access availability for included articles')\n .requiredOption('--session <id>', 'session ID')\n .option('--format <format>', 'output format (table or json)', 'table')\n .action(async (options: { session: string; format: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n const sessionDir = join(sessionsDir, options.session);\n const config = await loadConfig(\n globalOpts.config ? { globalConfigPath: globalOpts.config } : {}\n );\n const result = await executeFulltextCheck({\n sessionDir,\n config: {\n unpaywallEmail: config.fulltext?.sources?.unpaywall_email ?? '',\n coreApiKey: config.fulltext?.sources?.core_api_key ?? '',\n ncbiEmail: config.fulltext?.sources?.ncbi_email ?? '',\n ncbiTool: config.fulltext?.sources?.ncbi_tool ?? 'search-hub',\n preferSources: config.fulltext?.sources?.prefer_sources ?? ['pmc', 'arxiv', 'unpaywall', 'core'],\n },\n });\n\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (!globalOpts.quiet) {\n console.log(`\\nOA Status Summary:`);\n console.log(` Open Access: ${result.summary.open}`);\n console.log(` Closed Access: ${result.summary.closed}`);\n console.log(` Unknown: ${result.summary.unknown}`);\n console.log(` Total: ${result.summary.total}`);\n if (result.summary.open > 0) {\n console.log(`\\nRun \\`fulltext fetch\\` to download available OA articles.`);\n }\n }\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('fetch')\n .description('Download available OA fulltexts')\n .argument('<session-id>', 'session ID')\n .option('--source <sources>', 'filter by source (comma-separated: pmc,arxiv,unpaywall,core)')\n .option('--no-convert-markdown', 'skip auto-conversion of PMC XML to Markdown')\n .option('--dry-run', 'show what would be downloaded without downloading', false)\n .action(async (sessionId: string, options: { source?: string; convertMarkdown: boolean; dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n const fetchOpts: Parameters<typeof executeFulltextFetch>[0] = {\n sessionId,\n sessionsDir,\n convertMarkdown: options.convertMarkdown,\n dryRun: options.dryRun,\n };\n if (options.source) fetchOpts.source = options.source.split(',');\n const result = await executeFulltextFetch(fetchOpts);\n\n if (!globalOpts.quiet) {\n if (result.dryRun) {\n console.log(`\\nDry run: would fetch ${result.articles.length} articles`);\n for (const article of result.articles) {\n console.log(` ${article.dirName}: ${article.title} (${article.locationCount} sources)`);\n }\n } else {\n console.log(`\\nFulltext Fetch Summary:`);\n console.log(` Downloaded: ${result.summary.downloaded}`);\n console.log(` Failed: ${result.summary.failed}`);\n console.log(` Skipped: ${result.summary.skipped}`);\n console.log(` Total: ${result.summary.total}`);\n }\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('status')\n .description('Show overall fulltext retrieval status')\n .argument('<session-id>', 'session ID')\n .option('--format <format>', 'output format (table or json)', 'table')\n .action(async (sessionId: string, options: { format: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n const sessionDir = join(sessionsDir, sessionId);\n const result = await executeFulltextStatus({\n sessionDir,\n format: options.format as 'table' | 'json',\n });\n\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (!globalOpts.quiet) {\n console.log(formatStatusOutput(result, sessionId));\n }\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('pending')\n .description('List articles needing manual fulltext download')\n .argument('<session-id>', 'session ID')\n .option('--format <format>', 'output format (table or json)', 'table')\n .option('--export <file>', 'export URLs to file for batch download')\n .action(async (sessionId: string, options: { format: string; export?: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n const sessionDir = join(sessionsDir, sessionId);\n const result = await executeFulltextPending({\n sessionDir,\n format: options.format as 'table' | 'json',\n exportPath: options.export,\n });\n\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (!globalOpts.quiet) {\n console.log(formatPendingOutput(result.articles, result.totalPending));\n if (options.export) {\n console.log(`\\nExported URLs to ${options.export}`);\n } else if (result.totalPending > 0) {\n console.log(`\\nExport URLs: fulltext pending ${sessionId} --export urls.txt`);\n }\n }\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n}\n\nfunction formatStatusOutput(result: FulltextStatusResult, sessionId: string): string {\n const lines = [\n `Fulltext Status: ${sessionId}`,\n '',\n ` Included articles: ${result.totalIncluded}`,\n ` With fulltext: ${result.withFulltext}`,\n ` - PDF only: ${result.pdfOnly}`,\n ` - Markdown only: ${result.markdownOnly}`,\n ` - Both: ${result.both}`,\n ` Pending: ${result.pending} (directories created, no files)`,\n ` Not initialized: ${result.notInitialized} (no directory)`,\n ];\n return lines.join('\\n');\n}\n\nfunction formatPendingOutput(articles: PendingArticle[], total: number): string {\n if (total === 0) {\n return 'All included articles have fulltext.';\n }\n\n const lines: string[] = [`${total} articles need fulltext:`, ''];\n\n for (const [i, article] of articles.entries()) {\n const num = i + 1;\n const identifier = article.dirName ?? '(not initialized)';\n lines.push(`${num}. ${identifier} - \"${article.title}\"`);\n if (article.doi) {\n lines.push(` DOI: ${article.doi}`);\n }\n if (article.publisherUrl) {\n lines.push(` Publisher: ${article.publisherUrl}`);\n }\n if (article.oaLocations) {\n for (const loc of article.oaLocations) {\n lines.push(` ${loc.source}: ${loc.url}`);\n }\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/cli/commands/fulltext/index.ts"],"sourcesContent":["/**\n * Fulltext command group registration.\n */\n\nimport type { Command } from 'commander';\nimport { executeFulltextInit } from './init.js';\nimport { executeFulltextSync } from './sync.js';\nimport { executeFulltextConvert } from './convert.js';\nimport { executeFulltextCheck } from './check.js';\nimport { executeFulltextFetch } from './fetch.js';\nimport { executeFulltextAttach } from './attach.js';\nimport { executeFulltextStatus, type FulltextStatusResult } from './status.js';\nimport { executeFulltextPending, type PendingArticle } from './pending.js';\nimport { formatInitOutput, formatSyncOutput } from './format.js';\nimport type { GlobalOptions } from '../../index.js';\nimport { EXIT_CODES } from '../../exit-codes.js';\nimport { sessionExists } from '../../../session/manager.js';\nimport { loadConfig } from '../../../config/index.js';\nimport { join } from 'node:path';\n\nexport function registerFulltextCommands(\n program: Command,\n getSessionsDir: (opts: GlobalOptions) => Promise<string>,\n): void {\n const fulltextCommand = program\n .command('fulltext')\n .description('Fulltext management: retrieval, conversion, attachment')\n .addHelpText('after', `\nExamples:\n $ search-hub fulltext init SESSION_ID # Create directories for included articles\n $ search-hub fulltext init SESSION_ID --dry-run # Preview what would be created\n $ search-hub fulltext sync SESSION_ID # Detect and register added files\n $ search-hub fulltext sync SESSION_ID --dry-run # Preview what would be synced\n $ search-hub fulltext convert SESSION_ID # Convert PMC XML to Markdown\n $ search-hub fulltext check --session SESSION_ID # Check OA availability\n $ search-hub fulltext fetch SESSION_ID # Download available OA articles\n $ search-hub fulltext fetch SESSION_ID --dry-run # Preview what would be downloaded\n $ search-hub fulltext fetch SESSION_ID --source pmc # Download from specific sources\n $ search-hub fulltext attach SESSION_ID # Attach fulltexts to ref entries\n $ search-hub fulltext attach SESSION_ID --dry-run # Preview what would be attached\n $ search-hub fulltext status SESSION_ID # Show fulltext retrieval status\n $ search-hub fulltext pending SESSION_ID # List articles needing download`);\n\n fulltextCommand\n .command('init')\n .description('Create directories for included articles with meta.json and README')\n .argument('<session-id>', 'session ID')\n .option('--dry-run', 'show what would be created without creating', false)\n .action(async (sessionId: string, options: { dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n const result = await executeFulltextInit({\n sessionId,\n sessionsDir,\n dryRun: options.dryRun,\n });\n\n if (!globalOpts.quiet) {\n console.log(formatInitOutput(result));\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error(\n 'Error:',\n error instanceof Error ? error.message : error,\n );\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('sync')\n .description('Detect and register manually added fulltext files')\n .argument('<session-id>', 'session ID')\n .option('--dry-run', 'show what would be synced without modifying', false)\n .action(async (sessionId: string, options: { dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n const result = await executeFulltextSync({\n sessionId,\n sessionsDir,\n dryRun: options.dryRun,\n });\n\n if (!globalOpts.quiet) {\n console.log(formatSyncOutput(result));\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error(\n 'Error:',\n error instanceof Error ? error.message : error,\n );\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('convert')\n .description('Convert PMC XML files to Markdown')\n .argument('<session-id>', 'session ID')\n .option('--article <dir>', 'convert specific article directory')\n .action(async (sessionId: string, options?: { article?: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n if (!(await sessionExists(sessionId, sessionsDir))) {\n if (!globalOpts.quiet) {\n console.error(`Error: session '${sessionId}' not found`);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n return;\n }\n\n const convertOpts: Parameters<typeof executeFulltextConvert>[0] = { sessionId };\n if (options?.article) convertOpts.article = options.article;\n const result = await executeFulltextConvert(convertOpts, sessionsDir);\n\n if (!globalOpts.quiet) {\n console.log(`Converted: ${result.converted} Skipped: ${result.skipped} Failed: ${result.failed}`);\n for (const article of result.articles) {\n const icon = article.status === 'converted' ? '+' : article.status === 'skipped' ? '-' : '!';\n console.log(` [${icon}] ${article.dirName}: ${article.title}`);\n }\n }\n\n process.exitCode = result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.SESSION_ERROR;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('attach')\n .description('Attach fulltext files to reference-manager entries')\n .argument('<session-id>', 'session ID')\n .option('--dry-run', 'show what would be attached without attaching', false)\n .action(async (sessionId: string, options: { dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n if (!(await sessionExists(sessionId, sessionsDir))) {\n if (!globalOpts.quiet) {\n console.error(`Error: session '${sessionId}' not found`);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n return;\n }\n\n const sessionDir = join(sessionsDir, sessionId);\n\n const result = await executeFulltextAttach({\n sessionDir,\n dryRun: options.dryRun,\n });\n\n if (!globalOpts.quiet) {\n const prefix = options.dryRun ? 'Would attach' : 'Attached';\n console.log(`\\nFulltext ${prefix.toLowerCase()}:`);\n if (result.summary.attached > 0) {\n const totalFiles = result.attached.reduce((sum, a) => sum + a.files.length, 0);\n console.log(` ✓ ${result.summary.attached} articles (${totalFiles} files)`);\n for (const item of result.attached) {\n console.log(` ${item.refId}: ${item.files.join(', ')}`);\n }\n }\n if (result.summary.skipped > 0) {\n console.log(` ⚠ ${result.summary.skipped} skipped`);\n }\n if (result.summary.failed > 0) {\n console.log(` ✗ ${result.summary.failed} failed`);\n for (const item of result.failed) {\n console.log(` ${item.dirName}: ${item.reason}`);\n }\n }\n if (result.summary.total === 0) {\n console.log(' No fulltext directories found.');\n }\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error(\n 'Error:',\n error instanceof Error ? error.message : error,\n );\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('check')\n .description('Check Open Access availability for included articles')\n .requiredOption('--session <id>', 'session ID')\n .option('--format <format>', 'output format (table or json)', 'table')\n .action(async (options: { session: string; format: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n const sessionDir = join(sessionsDir, options.session);\n const config = await loadConfig(\n globalOpts.config ? { globalConfigPath: globalOpts.config } : {}\n );\n const result = await executeFulltextCheck({\n sessionDir,\n config: {\n unpaywallEmail: config.fulltext?.sources?.unpaywall_email ?? '',\n coreApiKey: config.fulltext?.sources?.core_api_key ?? '',\n ncbiEmail: config.fulltext?.sources?.ncbi_email ?? '',\n ncbiTool: config.fulltext?.sources?.ncbi_tool ?? 'search-hub',\n preferSources: config.fulltext?.sources?.prefer_sources ?? ['pmc', 'arxiv', 'unpaywall', 'core'],\n },\n });\n\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (!globalOpts.quiet) {\n console.log(`\\nOA Status Summary:`);\n console.log(` Open Access: ${result.summary.open}`);\n console.log(` Closed Access: ${result.summary.closed}`);\n console.log(` Unknown: ${result.summary.unknown}`);\n console.log(` Total: ${result.summary.total}`);\n const rejected = result.articles.filter((a) => a.rejectedPmcid);\n if (rejected.length > 0) {\n console.log(`\\nWarnings:`);\n for (const article of rejected) {\n console.log(\n ` Discovered ${article.rejectedPmcid} does not match \"${article.title}\" — PMC locations dropped.`\n );\n }\n }\n if (result.summary.open > 0) {\n console.log(`\\nRun \\`fulltext fetch\\` to download available OA articles.`);\n }\n }\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('fetch')\n .description('Download available OA fulltexts')\n .argument('<session-id>', 'session ID')\n .option('--source <sources>', 'filter by source (comma-separated: pmc,arxiv,unpaywall,core)')\n .option('--no-convert-markdown', 'skip auto-conversion of PMC XML to Markdown')\n .option('--dry-run', 'show what would be downloaded without downloading', false)\n .action(async (sessionId: string, options: { source?: string; convertMarkdown: boolean; dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n const fetchOpts: Parameters<typeof executeFulltextFetch>[0] = {\n sessionId,\n sessionsDir,\n convertMarkdown: options.convertMarkdown,\n dryRun: options.dryRun,\n };\n if (options.source) fetchOpts.source = options.source.split(',');\n const result = await executeFulltextFetch(fetchOpts);\n\n if (!globalOpts.quiet) {\n if (result.dryRun) {\n console.log(`\\nDry run: would fetch ${result.articles.length} articles`);\n for (const article of result.articles) {\n console.log(` ${article.dirName}: ${article.title} (${article.locationCount} sources)`);\n }\n } else {\n console.log(`\\nFulltext Fetch Summary:`);\n console.log(` Downloaded: ${result.summary.downloaded}`);\n console.log(` Failed: ${result.summary.failed}`);\n console.log(` Skipped: ${result.summary.skipped}`);\n console.log(` Total: ${result.summary.total}`);\n }\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('status')\n .description('Show overall fulltext retrieval status')\n .argument('<session-id>', 'session ID')\n .option('--format <format>', 'output format (table or json)', 'table')\n .action(async (sessionId: string, options: { format: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n const sessionDir = join(sessionsDir, sessionId);\n const result = await executeFulltextStatus({\n sessionDir,\n format: options.format as 'table' | 'json',\n });\n\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (!globalOpts.quiet) {\n console.log(formatStatusOutput(result, sessionId));\n }\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('pending')\n .description('List articles needing manual fulltext download')\n .argument('<session-id>', 'session ID')\n .option('--format <format>', 'output format (table or json)', 'table')\n .option('--export <file>', 'export URLs to file for batch download')\n .action(async (sessionId: string, options: { format: string; export?: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n const sessionDir = join(sessionsDir, sessionId);\n const result = await executeFulltextPending({\n sessionDir,\n format: options.format as 'table' | 'json',\n exportPath: options.export,\n });\n\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (!globalOpts.quiet) {\n console.log(formatPendingOutput(result.articles, result.totalPending));\n if (options.export) {\n console.log(`\\nExported URLs to ${options.export}`);\n } else if (result.totalPending > 0) {\n console.log(`\\nExport URLs: fulltext pending ${sessionId} --export urls.txt`);\n }\n }\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n}\n\nfunction formatStatusOutput(result: FulltextStatusResult, sessionId: string): string {\n const lines = [\n `Fulltext Status: ${sessionId}`,\n '',\n ` Included articles: ${result.totalIncluded}`,\n ` With fulltext: ${result.withFulltext}`,\n ` - PDF only: ${result.pdfOnly}`,\n ` - Markdown only: ${result.markdownOnly}`,\n ` - Both: ${result.both}`,\n ` Pending: ${result.pending} (directories created, no files)`,\n ` Not initialized: ${result.notInitialized} (no directory)`,\n ];\n return lines.join('\\n');\n}\n\nfunction formatPendingOutput(articles: PendingArticle[], total: number): string {\n if (total === 0) {\n return 'All included articles have fulltext.';\n }\n\n const lines: string[] = [`${total} articles need fulltext:`, ''];\n\n for (const [i, article] of articles.entries()) {\n const num = i + 1;\n const identifier = article.dirName ?? '(not initialized)';\n lines.push(`${num}. ${identifier} - \"${article.title}\"`);\n if (article.doi) {\n lines.push(` DOI: ${article.doi}`);\n }\n if (article.publisherUrl) {\n lines.push(` Publisher: ${article.publisherUrl}`);\n }\n if (article.oaLocations) {\n for (const loc of article.oaLocations) {\n lines.push(` ${loc.source}: ${loc.url}`);\n }\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * PMCID verification against source article metadata.
3
+ *
4
+ * OA discovery can resolve a PMCID that belongs to a different paper
5
+ * (e.g. a citing article returned by elink). Before attaching PMC
6
+ * locations to an article, cross-check the PMC record's DOI/PMID/title
7
+ * against the source article via NCBI esummary (issue #146).
8
+ */
9
+ export interface PmcidVerifyArticle {
10
+ doi?: string;
11
+ pmid?: string;
12
+ title?: string;
13
+ }
14
+ export type PmcidVerification = 'match' | 'mismatch' | 'unverified';
15
+ export interface VerifyPmcidOptions {
16
+ ncbiEmail?: string;
17
+ ncbiTool?: string;
18
+ /** Injectable fetch for testing */
19
+ fetchFn?: typeof fetch;
20
+ }
21
+ /**
22
+ * Verify that a PMCID actually refers to the given article.
23
+ * Compares by DOI first, then PMID, then normalized title.
24
+ * Returns 'unverified' when the PMC record cannot be fetched or
25
+ * shares no comparable field with the article.
26
+ */
27
+ export declare function verifyPmcid(pmcid: string, article: PmcidVerifyArticle, options?: VerifyPmcidOptions): Promise<PmcidVerification>;
28
+ //# sourceMappingURL=verify-pmcid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-pmcid.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/verify-pmcid.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,UAAU,GAAG,YAAY,CAAC;AAEpE,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAuDD;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAuB5B"}
@@ -0,0 +1,51 @@
1
+ const ESUMMARY_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi";
2
+ function normalizeDoi(doi) {
3
+ return doi.trim().toLowerCase().replace(/^https?:\/\/(dx\.)?doi\.org\//, "").replace(/^doi:/, "");
4
+ }
5
+ function normalizeTitle(title) {
6
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "");
7
+ }
8
+ async function fetchPmcRecord(pmcid, options) {
9
+ const numericId = pmcid.replace(/^PMC/i, "");
10
+ const params = new URLSearchParams({ db: "pmc", id: numericId, retmode: "json" });
11
+ if (options.ncbiTool) params.set("tool", options.ncbiTool);
12
+ if (options.ncbiEmail) params.set("email", options.ncbiEmail);
13
+ const fetchFn = options.fetchFn ?? fetch;
14
+ try {
15
+ const response = await fetchFn(`${ESUMMARY_URL}?${params.toString()}`);
16
+ if (!response.ok) return null;
17
+ const data = await response.json();
18
+ const record = data.result?.[numericId];
19
+ if (!record || typeof record !== "object") return null;
20
+ return record;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ function findArticleId(record, idtype) {
26
+ return record.articleids?.find((id) => id.idtype === idtype)?.value;
27
+ }
28
+ async function verifyPmcid(pmcid, article, options = {}) {
29
+ const record = await fetchPmcRecord(pmcid, options);
30
+ if (!record) return "unverified";
31
+ const recordDoi = findArticleId(record, "doi");
32
+ if (article.doi && recordDoi) {
33
+ return normalizeDoi(article.doi) === normalizeDoi(recordDoi) ? "match" : "mismatch";
34
+ }
35
+ const recordPmid = findArticleId(record, "pmid");
36
+ if (article.pmid && recordPmid) {
37
+ return article.pmid.trim() === recordPmid.trim() ? "match" : "mismatch";
38
+ }
39
+ if (article.title && record.title) {
40
+ const articleTitle = normalizeTitle(article.title);
41
+ const recordTitle = normalizeTitle(record.title);
42
+ if (articleTitle && recordTitle) {
43
+ return articleTitle === recordTitle ? "match" : "mismatch";
44
+ }
45
+ }
46
+ return "unverified";
47
+ }
48
+ export {
49
+ verifyPmcid
50
+ };
51
+ //# sourceMappingURL=verify-pmcid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-pmcid.js","sources":["../../../../src/cli/commands/fulltext/verify-pmcid.ts"],"sourcesContent":["/**\n * PMCID verification against source article metadata.\n *\n * OA discovery can resolve a PMCID that belongs to a different paper\n * (e.g. a citing article returned by elink). Before attaching PMC\n * locations to an article, cross-check the PMC record's DOI/PMID/title\n * against the source article via NCBI esummary (issue #146).\n */\n\nconst ESUMMARY_URL = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi';\n\nexport interface PmcidVerifyArticle {\n doi?: string;\n pmid?: string;\n title?: string;\n}\n\nexport type PmcidVerification = 'match' | 'mismatch' | 'unverified';\n\nexport interface VerifyPmcidOptions {\n ncbiEmail?: string;\n ncbiTool?: string;\n /** Injectable fetch for testing */\n fetchFn?: typeof fetch;\n}\n\ninterface EsummaryArticleId {\n idtype?: string;\n value?: string;\n}\n\ninterface EsummaryRecord {\n title?: string;\n articleids?: EsummaryArticleId[];\n}\n\n/** Lowercase a DOI and strip url/prefix decorations for comparison. */\nfunction normalizeDoi(doi: string): string {\n return doi\n .trim()\n .toLowerCase()\n .replace(/^https?:\\/\\/(dx\\.)?doi\\.org\\//, '')\n .replace(/^doi:/, '');\n}\n\n/** Reduce a title to lowercase alphanumerics for tolerant comparison. */\nfunction normalizeTitle(title: string): string {\n return title.toLowerCase().replace(/[^a-z0-9]+/g, '');\n}\n\n/** Fetch the esummary record for a PMC ID. Returns null on any failure. */\nasync function fetchPmcRecord(\n pmcid: string,\n options: VerifyPmcidOptions\n): Promise<EsummaryRecord | null> {\n const numericId = pmcid.replace(/^PMC/i, '');\n const params = new URLSearchParams({ db: 'pmc', id: numericId, retmode: 'json' });\n if (options.ncbiTool) params.set('tool', options.ncbiTool);\n if (options.ncbiEmail) params.set('email', options.ncbiEmail);\n\n const fetchFn = options.fetchFn ?? fetch;\n try {\n const response = await fetchFn(`${ESUMMARY_URL}?${params.toString()}`);\n if (!response.ok) return null;\n const data = (await response.json()) as {\n result?: Record<string, unknown>;\n };\n const record = data.result?.[numericId];\n if (!record || typeof record !== 'object') return null;\n return record as EsummaryRecord;\n } catch {\n return null;\n }\n}\n\nfunction findArticleId(record: EsummaryRecord, idtype: string): string | undefined {\n return record.articleids?.find((id) => id.idtype === idtype)?.value;\n}\n\n/**\n * Verify that a PMCID actually refers to the given article.\n * Compares by DOI first, then PMID, then normalized title.\n * Returns 'unverified' when the PMC record cannot be fetched or\n * shares no comparable field with the article.\n */\nexport async function verifyPmcid(\n pmcid: string,\n article: PmcidVerifyArticle,\n options: VerifyPmcidOptions = {}\n): Promise<PmcidVerification> {\n const record = await fetchPmcRecord(pmcid, options);\n if (!record) return 'unverified';\n\n const recordDoi = findArticleId(record, 'doi');\n if (article.doi && recordDoi) {\n return normalizeDoi(article.doi) === normalizeDoi(recordDoi) ? 'match' : 'mismatch';\n }\n\n const recordPmid = findArticleId(record, 'pmid');\n if (article.pmid && recordPmid) {\n return article.pmid.trim() === recordPmid.trim() ? 'match' : 'mismatch';\n }\n\n if (article.title && record.title) {\n const articleTitle = normalizeTitle(article.title);\n const recordTitle = normalizeTitle(record.title);\n if (articleTitle && recordTitle) {\n return articleTitle === recordTitle ? 'match' : 'mismatch';\n }\n }\n\n return 'unverified';\n}\n"],"names":[],"mappings":"AASA,MAAM,eAAe;AA4BrB,SAAS,aAAa,KAAqB;AACzC,SAAO,IACJ,OACA,cACA,QAAQ,iCAAiC,EAAE,EAC3C,QAAQ,SAAS,EAAE;AACxB;AAGA,SAAS,eAAe,OAAuB;AAC7C,SAAO,MAAM,YAAA,EAAc,QAAQ,eAAe,EAAE;AACtD;AAGA,eAAe,eACb,OACA,SACgC;AAChC,QAAM,YAAY,MAAM,QAAQ,SAAS,EAAE;AAC3C,QAAM,SAAS,IAAI,gBAAgB,EAAE,IAAI,OAAO,IAAI,WAAW,SAAS,QAAQ;AAChF,MAAI,QAAQ,SAAU,QAAO,IAAI,QAAQ,QAAQ,QAAQ;AACzD,MAAI,QAAQ,UAAW,QAAO,IAAI,SAAS,QAAQ,SAAS;AAE5D,QAAM,UAAU,QAAQ,WAAW;AACnC,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,GAAG,YAAY,IAAI,OAAO,SAAA,CAAU,EAAE;AACrE,QAAI,CAAC,SAAS,GAAI,QAAO;AACzB,UAAM,OAAQ,MAAM,SAAS,KAAA;AAG7B,UAAM,SAAS,KAAK,SAAS,SAAS;AACtC,QAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,cAAc,QAAwB,QAAoC;AACjF,SAAO,OAAO,YAAY,KAAK,CAAC,OAAO,GAAG,WAAW,MAAM,GAAG;AAChE;AAQA,eAAsB,YACpB,OACA,SACA,UAA8B,CAAA,GACF;AAC5B,QAAM,SAAS,MAAM,eAAe,OAAO,OAAO;AAClD,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,YAAY,cAAc,QAAQ,KAAK;AAC7C,MAAI,QAAQ,OAAO,WAAW;AAC5B,WAAO,aAAa,QAAQ,GAAG,MAAM,aAAa,SAAS,IAAI,UAAU;AAAA,EAC3E;AAEA,QAAM,aAAa,cAAc,QAAQ,MAAM;AAC/C,MAAI,QAAQ,QAAQ,YAAY;AAC9B,WAAO,QAAQ,KAAK,KAAA,MAAW,WAAW,KAAA,IAAS,UAAU;AAAA,EAC/D;AAEA,MAAI,QAAQ,SAAS,OAAO,OAAO;AACjC,UAAM,eAAe,eAAe,QAAQ,KAAK;AACjD,UAAM,cAAc,eAAe,OAAO,KAAK;AAC/C,QAAI,gBAAgB,aAAa;AAC/B,aAAO,iBAAiB,cAAc,UAAU;AAAA,IAClD;AAAA,EACF;AAEA,SAAO;AACT;"}
@@ -14,7 +14,6 @@ export declare const QUERY_SCHEMA_FILENAME = "query.schema.json";
14
14
  * @returns The YAML template string with comments
15
15
  */
16
16
  export declare function generateQueryTemplate(title?: string): string;
17
- export declare const QUERIES_DIR = "queries";
18
17
  export interface WriteQueryTemplateOptions {
19
18
  title: string;
20
19
  output?: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"AASA;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYzD;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AA2DzD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAI5D;AAED,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAwChJ"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"AAUA;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYzD;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AA2DzD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAI5D;AAGD,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAwChJ"}
@@ -1,6 +1,7 @@
1
1
  import { access, mkdir, writeFile } from "node:fs/promises";
2
2
  import { join, dirname } from "node:path";
3
3
  import { generateQueryJSONSchema } from "../../../query/json-schema.js";
4
+ import { getQueriesDir } from "../../../config/paths.js";
4
5
  function sanitizeForFilename(title) {
5
6
  const result = title.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
6
7
  if (!result) {
@@ -69,13 +70,12 @@ function generateQueryTemplate(title) {
69
70
  const escaped = title.replace(/"/g, '\\"');
70
71
  return QUERY_TEMPLATE.replace("name: my_search", `name: "${escaped}"`);
71
72
  }
72
- const QUERIES_DIR = "queries";
73
73
  async function writeQueryTemplate(options) {
74
74
  const template = generateQueryTemplate(options.title);
75
75
  if (options.stdout) {
76
76
  return { success: true, message: template };
77
77
  }
78
- const outputPath = options.output ?? join(options.cwd ?? process.cwd(), QUERIES_DIR, `${sanitizeForFilename(options.title)}.yaml`);
78
+ const outputPath = options.output ?? join(getQueriesDir(options.cwd), `${sanitizeForFilename(options.title)}.yaml`);
79
79
  if (!options.force) {
80
80
  try {
81
81
  await access(outputPath);
@@ -98,7 +98,6 @@ async function writeQueryTemplate(options) {
98
98
  };
99
99
  }
100
100
  export {
101
- QUERIES_DIR,
102
101
  QUERY_SCHEMA_FILENAME,
103
102
  generateQueryTemplate,
104
103
  sanitizeForFilename,
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sources":["../../../../src/cli/commands/query/init.ts"],"sourcesContent":["/**\n * Query init command implementation.\n *\n * Generates a skeleton YAML query file with helpful comments.\n */\nimport { writeFile as fsWriteFile, access, mkdir } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { generateQueryJSONSchema } from \"../../../query/json-schema.js\";\n\n/**\n * Sanitize a title string into a safe filename (without extension).\n */\nexport function sanitizeForFilename(title: string): string {\n const result = title\n .trim()\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9_-]/g, '')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n if (!result) {\n throw new Error('Title produces an empty filename after sanitization');\n }\n return result;\n}\n\n/**\n * The YAML template string with comments preserved.\n * This is a raw string (not generated by a YAML library) so comments are kept.\n */\nexport const QUERY_SCHEMA_FILENAME = \"query.schema.json\";\n\n// prettier-ignore\nconst QUERY_TEMPLATE =\n `# yaml-language-server: $schema=./${QUERY_SCHEMA_FILENAME}\\n` +\n \"name: my_search\\n\" +\n \"description: \\\"\\\"\\n\" +\n \"\\n\" +\n \"query:\\n\" +\n \" - id: concept-1 # Unique block identifier (for provider replacements)\\n\" +\n \" field: title_abstract # title, abstract, title_abstract, author, keyword, all\\n\" +\n \" terms:\\n\" +\n \" keywords:\\n\" +\n \" - \\\"search term 1\\\"\\n\" +\n \" - \\\"search term 2\\\"\\n\" +\n \" # mesh: # PubMed MeSH terms (optional)\\n\" +\n \" # - \\\"MeSH Heading\\\"\\n\" +\n \" # eric: # ERIC Descriptors (optional, ERIC only)\\n\" +\n \" # - \\\"ERIC Descriptor\\\"\\n\" +\n \" exclude: [] # Terms to exclude (NOT operator)\\n\" +\n \" # Tip: Use exclude to filter out false matches from short keywords/acronyms\\n\" +\n \" # exclude:\\n\" +\n \" # - \\\"unwanted term\\\"\\n\" +\n \" # - \\\"irrelevant topic\\\"\\n\" +\n \" operator: OR # How to combine terms within this block\\n\" +\n \"\\n\" +\n \" # Add more blocks — blocks are AND'd together\\n\" +\n \" # - id: concept-2\\n\" +\n \" # field: title_abstract\\n\" +\n \" # terms:\\n\" +\n \" # keywords:\\n\" +\n \" # - \\\"another term\\\"\\n\" +\n \" # operator: OR\\n\" +\n \"\\n\" +\n \"# filters: # Optional: apply to all databases\\n\" +\n \"# year_from: 2020\\n\" +\n \"# year_to: 2026\\n\" +\n \"# language:\\n\" +\n \"# - en\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Review\\\"\\n\" +\n \"# - \\\"Comment\\\"\\n\" +\n \"\\n\" +\n \"# providers: # Optional: per-database block replacements & filter additions\\n\" +\n \"# pubmed:\\n\" +\n \"# replaces:\\n\" +\n \"# concept-1: # Replace block by id\\n\" +\n \"# field: keyword\\n\" +\n \"# terms:\\n\" +\n \"# mesh:\\n\" +\n \"# - \\\"MeSH Heading\\\"\\n\" +\n \"# operator: OR\\n\" +\n \"# adds:\\n\" +\n \"# filters:\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Letter\\\"\\n\";\n\n/**\n * Generate the query template YAML string.\n *\n * @param title - Optional title to set as the query name\n * @returns The YAML template string with comments\n */\nexport function generateQueryTemplate(title?: string): string {\n if (!title) return QUERY_TEMPLATE;\n const escaped = title.replace(/\"/g, '\\\\\"');\n return QUERY_TEMPLATE.replace('name: my_search', `name: \"${escaped}\"`);\n}\n\nexport const QUERIES_DIR = \"queries\";\n\nexport interface WriteQueryTemplateOptions {\n title: string;\n output?: string | undefined;\n stdout?: boolean | undefined;\n force?: boolean | undefined;\n cwd?: string | undefined;\n}\n\n/**\n * Write the query template to a file or return it as a message.\n *\n * Output priority:\n * 1. --stdout → return template as message (no file)\n * 2. -o <path> → write to that path\n * 3. default → write to queries/<sanitized-title>.yaml\n */\nexport async function writeQueryTemplate(options: WriteQueryTemplateOptions): Promise<{ success: boolean; message: string; outputPath?: string }> {\n const template = generateQueryTemplate(options.title);\n\n if (options.stdout) {\n return { success: true, message: template };\n }\n\n // Determine output path\n const outputPath = options.output\n ?? join(options.cwd ?? process.cwd(), QUERIES_DIR, `${sanitizeForFilename(options.title)}.yaml`);\n\n // Check if file exists (unless force is set)\n if (!options.force) {\n try {\n await access(outputPath);\n return {\n success: false,\n message: `File already exists: ${outputPath}. Use --force to overwrite.`,\n };\n } catch {\n // File does not exist, proceed\n }\n }\n\n // Ensure parent directory exists\n await mkdir(dirname(outputPath), { recursive: true });\n\n // Write template\n await fsWriteFile(outputPath, template, \"utf-8\");\n\n // Generate JSON Schema file alongside output\n const schemaPath = join(dirname(outputPath), QUERY_SCHEMA_FILENAME);\n const jsonSchema = generateQueryJSONSchema();\n await fsWriteFile(schemaPath, JSON.stringify(jsonSchema, null, 2) + \"\\n\", \"utf-8\");\n\n return {\n success: true,\n message: `Created: ${outputPath}`,\n outputPath,\n };\n}\n"],"names":["fsWriteFile"],"mappings":";;;AAYO,SAAS,oBAAoB,OAAuB;AACzD,QAAM,SAAS,MACZ,KAAA,EACA,cACA,QAAQ,QAAQ,GAAG,EACnB,QAAQ,gBAAgB,EAAE,EAC1B,QAAQ,OAAO,GAAG,EAClB,QAAQ,UAAU,EAAE;AACvB,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AACA,SAAO;AACT;AAMO,MAAM,wBAAwB;AAGrC,MAAM,iBACJ,qCAAqC,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6DrD,SAAS,sBAAsB,OAAwB;AAC5D,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,MAAM,QAAQ,MAAM,KAAK;AACzC,SAAO,eAAe,QAAQ,mBAAmB,UAAU,OAAO,GAAG;AACvE;AAEO,MAAM,cAAc;AAkB3B,eAAsB,mBAAmB,SAAyG;AAChJ,QAAM,WAAW,sBAAsB,QAAQ,KAAK;AAEpD,MAAI,QAAQ,QAAQ;AAClB,WAAO,EAAE,SAAS,MAAM,SAAS,SAAA;AAAA,EACnC;AAGA,QAAM,aAAa,QAAQ,UACtB,KAAK,QAAQ,OAAO,QAAQ,IAAA,GAAO,aAAa,GAAG,oBAAoB,QAAQ,KAAK,CAAC,OAAO;AAGjG,MAAI,CAAC,QAAQ,OAAO;AAClB,QAAI;AACF,YAAM,OAAO,UAAU;AACvB,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,wBAAwB,UAAU;AAAA,MAAA;AAAA,IAE/C,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,MAAM,QAAQ,UAAU,GAAG,EAAE,WAAW,MAAM;AAGpD,QAAMA,UAAY,YAAY,UAAU,OAAO;AAG/C,QAAM,aAAa,KAAK,QAAQ,UAAU,GAAG,qBAAqB;AAClE,QAAM,aAAa,wBAAA;AACnB,QAAMA,UAAY,YAAY,KAAK,UAAU,YAAY,MAAM,CAAC,IAAI,MAAM,OAAO;AAEjF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS,YAAY,UAAU;AAAA,IAC/B;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"init.js","sources":["../../../../src/cli/commands/query/init.ts"],"sourcesContent":["/**\n * Query init command implementation.\n *\n * Generates a skeleton YAML query file with helpful comments.\n */\nimport { writeFile as fsWriteFile, access, mkdir } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { generateQueryJSONSchema } from \"../../../query/json-schema.js\";\nimport { getQueriesDir } from \"../../../config/paths.js\";\n\n/**\n * Sanitize a title string into a safe filename (without extension).\n */\nexport function sanitizeForFilename(title: string): string {\n const result = title\n .trim()\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9_-]/g, '')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n if (!result) {\n throw new Error('Title produces an empty filename after sanitization');\n }\n return result;\n}\n\n/**\n * The YAML template string with comments preserved.\n * This is a raw string (not generated by a YAML library) so comments are kept.\n */\nexport const QUERY_SCHEMA_FILENAME = \"query.schema.json\";\n\n// prettier-ignore\nconst QUERY_TEMPLATE =\n `# yaml-language-server: $schema=./${QUERY_SCHEMA_FILENAME}\\n` +\n \"name: my_search\\n\" +\n \"description: \\\"\\\"\\n\" +\n \"\\n\" +\n \"query:\\n\" +\n \" - id: concept-1 # Unique block identifier (for provider replacements)\\n\" +\n \" field: title_abstract # title, abstract, title_abstract, author, keyword, all\\n\" +\n \" terms:\\n\" +\n \" keywords:\\n\" +\n \" - \\\"search term 1\\\"\\n\" +\n \" - \\\"search term 2\\\"\\n\" +\n \" # mesh: # PubMed MeSH terms (optional)\\n\" +\n \" # - \\\"MeSH Heading\\\"\\n\" +\n \" # eric: # ERIC Descriptors (optional, ERIC only)\\n\" +\n \" # - \\\"ERIC Descriptor\\\"\\n\" +\n \" exclude: [] # Terms to exclude (NOT operator)\\n\" +\n \" # Tip: Use exclude to filter out false matches from short keywords/acronyms\\n\" +\n \" # exclude:\\n\" +\n \" # - \\\"unwanted term\\\"\\n\" +\n \" # - \\\"irrelevant topic\\\"\\n\" +\n \" operator: OR # How to combine terms within this block\\n\" +\n \"\\n\" +\n \" # Add more blocks — blocks are AND'd together\\n\" +\n \" # - id: concept-2\\n\" +\n \" # field: title_abstract\\n\" +\n \" # terms:\\n\" +\n \" # keywords:\\n\" +\n \" # - \\\"another term\\\"\\n\" +\n \" # operator: OR\\n\" +\n \"\\n\" +\n \"# filters: # Optional: apply to all databases\\n\" +\n \"# year_from: 2020\\n\" +\n \"# year_to: 2026\\n\" +\n \"# language:\\n\" +\n \"# - en\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Review\\\"\\n\" +\n \"# - \\\"Comment\\\"\\n\" +\n \"\\n\" +\n \"# providers: # Optional: per-database block replacements & filter additions\\n\" +\n \"# pubmed:\\n\" +\n \"# replaces:\\n\" +\n \"# concept-1: # Replace block by id\\n\" +\n \"# field: keyword\\n\" +\n \"# terms:\\n\" +\n \"# mesh:\\n\" +\n \"# - \\\"MeSH Heading\\\"\\n\" +\n \"# operator: OR\\n\" +\n \"# adds:\\n\" +\n \"# filters:\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Letter\\\"\\n\";\n\n/**\n * Generate the query template YAML string.\n *\n * @param title - Optional title to set as the query name\n * @returns The YAML template string with comments\n */\nexport function generateQueryTemplate(title?: string): string {\n if (!title) return QUERY_TEMPLATE;\n const escaped = title.replace(/\"/g, '\\\\\"');\n return QUERY_TEMPLATE.replace('name: my_search', `name: \"${escaped}\"`);\n}\n\n\nexport interface WriteQueryTemplateOptions {\n title: string;\n output?: string | undefined;\n stdout?: boolean | undefined;\n force?: boolean | undefined;\n cwd?: string | undefined;\n}\n\n/**\n * Write the query template to a file or return it as a message.\n *\n * Output priority:\n * 1. --stdout → return template as message (no file)\n * 2. -o <path> → write to that path\n * 3. default → write to queries/<sanitized-title>.yaml\n */\nexport async function writeQueryTemplate(options: WriteQueryTemplateOptions): Promise<{ success: boolean; message: string; outputPath?: string }> {\n const template = generateQueryTemplate(options.title);\n\n if (options.stdout) {\n return { success: true, message: template };\n }\n\n // Determine output path\n const outputPath = options.output\n ?? join(getQueriesDir(options.cwd), `${sanitizeForFilename(options.title)}.yaml`);\n\n // Check if file exists (unless force is set)\n if (!options.force) {\n try {\n await access(outputPath);\n return {\n success: false,\n message: `File already exists: ${outputPath}. Use --force to overwrite.`,\n };\n } catch {\n // File does not exist, proceed\n }\n }\n\n // Ensure parent directory exists\n await mkdir(dirname(outputPath), { recursive: true });\n\n // Write template\n await fsWriteFile(outputPath, template, \"utf-8\");\n\n // Generate JSON Schema file alongside output\n const schemaPath = join(dirname(outputPath), QUERY_SCHEMA_FILENAME);\n const jsonSchema = generateQueryJSONSchema();\n await fsWriteFile(schemaPath, JSON.stringify(jsonSchema, null, 2) + \"\\n\", \"utf-8\");\n\n return {\n success: true,\n message: `Created: ${outputPath}`,\n outputPath,\n };\n}\n"],"names":["fsWriteFile"],"mappings":";;;;AAaO,SAAS,oBAAoB,OAAuB;AACzD,QAAM,SAAS,MACZ,KAAA,EACA,cACA,QAAQ,QAAQ,GAAG,EACnB,QAAQ,gBAAgB,EAAE,EAC1B,QAAQ,OAAO,GAAG,EAClB,QAAQ,UAAU,EAAE;AACvB,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AACA,SAAO;AACT;AAMO,MAAM,wBAAwB;AAGrC,MAAM,iBACJ,qCAAqC,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6DrD,SAAS,sBAAsB,OAAwB;AAC5D,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,MAAM,QAAQ,MAAM,KAAK;AACzC,SAAO,eAAe,QAAQ,mBAAmB,UAAU,OAAO,GAAG;AACvE;AAmBA,eAAsB,mBAAmB,SAAyG;AAChJ,QAAM,WAAW,sBAAsB,QAAQ,KAAK;AAEpD,MAAI,QAAQ,QAAQ;AAClB,WAAO,EAAE,SAAS,MAAM,SAAS,SAAA;AAAA,EACnC;AAGA,QAAM,aAAa,QAAQ,UACtB,KAAK,cAAc,QAAQ,GAAG,GAAG,GAAG,oBAAoB,QAAQ,KAAK,CAAC,OAAO;AAGlF,MAAI,CAAC,QAAQ,OAAO;AAClB,QAAI;AACF,YAAM,OAAO,UAAU;AACvB,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,wBAAwB,UAAU;AAAA,MAAA;AAAA,IAE/C,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,MAAM,QAAQ,UAAU,GAAG,EAAE,WAAW,MAAM;AAGpD,QAAMA,UAAY,YAAY,UAAU,OAAO;AAG/C,QAAM,aAAa,KAAK,QAAQ,UAAU,GAAG,qBAAqB;AAClE,QAAM,aAAa,wBAAA;AACnB,QAAMA,UAAY,YAAY,KAAK,UAAU,YAAY,MAAM,CAAC,IAAI,MAAM,OAAO;AAEjF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS,YAAY,UAAU;AAAA,IAC/B;AAAA,EAAA;AAEJ;"}
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/resolve.ts"],"names":[],"mappings":"AAYA,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,IAAI,EAAE,MAAM;CAIzB;AAiBD,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA0CnE"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/resolve.ts"],"names":[],"mappings":"AAcA,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,IAAI,EAAE,MAAM;CAIzB;AAiBD,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2CnE"}
@@ -1,4 +1,6 @@
1
1
  import { stat } from "node:fs/promises";
2
+ import { relative } from "node:path";
3
+ import { getQueriesDir } from "../../../config/paths.js";
2
4
  class NotAFileError extends Error {
3
5
  constructor(path) {
4
6
  super(`Path is not a file: ${path}`);
@@ -31,14 +33,15 @@ async function resolveQueryFile(arg) {
31
33
  return withExt;
32
34
  }
33
35
  }
36
+ const queriesDir = relative(process.cwd(), getQueriesDir());
34
37
  const basename = arg.endsWith(".yaml") || arg.endsWith(".yml") ? arg : `${arg}.yaml`;
35
- const inQueries = `queries/${basename}`;
38
+ const inQueries = `${queriesDir}/${basename}`;
36
39
  candidates.push(inQueries);
37
40
  if (await isFile(inQueries)) {
38
41
  return inQueries;
39
42
  }
40
43
  if (!arg.endsWith(".yaml") && !arg.endsWith(".yml")) {
41
- const inQueriesYml = `queries/${arg}.yml`;
44
+ const inQueriesYml = `${queriesDir}/${arg}.yml`;
42
45
  candidates.push(inQueriesYml);
43
46
  if (await isFile(inQueriesYml)) {
44
47
  return inQueriesYml;
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.js","sources":["../../../../src/cli/commands/query/resolve.ts"],"sourcesContent":["/**\n * Smart query file resolution.\n *\n * Resolution order:\n * 1. Exact path exists → use it\n * 2. <arg>.yaml exists → use it\n * 3. queries/<arg>.yaml exists → use it\n * 4. queries/<arg>.yml exists → use it\n * 5. Error with tried paths\n */\nimport { stat } from 'node:fs/promises';\n\nexport class NotAFileError extends Error {\n constructor(path: string) {\n super(`Path is not a file: ${path}`);\n this.name = 'NotAFileError';\n }\n}\n\nasync function isFile(path: string): Promise<boolean> {\n try {\n const s = await stat(path);\n if (!s.isFile()) {\n throw new NotAFileError(path);\n }\n return true;\n } catch (error) {\n if (error instanceof NotAFileError) {\n throw error;\n }\n return false;\n }\n}\n\nexport async function resolveQueryFile(arg: string): Promise<string> {\n // 1. Exact path\n if (await isFile(arg)) {\n return arg;\n }\n\n const candidates: string[] = [];\n\n // 2. arg + .yaml (skip if already ends with .yaml)\n if (!arg.endsWith('.yaml') && !arg.endsWith('.yml')) {\n const withExt = `${arg}.yaml`;\n candidates.push(withExt);\n if (await isFile(withExt)) {\n return withExt;\n }\n }\n\n // 3. queries/<arg>.yaml\n const basename = arg.endsWith('.yaml') || arg.endsWith('.yml') ? arg : `${arg}.yaml`;\n const inQueries = `queries/${basename}`;\n candidates.push(inQueries);\n if (await isFile(inQueries)) {\n return inQueries;\n }\n\n // 4. queries/<arg>.yml (skip if arg already has extension)\n if (!arg.endsWith('.yaml') && !arg.endsWith('.yml')) {\n const inQueriesYml = `queries/${arg}.yml`;\n candidates.push(inQueriesYml);\n if (await isFile(inQueriesYml)) {\n return inQueriesYml;\n }\n }\n\n // 5. Error\n const tried = [`./${arg}`, ...candidates.map(c => `./${c}`)];\n throw new Error(\n `Query file not found: \"${arg}\"\\n` +\n ` Tried:\\n` +\n tried.map(p => ` ${p}`).join('\\n') + '\\n' +\n ` Create a new query: search-hub query init \"${arg}\"`\n );\n}\n"],"names":[],"mappings":";AAYO,MAAM,sBAAsB,MAAM;AAAA,EACvC,YAAY,MAAc;AACxB,UAAM,uBAAuB,IAAI,EAAE;AACnC,SAAK,OAAO;AAAA,EACd;AACF;AAEA,eAAe,OAAO,MAAgC;AACpD,MAAI;AACF,UAAM,IAAI,MAAM,KAAK,IAAI;AACzB,QAAI,CAAC,EAAE,UAAU;AACf,YAAM,IAAI,cAAc,IAAI;AAAA,IAC9B;AACA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,QAAI,iBAAiB,eAAe;AAClC,YAAM;AAAA,IACR;AACA,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,iBAAiB,KAA8B;AAEnE,MAAI,MAAM,OAAO,GAAG,GAAG;AACrB,WAAO;AAAA,EACT;AAEA,QAAM,aAAuB,CAAA;AAG7B,MAAI,CAAC,IAAI,SAAS,OAAO,KAAK,CAAC,IAAI,SAAS,MAAM,GAAG;AACnD,UAAM,UAAU,GAAG,GAAG;AACtB,eAAW,KAAK,OAAO;AACvB,QAAI,MAAM,OAAO,OAAO,GAAG;AACzB,aAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,WAAW,IAAI,SAAS,OAAO,KAAK,IAAI,SAAS,MAAM,IAAI,MAAM,GAAG,GAAG;AAC7E,QAAM,YAAY,WAAW,QAAQ;AACrC,aAAW,KAAK,SAAS;AACzB,MAAI,MAAM,OAAO,SAAS,GAAG;AAC3B,WAAO;AAAA,EACT;AAGA,MAAI,CAAC,IAAI,SAAS,OAAO,KAAK,CAAC,IAAI,SAAS,MAAM,GAAG;AACnD,UAAM,eAAe,WAAW,GAAG;AACnC,eAAW,KAAK,YAAY;AAC5B,QAAI,MAAM,OAAO,YAAY,GAAG;AAC9B,aAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,QAAQ,CAAC,KAAK,GAAG,IAAI,GAAG,WAAW,IAAI,CAAA,MAAK,KAAK,CAAC,EAAE,CAAC;AAC3D,QAAM,IAAI;AAAA,IACR,0BAA0B,GAAG;AAAA;AAAA,IAE7B,MAAM,IAAI,CAAA,MAAK,OAAO,CAAC,EAAE,EAAE,KAAK,IAAI,IAAI;AAAA,+CACQ,GAAG;AAAA,EAAA;AAEvD;"}
1
+ {"version":3,"file":"resolve.js","sources":["../../../../src/cli/commands/query/resolve.ts"],"sourcesContent":["/**\n * Smart query file resolution.\n *\n * Resolution order:\n * 1. Exact path exists → use it\n * 2. <arg>.yaml exists → use it\n * 3. .search-hub/queries/<arg>.yaml exists → use it\n * 4. .search-hub/queries/<arg>.yml exists → use it\n * 5. Error with tried paths\n */\nimport { stat } from 'node:fs/promises';\nimport { relative } from 'node:path';\nimport { getQueriesDir } from '../../../config/paths.js';\n\nexport class NotAFileError extends Error {\n constructor(path: string) {\n super(`Path is not a file: ${path}`);\n this.name = 'NotAFileError';\n }\n}\n\nasync function isFile(path: string): Promise<boolean> {\n try {\n const s = await stat(path);\n if (!s.isFile()) {\n throw new NotAFileError(path);\n }\n return true;\n } catch (error) {\n if (error instanceof NotAFileError) {\n throw error;\n }\n return false;\n }\n}\n\nexport async function resolveQueryFile(arg: string): Promise<string> {\n // 1. Exact path\n if (await isFile(arg)) {\n return arg;\n }\n\n const candidates: string[] = [];\n\n // 2. arg + .yaml (skip if already ends with .yaml)\n if (!arg.endsWith('.yaml') && !arg.endsWith('.yml')) {\n const withExt = `${arg}.yaml`;\n candidates.push(withExt);\n if (await isFile(withExt)) {\n return withExt;\n }\n }\n\n // 3. .search-hub/queries/<arg>.yaml\n const queriesDir = relative(process.cwd(), getQueriesDir());\n const basename = arg.endsWith('.yaml') || arg.endsWith('.yml') ? arg : `${arg}.yaml`;\n const inQueries = `${queriesDir}/${basename}`;\n candidates.push(inQueries);\n if (await isFile(inQueries)) {\n return inQueries;\n }\n\n // 4. .search-hub/queries/<arg>.yml (skip if arg already has extension)\n if (!arg.endsWith('.yaml') && !arg.endsWith('.yml')) {\n const inQueriesYml = `${queriesDir}/${arg}.yml`;\n candidates.push(inQueriesYml);\n if (await isFile(inQueriesYml)) {\n return inQueriesYml;\n }\n }\n\n // 5. Error\n const tried = [`./${arg}`, ...candidates.map(c => `./${c}`)];\n throw new Error(\n `Query file not found: \"${arg}\"\\n` +\n ` Tried:\\n` +\n tried.map(p => ` ${p}`).join('\\n') + '\\n' +\n ` Create a new query: search-hub query init \"${arg}\"`\n );\n}\n"],"names":[],"mappings":";;;AAcO,MAAM,sBAAsB,MAAM;AAAA,EACvC,YAAY,MAAc;AACxB,UAAM,uBAAuB,IAAI,EAAE;AACnC,SAAK,OAAO;AAAA,EACd;AACF;AAEA,eAAe,OAAO,MAAgC;AACpD,MAAI;AACF,UAAM,IAAI,MAAM,KAAK,IAAI;AACzB,QAAI,CAAC,EAAE,UAAU;AACf,YAAM,IAAI,cAAc,IAAI;AAAA,IAC9B;AACA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,QAAI,iBAAiB,eAAe;AAClC,YAAM;AAAA,IACR;AACA,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,iBAAiB,KAA8B;AAEnE,MAAI,MAAM,OAAO,GAAG,GAAG;AACrB,WAAO;AAAA,EACT;AAEA,QAAM,aAAuB,CAAA;AAG7B,MAAI,CAAC,IAAI,SAAS,OAAO,KAAK,CAAC,IAAI,SAAS,MAAM,GAAG;AACnD,UAAM,UAAU,GAAG,GAAG;AACtB,eAAW,KAAK,OAAO;AACvB,QAAI,MAAM,OAAO,OAAO,GAAG;AACzB,aAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,aAAa,SAAS,QAAQ,IAAA,GAAO,eAAe;AAC1D,QAAM,WAAW,IAAI,SAAS,OAAO,KAAK,IAAI,SAAS,MAAM,IAAI,MAAM,GAAG,GAAG;AAC7E,QAAM,YAAY,GAAG,UAAU,IAAI,QAAQ;AAC3C,aAAW,KAAK,SAAS;AACzB,MAAI,MAAM,OAAO,SAAS,GAAG;AAC3B,WAAO;AAAA,EACT;AAGA,MAAI,CAAC,IAAI,SAAS,OAAO,KAAK,CAAC,IAAI,SAAS,MAAM,GAAG;AACnD,UAAM,eAAe,GAAG,UAAU,IAAI,GAAG;AACzC,eAAW,KAAK,YAAY;AAC5B,QAAI,MAAM,OAAO,YAAY,GAAG;AAC9B,aAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,QAAQ,CAAC,KAAK,GAAG,IAAI,GAAG,WAAW,IAAI,CAAA,MAAK,KAAK,CAAC,EAAE,CAAC;AAC3D,QAAM,IAAI;AAAA,IACR,0BAA0B,GAAG;AAAA;AAAA,IAE7B,MAAM,IAAI,CAAA,MAAK,OAAO,CAAC,EAAE,EAAE,KAAK,IAAI,IAAI;AAAA,+CACQ,GAAG;AAAA,EAAA;AAEvD;"}
@@ -74,6 +74,10 @@ export declare function getReviewSummary(sessionId: string, sessionsDir: string)
74
74
  * Get articles with finalDecision='include' from review file.
75
75
  * Converts from ArticleEntry format to Article format.
76
76
  *
77
+ * Authors are taken from the session's structured search results when the
78
+ * article can be matched by identifier; the authors string in reviews.yaml
79
+ * is only used as a fallback.
80
+ *
77
81
  * @throws Error if mergedFrom is missing or empty (indicates legacy review file)
78
82
  */
79
83
  export declare function getIncludedArticles(sessionId: string, sessionsDir: string): Promise<Article[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/register.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/register.ts"],"names":[],"mappings":"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"}