@ncukondo/search-hub 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/cli/commands/query/assess.d.ts +15 -0
  2. package/dist/cli/commands/query/assess.d.ts.map +1 -0
  3. package/dist/cli/commands/query/assess.js +38 -0
  4. package/dist/cli/commands/query/assess.js.map +1 -0
  5. package/dist/cli/commands/query/iteration-log.d.ts +58 -0
  6. package/dist/cli/commands/query/iteration-log.d.ts.map +1 -0
  7. package/dist/cli/commands/query/iteration-log.js +115 -0
  8. package/dist/cli/commands/query/iteration-log.js.map +1 -0
  9. package/dist/cli/commands/query/log.d.ts +9 -0
  10. package/dist/cli/commands/query/log.d.ts.map +1 -0
  11. package/dist/cli/commands/query/log.js +64 -0
  12. package/dist/cli/commands/query/log.js.map +1 -0
  13. package/dist/cli/commands/register.d.ts +1 -1
  14. package/dist/cli/commands/register.js.map +1 -1
  15. package/dist/cli/commands/review/finalize.js +6 -6
  16. package/dist/cli/commands/review/finalize.js.map +1 -1
  17. package/dist/cli/commands/review/init.d.ts.map +1 -1
  18. package/dist/cli/commands/review/init.js +5 -21
  19. package/dist/cli/commands/review/init.js.map +1 -1
  20. package/dist/cli/commands/review/list.d.ts +1 -1
  21. package/dist/cli/commands/review/list.d.ts.map +1 -1
  22. package/dist/cli/commands/review/list.js.map +1 -1
  23. package/dist/cli/commands/review/next-steps.d.ts +1 -1
  24. package/dist/cli/commands/review/next-steps.js +3 -3
  25. package/dist/cli/commands/review/next-steps.js.map +1 -1
  26. package/dist/cli/commands/review/schema.d.ts +199 -0
  27. package/dist/cli/commands/review/schema.d.ts.map +1 -0
  28. package/dist/cli/commands/review/schema.js +77 -0
  29. package/dist/cli/commands/review/schema.js.map +1 -0
  30. package/dist/cli/commands/review/status.d.ts +2 -2
  31. package/dist/cli/commands/review/status.d.ts.map +1 -1
  32. package/dist/cli/commands/review/status.js +13 -13
  33. package/dist/cli/commands/review/status.js.map +1 -1
  34. package/dist/cli/commands/review/types.d.ts +20 -75
  35. package/dist/cli/commands/review/types.d.ts.map +1 -1
  36. package/dist/cli/commands/review/types.js +8 -9
  37. package/dist/cli/commands/review/types.js.map +1 -1
  38. package/dist/cli/index.d.ts.map +1 -1
  39. package/dist/cli/index.js +78 -4
  40. package/dist/cli/index.js.map +1 -1
  41. package/dist/cli/suggestions/rules.d.ts.map +1 -1
  42. package/dist/cli/suggestions/rules.js +22 -2
  43. package/dist/cli/suggestions/rules.js.map +1 -1
  44. package/package.json +1 -1
@@ -0,0 +1,15 @@
1
+ export interface AssessOptions {
2
+ verdict?: string | undefined;
3
+ precision?: string | undefined;
4
+ comment?: string | undefined;
5
+ }
6
+ export interface AssessResult {
7
+ success: boolean;
8
+ error?: string;
9
+ }
10
+ /**
11
+ * Execute the query assess command.
12
+ * Validates inputs and appends an assessment entry to the log.
13
+ */
14
+ export declare function executeQueryAssess(queryFile: string, options: AssessOptions): Promise<AssessResult>;
15
+ //# sourceMappingURL=assess.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assess.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/assess.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,YAAY,CAAC,CAqCvB"}
@@ -0,0 +1,38 @@
1
+ import { access } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { formatTimestamp, appendLogEntry } from "./iteration-log.js";
4
+ async function executeQueryAssess(queryFile, options) {
5
+ if (!options.verdict && !options.precision && !options.comment) {
6
+ return {
7
+ success: false,
8
+ error: "At least one of --verdict, --precision, or --comment is required"
9
+ };
10
+ }
11
+ try {
12
+ await access(queryFile, constants.R_OK);
13
+ } catch {
14
+ return {
15
+ success: false,
16
+ error: `Query file not found: ${queryFile}`
17
+ };
18
+ }
19
+ const entry = {
20
+ date: formatTimestamp(),
21
+ type: "assessment"
22
+ };
23
+ if (options.verdict !== void 0) {
24
+ entry.verdict = options.verdict;
25
+ }
26
+ if (options.precision !== void 0) {
27
+ entry.precision = options.precision;
28
+ }
29
+ if (options.comment !== void 0) {
30
+ entry.comment = options.comment;
31
+ }
32
+ await appendLogEntry(queryFile, entry);
33
+ return { success: true };
34
+ }
35
+ export {
36
+ executeQueryAssess
37
+ };
38
+ //# sourceMappingURL=assess.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assess.js","sources":["../../../../src/cli/commands/query/assess.ts"],"sourcesContent":["/**\n * Query Assess Command\n *\n * Records a structured assessment of the current query iteration\n * to the search iteration log.\n */\nimport { access } from 'node:fs/promises';\nimport { constants } from 'node:fs';\nimport {\n appendLogEntry,\n formatTimestamp,\n type AssessmentLogEntry,\n} from './iteration-log.js';\n\nexport interface AssessOptions {\n verdict?: string | undefined;\n precision?: string | undefined;\n comment?: string | undefined;\n}\n\nexport interface AssessResult {\n success: boolean;\n error?: string;\n}\n\n/**\n * Execute the query assess command.\n * Validates inputs and appends an assessment entry to the log.\n */\nexport async function executeQueryAssess(\n queryFile: string,\n options: AssessOptions,\n): Promise<AssessResult> {\n // Validate at least one option is provided\n if (!options.verdict && !options.precision && !options.comment) {\n return {\n success: false,\n error: 'At least one of --verdict, --precision, or --comment is required',\n };\n }\n\n // Validate query file exists\n try {\n await access(queryFile, constants.R_OK);\n } catch {\n return {\n success: false,\n error: `Query file not found: ${queryFile}`,\n };\n }\n\n const entry: AssessmentLogEntry = {\n date: formatTimestamp(),\n type: 'assessment',\n };\n\n if (options.verdict !== undefined) {\n entry.verdict = options.verdict;\n }\n if (options.precision !== undefined) {\n entry.precision = options.precision;\n }\n if (options.comment !== undefined) {\n entry.comment = options.comment;\n }\n\n await appendLogEntry(queryFile, entry);\n\n return { success: true };\n}\n"],"names":[],"mappings":";;;AA6BA,eAAsB,mBACpB,WACA,SACuB;AAEvB,MAAI,CAAC,QAAQ,WAAW,CAAC,QAAQ,aAAa,CAAC,QAAQ,SAAS;AAC9D,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AAGA,MAAI;AACF,UAAM,OAAO,WAAW,UAAU,IAAI;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,yBAAyB,SAAS;AAAA,IAAA;AAAA,EAE7C;AAEA,QAAM,QAA4B;AAAA,IAChC,MAAM,gBAAA;AAAA,IACN,MAAM;AAAA,EAAA;AAGR,MAAI,QAAQ,YAAY,QAAW;AACjC,UAAM,UAAU,QAAQ;AAAA,EAC1B;AACA,MAAI,QAAQ,cAAc,QAAW;AACnC,UAAM,YAAY,QAAQ;AAAA,EAC5B;AACA,MAAI,QAAQ,YAAY,QAAW;AACjC,UAAM,UAAU,QAAQ;AAAA,EAC1B;AAEA,QAAM,eAAe,WAAW,KAAK;AAErC,SAAO,EAAE,SAAS,KAAA;AACpB;"}
@@ -0,0 +1,58 @@
1
+ import { CountResult, PreviewResult } from '../search.js';
2
+ export interface CountLogEntry {
3
+ date: string;
4
+ type: 'count';
5
+ query_hash: string;
6
+ counts: Record<string, number>;
7
+ total: number;
8
+ }
9
+ export interface PreviewLogEntry {
10
+ date: string;
11
+ type: 'preview';
12
+ query_hash: string;
13
+ counts: Record<string, number>;
14
+ total: number;
15
+ titles: Record<string, string[]>;
16
+ }
17
+ export interface AssessmentLogEntry {
18
+ date: string;
19
+ type: 'assessment';
20
+ verdict?: string;
21
+ precision?: string;
22
+ comment?: string;
23
+ }
24
+ export type LogEntry = CountLogEntry | PreviewLogEntry | AssessmentLogEntry;
25
+ /**
26
+ * Derive the log file path from a query file path.
27
+ * `{dir}/{basename}.yaml` → `{dir}/{basename}.search-log.yaml`
28
+ */
29
+ export declare function getLogFilePath(queryFilePath: string): string;
30
+ /**
31
+ * Generate a timestamp in "YYYY-MM-DD HH:mm" format.
32
+ */
33
+ export declare function formatTimestamp(date?: Date): string;
34
+ /**
35
+ * Compute a short hash of the query file content.
36
+ * Uses the same algorithm as session creation (SHA-256, first 8 hex chars).
37
+ */
38
+ export declare function computeQueryHash(queryContent: string): string;
39
+ /**
40
+ * Build a CountLogEntry from count-only results.
41
+ */
42
+ export declare function buildCountLogEntry(queryHash: string, results: CountResult[]): CountLogEntry;
43
+ /**
44
+ * Build a PreviewLogEntry from preview results.
45
+ * Stores at most `maxTitles` titles per provider to avoid log bloat.
46
+ */
47
+ export declare function buildPreviewLogEntry(queryHash: string, results: PreviewResult[], maxTitles?: number): PreviewLogEntry;
48
+ /**
49
+ * Read log entries from the iteration log file for a given query file.
50
+ * Returns an empty array when the log file does not exist or is empty.
51
+ */
52
+ export declare function readLogEntries(queryFilePath: string): Promise<LogEntry[]>;
53
+ /**
54
+ * Append a log entry to the iteration log file.
55
+ * Creates the file with a header comment if it does not exist.
56
+ */
57
+ export declare function appendLogEntry(queryFilePath: string, entry: LogEntry): Promise<void>;
58
+ //# sourceMappingURL=iteration-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iteration-log.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/iteration-log.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAI/D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,QAAQ,GAAG,aAAa,GAAG,eAAe,GAAG,kBAAkB,CAAC;AAI5E;;;GAGG;AACH,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAK5D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,GAAE,IAAiB,GAAG,MAAM,CAO/D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,WAAW,EAAE,GACrB,aAAa,CAgBf;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,aAAa,EAAE,EACxB,SAAS,SAAI,GACZ,eAAe,CAqBjB;AAmBD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CA2B/E;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,IAAI,CAAC,CAoBf"}
@@ -0,0 +1,115 @@
1
+ import { writeFile, readFile } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { parse, stringify } from "yaml";
5
+ function getLogFilePath(queryFilePath) {
6
+ const dir = dirname(queryFilePath);
7
+ const base = basename(queryFilePath).replace(/\.ya?ml$/, "");
8
+ const logName = `${base}.search-log.yaml`;
9
+ return dir === "." ? logName : join(dir, logName);
10
+ }
11
+ function formatTimestamp(date = /* @__PURE__ */ new Date()) {
12
+ const year = date.getFullYear();
13
+ const month = String(date.getMonth() + 1).padStart(2, "0");
14
+ const day = String(date.getDate()).padStart(2, "0");
15
+ const hours = String(date.getHours()).padStart(2, "0");
16
+ const minutes = String(date.getMinutes()).padStart(2, "0");
17
+ return `${year}-${month}-${day} ${hours}:${minutes}`;
18
+ }
19
+ function computeQueryHash(queryContent) {
20
+ return createHash("sha256").update(queryContent).digest("hex").slice(0, 8);
21
+ }
22
+ function buildCountLogEntry(queryHash, results) {
23
+ const counts = {};
24
+ let total = 0;
25
+ for (const r of results) {
26
+ if (!r.error) {
27
+ counts[r.provider] = r.count;
28
+ total += r.count;
29
+ }
30
+ }
31
+ return {
32
+ date: formatTimestamp(),
33
+ type: "count",
34
+ query_hash: queryHash,
35
+ counts,
36
+ total
37
+ };
38
+ }
39
+ function buildPreviewLogEntry(queryHash, results, maxTitles = 5) {
40
+ const counts = {};
41
+ const titles = {};
42
+ let total = 0;
43
+ for (const r of results) {
44
+ if (!r.error) {
45
+ counts[r.provider] = r.count;
46
+ total += r.count;
47
+ if (r.titles.length > 0) {
48
+ titles[r.provider] = r.titles.slice(0, maxTitles);
49
+ }
50
+ }
51
+ }
52
+ return {
53
+ date: formatTimestamp(),
54
+ type: "preview",
55
+ query_hash: queryHash,
56
+ counts,
57
+ total,
58
+ titles
59
+ };
60
+ }
61
+ async function readRawLog(logPath) {
62
+ try {
63
+ return await readFile(logPath, "utf-8");
64
+ } catch (error) {
65
+ if (error.code === "ENOENT") {
66
+ return null;
67
+ }
68
+ throw error;
69
+ }
70
+ }
71
+ async function readLogEntries(queryFilePath) {
72
+ const logPath = getLogFilePath(queryFilePath);
73
+ const content = await readRawLog(logPath);
74
+ if (content === null || !content.trim()) {
75
+ return [];
76
+ }
77
+ const parsed = parse(content);
78
+ if (parsed === null || parsed === void 0) {
79
+ return [];
80
+ }
81
+ if (!Array.isArray(parsed)) {
82
+ return [];
83
+ }
84
+ return parsed.filter(
85
+ (entry) => entry !== null && typeof entry === "object" && typeof entry.type === "string" && ["count", "preview", "assessment"].includes(entry.type)
86
+ );
87
+ }
88
+ async function appendLogEntry(queryFilePath, entry) {
89
+ const logPath = getLogFilePath(queryFilePath);
90
+ const queryBasename = basename(queryFilePath);
91
+ const existing = await readRawLog(logPath);
92
+ const entryYaml = stringify([entry], { lineWidth: 0 }).trimEnd();
93
+ let content;
94
+ if (existing === null) {
95
+ const header = `# Search iteration log for ${queryBasename}
96
+ # Auto-generated by search-hub. You can also edit this file manually.
97
+
98
+ `;
99
+ content = header + entryYaml + "\n";
100
+ } else {
101
+ const trimmed = existing.trimEnd();
102
+ content = trimmed + "\n\n" + entryYaml + "\n";
103
+ }
104
+ await writeFile(logPath, content, "utf-8");
105
+ }
106
+ export {
107
+ appendLogEntry,
108
+ buildCountLogEntry,
109
+ buildPreviewLogEntry,
110
+ computeQueryHash,
111
+ formatTimestamp,
112
+ getLogFilePath,
113
+ readLogEntries
114
+ };
115
+ //# sourceMappingURL=iteration-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iteration-log.js","sources":["../../../../src/cli/commands/query/iteration-log.ts"],"sourcesContent":["/**\n * Query iteration log I/O.\n *\n * Reads and appends entries to a search iteration log file\n * that lives alongside the query YAML file.\n */\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { createHash } from 'node:crypto';\nimport { basename, dirname, join } from 'node:path';\nimport { parse, stringify } from 'yaml';\nimport type { CountResult, PreviewResult } from '../search.js';\n\n// ── Types ───────────────────────────────────────────────────────────\n\nexport interface CountLogEntry {\n date: string;\n type: 'count';\n query_hash: string;\n counts: Record<string, number>;\n total: number;\n}\n\nexport interface PreviewLogEntry {\n date: string;\n type: 'preview';\n query_hash: string;\n counts: Record<string, number>;\n total: number;\n titles: Record<string, string[]>;\n}\n\nexport interface AssessmentLogEntry {\n date: string;\n type: 'assessment';\n verdict?: string;\n precision?: string;\n comment?: string;\n}\n\nexport type LogEntry = CountLogEntry | PreviewLogEntry | AssessmentLogEntry;\n\n// ── Helpers ─────────────────────────────────────────────────────────\n\n/**\n * Derive the log file path from a query file path.\n * `{dir}/{basename}.yaml` → `{dir}/{basename}.search-log.yaml`\n */\nexport function getLogFilePath(queryFilePath: string): string {\n const dir = dirname(queryFilePath);\n const base = basename(queryFilePath).replace(/\\.ya?ml$/, '');\n const logName = `${base}.search-log.yaml`;\n return dir === '.' ? logName : join(dir, logName);\n}\n\n/**\n * Generate a timestamp in \"YYYY-MM-DD HH:mm\" format.\n */\nexport function formatTimestamp(date: Date = new Date()): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const day = String(date.getDate()).padStart(2, '0');\n const hours = String(date.getHours()).padStart(2, '0');\n const minutes = String(date.getMinutes()).padStart(2, '0');\n return `${year}-${month}-${day} ${hours}:${minutes}`;\n}\n\n/**\n * Compute a short hash of the query file content.\n * Uses the same algorithm as session creation (SHA-256, first 8 hex chars).\n */\nexport function computeQueryHash(queryContent: string): string {\n return createHash('sha256').update(queryContent).digest('hex').slice(0, 8);\n}\n\n/**\n * Build a CountLogEntry from count-only results.\n */\nexport function buildCountLogEntry(\n queryHash: string,\n results: CountResult[],\n): CountLogEntry {\n const counts: Record<string, number> = {};\n let total = 0;\n for (const r of results) {\n if (!r.error) {\n counts[r.provider] = r.count;\n total += r.count;\n }\n }\n return {\n date: formatTimestamp(),\n type: 'count',\n query_hash: queryHash,\n counts,\n total,\n };\n}\n\n/**\n * Build a PreviewLogEntry from preview results.\n * Stores at most `maxTitles` titles per provider to avoid log bloat.\n */\nexport function buildPreviewLogEntry(\n queryHash: string,\n results: PreviewResult[],\n maxTitles = 5,\n): PreviewLogEntry {\n const counts: Record<string, number> = {};\n const titles: Record<string, string[]> = {};\n let total = 0;\n for (const r of results) {\n if (!r.error) {\n counts[r.provider] = r.count;\n total += r.count;\n if (r.titles.length > 0) {\n titles[r.provider] = r.titles.slice(0, maxTitles);\n }\n }\n }\n return {\n date: formatTimestamp(),\n type: 'preview',\n query_hash: queryHash,\n counts,\n total,\n titles,\n };\n}\n\n// ── Read / Write ────────────────────────────────────────────────────\n\n/**\n * Read raw content of the log file, preserving comments.\n * Returns null when the file does not exist.\n */\nasync function readRawLog(logPath: string): Promise<string | null> {\n try {\n return await readFile(logPath, 'utf-8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return null;\n }\n throw error;\n }\n}\n\n/**\n * Read log entries from the iteration log file for a given query file.\n * Returns an empty array when the log file does not exist or is empty.\n */\nexport async function readLogEntries(queryFilePath: string): Promise<LogEntry[]> {\n const logPath = getLogFilePath(queryFilePath);\n const content = await readRawLog(logPath);\n\n if (content === null || !content.trim()) {\n return [];\n }\n\n const parsed = parse(content);\n\n // YAML with only comments parses to null\n if (parsed === null || parsed === undefined) {\n return [];\n }\n\n if (!Array.isArray(parsed)) {\n return [];\n }\n\n // Filter out malformed entries that lack a valid type field\n return parsed.filter(\n (entry): entry is LogEntry =>\n entry !== null &&\n typeof entry === 'object' &&\n typeof entry.type === 'string' &&\n ['count', 'preview', 'assessment'].includes(entry.type),\n );\n}\n\n/**\n * Append a log entry to the iteration log file.\n * Creates the file with a header comment if it does not exist.\n */\nexport async function appendLogEntry(\n queryFilePath: string,\n entry: LogEntry,\n): Promise<void> {\n const logPath = getLogFilePath(queryFilePath);\n const queryBasename = basename(queryFilePath);\n const existing = await readRawLog(logPath);\n\n // Stringify the single entry as a YAML sequence item\n const entryYaml = stringify([entry], { lineWidth: 0 }).trimEnd();\n\n let content: string;\n if (existing === null) {\n const header =\n `# Search iteration log for ${queryBasename}\\n` +\n `# Auto-generated by search-hub. You can also edit this file manually.\\n\\n`;\n content = header + entryYaml + '\\n';\n } else {\n const trimmed = existing.trimEnd();\n content = trimmed + '\\n\\n' + entryYaml + '\\n';\n }\n\n await writeFile(logPath, content, 'utf-8');\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { LogEntry } from './iteration-log.js';
2
+ export interface LogOutputOptions {
3
+ json?: boolean | undefined;
4
+ }
5
+ /**
6
+ * Format log entries for display.
7
+ */
8
+ export declare function formatLogOutput(entries: LogEntry[], options?: LogOutputOptions): string;
9
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/log.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EACV,QAAQ,EAIT,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC5B;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,QAAQ,EAAE,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,CA2BR"}
@@ -0,0 +1,64 @@
1
+ function formatLogOutput(entries, options) {
2
+ if (options?.json) {
3
+ return JSON.stringify(entries, null, 2);
4
+ }
5
+ if (entries.length === 0) {
6
+ return "No iteration log entries.";
7
+ }
8
+ const lines = [];
9
+ for (const entry of entries) {
10
+ switch (entry.type) {
11
+ case "count":
12
+ lines.push(formatCountEntry(entry));
13
+ break;
14
+ case "preview":
15
+ lines.push(formatPreviewEntry(entry));
16
+ break;
17
+ case "assessment":
18
+ lines.push(formatAssessmentEntry(entry));
19
+ break;
20
+ }
21
+ lines.push("");
22
+ }
23
+ return lines.join("\n").trimEnd();
24
+ }
25
+ function formatCountEntry(entry) {
26
+ const parts = [];
27
+ parts.push(`[${entry.date}] count (${entry.query_hash})`);
28
+ for (const [provider, count] of Object.entries(entry.counts)) {
29
+ parts.push(` ${provider}: ${count}`);
30
+ }
31
+ parts.push(` total: ${entry.total}`);
32
+ return parts.join("\n");
33
+ }
34
+ function formatPreviewEntry(entry) {
35
+ const parts = [];
36
+ parts.push(`[${entry.date}] preview (${entry.query_hash})`);
37
+ for (const [provider, count] of Object.entries(entry.counts)) {
38
+ parts.push(` ${provider}: ${count}`);
39
+ const providerTitles = entry.titles[provider];
40
+ if (providerTitles) {
41
+ for (const title of providerTitles) {
42
+ parts.push(` • ${title}`);
43
+ }
44
+ }
45
+ }
46
+ parts.push(` total: ${entry.total}`);
47
+ return parts.join("\n");
48
+ }
49
+ function formatAssessmentEntry(entry) {
50
+ const parts = [];
51
+ const meta = [];
52
+ if (entry.verdict) meta.push(`verdict: ${entry.verdict}`);
53
+ if (entry.precision) meta.push(`precision: ${entry.precision}`);
54
+ const metaStr = meta.length > 0 ? ` ${meta.join(", ")}` : "";
55
+ parts.push(`[${entry.date}] assessment:${metaStr}`);
56
+ if (entry.comment) {
57
+ parts.push(` ${entry.comment}`);
58
+ }
59
+ return parts.join("\n");
60
+ }
61
+ export {
62
+ formatLogOutput
63
+ };
64
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sources":["../../../../src/cli/commands/query/log.ts"],"sourcesContent":["/**\n * Query Log Command\n *\n * Formats and displays the query iteration log.\n */\nimport type {\n LogEntry,\n CountLogEntry,\n PreviewLogEntry,\n AssessmentLogEntry,\n} from './iteration-log.js';\n\nexport interface LogOutputOptions {\n json?: boolean | undefined;\n}\n\n/**\n * Format log entries for display.\n */\nexport function formatLogOutput(\n entries: LogEntry[],\n options?: LogOutputOptions,\n): string {\n if (options?.json) {\n return JSON.stringify(entries, null, 2);\n }\n\n if (entries.length === 0) {\n return 'No iteration log entries.';\n }\n\n const lines: string[] = [];\n\n for (const entry of entries) {\n switch (entry.type) {\n case 'count':\n lines.push(formatCountEntry(entry));\n break;\n case 'preview':\n lines.push(formatPreviewEntry(entry));\n break;\n case 'assessment':\n lines.push(formatAssessmentEntry(entry));\n break;\n }\n lines.push('');\n }\n\n return lines.join('\\n').trimEnd();\n}\n\nfunction formatCountEntry(entry: CountLogEntry): string {\n const parts: string[] = [];\n parts.push(`[${entry.date}] count (${entry.query_hash})`);\n for (const [provider, count] of Object.entries(entry.counts)) {\n parts.push(` ${provider}: ${count}`);\n }\n parts.push(` total: ${entry.total}`);\n return parts.join('\\n');\n}\n\nfunction formatPreviewEntry(entry: PreviewLogEntry): string {\n const parts: string[] = [];\n parts.push(`[${entry.date}] preview (${entry.query_hash})`);\n for (const [provider, count] of Object.entries(entry.counts)) {\n parts.push(` ${provider}: ${count}`);\n const providerTitles = entry.titles[provider];\n if (providerTitles) {\n for (const title of providerTitles) {\n parts.push(` • ${title}`);\n }\n }\n }\n parts.push(` total: ${entry.total}`);\n return parts.join('\\n');\n}\n\nfunction formatAssessmentEntry(entry: AssessmentLogEntry): string {\n const parts: string[] = [];\n const meta: string[] = [];\n if (entry.verdict) meta.push(`verdict: ${entry.verdict}`);\n if (entry.precision) meta.push(`precision: ${entry.precision}`);\n const metaStr = meta.length > 0 ? ` ${meta.join(', ')}` : '';\n parts.push(`[${entry.date}] assessment:${metaStr}`);\n if (entry.comment) {\n parts.push(` ${entry.comment}`);\n }\n return parts.join('\\n');\n}\n"],"names":[],"mappings":"AAmBO,SAAS,gBACd,SACA,SACQ;AACR,MAAI,SAAS,MAAM;AACjB,WAAO,KAAK,UAAU,SAAS,MAAM,CAAC;AAAA,EACxC;AAEA,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,QAAM,QAAkB,CAAA;AAExB,aAAW,SAAS,SAAS;AAC3B,YAAQ,MAAM,MAAA;AAAA,MACZ,KAAK;AACH,cAAM,KAAK,iBAAiB,KAAK,CAAC;AAClC;AAAA,MACF,KAAK;AACH,cAAM,KAAK,mBAAmB,KAAK,CAAC;AACpC;AAAA,MACF,KAAK;AACH,cAAM,KAAK,sBAAsB,KAAK,CAAC;AACvC;AAAA,IAAA;AAEJ,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,SAAO,MAAM,KAAK,IAAI,EAAE,QAAA;AAC1B;AAEA,SAAS,iBAAiB,OAA8B;AACtD,QAAM,QAAkB,CAAA;AACxB,QAAM,KAAK,IAAI,MAAM,IAAI,YAAY,MAAM,UAAU,GAAG;AACxD,aAAW,CAAC,UAAU,KAAK,KAAK,OAAO,QAAQ,MAAM,MAAM,GAAG;AAC5D,UAAM,KAAK,KAAK,QAAQ,KAAK,KAAK,EAAE;AAAA,EACtC;AACA,QAAM,KAAK,YAAY,MAAM,KAAK,EAAE;AACpC,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,mBAAmB,OAAgC;AAC1D,QAAM,QAAkB,CAAA;AACxB,QAAM,KAAK,IAAI,MAAM,IAAI,cAAc,MAAM,UAAU,GAAG;AAC1D,aAAW,CAAC,UAAU,KAAK,KAAK,OAAO,QAAQ,MAAM,MAAM,GAAG;AAC5D,UAAM,KAAK,KAAK,QAAQ,KAAK,KAAK,EAAE;AACpC,UAAM,iBAAiB,MAAM,OAAO,QAAQ;AAC5C,QAAI,gBAAgB;AAClB,iBAAW,SAAS,gBAAgB;AAClC,cAAM,KAAK,SAAS,KAAK,EAAE;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK,YAAY,MAAM,KAAK,EAAE;AACpC,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,sBAAsB,OAAmC;AAChE,QAAM,QAAkB,CAAA;AACxB,QAAM,OAAiB,CAAA;AACvB,MAAI,MAAM,QAAS,MAAK,KAAK,YAAY,MAAM,OAAO,EAAE;AACxD,MAAI,MAAM,UAAW,MAAK,KAAK,cAAc,MAAM,SAAS,EAAE;AAC9D,QAAM,UAAU,KAAK,SAAS,IAAI,IAAI,KAAK,KAAK,IAAI,CAAC,KAAK;AAC1D,QAAM,KAAK,IAAI,MAAM,IAAI,gBAAgB,OAAO,EAAE;AAClD,MAAI,MAAM,SAAS;AACjB,UAAM,KAAK,KAAK,MAAM,OAAO,EAAE;AAAA,EACjC;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
@@ -58,7 +58,7 @@ export interface ReviewSummary {
58
58
  included: number;
59
59
  /** Articles with finalDecision='exclude' */
60
60
  excluded: number;
61
- /** Articles without finalDecision (pending, incomplete, uncertain, agreed, conflicting) */
61
+ /** Articles without finalDecision (pending, incomplete, all-uncertain, agreed, divided) */
62
62
  pending: number;
63
63
  }
64
64
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"register.js","sources":["../../../src/cli/commands/register.ts"],"sourcesContent":["/**\n * Register command for reference-manager integration.\n * Registers search results with reference-manager CLI.\n */\n\nimport { join } from 'node:path';\nimport { readFile, access } from 'node:fs/promises';\nimport { constants } from 'node:fs';\nimport { createInterface } from 'node:readline';\nimport { parse as parseYaml } from 'yaml';\nimport type { ProviderName, Article, Author } from '../../providers/base/types.js';\nimport { parseProviderNames } from '../utils/validation.js';\nimport { classifyStatus, type ReviewFile } from './review/types.js';\n\nexport interface RegisterCommandOptions {\n sessionId: string;\n providers?: ProviderName[];\n dryRun: boolean;\n withAbstracts: boolean;\n /** Register only reviewed articles with finalDecision='include' */\n reviewed?: boolean;\n /** Register all articles, ignoring reviews */\n all?: boolean;\n /** Skip confirmation prompts */\n force?: boolean;\n /** Suppress tips and suggestions */\n quiet?: boolean;\n}\n\nexport interface CommandLineOptions {\n db?: string | undefined;\n dryRun?: boolean | undefined;\n withAbstracts?: boolean | undefined;\n reviewed?: boolean | undefined;\n all?: boolean | undefined;\n force?: boolean | undefined;\n quiet?: boolean | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\n/**\n * Parse command line options into RegisterCommandOptions.\n */\nexport function parseRegisterOptions(\n sessionId: string,\n options: CommandLineOptions\n): RegisterCommandOptions {\n const result: RegisterCommandOptions = {\n sessionId,\n dryRun: options.dryRun ?? false,\n withAbstracts: options.withAbstracts ?? false,\n reviewed: options.reviewed ?? false,\n all: options.all ?? false,\n force: options.force ?? false,\n quiet: options.quiet ?? false,\n };\n\n if (options.db) {\n result.providers = parseProviderNames(options.db);\n }\n\n return result;\n}\n\n/**\n * Validate register command input.\n */\nexport function validateRegisterInput(options: RegisterCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Format registration summary for CLI output.\n */\nexport function formatRegistrationSummary(summary: {\n total: number;\n added: number;\n skipped: number;\n failed: number;\n noId: number;\n}): string {\n const lines: string[] = ['Registration complete:'];\n\n // Added\n lines.push(` ✓ ${summary.added} added`);\n\n // Duplicates (skipped)\n if (summary.skipped > 0) {\n lines.push(` ⚠ ${summary.skipped} duplicates (already in library)`);\n }\n\n // Failed\n if (summary.failed > 0) {\n lines.push(` ✗ ${summary.failed} failed`);\n }\n\n // No ID (skipped)\n if (summary.noId > 0) {\n lines.push(` - ${summary.noId} skipped (no identifier)`);\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Get registration identifier for an article.\n * PMID is preferred over DOI for better metadata quality.\n */\nfunction getRegistrationId(article: Article): string | null {\n if (article.pmid) {\n return `pmid:${article.pmid}`;\n }\n if (article.doi) {\n return article.doi;\n }\n return null;\n}\n\n/**\n * Format dry run output showing what would be registered.\n */\nexport function formatDryRunOutput(articles: Article[]): string {\n const withId: Array<{ article: Article; id: string }> = [];\n const withoutId: Article[] = [];\n\n for (const article of articles) {\n const id = getRegistrationId(article);\n if (id) {\n withId.push({ article, id });\n } else {\n withoutId.push(article);\n }\n }\n\n const lines: string[] = [];\n\n // Summary\n lines.push(\n `Would register ${withId.length} reference${withId.length !== 1 ? 's' : ''}:`\n );\n\n // List articles with IDs\n for (const { id, article } of withId) {\n const title = article.title.length > 60\n ? article.title.substring(0, 57) + '...'\n : article.title;\n lines.push(` - ${id}: ${title}`);\n }\n\n // Details about articles without DOI/PMID\n if (withoutId.length > 0) {\n lines.push('');\n lines.push(\n `${withoutId.length} article${withoutId.length !== 1 ? 's' : ''} will be skipped (no DOI or PMID):`\n );\n\n const maxDisplay = 10;\n const displayed = withoutId.slice(0, maxDisplay);\n\n for (const article of displayed) {\n const truncatedTitle = article.title.length > 50\n ? article.title.substring(0, 50) + '...'\n : article.title;\n\n const altIds = getAlternativeIds(article);\n const hasAltIds = altIds.length > 0 ? `, has: ${altIds.join(', ')}` : '';\n\n lines.push(` - \"${truncatedTitle}\" (source: ${article.source}${hasAltIds})`);\n }\n\n if (withoutId.length > maxDisplay) {\n lines.push(` ... and ${withoutId.length - maxDisplay} more`);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Get alternative (non-DOI/PMID) identifiers for an article.\n */\nfunction getAlternativeIds(article: Article): string[] {\n const ids: string[] = [];\n if (article.arxivId) ids.push(`arxiv:${article.arxivId}`);\n if (article.ericId) ids.push(`eric:${article.ericId}`);\n if (article.scopusId) ids.push(`scopus:${article.scopusId}`);\n return ids;\n}\n\n/**\n * Summary of review decisions for a session.\n */\nexport interface ReviewSummary {\n /** Total articles in review file */\n total: number;\n /** Articles with finalDecision='include' */\n included: number;\n /** Articles with finalDecision='exclude' */\n excluded: number;\n /** Articles without finalDecision (pending, incomplete, uncertain, agreed, conflicting) */\n pending: number;\n}\n\n/**\n * Check if a session has a reviews.yaml file.\n */\nexport async function hasReviewFile(sessionId: string, sessionsDir: string): Promise<boolean> {\n const reviewsPath = join(sessionsDir, sessionId, '.internal', 'reviews.yaml');\n try {\n await access(reviewsPath, constants.R_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Load and parse the review file for a session.\n */\nasync function loadReviewFile(sessionId: string, sessionsDir: string): Promise<ReviewFile> {\n const reviewsPath = join(sessionsDir, sessionId, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Get review summary (counts) for a session.\n * Throws if reviews.yaml does not exist.\n */\nexport async function getReviewSummary(sessionId: string, sessionsDir: string): Promise<ReviewSummary> {\n const reviewFile = await loadReviewFile(sessionId, sessionsDir);\n const articles = reviewFile.articles ?? [];\n\n const summary: ReviewSummary = {\n total: articles.length,\n included: 0,\n excluded: 0,\n pending: 0,\n };\n\n for (const article of articles) {\n const status = classifyStatus(article);\n\n if (status === 'finalized') {\n if (article.finalDecision === 'include') {\n summary.included++;\n } else {\n summary.excluded++;\n }\n } else {\n // pending, incomplete, uncertain, agreed, conflicting all count as pending for registration\n summary.pending++;\n }\n }\n\n return summary;\n}\n\n/**\n * Parse author name string into Author object.\n * Simple heuristic: last word is family name, rest is given name.\n */\nfunction parseAuthorName(name: string): Author {\n const parts = name.trim().split(/\\s+/);\n if (parts.length === 1) {\n return { family: parts[0] ?? '' };\n }\n // Last part is family name (most common pattern in scientific citations)\n const family = parts.pop() ?? '';\n const given = parts.join(' ');\n return { family, given };\n}\n\n/**\n * Get articles with finalDecision='include' from review file.\n * Converts from ArticleEntry format to Article format.\n *\n * @throws Error if mergedFrom is missing or empty (indicates legacy review file)\n */\nexport async function getIncludedArticles(sessionId: string, sessionsDir: string): Promise<Article[]> {\n const reviewFile = await loadReviewFile(sessionId, sessionsDir);\n const articles = reviewFile.articles ?? [];\n\n return articles\n .filter((entry) => entry.finalDecision === 'include')\n .map((entry): Article => {\n // Validate mergedFrom exists\n if (!entry.mergedFrom) {\n throw new Error(\n `Article \"${entry.title}\" has mergedFrom missing. ` +\n `This may be a legacy review file created before source tracking was fixed. ` +\n `Please re-run 'review init' to regenerate the review file with source tracking.`\n );\n }\n if (entry.mergedFrom.length === 0) {\n throw new Error(\n `Article \"${entry.title}\" has empty mergedFrom array. ` +\n `This is an invalid state - please re-run 'review init' to regenerate.`\n );\n }\n\n const authors: Author[] = entry.authors\n ? entry.authors.split(/,\\s*/).map(parseAuthorName)\n : [];\n\n // Get source from the first entry in mergedFrom\n const source = entry.mergedFrom[0]!.source as ProviderName;\n\n const article: Article = {\n title: entry.title,\n authors,\n source,\n retrievedAt: new Date().toISOString(),\n };\n // Only set optional fields if they have values\n if (entry.doi) article.doi = entry.doi;\n if (entry.pmid) article.pmid = entry.pmid;\n if (entry.scopusId) article.scopusId = entry.scopusId;\n if (entry.arxivId) article.arxivId = entry.arxivId;\n if (entry.ericId) article.ericId = entry.ericId;\n if (entry.abstract) article.abstract = entry.abstract;\n if (entry.year) article.publicationDate = entry.year;\n return article;\n });\n}\n\n/**\n * Format message when reviews exist but no flag specified.\n */\nexport function formatReviewRequiredMessage(summary: ReviewSummary, sessionId: string): string {\n return `This session has a review file.\n Status: ${summary.included} include / ${summary.excluded} exclude / ${summary.pending} pending\n\nPlease specify which articles to register:\n --reviewed Register ${summary.included} included articles\n --all Register all ${summary.total} articles (ignore reviews)\n\nExample:\n search-hub register ${sessionId} --reviewed`;\n}\n\n/**\n * Format error when --reviewed used but no articles are included.\n */\nexport function formatNoIncludedArticlesError(summary: ReviewSummary, sessionId: string): string {\n return `Error: No articles marked as 'include' in reviews.\n Status: ${summary.included} include / ${summary.excluded} exclude / ${summary.pending} pending\n\nRun 'search-hub review status ${sessionId}' for details.`;\n}\n\n/**\n * Format warning when pending articles exist with --reviewed.\n */\nexport function formatPendingWarning(summary: ReviewSummary): string {\n const articleWord = summary.pending === 1 ? 'article' : 'articles';\n return `Warning: ${summary.pending} ${articleWord} still pending review (will be skipped).\nRegistering ${summary.included} included articles...\n\nProceed? [Y/n]`;\n}\n\n\n/**\n * Format note when --all is used with reviews.yaml present.\n */\nexport function formatIgnoringReviewsNote(total: number): string {\n return `Note: Ignoring review decisions. Registering all ${total} articles.`;\n}\n\n/**\n * Prompt user for Y/n confirmation.\n * Returns true if user confirms (Y/y/Enter), false otherwise.\n */\nexport async function confirmPrompt(\n input: NodeJS.ReadableStream = process.stdin,\n output: NodeJS.WritableStream = process.stdout\n): Promise<boolean> {\n const rl = createInterface({\n input,\n output,\n terminal: false,\n });\n\n return new Promise((resolve) => {\n rl.question('', (answer) => {\n rl.close();\n const trimmed = answer.trim().toLowerCase();\n // Empty (Enter) or 'y' or 'yes' means confirm\n resolve(trimmed === '' || trimmed === 'y' || trimmed === 'yes');\n });\n });\n}\n"],"names":["parseYaml"],"mappings":";;;;;;;AA+CO,SAAS,qBACd,WACA,SACwB;AACxB,QAAM,SAAiC;AAAA,IACrC;AAAA,IACA,QAAQ,QAAQ,UAAU;AAAA,IAC1B,eAAe,QAAQ,iBAAiB;AAAA,IACxC,UAAU,QAAQ,YAAY;AAAA,IAC9B,KAAK,QAAQ,OAAO;AAAA,IACpB,OAAO,QAAQ,SAAS;AAAA,IACxB,OAAO,QAAQ,SAAS;AAAA,EAAA;AAG1B,MAAI,QAAQ,IAAI;AACd,WAAO,YAAY,mBAAmB,QAAQ,EAAE;AAAA,EAClD;AAEA,SAAO;AACT;AAKO,SAAS,sBAAsB,SAAmD;AACvF,MAAI,CAAC,QAAQ,aAAa,QAAQ,UAAU,KAAA,MAAW,IAAI;AACzD,WAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO;AAAA,IAAA;AAAA,EAEX;AAEA,SAAO,EAAE,OAAO,KAAA;AAClB;AAKO,SAAS,0BAA0B,SAM/B;AACT,QAAM,QAAkB,CAAC,wBAAwB;AAGjD,QAAM,KAAK,OAAO,QAAQ,KAAK,QAAQ;AAGvC,MAAI,QAAQ,UAAU,GAAG;AACvB,UAAM,KAAK,OAAO,QAAQ,OAAO,kCAAkC;AAAA,EACrE;AAGA,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,KAAK,OAAO,QAAQ,MAAM,SAAS;AAAA,EAC3C;AAGA,MAAI,QAAQ,OAAO,GAAG;AACpB,UAAM,KAAK,OAAO,QAAQ,IAAI,0BAA0B;AAAA,EAC1D;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAMA,SAAS,kBAAkB,SAAiC;AAC1D,MAAI,QAAQ,MAAM;AAChB,WAAO,QAAQ,QAAQ,IAAI;AAAA,EAC7B;AACA,MAAI,QAAQ,KAAK;AACf,WAAO,QAAQ;AAAA,EACjB;AACA,SAAO;AACT;AAKO,SAAS,mBAAmB,UAA6B;AAC9D,QAAM,SAAkD,CAAA;AACxD,QAAM,YAAuB,CAAA;AAE7B,aAAW,WAAW,UAAU;AAC9B,UAAM,KAAK,kBAAkB,OAAO;AACpC,QAAI,IAAI;AACN,aAAO,KAAK,EAAE,SAAS,GAAA,CAAI;AAAA,IAC7B,OAAO;AACL,gBAAU,KAAK,OAAO;AAAA,IACxB;AAAA,EACF;AAEA,QAAM,QAAkB,CAAA;AAGxB,QAAM;AAAA,IACJ,kBAAkB,OAAO,MAAM,aAAa,OAAO,WAAW,IAAI,MAAM,EAAE;AAAA,EAAA;AAI5E,aAAW,EAAE,IAAI,QAAA,KAAa,QAAQ;AACpC,UAAM,QAAQ,QAAQ,MAAM,SAAS,KACjC,QAAQ,MAAM,UAAU,GAAG,EAAE,IAAI,QACjC,QAAQ;AACZ,UAAM,KAAK,OAAO,EAAE,KAAK,KAAK,EAAE;AAAA,EAClC;AAGA,MAAI,UAAU,SAAS,GAAG;AACxB,UAAM,KAAK,EAAE;AACb,UAAM;AAAA,MACJ,GAAG,UAAU,MAAM,WAAW,UAAU,WAAW,IAAI,MAAM,EAAE;AAAA,IAAA;AAGjE,UAAM,aAAa;AACnB,UAAM,YAAY,UAAU,MAAM,GAAG,UAAU;AAE/C,eAAW,WAAW,WAAW;AAC/B,YAAM,iBAAiB,QAAQ,MAAM,SAAS,KAC1C,QAAQ,MAAM,UAAU,GAAG,EAAE,IAAI,QACjC,QAAQ;AAEZ,YAAM,SAAS,kBAAkB,OAAO;AACxC,YAAM,YAAY,OAAO,SAAS,IAAI,UAAU,OAAO,KAAK,IAAI,CAAC,KAAK;AAEtE,YAAM,KAAK,QAAQ,cAAc,cAAc,QAAQ,MAAM,GAAG,SAAS,GAAG;AAAA,IAC9E;AAEA,QAAI,UAAU,SAAS,YAAY;AACjC,YAAM,KAAK,aAAa,UAAU,SAAS,UAAU,OAAO;AAAA,IAC9D;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAKA,SAAS,kBAAkB,SAA4B;AACrD,QAAM,MAAgB,CAAA;AACtB,MAAI,QAAQ,QAAS,KAAI,KAAK,SAAS,QAAQ,OAAO,EAAE;AACxD,MAAI,QAAQ,OAAQ,KAAI,KAAK,QAAQ,QAAQ,MAAM,EAAE;AACrD,MAAI,QAAQ,SAAU,KAAI,KAAK,UAAU,QAAQ,QAAQ,EAAE;AAC3D,SAAO;AACT;AAmBA,eAAsB,cAAc,WAAmB,aAAuC;AAC5F,QAAM,cAAc,KAAK,aAAa,WAAW,aAAa,cAAc;AAC5E,MAAI;AACF,UAAM,OAAO,aAAa,UAAU,IAAI;AACxC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAe,eAAe,WAAmB,aAA0C;AACzF,QAAM,cAAc,KAAK,aAAa,WAAW,aAAa,cAAc;AAC5E,QAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,SAAOA,MAAU,OAAO;AAC1B;AAMA,eAAsB,iBAAiB,WAAmB,aAA6C;AACrG,QAAM,aAAa,MAAM,eAAe,WAAW,WAAW;AAC9D,QAAM,WAAW,WAAW,YAAY,CAAA;AAExC,QAAM,UAAyB;AAAA,IAC7B,OAAO,SAAS;AAAA,IAChB,UAAU;AAAA,IACV,UAAU;AAAA,IACV,SAAS;AAAA,EAAA;AAGX,aAAW,WAAW,UAAU;AAC9B,UAAM,SAAS,eAAe,OAAO;AAErC,QAAI,WAAW,aAAa;AAC1B,UAAI,QAAQ,kBAAkB,WAAW;AACvC,gBAAQ;AAAA,MACV,OAAO;AACL,gBAAQ;AAAA,MACV;AAAA,IACF,OAAO;AAEL,cAAQ;AAAA,IACV;AAAA,EACF;AAEA,SAAO;AACT;AAMA,SAAS,gBAAgB,MAAsB;AAC7C,QAAM,QAAQ,KAAK,KAAA,EAAO,MAAM,KAAK;AACrC,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,EAAE,QAAQ,MAAM,CAAC,KAAK,GAAA;AAAA,EAC/B;AAEA,QAAM,SAAS,MAAM,IAAA,KAAS;AAC9B,QAAM,QAAQ,MAAM,KAAK,GAAG;AAC5B,SAAO,EAAE,QAAQ,MAAA;AACnB;AAQA,eAAsB,oBAAoB,WAAmB,aAAyC;AACpG,QAAM,aAAa,MAAM,eAAe,WAAW,WAAW;AAC9D,QAAM,WAAW,WAAW,YAAY,CAAA;AAExC,SAAO,SACJ,OAAO,CAAC,UAAU,MAAM,kBAAkB,SAAS,EACnD,IAAI,CAAC,UAAmB;AAEvB,QAAI,CAAC,MAAM,YAAY;AACrB,YAAM,IAAI;AAAA,QACR,YAAY,MAAM,KAAK;AAAA,MAAA;AAAA,IAI3B;AACA,QAAI,MAAM,WAAW,WAAW,GAAG;AACjC,YAAM,IAAI;AAAA,QACR,YAAY,MAAM,KAAK;AAAA,MAAA;AAAA,IAG3B;AAEA,UAAM,UAAoB,MAAM,UAC5B,MAAM,QAAQ,MAAM,MAAM,EAAE,IAAI,eAAe,IAC/C,CAAA;AAGJ,UAAM,SAAS,MAAM,WAAW,CAAC,EAAG;AAEpC,UAAM,UAAmB;AAAA,MACvB,OAAO,MAAM;AAAA,MACb;AAAA,MACA;AAAA,MACA,cAAa,oBAAI,KAAA,GAAO,YAAA;AAAA,IAAY;AAGtC,QAAI,MAAM,IAAK,SAAQ,MAAM,MAAM;AACnC,QAAI,MAAM,KAAM,SAAQ,OAAO,MAAM;AACrC,QAAI,MAAM,SAAU,SAAQ,WAAW,MAAM;AAC7C,QAAI,MAAM,QAAS,SAAQ,UAAU,MAAM;AAC3C,QAAI,MAAM,OAAQ,SAAQ,SAAS,MAAM;AACzC,QAAI,MAAM,SAAU,SAAQ,WAAW,MAAM;AAC7C,QAAI,MAAM,KAAM,SAAQ,kBAAkB,MAAM;AAChD,WAAO;AAAA,EACT,CAAC;AACL;AAKO,SAAS,4BAA4B,SAAwB,WAA2B;AAC7F,SAAO;AAAA,YACG,QAAQ,QAAQ,cAAc,QAAQ,QAAQ,cAAc,QAAQ,OAAO;AAAA;AAAA;AAAA,0BAG7D,QAAQ,QAAQ;AAAA,8BACZ,QAAQ,KAAK;AAAA;AAAA;AAAA,wBAGnB,SAAS;AACjC;AAKO,SAAS,8BAA8B,SAAwB,WAA2B;AAC/F,SAAO;AAAA,YACG,QAAQ,QAAQ,cAAc,QAAQ,QAAQ,cAAc,QAAQ,OAAO;AAAA;AAAA,gCAEvD,SAAS;AACzC;AAKO,SAAS,qBAAqB,SAAgC;AACnE,QAAM,cAAc,QAAQ,YAAY,IAAI,YAAY;AACxD,SAAO,YAAY,QAAQ,OAAO,IAAI,WAAW;AAAA,cACrC,QAAQ,QAAQ;AAAA;AAAA;AAG9B;AAMO,SAAS,0BAA0B,OAAuB;AAC/D,SAAO,oDAAoD,KAAK;AAClE;AAMA,eAAsB,cACpB,QAA+B,QAAQ,OACvC,SAAgC,QAAQ,QACtB;AAClB,QAAM,KAAK,gBAAgB;AAAA,IACzB;AAAA,IACA;AAAA,IACA,UAAU;AAAA,EAAA,CACX;AAED,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,OAAG,SAAS,IAAI,CAAC,WAAW;AAC1B,SAAG,MAAA;AACH,YAAM,UAAU,OAAO,KAAA,EAAO,YAAA;AAE9B,cAAQ,YAAY,MAAM,YAAY,OAAO,YAAY,KAAK;AAAA,IAChE,CAAC;AAAA,EACH,CAAC;AACH;"}
1
+ {"version":3,"file":"register.js","sources":["../../../src/cli/commands/register.ts"],"sourcesContent":["/**\n * Register command for reference-manager integration.\n * Registers search results with reference-manager CLI.\n */\n\nimport { join } from 'node:path';\nimport { readFile, access } from 'node:fs/promises';\nimport { constants } from 'node:fs';\nimport { createInterface } from 'node:readline';\nimport { parse as parseYaml } from 'yaml';\nimport type { ProviderName, Article, Author } from '../../providers/base/types.js';\nimport { parseProviderNames } from '../utils/validation.js';\nimport { classifyStatus, type ReviewFile } from './review/types.js';\n\nexport interface RegisterCommandOptions {\n sessionId: string;\n providers?: ProviderName[];\n dryRun: boolean;\n withAbstracts: boolean;\n /** Register only reviewed articles with finalDecision='include' */\n reviewed?: boolean;\n /** Register all articles, ignoring reviews */\n all?: boolean;\n /** Skip confirmation prompts */\n force?: boolean;\n /** Suppress tips and suggestions */\n quiet?: boolean;\n}\n\nexport interface CommandLineOptions {\n db?: string | undefined;\n dryRun?: boolean | undefined;\n withAbstracts?: boolean | undefined;\n reviewed?: boolean | undefined;\n all?: boolean | undefined;\n force?: boolean | undefined;\n quiet?: boolean | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\n/**\n * Parse command line options into RegisterCommandOptions.\n */\nexport function parseRegisterOptions(\n sessionId: string,\n options: CommandLineOptions\n): RegisterCommandOptions {\n const result: RegisterCommandOptions = {\n sessionId,\n dryRun: options.dryRun ?? false,\n withAbstracts: options.withAbstracts ?? false,\n reviewed: options.reviewed ?? false,\n all: options.all ?? false,\n force: options.force ?? false,\n quiet: options.quiet ?? false,\n };\n\n if (options.db) {\n result.providers = parseProviderNames(options.db);\n }\n\n return result;\n}\n\n/**\n * Validate register command input.\n */\nexport function validateRegisterInput(options: RegisterCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Format registration summary for CLI output.\n */\nexport function formatRegistrationSummary(summary: {\n total: number;\n added: number;\n skipped: number;\n failed: number;\n noId: number;\n}): string {\n const lines: string[] = ['Registration complete:'];\n\n // Added\n lines.push(` ✓ ${summary.added} added`);\n\n // Duplicates (skipped)\n if (summary.skipped > 0) {\n lines.push(` ⚠ ${summary.skipped} duplicates (already in library)`);\n }\n\n // Failed\n if (summary.failed > 0) {\n lines.push(` ✗ ${summary.failed} failed`);\n }\n\n // No ID (skipped)\n if (summary.noId > 0) {\n lines.push(` - ${summary.noId} skipped (no identifier)`);\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Get registration identifier for an article.\n * PMID is preferred over DOI for better metadata quality.\n */\nfunction getRegistrationId(article: Article): string | null {\n if (article.pmid) {\n return `pmid:${article.pmid}`;\n }\n if (article.doi) {\n return article.doi;\n }\n return null;\n}\n\n/**\n * Format dry run output showing what would be registered.\n */\nexport function formatDryRunOutput(articles: Article[]): string {\n const withId: Array<{ article: Article; id: string }> = [];\n const withoutId: Article[] = [];\n\n for (const article of articles) {\n const id = getRegistrationId(article);\n if (id) {\n withId.push({ article, id });\n } else {\n withoutId.push(article);\n }\n }\n\n const lines: string[] = [];\n\n // Summary\n lines.push(\n `Would register ${withId.length} reference${withId.length !== 1 ? 's' : ''}:`\n );\n\n // List articles with IDs\n for (const { id, article } of withId) {\n const title = article.title.length > 60\n ? article.title.substring(0, 57) + '...'\n : article.title;\n lines.push(` - ${id}: ${title}`);\n }\n\n // Details about articles without DOI/PMID\n if (withoutId.length > 0) {\n lines.push('');\n lines.push(\n `${withoutId.length} article${withoutId.length !== 1 ? 's' : ''} will be skipped (no DOI or PMID):`\n );\n\n const maxDisplay = 10;\n const displayed = withoutId.slice(0, maxDisplay);\n\n for (const article of displayed) {\n const truncatedTitle = article.title.length > 50\n ? article.title.substring(0, 50) + '...'\n : article.title;\n\n const altIds = getAlternativeIds(article);\n const hasAltIds = altIds.length > 0 ? `, has: ${altIds.join(', ')}` : '';\n\n lines.push(` - \"${truncatedTitle}\" (source: ${article.source}${hasAltIds})`);\n }\n\n if (withoutId.length > maxDisplay) {\n lines.push(` ... and ${withoutId.length - maxDisplay} more`);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Get alternative (non-DOI/PMID) identifiers for an article.\n */\nfunction getAlternativeIds(article: Article): string[] {\n const ids: string[] = [];\n if (article.arxivId) ids.push(`arxiv:${article.arxivId}`);\n if (article.ericId) ids.push(`eric:${article.ericId}`);\n if (article.scopusId) ids.push(`scopus:${article.scopusId}`);\n return ids;\n}\n\n/**\n * Summary of review decisions for a session.\n */\nexport interface ReviewSummary {\n /** Total articles in review file */\n total: number;\n /** Articles with finalDecision='include' */\n included: number;\n /** Articles with finalDecision='exclude' */\n excluded: number;\n /** Articles without finalDecision (pending, incomplete, all-uncertain, agreed, divided) */\n pending: number;\n}\n\n/**\n * Check if a session has a reviews.yaml file.\n */\nexport async function hasReviewFile(sessionId: string, sessionsDir: string): Promise<boolean> {\n const reviewsPath = join(sessionsDir, sessionId, '.internal', 'reviews.yaml');\n try {\n await access(reviewsPath, constants.R_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Load and parse the review file for a session.\n */\nasync function loadReviewFile(sessionId: string, sessionsDir: string): Promise<ReviewFile> {\n const reviewsPath = join(sessionsDir, sessionId, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Get review summary (counts) for a session.\n * Throws if reviews.yaml does not exist.\n */\nexport async function getReviewSummary(sessionId: string, sessionsDir: string): Promise<ReviewSummary> {\n const reviewFile = await loadReviewFile(sessionId, sessionsDir);\n const articles = reviewFile.articles ?? [];\n\n const summary: ReviewSummary = {\n total: articles.length,\n included: 0,\n excluded: 0,\n pending: 0,\n };\n\n for (const article of articles) {\n const status = classifyStatus(article);\n\n if (status === 'finalized') {\n if (article.finalDecision === 'include') {\n summary.included++;\n } else {\n summary.excluded++;\n }\n } else {\n // pending, incomplete, all-uncertain, agreed, divided all count as pending for registration\n summary.pending++;\n }\n }\n\n return summary;\n}\n\n/**\n * Parse author name string into Author object.\n * Simple heuristic: last word is family name, rest is given name.\n */\nfunction parseAuthorName(name: string): Author {\n const parts = name.trim().split(/\\s+/);\n if (parts.length === 1) {\n return { family: parts[0] ?? '' };\n }\n // Last part is family name (most common pattern in scientific citations)\n const family = parts.pop() ?? '';\n const given = parts.join(' ');\n return { family, given };\n}\n\n/**\n * Get articles with finalDecision='include' from review file.\n * Converts from ArticleEntry format to Article format.\n *\n * @throws Error if mergedFrom is missing or empty (indicates legacy review file)\n */\nexport async function getIncludedArticles(sessionId: string, sessionsDir: string): Promise<Article[]> {\n const reviewFile = await loadReviewFile(sessionId, sessionsDir);\n const articles = reviewFile.articles ?? [];\n\n return articles\n .filter((entry) => entry.finalDecision === 'include')\n .map((entry): Article => {\n // Validate mergedFrom exists\n if (!entry.mergedFrom) {\n throw new Error(\n `Article \"${entry.title}\" has mergedFrom missing. ` +\n `This may be a legacy review file created before source tracking was fixed. ` +\n `Please re-run 'review init' to regenerate the review file with source tracking.`\n );\n }\n if (entry.mergedFrom.length === 0) {\n throw new Error(\n `Article \"${entry.title}\" has empty mergedFrom array. ` +\n `This is an invalid state - please re-run 'review init' to regenerate.`\n );\n }\n\n const authors: Author[] = entry.authors\n ? entry.authors.split(/,\\s*/).map(parseAuthorName)\n : [];\n\n // Get source from the first entry in mergedFrom\n const source = entry.mergedFrom[0]!.source as ProviderName;\n\n const article: Article = {\n title: entry.title,\n authors,\n source,\n retrievedAt: new Date().toISOString(),\n };\n // Only set optional fields if they have values\n if (entry.doi) article.doi = entry.doi;\n if (entry.pmid) article.pmid = entry.pmid;\n if (entry.scopusId) article.scopusId = entry.scopusId;\n if (entry.arxivId) article.arxivId = entry.arxivId;\n if (entry.ericId) article.ericId = entry.ericId;\n if (entry.abstract) article.abstract = entry.abstract;\n if (entry.year) article.publicationDate = entry.year;\n return article;\n });\n}\n\n/**\n * Format message when reviews exist but no flag specified.\n */\nexport function formatReviewRequiredMessage(summary: ReviewSummary, sessionId: string): string {\n return `This session has a review file.\n Status: ${summary.included} include / ${summary.excluded} exclude / ${summary.pending} pending\n\nPlease specify which articles to register:\n --reviewed Register ${summary.included} included articles\n --all Register all ${summary.total} articles (ignore reviews)\n\nExample:\n search-hub register ${sessionId} --reviewed`;\n}\n\n/**\n * Format error when --reviewed used but no articles are included.\n */\nexport function formatNoIncludedArticlesError(summary: ReviewSummary, sessionId: string): string {\n return `Error: No articles marked as 'include' in reviews.\n Status: ${summary.included} include / ${summary.excluded} exclude / ${summary.pending} pending\n\nRun 'search-hub review status ${sessionId}' for details.`;\n}\n\n/**\n * Format warning when pending articles exist with --reviewed.\n */\nexport function formatPendingWarning(summary: ReviewSummary): string {\n const articleWord = summary.pending === 1 ? 'article' : 'articles';\n return `Warning: ${summary.pending} ${articleWord} still pending review (will be skipped).\nRegistering ${summary.included} included articles...\n\nProceed? [Y/n]`;\n}\n\n\n/**\n * Format note when --all is used with reviews.yaml present.\n */\nexport function formatIgnoringReviewsNote(total: number): string {\n return `Note: Ignoring review decisions. Registering all ${total} articles.`;\n}\n\n/**\n * Prompt user for Y/n confirmation.\n * Returns true if user confirms (Y/y/Enter), false otherwise.\n */\nexport async function confirmPrompt(\n input: NodeJS.ReadableStream = process.stdin,\n output: NodeJS.WritableStream = process.stdout\n): Promise<boolean> {\n const rl = createInterface({\n input,\n output,\n terminal: false,\n });\n\n return new Promise((resolve) => {\n rl.question('', (answer) => {\n rl.close();\n const trimmed = answer.trim().toLowerCase();\n // Empty (Enter) or 'y' or 'yes' means confirm\n resolve(trimmed === '' || trimmed === 'y' || trimmed === 'yes');\n });\n });\n}\n"],"names":["parseYaml"],"mappings":";;;;;;;AA+CO,SAAS,qBACd,WACA,SACwB;AACxB,QAAM,SAAiC;AAAA,IACrC;AAAA,IACA,QAAQ,QAAQ,UAAU;AAAA,IAC1B,eAAe,QAAQ,iBAAiB;AAAA,IACxC,UAAU,QAAQ,YAAY;AAAA,IAC9B,KAAK,QAAQ,OAAO;AAAA,IACpB,OAAO,QAAQ,SAAS;AAAA,IACxB,OAAO,QAAQ,SAAS;AAAA,EAAA;AAG1B,MAAI,QAAQ,IAAI;AACd,WAAO,YAAY,mBAAmB,QAAQ,EAAE;AAAA,EAClD;AAEA,SAAO;AACT;AAKO,SAAS,sBAAsB,SAAmD;AACvF,MAAI,CAAC,QAAQ,aAAa,QAAQ,UAAU,KAAA,MAAW,IAAI;AACzD,WAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO;AAAA,IAAA;AAAA,EAEX;AAEA,SAAO,EAAE,OAAO,KAAA;AAClB;AAKO,SAAS,0BAA0B,SAM/B;AACT,QAAM,QAAkB,CAAC,wBAAwB;AAGjD,QAAM,KAAK,OAAO,QAAQ,KAAK,QAAQ;AAGvC,MAAI,QAAQ,UAAU,GAAG;AACvB,UAAM,KAAK,OAAO,QAAQ,OAAO,kCAAkC;AAAA,EACrE;AAGA,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,KAAK,OAAO,QAAQ,MAAM,SAAS;AAAA,EAC3C;AAGA,MAAI,QAAQ,OAAO,GAAG;AACpB,UAAM,KAAK,OAAO,QAAQ,IAAI,0BAA0B;AAAA,EAC1D;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAMA,SAAS,kBAAkB,SAAiC;AAC1D,MAAI,QAAQ,MAAM;AAChB,WAAO,QAAQ,QAAQ,IAAI;AAAA,EAC7B;AACA,MAAI,QAAQ,KAAK;AACf,WAAO,QAAQ;AAAA,EACjB;AACA,SAAO;AACT;AAKO,SAAS,mBAAmB,UAA6B;AAC9D,QAAM,SAAkD,CAAA;AACxD,QAAM,YAAuB,CAAA;AAE7B,aAAW,WAAW,UAAU;AAC9B,UAAM,KAAK,kBAAkB,OAAO;AACpC,QAAI,IAAI;AACN,aAAO,KAAK,EAAE,SAAS,GAAA,CAAI;AAAA,IAC7B,OAAO;AACL,gBAAU,KAAK,OAAO;AAAA,IACxB;AAAA,EACF;AAEA,QAAM,QAAkB,CAAA;AAGxB,QAAM;AAAA,IACJ,kBAAkB,OAAO,MAAM,aAAa,OAAO,WAAW,IAAI,MAAM,EAAE;AAAA,EAAA;AAI5E,aAAW,EAAE,IAAI,QAAA,KAAa,QAAQ;AACpC,UAAM,QAAQ,QAAQ,MAAM,SAAS,KACjC,QAAQ,MAAM,UAAU,GAAG,EAAE,IAAI,QACjC,QAAQ;AACZ,UAAM,KAAK,OAAO,EAAE,KAAK,KAAK,EAAE;AAAA,EAClC;AAGA,MAAI,UAAU,SAAS,GAAG;AACxB,UAAM,KAAK,EAAE;AACb,UAAM;AAAA,MACJ,GAAG,UAAU,MAAM,WAAW,UAAU,WAAW,IAAI,MAAM,EAAE;AAAA,IAAA;AAGjE,UAAM,aAAa;AACnB,UAAM,YAAY,UAAU,MAAM,GAAG,UAAU;AAE/C,eAAW,WAAW,WAAW;AAC/B,YAAM,iBAAiB,QAAQ,MAAM,SAAS,KAC1C,QAAQ,MAAM,UAAU,GAAG,EAAE,IAAI,QACjC,QAAQ;AAEZ,YAAM,SAAS,kBAAkB,OAAO;AACxC,YAAM,YAAY,OAAO,SAAS,IAAI,UAAU,OAAO,KAAK,IAAI,CAAC,KAAK;AAEtE,YAAM,KAAK,QAAQ,cAAc,cAAc,QAAQ,MAAM,GAAG,SAAS,GAAG;AAAA,IAC9E;AAEA,QAAI,UAAU,SAAS,YAAY;AACjC,YAAM,KAAK,aAAa,UAAU,SAAS,UAAU,OAAO;AAAA,IAC9D;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAKA,SAAS,kBAAkB,SAA4B;AACrD,QAAM,MAAgB,CAAA;AACtB,MAAI,QAAQ,QAAS,KAAI,KAAK,SAAS,QAAQ,OAAO,EAAE;AACxD,MAAI,QAAQ,OAAQ,KAAI,KAAK,QAAQ,QAAQ,MAAM,EAAE;AACrD,MAAI,QAAQ,SAAU,KAAI,KAAK,UAAU,QAAQ,QAAQ,EAAE;AAC3D,SAAO;AACT;AAmBA,eAAsB,cAAc,WAAmB,aAAuC;AAC5F,QAAM,cAAc,KAAK,aAAa,WAAW,aAAa,cAAc;AAC5E,MAAI;AACF,UAAM,OAAO,aAAa,UAAU,IAAI;AACxC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAe,eAAe,WAAmB,aAA0C;AACzF,QAAM,cAAc,KAAK,aAAa,WAAW,aAAa,cAAc;AAC5E,QAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,SAAOA,MAAU,OAAO;AAC1B;AAMA,eAAsB,iBAAiB,WAAmB,aAA6C;AACrG,QAAM,aAAa,MAAM,eAAe,WAAW,WAAW;AAC9D,QAAM,WAAW,WAAW,YAAY,CAAA;AAExC,QAAM,UAAyB;AAAA,IAC7B,OAAO,SAAS;AAAA,IAChB,UAAU;AAAA,IACV,UAAU;AAAA,IACV,SAAS;AAAA,EAAA;AAGX,aAAW,WAAW,UAAU;AAC9B,UAAM,SAAS,eAAe,OAAO;AAErC,QAAI,WAAW,aAAa;AAC1B,UAAI,QAAQ,kBAAkB,WAAW;AACvC,gBAAQ;AAAA,MACV,OAAO;AACL,gBAAQ;AAAA,MACV;AAAA,IACF,OAAO;AAEL,cAAQ;AAAA,IACV;AAAA,EACF;AAEA,SAAO;AACT;AAMA,SAAS,gBAAgB,MAAsB;AAC7C,QAAM,QAAQ,KAAK,KAAA,EAAO,MAAM,KAAK;AACrC,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,EAAE,QAAQ,MAAM,CAAC,KAAK,GAAA;AAAA,EAC/B;AAEA,QAAM,SAAS,MAAM,IAAA,KAAS;AAC9B,QAAM,QAAQ,MAAM,KAAK,GAAG;AAC5B,SAAO,EAAE,QAAQ,MAAA;AACnB;AAQA,eAAsB,oBAAoB,WAAmB,aAAyC;AACpG,QAAM,aAAa,MAAM,eAAe,WAAW,WAAW;AAC9D,QAAM,WAAW,WAAW,YAAY,CAAA;AAExC,SAAO,SACJ,OAAO,CAAC,UAAU,MAAM,kBAAkB,SAAS,EACnD,IAAI,CAAC,UAAmB;AAEvB,QAAI,CAAC,MAAM,YAAY;AACrB,YAAM,IAAI;AAAA,QACR,YAAY,MAAM,KAAK;AAAA,MAAA;AAAA,IAI3B;AACA,QAAI,MAAM,WAAW,WAAW,GAAG;AACjC,YAAM,IAAI;AAAA,QACR,YAAY,MAAM,KAAK;AAAA,MAAA;AAAA,IAG3B;AAEA,UAAM,UAAoB,MAAM,UAC5B,MAAM,QAAQ,MAAM,MAAM,EAAE,IAAI,eAAe,IAC/C,CAAA;AAGJ,UAAM,SAAS,MAAM,WAAW,CAAC,EAAG;AAEpC,UAAM,UAAmB;AAAA,MACvB,OAAO,MAAM;AAAA,MACb;AAAA,MACA;AAAA,MACA,cAAa,oBAAI,KAAA,GAAO,YAAA;AAAA,IAAY;AAGtC,QAAI,MAAM,IAAK,SAAQ,MAAM,MAAM;AACnC,QAAI,MAAM,KAAM,SAAQ,OAAO,MAAM;AACrC,QAAI,MAAM,SAAU,SAAQ,WAAW,MAAM;AAC7C,QAAI,MAAM,QAAS,SAAQ,UAAU,MAAM;AAC3C,QAAI,MAAM,OAAQ,SAAQ,SAAS,MAAM;AACzC,QAAI,MAAM,SAAU,SAAQ,WAAW,MAAM;AAC7C,QAAI,MAAM,KAAM,SAAQ,kBAAkB,MAAM;AAChD,WAAO;AAAA,EACT,CAAC;AACL;AAKO,SAAS,4BAA4B,SAAwB,WAA2B;AAC7F,SAAO;AAAA,YACG,QAAQ,QAAQ,cAAc,QAAQ,QAAQ,cAAc,QAAQ,OAAO;AAAA;AAAA;AAAA,0BAG7D,QAAQ,QAAQ;AAAA,8BACZ,QAAQ,KAAK;AAAA;AAAA;AAAA,wBAGnB,SAAS;AACjC;AAKO,SAAS,8BAA8B,SAAwB,WAA2B;AAC/F,SAAO;AAAA,YACG,QAAQ,QAAQ,cAAc,QAAQ,QAAQ,cAAc,QAAQ,OAAO;AAAA;AAAA,gCAEvD,SAAS;AACzC;AAKO,SAAS,qBAAqB,SAAgC;AACnE,QAAM,cAAc,QAAQ,YAAY,IAAI,YAAY;AACxD,SAAO,YAAY,QAAQ,OAAO,IAAI,WAAW;AAAA,cACrC,QAAQ,QAAQ;AAAA;AAAA;AAG9B;AAMO,SAAS,0BAA0B,OAAuB;AAC/D,SAAO,oDAAoD,KAAK;AAClE;AAMA,eAAsB,cACpB,QAA+B,QAAQ,OACvC,SAAgC,QAAQ,QACtB;AAClB,QAAM,KAAK,gBAAgB;AAAA,IACzB;AAAA,IACA;AAAA,IACA,UAAU;AAAA,EAAA,CACX;AAED,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,OAAG,SAAS,IAAI,CAAC,WAAW;AAC1B,SAAG,MAAA;AACH,YAAM,UAAU,OAAO,KAAA,EAAO,YAAA;AAE9B,cAAQ,YAAY,MAAM,YAAY,OAAO,YAAY,KAAK;AAAA,IAChE,CAAC;AAAA,EACH,CAAC;AACH;"}
@@ -6,10 +6,10 @@ function createEmptySkippedByStatus() {
6
6
  return {
7
7
  pending: 0,
8
8
  incomplete: 0,
9
- uncertain: 0,
9
+ "all-uncertain": 0,
10
10
  "agreed-include": 0,
11
11
  "agreed-exclude": 0,
12
- conflicting: 0,
12
+ divided: 0,
13
13
  finalized: 0
14
14
  };
15
15
  }
@@ -74,11 +74,11 @@ function formatFinalizeOutput(result, options) {
74
74
  if (result.skippedByStatus.incomplete > 0) {
75
75
  skippedParts.push(`${result.skippedByStatus.incomplete} incomplete`);
76
76
  }
77
- if (result.skippedByStatus.uncertain > 0) {
78
- skippedParts.push(`${result.skippedByStatus.uncertain} uncertain`);
77
+ if (result.skippedByStatus["all-uncertain"] > 0) {
78
+ skippedParts.push(`${result.skippedByStatus["all-uncertain"]} all-uncertain`);
79
79
  }
80
- if (result.skippedByStatus.conflicting > 0) {
81
- skippedParts.push(`${result.skippedByStatus.conflicting} conflicting`);
80
+ if (result.skippedByStatus.divided > 0) {
81
+ skippedParts.push(`${result.skippedByStatus.divided} divided`);
82
82
  }
83
83
  if (options?.decision && result.skippedByStatus["agreed-include"] > 0) {
84
84
  skippedParts.push(`${result.skippedByStatus["agreed-include"]} agreed-include (filtered)`);
@@ -1 +1 @@
1
- {"version":3,"file":"finalize.js","sources":["../../../../src/cli/commands/review/finalize.ts"],"sourcesContent":["/**\n * review finalize command - Auto-set finalDecision for articles with consensus\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ReviewStatus } from './types.js';\n\nexport interface ReviewFinalizeOptions {\n sessionId: string;\n dryRun?: boolean;\n minReviewers?: number;\n decision?: 'include' | 'exclude';\n}\n\nexport interface ReviewFinalizeResult {\n includedCount: number;\n excludedCount: number;\n skippedByStatus: Record<ReviewStatus, number>;\n}\n\nfunction createEmptySkippedByStatus(): Record<ReviewStatus, number> {\n return {\n pending: 0,\n incomplete: 0,\n uncertain: 0,\n 'agreed-include': 0,\n 'agreed-exclude': 0,\n 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;"}
1
+ {"version":3,"file":"finalize.js","sources":["../../../../src/cli/commands/review/finalize.ts"],"sourcesContent":["/**\n * review finalize command - Auto-set finalDecision for articles with consensus\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { classifyStatus, type ReviewFile, type ReviewStatus } from './types.js';\n\nexport interface ReviewFinalizeOptions {\n sessionId: string;\n dryRun?: boolean;\n minReviewers?: number;\n decision?: 'include' | 'exclude';\n}\n\nexport interface ReviewFinalizeResult {\n includedCount: number;\n excludedCount: number;\n skippedByStatus: Record<ReviewStatus, number>;\n}\n\nfunction createEmptySkippedByStatus(): Record<ReviewStatus, number> {\n return {\n pending: 0,\n incomplete: 0,\n 'all-uncertain': 0,\n 'agreed-include': 0,\n 'agreed-exclude': 0,\n divided: 0,\n finalized: 0,\n };\n}\n\n/**\n * Execute review finalize command\n */\nexport async function executeReviewFinalize(\n options: ReviewFinalizeOptions,\n sessionsDir: string\n): Promise<ReviewFinalizeResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(content) as ReviewFile;\n\n const reviewers = reviewFile.reviewers ?? [];\n const minReviewers = options.minReviewers ?? 1;\n\n const result: ReviewFinalizeResult = {\n includedCount: 0,\n excludedCount: 0,\n skippedByStatus: createEmptySkippedByStatus(),\n };\n\n for (const article of reviewFile.articles) {\n const status = classifyStatus(article, reviewers);\n\n if (status === 'agreed-include' || status === 'agreed-exclude') {\n // Check decision filter\n const consensusDecision = status === 'agreed-include' ? 'include' : 'exclude';\n if (options.decision && options.decision !== consensusDecision) {\n result.skippedByStatus[status]++;\n continue;\n }\n\n // Check minimum reviewer count\n const reviews = article.reviews ?? [];\n const uniqueReviewers = new Set(reviews.map((r) => r.reviewer));\n if (uniqueReviewers.size < minReviewers) {\n result.skippedByStatus[status]++;\n continue;\n }\n\n if (!options.dryRun) {\n article.finalDecision = consensusDecision;\n }\n\n if (status === 'agreed-include') {\n result.includedCount++;\n } else {\n result.excludedCount++;\n }\n } else {\n result.skippedByStatus[status]++;\n }\n }\n\n // Write back if not dry-run\n if (!options.dryRun) {\n const yamlContent = stringifyYaml(reviewFile, { lineWidth: 0 });\n const schemaComment = `# yaml-language-server: $schema=./review.schema.json\\n`;\n await writeFile(reviewsPath, schemaComment + yamlContent, 'utf-8');\n }\n\n return result;\n}\n\n/**\n * Format finalize result as human-readable string\n */\nexport function formatFinalizeOutput(\n result: ReviewFinalizeResult,\n options?: { dryRun?: boolean; decision?: 'include' | 'exclude' }\n): string {\n const lines: string[] = [];\n\n if (options?.dryRun) {\n lines.push('Dry run - no changes made');\n lines.push('');\n }\n\n const total = result.includedCount + result.excludedCount;\n lines.push(`Finalized ${total} articles (${result.includedCount} include, ${result.excludedCount} exclude)`);\n\n // Build skipped summary (only non-zero, non-agreed statuses)\n const skippedParts: string[] = [];\n if (result.skippedByStatus.pending > 0) {\n skippedParts.push(`${result.skippedByStatus.pending} pending`);\n }\n if (result.skippedByStatus.incomplete > 0) {\n skippedParts.push(`${result.skippedByStatus.incomplete} incomplete`);\n }\n if (result.skippedByStatus['all-uncertain'] > 0) {\n skippedParts.push(`${result.skippedByStatus['all-uncertain']} all-uncertain`);\n }\n if (result.skippedByStatus.divided > 0) {\n skippedParts.push(`${result.skippedByStatus.divided} divided`);\n }\n\n // Show filtered-out agreed counts when --decision is active\n if (options?.decision && result.skippedByStatus['agreed-include'] > 0) {\n skippedParts.push(`${result.skippedByStatus['agreed-include']} agreed-include (filtered)`);\n }\n if (options?.decision && result.skippedByStatus['agreed-exclude'] > 0) {\n skippedParts.push(`${result.skippedByStatus['agreed-exclude']} agreed-exclude (filtered)`);\n }\n\n if (skippedParts.length > 0) {\n lines.push(`Skipped: ${skippedParts.join(', ')}`);\n }\n\n return lines.join('\\n');\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AA+ED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAmF3B"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAYH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAyDD;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,CAAC,CA6E3B"}
@@ -1,9 +1,10 @@
1
- import { join, dirname } from "node:path";
2
- import { access, mkdir, writeFile, copyFile } from "node:fs/promises";
1
+ import { join } from "node:path";
2
+ import { access, mkdir, writeFile } from "node:fs/promises";
3
3
  import { stringify } from "yaml";
4
4
  import { loadSession } from "../../../session/manager.js";
5
5
  import { loadResults } from "../../../session/results-io.js";
6
6
  import { deduplicateForReview } from "./dedup.js";
7
+ import { generateReviewJSONSchema } from "./schema.js";
7
8
  function formatAuthors(authors) {
8
9
  return authors.map((a) => {
9
10
  const parts = [];
@@ -38,20 +39,6 @@ function articleToEntry(article) {
38
39
  }
39
40
  return entry;
40
41
  }
41
- async function findSchemaSource() {
42
- const possiblePaths = [
43
- join(dirname(import.meta.url.replace("file://", "")), "../../../../schemas/review.schema.json"),
44
- join(process.cwd(), "schemas/review.schema.json")
45
- ];
46
- for (const path of possiblePaths) {
47
- try {
48
- await access(path);
49
- return path;
50
- } catch {
51
- }
52
- }
53
- throw new Error("Could not find review.schema.json");
54
- }
55
42
  async function executeReviewInit(options, sessionsDir) {
56
43
  const sessionDir = join(sessionsDir, options.sessionId);
57
44
  const session = await loadSession(options.sessionId, sessionsDir);
@@ -98,11 +85,8 @@ async function executeReviewInit(options, sessionsDir) {
98
85
  );
99
86
  await writeFile(reviewsPath, finalContent, "utf-8");
100
87
  const schemaDestPath = join(internalDir, "review.schema.json");
101
- try {
102
- const schemaSourcePath = await findSchemaSource();
103
- await copyFile(schemaSourcePath, schemaDestPath);
104
- } catch {
105
- }
88
+ const jsonSchema = generateReviewJSONSchema();
89
+ await writeFile(schemaDestPath, JSON.stringify(jsonSchema, null, 2) + "\n", "utf-8");
106
90
  return {
107
91
  reviewsPath,
108
92
  articleCount: articleEntries.length,
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sources":["../../../../src/cli/commands/review/init.ts"],"sourcesContent":["/**\n * review init command - Generate reviews.yaml from session results\n */\n\nimport { join, dirname } from 'node:path';\nimport { writeFile, mkdir, access, copyFile } from 'node:fs/promises';\nimport { stringify as stringifyYaml } from 'yaml';\nimport type { Article, Author, ProviderName } from '../../../providers/base/types.js';\nimport { loadSession } from '../../../session/manager.js';\nimport { loadResults } from '../../../session/results-io.js';\nimport { deduplicateForReview } from './dedup.js';\nimport type { ArticleEntry, ReviewFile, MergedSource } from './types.js';\n\nexport interface ReviewInitOptions {\n sessionId: string;\n force?: boolean;\n}\n\nexport interface ReviewInitResult {\n reviewsPath: string;\n articleCount: number;\n duplicatesRemoved: number;\n}\n\n/**\n * Format authors array to string\n */\nfunction formatAuthors(authors: Author[]): string {\n return authors\n .map((a) => {\n const parts: string[] = [];\n if (a.family) parts.push(a.family);\n if (a.given) parts.push(a.given.charAt(0));\n return parts.join(' ');\n })\n .join(', ');\n}\n\n/**\n * Extract year from publication date\n */\nfunction extractYear(publicationDate?: string): string | undefined {\n if (!publicationDate) return undefined;\n const year = publicationDate.substring(0, 4);\n return /^\\d{4}$/.test(year) ? year : undefined;\n}\n\n/**\n * Convert Article to ArticleEntry for review file\n */\nfunction articleToEntry(article: Article & { mergedFrom?: MergedSource[] }): ArticleEntry {\n const entry: ArticleEntry = {\n title: article.title,\n reviews: [],\n };\n\n // Add identifiers\n if (article.doi) entry.doi = article.doi;\n if (article.pmid) entry.pmid = article.pmid;\n if (article.scopusId) entry.scopusId = article.scopusId;\n if (article.arxivId) entry.arxivId = article.arxivId;\n if (article.ericId) entry.ericId = article.ericId;\n\n // Add bibliographic info\n if (article.authors && article.authors.length > 0) {\n entry.authors = formatAuthors(article.authors);\n }\n const year = extractYear(article.publicationDate);\n if (year) entry.year = year;\n if (article.abstract) entry.abstract = article.abstract;\n\n // Add deduplication tracking\n if (article.mergedFrom && article.mergedFrom.length > 0) {\n entry.mergedFrom = article.mergedFrom;\n }\n\n return entry;\n}\n\n/**\n * Find the schema file location (in the package)\n */\nasync function findSchemaSource(): Promise<string> {\n // Try relative to this file (src/cli/commands/review -> schemas)\n const possiblePaths = [\n join(dirname(import.meta.url.replace('file://', '')), '../../../../schemas/review.schema.json'),\n join(process.cwd(), 'schemas/review.schema.json'),\n ];\n\n for (const path of possiblePaths) {\n try {\n await access(path);\n return path;\n } catch {\n // Try next path\n }\n }\n\n throw new Error('Could not find review.schema.json');\n}\n\n/**\n * Execute review init command\n */\nexport async function executeReviewInit(\n options: ReviewInitOptions,\n sessionsDir: string\n): Promise<ReviewInitResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n\n // Load session file\n const session = await loadSession(options.sessionId, sessionsDir);\n\n // Check if .internal/reviews.yaml already exists\n const internalDir = join(sessionDir, '.internal');\n const reviewsPath = join(internalDir, 'reviews.yaml');\n try {\n await access(reviewsPath);\n if (!options.force) {\n throw new Error(`reviews.yaml already exists. Use --force to overwrite.`);\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw err;\n }\n }\n\n // Create .internal/ directory\n await mkdir(internalDir, { recursive: true });\n\n // Load all results from session\n const allArticles: Article[] = [];\n const providers = Object.keys(session.databases) as ProviderName[];\n\n for (const provider of providers) {\n const dbStatus = session.databases[provider];\n if (!dbStatus) continue;\n const articles = await loadResults(sessionDir, provider);\n allArticles.push(...articles);\n }\n\n // Deduplicate with mergedFrom tracking\n const { articles: dedupedArticles, duplicatesRemoved } = deduplicateForReview(allArticles);\n\n // Convert to ArticleEntry format\n const articleEntries = dedupedArticles.map(articleToEntry);\n\n // Build review file\n const reviewFile: ReviewFile = {\n sessionId: options.sessionId,\n articles: articleEntries,\n };\n\n // Generate YAML with schema reference comment\n const yamlContent = stringifyYaml(reviewFile, {\n lineWidth: 0, // Disable line wrapping\n });\n\n // Add schema reference comment at top (local copy alongside reviews.yaml)\n const schemaComment = `# yaml-language-server: $schema=./review.schema.json\\n`;\n\n // Replace empty reviews arrays with commented example\n const reviewsExample = `reviews:\n # - reviewer: human:your-name\n # decision: include # include / exclude / uncertain\n # comment: reason`;\n const finalContent = schemaComment + yamlContent.replace(\n /reviews: \\[\\]/g,\n reviewsExample\n );\n\n // Write reviews.yaml\n await writeFile(reviewsPath, finalContent, 'utf-8');\n\n // Copy schema file to .internal/ alongside reviews.yaml\n const schemaDestPath = join(internalDir, 'review.schema.json');\n\n try {\n const schemaSourcePath = await findSchemaSource();\n await copyFile(schemaSourcePath, schemaDestPath);\n } catch {\n // If we can't find the schema file, skip copying\n // This might happen in test environments\n }\n\n return {\n reviewsPath,\n articleCount: articleEntries.length,\n duplicatesRemoved,\n };\n}\n"],"names":["stringifyYaml"],"mappings":"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"}
1
+ {"version":3,"file":"init.js","sources":["../../../../src/cli/commands/review/init.ts"],"sourcesContent":["/**\n * review init command - Generate reviews.yaml from session results\n */\n\nimport { join } from 'node:path';\nimport { writeFile, mkdir, access } from 'node:fs/promises';\nimport { stringify as stringifyYaml } from 'yaml';\nimport type { Article, Author, ProviderName } from '../../../providers/base/types.js';\nimport { loadSession } from '../../../session/manager.js';\nimport { loadResults } from '../../../session/results-io.js';\nimport { deduplicateForReview } from './dedup.js';\nimport { generateReviewJSONSchema } from './schema.js';\nimport type { ArticleEntry, ReviewFile, MergedSource } from './types.js';\n\nexport interface ReviewInitOptions {\n sessionId: string;\n force?: boolean;\n}\n\nexport interface ReviewInitResult {\n reviewsPath: string;\n articleCount: number;\n duplicatesRemoved: number;\n}\n\n/**\n * Format authors array to string\n */\nfunction formatAuthors(authors: Author[]): string {\n return authors\n .map((a) => {\n const parts: string[] = [];\n if (a.family) parts.push(a.family);\n if (a.given) parts.push(a.given.charAt(0));\n return parts.join(' ');\n })\n .join(', ');\n}\n\n/**\n * Extract year from publication date\n */\nfunction extractYear(publicationDate?: string): string | undefined {\n if (!publicationDate) return undefined;\n const year = publicationDate.substring(0, 4);\n return /^\\d{4}$/.test(year) ? year : undefined;\n}\n\n/**\n * Convert Article to ArticleEntry for review file\n */\nfunction articleToEntry(article: Article & { mergedFrom?: MergedSource[] }): ArticleEntry {\n const entry: ArticleEntry = {\n title: article.title,\n reviews: [],\n };\n\n // Add identifiers\n if (article.doi) entry.doi = article.doi;\n if (article.pmid) entry.pmid = article.pmid;\n if (article.scopusId) entry.scopusId = article.scopusId;\n if (article.arxivId) entry.arxivId = article.arxivId;\n if (article.ericId) entry.ericId = article.ericId;\n\n // Add bibliographic info\n if (article.authors && article.authors.length > 0) {\n entry.authors = formatAuthors(article.authors);\n }\n const year = extractYear(article.publicationDate);\n if (year) entry.year = year;\n if (article.abstract) entry.abstract = article.abstract;\n\n // Add deduplication tracking\n if (article.mergedFrom && article.mergedFrom.length > 0) {\n entry.mergedFrom = article.mergedFrom;\n }\n\n return entry;\n}\n\n/**\n * Execute review init command\n */\nexport async function executeReviewInit(\n options: ReviewInitOptions,\n sessionsDir: string\n): Promise<ReviewInitResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n\n // Load session file\n const session = await loadSession(options.sessionId, sessionsDir);\n\n // Check if .internal/reviews.yaml already exists\n const internalDir = join(sessionDir, '.internal');\n const reviewsPath = join(internalDir, 'reviews.yaml');\n try {\n await access(reviewsPath);\n if (!options.force) {\n throw new Error(`reviews.yaml already exists. Use --force to overwrite.`);\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw err;\n }\n }\n\n // Create .internal/ directory\n await mkdir(internalDir, { recursive: true });\n\n // Load all results from session\n const allArticles: Article[] = [];\n const providers = Object.keys(session.databases) as ProviderName[];\n\n for (const provider of providers) {\n const dbStatus = session.databases[provider];\n if (!dbStatus) continue;\n const articles = await loadResults(sessionDir, provider);\n allArticles.push(...articles);\n }\n\n // Deduplicate with mergedFrom tracking\n const { articles: dedupedArticles, duplicatesRemoved } = deduplicateForReview(allArticles);\n\n // Convert to ArticleEntry format\n const articleEntries = dedupedArticles.map(articleToEntry);\n\n // Build review file\n const reviewFile: ReviewFile = {\n sessionId: options.sessionId,\n articles: articleEntries,\n };\n\n // Generate YAML with schema reference comment\n const yamlContent = stringifyYaml(reviewFile, {\n lineWidth: 0, // Disable line wrapping\n });\n\n // Add schema reference comment at top (local copy alongside reviews.yaml)\n const schemaComment = `# yaml-language-server: $schema=./review.schema.json\\n`;\n\n // Replace empty reviews arrays with commented example\n const reviewsExample = `reviews:\n # - reviewer: human:your-name\n # decision: include # include / exclude / uncertain\n # comment: reason`;\n const finalContent = schemaComment + yamlContent.replace(\n /reviews: \\[\\]/g,\n reviewsExample\n );\n\n // Write reviews.yaml\n await writeFile(reviewsPath, finalContent, 'utf-8');\n\n // Generate and write schema file to .internal/ alongside reviews.yaml\n const schemaDestPath = join(internalDir, 'review.schema.json');\n const jsonSchema = generateReviewJSONSchema();\n await writeFile(schemaDestPath, JSON.stringify(jsonSchema, null, 2) + '\\n', 'utf-8');\n\n return {\n reviewsPath,\n articleCount: articleEntries.length,\n duplicatesRemoved,\n };\n}\n"],"names":["stringifyYaml"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { ReviewStatus } from './types.js';
2
- export type ListFilter = 'pending' | 'incomplete' | 'uncertain' | 'agreed-include' | 'agreed-exclude' | 'conflicting' | 'finalized' | 'all';
2
+ export type ListFilter = 'pending' | 'incomplete' | 'all-uncertain' | 'agreed-include' | 'agreed-exclude' | 'divided' | 'finalized' | 'all';
3
3
  export interface ReviewListOptions {
4
4
  sessionId: string;
5
5
  filter?: ListFilter;