@ncukondo/search-hub 0.15.1 → 0.17.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.
@@ -18,9 +18,6 @@ export interface CommandLineOptions {
18
18
  output?: string | undefined;
19
19
  idType?: string | undefined;
20
20
  query?: string | undefined;
21
- filterYear?: string | undefined;
22
- filterTitle?: string | undefined;
23
- filterAbstract?: string | undefined;
24
21
  }
25
22
  export interface ValidationResult {
26
23
  valid: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/export.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAI7D,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;AACjE,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC;AAE5C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kBAAkB,GAC1B,oBAAoB,CAetB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,gBAAgB,CAgCnF;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CA+BrE;AAeD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,QAAQ,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAuBrF;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAKvD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAGzD;AAkBD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,mBAAmB,CAiD5E;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,EAAE,CAmCnF"}
1
+ {"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/export.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAI7D,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;AACjE,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC;AAE5C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kBAAkB,GAC1B,oBAAoB,CAetB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,gBAAgB,CAgCnF;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CA+BrE;AAeD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,QAAQ,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAuBrF;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAKvD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAGzD;AAkBD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,mBAAmB,CAiD5E;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,EAAE,CAmCnF"}
@@ -177,37 +177,8 @@ function deduplicateArticles(articles) {
177
177
  }
178
178
  return { articles: unique, duplicatesRemoved };
179
179
  }
180
- function filterArticles(articles, filter) {
181
- const hasYearFilter = filter.yearFrom !== void 0 || filter.yearTo !== void 0;
182
- const hasTitleFilter = filter.titleKeywords !== void 0 && filter.titleKeywords.length > 0;
183
- const hasAbstractFilter = filter.abstractKeywords !== void 0 && filter.abstractKeywords.length > 0;
184
- if (!hasYearFilter && !hasTitleFilter && !hasAbstractFilter) {
185
- return articles;
186
- }
187
- return articles.filter((article) => {
188
- if (hasYearFilter) {
189
- const year = extractYear(article.publicationDate);
190
- if (year === null) return false;
191
- if (filter.yearFrom !== void 0 && year < filter.yearFrom) return false;
192
- if (filter.yearTo !== void 0 && year > filter.yearTo) return false;
193
- }
194
- if (hasTitleFilter) {
195
- const titleLower = article.title.toLowerCase();
196
- const matched = filter.titleKeywords.some((kw) => titleLower.includes(kw.toLowerCase()));
197
- if (!matched) return false;
198
- }
199
- if (hasAbstractFilter) {
200
- if (!article.abstract) return false;
201
- const abstractLower = article.abstract.toLowerCase();
202
- const matched = filter.abstractKeywords.some((kw) => abstractLower.includes(kw.toLowerCase()));
203
- if (!matched) return false;
204
- }
205
- return true;
206
- });
207
- }
208
180
  export {
209
181
  deduplicateArticles,
210
- filterArticles,
211
182
  formatCslJson,
212
183
  formatIds,
213
184
  formatJson,
@@ -1 +1 @@
1
- {"version":3,"file":"export.js","sources":["../../../src/cli/commands/export.ts"],"sourcesContent":["import type { Article } from '../../providers/base/types.js';\nimport { articlesToCslJson } from '../../integration/csl-json.js';\nimport { getArticleKeys } from './session-utils.js';\n\nexport type ExportFormat = 'ids' | 'json' | 'jsonl' | 'csl-json';\nexport type IdType = 'doi' | 'pmid' | 'all';\n\nexport interface ExportFilter {\n yearFrom?: number;\n yearTo?: number;\n titleKeywords?: string[];\n abstractKeywords?: string[];\n}\n\nexport interface ExportCommandOptions {\n sessionId: string;\n format: ExportFormat;\n outputPath?: string;\n idType?: IdType;\n}\n\nexport interface CommandLineOptions {\n format?: string | undefined;\n output?: string | undefined;\n idType?: string | undefined;\n query?: string | undefined;\n filterYear?: string | undefined;\n filterTitle?: string | undefined;\n filterAbstract?: string | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\nconst VALID_FORMATS: ExportFormat[] = ['ids', 'json', 'jsonl', 'csl-json'];\nconst VALID_ID_TYPES: IdType[] = ['doi', 'pmid', 'all'];\n\nexport function parseExportOptions(\n sessionId: string,\n options: CommandLineOptions\n): ExportCommandOptions {\n const result: ExportCommandOptions = {\n sessionId,\n format: (options.format as ExportFormat) || 'jsonl',\n };\n\n if (options.output) {\n result.outputPath = options.output;\n }\n\n if (options.idType) {\n result.idType = options.idType as IdType;\n }\n\n return result;\n}\n\nexport function validateExportInput(options: ExportCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n if (!VALID_FORMATS.includes(options.format)) {\n return {\n valid: false,\n error: `Invalid format: ${options.format}. Valid formats are: ${VALID_FORMATS.join(', ')}`,\n };\n }\n\n if (options.idType) {\n if (!VALID_ID_TYPES.includes(options.idType)) {\n return {\n valid: false,\n error: `Invalid id-type: ${options.idType}. Valid types are: ${VALID_ID_TYPES.join(', ')}`,\n };\n }\n\n if (options.format !== 'ids') {\n return {\n valid: false,\n error: '--id-type can only be used with --format ids',\n };\n }\n }\n\n return { valid: true };\n}\n\nexport function formatIds(articles: Article[], idType: IdType): string {\n if (idType === 'all') {\n const groups: string[] = [];\n for (const article of articles) {\n const ids: string[] = [];\n if (article.pmid) ids.push(`pmid:${article.pmid}`);\n if (article.doi) ids.push(`doi:${article.doi}`);\n if (article.arxivId) ids.push(`arxiv:${article.arxivId}`);\n if (article.scopusId) ids.push(`scopus:${article.scopusId}`);\n if (article.ericId) ids.push(`eric:${article.ericId}`);\n if (ids.length > 0) {\n groups.push(ids.join('\\n'));\n }\n }\n return groups.join('\\n\\n');\n }\n\n const ids: string[] = [];\n for (const article of articles) {\n if (idType === 'doi') {\n if (article.doi) {\n ids.push(article.doi);\n }\n } else if (idType === 'pmid') {\n if (article.pmid) {\n ids.push(article.pmid);\n }\n }\n }\n\n return ids.join('\\n');\n}\n\nfunction extractYear(publicationDate: string | undefined): number | null {\n if (!publicationDate) return null;\n const year = parseInt(publicationDate.substring(0, 4), 10);\n return Number.isNaN(year) ? null : year;\n}\n\nfunction addYearField(articles: Article[]): (Article & { year: number | null })[] {\n return articles.map((article) => ({\n ...article,\n year: extractYear(article.publicationDate),\n }));\n}\n\nexport interface JsonExportMetadata {\n sessionId: string;\n sessionName: string;\n createdAt: string;\n databases: Record<string, number>;\n}\n\nexport function formatJson(articles: Article[], metadata?: JsonExportMetadata): string {\n const articlesWithYear = addYearField(articles);\n\n if (!metadata) {\n return JSON.stringify(articlesWithYear, null, 2);\n }\n\n return JSON.stringify(\n {\n session: {\n id: metadata.sessionId,\n name: metadata.sessionName,\n createdAt: metadata.createdAt,\n },\n summary: {\n totalResults: articles.length,\n databases: metadata.databases,\n },\n results: articlesWithYear,\n },\n null,\n 2\n );\n}\n\nexport function formatJsonl(articles: Article[]): string {\n if (articles.length === 0) {\n return '';\n }\n return addYearField(articles).map((article) => JSON.stringify(article)).join('\\n');\n}\n\nexport function formatCslJson(articles: Article[]): string {\n const cslItems = articlesToCslJson(articles);\n return JSON.stringify(cslItems, null, 2);\n}\n\n\nconst METADATA_FIELDS: (keyof Article)[] = [\n 'doi', 'pmid', 'arxivId', 'scopusId', 'ericId',\n 'abstract', 'publicationDate', 'journal', 'volume', 'issue', 'pages',\n];\n\nfunction countMetadataFields(article: Article): number {\n let count = 0;\n for (const field of METADATA_FIELDS) {\n if (article[field] !== undefined && article[field] !== '') {\n count++;\n }\n }\n return count;\n}\n\nexport interface DeduplicationResult {\n articles: Article[];\n duplicatesRemoved: number;\n}\n\nexport function deduplicateArticles(articles: Article[]): DeduplicationResult {\n // Map from identifier key to index in the unique array\n const keyToIndex = new Map<string, number>();\n const unique: Article[] = [];\n let duplicatesRemoved = 0;\n\n for (const article of articles) {\n const keys = getArticleKeys(article);\n\n if (keys.length === 0) {\n // No identifiers - cannot deduplicate, keep the article\n unique.push(article);\n continue;\n }\n\n // Check if any identifier has been seen before\n let existingIndex: number | undefined;\n for (const key of keys) {\n const idx = keyToIndex.get(key);\n if (idx !== undefined) {\n existingIndex = idx;\n break;\n }\n }\n\n if (existingIndex !== undefined) {\n // Duplicate found - compare metadata richness\n const existing = unique[existingIndex]!;\n if (countMetadataFields(article) > countMetadataFields(existing)) {\n // Replace with the richer record\n unique[existingIndex] = article;\n // Update all keys to point to the same index\n const newKeys = getArticleKeys(article);\n for (const key of newKeys) {\n keyToIndex.set(key, existingIndex);\n }\n }\n duplicatesRemoved++;\n } else {\n const index = unique.length;\n unique.push(article);\n // Map all identifiers to this index\n for (const key of keys) {\n keyToIndex.set(key, index);\n }\n }\n }\n\n return { articles: unique, duplicatesRemoved };\n}\n\nexport function filterArticles(articles: Article[], filter: ExportFilter): Article[] {\n const hasYearFilter = filter.yearFrom !== undefined || filter.yearTo !== undefined;\n const hasTitleFilter = filter.titleKeywords !== undefined && filter.titleKeywords.length > 0;\n const hasAbstractFilter = filter.abstractKeywords !== undefined && filter.abstractKeywords.length > 0;\n\n if (!hasYearFilter && !hasTitleFilter && !hasAbstractFilter) {\n return articles;\n }\n\n return articles.filter((article) => {\n // Year filter (AND with other filters)\n if (hasYearFilter) {\n const year = extractYear(article.publicationDate);\n if (year === null) return false;\n if (filter.yearFrom !== undefined && year < filter.yearFrom) return false;\n if (filter.yearTo !== undefined && year > filter.yearTo) return false;\n }\n\n // Title keyword filter (OR within keywords, AND with other filters)\n if (hasTitleFilter) {\n const titleLower = article.title.toLowerCase();\n const matched = filter.titleKeywords!.some((kw) => titleLower.includes(kw.toLowerCase()));\n if (!matched) return false;\n }\n\n // Abstract keyword filter (OR within keywords, AND with other filters)\n if (hasAbstractFilter) {\n if (!article.abstract) return false;\n const abstractLower = article.abstract.toLowerCase();\n const matched = filter.abstractKeywords!.some((kw) => abstractLower.includes(kw.toLowerCase()));\n if (!matched) return false;\n }\n\n return true;\n });\n}\n"],"names":["ids"],"mappings":"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"}
1
+ {"version":3,"file":"export.js","sources":["../../../src/cli/commands/export.ts"],"sourcesContent":["import type { Article } from '../../providers/base/types.js';\nimport { articlesToCslJson } from '../../integration/csl-json.js';\nimport { getArticleKeys } from './session-utils.js';\n\nexport type ExportFormat = 'ids' | 'json' | 'jsonl' | 'csl-json';\nexport type IdType = 'doi' | 'pmid' | 'all';\n\nexport interface ExportFilter {\n yearFrom?: number;\n yearTo?: number;\n titleKeywords?: string[];\n abstractKeywords?: string[];\n}\n\nexport interface ExportCommandOptions {\n sessionId: string;\n format: ExportFormat;\n outputPath?: string;\n idType?: IdType;\n}\n\nexport interface CommandLineOptions {\n format?: string | undefined;\n output?: string | undefined;\n idType?: string | undefined;\n query?: string | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\nconst VALID_FORMATS: ExportFormat[] = ['ids', 'json', 'jsonl', 'csl-json'];\nconst VALID_ID_TYPES: IdType[] = ['doi', 'pmid', 'all'];\n\nexport function parseExportOptions(\n sessionId: string,\n options: CommandLineOptions\n): ExportCommandOptions {\n const result: ExportCommandOptions = {\n sessionId,\n format: (options.format as ExportFormat) || 'jsonl',\n };\n\n if (options.output) {\n result.outputPath = options.output;\n }\n\n if (options.idType) {\n result.idType = options.idType as IdType;\n }\n\n return result;\n}\n\nexport function validateExportInput(options: ExportCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n if (!VALID_FORMATS.includes(options.format)) {\n return {\n valid: false,\n error: `Invalid format: ${options.format}. Valid formats are: ${VALID_FORMATS.join(', ')}`,\n };\n }\n\n if (options.idType) {\n if (!VALID_ID_TYPES.includes(options.idType)) {\n return {\n valid: false,\n error: `Invalid id-type: ${options.idType}. Valid types are: ${VALID_ID_TYPES.join(', ')}`,\n };\n }\n\n if (options.format !== 'ids') {\n return {\n valid: false,\n error: '--id-type can only be used with --format ids',\n };\n }\n }\n\n return { valid: true };\n}\n\nexport function formatIds(articles: Article[], idType: IdType): string {\n if (idType === 'all') {\n const groups: string[] = [];\n for (const article of articles) {\n const ids: string[] = [];\n if (article.pmid) ids.push(`pmid:${article.pmid}`);\n if (article.doi) ids.push(`doi:${article.doi}`);\n if (article.arxivId) ids.push(`arxiv:${article.arxivId}`);\n if (article.scopusId) ids.push(`scopus:${article.scopusId}`);\n if (article.ericId) ids.push(`eric:${article.ericId}`);\n if (ids.length > 0) {\n groups.push(ids.join('\\n'));\n }\n }\n return groups.join('\\n\\n');\n }\n\n const ids: string[] = [];\n for (const article of articles) {\n if (idType === 'doi') {\n if (article.doi) {\n ids.push(article.doi);\n }\n } else if (idType === 'pmid') {\n if (article.pmid) {\n ids.push(article.pmid);\n }\n }\n }\n\n return ids.join('\\n');\n}\n\nfunction extractYear(publicationDate: string | undefined): number | null {\n if (!publicationDate) return null;\n const year = parseInt(publicationDate.substring(0, 4), 10);\n return Number.isNaN(year) ? null : year;\n}\n\nfunction addYearField(articles: Article[]): (Article & { year: number | null })[] {\n return articles.map((article) => ({\n ...article,\n year: extractYear(article.publicationDate),\n }));\n}\n\nexport interface JsonExportMetadata {\n sessionId: string;\n sessionName: string;\n createdAt: string;\n databases: Record<string, number>;\n}\n\nexport function formatJson(articles: Article[], metadata?: JsonExportMetadata): string {\n const articlesWithYear = addYearField(articles);\n\n if (!metadata) {\n return JSON.stringify(articlesWithYear, null, 2);\n }\n\n return JSON.stringify(\n {\n session: {\n id: metadata.sessionId,\n name: metadata.sessionName,\n createdAt: metadata.createdAt,\n },\n summary: {\n totalResults: articles.length,\n databases: metadata.databases,\n },\n results: articlesWithYear,\n },\n null,\n 2\n );\n}\n\nexport function formatJsonl(articles: Article[]): string {\n if (articles.length === 0) {\n return '';\n }\n return addYearField(articles).map((article) => JSON.stringify(article)).join('\\n');\n}\n\nexport function formatCslJson(articles: Article[]): string {\n const cslItems = articlesToCslJson(articles);\n return JSON.stringify(cslItems, null, 2);\n}\n\n\nconst METADATA_FIELDS: (keyof Article)[] = [\n 'doi', 'pmid', 'arxivId', 'scopusId', 'ericId',\n 'abstract', 'publicationDate', 'journal', 'volume', 'issue', 'pages',\n];\n\nfunction countMetadataFields(article: Article): number {\n let count = 0;\n for (const field of METADATA_FIELDS) {\n if (article[field] !== undefined && article[field] !== '') {\n count++;\n }\n }\n return count;\n}\n\nexport interface DeduplicationResult {\n articles: Article[];\n duplicatesRemoved: number;\n}\n\nexport function deduplicateArticles(articles: Article[]): DeduplicationResult {\n // Map from identifier key to index in the unique array\n const keyToIndex = new Map<string, number>();\n const unique: Article[] = [];\n let duplicatesRemoved = 0;\n\n for (const article of articles) {\n const keys = getArticleKeys(article);\n\n if (keys.length === 0) {\n // No identifiers - cannot deduplicate, keep the article\n unique.push(article);\n continue;\n }\n\n // Check if any identifier has been seen before\n let existingIndex: number | undefined;\n for (const key of keys) {\n const idx = keyToIndex.get(key);\n if (idx !== undefined) {\n existingIndex = idx;\n break;\n }\n }\n\n if (existingIndex !== undefined) {\n // Duplicate found - compare metadata richness\n const existing = unique[existingIndex]!;\n if (countMetadataFields(article) > countMetadataFields(existing)) {\n // Replace with the richer record\n unique[existingIndex] = article;\n // Update all keys to point to the same index\n const newKeys = getArticleKeys(article);\n for (const key of newKeys) {\n keyToIndex.set(key, existingIndex);\n }\n }\n duplicatesRemoved++;\n } else {\n const index = unique.length;\n unique.push(article);\n // Map all identifiers to this index\n for (const key of keys) {\n keyToIndex.set(key, index);\n }\n }\n }\n\n return { articles: unique, duplicatesRemoved };\n}\n\nexport function filterArticles(articles: Article[], filter: ExportFilter): Article[] {\n const hasYearFilter = filter.yearFrom !== undefined || filter.yearTo !== undefined;\n const hasTitleFilter = filter.titleKeywords !== undefined && filter.titleKeywords.length > 0;\n const hasAbstractFilter = filter.abstractKeywords !== undefined && filter.abstractKeywords.length > 0;\n\n if (!hasYearFilter && !hasTitleFilter && !hasAbstractFilter) {\n return articles;\n }\n\n return articles.filter((article) => {\n // Year filter (AND with other filters)\n if (hasYearFilter) {\n const year = extractYear(article.publicationDate);\n if (year === null) return false;\n if (filter.yearFrom !== undefined && year < filter.yearFrom) return false;\n if (filter.yearTo !== undefined && year > filter.yearTo) return false;\n }\n\n // Title keyword filter (OR within keywords, AND with other filters)\n if (hasTitleFilter) {\n const titleLower = article.title.toLowerCase();\n const matched = filter.titleKeywords!.some((kw) => titleLower.includes(kw.toLowerCase()));\n if (!matched) return false;\n }\n\n // Abstract keyword filter (OR within keywords, AND with other filters)\n if (hasAbstractFilter) {\n if (!article.abstract) return false;\n const abstractLower = article.abstract.toLowerCase();\n const matched = filter.abstractKeywords!.some((kw) => abstractLower.includes(kw.toLowerCase()));\n if (!matched) return false;\n }\n\n return true;\n });\n}\n"],"names":["ids"],"mappings":";;AAiCA,MAAM,gBAAgC,CAAC,OAAO,QAAQ,SAAS,UAAU;AACzE,MAAM,iBAA2B,CAAC,OAAO,QAAQ,KAAK;AAE/C,SAAS,mBACd,WACA,SACsB;AACtB,QAAM,SAA+B;AAAA,IACnC;AAAA,IACA,QAAS,QAAQ,UAA2B;AAAA,EAAA;AAG9C,MAAI,QAAQ,QAAQ;AAClB,WAAO,aAAa,QAAQ;AAAA,EAC9B;AAEA,MAAI,QAAQ,QAAQ;AAClB,WAAO,SAAS,QAAQ;AAAA,EAC1B;AAEA,SAAO;AACT;AAEO,SAAS,oBAAoB,SAAiD;AACnF,MAAI,CAAC,QAAQ,aAAa,QAAQ,UAAU,KAAA,MAAW,IAAI;AACzD,WAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO;AAAA,IAAA;AAAA,EAEX;AAEA,MAAI,CAAC,cAAc,SAAS,QAAQ,MAAM,GAAG;AAC3C,WAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO,mBAAmB,QAAQ,MAAM,wBAAwB,cAAc,KAAK,IAAI,CAAC;AAAA,IAAA;AAAA,EAE5F;AAEA,MAAI,QAAQ,QAAQ;AAClB,QAAI,CAAC,eAAe,SAAS,QAAQ,MAAM,GAAG;AAC5C,aAAO;AAAA,QACL,OAAO;AAAA,QACP,OAAO,oBAAoB,QAAQ,MAAM,sBAAsB,eAAe,KAAK,IAAI,CAAC;AAAA,MAAA;AAAA,IAE5F;AAEA,QAAI,QAAQ,WAAW,OAAO;AAC5B,aAAO;AAAA,QACL,OAAO;AAAA,QACP,OAAO;AAAA,MAAA;AAAA,IAEX;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,KAAA;AAClB;AAEO,SAAS,UAAU,UAAqB,QAAwB;AACrE,MAAI,WAAW,OAAO;AACpB,UAAM,SAAmB,CAAA;AACzB,eAAW,WAAW,UAAU;AAC9B,YAAMA,OAAgB,CAAA;AACtB,UAAI,QAAQ,KAAMA,MAAI,KAAK,QAAQ,QAAQ,IAAI,EAAE;AACjD,UAAI,QAAQ,IAAKA,MAAI,KAAK,OAAO,QAAQ,GAAG,EAAE;AAC9C,UAAI,QAAQ,QAASA,MAAI,KAAK,SAAS,QAAQ,OAAO,EAAE;AACxD,UAAI,QAAQ,SAAUA,MAAI,KAAK,UAAU,QAAQ,QAAQ,EAAE;AAC3D,UAAI,QAAQ,OAAQA,MAAI,KAAK,QAAQ,QAAQ,MAAM,EAAE;AACrD,UAAIA,KAAI,SAAS,GAAG;AAClB,eAAO,KAAKA,KAAI,KAAK,IAAI,CAAC;AAAA,MAC5B;AAAA,IACF;AACA,WAAO,OAAO,KAAK,MAAM;AAAA,EAC3B;AAEA,QAAM,MAAgB,CAAA;AACtB,aAAW,WAAW,UAAU;AAC9B,QAAI,WAAW,OAAO;AACpB,UAAI,QAAQ,KAAK;AACf,YAAI,KAAK,QAAQ,GAAG;AAAA,MACtB;AAAA,IACF,WAAW,WAAW,QAAQ;AAC5B,UAAI,QAAQ,MAAM;AAChB,YAAI,KAAK,QAAQ,IAAI;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AAEA,SAAO,IAAI,KAAK,IAAI;AACtB;AAEA,SAAS,YAAY,iBAAoD;AACvE,MAAI,CAAC,gBAAiB,QAAO;AAC7B,QAAM,OAAO,SAAS,gBAAgB,UAAU,GAAG,CAAC,GAAG,EAAE;AACzD,SAAO,OAAO,MAAM,IAAI,IAAI,OAAO;AACrC;AAEA,SAAS,aAAa,UAA4D;AAChF,SAAO,SAAS,IAAI,CAAC,aAAa;AAAA,IAChC,GAAG;AAAA,IACH,MAAM,YAAY,QAAQ,eAAe;AAAA,EAAA,EACzC;AACJ;AASO,SAAS,WAAW,UAAqB,UAAuC;AACrF,QAAM,mBAAmB,aAAa,QAAQ;AAE9C,MAAI,CAAC,UAAU;AACb,WAAO,KAAK,UAAU,kBAAkB,MAAM,CAAC;AAAA,EACjD;AAEA,SAAO,KAAK;AAAA,IACV;AAAA,MACE,SAAS;AAAA,QACP,IAAI,SAAS;AAAA,QACb,MAAM,SAAS;AAAA,QACf,WAAW,SAAS;AAAA,MAAA;AAAA,MAEtB,SAAS;AAAA,QACP,cAAc,SAAS;AAAA,QACvB,WAAW,SAAS;AAAA,MAAA;AAAA,MAEtB,SAAS;AAAA,IAAA;AAAA,IAEX;AAAA,IACA;AAAA,EAAA;AAEJ;AAEO,SAAS,YAAY,UAA6B;AACvD,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO;AAAA,EACT;AACA,SAAO,aAAa,QAAQ,EAAE,IAAI,CAAC,YAAY,KAAK,UAAU,OAAO,CAAC,EAAE,KAAK,IAAI;AACnF;AAEO,SAAS,cAAc,UAA6B;AACzD,QAAM,WAAW,kBAAkB,QAAQ;AAC3C,SAAO,KAAK,UAAU,UAAU,MAAM,CAAC;AACzC;AAGA,MAAM,kBAAqC;AAAA,EACzC;AAAA,EAAO;AAAA,EAAQ;AAAA,EAAW;AAAA,EAAY;AAAA,EACtC;AAAA,EAAY;AAAA,EAAmB;AAAA,EAAW;AAAA,EAAU;AAAA,EAAS;AAC/D;AAEA,SAAS,oBAAoB,SAA0B;AACrD,MAAI,QAAQ;AACZ,aAAW,SAAS,iBAAiB;AACnC,QAAI,QAAQ,KAAK,MAAM,UAAa,QAAQ,KAAK,MAAM,IAAI;AACzD;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,oBAAoB,UAA0C;AAE5E,QAAM,iCAAiB,IAAA;AACvB,QAAM,SAAoB,CAAA;AAC1B,MAAI,oBAAoB;AAExB,aAAW,WAAW,UAAU;AAC9B,UAAM,OAAO,eAAe,OAAO;AAEnC,QAAI,KAAK,WAAW,GAAG;AAErB,aAAO,KAAK,OAAO;AACnB;AAAA,IACF;AAGA,QAAI;AACJ,eAAW,OAAO,MAAM;AACtB,YAAM,MAAM,WAAW,IAAI,GAAG;AAC9B,UAAI,QAAQ,QAAW;AACrB,wBAAgB;AAChB;AAAA,MACF;AAAA,IACF;AAEA,QAAI,kBAAkB,QAAW;AAE/B,YAAM,WAAW,OAAO,aAAa;AACrC,UAAI,oBAAoB,OAAO,IAAI,oBAAoB,QAAQ,GAAG;AAEhE,eAAO,aAAa,IAAI;AAExB,cAAM,UAAU,eAAe,OAAO;AACtC,mBAAW,OAAO,SAAS;AACzB,qBAAW,IAAI,KAAK,aAAa;AAAA,QACnC;AAAA,MACF;AACA;AAAA,IACF,OAAO;AACL,YAAM,QAAQ,OAAO;AACrB,aAAO,KAAK,OAAO;AAEnB,iBAAW,OAAO,MAAM;AACtB,mBAAW,IAAI,KAAK,KAAK;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,QAAQ,kBAAA;AAC7B;"}
@@ -1,5 +1,4 @@
1
1
  import { Article } from '../../providers/base/types.js';
2
- import { ExportFilter } from './export.js';
3
2
  export interface ResultsCommandOptions {
4
3
  sessionId: string;
5
4
  limit?: number;
@@ -7,7 +6,6 @@ export interface ResultsCommandOptions {
7
6
  json: boolean;
8
7
  fields?: string[];
9
8
  query?: string;
10
- filter?: ExportFilter;
11
9
  showAbstract: boolean;
12
10
  abstractLength?: number;
13
11
  }
@@ -17,9 +15,6 @@ export interface CommandLineOptions {
17
15
  json?: boolean | undefined;
18
16
  fields?: string | undefined;
19
17
  query?: string | undefined;
20
- filterYear?: string | undefined;
21
- filterTitle?: string | undefined;
22
- filterAbstract?: string | undefined;
23
18
  abstract?: boolean | undefined;
24
19
  abstractLength?: string | undefined;
25
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/results.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kBAAkB,GAC1B,qBAAqB,CAiEvB;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,gBAAgB,CA+BrF;AAoBD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,aAAa,GACrB,MAAM,CA+DR;AASD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAG7D"}
1
+ {"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/results.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAE7D,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kBAAkB,GAC1B,qBAAqB,CA6BvB;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,gBAAgB,CAuBrF;AAoBD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,aAAa,GACrB,MAAM,CA+DR;AASD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAG7D"}
@@ -19,36 +19,6 @@ function parseResultsOptions(sessionId, options) {
19
19
  if (options.query !== void 0) {
20
20
  result.query = options.query;
21
21
  }
22
- const filter = {};
23
- let hasFilter = false;
24
- if (options.filterYear) {
25
- const parts = options.filterYear.split("-");
26
- if (parts.length === 2) {
27
- const from = parseInt(parts[0], 10);
28
- const to = parseInt(parts[1], 10);
29
- if (!Number.isNaN(from)) filter.yearFrom = from;
30
- if (!Number.isNaN(to)) filter.yearTo = to;
31
- hasFilter = true;
32
- } else if (parts.length === 1) {
33
- const year = parseInt(parts[0], 10);
34
- if (!Number.isNaN(year)) {
35
- filter.yearFrom = year;
36
- filter.yearTo = year;
37
- hasFilter = true;
38
- }
39
- }
40
- }
41
- if (options.filterTitle) {
42
- filter.titleKeywords = options.filterTitle.split(",").map((s) => s.trim()).filter(Boolean);
43
- hasFilter = true;
44
- }
45
- if (options.filterAbstract) {
46
- filter.abstractKeywords = options.filterAbstract.split(",").map((s) => s.trim()).filter(Boolean);
47
- hasFilter = true;
48
- }
49
- if (hasFilter) {
50
- result.filter = filter;
51
- }
52
22
  return result;
53
23
  }
54
24
  function validateResultsInput(options) {
@@ -70,12 +40,6 @@ function validateResultsInput(options) {
70
40
  error: "offset must be a non-negative number"
71
41
  };
72
42
  }
73
- if (options.query !== void 0 && options.query !== "" && options.filter) {
74
- return {
75
- valid: false,
76
- error: "Cannot use -q/--query together with --filter-year, --filter-title, or --filter-abstract. Use -q only."
77
- };
78
- }
79
43
  return { valid: true };
80
44
  }
81
45
  const DEFAULT_TITLE_MAX_LENGTH = 70;
@@ -1 +1 @@
1
- {"version":3,"file":"results.js","sources":["../../../src/cli/commands/results.ts"],"sourcesContent":["/**\n * Results command - display articles from a session in the terminal.\n */\nimport type { Article } from '../../providers/base/types.js';\nimport type { ExportFilter } from './export.js';\n\nexport interface ResultsCommandOptions {\n sessionId: string;\n limit?: number;\n offset?: number;\n json: boolean;\n fields?: string[];\n query?: string;\n filter?: ExportFilter;\n showAbstract: boolean;\n abstractLength?: number;\n}\n\nexport interface CommandLineOptions {\n limit?: string | undefined;\n offset?: string | undefined;\n json?: boolean | undefined;\n fields?: string | undefined;\n query?: string | undefined;\n filterYear?: string | undefined;\n filterTitle?: string | undefined;\n filterAbstract?: string | undefined;\n abstract?: boolean | undefined;\n abstractLength?: string | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\nexport interface FormatOptions {\n sessionId: string;\n sessionName: string;\n total: number;\n offset?: number | undefined;\n filteredFrom?: number | undefined;\n showAbstract?: boolean | undefined;\n abstractLength?: number | undefined;\n}\n\nexport function parseResultsOptions(\n sessionId: string,\n options: CommandLineOptions\n): ResultsCommandOptions {\n const result: ResultsCommandOptions = {\n sessionId,\n json: options.json ?? false,\n showAbstract: options.abstract ?? false,\n };\n\n if (options.abstractLength) {\n result.abstractLength = parseInt(options.abstractLength, 10);\n }\n\n if (options.limit) {\n result.limit = parseInt(options.limit, 10);\n }\n\n if (options.offset) {\n result.offset = parseInt(options.offset, 10);\n }\n\n if (options.fields) {\n result.fields = options.fields.split(',').map((f) => f.trim());\n }\n\n // Handle -q / --query\n if (options.query !== undefined) {\n result.query = options.query;\n }\n\n // Parse legacy filters\n const filter: ExportFilter = {};\n let hasFilter = false;\n\n if (options.filterYear) {\n const parts = options.filterYear.split('-');\n if (parts.length === 2) {\n const from = parseInt(parts[0]!, 10);\n const to = parseInt(parts[1]!, 10);\n if (!Number.isNaN(from)) filter.yearFrom = from;\n if (!Number.isNaN(to)) filter.yearTo = to;\n hasFilter = true;\n } else if (parts.length === 1) {\n const year = parseInt(parts[0]!, 10);\n if (!Number.isNaN(year)) {\n filter.yearFrom = year;\n filter.yearTo = year;\n hasFilter = true;\n }\n }\n }\n\n if (options.filterTitle) {\n filter.titleKeywords = options.filterTitle.split(',').map((s) => s.trim()).filter(Boolean);\n hasFilter = true;\n }\n\n if (options.filterAbstract) {\n filter.abstractKeywords = options.filterAbstract.split(',').map((s) => s.trim()).filter(Boolean);\n hasFilter = true;\n }\n\n if (hasFilter) {\n result.filter = filter;\n }\n\n return result;\n}\n\nexport function validateResultsInput(options: ResultsCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n if (options.limit !== undefined && options.limit < 0) {\n return {\n valid: false,\n error: 'limit must be a non-negative number',\n };\n }\n\n if (options.offset !== undefined && options.offset < 0) {\n return {\n valid: false,\n error: 'offset must be a non-negative number',\n };\n }\n\n // -q and legacy filter flags are mutually exclusive\n if (options.query !== undefined && options.query !== '' && options.filter) {\n return {\n valid: false,\n error: 'Cannot use -q/--query together with --filter-year, --filter-title, or --filter-abstract. Use -q only.',\n };\n }\n\n return { valid: true };\n}\n\nconst DEFAULT_TITLE_MAX_LENGTH = 70;\nconst DEFAULT_ABSTRACT_MAX_LENGTH = 300;\n\nfunction extractYear(publicationDate: string | undefined): number | null {\n if (!publicationDate) return null;\n const year = parseInt(publicationDate.substring(0, 4), 10);\n return Number.isNaN(year) ? null : year;\n}\n\nfunction truncateText(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n return text.substring(0, maxLength - 3) + '...';\n}\n\nfunction truncateTitle(title: string, maxLength: number = DEFAULT_TITLE_MAX_LENGTH): string {\n return truncateText(title, maxLength);\n}\n\nexport function formatResultsList(\n articles: Article[],\n options: FormatOptions\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push(`Results: ${options.sessionName} (${options.sessionId})`);\n\n if (articles.length === 0) {\n lines.push('No articles found.');\n return lines.join('\\n');\n }\n\n // Pagination info\n const offset = options.offset ?? 0;\n const startNum = offset + 1;\n const endNum = offset + articles.length;\n const articleWord = options.total === 1 ? 'article' : 'articles';\n\n let countInfo = `Showing ${startNum}-${endNum} of ${options.total} ${articleWord}`;\n if (options.filteredFrom !== undefined && options.filteredFrom !== options.total) {\n countInfo += ` (filtered from ${options.filteredFrom})`;\n }\n lines.push(countInfo);\n lines.push('');\n\n // Articles\n for (let i = 0; i < articles.length; i++) {\n const article = articles[i]!;\n const num = offset + i + 1;\n const year = extractYear(article.publicationDate);\n const yearStr = year !== null ? String(year) : '----';\n const title = truncateTitle(article.title);\n\n lines.push(`${num.toString().padStart(2)}. [${yearStr}] ${title}`);\n\n if (article.journal) {\n lines.push(` ${article.journal}`);\n }\n\n if (article.doi) {\n lines.push(` DOI: ${article.doi}`);\n }\n\n if (options.showAbstract) {\n lines.push('');\n if (article.abstract && article.abstract.trim() !== '') {\n const maxLength = options.abstractLength ?? DEFAULT_ABSTRACT_MAX_LENGTH;\n const truncatedAbstract = truncateText(article.abstract, maxLength);\n lines.push(` Abstract: ${truncatedAbstract}`);\n } else {\n lines.push(' (No abstract available)');\n }\n }\n\n lines.push('');\n }\n\n // Tip: show query filter hint when not already filtering\n if (options.filteredFrom === undefined) {\n lines.push('Tip: Use -q to filter: results SESSION -q \"author:smith year:2023\"');\n }\n lines.push('Tip: Use check to verify coverage: check SESSION --file known-dois.txt');\n\n return lines.join('\\n').trimEnd();\n}\n\nfunction addYearField(articles: Article[]): (Article & { year: number | null })[] {\n return articles.map((article) => ({\n ...article,\n year: extractYear(article.publicationDate),\n }));\n}\n\nexport function formatResultsJson(articles: Article[]): string {\n const articlesWithYear = addYearField(articles);\n return JSON.stringify(articlesWithYear, null, 2);\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"results.js","sources":["../../../src/cli/commands/results.ts"],"sourcesContent":["/**\n * Results command - display articles from a session in the terminal.\n */\nimport type { Article } from '../../providers/base/types.js';\n\nexport interface ResultsCommandOptions {\n sessionId: string;\n limit?: number;\n offset?: number;\n json: boolean;\n fields?: string[];\n query?: string;\n showAbstract: boolean;\n abstractLength?: number;\n}\n\nexport interface CommandLineOptions {\n limit?: string | undefined;\n offset?: string | undefined;\n json?: boolean | undefined;\n fields?: string | undefined;\n query?: string | undefined;\n abstract?: boolean | undefined;\n abstractLength?: string | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\nexport interface FormatOptions {\n sessionId: string;\n sessionName: string;\n total: number;\n offset?: number | undefined;\n filteredFrom?: number | undefined;\n showAbstract?: boolean | undefined;\n abstractLength?: number | undefined;\n}\n\nexport function parseResultsOptions(\n sessionId: string,\n options: CommandLineOptions\n): ResultsCommandOptions {\n const result: ResultsCommandOptions = {\n sessionId,\n json: options.json ?? false,\n showAbstract: options.abstract ?? false,\n };\n\n if (options.abstractLength) {\n result.abstractLength = parseInt(options.abstractLength, 10);\n }\n\n if (options.limit) {\n result.limit = parseInt(options.limit, 10);\n }\n\n if (options.offset) {\n result.offset = parseInt(options.offset, 10);\n }\n\n if (options.fields) {\n result.fields = options.fields.split(',').map((f) => f.trim());\n }\n\n // Handle -q / --query\n if (options.query !== undefined) {\n result.query = options.query;\n }\n\n return result;\n}\n\nexport function validateResultsInput(options: ResultsCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n if (options.limit !== undefined && options.limit < 0) {\n return {\n valid: false,\n error: 'limit must be a non-negative number',\n };\n }\n\n if (options.offset !== undefined && options.offset < 0) {\n return {\n valid: false,\n error: 'offset must be a non-negative number',\n };\n }\n\n return { valid: true };\n}\n\nconst DEFAULT_TITLE_MAX_LENGTH = 70;\nconst DEFAULT_ABSTRACT_MAX_LENGTH = 300;\n\nfunction extractYear(publicationDate: string | undefined): number | null {\n if (!publicationDate) return null;\n const year = parseInt(publicationDate.substring(0, 4), 10);\n return Number.isNaN(year) ? null : year;\n}\n\nfunction truncateText(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n return text.substring(0, maxLength - 3) + '...';\n}\n\nfunction truncateTitle(title: string, maxLength: number = DEFAULT_TITLE_MAX_LENGTH): string {\n return truncateText(title, maxLength);\n}\n\nexport function formatResultsList(\n articles: Article[],\n options: FormatOptions\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push(`Results: ${options.sessionName} (${options.sessionId})`);\n\n if (articles.length === 0) {\n lines.push('No articles found.');\n return lines.join('\\n');\n }\n\n // Pagination info\n const offset = options.offset ?? 0;\n const startNum = offset + 1;\n const endNum = offset + articles.length;\n const articleWord = options.total === 1 ? 'article' : 'articles';\n\n let countInfo = `Showing ${startNum}-${endNum} of ${options.total} ${articleWord}`;\n if (options.filteredFrom !== undefined && options.filteredFrom !== options.total) {\n countInfo += ` (filtered from ${options.filteredFrom})`;\n }\n lines.push(countInfo);\n lines.push('');\n\n // Articles\n for (let i = 0; i < articles.length; i++) {\n const article = articles[i]!;\n const num = offset + i + 1;\n const year = extractYear(article.publicationDate);\n const yearStr = year !== null ? String(year) : '----';\n const title = truncateTitle(article.title);\n\n lines.push(`${num.toString().padStart(2)}. [${yearStr}] ${title}`);\n\n if (article.journal) {\n lines.push(` ${article.journal}`);\n }\n\n if (article.doi) {\n lines.push(` DOI: ${article.doi}`);\n }\n\n if (options.showAbstract) {\n lines.push('');\n if (article.abstract && article.abstract.trim() !== '') {\n const maxLength = options.abstractLength ?? DEFAULT_ABSTRACT_MAX_LENGTH;\n const truncatedAbstract = truncateText(article.abstract, maxLength);\n lines.push(` Abstract: ${truncatedAbstract}`);\n } else {\n lines.push(' (No abstract available)');\n }\n }\n\n lines.push('');\n }\n\n // Tip: show query filter hint when not already filtering\n if (options.filteredFrom === undefined) {\n lines.push('Tip: Use -q to filter: results SESSION -q \"author:smith year:2023\"');\n }\n lines.push('Tip: Use check to verify coverage: check SESSION --file known-dois.txt');\n\n return lines.join('\\n').trimEnd();\n}\n\nfunction addYearField(articles: Article[]): (Article & { year: number | null })[] {\n return articles.map((article) => ({\n ...article,\n year: extractYear(article.publicationDate),\n }));\n}\n\nexport function formatResultsJson(articles: Article[]): string {\n const articlesWithYear = addYearField(articles);\n return JSON.stringify(articlesWithYear, null, 2);\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/extract.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAsD,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAErH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE9D,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAGD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAgLD;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAU/C;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,EAC7B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,mBAAmB,CAAC,CAsG9B"}
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/extract.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAsD,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAErH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE9D,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAGD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAgLD;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAU/C;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,EAC7B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,mBAAmB,CAAC,CAkH9B"}
@@ -167,10 +167,14 @@ async function executeReviewExtract(options, sessionsDir) {
167
167
  };
168
168
  const yamlContent = stringify(outputFile, { lineWidth: 0 });
169
169
  const decisionComment = getDecisionInlineComment(options.basis);
170
- const yamlWithComments = yamlContent.replace(
170
+ let yamlWithComments = yamlContent.replace(
171
171
  /^(\s*-?\s*)decision: uncertain$/gm,
172
172
  `$1decision: uncertain ${decisionComment}`
173
173
  );
174
+ yamlWithComments = yamlWithComments.replace(
175
+ /^(\s*)comment: ""$/gm,
176
+ '$1comment: "" # reason for decision'
177
+ );
174
178
  const guidanceComment = getBasisGuidanceComment(options.basis);
175
179
  finalContent = guidanceComment + yamlWithComments;
176
180
  } else {
@@ -180,10 +184,14 @@ async function executeReviewExtract(options, sessionsDir) {
180
184
  articles: paginated.map((article) => buildFinalizeArticle(article, options.basis))
181
185
  };
182
186
  const yamlContent = stringify(outputFile, { lineWidth: 0 });
183
- const yamlWithComments = yamlContent.replace(
187
+ let yamlWithComments = yamlContent.replace(
184
188
  /^(\s*)finalDecision: null$/gm,
185
189
  "$1finalDecision: # include / exclude"
186
190
  );
191
+ yamlWithComments = yamlWithComments.replace(
192
+ /^(\s*)reviews: \[\]$/gm,
193
+ "$1reviews: [] # add new reviews here"
194
+ );
187
195
  const guidanceComment = getFinalDecisionGuidanceComment();
188
196
  finalContent = guidanceComment + yamlWithComments;
189
197
  }
@@ -1 +1 @@
1
- {"version":3,"file":"extract.js","sources":["../../../../src/cli/commands/review/extract.ts"],"sourcesContent":["/**\n * review extract command - Extract subset of articles for distributed review\n */\n\nimport { join, dirname } from 'node:path';\nimport { readFile, writeFile, mkdir, copyFile, access } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ArticleEntry, type ReviewStatus, type ReviewBasis } from './types.js';\n\nexport type SortOption = 'year' | 'title' | 'random' | 'none';\n\nexport interface ReviewExtractOptions {\n sessionId: string;\n filter?: ReviewStatus[];\n sort?: SortOption;\n seed?: number;\n limit?: number;\n offset?: number;\n /** Basis for the review (title, abstract, fulltext). When specified, outputs screening format. */\n basis?: ReviewBasis;\n /** Reviewer identifier (e.g., \"ai:claude\"). Required for all extract modes. */\n reviewer?: string;\n /** Name for the review subset (output goes to for-review/<name>/review.yaml) */\n name: string;\n /** When true, outputs final decision format with reviewHistory and finalDecision fields. */\n finalize?: boolean;\n}\n\n\nexport interface ReviewExtractResult {\n outputPath: string;\n extractedCount: number;\n totalMatching: number;\n}\n\n/**\n * Load review file from session directory\n */\nasync function loadReviewFile(sessionDir: string): Promise<ReviewFile> {\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Seeded random number generator (Fisher-Yates shuffle with LCG)\n */\nfunction seededShuffle<T>(array: T[], seed: number): T[] {\n const result = [...array];\n let currentSeed = seed;\n\n // Linear congruential generator\n function random(): number {\n currentSeed = (currentSeed * 1664525 + 1013904223) % 4294967296;\n return currentSeed / 4294967296;\n }\n\n // Fisher-Yates shuffle\n for (let i = result.length - 1; i > 0; i--) {\n const j = Math.floor(random() * (i + 1));\n [result[i], result[j]] = [result[j]!, result[i]!];\n }\n\n return result;\n}\n\n/**\n * Get the best identifier for an article (doi > pmid > scopusId > arxivId > ericId > title)\n */\nfunction getArticleId(article: ArticleEntry): string {\n if (article.doi) return article.doi;\n if (article.pmid) return article.pmid;\n if (article.scopusId) return article.scopusId;\n if (article.arxivId) return article.arxivId;\n if (article.ericId) return article.ericId;\n return article.title;\n}\n\nfunction getBasisGuidanceComment(basis: ReviewBasis): string {\n const schemaLine = '# yaml-language-server: $schema=./review.schema.json';\n switch (basis) {\n case 'title':\n return [\n schemaLine,\n '# Screening by title only.',\n '# Mark clearly irrelevant items as \"exclude\" with a comment explaining the reason.',\n '# Leave everything else as \"uncertain\".',\n '',\n ].join('\\n');\n case 'abstract':\n return [\n schemaLine,\n '# Screening by title and abstract.',\n '# You should be able to decide \"include\" or \"exclude\" for most items at this stage.',\n '# Mark remaining ambiguous items as \"uncertain\" with a comment explaining why.',\n '',\n ].join('\\n');\n case 'fulltext':\n return [\n schemaLine,\n '# Screening by full text. This is the final decision stage.',\n '# Decide \"include\" or \"exclude\" for each item.',\n '# Use \"uncertain\" only when absolutely unavoidable, with a comment explaining why.',\n '',\n ].join('\\n');\n }\n}\n\nfunction getDecisionInlineComment(basis: ReviewBasis): string {\n switch (basis) {\n case 'title':\n return '# exclude / uncertain';\n case 'abstract':\n case 'fulltext':\n return '# include / exclude / uncertain';\n }\n}\n\n/** Build a screening article for --basis mode: only include fields relevant to the basis */\nfunction buildScreeningArticle(article: ArticleEntry, basis: ReviewBasis): ArticleEntry {\n // Start with identifiers\n const result: ArticleEntry = { title: article.title, reviews: [] };\n if (article.doi) result.doi = article.doi;\n if (article.pmid) result.pmid = article.pmid;\n if (article.scopusId) result.scopusId = article.scopusId;\n if (article.arxivId) result.arxivId = article.arxivId;\n if (article.ericId) result.ericId = article.ericId;\n\n // Include abstract for abstract and fulltext basis\n if ((basis === 'abstract' || basis === 'fulltext') && article.abstract) {\n result.abstract = article.abstract;\n }\n\n // Include fulltext ref for fulltext basis\n if (basis === 'fulltext' && article.fulltext) {\n result.fulltext = article.fulltext;\n }\n\n // Pre-populate reviews (reviewer omitted; filled from top-level field on merge)\n result.reviews = [{ decision: 'uncertain' as const, comment: '' } as ArticleEntry['reviews'][0]];\n\n return result;\n}\n\n/** Build a finalize article with reviewHistory and finalDecision, optionally scoped by basis */\nfunction buildFinalizeArticle(article: ArticleEntry, basis?: ReviewBasis): ArticleEntry {\n const result: ArticleEntry = { title: article.title, reviews: [] };\n\n // Always include identifiers\n if (article.doi) result.doi = article.doi;\n if (article.pmid) result.pmid = article.pmid;\n if (article.scopusId) result.scopusId = article.scopusId;\n if (article.arxivId) result.arxivId = article.arxivId;\n if (article.ericId) result.ericId = article.ericId;\n\n // Always include bibliographic metadata\n if (article.authors) result.authors = article.authors;\n if (article.year) result.year = article.year;\n\n // Scope content by basis (or include all if no basis)\n if (!basis || basis === 'abstract' || basis === 'fulltext') {\n if (article.abstract) result.abstract = article.abstract;\n }\n if (!basis || basis === 'fulltext') {\n if (article.fulltext) result.fulltext = article.fulltext;\n }\n\n // Add reviewHistory (existing reviews, read-only)\n result.reviewHistory = article.reviews ?? [];\n\n // Empty reviews for new reviews\n result.reviews = [];\n\n // Null finalDecision as placeholder\n result.finalDecision = null;\n\n return result;\n}\n\nfunction getFinalDecisionGuidanceComment(): string {\n return [\n '# yaml-language-server: $schema=./review.schema.json',\n '# Final decision file: set finalDecision on each article',\n '# Valid decisions: include / exclude / null',\n '',\n ].join('\\n');\n}\n\n/**\n * Sort articles based on sort option\n */\nfunction sortArticles(articles: ArticleEntry[], sort: SortOption, seed?: number): ArticleEntry[] {\n switch (sort) {\n case 'year':\n return [...articles].sort((a, b) => {\n const yearA = a.year ?? '';\n const yearB = b.year ?? '';\n return yearA.localeCompare(yearB);\n });\n case 'title':\n return [...articles].sort((a, b) => a.title.localeCompare(b.title));\n case 'random':\n return seededShuffle(articles, seed ?? Date.now());\n case 'none':\n default:\n return articles;\n }\n}\n\n/**\n * Validate the name parameter for extract\n */\nexport function validateName(name: string): void {\n if (!name || name.trim() === '') {\n throw new Error('--name must not be empty');\n }\n if (name.includes('/') || name.includes('\\\\')) {\n throw new Error(`--name must not contain path separators: \"${name}\"`);\n }\n if (name.includes('..')) {\n throw new Error(`--name must not contain \"..\": \"${name}\"`);\n }\n}\n\n/**\n * Execute review extract command\n */\nexport async function executeReviewExtract(\n options: ReviewExtractOptions,\n sessionsDir: string\n): Promise<ReviewExtractResult> {\n validateName(options.name);\n\n const sessionDir = join(sessionsDir, options.sessionId);\n const outputPath = join(sessionDir, 'for-review', options.name, 'review.yaml');\n const reviewFile = await loadReviewFile(sessionDir);\n\n // Filter articles by status\n const reviewers = reviewFile.reviewers;\n let filtered: ArticleEntry[];\n if (options.filter && options.filter.length > 0) {\n filtered = reviewFile.articles.filter((article) => {\n const status = classifyStatus(article, reviewers);\n return options.filter!.includes(status);\n });\n } else {\n filtered = [...reviewFile.articles];\n }\n\n const totalMatching = filtered.length;\n\n // Sort articles\n const sorted = sortArticles(filtered, options.sort ?? 'none', options.seed);\n\n // Apply pagination\n let paginated = sorted;\n if (options.offset !== undefined && options.offset > 0) {\n paginated = paginated.slice(options.offset);\n }\n if (options.limit !== undefined && options.limit > 0) {\n paginated = paginated.slice(0, options.limit);\n }\n\n if (!options.reviewer) {\n throw new Error('--reviewer is required for review file extract');\n }\n\n let finalContent: string;\n\n if (options.basis && !options.finalize) {\n // Screening mode: basis-scoped content with pre-populated reviews\n const outputFile: ReviewFile = {\n sessionId: options.sessionId,\n basis: options.basis,\n reviewer: options.reviewer,\n articles: paginated.map((article) => buildScreeningArticle(article, options.basis!)),\n };\n\n const yamlContent = stringifyYaml(outputFile, { lineWidth: 0 });\n\n // Add decision inline comments\n const decisionComment = getDecisionInlineComment(options.basis);\n const yamlWithComments = yamlContent.replace(\n /^(\\s*-?\\s*)decision: uncertain$/gm,\n `$1decision: uncertain ${decisionComment}`\n );\n\n const guidanceComment = getBasisGuidanceComment(options.basis);\n finalContent = guidanceComment + yamlWithComments;\n } else {\n // Final decision mode: --finalize, or no --basis (backward compat)\n const outputFile: ReviewFile = {\n sessionId: options.sessionId,\n reviewer: options.reviewer,\n articles: paginated.map((article) => buildFinalizeArticle(article, options.basis)),\n };\n\n const yamlContent = stringifyYaml(outputFile, { lineWidth: 0 });\n\n // Replace finalDecision: null with a commented placeholder for user guidance\n const yamlWithComments = yamlContent.replace(\n /^(\\s*)finalDecision: null$/gm,\n '$1finalDecision: # include / exclude'\n );\n\n const guidanceComment = getFinalDecisionGuidanceComment();\n finalContent = guidanceComment + yamlWithComments;\n }\n\n // Ensure output directory exists\n const outputDir = dirname(outputPath);\n await mkdir(outputDir, { recursive: true });\n\n // Write output YAML\n await writeFile(outputPath, finalContent, 'utf-8');\n\n // Copy schema file to output directory if it exists\n const schemaSourcePath = join(sessionDir, '.internal', 'review.schema.json');\n const schemaDestPath = join(outputDir, 'review.schema.json');\n\n try {\n await access(schemaSourcePath);\n await copyFile(schemaSourcePath, schemaDestPath);\n } catch {\n // Schema file doesn't exist, skip copying\n }\n\n return {\n outputPath,\n extractedCount: paginated.length,\n totalMatching,\n };\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":"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"}
1
+ {"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 let yamlWithComments = yamlContent.replace(\n /^(\\s*-?\\s*)decision: uncertain$/gm,\n `$1decision: uncertain ${decisionComment}`\n );\n\n // Add comment field inline guidance\n yamlWithComments = yamlWithComments.replace(\n /^(\\s*)comment: \"\"$/gm,\n '$1comment: \"\" # reason for decision'\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 let yamlWithComments = yamlContent.replace(\n /^(\\s*)finalDecision: null$/gm,\n '$1finalDecision: # include / exclude'\n );\n\n // Add reviews array inline guidance\n yamlWithComments = yamlWithComments.replace(\n /^(\\s*)reviews: \\[\\]$/gm,\n '$1reviews: [] # add new reviews here'\n );\n\n const guidanceComment = getFinalDecisionGuidanceComment();\n finalContent = guidanceComment + yamlWithComments;\n }\n\n // Ensure output directory exists\n const outputDir = dirname(outputPath);\n await mkdir(outputDir, { recursive: true });\n\n // Write output YAML\n await writeFile(outputPath, finalContent, 'utf-8');\n\n // Copy schema file to output directory if it exists\n const schemaSourcePath = join(sessionDir, '.internal', 'review.schema.json');\n const schemaDestPath = join(outputDir, 'review.schema.json');\n\n try {\n await access(schemaSourcePath);\n await copyFile(schemaSourcePath, schemaDestPath);\n } catch {\n // Schema file doesn't exist, skip copying\n }\n\n return {\n outputPath,\n extractedCount: paginated.length,\n totalMatching,\n };\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":"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"}
@@ -3,6 +3,7 @@ export interface ReviewFinalizeOptions {
3
3
  sessionId: string;
4
4
  dryRun?: boolean;
5
5
  minReviewers?: number;
6
+ decision?: 'include' | 'exclude';
6
7
  }
7
8
  export interface ReviewFinalizeResult {
8
9
  includedCount: number;
@@ -18,5 +19,6 @@ export declare function executeReviewFinalize(options: ReviewFinalizeOptions, se
18
19
  */
19
20
  export declare function formatFinalizeOutput(result: ReviewFinalizeResult, options?: {
20
21
  dryRun?: boolean;
22
+ decision?: 'include' | 'exclude';
21
23
  }): string;
22
24
  //# sourceMappingURL=finalize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"finalize.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/finalize.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAmC,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEhF,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;CAC/C;AAcD;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,qBAAqB,EAC9B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,CAAC,CAiD/B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7B,MAAM,CA+BR"}
1
+ {"version":3,"file":"finalize.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/finalize.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAmC,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEhF,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;CAC/C;AAcD;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,qBAAqB,EAC9B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,CAAC,CAwD/B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;CAAE,GAC/D,MAAM,CAuCR"}
@@ -28,6 +28,11 @@ async function executeReviewFinalize(options, sessionsDir) {
28
28
  for (const article of reviewFile.articles) {
29
29
  const status = classifyStatus(article, reviewers);
30
30
  if (status === "agreed-include" || status === "agreed-exclude") {
31
+ const consensusDecision = status === "agreed-include" ? "include" : "exclude";
32
+ if (options.decision && options.decision !== consensusDecision) {
33
+ result.skippedByStatus[status]++;
34
+ continue;
35
+ }
31
36
  const reviews = article.reviews ?? [];
32
37
  const uniqueReviewers = new Set(reviews.map((r) => r.reviewer));
33
38
  if (uniqueReviewers.size < minReviewers) {
@@ -35,7 +40,7 @@ async function executeReviewFinalize(options, sessionsDir) {
35
40
  continue;
36
41
  }
37
42
  if (!options.dryRun) {
38
- article.finalDecision = status === "agreed-include" ? "include" : "exclude";
43
+ article.finalDecision = consensusDecision;
39
44
  }
40
45
  if (status === "agreed-include") {
41
46
  result.includedCount++;
@@ -75,6 +80,12 @@ function formatFinalizeOutput(result, options) {
75
80
  if (result.skippedByStatus.conflicting > 0) {
76
81
  skippedParts.push(`${result.skippedByStatus.conflicting} conflicting`);
77
82
  }
83
+ if (options?.decision && result.skippedByStatus["agreed-include"] > 0) {
84
+ skippedParts.push(`${result.skippedByStatus["agreed-include"]} agreed-include (filtered)`);
85
+ }
86
+ if (options?.decision && result.skippedByStatus["agreed-exclude"] > 0) {
87
+ skippedParts.push(`${result.skippedByStatus["agreed-exclude"]} agreed-exclude (filtered)`);
88
+ }
78
89
  if (skippedParts.length > 0) {
79
90
  lines.push(`Skipped: ${skippedParts.join(", ")}`);
80
91
  }
@@ -1 +1 @@
1
- {"version":3,"file":"finalize.js","sources":["../../../../src/cli/commands/review/finalize.ts"],"sourcesContent":["/**\n * review finalize command - Auto-set finalDecision for articles with consensus\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ReviewStatus } from './types.js';\n\nexport interface ReviewFinalizeOptions {\n sessionId: string;\n dryRun?: boolean;\n minReviewers?: number;\n}\n\nexport interface ReviewFinalizeResult {\n includedCount: number;\n excludedCount: number;\n skippedByStatus: Record<ReviewStatus, number>;\n}\n\nfunction createEmptySkippedByStatus(): Record<ReviewStatus, number> {\n return {\n pending: 0,\n incomplete: 0,\n uncertain: 0,\n 'agreed-include': 0,\n 'agreed-exclude': 0,\n conflicting: 0,\n finalized: 0,\n };\n}\n\n/**\n * Execute review finalize command\n */\nexport async function executeReviewFinalize(\n options: ReviewFinalizeOptions,\n sessionsDir: string\n): Promise<ReviewFinalizeResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(content) as ReviewFile;\n\n const reviewers = reviewFile.reviewers ?? [];\n const minReviewers = options.minReviewers ?? 1;\n\n const result: ReviewFinalizeResult = {\n includedCount: 0,\n excludedCount: 0,\n skippedByStatus: createEmptySkippedByStatus(),\n };\n\n for (const article of reviewFile.articles) {\n const status = classifyStatus(article, reviewers);\n\n if (status === 'agreed-include' || status === 'agreed-exclude') {\n // Check minimum reviewer count\n const reviews = article.reviews ?? [];\n const uniqueReviewers = new Set(reviews.map((r) => r.reviewer));\n if (uniqueReviewers.size < minReviewers) {\n result.skippedByStatus[status]++;\n continue;\n }\n\n if (!options.dryRun) {\n article.finalDecision = status === 'agreed-include' ? 'include' : 'exclude';\n }\n\n if (status === 'agreed-include') {\n result.includedCount++;\n } else {\n result.excludedCount++;\n }\n } else {\n result.skippedByStatus[status]++;\n }\n }\n\n // Write back if not dry-run\n if (!options.dryRun) {\n const yamlContent = stringifyYaml(reviewFile, { lineWidth: 0 });\n const schemaComment = `# yaml-language-server: $schema=./review.schema.json\\n`;\n await writeFile(reviewsPath, schemaComment + yamlContent, 'utf-8');\n }\n\n return result;\n}\n\n/**\n * Format finalize result as human-readable string\n */\nexport function formatFinalizeOutput(\n result: ReviewFinalizeResult,\n options?: { dryRun?: boolean }\n): string {\n const lines: string[] = [];\n\n if (options?.dryRun) {\n lines.push('Dry run - no changes made');\n lines.push('');\n }\n\n const total = result.includedCount + result.excludedCount;\n lines.push(`Finalized ${total} articles (${result.includedCount} include, ${result.excludedCount} exclude)`);\n\n // Build skipped summary (only non-zero, non-agreed statuses)\n const skippedParts: string[] = [];\n if (result.skippedByStatus.pending > 0) {\n skippedParts.push(`${result.skippedByStatus.pending} pending`);\n }\n if (result.skippedByStatus.incomplete > 0) {\n skippedParts.push(`${result.skippedByStatus.incomplete} incomplete`);\n }\n if (result.skippedByStatus.uncertain > 0) {\n skippedParts.push(`${result.skippedByStatus.uncertain} uncertain`);\n }\n if (result.skippedByStatus.conflicting > 0) {\n skippedParts.push(`${result.skippedByStatus.conflicting} conflicting`);\n }\n\n if (skippedParts.length > 0) {\n lines.push(`Skipped: ${skippedParts.join(', ')}`);\n }\n\n return lines.join('\\n');\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":";;;;AAqBA,SAAS,6BAA2D;AAClE,SAAO;AAAA,IACL,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,kBAAkB;AAAA,IAClB,aAAa;AAAA,IACb,WAAW;AAAA,EAAA;AAEf;AAKA,eAAsB,sBACpB,SACA,aAC+B;AAC/B,QAAM,aAAa,KAAK,aAAa,QAAQ,SAAS;AACtD,QAAM,cAAc,KAAK,YAAY,aAAa,cAAc;AAChE,QAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,QAAM,aAAaA,MAAU,OAAO;AAEpC,QAAM,YAAY,WAAW,aAAa,CAAA;AAC1C,QAAM,eAAe,QAAQ,gBAAgB;AAE7C,QAAM,SAA+B;AAAA,IACnC,eAAe;AAAA,IACf,eAAe;AAAA,IACf,iBAAiB,2BAAA;AAAA,EAA2B;AAG9C,aAAW,WAAW,WAAW,UAAU;AACzC,UAAM,SAAS,eAAe,SAAS,SAAS;AAEhD,QAAI,WAAW,oBAAoB,WAAW,kBAAkB;AAE9D,YAAM,UAAU,QAAQ,WAAW,CAAA;AACnC,YAAM,kBAAkB,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC;AAC9D,UAAI,gBAAgB,OAAO,cAAc;AACvC,eAAO,gBAAgB,MAAM;AAC7B;AAAA,MACF;AAEA,UAAI,CAAC,QAAQ,QAAQ;AACnB,gBAAQ,gBAAgB,WAAW,mBAAmB,YAAY;AAAA,MACpE;AAEA,UAAI,WAAW,kBAAkB;AAC/B,eAAO;AAAA,MACT,OAAO;AACL,eAAO;AAAA,MACT;AAAA,IACF,OAAO;AACL,aAAO,gBAAgB,MAAM;AAAA,IAC/B;AAAA,EACF;AAGA,MAAI,CAAC,QAAQ,QAAQ;AACnB,UAAM,cAAcC,UAAc,YAAY,EAAE,WAAW,GAAG;AAC9D,UAAM,gBAAgB;AAAA;AACtB,UAAM,UAAU,aAAa,gBAAgB,aAAa,OAAO;AAAA,EACnE;AAEA,SAAO;AACT;AAKO,SAAS,qBACd,QACA,SACQ;AACR,QAAM,QAAkB,CAAA;AAExB,MAAI,SAAS,QAAQ;AACnB,UAAM,KAAK,2BAA2B;AACtC,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,QAAM,QAAQ,OAAO,gBAAgB,OAAO;AAC5C,QAAM,KAAK,aAAa,KAAK,cAAc,OAAO,aAAa,aAAa,OAAO,aAAa,WAAW;AAG3G,QAAM,eAAyB,CAAA;AAC/B,MAAI,OAAO,gBAAgB,UAAU,GAAG;AACtC,iBAAa,KAAK,GAAG,OAAO,gBAAgB,OAAO,UAAU;AAAA,EAC/D;AACA,MAAI,OAAO,gBAAgB,aAAa,GAAG;AACzC,iBAAa,KAAK,GAAG,OAAO,gBAAgB,UAAU,aAAa;AAAA,EACrE;AACA,MAAI,OAAO,gBAAgB,YAAY,GAAG;AACxC,iBAAa,KAAK,GAAG,OAAO,gBAAgB,SAAS,YAAY;AAAA,EACnE;AACA,MAAI,OAAO,gBAAgB,cAAc,GAAG;AAC1C,iBAAa,KAAK,GAAG,OAAO,gBAAgB,WAAW,cAAc;AAAA,EACvE;AAEA,MAAI,aAAa,SAAS,GAAG;AAC3B,UAAM,KAAK,YAAY,aAAa,KAAK,IAAI,CAAC,EAAE;AAAA,EAClD;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
1
+ {"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 conflicting: 0,\n finalized: 0,\n };\n}\n\n/**\n * Execute review finalize command\n */\nexport async function executeReviewFinalize(\n options: ReviewFinalizeOptions,\n sessionsDir: string\n): Promise<ReviewFinalizeResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(content) as ReviewFile;\n\n const reviewers = reviewFile.reviewers ?? [];\n const minReviewers = options.minReviewers ?? 1;\n\n const result: ReviewFinalizeResult = {\n includedCount: 0,\n excludedCount: 0,\n skippedByStatus: createEmptySkippedByStatus(),\n };\n\n for (const article of reviewFile.articles) {\n const status = classifyStatus(article, reviewers);\n\n if (status === 'agreed-include' || status === 'agreed-exclude') {\n // Check 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.uncertain > 0) {\n skippedParts.push(`${result.skippedByStatus.uncertain} uncertain`);\n }\n if (result.skippedByStatus.conflicting > 0) {\n skippedParts.push(`${result.skippedByStatus.conflicting} conflicting`);\n }\n\n // 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 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuLpC;;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,CAygFvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqLpC;;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,CAu9EvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
package/dist/cli/index.js CHANGED
@@ -23,7 +23,7 @@ import { parseSearchOptions, validateSearchInput, formatShortKeywordWarning, for
23
23
  import { parseResumeOptions, validateResumeInput, getResumableProvidersForCommand } from "./commands/resume.js";
24
24
  import { executeResume } from "./commands/resume-executor.js";
25
25
  import { formatVerboseProviderDetails } from "./commands/search-utils.js";
26
- import { parseExportOptions, validateExportInput, deduplicateArticles, filterArticles, formatIds, formatJson, formatCslJson, formatJsonl } from "./commands/export.js";
26
+ import { parseExportOptions, validateExportInput, deduplicateArticles, formatIds, formatJson, formatCslJson, formatJsonl } from "./commands/export.js";
27
27
  import { computeSummary, formatSummaryJson, formatSummary } from "./commands/summary.js";
28
28
  import { parseResultsOptions, validateResultsInput, formatResultsJson, formatResultsList } from "./commands/results.js";
29
29
  import { filterByQuery } from "./commands/query-filter.js";
@@ -728,7 +728,7 @@ Resume completed. ${execResult.resumed} provider(s) resumed.`);
728
728
  }
729
729
  }
730
730
  );
731
- program.command("export").description("Export session results to various formats").argument("<session-id>", "session ID to export").option("--format <fmt>", "output format: ids, json, jsonl, csl-json", "jsonl").option("-o, --output <path>", "output file path (default: stdout)").option("--id-type <type>", "for ids format: doi, pmid, all").option("--no-dedup", "disable deduplication of results").option("-q, --query <expr>", "filter results with query expression").option("--filter-year <range>", "year range filter (deprecated, use -q)").option("--filter-title <keywords>", "title keyword filter (deprecated, use -q)").option("--filter-abstract <keywords>", "abstract keyword filter (deprecated, use -q)").addHelpText("after", `
731
+ program.command("export").description("Export session results to various formats").argument("<session-id>", "session ID to export").option("--format <fmt>", "output format: ids, json, jsonl, csl-json", "jsonl").option("-o, --output <path>", "output file path (default: stdout)").option("--id-type <type>", "for ids format: doi, pmid, all").option("--no-dedup", "disable deduplication of results").option("-q, --query <expr>", "filter results with query expression").addHelpText("after", `
732
732
  Examples:
733
733
  $ search-hub export SESSION_ID # JSONL to stdout
734
734
  $ search-hub export SESSION_ID --format json # JSON to stdout
@@ -755,14 +755,6 @@ Query syntax:
755
755
  async (sessionId, options) => {
756
756
  const globalOpts = program.opts();
757
757
  try {
758
- const hasLegacyFilter = options?.filterYear || options?.filterTitle || options?.filterAbstract;
759
- if (options?.query && hasLegacyFilter) {
760
- if (!globalOpts.quiet) {
761
- console.error("Error: Cannot use -q/--query together with --filter-year, --filter-title, or --filter-abstract. Use -q only.");
762
- }
763
- process.exitCode = EXIT_CODES.SESSION_ERROR;
764
- return;
765
- }
766
758
  const exportOpts = parseExportOptions(sessionId, {
767
759
  format: options?.format,
768
760
  output: options?.output,
@@ -805,33 +797,6 @@ Query syntax:
805
797
  if (options?.query) {
806
798
  exportArticles = filterByQuery(exportArticles, options.query);
807
799
  hasFilter = true;
808
- } else {
809
- const filter = {};
810
- if (options?.filterYear) {
811
- const parts = options.filterYear.split("-");
812
- if (parts.length === 2) {
813
- const from = parseInt(parts[0], 10);
814
- const to = parseInt(parts[1], 10);
815
- if (!Number.isNaN(from)) filter.yearFrom = from;
816
- if (!Number.isNaN(to)) filter.yearTo = to;
817
- } else if (parts.length === 1) {
818
- const year = parseInt(parts[0], 10);
819
- if (!Number.isNaN(year)) {
820
- filter.yearFrom = year;
821
- filter.yearTo = year;
822
- }
823
- }
824
- }
825
- if (options?.filterTitle) {
826
- filter.titleKeywords = options.filterTitle.split(",").map((s) => s.trim()).filter(Boolean);
827
- }
828
- if (options?.filterAbstract) {
829
- filter.abstractKeywords = options.filterAbstract.split(",").map((s) => s.trim()).filter(Boolean);
830
- }
831
- hasFilter = !!(filter.yearFrom !== void 0 || filter.yearTo !== void 0 || filter.titleKeywords && filter.titleKeywords.length > 0 || filter.abstractKeywords && filter.abstractKeywords.length > 0);
832
- if (hasFilter) {
833
- exportArticles = filterArticles(exportArticles, filter);
834
- }
835
800
  }
836
801
  let output;
837
802
  if (exportOpts.format === "ids") {
@@ -939,7 +904,7 @@ Examples:
939
904
  }
940
905
  }
941
906
  );
942
- program.command("results").description("List articles from a session with title, year, and journal").argument("<session-id>", "session ID to list results from").option("--limit <n>", "maximum number of results to show").option("--offset <n>", "skip first n results").option("--json", "output as JSON array").option("--fields <fields>", "fields to display (comma-separated)").option("-q, --query <expr>", "filter results with query expression").option("--filter-year <range>", "year range filter (deprecated, use -q)").option("--filter-title <keywords>", "title keyword filter (deprecated, use -q)").option("--filter-abstract <keywords>", "abstract keyword filter (deprecated, use -q)").option("--abstract", "show abstracts with results").option("--abstract-length <n>", "maximum abstract length in characters (default: 300)").addHelpText("after", `
907
+ program.command("results").description("List articles from a session with title, year, and journal").argument("<session-id>", "session ID to list results from").option("--limit <n>", "maximum number of results to show").option("--offset <n>", "skip first n results").option("--json", "output as JSON array").option("--fields <fields>", "fields to display (comma-separated)").option("-q, --query <expr>", "filter results with query expression").option("--abstract", "show abstracts with results").option("--abstract-length <n>", "maximum abstract length in characters (default: 300)").addHelpText("after", `
943
908
  Examples:
944
909
  $ search-hub results SESSION_ID # List all articles
945
910
  $ search-hub results SESSION_ID --limit 20 # First 20 articles
@@ -972,9 +937,6 @@ Query syntax:
972
937
  json: options?.json,
973
938
  fields: options?.fields,
974
939
  query: options?.query,
975
- filterYear: options?.filterYear,
976
- filterTitle: options?.filterTitle,
977
- filterAbstract: options?.filterAbstract,
978
940
  abstract: options?.abstract,
979
941
  abstractLength: options?.abstractLength
980
942
  });
@@ -1009,12 +971,6 @@ Query syntax:
1009
971
  if (displayArticles.length !== preFilterCount) {
1010
972
  filteredFrom = preFilterCount;
1011
973
  }
1012
- } else if (resultsOpts.filter) {
1013
- const preFilterCount = displayArticles.length;
1014
- displayArticles = filterArticles(displayArticles, resultsOpts.filter);
1015
- if (displayArticles.length !== preFilterCount) {
1016
- filteredFrom = preFilterCount;
1017
- }
1018
974
  }
1019
975
  const total = displayArticles.length;
1020
976
  const offset = resultsOpts.offset ?? 0;
@@ -1834,13 +1790,21 @@ Examples:
1834
1790
  process.exitCode = EXIT_CODES.SESSION_ERROR;
1835
1791
  }
1836
1792
  });
1837
- reviewCommand.command("finalize").description("Auto-set finalDecision for articles with reviewer consensus").requiredOption("--session <id>", "session ID").option("--dry-run", "preview without changes", false).option("--min-reviewers <n>", "minimum agreeing reviewers needed", "1").action(async (options) => {
1793
+ reviewCommand.command("finalize").description("Auto-set finalDecision for articles with reviewer consensus").requiredOption("--session <id>", "session ID").option("--dry-run", "preview without changes", false).option("--min-reviewers <n>", "minimum agreeing reviewers needed", "1").option("--decision <type>", "only finalize this decision type (include or exclude)").action(async (options) => {
1838
1794
  const globalOpts = program.opts();
1839
1795
  try {
1840
1796
  const sessionsDir = await getSessionsDir(globalOpts);
1797
+ if (options.decision && options.decision !== "include" && options.decision !== "exclude") {
1798
+ if (!globalOpts.quiet) {
1799
+ console.error(`Error: --decision must be "include" or "exclude", got "${options.decision}"`);
1800
+ }
1801
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
1802
+ return;
1803
+ }
1841
1804
  const finalizeOptions = {
1842
1805
  sessionId: options.session,
1843
- ...options.dryRun && { dryRun: options.dryRun }
1806
+ ...options.dryRun && { dryRun: options.dryRun },
1807
+ ...options.decision && { decision: options.decision }
1844
1808
  };
1845
1809
  const minReviewers = parseInt(options.minReviewers, 10);
1846
1810
  if (!Number.isNaN(minReviewers) && minReviewers > 1) {
@@ -1848,7 +1812,10 @@ Examples:
1848
1812
  }
1849
1813
  const result = await executeReviewFinalize(finalizeOptions, sessionsDir);
1850
1814
  if (!globalOpts.quiet) {
1851
- console.log(formatFinalizeOutput(result, { dryRun: options.dryRun }));
1815
+ console.log(formatFinalizeOutput(result, {
1816
+ dryRun: options.dryRun,
1817
+ ...finalizeOptions.decision && { decision: finalizeOptions.decision }
1818
+ }));
1852
1819
  if (!options.dryRun) {
1853
1820
  const statusResult = await executeReviewStatus({ sessionId: options.session }, sessionsDir);
1854
1821
  const suggestion = formatSuggestion(getSuggestion({