@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.
- package/dist/cli/commands/export.d.ts +0 -3
- package/dist/cli/commands/export.d.ts.map +1 -1
- package/dist/cli/commands/export.js +0 -29
- package/dist/cli/commands/export.js.map +1 -1
- package/dist/cli/commands/results.d.ts +0 -5
- package/dist/cli/commands/results.d.ts.map +1 -1
- package/dist/cli/commands/results.js +0 -36
- package/dist/cli/commands/results.js.map +1 -1
- package/dist/cli/commands/review/extract.d.ts.map +1 -1
- package/dist/cli/commands/review/extract.js +10 -2
- package/dist/cli/commands/review/extract.js.map +1 -1
- package/dist/cli/commands/review/finalize.d.ts +2 -0
- package/dist/cli/commands/review/finalize.d.ts.map +1 -1
- package/dist/cli/commands/review/finalize.js +12 -1
- package/dist/cli/commands/review/finalize.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +17 -50
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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;
|
|
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":"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"}
|
|
@@ -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;
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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 =
|
|
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 =
|
|
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":";;;;AAsBA,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,oBAAoB,WAAW,mBAAmB,YAAY;AACpE,UAAI,QAAQ,YAAY,QAAQ,aAAa,mBAAmB;AAC9D,eAAO,gBAAgB,MAAM;AAC7B;AAAA,MACF;AAGA,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;AAAA,MAC1B;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;AAGA,MAAI,SAAS,YAAY,OAAO,gBAAgB,gBAAgB,IAAI,GAAG;AACrE,iBAAa,KAAK,GAAG,OAAO,gBAAgB,gBAAgB,CAAC,4BAA4B;AAAA,EAC3F;AACA,MAAI,SAAS,YAAY,OAAO,gBAAgB,gBAAgB,IAAI,GAAG;AACrE,iBAAa,KAAK,GAAG,OAAO,gBAAgB,gBAAgB,CAAC,4BAA4B;AAAA,EAC3F;AAEA,MAAI,aAAa,SAAS,GAAG;AAC3B,UAAM,KAAK,YAAY,aAAa,KAAK,IAAI,CAAC,EAAE;AAAA,EAClD;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;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,
|
|
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").
|
|
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("--
|
|
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, {
|
|
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({
|