@ncukondo/search-hub 0.5.0 → 0.7.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/diff.d.ts +57 -2
- package/dist/cli/commands/diff.d.ts.map +1 -1
- package/dist/cli/commands/diff.js +174 -3
- package/dist/cli/commands/diff.js.map +1 -1
- package/dist/cli/commands/query/init.d.ts.map +1 -1
- package/dist/cli/commands/query/init.js +2 -0
- package/dist/cli/commands/query/init.js.map +1 -1
- package/dist/cli/commands/register.js +2 -2
- package/dist/cli/commands/register.js.map +1 -1
- package/dist/cli/commands/review/export.js +1 -1
- package/dist/cli/commands/review/export.js.map +1 -1
- package/dist/cli/commands/review/extract.d.ts +5 -1
- package/dist/cli/commands/review/extract.d.ts.map +1 -1
- package/dist/cli/commands/review/extract.js +43 -10
- package/dist/cli/commands/review/extract.js.map +1 -1
- package/dist/cli/commands/review/init.d.ts.map +1 -1
- package/dist/cli/commands/review/init.js +5 -3
- package/dist/cli/commands/review/init.js.map +1 -1
- package/dist/cli/commands/review/list.d.ts +17 -0
- package/dist/cli/commands/review/list.d.ts.map +1 -1
- package/dist/cli/commands/review/list.js +19 -2
- package/dist/cli/commands/review/list.js.map +1 -1
- package/dist/cli/commands/review/mark.d.ts +24 -0
- package/dist/cli/commands/review/mark.d.ts.map +1 -0
- package/dist/cli/commands/review/mark.js +55 -0
- package/dist/cli/commands/review/mark.js.map +1 -0
- package/dist/cli/commands/review/merge.d.ts.map +1 -1
- package/dist/cli/commands/review/merge.js +79 -19
- package/dist/cli/commands/review/merge.js.map +1 -1
- package/dist/cli/commands/review/status.d.ts.map +1 -1
- package/dist/cli/commands/review/status.js +17 -3
- package/dist/cli/commands/review/status.js.map +1 -1
- package/dist/cli/commands/review/types.d.ts +26 -1
- package/dist/cli/commands/review/types.d.ts.map +1 -1
- package/dist/cli/commands/review/types.js.map +1 -1
- package/dist/cli/commands/session-utils.d.ts +11 -0
- package/dist/cli/commands/session-utils.d.ts.map +1 -1
- package/dist/cli/commands/session-utils.js +14 -1
- package/dist/cli/commands/session-utils.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +81 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/providers/eric/translator.d.ts.map +1 -1
- package/dist/providers/eric/translator.js +17 -11
- package/dist/providers/eric/translator.js.map +1 -1
- package/dist/query/types.d.ts +2 -0
- package/dist/query/types.d.ts.map +1 -1
- package/dist/query/validator.d.ts +5 -0
- package/dist/query/validator.d.ts.map +1 -1
- package/dist/query/validator.js +1 -0
- package/dist/query/validator.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Article } from '../../providers/base/types.js';
|
|
2
|
+
import { QueryAST, FieldType } from '../../query/types.js';
|
|
2
3
|
export interface DiffResult {
|
|
3
4
|
session1Count: number;
|
|
4
5
|
session2Count: number;
|
|
@@ -15,9 +16,63 @@ export interface DiffResult {
|
|
|
15
16
|
*/
|
|
16
17
|
export declare function computeDiff(session1: Article[], session2: Article[]): DiffResult;
|
|
17
18
|
export type ShowFilter = 'added' | 'removed' | 'common';
|
|
19
|
+
/**
|
|
20
|
+
* Options for formatting diff with query information.
|
|
21
|
+
*/
|
|
22
|
+
export interface FormatDiffOptions {
|
|
23
|
+
queryDiff?: QueryDiff | undefined;
|
|
24
|
+
noQueryDiff?: boolean | undefined;
|
|
25
|
+
showQueryDiffPlaceholder?: boolean | undefined;
|
|
26
|
+
}
|
|
18
27
|
/**
|
|
19
28
|
* Format diff result as human-readable text.
|
|
20
29
|
*/
|
|
21
|
-
export declare function formatDiff(diff: DiffResult, session1Id: string, session2Id: string, show?: ShowFilter): string;
|
|
22
|
-
export declare function formatDiffJson(diff: DiffResult, session1Id: string, session2Id: string, show?: ShowFilter): string;
|
|
30
|
+
export declare function formatDiff(diff: DiffResult, session1Id: string, session2Id: string, show?: ShowFilter, options?: FormatDiffOptions): string;
|
|
31
|
+
export declare function formatDiffJson(diff: DiffResult, session1Id: string, session2Id: string, show?: ShowFilter, options?: FormatDiffOptions): string;
|
|
32
|
+
/**
|
|
33
|
+
* Diff result for a single query block.
|
|
34
|
+
*/
|
|
35
|
+
export interface BlockDiff {
|
|
36
|
+
index: number;
|
|
37
|
+
field: FieldType;
|
|
38
|
+
added: string[];
|
|
39
|
+
removed: string[];
|
|
40
|
+
meshAdded?: string[];
|
|
41
|
+
meshRemoved?: string[];
|
|
42
|
+
emtreeAdded?: string[];
|
|
43
|
+
emtreeRemoved?: string[];
|
|
44
|
+
excludeAdded?: string[];
|
|
45
|
+
excludeRemoved?: string[];
|
|
46
|
+
hasChanges: boolean;
|
|
47
|
+
isNew?: boolean;
|
|
48
|
+
isRemoved?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Diff result for query filters.
|
|
52
|
+
*/
|
|
53
|
+
export interface FilterDiff {
|
|
54
|
+
yearFromChanged: boolean;
|
|
55
|
+
oldYearFrom?: number | undefined;
|
|
56
|
+
newYearFrom?: number | undefined;
|
|
57
|
+
yearToChanged: boolean;
|
|
58
|
+
oldYearTo?: number | undefined;
|
|
59
|
+
newYearTo?: number | undefined;
|
|
60
|
+
languagesAdded: string[];
|
|
61
|
+
languagesRemoved: string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Complete query diff result.
|
|
65
|
+
*/
|
|
66
|
+
export interface QueryDiff {
|
|
67
|
+
blocks: BlockDiff[];
|
|
68
|
+
filters: FilterDiff;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Compute the diff between two QueryAST objects.
|
|
72
|
+
*/
|
|
73
|
+
export declare function computeQueryDiff(query1: QueryAST, query2: QueryAST): QueryDiff;
|
|
74
|
+
/**
|
|
75
|
+
* Format query diff as human-readable text.
|
|
76
|
+
*/
|
|
77
|
+
export declare function formatQueryDiff(queryDiff: QueryDiff): string;
|
|
23
78
|
//# sourceMappingURL=diff.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAC7D,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAa,MAAM,sBAAsB,CAAC;AAG3E,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,OAAO,EAAE,OAAO,EAAE,CAAC;IACnB,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,UAAU,CAyDhF;AAED,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,CAAC;AAoBxD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAClC,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAClC,wBAAwB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChD;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE,UAAU,EACjB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,MAAM,CAyDR;AAqBD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE,UAAU,EACjB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,MAAM,CA6BR;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE,UAAU,CAAC;CACrB;AA2ED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAG,SAAS,CAqC9E;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAuG5D"}
|
|
@@ -57,12 +57,22 @@ function formatArticleLine(prefix, article) {
|
|
|
57
57
|
const yearPart = year ? `[${year}] ` : "";
|
|
58
58
|
return ` ${prefix} ${yearPart}${article.title}`;
|
|
59
59
|
}
|
|
60
|
-
function formatDiff(diff, session1Id, session2Id, show) {
|
|
60
|
+
function formatDiff(diff, session1Id, session2Id, show, options) {
|
|
61
61
|
const lines = [];
|
|
62
62
|
lines.push(`Diff: ${session1Id} → ${session2Id}`);
|
|
63
63
|
lines.push(` Session 1: ${diff.session1Count} articles (${session1Id})`);
|
|
64
64
|
lines.push(` Session 2: ${diff.session2Count} articles (${session2Id})`);
|
|
65
65
|
lines.push("");
|
|
66
|
+
const shouldShowQueryDiff = options?.queryDiff && !options?.noQueryDiff;
|
|
67
|
+
const shouldShowPlaceholder = options?.showQueryDiffPlaceholder && !options?.queryDiff && !options?.noQueryDiff;
|
|
68
|
+
if (shouldShowPlaceholder) {
|
|
69
|
+
lines.push("Query changes: (query data not available)");
|
|
70
|
+
lines.push("");
|
|
71
|
+
} else if (shouldShowQueryDiff) {
|
|
72
|
+
lines.push(formatQueryDiff(options.queryDiff));
|
|
73
|
+
lines.push("");
|
|
74
|
+
lines.push("Result changes:");
|
|
75
|
+
}
|
|
66
76
|
lines.push(` Common: ${diff.common.length} articles`);
|
|
67
77
|
lines.push(` Added: ${diff.added.length} articles (in ${session2Id} but not ${session1Id})`);
|
|
68
78
|
lines.push(` Removed: ${diff.removed.length} articles (in ${session1Id} but not ${session2Id})`);
|
|
@@ -92,7 +102,7 @@ function formatDiff(diff, session1Id, session2Id, show) {
|
|
|
92
102
|
}
|
|
93
103
|
return lines.join("\n");
|
|
94
104
|
}
|
|
95
|
-
function formatDiffJson(diff, session1Id, session2Id, show) {
|
|
105
|
+
function formatDiffJson(diff, session1Id, session2Id, show, options) {
|
|
96
106
|
const result = {
|
|
97
107
|
session1: session1Id,
|
|
98
108
|
session2: session2Id,
|
|
@@ -104,6 +114,9 @@ function formatDiffJson(diff, session1Id, session2Id, show) {
|
|
|
104
114
|
removedCount: diff.removed.length
|
|
105
115
|
}
|
|
106
116
|
};
|
|
117
|
+
if (options?.queryDiff && !options?.noQueryDiff) {
|
|
118
|
+
result.queryDiff = options.queryDiff;
|
|
119
|
+
}
|
|
107
120
|
if (!show || show === "added") {
|
|
108
121
|
result.added = diff.added;
|
|
109
122
|
}
|
|
@@ -115,9 +128,167 @@ function formatDiffJson(diff, session1Id, session2Id, show) {
|
|
|
115
128
|
}
|
|
116
129
|
return JSON.stringify(result, null, 2);
|
|
117
130
|
}
|
|
131
|
+
function setDiff(arr1, arr2) {
|
|
132
|
+
const set1 = new Set(arr1);
|
|
133
|
+
return arr2.filter((item) => !set1.has(item));
|
|
134
|
+
}
|
|
135
|
+
function compareBlocks(block1, block2, index, field) {
|
|
136
|
+
const emptyTerms = { keywords: [], mesh: [], emtree: [], exclude: [] };
|
|
137
|
+
const terms1 = block1 ?? emptyTerms;
|
|
138
|
+
const terms2 = block2 ?? emptyTerms;
|
|
139
|
+
const added = setDiff(terms1.keywords, terms2.keywords);
|
|
140
|
+
const removed = setDiff(terms2.keywords, terms1.keywords);
|
|
141
|
+
const meshAdded = setDiff(terms1.mesh ?? [], terms2.mesh ?? []);
|
|
142
|
+
const meshRemoved = setDiff(terms2.mesh ?? [], terms1.mesh ?? []);
|
|
143
|
+
const emtreeAdded = setDiff(terms1.emtree ?? [], terms2.emtree ?? []);
|
|
144
|
+
const emtreeRemoved = setDiff(terms2.emtree ?? [], terms1.emtree ?? []);
|
|
145
|
+
const excludeAdded = setDiff(terms1.exclude ?? [], terms2.exclude ?? []);
|
|
146
|
+
const excludeRemoved = setDiff(terms2.exclude ?? [], terms1.exclude ?? []);
|
|
147
|
+
const hasChanges = added.length > 0 || removed.length > 0 || meshAdded.length > 0 || meshRemoved.length > 0 || emtreeAdded.length > 0 || emtreeRemoved.length > 0 || excludeAdded.length > 0 || excludeRemoved.length > 0;
|
|
148
|
+
const result = {
|
|
149
|
+
index,
|
|
150
|
+
field,
|
|
151
|
+
added,
|
|
152
|
+
removed,
|
|
153
|
+
hasChanges
|
|
154
|
+
};
|
|
155
|
+
if (meshAdded.length > 0 || meshRemoved.length > 0) {
|
|
156
|
+
result.meshAdded = meshAdded;
|
|
157
|
+
result.meshRemoved = meshRemoved;
|
|
158
|
+
}
|
|
159
|
+
if (emtreeAdded.length > 0 || emtreeRemoved.length > 0) {
|
|
160
|
+
result.emtreeAdded = emtreeAdded;
|
|
161
|
+
result.emtreeRemoved = emtreeRemoved;
|
|
162
|
+
}
|
|
163
|
+
if (excludeAdded.length > 0 || excludeRemoved.length > 0) {
|
|
164
|
+
result.excludeAdded = excludeAdded;
|
|
165
|
+
result.excludeRemoved = excludeRemoved;
|
|
166
|
+
}
|
|
167
|
+
if (!block1) {
|
|
168
|
+
result.isNew = true;
|
|
169
|
+
result.hasChanges = true;
|
|
170
|
+
}
|
|
171
|
+
if (!block2) {
|
|
172
|
+
result.isRemoved = true;
|
|
173
|
+
result.hasChanges = true;
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
function computeQueryDiff(query1, query2) {
|
|
178
|
+
const blocks = [];
|
|
179
|
+
const maxBlocks = Math.max(query1.blocks.length, query2.blocks.length);
|
|
180
|
+
for (let i = 0; i < maxBlocks; i++) {
|
|
181
|
+
const block1 = query1.blocks[i];
|
|
182
|
+
const block2 = query2.blocks[i];
|
|
183
|
+
const field = block2?.field ?? block1?.field ?? "all";
|
|
184
|
+
const blockDiff = compareBlocks(
|
|
185
|
+
block1?.terms,
|
|
186
|
+
block2?.terms,
|
|
187
|
+
i,
|
|
188
|
+
field
|
|
189
|
+
);
|
|
190
|
+
blocks.push(blockDiff);
|
|
191
|
+
}
|
|
192
|
+
const filters = {
|
|
193
|
+
yearFromChanged: query1.filters.yearFrom !== query2.filters.yearFrom,
|
|
194
|
+
yearToChanged: query1.filters.yearTo !== query2.filters.yearTo,
|
|
195
|
+
languagesAdded: setDiff(query1.filters.languages ?? [], query2.filters.languages ?? []),
|
|
196
|
+
languagesRemoved: setDiff(query2.filters.languages ?? [], query1.filters.languages ?? [])
|
|
197
|
+
};
|
|
198
|
+
if (filters.yearFromChanged) {
|
|
199
|
+
filters.oldYearFrom = query1.filters.yearFrom;
|
|
200
|
+
filters.newYearFrom = query2.filters.yearFrom;
|
|
201
|
+
}
|
|
202
|
+
if (filters.yearToChanged) {
|
|
203
|
+
filters.oldYearTo = query1.filters.yearTo;
|
|
204
|
+
filters.newYearTo = query2.filters.yearTo;
|
|
205
|
+
}
|
|
206
|
+
return { blocks, filters };
|
|
207
|
+
}
|
|
208
|
+
function formatQueryDiff(queryDiff) {
|
|
209
|
+
const lines = [];
|
|
210
|
+
lines.push("Query changes:");
|
|
211
|
+
for (const block of queryDiff.blocks) {
|
|
212
|
+
const blockNum = block.index + 1;
|
|
213
|
+
let blockHeader = ` Block ${blockNum} (${block.field})`;
|
|
214
|
+
if (block.isNew) {
|
|
215
|
+
blockHeader += " (new block)";
|
|
216
|
+
} else if (block.isRemoved) {
|
|
217
|
+
blockHeader += " (removed block)";
|
|
218
|
+
}
|
|
219
|
+
if (!block.hasChanges) {
|
|
220
|
+
lines.push(`${blockHeader}: no changes`);
|
|
221
|
+
} else {
|
|
222
|
+
lines.push(`${blockHeader}:`);
|
|
223
|
+
for (const keyword of block.added) {
|
|
224
|
+
lines.push(` + ${keyword}`);
|
|
225
|
+
}
|
|
226
|
+
for (const keyword of block.removed) {
|
|
227
|
+
lines.push(` - ${keyword}`);
|
|
228
|
+
}
|
|
229
|
+
if (block.meshAdded) {
|
|
230
|
+
for (const term of block.meshAdded) {
|
|
231
|
+
lines.push(` + [MeSH] ${term}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (block.meshRemoved) {
|
|
235
|
+
for (const term of block.meshRemoved) {
|
|
236
|
+
lines.push(` - [MeSH] ${term}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (block.emtreeAdded) {
|
|
240
|
+
for (const term of block.emtreeAdded) {
|
|
241
|
+
lines.push(` + [Emtree] ${term}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (block.emtreeRemoved) {
|
|
245
|
+
for (const term of block.emtreeRemoved) {
|
|
246
|
+
lines.push(` - [Emtree] ${term}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (block.excludeAdded) {
|
|
250
|
+
for (const term of block.excludeAdded) {
|
|
251
|
+
lines.push(` + [exclude] ${term}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (block.excludeRemoved) {
|
|
255
|
+
for (const term of block.excludeRemoved) {
|
|
256
|
+
lines.push(` - [exclude] ${term}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const hasFilterChanges = queryDiff.filters.yearFromChanged || queryDiff.filters.yearToChanged || queryDiff.filters.languagesAdded.length > 0 || queryDiff.filters.languagesRemoved.length > 0;
|
|
262
|
+
if (hasFilterChanges) {
|
|
263
|
+
lines.push("");
|
|
264
|
+
lines.push(" Filters:");
|
|
265
|
+
if (queryDiff.filters.yearFromChanged) {
|
|
266
|
+
const oldVal = queryDiff.filters.oldYearFrom ?? "(none)";
|
|
267
|
+
const newVal = queryDiff.filters.newYearFrom ?? "(none)";
|
|
268
|
+
lines.push(` yearFrom: ${oldVal} → ${newVal}`);
|
|
269
|
+
}
|
|
270
|
+
if (queryDiff.filters.yearToChanged) {
|
|
271
|
+
const oldVal = queryDiff.filters.oldYearTo ?? "(none)";
|
|
272
|
+
const newVal = queryDiff.filters.newYearTo ?? "(none)";
|
|
273
|
+
lines.push(` yearTo: ${oldVal} → ${newVal}`);
|
|
274
|
+
}
|
|
275
|
+
if (queryDiff.filters.languagesAdded.length > 0 || queryDiff.filters.languagesRemoved.length > 0) {
|
|
276
|
+
lines.push(" languages:");
|
|
277
|
+
for (const lang of queryDiff.filters.languagesAdded) {
|
|
278
|
+
lines.push(` + ${lang}`);
|
|
279
|
+
}
|
|
280
|
+
for (const lang of queryDiff.filters.languagesRemoved) {
|
|
281
|
+
lines.push(` - ${lang}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return lines.join("\n");
|
|
286
|
+
}
|
|
118
287
|
export {
|
|
119
288
|
computeDiff,
|
|
289
|
+
computeQueryDiff,
|
|
120
290
|
formatDiff,
|
|
121
|
-
formatDiffJson
|
|
291
|
+
formatDiffJson,
|
|
292
|
+
formatQueryDiff
|
|
122
293
|
};
|
|
123
294
|
//# sourceMappingURL=diff.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diff.js","sources":["../../../src/cli/commands/diff.ts"],"sourcesContent":["import type { Article } from '../../providers/base/types.js';\nimport { getArticleKeys } from './session-utils.js';\n\nexport interface DiffResult {\n session1Count: number;\n session2Count: number;\n added: Article[];\n removed: Article[];\n common: Article[];\n}\n\n/**\n * Compute the diff between two sets of articles.\n *\n * Articles are matched by identifiers (DOI, PMID, arXiv ID, Scopus ID, ERIC ID).\n * Two articles are considered the same if they share any identifier.\n * Articles without identifiers cannot be matched.\n */\nexport function computeDiff(session1: Article[], session2: Article[]): DiffResult {\n // Build a set of all identifier keys from session1\n const session1Keys = new Set<string>();\n for (const article of session1) {\n for (const key of getArticleKeys(article)) {\n session1Keys.add(key);\n }\n }\n\n // Build a set of all identifier keys from session2\n const session2Keys = new Set<string>();\n for (const article of session2) {\n for (const key of getArticleKeys(article)) {\n session2Keys.add(key);\n }\n }\n\n // Classify session1 articles as common or removed\n const common: Article[] = [];\n const removed: Article[] = [];\n for (const article of session1) {\n const keys = getArticleKeys(article);\n if (keys.length === 0) {\n // No identifiers - cannot match, treat as removed\n removed.push(article);\n continue;\n }\n const isInSession2 = keys.some((key) => session2Keys.has(key));\n if (isInSession2) {\n common.push(article);\n } else {\n removed.push(article);\n }\n }\n\n // Classify session2 articles as added (those not in session1)\n const added: Article[] = [];\n for (const article of session2) {\n const keys = getArticleKeys(article);\n if (keys.length === 0) {\n // No identifiers - cannot match, treat as added\n added.push(article);\n continue;\n }\n const isInSession1 = keys.some((key) => session1Keys.has(key));\n if (!isInSession1) {\n added.push(article);\n }\n }\n\n return {\n session1Count: session1.length,\n session2Count: session2.length,\n added,\n removed,\n common,\n };\n}\n\nexport type ShowFilter = 'added' | 'removed' | 'common';\n\n/**\n * Extract a year string from publicationDate, or return empty string.\n */\nfunction extractYear(publicationDate: string | undefined): string {\n if (!publicationDate) return '';\n const year = publicationDate.substring(0, 4);\n return /^\\d{4}$/.test(year) ? year : '';\n}\n\n/**\n * Format an article line for display.\n */\nfunction formatArticleLine(prefix: string, article: Article): string {\n const year = extractYear(article.publicationDate);\n const yearPart = year ? `[${year}] ` : '';\n return ` ${prefix} ${yearPart}${article.title}`;\n}\n\n/**\n * Format diff result as human-readable text.\n */\nexport function formatDiff(\n diff: DiffResult,\n session1Id: string,\n session2Id: string,\n show?: ShowFilter,\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push(`Diff: ${session1Id} → ${session2Id}`);\n lines.push(` Session 1: ${diff.session1Count} articles (${session1Id})`);\n lines.push(` Session 2: ${diff.session2Count} articles (${session2Id})`);\n lines.push('');\n\n // Summary counts\n lines.push(` Common: ${diff.common.length} articles`);\n lines.push(` Added: ${diff.added.length} articles (in ${session2Id} but not ${session1Id})`);\n lines.push(` Removed: ${diff.removed.length} articles (in ${session1Id} but not ${session2Id})`);\n\n // Article lists based on show filter\n const showAdded = !show || show === 'added';\n const showRemoved = !show || show === 'removed';\n const showCommon = show === 'common';\n\n if (showAdded && diff.added.length > 0) {\n lines.push('');\n lines.push(`Added (+${diff.added.length}):`);\n for (const article of diff.added) {\n lines.push(formatArticleLine('+', article));\n }\n }\n\n if (showRemoved && diff.removed.length > 0) {\n lines.push('');\n lines.push(`Removed (-${diff.removed.length}):`);\n for (const article of diff.removed) {\n lines.push(formatArticleLine('-', article));\n }\n }\n\n if (showCommon && diff.common.length > 0) {\n lines.push('');\n lines.push(`Common (${diff.common.length}):`);\n for (const article of diff.common) {\n lines.push(formatArticleLine('=', article));\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format diff result as JSON.\n */\ninterface DiffJsonOutput {\n session1: string;\n session2: string;\n summary: {\n session1Count: number;\n session2Count: number;\n commonCount: number;\n addedCount: number;\n removedCount: number;\n };\n added?: Article[];\n removed?: Article[];\n common?: Article[];\n}\n\nexport function formatDiffJson(\n diff: DiffResult,\n session1Id: string,\n session2Id: string,\n show?: ShowFilter,\n): string {\n const result: DiffJsonOutput = {\n session1: session1Id,\n session2: session2Id,\n summary: {\n session1Count: diff.session1Count,\n session2Count: diff.session2Count,\n commonCount: diff.common.length,\n addedCount: diff.added.length,\n removedCount: diff.removed.length,\n },\n };\n\n if (!show || show === 'added') {\n result.added = diff.added;\n }\n if (!show || show === 'removed') {\n result.removed = diff.removed;\n }\n if (!show || show === 'common') {\n result.common = diff.common;\n }\n\n return JSON.stringify(result, null, 2);\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"diff.js","sources":["../../../src/cli/commands/diff.ts"],"sourcesContent":["import type { Article } from '../../providers/base/types.js';\nimport type { QueryAST, FieldType, TermBlock } from '../../query/types.js';\nimport { getArticleKeys } from './session-utils.js';\n\nexport interface DiffResult {\n session1Count: number;\n session2Count: number;\n added: Article[];\n removed: Article[];\n common: Article[];\n}\n\n/**\n * Compute the diff between two sets of articles.\n *\n * Articles are matched by identifiers (DOI, PMID, arXiv ID, Scopus ID, ERIC ID).\n * Two articles are considered the same if they share any identifier.\n * Articles without identifiers cannot be matched.\n */\nexport function computeDiff(session1: Article[], session2: Article[]): DiffResult {\n // Build a set of all identifier keys from session1\n const session1Keys = new Set<string>();\n for (const article of session1) {\n for (const key of getArticleKeys(article)) {\n session1Keys.add(key);\n }\n }\n\n // Build a set of all identifier keys from session2\n const session2Keys = new Set<string>();\n for (const article of session2) {\n for (const key of getArticleKeys(article)) {\n session2Keys.add(key);\n }\n }\n\n // Classify session1 articles as common or removed\n const common: Article[] = [];\n const removed: Article[] = [];\n for (const article of session1) {\n const keys = getArticleKeys(article);\n if (keys.length === 0) {\n // No identifiers - cannot match, treat as removed\n removed.push(article);\n continue;\n }\n const isInSession2 = keys.some((key) => session2Keys.has(key));\n if (isInSession2) {\n common.push(article);\n } else {\n removed.push(article);\n }\n }\n\n // Classify session2 articles as added (those not in session1)\n const added: Article[] = [];\n for (const article of session2) {\n const keys = getArticleKeys(article);\n if (keys.length === 0) {\n // No identifiers - cannot match, treat as added\n added.push(article);\n continue;\n }\n const isInSession1 = keys.some((key) => session1Keys.has(key));\n if (!isInSession1) {\n added.push(article);\n }\n }\n\n return {\n session1Count: session1.length,\n session2Count: session2.length,\n added,\n removed,\n common,\n };\n}\n\nexport type ShowFilter = 'added' | 'removed' | 'common';\n\n/**\n * Extract a year string from publicationDate, or return empty string.\n */\nfunction extractYear(publicationDate: string | undefined): string {\n if (!publicationDate) return '';\n const year = publicationDate.substring(0, 4);\n return /^\\d{4}$/.test(year) ? year : '';\n}\n\n/**\n * Format an article line for display.\n */\nfunction formatArticleLine(prefix: string, article: Article): string {\n const year = extractYear(article.publicationDate);\n const yearPart = year ? `[${year}] ` : '';\n return ` ${prefix} ${yearPart}${article.title}`;\n}\n\n/**\n * Options for formatting diff with query information.\n */\nexport interface FormatDiffOptions {\n queryDiff?: QueryDiff | undefined;\n noQueryDiff?: boolean | undefined;\n showQueryDiffPlaceholder?: boolean | undefined;\n}\n\n/**\n * Format diff result as human-readable text.\n */\nexport function formatDiff(\n diff: DiffResult,\n session1Id: string,\n session2Id: string,\n show?: ShowFilter,\n options?: FormatDiffOptions,\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push(`Diff: ${session1Id} → ${session2Id}`);\n lines.push(` Session 1: ${diff.session1Count} articles (${session1Id})`);\n lines.push(` Session 2: ${diff.session2Count} articles (${session2Id})`);\n lines.push('');\n\n // Query changes section (if available and not disabled)\n const shouldShowQueryDiff = options?.queryDiff && !options?.noQueryDiff;\n const shouldShowPlaceholder = options?.showQueryDiffPlaceholder && !options?.queryDiff && !options?.noQueryDiff;\n\n if (shouldShowPlaceholder) {\n lines.push('Query changes: (query data not available)');\n lines.push('');\n } else if (shouldShowQueryDiff) {\n lines.push(formatQueryDiff(options.queryDiff!));\n lines.push('');\n lines.push('Result changes:');\n }\n\n // Summary counts\n lines.push(` Common: ${diff.common.length} articles`);\n lines.push(` Added: ${diff.added.length} articles (in ${session2Id} but not ${session1Id})`);\n lines.push(` Removed: ${diff.removed.length} articles (in ${session1Id} but not ${session2Id})`);\n\n // Article lists based on show filter\n const showAdded = !show || show === 'added';\n const showRemoved = !show || show === 'removed';\n const showCommon = show === 'common';\n\n if (showAdded && diff.added.length > 0) {\n lines.push('');\n lines.push(`Added (+${diff.added.length}):`);\n for (const article of diff.added) {\n lines.push(formatArticleLine('+', article));\n }\n }\n\n if (showRemoved && diff.removed.length > 0) {\n lines.push('');\n lines.push(`Removed (-${diff.removed.length}):`);\n for (const article of diff.removed) {\n lines.push(formatArticleLine('-', article));\n }\n }\n\n if (showCommon && diff.common.length > 0) {\n lines.push('');\n lines.push(`Common (${diff.common.length}):`);\n for (const article of diff.common) {\n lines.push(formatArticleLine('=', article));\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format diff result as JSON.\n */\ninterface DiffJsonOutput {\n session1: string;\n session2: string;\n summary: {\n session1Count: number;\n session2Count: number;\n commonCount: number;\n addedCount: number;\n removedCount: number;\n };\n queryDiff?: QueryDiff;\n added?: Article[];\n removed?: Article[];\n common?: Article[];\n}\n\nexport function formatDiffJson(\n diff: DiffResult,\n session1Id: string,\n session2Id: string,\n show?: ShowFilter,\n options?: FormatDiffOptions,\n): string {\n const result: DiffJsonOutput = {\n session1: session1Id,\n session2: session2Id,\n summary: {\n session1Count: diff.session1Count,\n session2Count: diff.session2Count,\n commonCount: diff.common.length,\n addedCount: diff.added.length,\n removedCount: diff.removed.length,\n },\n };\n\n // Add queryDiff if available and not disabled\n if (options?.queryDiff && !options?.noQueryDiff) {\n result.queryDiff = options.queryDiff;\n }\n\n if (!show || show === 'added') {\n result.added = diff.added;\n }\n if (!show || show === 'removed') {\n result.removed = diff.removed;\n }\n if (!show || show === 'common') {\n result.common = diff.common;\n }\n\n return JSON.stringify(result, null, 2);\n}\n\n/**\n * Diff result for a single query block.\n */\nexport interface BlockDiff {\n index: number;\n field: FieldType;\n added: string[];\n removed: string[];\n meshAdded?: string[];\n meshRemoved?: string[];\n emtreeAdded?: string[];\n emtreeRemoved?: string[];\n excludeAdded?: string[];\n excludeRemoved?: string[];\n hasChanges: boolean;\n isNew?: boolean;\n isRemoved?: boolean;\n}\n\n/**\n * Diff result for query filters.\n */\nexport interface FilterDiff {\n yearFromChanged: boolean;\n oldYearFrom?: number | undefined;\n newYearFrom?: number | undefined;\n yearToChanged: boolean;\n oldYearTo?: number | undefined;\n newYearTo?: number | undefined;\n languagesAdded: string[];\n languagesRemoved: string[];\n}\n\n/**\n * Complete query diff result.\n */\nexport interface QueryDiff {\n blocks: BlockDiff[];\n filters: FilterDiff;\n}\n\n/**\n * Compute the set difference: elements in arr2 not in arr1.\n */\nfunction setDiff(arr1: string[], arr2: string[]): string[] {\n const set1 = new Set(arr1);\n return arr2.filter((item) => !set1.has(item));\n}\n\n/**\n * Compare two query blocks and return the differences.\n */\nfunction compareBlocks(\n block1: TermBlock | undefined,\n block2: TermBlock | undefined,\n index: number,\n field: FieldType,\n): BlockDiff {\n const emptyTerms = { keywords: [] as string[], mesh: [] as string[], emtree: [] as string[], exclude: [] as string[] };\n const terms1 = block1 ?? emptyTerms;\n const terms2 = block2 ?? emptyTerms;\n\n const added = setDiff(terms1.keywords, terms2.keywords);\n const removed = setDiff(terms2.keywords, terms1.keywords);\n const meshAdded = setDiff(terms1.mesh ?? [], terms2.mesh ?? []);\n const meshRemoved = setDiff(terms2.mesh ?? [], terms1.mesh ?? []);\n const emtreeAdded = setDiff(terms1.emtree ?? [], terms2.emtree ?? []);\n const emtreeRemoved = setDiff(terms2.emtree ?? [], terms1.emtree ?? []);\n const excludeAdded = setDiff(terms1.exclude ?? [], terms2.exclude ?? []);\n const excludeRemoved = setDiff(terms2.exclude ?? [], terms1.exclude ?? []);\n\n const hasChanges =\n added.length > 0 ||\n removed.length > 0 ||\n meshAdded.length > 0 ||\n meshRemoved.length > 0 ||\n emtreeAdded.length > 0 ||\n emtreeRemoved.length > 0 ||\n excludeAdded.length > 0 ||\n excludeRemoved.length > 0;\n\n const result: BlockDiff = {\n index,\n field,\n added,\n removed,\n hasChanges,\n };\n\n if (meshAdded.length > 0 || meshRemoved.length > 0) {\n result.meshAdded = meshAdded;\n result.meshRemoved = meshRemoved;\n }\n if (emtreeAdded.length > 0 || emtreeRemoved.length > 0) {\n result.emtreeAdded = emtreeAdded;\n result.emtreeRemoved = emtreeRemoved;\n }\n if (excludeAdded.length > 0 || excludeRemoved.length > 0) {\n result.excludeAdded = excludeAdded;\n result.excludeRemoved = excludeRemoved;\n }\n\n if (!block1) {\n result.isNew = true;\n result.hasChanges = true;\n }\n if (!block2) {\n result.isRemoved = true;\n result.hasChanges = true;\n }\n\n return result;\n}\n\n/**\n * Compute the diff between two QueryAST objects.\n */\nexport function computeQueryDiff(query1: QueryAST, query2: QueryAST): QueryDiff {\n const blocks: BlockDiff[] = [];\n\n const maxBlocks = Math.max(query1.blocks.length, query2.blocks.length);\n\n for (let i = 0; i < maxBlocks; i++) {\n const block1 = query1.blocks[i];\n const block2 = query2.blocks[i];\n\n const field = block2?.field ?? block1?.field ?? 'all';\n const blockDiff = compareBlocks(\n block1?.terms,\n block2?.terms,\n i,\n field,\n );\n blocks.push(blockDiff);\n }\n\n // Compare filters\n const filters: FilterDiff = {\n yearFromChanged: query1.filters.yearFrom !== query2.filters.yearFrom,\n yearToChanged: query1.filters.yearTo !== query2.filters.yearTo,\n languagesAdded: setDiff(query1.filters.languages ?? [], query2.filters.languages ?? []),\n languagesRemoved: setDiff(query2.filters.languages ?? [], query1.filters.languages ?? []),\n };\n\n if (filters.yearFromChanged) {\n filters.oldYearFrom = query1.filters.yearFrom;\n filters.newYearFrom = query2.filters.yearFrom;\n }\n if (filters.yearToChanged) {\n filters.oldYearTo = query1.filters.yearTo;\n filters.newYearTo = query2.filters.yearTo;\n }\n\n return { blocks, filters };\n}\n\n/**\n * Format query diff as human-readable text.\n */\nexport function formatQueryDiff(queryDiff: QueryDiff): string {\n const lines: string[] = [];\n\n lines.push('Query changes:');\n\n // Format block changes\n for (const block of queryDiff.blocks) {\n const blockNum = block.index + 1;\n let blockHeader = ` Block ${blockNum} (${block.field})`;\n if (block.isNew) {\n blockHeader += ' (new block)';\n } else if (block.isRemoved) {\n blockHeader += ' (removed block)';\n }\n\n if (!block.hasChanges) {\n lines.push(`${blockHeader}: no changes`);\n } else {\n lines.push(`${blockHeader}:`);\n\n // Added keywords\n for (const keyword of block.added) {\n lines.push(` + ${keyword}`);\n }\n\n // Removed keywords\n for (const keyword of block.removed) {\n lines.push(` - ${keyword}`);\n }\n\n // MeSH changes\n if (block.meshAdded) {\n for (const term of block.meshAdded) {\n lines.push(` + [MeSH] ${term}`);\n }\n }\n if (block.meshRemoved) {\n for (const term of block.meshRemoved) {\n lines.push(` - [MeSH] ${term}`);\n }\n }\n\n // Emtree changes\n if (block.emtreeAdded) {\n for (const term of block.emtreeAdded) {\n lines.push(` + [Emtree] ${term}`);\n }\n }\n if (block.emtreeRemoved) {\n for (const term of block.emtreeRemoved) {\n lines.push(` - [Emtree] ${term}`);\n }\n }\n\n // Exclude changes\n if (block.excludeAdded) {\n for (const term of block.excludeAdded) {\n lines.push(` + [exclude] ${term}`);\n }\n }\n if (block.excludeRemoved) {\n for (const term of block.excludeRemoved) {\n lines.push(` - [exclude] ${term}`);\n }\n }\n }\n }\n\n // Format filter changes\n const hasFilterChanges =\n queryDiff.filters.yearFromChanged ||\n queryDiff.filters.yearToChanged ||\n queryDiff.filters.languagesAdded.length > 0 ||\n queryDiff.filters.languagesRemoved.length > 0;\n\n if (hasFilterChanges) {\n lines.push('');\n lines.push(' Filters:');\n\n if (queryDiff.filters.yearFromChanged) {\n const oldVal = queryDiff.filters.oldYearFrom ?? '(none)';\n const newVal = queryDiff.filters.newYearFrom ?? '(none)';\n lines.push(` yearFrom: ${oldVal} → ${newVal}`);\n }\n\n if (queryDiff.filters.yearToChanged) {\n const oldVal = queryDiff.filters.oldYearTo ?? '(none)';\n const newVal = queryDiff.filters.newYearTo ?? '(none)';\n lines.push(` yearTo: ${oldVal} → ${newVal}`);\n }\n\n if (queryDiff.filters.languagesAdded.length > 0 || queryDiff.filters.languagesRemoved.length > 0) {\n lines.push(' languages:');\n for (const lang of queryDiff.filters.languagesAdded) {\n lines.push(` + ${lang}`);\n }\n for (const lang of queryDiff.filters.languagesRemoved) {\n lines.push(` - ${lang}`);\n }\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"AAyDA;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA4BjD"}
|
|
@@ -10,6 +10,8 @@ query:
|
|
|
10
10
|
- "search term 2"
|
|
11
11
|
# mesh: # PubMed MeSH terms (optional)
|
|
12
12
|
# - "MeSH Heading"
|
|
13
|
+
# eric: # ERIC Descriptors (optional, ERIC only)
|
|
14
|
+
# - "ERIC Descriptor"
|
|
13
15
|
exclude: [] # Terms to exclude (NOT operator)
|
|
14
16
|
# Tip: Use exclude to filter out false matches from short keywords/acronyms
|
|
15
17
|
# exclude:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sources":["../../../../src/cli/commands/query/init.ts"],"sourcesContent":["/**\n * Query init command implementation.\n *\n * Generates a skeleton YAML query file with helpful comments.\n */\nimport { writeFile as fsWriteFile, access } from \"node:fs/promises\";\n\n/**\n * The YAML template string with comments preserved.\n * This is a raw string (not generated by a YAML library) so comments are kept.\n */\n// prettier-ignore\nconst QUERY_TEMPLATE =\n \"name: my_search\\n\" +\n \"description: \\\"\\\"\\n\" +\n \"\\n\" +\n \"query:\\n\" +\n \" - field: title_abstract # title, abstract, title_abstract, author, keyword, all\\n\" +\n \" terms:\\n\" +\n \" keywords:\\n\" +\n \" - \\\"search term 1\\\"\\n\" +\n \" - \\\"search term 2\\\"\\n\" +\n \" # mesh: # PubMed MeSH terms (optional)\\n\" +\n \" # - \\\"MeSH Heading\\\"\\n\" +\n \" exclude: [] # Terms to exclude (NOT operator)\\n\" +\n \" # Tip: Use exclude to filter out false matches from short keywords/acronyms\\n\" +\n \" # exclude:\\n\" +\n \" # - \\\"unwanted term\\\"\\n\" +\n \" # - \\\"irrelevant topic\\\"\\n\" +\n \" operator: OR # How to combine terms within this block\\n\" +\n \"\\n\" +\n \" # Add more blocks — blocks are AND'd together\\n\" +\n \" # - field: title_abstract\\n\" +\n \" # terms:\\n\" +\n \" # keywords:\\n\" +\n \" # - \\\"another term\\\"\\n\" +\n \" # operator: OR\\n\" +\n \"\\n\" +\n \"# filters: # Optional: apply to all databases\\n\" +\n \"# year_from: 2020\\n\" +\n \"# year_to: 2026\\n\" +\n \"# language:\\n\" +\n \"# - en\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Review\\\"\\n\" +\n \"# - \\\"Comment\\\"\\n\" +\n \"\\n\" +\n \"# overrides: # Optional: database-specific settings\\n\" +\n \"# pubmed:\\n\" +\n \"# filters:\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Letter\\\"\\n\";\n\n/**\n * Generate the query template YAML string.\n *\n * @returns The YAML template string with comments\n */\nexport function generateQueryTemplate(): string {\n return QUERY_TEMPLATE;\n}\n\n/**\n * Write the query template to a file or return it as a message.\n *\n * @param options - Output options\n * @param options.output - File path to write to (if omitted, returns template in message)\n * @param options.force - Whether to overwrite existing files\n * @returns Result with success status and message\n */\nexport async function writeQueryTemplate(options: {\n output?: string;\n force?: boolean;\n}): Promise<{ success: boolean; message: string }> {\n const template = generateQueryTemplate();\n\n if (!options.output) {\n // No output file specified, return template as message\n return { success: true, message: template };\n }\n\n // Check if file exists (unless force is set)\n if (!options.force) {\n try {\n await access(options.output);\n // File exists and force is not set\n return {\n success: false,\n message: `File already exists: ${options.output}. Use --force to overwrite.`,\n };\n } catch {\n // File does not exist, proceed\n }\n }\n\n // Write to file\n await fsWriteFile(options.output, template, \"utf-8\");\n return {\n success: true,\n message: `Template written to ${options.output}`,\n };\n}\n"],"names":["fsWriteFile"],"mappings":";AAYA,MAAM,iBACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
1
|
+
{"version":3,"file":"init.js","sources":["../../../../src/cli/commands/query/init.ts"],"sourcesContent":["/**\n * Query init command implementation.\n *\n * Generates a skeleton YAML query file with helpful comments.\n */\nimport { writeFile as fsWriteFile, access } from \"node:fs/promises\";\n\n/**\n * The YAML template string with comments preserved.\n * This is a raw string (not generated by a YAML library) so comments are kept.\n */\n// prettier-ignore\nconst QUERY_TEMPLATE =\n \"name: my_search\\n\" +\n \"description: \\\"\\\"\\n\" +\n \"\\n\" +\n \"query:\\n\" +\n \" - field: title_abstract # title, abstract, title_abstract, author, keyword, all\\n\" +\n \" terms:\\n\" +\n \" keywords:\\n\" +\n \" - \\\"search term 1\\\"\\n\" +\n \" - \\\"search term 2\\\"\\n\" +\n \" # mesh: # PubMed MeSH terms (optional)\\n\" +\n \" # - \\\"MeSH Heading\\\"\\n\" +\n \" # eric: # ERIC Descriptors (optional, ERIC only)\\n\" +\n \" # - \\\"ERIC Descriptor\\\"\\n\" +\n \" exclude: [] # Terms to exclude (NOT operator)\\n\" +\n \" # Tip: Use exclude to filter out false matches from short keywords/acronyms\\n\" +\n \" # exclude:\\n\" +\n \" # - \\\"unwanted term\\\"\\n\" +\n \" # - \\\"irrelevant topic\\\"\\n\" +\n \" operator: OR # How to combine terms within this block\\n\" +\n \"\\n\" +\n \" # Add more blocks — blocks are AND'd together\\n\" +\n \" # - field: title_abstract\\n\" +\n \" # terms:\\n\" +\n \" # keywords:\\n\" +\n \" # - \\\"another term\\\"\\n\" +\n \" # operator: OR\\n\" +\n \"\\n\" +\n \"# filters: # Optional: apply to all databases\\n\" +\n \"# year_from: 2020\\n\" +\n \"# year_to: 2026\\n\" +\n \"# language:\\n\" +\n \"# - en\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Review\\\"\\n\" +\n \"# - \\\"Comment\\\"\\n\" +\n \"\\n\" +\n \"# overrides: # Optional: database-specific settings\\n\" +\n \"# pubmed:\\n\" +\n \"# filters:\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Letter\\\"\\n\";\n\n/**\n * Generate the query template YAML string.\n *\n * @returns The YAML template string with comments\n */\nexport function generateQueryTemplate(): string {\n return QUERY_TEMPLATE;\n}\n\n/**\n * Write the query template to a file or return it as a message.\n *\n * @param options - Output options\n * @param options.output - File path to write to (if omitted, returns template in message)\n * @param options.force - Whether to overwrite existing files\n * @returns Result with success status and message\n */\nexport async function writeQueryTemplate(options: {\n output?: string;\n force?: boolean;\n}): Promise<{ success: boolean; message: string }> {\n const template = generateQueryTemplate();\n\n if (!options.output) {\n // No output file specified, return template as message\n return { success: true, message: template };\n }\n\n // Check if file exists (unless force is set)\n if (!options.force) {\n try {\n await access(options.output);\n // File exists and force is not set\n return {\n success: false,\n message: `File already exists: ${options.output}. Use --force to overwrite.`,\n };\n } catch {\n // File does not exist, proceed\n }\n }\n\n // Write to file\n await fsWriteFile(options.output, template, \"utf-8\");\n return {\n success: true,\n message: `Template written to ${options.output}`,\n };\n}\n"],"names":["fsWriteFile"],"mappings":";AAYA,MAAM,iBACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiDK,SAAS,wBAAgC;AAC9C,SAAO;AACT;AAUA,eAAsB,mBAAmB,SAGU;AACjD,QAAM,WAAW,sBAAA;AAEjB,MAAI,CAAC,QAAQ,QAAQ;AAEnB,WAAO,EAAE,SAAS,MAAM,SAAS,SAAA;AAAA,EACnC;AAGA,MAAI,CAAC,QAAQ,OAAO;AAClB,QAAI;AACF,YAAM,OAAO,QAAQ,MAAM;AAE3B,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,wBAAwB,QAAQ,MAAM;AAAA,MAAA;AAAA,IAEnD,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAMA,UAAY,QAAQ,QAAQ,UAAU,OAAO;AACnD,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS,uBAAuB,QAAQ,MAAM;AAAA,EAAA;AAElD;"}
|
|
@@ -98,7 +98,7 @@ function getAlternativeIds(article) {
|
|
|
98
98
|
return ids;
|
|
99
99
|
}
|
|
100
100
|
async function hasReviewFile(sessionId, sessionsDir) {
|
|
101
|
-
const reviewsPath = join(sessionsDir, sessionId, "reviews.yaml");
|
|
101
|
+
const reviewsPath = join(sessionsDir, sessionId, ".internal", "reviews.yaml");
|
|
102
102
|
try {
|
|
103
103
|
await access(reviewsPath, constants.R_OK);
|
|
104
104
|
return true;
|
|
@@ -107,7 +107,7 @@ async function hasReviewFile(sessionId, sessionsDir) {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
async function loadReviewFile(sessionId, sessionsDir) {
|
|
110
|
-
const reviewsPath = join(sessionsDir, sessionId, "reviews.yaml");
|
|
110
|
+
const reviewsPath = join(sessionsDir, sessionId, ".internal", "reviews.yaml");
|
|
111
111
|
const content = await readFile(reviewsPath, "utf-8");
|
|
112
112
|
return parse(content);
|
|
113
113
|
}
|
|
@@ -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, needs-final, 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, '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, '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, needs-final, 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 * Format tip about review workflow for users who haven't used it.\n */\nexport function formatReviewWorkflowTip(sessionId: string): string {\n return `\nTip: For systematic reviews, consider using the review workflow:\n 1. search-hub review init ${sessionId}\n 2. (AI/human review in reviews.yaml)\n 3. search-hub register ${sessionId} --reviewed`;\n}\n\n\n/**\n * Format note when --all is used with reviews.yaml present.\n */\nexport function formatIgnoringReviewsNote(total: number): string {\n return `Note: Ignoring review decisions. Registering all ${total} articles.`;\n}\n\n/**\n * Prompt user for Y/n confirmation.\n * Returns true if user confirms (Y/y/Enter), false otherwise.\n */\nexport async function confirmPrompt(\n input: NodeJS.ReadableStream = process.stdin,\n output: NodeJS.WritableStream = process.stdout\n): Promise<boolean> {\n const rl = createInterface({\n input,\n output,\n terminal: false,\n });\n\n return new Promise((resolve) => {\n rl.question('', (answer) => {\n rl.close();\n const trimmed = answer.trim().toLowerCase();\n // Empty (Enter) or 'y' or 'yes' means confirm\n resolve(trimmed === '' || trimmed === 'y' || trimmed === 'yes');\n });\n });\n}\n"],"names":["parseYaml"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"register.js","sources":["../../../src/cli/commands/register.ts"],"sourcesContent":["/**\n * Register command for reference-manager integration.\n * Registers search results with reference-manager CLI.\n */\n\nimport { join } from 'node:path';\nimport { readFile, access } from 'node:fs/promises';\nimport { constants } from 'node:fs';\nimport { createInterface } from 'node:readline';\nimport { parse as parseYaml } from 'yaml';\nimport type { ProviderName, Article, Author } from '../../providers/base/types.js';\nimport { parseProviderNames } from '../utils/validation.js';\nimport { classifyStatus, type ReviewFile } from './review/types.js';\n\nexport interface RegisterCommandOptions {\n sessionId: string;\n providers?: ProviderName[];\n dryRun: boolean;\n withAbstracts: boolean;\n /** Register only reviewed articles with finalDecision='include' */\n reviewed?: boolean;\n /** Register all articles, ignoring reviews */\n all?: boolean;\n /** Skip confirmation prompts */\n force?: boolean;\n /** Suppress tips and suggestions */\n quiet?: boolean;\n}\n\nexport interface CommandLineOptions {\n db?: string | undefined;\n dryRun?: boolean | undefined;\n withAbstracts?: boolean | undefined;\n reviewed?: boolean | undefined;\n all?: boolean | undefined;\n force?: boolean | undefined;\n quiet?: boolean | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\n/**\n * Parse command line options into RegisterCommandOptions.\n */\nexport function parseRegisterOptions(\n sessionId: string,\n options: CommandLineOptions\n): RegisterCommandOptions {\n const result: RegisterCommandOptions = {\n sessionId,\n dryRun: options.dryRun ?? false,\n withAbstracts: options.withAbstracts ?? false,\n reviewed: options.reviewed ?? false,\n all: options.all ?? false,\n force: options.force ?? false,\n quiet: options.quiet ?? false,\n };\n\n if (options.db) {\n result.providers = parseProviderNames(options.db);\n }\n\n return result;\n}\n\n/**\n * Validate register command input.\n */\nexport function validateRegisterInput(options: RegisterCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Format registration summary for CLI output.\n */\nexport function formatRegistrationSummary(summary: {\n total: number;\n added: number;\n skipped: number;\n failed: number;\n noId: number;\n}): string {\n const lines: string[] = ['Registration complete:'];\n\n // Added\n lines.push(` ✓ ${summary.added} added`);\n\n // Duplicates (skipped)\n if (summary.skipped > 0) {\n lines.push(` ⚠ ${summary.skipped} duplicates (already in library)`);\n }\n\n // Failed\n if (summary.failed > 0) {\n lines.push(` ✗ ${summary.failed} failed`);\n }\n\n // No ID (skipped)\n if (summary.noId > 0) {\n lines.push(` - ${summary.noId} skipped (no identifier)`);\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Get registration identifier for an article.\n * PMID is preferred over DOI for better metadata quality.\n */\nfunction getRegistrationId(article: Article): string | null {\n if (article.pmid) {\n return `pmid:${article.pmid}`;\n }\n if (article.doi) {\n return article.doi;\n }\n return null;\n}\n\n/**\n * Format dry run output showing what would be registered.\n */\nexport function formatDryRunOutput(articles: Article[]): string {\n const withId: Array<{ article: Article; id: string }> = [];\n const withoutId: Article[] = [];\n\n for (const article of articles) {\n const id = getRegistrationId(article);\n if (id) {\n withId.push({ article, id });\n } else {\n withoutId.push(article);\n }\n }\n\n const lines: string[] = [];\n\n // Summary\n lines.push(\n `Would register ${withId.length} reference${withId.length !== 1 ? 's' : ''}:`\n );\n\n // List articles with IDs\n for (const { id, article } of withId) {\n const title = article.title.length > 60\n ? article.title.substring(0, 57) + '...'\n : article.title;\n lines.push(` - ${id}: ${title}`);\n }\n\n // Details about articles without DOI/PMID\n if (withoutId.length > 0) {\n lines.push('');\n lines.push(\n `${withoutId.length} article${withoutId.length !== 1 ? 's' : ''} will be skipped (no DOI or PMID):`\n );\n\n const maxDisplay = 10;\n const displayed = withoutId.slice(0, maxDisplay);\n\n for (const article of displayed) {\n const truncatedTitle = article.title.length > 50\n ? article.title.substring(0, 50) + '...'\n : article.title;\n\n const altIds = getAlternativeIds(article);\n const hasAltIds = altIds.length > 0 ? `, has: ${altIds.join(', ')}` : '';\n\n lines.push(` - \"${truncatedTitle}\" (source: ${article.source}${hasAltIds})`);\n }\n\n if (withoutId.length > maxDisplay) {\n lines.push(` ... and ${withoutId.length - maxDisplay} more`);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Get alternative (non-DOI/PMID) identifiers for an article.\n */\nfunction getAlternativeIds(article: Article): string[] {\n const ids: string[] = [];\n if (article.arxivId) ids.push(`arxiv:${article.arxivId}`);\n if (article.ericId) ids.push(`eric:${article.ericId}`);\n if (article.scopusId) ids.push(`scopus:${article.scopusId}`);\n return ids;\n}\n\n/**\n * Summary of review decisions for a session.\n */\nexport interface ReviewSummary {\n /** Total articles in review file */\n total: number;\n /** Articles with finalDecision='include' */\n included: number;\n /** Articles with finalDecision='exclude' */\n excluded: number;\n /** Articles without finalDecision (pending, needs-final, 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, needs-final, 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 * Format tip about review workflow for users who haven't used it.\n */\nexport function formatReviewWorkflowTip(sessionId: string): string {\n return `\nTip: For systematic reviews, consider using the review workflow:\n 1. search-hub review init ${sessionId}\n 2. (AI/human review in reviews.yaml)\n 3. search-hub register ${sessionId} --reviewed`;\n}\n\n\n/**\n * Format note when --all is used with reviews.yaml present.\n */\nexport function formatIgnoringReviewsNote(total: number): string {\n return `Note: Ignoring review decisions. Registering all ${total} articles.`;\n}\n\n/**\n * Prompt user for Y/n confirmation.\n * Returns true if user confirms (Y/y/Enter), false otherwise.\n */\nexport async function confirmPrompt(\n input: NodeJS.ReadableStream = process.stdin,\n output: NodeJS.WritableStream = process.stdout\n): Promise<boolean> {\n const rl = createInterface({\n input,\n output,\n terminal: false,\n });\n\n return new Promise((resolve) => {\n rl.question('', (answer) => {\n rl.close();\n const trimmed = answer.trim().toLowerCase();\n // Empty (Enter) or 'y' or 'yes' means confirm\n resolve(trimmed === '' || trimmed === 'y' || trimmed === 'yes');\n });\n });\n}\n"],"names":["parseYaml"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { join, dirname } from "node:path";
|
|
|
2
2
|
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
|
3
3
|
import { stringify, parse } from "yaml";
|
|
4
4
|
async function loadReviewFile(sessionDir) {
|
|
5
|
-
const reviewsPath = join(sessionDir, "reviews.yaml");
|
|
5
|
+
const reviewsPath = join(sessionDir, ".internal", "reviews.yaml");
|
|
6
6
|
const content = await readFile(reviewsPath, "utf-8");
|
|
7
7
|
return parse(content);
|
|
8
8
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export.js","sources":["../../../../src/cli/commands/review/export.ts"],"sourcesContent":["/**\n * review export command - Export articles based on final decision\n */\n\nimport { join, dirname } from 'node:path';\nimport { readFile, writeFile, mkdir } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { ReviewFile, ArticleEntry } from './types.js';\n\nexport type ExportFormat = 'yaml' | 'json' | 'jsonl';\nexport type ExportFilter = 'included' | 'excluded';\n\nexport interface ReviewExportOptions {\n sessionId: string;\n only: ExportFilter;\n output: string;\n format: ExportFormat;\n}\n\nexport interface ReviewExportResult {\n outputPath: string;\n exportedCount: number;\n format: ExportFormat;\n}\n\n/**\n * Exported article structure (without review details)\n */\ninterface ExportedArticle {\n title: string;\n doi?: string;\n pmid?: string;\n scopusId?: string;\n arxivId?: string;\n ericId?: string;\n year?: string;\n authors?: string;\n abstract?: string;\n finalDecision: 'include' | 'exclude';\n}\n\n/**\n * Load review file from session directory\n */\nasync function loadReviewFile(sessionDir: string): Promise<ReviewFile> {\n const reviewsPath = join(sessionDir, 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Convert ArticleEntry to exported format (without reviews)\n */\nfunction articleToExport(article: ArticleEntry): ExportedArticle {\n const exported: ExportedArticle = {\n title: article.title,\n finalDecision: article.finalDecision!,\n };\n\n if (article.doi) exported.doi = article.doi;\n if (article.pmid) exported.pmid = article.pmid;\n if (article.scopusId) exported.scopusId = article.scopusId;\n if (article.arxivId) exported.arxivId = article.arxivId;\n if (article.ericId) exported.ericId = article.ericId;\n if (article.year) exported.year = article.year;\n if (article.authors) exported.authors = article.authors;\n if (article.abstract) exported.abstract = article.abstract;\n\n return exported;\n}\n\n/**\n * Execute review export command\n */\nexport async function executeReviewExport(\n options: ReviewExportOptions,\n sessionsDir: string\n): Promise<ReviewExportResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n const reviewFile = await loadReviewFile(sessionDir);\n\n // Filter articles by final decision\n const targetDecision = options.only === 'included' ? 'include' : 'exclude';\n const filtered = reviewFile.articles.filter(\n (article) => article.finalDecision === targetDecision\n );\n\n // Convert to export format\n const exported = filtered.map(articleToExport);\n\n // Ensure output directory exists\n const outputDir = dirname(options.output);\n await mkdir(outputDir, { recursive: true });\n\n // Write output in requested format\n let content: string;\n switch (options.format) {\n case 'yaml':\n content = stringifyYaml({ articles: exported }, { lineWidth: 0 });\n break;\n case 'json':\n content = JSON.stringify({ articles: exported }, null, 2);\n break;\n case 'jsonl':\n content = exported.map((a) => JSON.stringify(a)).join('\\n');\n if (content) content += '\\n';\n break;\n }\n\n await writeFile(options.output, content, 'utf-8');\n\n return {\n outputPath: options.output,\n exportedCount: exported.length,\n format: options.format,\n };\n}\n\n/**\n * Format export result as human-readable string\n */\nexport function formatExportOutput(result: ReviewExportResult): string {\n if (result.exportedCount === 0) {\n return 'No articles matched the filter.';\n }\n\n return `Exported ${result.exportedCount} article(s) to ${result.outputPath} (${result.format})`;\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":";;;AA4CA,eAAe,eAAe,YAAyC;AACrE,QAAM,cAAc,KAAK,YAAY,cAAc;
|
|
1
|
+
{"version":3,"file":"export.js","sources":["../../../../src/cli/commands/review/export.ts"],"sourcesContent":["/**\n * review export command - Export articles based on final decision\n */\n\nimport { join, dirname } from 'node:path';\nimport { readFile, writeFile, mkdir } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { ReviewFile, ArticleEntry } from './types.js';\n\nexport type ExportFormat = 'yaml' | 'json' | 'jsonl';\nexport type ExportFilter = 'included' | 'excluded';\n\nexport interface ReviewExportOptions {\n sessionId: string;\n only: ExportFilter;\n output: string;\n format: ExportFormat;\n}\n\nexport interface ReviewExportResult {\n outputPath: string;\n exportedCount: number;\n format: ExportFormat;\n}\n\n/**\n * Exported article structure (without review details)\n */\ninterface ExportedArticle {\n title: string;\n doi?: string;\n pmid?: string;\n scopusId?: string;\n arxivId?: string;\n ericId?: string;\n year?: string;\n authors?: string;\n abstract?: string;\n finalDecision: 'include' | 'exclude';\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 * Convert ArticleEntry to exported format (without reviews)\n */\nfunction articleToExport(article: ArticleEntry): ExportedArticle {\n const exported: ExportedArticle = {\n title: article.title,\n finalDecision: article.finalDecision!,\n };\n\n if (article.doi) exported.doi = article.doi;\n if (article.pmid) exported.pmid = article.pmid;\n if (article.scopusId) exported.scopusId = article.scopusId;\n if (article.arxivId) exported.arxivId = article.arxivId;\n if (article.ericId) exported.ericId = article.ericId;\n if (article.year) exported.year = article.year;\n if (article.authors) exported.authors = article.authors;\n if (article.abstract) exported.abstract = article.abstract;\n\n return exported;\n}\n\n/**\n * Execute review export command\n */\nexport async function executeReviewExport(\n options: ReviewExportOptions,\n sessionsDir: string\n): Promise<ReviewExportResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n const reviewFile = await loadReviewFile(sessionDir);\n\n // Filter articles by final decision\n const targetDecision = options.only === 'included' ? 'include' : 'exclude';\n const filtered = reviewFile.articles.filter(\n (article) => article.finalDecision === targetDecision\n );\n\n // Convert to export format\n const exported = filtered.map(articleToExport);\n\n // Ensure output directory exists\n const outputDir = dirname(options.output);\n await mkdir(outputDir, { recursive: true });\n\n // Write output in requested format\n let content: string;\n switch (options.format) {\n case 'yaml':\n content = stringifyYaml({ articles: exported }, { lineWidth: 0 });\n break;\n case 'json':\n content = JSON.stringify({ articles: exported }, null, 2);\n break;\n case 'jsonl':\n content = exported.map((a) => JSON.stringify(a)).join('\\n');\n if (content) content += '\\n';\n break;\n }\n\n await writeFile(options.output, content, 'utf-8');\n\n return {\n outputPath: options.output,\n exportedCount: exported.length,\n format: options.format,\n };\n}\n\n/**\n * Format export result as human-readable string\n */\nexport function formatExportOutput(result: ReviewExportResult): string {\n if (result.exportedCount === 0) {\n return 'No articles matched the filter.';\n }\n\n return `Exported ${result.exportedCount} article(s) to ${result.outputPath} (${result.format})`;\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":";;;AA4CA,eAAe,eAAe,YAAyC;AACrE,QAAM,cAAc,KAAK,YAAY,aAAa,cAAc;AAChE,QAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,SAAOA,MAAU,OAAO;AAC1B;AAKA,SAAS,gBAAgB,SAAwC;AAC/D,QAAM,WAA4B;AAAA,IAChC,OAAO,QAAQ;AAAA,IACf,eAAe,QAAQ;AAAA,EAAA;AAGzB,MAAI,QAAQ,IAAK,UAAS,MAAM,QAAQ;AACxC,MAAI,QAAQ,KAAM,UAAS,OAAO,QAAQ;AAC1C,MAAI,QAAQ,SAAU,UAAS,WAAW,QAAQ;AAClD,MAAI,QAAQ,QAAS,UAAS,UAAU,QAAQ;AAChD,MAAI,QAAQ,OAAQ,UAAS,SAAS,QAAQ;AAC9C,MAAI,QAAQ,KAAM,UAAS,OAAO,QAAQ;AAC1C,MAAI,QAAQ,QAAS,UAAS,UAAU,QAAQ;AAChD,MAAI,QAAQ,SAAU,UAAS,WAAW,QAAQ;AAElD,SAAO;AACT;AAKA,eAAsB,oBACpB,SACA,aAC6B;AAC7B,QAAM,aAAa,KAAK,aAAa,QAAQ,SAAS;AACtD,QAAM,aAAa,MAAM,eAAe,UAAU;AAGlD,QAAM,iBAAiB,QAAQ,SAAS,aAAa,YAAY;AACjE,QAAM,WAAW,WAAW,SAAS;AAAA,IACnC,CAAC,YAAY,QAAQ,kBAAkB;AAAA,EAAA;AAIzC,QAAM,WAAW,SAAS,IAAI,eAAe;AAG7C,QAAM,YAAY,QAAQ,QAAQ,MAAM;AACxC,QAAM,MAAM,WAAW,EAAE,WAAW,MAAM;AAG1C,MAAI;AACJ,UAAQ,QAAQ,QAAA;AAAA,IACd,KAAK;AACH,gBAAUC,UAAc,EAAE,UAAU,SAAA,GAAY,EAAE,WAAW,GAAG;AAChE;AAAA,IACF,KAAK;AACH,gBAAU,KAAK,UAAU,EAAE,UAAU,SAAA,GAAY,MAAM,CAAC;AACxD;AAAA,IACF,KAAK;AACH,gBAAU,SAAS,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,IAAI;AAC1D,UAAI,QAAS,YAAW;AACxB;AAAA,EAAA;AAGJ,QAAM,UAAU,QAAQ,QAAQ,SAAS,OAAO;AAEhD,SAAO;AAAA,IACL,YAAY,QAAQ;AAAA,IACpB,eAAe,SAAS;AAAA,IACxB,QAAQ,QAAQ;AAAA,EAAA;AAEpB;AAKO,SAAS,mBAAmB,QAAoC;AACrE,MAAI,OAAO,kBAAkB,GAAG;AAC9B,WAAO;AAAA,EACT;AAEA,SAAO,YAAY,OAAO,aAAa,kBAAkB,OAAO,UAAU,KAAK,OAAO,MAAM;AAC9F;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReviewStatus } from './types.js';
|
|
1
|
+
import { ReviewStatus, ReviewBasis } from './types.js';
|
|
2
2
|
export type SortOption = 'year' | 'title' | 'random' | 'none';
|
|
3
3
|
export interface ReviewExtractOptions {
|
|
4
4
|
sessionId: string;
|
|
@@ -7,6 +7,10 @@ export interface ReviewExtractOptions {
|
|
|
7
7
|
seed?: number;
|
|
8
8
|
limit?: number;
|
|
9
9
|
offset?: number;
|
|
10
|
+
/** Basis for the review (title, abstract). When specified, outputs work file format. */
|
|
11
|
+
basis?: ReviewBasis;
|
|
12
|
+
/** Reviewer identifier (e.g., "ai:claude"). Required when basis is specified. */
|
|
13
|
+
reviewer?: string;
|
|
10
14
|
output: string;
|
|
11
15
|
}
|
|
12
16
|
export interface ReviewExtractResult {
|
|
@@ -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,MAAM,YAAY,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,EAAuC,MAAM,YAAY,CAAC;AAE1J,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,wFAAwF;IACxF,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAGD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAkED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,EAC7B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,mBAAmB,CAAC,CAiG9B"}
|