@ncukondo/search-hub 0.12.2 → 0.14.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 (90) hide show
  1. package/dist/cli/commands/diff.js +2 -2
  2. package/dist/cli/commands/diff.js.map +1 -1
  3. package/dist/cli/commands/query/init.d.ts +5 -0
  4. package/dist/cli/commands/query/init.d.ts.map +1 -1
  5. package/dist/cli/commands/query/init.js +9 -1
  6. package/dist/cli/commands/query/init.js.map +1 -1
  7. package/dist/cli/commands/query/translate.d.ts.map +1 -1
  8. package/dist/cli/commands/query/translate.js +5 -0
  9. package/dist/cli/commands/query/translate.js.map +1 -1
  10. package/dist/cli/commands/query/validate.d.ts +22 -1
  11. package/dist/cli/commands/query/validate.d.ts.map +1 -1
  12. package/dist/cli/commands/query/validate.js +65 -22
  13. package/dist/cli/commands/query/validate.js.map +1 -1
  14. package/dist/cli/commands/review/extract.d.ts.map +1 -1
  15. package/dist/cli/commands/review/extract.js +1 -2
  16. package/dist/cli/commands/review/extract.js.map +1 -1
  17. package/dist/cli/commands/review/finalize.d.ts.map +1 -1
  18. package/dist/cli/commands/review/finalize.js +1 -2
  19. package/dist/cli/commands/review/finalize.js.map +1 -1
  20. package/dist/cli/commands/review/init.d.ts.map +1 -1
  21. package/dist/cli/commands/review/init.js +2 -5
  22. package/dist/cli/commands/review/init.js.map +1 -1
  23. package/dist/cli/commands/review/merge.d.ts.map +1 -1
  24. package/dist/cli/commands/review/merge.js +1 -2
  25. package/dist/cli/commands/review/merge.js.map +1 -1
  26. package/dist/cli/index.d.ts.map +1 -1
  27. package/dist/cli/index.js +81 -7
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/cli/suggestions/index.d.ts.map +1 -1
  30. package/dist/cli/suggestions/index.js +10 -0
  31. package/dist/cli/suggestions/index.js.map +1 -1
  32. package/dist/cli/suggestions/rules.d.ts.map +1 -1
  33. package/dist/cli/suggestions/rules.js +21 -8
  34. package/dist/cli/suggestions/rules.js.map +1 -1
  35. package/dist/cli/suggestions/types.d.ts +11 -0
  36. package/dist/cli/suggestions/types.d.ts.map +1 -1
  37. package/dist/index.js +5 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/providers/arxiv/translator.d.ts.map +1 -1
  40. package/dist/providers/arxiv/translator.js +5 -2
  41. package/dist/providers/arxiv/translator.js.map +1 -1
  42. package/dist/providers/base/types.d.ts +2 -0
  43. package/dist/providers/base/types.d.ts.map +1 -1
  44. package/dist/providers/base/types.js.map +1 -1
  45. package/dist/providers/base/warnings.d.ts +14 -0
  46. package/dist/providers/base/warnings.d.ts.map +1 -0
  47. package/dist/providers/base/warnings.js +33 -0
  48. package/dist/providers/base/warnings.js.map +1 -0
  49. package/dist/providers/eric/translator.d.ts.map +1 -1
  50. package/dist/providers/eric/translator.js +5 -2
  51. package/dist/providers/eric/translator.js.map +1 -1
  52. package/dist/providers/pubmed/translator.d.ts.map +1 -1
  53. package/dist/providers/pubmed/translator.js +5 -2
  54. package/dist/providers/pubmed/translator.js.map +1 -1
  55. package/dist/providers/scopus/translator.d.ts.map +1 -1
  56. package/dist/providers/scopus/translator.js +22 -5
  57. package/dist/providers/scopus/translator.js.map +1 -1
  58. package/dist/query/__test-helpers__/mock-mesh-client.d.ts +12 -0
  59. package/dist/query/__test-helpers__/mock-mesh-client.d.ts.map +1 -0
  60. package/dist/query/index.d.ts +4 -0
  61. package/dist/query/index.d.ts.map +1 -1
  62. package/dist/query/json-schema.d.ts +3 -0
  63. package/dist/query/json-schema.d.ts.map +1 -0
  64. package/dist/query/json-schema.js +48 -0
  65. package/dist/query/json-schema.js.map +1 -0
  66. package/dist/query/mesh-lookup.d.ts +47 -0
  67. package/dist/query/mesh-lookup.d.ts.map +1 -0
  68. package/dist/query/mesh-lookup.js +214 -0
  69. package/dist/query/mesh-lookup.js.map +1 -0
  70. package/dist/query/parser.js +1 -1
  71. package/dist/query/parser.js.map +1 -1
  72. package/dist/query/types.d.ts +2 -2
  73. package/dist/query/types.d.ts.map +1 -1
  74. package/dist/query/validator.d.ts +5 -5
  75. package/dist/query/validator.d.ts.map +1 -1
  76. package/dist/query/validator.js +5 -2
  77. package/dist/query/validator.js.map +1 -1
  78. package/dist/query/vocab-cache.d.ts +15 -0
  79. package/dist/query/vocab-cache.d.ts.map +1 -0
  80. package/dist/query/vocab-cache.js +44 -0
  81. package/dist/query/vocab-cache.js.map +1 -0
  82. package/dist/query/vocab-validator.d.ts +71 -0
  83. package/dist/query/vocab-validator.d.ts.map +1 -0
  84. package/dist/query/vocab-validator.js +153 -0
  85. package/dist/query/vocab-validator.js.map +1 -0
  86. package/dist/utils/levenshtein.d.ts +6 -0
  87. package/dist/utils/levenshtein.d.ts.map +1 -0
  88. package/dist/utils/levenshtein.js +21 -0
  89. package/dist/utils/levenshtein.js.map +1 -0
  90. package/package.json +1 -1
@@ -86,8 +86,7 @@ async function executeReviewInit(options, sessionsDir) {
86
86
  lineWidth: 0
87
87
  // Disable line wrapping
88
88
  });
89
- const schemaPath = "../../../../.search-hub/schemas/review.schema.json";
90
- const schemaComment = `# yaml-language-server: $schema=${schemaPath}
89
+ const schemaComment = `# yaml-language-server: $schema=./review.schema.json
91
90
  `;
92
91
  const reviewsExample = `reviews:
93
92
  # - reviewer: human:your-name
@@ -98,9 +97,7 @@ async function executeReviewInit(options, sessionsDir) {
98
97
  reviewsExample
99
98
  );
100
99
  await writeFile(reviewsPath, finalContent, "utf-8");
101
- const schemasDir = join(dirname(sessionsDir), ".search-hub", "schemas");
102
- await mkdir(schemasDir, { recursive: true });
103
- const schemaDestPath = join(schemasDir, "review.schema.json");
100
+ const schemaDestPath = join(internalDir, "review.schema.json");
104
101
  try {
105
102
  const schemaSourcePath = await findSchemaSource();
106
103
  await copyFile(schemaSourcePath, schemaDestPath);
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sources":["../../../../src/cli/commands/review/init.ts"],"sourcesContent":["/**\n * review init command - Generate reviews.yaml from session results\n */\n\nimport { join, dirname } from 'node:path';\nimport { writeFile, mkdir, access, copyFile } from 'node:fs/promises';\nimport { stringify as stringifyYaml } from 'yaml';\nimport type { Article, Author, ProviderName } from '../../../providers/base/types.js';\nimport { loadSession } from '../../../session/manager.js';\nimport { loadResults } from '../../../session/results-io.js';\nimport { deduplicateForReview } from './dedup.js';\nimport type { ArticleEntry, ReviewFile, MergedSource } from './types.js';\n\nexport interface ReviewInitOptions {\n sessionId: string;\n force?: boolean;\n}\n\nexport interface ReviewInitResult {\n reviewsPath: string;\n articleCount: number;\n duplicatesRemoved: number;\n}\n\n/**\n * Format authors array to string\n */\nfunction formatAuthors(authors: Author[]): string {\n return authors\n .map((a) => {\n const parts: string[] = [];\n if (a.family) parts.push(a.family);\n if (a.given) parts.push(a.given.charAt(0));\n return parts.join(' ');\n })\n .join(', ');\n}\n\n/**\n * Extract year from publication date\n */\nfunction extractYear(publicationDate?: string): string | undefined {\n if (!publicationDate) return undefined;\n const year = publicationDate.substring(0, 4);\n return /^\\d{4}$/.test(year) ? year : undefined;\n}\n\n/**\n * Convert Article to ArticleEntry for review file\n */\nfunction articleToEntry(article: Article & { mergedFrom?: MergedSource[] }): ArticleEntry {\n const entry: ArticleEntry = {\n title: article.title,\n reviews: [],\n };\n\n // Add identifiers\n if (article.doi) entry.doi = article.doi;\n if (article.pmid) entry.pmid = article.pmid;\n if (article.scopusId) entry.scopusId = article.scopusId;\n if (article.arxivId) entry.arxivId = article.arxivId;\n if (article.ericId) entry.ericId = article.ericId;\n\n // Add bibliographic info\n if (article.authors && article.authors.length > 0) {\n entry.authors = formatAuthors(article.authors);\n }\n const year = extractYear(article.publicationDate);\n if (year) entry.year = year;\n if (article.abstract) entry.abstract = article.abstract;\n\n // Add deduplication tracking\n if (article.mergedFrom && article.mergedFrom.length > 0) {\n entry.mergedFrom = article.mergedFrom;\n }\n\n return entry;\n}\n\n/**\n * Find the schema file location (in the package)\n */\nasync function findSchemaSource(): Promise<string> {\n // Try relative to this file (src/cli/commands/review -> schemas)\n const possiblePaths = [\n join(dirname(import.meta.url.replace('file://', '')), '../../../../schemas/review.schema.json'),\n join(process.cwd(), 'schemas/review.schema.json'),\n ];\n\n for (const path of possiblePaths) {\n try {\n await access(path);\n return path;\n } catch {\n // Try next path\n }\n }\n\n throw new Error('Could not find review.schema.json');\n}\n\n/**\n * Execute review init command\n */\nexport async function executeReviewInit(\n options: ReviewInitOptions,\n sessionsDir: string\n): Promise<ReviewInitResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n\n // Load session file\n const session = await loadSession(options.sessionId, sessionsDir);\n\n // Check if .internal/reviews.yaml already exists\n const internalDir = join(sessionDir, '.internal');\n const reviewsPath = join(internalDir, 'reviews.yaml');\n try {\n await access(reviewsPath);\n if (!options.force) {\n throw new Error(`reviews.yaml already exists. Use --force to overwrite.`);\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw err;\n }\n }\n\n // Create .internal/ directory\n await mkdir(internalDir, { recursive: true });\n\n // Load all results from session\n const allArticles: Article[] = [];\n const providers = Object.keys(session.databases) as ProviderName[];\n\n for (const provider of providers) {\n const dbStatus = session.databases[provider];\n if (!dbStatus) continue;\n const articles = await loadResults(sessionDir, provider);\n allArticles.push(...articles);\n }\n\n // Deduplicate with mergedFrom tracking\n const { articles: dedupedArticles, duplicatesRemoved } = deduplicateForReview(allArticles);\n\n // Convert to ArticleEntry format\n const articleEntries = dedupedArticles.map(articleToEntry);\n\n // Build review file\n const reviewFile: ReviewFile = {\n sessionId: options.sessionId,\n articles: articleEntries,\n };\n\n // Generate YAML with schema reference comment\n const yamlContent = stringifyYaml(reviewFile, {\n lineWidth: 0, // Disable line wrapping\n });\n\n // Add schema reference comment at top\n // Path from sessions/{id}/.internal/ to .search-hub/schemas/\n const schemaPath = '../../../../.search-hub/schemas/review.schema.json';\n const schemaComment = `# yaml-language-server: $schema=${schemaPath}\\n`;\n\n // Replace empty reviews arrays with commented example\n const reviewsExample = `reviews:\n # - reviewer: human:your-name\n # decision: include # include / exclude / uncertain\n # comment: reason`;\n const finalContent = schemaComment + yamlContent.replace(\n /reviews: \\[\\]/g,\n reviewsExample\n );\n\n // Write reviews.yaml\n await writeFile(reviewsPath, finalContent, 'utf-8');\n\n // Copy schema file to .search-hub/schemas/\n const schemasDir = join(dirname(sessionsDir), '.search-hub', 'schemas');\n await mkdir(schemasDir, { recursive: true });\n const schemaDestPath = join(schemasDir, 'review.schema.json');\n\n try {\n const schemaSourcePath = await findSchemaSource();\n await copyFile(schemaSourcePath, schemaDestPath);\n } catch {\n // If we can't find the schema file, skip copying\n // This might happen in test environments\n }\n\n return {\n reviewsPath,\n articleCount: articleEntries.length,\n duplicatesRemoved,\n };\n}\n"],"names":["stringifyYaml"],"mappings":"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"}
1
+ {"version":3,"file":"init.js","sources":["../../../../src/cli/commands/review/init.ts"],"sourcesContent":["/**\n * review init command - Generate reviews.yaml from session results\n */\n\nimport { join, dirname } from 'node:path';\nimport { writeFile, mkdir, access, copyFile } from 'node:fs/promises';\nimport { stringify as stringifyYaml } from 'yaml';\nimport type { Article, Author, ProviderName } from '../../../providers/base/types.js';\nimport { loadSession } from '../../../session/manager.js';\nimport { loadResults } from '../../../session/results-io.js';\nimport { deduplicateForReview } from './dedup.js';\nimport type { ArticleEntry, ReviewFile, MergedSource } from './types.js';\n\nexport interface ReviewInitOptions {\n sessionId: string;\n force?: boolean;\n}\n\nexport interface ReviewInitResult {\n reviewsPath: string;\n articleCount: number;\n duplicatesRemoved: number;\n}\n\n/**\n * Format authors array to string\n */\nfunction formatAuthors(authors: Author[]): string {\n return authors\n .map((a) => {\n const parts: string[] = [];\n if (a.family) parts.push(a.family);\n if (a.given) parts.push(a.given.charAt(0));\n return parts.join(' ');\n })\n .join(', ');\n}\n\n/**\n * Extract year from publication date\n */\nfunction extractYear(publicationDate?: string): string | undefined {\n if (!publicationDate) return undefined;\n const year = publicationDate.substring(0, 4);\n return /^\\d{4}$/.test(year) ? year : undefined;\n}\n\n/**\n * Convert Article to ArticleEntry for review file\n */\nfunction articleToEntry(article: Article & { mergedFrom?: MergedSource[] }): ArticleEntry {\n const entry: ArticleEntry = {\n title: article.title,\n reviews: [],\n };\n\n // Add identifiers\n if (article.doi) entry.doi = article.doi;\n if (article.pmid) entry.pmid = article.pmid;\n if (article.scopusId) entry.scopusId = article.scopusId;\n if (article.arxivId) entry.arxivId = article.arxivId;\n if (article.ericId) entry.ericId = article.ericId;\n\n // Add bibliographic info\n if (article.authors && article.authors.length > 0) {\n entry.authors = formatAuthors(article.authors);\n }\n const year = extractYear(article.publicationDate);\n if (year) entry.year = year;\n if (article.abstract) entry.abstract = article.abstract;\n\n // Add deduplication tracking\n if (article.mergedFrom && article.mergedFrom.length > 0) {\n entry.mergedFrom = article.mergedFrom;\n }\n\n return entry;\n}\n\n/**\n * Find the schema file location (in the package)\n */\nasync function findSchemaSource(): Promise<string> {\n // Try relative to this file (src/cli/commands/review -> schemas)\n const possiblePaths = [\n join(dirname(import.meta.url.replace('file://', '')), '../../../../schemas/review.schema.json'),\n join(process.cwd(), 'schemas/review.schema.json'),\n ];\n\n for (const path of possiblePaths) {\n try {\n await access(path);\n return path;\n } catch {\n // Try next path\n }\n }\n\n throw new Error('Could not find review.schema.json');\n}\n\n/**\n * Execute review init command\n */\nexport async function executeReviewInit(\n options: ReviewInitOptions,\n sessionsDir: string\n): Promise<ReviewInitResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n\n // Load session file\n const session = await loadSession(options.sessionId, sessionsDir);\n\n // Check if .internal/reviews.yaml already exists\n const internalDir = join(sessionDir, '.internal');\n const reviewsPath = join(internalDir, 'reviews.yaml');\n try {\n await access(reviewsPath);\n if (!options.force) {\n throw new Error(`reviews.yaml already exists. Use --force to overwrite.`);\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw err;\n }\n }\n\n // Create .internal/ directory\n await mkdir(internalDir, { recursive: true });\n\n // Load all results from session\n const allArticles: Article[] = [];\n const providers = Object.keys(session.databases) as ProviderName[];\n\n for (const provider of providers) {\n const dbStatus = session.databases[provider];\n if (!dbStatus) continue;\n const articles = await loadResults(sessionDir, provider);\n allArticles.push(...articles);\n }\n\n // Deduplicate with mergedFrom tracking\n const { articles: dedupedArticles, duplicatesRemoved } = deduplicateForReview(allArticles);\n\n // Convert to ArticleEntry format\n const articleEntries = dedupedArticles.map(articleToEntry);\n\n // Build review file\n const reviewFile: ReviewFile = {\n sessionId: options.sessionId,\n articles: articleEntries,\n };\n\n // Generate YAML with schema reference comment\n const yamlContent = stringifyYaml(reviewFile, {\n lineWidth: 0, // Disable line wrapping\n });\n\n // Add schema reference comment at top (local copy alongside reviews.yaml)\n const schemaComment = `# yaml-language-server: $schema=./review.schema.json\\n`;\n\n // Replace empty reviews arrays with commented example\n const reviewsExample = `reviews:\n # - reviewer: human:your-name\n # decision: include # include / exclude / uncertain\n # comment: reason`;\n const finalContent = schemaComment + yamlContent.replace(\n /reviews: \\[\\]/g,\n reviewsExample\n );\n\n // Write reviews.yaml\n await writeFile(reviewsPath, finalContent, 'utf-8');\n\n // Copy schema file to .internal/ alongside reviews.yaml\n const schemaDestPath = join(internalDir, 'review.schema.json');\n\n try {\n const schemaSourcePath = await findSchemaSource();\n await copyFile(schemaSourcePath, schemaDestPath);\n } catch {\n // If we can't find the schema file, skip copying\n // This might happen in test environments\n }\n\n return {\n reviewsPath,\n articleCount: articleEntries.length,\n duplicatesRemoved,\n };\n}\n"],"names":["stringifyYaml"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/merge.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAkC,WAAW,EAAE,MAAM,YAAY,CAAC;AA0B1F,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,0BAA0B,EAAE,MAAM,CAAC;IACnC,0BAA0B,EAAE,MAAM,CAAC;IACnC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAmBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAQ/F;AA8ND;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAoC5B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAgDpF"}
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/merge.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAkC,WAAW,EAAE,MAAM,YAAY,CAAC;AA0B1F,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,0BAA0B,EAAE,MAAM,CAAC;IACnC,0BAA0B,EAAE,MAAM,CAAC;IACnC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAmBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAQ/F;AA8ND;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAkC5B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAgDpF"}
@@ -194,8 +194,7 @@ async function executeReviewMerge(options, sessionsDir) {
194
194
  const yamlContent = stringify(mainFile, {
195
195
  lineWidth: 0
196
196
  });
197
- const schemaPath = "../../../../.search-hub/schemas/review.schema.json";
198
- const schemaComment = `# yaml-language-server: $schema=${schemaPath}
197
+ const schemaComment = `# yaml-language-server: $schema=./review.schema.json
199
198
  `;
200
199
  const finalContent = schemaComment + yamlContent;
201
200
  await writeFile(mainReviewsPath, finalContent, "utf-8");
@@ -1 +1 @@
1
- {"version":3,"file":"merge.js","sources":["../../../../src/cli/commands/review/merge.ts"],"sourcesContent":["/**\n * review merge command - Merge edited file back into main reviews.yaml\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { ReviewFile, ArticleEntry, Review, WorkFile, ReviewBasis } from './types.js';\nimport { validateName } from './extract.js';\n\n\n/**\n * Check if a file is a work file (has basis field)\n */\n/** @deprecated Detects old WorkFile format (flat id/decision fields). Kept for backward compat. */\nfunction isWorkFile(file: unknown): file is WorkFile {\n if (\n typeof file !== 'object' ||\n file === null ||\n !('basis' in file) ||\n !('reviewer' in file) ||\n !('articles' in file) ||\n !Array.isArray((file as WorkFile).articles)\n ) {\n return false;\n }\n // Distinguish from new ReviewFile-with-basis: old WorkFile has articles with flat `id` + `decision` fields\n const articles = (file as WorkFile).articles;\n if (articles.length === 0) return false;\n const first = articles[0]!;\n return 'id' in first && 'decision' in first && !('reviews' in first);\n}\n\nexport interface ReviewMergeOptions {\n sessionId: string;\n /** Name of the review subset (reads from for-review/<name>/review.yaml) */\n name: string;\n dryRun?: boolean;\n}\n\nexport interface ReviewMergeResult {\n reviewsAdded: number;\n decisionsSet: number;\n includeCount: number;\n excludeCount: number;\n uncertainCount: number;\n finalDecisionsSet: number;\n finalDecisionsIncludeCount: number;\n finalDecisionsExcludeCount: number;\n warnings: string[];\n}\n\n/**\n * Load review file from path\n */\nasync function loadReviewFile(path: string): Promise<ReviewFile> {\n const content = await readFile(path, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Auto-detect review basis from article data: fulltext > abstract > title\n */\nfunction detectBasis(article: ArticleEntry): ReviewBasis {\n if (article.fulltext) return 'fulltext';\n if (article.abstract) return 'abstract';\n return 'title';\n}\n\n/**\n * Register a reviewer in the review file's reviewers registry.\n * Deduplicates by name+basis pair.\n */\nexport function registerReviewer(reviewFile: ReviewFile, name: string, basis: ReviewBasis): void {\n if (!reviewFile.reviewers) {\n reviewFile.reviewers = [];\n }\n const exists = reviewFile.reviewers.some((r) => r.name === name && r.basis === basis);\n if (!exists) {\n reviewFile.reviewers.push({ name, basis });\n }\n}\n\n/**\n * Match article from extracted file to main file\n */\nfunction findMatchingArticle(\n extracted: ArticleEntry,\n mainArticles: ArticleEntry[]\n): ArticleEntry | undefined {\n // Try matching by various identifiers\n for (const main of mainArticles) {\n if (extracted.pmid && main.pmid && extracted.pmid === main.pmid) {\n return main;\n }\n if (extracted.doi && main.doi && extracted.doi.toLowerCase() === main.doi.toLowerCase()) {\n return main;\n }\n if (extracted.scopusId && main.scopusId && extracted.scopusId === main.scopusId) {\n return main;\n }\n if (extracted.arxivId && main.arxivId && extracted.arxivId === main.arxivId) {\n return main;\n }\n if (extracted.ericId && main.ericId && extracted.ericId === main.ericId) {\n return main;\n }\n }\n return undefined;\n}\n\n/**\n * Match work file article by id to main file\n * The id can be a DOI, PMID, ScopusId, ArxivId, EricId, or title\n */\nfunction findMatchingArticleById(\n id: string,\n mainArticles: ArticleEntry[]\n): ArticleEntry | undefined {\n for (const main of mainArticles) {\n // Match by DOI (case-insensitive)\n if (main.doi && main.doi.toLowerCase() === id.toLowerCase()) {\n return main;\n }\n // Match by PMID\n if (main.pmid && main.pmid === id) {\n return main;\n }\n // Match by ScopusId\n if (main.scopusId && main.scopusId === id) {\n return main;\n }\n // Match by ArxivId\n if (main.arxivId && main.arxivId === id) {\n return main;\n }\n // Match by EricId\n if (main.ericId && main.ericId === id) {\n return main;\n }\n // Match by title (fallback, case-insensitive)\n if (main.title.toLowerCase() === id.toLowerCase()) {\n return main;\n }\n }\n return undefined;\n}\n\n/**\n * Process work file format (with basis/reviewer)\n */\nfunction processWorkFile(\n workFile: WorkFile,\n mainFile: ReviewFile,\n options: ReviewMergeOptions\n): ReviewMergeResult {\n const result: ReviewMergeResult = {\n reviewsAdded: 0,\n decisionsSet: 0,\n includeCount: 0,\n excludeCount: 0,\n uncertainCount: 0,\n finalDecisionsSet: 0,\n finalDecisionsIncludeCount: 0,\n finalDecisionsExcludeCount: 0,\n warnings: [],\n };\n\n const timestamp = new Date().toISOString();\n\n // Register the reviewer for this work file's basis\n if (!options.dryRun) {\n registerReviewer(mainFile, workFile.reviewer, workFile.basis);\n }\n\n for (const workArticle of workFile.articles) {\n // Skip articles with null decision (not yet reviewed)\n if (workArticle.decision === null) {\n continue;\n }\n\n const mainArticle = findMatchingArticleById(workArticle.id, mainFile.articles);\n\n if (!mainArticle) {\n result.warnings.push(`Article not found in main file: id=\"${workArticle.id}\"`);\n continue;\n }\n\n // Ensure mainArticle.reviews is an array\n if (!mainArticle.reviews) {\n mainArticle.reviews = [];\n }\n\n // Create review from work file article\n const review: Review = {\n reviewer: workFile.reviewer,\n decision: workArticle.decision,\n basis: workFile.basis,\n timestamp,\n };\n\n // Add comment if provided\n if (workArticle.comment) {\n review.comment = workArticle.comment;\n }\n\n if (!options.dryRun) {\n mainArticle.reviews.push(review);\n }\n result.reviewsAdded++;\n\n // Count decision types\n if (workArticle.decision === 'include') result.includeCount++;\n else if (workArticle.decision === 'exclude') result.excludeCount++;\n else if (workArticle.decision === 'uncertain') result.uncertainCount++;\n }\n\n return result;\n}\n\n/**\n * Process review file format (with reviewHistory separation)\n */\nfunction processReviewFile(\n extractedFile: ReviewFile,\n mainFile: ReviewFile,\n options: ReviewMergeOptions\n): ReviewMergeResult {\n const result: ReviewMergeResult = {\n reviewsAdded: 0,\n decisionsSet: 0,\n includeCount: 0,\n excludeCount: 0,\n uncertainCount: 0,\n finalDecisionsSet: 0,\n finalDecisionsIncludeCount: 0,\n finalDecisionsExcludeCount: 0,\n warnings: [],\n };\n\n const timestamp = new Date().toISOString();\n const topLevelReviewer = extractedFile.reviewer;\n\n for (const extracted of extractedFile.articles) {\n const mainArticle = findMatchingArticle(extracted, mainFile.articles);\n\n if (!mainArticle) {\n result.warnings.push(`Article not found in main file: \"${extracted.title}\"`);\n continue;\n }\n\n // Merge only reviews[] (reviewHistory is ignored)\n const extractedReviews = extracted.reviews ?? [];\n\n // Ensure mainArticle.reviews is an array\n if (!mainArticle.reviews) {\n mainArticle.reviews = [];\n }\n\n for (const review of extractedReviews) {\n // Fill in reviewer from top-level field if not on individual review\n const reviewer = review.reviewer ?? topLevelReviewer;\n if (!reviewer) {\n throw new Error('reviewer is required: set reviewer on individual review or top-level ReviewFile');\n }\n // Fill in basis from review, top-level field, or auto-detect from article data\n const basis = review.basis ?? extractedFile.basis ?? detectBasis(extracted);\n\n if (!options.dryRun) {\n const mergedReview: Review = {\n ...review,\n reviewer,\n basis,\n timestamp: review.timestamp ?? timestamp,\n };\n mainArticle.reviews.push(mergedReview);\n\n // Register reviewer\n registerReviewer(mainFile, reviewer, basis);\n }\n result.reviewsAdded++;\n\n // Count decision types\n if (review.decision === 'include') result.includeCount++;\n else if (review.decision === 'exclude') result.excludeCount++;\n else if (review.decision === 'uncertain') result.uncertainCount++;\n }\n\n // Overwrite finalDecision if set in extracted (null means unset)\n if (extracted.finalDecision !== undefined && extracted.finalDecision !== null) {\n if (!options.dryRun) {\n mainArticle.finalDecision = extracted.finalDecision;\n }\n result.decisionsSet++;\n result.finalDecisionsSet++;\n if (extracted.finalDecision === 'include') result.finalDecisionsIncludeCount++;\n else if (extracted.finalDecision === 'exclude') result.finalDecisionsExcludeCount++;\n }\n }\n\n return result;\n}\n\n/**\n * Execute review merge command\n */\nexport async function executeReviewMerge(\n options: ReviewMergeOptions,\n sessionsDir: string\n): Promise<ReviewMergeResult> {\n validateName(options.name);\n const sessionDir = join(sessionsDir, options.sessionId);\n const mainReviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const filePath = join(sessionDir, 'for-review', options.name, 'review.yaml');\n\n // Load both files\n const mainFile = await loadReviewFile(mainReviewsPath);\n const content = await readFile(filePath, 'utf-8');\n const inputFile = parseYaml(content);\n\n let result: ReviewMergeResult;\n\n // Detect file format and process accordingly\n if (isWorkFile(inputFile)) {\n result = processWorkFile(inputFile, mainFile, options);\n } else {\n result = processReviewFile(inputFile as ReviewFile, mainFile, options);\n }\n\n // Write back if not dry-run\n if (!options.dryRun) {\n const yamlContent = stringifyYaml(mainFile, {\n lineWidth: 0,\n });\n\n // Preserve schema reference comment\n // Path from sessions/{id}/.internal/ to .search-hub/schemas/\n const schemaPath = '../../../../.search-hub/schemas/review.schema.json';\n const schemaComment = `# yaml-language-server: $schema=${schemaPath}\\n`;\n const finalContent = schemaComment + yamlContent;\n\n await writeFile(mainReviewsPath, finalContent, 'utf-8');\n }\n\n return result;\n}\n\n/**\n * Format merge result as human-readable string\n */\nexport function formatMergeOutput(result: ReviewMergeResult, dryRun: boolean): string {\n const lines: string[] = [];\n\n if (dryRun) {\n lines.push('Dry run - no changes made');\n lines.push('');\n }\n\n lines.push('Merge Summary:');\n\n // Build reviews line with decision breakdown\n if (result.reviewsAdded === 0) {\n lines.push(` Reviews added: 0`);\n } else {\n const parts: string[] = [];\n if (result.excludeCount > 0) parts.push(`${result.excludeCount} exclude`);\n if (result.includeCount > 0) parts.push(`${result.includeCount} include`);\n if (result.uncertainCount > 0) parts.push(`${result.uncertainCount} uncertain`);\n\n if (parts.length > 0) {\n lines.push(` Reviews added: ${result.reviewsAdded} (${parts.join(', ')})`);\n } else {\n lines.push(` Reviews added: ${result.reviewsAdded}`);\n }\n }\n\n // Show final decisions only when some were set\n if (result.finalDecisionsSet > 0) {\n const parts: string[] = [];\n if (result.finalDecisionsIncludeCount > 0) parts.push(`${result.finalDecisionsIncludeCount} include`);\n if (result.finalDecisionsExcludeCount > 0) parts.push(`${result.finalDecisionsExcludeCount} exclude`);\n\n if (parts.length > 0) {\n lines.push(` Final decisions set: ${result.finalDecisionsSet} (${parts.join(', ')})`);\n } else {\n lines.push(` Final decisions set: ${result.finalDecisionsSet}`);\n }\n }\n\n if (result.warnings.length > 0) {\n lines.push('');\n lines.push('Warnings:');\n for (const warning of result.warnings) {\n lines.push(` - ${warning}`);\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":";;;;AAeA,SAAS,WAAW,MAAiC;AACnD,MACE,OAAO,SAAS,YAChB,SAAS,QACT,EAAE,WAAW,SACb,EAAE,cAAc,SAChB,EAAE,cAAc,SAChB,CAAC,MAAM,QAAS,KAAkB,QAAQ,GAC1C;AACA,WAAO;AAAA,EACT;AAEA,QAAM,WAAY,KAAkB;AACpC,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,QAAM,QAAQ,SAAS,CAAC;AACxB,SAAO,QAAQ,SAAS,cAAc,SAAS,EAAE,aAAa;AAChE;AAwBA,eAAe,eAAe,MAAmC;AAC/D,QAAM,UAAU,MAAM,SAAS,MAAM,OAAO;AAC5C,SAAOA,MAAU,OAAO;AAC1B;AAKA,SAAS,YAAY,SAAoC;AACvD,MAAI,QAAQ,SAAU,QAAO;AAC7B,MAAI,QAAQ,SAAU,QAAO;AAC7B,SAAO;AACT;AAMO,SAAS,iBAAiB,YAAwB,MAAc,OAA0B;AAC/F,MAAI,CAAC,WAAW,WAAW;AACzB,eAAW,YAAY,CAAA;AAAA,EACzB;AACA,QAAM,SAAS,WAAW,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,UAAU,KAAK;AACpF,MAAI,CAAC,QAAQ;AACX,eAAW,UAAU,KAAK,EAAE,MAAM,OAAO;AAAA,EAC3C;AACF;AAKA,SAAS,oBACP,WACA,cAC0B;AAE1B,aAAW,QAAQ,cAAc;AAC/B,QAAI,UAAU,QAAQ,KAAK,QAAQ,UAAU,SAAS,KAAK,MAAM;AAC/D,aAAO;AAAA,IACT;AACA,QAAI,UAAU,OAAO,KAAK,OAAO,UAAU,IAAI,kBAAkB,KAAK,IAAI,YAAA,GAAe;AACvF,aAAO;AAAA,IACT;AACA,QAAI,UAAU,YAAY,KAAK,YAAY,UAAU,aAAa,KAAK,UAAU;AAC/E,aAAO;AAAA,IACT;AACA,QAAI,UAAU,WAAW,KAAK,WAAW,UAAU,YAAY,KAAK,SAAS;AAC3E,aAAO;AAAA,IACT;AACA,QAAI,UAAU,UAAU,KAAK,UAAU,UAAU,WAAW,KAAK,QAAQ;AACvE,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAMA,SAAS,wBACP,IACA,cAC0B;AAC1B,aAAW,QAAQ,cAAc;AAE/B,QAAI,KAAK,OAAO,KAAK,IAAI,kBAAkB,GAAG,eAAe;AAC3D,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,QAAQ,KAAK,SAAS,IAAI;AACjC,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,YAAY,KAAK,aAAa,IAAI;AACzC,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,WAAW,KAAK,YAAY,IAAI;AACvC,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,UAAU,KAAK,WAAW,IAAI;AACrC,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,MAAM,YAAA,MAAkB,GAAG,eAAe;AACjD,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,gBACP,UACA,UACA,SACmB;AACnB,QAAM,SAA4B;AAAA,IAChC,cAAc;AAAA,IACd,cAAc;AAAA,IACd,cAAc;AAAA,IACd,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,mBAAmB;AAAA,IACnB,4BAA4B;AAAA,IAC5B,4BAA4B;AAAA,IAC5B,UAAU,CAAA;AAAA,EAAC;AAGb,QAAM,aAAY,oBAAI,KAAA,GAAO,YAAA;AAG7B,MAAI,CAAC,QAAQ,QAAQ;AACnB,qBAAiB,UAAU,SAAS,UAAU,SAAS,KAAK;AAAA,EAC9D;AAEA,aAAW,eAAe,SAAS,UAAU;AAE3C,QAAI,YAAY,aAAa,MAAM;AACjC;AAAA,IACF;AAEA,UAAM,cAAc,wBAAwB,YAAY,IAAI,SAAS,QAAQ;AAE7E,QAAI,CAAC,aAAa;AAChB,aAAO,SAAS,KAAK,uCAAuC,YAAY,EAAE,GAAG;AAC7E;AAAA,IACF;AAGA,QAAI,CAAC,YAAY,SAAS;AACxB,kBAAY,UAAU,CAAA;AAAA,IACxB;AAGA,UAAM,SAAiB;AAAA,MACrB,UAAU,SAAS;AAAA,MACnB,UAAU,YAAY;AAAA,MACtB,OAAO,SAAS;AAAA,MAChB;AAAA,IAAA;AAIF,QAAI,YAAY,SAAS;AACvB,aAAO,UAAU,YAAY;AAAA,IAC/B;AAEA,QAAI,CAAC,QAAQ,QAAQ;AACnB,kBAAY,QAAQ,KAAK,MAAM;AAAA,IACjC;AACA,WAAO;AAGP,QAAI,YAAY,aAAa,UAAW,QAAO;AAAA,aACtC,YAAY,aAAa,UAAW,QAAO;AAAA,aAC3C,YAAY,aAAa,YAAa,QAAO;AAAA,EACxD;AAEA,SAAO;AACT;AAKA,SAAS,kBACP,eACA,UACA,SACmB;AACnB,QAAM,SAA4B;AAAA,IAChC,cAAc;AAAA,IACd,cAAc;AAAA,IACd,cAAc;AAAA,IACd,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,mBAAmB;AAAA,IACnB,4BAA4B;AAAA,IAC5B,4BAA4B;AAAA,IAC5B,UAAU,CAAA;AAAA,EAAC;AAGb,QAAM,aAAY,oBAAI,KAAA,GAAO,YAAA;AAC7B,QAAM,mBAAmB,cAAc;AAEvC,aAAW,aAAa,cAAc,UAAU;AAC9C,UAAM,cAAc,oBAAoB,WAAW,SAAS,QAAQ;AAEpE,QAAI,CAAC,aAAa;AAChB,aAAO,SAAS,KAAK,oCAAoC,UAAU,KAAK,GAAG;AAC3E;AAAA,IACF;AAGA,UAAM,mBAAmB,UAAU,WAAW,CAAA;AAG9C,QAAI,CAAC,YAAY,SAAS;AACxB,kBAAY,UAAU,CAAA;AAAA,IACxB;AAEA,eAAW,UAAU,kBAAkB;AAErC,YAAM,WAAW,OAAO,YAAY;AACpC,UAAI,CAAC,UAAU;AACb,cAAM,IAAI,MAAM,iFAAiF;AAAA,MACnG;AAEA,YAAM,QAAQ,OAAO,SAAS,cAAc,SAAS,YAAY,SAAS;AAE1E,UAAI,CAAC,QAAQ,QAAQ;AACnB,cAAM,eAAuB;AAAA,UAC3B,GAAG;AAAA,UACH;AAAA,UACA;AAAA,UACA,WAAW,OAAO,aAAa;AAAA,QAAA;AAEjC,oBAAY,QAAQ,KAAK,YAAY;AAGrC,yBAAiB,UAAU,UAAU,KAAK;AAAA,MAC5C;AACA,aAAO;AAGP,UAAI,OAAO,aAAa,UAAW,QAAO;AAAA,eACjC,OAAO,aAAa,UAAW,QAAO;AAAA,eACtC,OAAO,aAAa,YAAa,QAAO;AAAA,IACnD;AAGA,QAAI,UAAU,kBAAkB,UAAa,UAAU,kBAAkB,MAAM;AAC7E,UAAI,CAAC,QAAQ,QAAQ;AACnB,oBAAY,gBAAgB,UAAU;AAAA,MACxC;AACA,aAAO;AACP,aAAO;AACP,UAAI,UAAU,kBAAkB,UAAW,QAAO;AAAA,eACzC,UAAU,kBAAkB,UAAW,QAAO;AAAA,IACzD;AAAA,EACF;AAEA,SAAO;AACT;AAKA,eAAsB,mBACpB,SACA,aAC4B;AAC5B,eAAa,QAAQ,IAAI;AACzB,QAAM,aAAa,KAAK,aAAa,QAAQ,SAAS;AACtD,QAAM,kBAAkB,KAAK,YAAY,aAAa,cAAc;AACpE,QAAM,WAAW,KAAK,YAAY,cAAc,QAAQ,MAAM,aAAa;AAG3E,QAAM,WAAW,MAAM,eAAe,eAAe;AACrD,QAAM,UAAU,MAAM,SAAS,UAAU,OAAO;AAChD,QAAM,YAAYA,MAAU,OAAO;AAEnC,MAAI;AAGJ,MAAI,WAAW,SAAS,GAAG;AACzB,aAAS,gBAAgB,WAAW,UAAU,OAAO;AAAA,EACvD,OAAO;AACL,aAAS,kBAAkB,WAAyB,UAAU,OAAO;AAAA,EACvE;AAGA,MAAI,CAAC,QAAQ,QAAQ;AACnB,UAAM,cAAcC,UAAc,UAAU;AAAA,MAC1C,WAAW;AAAA,IAAA,CACZ;AAID,UAAM,aAAa;AACnB,UAAM,gBAAgB,mCAAmC,UAAU;AAAA;AACnE,UAAM,eAAe,gBAAgB;AAErC,UAAM,UAAU,iBAAiB,cAAc,OAAO;AAAA,EACxD;AAEA,SAAO;AACT;AAKO,SAAS,kBAAkB,QAA2B,QAAyB;AACpF,QAAM,QAAkB,CAAA;AAExB,MAAI,QAAQ;AACV,UAAM,KAAK,2BAA2B;AACtC,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,QAAM,KAAK,gBAAgB;AAG3B,MAAI,OAAO,iBAAiB,GAAG;AAC7B,UAAM,KAAK,oBAAoB;AAAA,EACjC,OAAO;AACL,UAAM,QAAkB,CAAA;AACxB,QAAI,OAAO,eAAe,EAAG,OAAM,KAAK,GAAG,OAAO,YAAY,UAAU;AACxE,QAAI,OAAO,eAAe,EAAG,OAAM,KAAK,GAAG,OAAO,YAAY,UAAU;AACxE,QAAI,OAAO,iBAAiB,EAAG,OAAM,KAAK,GAAG,OAAO,cAAc,YAAY;AAE9E,QAAI,MAAM,SAAS,GAAG;AACpB,YAAM,KAAK,oBAAoB,OAAO,YAAY,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG;AAAA,IAC5E,OAAO;AACL,YAAM,KAAK,oBAAoB,OAAO,YAAY,EAAE;AAAA,IACtD;AAAA,EACF;AAGA,MAAI,OAAO,oBAAoB,GAAG;AAChC,UAAM,QAAkB,CAAA;AACxB,QAAI,OAAO,6BAA6B,EAAG,OAAM,KAAK,GAAG,OAAO,0BAA0B,UAAU;AACpG,QAAI,OAAO,6BAA6B,EAAG,OAAM,KAAK,GAAG,OAAO,0BAA0B,UAAU;AAEpG,QAAI,MAAM,SAAS,GAAG;AACpB,YAAM,KAAK,0BAA0B,OAAO,iBAAiB,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG;AAAA,IACvF,OAAO;AACL,YAAM,KAAK,0BAA0B,OAAO,iBAAiB,EAAE;AAAA,IACjE;AAAA,EACF;AAEA,MAAI,OAAO,SAAS,SAAS,GAAG;AAC9B,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,WAAW;AACtB,eAAW,WAAW,OAAO,UAAU;AACrC,YAAM,KAAK,OAAO,OAAO,EAAE;AAAA,IAC7B;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
1
+ {"version":3,"file":"merge.js","sources":["../../../../src/cli/commands/review/merge.ts"],"sourcesContent":["/**\n * review merge command - Merge edited file back into main reviews.yaml\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { ReviewFile, ArticleEntry, Review, WorkFile, ReviewBasis } from './types.js';\nimport { validateName } from './extract.js';\n\n\n/**\n * Check if a file is a work file (has basis field)\n */\n/** @deprecated Detects old WorkFile format (flat id/decision fields). Kept for backward compat. */\nfunction isWorkFile(file: unknown): file is WorkFile {\n if (\n typeof file !== 'object' ||\n file === null ||\n !('basis' in file) ||\n !('reviewer' in file) ||\n !('articles' in file) ||\n !Array.isArray((file as WorkFile).articles)\n ) {\n return false;\n }\n // Distinguish from new ReviewFile-with-basis: old WorkFile has articles with flat `id` + `decision` fields\n const articles = (file as WorkFile).articles;\n if (articles.length === 0) return false;\n const first = articles[0]!;\n return 'id' in first && 'decision' in first && !('reviews' in first);\n}\n\nexport interface ReviewMergeOptions {\n sessionId: string;\n /** Name of the review subset (reads from for-review/<name>/review.yaml) */\n name: string;\n dryRun?: boolean;\n}\n\nexport interface ReviewMergeResult {\n reviewsAdded: number;\n decisionsSet: number;\n includeCount: number;\n excludeCount: number;\n uncertainCount: number;\n finalDecisionsSet: number;\n finalDecisionsIncludeCount: number;\n finalDecisionsExcludeCount: number;\n warnings: string[];\n}\n\n/**\n * Load review file from path\n */\nasync function loadReviewFile(path: string): Promise<ReviewFile> {\n const content = await readFile(path, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Auto-detect review basis from article data: fulltext > abstract > title\n */\nfunction detectBasis(article: ArticleEntry): ReviewBasis {\n if (article.fulltext) return 'fulltext';\n if (article.abstract) return 'abstract';\n return 'title';\n}\n\n/**\n * Register a reviewer in the review file's reviewers registry.\n * Deduplicates by name+basis pair.\n */\nexport function registerReviewer(reviewFile: ReviewFile, name: string, basis: ReviewBasis): void {\n if (!reviewFile.reviewers) {\n reviewFile.reviewers = [];\n }\n const exists = reviewFile.reviewers.some((r) => r.name === name && r.basis === basis);\n if (!exists) {\n reviewFile.reviewers.push({ name, basis });\n }\n}\n\n/**\n * Match article from extracted file to main file\n */\nfunction findMatchingArticle(\n extracted: ArticleEntry,\n mainArticles: ArticleEntry[]\n): ArticleEntry | undefined {\n // Try matching by various identifiers\n for (const main of mainArticles) {\n if (extracted.pmid && main.pmid && extracted.pmid === main.pmid) {\n return main;\n }\n if (extracted.doi && main.doi && extracted.doi.toLowerCase() === main.doi.toLowerCase()) {\n return main;\n }\n if (extracted.scopusId && main.scopusId && extracted.scopusId === main.scopusId) {\n return main;\n }\n if (extracted.arxivId && main.arxivId && extracted.arxivId === main.arxivId) {\n return main;\n }\n if (extracted.ericId && main.ericId && extracted.ericId === main.ericId) {\n return main;\n }\n }\n return undefined;\n}\n\n/**\n * Match work file article by id to main file\n * The id can be a DOI, PMID, ScopusId, ArxivId, EricId, or title\n */\nfunction findMatchingArticleById(\n id: string,\n mainArticles: ArticleEntry[]\n): ArticleEntry | undefined {\n for (const main of mainArticles) {\n // Match by DOI (case-insensitive)\n if (main.doi && main.doi.toLowerCase() === id.toLowerCase()) {\n return main;\n }\n // Match by PMID\n if (main.pmid && main.pmid === id) {\n return main;\n }\n // Match by ScopusId\n if (main.scopusId && main.scopusId === id) {\n return main;\n }\n // Match by ArxivId\n if (main.arxivId && main.arxivId === id) {\n return main;\n }\n // Match by EricId\n if (main.ericId && main.ericId === id) {\n return main;\n }\n // Match by title (fallback, case-insensitive)\n if (main.title.toLowerCase() === id.toLowerCase()) {\n return main;\n }\n }\n return undefined;\n}\n\n/**\n * Process work file format (with basis/reviewer)\n */\nfunction processWorkFile(\n workFile: WorkFile,\n mainFile: ReviewFile,\n options: ReviewMergeOptions\n): ReviewMergeResult {\n const result: ReviewMergeResult = {\n reviewsAdded: 0,\n decisionsSet: 0,\n includeCount: 0,\n excludeCount: 0,\n uncertainCount: 0,\n finalDecisionsSet: 0,\n finalDecisionsIncludeCount: 0,\n finalDecisionsExcludeCount: 0,\n warnings: [],\n };\n\n const timestamp = new Date().toISOString();\n\n // Register the reviewer for this work file's basis\n if (!options.dryRun) {\n registerReviewer(mainFile, workFile.reviewer, workFile.basis);\n }\n\n for (const workArticle of workFile.articles) {\n // Skip articles with null decision (not yet reviewed)\n if (workArticle.decision === null) {\n continue;\n }\n\n const mainArticle = findMatchingArticleById(workArticle.id, mainFile.articles);\n\n if (!mainArticle) {\n result.warnings.push(`Article not found in main file: id=\"${workArticle.id}\"`);\n continue;\n }\n\n // Ensure mainArticle.reviews is an array\n if (!mainArticle.reviews) {\n mainArticle.reviews = [];\n }\n\n // Create review from work file article\n const review: Review = {\n reviewer: workFile.reviewer,\n decision: workArticle.decision,\n basis: workFile.basis,\n timestamp,\n };\n\n // Add comment if provided\n if (workArticle.comment) {\n review.comment = workArticle.comment;\n }\n\n if (!options.dryRun) {\n mainArticle.reviews.push(review);\n }\n result.reviewsAdded++;\n\n // Count decision types\n if (workArticle.decision === 'include') result.includeCount++;\n else if (workArticle.decision === 'exclude') result.excludeCount++;\n else if (workArticle.decision === 'uncertain') result.uncertainCount++;\n }\n\n return result;\n}\n\n/**\n * Process review file format (with reviewHistory separation)\n */\nfunction processReviewFile(\n extractedFile: ReviewFile,\n mainFile: ReviewFile,\n options: ReviewMergeOptions\n): ReviewMergeResult {\n const result: ReviewMergeResult = {\n reviewsAdded: 0,\n decisionsSet: 0,\n includeCount: 0,\n excludeCount: 0,\n uncertainCount: 0,\n finalDecisionsSet: 0,\n finalDecisionsIncludeCount: 0,\n finalDecisionsExcludeCount: 0,\n warnings: [],\n };\n\n const timestamp = new Date().toISOString();\n const topLevelReviewer = extractedFile.reviewer;\n\n for (const extracted of extractedFile.articles) {\n const mainArticle = findMatchingArticle(extracted, mainFile.articles);\n\n if (!mainArticle) {\n result.warnings.push(`Article not found in main file: \"${extracted.title}\"`);\n continue;\n }\n\n // Merge only reviews[] (reviewHistory is ignored)\n const extractedReviews = extracted.reviews ?? [];\n\n // Ensure mainArticle.reviews is an array\n if (!mainArticle.reviews) {\n mainArticle.reviews = [];\n }\n\n for (const review of extractedReviews) {\n // Fill in reviewer from top-level field if not on individual review\n const reviewer = review.reviewer ?? topLevelReviewer;\n if (!reviewer) {\n throw new Error('reviewer is required: set reviewer on individual review or top-level ReviewFile');\n }\n // Fill in basis from review, top-level field, or auto-detect from article data\n const basis = review.basis ?? extractedFile.basis ?? detectBasis(extracted);\n\n if (!options.dryRun) {\n const mergedReview: Review = {\n ...review,\n reviewer,\n basis,\n timestamp: review.timestamp ?? timestamp,\n };\n mainArticle.reviews.push(mergedReview);\n\n // Register reviewer\n registerReviewer(mainFile, reviewer, basis);\n }\n result.reviewsAdded++;\n\n // Count decision types\n if (review.decision === 'include') result.includeCount++;\n else if (review.decision === 'exclude') result.excludeCount++;\n else if (review.decision === 'uncertain') result.uncertainCount++;\n }\n\n // Overwrite finalDecision if set in extracted (null means unset)\n if (extracted.finalDecision !== undefined && extracted.finalDecision !== null) {\n if (!options.dryRun) {\n mainArticle.finalDecision = extracted.finalDecision;\n }\n result.decisionsSet++;\n result.finalDecisionsSet++;\n if (extracted.finalDecision === 'include') result.finalDecisionsIncludeCount++;\n else if (extracted.finalDecision === 'exclude') result.finalDecisionsExcludeCount++;\n }\n }\n\n return result;\n}\n\n/**\n * Execute review merge command\n */\nexport async function executeReviewMerge(\n options: ReviewMergeOptions,\n sessionsDir: string\n): Promise<ReviewMergeResult> {\n validateName(options.name);\n const sessionDir = join(sessionsDir, options.sessionId);\n const mainReviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const filePath = join(sessionDir, 'for-review', options.name, 'review.yaml');\n\n // Load both files\n const mainFile = await loadReviewFile(mainReviewsPath);\n const content = await readFile(filePath, 'utf-8');\n const inputFile = parseYaml(content);\n\n let result: ReviewMergeResult;\n\n // Detect file format and process accordingly\n if (isWorkFile(inputFile)) {\n result = processWorkFile(inputFile, mainFile, options);\n } else {\n result = processReviewFile(inputFile as ReviewFile, mainFile, options);\n }\n\n // Write back if not dry-run\n if (!options.dryRun) {\n const yamlContent = stringifyYaml(mainFile, {\n lineWidth: 0,\n });\n\n // Preserve schema reference comment (local copy alongside reviews.yaml)\n const schemaComment = `# yaml-language-server: $schema=./review.schema.json\\n`;\n const finalContent = schemaComment + yamlContent;\n\n await writeFile(mainReviewsPath, finalContent, 'utf-8');\n }\n\n return result;\n}\n\n/**\n * Format merge result as human-readable string\n */\nexport function formatMergeOutput(result: ReviewMergeResult, dryRun: boolean): string {\n const lines: string[] = [];\n\n if (dryRun) {\n lines.push('Dry run - no changes made');\n lines.push('');\n }\n\n lines.push('Merge Summary:');\n\n // Build reviews line with decision breakdown\n if (result.reviewsAdded === 0) {\n lines.push(` Reviews added: 0`);\n } else {\n const parts: string[] = [];\n if (result.excludeCount > 0) parts.push(`${result.excludeCount} exclude`);\n if (result.includeCount > 0) parts.push(`${result.includeCount} include`);\n if (result.uncertainCount > 0) parts.push(`${result.uncertainCount} uncertain`);\n\n if (parts.length > 0) {\n lines.push(` Reviews added: ${result.reviewsAdded} (${parts.join(', ')})`);\n } else {\n lines.push(` Reviews added: ${result.reviewsAdded}`);\n }\n }\n\n // Show final decisions only when some were set\n if (result.finalDecisionsSet > 0) {\n const parts: string[] = [];\n if (result.finalDecisionsIncludeCount > 0) parts.push(`${result.finalDecisionsIncludeCount} include`);\n if (result.finalDecisionsExcludeCount > 0) parts.push(`${result.finalDecisionsExcludeCount} exclude`);\n\n if (parts.length > 0) {\n lines.push(` Final decisions set: ${result.finalDecisionsSet} (${parts.join(', ')})`);\n } else {\n lines.push(` Final decisions set: ${result.finalDecisionsSet}`);\n }\n }\n\n if (result.warnings.length > 0) {\n lines.push('');\n lines.push('Warnings:');\n for (const warning of result.warnings) {\n lines.push(` - ${warning}`);\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":"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"}
@@ -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;AAqKpC;;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,CA0tEvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiLpC;;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,CAkzEvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
package/dist/cli/index.js CHANGED
@@ -9,12 +9,16 @@ import "../config/schema.js";
9
9
  import { getDefaultConfig } from "../config/defaults.js";
10
10
  import { getDefaultConfigPath } from "../config/paths.js";
11
11
  import { viewConfig, viewConfigKey, setConfigKey } from "./commands/config.js";
12
- import { validateQueryCommand, formatValidateResult } from "./commands/query/validate.js";
12
+ import { detectSchemaLink, validateQueryCommand, formatValidateResult, formatVocabValidationOutput, hasVocabErrors } from "./commands/query/validate.js";
13
+ import { MeSHLookupClient } from "../query/mesh-lookup.js";
14
+ import { RateLimiter } from "../providers/base/rate-limiter.js";
15
+ import { VocabCache } from "../query/vocab-cache.js";
16
+ import { createEricCountValidator, createEmtreeCountValidator } from "../query/vocab-validator.js";
17
+ import { createProviderInstance, executePreview, executeCountOnly, executeSearch } from "./commands/search-executor.js";
13
18
  import { translateQueryCommand, formatTranslateResult } from "./commands/query/translate.js";
14
19
  import { writeQueryTemplate, generateQueryTemplate } from "./commands/query/init.js";
15
20
  import { getSessionDetails, computeDeduplicationStats, formatSessionDetails, listSessionsForDisplay, formatSessionList } from "./commands/status.js";
16
21
  import { parseSearchOptions, validateSearchInput, formatShortKeywordWarning, formatDryRunOutput, formatPreviewOutput, formatCountOnlyOutput } from "./commands/search.js";
17
- import { executePreview, executeCountOnly, executeSearch } from "./commands/search-executor.js";
18
22
  import { parseResumeOptions, validateResumeInput, getResumableProvidersForCommand } from "./commands/resume.js";
19
23
  import { executeResume } from "./commands/resume-executor.js";
20
24
  import { formatVerboseProviderDetails } from "./commands/search-utils.js";
@@ -172,16 +176,86 @@ Query YAML format (minimal):
172
176
  operator: OR
173
177
 
174
178
  Use "search-hub query init" to generate a template.`);
175
- queryCommand.command("validate").description("Validate query YAML file").argument("<file>", "path to query YAML file").addHelpText("after", `
179
+ queryCommand.command("validate").description("Validate query YAML file (auto-checks controlled vocabulary)").argument("<file>", "path to query YAML file").option("--no-vocab", "skip controlled vocabulary validation").option("--no-cache", "skip vocabulary lookup cache").addHelpText("after", `
176
180
  Examples:
177
- $ search-hub query validate ./diabetes-ai.yaml`).action(async (file) => {
181
+ $ search-hub query validate ./diabetes-ai.yaml
182
+ $ search-hub query validate ./diabetes-ai.yaml --no-vocab # Skip MeSH check
183
+ $ search-hub query validate ./diabetes-ai.yaml --no-cache # Ignore cache`).action(async (file, opts) => {
178
184
  const globalOpts = program.opts();
179
185
  try {
180
- const result = await validateQueryCommand(file);
186
+ const noVocab = opts.vocab === false;
187
+ const noCache = opts.cache === false;
188
+ let cache;
189
+ if (!noVocab && !noCache) {
190
+ cache = new VocabCache();
191
+ await cache.load();
192
+ }
193
+ const hasSchema = await detectSchemaLink(file);
194
+ if (noVocab) {
195
+ const result2 = await validateQueryCommand(file, { noVocab });
196
+ if (!globalOpts.quiet) {
197
+ let output = formatValidateResult(result2, file);
198
+ const suggestion = formatSuggestion(getSuggestion({
199
+ command: "query validate",
200
+ queryFile: file,
201
+ validationSuccess: result2.success,
202
+ hasSchemaLink: hasSchema
203
+ }));
204
+ if (suggestion) output += "\n" + suggestion;
205
+ console.log(output);
206
+ }
207
+ process.exitCode = !result2.success ? EXIT_CODES.QUERY_ERROR : EXIT_CODES.SUCCESS;
208
+ return;
209
+ }
210
+ const rateLimiter = new RateLimiter({ tokensPerSecond: 3 });
211
+ const meshClient = new MeSHLookupClient({
212
+ rateLimiter,
213
+ ...cache ? { cache } : {}
214
+ });
215
+ const countValidators = [];
216
+ let config2;
217
+ try {
218
+ config2 = await loadConfig(
219
+ globalOpts.config ? { explicitConfigPath: globalOpts.config } : {}
220
+ );
221
+ } catch {
222
+ }
223
+ if (config2) {
224
+ const ericProvider = createProviderInstance("eric", config2);
225
+ if (ericProvider) {
226
+ countValidators.push(
227
+ createEricCountValidator(ericProvider, cache ? { cache } : void 0)
228
+ );
229
+ }
230
+ const scopusProvider = createProviderInstance("scopus", config2);
231
+ if (scopusProvider) {
232
+ countValidators.push(
233
+ createEmtreeCountValidator(scopusProvider, cache ? { cache } : void 0)
234
+ );
235
+ }
236
+ }
237
+ const result = await validateQueryCommand(file, {
238
+ meshClient,
239
+ ...countValidators.length > 0 ? { countValidators } : {}
240
+ });
241
+ if (cache) {
242
+ await cache.save();
243
+ }
181
244
  if (!globalOpts.quiet) {
182
- console.log(formatValidateResult(result, file));
245
+ let output = formatValidateResult(result, file);
246
+ if (result.vocabResult) {
247
+ output += formatVocabValidationOutput(result.vocabResult);
248
+ }
249
+ const suggestion = formatSuggestion(getSuggestion({
250
+ command: "query validate",
251
+ queryFile: file,
252
+ validationSuccess: result.success && !hasVocabErrors(result),
253
+ hasSchemaLink: hasSchema
254
+ }));
255
+ if (suggestion) output += "\n" + suggestion;
256
+ console.log(output);
183
257
  }
184
- process.exitCode = result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.QUERY_ERROR;
258
+ process.exitCode = !result.success || hasVocabErrors(result) ? EXIT_CODES.QUERY_ERROR : EXIT_CODES.SUCCESS;
185
259
  } catch (error) {
186
260
  if (!globalOpts.quiet) {
187
261
  console.error(