@ncukondo/search-hub 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/merge.d.ts +82 -0
- package/dist/cli/commands/merge.d.ts.map +1 -0
- package/dist/cli/commands/merge.js +221 -0
- package/dist/cli/commands/merge.js.map +1 -0
- package/dist/cli/commands/resume.d.ts.map +1 -1
- package/dist/cli/commands/resume.js +7 -0
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/commands/review/extract.d.ts +3 -1
- package/dist/cli/commands/review/extract.d.ts.map +1 -1
- package/dist/cli/commands/review/extract.js +75 -45
- package/dist/cli/commands/review/extract.js.map +1 -1
- package/dist/cli/commands/review/mark.d.ts.map +1 -1
- package/dist/cli/commands/review/mark.js +69 -22
- package/dist/cli/commands/review/mark.js.map +1 -1
- package/dist/cli/commands/review/merge.d.ts.map +1 -1
- package/dist/cli/commands/review/merge.js +8 -2
- package/dist/cli/commands/review/merge.js.map +1 -1
- package/dist/cli/commands/review/types.d.ts +4 -0
- package/dist/cli/commands/review/types.d.ts.map +1 -1
- package/dist/cli/commands/review/types.js +10 -3
- package/dist/cli/commands/review/types.js.map +1 -1
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +143 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/suggestions/rules.d.ts.map +1 -1
- package/dist/cli/suggestions/rules.js +20 -2
- package/dist/cli/suggestions/rules.js.map +1 -1
- package/dist/cli/suggestions/types.d.ts +6 -0
- package/dist/cli/suggestions/types.d.ts.map +1 -1
- package/dist/session/types.d.ts +14 -1
- package/dist/session/types.d.ts.map +1 -1
- package/dist/session/types.js +7 -0
- package/dist/session/types.js.map +1 -0
- package/package.json +1 -1
|
@@ -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\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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"}
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mark.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/mark.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"mark.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/mark.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAsC,MAAM,YAAY,CAAC;AAGrF,MAAM,WAAW,iBAAiB;IAChC,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,wBAAwB;IACxB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAuDD;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA4D7F"}
|
|
@@ -1,36 +1,83 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { parse, stringify } from "yaml";
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
function extractLeadingComments(content) {
|
|
4
|
+
const lines = content.split("\n");
|
|
5
|
+
const commentLines = [];
|
|
6
|
+
for (const line of lines) {
|
|
7
|
+
if (line.startsWith("#") || line.trim() === "") {
|
|
8
|
+
commentLines.push(line);
|
|
9
|
+
} else {
|
|
10
|
+
break;
|
|
11
|
+
}
|
|
8
12
|
}
|
|
9
|
-
return
|
|
13
|
+
if (commentLines.length === 0) return "";
|
|
14
|
+
return commentLines.join("\n") + "\n";
|
|
10
15
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
function isWorkFile(file) {
|
|
17
|
+
if (typeof file !== "object" || file === null || !("basis" in file) || !("articles" in file) || !Array.isArray(file.articles)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const articles = file.articles;
|
|
21
|
+
if (articles.length === 0) return false;
|
|
22
|
+
const first = articles[0];
|
|
23
|
+
return "id" in first && "decision" in first && !("reviews" in first);
|
|
24
|
+
}
|
|
25
|
+
function findArticleByIdentifier(id, articles) {
|
|
26
|
+
for (const article of articles) {
|
|
27
|
+
if (article.doi && article.doi.toLowerCase() === id.toLowerCase()) return article;
|
|
28
|
+
if (article.pmid && article.pmid === id) return article;
|
|
29
|
+
if (article.scopusId && article.scopusId === id) return article;
|
|
30
|
+
if (article.arxivId && article.arxivId === id) return article;
|
|
31
|
+
if (article.ericId && article.ericId === id) return article;
|
|
32
|
+
if (article.title.toLowerCase() === id.toLowerCase()) return article;
|
|
33
|
+
}
|
|
34
|
+
return void 0;
|
|
14
35
|
}
|
|
15
36
|
async function executeReviewMark(options) {
|
|
16
|
-
|
|
37
|
+
if (!options.id || !options.decision) {
|
|
38
|
+
throw new Error("--id and --decision must be specified");
|
|
39
|
+
}
|
|
40
|
+
const rawContent = await readFile(options.file, "utf-8");
|
|
41
|
+
const parsed = parse(rawContent);
|
|
17
42
|
const result = {
|
|
18
43
|
marked: 0,
|
|
19
44
|
warnings: []
|
|
20
45
|
};
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
if (isWorkFile(parsed)) {
|
|
47
|
+
const workFile = parsed;
|
|
48
|
+
const article = workFile.articles.find((a) => a.id === options.id);
|
|
49
|
+
if (!article) {
|
|
50
|
+
throw new Error(`Article not found: ${options.id}`);
|
|
51
|
+
}
|
|
52
|
+
article.decision = options.decision;
|
|
53
|
+
if (options.comment !== void 0) {
|
|
54
|
+
article.comment = options.comment;
|
|
55
|
+
}
|
|
56
|
+
result.marked = 1;
|
|
57
|
+
const yamlContent = stringify(workFile, { lineWidth: 0 });
|
|
58
|
+
const leadingComments = extractLeadingComments(rawContent);
|
|
59
|
+
await writeFile(options.file, leadingComments + yamlContent, "utf-8");
|
|
60
|
+
} else {
|
|
61
|
+
const reviewFile = parsed;
|
|
62
|
+
if (!reviewFile.basis) {
|
|
63
|
+
throw new Error('Invalid review file: missing "basis" field. This command only works with files generated by "review extract --basis".');
|
|
64
|
+
}
|
|
65
|
+
const article = findArticleByIdentifier(options.id, reviewFile.articles);
|
|
66
|
+
if (!article) {
|
|
67
|
+
throw new Error(`Article not found: ${options.id}`);
|
|
68
|
+
}
|
|
69
|
+
if (!article.reviews || article.reviews.length === 0) {
|
|
70
|
+
article.reviews = [{ decision: "uncertain", comment: "" }];
|
|
71
|
+
}
|
|
72
|
+
article.reviews[0].decision = options.decision;
|
|
73
|
+
if (options.comment !== void 0) {
|
|
74
|
+
article.reviews[0].comment = options.comment;
|
|
75
|
+
}
|
|
76
|
+
result.marked = 1;
|
|
77
|
+
const yamlContent = stringify(reviewFile, { lineWidth: 0 });
|
|
78
|
+
const leadingComments = extractLeadingComments(rawContent);
|
|
79
|
+
await writeFile(options.file, leadingComments + yamlContent, "utf-8");
|
|
31
80
|
}
|
|
32
|
-
result.marked = 1;
|
|
33
|
-
await saveWorkFile(options.file, workFile);
|
|
34
81
|
return result;
|
|
35
82
|
}
|
|
36
83
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mark.js","sources":["../../../../src/cli/commands/review/mark.ts"],"sourcesContent":["/**\n * review mark command - Mark decisions in work files\n */\n\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { ReviewDecision, WorkFile } from './types.js';\n\n\nexport interface ReviewMarkOptions {\n /** Path to the work file */\n file: string;\n /** Article ID to mark */\n id?: string;\n /** Decision to apply */\n decision?: ReviewDecision;\n /** Optional comment */\n comment?: string;\n}\n\nexport interface ReviewMarkResult {\n /** Number of articles marked */\n marked: number;\n /** Warnings (e.g., articles not found) */\n warnings: string[];\n}\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"mark.js","sources":["../../../../src/cli/commands/review/mark.ts"],"sourcesContent":["/**\n * review mark command - Mark decisions in review/work files\n */\n\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { ReviewDecision, ReviewFile, WorkFile, ArticleEntry } from './types.js';\n\n\nexport interface ReviewMarkOptions {\n /** Path to the work file */\n file: string;\n /** Article ID to mark */\n id?: string;\n /** Decision to apply */\n decision?: ReviewDecision;\n /** Optional comment */\n comment?: string;\n}\n\nexport interface ReviewMarkResult {\n /** Number of articles marked */\n marked: number;\n /** Warnings (e.g., articles not found) */\n warnings: string[];\n}\n\n/**\n * Extract leading comment lines from YAML content (lines starting with #)\n */\nfunction extractLeadingComments(content: string): string {\n const lines = content.split('\\n');\n const commentLines: string[] = [];\n for (const line of lines) {\n if (line.startsWith('#') || line.trim() === '') {\n commentLines.push(line);\n } else {\n break;\n }\n }\n // Return empty string if no comments found\n if (commentLines.length === 0) return '';\n return commentLines.join('\\n') + '\\n';\n}\n\n/** @deprecated Detects old WorkFile format (flat id/decision fields). Kept for backward compat. */\nfunction isWorkFile(file: unknown): file is WorkFile {\n if (\n typeof file !== 'object' ||\n file === null ||\n !('basis' in file) ||\n !('articles' in file) ||\n !Array.isArray((file as WorkFile).articles)\n ) {\n return false;\n }\n const articles = (file as WorkFile).articles;\n if (articles.length === 0) return false;\n const first = articles[0]!;\n return 'id' in first && 'decision' in first && !('reviews' in first);\n}\n\n/**\n * Find article by identifier (doi, pmid, scopusId, arxivId, ericId, title)\n */\nfunction findArticleByIdentifier(\n id: string,\n articles: ArticleEntry[]\n): ArticleEntry | undefined {\n for (const article of articles) {\n if (article.doi && article.doi.toLowerCase() === id.toLowerCase()) return article;\n if (article.pmid && article.pmid === id) return article;\n if (article.scopusId && article.scopusId === id) return article;\n if (article.arxivId && article.arxivId === id) return article;\n if (article.ericId && article.ericId === id) return article;\n if (article.title.toLowerCase() === id.toLowerCase()) return article;\n }\n return undefined;\n}\n\n/**\n * Execute review mark command\n */\nexport async function executeReviewMark(options: ReviewMarkOptions): Promise<ReviewMarkResult> {\n if (!options.id || !options.decision) {\n throw new Error('--id and --decision must be specified');\n }\n\n const rawContent = await readFile(options.file, 'utf-8');\n const parsed = parseYaml(rawContent);\n\n const result: ReviewMarkResult = {\n marked: 0,\n warnings: [],\n };\n\n if (isWorkFile(parsed)) {\n // Old WorkFile format (backward compat)\n const workFile = parsed as WorkFile;\n const article = workFile.articles.find((a) => a.id === options.id);\n if (!article) {\n throw new Error(`Article not found: ${options.id}`);\n }\n article.decision = options.decision;\n if (options.comment !== undefined) {\n article.comment = options.comment;\n }\n result.marked = 1;\n\n const yamlContent = stringifyYaml(workFile, { lineWidth: 0 });\n const leadingComments = extractLeadingComments(rawContent);\n await writeFile(options.file, leadingComments + yamlContent, 'utf-8');\n } else {\n // New ReviewFile screening format\n const reviewFile = parsed as ReviewFile;\n\n if (!reviewFile.basis) {\n throw new Error('Invalid review file: missing \"basis\" field. This command only works with files generated by \"review extract --basis\".');\n }\n\n const article = findArticleByIdentifier(options.id, reviewFile.articles);\n if (!article) {\n throw new Error(`Article not found: ${options.id}`);\n }\n\n // Ensure reviews array exists with at least one entry\n if (!article.reviews || article.reviews.length === 0) {\n article.reviews = [{ decision: 'uncertain', comment: '' } as ArticleEntry['reviews'][0]];\n }\n\n // Update first review entry\n article.reviews[0]!.decision = options.decision;\n if (options.comment !== undefined) {\n article.reviews[0]!.comment = options.comment;\n }\n result.marked = 1;\n\n const yamlContent = stringifyYaml(reviewFile, { lineWidth: 0 });\n const leadingComments = extractLeadingComments(rawContent);\n await writeFile(options.file, leadingComments + yamlContent, 'utf-8');\n }\n\n return result;\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/merge.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAkC,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/merge.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAkC,WAAW,EAAE,MAAM,YAAY,CAAC;AA0B1F,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,0BAA0B,EAAE,MAAM,CAAC;IACnC,0BAA0B,EAAE,MAAM,CAAC;IACnC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAmBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAQ/F;AA8ND;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAoC5B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAgDpF"}
|
|
@@ -3,7 +3,13 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { parse, stringify } from "yaml";
|
|
4
4
|
import { validateName } from "./extract.js";
|
|
5
5
|
function isWorkFile(file) {
|
|
6
|
-
|
|
6
|
+
if (typeof file !== "object" || file === null || !("basis" in file) || !("reviewer" in file) || !("articles" in file) || !Array.isArray(file.articles)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const articles = file.articles;
|
|
10
|
+
if (articles.length === 0) return false;
|
|
11
|
+
const first = articles[0];
|
|
12
|
+
return "id" in first && "decision" in first && !("reviews" in first);
|
|
7
13
|
}
|
|
8
14
|
async function loadReviewFile(path) {
|
|
9
15
|
const content = await readFile(path, "utf-8");
|
|
@@ -142,7 +148,7 @@ function processReviewFile(extractedFile, mainFile, options) {
|
|
|
142
148
|
if (!reviewer) {
|
|
143
149
|
throw new Error("reviewer is required: set reviewer on individual review or top-level ReviewFile");
|
|
144
150
|
}
|
|
145
|
-
const basis = review.basis ?? detectBasis(extracted);
|
|
151
|
+
const basis = review.basis ?? extractedFile.basis ?? detectBasis(extracted);
|
|
146
152
|
if (!options.dryRun) {
|
|
147
153
|
const mergedReview = {
|
|
148
154
|
...review,
|
|
@@ -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 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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"}
|
|
1
|
+
{"version":3,"file":"merge.js","sources":["../../../../src/cli/commands/review/merge.ts"],"sourcesContent":["/**\n * review merge command - Merge edited file back into main reviews.yaml\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { ReviewFile, ArticleEntry, Review, WorkFile, ReviewBasis } from './types.js';\nimport { validateName } from './extract.js';\n\n\n/**\n * Check if a file is a work file (has basis field)\n */\n/** @deprecated Detects old WorkFile format (flat id/decision fields). Kept for backward compat. */\nfunction isWorkFile(file: unknown): file is WorkFile {\n if (\n typeof file !== 'object' ||\n file === null ||\n !('basis' in file) ||\n !('reviewer' in file) ||\n !('articles' in file) ||\n !Array.isArray((file as WorkFile).articles)\n ) {\n return false;\n }\n // Distinguish from new ReviewFile-with-basis: old WorkFile has articles with flat `id` + `decision` fields\n const articles = (file as WorkFile).articles;\n if (articles.length === 0) return false;\n const first = articles[0]!;\n return 'id' in first && 'decision' in first && !('reviews' in first);\n}\n\nexport interface ReviewMergeOptions {\n sessionId: string;\n /** Name of the review subset (reads from for-review/<name>/review.yaml) */\n name: string;\n dryRun?: boolean;\n}\n\nexport interface ReviewMergeResult {\n reviewsAdded: number;\n decisionsSet: number;\n includeCount: number;\n excludeCount: number;\n uncertainCount: number;\n finalDecisionsSet: number;\n finalDecisionsIncludeCount: number;\n finalDecisionsExcludeCount: number;\n warnings: string[];\n}\n\n/**\n * Load review file from path\n */\nasync function loadReviewFile(path: string): Promise<ReviewFile> {\n const content = await readFile(path, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Auto-detect review basis from article data: fulltext > abstract > title\n */\nfunction detectBasis(article: ArticleEntry): ReviewBasis {\n if (article.fulltext) return 'fulltext';\n if (article.abstract) return 'abstract';\n return 'title';\n}\n\n/**\n * Register a reviewer in the review file's reviewers registry.\n * Deduplicates by name+basis pair.\n */\nexport function registerReviewer(reviewFile: ReviewFile, name: string, basis: ReviewBasis): void {\n if (!reviewFile.reviewers) {\n reviewFile.reviewers = [];\n }\n const exists = reviewFile.reviewers.some((r) => r.name === name && r.basis === basis);\n if (!exists) {\n reviewFile.reviewers.push({ name, basis });\n }\n}\n\n/**\n * Match article from extracted file to main file\n */\nfunction findMatchingArticle(\n extracted: ArticleEntry,\n mainArticles: ArticleEntry[]\n): ArticleEntry | undefined {\n // Try matching by various identifiers\n for (const main of mainArticles) {\n if (extracted.pmid && main.pmid && extracted.pmid === main.pmid) {\n return main;\n }\n if (extracted.doi && main.doi && extracted.doi.toLowerCase() === main.doi.toLowerCase()) {\n return main;\n }\n if (extracted.scopusId && main.scopusId && extracted.scopusId === main.scopusId) {\n return main;\n }\n if (extracted.arxivId && main.arxivId && extracted.arxivId === main.arxivId) {\n return main;\n }\n if (extracted.ericId && main.ericId && extracted.ericId === main.ericId) {\n return main;\n }\n }\n return undefined;\n}\n\n/**\n * Match work file article by id to main file\n * The id can be a DOI, PMID, ScopusId, ArxivId, EricId, or title\n */\nfunction findMatchingArticleById(\n id: string,\n mainArticles: ArticleEntry[]\n): ArticleEntry | undefined {\n for (const main of mainArticles) {\n // Match by DOI (case-insensitive)\n if (main.doi && main.doi.toLowerCase() === id.toLowerCase()) {\n return main;\n }\n // Match by PMID\n if (main.pmid && main.pmid === id) {\n return main;\n }\n // Match by ScopusId\n if (main.scopusId && main.scopusId === id) {\n return main;\n }\n // Match by ArxivId\n if (main.arxivId && main.arxivId === id) {\n return main;\n }\n // Match by EricId\n if (main.ericId && main.ericId === id) {\n return main;\n }\n // Match by title (fallback, case-insensitive)\n if (main.title.toLowerCase() === id.toLowerCase()) {\n return main;\n }\n }\n return undefined;\n}\n\n/**\n * Process work file format (with basis/reviewer)\n */\nfunction processWorkFile(\n workFile: WorkFile,\n mainFile: ReviewFile,\n options: ReviewMergeOptions\n): ReviewMergeResult {\n const result: ReviewMergeResult = {\n reviewsAdded: 0,\n decisionsSet: 0,\n includeCount: 0,\n excludeCount: 0,\n uncertainCount: 0,\n finalDecisionsSet: 0,\n finalDecisionsIncludeCount: 0,\n finalDecisionsExcludeCount: 0,\n warnings: [],\n };\n\n const timestamp = new Date().toISOString();\n\n // Register the reviewer for this work file's basis\n if (!options.dryRun) {\n registerReviewer(mainFile, workFile.reviewer, workFile.basis);\n }\n\n for (const workArticle of workFile.articles) {\n // Skip articles with null decision (not yet reviewed)\n if (workArticle.decision === null) {\n continue;\n }\n\n const mainArticle = findMatchingArticleById(workArticle.id, mainFile.articles);\n\n if (!mainArticle) {\n result.warnings.push(`Article not found in main file: id=\"${workArticle.id}\"`);\n continue;\n }\n\n // Ensure mainArticle.reviews is an array\n if (!mainArticle.reviews) {\n mainArticle.reviews = [];\n }\n\n // Create review from work file article\n const review: Review = {\n reviewer: workFile.reviewer,\n decision: workArticle.decision,\n basis: workFile.basis,\n timestamp,\n };\n\n // Add comment if provided\n if (workArticle.comment) {\n review.comment = workArticle.comment;\n }\n\n if (!options.dryRun) {\n mainArticle.reviews.push(review);\n }\n result.reviewsAdded++;\n\n // Count decision types\n if (workArticle.decision === 'include') result.includeCount++;\n else if (workArticle.decision === 'exclude') result.excludeCount++;\n else if (workArticle.decision === 'uncertain') result.uncertainCount++;\n }\n\n return result;\n}\n\n/**\n * Process review file format (with reviewHistory separation)\n */\nfunction processReviewFile(\n extractedFile: ReviewFile,\n mainFile: ReviewFile,\n options: ReviewMergeOptions\n): ReviewMergeResult {\n const result: ReviewMergeResult = {\n reviewsAdded: 0,\n decisionsSet: 0,\n includeCount: 0,\n excludeCount: 0,\n uncertainCount: 0,\n finalDecisionsSet: 0,\n finalDecisionsIncludeCount: 0,\n finalDecisionsExcludeCount: 0,\n warnings: [],\n };\n\n const timestamp = new Date().toISOString();\n const topLevelReviewer = extractedFile.reviewer;\n\n for (const extracted of extractedFile.articles) {\n const mainArticle = findMatchingArticle(extracted, mainFile.articles);\n\n if (!mainArticle) {\n result.warnings.push(`Article not found in main file: \"${extracted.title}\"`);\n continue;\n }\n\n // Merge only reviews[] (reviewHistory is ignored)\n const extractedReviews = extracted.reviews ?? [];\n\n // Ensure mainArticle.reviews is an array\n if (!mainArticle.reviews) {\n mainArticle.reviews = [];\n }\n\n for (const review of extractedReviews) {\n // Fill in reviewer from top-level field if not on individual review\n const reviewer = review.reviewer ?? topLevelReviewer;\n if (!reviewer) {\n throw new Error('reviewer is required: set reviewer on individual review or top-level ReviewFile');\n }\n // Fill in basis from review, top-level field, or auto-detect from article data\n const basis = review.basis ?? extractedFile.basis ?? detectBasis(extracted);\n\n if (!options.dryRun) {\n const mergedReview: Review = {\n ...review,\n reviewer,\n basis,\n timestamp: review.timestamp ?? timestamp,\n };\n mainArticle.reviews.push(mergedReview);\n\n // Register reviewer\n registerReviewer(mainFile, reviewer, basis);\n }\n result.reviewsAdded++;\n\n // Count decision types\n if (review.decision === 'include') result.includeCount++;\n else if (review.decision === 'exclude') result.excludeCount++;\n else if (review.decision === 'uncertain') result.uncertainCount++;\n }\n\n // Overwrite finalDecision if set in extracted (null means unset)\n if (extracted.finalDecision !== undefined && extracted.finalDecision !== null) {\n if (!options.dryRun) {\n mainArticle.finalDecision = extracted.finalDecision;\n }\n result.decisionsSet++;\n result.finalDecisionsSet++;\n if (extracted.finalDecision === 'include') result.finalDecisionsIncludeCount++;\n else if (extracted.finalDecision === 'exclude') result.finalDecisionsExcludeCount++;\n }\n }\n\n return result;\n}\n\n/**\n * Execute review merge command\n */\nexport async function executeReviewMerge(\n options: ReviewMergeOptions,\n sessionsDir: string\n): Promise<ReviewMergeResult> {\n validateName(options.name);\n const sessionDir = join(sessionsDir, options.sessionId);\n const mainReviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const filePath = join(sessionDir, 'for-review', options.name, 'review.yaml');\n\n // Load both files\n const mainFile = await loadReviewFile(mainReviewsPath);\n const content = await readFile(filePath, 'utf-8');\n const inputFile = parseYaml(content);\n\n let result: ReviewMergeResult;\n\n // Detect file format and process accordingly\n if (isWorkFile(inputFile)) {\n result = processWorkFile(inputFile, mainFile, options);\n } else {\n result = processReviewFile(inputFile as ReviewFile, mainFile, options);\n }\n\n // Write back if not dry-run\n if (!options.dryRun) {\n const yamlContent = stringifyYaml(mainFile, {\n lineWidth: 0,\n });\n\n // Preserve schema reference comment\n // Path from sessions/{id}/.internal/ to .search-hub/schemas/\n const schemaPath = '../../../../.search-hub/schemas/review.schema.json';\n const schemaComment = `# yaml-language-server: $schema=${schemaPath}\\n`;\n const finalContent = schemaComment + yamlContent;\n\n await writeFile(mainReviewsPath, finalContent, 'utf-8');\n }\n\n return result;\n}\n\n/**\n * Format merge result as human-readable string\n */\nexport function formatMergeOutput(result: ReviewMergeResult, dryRun: boolean): string {\n const lines: string[] = [];\n\n if (dryRun) {\n lines.push('Dry run - no changes made');\n lines.push('');\n }\n\n lines.push('Merge Summary:');\n\n // Build reviews line with decision breakdown\n if (result.reviewsAdded === 0) {\n lines.push(` Reviews added: 0`);\n } else {\n const parts: string[] = [];\n if (result.excludeCount > 0) parts.push(`${result.excludeCount} exclude`);\n if (result.includeCount > 0) parts.push(`${result.includeCount} include`);\n if (result.uncertainCount > 0) parts.push(`${result.uncertainCount} uncertain`);\n\n if (parts.length > 0) {\n lines.push(` Reviews added: ${result.reviewsAdded} (${parts.join(', ')})`);\n } else {\n lines.push(` Reviews added: ${result.reviewsAdded}`);\n }\n }\n\n // Show final decisions only when some were set\n if (result.finalDecisionsSet > 0) {\n const parts: string[] = [];\n if (result.finalDecisionsIncludeCount > 0) parts.push(`${result.finalDecisionsIncludeCount} include`);\n if (result.finalDecisionsExcludeCount > 0) parts.push(`${result.finalDecisionsExcludeCount} exclude`);\n\n if (parts.length > 0) {\n lines.push(` Final decisions set: ${result.finalDecisionsSet} (${parts.join(', ')})`);\n } else {\n lines.push(` Final decisions set: ${result.finalDecisionsSet}`);\n }\n }\n\n if (result.warnings.length > 0) {\n lines.push('');\n lines.push('Warnings:');\n for (const warning of result.warnings) {\n lines.push(` - ${warning}`);\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":";;;;AAeA,SAAS,WAAW,MAAiC;AACnD,MACE,OAAO,SAAS,YAChB,SAAS,QACT,EAAE,WAAW,SACb,EAAE,cAAc,SAChB,EAAE,cAAc,SAChB,CAAC,MAAM,QAAS,KAAkB,QAAQ,GAC1C;AACA,WAAO;AAAA,EACT;AAEA,QAAM,WAAY,KAAkB;AACpC,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,QAAM,QAAQ,SAAS,CAAC;AACxB,SAAO,QAAQ,SAAS,cAAc,SAAS,EAAE,aAAa;AAChE;AAwBA,eAAe,eAAe,MAAmC;AAC/D,QAAM,UAAU,MAAM,SAAS,MAAM,OAAO;AAC5C,SAAOA,MAAU,OAAO;AAC1B;AAKA,SAAS,YAAY,SAAoC;AACvD,MAAI,QAAQ,SAAU,QAAO;AAC7B,MAAI,QAAQ,SAAU,QAAO;AAC7B,SAAO;AACT;AAMO,SAAS,iBAAiB,YAAwB,MAAc,OAA0B;AAC/F,MAAI,CAAC,WAAW,WAAW;AACzB,eAAW,YAAY,CAAA;AAAA,EACzB;AACA,QAAM,SAAS,WAAW,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,UAAU,KAAK;AACpF,MAAI,CAAC,QAAQ;AACX,eAAW,UAAU,KAAK,EAAE,MAAM,OAAO;AAAA,EAC3C;AACF;AAKA,SAAS,oBACP,WACA,cAC0B;AAE1B,aAAW,QAAQ,cAAc;AAC/B,QAAI,UAAU,QAAQ,KAAK,QAAQ,UAAU,SAAS,KAAK,MAAM;AAC/D,aAAO;AAAA,IACT;AACA,QAAI,UAAU,OAAO,KAAK,OAAO,UAAU,IAAI,kBAAkB,KAAK,IAAI,YAAA,GAAe;AACvF,aAAO;AAAA,IACT;AACA,QAAI,UAAU,YAAY,KAAK,YAAY,UAAU,aAAa,KAAK,UAAU;AAC/E,aAAO;AAAA,IACT;AACA,QAAI,UAAU,WAAW,KAAK,WAAW,UAAU,YAAY,KAAK,SAAS;AAC3E,aAAO;AAAA,IACT;AACA,QAAI,UAAU,UAAU,KAAK,UAAU,UAAU,WAAW,KAAK,QAAQ;AACvE,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAMA,SAAS,wBACP,IACA,cAC0B;AAC1B,aAAW,QAAQ,cAAc;AAE/B,QAAI,KAAK,OAAO,KAAK,IAAI,kBAAkB,GAAG,eAAe;AAC3D,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,QAAQ,KAAK,SAAS,IAAI;AACjC,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,YAAY,KAAK,aAAa,IAAI;AACzC,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,WAAW,KAAK,YAAY,IAAI;AACvC,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,UAAU,KAAK,WAAW,IAAI;AACrC,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,MAAM,YAAA,MAAkB,GAAG,eAAe;AACjD,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,gBACP,UACA,UACA,SACmB;AACnB,QAAM,SAA4B;AAAA,IAChC,cAAc;AAAA,IACd,cAAc;AAAA,IACd,cAAc;AAAA,IACd,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,mBAAmB;AAAA,IACnB,4BAA4B;AAAA,IAC5B,4BAA4B;AAAA,IAC5B,UAAU,CAAA;AAAA,EAAC;AAGb,QAAM,aAAY,oBAAI,KAAA,GAAO,YAAA;AAG7B,MAAI,CAAC,QAAQ,QAAQ;AACnB,qBAAiB,UAAU,SAAS,UAAU,SAAS,KAAK;AAAA,EAC9D;AAEA,aAAW,eAAe,SAAS,UAAU;AAE3C,QAAI,YAAY,aAAa,MAAM;AACjC;AAAA,IACF;AAEA,UAAM,cAAc,wBAAwB,YAAY,IAAI,SAAS,QAAQ;AAE7E,QAAI,CAAC,aAAa;AAChB,aAAO,SAAS,KAAK,uCAAuC,YAAY,EAAE,GAAG;AAC7E;AAAA,IACF;AAGA,QAAI,CAAC,YAAY,SAAS;AACxB,kBAAY,UAAU,CAAA;AAAA,IACxB;AAGA,UAAM,SAAiB;AAAA,MACrB,UAAU,SAAS;AAAA,MACnB,UAAU,YAAY;AAAA,MACtB,OAAO,SAAS;AAAA,MAChB;AAAA,IAAA;AAIF,QAAI,YAAY,SAAS;AACvB,aAAO,UAAU,YAAY;AAAA,IAC/B;AAEA,QAAI,CAAC,QAAQ,QAAQ;AACnB,kBAAY,QAAQ,KAAK,MAAM;AAAA,IACjC;AACA,WAAO;AAGP,QAAI,YAAY,aAAa,UAAW,QAAO;AAAA,aACtC,YAAY,aAAa,UAAW,QAAO;AAAA,aAC3C,YAAY,aAAa,YAAa,QAAO;AAAA,EACxD;AAEA,SAAO;AACT;AAKA,SAAS,kBACP,eACA,UACA,SACmB;AACnB,QAAM,SAA4B;AAAA,IAChC,cAAc;AAAA,IACd,cAAc;AAAA,IACd,cAAc;AAAA,IACd,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,mBAAmB;AAAA,IACnB,4BAA4B;AAAA,IAC5B,4BAA4B;AAAA,IAC5B,UAAU,CAAA;AAAA,EAAC;AAGb,QAAM,aAAY,oBAAI,KAAA,GAAO,YAAA;AAC7B,QAAM,mBAAmB,cAAc;AAEvC,aAAW,aAAa,cAAc,UAAU;AAC9C,UAAM,cAAc,oBAAoB,WAAW,SAAS,QAAQ;AAEpE,QAAI,CAAC,aAAa;AAChB,aAAO,SAAS,KAAK,oCAAoC,UAAU,KAAK,GAAG;AAC3E;AAAA,IACF;AAGA,UAAM,mBAAmB,UAAU,WAAW,CAAA;AAG9C,QAAI,CAAC,YAAY,SAAS;AACxB,kBAAY,UAAU,CAAA;AAAA,IACxB;AAEA,eAAW,UAAU,kBAAkB;AAErC,YAAM,WAAW,OAAO,YAAY;AACpC,UAAI,CAAC,UAAU;AACb,cAAM,IAAI,MAAM,iFAAiF;AAAA,MACnG;AAEA,YAAM,QAAQ,OAAO,SAAS,cAAc,SAAS,YAAY,SAAS;AAE1E,UAAI,CAAC,QAAQ,QAAQ;AACnB,cAAM,eAAuB;AAAA,UAC3B,GAAG;AAAA,UACH;AAAA,UACA;AAAA,UACA,WAAW,OAAO,aAAa;AAAA,QAAA;AAEjC,oBAAY,QAAQ,KAAK,YAAY;AAGrC,yBAAiB,UAAU,UAAU,KAAK;AAAA,MAC5C;AACA,aAAO;AAGP,UAAI,OAAO,aAAa,UAAW,QAAO;AAAA,eACjC,OAAO,aAAa,UAAW,QAAO;AAAA,eACtC,OAAO,aAAa,YAAa,QAAO;AAAA,IACnD;AAGA,QAAI,UAAU,kBAAkB,UAAa,UAAU,kBAAkB,MAAM;AAC7E,UAAI,CAAC,QAAQ,QAAQ;AACnB,oBAAY,gBAAgB,UAAU;AAAA,MACxC;AACA,aAAO;AACP,aAAO;AACP,UAAI,UAAU,kBAAkB,UAAW,QAAO;AAAA,eACzC,UAAU,kBAAkB,UAAW,QAAO;AAAA,IACzD;AAAA,EACF;AAEA,SAAO;AACT;AAKA,eAAsB,mBACpB,SACA,aAC4B;AAC5B,eAAa,QAAQ,IAAI;AACzB,QAAM,aAAa,KAAK,aAAa,QAAQ,SAAS;AACtD,QAAM,kBAAkB,KAAK,YAAY,aAAa,cAAc;AACpE,QAAM,WAAW,KAAK,YAAY,cAAc,QAAQ,MAAM,aAAa;AAG3E,QAAM,WAAW,MAAM,eAAe,eAAe;AACrD,QAAM,UAAU,MAAM,SAAS,UAAU,OAAO;AAChD,QAAM,YAAYA,MAAU,OAAO;AAEnC,MAAI;AAGJ,MAAI,WAAW,SAAS,GAAG;AACzB,aAAS,gBAAgB,WAAW,UAAU,OAAO;AAAA,EACvD,OAAO;AACL,aAAS,kBAAkB,WAAyB,UAAU,OAAO;AAAA,EACvE;AAGA,MAAI,CAAC,QAAQ,QAAQ;AACnB,UAAM,cAAcC,UAAc,UAAU;AAAA,MAC1C,WAAW;AAAA,IAAA,CACZ;AAID,UAAM,aAAa;AACnB,UAAM,gBAAgB,mCAAmC,UAAU;AAAA;AACnE,UAAM,eAAe,gBAAgB;AAErC,UAAM,UAAU,iBAAiB,cAAc,OAAO;AAAA,EACxD;AAEA,SAAO;AACT;AAKO,SAAS,kBAAkB,QAA2B,QAAyB;AACpF,QAAM,QAAkB,CAAA;AAExB,MAAI,QAAQ;AACV,UAAM,KAAK,2BAA2B;AACtC,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,QAAM,KAAK,gBAAgB;AAG3B,MAAI,OAAO,iBAAiB,GAAG;AAC7B,UAAM,KAAK,oBAAoB;AAAA,EACjC,OAAO;AACL,UAAM,QAAkB,CAAA;AACxB,QAAI,OAAO,eAAe,EAAG,OAAM,KAAK,GAAG,OAAO,YAAY,UAAU;AACxE,QAAI,OAAO,eAAe,EAAG,OAAM,KAAK,GAAG,OAAO,YAAY,UAAU;AACxE,QAAI,OAAO,iBAAiB,EAAG,OAAM,KAAK,GAAG,OAAO,cAAc,YAAY;AAE9E,QAAI,MAAM,SAAS,GAAG;AACpB,YAAM,KAAK,oBAAoB,OAAO,YAAY,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG;AAAA,IAC5E,OAAO;AACL,YAAM,KAAK,oBAAoB,OAAO,YAAY,EAAE;AAAA,IACtD;AAAA,EACF;AAGA,MAAI,OAAO,oBAAoB,GAAG;AAChC,UAAM,QAAkB,CAAA;AACxB,QAAI,OAAO,6BAA6B,EAAG,OAAM,KAAK,GAAG,OAAO,0BAA0B,UAAU;AACpG,QAAI,OAAO,6BAA6B,EAAG,OAAM,KAAK,GAAG,OAAO,0BAA0B,UAAU;AAEpG,QAAI,MAAM,SAAS,GAAG;AACpB,YAAM,KAAK,0BAA0B,OAAO,iBAAiB,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG;AAAA,IACvF,OAAO;AACL,YAAM,KAAK,0BAA0B,OAAO,iBAAiB,EAAE;AAAA,IACjE;AAAA,EACF;AAEA,MAAI,OAAO,SAAS,SAAS,GAAG;AAC9B,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,WAAW;AACtB,eAAW,WAAW,OAAO,UAAU;AACrC,YAAM,KAAK,OAAO,OAAO,EAAE;AAAA,IAC7B;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
|
|
@@ -63,6 +63,8 @@ export interface ReviewFile {
|
|
|
63
63
|
criteria?: string;
|
|
64
64
|
/** Reviewer identifier (only in extracted ReviewFiles) */
|
|
65
65
|
reviewer?: string;
|
|
66
|
+
/** Basis level for screening (only in extracted ReviewFiles) */
|
|
67
|
+
basis?: ReviewBasis;
|
|
66
68
|
articles: ArticleEntry[];
|
|
67
69
|
/** Registry of reviewers who participated at each basis level */
|
|
68
70
|
reviewers?: ReviewerRecord[];
|
|
@@ -70,6 +72,7 @@ export interface ReviewFile {
|
|
|
70
72
|
/**
|
|
71
73
|
* Work file article entry for AI agent workflow
|
|
72
74
|
*/
|
|
75
|
+
/** @deprecated Use ReviewFile format with reviews[] instead. Kept for backward compatibility. */
|
|
73
76
|
export interface WorkFileArticle {
|
|
74
77
|
id: string;
|
|
75
78
|
title: string;
|
|
@@ -82,6 +85,7 @@ export interface WorkFileArticle {
|
|
|
82
85
|
/**
|
|
83
86
|
* Work file structure for AI agent workflow
|
|
84
87
|
*/
|
|
88
|
+
/** @deprecated Use ReviewFile format with basis field instead. Kept for backward compatibility. */
|
|
85
89
|
export interface WorkFile {
|
|
86
90
|
sessionId: string;
|
|
87
91
|
basis: ReviewBasis;
|
|
@@ -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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"}
|
|
@@ -17,10 +17,17 @@ function classifyStatus(entry, registeredReviewers) {
|
|
|
17
17
|
}
|
|
18
18
|
if (registeredReviewers && registeredReviewers.length > 0) {
|
|
19
19
|
const reviewerNames = new Set(reviews.map((r) => r.reviewer));
|
|
20
|
-
|
|
20
|
+
let highestReviewedRank = 0;
|
|
21
|
+
for (const r of reviews) {
|
|
22
|
+
highestReviewedRank = Math.max(highestReviewedRank, basisRank(r.basis));
|
|
23
|
+
}
|
|
24
|
+
const applicableReviewers = highestReviewedRank === 0 ? registeredReviewers : registeredReviewers.filter(
|
|
25
|
+
(reg) => basisRank(reg.basis) <= highestReviewedRank
|
|
26
|
+
);
|
|
27
|
+
const hasAllReviewers = applicableReviewers.every(
|
|
21
28
|
(reg) => reviewerNames.has(reg.name)
|
|
22
29
|
);
|
|
23
|
-
if (!hasAllReviewers) {
|
|
30
|
+
if (applicableReviewers.length > 0 && !hasAllReviewers) {
|
|
24
31
|
return "incomplete";
|
|
25
32
|
}
|
|
26
33
|
}
|
|
@@ -52,7 +59,7 @@ function classifyStatus(entry, registeredReviewers) {
|
|
|
52
59
|
}
|
|
53
60
|
const effectiveDecisions = [];
|
|
54
61
|
for (const { decision, rank } of reviewerMap.values()) {
|
|
55
|
-
if (
|
|
62
|
+
if (rank < highestDefinitiveRank) {
|
|
56
63
|
continue;
|
|
57
64
|
}
|
|
58
65
|
effectiveDecisions.push(decision);
|
|
@@ -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 * 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
|
+
{"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 /** Basis level for screening (only in extracted ReviewFiles) */\n basis?: ReviewBasis;\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 */\n/** @deprecated Use ReviewFile format with reviews[] instead. Kept for backward compatibility. */\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 */\n/** @deprecated Use ReviewFile format with basis field instead. Kept for backward compatibility. */\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 // Only check reviewers whose registered basis ≤ article's highest reviewed basis\n if (registeredReviewers && registeredReviewers.length > 0) {\n const reviewerNames = new Set(reviews.map((r) => r.reviewer));\n let highestReviewedRank = 0;\n for (const r of reviews) {\n highestReviewedRank = Math.max(highestReviewedRank, basisRank(r.basis));\n }\n // When reviews have no basis (legacy), check all registered reviewers\n const applicableReviewers = highestReviewedRank === 0\n ? registeredReviewers\n : registeredReviewers.filter(\n (reg) => basisRank(reg.basis) <= highestReviewedRank\n );\n const hasAllReviewers = applicableReviewers.every((reg) =>\n reviewerNames.has(reg.name)\n );\n if (applicableReviewers.length > 0 && !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 effective decision\n // is at a lower basis than the highest global definitive\n const effectiveDecisions: ReviewDecision[] = [];\n for (const { decision, rank } of reviewerMap.values()) {\n if (rank < highestDefinitiveRank) {\n // This reviewer's decision is at a lower basis than the highest 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":"AAwHA,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;AAIA,MAAI,uBAAuB,oBAAoB,SAAS,GAAG;AACzD,UAAM,gBAAgB,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC;AAC5D,QAAI,sBAAsB;AAC1B,eAAW,KAAK,SAAS;AACvB,4BAAsB,KAAK,IAAI,qBAAqB,UAAU,EAAE,KAAK,CAAC;AAAA,IACxE;AAEA,UAAM,sBAAsB,wBAAwB,IAChD,sBACA,oBAAoB;AAAA,MAClB,CAAC,QAAQ,UAAU,IAAI,KAAK,KAAK;AAAA,IAAA;AAEvC,UAAM,kBAAkB,oBAAoB;AAAA,MAAM,CAAC,QACjD,cAAc,IAAI,IAAI,IAAI;AAAA,IAAA;AAE5B,QAAI,oBAAoB,SAAS,KAAK,CAAC,iBAAiB;AACtD,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,OAAO,uBAAuB;AAEhC;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;"}
|
|
@@ -43,7 +43,7 @@ async function getSessionDetails(sessionId, sessionsDir) {
|
|
|
43
43
|
status: session.summary.status,
|
|
44
44
|
createdAt: session.createdAt,
|
|
45
45
|
updatedAt: session.updatedAt,
|
|
46
|
-
queryFile: session.query
|
|
46
|
+
queryFile: session.query?.file ?? "",
|
|
47
47
|
totalHits: session.summary.totalHits,
|
|
48
48
|
totalRetrieved: session.summary.totalRetrieved,
|
|
49
49
|
databases
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.js","sources":["../../../src/cli/commands/status.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { listSessions, loadSession } from '../../session/manager.js';\nimport { deduplicateArticles } from './export.js';\nimport { loadNotes, formatNotesList, type NoteEntry } from './notes.js';\nimport { loadResults } from '../../session/results-io.js';\nimport type { ProviderName } from '../../providers/base/types.js';\n\nexport interface SessionListItem {\n id: string;\n name: string;\n status: string;\n createdAt: string;\n progress: string;\n}\n\nexport interface DatabaseDetails {\n provider: string;\n status: string;\n totalHits: number;\n retrievedCount: number;\n error?: string;\n}\n\nexport interface SessionDetails {\n id: string;\n name: string;\n description?: string;\n status: string;\n createdAt: string;\n updatedAt: string;\n queryFile: string;\n totalHits: number;\n totalRetrieved: number;\n databases: DatabaseDetails[];\n uniqueArticles?: number;\n duplicatesRemoved?: number;\n notes?: NoteEntry[];\n}\n\nexport interface ListOptions {\n all: boolean;\n}\n\nexport interface FormatOptions {\n json: boolean;\n}\n\nexport interface SessionListResult {\n sessions: SessionListItem[];\n totalCount: number;\n filteredCount: number;\n showingAll: boolean;\n}\n\nexport async function listSessionsForDisplay(\n sessionsDir: string,\n options: ListOptions\n): Promise<SessionListResult> {\n const summaries = await listSessions(sessionsDir);\n\n const filtered = options.all\n ? summaries\n : summaries.filter((s) => s.status !== 'completed');\n\n const sessions = filtered.map((s) => ({\n id: s.id,\n name: s.name,\n status: s.status,\n createdAt: s.createdAt,\n progress: `${s.totalRetrieved}/${s.totalHits}`,\n }));\n\n return {\n sessions,\n totalCount: summaries.length,\n filteredCount: filtered.length,\n showingAll: options.all,\n };\n}\n\nexport async function getSessionDetails(\n sessionId: string,\n sessionsDir: string\n): Promise<{ success: boolean; session?: SessionDetails; error?: string }> {\n try {\n const session = await loadSession(sessionId, sessionsDir);\n\n const databases: DatabaseDetails[] = [];\n for (const [provider, dbStatus] of Object.entries(session.databases)) {\n if (!dbStatus) continue;\n const dbDetail: DatabaseDetails = {\n provider,\n status: dbStatus.status,\n totalHits: dbStatus.totalHits ?? 0,\n retrievedCount: dbStatus.retrievedCount ?? 0,\n };\n if (dbStatus.error?.message) {\n dbDetail.error = dbStatus.error.message;\n }\n databases.push(dbDetail);\n }\n\n const sessionDetails: SessionDetails = {\n id: session.id,\n name: session.name,\n status: session.summary.status,\n createdAt: session.createdAt,\n updatedAt: session.updatedAt,\n queryFile: session.query.file,\n totalHits: session.summary.totalHits,\n totalRetrieved: session.summary.totalRetrieved,\n databases,\n };\n if (session.description) {\n sessionDetails.description = session.description;\n }\n\n // Load notes (optional - don't fail if notes can't be loaded)\n try {\n const sessionDir = join(sessionsDir, sessionId);\n const notes = await loadNotes(sessionDir);\n if (notes.length > 0) {\n sessionDetails.notes = notes;\n }\n } catch {\n // Notes are optional - ignore errors\n }\n\n return {\n success: true,\n session: sessionDetails,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n return {\n success: false,\n error: message,\n };\n }\n}\n\nexport async function computeDeduplicationStats(\n sessionId: string,\n sessionsDir: string,\n session: { databases: Record<string, { files?: { results?: string } } | undefined> }\n): Promise<{ uniqueArticles: number; duplicatesRemoved: number }> {\n const articles: import('../../providers/base/types.js').Article[] = [];\n const sessionDir = join(sessionsDir, sessionId);\n\n for (const [providerName] of Object.entries(session.databases)) {\n const providerArticles = await loadResults(sessionDir, providerName as ProviderName);\n articles.push(...providerArticles);\n }\n\n if (articles.length === 0) {\n return { uniqueArticles: 0, duplicatesRemoved: 0 };\n }\n\n const result = deduplicateArticles(articles);\n return {\n uniqueArticles: result.articles.length,\n duplicatesRemoved: result.duplicatesRemoved,\n };\n}\n\nexport function formatSessionList(\n result: SessionListResult,\n options: FormatOptions\n): string {\n if (options.json) {\n return JSON.stringify(result.sessions, null, 2);\n }\n\n if (result.sessions.length === 0) {\n if (result.totalCount === 0) {\n return 'No sessions found.';\n }\n // Sessions exist but are filtered out (all completed)\n const completedCount = result.totalCount - result.filteredCount;\n return `No active sessions. ${completedCount} completed session${completedCount === 1 ? '' : 's'} hidden (use --all to show).`;\n }\n\n const header = `${'ID'.padEnd(35)} ${'NAME'.padEnd(20)} ${'STATUS'.padEnd(15)} ${'PROGRESS'.padEnd(12)} CREATED`;\n const separator = '-'.repeat(100);\n\n const rows = result.sessions.map((s) => {\n const date = new Date(s.createdAt).toLocaleDateString();\n return `${s.id.padEnd(35)} ${s.name.padEnd(20)} ${s.status.padEnd(15)} ${s.progress.padEnd(12)} ${date}`;\n });\n\n const lines = [header, separator, ...rows];\n\n // Show hint about hidden completed sessions if not showing all\n if (!result.showingAll && result.totalCount > result.filteredCount) {\n const hiddenCount = result.totalCount - result.filteredCount;\n lines.push('');\n lines.push(`(${hiddenCount} completed session${hiddenCount === 1 ? '' : 's'} hidden, use --all to show)`);\n }\n\n return lines.join('\\n');\n}\n\nexport function formatSessionDetails(\n details: SessionDetails,\n options: FormatOptions\n): string {\n if (options.json) {\n return JSON.stringify(details, null, 2);\n }\n\n const lines: string[] = [];\n\n lines.push(`Session: ${details.name}`);\n lines.push(`ID: ${details.id}`);\n if (details.description) {\n lines.push(`Description: ${details.description}`);\n }\n lines.push(`Status: ${details.status}`);\n lines.push(`Query File: ${details.queryFile}`);\n lines.push(`Created: ${new Date(details.createdAt).toLocaleString()}`);\n lines.push(`Updated: ${new Date(details.updatedAt).toLocaleString()}`);\n lines.push('');\n if (details.duplicatesRemoved !== undefined && details.duplicatesRemoved > 0) {\n lines.push(`Total: ${details.totalRetrieved} raw / ${details.uniqueArticles} unique (${details.duplicatesRemoved} duplicates)`);\n } else {\n lines.push(`Total: ${details.totalRetrieved}/${details.totalHits} results`);\n }\n lines.push('');\n lines.push('Databases:');\n\n for (const db of details.databases) {\n const statusIcon = getStatusIcon(db.status);\n let line = ` ${statusIcon} ${db.provider.padEnd(10)} ${db.status.padEnd(12)} ${db.retrievedCount}/${db.totalHits}`;\n if (db.error) {\n line += ` (${db.error})`;\n }\n lines.push(line);\n }\n\n // Display notes if present\n if (details.notes && details.notes.length > 0) {\n lines.push('');\n lines.push('Notes:');\n lines.push(formatNotesList(details.notes));\n }\n\n return lines.join('\\n');\n}\n\nfunction getStatusIcon(status: string): string {\n switch (status) {\n case 'completed':\n return '\\u2713'; // ✓\n case 'failed':\n return '\\u2717'; // ✗\n case 'in_progress':\n return '\\u280B'; // ⠋\n case 'pending':\n return '\\u25FC'; // ◼\n default:\n return ' ';\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"status.js","sources":["../../../src/cli/commands/status.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { listSessions, loadSession } from '../../session/manager.js';\nimport { deduplicateArticles } from './export.js';\nimport { loadNotes, formatNotesList, type NoteEntry } from './notes.js';\nimport { loadResults } from '../../session/results-io.js';\nimport type { ProviderName } from '../../providers/base/types.js';\n\nexport interface SessionListItem {\n id: string;\n name: string;\n status: string;\n createdAt: string;\n progress: string;\n}\n\nexport interface DatabaseDetails {\n provider: string;\n status: string;\n totalHits: number;\n retrievedCount: number;\n error?: string;\n}\n\nexport interface SessionDetails {\n id: string;\n name: string;\n description?: string;\n status: string;\n createdAt: string;\n updatedAt: string;\n queryFile: string;\n totalHits: number;\n totalRetrieved: number;\n databases: DatabaseDetails[];\n uniqueArticles?: number;\n duplicatesRemoved?: number;\n notes?: NoteEntry[];\n}\n\nexport interface ListOptions {\n all: boolean;\n}\n\nexport interface FormatOptions {\n json: boolean;\n}\n\nexport interface SessionListResult {\n sessions: SessionListItem[];\n totalCount: number;\n filteredCount: number;\n showingAll: boolean;\n}\n\nexport async function listSessionsForDisplay(\n sessionsDir: string,\n options: ListOptions\n): Promise<SessionListResult> {\n const summaries = await listSessions(sessionsDir);\n\n const filtered = options.all\n ? summaries\n : summaries.filter((s) => s.status !== 'completed');\n\n const sessions = filtered.map((s) => ({\n id: s.id,\n name: s.name,\n status: s.status,\n createdAt: s.createdAt,\n progress: `${s.totalRetrieved}/${s.totalHits}`,\n }));\n\n return {\n sessions,\n totalCount: summaries.length,\n filteredCount: filtered.length,\n showingAll: options.all,\n };\n}\n\nexport async function getSessionDetails(\n sessionId: string,\n sessionsDir: string\n): Promise<{ success: boolean; session?: SessionDetails; error?: string }> {\n try {\n const session = await loadSession(sessionId, sessionsDir);\n\n const databases: DatabaseDetails[] = [];\n for (const [provider, dbStatus] of Object.entries(session.databases)) {\n if (!dbStatus) continue;\n const dbDetail: DatabaseDetails = {\n provider,\n status: dbStatus.status,\n totalHits: dbStatus.totalHits ?? 0,\n retrievedCount: dbStatus.retrievedCount ?? 0,\n };\n if (dbStatus.error?.message) {\n dbDetail.error = dbStatus.error.message;\n }\n databases.push(dbDetail);\n }\n\n const sessionDetails: SessionDetails = {\n id: session.id,\n name: session.name,\n status: session.summary.status,\n createdAt: session.createdAt,\n updatedAt: session.updatedAt,\n queryFile: session.query?.file ?? '',\n totalHits: session.summary.totalHits,\n totalRetrieved: session.summary.totalRetrieved,\n databases,\n };\n if (session.description) {\n sessionDetails.description = session.description;\n }\n\n // Load notes (optional - don't fail if notes can't be loaded)\n try {\n const sessionDir = join(sessionsDir, sessionId);\n const notes = await loadNotes(sessionDir);\n if (notes.length > 0) {\n sessionDetails.notes = notes;\n }\n } catch {\n // Notes are optional - ignore errors\n }\n\n return {\n success: true,\n session: sessionDetails,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n return {\n success: false,\n error: message,\n };\n }\n}\n\nexport async function computeDeduplicationStats(\n sessionId: string,\n sessionsDir: string,\n session: { databases: Record<string, { files?: { results?: string } } | undefined> }\n): Promise<{ uniqueArticles: number; duplicatesRemoved: number }> {\n const articles: import('../../providers/base/types.js').Article[] = [];\n const sessionDir = join(sessionsDir, sessionId);\n\n for (const [providerName] of Object.entries(session.databases)) {\n const providerArticles = await loadResults(sessionDir, providerName as ProviderName);\n articles.push(...providerArticles);\n }\n\n if (articles.length === 0) {\n return { uniqueArticles: 0, duplicatesRemoved: 0 };\n }\n\n const result = deduplicateArticles(articles);\n return {\n uniqueArticles: result.articles.length,\n duplicatesRemoved: result.duplicatesRemoved,\n };\n}\n\nexport function formatSessionList(\n result: SessionListResult,\n options: FormatOptions\n): string {\n if (options.json) {\n return JSON.stringify(result.sessions, null, 2);\n }\n\n if (result.sessions.length === 0) {\n if (result.totalCount === 0) {\n return 'No sessions found.';\n }\n // Sessions exist but are filtered out (all completed)\n const completedCount = result.totalCount - result.filteredCount;\n return `No active sessions. ${completedCount} completed session${completedCount === 1 ? '' : 's'} hidden (use --all to show).`;\n }\n\n const header = `${'ID'.padEnd(35)} ${'NAME'.padEnd(20)} ${'STATUS'.padEnd(15)} ${'PROGRESS'.padEnd(12)} CREATED`;\n const separator = '-'.repeat(100);\n\n const rows = result.sessions.map((s) => {\n const date = new Date(s.createdAt).toLocaleDateString();\n return `${s.id.padEnd(35)} ${s.name.padEnd(20)} ${s.status.padEnd(15)} ${s.progress.padEnd(12)} ${date}`;\n });\n\n const lines = [header, separator, ...rows];\n\n // Show hint about hidden completed sessions if not showing all\n if (!result.showingAll && result.totalCount > result.filteredCount) {\n const hiddenCount = result.totalCount - result.filteredCount;\n lines.push('');\n lines.push(`(${hiddenCount} completed session${hiddenCount === 1 ? '' : 's'} hidden, use --all to show)`);\n }\n\n return lines.join('\\n');\n}\n\nexport function formatSessionDetails(\n details: SessionDetails,\n options: FormatOptions\n): string {\n if (options.json) {\n return JSON.stringify(details, null, 2);\n }\n\n const lines: string[] = [];\n\n lines.push(`Session: ${details.name}`);\n lines.push(`ID: ${details.id}`);\n if (details.description) {\n lines.push(`Description: ${details.description}`);\n }\n lines.push(`Status: ${details.status}`);\n lines.push(`Query File: ${details.queryFile}`);\n lines.push(`Created: ${new Date(details.createdAt).toLocaleString()}`);\n lines.push(`Updated: ${new Date(details.updatedAt).toLocaleString()}`);\n lines.push('');\n if (details.duplicatesRemoved !== undefined && details.duplicatesRemoved > 0) {\n lines.push(`Total: ${details.totalRetrieved} raw / ${details.uniqueArticles} unique (${details.duplicatesRemoved} duplicates)`);\n } else {\n lines.push(`Total: ${details.totalRetrieved}/${details.totalHits} results`);\n }\n lines.push('');\n lines.push('Databases:');\n\n for (const db of details.databases) {\n const statusIcon = getStatusIcon(db.status);\n let line = ` ${statusIcon} ${db.provider.padEnd(10)} ${db.status.padEnd(12)} ${db.retrievedCount}/${db.totalHits}`;\n if (db.error) {\n line += ` (${db.error})`;\n }\n lines.push(line);\n }\n\n // Display notes if present\n if (details.notes && details.notes.length > 0) {\n lines.push('');\n lines.push('Notes:');\n lines.push(formatNotesList(details.notes));\n }\n\n return lines.join('\\n');\n}\n\nfunction getStatusIcon(status: string): string {\n switch (status) {\n case 'completed':\n return '\\u2713'; // ✓\n case 'failed':\n return '\\u2717'; // ✗\n case 'in_progress':\n return '\\u280B'; // ⠋\n case 'pending':\n return '\\u25FC'; // ◼\n default:\n return ' ';\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqKpC;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAktEvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
|