@ncukondo/search-hub 0.14.0 → 0.15.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/README.md +39 -9
- package/dist/cli/commands/check.d.ts +34 -0
- package/dist/cli/commands/check.d.ts.map +1 -0
- package/dist/cli/commands/check.js +126 -0
- package/dist/cli/commands/check.js.map +1 -0
- package/dist/cli/commands/export.d.ts +5 -3
- package/dist/cli/commands/export.d.ts.map +1 -1
- package/dist/cli/commands/export.js +0 -4
- package/dist/cli/commands/export.js.map +1 -1
- package/dist/cli/commands/query/init.d.ts.map +1 -1
- package/dist/cli/commands/query/init.js +17 -7
- package/dist/cli/commands/query/init.js.map +1 -1
- package/dist/cli/commands/query/inspect.d.ts +36 -0
- package/dist/cli/commands/query/inspect.d.ts.map +1 -0
- package/dist/cli/commands/query/inspect.js +155 -0
- package/dist/cli/commands/query/inspect.js.map +1 -0
- package/dist/cli/commands/query/translate.d.ts.map +1 -1
- package/dist/cli/commands/query/translate.js +3 -1
- package/dist/cli/commands/query/translate.js.map +1 -1
- package/dist/cli/commands/query-filter.d.ts +13 -0
- package/dist/cli/commands/query-filter.d.ts.map +1 -0
- package/dist/cli/commands/query-filter.js +149 -0
- package/dist/cli/commands/query-filter.js.map +1 -0
- package/dist/cli/commands/results.d.ts +3 -3
- package/dist/cli/commands/results.d.ts.map +1 -1
- package/dist/cli/commands/results.js +12 -3
- package/dist/cli/commands/results.js.map +1 -1
- package/dist/cli/commands/search-executor.d.ts.map +1 -1
- package/dist/cli/commands/search-executor.js +12 -7
- package/dist/cli/commands/search-executor.js.map +1 -1
- package/dist/cli/e2e-helpers.d.ts +5 -2
- package/dist/cli/e2e-helpers.d.ts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +228 -45
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/providers/arxiv/provider.d.ts +3 -3
- package/dist/providers/arxiv/provider.d.ts.map +1 -1
- package/dist/providers/arxiv/provider.js +3 -3
- package/dist/providers/arxiv/provider.js.map +1 -1
- package/dist/providers/arxiv/translator.d.ts +3 -3
- package/dist/providers/arxiv/translator.d.ts.map +1 -1
- package/dist/providers/arxiv/translator.js +6 -8
- package/dist/providers/arxiv/translator.js.map +1 -1
- package/dist/providers/base/index.d.ts +1 -1
- package/dist/providers/base/index.d.ts.map +1 -1
- package/dist/providers/base/mock-provider.d.ts +2 -2
- package/dist/providers/base/mock-provider.d.ts.map +1 -1
- package/dist/providers/base/mock-provider.js +2 -9
- package/dist/providers/base/mock-provider.js.map +1 -1
- package/dist/providers/base/provider.d.ts +3 -3
- package/dist/providers/base/provider.d.ts.map +1 -1
- package/dist/providers/base/provider.js.map +1 -1
- package/dist/providers/base/types.d.ts +4 -6
- package/dist/providers/base/types.d.ts.map +1 -1
- package/dist/providers/base/types.js.map +1 -1
- package/dist/providers/eric/provider.d.ts +3 -3
- package/dist/providers/eric/provider.d.ts.map +1 -1
- package/dist/providers/eric/provider.js +3 -3
- package/dist/providers/eric/provider.js.map +1 -1
- package/dist/providers/eric/translator.d.ts +5 -5
- package/dist/providers/eric/translator.d.ts.map +1 -1
- package/dist/providers/eric/translator.js +6 -7
- package/dist/providers/eric/translator.js.map +1 -1
- package/dist/providers/pubmed/provider.d.ts +3 -3
- package/dist/providers/pubmed/provider.d.ts.map +1 -1
- package/dist/providers/pubmed/provider.js +3 -3
- package/dist/providers/pubmed/provider.js.map +1 -1
- package/dist/providers/pubmed/translator.d.ts +3 -3
- package/dist/providers/pubmed/translator.d.ts.map +1 -1
- package/dist/providers/pubmed/translator.js +4 -23
- package/dist/providers/pubmed/translator.js.map +1 -1
- package/dist/providers/scopus/provider.d.ts +3 -3
- package/dist/providers/scopus/provider.d.ts.map +1 -1
- package/dist/providers/scopus/provider.js +3 -3
- package/dist/providers/scopus/provider.js.map +1 -1
- package/dist/providers/scopus/translator.d.ts +3 -3
- package/dist/providers/scopus/translator.d.ts.map +1 -1
- package/dist/providers/scopus/translator.js +7 -9
- package/dist/providers/scopus/translator.js.map +1 -1
- package/dist/query/index.d.ts +3 -2
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/json-schema.d.ts.map +1 -1
- package/dist/query/json-schema.js +20 -11
- package/dist/query/json-schema.js.map +1 -1
- package/dist/query/resolver.d.ts +14 -0
- package/dist/query/resolver.d.ts.map +1 -0
- package/dist/query/resolver.js +61 -0
- package/dist/query/resolver.js.map +1 -0
- package/dist/query/types.d.ts +31 -11
- package/dist/query/types.d.ts.map +1 -1
- package/dist/query/validator.d.ts +659 -348
- package/dist/query/validator.d.ts.map +1 -1
- package/dist/query/validator.js +70 -30
- package/dist/query/validator.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,9 +8,14 @@ A CLI tool for systematic literature searching across multiple academic database
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
10
10
|
- **Multi-database search**: PubMed, ERIC, arXiv, Scopus (Web of Science, Embase planned)
|
|
11
|
-
- **Unified query syntax**: YAML-based DSL with automatic translation
|
|
11
|
+
- **Unified query syntax**: YAML-based DSL with automatic translation and JSON Schema support
|
|
12
|
+
- **Controlled vocabulary validation**: Validates MeSH, ERIC descriptors, and Emtree terms with typo suggestions
|
|
12
13
|
- **Reproducible searches**: Full session logging for PRISMA reporting
|
|
14
|
+
- **Result filtering**: Flexible query expressions (`-q`) to search and filter results by title, abstract, author, year, and more
|
|
15
|
+
- **Coverage verification**: Check whether known articles appear in search results for query quality validation
|
|
16
|
+
- **Session comparison**: Diff results between query iterations to track refinements
|
|
13
17
|
- **Resume support**: Continue interrupted searches at DB or page level
|
|
18
|
+
- **Review workflow**: Multi-reviewer screening with agreement tracking and finalization
|
|
14
19
|
- **Fulltext management**: OA discovery, automatic retrieval, PMC XML to Markdown conversion
|
|
15
20
|
- **Reference manager integration**: Works with [reference-manager](https://github.com/ncukondo/reference-manager)
|
|
16
21
|
|
|
@@ -37,8 +42,15 @@ This creates config and data directories in platform-specific locations:
|
|
|
37
42
|
| macOS | `~/Library/Preferences/search-hub/` | `~/Library/Application Support/search-hub/` |
|
|
38
43
|
| Windows | `%APPDATA%/search-hub/Config/` | `%LOCALAPPDATA%/search-hub/Data/` |
|
|
39
44
|
|
|
40
|
-
2. Create a query file
|
|
45
|
+
2. Create a query file:
|
|
46
|
+
```bash
|
|
47
|
+
search-hub query init -o query.yaml
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This generates a YAML template with JSON Schema support for editor autocompletion. Edit it to define your search:
|
|
51
|
+
|
|
41
52
|
```yaml
|
|
53
|
+
# yaml-language-server: $schema=./query.schema.json
|
|
42
54
|
name: my_review
|
|
43
55
|
description: "Literature search for scoping review"
|
|
44
56
|
|
|
@@ -56,12 +68,19 @@ filters:
|
|
|
56
68
|
- en
|
|
57
69
|
```
|
|
58
70
|
|
|
59
|
-
3.
|
|
71
|
+
3. Validate the query:
|
|
72
|
+
```bash
|
|
73
|
+
search-hub query validate query.yaml
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This checks structure, validates controlled vocabulary terms (MeSH, ERIC descriptors, Emtree) against external APIs, and suggests corrections for typos.
|
|
77
|
+
|
|
78
|
+
4. Run search:
|
|
60
79
|
```bash
|
|
61
80
|
search-hub search query.yaml
|
|
62
81
|
```
|
|
63
82
|
|
|
64
|
-
|
|
83
|
+
5. Export results:
|
|
65
84
|
```bash
|
|
66
85
|
search-hub export <session-id> --format ids
|
|
67
86
|
```
|
|
@@ -80,20 +99,26 @@ Developing an effective search query is iterative. Start broad, then refine base
|
|
|
80
99
|
2. **Review initial results** - Check titles to assess quality:
|
|
81
100
|
```bash
|
|
82
101
|
search-hub results <session-v1> --limit 50
|
|
102
|
+
search-hub results <session-v1> -q "title:diabetes year:2023-2025"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
3. **Check coverage** - Verify known relevant articles are captured:
|
|
106
|
+
```bash
|
|
107
|
+
search-hub check <session-v1> --file known-articles.txt
|
|
83
108
|
```
|
|
84
109
|
|
|
85
|
-
|
|
110
|
+
4. **Refine the query** - Copy and modify your query file:
|
|
86
111
|
```bash
|
|
87
112
|
cp query-v1.yaml query-v2.yaml
|
|
88
113
|
# Edit query-v2.yaml to add/remove terms, adjust filters
|
|
89
114
|
```
|
|
90
115
|
|
|
91
|
-
|
|
116
|
+
5. **Run the refined search**:
|
|
92
117
|
```bash
|
|
93
118
|
search-hub search query-v2.yaml --max-results 100
|
|
94
119
|
```
|
|
95
120
|
|
|
96
|
-
|
|
121
|
+
6. **Compare results with diff** - See what changed:
|
|
97
122
|
```bash
|
|
98
123
|
search-hub diff <session-v1> <session-v2> --show removed
|
|
99
124
|
```
|
|
@@ -106,6 +131,11 @@ Developing an effective search query is iterative. Start broad, then refine base
|
|
|
106
131
|
search-hub search query.yaml --count-only
|
|
107
132
|
```
|
|
108
133
|
|
|
134
|
+
- **Use `--preview`** to see hit counts with sample titles:
|
|
135
|
+
```bash
|
|
136
|
+
search-hub search query.yaml --preview
|
|
137
|
+
```
|
|
138
|
+
|
|
109
139
|
- **Use `--dry-run`** to preview translations: See exactly what query each database will receive.
|
|
110
140
|
```bash
|
|
111
141
|
search-hub search query.yaml --dry-run
|
|
@@ -139,10 +169,10 @@ See [Fulltext Management Guide](./docs/fulltext.md) for details.
|
|
|
139
169
|
|
|
140
170
|
## Documentation
|
|
141
171
|
|
|
142
|
-
- [Query Guide](./docs/query-guide.md) - How to write query files
|
|
172
|
+
- [Query Guide](./docs/query-guide.md) - How to write query files (DSL, JSON Schema, vocabulary validation)
|
|
143
173
|
- [Command Reference](./docs/commands.md) - All CLI commands and options
|
|
144
174
|
- [Configuration](./docs/configuration.md) - Setup and configuration
|
|
145
|
-
- [Databases](./docs/databases.md) - Supported databases and tips
|
|
175
|
+
- [Databases](./docs/databases.md) - Supported databases, controlled vocabularies, and tips
|
|
146
176
|
- [Fulltext Management](./docs/fulltext.md) - Fulltext retrieval and management
|
|
147
177
|
|
|
148
178
|
## Development
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Article, ProviderName } from '../../providers/base/types.js';
|
|
2
|
+
export interface ParsedIdentifier {
|
|
3
|
+
type: 'doi' | 'pmid' | 'arxiv';
|
|
4
|
+
value: string;
|
|
5
|
+
raw: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function parseIdentifierFile(content: string): ParsedIdentifier[];
|
|
8
|
+
export interface FoundItem {
|
|
9
|
+
query: string;
|
|
10
|
+
type: ParsedIdentifier['type'];
|
|
11
|
+
sources: ProviderName[];
|
|
12
|
+
title: string;
|
|
13
|
+
}
|
|
14
|
+
export interface MissingItem {
|
|
15
|
+
query: string;
|
|
16
|
+
type: ParsedIdentifier['type'];
|
|
17
|
+
}
|
|
18
|
+
export interface CheckResult {
|
|
19
|
+
total: number;
|
|
20
|
+
foundCount: number;
|
|
21
|
+
missingCount: number;
|
|
22
|
+
coverage: number;
|
|
23
|
+
found: FoundItem[];
|
|
24
|
+
missing: MissingItem[];
|
|
25
|
+
}
|
|
26
|
+
export declare function checkCoverage(articles: Article[], identifiers: ParsedIdentifier[]): CheckResult;
|
|
27
|
+
export interface FormatCheckOptions {
|
|
28
|
+
sessionId: string;
|
|
29
|
+
source: string;
|
|
30
|
+
missingOnly?: boolean | undefined;
|
|
31
|
+
}
|
|
32
|
+
export declare function formatCheckResult(result: CheckResult, options: FormatCheckOptions): string;
|
|
33
|
+
export declare function formatCheckResultJson(result: CheckResult, options: FormatCheckOptions): string;
|
|
34
|
+
//# sourceMappingURL=check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/check.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAG3E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAavE;AAuBD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,gBAAgB,EAAE,GAAG,WAAW,CA+C/F;AASD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACnC;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,kBAAkB,GAAG,MAAM,CA0B1F;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAqB9F"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { getArticleKeys } from "./session-utils.js";
|
|
2
|
+
function parseIdentifierFile(content) {
|
|
3
|
+
const results = [];
|
|
4
|
+
const lines = content.split("\n");
|
|
5
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6
|
+
const trimmed = lines[i].trim();
|
|
7
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
8
|
+
const parsed = parseLine(trimmed, i + 1);
|
|
9
|
+
results.push(parsed);
|
|
10
|
+
}
|
|
11
|
+
return results;
|
|
12
|
+
}
|
|
13
|
+
function parseLine(line, lineNumber) {
|
|
14
|
+
const prefixMatch = line.match(/^(doi|pmid|arxiv):(.+)$/i);
|
|
15
|
+
if (prefixMatch) {
|
|
16
|
+
const prefix = prefixMatch[1].toLowerCase();
|
|
17
|
+
return { type: prefix, value: prefixMatch[2].trim(), raw: line };
|
|
18
|
+
}
|
|
19
|
+
if (line.startsWith("10.")) {
|
|
20
|
+
return { type: "doi", value: line, raw: line };
|
|
21
|
+
}
|
|
22
|
+
if (/^\d+$/.test(line)) {
|
|
23
|
+
return { type: "pmid", value: line, raw: line };
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`Unrecognizable identifier at line ${lineNumber}: ${line}`);
|
|
26
|
+
}
|
|
27
|
+
function checkCoverage(articles, identifiers) {
|
|
28
|
+
if (identifiers.length === 0) {
|
|
29
|
+
return { total: 0, foundCount: 0, missingCount: 0, coverage: 0, found: [], missing: [] };
|
|
30
|
+
}
|
|
31
|
+
const keyToArticles = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const article of articles) {
|
|
33
|
+
for (const key of getArticleKeys(article)) {
|
|
34
|
+
const existing = keyToArticles.get(key);
|
|
35
|
+
if (existing) {
|
|
36
|
+
existing.push(article);
|
|
37
|
+
} else {
|
|
38
|
+
keyToArticles.set(key, [article]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const found = [];
|
|
43
|
+
const missing = [];
|
|
44
|
+
for (const id of identifiers) {
|
|
45
|
+
const key = identifierToKey(id);
|
|
46
|
+
const matched = keyToArticles.get(key);
|
|
47
|
+
if (matched && matched.length > 0) {
|
|
48
|
+
const sources = [...new Set(matched.map((a) => a.source))];
|
|
49
|
+
found.push({
|
|
50
|
+
query: id.raw,
|
|
51
|
+
type: id.type,
|
|
52
|
+
sources,
|
|
53
|
+
title: matched[0].title
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
missing.push({ query: id.raw, type: id.type });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const total = identifiers.length;
|
|
60
|
+
return {
|
|
61
|
+
total,
|
|
62
|
+
foundCount: found.length,
|
|
63
|
+
missingCount: missing.length,
|
|
64
|
+
coverage: found.length / total,
|
|
65
|
+
found,
|
|
66
|
+
missing
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function identifierToKey(id) {
|
|
70
|
+
if (id.type === "doi") {
|
|
71
|
+
return `doi:${id.value.toLowerCase()}`;
|
|
72
|
+
}
|
|
73
|
+
return `${id.type}:${id.value}`;
|
|
74
|
+
}
|
|
75
|
+
function formatCheckResult(result, options) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
const pct = result.total > 0 ? (result.coverage * 100).toFixed(1) : "0.0";
|
|
78
|
+
lines.push(`Coverage: ${options.sessionId}`);
|
|
79
|
+
lines.push(`Source: ${options.source} (${result.total} identifiers)`);
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push(`Found: ${result.foundCount}/${result.total} (${pct}%)`);
|
|
82
|
+
if (result.missing.length > 0) {
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push(`Missing (${result.missingCount}):`);
|
|
85
|
+
for (const m of result.missing) {
|
|
86
|
+
lines.push(` ${m.query}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!options.missingOnly && result.found.length > 0) {
|
|
90
|
+
lines.push("");
|
|
91
|
+
lines.push(`Found (${result.foundCount}):`);
|
|
92
|
+
for (const f of result.found) {
|
|
93
|
+
lines.push(` ${f.query} → ${f.title} (${f.sources.join(", ")})`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return lines.join("\n");
|
|
97
|
+
}
|
|
98
|
+
function formatCheckResultJson(result, options) {
|
|
99
|
+
return JSON.stringify({
|
|
100
|
+
session: options.sessionId,
|
|
101
|
+
source: options.source,
|
|
102
|
+
total: result.total,
|
|
103
|
+
found: result.foundCount,
|
|
104
|
+
missing: result.missingCount,
|
|
105
|
+
coverage: result.coverage,
|
|
106
|
+
details: {
|
|
107
|
+
found: result.found.map((f) => ({
|
|
108
|
+
query: f.query,
|
|
109
|
+
type: f.type,
|
|
110
|
+
sources: f.sources,
|
|
111
|
+
title: f.title
|
|
112
|
+
})),
|
|
113
|
+
missing: result.missing.map((m) => ({
|
|
114
|
+
query: m.query,
|
|
115
|
+
type: m.type
|
|
116
|
+
}))
|
|
117
|
+
}
|
|
118
|
+
}, null, 2);
|
|
119
|
+
}
|
|
120
|
+
export {
|
|
121
|
+
checkCoverage,
|
|
122
|
+
formatCheckResult,
|
|
123
|
+
formatCheckResultJson,
|
|
124
|
+
parseIdentifierFile
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.js","sources":["../../../src/cli/commands/check.ts"],"sourcesContent":["/**\n * Coverage check command - verifies known articles are present in session results.\n */\n\nimport type { Article, ProviderName } from '../../providers/base/types.js';\nimport { getArticleKeys } from './session-utils.js';\n\nexport interface ParsedIdentifier {\n type: 'doi' | 'pmid' | 'arxiv';\n value: string;\n raw: string;\n}\n\nexport function parseIdentifierFile(content: string): ParsedIdentifier[] {\n const results: ParsedIdentifier[] = [];\n const lines = content.split('\\n');\n\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim();\n if (trimmed === '' || trimmed.startsWith('#')) continue;\n\n const parsed = parseLine(trimmed, i + 1);\n results.push(parsed);\n }\n\n return results;\n}\n\nfunction parseLine(line: string, lineNumber: number): ParsedIdentifier {\n // Check for explicit prefix (case-insensitive)\n const prefixMatch = line.match(/^(doi|pmid|arxiv):(.+)$/i);\n if (prefixMatch) {\n const prefix = prefixMatch[1]!.toLowerCase() as 'doi' | 'pmid' | 'arxiv';\n return { type: prefix, value: prefixMatch[2]!.trim(), raw: line };\n }\n\n // Auto-detect: starts with \"10.\" → DOI\n if (line.startsWith('10.')) {\n return { type: 'doi', value: line, raw: line };\n }\n\n // Auto-detect: all digits → PMID\n if (/^\\d+$/.test(line)) {\n return { type: 'pmid', value: line, raw: line };\n }\n\n throw new Error(`Unrecognizable identifier at line ${lineNumber}: ${line}`);\n}\n\nexport interface FoundItem {\n query: string;\n type: ParsedIdentifier['type'];\n sources: ProviderName[];\n title: string;\n}\n\nexport interface MissingItem {\n query: string;\n type: ParsedIdentifier['type'];\n}\n\nexport interface CheckResult {\n total: number;\n foundCount: number;\n missingCount: number;\n coverage: number;\n found: FoundItem[];\n missing: MissingItem[];\n}\n\nexport function checkCoverage(articles: Article[], identifiers: ParsedIdentifier[]): CheckResult {\n if (identifiers.length === 0) {\n return { total: 0, foundCount: 0, missingCount: 0, coverage: 0, found: [], missing: [] };\n }\n\n // Build a lookup: key → list of articles with that key\n const keyToArticles = new Map<string, Article[]>();\n for (const article of articles) {\n for (const key of getArticleKeys(article)) {\n const existing = keyToArticles.get(key);\n if (existing) {\n existing.push(article);\n } else {\n keyToArticles.set(key, [article]);\n }\n }\n }\n\n const found: FoundItem[] = [];\n const missing: MissingItem[] = [];\n\n for (const id of identifiers) {\n const key = identifierToKey(id);\n const matched = keyToArticles.get(key);\n\n if (matched && matched.length > 0) {\n const sources = [...new Set(matched.map(a => a.source))];\n found.push({\n query: id.raw,\n type: id.type,\n sources,\n title: matched[0]!.title,\n });\n } else {\n missing.push({ query: id.raw, type: id.type });\n }\n }\n\n const total = identifiers.length;\n return {\n total,\n foundCount: found.length,\n missingCount: missing.length,\n coverage: found.length / total,\n found,\n missing,\n };\n}\n\nfunction identifierToKey(id: ParsedIdentifier): string {\n if (id.type === 'doi') {\n return `doi:${id.value.toLowerCase()}`;\n }\n return `${id.type}:${id.value}`;\n}\n\nexport interface FormatCheckOptions {\n sessionId: string;\n source: string;\n missingOnly?: boolean | undefined;\n}\n\nexport function formatCheckResult(result: CheckResult, options: FormatCheckOptions): string {\n const lines: string[] = [];\n const pct = result.total > 0 ? (result.coverage * 100).toFixed(1) : '0.0';\n\n lines.push(`Coverage: ${options.sessionId}`);\n lines.push(`Source: ${options.source} (${result.total} identifiers)`);\n lines.push('');\n lines.push(`Found: ${result.foundCount}/${result.total} (${pct}%)`);\n\n if (result.missing.length > 0) {\n lines.push('');\n lines.push(`Missing (${result.missingCount}):`);\n for (const m of result.missing) {\n lines.push(` ${m.query}`);\n }\n }\n\n if (!options.missingOnly && result.found.length > 0) {\n lines.push('');\n lines.push(`Found (${result.foundCount}):`);\n for (const f of result.found) {\n lines.push(` ${f.query} → ${f.title} (${f.sources.join(', ')})`);\n }\n }\n\n return lines.join('\\n');\n}\n\nexport function formatCheckResultJson(result: CheckResult, options: FormatCheckOptions): string {\n return JSON.stringify({\n session: options.sessionId,\n source: options.source,\n total: result.total,\n found: result.foundCount,\n missing: result.missingCount,\n coverage: result.coverage,\n details: {\n found: result.found.map(f => ({\n query: f.query,\n type: f.type,\n sources: f.sources,\n title: f.title,\n })),\n missing: result.missing.map(m => ({\n query: m.query,\n type: m.type,\n })),\n },\n }, null, 2);\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Article } from '../../providers/base/types.js';
|
|
2
2
|
export type ExportFormat = 'ids' | 'json' | 'jsonl' | 'csl-json';
|
|
3
3
|
export type IdType = 'doi' | 'pmid' | 'all';
|
|
4
4
|
export interface ExportFilter {
|
|
@@ -11,14 +11,16 @@ export interface ExportCommandOptions {
|
|
|
11
11
|
sessionId: string;
|
|
12
12
|
format: ExportFormat;
|
|
13
13
|
outputPath?: string;
|
|
14
|
-
providers?: ProviderName[];
|
|
15
14
|
idType?: IdType;
|
|
16
15
|
}
|
|
17
16
|
export interface CommandLineOptions {
|
|
18
17
|
format?: string | undefined;
|
|
19
18
|
output?: string | undefined;
|
|
20
|
-
db?: string | undefined;
|
|
21
19
|
idType?: string | undefined;
|
|
20
|
+
query?: string | undefined;
|
|
21
|
+
filterYear?: string | undefined;
|
|
22
|
+
filterTitle?: string | undefined;
|
|
23
|
+
filterAbstract?: string | undefined;
|
|
22
24
|
}
|
|
23
25
|
export interface ValidationResult {
|
|
24
26
|
valid: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/export.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/export.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAI7D,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;AACjE,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC;AAE5C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kBAAkB,GAC1B,oBAAoB,CAetB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,gBAAgB,CAgCnF;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CA+BrE;AAeD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,QAAQ,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAuBrF;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAKvD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAGzD;AAkBD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,mBAAmB,CAiD5E;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,EAAE,CAmCnF"}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { parseProviderNames } from "../utils/validation.js";
|
|
2
1
|
import { articlesToCslJson } from "../../integration/csl-json.js";
|
|
3
2
|
import { getArticleKeys } from "./session-utils.js";
|
|
4
3
|
const VALID_FORMATS = ["ids", "json", "jsonl", "csl-json"];
|
|
@@ -11,9 +10,6 @@ function parseExportOptions(sessionId, options) {
|
|
|
11
10
|
if (options.output) {
|
|
12
11
|
result.outputPath = options.output;
|
|
13
12
|
}
|
|
14
|
-
if (options.db) {
|
|
15
|
-
result.providers = parseProviderNames(options.db);
|
|
16
|
-
}
|
|
17
13
|
if (options.idType) {
|
|
18
14
|
result.idType = options.idType;
|
|
19
15
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export.js","sources":["../../../src/cli/commands/export.ts"],"sourcesContent":["import type { ProviderName } from '../../providers/base/types.js';\nimport type { Article } from '../../providers/base/types.js';\nimport { parseProviderNames } from '../utils/validation.js';\nimport { articlesToCslJson } from '../../integration/csl-json.js';\nimport { getArticleKeys } from './session-utils.js';\n\nexport type ExportFormat = 'ids' | 'json' | 'jsonl' | 'csl-json';\nexport type IdType = 'doi' | 'pmid' | 'all';\n\nexport interface ExportFilter {\n yearFrom?: number;\n yearTo?: number;\n titleKeywords?: string[];\n abstractKeywords?: string[];\n}\n\nexport interface ExportCommandOptions {\n sessionId: string;\n format: ExportFormat;\n outputPath?: string;\n providers?: ProviderName[];\n idType?: IdType;\n}\n\nexport interface CommandLineOptions {\n format?: string | undefined;\n output?: string | undefined;\n db?: string | undefined;\n idType?: string | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\nconst VALID_FORMATS: ExportFormat[] = ['ids', 'json', 'jsonl', 'csl-json'];\nconst VALID_ID_TYPES: IdType[] = ['doi', 'pmid', 'all'];\n\nexport function parseExportOptions(\n sessionId: string,\n options: CommandLineOptions\n): ExportCommandOptions {\n const result: ExportCommandOptions = {\n sessionId,\n format: (options.format as ExportFormat) || 'jsonl',\n };\n\n if (options.output) {\n result.outputPath = options.output;\n }\n\n if (options.db) {\n result.providers = parseProviderNames(options.db);\n }\n\n if (options.idType) {\n result.idType = options.idType as IdType;\n }\n\n return result;\n}\n\nexport function validateExportInput(options: ExportCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n if (!VALID_FORMATS.includes(options.format)) {\n return {\n valid: false,\n error: `Invalid format: ${options.format}. Valid formats are: ${VALID_FORMATS.join(', ')}`,\n };\n }\n\n if (options.idType) {\n if (!VALID_ID_TYPES.includes(options.idType)) {\n return {\n valid: false,\n error: `Invalid id-type: ${options.idType}. Valid types are: ${VALID_ID_TYPES.join(', ')}`,\n };\n }\n\n if (options.format !== 'ids') {\n return {\n valid: false,\n error: '--id-type can only be used with --format ids',\n };\n }\n }\n\n return { valid: true };\n}\n\nexport function formatIds(articles: Article[], idType: IdType): string {\n if (idType === 'all') {\n const groups: string[] = [];\n for (const article of articles) {\n const ids: string[] = [];\n if (article.pmid) ids.push(`pmid:${article.pmid}`);\n if (article.doi) ids.push(`doi:${article.doi}`);\n if (article.arxivId) ids.push(`arxiv:${article.arxivId}`);\n if (article.scopusId) ids.push(`scopus:${article.scopusId}`);\n if (article.ericId) ids.push(`eric:${article.ericId}`);\n if (ids.length > 0) {\n groups.push(ids.join('\\n'));\n }\n }\n return groups.join('\\n\\n');\n }\n\n const ids: string[] = [];\n for (const article of articles) {\n if (idType === 'doi') {\n if (article.doi) {\n ids.push(article.doi);\n }\n } else if (idType === 'pmid') {\n if (article.pmid) {\n ids.push(article.pmid);\n }\n }\n }\n\n return ids.join('\\n');\n}\n\nfunction extractYear(publicationDate: string | undefined): number | null {\n if (!publicationDate) return null;\n const year = parseInt(publicationDate.substring(0, 4), 10);\n return Number.isNaN(year) ? null : year;\n}\n\nfunction addYearField(articles: Article[]): (Article & { year: number | null })[] {\n return articles.map((article) => ({\n ...article,\n year: extractYear(article.publicationDate),\n }));\n}\n\nexport interface JsonExportMetadata {\n sessionId: string;\n sessionName: string;\n createdAt: string;\n databases: Record<string, number>;\n}\n\nexport function formatJson(articles: Article[], metadata?: JsonExportMetadata): string {\n const articlesWithYear = addYearField(articles);\n\n if (!metadata) {\n return JSON.stringify(articlesWithYear, null, 2);\n }\n\n return JSON.stringify(\n {\n session: {\n id: metadata.sessionId,\n name: metadata.sessionName,\n createdAt: metadata.createdAt,\n },\n summary: {\n totalResults: articles.length,\n databases: metadata.databases,\n },\n results: articlesWithYear,\n },\n null,\n 2\n );\n}\n\nexport function formatJsonl(articles: Article[]): string {\n if (articles.length === 0) {\n return '';\n }\n return addYearField(articles).map((article) => JSON.stringify(article)).join('\\n');\n}\n\nexport function formatCslJson(articles: Article[]): string {\n const cslItems = articlesToCslJson(articles);\n return JSON.stringify(cslItems, null, 2);\n}\n\n\nconst METADATA_FIELDS: (keyof Article)[] = [\n 'doi', 'pmid', 'arxivId', 'scopusId', 'ericId',\n 'abstract', 'publicationDate', 'journal', 'volume', 'issue', 'pages',\n];\n\nfunction countMetadataFields(article: Article): number {\n let count = 0;\n for (const field of METADATA_FIELDS) {\n if (article[field] !== undefined && article[field] !== '') {\n count++;\n }\n }\n return count;\n}\n\nexport interface DeduplicationResult {\n articles: Article[];\n duplicatesRemoved: number;\n}\n\nexport function deduplicateArticles(articles: Article[]): DeduplicationResult {\n // Map from identifier key to index in the unique array\n const keyToIndex = new Map<string, number>();\n const unique: Article[] = [];\n let duplicatesRemoved = 0;\n\n for (const article of articles) {\n const keys = getArticleKeys(article);\n\n if (keys.length === 0) {\n // No identifiers - cannot deduplicate, keep the article\n unique.push(article);\n continue;\n }\n\n // Check if any identifier has been seen before\n let existingIndex: number | undefined;\n for (const key of keys) {\n const idx = keyToIndex.get(key);\n if (idx !== undefined) {\n existingIndex = idx;\n break;\n }\n }\n\n if (existingIndex !== undefined) {\n // Duplicate found - compare metadata richness\n const existing = unique[existingIndex]!;\n if (countMetadataFields(article) > countMetadataFields(existing)) {\n // Replace with the richer record\n unique[existingIndex] = article;\n // Update all keys to point to the same index\n const newKeys = getArticleKeys(article);\n for (const key of newKeys) {\n keyToIndex.set(key, existingIndex);\n }\n }\n duplicatesRemoved++;\n } else {\n const index = unique.length;\n unique.push(article);\n // Map all identifiers to this index\n for (const key of keys) {\n keyToIndex.set(key, index);\n }\n }\n }\n\n return { articles: unique, duplicatesRemoved };\n}\n\nexport function filterArticles(articles: Article[], filter: ExportFilter): Article[] {\n const hasYearFilter = filter.yearFrom !== undefined || filter.yearTo !== undefined;\n const hasTitleFilter = filter.titleKeywords !== undefined && filter.titleKeywords.length > 0;\n const hasAbstractFilter = filter.abstractKeywords !== undefined && filter.abstractKeywords.length > 0;\n\n if (!hasYearFilter && !hasTitleFilter && !hasAbstractFilter) {\n return articles;\n }\n\n return articles.filter((article) => {\n // Year filter (AND with other filters)\n if (hasYearFilter) {\n const year = extractYear(article.publicationDate);\n if (year === null) return false;\n if (filter.yearFrom !== undefined && year < filter.yearFrom) return false;\n if (filter.yearTo !== undefined && year > filter.yearTo) return false;\n }\n\n // Title keyword filter (OR within keywords, AND with other filters)\n if (hasTitleFilter) {\n const titleLower = article.title.toLowerCase();\n const matched = filter.titleKeywords!.some((kw) => titleLower.includes(kw.toLowerCase()));\n if (!matched) return false;\n }\n\n // Abstract keyword filter (OR within keywords, AND with other filters)\n if (hasAbstractFilter) {\n if (!article.abstract) return false;\n const abstractLower = article.abstract.toLowerCase();\n const matched = filter.abstractKeywords!.some((kw) => abstractLower.includes(kw.toLowerCase()));\n if (!matched) return false;\n }\n\n return true;\n });\n}\n"],"names":["ids"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"export.js","sources":["../../../src/cli/commands/export.ts"],"sourcesContent":["import type { Article } from '../../providers/base/types.js';\nimport { articlesToCslJson } from '../../integration/csl-json.js';\nimport { getArticleKeys } from './session-utils.js';\n\nexport type ExportFormat = 'ids' | 'json' | 'jsonl' | 'csl-json';\nexport type IdType = 'doi' | 'pmid' | 'all';\n\nexport interface ExportFilter {\n yearFrom?: number;\n yearTo?: number;\n titleKeywords?: string[];\n abstractKeywords?: string[];\n}\n\nexport interface ExportCommandOptions {\n sessionId: string;\n format: ExportFormat;\n outputPath?: string;\n idType?: IdType;\n}\n\nexport interface CommandLineOptions {\n format?: string | undefined;\n output?: string | undefined;\n idType?: string | undefined;\n query?: string | undefined;\n filterYear?: string | undefined;\n filterTitle?: string | undefined;\n filterAbstract?: string | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\nconst VALID_FORMATS: ExportFormat[] = ['ids', 'json', 'jsonl', 'csl-json'];\nconst VALID_ID_TYPES: IdType[] = ['doi', 'pmid', 'all'];\n\nexport function parseExportOptions(\n sessionId: string,\n options: CommandLineOptions\n): ExportCommandOptions {\n const result: ExportCommandOptions = {\n sessionId,\n format: (options.format as ExportFormat) || 'jsonl',\n };\n\n if (options.output) {\n result.outputPath = options.output;\n }\n\n if (options.idType) {\n result.idType = options.idType as IdType;\n }\n\n return result;\n}\n\nexport function validateExportInput(options: ExportCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n if (!VALID_FORMATS.includes(options.format)) {\n return {\n valid: false,\n error: `Invalid format: ${options.format}. Valid formats are: ${VALID_FORMATS.join(', ')}`,\n };\n }\n\n if (options.idType) {\n if (!VALID_ID_TYPES.includes(options.idType)) {\n return {\n valid: false,\n error: `Invalid id-type: ${options.idType}. Valid types are: ${VALID_ID_TYPES.join(', ')}`,\n };\n }\n\n if (options.format !== 'ids') {\n return {\n valid: false,\n error: '--id-type can only be used with --format ids',\n };\n }\n }\n\n return { valid: true };\n}\n\nexport function formatIds(articles: Article[], idType: IdType): string {\n if (idType === 'all') {\n const groups: string[] = [];\n for (const article of articles) {\n const ids: string[] = [];\n if (article.pmid) ids.push(`pmid:${article.pmid}`);\n if (article.doi) ids.push(`doi:${article.doi}`);\n if (article.arxivId) ids.push(`arxiv:${article.arxivId}`);\n if (article.scopusId) ids.push(`scopus:${article.scopusId}`);\n if (article.ericId) ids.push(`eric:${article.ericId}`);\n if (ids.length > 0) {\n groups.push(ids.join('\\n'));\n }\n }\n return groups.join('\\n\\n');\n }\n\n const ids: string[] = [];\n for (const article of articles) {\n if (idType === 'doi') {\n if (article.doi) {\n ids.push(article.doi);\n }\n } else if (idType === 'pmid') {\n if (article.pmid) {\n ids.push(article.pmid);\n }\n }\n }\n\n return ids.join('\\n');\n}\n\nfunction extractYear(publicationDate: string | undefined): number | null {\n if (!publicationDate) return null;\n const year = parseInt(publicationDate.substring(0, 4), 10);\n return Number.isNaN(year) ? null : year;\n}\n\nfunction addYearField(articles: Article[]): (Article & { year: number | null })[] {\n return articles.map((article) => ({\n ...article,\n year: extractYear(article.publicationDate),\n }));\n}\n\nexport interface JsonExportMetadata {\n sessionId: string;\n sessionName: string;\n createdAt: string;\n databases: Record<string, number>;\n}\n\nexport function formatJson(articles: Article[], metadata?: JsonExportMetadata): string {\n const articlesWithYear = addYearField(articles);\n\n if (!metadata) {\n return JSON.stringify(articlesWithYear, null, 2);\n }\n\n return JSON.stringify(\n {\n session: {\n id: metadata.sessionId,\n name: metadata.sessionName,\n createdAt: metadata.createdAt,\n },\n summary: {\n totalResults: articles.length,\n databases: metadata.databases,\n },\n results: articlesWithYear,\n },\n null,\n 2\n );\n}\n\nexport function formatJsonl(articles: Article[]): string {\n if (articles.length === 0) {\n return '';\n }\n return addYearField(articles).map((article) => JSON.stringify(article)).join('\\n');\n}\n\nexport function formatCslJson(articles: Article[]): string {\n const cslItems = articlesToCslJson(articles);\n return JSON.stringify(cslItems, null, 2);\n}\n\n\nconst METADATA_FIELDS: (keyof Article)[] = [\n 'doi', 'pmid', 'arxivId', 'scopusId', 'ericId',\n 'abstract', 'publicationDate', 'journal', 'volume', 'issue', 'pages',\n];\n\nfunction countMetadataFields(article: Article): number {\n let count = 0;\n for (const field of METADATA_FIELDS) {\n if (article[field] !== undefined && article[field] !== '') {\n count++;\n }\n }\n return count;\n}\n\nexport interface DeduplicationResult {\n articles: Article[];\n duplicatesRemoved: number;\n}\n\nexport function deduplicateArticles(articles: Article[]): DeduplicationResult {\n // Map from identifier key to index in the unique array\n const keyToIndex = new Map<string, number>();\n const unique: Article[] = [];\n let duplicatesRemoved = 0;\n\n for (const article of articles) {\n const keys = getArticleKeys(article);\n\n if (keys.length === 0) {\n // No identifiers - cannot deduplicate, keep the article\n unique.push(article);\n continue;\n }\n\n // Check if any identifier has been seen before\n let existingIndex: number | undefined;\n for (const key of keys) {\n const idx = keyToIndex.get(key);\n if (idx !== undefined) {\n existingIndex = idx;\n break;\n }\n }\n\n if (existingIndex !== undefined) {\n // Duplicate found - compare metadata richness\n const existing = unique[existingIndex]!;\n if (countMetadataFields(article) > countMetadataFields(existing)) {\n // Replace with the richer record\n unique[existingIndex] = article;\n // Update all keys to point to the same index\n const newKeys = getArticleKeys(article);\n for (const key of newKeys) {\n keyToIndex.set(key, existingIndex);\n }\n }\n duplicatesRemoved++;\n } else {\n const index = unique.length;\n unique.push(article);\n // Map all identifiers to this index\n for (const key of keys) {\n keyToIndex.set(key, index);\n }\n }\n }\n\n return { articles: unique, duplicatesRemoved };\n}\n\nexport function filterArticles(articles: Article[], filter: ExportFilter): Article[] {\n const hasYearFilter = filter.yearFrom !== undefined || filter.yearTo !== undefined;\n const hasTitleFilter = filter.titleKeywords !== undefined && filter.titleKeywords.length > 0;\n const hasAbstractFilter = filter.abstractKeywords !== undefined && filter.abstractKeywords.length > 0;\n\n if (!hasYearFilter && !hasTitleFilter && !hasAbstractFilter) {\n return articles;\n }\n\n return articles.filter((article) => {\n // Year filter (AND with other filters)\n if (hasYearFilter) {\n const year = extractYear(article.publicationDate);\n if (year === null) return false;\n if (filter.yearFrom !== undefined && year < filter.yearFrom) return false;\n if (filter.yearTo !== undefined && year > filter.yearTo) return false;\n }\n\n // Title keyword filter (OR within keywords, AND with other filters)\n if (hasTitleFilter) {\n const titleLower = article.title.toLowerCase();\n const matched = filter.titleKeywords!.some((kw) => titleLower.includes(kw.toLowerCase()));\n if (!matched) return false;\n }\n\n // Abstract keyword filter (OR within keywords, AND with other filters)\n if (hasAbstractFilter) {\n if (!article.abstract) return false;\n const abstractLower = article.abstract.toLowerCase();\n const matched = filter.abstractKeywords!.some((kw) => abstractLower.includes(kw.toLowerCase()));\n if (!matched) return false;\n }\n\n return true;\n });\n}\n"],"names":["ids"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AA2DzD;;;;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,CAkCjD"}
|
|
@@ -7,7 +7,8 @@ name: my_search
|
|
|
7
7
|
description: ""
|
|
8
8
|
|
|
9
9
|
query:
|
|
10
|
-
-
|
|
10
|
+
- id: concept-1 # Unique block identifier (for provider replacements)
|
|
11
|
+
field: title_abstract # title, abstract, title_abstract, author, keyword, all
|
|
11
12
|
terms:
|
|
12
13
|
keywords:
|
|
13
14
|
- "search term 1"
|
|
@@ -24,7 +25,8 @@ query:
|
|
|
24
25
|
operator: OR # How to combine terms within this block
|
|
25
26
|
|
|
26
27
|
# Add more blocks — blocks are AND'd together
|
|
27
|
-
# -
|
|
28
|
+
# - id: concept-2
|
|
29
|
+
# field: title_abstract
|
|
28
30
|
# terms:
|
|
29
31
|
# keywords:
|
|
30
32
|
# - "another term"
|
|
@@ -40,12 +42,20 @@ query:
|
|
|
40
42
|
# - "Review"
|
|
41
43
|
# - "Comment"
|
|
42
44
|
|
|
43
|
-
#
|
|
45
|
+
# providers: # Optional: per-database block replacements & filter additions
|
|
44
46
|
# pubmed:
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
47
|
+
# replaces:
|
|
48
|
+
# concept-1: # Replace block by id
|
|
49
|
+
# field: keyword
|
|
50
|
+
# terms:
|
|
51
|
+
# mesh:
|
|
52
|
+
# - "MeSH Heading"
|
|
53
|
+
# operator: OR
|
|
54
|
+
# adds:
|
|
55
|
+
# filters:
|
|
56
|
+
# publication_types:
|
|
57
|
+
# exclude:
|
|
58
|
+
# - "Letter"
|
|
49
59
|
`;
|
|
50
60
|
function generateQueryTemplate() {
|
|
51
61
|
return QUERY_TEMPLATE;
|
|
@@ -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\";\nimport { dirname, join } from \"node:path\";\nimport { generateQueryJSONSchema } from \"../../../query/json-schema.js\";\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 */\nexport const QUERY_SCHEMA_FILENAME = \"query.schema.json\";\n\n// prettier-ignore\nconst QUERY_TEMPLATE =\n `# yaml-language-server: $schema=./${QUERY_SCHEMA_FILENAME}\\n` +\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 \"#
|
|
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\";\nimport { dirname, join } from \"node:path\";\nimport { generateQueryJSONSchema } from \"../../../query/json-schema.js\";\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 */\nexport const QUERY_SCHEMA_FILENAME = \"query.schema.json\";\n\n// prettier-ignore\nconst QUERY_TEMPLATE =\n `# yaml-language-server: $schema=./${QUERY_SCHEMA_FILENAME}\\n` +\n \"name: my_search\\n\" +\n \"description: \\\"\\\"\\n\" +\n \"\\n\" +\n \"query:\\n\" +\n \" - id: concept-1 # Unique block identifier (for provider replacements)\\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 \" # - id: concept-2\\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 \"# providers: # Optional: per-database block replacements & filter additions\\n\" +\n \"# pubmed:\\n\" +\n \"# replaces:\\n\" +\n \"# concept-1: # Replace block by id\\n\" +\n \"# field: keyword\\n\" +\n \"# terms:\\n\" +\n \"# mesh:\\n\" +\n \"# - \\\"MeSH Heading\\\"\\n\" +\n \"# operator: OR\\n\" +\n \"# adds:\\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\n // Generate JSON Schema file alongside output\n const schemaPath = join(dirname(options.output), QUERY_SCHEMA_FILENAME);\n const jsonSchema = generateQueryJSONSchema();\n await fsWriteFile(schemaPath, JSON.stringify(jsonSchema, null, 2) + \"\\n\", \"utf-8\");\n\n return {\n success: true,\n message: `Template written to ${options.output}`,\n };\n}\n"],"names":["fsWriteFile"],"mappings":";;;AAaO,MAAM,wBAAwB;AAGrC,MAAM,iBACJ,qCAAqC,qBAAqB;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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4DrD,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;AAGnD,QAAM,aAAa,KAAK,QAAQ,QAAQ,MAAM,GAAG,qBAAqB;AACtE,QAAM,aAAa,wBAAA;AACnB,QAAMA,UAAY,YAAY,KAAK,UAAU,YAAY,MAAM,CAAC,IAAI,MAAM,OAAO;AAEjF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS,uBAAuB,QAAQ,MAAM;AAAA,EAAA;AAElD;"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { QueryAST } from '../../../query/types.js';
|
|
2
|
+
import { ProviderName } from '../../../providers/base/types.js';
|
|
3
|
+
export interface BlockInspectRow {
|
|
4
|
+
id: string;
|
|
5
|
+
status: Partial<Record<ProviderName, 'default' | 'replaced'>>;
|
|
6
|
+
}
|
|
7
|
+
export interface FilterInspectRow {
|
|
8
|
+
filterKey: string;
|
|
9
|
+
values: Partial<Record<ProviderName, string>>;
|
|
10
|
+
}
|
|
11
|
+
export interface InspectResult {
|
|
12
|
+
name: string;
|
|
13
|
+
providers: ProviderName[];
|
|
14
|
+
blocks: BlockInspectRow[];
|
|
15
|
+
addedFilters: FilterInspectRow[];
|
|
16
|
+
}
|
|
17
|
+
export interface InspectCommandResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
result?: InspectResult;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Inspect a QueryAST to determine block resolution and filter additions per provider.
|
|
24
|
+
*/
|
|
25
|
+
export declare function inspectQuery(ast: QueryAST, enabledProviders: ProviderName[]): InspectResult;
|
|
26
|
+
/**
|
|
27
|
+
* Format InspectResult as an aligned table string.
|
|
28
|
+
*/
|
|
29
|
+
export declare function formatInspectOutput(result: InspectResult): string;
|
|
30
|
+
/**
|
|
31
|
+
* Run the inspect command on a query file.
|
|
32
|
+
*/
|
|
33
|
+
export declare function inspectQueryCommand(filePath: string, options?: {
|
|
34
|
+
providers?: ProviderName[];
|
|
35
|
+
}): Promise<InspectCommandResult>;
|
|
36
|
+
//# sourceMappingURL=inspect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/inspect.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAW,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAYrE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,YAAY,EAAE,gBAAgB,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAYD;;GAEG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,QAAQ,EACb,gBAAgB,EAAE,YAAY,EAAE,GAC/B,aAAa,CA4Cf;AAiCD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CA+BjE;AAgDD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,YAAY,EAAE,CAAA;CAAO,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAuB/B"}
|