@ncukondo/search-hub 0.18.0 → 0.19.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/register.d.ts +1 -1
- package/dist/cli/commands/register.js.map +1 -1
- package/dist/cli/commands/review/finalize.js +6 -6
- package/dist/cli/commands/review/finalize.js.map +1 -1
- package/dist/cli/commands/review/list.d.ts +1 -1
- package/dist/cli/commands/review/list.d.ts.map +1 -1
- package/dist/cli/commands/review/list.js.map +1 -1
- package/dist/cli/commands/review/next-steps.d.ts +1 -1
- package/dist/cli/commands/review/next-steps.js +3 -3
- package/dist/cli/commands/review/next-steps.js.map +1 -1
- package/dist/cli/commands/review/status.d.ts +2 -2
- package/dist/cli/commands/review/status.d.ts.map +1 -1
- package/dist/cli/commands/review/status.js +13 -13
- package/dist/cli/commands/review/status.js.map +1 -1
- package/dist/cli/commands/review/types.d.ts +5 -5
- package/dist/cli/commands/review/types.d.ts.map +1 -1
- package/dist/cli/commands/review/types.js +6 -9
- package/dist/cli/commands/review/types.js.map +1 -1
- package/dist/cli/index.js +3 -3
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -58,7 +58,7 @@ export interface ReviewSummary {
|
|
|
58
58
|
included: number;
|
|
59
59
|
/** Articles with finalDecision='exclude' */
|
|
60
60
|
excluded: number;
|
|
61
|
-
/** Articles without finalDecision (pending, incomplete, uncertain, agreed,
|
|
61
|
+
/** Articles without finalDecision (pending, incomplete, all-uncertain, agreed, divided) */
|
|
62
62
|
pending: number;
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register.js","sources":["../../../src/cli/commands/register.ts"],"sourcesContent":["/**\n * Register command for reference-manager integration.\n * Registers search results with reference-manager CLI.\n */\n\nimport { join } from 'node:path';\nimport { readFile, access } from 'node:fs/promises';\nimport { constants } from 'node:fs';\nimport { createInterface } from 'node:readline';\nimport { parse as parseYaml } from 'yaml';\nimport type { ProviderName, Article, Author } from '../../providers/base/types.js';\nimport { parseProviderNames } from '../utils/validation.js';\nimport { classifyStatus, type ReviewFile } from './review/types.js';\n\nexport interface RegisterCommandOptions {\n sessionId: string;\n providers?: ProviderName[];\n dryRun: boolean;\n withAbstracts: boolean;\n /** Register only reviewed articles with finalDecision='include' */\n reviewed?: boolean;\n /** Register all articles, ignoring reviews */\n all?: boolean;\n /** Skip confirmation prompts */\n force?: boolean;\n /** Suppress tips and suggestions */\n quiet?: boolean;\n}\n\nexport interface CommandLineOptions {\n db?: string | undefined;\n dryRun?: boolean | undefined;\n withAbstracts?: boolean | undefined;\n reviewed?: boolean | undefined;\n all?: boolean | undefined;\n force?: boolean | undefined;\n quiet?: boolean | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\n/**\n * Parse command line options into RegisterCommandOptions.\n */\nexport function parseRegisterOptions(\n sessionId: string,\n options: CommandLineOptions\n): RegisterCommandOptions {\n const result: RegisterCommandOptions = {\n sessionId,\n dryRun: options.dryRun ?? false,\n withAbstracts: options.withAbstracts ?? false,\n reviewed: options.reviewed ?? false,\n all: options.all ?? false,\n force: options.force ?? false,\n quiet: options.quiet ?? false,\n };\n\n if (options.db) {\n result.providers = parseProviderNames(options.db);\n }\n\n return result;\n}\n\n/**\n * Validate register command input.\n */\nexport function validateRegisterInput(options: RegisterCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Format registration summary for CLI output.\n */\nexport function formatRegistrationSummary(summary: {\n total: number;\n added: number;\n skipped: number;\n failed: number;\n noId: number;\n}): string {\n const lines: string[] = ['Registration complete:'];\n\n // Added\n lines.push(` ✓ ${summary.added} added`);\n\n // Duplicates (skipped)\n if (summary.skipped > 0) {\n lines.push(` ⚠ ${summary.skipped} duplicates (already in library)`);\n }\n\n // Failed\n if (summary.failed > 0) {\n lines.push(` ✗ ${summary.failed} failed`);\n }\n\n // No ID (skipped)\n if (summary.noId > 0) {\n lines.push(` - ${summary.noId} skipped (no identifier)`);\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Get registration identifier for an article.\n * PMID is preferred over DOI for better metadata quality.\n */\nfunction getRegistrationId(article: Article): string | null {\n if (article.pmid) {\n return `pmid:${article.pmid}`;\n }\n if (article.doi) {\n return article.doi;\n }\n return null;\n}\n\n/**\n * Format dry run output showing what would be registered.\n */\nexport function formatDryRunOutput(articles: Article[]): string {\n const withId: Array<{ article: Article; id: string }> = [];\n const withoutId: Article[] = [];\n\n for (const article of articles) {\n const id = getRegistrationId(article);\n if (id) {\n withId.push({ article, id });\n } else {\n withoutId.push(article);\n }\n }\n\n const lines: string[] = [];\n\n // Summary\n lines.push(\n `Would register ${withId.length} reference${withId.length !== 1 ? 's' : ''}:`\n );\n\n // List articles with IDs\n for (const { id, article } of withId) {\n const title = article.title.length > 60\n ? article.title.substring(0, 57) + '...'\n : article.title;\n lines.push(` - ${id}: ${title}`);\n }\n\n // Details about articles without DOI/PMID\n if (withoutId.length > 0) {\n lines.push('');\n lines.push(\n `${withoutId.length} article${withoutId.length !== 1 ? 's' : ''} will be skipped (no DOI or PMID):`\n );\n\n const maxDisplay = 10;\n const displayed = withoutId.slice(0, maxDisplay);\n\n for (const article of displayed) {\n const truncatedTitle = article.title.length > 50\n ? article.title.substring(0, 50) + '...'\n : article.title;\n\n const altIds = getAlternativeIds(article);\n const hasAltIds = altIds.length > 0 ? `, has: ${altIds.join(', ')}` : '';\n\n lines.push(` - \"${truncatedTitle}\" (source: ${article.source}${hasAltIds})`);\n }\n\n if (withoutId.length > maxDisplay) {\n lines.push(` ... and ${withoutId.length - maxDisplay} more`);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Get alternative (non-DOI/PMID) identifiers for an article.\n */\nfunction getAlternativeIds(article: Article): string[] {\n const ids: string[] = [];\n if (article.arxivId) ids.push(`arxiv:${article.arxivId}`);\n if (article.ericId) ids.push(`eric:${article.ericId}`);\n if (article.scopusId) ids.push(`scopus:${article.scopusId}`);\n return ids;\n}\n\n/**\n * Summary of review decisions for a session.\n */\nexport interface ReviewSummary {\n /** Total articles in review file */\n total: number;\n /** Articles with finalDecision='include' */\n included: number;\n /** Articles with finalDecision='exclude' */\n excluded: number;\n /** Articles without finalDecision (pending, incomplete, uncertain, agreed, conflicting) */\n pending: number;\n}\n\n/**\n * Check if a session has a reviews.yaml file.\n */\nexport async function hasReviewFile(sessionId: string, sessionsDir: string): Promise<boolean> {\n const reviewsPath = join(sessionsDir, sessionId, '.internal', 'reviews.yaml');\n try {\n await access(reviewsPath, constants.R_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Load and parse the review file for a session.\n */\nasync function loadReviewFile(sessionId: string, sessionsDir: string): Promise<ReviewFile> {\n const reviewsPath = join(sessionsDir, sessionId, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Get review summary (counts) for a session.\n * Throws if reviews.yaml does not exist.\n */\nexport async function getReviewSummary(sessionId: string, sessionsDir: string): Promise<ReviewSummary> {\n const reviewFile = await loadReviewFile(sessionId, sessionsDir);\n const articles = reviewFile.articles ?? [];\n\n const summary: ReviewSummary = {\n total: articles.length,\n included: 0,\n excluded: 0,\n pending: 0,\n };\n\n for (const article of articles) {\n const status = classifyStatus(article);\n\n if (status === 'finalized') {\n if (article.finalDecision === 'include') {\n summary.included++;\n } else {\n summary.excluded++;\n }\n } else {\n // pending, incomplete, uncertain, agreed, conflicting all count as pending for registration\n summary.pending++;\n }\n }\n\n return summary;\n}\n\n/**\n * Parse author name string into Author object.\n * Simple heuristic: last word is family name, rest is given name.\n */\nfunction parseAuthorName(name: string): Author {\n const parts = name.trim().split(/\\s+/);\n if (parts.length === 1) {\n return { family: parts[0] ?? '' };\n }\n // Last part is family name (most common pattern in scientific citations)\n const family = parts.pop() ?? '';\n const given = parts.join(' ');\n return { family, given };\n}\n\n/**\n * Get articles with finalDecision='include' from review file.\n * Converts from ArticleEntry format to Article format.\n *\n * @throws Error if mergedFrom is missing or empty (indicates legacy review file)\n */\nexport async function getIncludedArticles(sessionId: string, sessionsDir: string): Promise<Article[]> {\n const reviewFile = await loadReviewFile(sessionId, sessionsDir);\n const articles = reviewFile.articles ?? [];\n\n return articles\n .filter((entry) => entry.finalDecision === 'include')\n .map((entry): Article => {\n // Validate mergedFrom exists\n if (!entry.mergedFrom) {\n throw new Error(\n `Article \"${entry.title}\" has mergedFrom missing. ` +\n `This may be a legacy review file created before source tracking was fixed. ` +\n `Please re-run 'review init' to regenerate the review file with source tracking.`\n );\n }\n if (entry.mergedFrom.length === 0) {\n throw new Error(\n `Article \"${entry.title}\" has empty mergedFrom array. ` +\n `This is an invalid state - please re-run 'review init' to regenerate.`\n );\n }\n\n const authors: Author[] = entry.authors\n ? entry.authors.split(/,\\s*/).map(parseAuthorName)\n : [];\n\n // Get source from the first entry in mergedFrom\n const source = entry.mergedFrom[0]!.source as ProviderName;\n\n const article: Article = {\n title: entry.title,\n authors,\n source,\n retrievedAt: new Date().toISOString(),\n };\n // Only set optional fields if they have values\n if (entry.doi) article.doi = entry.doi;\n if (entry.pmid) article.pmid = entry.pmid;\n if (entry.scopusId) article.scopusId = entry.scopusId;\n if (entry.arxivId) article.arxivId = entry.arxivId;\n if (entry.ericId) article.ericId = entry.ericId;\n if (entry.abstract) article.abstract = entry.abstract;\n if (entry.year) article.publicationDate = entry.year;\n return article;\n });\n}\n\n/**\n * Format message when reviews exist but no flag specified.\n */\nexport function formatReviewRequiredMessage(summary: ReviewSummary, sessionId: string): string {\n return `This session has a review file.\n Status: ${summary.included} include / ${summary.excluded} exclude / ${summary.pending} pending\n\nPlease specify which articles to register:\n --reviewed Register ${summary.included} included articles\n --all Register all ${summary.total} articles (ignore reviews)\n\nExample:\n search-hub register ${sessionId} --reviewed`;\n}\n\n/**\n * Format error when --reviewed used but no articles are included.\n */\nexport function formatNoIncludedArticlesError(summary: ReviewSummary, sessionId: string): string {\n return `Error: No articles marked as 'include' in reviews.\n Status: ${summary.included} include / ${summary.excluded} exclude / ${summary.pending} pending\n\nRun 'search-hub review status ${sessionId}' for details.`;\n}\n\n/**\n * Format warning when pending articles exist with --reviewed.\n */\nexport function formatPendingWarning(summary: ReviewSummary): string {\n const articleWord = summary.pending === 1 ? 'article' : 'articles';\n return `Warning: ${summary.pending} ${articleWord} still pending review (will be skipped).\nRegistering ${summary.included} included articles...\n\nProceed? [Y/n]`;\n}\n\n\n/**\n * Format note when --all is used with reviews.yaml present.\n */\nexport function formatIgnoringReviewsNote(total: number): string {\n return `Note: Ignoring review decisions. Registering all ${total} articles.`;\n}\n\n/**\n * Prompt user for Y/n confirmation.\n * Returns true if user confirms (Y/y/Enter), false otherwise.\n */\nexport async function confirmPrompt(\n input: NodeJS.ReadableStream = process.stdin,\n output: NodeJS.WritableStream = process.stdout\n): Promise<boolean> {\n const rl = createInterface({\n input,\n output,\n terminal: false,\n });\n\n return new Promise((resolve) => {\n rl.question('', (answer) => {\n rl.close();\n const trimmed = answer.trim().toLowerCase();\n // Empty (Enter) or 'y' or 'yes' means confirm\n resolve(trimmed === '' || trimmed === 'y' || trimmed === 'yes');\n });\n });\n}\n"],"names":["parseYaml"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"register.js","sources":["../../../src/cli/commands/register.ts"],"sourcesContent":["/**\n * Register command for reference-manager integration.\n * Registers search results with reference-manager CLI.\n */\n\nimport { join } from 'node:path';\nimport { readFile, access } from 'node:fs/promises';\nimport { constants } from 'node:fs';\nimport { createInterface } from 'node:readline';\nimport { parse as parseYaml } from 'yaml';\nimport type { ProviderName, Article, Author } from '../../providers/base/types.js';\nimport { parseProviderNames } from '../utils/validation.js';\nimport { classifyStatus, type ReviewFile } from './review/types.js';\n\nexport interface RegisterCommandOptions {\n sessionId: string;\n providers?: ProviderName[];\n dryRun: boolean;\n withAbstracts: boolean;\n /** Register only reviewed articles with finalDecision='include' */\n reviewed?: boolean;\n /** Register all articles, ignoring reviews */\n all?: boolean;\n /** Skip confirmation prompts */\n force?: boolean;\n /** Suppress tips and suggestions */\n quiet?: boolean;\n}\n\nexport interface CommandLineOptions {\n db?: string | undefined;\n dryRun?: boolean | undefined;\n withAbstracts?: boolean | undefined;\n reviewed?: boolean | undefined;\n all?: boolean | undefined;\n force?: boolean | undefined;\n quiet?: boolean | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\n/**\n * Parse command line options into RegisterCommandOptions.\n */\nexport function parseRegisterOptions(\n sessionId: string,\n options: CommandLineOptions\n): RegisterCommandOptions {\n const result: RegisterCommandOptions = {\n sessionId,\n dryRun: options.dryRun ?? false,\n withAbstracts: options.withAbstracts ?? false,\n reviewed: options.reviewed ?? false,\n all: options.all ?? false,\n force: options.force ?? false,\n quiet: options.quiet ?? false,\n };\n\n if (options.db) {\n result.providers = parseProviderNames(options.db);\n }\n\n return result;\n}\n\n/**\n * Validate register command input.\n */\nexport function validateRegisterInput(options: RegisterCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Format registration summary for CLI output.\n */\nexport function formatRegistrationSummary(summary: {\n total: number;\n added: number;\n skipped: number;\n failed: number;\n noId: number;\n}): string {\n const lines: string[] = ['Registration complete:'];\n\n // Added\n lines.push(` ✓ ${summary.added} added`);\n\n // Duplicates (skipped)\n if (summary.skipped > 0) {\n lines.push(` ⚠ ${summary.skipped} duplicates (already in library)`);\n }\n\n // Failed\n if (summary.failed > 0) {\n lines.push(` ✗ ${summary.failed} failed`);\n }\n\n // No ID (skipped)\n if (summary.noId > 0) {\n lines.push(` - ${summary.noId} skipped (no identifier)`);\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Get registration identifier for an article.\n * PMID is preferred over DOI for better metadata quality.\n */\nfunction getRegistrationId(article: Article): string | null {\n if (article.pmid) {\n return `pmid:${article.pmid}`;\n }\n if (article.doi) {\n return article.doi;\n }\n return null;\n}\n\n/**\n * Format dry run output showing what would be registered.\n */\nexport function formatDryRunOutput(articles: Article[]): string {\n const withId: Array<{ article: Article; id: string }> = [];\n const withoutId: Article[] = [];\n\n for (const article of articles) {\n const id = getRegistrationId(article);\n if (id) {\n withId.push({ article, id });\n } else {\n withoutId.push(article);\n }\n }\n\n const lines: string[] = [];\n\n // Summary\n lines.push(\n `Would register ${withId.length} reference${withId.length !== 1 ? 's' : ''}:`\n );\n\n // List articles with IDs\n for (const { id, article } of withId) {\n const title = article.title.length > 60\n ? article.title.substring(0, 57) + '...'\n : article.title;\n lines.push(` - ${id}: ${title}`);\n }\n\n // Details about articles without DOI/PMID\n if (withoutId.length > 0) {\n lines.push('');\n lines.push(\n `${withoutId.length} article${withoutId.length !== 1 ? 's' : ''} will be skipped (no DOI or PMID):`\n );\n\n const maxDisplay = 10;\n const displayed = withoutId.slice(0, maxDisplay);\n\n for (const article of displayed) {\n const truncatedTitle = article.title.length > 50\n ? article.title.substring(0, 50) + '...'\n : article.title;\n\n const altIds = getAlternativeIds(article);\n const hasAltIds = altIds.length > 0 ? `, has: ${altIds.join(', ')}` : '';\n\n lines.push(` - \"${truncatedTitle}\" (source: ${article.source}${hasAltIds})`);\n }\n\n if (withoutId.length > maxDisplay) {\n lines.push(` ... and ${withoutId.length - maxDisplay} more`);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Get alternative (non-DOI/PMID) identifiers for an article.\n */\nfunction getAlternativeIds(article: Article): string[] {\n const ids: string[] = [];\n if (article.arxivId) ids.push(`arxiv:${article.arxivId}`);\n if (article.ericId) ids.push(`eric:${article.ericId}`);\n if (article.scopusId) ids.push(`scopus:${article.scopusId}`);\n return ids;\n}\n\n/**\n * Summary of review decisions for a session.\n */\nexport interface ReviewSummary {\n /** Total articles in review file */\n total: number;\n /** Articles with finalDecision='include' */\n included: number;\n /** Articles with finalDecision='exclude' */\n excluded: number;\n /** Articles without finalDecision (pending, incomplete, all-uncertain, agreed, divided) */\n pending: number;\n}\n\n/**\n * Check if a session has a reviews.yaml file.\n */\nexport async function hasReviewFile(sessionId: string, sessionsDir: string): Promise<boolean> {\n const reviewsPath = join(sessionsDir, sessionId, '.internal', 'reviews.yaml');\n try {\n await access(reviewsPath, constants.R_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Load and parse the review file for a session.\n */\nasync function loadReviewFile(sessionId: string, sessionsDir: string): Promise<ReviewFile> {\n const reviewsPath = join(sessionsDir, sessionId, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Get review summary (counts) for a session.\n * Throws if reviews.yaml does not exist.\n */\nexport async function getReviewSummary(sessionId: string, sessionsDir: string): Promise<ReviewSummary> {\n const reviewFile = await loadReviewFile(sessionId, sessionsDir);\n const articles = reviewFile.articles ?? [];\n\n const summary: ReviewSummary = {\n total: articles.length,\n included: 0,\n excluded: 0,\n pending: 0,\n };\n\n for (const article of articles) {\n const status = classifyStatus(article);\n\n if (status === 'finalized') {\n if (article.finalDecision === 'include') {\n summary.included++;\n } else {\n summary.excluded++;\n }\n } else {\n // pending, incomplete, all-uncertain, agreed, divided all count as pending for registration\n summary.pending++;\n }\n }\n\n return summary;\n}\n\n/**\n * Parse author name string into Author object.\n * Simple heuristic: last word is family name, rest is given name.\n */\nfunction parseAuthorName(name: string): Author {\n const parts = name.trim().split(/\\s+/);\n if (parts.length === 1) {\n return { family: parts[0] ?? '' };\n }\n // Last part is family name (most common pattern in scientific citations)\n const family = parts.pop() ?? '';\n const given = parts.join(' ');\n return { family, given };\n}\n\n/**\n * Get articles with finalDecision='include' from review file.\n * Converts from ArticleEntry format to Article format.\n *\n * @throws Error if mergedFrom is missing or empty (indicates legacy review file)\n */\nexport async function getIncludedArticles(sessionId: string, sessionsDir: string): Promise<Article[]> {\n const reviewFile = await loadReviewFile(sessionId, sessionsDir);\n const articles = reviewFile.articles ?? [];\n\n return articles\n .filter((entry) => entry.finalDecision === 'include')\n .map((entry): Article => {\n // Validate mergedFrom exists\n if (!entry.mergedFrom) {\n throw new Error(\n `Article \"${entry.title}\" has mergedFrom missing. ` +\n `This may be a legacy review file created before source tracking was fixed. ` +\n `Please re-run 'review init' to regenerate the review file with source tracking.`\n );\n }\n if (entry.mergedFrom.length === 0) {\n throw new Error(\n `Article \"${entry.title}\" has empty mergedFrom array. ` +\n `This is an invalid state - please re-run 'review init' to regenerate.`\n );\n }\n\n const authors: Author[] = entry.authors\n ? entry.authors.split(/,\\s*/).map(parseAuthorName)\n : [];\n\n // Get source from the first entry in mergedFrom\n const source = entry.mergedFrom[0]!.source as ProviderName;\n\n const article: Article = {\n title: entry.title,\n authors,\n source,\n retrievedAt: new Date().toISOString(),\n };\n // Only set optional fields if they have values\n if (entry.doi) article.doi = entry.doi;\n if (entry.pmid) article.pmid = entry.pmid;\n if (entry.scopusId) article.scopusId = entry.scopusId;\n if (entry.arxivId) article.arxivId = entry.arxivId;\n if (entry.ericId) article.ericId = entry.ericId;\n if (entry.abstract) article.abstract = entry.abstract;\n if (entry.year) article.publicationDate = entry.year;\n return article;\n });\n}\n\n/**\n * Format message when reviews exist but no flag specified.\n */\nexport function formatReviewRequiredMessage(summary: ReviewSummary, sessionId: string): string {\n return `This session has a review file.\n Status: ${summary.included} include / ${summary.excluded} exclude / ${summary.pending} pending\n\nPlease specify which articles to register:\n --reviewed Register ${summary.included} included articles\n --all Register all ${summary.total} articles (ignore reviews)\n\nExample:\n search-hub register ${sessionId} --reviewed`;\n}\n\n/**\n * Format error when --reviewed used but no articles are included.\n */\nexport function formatNoIncludedArticlesError(summary: ReviewSummary, sessionId: string): string {\n return `Error: No articles marked as 'include' in reviews.\n Status: ${summary.included} include / ${summary.excluded} exclude / ${summary.pending} pending\n\nRun 'search-hub review status ${sessionId}' for details.`;\n}\n\n/**\n * Format warning when pending articles exist with --reviewed.\n */\nexport function formatPendingWarning(summary: ReviewSummary): string {\n const articleWord = summary.pending === 1 ? 'article' : 'articles';\n return `Warning: ${summary.pending} ${articleWord} still pending review (will be skipped).\nRegistering ${summary.included} included articles...\n\nProceed? [Y/n]`;\n}\n\n\n/**\n * Format note when --all is used with reviews.yaml present.\n */\nexport function formatIgnoringReviewsNote(total: number): string {\n return `Note: Ignoring review decisions. Registering all ${total} articles.`;\n}\n\n/**\n * Prompt user for Y/n confirmation.\n * Returns true if user confirms (Y/y/Enter), false otherwise.\n */\nexport async function confirmPrompt(\n input: NodeJS.ReadableStream = process.stdin,\n output: NodeJS.WritableStream = process.stdout\n): Promise<boolean> {\n const rl = createInterface({\n input,\n output,\n terminal: false,\n });\n\n return new Promise((resolve) => {\n rl.question('', (answer) => {\n rl.close();\n const trimmed = answer.trim().toLowerCase();\n // Empty (Enter) or 'y' or 'yes' means confirm\n resolve(trimmed === '' || trimmed === 'y' || trimmed === 'yes');\n });\n });\n}\n"],"names":["parseYaml"],"mappings":";;;;;;;AA+CO,SAAS,qBACd,WACA,SACwB;AACxB,QAAM,SAAiC;AAAA,IACrC;AAAA,IACA,QAAQ,QAAQ,UAAU;AAAA,IAC1B,eAAe,QAAQ,iBAAiB;AAAA,IACxC,UAAU,QAAQ,YAAY;AAAA,IAC9B,KAAK,QAAQ,OAAO;AAAA,IACpB,OAAO,QAAQ,SAAS;AAAA,IACxB,OAAO,QAAQ,SAAS;AAAA,EAAA;AAG1B,MAAI,QAAQ,IAAI;AACd,WAAO,YAAY,mBAAmB,QAAQ,EAAE;AAAA,EAClD;AAEA,SAAO;AACT;AAKO,SAAS,sBAAsB,SAAmD;AACvF,MAAI,CAAC,QAAQ,aAAa,QAAQ,UAAU,KAAA,MAAW,IAAI;AACzD,WAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO;AAAA,IAAA;AAAA,EAEX;AAEA,SAAO,EAAE,OAAO,KAAA;AAClB;AAKO,SAAS,0BAA0B,SAM/B;AACT,QAAM,QAAkB,CAAC,wBAAwB;AAGjD,QAAM,KAAK,OAAO,QAAQ,KAAK,QAAQ;AAGvC,MAAI,QAAQ,UAAU,GAAG;AACvB,UAAM,KAAK,OAAO,QAAQ,OAAO,kCAAkC;AAAA,EACrE;AAGA,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,KAAK,OAAO,QAAQ,MAAM,SAAS;AAAA,EAC3C;AAGA,MAAI,QAAQ,OAAO,GAAG;AACpB,UAAM,KAAK,OAAO,QAAQ,IAAI,0BAA0B;AAAA,EAC1D;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAMA,SAAS,kBAAkB,SAAiC;AAC1D,MAAI,QAAQ,MAAM;AAChB,WAAO,QAAQ,QAAQ,IAAI;AAAA,EAC7B;AACA,MAAI,QAAQ,KAAK;AACf,WAAO,QAAQ;AAAA,EACjB;AACA,SAAO;AACT;AAKO,SAAS,mBAAmB,UAA6B;AAC9D,QAAM,SAAkD,CAAA;AACxD,QAAM,YAAuB,CAAA;AAE7B,aAAW,WAAW,UAAU;AAC9B,UAAM,KAAK,kBAAkB,OAAO;AACpC,QAAI,IAAI;AACN,aAAO,KAAK,EAAE,SAAS,GAAA,CAAI;AAAA,IAC7B,OAAO;AACL,gBAAU,KAAK,OAAO;AAAA,IACxB;AAAA,EACF;AAEA,QAAM,QAAkB,CAAA;AAGxB,QAAM;AAAA,IACJ,kBAAkB,OAAO,MAAM,aAAa,OAAO,WAAW,IAAI,MAAM,EAAE;AAAA,EAAA;AAI5E,aAAW,EAAE,IAAI,QAAA,KAAa,QAAQ;AACpC,UAAM,QAAQ,QAAQ,MAAM,SAAS,KACjC,QAAQ,MAAM,UAAU,GAAG,EAAE,IAAI,QACjC,QAAQ;AACZ,UAAM,KAAK,OAAO,EAAE,KAAK,KAAK,EAAE;AAAA,EAClC;AAGA,MAAI,UAAU,SAAS,GAAG;AACxB,UAAM,KAAK,EAAE;AACb,UAAM;AAAA,MACJ,GAAG,UAAU,MAAM,WAAW,UAAU,WAAW,IAAI,MAAM,EAAE;AAAA,IAAA;AAGjE,UAAM,aAAa;AACnB,UAAM,YAAY,UAAU,MAAM,GAAG,UAAU;AAE/C,eAAW,WAAW,WAAW;AAC/B,YAAM,iBAAiB,QAAQ,MAAM,SAAS,KAC1C,QAAQ,MAAM,UAAU,GAAG,EAAE,IAAI,QACjC,QAAQ;AAEZ,YAAM,SAAS,kBAAkB,OAAO;AACxC,YAAM,YAAY,OAAO,SAAS,IAAI,UAAU,OAAO,KAAK,IAAI,CAAC,KAAK;AAEtE,YAAM,KAAK,QAAQ,cAAc,cAAc,QAAQ,MAAM,GAAG,SAAS,GAAG;AAAA,IAC9E;AAEA,QAAI,UAAU,SAAS,YAAY;AACjC,YAAM,KAAK,aAAa,UAAU,SAAS,UAAU,OAAO;AAAA,IAC9D;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAKA,SAAS,kBAAkB,SAA4B;AACrD,QAAM,MAAgB,CAAA;AACtB,MAAI,QAAQ,QAAS,KAAI,KAAK,SAAS,QAAQ,OAAO,EAAE;AACxD,MAAI,QAAQ,OAAQ,KAAI,KAAK,QAAQ,QAAQ,MAAM,EAAE;AACrD,MAAI,QAAQ,SAAU,KAAI,KAAK,UAAU,QAAQ,QAAQ,EAAE;AAC3D,SAAO;AACT;AAmBA,eAAsB,cAAc,WAAmB,aAAuC;AAC5F,QAAM,cAAc,KAAK,aAAa,WAAW,aAAa,cAAc;AAC5E,MAAI;AACF,UAAM,OAAO,aAAa,UAAU,IAAI;AACxC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAe,eAAe,WAAmB,aAA0C;AACzF,QAAM,cAAc,KAAK,aAAa,WAAW,aAAa,cAAc;AAC5E,QAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,SAAOA,MAAU,OAAO;AAC1B;AAMA,eAAsB,iBAAiB,WAAmB,aAA6C;AACrG,QAAM,aAAa,MAAM,eAAe,WAAW,WAAW;AAC9D,QAAM,WAAW,WAAW,YAAY,CAAA;AAExC,QAAM,UAAyB;AAAA,IAC7B,OAAO,SAAS;AAAA,IAChB,UAAU;AAAA,IACV,UAAU;AAAA,IACV,SAAS;AAAA,EAAA;AAGX,aAAW,WAAW,UAAU;AAC9B,UAAM,SAAS,eAAe,OAAO;AAErC,QAAI,WAAW,aAAa;AAC1B,UAAI,QAAQ,kBAAkB,WAAW;AACvC,gBAAQ;AAAA,MACV,OAAO;AACL,gBAAQ;AAAA,MACV;AAAA,IACF,OAAO;AAEL,cAAQ;AAAA,IACV;AAAA,EACF;AAEA,SAAO;AACT;AAMA,SAAS,gBAAgB,MAAsB;AAC7C,QAAM,QAAQ,KAAK,KAAA,EAAO,MAAM,KAAK;AACrC,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,EAAE,QAAQ,MAAM,CAAC,KAAK,GAAA;AAAA,EAC/B;AAEA,QAAM,SAAS,MAAM,IAAA,KAAS;AAC9B,QAAM,QAAQ,MAAM,KAAK,GAAG;AAC5B,SAAO,EAAE,QAAQ,MAAA;AACnB;AAQA,eAAsB,oBAAoB,WAAmB,aAAyC;AACpG,QAAM,aAAa,MAAM,eAAe,WAAW,WAAW;AAC9D,QAAM,WAAW,WAAW,YAAY,CAAA;AAExC,SAAO,SACJ,OAAO,CAAC,UAAU,MAAM,kBAAkB,SAAS,EACnD,IAAI,CAAC,UAAmB;AAEvB,QAAI,CAAC,MAAM,YAAY;AACrB,YAAM,IAAI;AAAA,QACR,YAAY,MAAM,KAAK;AAAA,MAAA;AAAA,IAI3B;AACA,QAAI,MAAM,WAAW,WAAW,GAAG;AACjC,YAAM,IAAI;AAAA,QACR,YAAY,MAAM,KAAK;AAAA,MAAA;AAAA,IAG3B;AAEA,UAAM,UAAoB,MAAM,UAC5B,MAAM,QAAQ,MAAM,MAAM,EAAE,IAAI,eAAe,IAC/C,CAAA;AAGJ,UAAM,SAAS,MAAM,WAAW,CAAC,EAAG;AAEpC,UAAM,UAAmB;AAAA,MACvB,OAAO,MAAM;AAAA,MACb;AAAA,MACA;AAAA,MACA,cAAa,oBAAI,KAAA,GAAO,YAAA;AAAA,IAAY;AAGtC,QAAI,MAAM,IAAK,SAAQ,MAAM,MAAM;AACnC,QAAI,MAAM,KAAM,SAAQ,OAAO,MAAM;AACrC,QAAI,MAAM,SAAU,SAAQ,WAAW,MAAM;AAC7C,QAAI,MAAM,QAAS,SAAQ,UAAU,MAAM;AAC3C,QAAI,MAAM,OAAQ,SAAQ,SAAS,MAAM;AACzC,QAAI,MAAM,SAAU,SAAQ,WAAW,MAAM;AAC7C,QAAI,MAAM,KAAM,SAAQ,kBAAkB,MAAM;AAChD,WAAO;AAAA,EACT,CAAC;AACL;AAKO,SAAS,4BAA4B,SAAwB,WAA2B;AAC7F,SAAO;AAAA,YACG,QAAQ,QAAQ,cAAc,QAAQ,QAAQ,cAAc,QAAQ,OAAO;AAAA;AAAA;AAAA,0BAG7D,QAAQ,QAAQ;AAAA,8BACZ,QAAQ,KAAK;AAAA;AAAA;AAAA,wBAGnB,SAAS;AACjC;AAKO,SAAS,8BAA8B,SAAwB,WAA2B;AAC/F,SAAO;AAAA,YACG,QAAQ,QAAQ,cAAc,QAAQ,QAAQ,cAAc,QAAQ,OAAO;AAAA;AAAA,gCAEvD,SAAS;AACzC;AAKO,SAAS,qBAAqB,SAAgC;AACnE,QAAM,cAAc,QAAQ,YAAY,IAAI,YAAY;AACxD,SAAO,YAAY,QAAQ,OAAO,IAAI,WAAW;AAAA,cACrC,QAAQ,QAAQ;AAAA;AAAA;AAG9B;AAMO,SAAS,0BAA0B,OAAuB;AAC/D,SAAO,oDAAoD,KAAK;AAClE;AAMA,eAAsB,cACpB,QAA+B,QAAQ,OACvC,SAAgC,QAAQ,QACtB;AAClB,QAAM,KAAK,gBAAgB;AAAA,IACzB;AAAA,IACA;AAAA,IACA,UAAU;AAAA,EAAA,CACX;AAED,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,OAAG,SAAS,IAAI,CAAC,WAAW;AAC1B,SAAG,MAAA;AACH,YAAM,UAAU,OAAO,KAAA,EAAO,YAAA;AAE9B,cAAQ,YAAY,MAAM,YAAY,OAAO,YAAY,KAAK;AAAA,IAChE,CAAC;AAAA,EACH,CAAC;AACH;"}
|
|
@@ -6,10 +6,10 @@ function createEmptySkippedByStatus() {
|
|
|
6
6
|
return {
|
|
7
7
|
pending: 0,
|
|
8
8
|
incomplete: 0,
|
|
9
|
-
uncertain: 0,
|
|
9
|
+
"all-uncertain": 0,
|
|
10
10
|
"agreed-include": 0,
|
|
11
11
|
"agreed-exclude": 0,
|
|
12
|
-
|
|
12
|
+
divided: 0,
|
|
13
13
|
finalized: 0
|
|
14
14
|
};
|
|
15
15
|
}
|
|
@@ -74,11 +74,11 @@ function formatFinalizeOutput(result, options) {
|
|
|
74
74
|
if (result.skippedByStatus.incomplete > 0) {
|
|
75
75
|
skippedParts.push(`${result.skippedByStatus.incomplete} incomplete`);
|
|
76
76
|
}
|
|
77
|
-
if (result.skippedByStatus
|
|
78
|
-
skippedParts.push(`${result.skippedByStatus
|
|
77
|
+
if (result.skippedByStatus["all-uncertain"] > 0) {
|
|
78
|
+
skippedParts.push(`${result.skippedByStatus["all-uncertain"]} all-uncertain`);
|
|
79
79
|
}
|
|
80
|
-
if (result.skippedByStatus.
|
|
81
|
-
skippedParts.push(`${result.skippedByStatus.
|
|
80
|
+
if (result.skippedByStatus.divided > 0) {
|
|
81
|
+
skippedParts.push(`${result.skippedByStatus.divided} divided`);
|
|
82
82
|
}
|
|
83
83
|
if (options?.decision && result.skippedByStatus["agreed-include"] > 0) {
|
|
84
84
|
skippedParts.push(`${result.skippedByStatus["agreed-include"]} agreed-include (filtered)`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"finalize.js","sources":["../../../../src/cli/commands/review/finalize.ts"],"sourcesContent":["/**\n * review finalize command - Auto-set finalDecision for articles with consensus\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ReviewStatus } from './types.js';\n\nexport interface ReviewFinalizeOptions {\n sessionId: string;\n dryRun?: boolean;\n minReviewers?: number;\n decision?: 'include' | 'exclude';\n}\n\nexport interface ReviewFinalizeResult {\n includedCount: number;\n excludedCount: number;\n skippedByStatus: Record<ReviewStatus, number>;\n}\n\nfunction createEmptySkippedByStatus(): Record<ReviewStatus, number> {\n return {\n pending: 0,\n incomplete: 0,\n uncertain: 0,\n 'agreed-include': 0,\n 'agreed-exclude': 0,\n
|
|
1
|
+
{"version":3,"file":"finalize.js","sources":["../../../../src/cli/commands/review/finalize.ts"],"sourcesContent":["/**\n * review finalize command - Auto-set finalDecision for articles with consensus\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ReviewStatus } from './types.js';\n\nexport interface ReviewFinalizeOptions {\n sessionId: string;\n dryRun?: boolean;\n minReviewers?: number;\n decision?: 'include' | 'exclude';\n}\n\nexport interface ReviewFinalizeResult {\n includedCount: number;\n excludedCount: number;\n skippedByStatus: Record<ReviewStatus, number>;\n}\n\nfunction createEmptySkippedByStatus(): Record<ReviewStatus, number> {\n return {\n pending: 0,\n incomplete: 0,\n 'all-uncertain': 0,\n 'agreed-include': 0,\n 'agreed-exclude': 0,\n divided: 0,\n finalized: 0,\n };\n}\n\n/**\n * Execute review finalize command\n */\nexport async function executeReviewFinalize(\n options: ReviewFinalizeOptions,\n sessionsDir: string\n): Promise<ReviewFinalizeResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(content) as ReviewFile;\n\n const reviewers = reviewFile.reviewers ?? [];\n const minReviewers = options.minReviewers ?? 1;\n\n const result: ReviewFinalizeResult = {\n includedCount: 0,\n excludedCount: 0,\n skippedByStatus: createEmptySkippedByStatus(),\n };\n\n for (const article of reviewFile.articles) {\n const status = classifyStatus(article, reviewers);\n\n if (status === 'agreed-include' || status === 'agreed-exclude') {\n // Check decision filter\n const consensusDecision = status === 'agreed-include' ? 'include' : 'exclude';\n if (options.decision && options.decision !== consensusDecision) {\n result.skippedByStatus[status]++;\n continue;\n }\n\n // Check minimum reviewer count\n const reviews = article.reviews ?? [];\n const uniqueReviewers = new Set(reviews.map((r) => r.reviewer));\n if (uniqueReviewers.size < minReviewers) {\n result.skippedByStatus[status]++;\n continue;\n }\n\n if (!options.dryRun) {\n article.finalDecision = consensusDecision;\n }\n\n if (status === 'agreed-include') {\n result.includedCount++;\n } else {\n result.excludedCount++;\n }\n } else {\n result.skippedByStatus[status]++;\n }\n }\n\n // Write back if not dry-run\n if (!options.dryRun) {\n const yamlContent = stringifyYaml(reviewFile, { lineWidth: 0 });\n const schemaComment = `# yaml-language-server: $schema=./review.schema.json\\n`;\n await writeFile(reviewsPath, schemaComment + yamlContent, 'utf-8');\n }\n\n return result;\n}\n\n/**\n * Format finalize result as human-readable string\n */\nexport function formatFinalizeOutput(\n result: ReviewFinalizeResult,\n options?: { dryRun?: boolean; decision?: 'include' | 'exclude' }\n): string {\n const lines: string[] = [];\n\n if (options?.dryRun) {\n lines.push('Dry run - no changes made');\n lines.push('');\n }\n\n const total = result.includedCount + result.excludedCount;\n lines.push(`Finalized ${total} articles (${result.includedCount} include, ${result.excludedCount} exclude)`);\n\n // Build skipped summary (only non-zero, non-agreed statuses)\n const skippedParts: string[] = [];\n if (result.skippedByStatus.pending > 0) {\n skippedParts.push(`${result.skippedByStatus.pending} pending`);\n }\n if (result.skippedByStatus.incomplete > 0) {\n skippedParts.push(`${result.skippedByStatus.incomplete} incomplete`);\n }\n if (result.skippedByStatus['all-uncertain'] > 0) {\n skippedParts.push(`${result.skippedByStatus['all-uncertain']} all-uncertain`);\n }\n if (result.skippedByStatus.divided > 0) {\n skippedParts.push(`${result.skippedByStatus.divided} divided`);\n }\n\n // Show filtered-out agreed counts when --decision is active\n if (options?.decision && result.skippedByStatus['agreed-include'] > 0) {\n skippedParts.push(`${result.skippedByStatus['agreed-include']} agreed-include (filtered)`);\n }\n if (options?.decision && result.skippedByStatus['agreed-exclude'] > 0) {\n skippedParts.push(`${result.skippedByStatus['agreed-exclude']} agreed-exclude (filtered)`);\n }\n\n if (skippedParts.length > 0) {\n lines.push(`Skipped: ${skippedParts.join(', ')}`);\n }\n\n return lines.join('\\n');\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ReviewStatus } from './types.js';
|
|
2
|
-
export type ListFilter = 'pending' | 'incomplete' | 'uncertain' | 'agreed-include' | 'agreed-exclude' | '
|
|
2
|
+
export type ListFilter = 'pending' | 'incomplete' | 'all-uncertain' | 'agreed-include' | 'agreed-exclude' | 'divided' | 'finalized' | 'all';
|
|
3
3
|
export interface ReviewListOptions {
|
|
4
4
|
sessionId: string;
|
|
5
5
|
filter?: ListFilter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/list.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAmC,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEhF,MAAM,MAAM,UAAU,GAClB,SAAS,GACT,YAAY,GACZ,
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/list.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAmC,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEhF,MAAM,MAAM,UAAU,GAClB,SAAS,GACT,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,SAAS,GACT,WAAW,GACX,KAAK,CAAC;AAEV,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAWD;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAuC3B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAkBjE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.js","sources":["../../../../src/cli/commands/review/list.ts"],"sourcesContent":["/**\n * review list command - List articles with optional filtering\n */\n\nimport { join } from 'node:path';\nimport { readFile } from 'node:fs/promises';\nimport { parse as parseYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ReviewStatus } from './types.js';\n\nexport type ListFilter =\n | 'pending'\n | 'incomplete'\n | 'uncertain'\n | 'agreed-include'\n | 'agreed-exclude'\n | '
|
|
1
|
+
{"version":3,"file":"list.js","sources":["../../../../src/cli/commands/review/list.ts"],"sourcesContent":["/**\n * review list command - List articles with optional filtering\n */\n\nimport { join } from 'node:path';\nimport { readFile } from 'node:fs/promises';\nimport { parse as parseYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ReviewStatus } from './types.js';\n\nexport type ListFilter =\n | 'pending'\n | 'incomplete'\n | 'all-uncertain'\n | 'agreed-include'\n | 'agreed-exclude'\n | 'divided'\n | 'finalized'\n | 'all';\n\nexport interface ReviewListOptions {\n sessionId: string;\n filter?: ListFilter;\n}\n\nexport interface ArticleListItem {\n title: string;\n pmid?: string;\n doi?: string;\n scopusId?: string;\n arxivId?: string;\n ericId?: string;\n year?: string;\n status: ReviewStatus;\n reviewCount: number;\n finalDecision?: 'include' | 'exclude';\n}\n\nexport interface ReviewListResult {\n sessionId: string;\n filter: ListFilter;\n articles: ArticleListItem[];\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 * Execute review list command\n */\nexport async function executeReviewList(\n options: ReviewListOptions,\n sessionsDir: string\n): Promise<ReviewListResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n const reviewFile = await loadReviewFile(sessionDir);\n const filter = options.filter ?? 'all';\n\n const articles: ArticleListItem[] = [];\n\n const reviewers = reviewFile.reviewers;\n for (const article of reviewFile.articles) {\n const status = classifyStatus(article, reviewers);\n\n // Apply filter\n if (filter !== 'all' && status !== filter) {\n continue;\n }\n\n const item: ArticleListItem = {\n title: article.title,\n status,\n reviewCount: (article.reviews ?? []).length,\n };\n\n // Add optional identifiers\n if (article.pmid) item.pmid = article.pmid;\n if (article.doi) item.doi = article.doi;\n if (article.scopusId) item.scopusId = article.scopusId;\n if (article.arxivId) item.arxivId = article.arxivId;\n if (article.ericId) item.ericId = article.ericId;\n if (article.year) item.year = article.year;\n if (article.finalDecision) item.finalDecision = article.finalDecision;\n\n articles.push(item);\n }\n\n return {\n sessionId: options.sessionId,\n filter,\n articles,\n };\n}\n\n/**\n * Format list result as human-readable string\n */\nexport function formatListOutput(result: ReviewListResult): string {\n if (result.articles.length === 0) {\n return `No articles found matching filter: ${result.filter}`;\n }\n\n const lines: string[] = [];\n lines.push(`${result.articles.length} articles (filter: ${result.filter})`);\n lines.push('');\n\n for (const article of result.articles) {\n const id = article.pmid ?? article.doi ?? article.scopusId ?? article.arxivId ?? article.ericId ?? '-';\n const year = article.year ?? '-';\n const decision = article.finalDecision ? ` [${article.finalDecision}]` : '';\n lines.push(`[${article.status}] ${article.title}`);\n lines.push(` ID: ${id} | Year: ${year} | Reviews: ${article.reviewCount}${decision}`);\n }\n\n return lines.join('\\n');\n}\n"],"names":["parseYaml"],"mappings":";;;;AA8CA,eAAe,eAAe,YAAyC;AACrE,QAAM,cAAc,KAAK,YAAY,aAAa,cAAc;AAChE,QAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,SAAOA,MAAU,OAAO;AAC1B;AAKA,eAAsB,kBACpB,SACA,aAC2B;AAC3B,QAAM,aAAa,KAAK,aAAa,QAAQ,SAAS;AACtD,QAAM,aAAa,MAAM,eAAe,UAAU;AAClD,QAAM,SAAS,QAAQ,UAAU;AAEjC,QAAM,WAA8B,CAAA;AAEpC,QAAM,YAAY,WAAW;AAC7B,aAAW,WAAW,WAAW,UAAU;AACzC,UAAM,SAAS,eAAe,SAAS,SAAS;AAGhD,QAAI,WAAW,SAAS,WAAW,QAAQ;AACzC;AAAA,IACF;AAEA,UAAM,OAAwB;AAAA,MAC5B,OAAO,QAAQ;AAAA,MACf;AAAA,MACA,cAAc,QAAQ,WAAW,IAAI;AAAA,IAAA;AAIvC,QAAI,QAAQ,KAAM,MAAK,OAAO,QAAQ;AACtC,QAAI,QAAQ,IAAK,MAAK,MAAM,QAAQ;AACpC,QAAI,QAAQ,SAAU,MAAK,WAAW,QAAQ;AAC9C,QAAI,QAAQ,QAAS,MAAK,UAAU,QAAQ;AAC5C,QAAI,QAAQ,OAAQ,MAAK,SAAS,QAAQ;AAC1C,QAAI,QAAQ,KAAM,MAAK,OAAO,QAAQ;AACtC,QAAI,QAAQ,cAAe,MAAK,gBAAgB,QAAQ;AAExD,aAAS,KAAK,IAAI;AAAA,EACpB;AAEA,SAAO;AAAA,IACL,WAAW,QAAQ;AAAA,IACnB;AAAA,IACA;AAAA,EAAA;AAEJ;AAKO,SAAS,iBAAiB,QAAkC;AACjE,MAAI,OAAO,SAAS,WAAW,GAAG;AAChC,WAAO,sCAAsC,OAAO,MAAM;AAAA,EAC5D;AAEA,QAAM,QAAkB,CAAA;AACxB,QAAM,KAAK,GAAG,OAAO,SAAS,MAAM,sBAAsB,OAAO,MAAM,GAAG;AAC1E,QAAM,KAAK,EAAE;AAEb,aAAW,WAAW,OAAO,UAAU;AACrC,UAAM,KAAK,QAAQ,QAAQ,QAAQ,OAAO,QAAQ,YAAY,QAAQ,WAAW,QAAQ,UAAU;AACnG,UAAM,OAAO,QAAQ,QAAQ;AAC7B,UAAM,WAAW,QAAQ,gBAAgB,KAAK,QAAQ,aAAa,MAAM;AACzE,UAAM,KAAK,IAAI,QAAQ,MAAM,KAAK,QAAQ,KAAK,EAAE;AACjD,UAAM,KAAK,SAAS,EAAE,YAAY,IAAI,eAAe,QAAQ,WAAW,GAAG,QAAQ,EAAE;AAAA,EACvF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
|
|
@@ -33,7 +33,7 @@ export declare function computeBatchContinuation(params: BatchContinuationParams
|
|
|
33
33
|
* Evaluation order (top-to-bottom, first match wins for primary suggestion):
|
|
34
34
|
* 1. pending > 0 → extract for title screening
|
|
35
35
|
* 2. agreed > 0 → finalize consensus articles
|
|
36
|
-
* 3. (
|
|
36
|
+
* 3. (divided + allUncertain + incomplete) > 0 → extract for next basis level
|
|
37
37
|
* 4. all finalized → register accepted articles
|
|
38
38
|
*
|
|
39
39
|
* Batch continuation is appended to seeAlso when applicable.
|
|
@@ -30,11 +30,11 @@ function generateReviewNextSteps(ctx) {
|
|
|
30
30
|
command: `search-hub review finalize --session ${sessionId}`,
|
|
31
31
|
description: `Finalize ${agreed} articles with consensus`
|
|
32
32
|
});
|
|
33
|
-
} else if (rs.
|
|
34
|
-
const unresolved = rs.
|
|
33
|
+
} else if (rs.divided > 0 || rs.allUncertain > 0 || rs.incomplete > 0) {
|
|
34
|
+
const unresolved = rs.divided + rs.allUncertain + rs.incomplete;
|
|
35
35
|
const nextBasis = detectNextBasis(rs.reviewers);
|
|
36
36
|
result.next.push({
|
|
37
|
-
command: `search-hub review extract --session ${sessionId} --filter
|
|
37
|
+
command: `search-hub review extract --session ${sessionId} --filter divided,all-uncertain,incomplete --basis ${nextBasis} --reviewer "<name>" --name ${nextBasis}-screening`,
|
|
38
38
|
description: `${unresolved} articles need ${nextBasis}-level review`
|
|
39
39
|
});
|
|
40
40
|
} else if (rs.finalized > 0 && rs.finalized === rs.total) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"next-steps.js","sources":["../../../../src/cli/commands/review/next-steps.ts"],"sourcesContent":["/**\n * Dynamic next steps generation for review workflow.\n * Generates context-aware suggestions based on current article status distribution.\n */\n\nimport type { ReviewStatusResult } from './status.js';\nimport type { ReviewBasis } from './types.js';\nimport type { Suggestion, SuggestionResult } from '../../suggestions/types.js';\n\nexport interface ReviewNextStepsContext {\n sessionId: string;\n statusResult: ReviewStatusResult;\n /** Extract name for batch continuation */\n extractName?: string;\n /** Number of articles extracted in current batch */\n extractedCount?: number;\n /** Total articles matching the filter */\n totalMatching?: number;\n /** Limit used in extract */\n limit?: number;\n /** Offset used in extract */\n offset?: number;\n}\n\nexport interface BatchContinuationParams {\n sessionId: string;\n extractName?: string | undefined;\n extractedCount: number;\n totalMatching: number;\n limit: number;\n offset?: number | undefined;\n}\n\n/**\n * Compute a batch continuation suggestion when --limit was used with remaining articles.\n * Returns null if no remaining articles.\n */\nexport function computeBatchContinuation(params: BatchContinuationParams): Suggestion | null {\n const nextOffset = (params.offset ?? 0) + params.extractedCount;\n const remaining = params.totalMatching - nextOffset;\n if (remaining <= 0) return null;\n\n const nextName = params.extractName ? `${params.extractName}-next` : 'next-batch';\n return {\n command: `search-hub review extract --session ${params.sessionId} --offset ${nextOffset} --limit ${params.limit} --name ${nextName}`,\n description: `${remaining} articles remaining — extract next batch`,\n };\n}\n\n/**\n * Detect the next review basis level from reviewer registry.\n * Progression: title → abstract → fulltext\n */\nfunction detectNextBasis(reviewers: ReviewStatusResult['reviewers']): ReviewBasis {\n const bases = new Set(reviewers.map((r) => r.basis));\n if (!bases.has('title')) return 'title';\n if (!bases.has('abstract')) return 'abstract';\n return 'fulltext';\n}\n\n/**\n * Generate dynamic next steps based on review status distribution.\n *\n * Evaluation order (top-to-bottom, first match wins for primary suggestion):\n * 1. pending > 0 → extract for title screening\n * 2. agreed > 0 → finalize consensus articles\n * 3. (
|
|
1
|
+
{"version":3,"file":"next-steps.js","sources":["../../../../src/cli/commands/review/next-steps.ts"],"sourcesContent":["/**\n * Dynamic next steps generation for review workflow.\n * Generates context-aware suggestions based on current article status distribution.\n */\n\nimport type { ReviewStatusResult } from './status.js';\nimport type { ReviewBasis } from './types.js';\nimport type { Suggestion, SuggestionResult } from '../../suggestions/types.js';\n\nexport interface ReviewNextStepsContext {\n sessionId: string;\n statusResult: ReviewStatusResult;\n /** Extract name for batch continuation */\n extractName?: string;\n /** Number of articles extracted in current batch */\n extractedCount?: number;\n /** Total articles matching the filter */\n totalMatching?: number;\n /** Limit used in extract */\n limit?: number;\n /** Offset used in extract */\n offset?: number;\n}\n\nexport interface BatchContinuationParams {\n sessionId: string;\n extractName?: string | undefined;\n extractedCount: number;\n totalMatching: number;\n limit: number;\n offset?: number | undefined;\n}\n\n/**\n * Compute a batch continuation suggestion when --limit was used with remaining articles.\n * Returns null if no remaining articles.\n */\nexport function computeBatchContinuation(params: BatchContinuationParams): Suggestion | null {\n const nextOffset = (params.offset ?? 0) + params.extractedCount;\n const remaining = params.totalMatching - nextOffset;\n if (remaining <= 0) return null;\n\n const nextName = params.extractName ? `${params.extractName}-next` : 'next-batch';\n return {\n command: `search-hub review extract --session ${params.sessionId} --offset ${nextOffset} --limit ${params.limit} --name ${nextName}`,\n description: `${remaining} articles remaining — extract next batch`,\n };\n}\n\n/**\n * Detect the next review basis level from reviewer registry.\n * Progression: title → abstract → fulltext\n */\nfunction detectNextBasis(reviewers: ReviewStatusResult['reviewers']): ReviewBasis {\n const bases = new Set(reviewers.map((r) => r.basis));\n if (!bases.has('title')) return 'title';\n if (!bases.has('abstract')) return 'abstract';\n return 'fulltext';\n}\n\n/**\n * Generate dynamic next steps based on review status distribution.\n *\n * Evaluation order (top-to-bottom, first match wins for primary suggestion):\n * 1. pending > 0 → extract for title screening\n * 2. agreed > 0 → finalize consensus articles\n * 3. (divided + allUncertain + incomplete) > 0 → extract for next basis level\n * 4. all finalized → register accepted articles\n *\n * Batch continuation is appended to seeAlso when applicable.\n */\nexport function generateReviewNextSteps(ctx: ReviewNextStepsContext): SuggestionResult | null {\n const { sessionId, statusResult: rs } = ctx;\n\n if (rs.total === 0) return null;\n\n const result: SuggestionResult = { next: [], seeAlso: [] };\n\n // 1. pending > 0: title screening incomplete\n if (rs.pending > 0) {\n result.next.push({\n command: `search-hub review extract --session ${sessionId} --basis title --filter pending --reviewer \"<name>\" --name title-screening`,\n description: `Extract ${rs.pending} pending articles for title screening`,\n });\n }\n // 2. agreed > 0: suggest finalization\n else {\n const agreed = rs.agreedInclude + rs.agreedExclude;\n if (agreed > 0) {\n result.next.push({\n command: `search-hub review finalize --session ${sessionId}`,\n description: `Finalize ${agreed} articles with consensus`,\n });\n }\n // 3. divided, all-uncertain, or incomplete > 0: suggest further review\n else if (rs.divided > 0 || rs.allUncertain > 0 || rs.incomplete > 0) {\n const unresolved = rs.divided + rs.allUncertain + rs.incomplete;\n const nextBasis = detectNextBasis(rs.reviewers);\n result.next.push({\n command: `search-hub review extract --session ${sessionId} --filter divided,all-uncertain,incomplete --basis ${nextBasis} --reviewer \"<name>\" --name ${nextBasis}-screening`,\n description: `${unresolved} articles need ${nextBasis}-level review`,\n });\n }\n // 4. All finalized\n else if (rs.finalized > 0 && rs.finalized === rs.total) {\n result.next.push({\n command: `search-hub register ${sessionId} --reviewed`,\n description: 'Register accepted articles',\n });\n }\n // No actionable state\n else {\n return null;\n }\n }\n\n // 5. Batch continuation (appended to seeAlso when applicable)\n if (\n ctx.limit !== undefined &&\n ctx.extractedCount !== undefined &&\n ctx.totalMatching !== undefined\n ) {\n const batch = computeBatchContinuation({\n sessionId,\n extractName: ctx.extractName,\n extractedCount: ctx.extractedCount,\n totalMatching: ctx.totalMatching,\n limit: ctx.limit,\n offset: ctx.offset,\n });\n if (batch) result.seeAlso.push(batch);\n }\n\n return result;\n}\n"],"names":[],"mappings":"AAqCO,SAAS,yBAAyB,QAAoD;AAC3F,QAAM,cAAc,OAAO,UAAU,KAAK,OAAO;AACjD,QAAM,YAAY,OAAO,gBAAgB;AACzC,MAAI,aAAa,EAAG,QAAO;AAE3B,QAAM,WAAW,OAAO,cAAc,GAAG,OAAO,WAAW,UAAU;AACrE,SAAO;AAAA,IACL,SAAS,uCAAuC,OAAO,SAAS,aAAa,UAAU,YAAY,OAAO,KAAK,WAAW,QAAQ;AAAA,IAClI,aAAa,GAAG,SAAS;AAAA,EAAA;AAE7B;AAMA,SAAS,gBAAgB,WAAyD;AAChF,QAAM,QAAQ,IAAI,IAAI,UAAU,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AACnD,MAAI,CAAC,MAAM,IAAI,OAAO,EAAG,QAAO;AAChC,MAAI,CAAC,MAAM,IAAI,UAAU,EAAG,QAAO;AACnC,SAAO;AACT;AAaO,SAAS,wBAAwB,KAAsD;AAC5F,QAAM,EAAE,WAAW,cAAc,GAAA,IAAO;AAExC,MAAI,GAAG,UAAU,EAAG,QAAO;AAE3B,QAAM,SAA2B,EAAE,MAAM,CAAA,GAAI,SAAS,CAAA,EAAC;AAGvD,MAAI,GAAG,UAAU,GAAG;AAClB,WAAO,KAAK,KAAK;AAAA,MACf,SAAS,uCAAuC,SAAS;AAAA,MACzD,aAAa,WAAW,GAAG,OAAO;AAAA,IAAA,CACnC;AAAA,EACH,OAEK;AACH,UAAM,SAAS,GAAG,gBAAgB,GAAG;AACrC,QAAI,SAAS,GAAG;AACd,aAAO,KAAK,KAAK;AAAA,QACf,SAAS,wCAAwC,SAAS;AAAA,QAC1D,aAAa,YAAY,MAAM;AAAA,MAAA,CAChC;AAAA,IACH,WAES,GAAG,UAAU,KAAK,GAAG,eAAe,KAAK,GAAG,aAAa,GAAG;AACnE,YAAM,aAAa,GAAG,UAAU,GAAG,eAAe,GAAG;AACrD,YAAM,YAAY,gBAAgB,GAAG,SAAS;AAC9C,aAAO,KAAK,KAAK;AAAA,QACf,SAAS,uCAAuC,SAAS,sDAAsD,SAAS,+BAA+B,SAAS;AAAA,QAChK,aAAa,GAAG,UAAU,kBAAkB,SAAS;AAAA,MAAA,CACtD;AAAA,IACH,WAES,GAAG,YAAY,KAAK,GAAG,cAAc,GAAG,OAAO;AACtD,aAAO,KAAK,KAAK;AAAA,QACf,SAAS,uBAAuB,SAAS;AAAA,QACzC,aAAa;AAAA,MAAA,CACd;AAAA,IACH,OAEK;AACH,aAAO;AAAA,IACT;AAAA,EACF;AAGA,MACE,IAAI,UAAU,UACd,IAAI,mBAAmB,UACvB,IAAI,kBAAkB,QACtB;AACA,UAAM,QAAQ,yBAAyB;AAAA,MACrC;AAAA,MACA,aAAa,IAAI;AAAA,MACjB,gBAAgB,IAAI;AAAA,MACpB,eAAe,IAAI;AAAA,MACnB,OAAO,IAAI;AAAA,MACX,QAAQ,IAAI;AAAA,IAAA,CACb;AACD,QAAI,MAAO,QAAO,QAAQ,KAAK,KAAK;AAAA,EACtC;AAEA,SAAO;AACT;"}
|
|
@@ -7,10 +7,10 @@ export interface ReviewStatusResult {
|
|
|
7
7
|
total: number;
|
|
8
8
|
pending: number;
|
|
9
9
|
incomplete: number;
|
|
10
|
-
|
|
10
|
+
allUncertain: number;
|
|
11
11
|
agreedInclude: number;
|
|
12
12
|
agreedExclude: number;
|
|
13
|
-
|
|
13
|
+
divided: number;
|
|
14
14
|
finalized: number;
|
|
15
15
|
included: number;
|
|
16
16
|
excluded: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/status.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAmC,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAElF,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/status.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAmC,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAElF,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,SAAS,EAAE,cAAc,EAAE,CAAC;CAC7B;AAWD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,mBAAmB,EAC5B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,kBAAkB,CAAC,CAwD7B;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAuBrE"}
|
|
@@ -14,10 +14,10 @@ async function executeReviewStatus(options, sessionsDir) {
|
|
|
14
14
|
const counts = {
|
|
15
15
|
pending: 0,
|
|
16
16
|
incomplete: 0,
|
|
17
|
-
|
|
17
|
+
allUncertain: 0,
|
|
18
18
|
agreedInclude: 0,
|
|
19
19
|
agreedExclude: 0,
|
|
20
|
-
|
|
20
|
+
divided: 0,
|
|
21
21
|
finalized: 0,
|
|
22
22
|
included: 0,
|
|
23
23
|
excluded: 0
|
|
@@ -31,8 +31,8 @@ async function executeReviewStatus(options, sessionsDir) {
|
|
|
31
31
|
case "incomplete":
|
|
32
32
|
counts.incomplete++;
|
|
33
33
|
break;
|
|
34
|
-
case "uncertain":
|
|
35
|
-
counts.
|
|
34
|
+
case "all-uncertain":
|
|
35
|
+
counts.allUncertain++;
|
|
36
36
|
break;
|
|
37
37
|
case "agreed-include":
|
|
38
38
|
counts.agreedInclude++;
|
|
@@ -40,8 +40,8 @@ async function executeReviewStatus(options, sessionsDir) {
|
|
|
40
40
|
case "agreed-exclude":
|
|
41
41
|
counts.agreedExclude++;
|
|
42
42
|
break;
|
|
43
|
-
case "
|
|
44
|
-
counts.
|
|
43
|
+
case "divided":
|
|
44
|
+
counts.divided++;
|
|
45
45
|
break;
|
|
46
46
|
case "finalized":
|
|
47
47
|
counts.finalized++;
|
|
@@ -65,13 +65,13 @@ function formatStatusOutput(result) {
|
|
|
65
65
|
const agreed = result.agreedInclude + result.agreedExclude;
|
|
66
66
|
const lines = [
|
|
67
67
|
`Review Progress: ${id}`,
|
|
68
|
-
` Total:
|
|
69
|
-
` Pending:
|
|
70
|
-
` Incomplete:
|
|
71
|
-
`
|
|
72
|
-
` Agreed:
|
|
73
|
-
`
|
|
74
|
-
` Finalized:
|
|
68
|
+
` Total: ${result.total}`,
|
|
69
|
+
` Pending: ${result.pending}`,
|
|
70
|
+
` Incomplete: ${result.incomplete}`,
|
|
71
|
+
` All-uncertain: ${result.allUncertain}`,
|
|
72
|
+
` Agreed: ${agreed} (include: ${result.agreedInclude}, exclude: ${result.agreedExclude})`,
|
|
73
|
+
` Divided: ${result.divided}`,
|
|
74
|
+
` Finalized: ${result.finalized} (include: ${result.included}, exclude: ${result.excluded})`
|
|
75
75
|
];
|
|
76
76
|
if (result.reviewers.length > 0) {
|
|
77
77
|
lines.push("");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.js","sources":["../../../../src/cli/commands/review/status.ts"],"sourcesContent":["/**\n * review status command - Show review progress summary\n */\n\nimport { join } from 'node:path';\nimport { readFile } from 'node:fs/promises';\nimport { parse as parseYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ReviewerRecord } from './types.js';\n\nexport interface ReviewStatusOptions {\n sessionId: string;\n}\n\nexport interface ReviewStatusResult {\n sessionId: string;\n total: number;\n pending: number;\n incomplete: number;\n
|
|
1
|
+
{"version":3,"file":"status.js","sources":["../../../../src/cli/commands/review/status.ts"],"sourcesContent":["/**\n * review status command - Show review progress summary\n */\n\nimport { join } from 'node:path';\nimport { readFile } from 'node:fs/promises';\nimport { parse as parseYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ReviewerRecord } from './types.js';\n\nexport interface ReviewStatusOptions {\n sessionId: string;\n}\n\nexport interface ReviewStatusResult {\n sessionId: string;\n total: number;\n pending: number;\n incomplete: number;\n allUncertain: number;\n agreedInclude: number;\n agreedExclude: number;\n divided: number;\n finalized: number;\n included: number;\n excluded: number;\n /** Registered reviewers from the review file */\n reviewers: ReviewerRecord[];\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 * Execute review status command\n */\nexport async function executeReviewStatus(\n options: ReviewStatusOptions,\n sessionsDir: string\n): Promise<ReviewStatusResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n const reviewFile = await loadReviewFile(sessionDir);\n\n const reviewers = reviewFile.reviewers ?? [];\n const counts = {\n pending: 0,\n incomplete: 0,\n allUncertain: 0,\n agreedInclude: 0,\n agreedExclude: 0,\n divided: 0,\n finalized: 0,\n included: 0,\n excluded: 0,\n };\n\n for (const article of reviewFile.articles) {\n const status = classifyStatus(article, reviewers);\n\n switch (status) {\n case 'pending':\n counts.pending++;\n break;\n case 'incomplete':\n counts.incomplete++;\n break;\n case 'all-uncertain':\n counts.allUncertain++;\n break;\n case 'agreed-include':\n counts.agreedInclude++;\n break;\n case 'agreed-exclude':\n counts.agreedExclude++;\n break;\n case 'divided':\n counts.divided++;\n break;\n case 'finalized':\n counts.finalized++;\n if (article.finalDecision === 'include') {\n counts.included++;\n } else {\n counts.excluded++;\n }\n break;\n }\n }\n\n return {\n sessionId: options.sessionId,\n total: reviewFile.articles.length,\n reviewers,\n ...counts,\n };\n}\n\n/**\n * Format status result as human-readable string\n */\nexport function formatStatusOutput(result: ReviewStatusResult): string {\n const id = result.sessionId;\n const agreed = result.agreedInclude + result.agreedExclude;\n const lines = [\n `Review Progress: ${id}`,\n ` Total: ${result.total}`,\n ` Pending: ${result.pending}`,\n ` Incomplete: ${result.incomplete}`,\n ` All-uncertain: ${result.allUncertain}`,\n ` Agreed: ${agreed} (include: ${result.agreedInclude}, exclude: ${result.agreedExclude})`,\n ` Divided: ${result.divided}`,\n ` Finalized: ${result.finalized} (include: ${result.included}, exclude: ${result.excluded})`,\n ];\n\n if (result.reviewers.length > 0) {\n lines.push('');\n lines.push('Reviewers:');\n for (const r of result.reviewers) {\n lines.push(` ${r.name} (${r.basis})`);\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":["parseYaml"],"mappings":";;;;AAgCA,eAAe,eAAe,YAAyC;AACrE,QAAM,cAAc,KAAK,YAAY,aAAa,cAAc;AAChE,QAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,SAAOA,MAAU,OAAO;AAC1B;AAKA,eAAsB,oBACpB,SACA,aAC6B;AAC7B,QAAM,aAAa,KAAK,aAAa,QAAQ,SAAS;AACtD,QAAM,aAAa,MAAM,eAAe,UAAU;AAElD,QAAM,YAAY,WAAW,aAAa,CAAA;AAC1C,QAAM,SAAS;AAAA,IACb,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,eAAe;AAAA,IACf,eAAe;AAAA,IACf,SAAS;AAAA,IACT,WAAW;AAAA,IACX,UAAU;AAAA,IACV,UAAU;AAAA,EAAA;AAGZ,aAAW,WAAW,WAAW,UAAU;AACzC,UAAM,SAAS,eAAe,SAAS,SAAS;AAEhD,YAAQ,QAAA;AAAA,MACN,KAAK;AACH,eAAO;AACP;AAAA,MACF,KAAK;AACH,eAAO;AACP;AAAA,MACF,KAAK;AACH,eAAO;AACP;AAAA,MACF,KAAK;AACH,eAAO;AACP;AAAA,MACF,KAAK;AACH,eAAO;AACP;AAAA,MACF,KAAK;AACH,eAAO;AACP;AAAA,MACF,KAAK;AACH,eAAO;AACP,YAAI,QAAQ,kBAAkB,WAAW;AACvC,iBAAO;AAAA,QACT,OAAO;AACL,iBAAO;AAAA,QACT;AACA;AAAA,IAAA;AAAA,EAEN;AAEA,SAAO;AAAA,IACL,WAAW,QAAQ;AAAA,IACnB,OAAO,WAAW,SAAS;AAAA,IAC3B;AAAA,IACA,GAAG;AAAA,EAAA;AAEP;AAKO,SAAS,mBAAmB,QAAoC;AACrE,QAAM,KAAK,OAAO;AAClB,QAAM,SAAS,OAAO,gBAAgB,OAAO;AAC7C,QAAM,QAAQ;AAAA,IACZ,oBAAoB,EAAE;AAAA,IACtB,sBAAsB,OAAO,KAAK;AAAA,IAClC,sBAAsB,OAAO,OAAO;AAAA,IACpC,sBAAsB,OAAO,UAAU;AAAA,IACvC,sBAAsB,OAAO,YAAY;AAAA,IACzC,sBAAsB,MAAM,eAAe,OAAO,aAAa,cAAc,OAAO,aAAa;AAAA,IACjG,sBAAsB,OAAO,OAAO;AAAA,IACpC,sBAAsB,OAAO,SAAS,eAAe,OAAO,QAAQ,cAAc,OAAO,QAAQ;AAAA,EAAA;AAGnG,MAAI,OAAO,UAAU,SAAS,GAAG;AAC/B,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,YAAY;AACvB,eAAW,KAAK,OAAO,WAAW;AAChC,YAAM,KAAK,KAAK,EAAE,IAAI,MAAM,EAAE,KAAK,GAAG;AAAA,IACxC;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
|
|
@@ -41,7 +41,7 @@ export declare function basisRank(basis: ReviewBasis | undefined): number;
|
|
|
41
41
|
/**
|
|
42
42
|
* Review status classification (7-state model)
|
|
43
43
|
*/
|
|
44
|
-
export type ReviewStatus = 'pending' | 'incomplete' | 'uncertain' | 'agreed-include' | 'agreed-exclude' | '
|
|
44
|
+
export type ReviewStatus = 'pending' | 'incomplete' | 'all-uncertain' | 'agreed-include' | 'agreed-exclude' | 'divided' | 'finalized';
|
|
45
45
|
/**
|
|
46
46
|
* Classify the review status of an article entry
|
|
47
47
|
*
|
|
@@ -49,10 +49,10 @@ export type ReviewStatus = 'pending' | 'incomplete' | 'uncertain' | 'agreed-incl
|
|
|
49
49
|
* 1. finalDecision set? → finalized
|
|
50
50
|
* 2. No reviews? → pending
|
|
51
51
|
* 3. Registered reviewer missing? → incomplete
|
|
52
|
-
* 4.
|
|
53
|
-
* 5.
|
|
54
|
-
* 6. All
|
|
55
|
-
* 7.
|
|
52
|
+
* 4. All uncertain? → all-uncertain
|
|
53
|
+
* 5. All include? → agreed-include
|
|
54
|
+
* 6. All exclude? → agreed-exclude
|
|
55
|
+
* 7. Any mix of decisions? → divided
|
|
56
56
|
*/
|
|
57
57
|
export declare function classifyStatus(entry: ArticleEntry, registeredReviewers?: ReviewerRecord[]): ReviewStatus;
|
|
58
58
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EACjB,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D;;GAEG;AACH,iGAAiG;AACjG,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,mGAAmG;AACnG,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAWD,wBAAgB,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,MAAM,CAGhE;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GACpB,SAAS,GACT,YAAY,GACZ,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EACjB,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D;;GAEG;AACH,iGAAiG;AACjG,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,mGAAmG;AACnG,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAWD,wBAAgB,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,MAAM,CAGhE;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GACpB,SAAS,GACT,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,SAAS,GACT,WAAW,CAAC;AAEhB;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,YAAY,EACnB,mBAAmB,CAAC,EAAE,cAAc,EAAE,GACrC,YAAY,CAmHd"}
|
|
@@ -69,19 +69,16 @@ function classifyStatus(entry, registeredReviewers) {
|
|
|
69
69
|
if (effectiveDecisions.length === 0) {
|
|
70
70
|
return "pending";
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (hasInclude && hasExclude) {
|
|
75
|
-
return "conflicting";
|
|
76
|
-
}
|
|
77
|
-
const hasUncertain = effectiveDecisions.includes("uncertain");
|
|
78
|
-
if (hasUncertain) {
|
|
79
|
-
return "uncertain";
|
|
72
|
+
if (effectiveDecisions.every((d) => d === "uncertain")) {
|
|
73
|
+
return "all-uncertain";
|
|
80
74
|
}
|
|
81
75
|
if (effectiveDecisions.every((d) => d === "include")) {
|
|
82
76
|
return "agreed-include";
|
|
83
77
|
}
|
|
84
|
-
|
|
78
|
+
if (effectiveDecisions.every((d) => d === "exclude")) {
|
|
79
|
+
return "agreed-exclude";
|
|
80
|
+
}
|
|
81
|
+
return "divided";
|
|
85
82
|
}
|
|
86
83
|
export {
|
|
87
84
|
basisRank,
|
|
@@ -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 * Core data types (ReviewDecision, ReviewBasis, Review, MergedSource,\n * ArticleEntry, ReviewerRecord, ReviewFile) are derived from Zod schemas\n * in schema.ts — single source of truth for types and JSON Schema.\n */\nimport * as z from 'zod';\nimport {\n reviewDecisionSchema,\n reviewBasisSchema,\n reviewSchema,\n mergedSourceSchema,\n articleEntrySchema,\n reviewerRecordSchema,\n reviewFileSchema,\n} from './schema.js';\n\nexport type ReviewDecision = z.infer<typeof reviewDecisionSchema>;\nexport type ReviewBasis = z.infer<typeof reviewBasisSchema>;\nexport type Review = z.infer<typeof reviewSchema>;\nexport type MergedSource = z.infer<typeof mergedSourceSchema>;\nexport type ArticleEntry = z.infer<typeof articleEntrySchema>;\nexport type ReviewerRecord = z.infer<typeof reviewerRecordSchema>;\nexport type ReviewFile = z.infer<typeof reviewFileSchema>;\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 | '
|
|
1
|
+
{"version":3,"file":"types.js","sources":["../../../../src/cli/commands/review/types.ts"],"sourcesContent":["/**\n * Review workflow types for article assessment tracking.\n *\n * Core data types (ReviewDecision, ReviewBasis, Review, MergedSource,\n * ArticleEntry, ReviewerRecord, ReviewFile) are derived from Zod schemas\n * in schema.ts — single source of truth for types and JSON Schema.\n */\nimport * as z from 'zod';\nimport {\n reviewDecisionSchema,\n reviewBasisSchema,\n reviewSchema,\n mergedSourceSchema,\n articleEntrySchema,\n reviewerRecordSchema,\n reviewFileSchema,\n} from './schema.js';\n\nexport type ReviewDecision = z.infer<typeof reviewDecisionSchema>;\nexport type ReviewBasis = z.infer<typeof reviewBasisSchema>;\nexport type Review = z.infer<typeof reviewSchema>;\nexport type MergedSource = z.infer<typeof mergedSourceSchema>;\nexport type ArticleEntry = z.infer<typeof articleEntrySchema>;\nexport type ReviewerRecord = z.infer<typeof reviewerRecordSchema>;\nexport type ReviewFile = z.infer<typeof reviewFileSchema>;\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 | 'all-uncertain'\n | 'agreed-include'\n | 'agreed-exclude'\n | 'divided'\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. All uncertain? → all-uncertain\n * 5. All include? → agreed-include\n * 6. All exclude? → agreed-exclude\n * 7. Any mix of decisions? → divided\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 all-uncertain: every effective decision is uncertain\n if (effectiveDecisions.every((d) => d === 'uncertain')) {\n return 'all-uncertain';\n }\n\n // 5. All include?\n if (effectiveDecisions.every((d) => d === 'include')) {\n return 'agreed-include';\n }\n\n // 6. All exclude?\n if (effectiveDecisions.every((d) => d === 'exclude')) {\n return 'agreed-exclude';\n }\n\n // 7. Any mix of different decisions → divided\n return 'divided';\n}\n"],"names":[],"mappings":";;AAsDA,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,MAAI,mBAAmB,MAAM,CAAC,MAAM,MAAM,WAAW,GAAG;AACtD,WAAO;AAAA,EACT;AAGA,MAAI,mBAAmB,MAAM,CAAC,MAAM,MAAM,SAAS,GAAG;AACpD,WAAO;AAAA,EACT;AAGA,MAAI,mBAAmB,MAAM,CAAC,MAAM,MAAM,SAAS,GAAG;AACpD,WAAO;AAAA,EACT;AAGA,SAAO;AACT;"}
|
package/dist/cli/index.js
CHANGED
|
@@ -1648,10 +1648,10 @@ Examples:
|
|
|
1648
1648
|
process.exitCode = EXIT_CODES.SESSION_ERROR;
|
|
1649
1649
|
}
|
|
1650
1650
|
});
|
|
1651
|
-
reviewCommand.command("list").description("List articles with optional filtering").requiredOption("--session <id>", "session ID").option("--filter <type>", "filter by status: pending, incomplete, uncertain, agreed-include, agreed-exclude,
|
|
1651
|
+
reviewCommand.command("list").description("List articles with optional filtering").requiredOption("--session <id>", "session ID").option("--filter <type>", "filter by status: pending, incomplete, all-uncertain, agreed-include, agreed-exclude, divided, finalized, all", "all").option("--json", "output as JSON").action(async (options) => {
|
|
1652
1652
|
const globalOpts = program.opts();
|
|
1653
1653
|
try {
|
|
1654
|
-
const validFilters = ["pending", "incomplete", "uncertain", "agreed-include", "agreed-exclude", "
|
|
1654
|
+
const validFilters = ["pending", "incomplete", "all-uncertain", "agreed-include", "agreed-exclude", "divided", "finalized", "all"];
|
|
1655
1655
|
const filter = options.filter ?? "all";
|
|
1656
1656
|
if (!validFilters.includes(filter)) {
|
|
1657
1657
|
if (!globalOpts.quiet) {
|
|
@@ -1681,7 +1681,7 @@ Examples:
|
|
|
1681
1681
|
process.exitCode = EXIT_CODES.SESSION_ERROR;
|
|
1682
1682
|
}
|
|
1683
1683
|
});
|
|
1684
|
-
reviewCommand.command("extract").description("Extract subset to for-review/<name>/review.yaml for distributed review").requiredOption("--session <id>", "session ID").requiredOption("--name <name>", "name for the review subset (output: for-review/<name>/review.yaml)").option("--filter <types>", "filter by status (comma-separated): pending, incomplete, uncertain, agreed-include, agreed-exclude,
|
|
1684
|
+
reviewCommand.command("extract").description("Extract subset to for-review/<name>/review.yaml for distributed review").requiredOption("--session <id>", "session ID").requiredOption("--name <name>", "name for the review subset (output: for-review/<name>/review.yaml)").option("--filter <types>", "filter by status (comma-separated): pending, incomplete, all-uncertain, agreed-include, agreed-exclude, divided, finalized").option("--sort <method>", "sort method: year, title, random, none", "none").option("--limit <n>", "limit number of articles").option("--offset <n>", "skip first n articles").option("--seed <n>", "random seed for reproducible sorting").option("--basis <type>", "basis for review: title, abstract, or fulltext").option("--reviewer <id>", 'reviewer identifier (e.g., "ai:claude")').option("--finalize", "extract for final decision (includes reviewHistory and finalDecision)").action(async (options) => {
|
|
1685
1685
|
const globalOpts = program.opts();
|
|
1686
1686
|
try {
|
|
1687
1687
|
const validSorts = ["year", "title", "random", "none"];
|