@ncukondo/search-hub 0.9.4 → 0.10.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.
@@ -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,EAAuC,MAAM,YAAY,CAAC;AAE1J,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,wFAAwF;IACxF,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAkED;;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,CAwH9B"}
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,EAAuC,MAAM,YAAY,CAAC;AAE1J,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,wFAAwF;IACxF,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AA4FD;;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,CA2H9B"}
@@ -28,6 +28,31 @@ function getArticleId(article) {
28
28
  if (article.ericId) return article.ericId;
29
29
  return article.title;
30
30
  }
31
+ function getBasisGuidanceComment(basis) {
32
+ switch (basis) {
33
+ case "title":
34
+ return [
35
+ "# Screening by title only.",
36
+ '# Mark clearly irrelevant items as "exclude" with a comment explaining the reason.',
37
+ '# Leave everything else as "uncertain".',
38
+ ""
39
+ ].join("\n");
40
+ case "abstract":
41
+ return [
42
+ "# Screening by title and abstract.",
43
+ '# You should be able to decide "include" or "exclude" for most items at this stage.',
44
+ '# Mark remaining ambiguous items as "uncertain" with a comment explaining why.',
45
+ ""
46
+ ].join("\n");
47
+ case "fulltext":
48
+ return [
49
+ "# Screening by full text. This is the final decision stage.",
50
+ '# Decide "include" or "exclude" for each item.',
51
+ '# Use "uncertain" only when absolutely unavoidable, with a comment explaining why.',
52
+ ""
53
+ ].join("\n");
54
+ }
55
+ }
31
56
  function sortArticles(articles, sort, seed) {
32
57
  switch (sort) {
33
58
  case "year":
@@ -105,7 +130,8 @@ async function executeReviewExtract(options, sessionsDir) {
105
130
  const yamlContent = stringify(workFile, {
106
131
  lineWidth: 0
107
132
  });
108
- finalContent = yamlContent;
133
+ const guidanceComment = getBasisGuidanceComment(options.basis);
134
+ finalContent = guidanceComment + yamlContent;
109
135
  } else {
110
136
  if (!options.reviewer) {
111
137
  throw new Error("--reviewer is required for review file extract");
@@ -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, type WorkFile, type WorkFileArticle } 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). When specified, outputs work file 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}\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\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 let finalContent: string;\n\n // If basis is specified, output work file format\n if (options.basis && options.reviewer) {\n const workFile: WorkFile = {\n sessionId: options.sessionId,\n basis: options.basis,\n reviewer: options.reviewer,\n articles: paginated.map((article) => {\n const workArticle: WorkFileArticle = {\n id: getArticleId(article),\n title: article.title,\n decision: 'uncertain',\n comment: '',\n };\n // Include abstract for abstract and fulltext basis\n if ((options.basis === 'abstract' || options.basis === 'fulltext') && article.abstract) {\n workArticle.abstract = article.abstract;\n }\n // Include fulltext dirName for fulltext basis\n if (options.basis === 'fulltext' && article.fulltext) {\n workArticle.fulltext = article.fulltext.dirName;\n }\n return workArticle;\n }),\n };\n\n const yamlContent = stringifyYaml(workFile, {\n lineWidth: 0,\n });\n finalContent = yamlContent;\n } else {\n if (!options.reviewer) {\n throw new Error('--reviewer is required for review file extract');\n }\n // Build output review file with reviewHistory separation\n const outputFile: ReviewFile = {\n sessionId: options.sessionId,\n ...(options.reviewer && { reviewer: options.reviewer }),\n articles: paginated.map((article) => ({\n ...article,\n reviewHistory: article.reviews ?? [],\n reviews: [],\n finalDecision: null,\n })),\n };\n\n // Generate YAML with schema reference\n const yamlContent = stringifyYaml(outputFile, {\n lineWidth: 0,\n });\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 // Schema reference pointing to adjacent file\n const schemaComment = `# yaml-language-server: $schema=./review.schema.json\\n`;\n finalContent = schemaComment + 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, type WorkFile, type WorkFileArticle } 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). When specified, outputs work file 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}\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 switch (basis) {\n case 'title':\n return [\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 '# 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 '# 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\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 let finalContent: string;\n\n // If basis is specified, output work file format\n if (options.basis && options.reviewer) {\n const workFile: WorkFile = {\n sessionId: options.sessionId,\n basis: options.basis,\n reviewer: options.reviewer,\n articles: paginated.map((article) => {\n const workArticle: WorkFileArticle = {\n id: getArticleId(article),\n title: article.title,\n decision: 'uncertain',\n comment: '',\n };\n // Include abstract for abstract and fulltext basis\n if ((options.basis === 'abstract' || options.basis === 'fulltext') && article.abstract) {\n workArticle.abstract = article.abstract;\n }\n // Include fulltext dirName for fulltext basis\n if (options.basis === 'fulltext' && article.fulltext) {\n workArticle.fulltext = article.fulltext.dirName;\n }\n return workArticle;\n }),\n };\n\n const yamlContent = stringifyYaml(workFile, {\n lineWidth: 0,\n });\n\n // Add basis-specific guidance comment at the top\n const guidanceComment = getBasisGuidanceComment(options.basis);\n finalContent = guidanceComment + yamlContent;\n } else {\n if (!options.reviewer) {\n throw new Error('--reviewer is required for review file extract');\n }\n // Build output review file with reviewHistory separation\n const outputFile: ReviewFile = {\n sessionId: options.sessionId,\n ...(options.reviewer && { reviewer: options.reviewer }),\n articles: paginated.map((article) => ({\n ...article,\n reviewHistory: article.reviews ?? [],\n reviews: [],\n finalDecision: null,\n })),\n };\n\n // Generate YAML with schema reference\n const yamlContent = stringifyYaml(outputFile, {\n lineWidth: 0,\n });\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 // Schema reference pointing to adjacent file\n const schemaComment = `# yaml-language-server: $schema=./review.schema.json\\n`;\n finalContent = schemaComment + 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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"}
@@ -8,6 +8,12 @@ export interface ReviewMergeOptions {
8
8
  export interface ReviewMergeResult {
9
9
  reviewsAdded: number;
10
10
  decisionsSet: number;
11
+ includeCount: number;
12
+ excludeCount: number;
13
+ uncertainCount: number;
14
+ finalDecisionsSet: number;
15
+ finalDecisionsIncludeCount: number;
16
+ finalDecisionsExcludeCount: number;
11
17
  warnings: string[];
12
18
  }
13
19
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/merge.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAkC,WAAW,EAAE,MAAM,YAAY,CAAC;AAkB1F,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAmBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAQ/F;AAqMD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAoC5B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAqBpF"}
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/merge.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAkC,WAAW,EAAE,MAAM,YAAY,CAAC;AAkB1F,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,0BAA0B,EAAE,MAAM,CAAC;IACnC,0BAA0B,EAAE,MAAM,CAAC;IACnC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAmBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAQ/F;AA8ND;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAoC5B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAgDpF"}
@@ -70,6 +70,12 @@ function processWorkFile(workFile, mainFile, options) {
70
70
  const result = {
71
71
  reviewsAdded: 0,
72
72
  decisionsSet: 0,
73
+ includeCount: 0,
74
+ excludeCount: 0,
75
+ uncertainCount: 0,
76
+ finalDecisionsSet: 0,
77
+ finalDecisionsIncludeCount: 0,
78
+ finalDecisionsExcludeCount: 0,
73
79
  warnings: []
74
80
  };
75
81
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -101,6 +107,9 @@ function processWorkFile(workFile, mainFile, options) {
101
107
  mainArticle.reviews.push(review);
102
108
  }
103
109
  result.reviewsAdded++;
110
+ if (workArticle.decision === "include") result.includeCount++;
111
+ else if (workArticle.decision === "exclude") result.excludeCount++;
112
+ else if (workArticle.decision === "uncertain") result.uncertainCount++;
104
113
  }
105
114
  return result;
106
115
  }
@@ -108,6 +117,12 @@ function processReviewFile(extractedFile, mainFile, options) {
108
117
  const result = {
109
118
  reviewsAdded: 0,
110
119
  decisionsSet: 0,
120
+ includeCount: 0,
121
+ excludeCount: 0,
122
+ uncertainCount: 0,
123
+ finalDecisionsSet: 0,
124
+ finalDecisionsIncludeCount: 0,
125
+ finalDecisionsExcludeCount: 0,
111
126
  warnings: []
112
127
  };
113
128
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -139,12 +154,18 @@ function processReviewFile(extractedFile, mainFile, options) {
139
154
  registerReviewer(mainFile, reviewer, basis);
140
155
  }
141
156
  result.reviewsAdded++;
157
+ if (review.decision === "include") result.includeCount++;
158
+ else if (review.decision === "exclude") result.excludeCount++;
159
+ else if (review.decision === "uncertain") result.uncertainCount++;
142
160
  }
143
161
  if (extracted.finalDecision !== void 0 && extracted.finalDecision !== null) {
144
162
  if (!options.dryRun) {
145
163
  mainArticle.finalDecision = extracted.finalDecision;
146
164
  }
147
165
  result.decisionsSet++;
166
+ result.finalDecisionsSet++;
167
+ if (extracted.finalDecision === "include") result.finalDecisionsIncludeCount++;
168
+ else if (extracted.finalDecision === "exclude") result.finalDecisionsExcludeCount++;
148
169
  }
149
170
  }
150
171
  return result;
@@ -182,8 +203,29 @@ function formatMergeOutput(result, dryRun) {
182
203
  lines.push("");
183
204
  }
184
205
  lines.push("Merge Summary:");
185
- lines.push(` Reviews added: ${result.reviewsAdded}`);
186
- lines.push(` Decisions set: ${result.decisionsSet}`);
206
+ if (result.reviewsAdded === 0) {
207
+ lines.push(` Reviews added: 0`);
208
+ } else {
209
+ const parts = [];
210
+ if (result.excludeCount > 0) parts.push(`${result.excludeCount} exclude`);
211
+ if (result.includeCount > 0) parts.push(`${result.includeCount} include`);
212
+ if (result.uncertainCount > 0) parts.push(`${result.uncertainCount} uncertain`);
213
+ if (parts.length > 0) {
214
+ lines.push(` Reviews added: ${result.reviewsAdded} (${parts.join(", ")})`);
215
+ } else {
216
+ lines.push(` Reviews added: ${result.reviewsAdded}`);
217
+ }
218
+ }
219
+ if (result.finalDecisionsSet > 0) {
220
+ const parts = [];
221
+ if (result.finalDecisionsIncludeCount > 0) parts.push(`${result.finalDecisionsIncludeCount} include`);
222
+ if (result.finalDecisionsExcludeCount > 0) parts.push(`${result.finalDecisionsExcludeCount} exclude`);
223
+ if (parts.length > 0) {
224
+ lines.push(` Final decisions set: ${result.finalDecisionsSet} (${parts.join(", ")})`);
225
+ } else {
226
+ lines.push(` Final decisions set: ${result.finalDecisionsSet}`);
227
+ }
228
+ }
187
229
  if (result.warnings.length > 0) {
188
230
  lines.push("");
189
231
  lines.push("Warnings:");
@@ -1 +1 @@
1
- {"version":3,"file":"merge.js","sources":["../../../../src/cli/commands/review/merge.ts"],"sourcesContent":["/**\n * review merge command - Merge edited file back into main reviews.yaml\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { ReviewFile, ArticleEntry, Review, WorkFile, ReviewBasis } from './types.js';\nimport { validateName } from './extract.js';\n\n\n/**\n * Check if a file is a work file (has basis field)\n */\nfunction isWorkFile(file: unknown): file is WorkFile {\n return (\n typeof file === 'object' &&\n file !== null &&\n 'basis' in file &&\n 'reviewer' in file &&\n 'articles' in file &&\n Array.isArray((file as WorkFile).articles)\n );\n}\n\nexport interface ReviewMergeOptions {\n sessionId: string;\n /** Name of the review subset (reads from for-review/<name>/review.yaml) */\n name: string;\n dryRun?: boolean;\n}\n\nexport interface ReviewMergeResult {\n reviewsAdded: number;\n decisionsSet: number;\n warnings: string[];\n}\n\n/**\n * Load review file from path\n */\nasync function loadReviewFile(path: string): Promise<ReviewFile> {\n const content = await readFile(path, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Auto-detect review basis from article data: fulltext > abstract > title\n */\nfunction detectBasis(article: ArticleEntry): ReviewBasis {\n if (article.fulltext) return 'fulltext';\n if (article.abstract) return 'abstract';\n return 'title';\n}\n\n/**\n * Register a reviewer in the review file's reviewers registry.\n * Deduplicates by name+basis pair.\n */\nexport function registerReviewer(reviewFile: ReviewFile, name: string, basis: ReviewBasis): void {\n if (!reviewFile.reviewers) {\n reviewFile.reviewers = [];\n }\n const exists = reviewFile.reviewers.some((r) => r.name === name && r.basis === basis);\n if (!exists) {\n reviewFile.reviewers.push({ name, basis });\n }\n}\n\n/**\n * Match article from extracted file to main file\n */\nfunction findMatchingArticle(\n extracted: ArticleEntry,\n mainArticles: ArticleEntry[]\n): ArticleEntry | undefined {\n // Try matching by various identifiers\n for (const main of mainArticles) {\n if (extracted.pmid && main.pmid && extracted.pmid === main.pmid) {\n return main;\n }\n if (extracted.doi && main.doi && extracted.doi.toLowerCase() === main.doi.toLowerCase()) {\n return main;\n }\n if (extracted.scopusId && main.scopusId && extracted.scopusId === main.scopusId) {\n return main;\n }\n if (extracted.arxivId && main.arxivId && extracted.arxivId === main.arxivId) {\n return main;\n }\n if (extracted.ericId && main.ericId && extracted.ericId === main.ericId) {\n return main;\n }\n }\n return undefined;\n}\n\n/**\n * Match work file article by id to main file\n * The id can be a DOI, PMID, ScopusId, ArxivId, EricId, or title\n */\nfunction findMatchingArticleById(\n id: string,\n mainArticles: ArticleEntry[]\n): ArticleEntry | undefined {\n for (const main of mainArticles) {\n // Match by DOI (case-insensitive)\n if (main.doi && main.doi.toLowerCase() === id.toLowerCase()) {\n return main;\n }\n // Match by PMID\n if (main.pmid && main.pmid === id) {\n return main;\n }\n // Match by ScopusId\n if (main.scopusId && main.scopusId === id) {\n return main;\n }\n // Match by ArxivId\n if (main.arxivId && main.arxivId === id) {\n return main;\n }\n // Match by EricId\n if (main.ericId && main.ericId === id) {\n return main;\n }\n // Match by title (fallback, case-insensitive)\n if (main.title.toLowerCase() === id.toLowerCase()) {\n return main;\n }\n }\n return undefined;\n}\n\n/**\n * Process work file format (with basis/reviewer)\n */\nfunction processWorkFile(\n workFile: WorkFile,\n mainFile: ReviewFile,\n options: ReviewMergeOptions\n): ReviewMergeResult {\n const result: ReviewMergeResult = {\n reviewsAdded: 0,\n decisionsSet: 0,\n warnings: [],\n };\n\n const timestamp = new Date().toISOString();\n\n // Register the reviewer for this work file's basis\n if (!options.dryRun) {\n registerReviewer(mainFile, workFile.reviewer, workFile.basis);\n }\n\n for (const workArticle of workFile.articles) {\n // Skip articles with null decision (not yet reviewed)\n if (workArticle.decision === null) {\n continue;\n }\n\n const mainArticle = findMatchingArticleById(workArticle.id, mainFile.articles);\n\n if (!mainArticle) {\n result.warnings.push(`Article not found in main file: id=\"${workArticle.id}\"`);\n continue;\n }\n\n // Ensure mainArticle.reviews is an array\n if (!mainArticle.reviews) {\n mainArticle.reviews = [];\n }\n\n // Create review from work file article\n const review: Review = {\n reviewer: workFile.reviewer,\n decision: workArticle.decision,\n basis: workFile.basis,\n timestamp,\n };\n\n // Add comment if provided\n if (workArticle.comment) {\n review.comment = workArticle.comment;\n }\n\n if (!options.dryRun) {\n mainArticle.reviews.push(review);\n }\n result.reviewsAdded++;\n }\n\n return result;\n}\n\n/**\n * Process review file format (with reviewHistory separation)\n */\nfunction processReviewFile(\n extractedFile: ReviewFile,\n mainFile: ReviewFile,\n options: ReviewMergeOptions\n): ReviewMergeResult {\n const result: ReviewMergeResult = {\n reviewsAdded: 0,\n decisionsSet: 0,\n warnings: [],\n };\n\n const timestamp = new Date().toISOString();\n const topLevelReviewer = extractedFile.reviewer;\n\n for (const extracted of extractedFile.articles) {\n const mainArticle = findMatchingArticle(extracted, mainFile.articles);\n\n if (!mainArticle) {\n result.warnings.push(`Article not found in main file: \"${extracted.title}\"`);\n continue;\n }\n\n // Merge only reviews[] (reviewHistory is ignored)\n const extractedReviews = extracted.reviews ?? [];\n\n // Ensure mainArticle.reviews is an array\n if (!mainArticle.reviews) {\n mainArticle.reviews = [];\n }\n\n for (const review of extractedReviews) {\n // Fill in reviewer from top-level field if not on individual review\n const reviewer = review.reviewer ?? topLevelReviewer;\n if (!reviewer) {\n throw new Error('reviewer is required: set reviewer on individual review or top-level ReviewFile');\n }\n // Fill in basis from review or auto-detect from article data\n const basis = review.basis ?? detectBasis(extracted);\n\n if (!options.dryRun) {\n const mergedReview: Review = {\n ...review,\n reviewer,\n basis,\n timestamp: review.timestamp ?? timestamp,\n };\n mainArticle.reviews.push(mergedReview);\n\n // Register reviewer\n registerReviewer(mainFile, reviewer, basis);\n }\n result.reviewsAdded++;\n }\n\n // Overwrite finalDecision if set in extracted (null means unset)\n if (extracted.finalDecision !== undefined && extracted.finalDecision !== null) {\n if (!options.dryRun) {\n mainArticle.finalDecision = extracted.finalDecision;\n }\n result.decisionsSet++;\n }\n }\n\n return result;\n}\n\n/**\n * Execute review merge command\n */\nexport async function executeReviewMerge(\n options: ReviewMergeOptions,\n sessionsDir: string\n): Promise<ReviewMergeResult> {\n validateName(options.name);\n const sessionDir = join(sessionsDir, options.sessionId);\n const mainReviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const filePath = join(sessionDir, 'for-review', options.name, 'review.yaml');\n\n // Load both files\n const mainFile = await loadReviewFile(mainReviewsPath);\n const content = await readFile(filePath, 'utf-8');\n const inputFile = parseYaml(content);\n\n let result: ReviewMergeResult;\n\n // Detect file format and process accordingly\n if (isWorkFile(inputFile)) {\n result = processWorkFile(inputFile, mainFile, options);\n } else {\n result = processReviewFile(inputFile as ReviewFile, mainFile, options);\n }\n\n // Write back if not dry-run\n if (!options.dryRun) {\n const yamlContent = stringifyYaml(mainFile, {\n lineWidth: 0,\n });\n\n // Preserve schema reference comment\n // Path from sessions/{id}/.internal/ to .search-hub/schemas/\n const schemaPath = '../../../../.search-hub/schemas/review.schema.json';\n const schemaComment = `# yaml-language-server: $schema=${schemaPath}\\n`;\n const finalContent = schemaComment + yamlContent;\n\n await writeFile(mainReviewsPath, finalContent, 'utf-8');\n }\n\n return result;\n}\n\n/**\n * Format merge result as human-readable string\n */\nexport function formatMergeOutput(result: ReviewMergeResult, dryRun: boolean): string {\n const lines: string[] = [];\n\n if (dryRun) {\n lines.push('Dry run - no changes made');\n lines.push('');\n }\n\n lines.push('Merge Summary:');\n lines.push(` Reviews added: ${result.reviewsAdded}`);\n lines.push(` Decisions set: ${result.decisionsSet}`);\n\n if (result.warnings.length > 0) {\n lines.push('');\n lines.push('Warnings:');\n for (const warning of result.warnings) {\n lines.push(` - ${warning}`);\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":"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"}
1
+ {"version":3,"file":"merge.js","sources":["../../../../src/cli/commands/review/merge.ts"],"sourcesContent":["/**\n * review merge command - Merge edited file back into main reviews.yaml\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { ReviewFile, ArticleEntry, Review, WorkFile, ReviewBasis } from './types.js';\nimport { validateName } from './extract.js';\n\n\n/**\n * Check if a file is a work file (has basis field)\n */\nfunction isWorkFile(file: unknown): file is WorkFile {\n return (\n typeof file === 'object' &&\n file !== null &&\n 'basis' in file &&\n 'reviewer' in file &&\n 'articles' in file &&\n Array.isArray((file as WorkFile).articles)\n );\n}\n\nexport interface ReviewMergeOptions {\n sessionId: string;\n /** Name of the review subset (reads from for-review/<name>/review.yaml) */\n name: string;\n dryRun?: boolean;\n}\n\nexport interface ReviewMergeResult {\n reviewsAdded: number;\n decisionsSet: number;\n includeCount: number;\n excludeCount: number;\n uncertainCount: number;\n finalDecisionsSet: number;\n finalDecisionsIncludeCount: number;\n finalDecisionsExcludeCount: number;\n warnings: string[];\n}\n\n/**\n * Load review file from path\n */\nasync function loadReviewFile(path: string): Promise<ReviewFile> {\n const content = await readFile(path, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Auto-detect review basis from article data: fulltext > abstract > title\n */\nfunction detectBasis(article: ArticleEntry): ReviewBasis {\n if (article.fulltext) return 'fulltext';\n if (article.abstract) return 'abstract';\n return 'title';\n}\n\n/**\n * Register a reviewer in the review file's reviewers registry.\n * Deduplicates by name+basis pair.\n */\nexport function registerReviewer(reviewFile: ReviewFile, name: string, basis: ReviewBasis): void {\n if (!reviewFile.reviewers) {\n reviewFile.reviewers = [];\n }\n const exists = reviewFile.reviewers.some((r) => r.name === name && r.basis === basis);\n if (!exists) {\n reviewFile.reviewers.push({ name, basis });\n }\n}\n\n/**\n * Match article from extracted file to main file\n */\nfunction findMatchingArticle(\n extracted: ArticleEntry,\n mainArticles: ArticleEntry[]\n): ArticleEntry | undefined {\n // Try matching by various identifiers\n for (const main of mainArticles) {\n if (extracted.pmid && main.pmid && extracted.pmid === main.pmid) {\n return main;\n }\n if (extracted.doi && main.doi && extracted.doi.toLowerCase() === main.doi.toLowerCase()) {\n return main;\n }\n if (extracted.scopusId && main.scopusId && extracted.scopusId === main.scopusId) {\n return main;\n }\n if (extracted.arxivId && main.arxivId && extracted.arxivId === main.arxivId) {\n return main;\n }\n if (extracted.ericId && main.ericId && extracted.ericId === main.ericId) {\n return main;\n }\n }\n return undefined;\n}\n\n/**\n * Match work file article by id to main file\n * The id can be a DOI, PMID, ScopusId, ArxivId, EricId, or title\n */\nfunction findMatchingArticleById(\n id: string,\n mainArticles: ArticleEntry[]\n): ArticleEntry | undefined {\n for (const main of mainArticles) {\n // Match by DOI (case-insensitive)\n if (main.doi && main.doi.toLowerCase() === id.toLowerCase()) {\n return main;\n }\n // Match by PMID\n if (main.pmid && main.pmid === id) {\n return main;\n }\n // Match by ScopusId\n if (main.scopusId && main.scopusId === id) {\n return main;\n }\n // Match by ArxivId\n if (main.arxivId && main.arxivId === id) {\n return main;\n }\n // Match by EricId\n if (main.ericId && main.ericId === id) {\n return main;\n }\n // Match by title (fallback, case-insensitive)\n if (main.title.toLowerCase() === id.toLowerCase()) {\n return main;\n }\n }\n return undefined;\n}\n\n/**\n * Process work file format (with basis/reviewer)\n */\nfunction processWorkFile(\n workFile: WorkFile,\n mainFile: ReviewFile,\n options: ReviewMergeOptions\n): ReviewMergeResult {\n const result: ReviewMergeResult = {\n reviewsAdded: 0,\n decisionsSet: 0,\n includeCount: 0,\n excludeCount: 0,\n uncertainCount: 0,\n finalDecisionsSet: 0,\n finalDecisionsIncludeCount: 0,\n finalDecisionsExcludeCount: 0,\n warnings: [],\n };\n\n const timestamp = new Date().toISOString();\n\n // Register the reviewer for this work file's basis\n if (!options.dryRun) {\n registerReviewer(mainFile, workFile.reviewer, workFile.basis);\n }\n\n for (const workArticle of workFile.articles) {\n // Skip articles with null decision (not yet reviewed)\n if (workArticle.decision === null) {\n continue;\n }\n\n const mainArticle = findMatchingArticleById(workArticle.id, mainFile.articles);\n\n if (!mainArticle) {\n result.warnings.push(`Article not found in main file: id=\"${workArticle.id}\"`);\n continue;\n }\n\n // Ensure mainArticle.reviews is an array\n if (!mainArticle.reviews) {\n mainArticle.reviews = [];\n }\n\n // Create review from work file article\n const review: Review = {\n reviewer: workFile.reviewer,\n decision: workArticle.decision,\n basis: workFile.basis,\n timestamp,\n };\n\n // Add comment if provided\n if (workArticle.comment) {\n review.comment = workArticle.comment;\n }\n\n if (!options.dryRun) {\n mainArticle.reviews.push(review);\n }\n result.reviewsAdded++;\n\n // Count decision types\n if (workArticle.decision === 'include') result.includeCount++;\n else if (workArticle.decision === 'exclude') result.excludeCount++;\n else if (workArticle.decision === 'uncertain') result.uncertainCount++;\n }\n\n return result;\n}\n\n/**\n * Process review file format (with reviewHistory separation)\n */\nfunction processReviewFile(\n extractedFile: ReviewFile,\n mainFile: ReviewFile,\n options: ReviewMergeOptions\n): ReviewMergeResult {\n const result: ReviewMergeResult = {\n reviewsAdded: 0,\n decisionsSet: 0,\n includeCount: 0,\n excludeCount: 0,\n uncertainCount: 0,\n finalDecisionsSet: 0,\n finalDecisionsIncludeCount: 0,\n finalDecisionsExcludeCount: 0,\n warnings: [],\n };\n\n const timestamp = new Date().toISOString();\n const topLevelReviewer = extractedFile.reviewer;\n\n for (const extracted of extractedFile.articles) {\n const mainArticle = findMatchingArticle(extracted, mainFile.articles);\n\n if (!mainArticle) {\n result.warnings.push(`Article not found in main file: \"${extracted.title}\"`);\n continue;\n }\n\n // Merge only reviews[] (reviewHistory is ignored)\n const extractedReviews = extracted.reviews ?? [];\n\n // Ensure mainArticle.reviews is an array\n if (!mainArticle.reviews) {\n mainArticle.reviews = [];\n }\n\n for (const review of extractedReviews) {\n // Fill in reviewer from top-level field if not on individual review\n const reviewer = review.reviewer ?? topLevelReviewer;\n if (!reviewer) {\n throw new Error('reviewer is required: set reviewer on individual review or top-level ReviewFile');\n }\n // Fill in basis from review or auto-detect from article data\n const basis = review.basis ?? detectBasis(extracted);\n\n if (!options.dryRun) {\n const mergedReview: Review = {\n ...review,\n reviewer,\n basis,\n timestamp: review.timestamp ?? timestamp,\n };\n mainArticle.reviews.push(mergedReview);\n\n // Register reviewer\n registerReviewer(mainFile, reviewer, basis);\n }\n result.reviewsAdded++;\n\n // Count decision types\n if (review.decision === 'include') result.includeCount++;\n else if (review.decision === 'exclude') result.excludeCount++;\n else if (review.decision === 'uncertain') result.uncertainCount++;\n }\n\n // Overwrite finalDecision if set in extracted (null means unset)\n if (extracted.finalDecision !== undefined && extracted.finalDecision !== null) {\n if (!options.dryRun) {\n mainArticle.finalDecision = extracted.finalDecision;\n }\n result.decisionsSet++;\n result.finalDecisionsSet++;\n if (extracted.finalDecision === 'include') result.finalDecisionsIncludeCount++;\n else if (extracted.finalDecision === 'exclude') result.finalDecisionsExcludeCount++;\n }\n }\n\n return result;\n}\n\n/**\n * Execute review merge command\n */\nexport async function executeReviewMerge(\n options: ReviewMergeOptions,\n sessionsDir: string\n): Promise<ReviewMergeResult> {\n validateName(options.name);\n const sessionDir = join(sessionsDir, options.sessionId);\n const mainReviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const filePath = join(sessionDir, 'for-review', options.name, 'review.yaml');\n\n // Load both files\n const mainFile = await loadReviewFile(mainReviewsPath);\n const content = await readFile(filePath, 'utf-8');\n const inputFile = parseYaml(content);\n\n let result: ReviewMergeResult;\n\n // Detect file format and process accordingly\n if (isWorkFile(inputFile)) {\n result = processWorkFile(inputFile, mainFile, options);\n } else {\n result = processReviewFile(inputFile as ReviewFile, mainFile, options);\n }\n\n // Write back if not dry-run\n if (!options.dryRun) {\n const yamlContent = stringifyYaml(mainFile, {\n lineWidth: 0,\n });\n\n // Preserve schema reference comment\n // Path from sessions/{id}/.internal/ to .search-hub/schemas/\n const schemaPath = '../../../../.search-hub/schemas/review.schema.json';\n const schemaComment = `# yaml-language-server: $schema=${schemaPath}\\n`;\n const finalContent = schemaComment + yamlContent;\n\n await writeFile(mainReviewsPath, finalContent, 'utf-8');\n }\n\n return result;\n}\n\n/**\n * Format merge result as human-readable string\n */\nexport function formatMergeOutput(result: ReviewMergeResult, dryRun: boolean): string {\n const lines: string[] = [];\n\n if (dryRun) {\n lines.push('Dry run - no changes made');\n lines.push('');\n }\n\n lines.push('Merge Summary:');\n\n // Build reviews line with decision breakdown\n if (result.reviewsAdded === 0) {\n lines.push(` Reviews added: 0`);\n } else {\n const parts: string[] = [];\n if (result.excludeCount > 0) parts.push(`${result.excludeCount} exclude`);\n if (result.includeCount > 0) parts.push(`${result.includeCount} include`);\n if (result.uncertainCount > 0) parts.push(`${result.uncertainCount} uncertain`);\n\n if (parts.length > 0) {\n lines.push(` Reviews added: ${result.reviewsAdded} (${parts.join(', ')})`);\n } else {\n lines.push(` Reviews added: ${result.reviewsAdded}`);\n }\n }\n\n // Show final decisions only when some were set\n if (result.finalDecisionsSet > 0) {\n const parts: string[] = [];\n if (result.finalDecisionsIncludeCount > 0) parts.push(`${result.finalDecisionsIncludeCount} include`);\n if (result.finalDecisionsExcludeCount > 0) parts.push(`${result.finalDecisionsExcludeCount} exclude`);\n\n if (parts.length > 0) {\n lines.push(` Final decisions set: ${result.finalDecisionsSet} (${parts.join(', ')})`);\n } else {\n lines.push(` Final decisions set: ${result.finalDecisionsSet}`);\n }\n }\n\n if (result.warnings.length > 0) {\n lines.push('');\n lines.push('Warnings:');\n for (const warning of result.warnings) {\n lines.push(` - ${warning}`);\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":"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"}
@@ -88,6 +88,7 @@ export interface WorkFile {
88
88
  reviewer: string;
89
89
  articles: WorkFileArticle[];
90
90
  }
91
+ export declare function basisRank(basis: ReviewBasis | undefined): number;
91
92
  /**
92
93
  * Review status classification (7-state model)
93
94
  */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/types.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,12 @@
1
+ const BASIS_RANK = {
2
+ title: 1,
3
+ abstract: 2,
4
+ fulltext: 3
5
+ };
6
+ function basisRank(basis) {
7
+ if (basis === void 0) return 0;
8
+ return BASIS_RANK[basis] ?? 0;
9
+ }
1
10
  function classifyStatus(entry, registeredReviewers) {
2
11
  if (entry.finalDecision !== void 0 && entry.finalDecision !== null) {
3
12
  return "finalized";
@@ -15,25 +24,58 @@ function classifyStatus(entry, registeredReviewers) {
15
24
  return "incomplete";
16
25
  }
17
26
  }
18
- const decisions = reviews.filter((r) => r.decision !== void 0).map((r) => r.decision);
19
- if (decisions.length === 0) {
27
+ const reviewsWithDecisions = reviews.filter((r) => r.decision !== void 0);
28
+ if (reviewsWithDecisions.length === 0) {
29
+ return "pending";
30
+ }
31
+ let highestDefinitiveRank = 0;
32
+ for (const r of reviewsWithDecisions) {
33
+ if (r.decision !== "uncertain") {
34
+ highestDefinitiveRank = Math.max(highestDefinitiveRank, basisRank(r.basis));
35
+ }
36
+ }
37
+ const reviewerMap = /* @__PURE__ */ new Map();
38
+ for (const r of reviewsWithDecisions) {
39
+ const rank = basisRank(r.basis);
40
+ const existing = reviewerMap.get(r.reviewer);
41
+ if (!existing) {
42
+ reviewerMap.set(r.reviewer, { decision: r.decision, rank });
43
+ } else {
44
+ if (r.decision !== "uncertain" && existing.decision === "uncertain") {
45
+ reviewerMap.set(r.reviewer, { decision: r.decision, rank });
46
+ } else if (r.decision !== "uncertain" && existing.decision !== "uncertain" && rank > existing.rank) {
47
+ reviewerMap.set(r.reviewer, { decision: r.decision, rank });
48
+ } else if (r.decision === "uncertain" && existing.decision === "uncertain" && rank > existing.rank) {
49
+ reviewerMap.set(r.reviewer, { decision: r.decision, rank });
50
+ }
51
+ }
52
+ }
53
+ const effectiveDecisions = [];
54
+ for (const { decision, rank } of reviewerMap.values()) {
55
+ if (decision === "uncertain" && rank < highestDefinitiveRank) {
56
+ continue;
57
+ }
58
+ effectiveDecisions.push(decision);
59
+ }
60
+ if (effectiveDecisions.length === 0) {
20
61
  return "pending";
21
62
  }
22
- const hasInclude = decisions.includes("include");
23
- const hasExclude = decisions.includes("exclude");
63
+ const hasInclude = effectiveDecisions.includes("include");
64
+ const hasExclude = effectiveDecisions.includes("exclude");
24
65
  if (hasInclude && hasExclude) {
25
66
  return "conflicting";
26
67
  }
27
- const hasUncertain = decisions.includes("uncertain");
68
+ const hasUncertain = effectiveDecisions.includes("uncertain");
28
69
  if (hasUncertain) {
29
70
  return "uncertain";
30
71
  }
31
- if (decisions.every((d) => d === "include")) {
72
+ if (effectiveDecisions.every((d) => d === "include")) {
32
73
  return "agreed-include";
33
74
  }
34
75
  return "agreed-exclude";
35
76
  }
36
77
  export {
78
+ basisRank,
37
79
  classifyStatus
38
80
  };
39
81
  //# sourceMappingURL=types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sources":["../../../../src/cli/commands/review/types.ts"],"sourcesContent":["/**\n * Review workflow types for article assessment tracking\n */\n\nimport type { ArticleFulltextRef } from '../../../fulltext/types.js';\n\nexport type ReviewDecision = 'include' | 'exclude' | 'uncertain';\n\n/**\n * Basis of the review decision (what information was used)\n */\nexport type ReviewBasis = 'title' | 'abstract' | 'fulltext';\n\n/**\n * Individual assessment of an article by a reviewer\n */\nexport interface Review {\n /** Reviewer identifier: \"human:name\" or \"ai:name\" */\n reviewer: string;\n /** Assessment decision */\n decision?: ReviewDecision;\n /** Basis of the decision (what information was used) */\n basis?: ReviewBasis;\n /** Optional comment or reason */\n comment?: string;\n /** ISO 8601 timestamp (optional - auto-assigned on merge if not provided) */\n timestamp?: string;\n}\n\n/**\n * Source information for merged duplicates\n */\nexport interface MergedSource {\n source: string;\n pmid?: string;\n doi?: string;\n scopusId?: string;\n arxivId?: string;\n ericId?: string;\n}\n\n/**\n * Article entry with identifiers, bibliographic info, and reviews\n */\nexport interface ArticleEntry {\n // Identifiers (at least one required for matching)\n doi?: string;\n pmid?: string;\n scopusId?: string;\n arxivId?: string;\n ericId?: string;\n\n // Bibliographic info (for reviewer reference)\n title: string;\n authors?: string;\n year?: string;\n abstract?: string;\n\n // Deduplication tracking\n mergedFrom?: MergedSource[];\n\n // Review data\n reviews: Review[];\n /** Historical reviews (only in extracted ReviewFiles, never in master file) */\n reviewHistory?: Review[];\n finalDecision?: 'include' | 'exclude' | null;\n\n // Fulltext reference (set by fulltext init/sync)\n fulltext?: ArticleFulltextRef;\n}\n\n/**\n * Top-level structure of the reviews.yaml file\n */\nexport interface ReviewerRecord {\n name: string;\n basis: ReviewBasis;\n}\n\nexport interface ReviewFile {\n sessionId: string;\n /** Path to inclusion criteria file */\n criteria?: string;\n /** Reviewer identifier (only in extracted ReviewFiles) */\n reviewer?: string;\n articles: ArticleEntry[];\n /** Registry of reviewers who participated at each basis level */\n reviewers?: ReviewerRecord[];\n}\n\n/**\n * Work file article entry for AI agent workflow\n */\nexport interface WorkFileArticle {\n id: string;\n title: string;\n abstract?: string;\n /** Fulltext directory name (only for fulltext basis) */\n fulltext?: string;\n decision: ReviewDecision | null;\n comment: string;\n}\n\n/**\n * Work file structure for AI agent workflow\n */\nexport interface WorkFile {\n sessionId: string;\n basis: ReviewBasis;\n reviewer: string;\n articles: WorkFileArticle[];\n}\n\n/**\n * Review status classification (7-state model)\n */\nexport type ReviewStatus =\n | 'pending'\n | 'incomplete'\n | 'uncertain'\n | 'agreed-include'\n | 'agreed-exclude'\n | 'conflicting'\n | 'finalized';\n\n/**\n * Classify the review status of an article entry\n *\n * Classification logic (in order):\n * 1. finalDecision set? → finalized\n * 2. No reviews? → pending\n * 3. Registered reviewer missing? → incomplete\n * 4. include AND exclude present? → conflicting\n * 5. Any uncertain? → uncertain\n * 6. All include? → agreed-include\n * 7. All exclude? → agreed-exclude\n */\nexport function classifyStatus(\n entry: ArticleEntry,\n registeredReviewers?: ReviewerRecord[]\n): ReviewStatus {\n // 1. Finalized takes precedence\n if (entry.finalDecision !== undefined && entry.finalDecision !== null) {\n return 'finalized';\n }\n\n // No reviews = pending (reviews can be null from YAML parsing with only comments)\n const reviews = entry.reviews ?? [];\n if (reviews.length === 0) {\n return 'pending';\n }\n\n // 3. Check for incomplete (registered reviewer missing)\n if (registeredReviewers && registeredReviewers.length > 0) {\n const reviewerNames = new Set(reviews.map((r) => r.reviewer));\n const hasAllReviewers = registeredReviewers.every((reg) =>\n reviewerNames.has(reg.name)\n );\n if (!hasAllReviewers) {\n return 'incomplete';\n }\n }\n\n // Get decisions from reviews that have them\n const decisions = reviews\n .filter((r) => r.decision !== undefined)\n .map((r) => r.decision!);\n\n if (decisions.length === 0) {\n // All reviews lack a decision — treat as pending\n return 'pending';\n }\n\n // 4. Check for conflicts: both include and exclude present\n const hasInclude = decisions.includes('include');\n const hasExclude = decisions.includes('exclude');\n if (hasInclude && hasExclude) {\n return 'conflicting';\n }\n\n // 5. Any uncertain?\n const hasUncertain = decisions.includes('uncertain');\n if (hasUncertain) {\n return 'uncertain';\n }\n\n // 6. All include?\n if (decisions.every((d) => d === 'include')) {\n return 'agreed-include';\n }\n\n // 7. All exclude (only remaining possibility after ruling out conflicts and uncertain)\n return 'agreed-exclude';\n}\n"],"names":[],"mappings":"AAyIO,SAAS,eACd,OACA,qBACc;AAEd,MAAI,MAAM,kBAAkB,UAAa,MAAM,kBAAkB,MAAM;AACrE,WAAO;AAAA,EACT;AAGA,QAAM,UAAU,MAAM,WAAW,CAAA;AACjC,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,EACT;AAGA,MAAI,uBAAuB,oBAAoB,SAAS,GAAG;AACzD,UAAM,gBAAgB,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC;AAC5D,UAAM,kBAAkB,oBAAoB;AAAA,MAAM,CAAC,QACjD,cAAc,IAAI,IAAI,IAAI;AAAA,IAAA;AAE5B,QAAI,CAAC,iBAAiB;AACpB,aAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,YAAY,QACf,OAAO,CAAC,MAAM,EAAE,aAAa,MAAS,EACtC,IAAI,CAAC,MAAM,EAAE,QAAS;AAEzB,MAAI,UAAU,WAAW,GAAG;AAE1B,WAAO;AAAA,EACT;AAGA,QAAM,aAAa,UAAU,SAAS,SAAS;AAC/C,QAAM,aAAa,UAAU,SAAS,SAAS;AAC/C,MAAI,cAAc,YAAY;AAC5B,WAAO;AAAA,EACT;AAGA,QAAM,eAAe,UAAU,SAAS,WAAW;AACnD,MAAI,cAAc;AAChB,WAAO;AAAA,EACT;AAGA,MAAI,UAAU,MAAM,CAAC,MAAM,MAAM,SAAS,GAAG;AAC3C,WAAO;AAAA,EACT;AAGA,SAAO;AACT;"}
1
+ {"version":3,"file":"types.js","sources":["../../../../src/cli/commands/review/types.ts"],"sourcesContent":["/**\n * Review workflow types for article assessment tracking\n */\n\nimport type { ArticleFulltextRef } from '../../../fulltext/types.js';\n\nexport type ReviewDecision = 'include' | 'exclude' | 'uncertain';\n\n/**\n * Basis of the review decision (what information was used)\n */\nexport type ReviewBasis = 'title' | 'abstract' | 'fulltext';\n\n/**\n * Individual assessment of an article by a reviewer\n */\nexport interface Review {\n /** Reviewer identifier: \"human:name\" or \"ai:name\" */\n reviewer: string;\n /** Assessment decision */\n decision?: ReviewDecision;\n /** Basis of the decision (what information was used) */\n basis?: ReviewBasis;\n /** Optional comment or reason */\n comment?: string;\n /** ISO 8601 timestamp (optional - auto-assigned on merge if not provided) */\n timestamp?: string;\n}\n\n/**\n * Source information for merged duplicates\n */\nexport interface MergedSource {\n source: string;\n pmid?: string;\n doi?: string;\n scopusId?: string;\n arxivId?: string;\n ericId?: string;\n}\n\n/**\n * Article entry with identifiers, bibliographic info, and reviews\n */\nexport interface ArticleEntry {\n // Identifiers (at least one required for matching)\n doi?: string;\n pmid?: string;\n scopusId?: string;\n arxivId?: string;\n ericId?: string;\n\n // Bibliographic info (for reviewer reference)\n title: string;\n authors?: string;\n year?: string;\n abstract?: string;\n\n // Deduplication tracking\n mergedFrom?: MergedSource[];\n\n // Review data\n reviews: Review[];\n /** Historical reviews (only in extracted ReviewFiles, never in master file) */\n reviewHistory?: Review[];\n finalDecision?: 'include' | 'exclude' | null;\n\n // Fulltext reference (set by fulltext init/sync)\n fulltext?: ArticleFulltextRef;\n}\n\n/**\n * Top-level structure of the reviews.yaml file\n */\nexport interface ReviewerRecord {\n name: string;\n basis: ReviewBasis;\n}\n\nexport interface ReviewFile {\n sessionId: string;\n /** Path to inclusion criteria file */\n criteria?: string;\n /** Reviewer identifier (only in extracted ReviewFiles) */\n reviewer?: string;\n articles: ArticleEntry[];\n /** Registry of reviewers who participated at each basis level */\n reviewers?: ReviewerRecord[];\n}\n\n/**\n * Work file article entry for AI agent workflow\n */\nexport interface WorkFileArticle {\n id: string;\n title: string;\n abstract?: string;\n /** Fulltext directory name (only for fulltext basis) */\n fulltext?: string;\n decision: ReviewDecision | null;\n comment: string;\n}\n\n/**\n * Work file structure for AI agent workflow\n */\nexport interface WorkFile {\n sessionId: string;\n basis: ReviewBasis;\n reviewer: string;\n articles: WorkFileArticle[];\n}\n\n/**\n * Basis priority rank: fulltext > abstract > title > undefined\n */\nconst BASIS_RANK: Record<string, number> = {\n title: 1,\n abstract: 2,\n fulltext: 3,\n};\n\nexport function basisRank(basis: ReviewBasis | undefined): number {\n if (basis === undefined) return 0;\n return BASIS_RANK[basis] ?? 0;\n}\n\n/**\n * Review status classification (7-state model)\n */\nexport type ReviewStatus =\n | 'pending'\n | 'incomplete'\n | 'uncertain'\n | 'agreed-include'\n | 'agreed-exclude'\n | 'conflicting'\n | 'finalized';\n\n/**\n * Classify the review status of an article entry\n *\n * Classification logic (in order):\n * 1. finalDecision set? → finalized\n * 2. No reviews? → pending\n * 3. Registered reviewer missing? → incomplete\n * 4. include AND exclude present? → conflicting\n * 5. Any uncertain? → uncertain\n * 6. All include? → agreed-include\n * 7. All exclude? → agreed-exclude\n */\nexport function classifyStatus(\n entry: ArticleEntry,\n registeredReviewers?: ReviewerRecord[]\n): ReviewStatus {\n // 1. Finalized takes precedence\n if (entry.finalDecision !== undefined && entry.finalDecision !== null) {\n return 'finalized';\n }\n\n // No reviews = pending (reviews can be null from YAML parsing with only comments)\n const reviews = entry.reviews ?? [];\n if (reviews.length === 0) {\n return 'pending';\n }\n\n // 3. Check for incomplete (registered reviewer missing)\n if (registeredReviewers && registeredReviewers.length > 0) {\n const reviewerNames = new Set(reviews.map((r) => r.reviewer));\n const hasAllReviewers = registeredReviewers.every((reg) =>\n reviewerNames.has(reg.name)\n );\n if (!hasAllReviewers) {\n return 'incomplete';\n }\n }\n\n // Get reviews that have decisions\n const reviewsWithDecisions = reviews.filter((r) => r.decision !== undefined);\n\n if (reviewsWithDecisions.length === 0) {\n // All reviews lack a decision — treat as pending\n return 'pending';\n }\n\n // Basis-priority resolution:\n // \"uncertain\" at a lower basis means \"need more info\" (escalate).\n // A definitive decision at a higher basis resolves that uncertainty.\n //\n // Algorithm:\n // 1. Find the highest basis rank among all definitive (include/exclude) reviews\n // 2. For each reviewer, compute their effective decision:\n // - Take their highest-basis definitive decision if they have one\n // - Otherwise, keep uncertain only if their uncertain rank >= highest definitive rank\n // (i.e., no higher-basis definitive exists globally to resolve it)\n // 3. Reviewers whose only reviews are uncertain at a lower basis than the\n // highest global definitive are excluded from consensus (their uncertainty was resolved)\n\n // Find highest definitive basis rank across ALL reviews\n let highestDefinitiveRank = 0;\n for (const r of reviewsWithDecisions) {\n if (r.decision !== 'uncertain') {\n highestDefinitiveRank = Math.max(highestDefinitiveRank, basisRank(r.basis));\n }\n }\n\n // For each reviewer, compute effective decision\n const reviewerMap = new Map<string, { decision: ReviewDecision; rank: number }>();\n for (const r of reviewsWithDecisions) {\n const rank = basisRank(r.basis);\n const existing = reviewerMap.get(r.reviewer);\n if (!existing) {\n reviewerMap.set(r.reviewer, { decision: r.decision!, rank });\n } else {\n // Prefer definitive over uncertain\n if (r.decision !== 'uncertain' && existing.decision === 'uncertain') {\n reviewerMap.set(r.reviewer, { decision: r.decision!, rank });\n } else if (r.decision !== 'uncertain' && existing.decision !== 'uncertain' && rank > existing.rank) {\n // Higher-basis definitive overrides lower-basis definitive\n reviewerMap.set(r.reviewer, { decision: r.decision!, rank });\n } else if (r.decision === 'uncertain' && existing.decision === 'uncertain' && rank > existing.rank) {\n reviewerMap.set(r.reviewer, { decision: r.decision!, rank });\n }\n }\n }\n\n // Collect effective decisions, excluding reviewers whose only decision is\n // uncertain at a lower basis than the highest global definitive\n const effectiveDecisions: ReviewDecision[] = [];\n for (const { decision, rank } of reviewerMap.values()) {\n if (decision === 'uncertain' && rank < highestDefinitiveRank) {\n // This reviewer's uncertainty was resolved by a higher-basis definitive — skip\n continue;\n }\n effectiveDecisions.push(decision);\n }\n\n if (effectiveDecisions.length === 0) {\n return 'pending';\n }\n\n // 4. Check for conflicts: both include and exclude present among effective decisions\n const hasInclude = effectiveDecisions.includes('include');\n const hasExclude = effectiveDecisions.includes('exclude');\n if (hasInclude && hasExclude) {\n return 'conflicting';\n }\n\n // 5. Any effective uncertain?\n const hasUncertain = effectiveDecisions.includes('uncertain');\n if (hasUncertain) {\n return 'uncertain';\n }\n\n // 6. All include?\n if (effectiveDecisions.every((d) => d === 'include')) {\n return 'agreed-include';\n }\n\n // 7. All exclude (only remaining possibility after ruling out conflicts and uncertain)\n return 'agreed-exclude';\n}\n"],"names":[],"mappings":"AAoHA,MAAM,aAAqC;AAAA,EACzC,OAAO;AAAA,EACP,UAAU;AAAA,EACV,UAAU;AACZ;AAEO,SAAS,UAAU,OAAwC;AAChE,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,WAAW,KAAK,KAAK;AAC9B;AA0BO,SAAS,eACd,OACA,qBACc;AAEd,MAAI,MAAM,kBAAkB,UAAa,MAAM,kBAAkB,MAAM;AACrE,WAAO;AAAA,EACT;AAGA,QAAM,UAAU,MAAM,WAAW,CAAA;AACjC,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,EACT;AAGA,MAAI,uBAAuB,oBAAoB,SAAS,GAAG;AACzD,UAAM,gBAAgB,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC;AAC5D,UAAM,kBAAkB,oBAAoB;AAAA,MAAM,CAAC,QACjD,cAAc,IAAI,IAAI,IAAI;AAAA,IAAA;AAE5B,QAAI,CAAC,iBAAiB;AACpB,aAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,uBAAuB,QAAQ,OAAO,CAAC,MAAM,EAAE,aAAa,MAAS;AAE3E,MAAI,qBAAqB,WAAW,GAAG;AAErC,WAAO;AAAA,EACT;AAgBA,MAAI,wBAAwB;AAC5B,aAAW,KAAK,sBAAsB;AACpC,QAAI,EAAE,aAAa,aAAa;AAC9B,8BAAwB,KAAK,IAAI,uBAAuB,UAAU,EAAE,KAAK,CAAC;AAAA,IAC5E;AAAA,EACF;AAGA,QAAM,kCAAkB,IAAA;AACxB,aAAW,KAAK,sBAAsB;AACpC,UAAM,OAAO,UAAU,EAAE,KAAK;AAC9B,UAAM,WAAW,YAAY,IAAI,EAAE,QAAQ;AAC3C,QAAI,CAAC,UAAU;AACb,kBAAY,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,UAAW,MAAM;AAAA,IAC7D,OAAO;AAEL,UAAI,EAAE,aAAa,eAAe,SAAS,aAAa,aAAa;AACnE,oBAAY,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,UAAW,MAAM;AAAA,MAC7D,WAAW,EAAE,aAAa,eAAe,SAAS,aAAa,eAAe,OAAO,SAAS,MAAM;AAElG,oBAAY,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,UAAW,MAAM;AAAA,MAC7D,WAAW,EAAE,aAAa,eAAe,SAAS,aAAa,eAAe,OAAO,SAAS,MAAM;AAClG,oBAAY,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,UAAW,MAAM;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AAIA,QAAM,qBAAuC,CAAA;AAC7C,aAAW,EAAE,UAAU,KAAA,KAAU,YAAY,UAAU;AACrD,QAAI,aAAa,eAAe,OAAO,uBAAuB;AAE5D;AAAA,IACF;AACA,uBAAmB,KAAK,QAAQ;AAAA,EAClC;AAEA,MAAI,mBAAmB,WAAW,GAAG;AACnC,WAAO;AAAA,EACT;AAGA,QAAM,aAAa,mBAAmB,SAAS,SAAS;AACxD,QAAM,aAAa,mBAAmB,SAAS,SAAS;AACxD,MAAI,cAAc,YAAY;AAC5B,WAAO;AAAA,EACT;AAGA,QAAM,eAAe,mBAAmB,SAAS,WAAW;AAC5D,MAAI,cAAc;AAChB,WAAO;AAAA,EACT;AAGA,MAAI,mBAAmB,MAAM,CAAC,MAAM,MAAM,SAAS,GAAG;AACpD,WAAO;AAAA,EACT;AAGA,SAAO;AACT;"}
@@ -1 +1 @@
1
- {"version":3,"file":"search-executor.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/search-executor.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAEV,QAAQ,EACR,YAAY,EAEb,MAAM,+BAA+B,CAAC;AAwBvC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAIrE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACnG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,aAAa,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;CACnD;AAOD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAOhF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,MAAM,GACb,QAAQ,GAAG,IAAI,CA2DjB;AAyCD;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,EAC7B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,UAAO,GAClB,OAAO,CAAC,qBAAqB,CAAC,CA4UhC;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,oBAAoB,EAC7B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,WAAW,EAAE,CAAC,CAuExB;AAGD;;GAEG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,oBAAoB,EAC7B,MAAM,EAAE,MAAM,EACd,SAAS,SAAI,GACZ,OAAO,CAAC,aAAa,EAAE,CAAC,CAoF1B"}
1
+ {"version":3,"file":"search-executor.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/search-executor.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAEV,QAAQ,EACR,YAAY,EAEb,MAAM,+BAA+B,CAAC;AAwBvC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAIrE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACnG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,aAAa,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;CACnD;AAOD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAOhF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,MAAM,GACb,QAAQ,GAAG,IAAI,CA2DjB;AAyCD;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,EAC7B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,UAAO,GAClB,OAAO,CAAC,qBAAqB,CAAC,CAuVhC;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,oBAAoB,EAC7B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,WAAW,EAAE,CAAC,CAuExB;AAGD;;GAEG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,oBAAoB,EAC7B,MAAM,EAAE,MAAM,EACd,SAAS,SAAI,GACZ,OAAO,CAAC,aAAa,EAAE,CAAC,CAoF1B"}
@@ -16,7 +16,7 @@ import { translateQuery as translateQuery$1 } from "../../providers/arxiv/transl
16
16
  import { translateQuery } from "../../providers/scopus/translator.js";
17
17
  import { stringify } from "yaml";
18
18
  import { registerArticles, saveRegistrationRecord } from "../../integration/register.js";
19
- import { buildFailureErrorMessage } from "./search-utils.js";
19
+ import { buildFailureErrorMessage, buildPartialErrorMessage } from "./search-utils.js";
20
20
  import { getConfigDir } from "../../config/paths.js";
21
21
  import { checkRefAvailable } from "../../integration/ref-cli.js";
22
22
  import { convertResultsToYaml, loadResults } from "../../session/results-io.js";
@@ -335,6 +335,15 @@ async function executeSearch(options, sessionsDir, config, showProgress = true)
335
335
  error: buildFailureErrorMessage(results)
336
336
  };
337
337
  }
338
+ if (options.strict && sessionStatus === "partial") {
339
+ return {
340
+ success: false,
341
+ sessionId,
342
+ sessionStatus,
343
+ results,
344
+ error: buildPartialErrorMessage(results)
345
+ };
346
+ }
338
347
  let autoRegisterResult;
339
348
  if (config.integration.reference_manager.enabled && config.integration.reference_manager.auto_register) {
340
349
  const refAvailable = await checkRefAvailable();