@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
@@ -136,8 +136,8 @@ function compareBlocks(block1, block2, index, field) {
136
136
  const emptyTerms = { keywords: [], mesh: [], emtree: [], exclude: [] };
137
137
  const terms1 = block1 ?? emptyTerms;
138
138
  const terms2 = block2 ?? emptyTerms;
139
- const added = setDiff(terms1.keywords, terms2.keywords);
140
- const removed = setDiff(terms2.keywords, terms1.keywords);
139
+ const added = setDiff(terms1.keywords ?? [], terms2.keywords ?? []);
140
+ const removed = setDiff(terms2.keywords ?? [], terms1.keywords ?? []);
141
141
  const meshAdded = setDiff(terms1.mesh ?? [], terms2.mesh ?? []);
142
142
  const meshRemoved = setDiff(terms2.mesh ?? [], terms1.mesh ?? []);
143
143
  const emtreeAdded = setDiff(terms1.emtree ?? [], terms2.emtree ?? []);
@@ -1 +1 @@
1
- {"version":3,"file":"diff.js","sources":["../../../src/cli/commands/diff.ts"],"sourcesContent":["import type { Article } from '../../providers/base/types.js';\nimport type { QueryAST, FieldType, TermBlock } from '../../query/types.js';\nimport { getArticleKeys } from './session-utils.js';\n\nexport interface DiffResult {\n session1Count: number;\n session2Count: number;\n added: Article[];\n removed: Article[];\n common: Article[];\n}\n\n/**\n * Compute the diff between two sets of articles.\n *\n * Articles are matched by identifiers (DOI, PMID, arXiv ID, Scopus ID, ERIC ID).\n * Two articles are considered the same if they share any identifier.\n * Articles without identifiers cannot be matched.\n */\nexport function computeDiff(session1: Article[], session2: Article[]): DiffResult {\n // Build a set of all identifier keys from session1\n const session1Keys = new Set<string>();\n for (const article of session1) {\n for (const key of getArticleKeys(article)) {\n session1Keys.add(key);\n }\n }\n\n // Build a set of all identifier keys from session2\n const session2Keys = new Set<string>();\n for (const article of session2) {\n for (const key of getArticleKeys(article)) {\n session2Keys.add(key);\n }\n }\n\n // Classify session1 articles as common or removed\n const common: Article[] = [];\n const removed: Article[] = [];\n for (const article of session1) {\n const keys = getArticleKeys(article);\n if (keys.length === 0) {\n // No identifiers - cannot match, treat as removed\n removed.push(article);\n continue;\n }\n const isInSession2 = keys.some((key) => session2Keys.has(key));\n if (isInSession2) {\n common.push(article);\n } else {\n removed.push(article);\n }\n }\n\n // Classify session2 articles as added (those not in session1)\n const added: Article[] = [];\n for (const article of session2) {\n const keys = getArticleKeys(article);\n if (keys.length === 0) {\n // No identifiers - cannot match, treat as added\n added.push(article);\n continue;\n }\n const isInSession1 = keys.some((key) => session1Keys.has(key));\n if (!isInSession1) {\n added.push(article);\n }\n }\n\n return {\n session1Count: session1.length,\n session2Count: session2.length,\n added,\n removed,\n common,\n };\n}\n\nexport type ShowFilter = 'added' | 'removed' | 'common';\n\n/**\n * Extract a year string from publicationDate, or return empty string.\n */\nfunction extractYear(publicationDate: string | undefined): string {\n if (!publicationDate) return '';\n const year = publicationDate.substring(0, 4);\n return /^\\d{4}$/.test(year) ? year : '';\n}\n\n/**\n * Format an article line for display.\n */\nfunction formatArticleLine(prefix: string, article: Article): string {\n const year = extractYear(article.publicationDate);\n const yearPart = year ? `[${year}] ` : '';\n return ` ${prefix} ${yearPart}${article.title}`;\n}\n\n/**\n * Options for formatting diff with query information.\n */\nexport interface FormatDiffOptions {\n queryDiff?: QueryDiff | undefined;\n noQueryDiff?: boolean | undefined;\n showQueryDiffPlaceholder?: boolean | undefined;\n}\n\n/**\n * Format diff result as human-readable text.\n */\nexport function formatDiff(\n diff: DiffResult,\n session1Id: string,\n session2Id: string,\n show?: ShowFilter,\n options?: FormatDiffOptions,\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push(`Diff: ${session1Id} → ${session2Id}`);\n lines.push(` Session 1: ${diff.session1Count} articles (${session1Id})`);\n lines.push(` Session 2: ${diff.session2Count} articles (${session2Id})`);\n lines.push('');\n\n // Query changes section (if available and not disabled)\n const shouldShowQueryDiff = options?.queryDiff && !options?.noQueryDiff;\n const shouldShowPlaceholder = options?.showQueryDiffPlaceholder && !options?.queryDiff && !options?.noQueryDiff;\n\n if (shouldShowPlaceholder) {\n lines.push('Query changes: (query data not available)');\n lines.push('');\n } else if (shouldShowQueryDiff) {\n lines.push(formatQueryDiff(options.queryDiff!));\n lines.push('');\n lines.push('Result changes:');\n }\n\n // Summary counts\n lines.push(` Common: ${diff.common.length} articles`);\n lines.push(` Added: ${diff.added.length} articles (in ${session2Id} but not ${session1Id})`);\n lines.push(` Removed: ${diff.removed.length} articles (in ${session1Id} but not ${session2Id})`);\n\n // Article lists based on show filter\n const showAdded = !show || show === 'added';\n const showRemoved = !show || show === 'removed';\n const showCommon = show === 'common';\n\n if (showAdded && diff.added.length > 0) {\n lines.push('');\n lines.push(`Added (+${diff.added.length}):`);\n for (const article of diff.added) {\n lines.push(formatArticleLine('+', article));\n }\n }\n\n if (showRemoved && diff.removed.length > 0) {\n lines.push('');\n lines.push(`Removed (-${diff.removed.length}):`);\n for (const article of diff.removed) {\n lines.push(formatArticleLine('-', article));\n }\n }\n\n if (showCommon && diff.common.length > 0) {\n lines.push('');\n lines.push(`Common (${diff.common.length}):`);\n for (const article of diff.common) {\n lines.push(formatArticleLine('=', article));\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format diff result as JSON.\n */\ninterface DiffJsonOutput {\n session1: string;\n session2: string;\n summary: {\n session1Count: number;\n session2Count: number;\n commonCount: number;\n addedCount: number;\n removedCount: number;\n };\n queryDiff?: QueryDiff;\n added?: Article[];\n removed?: Article[];\n common?: Article[];\n}\n\nexport function formatDiffJson(\n diff: DiffResult,\n session1Id: string,\n session2Id: string,\n show?: ShowFilter,\n options?: FormatDiffOptions,\n): string {\n const result: DiffJsonOutput = {\n session1: session1Id,\n session2: session2Id,\n summary: {\n session1Count: diff.session1Count,\n session2Count: diff.session2Count,\n commonCount: diff.common.length,\n addedCount: diff.added.length,\n removedCount: diff.removed.length,\n },\n };\n\n // Add queryDiff if available and not disabled\n if (options?.queryDiff && !options?.noQueryDiff) {\n result.queryDiff = options.queryDiff;\n }\n\n if (!show || show === 'added') {\n result.added = diff.added;\n }\n if (!show || show === 'removed') {\n result.removed = diff.removed;\n }\n if (!show || show === 'common') {\n result.common = diff.common;\n }\n\n return JSON.stringify(result, null, 2);\n}\n\n/**\n * Diff result for a single query block.\n */\nexport interface BlockDiff {\n index: number;\n field: FieldType;\n added: string[];\n removed: string[];\n meshAdded?: string[];\n meshRemoved?: string[];\n emtreeAdded?: string[];\n emtreeRemoved?: string[];\n excludeAdded?: string[];\n excludeRemoved?: string[];\n hasChanges: boolean;\n isNew?: boolean;\n isRemoved?: boolean;\n}\n\n/**\n * Diff result for query filters.\n */\nexport interface FilterDiff {\n yearFromChanged: boolean;\n oldYearFrom?: number | undefined;\n newYearFrom?: number | undefined;\n yearToChanged: boolean;\n oldYearTo?: number | undefined;\n newYearTo?: number | undefined;\n languagesAdded: string[];\n languagesRemoved: string[];\n}\n\n/**\n * Complete query diff result.\n */\nexport interface QueryDiff {\n blocks: BlockDiff[];\n filters: FilterDiff;\n}\n\n/**\n * Compute the set difference: elements in arr2 not in arr1.\n */\nfunction setDiff(arr1: string[], arr2: string[]): string[] {\n const set1 = new Set(arr1);\n return arr2.filter((item) => !set1.has(item));\n}\n\n/**\n * Compare two query blocks and return the differences.\n */\nfunction compareBlocks(\n block1: TermBlock | undefined,\n block2: TermBlock | undefined,\n index: number,\n field: FieldType,\n): BlockDiff {\n const emptyTerms = { keywords: [] as string[], mesh: [] as string[], emtree: [] as string[], exclude: [] as string[] };\n const terms1 = block1 ?? emptyTerms;\n const terms2 = block2 ?? emptyTerms;\n\n const added = setDiff(terms1.keywords, terms2.keywords);\n const removed = setDiff(terms2.keywords, terms1.keywords);\n const meshAdded = setDiff(terms1.mesh ?? [], terms2.mesh ?? []);\n const meshRemoved = setDiff(terms2.mesh ?? [], terms1.mesh ?? []);\n const emtreeAdded = setDiff(terms1.emtree ?? [], terms2.emtree ?? []);\n const emtreeRemoved = setDiff(terms2.emtree ?? [], terms1.emtree ?? []);\n const excludeAdded = setDiff(terms1.exclude ?? [], terms2.exclude ?? []);\n const excludeRemoved = setDiff(terms2.exclude ?? [], terms1.exclude ?? []);\n\n const hasChanges =\n added.length > 0 ||\n removed.length > 0 ||\n meshAdded.length > 0 ||\n meshRemoved.length > 0 ||\n emtreeAdded.length > 0 ||\n emtreeRemoved.length > 0 ||\n excludeAdded.length > 0 ||\n excludeRemoved.length > 0;\n\n const result: BlockDiff = {\n index,\n field,\n added,\n removed,\n hasChanges,\n };\n\n if (meshAdded.length > 0 || meshRemoved.length > 0) {\n result.meshAdded = meshAdded;\n result.meshRemoved = meshRemoved;\n }\n if (emtreeAdded.length > 0 || emtreeRemoved.length > 0) {\n result.emtreeAdded = emtreeAdded;\n result.emtreeRemoved = emtreeRemoved;\n }\n if (excludeAdded.length > 0 || excludeRemoved.length > 0) {\n result.excludeAdded = excludeAdded;\n result.excludeRemoved = excludeRemoved;\n }\n\n if (!block1) {\n result.isNew = true;\n result.hasChanges = true;\n }\n if (!block2) {\n result.isRemoved = true;\n result.hasChanges = true;\n }\n\n return result;\n}\n\n/**\n * Compute the diff between two QueryAST objects.\n */\nexport function computeQueryDiff(query1: QueryAST, query2: QueryAST): QueryDiff {\n const blocks: BlockDiff[] = [];\n\n const maxBlocks = Math.max(query1.blocks.length, query2.blocks.length);\n\n for (let i = 0; i < maxBlocks; i++) {\n const block1 = query1.blocks[i];\n const block2 = query2.blocks[i];\n\n const field = block2?.field ?? block1?.field ?? 'all';\n const blockDiff = compareBlocks(\n block1?.terms,\n block2?.terms,\n i,\n field,\n );\n blocks.push(blockDiff);\n }\n\n // Compare filters\n const filters: FilterDiff = {\n yearFromChanged: query1.filters.yearFrom !== query2.filters.yearFrom,\n yearToChanged: query1.filters.yearTo !== query2.filters.yearTo,\n languagesAdded: setDiff(query1.filters.languages ?? [], query2.filters.languages ?? []),\n languagesRemoved: setDiff(query2.filters.languages ?? [], query1.filters.languages ?? []),\n };\n\n if (filters.yearFromChanged) {\n filters.oldYearFrom = query1.filters.yearFrom;\n filters.newYearFrom = query2.filters.yearFrom;\n }\n if (filters.yearToChanged) {\n filters.oldYearTo = query1.filters.yearTo;\n filters.newYearTo = query2.filters.yearTo;\n }\n\n return { blocks, filters };\n}\n\n/**\n * Format query diff as human-readable text.\n */\nexport function formatQueryDiff(queryDiff: QueryDiff): string {\n const lines: string[] = [];\n\n lines.push('Query changes:');\n\n // Format block changes\n for (const block of queryDiff.blocks) {\n const blockNum = block.index + 1;\n let blockHeader = ` Block ${blockNum} (${block.field})`;\n if (block.isNew) {\n blockHeader += ' (new block)';\n } else if (block.isRemoved) {\n blockHeader += ' (removed block)';\n }\n\n if (!block.hasChanges) {\n lines.push(`${blockHeader}: no changes`);\n } else {\n lines.push(`${blockHeader}:`);\n\n // Added keywords\n for (const keyword of block.added) {\n lines.push(` + ${keyword}`);\n }\n\n // Removed keywords\n for (const keyword of block.removed) {\n lines.push(` - ${keyword}`);\n }\n\n // MeSH changes\n if (block.meshAdded) {\n for (const term of block.meshAdded) {\n lines.push(` + [MeSH] ${term}`);\n }\n }\n if (block.meshRemoved) {\n for (const term of block.meshRemoved) {\n lines.push(` - [MeSH] ${term}`);\n }\n }\n\n // Emtree changes\n if (block.emtreeAdded) {\n for (const term of block.emtreeAdded) {\n lines.push(` + [Emtree] ${term}`);\n }\n }\n if (block.emtreeRemoved) {\n for (const term of block.emtreeRemoved) {\n lines.push(` - [Emtree] ${term}`);\n }\n }\n\n // Exclude changes\n if (block.excludeAdded) {\n for (const term of block.excludeAdded) {\n lines.push(` + [exclude] ${term}`);\n }\n }\n if (block.excludeRemoved) {\n for (const term of block.excludeRemoved) {\n lines.push(` - [exclude] ${term}`);\n }\n }\n }\n }\n\n // Format filter changes\n const hasFilterChanges =\n queryDiff.filters.yearFromChanged ||\n queryDiff.filters.yearToChanged ||\n queryDiff.filters.languagesAdded.length > 0 ||\n queryDiff.filters.languagesRemoved.length > 0;\n\n if (hasFilterChanges) {\n lines.push('');\n lines.push(' Filters:');\n\n if (queryDiff.filters.yearFromChanged) {\n const oldVal = queryDiff.filters.oldYearFrom ?? '(none)';\n const newVal = queryDiff.filters.newYearFrom ?? '(none)';\n lines.push(` yearFrom: ${oldVal} → ${newVal}`);\n }\n\n if (queryDiff.filters.yearToChanged) {\n const oldVal = queryDiff.filters.oldYearTo ?? '(none)';\n const newVal = queryDiff.filters.newYearTo ?? '(none)';\n lines.push(` yearTo: ${oldVal} → ${newVal}`);\n }\n\n if (queryDiff.filters.languagesAdded.length > 0 || queryDiff.filters.languagesRemoved.length > 0) {\n lines.push(' languages:');\n for (const lang of queryDiff.filters.languagesAdded) {\n lines.push(` + ${lang}`);\n }\n for (const lang of queryDiff.filters.languagesRemoved) {\n lines.push(` - ${lang}`);\n }\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"diff.js","sources":["../../../src/cli/commands/diff.ts"],"sourcesContent":["import type { Article } from '../../providers/base/types.js';\nimport type { QueryAST, FieldType, TermBlock } from '../../query/types.js';\nimport { getArticleKeys } from './session-utils.js';\n\nexport interface DiffResult {\n session1Count: number;\n session2Count: number;\n added: Article[];\n removed: Article[];\n common: Article[];\n}\n\n/**\n * Compute the diff between two sets of articles.\n *\n * Articles are matched by identifiers (DOI, PMID, arXiv ID, Scopus ID, ERIC ID).\n * Two articles are considered the same if they share any identifier.\n * Articles without identifiers cannot be matched.\n */\nexport function computeDiff(session1: Article[], session2: Article[]): DiffResult {\n // Build a set of all identifier keys from session1\n const session1Keys = new Set<string>();\n for (const article of session1) {\n for (const key of getArticleKeys(article)) {\n session1Keys.add(key);\n }\n }\n\n // Build a set of all identifier keys from session2\n const session2Keys = new Set<string>();\n for (const article of session2) {\n for (const key of getArticleKeys(article)) {\n session2Keys.add(key);\n }\n }\n\n // Classify session1 articles as common or removed\n const common: Article[] = [];\n const removed: Article[] = [];\n for (const article of session1) {\n const keys = getArticleKeys(article);\n if (keys.length === 0) {\n // No identifiers - cannot match, treat as removed\n removed.push(article);\n continue;\n }\n const isInSession2 = keys.some((key) => session2Keys.has(key));\n if (isInSession2) {\n common.push(article);\n } else {\n removed.push(article);\n }\n }\n\n // Classify session2 articles as added (those not in session1)\n const added: Article[] = [];\n for (const article of session2) {\n const keys = getArticleKeys(article);\n if (keys.length === 0) {\n // No identifiers - cannot match, treat as added\n added.push(article);\n continue;\n }\n const isInSession1 = keys.some((key) => session1Keys.has(key));\n if (!isInSession1) {\n added.push(article);\n }\n }\n\n return {\n session1Count: session1.length,\n session2Count: session2.length,\n added,\n removed,\n common,\n };\n}\n\nexport type ShowFilter = 'added' | 'removed' | 'common';\n\n/**\n * Extract a year string from publicationDate, or return empty string.\n */\nfunction extractYear(publicationDate: string | undefined): string {\n if (!publicationDate) return '';\n const year = publicationDate.substring(0, 4);\n return /^\\d{4}$/.test(year) ? year : '';\n}\n\n/**\n * Format an article line for display.\n */\nfunction formatArticleLine(prefix: string, article: Article): string {\n const year = extractYear(article.publicationDate);\n const yearPart = year ? `[${year}] ` : '';\n return ` ${prefix} ${yearPart}${article.title}`;\n}\n\n/**\n * Options for formatting diff with query information.\n */\nexport interface FormatDiffOptions {\n queryDiff?: QueryDiff | undefined;\n noQueryDiff?: boolean | undefined;\n showQueryDiffPlaceholder?: boolean | undefined;\n}\n\n/**\n * Format diff result as human-readable text.\n */\nexport function formatDiff(\n diff: DiffResult,\n session1Id: string,\n session2Id: string,\n show?: ShowFilter,\n options?: FormatDiffOptions,\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push(`Diff: ${session1Id} → ${session2Id}`);\n lines.push(` Session 1: ${diff.session1Count} articles (${session1Id})`);\n lines.push(` Session 2: ${diff.session2Count} articles (${session2Id})`);\n lines.push('');\n\n // Query changes section (if available and not disabled)\n const shouldShowQueryDiff = options?.queryDiff && !options?.noQueryDiff;\n const shouldShowPlaceholder = options?.showQueryDiffPlaceholder && !options?.queryDiff && !options?.noQueryDiff;\n\n if (shouldShowPlaceholder) {\n lines.push('Query changes: (query data not available)');\n lines.push('');\n } else if (shouldShowQueryDiff) {\n lines.push(formatQueryDiff(options.queryDiff!));\n lines.push('');\n lines.push('Result changes:');\n }\n\n // Summary counts\n lines.push(` Common: ${diff.common.length} articles`);\n lines.push(` Added: ${diff.added.length} articles (in ${session2Id} but not ${session1Id})`);\n lines.push(` Removed: ${diff.removed.length} articles (in ${session1Id} but not ${session2Id})`);\n\n // Article lists based on show filter\n const showAdded = !show || show === 'added';\n const showRemoved = !show || show === 'removed';\n const showCommon = show === 'common';\n\n if (showAdded && diff.added.length > 0) {\n lines.push('');\n lines.push(`Added (+${diff.added.length}):`);\n for (const article of diff.added) {\n lines.push(formatArticleLine('+', article));\n }\n }\n\n if (showRemoved && diff.removed.length > 0) {\n lines.push('');\n lines.push(`Removed (-${diff.removed.length}):`);\n for (const article of diff.removed) {\n lines.push(formatArticleLine('-', article));\n }\n }\n\n if (showCommon && diff.common.length > 0) {\n lines.push('');\n lines.push(`Common (${diff.common.length}):`);\n for (const article of diff.common) {\n lines.push(formatArticleLine('=', article));\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format diff result as JSON.\n */\ninterface DiffJsonOutput {\n session1: string;\n session2: string;\n summary: {\n session1Count: number;\n session2Count: number;\n commonCount: number;\n addedCount: number;\n removedCount: number;\n };\n queryDiff?: QueryDiff;\n added?: Article[];\n removed?: Article[];\n common?: Article[];\n}\n\nexport function formatDiffJson(\n diff: DiffResult,\n session1Id: string,\n session2Id: string,\n show?: ShowFilter,\n options?: FormatDiffOptions,\n): string {\n const result: DiffJsonOutput = {\n session1: session1Id,\n session2: session2Id,\n summary: {\n session1Count: diff.session1Count,\n session2Count: diff.session2Count,\n commonCount: diff.common.length,\n addedCount: diff.added.length,\n removedCount: diff.removed.length,\n },\n };\n\n // Add queryDiff if available and not disabled\n if (options?.queryDiff && !options?.noQueryDiff) {\n result.queryDiff = options.queryDiff;\n }\n\n if (!show || show === 'added') {\n result.added = diff.added;\n }\n if (!show || show === 'removed') {\n result.removed = diff.removed;\n }\n if (!show || show === 'common') {\n result.common = diff.common;\n }\n\n return JSON.stringify(result, null, 2);\n}\n\n/**\n * Diff result for a single query block.\n */\nexport interface BlockDiff {\n index: number;\n field: FieldType;\n added: string[];\n removed: string[];\n meshAdded?: string[];\n meshRemoved?: string[];\n emtreeAdded?: string[];\n emtreeRemoved?: string[];\n excludeAdded?: string[];\n excludeRemoved?: string[];\n hasChanges: boolean;\n isNew?: boolean;\n isRemoved?: boolean;\n}\n\n/**\n * Diff result for query filters.\n */\nexport interface FilterDiff {\n yearFromChanged: boolean;\n oldYearFrom?: number | undefined;\n newYearFrom?: number | undefined;\n yearToChanged: boolean;\n oldYearTo?: number | undefined;\n newYearTo?: number | undefined;\n languagesAdded: string[];\n languagesRemoved: string[];\n}\n\n/**\n * Complete query diff result.\n */\nexport interface QueryDiff {\n blocks: BlockDiff[];\n filters: FilterDiff;\n}\n\n/**\n * Compute the set difference: elements in arr2 not in arr1.\n */\nfunction setDiff(arr1: string[], arr2: string[]): string[] {\n const set1 = new Set(arr1);\n return arr2.filter((item) => !set1.has(item));\n}\n\n/**\n * Compare two query blocks and return the differences.\n */\nfunction compareBlocks(\n block1: TermBlock | undefined,\n block2: TermBlock | undefined,\n index: number,\n field: FieldType,\n): BlockDiff {\n const emptyTerms = { keywords: [] as string[], mesh: [] as string[], emtree: [] as string[], exclude: [] as string[] };\n const terms1 = block1 ?? emptyTerms;\n const terms2 = block2 ?? emptyTerms;\n\n const added = setDiff(terms1.keywords ?? [], terms2.keywords ?? []);\n const removed = setDiff(terms2.keywords ?? [], terms1.keywords ?? []);\n const meshAdded = setDiff(terms1.mesh ?? [], terms2.mesh ?? []);\n const meshRemoved = setDiff(terms2.mesh ?? [], terms1.mesh ?? []);\n const emtreeAdded = setDiff(terms1.emtree ?? [], terms2.emtree ?? []);\n const emtreeRemoved = setDiff(terms2.emtree ?? [], terms1.emtree ?? []);\n const excludeAdded = setDiff(terms1.exclude ?? [], terms2.exclude ?? []);\n const excludeRemoved = setDiff(terms2.exclude ?? [], terms1.exclude ?? []);\n\n const hasChanges =\n added.length > 0 ||\n removed.length > 0 ||\n meshAdded.length > 0 ||\n meshRemoved.length > 0 ||\n emtreeAdded.length > 0 ||\n emtreeRemoved.length > 0 ||\n excludeAdded.length > 0 ||\n excludeRemoved.length > 0;\n\n const result: BlockDiff = {\n index,\n field,\n added,\n removed,\n hasChanges,\n };\n\n if (meshAdded.length > 0 || meshRemoved.length > 0) {\n result.meshAdded = meshAdded;\n result.meshRemoved = meshRemoved;\n }\n if (emtreeAdded.length > 0 || emtreeRemoved.length > 0) {\n result.emtreeAdded = emtreeAdded;\n result.emtreeRemoved = emtreeRemoved;\n }\n if (excludeAdded.length > 0 || excludeRemoved.length > 0) {\n result.excludeAdded = excludeAdded;\n result.excludeRemoved = excludeRemoved;\n }\n\n if (!block1) {\n result.isNew = true;\n result.hasChanges = true;\n }\n if (!block2) {\n result.isRemoved = true;\n result.hasChanges = true;\n }\n\n return result;\n}\n\n/**\n * Compute the diff between two QueryAST objects.\n */\nexport function computeQueryDiff(query1: QueryAST, query2: QueryAST): QueryDiff {\n const blocks: BlockDiff[] = [];\n\n const maxBlocks = Math.max(query1.blocks.length, query2.blocks.length);\n\n for (let i = 0; i < maxBlocks; i++) {\n const block1 = query1.blocks[i];\n const block2 = query2.blocks[i];\n\n const field = block2?.field ?? block1?.field ?? 'all';\n const blockDiff = compareBlocks(\n block1?.terms,\n block2?.terms,\n i,\n field,\n );\n blocks.push(blockDiff);\n }\n\n // Compare filters\n const filters: FilterDiff = {\n yearFromChanged: query1.filters.yearFrom !== query2.filters.yearFrom,\n yearToChanged: query1.filters.yearTo !== query2.filters.yearTo,\n languagesAdded: setDiff(query1.filters.languages ?? [], query2.filters.languages ?? []),\n languagesRemoved: setDiff(query2.filters.languages ?? [], query1.filters.languages ?? []),\n };\n\n if (filters.yearFromChanged) {\n filters.oldYearFrom = query1.filters.yearFrom;\n filters.newYearFrom = query2.filters.yearFrom;\n }\n if (filters.yearToChanged) {\n filters.oldYearTo = query1.filters.yearTo;\n filters.newYearTo = query2.filters.yearTo;\n }\n\n return { blocks, filters };\n}\n\n/**\n * Format query diff as human-readable text.\n */\nexport function formatQueryDiff(queryDiff: QueryDiff): string {\n const lines: string[] = [];\n\n lines.push('Query changes:');\n\n // Format block changes\n for (const block of queryDiff.blocks) {\n const blockNum = block.index + 1;\n let blockHeader = ` Block ${blockNum} (${block.field})`;\n if (block.isNew) {\n blockHeader += ' (new block)';\n } else if (block.isRemoved) {\n blockHeader += ' (removed block)';\n }\n\n if (!block.hasChanges) {\n lines.push(`${blockHeader}: no changes`);\n } else {\n lines.push(`${blockHeader}:`);\n\n // Added keywords\n for (const keyword of block.added) {\n lines.push(` + ${keyword}`);\n }\n\n // Removed keywords\n for (const keyword of block.removed) {\n lines.push(` - ${keyword}`);\n }\n\n // MeSH changes\n if (block.meshAdded) {\n for (const term of block.meshAdded) {\n lines.push(` + [MeSH] ${term}`);\n }\n }\n if (block.meshRemoved) {\n for (const term of block.meshRemoved) {\n lines.push(` - [MeSH] ${term}`);\n }\n }\n\n // Emtree changes\n if (block.emtreeAdded) {\n for (const term of block.emtreeAdded) {\n lines.push(` + [Emtree] ${term}`);\n }\n }\n if (block.emtreeRemoved) {\n for (const term of block.emtreeRemoved) {\n lines.push(` - [Emtree] ${term}`);\n }\n }\n\n // Exclude changes\n if (block.excludeAdded) {\n for (const term of block.excludeAdded) {\n lines.push(` + [exclude] ${term}`);\n }\n }\n if (block.excludeRemoved) {\n for (const term of block.excludeRemoved) {\n lines.push(` - [exclude] ${term}`);\n }\n }\n }\n }\n\n // Format filter changes\n const hasFilterChanges =\n queryDiff.filters.yearFromChanged ||\n queryDiff.filters.yearToChanged ||\n queryDiff.filters.languagesAdded.length > 0 ||\n queryDiff.filters.languagesRemoved.length > 0;\n\n if (hasFilterChanges) {\n lines.push('');\n lines.push(' Filters:');\n\n if (queryDiff.filters.yearFromChanged) {\n const oldVal = queryDiff.filters.oldYearFrom ?? '(none)';\n const newVal = queryDiff.filters.newYearFrom ?? '(none)';\n lines.push(` yearFrom: ${oldVal} → ${newVal}`);\n }\n\n if (queryDiff.filters.yearToChanged) {\n const oldVal = queryDiff.filters.oldYearTo ?? '(none)';\n const newVal = queryDiff.filters.newYearTo ?? '(none)';\n lines.push(` yearTo: ${oldVal} → ${newVal}`);\n }\n\n if (queryDiff.filters.languagesAdded.length > 0 || queryDiff.filters.languagesRemoved.length > 0) {\n lines.push(' languages:');\n for (const lang of queryDiff.filters.languagesAdded) {\n lines.push(` + ${lang}`);\n }\n for (const lang of queryDiff.filters.languagesRemoved) {\n lines.push(` - ${lang}`);\n }\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":[],"mappings":"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"}
@@ -1,3 +1,8 @@
1
+ /**
2
+ * The YAML template string with comments preserved.
3
+ * This is a raw string (not generated by a YAML library) so comments are kept.
4
+ */
5
+ export declare const QUERY_SCHEMA_FILENAME = "query.schema.json";
1
6
  /**
2
7
  * Generate the query template YAML string.
3
8
  *
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"AAyDA;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA4BjD"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAiDzD;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAkCjD"}
@@ -1,5 +1,9 @@
1
1
  import { access, writeFile } from "node:fs/promises";
2
- const QUERY_TEMPLATE = `name: my_search
2
+ import { join, dirname } from "node:path";
3
+ import { generateQueryJSONSchema } from "../../../query/json-schema.js";
4
+ const QUERY_SCHEMA_FILENAME = "query.schema.json";
5
+ const QUERY_TEMPLATE = `# yaml-language-server: $schema=./${QUERY_SCHEMA_FILENAME}
6
+ name: my_search
3
7
  description: ""
4
8
 
5
9
  query:
@@ -62,12 +66,16 @@ async function writeQueryTemplate(options) {
62
66
  }
63
67
  }
64
68
  await writeFile(options.output, template, "utf-8");
69
+ const schemaPath = join(dirname(options.output), QUERY_SCHEMA_FILENAME);
70
+ const jsonSchema = generateQueryJSONSchema();
71
+ await writeFile(schemaPath, JSON.stringify(jsonSchema, null, 2) + "\n", "utf-8");
65
72
  return {
66
73
  success: true,
67
74
  message: `Template written to ${options.output}`
68
75
  };
69
76
  }
70
77
  export {
78
+ QUERY_SCHEMA_FILENAME,
71
79
  generateQueryTemplate,
72
80
  writeQueryTemplate
73
81
  };
@@ -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 } from \"node:fs/promises\";\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 */\n// prettier-ignore\nconst QUERY_TEMPLATE =\n \"name: my_search\\n\" +\n \"description: \\\"\\\"\\n\" +\n \"\\n\" +\n \"query:\\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 \" # - 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 \"# overrides: # Optional: database-specific settings\\n\" +\n \"# pubmed:\\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 * @returns The YAML template string with comments\n */\nexport function generateQueryTemplate(): string {\n return QUERY_TEMPLATE;\n}\n\n/**\n * Write the query template to a file or return it as a message.\n *\n * @param options - Output options\n * @param options.output - File path to write to (if omitted, returns template in message)\n * @param options.force - Whether to overwrite existing files\n * @returns Result with success status and message\n */\nexport async function writeQueryTemplate(options: {\n output?: string;\n force?: boolean;\n}): Promise<{ success: boolean; message: string }> {\n const template = generateQueryTemplate();\n\n if (!options.output) {\n // No output file specified, return template as message\n return { success: true, message: template };\n }\n\n // Check if file exists (unless force is set)\n if (!options.force) {\n try {\n await access(options.output);\n // File exists and force is not set\n return {\n success: false,\n message: `File already exists: ${options.output}. Use --force to overwrite.`,\n };\n } catch {\n // File does not exist, proceed\n }\n }\n\n // Write to file\n await fsWriteFile(options.output, template, \"utf-8\");\n return {\n success: true,\n message: `Template written to ${options.output}`,\n };\n}\n"],"names":["fsWriteFile"],"mappings":";AAYA,MAAM,iBACJ;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;AAiDK,SAAS,wBAAgC;AAC9C,SAAO;AACT;AAUA,eAAsB,mBAAmB,SAGU;AACjD,QAAM,WAAW,sBAAA;AAEjB,MAAI,CAAC,QAAQ,QAAQ;AAEnB,WAAO,EAAE,SAAS,MAAM,SAAS,SAAA;AAAA,EACnC;AAGA,MAAI,CAAC,QAAQ,OAAO;AAClB,QAAI;AACF,YAAM,OAAO,QAAQ,MAAM;AAE3B,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,wBAAwB,QAAQ,MAAM;AAAA,MAAA;AAAA,IAEnD,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAMA,UAAY,QAAQ,QAAQ,UAAU,OAAO;AACnD,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS,uBAAuB,QAAQ,MAAM;AAAA,EAAA;AAElD;"}
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 } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { generateQueryJSONSchema } from \"../../../query/json-schema.js\";\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 \" - 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 \" # - 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 \"# overrides: # Optional: database-specific settings\\n\" +\n \"# pubmed:\\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 * @returns The YAML template string with comments\n */\nexport function generateQueryTemplate(): string {\n return QUERY_TEMPLATE;\n}\n\n/**\n * Write the query template to a file or return it as a message.\n *\n * @param options - Output options\n * @param options.output - File path to write to (if omitted, returns template in message)\n * @param options.force - Whether to overwrite existing files\n * @returns Result with success status and message\n */\nexport async function writeQueryTemplate(options: {\n output?: string;\n force?: boolean;\n}): Promise<{ success: boolean; message: string }> {\n const template = generateQueryTemplate();\n\n if (!options.output) {\n // No output file specified, return template as message\n return { success: true, message: template };\n }\n\n // Check if file exists (unless force is set)\n if (!options.force) {\n try {\n await access(options.output);\n // File exists and force is not set\n return {\n success: false,\n message: `File already exists: ${options.output}. Use --force to overwrite.`,\n };\n } catch {\n // File does not exist, proceed\n }\n }\n\n // Write to file\n await fsWriteFile(options.output, template, \"utf-8\");\n\n // Generate JSON Schema file alongside output\n const schemaPath = join(dirname(options.output), 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: `Template written to ${options.output}`,\n };\n}\n"],"names":["fsWriteFile"],"mappings":";;;AAaO,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;AAkDrD,SAAS,wBAAgC;AAC9C,SAAO;AACT;AAUA,eAAsB,mBAAmB,SAGU;AACjD,QAAM,WAAW,sBAAA;AAEjB,MAAI,CAAC,QAAQ,QAAQ;AAEnB,WAAO,EAAE,SAAS,MAAM,SAAS,SAAA;AAAA,EACnC;AAGA,MAAI,CAAC,QAAQ,OAAO;AAClB,QAAI;AACF,YAAM,OAAO,QAAQ,MAAM;AAE3B,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,wBAAwB,QAAQ,MAAM;AAAA,MAAA;AAAA,IAEnD,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAMA,UAAY,QAAQ,QAAQ,UAAU,OAAO;AAGnD,QAAM,aAAa,KAAK,QAAQ,QAAQ,MAAM,GAAG,qBAAqB;AACtE,QAAM,aAAa,wBAAA;AACnB,QAAMA,UAAY,YAAY,KAAK,UAAU,YAAY,MAAM,CAAC,IAAI,MAAM,OAAO;AAEjF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS,uBAAuB,QAAQ,MAAM;AAAA,EAAA;AAElD;"}
@@ -1 +1 @@
1
- {"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/translate.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAwBtF;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0CAA0C;IAC1C,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oCAAoC;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAChD;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAuD1B;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,MAAM,GACf,MAAM,CAgBR"}
1
+ {"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/translate.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAwBtF;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0CAA0C;IAC1C,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oCAAoC;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAChD;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAuD1B;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,MAAM,GACf,MAAM,CAqBR"}
@@ -64,6 +64,11 @@ function formatTranslateResult(result, filePath) {
64
64
  for (const [provider, translation] of Object.entries(result.translations)) {
65
65
  lines.push(`[${provider.toUpperCase()}]`);
66
66
  lines.push(translation.native);
67
+ if (translation.warnings && translation.warnings.length > 0) {
68
+ for (const warning of translation.warnings) {
69
+ lines.push(`⚠ ${warning}`);
70
+ }
71
+ }
67
72
  lines.push("");
68
73
  }
69
74
  }
@@ -1 +1 @@
1
- {"version":3,"file":"translate.js","sources":["../../../../src/cli/commands/query/translate.ts"],"sourcesContent":["/**\n * Query translate command implementation.\n *\n * Translates a YAML query file to native database query syntax for each provider.\n */\nimport { readFile } from 'node:fs/promises';\nimport { parseQueryString } from '../../../query/index.js';\nimport type { TranslatedQuery, ProviderName } from '../../../providers/base/types.js';\nimport { translateQuery as translatePubmed } from '../../../providers/pubmed/translator.js';\nimport { translateQuery as translateEric } from '../../../providers/eric/translator.js';\nimport { translateQuery as translateArxiv } from '../../../providers/arxiv/translator.js';\nimport { translateQuery as translateScopus } from '../../../providers/scopus/translator.js';\n\n/**\n * Available translators by provider name.\n */\nconst translators: Record<\n string,\n (ast: Parameters<typeof translatePubmed>[0]) => TranslatedQuery\n> = {\n pubmed: translatePubmed,\n eric: translateEric,\n arxiv: translateArxiv,\n scopus: translateScopus,\n};\n\n/**\n * Default providers to translate for.\n */\nconst DEFAULT_PROVIDERS: ProviderName[] = ['pubmed', 'eric', 'arxiv', 'scopus'];\n\n/**\n * Options for translate command.\n */\nexport interface TranslateOptions {\n /** Specific providers to translate for */\n providers?: ProviderName[];\n}\n\n/**\n * Result of query translation.\n */\nexport interface TranslateResult {\n /** Whether translation succeeded */\n success: boolean;\n /** Error message if failed */\n error?: string;\n /** Translations by provider name */\n translations?: Record<string, TranslatedQuery>;\n}\n\n/**\n * Translate a query YAML file to native syntax for each provider.\n *\n * @param filePath - Path to the query file\n * @param options - Translation options\n * @returns Translation result\n */\nexport async function translateQueryCommand(\n filePath: string,\n options: TranslateOptions = {}\n): Promise<TranslateResult> {\n // Read file\n let content: string;\n try {\n content = await readFile(filePath, 'utf-8');\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Failed to read file';\n return {\n success: false,\n error: message,\n };\n }\n\n // Parse and validate\n let ast;\n try {\n ast = parseQueryString(content);\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Failed to parse query file';\n return {\n success: false,\n error: message,\n };\n }\n\n // Determine which providers to translate for\n const providers = options.providers ?? DEFAULT_PROVIDERS;\n\n // Translate for each provider\n const translations: Record<string, TranslatedQuery> = {};\n\n for (const provider of providers) {\n const translator = translators[provider];\n if (translator) {\n try {\n translations[provider] = translator(ast);\n } catch (error) {\n const message =\n error instanceof Error\n ? error.message\n : `Failed to translate for ${provider}`;\n return {\n success: false,\n error: `${provider}: ${message}`,\n };\n }\n }\n }\n\n return {\n success: true,\n translations,\n };\n}\n\n/**\n * Format translation result for display.\n */\nexport function formatTranslateResult(\n result: TranslateResult,\n filePath: string\n): string {\n if (!result.success) {\n return `✗ Failed to translate: ${filePath}\\n Error: ${result.error}`;\n }\n\n const lines = [`Translations for: ${filePath}`, ''];\n\n if (result.translations) {\n for (const [provider, translation] of Object.entries(result.translations)) {\n lines.push(`[${provider.toUpperCase()}]`);\n lines.push(translation.native);\n lines.push('');\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":["translatePubmed","translateEric","translateArxiv","translateScopus"],"mappings":";;;;;;;AAgBA,MAAM,cAGF;AAAA,EACF,QAAQA;AAAAA,EACR,MAAMC;AAAAA,EACN,OAAOC;AAAAA,EACP,QAAQC;AACV;AAKA,MAAM,oBAAoC,CAAC,UAAU,QAAQ,SAAS,QAAQ;AA6B9E,eAAsB,sBACpB,UACA,UAA4B,IACF;AAE1B,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,SAAS,UAAU,OAAO;AAAA,EAC5C,SAAS,OAAO;AACd,UAAM,UACJ,iBAAiB,QAAQ,MAAM,UAAU;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AAGA,MAAI;AACJ,MAAI;AACF,UAAM,iBAAiB,OAAO;AAAA,EAChC,SAAS,OAAO;AACd,UAAM,UACJ,iBAAiB,QAAQ,MAAM,UAAU;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AAGA,QAAM,YAAY,QAAQ,aAAa;AAGvC,QAAM,eAAgD,CAAA;AAEtD,aAAW,YAAY,WAAW;AAChC,UAAM,aAAa,YAAY,QAAQ;AACvC,QAAI,YAAY;AACd,UAAI;AACF,qBAAa,QAAQ,IAAI,WAAW,GAAG;AAAA,MACzC,SAAS,OAAO;AACd,cAAM,UACJ,iBAAiB,QACb,MAAM,UACN,2BAA2B,QAAQ;AACzC,eAAO;AAAA,UACL,SAAS;AAAA,UACT,OAAO,GAAG,QAAQ,KAAK,OAAO;AAAA,QAAA;AAAA,MAElC;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,EAAA;AAEJ;AAKO,SAAS,sBACd,QACA,UACQ;AACR,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,0BAA0B,QAAQ;AAAA,WAAc,OAAO,KAAK;AAAA,EACrE;AAEA,QAAM,QAAQ,CAAC,qBAAqB,QAAQ,IAAI,EAAE;AAElD,MAAI,OAAO,cAAc;AACvB,eAAW,CAAC,UAAU,WAAW,KAAK,OAAO,QAAQ,OAAO,YAAY,GAAG;AACzE,YAAM,KAAK,IAAI,SAAS,YAAA,CAAa,GAAG;AACxC,YAAM,KAAK,YAAY,MAAM;AAC7B,YAAM,KAAK,EAAE;AAAA,IACf;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
1
+ {"version":3,"file":"translate.js","sources":["../../../../src/cli/commands/query/translate.ts"],"sourcesContent":["/**\n * Query translate command implementation.\n *\n * Translates a YAML query file to native database query syntax for each provider.\n */\nimport { readFile } from 'node:fs/promises';\nimport { parseQueryString } from '../../../query/index.js';\nimport type { TranslatedQuery, ProviderName } from '../../../providers/base/types.js';\nimport { translateQuery as translatePubmed } from '../../../providers/pubmed/translator.js';\nimport { translateQuery as translateEric } from '../../../providers/eric/translator.js';\nimport { translateQuery as translateArxiv } from '../../../providers/arxiv/translator.js';\nimport { translateQuery as translateScopus } from '../../../providers/scopus/translator.js';\n\n/**\n * Available translators by provider name.\n */\nconst translators: Record<\n string,\n (ast: Parameters<typeof translatePubmed>[0]) => TranslatedQuery\n> = {\n pubmed: translatePubmed,\n eric: translateEric,\n arxiv: translateArxiv,\n scopus: translateScopus,\n};\n\n/**\n * Default providers to translate for.\n */\nconst DEFAULT_PROVIDERS: ProviderName[] = ['pubmed', 'eric', 'arxiv', 'scopus'];\n\n/**\n * Options for translate command.\n */\nexport interface TranslateOptions {\n /** Specific providers to translate for */\n providers?: ProviderName[];\n}\n\n/**\n * Result of query translation.\n */\nexport interface TranslateResult {\n /** Whether translation succeeded */\n success: boolean;\n /** Error message if failed */\n error?: string;\n /** Translations by provider name */\n translations?: Record<string, TranslatedQuery>;\n}\n\n/**\n * Translate a query YAML file to native syntax for each provider.\n *\n * @param filePath - Path to the query file\n * @param options - Translation options\n * @returns Translation result\n */\nexport async function translateQueryCommand(\n filePath: string,\n options: TranslateOptions = {}\n): Promise<TranslateResult> {\n // Read file\n let content: string;\n try {\n content = await readFile(filePath, 'utf-8');\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Failed to read file';\n return {\n success: false,\n error: message,\n };\n }\n\n // Parse and validate\n let ast;\n try {\n ast = parseQueryString(content);\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Failed to parse query file';\n return {\n success: false,\n error: message,\n };\n }\n\n // Determine which providers to translate for\n const providers = options.providers ?? DEFAULT_PROVIDERS;\n\n // Translate for each provider\n const translations: Record<string, TranslatedQuery> = {};\n\n for (const provider of providers) {\n const translator = translators[provider];\n if (translator) {\n try {\n translations[provider] = translator(ast);\n } catch (error) {\n const message =\n error instanceof Error\n ? error.message\n : `Failed to translate for ${provider}`;\n return {\n success: false,\n error: `${provider}: ${message}`,\n };\n }\n }\n }\n\n return {\n success: true,\n translations,\n };\n}\n\n/**\n * Format translation result for display.\n */\nexport function formatTranslateResult(\n result: TranslateResult,\n filePath: string\n): string {\n if (!result.success) {\n return `✗ Failed to translate: ${filePath}\\n Error: ${result.error}`;\n }\n\n const lines = [`Translations for: ${filePath}`, ''];\n\n if (result.translations) {\n for (const [provider, translation] of Object.entries(result.translations)) {\n lines.push(`[${provider.toUpperCase()}]`);\n lines.push(translation.native);\n if (translation.warnings && translation.warnings.length > 0) {\n for (const warning of translation.warnings) {\n lines.push(`⚠ ${warning}`);\n }\n }\n lines.push('');\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":["translatePubmed","translateEric","translateArxiv","translateScopus"],"mappings":";;;;;;;AAgBA,MAAM,cAGF;AAAA,EACF,QAAQA;AAAAA,EACR,MAAMC;AAAAA,EACN,OAAOC;AAAAA,EACP,QAAQC;AACV;AAKA,MAAM,oBAAoC,CAAC,UAAU,QAAQ,SAAS,QAAQ;AA6B9E,eAAsB,sBACpB,UACA,UAA4B,IACF;AAE1B,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,SAAS,UAAU,OAAO;AAAA,EAC5C,SAAS,OAAO;AACd,UAAM,UACJ,iBAAiB,QAAQ,MAAM,UAAU;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AAGA,MAAI;AACJ,MAAI;AACF,UAAM,iBAAiB,OAAO;AAAA,EAChC,SAAS,OAAO;AACd,UAAM,UACJ,iBAAiB,QAAQ,MAAM,UAAU;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AAGA,QAAM,YAAY,QAAQ,aAAa;AAGvC,QAAM,eAAgD,CAAA;AAEtD,aAAW,YAAY,WAAW;AAChC,UAAM,aAAa,YAAY,QAAQ;AACvC,QAAI,YAAY;AACd,UAAI;AACF,qBAAa,QAAQ,IAAI,WAAW,GAAG;AAAA,MACzC,SAAS,OAAO;AACd,cAAM,UACJ,iBAAiB,QACb,MAAM,UACN,2BAA2B,QAAQ;AACzC,eAAO;AAAA,UACL,SAAS;AAAA,UACT,OAAO,GAAG,QAAQ,KAAK,OAAO;AAAA,QAAA;AAAA,MAElC;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,EAAA;AAEJ;AAKO,SAAS,sBACd,QACA,UACQ;AACR,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,0BAA0B,QAAQ;AAAA,WAAc,OAAO,KAAK;AAAA,EACrE;AAEA,QAAM,QAAQ,CAAC,qBAAqB,QAAQ,IAAI,EAAE;AAElD,MAAI,OAAO,cAAc;AACvB,eAAW,CAAC,UAAU,WAAW,KAAK,OAAO,QAAQ,OAAO,YAAY,GAAG;AACzE,YAAM,KAAK,IAAI,SAAS,YAAA,CAAa,GAAG;AACxC,YAAM,KAAK,YAAY,MAAM;AAC7B,UAAI,YAAY,YAAY,YAAY,SAAS,SAAS,GAAG;AAC3D,mBAAW,WAAW,YAAY,UAAU;AAC1C,gBAAM,KAAK,KAAK,OAAO,EAAE;AAAA,QAC3B;AAAA,MACF;AACA,YAAM,KAAK,EAAE;AAAA,IACf;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
@@ -1,3 +1,5 @@
1
+ import { MeSHLookupClient } from '../../../query/mesh-lookup.js';
2
+ import { CountVocabValidator, VocabValidationResult } from '../../../query/vocab-validator.js';
1
3
  /**
2
4
  * Result of query validation.
3
5
  */
@@ -10,6 +12,8 @@ export interface ValidateResult {
10
12
  queryName?: string;
11
13
  /** Number of query blocks (if valid) */
12
14
  blockCount?: number;
15
+ /** Controlled vocabulary validation results (auto-checked by default) */
16
+ vocabResult?: VocabValidationResult;
13
17
  }
14
18
  /**
15
19
  * Validate a query YAML file.
@@ -17,9 +21,26 @@ export interface ValidateResult {
17
21
  * @param filePath - Path to the query file
18
22
  * @returns Validation result
19
23
  */
20
- export declare function validateQueryCommand(filePath: string): Promise<ValidateResult>;
24
+ export declare function validateQueryCommand(filePath: string, options?: {
25
+ meshClient?: MeSHLookupClient;
26
+ noVocab?: boolean;
27
+ countValidators?: CountVocabValidator[];
28
+ }): Promise<ValidateResult>;
21
29
  /**
22
30
  * Format validation result for display.
23
31
  */
24
32
  export declare function formatValidateResult(result: ValidateResult, filePath: string): string;
33
+ /**
34
+ * Check if a validation result contains invalid controlled vocabulary terms.
35
+ */
36
+ export declare function hasVocabErrors(result: ValidateResult): boolean;
37
+ /**
38
+ * Detect whether a YAML file has a yaml-language-server $schema comment
39
+ * in its first 5 lines.
40
+ */
41
+ export declare function detectSchemaLink(filePath: string): Promise<boolean>;
42
+ /**
43
+ * Format controlled vocabulary validation results for display.
44
+ */
45
+ export declare function formatVocabValidationOutput(result: VocabValidationResult): string;
25
46
  //# sourceMappingURL=validate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/validate.ts"],"names":[],"mappings":"AASA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,CAAC,CAiDzB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,GACf,MAAM,CAiBR"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/validate.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEtE,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC3B,MAAM,mCAAmC,CAAC;AAE3C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACrC;AAyCD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACzC,GACA,OAAO,CAAC,cAAc,CAAC,CA0BzB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,GACf,MAAM,CAiBR;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAE9D;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQzE;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,qBAAqB,GAC5B,MAAM,CA6BR"}
@@ -1,49 +1,56 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { parseQueryString } from "../../../query/parser.js";
3
3
  import { ValidationError } from "../../../query/validator.js";
4
+ import { extractControlledVocabTerms, validateControlledVocab } from "../../../query/vocab-validator.js";
4
5
  import { ZodError } from "zod";
5
- async function validateQueryCommand(filePath) {
6
+ async function parseQueryFile(filePath) {
6
7
  let content;
7
8
  try {
8
9
  content = await readFile(filePath, "utf-8");
9
10
  } catch (error) {
10
11
  const message = error instanceof Error ? error.message : "Failed to read file";
11
- return {
12
- success: false,
13
- errors: [message]
14
- };
12
+ return { result: { success: false, errors: [message] } };
15
13
  }
16
14
  try {
17
15
  const ast = parseQueryString(content);
18
- return {
19
- success: true,
20
- queryName: ast.name,
21
- blockCount: ast.blocks.length
22
- };
16
+ return { ast };
23
17
  } catch (error) {
24
18
  if (error instanceof ZodError) {
25
19
  const errors = error.issues.map((e) => {
26
20
  const path = e.path.join(".");
27
21
  return path ? `${path}: ${e.message}` : e.message;
28
22
  });
29
- return {
30
- success: false,
31
- errors
32
- };
23
+ return { result: { success: false, errors } };
33
24
  }
34
25
  if (error instanceof ValidationError) {
35
- return {
36
- success: false,
37
- errors: [error.message]
38
- };
26
+ return { result: { success: false, errors: [error.message] } };
39
27
  }
40
28
  const message = error instanceof Error ? error.message : "Unknown validation error";
41
- return {
42
- success: false,
43
- errors: [message]
44
- };
29
+ return { result: { success: false, errors: [message] } };
45
30
  }
46
31
  }
32
+ async function validateQueryCommand(filePath, options) {
33
+ const parsed = await parseQueryFile(filePath);
34
+ if ("result" in parsed) {
35
+ return parsed.result;
36
+ }
37
+ const result = {
38
+ success: true,
39
+ queryName: parsed.ast.name,
40
+ blockCount: parsed.ast.blocks.length
41
+ };
42
+ if (options?.meshClient && !options.noVocab) {
43
+ const terms = extractControlledVocabTerms(parsed.ast);
44
+ if (terms.length > 0) {
45
+ result.vocabResult = await validateControlledVocab(
46
+ parsed.ast,
47
+ options.meshClient,
48
+ options.countValidators ? { countValidators: options.countValidators } : void 0
49
+ );
50
+ }
51
+ }
52
+ return result;
53
+ }
47
54
  function formatValidateResult(result, filePath) {
48
55
  if (result.success) {
49
56
  const lines2 = [
@@ -61,8 +68,44 @@ function formatValidateResult(result, filePath) {
61
68
  }
62
69
  return lines.join("\n");
63
70
  }
71
+ function hasVocabErrors(result) {
72
+ return (result.vocabResult?.invalid.length ?? 0) > 0;
73
+ }
74
+ async function detectSchemaLink(filePath) {
75
+ try {
76
+ const content = await readFile(filePath, "utf-8");
77
+ const lines = content.split("\n").slice(0, 5);
78
+ return lines.some((line) => /yaml-language-server.*\$schema=/.test(line));
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+ function formatVocabValidationOutput(result) {
84
+ if (result.valid.length === 0 && result.invalid.length === 0 && result.errors.length === 0) {
85
+ return "";
86
+ }
87
+ const lines = ["", "Controlled vocabulary:"];
88
+ for (const item of result.valid) {
89
+ lines.push(` ✓ ${item.vocabulary}: "${item.term}"`);
90
+ }
91
+ for (const item of result.invalid) {
92
+ lines.push(` ✗ ${item.vocabulary}: "${item.term}" — not found`);
93
+ if (item.suggestions && item.suggestions.length > 0) {
94
+ lines.push(
95
+ ` Did you mean: ${item.suggestions.map((s) => `"${s}"`).join(", ")}`
96
+ );
97
+ }
98
+ }
99
+ for (const item of result.errors) {
100
+ lines.push(` ⚠ ${item.vocabulary}: "${item.term}" — ${item.error}`);
101
+ }
102
+ return lines.join("\n");
103
+ }
64
104
  export {
105
+ detectSchemaLink,
65
106
  formatValidateResult,
107
+ formatVocabValidationOutput,
108
+ hasVocabErrors,
66
109
  validateQueryCommand
67
110
  };
68
111
  //# sourceMappingURL=validate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate.js","sources":["../../../../src/cli/commands/query/validate.ts"],"sourcesContent":["/**\n * Query validate command implementation.\n *\n * Validates a YAML query file and reports any errors.\n */\nimport { readFile } from 'node:fs/promises';\nimport { parseQueryString, ValidationError } from '../../../query/index.js';\nimport { ZodError } from 'zod';\n\n/**\n * Result of query validation.\n */\nexport interface ValidateResult {\n /** Whether validation succeeded */\n success: boolean;\n /** Error messages if validation failed */\n errors?: string[];\n /** Query name (if valid) */\n queryName?: string;\n /** Number of query blocks (if valid) */\n blockCount?: number;\n}\n\n/**\n * Validate a query YAML file.\n *\n * @param filePath - Path to the query file\n * @returns Validation result\n */\nexport async function validateQueryCommand(\n filePath: string\n): Promise<ValidateResult> {\n // Read file\n let content: string;\n try {\n content = await readFile(filePath, 'utf-8');\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Failed to read file';\n return {\n success: false,\n errors: [message],\n };\n }\n\n // Parse and validate using query module\n try {\n const ast = parseQueryString(content);\n return {\n success: true,\n queryName: ast.name,\n blockCount: ast.blocks.length,\n };\n } catch (error) {\n if (error instanceof ZodError) {\n const errors = error.issues.map((e) => {\n const path = e.path.join('.');\n return path ? `${path}: ${e.message}` : e.message;\n });\n return {\n success: false,\n errors,\n };\n }\n\n if (error instanceof ValidationError) {\n return {\n success: false,\n errors: [error.message],\n };\n }\n\n // YAML parse error or other error\n const message =\n error instanceof Error ? error.message : 'Unknown validation error';\n return {\n success: false,\n errors: [message],\n };\n }\n}\n\n/**\n * Format validation result for display.\n */\nexport function formatValidateResult(\n result: ValidateResult,\n filePath: string\n): string {\n if (result.success) {\n const lines = [\n `✓ Valid query file: ${filePath}`,\n ` Name: ${result.queryName}`,\n ` Blocks: ${result.blockCount}`,\n ];\n return lines.join('\\n');\n }\n\n const lines = [`✗ Invalid query file: ${filePath}`, '', 'Errors:'];\n if (result.errors) {\n for (const error of result.errors) {\n lines.push(` - ${error}`);\n }\n }\n return lines.join('\\n');\n}\n"],"names":["lines"],"mappings":";;;;AA6BA,eAAsB,qBACpB,UACyB;AAEzB,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,SAAS,UAAU,OAAO;AAAA,EAC5C,SAAS,OAAO;AACd,UAAM,UACJ,iBAAiB,QAAQ,MAAM,UAAU;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,QAAQ,CAAC,OAAO;AAAA,IAAA;AAAA,EAEpB;AAGA,MAAI;AACF,UAAM,MAAM,iBAAiB,OAAO;AACpC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,WAAW,IAAI;AAAA,MACf,YAAY,IAAI,OAAO;AAAA,IAAA;AAAA,EAE3B,SAAS,OAAO;AACd,QAAI,iBAAiB,UAAU;AAC7B,YAAM,SAAS,MAAM,OAAO,IAAI,CAAC,MAAM;AACrC,cAAM,OAAO,EAAE,KAAK,KAAK,GAAG;AAC5B,eAAO,OAAO,GAAG,IAAI,KAAK,EAAE,OAAO,KAAK,EAAE;AAAA,MAC5C,CAAC;AACD,aAAO;AAAA,QACL,SAAS;AAAA,QACT;AAAA,MAAA;AAAA,IAEJ;AAEA,QAAI,iBAAiB,iBAAiB;AACpC,aAAO;AAAA,QACL,SAAS;AAAA,QACT,QAAQ,CAAC,MAAM,OAAO;AAAA,MAAA;AAAA,IAE1B;AAGA,UAAM,UACJ,iBAAiB,QAAQ,MAAM,UAAU;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,QAAQ,CAAC,OAAO;AAAA,IAAA;AAAA,EAEpB;AACF;AAKO,SAAS,qBACd,QACA,UACQ;AACR,MAAI,OAAO,SAAS;AAClB,UAAMA,SAAQ;AAAA,MACZ,uBAAuB,QAAQ;AAAA,MAC/B,WAAW,OAAO,SAAS;AAAA,MAC3B,aAAa,OAAO,UAAU;AAAA,IAAA;AAEhC,WAAOA,OAAM,KAAK,IAAI;AAAA,EACxB;AAEA,QAAM,QAAQ,CAAC,yBAAyB,QAAQ,IAAI,IAAI,SAAS;AACjE,MAAI,OAAO,QAAQ;AACjB,eAAW,SAAS,OAAO,QAAQ;AACjC,YAAM,KAAK,OAAO,KAAK,EAAE;AAAA,IAC3B;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
1
+ {"version":3,"file":"validate.js","sources":["../../../../src/cli/commands/query/validate.ts"],"sourcesContent":["/**\n * Query validate command implementation.\n *\n * Validates a YAML query file and reports any errors.\n * Optionally validates controlled vocabulary terms (MeSH, ERIC, Emtree) against external APIs.\n */\nimport { readFile } from 'node:fs/promises';\nimport { parseQueryString, ValidationError } from '../../../query/index.js';\nimport { ZodError } from 'zod';\nimport type { MeSHLookupClient } from '../../../query/mesh-lookup.js';\nimport type { QueryAST } from '../../../query/types.js';\nimport {\n extractControlledVocabTerms,\n validateControlledVocab,\n type CountVocabValidator,\n type VocabValidationResult,\n} from '../../../query/vocab-validator.js';\n\n/**\n * Result of query validation.\n */\nexport interface ValidateResult {\n /** Whether validation succeeded */\n success: boolean;\n /** Error messages if validation failed */\n errors?: string[];\n /** Query name (if valid) */\n queryName?: string;\n /** Number of query blocks (if valid) */\n blockCount?: number;\n /** Controlled vocabulary validation results (auto-checked by default) */\n vocabResult?: VocabValidationResult;\n}\n\n/**\n * Read and parse a query YAML file.\n *\n * @returns The parsed AST on success, or a ValidateResult with errors on failure.\n */\nasync function parseQueryFile(\n filePath: string\n): Promise<{ ast: QueryAST } | { result: ValidateResult }> {\n let content: string;\n try {\n content = await readFile(filePath, 'utf-8');\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Failed to read file';\n return { result: { success: false, errors: [message] } };\n }\n\n try {\n const ast = parseQueryString(content);\n return { ast };\n } catch (error) {\n if (error instanceof ZodError) {\n const errors = error.issues.map((e) => {\n const path = e.path.join('.');\n return path ? `${path}: ${e.message}` : e.message;\n });\n return { result: { success: false, errors } };\n }\n\n if (error instanceof ValidationError) {\n return { result: { success: false, errors: [error.message] } };\n }\n\n const message =\n error instanceof Error ? error.message : 'Unknown validation error';\n return { result: { success: false, errors: [message] } };\n }\n}\n\n/**\n * Validate a query YAML file.\n *\n * @param filePath - Path to the query file\n * @returns Validation result\n */\nexport async function validateQueryCommand(\n filePath: string,\n options?: {\n meshClient?: MeSHLookupClient;\n noVocab?: boolean;\n countValidators?: CountVocabValidator[];\n }\n): Promise<ValidateResult> {\n const parsed = await parseQueryFile(filePath);\n\n if ('result' in parsed) {\n return parsed.result;\n }\n\n const result: ValidateResult = {\n success: true,\n queryName: parsed.ast.name,\n blockCount: parsed.ast.blocks.length,\n };\n\n // Auto-validate vocab when controlled vocab terms exist\n if (options?.meshClient && !options.noVocab) {\n const terms = extractControlledVocabTerms(parsed.ast);\n if (terms.length > 0) {\n result.vocabResult = await validateControlledVocab(\n parsed.ast,\n options.meshClient,\n options.countValidators ? { countValidators: options.countValidators } : undefined\n );\n }\n }\n\n return result;\n}\n\n/**\n * Format validation result for display.\n */\nexport function formatValidateResult(\n result: ValidateResult,\n filePath: string\n): string {\n if (result.success) {\n const lines = [\n `✓ Valid query file: ${filePath}`,\n ` Name: ${result.queryName}`,\n ` Blocks: ${result.blockCount}`,\n ];\n return lines.join('\\n');\n }\n\n const lines = [`✗ Invalid query file: ${filePath}`, '', 'Errors:'];\n if (result.errors) {\n for (const error of result.errors) {\n lines.push(` - ${error}`);\n }\n }\n return lines.join('\\n');\n}\n\n/**\n * Check if a validation result contains invalid controlled vocabulary terms.\n */\nexport function hasVocabErrors(result: ValidateResult): boolean {\n return (result.vocabResult?.invalid.length ?? 0) > 0;\n}\n\n/**\n * Detect whether a YAML file has a yaml-language-server $schema comment\n * in its first 5 lines.\n */\nexport async function detectSchemaLink(filePath: string): Promise<boolean> {\n try {\n const content = await readFile(filePath, 'utf-8');\n const lines = content.split('\\n').slice(0, 5);\n return lines.some((line) => /yaml-language-server.*\\$schema=/.test(line));\n } catch {\n return false;\n }\n}\n\n/**\n * Format controlled vocabulary validation results for display.\n */\nexport function formatVocabValidationOutput(\n result: VocabValidationResult\n): string {\n if (\n result.valid.length === 0 &&\n result.invalid.length === 0 &&\n result.errors.length === 0\n ) {\n return '';\n }\n\n const lines: string[] = ['', 'Controlled vocabulary:'];\n\n for (const item of result.valid) {\n lines.push(` ✓ ${item.vocabulary}: \"${item.term}\"`);\n }\n\n for (const item of result.invalid) {\n lines.push(` ✗ ${item.vocabulary}: \"${item.term}\" — not found`);\n if (item.suggestions && item.suggestions.length > 0) {\n lines.push(\n ` Did you mean: ${item.suggestions.map((s) => `\"${s}\"`).join(', ')}`\n );\n }\n }\n\n for (const item of result.errors) {\n lines.push(` ⚠ ${item.vocabulary}: \"${item.term}\" — ${item.error}`);\n }\n\n return lines.join('\\n');\n}\n"],"names":["lines"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/extract.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAsD,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAErH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE9D,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAGD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAgLD;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAU/C;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,EAC7B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,mBAAmB,CAAC,CAuG9B"}
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/extract.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAsD,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAErH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE9D,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAGD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAgLD;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAU/C;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,EAC7B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,mBAAmB,CAAC,CAsG9B"}
@@ -190,8 +190,7 @@ async function executeReviewExtract(options, sessionsDir) {
190
190
  const outputDir = dirname(outputPath);
191
191
  await mkdir(outputDir, { recursive: true });
192
192
  await writeFile(outputPath, finalContent, "utf-8");
193
- const schemasDir = join(dirname(sessionsDir), ".search-hub", "schemas");
194
- const schemaSourcePath = join(schemasDir, "review.schema.json");
193
+ const schemaSourcePath = join(sessionDir, ".internal", "review.schema.json");
195
194
  const schemaDestPath = join(outputDir, "review.schema.json");
196
195
  try {
197
196
  await access(schemaSourcePath);
@@ -1 +1 @@
1
- {"version":3,"file":"extract.js","sources":["../../../../src/cli/commands/review/extract.ts"],"sourcesContent":["/**\n * review extract command - Extract subset of articles for distributed review\n */\n\nimport { join, dirname } from 'node:path';\nimport { readFile, writeFile, mkdir, copyFile, access } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ArticleEntry, type ReviewStatus, type ReviewBasis } from './types.js';\n\nexport type SortOption = 'year' | 'title' | 'random' | 'none';\n\nexport interface ReviewExtractOptions {\n sessionId: string;\n filter?: ReviewStatus[];\n sort?: SortOption;\n seed?: number;\n limit?: number;\n offset?: number;\n /** Basis for the review (title, abstract, fulltext). When specified, outputs screening format. */\n basis?: ReviewBasis;\n /** Reviewer identifier (e.g., \"ai:claude\"). Required for all extract modes. */\n reviewer?: string;\n /** Name for the review subset (output goes to for-review/<name>/review.yaml) */\n name: string;\n /** When true, outputs final decision format with reviewHistory and finalDecision fields. */\n finalize?: boolean;\n}\n\n\nexport interface ReviewExtractResult {\n outputPath: string;\n extractedCount: number;\n totalMatching: number;\n}\n\n/**\n * Load review file from session directory\n */\nasync function loadReviewFile(sessionDir: string): Promise<ReviewFile> {\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Seeded random number generator (Fisher-Yates shuffle with LCG)\n */\nfunction seededShuffle<T>(array: T[], seed: number): T[] {\n const result = [...array];\n let currentSeed = seed;\n\n // Linear congruential generator\n function random(): number {\n currentSeed = (currentSeed * 1664525 + 1013904223) % 4294967296;\n return currentSeed / 4294967296;\n }\n\n // Fisher-Yates shuffle\n for (let i = result.length - 1; i > 0; i--) {\n const j = Math.floor(random() * (i + 1));\n [result[i], result[j]] = [result[j]!, result[i]!];\n }\n\n return result;\n}\n\n/**\n * Get the best identifier for an article (doi > pmid > scopusId > arxivId > ericId > title)\n */\nfunction getArticleId(article: ArticleEntry): string {\n if (article.doi) return article.doi;\n if (article.pmid) return article.pmid;\n if (article.scopusId) return article.scopusId;\n if (article.arxivId) return article.arxivId;\n if (article.ericId) return article.ericId;\n return article.title;\n}\n\nfunction getBasisGuidanceComment(basis: ReviewBasis): string {\n const schemaLine = '# yaml-language-server: $schema=./review.schema.json';\n switch (basis) {\n case 'title':\n return [\n schemaLine,\n '# Screening by title only.',\n '# Mark clearly irrelevant items as \"exclude\" with a comment explaining the reason.',\n '# Leave everything else as \"uncertain\".',\n '',\n ].join('\\n');\n case 'abstract':\n return [\n schemaLine,\n '# Screening by title and abstract.',\n '# You should be able to decide \"include\" or \"exclude\" for most items at this stage.',\n '# Mark remaining ambiguous items as \"uncertain\" with a comment explaining why.',\n '',\n ].join('\\n');\n case 'fulltext':\n return [\n schemaLine,\n '# Screening by full text. This is the final decision stage.',\n '# Decide \"include\" or \"exclude\" for each item.',\n '# Use \"uncertain\" only when absolutely unavoidable, with a comment explaining why.',\n '',\n ].join('\\n');\n }\n}\n\nfunction getDecisionInlineComment(basis: ReviewBasis): string {\n switch (basis) {\n case 'title':\n return '# exclude / uncertain';\n case 'abstract':\n case 'fulltext':\n return '# include / exclude / uncertain';\n }\n}\n\n/** Build a screening article for --basis mode: only include fields relevant to the basis */\nfunction buildScreeningArticle(article: ArticleEntry, basis: ReviewBasis): ArticleEntry {\n // Start with identifiers\n const result: ArticleEntry = { title: article.title, reviews: [] };\n if (article.doi) result.doi = article.doi;\n if (article.pmid) result.pmid = article.pmid;\n if (article.scopusId) result.scopusId = article.scopusId;\n if (article.arxivId) result.arxivId = article.arxivId;\n if (article.ericId) result.ericId = article.ericId;\n\n // Include abstract for abstract and fulltext basis\n if ((basis === 'abstract' || basis === 'fulltext') && article.abstract) {\n result.abstract = article.abstract;\n }\n\n // Include fulltext ref for fulltext basis\n if (basis === 'fulltext' && article.fulltext) {\n result.fulltext = article.fulltext;\n }\n\n // Pre-populate reviews (reviewer omitted; filled from top-level field on merge)\n result.reviews = [{ decision: 'uncertain' as const, comment: '' } as ArticleEntry['reviews'][0]];\n\n return result;\n}\n\n/** Build a finalize article with reviewHistory and finalDecision, optionally scoped by basis */\nfunction buildFinalizeArticle(article: ArticleEntry, basis?: ReviewBasis): ArticleEntry {\n const result: ArticleEntry = { title: article.title, reviews: [] };\n\n // Always include identifiers\n if (article.doi) result.doi = article.doi;\n if (article.pmid) result.pmid = article.pmid;\n if (article.scopusId) result.scopusId = article.scopusId;\n if (article.arxivId) result.arxivId = article.arxivId;\n if (article.ericId) result.ericId = article.ericId;\n\n // Always include bibliographic metadata\n if (article.authors) result.authors = article.authors;\n if (article.year) result.year = article.year;\n\n // Scope content by basis (or include all if no basis)\n if (!basis || basis === 'abstract' || basis === 'fulltext') {\n if (article.abstract) result.abstract = article.abstract;\n }\n if (!basis || basis === 'fulltext') {\n if (article.fulltext) result.fulltext = article.fulltext;\n }\n\n // Add reviewHistory (existing reviews, read-only)\n result.reviewHistory = article.reviews ?? [];\n\n // Empty reviews for new reviews\n result.reviews = [];\n\n // Null finalDecision as placeholder\n result.finalDecision = null;\n\n return result;\n}\n\nfunction getFinalDecisionGuidanceComment(): string {\n return [\n '# yaml-language-server: $schema=./review.schema.json',\n '# Final decision file: set finalDecision on each article',\n '# Valid decisions: include / exclude / null',\n '',\n ].join('\\n');\n}\n\n/**\n * Sort articles based on sort option\n */\nfunction sortArticles(articles: ArticleEntry[], sort: SortOption, seed?: number): ArticleEntry[] {\n switch (sort) {\n case 'year':\n return [...articles].sort((a, b) => {\n const yearA = a.year ?? '';\n const yearB = b.year ?? '';\n return yearA.localeCompare(yearB);\n });\n case 'title':\n return [...articles].sort((a, b) => a.title.localeCompare(b.title));\n case 'random':\n return seededShuffle(articles, seed ?? Date.now());\n case 'none':\n default:\n return articles;\n }\n}\n\n/**\n * Validate the name parameter for extract\n */\nexport function validateName(name: string): void {\n if (!name || name.trim() === '') {\n throw new Error('--name must not be empty');\n }\n if (name.includes('/') || name.includes('\\\\')) {\n throw new Error(`--name must not contain path separators: \"${name}\"`);\n }\n if (name.includes('..')) {\n throw new Error(`--name must not contain \"..\": \"${name}\"`);\n }\n}\n\n/**\n * Execute review extract command\n */\nexport async function executeReviewExtract(\n options: ReviewExtractOptions,\n sessionsDir: string\n): Promise<ReviewExtractResult> {\n validateName(options.name);\n\n const sessionDir = join(sessionsDir, options.sessionId);\n const outputPath = join(sessionDir, 'for-review', options.name, 'review.yaml');\n const reviewFile = await loadReviewFile(sessionDir);\n\n // Filter articles by status\n const reviewers = reviewFile.reviewers;\n let filtered: ArticleEntry[];\n if (options.filter && options.filter.length > 0) {\n filtered = reviewFile.articles.filter((article) => {\n const status = classifyStatus(article, reviewers);\n return options.filter!.includes(status);\n });\n } else {\n filtered = [...reviewFile.articles];\n }\n\n const totalMatching = filtered.length;\n\n // Sort articles\n const sorted = sortArticles(filtered, options.sort ?? 'none', options.seed);\n\n // Apply pagination\n let paginated = sorted;\n if (options.offset !== undefined && options.offset > 0) {\n paginated = paginated.slice(options.offset);\n }\n if (options.limit !== undefined && options.limit > 0) {\n paginated = paginated.slice(0, options.limit);\n }\n\n if (!options.reviewer) {\n throw new Error('--reviewer is required for review file extract');\n }\n\n let finalContent: string;\n\n if (options.basis && !options.finalize) {\n // Screening mode: basis-scoped content with pre-populated reviews\n const outputFile: ReviewFile = {\n sessionId: options.sessionId,\n basis: options.basis,\n reviewer: options.reviewer,\n articles: paginated.map((article) => buildScreeningArticle(article, options.basis!)),\n };\n\n const yamlContent = stringifyYaml(outputFile, { lineWidth: 0 });\n\n // Add decision inline comments\n const decisionComment = getDecisionInlineComment(options.basis);\n const yamlWithComments = yamlContent.replace(\n /^(\\s*-?\\s*)decision: uncertain$/gm,\n `$1decision: uncertain ${decisionComment}`\n );\n\n const guidanceComment = getBasisGuidanceComment(options.basis);\n finalContent = guidanceComment + yamlWithComments;\n } else {\n // Final decision mode: --finalize, or no --basis (backward compat)\n const outputFile: ReviewFile = {\n sessionId: options.sessionId,\n reviewer: options.reviewer,\n articles: paginated.map((article) => buildFinalizeArticle(article, options.basis)),\n };\n\n const yamlContent = stringifyYaml(outputFile, { lineWidth: 0 });\n\n // Replace finalDecision: null with a commented placeholder for user guidance\n const yamlWithComments = yamlContent.replace(\n /^(\\s*)finalDecision: null$/gm,\n '$1finalDecision: # include / exclude'\n );\n\n const guidanceComment = getFinalDecisionGuidanceComment();\n finalContent = guidanceComment + yamlWithComments;\n }\n\n // Ensure output directory exists\n const outputDir = dirname(outputPath);\n await mkdir(outputDir, { recursive: true });\n\n // Write output YAML\n await writeFile(outputPath, finalContent, 'utf-8');\n\n // Copy schema file to output directory if it exists\n const schemasDir = join(dirname(sessionsDir), '.search-hub', 'schemas');\n const schemaSourcePath = join(schemasDir, 'review.schema.json');\n const schemaDestPath = join(outputDir, 'review.schema.json');\n\n try {\n await access(schemaSourcePath);\n await copyFile(schemaSourcePath, schemaDestPath);\n } catch {\n // Schema file doesn't exist, skip copying\n }\n\n return {\n outputPath,\n extractedCount: paginated.length,\n totalMatching,\n };\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":"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"}
1
+ {"version":3,"file":"extract.js","sources":["../../../../src/cli/commands/review/extract.ts"],"sourcesContent":["/**\n * review extract command - Extract subset of articles for distributed review\n */\n\nimport { join, dirname } from 'node:path';\nimport { readFile, writeFile, mkdir, copyFile, access } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ArticleEntry, type ReviewStatus, type ReviewBasis } from './types.js';\n\nexport type SortOption = 'year' | 'title' | 'random' | 'none';\n\nexport interface ReviewExtractOptions {\n sessionId: string;\n filter?: ReviewStatus[];\n sort?: SortOption;\n seed?: number;\n limit?: number;\n offset?: number;\n /** Basis for the review (title, abstract, fulltext). When specified, outputs screening format. */\n basis?: ReviewBasis;\n /** Reviewer identifier (e.g., \"ai:claude\"). Required for all extract modes. */\n reviewer?: string;\n /** Name for the review subset (output goes to for-review/<name>/review.yaml) */\n name: string;\n /** When true, outputs final decision format with reviewHistory and finalDecision fields. */\n finalize?: boolean;\n}\n\n\nexport interface ReviewExtractResult {\n outputPath: string;\n extractedCount: number;\n totalMatching: number;\n}\n\n/**\n * Load review file from session directory\n */\nasync function loadReviewFile(sessionDir: string): Promise<ReviewFile> {\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Seeded random number generator (Fisher-Yates shuffle with LCG)\n */\nfunction seededShuffle<T>(array: T[], seed: number): T[] {\n const result = [...array];\n let currentSeed = seed;\n\n // Linear congruential generator\n function random(): number {\n currentSeed = (currentSeed * 1664525 + 1013904223) % 4294967296;\n return currentSeed / 4294967296;\n }\n\n // Fisher-Yates shuffle\n for (let i = result.length - 1; i > 0; i--) {\n const j = Math.floor(random() * (i + 1));\n [result[i], result[j]] = [result[j]!, result[i]!];\n }\n\n return result;\n}\n\n/**\n * Get the best identifier for an article (doi > pmid > scopusId > arxivId > ericId > title)\n */\nfunction getArticleId(article: ArticleEntry): string {\n if (article.doi) return article.doi;\n if (article.pmid) return article.pmid;\n if (article.scopusId) return article.scopusId;\n if (article.arxivId) return article.arxivId;\n if (article.ericId) return article.ericId;\n return article.title;\n}\n\nfunction getBasisGuidanceComment(basis: ReviewBasis): string {\n const schemaLine = '# yaml-language-server: $schema=./review.schema.json';\n switch (basis) {\n case 'title':\n return [\n schemaLine,\n '# Screening by title only.',\n '# Mark clearly irrelevant items as \"exclude\" with a comment explaining the reason.',\n '# Leave everything else as \"uncertain\".',\n '',\n ].join('\\n');\n case 'abstract':\n return [\n schemaLine,\n '# Screening by title and abstract.',\n '# You should be able to decide \"include\" or \"exclude\" for most items at this stage.',\n '# Mark remaining ambiguous items as \"uncertain\" with a comment explaining why.',\n '',\n ].join('\\n');\n case 'fulltext':\n return [\n schemaLine,\n '# Screening by full text. This is the final decision stage.',\n '# Decide \"include\" or \"exclude\" for each item.',\n '# Use \"uncertain\" only when absolutely unavoidable, with a comment explaining why.',\n '',\n ].join('\\n');\n }\n}\n\nfunction getDecisionInlineComment(basis: ReviewBasis): string {\n switch (basis) {\n case 'title':\n return '# exclude / uncertain';\n case 'abstract':\n case 'fulltext':\n return '# include / exclude / uncertain';\n }\n}\n\n/** Build a screening article for --basis mode: only include fields relevant to the basis */\nfunction buildScreeningArticle(article: ArticleEntry, basis: ReviewBasis): ArticleEntry {\n // Start with identifiers\n const result: ArticleEntry = { title: article.title, reviews: [] };\n if (article.doi) result.doi = article.doi;\n if (article.pmid) result.pmid = article.pmid;\n if (article.scopusId) result.scopusId = article.scopusId;\n if (article.arxivId) result.arxivId = article.arxivId;\n if (article.ericId) result.ericId = article.ericId;\n\n // Include abstract for abstract and fulltext basis\n if ((basis === 'abstract' || basis === 'fulltext') && article.abstract) {\n result.abstract = article.abstract;\n }\n\n // Include fulltext ref for fulltext basis\n if (basis === 'fulltext' && article.fulltext) {\n result.fulltext = article.fulltext;\n }\n\n // Pre-populate reviews (reviewer omitted; filled from top-level field on merge)\n result.reviews = [{ decision: 'uncertain' as const, comment: '' } as ArticleEntry['reviews'][0]];\n\n return result;\n}\n\n/** Build a finalize article with reviewHistory and finalDecision, optionally scoped by basis */\nfunction buildFinalizeArticle(article: ArticleEntry, basis?: ReviewBasis): ArticleEntry {\n const result: ArticleEntry = { title: article.title, reviews: [] };\n\n // Always include identifiers\n if (article.doi) result.doi = article.doi;\n if (article.pmid) result.pmid = article.pmid;\n if (article.scopusId) result.scopusId = article.scopusId;\n if (article.arxivId) result.arxivId = article.arxivId;\n if (article.ericId) result.ericId = article.ericId;\n\n // Always include bibliographic metadata\n if (article.authors) result.authors = article.authors;\n if (article.year) result.year = article.year;\n\n // Scope content by basis (or include all if no basis)\n if (!basis || basis === 'abstract' || basis === 'fulltext') {\n if (article.abstract) result.abstract = article.abstract;\n }\n if (!basis || basis === 'fulltext') {\n if (article.fulltext) result.fulltext = article.fulltext;\n }\n\n // Add reviewHistory (existing reviews, read-only)\n result.reviewHistory = article.reviews ?? [];\n\n // Empty reviews for new reviews\n result.reviews = [];\n\n // Null finalDecision as placeholder\n result.finalDecision = null;\n\n return result;\n}\n\nfunction getFinalDecisionGuidanceComment(): string {\n return [\n '# yaml-language-server: $schema=./review.schema.json',\n '# Final decision file: set finalDecision on each article',\n '# Valid decisions: include / exclude / null',\n '',\n ].join('\\n');\n}\n\n/**\n * Sort articles based on sort option\n */\nfunction sortArticles(articles: ArticleEntry[], sort: SortOption, seed?: number): ArticleEntry[] {\n switch (sort) {\n case 'year':\n return [...articles].sort((a, b) => {\n const yearA = a.year ?? '';\n const yearB = b.year ?? '';\n return yearA.localeCompare(yearB);\n });\n case 'title':\n return [...articles].sort((a, b) => a.title.localeCompare(b.title));\n case 'random':\n return seededShuffle(articles, seed ?? Date.now());\n case 'none':\n default:\n return articles;\n }\n}\n\n/**\n * Validate the name parameter for extract\n */\nexport function validateName(name: string): void {\n if (!name || name.trim() === '') {\n throw new Error('--name must not be empty');\n }\n if (name.includes('/') || name.includes('\\\\')) {\n throw new Error(`--name must not contain path separators: \"${name}\"`);\n }\n if (name.includes('..')) {\n throw new Error(`--name must not contain \"..\": \"${name}\"`);\n }\n}\n\n/**\n * Execute review extract command\n */\nexport async function executeReviewExtract(\n options: ReviewExtractOptions,\n sessionsDir: string\n): Promise<ReviewExtractResult> {\n validateName(options.name);\n\n const sessionDir = join(sessionsDir, options.sessionId);\n const outputPath = join(sessionDir, 'for-review', options.name, 'review.yaml');\n const reviewFile = await loadReviewFile(sessionDir);\n\n // Filter articles by status\n const reviewers = reviewFile.reviewers;\n let filtered: ArticleEntry[];\n if (options.filter && options.filter.length > 0) {\n filtered = reviewFile.articles.filter((article) => {\n const status = classifyStatus(article, reviewers);\n return options.filter!.includes(status);\n });\n } else {\n filtered = [...reviewFile.articles];\n }\n\n const totalMatching = filtered.length;\n\n // Sort articles\n const sorted = sortArticles(filtered, options.sort ?? 'none', options.seed);\n\n // Apply pagination\n let paginated = sorted;\n if (options.offset !== undefined && options.offset > 0) {\n paginated = paginated.slice(options.offset);\n }\n if (options.limit !== undefined && options.limit > 0) {\n paginated = paginated.slice(0, options.limit);\n }\n\n if (!options.reviewer) {\n throw new Error('--reviewer is required for review file extract');\n }\n\n let finalContent: string;\n\n if (options.basis && !options.finalize) {\n // Screening mode: basis-scoped content with pre-populated reviews\n const outputFile: ReviewFile = {\n sessionId: options.sessionId,\n basis: options.basis,\n reviewer: options.reviewer,\n articles: paginated.map((article) => buildScreeningArticle(article, options.basis!)),\n };\n\n const yamlContent = stringifyYaml(outputFile, { lineWidth: 0 });\n\n // Add decision inline comments\n const decisionComment = getDecisionInlineComment(options.basis);\n const yamlWithComments = yamlContent.replace(\n /^(\\s*-?\\s*)decision: uncertain$/gm,\n `$1decision: uncertain ${decisionComment}`\n );\n\n const guidanceComment = getBasisGuidanceComment(options.basis);\n finalContent = guidanceComment + yamlWithComments;\n } else {\n // Final decision mode: --finalize, or no --basis (backward compat)\n const outputFile: ReviewFile = {\n sessionId: options.sessionId,\n reviewer: options.reviewer,\n articles: paginated.map((article) => buildFinalizeArticle(article, options.basis)),\n };\n\n const yamlContent = stringifyYaml(outputFile, { lineWidth: 0 });\n\n // Replace finalDecision: null with a commented placeholder for user guidance\n const yamlWithComments = yamlContent.replace(\n /^(\\s*)finalDecision: null$/gm,\n '$1finalDecision: # include / exclude'\n );\n\n const guidanceComment = getFinalDecisionGuidanceComment();\n finalContent = guidanceComment + yamlWithComments;\n }\n\n // Ensure output directory exists\n const outputDir = dirname(outputPath);\n await mkdir(outputDir, { recursive: true });\n\n // Write output YAML\n await writeFile(outputPath, finalContent, 'utf-8');\n\n // Copy schema file to output directory if it exists\n const schemaSourcePath = join(sessionDir, '.internal', 'review.schema.json');\n const schemaDestPath = join(outputDir, 'review.schema.json');\n\n try {\n await access(schemaSourcePath);\n await copyFile(schemaSourcePath, schemaDestPath);\n } catch {\n // Schema file doesn't exist, skip copying\n }\n\n return {\n outputPath,\n extractedCount: paginated.length,\n totalMatching,\n };\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"finalize.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/finalize.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAmC,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEhF,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;CAC/C;AAcD;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,qBAAqB,EAC9B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,CAAC,CAkD/B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7B,MAAM,CA+BR"}
1
+ {"version":3,"file":"finalize.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/finalize.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAmC,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEhF,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;CAC/C;AAcD;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,qBAAqB,EAC9B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,CAAC,CAiD/B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7B,MAAM,CA+BR"}
@@ -48,8 +48,7 @@ async function executeReviewFinalize(options, sessionsDir) {
48
48
  }
49
49
  if (!options.dryRun) {
50
50
  const yamlContent = stringify(reviewFile, { lineWidth: 0 });
51
- const schemaPath = "../../../../.search-hub/schemas/review.schema.json";
52
- const schemaComment = `# yaml-language-server: $schema=${schemaPath}
51
+ const schemaComment = `# yaml-language-server: $schema=./review.schema.json
53
52
  `;
54
53
  await writeFile(reviewsPath, schemaComment + yamlContent, "utf-8");
55
54
  }
@@ -1 +1 @@
1
- {"version":3,"file":"finalize.js","sources":["../../../../src/cli/commands/review/finalize.ts"],"sourcesContent":["/**\n * review finalize command - Auto-set finalDecision for articles with consensus\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ReviewStatus } from './types.js';\n\nexport interface ReviewFinalizeOptions {\n sessionId: string;\n dryRun?: boolean;\n minReviewers?: number;\n}\n\nexport interface ReviewFinalizeResult {\n includedCount: number;\n excludedCount: number;\n skippedByStatus: Record<ReviewStatus, number>;\n}\n\nfunction createEmptySkippedByStatus(): Record<ReviewStatus, number> {\n return {\n pending: 0,\n incomplete: 0,\n uncertain: 0,\n 'agreed-include': 0,\n 'agreed-exclude': 0,\n conflicting: 0,\n finalized: 0,\n };\n}\n\n/**\n * Execute review finalize command\n */\nexport async function executeReviewFinalize(\n options: ReviewFinalizeOptions,\n sessionsDir: string\n): Promise<ReviewFinalizeResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(content) as ReviewFile;\n\n const reviewers = reviewFile.reviewers ?? [];\n const minReviewers = options.minReviewers ?? 1;\n\n const result: ReviewFinalizeResult = {\n includedCount: 0,\n excludedCount: 0,\n skippedByStatus: createEmptySkippedByStatus(),\n };\n\n for (const article of reviewFile.articles) {\n const status = classifyStatus(article, reviewers);\n\n if (status === 'agreed-include' || status === 'agreed-exclude') {\n // Check minimum reviewer count\n const reviews = article.reviews ?? [];\n const uniqueReviewers = new Set(reviews.map((r) => r.reviewer));\n if (uniqueReviewers.size < minReviewers) {\n result.skippedByStatus[status]++;\n continue;\n }\n\n if (!options.dryRun) {\n article.finalDecision = status === 'agreed-include' ? 'include' : 'exclude';\n }\n\n if (status === 'agreed-include') {\n result.includedCount++;\n } else {\n result.excludedCount++;\n }\n } else {\n result.skippedByStatus[status]++;\n }\n }\n\n // Write back if not dry-run\n if (!options.dryRun) {\n const yamlContent = stringifyYaml(reviewFile, { lineWidth: 0 });\n const schemaPath = '../../../../.search-hub/schemas/review.schema.json';\n const schemaComment = `# yaml-language-server: $schema=${schemaPath}\\n`;\n await writeFile(reviewsPath, schemaComment + yamlContent, 'utf-8');\n }\n\n return result;\n}\n\n/**\n * Format finalize result as human-readable string\n */\nexport function formatFinalizeOutput(\n result: ReviewFinalizeResult,\n options?: { dryRun?: boolean }\n): string {\n const lines: string[] = [];\n\n if (options?.dryRun) {\n lines.push('Dry run - no changes made');\n lines.push('');\n }\n\n const total = result.includedCount + result.excludedCount;\n lines.push(`Finalized ${total} articles (${result.includedCount} include, ${result.excludedCount} exclude)`);\n\n // Build skipped summary (only non-zero, non-agreed statuses)\n const skippedParts: string[] = [];\n if (result.skippedByStatus.pending > 0) {\n skippedParts.push(`${result.skippedByStatus.pending} pending`);\n }\n if (result.skippedByStatus.incomplete > 0) {\n skippedParts.push(`${result.skippedByStatus.incomplete} incomplete`);\n }\n if (result.skippedByStatus.uncertain > 0) {\n skippedParts.push(`${result.skippedByStatus.uncertain} uncertain`);\n }\n if (result.skippedByStatus.conflicting > 0) {\n skippedParts.push(`${result.skippedByStatus.conflicting} conflicting`);\n }\n\n if (skippedParts.length > 0) {\n lines.push(`Skipped: ${skippedParts.join(', ')}`);\n }\n\n return lines.join('\\n');\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":";;;;AAqBA,SAAS,6BAA2D;AAClE,SAAO;AAAA,IACL,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,kBAAkB;AAAA,IAClB,aAAa;AAAA,IACb,WAAW;AAAA,EAAA;AAEf;AAKA,eAAsB,sBACpB,SACA,aAC+B;AAC/B,QAAM,aAAa,KAAK,aAAa,QAAQ,SAAS;AACtD,QAAM,cAAc,KAAK,YAAY,aAAa,cAAc;AAChE,QAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,QAAM,aAAaA,MAAU,OAAO;AAEpC,QAAM,YAAY,WAAW,aAAa,CAAA;AAC1C,QAAM,eAAe,QAAQ,gBAAgB;AAE7C,QAAM,SAA+B;AAAA,IACnC,eAAe;AAAA,IACf,eAAe;AAAA,IACf,iBAAiB,2BAAA;AAAA,EAA2B;AAG9C,aAAW,WAAW,WAAW,UAAU;AACzC,UAAM,SAAS,eAAe,SAAS,SAAS;AAEhD,QAAI,WAAW,oBAAoB,WAAW,kBAAkB;AAE9D,YAAM,UAAU,QAAQ,WAAW,CAAA;AACnC,YAAM,kBAAkB,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC;AAC9D,UAAI,gBAAgB,OAAO,cAAc;AACvC,eAAO,gBAAgB,MAAM;AAC7B;AAAA,MACF;AAEA,UAAI,CAAC,QAAQ,QAAQ;AACnB,gBAAQ,gBAAgB,WAAW,mBAAmB,YAAY;AAAA,MACpE;AAEA,UAAI,WAAW,kBAAkB;AAC/B,eAAO;AAAA,MACT,OAAO;AACL,eAAO;AAAA,MACT;AAAA,IACF,OAAO;AACL,aAAO,gBAAgB,MAAM;AAAA,IAC/B;AAAA,EACF;AAGA,MAAI,CAAC,QAAQ,QAAQ;AACnB,UAAM,cAAcC,UAAc,YAAY,EAAE,WAAW,GAAG;AAC9D,UAAM,aAAa;AACnB,UAAM,gBAAgB,mCAAmC,UAAU;AAAA;AACnE,UAAM,UAAU,aAAa,gBAAgB,aAAa,OAAO;AAAA,EACnE;AAEA,SAAO;AACT;AAKO,SAAS,qBACd,QACA,SACQ;AACR,QAAM,QAAkB,CAAA;AAExB,MAAI,SAAS,QAAQ;AACnB,UAAM,KAAK,2BAA2B;AACtC,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,QAAM,QAAQ,OAAO,gBAAgB,OAAO;AAC5C,QAAM,KAAK,aAAa,KAAK,cAAc,OAAO,aAAa,aAAa,OAAO,aAAa,WAAW;AAG3G,QAAM,eAAyB,CAAA;AAC/B,MAAI,OAAO,gBAAgB,UAAU,GAAG;AACtC,iBAAa,KAAK,GAAG,OAAO,gBAAgB,OAAO,UAAU;AAAA,EAC/D;AACA,MAAI,OAAO,gBAAgB,aAAa,GAAG;AACzC,iBAAa,KAAK,GAAG,OAAO,gBAAgB,UAAU,aAAa;AAAA,EACrE;AACA,MAAI,OAAO,gBAAgB,YAAY,GAAG;AACxC,iBAAa,KAAK,GAAG,OAAO,gBAAgB,SAAS,YAAY;AAAA,EACnE;AACA,MAAI,OAAO,gBAAgB,cAAc,GAAG;AAC1C,iBAAa,KAAK,GAAG,OAAO,gBAAgB,WAAW,cAAc;AAAA,EACvE;AAEA,MAAI,aAAa,SAAS,GAAG;AAC3B,UAAM,KAAK,YAAY,aAAa,KAAK,IAAI,CAAC,EAAE;AAAA,EAClD;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
1
+ {"version":3,"file":"finalize.js","sources":["../../../../src/cli/commands/review/finalize.ts"],"sourcesContent":["/**\n * review finalize command - Auto-set finalDecision for articles with consensus\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ReviewStatus } from './types.js';\n\nexport interface ReviewFinalizeOptions {\n sessionId: string;\n dryRun?: boolean;\n minReviewers?: number;\n}\n\nexport interface ReviewFinalizeResult {\n includedCount: number;\n excludedCount: number;\n skippedByStatus: Record<ReviewStatus, number>;\n}\n\nfunction createEmptySkippedByStatus(): Record<ReviewStatus, number> {\n return {\n pending: 0,\n incomplete: 0,\n uncertain: 0,\n 'agreed-include': 0,\n 'agreed-exclude': 0,\n conflicting: 0,\n finalized: 0,\n };\n}\n\n/**\n * Execute review finalize command\n */\nexport async function executeReviewFinalize(\n options: ReviewFinalizeOptions,\n sessionsDir: string\n): Promise<ReviewFinalizeResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(content) as ReviewFile;\n\n const reviewers = reviewFile.reviewers ?? [];\n const minReviewers = options.minReviewers ?? 1;\n\n const result: ReviewFinalizeResult = {\n includedCount: 0,\n excludedCount: 0,\n skippedByStatus: createEmptySkippedByStatus(),\n };\n\n for (const article of reviewFile.articles) {\n const status = classifyStatus(article, reviewers);\n\n if (status === 'agreed-include' || status === 'agreed-exclude') {\n // Check minimum reviewer count\n const reviews = article.reviews ?? [];\n const uniqueReviewers = new Set(reviews.map((r) => r.reviewer));\n if (uniqueReviewers.size < minReviewers) {\n result.skippedByStatus[status]++;\n continue;\n }\n\n if (!options.dryRun) {\n article.finalDecision = status === 'agreed-include' ? 'include' : 'exclude';\n }\n\n if (status === 'agreed-include') {\n result.includedCount++;\n } else {\n result.excludedCount++;\n }\n } else {\n result.skippedByStatus[status]++;\n }\n }\n\n // Write back if not dry-run\n if (!options.dryRun) {\n const yamlContent = stringifyYaml(reviewFile, { lineWidth: 0 });\n const schemaComment = `# yaml-language-server: $schema=./review.schema.json\\n`;\n await writeFile(reviewsPath, schemaComment + yamlContent, 'utf-8');\n }\n\n return result;\n}\n\n/**\n * Format finalize result as human-readable string\n */\nexport function formatFinalizeOutput(\n result: ReviewFinalizeResult,\n options?: { dryRun?: boolean }\n): string {\n const lines: string[] = [];\n\n if (options?.dryRun) {\n lines.push('Dry run - no changes made');\n lines.push('');\n }\n\n const total = result.includedCount + result.excludedCount;\n lines.push(`Finalized ${total} articles (${result.includedCount} include, ${result.excludedCount} exclude)`);\n\n // Build skipped summary (only non-zero, non-agreed statuses)\n const skippedParts: string[] = [];\n if (result.skippedByStatus.pending > 0) {\n skippedParts.push(`${result.skippedByStatus.pending} pending`);\n }\n if (result.skippedByStatus.incomplete > 0) {\n skippedParts.push(`${result.skippedByStatus.incomplete} incomplete`);\n }\n if (result.skippedByStatus.uncertain > 0) {\n skippedParts.push(`${result.skippedByStatus.uncertain} uncertain`);\n }\n if (result.skippedByStatus.conflicting > 0) {\n skippedParts.push(`${result.skippedByStatus.conflicting} conflicting`);\n }\n\n if (skippedParts.length > 0) {\n lines.push(`Skipped: ${skippedParts.join(', ')}`);\n }\n\n return lines.join('\\n');\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":";;;;AAqBA,SAAS,6BAA2D;AAClE,SAAO;AAAA,IACL,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,kBAAkB;AAAA,IAClB,aAAa;AAAA,IACb,WAAW;AAAA,EAAA;AAEf;AAKA,eAAsB,sBACpB,SACA,aAC+B;AAC/B,QAAM,aAAa,KAAK,aAAa,QAAQ,SAAS;AACtD,QAAM,cAAc,KAAK,YAAY,aAAa,cAAc;AAChE,QAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,QAAM,aAAaA,MAAU,OAAO;AAEpC,QAAM,YAAY,WAAW,aAAa,CAAA;AAC1C,QAAM,eAAe,QAAQ,gBAAgB;AAE7C,QAAM,SAA+B;AAAA,IACnC,eAAe;AAAA,IACf,eAAe;AAAA,IACf,iBAAiB,2BAAA;AAAA,EAA2B;AAG9C,aAAW,WAAW,WAAW,UAAU;AACzC,UAAM,SAAS,eAAe,SAAS,SAAS;AAEhD,QAAI,WAAW,oBAAoB,WAAW,kBAAkB;AAE9D,YAAM,UAAU,QAAQ,WAAW,CAAA;AACnC,YAAM,kBAAkB,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC;AAC9D,UAAI,gBAAgB,OAAO,cAAc;AACvC,eAAO,gBAAgB,MAAM;AAC7B;AAAA,MACF;AAEA,UAAI,CAAC,QAAQ,QAAQ;AACnB,gBAAQ,gBAAgB,WAAW,mBAAmB,YAAY;AAAA,MACpE;AAEA,UAAI,WAAW,kBAAkB;AAC/B,eAAO;AAAA,MACT,OAAO;AACL,eAAO;AAAA,MACT;AAAA,IACF,OAAO;AACL,aAAO,gBAAgB,MAAM;AAAA,IAC/B;AAAA,EACF;AAGA,MAAI,CAAC,QAAQ,QAAQ;AACnB,UAAM,cAAcC,UAAc,YAAY,EAAE,WAAW,GAAG;AAC9D,UAAM,gBAAgB;AAAA;AACtB,UAAM,UAAU,aAAa,gBAAgB,aAAa,OAAO;AAAA,EACnE;AAEA,SAAO;AACT;AAKO,SAAS,qBACd,QACA,SACQ;AACR,QAAM,QAAkB,CAAA;AAExB,MAAI,SAAS,QAAQ;AACnB,UAAM,KAAK,2BAA2B;AACtC,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,QAAM,QAAQ,OAAO,gBAAgB,OAAO;AAC5C,QAAM,KAAK,aAAa,KAAK,cAAc,OAAO,aAAa,aAAa,OAAO,aAAa,WAAW;AAG3G,QAAM,eAAyB,CAAA;AAC/B,MAAI,OAAO,gBAAgB,UAAU,GAAG;AACtC,iBAAa,KAAK,GAAG,OAAO,gBAAgB,OAAO,UAAU;AAAA,EAC/D;AACA,MAAI,OAAO,gBAAgB,aAAa,GAAG;AACzC,iBAAa,KAAK,GAAG,OAAO,gBAAgB,UAAU,aAAa;AAAA,EACrE;AACA,MAAI,OAAO,gBAAgB,YAAY,GAAG;AACxC,iBAAa,KAAK,GAAG,OAAO,gBAAgB,SAAS,YAAY;AAAA,EACnE;AACA,MAAI,OAAO,gBAAgB,cAAc,GAAG;AAC1C,iBAAa,KAAK,GAAG,OAAO,gBAAgB,WAAW,cAAc;AAAA,EACvE;AAEA,MAAI,aAAa,SAAS,GAAG;AAC3B,UAAM,KAAK,YAAY,aAAa,KAAK,IAAI,CAAC,EAAE;AAAA,EAClD;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AA+ED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAuF3B"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AA+ED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAmF3B"}