@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.
Files changed (97) hide show
  1. package/README.md +39 -9
  2. package/dist/cli/commands/check.d.ts +34 -0
  3. package/dist/cli/commands/check.d.ts.map +1 -0
  4. package/dist/cli/commands/check.js +126 -0
  5. package/dist/cli/commands/check.js.map +1 -0
  6. package/dist/cli/commands/export.d.ts +5 -3
  7. package/dist/cli/commands/export.d.ts.map +1 -1
  8. package/dist/cli/commands/export.js +0 -4
  9. package/dist/cli/commands/export.js.map +1 -1
  10. package/dist/cli/commands/query/init.d.ts.map +1 -1
  11. package/dist/cli/commands/query/init.js +17 -7
  12. package/dist/cli/commands/query/init.js.map +1 -1
  13. package/dist/cli/commands/query/inspect.d.ts +36 -0
  14. package/dist/cli/commands/query/inspect.d.ts.map +1 -0
  15. package/dist/cli/commands/query/inspect.js +155 -0
  16. package/dist/cli/commands/query/inspect.js.map +1 -0
  17. package/dist/cli/commands/query/translate.d.ts.map +1 -1
  18. package/dist/cli/commands/query/translate.js +3 -1
  19. package/dist/cli/commands/query/translate.js.map +1 -1
  20. package/dist/cli/commands/query-filter.d.ts +13 -0
  21. package/dist/cli/commands/query-filter.d.ts.map +1 -0
  22. package/dist/cli/commands/query-filter.js +149 -0
  23. package/dist/cli/commands/query-filter.js.map +1 -0
  24. package/dist/cli/commands/results.d.ts +3 -3
  25. package/dist/cli/commands/results.d.ts.map +1 -1
  26. package/dist/cli/commands/results.js +12 -3
  27. package/dist/cli/commands/results.js.map +1 -1
  28. package/dist/cli/commands/search-executor.d.ts.map +1 -1
  29. package/dist/cli/commands/search-executor.js +12 -7
  30. package/dist/cli/commands/search-executor.js.map +1 -1
  31. package/dist/cli/e2e-helpers.d.ts +5 -2
  32. package/dist/cli/e2e-helpers.d.ts.map +1 -1
  33. package/dist/cli/index.d.ts.map +1 -1
  34. package/dist/cli/index.js +228 -45
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/index.js +4 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/providers/arxiv/provider.d.ts +3 -3
  39. package/dist/providers/arxiv/provider.d.ts.map +1 -1
  40. package/dist/providers/arxiv/provider.js +3 -3
  41. package/dist/providers/arxiv/provider.js.map +1 -1
  42. package/dist/providers/arxiv/translator.d.ts +3 -3
  43. package/dist/providers/arxiv/translator.d.ts.map +1 -1
  44. package/dist/providers/arxiv/translator.js +6 -8
  45. package/dist/providers/arxiv/translator.js.map +1 -1
  46. package/dist/providers/base/index.d.ts +1 -1
  47. package/dist/providers/base/index.d.ts.map +1 -1
  48. package/dist/providers/base/mock-provider.d.ts +2 -2
  49. package/dist/providers/base/mock-provider.d.ts.map +1 -1
  50. package/dist/providers/base/mock-provider.js +2 -9
  51. package/dist/providers/base/mock-provider.js.map +1 -1
  52. package/dist/providers/base/provider.d.ts +3 -3
  53. package/dist/providers/base/provider.d.ts.map +1 -1
  54. package/dist/providers/base/provider.js.map +1 -1
  55. package/dist/providers/base/types.d.ts +4 -6
  56. package/dist/providers/base/types.d.ts.map +1 -1
  57. package/dist/providers/base/types.js.map +1 -1
  58. package/dist/providers/eric/provider.d.ts +3 -3
  59. package/dist/providers/eric/provider.d.ts.map +1 -1
  60. package/dist/providers/eric/provider.js +3 -3
  61. package/dist/providers/eric/provider.js.map +1 -1
  62. package/dist/providers/eric/translator.d.ts +5 -5
  63. package/dist/providers/eric/translator.d.ts.map +1 -1
  64. package/dist/providers/eric/translator.js +6 -7
  65. package/dist/providers/eric/translator.js.map +1 -1
  66. package/dist/providers/pubmed/provider.d.ts +3 -3
  67. package/dist/providers/pubmed/provider.d.ts.map +1 -1
  68. package/dist/providers/pubmed/provider.js +3 -3
  69. package/dist/providers/pubmed/provider.js.map +1 -1
  70. package/dist/providers/pubmed/translator.d.ts +3 -3
  71. package/dist/providers/pubmed/translator.d.ts.map +1 -1
  72. package/dist/providers/pubmed/translator.js +4 -23
  73. package/dist/providers/pubmed/translator.js.map +1 -1
  74. package/dist/providers/scopus/provider.d.ts +3 -3
  75. package/dist/providers/scopus/provider.d.ts.map +1 -1
  76. package/dist/providers/scopus/provider.js +3 -3
  77. package/dist/providers/scopus/provider.js.map +1 -1
  78. package/dist/providers/scopus/translator.d.ts +3 -3
  79. package/dist/providers/scopus/translator.d.ts.map +1 -1
  80. package/dist/providers/scopus/translator.js +7 -9
  81. package/dist/providers/scopus/translator.js.map +1 -1
  82. package/dist/query/index.d.ts +3 -2
  83. package/dist/query/index.d.ts.map +1 -1
  84. package/dist/query/json-schema.d.ts.map +1 -1
  85. package/dist/query/json-schema.js +20 -11
  86. package/dist/query/json-schema.js.map +1 -1
  87. package/dist/query/resolver.d.ts +14 -0
  88. package/dist/query/resolver.d.ts.map +1 -0
  89. package/dist/query/resolver.js +61 -0
  90. package/dist/query/resolver.js.map +1 -0
  91. package/dist/query/types.d.ts +31 -11
  92. package/dist/query/types.d.ts.map +1 -1
  93. package/dist/query/validator.d.ts +659 -348
  94. package/dist/query/validator.d.ts.map +1 -1
  95. package/dist/query/validator.js +70 -30
  96. package/dist/query/validator.js.map +1 -1
  97. package/package.json +1 -1
@@ -0,0 +1,155 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { parseQueryString } from "../../../query/parser.js";
3
+ const PROVIDER_DISPLAY_NAMES = {
4
+ pubmed: "PubMed",
5
+ eric: "ERIC",
6
+ arxiv: "arXiv",
7
+ scopus: "Scopus",
8
+ wos: "WoS",
9
+ embase: "Embase"
10
+ };
11
+ const DEFAULT_PROVIDERS = [
12
+ "pubmed",
13
+ "eric",
14
+ "arxiv",
15
+ "scopus"
16
+ ];
17
+ function inspectQuery(ast, enabledProviders) {
18
+ const blocks = ast.blocks.map((block) => {
19
+ const status = {};
20
+ for (const provider of enabledProviders) {
21
+ const section = ast.providers[provider];
22
+ if (section?.replaces?.[block.id]) {
23
+ status[provider] = "replaced";
24
+ } else {
25
+ status[provider] = "default";
26
+ }
27
+ }
28
+ return { id: block.id, status };
29
+ });
30
+ const filterKeysSet = /* @__PURE__ */ new Set();
31
+ for (const provider of enabledProviders) {
32
+ const section = ast.providers[provider];
33
+ if (section?.adds?.filters) {
34
+ for (const key of Object.keys(section.adds.filters)) {
35
+ filterKeysSet.add(key);
36
+ }
37
+ }
38
+ }
39
+ const addedFilters = [];
40
+ for (const filterKey of filterKeysSet) {
41
+ const row = { filterKey, values: {} };
42
+ for (const provider of enabledProviders) {
43
+ const section = ast.providers[provider];
44
+ const filterValue = section?.adds?.filters?.[filterKey];
45
+ if (filterValue !== void 0) {
46
+ row.values[provider] = formatFilterValue(filterValue);
47
+ }
48
+ }
49
+ addedFilters.push(row);
50
+ }
51
+ return {
52
+ name: ast.name,
53
+ providers: enabledProviders,
54
+ blocks,
55
+ addedFilters
56
+ };
57
+ }
58
+ function formatFilterValue(value) {
59
+ if (Array.isArray(value)) {
60
+ const joined = value.join(", ");
61
+ if (joined.length > 20) {
62
+ return joined.substring(0, 17) + "..";
63
+ }
64
+ return joined;
65
+ }
66
+ if (typeof value === "object" && value !== null) {
67
+ const obj = value;
68
+ const parts = [];
69
+ if (Array.isArray(obj["include"])) {
70
+ parts.push(...obj["include"]);
71
+ }
72
+ if (Array.isArray(obj["exclude"])) {
73
+ parts.push(...obj["exclude"].map((v) => `-${v}`));
74
+ }
75
+ const joined = parts.join(", ");
76
+ if (joined.length > 20) {
77
+ return joined.substring(0, 17) + "..";
78
+ }
79
+ return joined || JSON.stringify(value);
80
+ }
81
+ return String(value);
82
+ }
83
+ function formatInspectOutput(result) {
84
+ const lines = [];
85
+ lines.push(`Query: ${result.name}`);
86
+ lines.push("");
87
+ const providerHeaders = result.providers.map(
88
+ (p) => PROVIDER_DISPLAY_NAMES[p] || p
89
+ );
90
+ const blockRows = result.blocks.map((block) => [
91
+ block.id,
92
+ ...result.providers.map((p) => block.status[p] || "default")
93
+ ]);
94
+ lines.push(
95
+ ...formatTable(["Block", ...providerHeaders], blockRows)
96
+ );
97
+ if (result.addedFilters.length > 0) {
98
+ lines.push("");
99
+ const filterRows = result.addedFilters.map((filter) => [
100
+ camelToSnakeCase(filter.filterKey),
101
+ ...result.providers.map((p) => filter.values[p] || "—")
102
+ ]);
103
+ lines.push(
104
+ ...formatTable(["Added Filters", ...providerHeaders], filterRows)
105
+ );
106
+ }
107
+ return lines.join("\n");
108
+ }
109
+ function camelToSnakeCase(str) {
110
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
111
+ }
112
+ function formatTable(headers, rows) {
113
+ const colCount = headers.length;
114
+ const colWidths = headers.map((h) => h.length);
115
+ for (const row of rows) {
116
+ for (let i = 0; i < colCount; i++) {
117
+ colWidths[i] = Math.max(colWidths[i], (row[i] || "").length);
118
+ }
119
+ }
120
+ const lines = [];
121
+ const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(" │ ");
122
+ lines.push(` ${headerLine}`);
123
+ const separatorLine = colWidths.map((w) => "─".repeat(w)).join("─┼─");
124
+ lines.push(` ${separatorLine}`);
125
+ for (const row of rows) {
126
+ const rowLine = row.map((cell, i) => (cell || "").padEnd(colWidths[i])).join(" │ ");
127
+ lines.push(` ${rowLine}`);
128
+ }
129
+ return lines;
130
+ }
131
+ async function inspectQueryCommand(filePath, options = {}) {
132
+ let content;
133
+ try {
134
+ content = await readFile(filePath, "utf-8");
135
+ } catch (error) {
136
+ const message = error instanceof Error ? error.message : "Failed to read file";
137
+ return { success: false, error: message };
138
+ }
139
+ let ast;
140
+ try {
141
+ ast = parseQueryString(content);
142
+ } catch (error) {
143
+ const message = error instanceof Error ? error.message : "Failed to parse query file";
144
+ return { success: false, error: message };
145
+ }
146
+ const providers = options.providers ?? DEFAULT_PROVIDERS;
147
+ const result = inspectQuery(ast, providers);
148
+ return { success: true, result };
149
+ }
150
+ export {
151
+ formatInspectOutput,
152
+ inspectQuery,
153
+ inspectQueryCommand
154
+ };
155
+ //# sourceMappingURL=inspect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspect.js","sources":["../../../../src/cli/commands/query/inspect.ts"],"sourcesContent":["/**\n * Query Inspect Command\n *\n * Visualizes how a query DSL file resolves for each provider,\n * showing which blocks use default vs. custom strategies and\n * which filters are added per provider.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { parseQueryString } from '../../../query/parser.js';\nimport type { QueryAST, Filters } from '../../../query/types.js';\nimport type { ProviderName } from '../../../providers/base/types.js';\n\n/** Provider display names for table headers */\nconst PROVIDER_DISPLAY_NAMES: Record<string, string> = {\n pubmed: 'PubMed',\n eric: 'ERIC',\n arxiv: 'arXiv',\n scopus: 'Scopus',\n wos: 'WoS',\n embase: 'Embase',\n};\n\nexport interface BlockInspectRow {\n id: string;\n status: Partial<Record<ProviderName, 'default' | 'replaced'>>;\n}\n\nexport interface FilterInspectRow {\n filterKey: string;\n values: Partial<Record<ProviderName, string>>;\n}\n\nexport interface InspectResult {\n name: string;\n providers: ProviderName[];\n blocks: BlockInspectRow[];\n addedFilters: FilterInspectRow[];\n}\n\nexport interface InspectCommandResult {\n success: boolean;\n error?: string;\n result?: InspectResult;\n}\n\n/**\n * Default providers to inspect for.\n */\nconst DEFAULT_PROVIDERS: ProviderName[] = [\n 'pubmed',\n 'eric',\n 'arxiv',\n 'scopus',\n];\n\n/**\n * Inspect a QueryAST to determine block resolution and filter additions per provider.\n */\nexport function inspectQuery(\n ast: QueryAST,\n enabledProviders: ProviderName[]\n): InspectResult {\n const blocks: BlockInspectRow[] = ast.blocks.map((block) => {\n const status: Partial<Record<ProviderName, 'default' | 'replaced'>> = {};\n for (const provider of enabledProviders) {\n const section = ast.providers[provider];\n if (section?.replaces?.[block.id]) {\n status[provider] = 'replaced';\n } else {\n status[provider] = 'default';\n }\n }\n return { id: block.id, status };\n });\n\n // Collect added filters across all providers\n const filterKeysSet = new Set<string>();\n for (const provider of enabledProviders) {\n const section = ast.providers[provider];\n if (section?.adds?.filters) {\n for (const key of Object.keys(section.adds.filters)) {\n filterKeysSet.add(key);\n }\n }\n }\n\n const addedFilters: FilterInspectRow[] = [];\n for (const filterKey of filterKeysSet) {\n const row: FilterInspectRow = { filterKey, values: {} };\n for (const provider of enabledProviders) {\n const section = ast.providers[provider];\n const filterValue = section?.adds?.filters?.[filterKey as keyof Filters];\n if (filterValue !== undefined) {\n row.values[provider] = formatFilterValue(filterValue);\n }\n }\n addedFilters.push(row);\n }\n\n return {\n name: ast.name,\n providers: enabledProviders,\n blocks,\n addedFilters,\n };\n}\n\n/**\n * Format a filter value for display.\n */\nfunction formatFilterValue(value: unknown): string {\n if (Array.isArray(value)) {\n const joined = value.join(', ');\n // Truncate long arrays\n if (joined.length > 20) {\n return joined.substring(0, 17) + '..';\n }\n return joined;\n }\n if (typeof value === 'object' && value !== null) {\n // For publicationTypes: { exclude: ['Review'] } → \"-Review\"\n const obj = value as Record<string, unknown>;\n const parts: string[] = [];\n if (Array.isArray(obj['include'])) {\n parts.push(...(obj['include'] as string[]));\n }\n if (Array.isArray(obj['exclude'])) {\n parts.push(...(obj['exclude'] as string[]).map((v) => `-${v}`));\n }\n const joined = parts.join(', ');\n if (joined.length > 20) {\n return joined.substring(0, 17) + '..';\n }\n return joined || JSON.stringify(value);\n }\n return String(value);\n}\n\n/**\n * Format InspectResult as an aligned table string.\n */\nexport function formatInspectOutput(result: InspectResult): string {\n const lines: string[] = [];\n lines.push(`Query: ${result.name}`);\n lines.push('');\n\n const providerHeaders = result.providers.map(\n (p) => PROVIDER_DISPLAY_NAMES[p] || p\n );\n\n // Block resolution table\n const blockRows = result.blocks.map((block) => [\n block.id,\n ...result.providers.map((p) => block.status[p] || 'default'),\n ]);\n lines.push(\n ...formatTable(['Block', ...providerHeaders], blockRows)\n );\n\n // Added filters table (only if there are added filters)\n if (result.addedFilters.length > 0) {\n lines.push('');\n const filterRows = result.addedFilters.map((filter) => [\n camelToSnakeCase(filter.filterKey),\n ...result.providers.map((p) => filter.values[p] || '\\u2014'),\n ]);\n lines.push(\n ...formatTable(['Added Filters', ...providerHeaders], filterRows)\n );\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Convert a camelCase string to snake_case.\n */\nfunction camelToSnakeCase(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);\n}\n\n/**\n * Format a table with headers and rows, with Unicode box drawing alignment.\n */\nfunction formatTable(headers: string[], rows: string[][]): string[] {\n const colCount = headers.length;\n const colWidths: number[] = headers.map((h) => h.length);\n\n // Compute column widths\n for (const row of rows) {\n for (let i = 0; i < colCount; i++) {\n colWidths[i] = Math.max(colWidths[i]!, (row[i] || '').length);\n }\n }\n\n const lines: string[] = [];\n\n // Header row\n const headerLine = headers\n .map((h, i) => h.padEnd(colWidths[i]!))\n .join(' \\u2502 ');\n lines.push(` ${headerLine}`);\n\n // Separator\n const separatorLine = colWidths\n .map((w) => '\\u2500'.repeat(w))\n .join('\\u2500\\u253C\\u2500');\n lines.push(` ${separatorLine}`);\n\n // Data rows\n for (const row of rows) {\n const rowLine = row\n .map((cell, i) => (cell || '').padEnd(colWidths[i]!))\n .join(' \\u2502 ');\n lines.push(` ${rowLine}`);\n }\n\n return lines;\n}\n\n/**\n * Run the inspect command on a query file.\n */\nexport async function inspectQueryCommand(\n filePath: string,\n options: { providers?: ProviderName[] } = {}\n): Promise<InspectCommandResult> {\n let content: string;\n try {\n content = await readFile(filePath, 'utf-8');\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Failed to read file';\n return { success: false, error: message };\n }\n\n let ast: QueryAST;\n try {\n ast = parseQueryString(content);\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Failed to parse query file';\n return { success: false, error: message };\n }\n\n const providers = options.providers ?? DEFAULT_PROVIDERS;\n const result = inspectQuery(ast, providers);\n\n return { success: true, result };\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/translate.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAwBtF;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0CAA0C;IAC1C,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oCAAoC;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAChD;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAuD1B;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,MAAM,GACf,MAAM,CAqBR"}
1
+ {"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/translate.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAyBtF;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0CAA0C;IAC1C,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oCAAoC;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAChD;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAwD1B;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,MAAM,GACf,MAAM,CAqBR"}
@@ -1,5 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { parseQueryString } from "../../../query/parser.js";
3
+ import { resolveForProvider } from "../../../query/resolver.js";
3
4
  import "../../../query/validator.js";
4
5
  import { translateQuery as translateQuery$3 } from "../../../providers/pubmed/translator.js";
5
6
  import { translateQuery as translateQuery$2 } from "../../../providers/eric/translator.js";
@@ -39,7 +40,8 @@ async function translateQueryCommand(filePath, options = {}) {
39
40
  const translator = translators[provider];
40
41
  if (translator) {
41
42
  try {
42
- translations[provider] = translator(ast);
43
+ const resolved = resolveForProvider(ast, provider);
44
+ translations[provider] = translator(resolved);
43
45
  } catch (error) {
44
46
  const message = error instanceof Error ? error.message : `Failed to translate for ${provider}`;
45
47
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"translate.js","sources":["../../../../src/cli/commands/query/translate.ts"],"sourcesContent":["/**\n * Query translate command implementation.\n *\n * Translates a YAML query file to native database query syntax for each provider.\n */\nimport { readFile } from 'node:fs/promises';\nimport { parseQueryString } from '../../../query/index.js';\nimport type { TranslatedQuery, ProviderName } from '../../../providers/base/types.js';\nimport { translateQuery as translatePubmed } from '../../../providers/pubmed/translator.js';\nimport { translateQuery as translateEric } from '../../../providers/eric/translator.js';\nimport { translateQuery as translateArxiv } from '../../../providers/arxiv/translator.js';\nimport { translateQuery as translateScopus } from '../../../providers/scopus/translator.js';\n\n/**\n * Available translators by provider name.\n */\nconst translators: Record<\n string,\n (ast: Parameters<typeof translatePubmed>[0]) => TranslatedQuery\n> = {\n pubmed: translatePubmed,\n eric: translateEric,\n arxiv: translateArxiv,\n scopus: translateScopus,\n};\n\n/**\n * Default providers to translate for.\n */\nconst DEFAULT_PROVIDERS: ProviderName[] = ['pubmed', 'eric', 'arxiv', 'scopus'];\n\n/**\n * Options for translate command.\n */\nexport interface TranslateOptions {\n /** Specific providers to translate for */\n providers?: ProviderName[];\n}\n\n/**\n * Result of query translation.\n */\nexport interface TranslateResult {\n /** Whether translation succeeded */\n success: boolean;\n /** Error message if failed */\n error?: string;\n /** Translations by provider name */\n translations?: Record<string, TranslatedQuery>;\n}\n\n/**\n * Translate a query YAML file to native syntax for each provider.\n *\n * @param filePath - Path to the query file\n * @param options - Translation options\n * @returns Translation result\n */\nexport async function translateQueryCommand(\n filePath: string,\n options: TranslateOptions = {}\n): Promise<TranslateResult> {\n // Read file\n let content: string;\n try {\n content = await readFile(filePath, 'utf-8');\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Failed to read file';\n return {\n success: false,\n error: message,\n };\n }\n\n // Parse and validate\n let ast;\n try {\n ast = parseQueryString(content);\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Failed to parse query file';\n return {\n success: false,\n error: message,\n };\n }\n\n // Determine which providers to translate for\n const providers = options.providers ?? DEFAULT_PROVIDERS;\n\n // Translate for each provider\n const translations: Record<string, TranslatedQuery> = {};\n\n for (const provider of providers) {\n const translator = translators[provider];\n if (translator) {\n try {\n translations[provider] = translator(ast);\n } catch (error) {\n const message =\n error instanceof Error\n ? error.message\n : `Failed to translate for ${provider}`;\n return {\n success: false,\n error: `${provider}: ${message}`,\n };\n }\n }\n }\n\n return {\n success: true,\n translations,\n };\n}\n\n/**\n * Format translation result for display.\n */\nexport function formatTranslateResult(\n result: TranslateResult,\n filePath: string\n): string {\n if (!result.success) {\n return `✗ Failed to translate: ${filePath}\\n Error: ${result.error}`;\n }\n\n const lines = [`Translations for: ${filePath}`, ''];\n\n if (result.translations) {\n for (const [provider, translation] of Object.entries(result.translations)) {\n lines.push(`[${provider.toUpperCase()}]`);\n lines.push(translation.native);\n if (translation.warnings && translation.warnings.length > 0) {\n for (const warning of translation.warnings) {\n lines.push(`⚠ ${warning}`);\n }\n }\n lines.push('');\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":["translatePubmed","translateEric","translateArxiv","translateScopus"],"mappings":";;;;;;;AAgBA,MAAM,cAGF;AAAA,EACF,QAAQA;AAAAA,EACR,MAAMC;AAAAA,EACN,OAAOC;AAAAA,EACP,QAAQC;AACV;AAKA,MAAM,oBAAoC,CAAC,UAAU,QAAQ,SAAS,QAAQ;AA6B9E,eAAsB,sBACpB,UACA,UAA4B,IACF;AAE1B,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,SAAS,UAAU,OAAO;AAAA,EAC5C,SAAS,OAAO;AACd,UAAM,UACJ,iBAAiB,QAAQ,MAAM,UAAU;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AAGA,MAAI;AACJ,MAAI;AACF,UAAM,iBAAiB,OAAO;AAAA,EAChC,SAAS,OAAO;AACd,UAAM,UACJ,iBAAiB,QAAQ,MAAM,UAAU;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AAGA,QAAM,YAAY,QAAQ,aAAa;AAGvC,QAAM,eAAgD,CAAA;AAEtD,aAAW,YAAY,WAAW;AAChC,UAAM,aAAa,YAAY,QAAQ;AACvC,QAAI,YAAY;AACd,UAAI;AACF,qBAAa,QAAQ,IAAI,WAAW,GAAG;AAAA,MACzC,SAAS,OAAO;AACd,cAAM,UACJ,iBAAiB,QACb,MAAM,UACN,2BAA2B,QAAQ;AACzC,eAAO;AAAA,UACL,SAAS;AAAA,UACT,OAAO,GAAG,QAAQ,KAAK,OAAO;AAAA,QAAA;AAAA,MAElC;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,EAAA;AAEJ;AAKO,SAAS,sBACd,QACA,UACQ;AACR,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,0BAA0B,QAAQ;AAAA,WAAc,OAAO,KAAK;AAAA,EACrE;AAEA,QAAM,QAAQ,CAAC,qBAAqB,QAAQ,IAAI,EAAE;AAElD,MAAI,OAAO,cAAc;AACvB,eAAW,CAAC,UAAU,WAAW,KAAK,OAAO,QAAQ,OAAO,YAAY,GAAG;AACzE,YAAM,KAAK,IAAI,SAAS,YAAA,CAAa,GAAG;AACxC,YAAM,KAAK,YAAY,MAAM;AAC7B,UAAI,YAAY,YAAY,YAAY,SAAS,SAAS,GAAG;AAC3D,mBAAW,WAAW,YAAY,UAAU;AAC1C,gBAAM,KAAK,KAAK,OAAO,EAAE;AAAA,QAC3B;AAAA,MACF;AACA,YAAM,KAAK,EAAE;AAAA,IACf;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
1
+ {"version":3,"file":"translate.js","sources":["../../../../src/cli/commands/query/translate.ts"],"sourcesContent":["/**\n * Query translate command implementation.\n *\n * Translates a YAML query file to native database query syntax for each provider.\n */\nimport { readFile } from 'node:fs/promises';\nimport { parseQueryString } from '../../../query/index.js';\nimport type { TranslatedQuery, ProviderName } from '../../../providers/base/types.js';\nimport { translateQuery as translatePubmed } from '../../../providers/pubmed/translator.js';\nimport { translateQuery as translateEric } from '../../../providers/eric/translator.js';\nimport { translateQuery as translateArxiv } from '../../../providers/arxiv/translator.js';\nimport { translateQuery as translateScopus } from '../../../providers/scopus/translator.js';\nimport { resolveForProvider } from '../../../query/resolver.js';\n\n/**\n * Available translators by provider name.\n */\nconst translators: Record<\n string,\n (ast: Parameters<typeof translatePubmed>[0]) => TranslatedQuery\n> = {\n pubmed: translatePubmed,\n eric: translateEric,\n arxiv: translateArxiv,\n scopus: translateScopus,\n};\n\n/**\n * Default providers to translate for.\n */\nconst DEFAULT_PROVIDERS: ProviderName[] = ['pubmed', 'eric', 'arxiv', 'scopus'];\n\n/**\n * Options for translate command.\n */\nexport interface TranslateOptions {\n /** Specific providers to translate for */\n providers?: ProviderName[];\n}\n\n/**\n * Result of query translation.\n */\nexport interface TranslateResult {\n /** Whether translation succeeded */\n success: boolean;\n /** Error message if failed */\n error?: string;\n /** Translations by provider name */\n translations?: Record<string, TranslatedQuery>;\n}\n\n/**\n * Translate a query YAML file to native syntax for each provider.\n *\n * @param filePath - Path to the query file\n * @param options - Translation options\n * @returns Translation result\n */\nexport async function translateQueryCommand(\n filePath: string,\n options: TranslateOptions = {}\n): Promise<TranslateResult> {\n // Read file\n let content: string;\n try {\n content = await readFile(filePath, 'utf-8');\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Failed to read file';\n return {\n success: false,\n error: message,\n };\n }\n\n // Parse and validate\n let ast;\n try {\n ast = parseQueryString(content);\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Failed to parse query file';\n return {\n success: false,\n error: message,\n };\n }\n\n // Determine which providers to translate for\n const providers = options.providers ?? DEFAULT_PROVIDERS;\n\n // Translate for each provider\n const translations: Record<string, TranslatedQuery> = {};\n\n for (const provider of providers) {\n const translator = translators[provider];\n if (translator) {\n try {\n const resolved = resolveForProvider(ast, provider as ProviderName);\n translations[provider] = translator(resolved);\n } catch (error) {\n const message =\n error instanceof Error\n ? error.message\n : `Failed to translate for ${provider}`;\n return {\n success: false,\n error: `${provider}: ${message}`,\n };\n }\n }\n }\n\n return {\n success: true,\n translations,\n };\n}\n\n/**\n * Format translation result for display.\n */\nexport function formatTranslateResult(\n result: TranslateResult,\n filePath: string\n): string {\n if (!result.success) {\n return `✗ Failed to translate: ${filePath}\\n Error: ${result.error}`;\n }\n\n const lines = [`Translations for: ${filePath}`, ''];\n\n if (result.translations) {\n for (const [provider, translation] of Object.entries(result.translations)) {\n lines.push(`[${provider.toUpperCase()}]`);\n lines.push(translation.native);\n if (translation.warnings && translation.warnings.length > 0) {\n for (const warning of translation.warnings) {\n lines.push(`⚠ ${warning}`);\n }\n }\n lines.push('');\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":["translatePubmed","translateEric","translateArxiv","translateScopus"],"mappings":";;;;;;;;AAiBA,MAAM,cAGF;AAAA,EACF,QAAQA;AAAAA,EACR,MAAMC;AAAAA,EACN,OAAOC;AAAAA,EACP,QAAQC;AACV;AAKA,MAAM,oBAAoC,CAAC,UAAU,QAAQ,SAAS,QAAQ;AA6B9E,eAAsB,sBACpB,UACA,UAA4B,IACF;AAE1B,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,SAAS,UAAU,OAAO;AAAA,EAC5C,SAAS,OAAO;AACd,UAAM,UACJ,iBAAiB,QAAQ,MAAM,UAAU;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AAGA,MAAI;AACJ,MAAI;AACF,UAAM,iBAAiB,OAAO;AAAA,EAChC,SAAS,OAAO;AACd,UAAM,UACJ,iBAAiB,QAAQ,MAAM,UAAU;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AAGA,QAAM,YAAY,QAAQ,aAAa;AAGvC,QAAM,eAAgD,CAAA;AAEtD,aAAW,YAAY,WAAW;AAChC,UAAM,aAAa,YAAY,QAAQ;AACvC,QAAI,YAAY;AACd,UAAI;AACF,cAAM,WAAW,mBAAmB,KAAK,QAAwB;AACjE,qBAAa,QAAQ,IAAI,WAAW,QAAQ;AAAA,MAC9C,SAAS,OAAO;AACd,cAAM,UACJ,iBAAiB,QACb,MAAM,UACN,2BAA2B,QAAQ;AACzC,eAAO;AAAA,UACL,SAAS;AAAA,UACT,OAAO,GAAG,QAAQ,KAAK,OAAO;AAAA,QAAA;AAAA,MAElC;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,EAAA;AAEJ;AAKO,SAAS,sBACd,QACA,UACQ;AACR,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,0BAA0B,QAAQ;AAAA,WAAc,OAAO,KAAK;AAAA,EACrE;AAEA,QAAM,QAAQ,CAAC,qBAAqB,QAAQ,IAAI,EAAE;AAElD,MAAI,OAAO,cAAc;AACvB,eAAW,CAAC,UAAU,WAAW,KAAK,OAAO,QAAQ,OAAO,YAAY,GAAG;AACzE,YAAM,KAAK,IAAI,SAAS,YAAA,CAAa,GAAG;AACxC,YAAM,KAAK,YAAY,MAAM;AAC7B,UAAI,YAAY,YAAY,YAAY,SAAS,SAAS,GAAG;AAC3D,mBAAW,WAAW,YAAY,UAAU;AAC1C,gBAAM,KAAK,KAAK,OAAO,EAAE;AAAA,QAC3B;AAAA,MACF;AACA,YAAM,KAAK,EAAE;AAAA,IACf;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
@@ -0,0 +1,13 @@
1
+ import { Article } from '../../providers/base/types.js';
2
+ export type QueryToken = {
3
+ type: 'text';
4
+ value: string;
5
+ } | {
6
+ type: 'field';
7
+ field: string;
8
+ value: string;
9
+ };
10
+ export declare function tokenizeQuery(query: string): QueryToken[];
11
+ export declare function matchArticle(article: Article, tokens: QueryToken[]): boolean;
12
+ export declare function filterByQuery(articles: Article[], query: string): Article[];
13
+ //# sourceMappingURL=query-filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-filter.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/query-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAI7D,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAUpD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,CA+DzD;AAuED,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,OAAO,CAsB5E;AAID,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE,CAQ3E"}
@@ -0,0 +1,149 @@
1
+ const KNOWN_FIELDS = /* @__PURE__ */ new Set([
2
+ "title",
3
+ "abstract",
4
+ "author",
5
+ "journal",
6
+ "year",
7
+ "doi",
8
+ "pmid",
9
+ "arxiv",
10
+ "scopus",
11
+ "eric",
12
+ "source"
13
+ ]);
14
+ function tokenizeQuery(query) {
15
+ const trimmed = query.trim();
16
+ if (!trimmed) return [];
17
+ const tokens = [];
18
+ let i = 0;
19
+ while (i < trimmed.length) {
20
+ while (i < trimmed.length && trimmed[i] === " ") i++;
21
+ if (i >= trimmed.length) break;
22
+ if (trimmed[i] === '"') {
23
+ const closing = trimmed.indexOf('"', i + 1);
24
+ if (closing === -1) {
25
+ tokens.push({ type: "text", value: trimmed.slice(i + 1) });
26
+ break;
27
+ }
28
+ tokens.push({ type: "text", value: trimmed.slice(i + 1, closing) });
29
+ i = closing + 1;
30
+ continue;
31
+ }
32
+ const start = i;
33
+ while (i < trimmed.length && trimmed[i] !== " ") i++;
34
+ const word = trimmed.slice(start, i);
35
+ const colonIdx = word.indexOf(":");
36
+ if (colonIdx > 0) {
37
+ const fieldName = word.slice(0, colonIdx);
38
+ if (KNOWN_FIELDS.has(fieldName)) {
39
+ let value = word.slice(colonIdx + 1);
40
+ if (value.startsWith('"')) {
41
+ const afterQuote = start + colonIdx + 2;
42
+ const closing = trimmed.indexOf('"', afterQuote);
43
+ if (closing === -1) {
44
+ value = trimmed.slice(afterQuote);
45
+ tokens.push({ type: "field", field: fieldName, value });
46
+ break;
47
+ }
48
+ value = trimmed.slice(afterQuote, closing);
49
+ tokens.push({ type: "field", field: fieldName, value });
50
+ i = closing + 1;
51
+ continue;
52
+ }
53
+ tokens.push({ type: "field", field: fieldName, value });
54
+ continue;
55
+ }
56
+ }
57
+ tokens.push({ type: "text", value: word });
58
+ }
59
+ return tokens;
60
+ }
61
+ function extractYear(dateStr) {
62
+ if (!dateStr) return null;
63
+ const match = dateStr.match(/(\d{4})/);
64
+ return match ? parseInt(match[1], 10) : null;
65
+ }
66
+ function matchIdField(articleValue, queryValue) {
67
+ if (!articleValue) return false;
68
+ return articleValue.toLowerCase() === queryValue.toLowerCase();
69
+ }
70
+ function matchSubstring(text, queryValue) {
71
+ if (!text) return false;
72
+ return text.toLowerCase().includes(queryValue.toLowerCase());
73
+ }
74
+ function matchYearToken(article, value) {
75
+ const year = extractYear(article.publicationDate);
76
+ if (year === null) return false;
77
+ const rangeMatch = value.match(/^(\d{4})-(\d{4})$/);
78
+ if (rangeMatch) {
79
+ const from = parseInt(rangeMatch[1], 10);
80
+ const to = parseInt(rangeMatch[2], 10);
81
+ return year >= from && year <= to;
82
+ }
83
+ const exact = parseInt(value, 10);
84
+ return !Number.isNaN(exact) && year === exact;
85
+ }
86
+ function matchSingleToken(article, token) {
87
+ if (token.type === "text") {
88
+ return matchSubstring(article.title, token.value) || matchSubstring(article.abstract, token.value);
89
+ }
90
+ switch (token.field) {
91
+ case "title":
92
+ return matchSubstring(article.title, token.value);
93
+ case "abstract":
94
+ return matchSubstring(article.abstract, token.value);
95
+ case "author":
96
+ return article.authors.some(
97
+ (a) => matchSubstring(a.family, token.value) || matchSubstring(a.given, token.value)
98
+ );
99
+ case "journal":
100
+ return matchSubstring(article.journal, token.value);
101
+ case "year":
102
+ return matchYearToken(article, token.value);
103
+ case "doi":
104
+ return matchIdField(article.doi, token.value);
105
+ case "pmid":
106
+ return matchIdField(article.pmid, token.value);
107
+ case "arxiv":
108
+ return matchIdField(article.arxivId, token.value);
109
+ case "scopus":
110
+ return matchIdField(article.scopusId, token.value);
111
+ case "eric":
112
+ return matchIdField(article.ericId, token.value);
113
+ case "source":
114
+ return article.source === token.value.toLowerCase();
115
+ default:
116
+ return false;
117
+ }
118
+ }
119
+ function matchArticle(article, tokens) {
120
+ if (tokens.length === 0) return true;
121
+ const groups = /* @__PURE__ */ new Map();
122
+ for (const token of tokens) {
123
+ const key = token.type === "text" ? "__text__" : token.field;
124
+ const group = groups.get(key);
125
+ if (group) {
126
+ group.push(token);
127
+ } else {
128
+ groups.set(key, [token]);
129
+ }
130
+ }
131
+ for (const groupTokens of groups.values()) {
132
+ const anyMatch = groupTokens.some((t) => matchSingleToken(article, t));
133
+ if (!anyMatch) return false;
134
+ }
135
+ return true;
136
+ }
137
+ function filterByQuery(articles, query) {
138
+ const trimmed = query.trim();
139
+ if (!trimmed) return articles;
140
+ const tokens = tokenizeQuery(trimmed);
141
+ if (tokens.length === 0) return articles;
142
+ return articles.filter((article) => matchArticle(article, tokens));
143
+ }
144
+ export {
145
+ filterByQuery,
146
+ matchArticle,
147
+ tokenizeQuery
148
+ };
149
+ //# sourceMappingURL=query-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-filter.js","sources":["../../../src/cli/commands/query-filter.ts"],"sourcesContent":["/**\n * Query filter module for filtering articles with a unified query expression.\n *\n * Syntax: field:value pairs and free text, space-separated.\n * - Free text searches title OR abstract\n * - Different fields: AND logic\n * - Same field repeated: OR logic\n */\nimport type { Article } from '../../providers/base/types.js';\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport type QueryToken =\n | { type: 'text'; value: string }\n | { type: 'field'; field: string; value: string };\n\nconst KNOWN_FIELDS = new Set([\n 'title', 'abstract', 'author', 'journal',\n 'year', 'doi', 'pmid', 'arxiv', 'scopus', 'eric',\n 'source',\n]);\n\n// ─── Tokenizer ──────────────────────────────────────────────────────────────\n\nexport function tokenizeQuery(query: string): QueryToken[] {\n const trimmed = query.trim();\n if (!trimmed) return [];\n\n const tokens: QueryToken[] = [];\n let i = 0;\n\n while (i < trimmed.length) {\n // Skip whitespace\n while (i < trimmed.length && trimmed[i] === ' ') i++;\n if (i >= trimmed.length) break;\n\n // Quoted free text: \"...\"\n if (trimmed[i] === '\"') {\n const closing = trimmed.indexOf('\"', i + 1);\n if (closing === -1) {\n // Unclosed quote — take rest as value\n tokens.push({ type: 'text', value: trimmed.slice(i + 1) });\n break;\n }\n tokens.push({ type: 'text', value: trimmed.slice(i + 1, closing) });\n i = closing + 1;\n continue;\n }\n\n // Read a word (until space or end)\n const start = i;\n while (i < trimmed.length && trimmed[i] !== ' ') i++;\n const word = trimmed.slice(start, i);\n\n // Check for field:value pattern\n const colonIdx = word.indexOf(':');\n if (colonIdx > 0) {\n const fieldName = word.slice(0, colonIdx);\n if (KNOWN_FIELDS.has(fieldName)) {\n let value = word.slice(colonIdx + 1);\n\n // Field with quoted value: field:\"...\"\n if (value.startsWith('\"')) {\n const afterQuote = start + colonIdx + 2; // position after opening quote\n const closing = trimmed.indexOf('\"', afterQuote);\n if (closing === -1) {\n // Unclosed quote\n value = trimmed.slice(afterQuote);\n tokens.push({ type: 'field', field: fieldName, value });\n break;\n }\n value = trimmed.slice(afterQuote, closing);\n tokens.push({ type: 'field', field: fieldName, value });\n i = closing + 1;\n continue;\n }\n\n tokens.push({ type: 'field', field: fieldName, value });\n continue;\n }\n }\n\n // Plain free text word (or unknown field prefix)\n tokens.push({ type: 'text', value: word });\n }\n\n return tokens;\n}\n\n// ─── Matcher ────────────────────────────────────────────────────────────────\n\nfunction extractYear(dateStr: string | undefined): number | null {\n if (!dateStr) return null;\n const match = dateStr.match(/(\\d{4})/);\n return match ? parseInt(match[1]!, 10) : null;\n}\n\nfunction matchIdField(articleValue: string | undefined, queryValue: string): boolean {\n if (!articleValue) return false;\n return articleValue.toLowerCase() === queryValue.toLowerCase();\n}\n\nfunction matchSubstring(text: string | undefined, queryValue: string): boolean {\n if (!text) return false;\n return text.toLowerCase().includes(queryValue.toLowerCase());\n}\n\nfunction matchYearToken(article: Article, value: string): boolean {\n const year = extractYear(article.publicationDate);\n if (year === null) return false;\n\n const rangeMatch = value.match(/^(\\d{4})-(\\d{4})$/);\n if (rangeMatch) {\n const from = parseInt(rangeMatch[1]!, 10);\n const to = parseInt(rangeMatch[2]!, 10);\n return year >= from && year <= to;\n }\n\n const exact = parseInt(value, 10);\n return !Number.isNaN(exact) && year === exact;\n}\n\nfunction matchSingleToken(article: Article, token: QueryToken): boolean {\n if (token.type === 'text') {\n return matchSubstring(article.title, token.value) ||\n matchSubstring(article.abstract, token.value);\n }\n\n switch (token.field) {\n case 'title':\n return matchSubstring(article.title, token.value);\n case 'abstract':\n return matchSubstring(article.abstract, token.value);\n case 'author':\n return article.authors.some(\n (a) => matchSubstring(a.family, token.value) || matchSubstring(a.given, token.value)\n );\n case 'journal':\n return matchSubstring(article.journal, token.value);\n case 'year':\n return matchYearToken(article, token.value);\n case 'doi':\n return matchIdField(article.doi, token.value);\n case 'pmid':\n return matchIdField(article.pmid, token.value);\n case 'arxiv':\n return matchIdField(article.arxivId, token.value);\n case 'scopus':\n return matchIdField(article.scopusId, token.value);\n case 'eric':\n return matchIdField(article.ericId, token.value);\n case 'source':\n return article.source === token.value.toLowerCase();\n default:\n return false;\n }\n}\n\nexport function matchArticle(article: Article, tokens: QueryToken[]): boolean {\n if (tokens.length === 0) return true;\n\n // Group tokens by their \"field key\" (field name, or 'text' for free text)\n const groups = new Map<string, QueryToken[]>();\n for (const token of tokens) {\n const key = token.type === 'text' ? '__text__' : token.field;\n const group = groups.get(key);\n if (group) {\n group.push(token);\n } else {\n groups.set(key, [token]);\n }\n }\n\n // AND across groups, OR within each group\n for (const groupTokens of groups.values()) {\n const anyMatch = groupTokens.some((t) => matchSingleToken(article, t));\n if (!anyMatch) return false;\n }\n\n return true;\n}\n\n// ─── Filter ─────────────────────────────────────────────────────────────────\n\nexport function filterByQuery(articles: Article[], query: string): Article[] {\n const trimmed = query.trim();\n if (!trimmed) return articles;\n\n const tokens = tokenizeQuery(trimmed);\n if (tokens.length === 0) return articles;\n\n return articles.filter((article) => matchArticle(article, tokens));\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { ProviderName, Article } from '../../providers/base/types.js';
1
+ import { Article } from '../../providers/base/types.js';
2
2
  import { ExportFilter } from './export.js';
3
3
  export interface ResultsCommandOptions {
4
4
  sessionId: string;
@@ -6,7 +6,7 @@ export interface ResultsCommandOptions {
6
6
  offset?: number;
7
7
  json: boolean;
8
8
  fields?: string[];
9
- providers?: ProviderName[];
9
+ query?: string;
10
10
  filter?: ExportFilter;
11
11
  showAbstract: boolean;
12
12
  abstractLength?: number;
@@ -16,7 +16,7 @@ export interface CommandLineOptions {
16
16
  offset?: string | undefined;
17
17
  json?: boolean | undefined;
18
18
  fields?: string | undefined;
19
- db?: string | undefined;
19
+ query?: string | undefined;
20
20
  filterYear?: string | undefined;
21
21
  filterTitle?: string | undefined;
22
22
  filterAbstract?: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/results.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAE3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kBAAkB,GAC1B,qBAAqB,CAgEvB;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,gBAAgB,CAuBrF;AAoBD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,aAAa,GACrB,MAAM,CAyDR;AASD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAG7D"}
1
+ {"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/results.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kBAAkB,GAC1B,qBAAqB,CAiEvB;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,gBAAgB,CA+BrF;AAoBD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,aAAa,GACrB,MAAM,CA+DR;AASD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAG7D"}
@@ -1,4 +1,3 @@
1
- import { parseProviderNames } from "../utils/validation.js";
2
1
  function parseResultsOptions(sessionId, options) {
3
2
  const result = {
4
3
  sessionId,
@@ -17,8 +16,8 @@ function parseResultsOptions(sessionId, options) {
17
16
  if (options.fields) {
18
17
  result.fields = options.fields.split(",").map((f) => f.trim());
19
18
  }
20
- if (options.db) {
21
- result.providers = parseProviderNames(options.db);
19
+ if (options.query !== void 0) {
20
+ result.query = options.query;
22
21
  }
23
22
  const filter = {};
24
23
  let hasFilter = false;
@@ -71,6 +70,12 @@ function validateResultsInput(options) {
71
70
  error: "offset must be a non-negative number"
72
71
  };
73
72
  }
73
+ if (options.query !== void 0 && options.query !== "" && options.filter) {
74
+ return {
75
+ valid: false,
76
+ error: "Cannot use -q/--query together with --filter-year, --filter-title, or --filter-abstract. Use -q only."
77
+ };
78
+ }
74
79
  return { valid: true };
75
80
  }
76
81
  const DEFAULT_TITLE_MAX_LENGTH = 70;
@@ -129,6 +134,10 @@ function formatResultsList(articles, options) {
129
134
  }
130
135
  lines.push("");
131
136
  }
137
+ if (options.filteredFrom === void 0) {
138
+ lines.push('Tip: Use -q to filter: results SESSION -q "author:smith year:2023"');
139
+ }
140
+ lines.push("Tip: Use check to verify coverage: check SESSION --file known-dois.txt");
132
141
  return lines.join("\n").trimEnd();
133
142
  }
134
143
  function addYearField(articles) {
@@ -1 +1 @@
1
- {"version":3,"file":"results.js","sources":["../../../src/cli/commands/results.ts"],"sourcesContent":["/**\n * Results command - display articles from a session in the terminal.\n */\nimport type { ProviderName, Article } from '../../providers/base/types.js';\nimport { parseProviderNames } from '../utils/validation.js';\nimport type { ExportFilter } from './export.js';\n\nexport interface ResultsCommandOptions {\n sessionId: string;\n limit?: number;\n offset?: number;\n json: boolean;\n fields?: string[];\n providers?: ProviderName[];\n filter?: ExportFilter;\n showAbstract: boolean;\n abstractLength?: number;\n}\n\nexport interface CommandLineOptions {\n limit?: string | undefined;\n offset?: string | undefined;\n json?: boolean | undefined;\n fields?: string | undefined;\n db?: string | undefined;\n filterYear?: string | undefined;\n filterTitle?: string | undefined;\n filterAbstract?: string | undefined;\n abstract?: boolean | undefined;\n abstractLength?: string | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\nexport interface FormatOptions {\n sessionId: string;\n sessionName: string;\n total: number;\n offset?: number | undefined;\n filteredFrom?: number | undefined;\n showAbstract?: boolean | undefined;\n abstractLength?: number | undefined;\n}\n\nexport function parseResultsOptions(\n sessionId: string,\n options: CommandLineOptions\n): ResultsCommandOptions {\n const result: ResultsCommandOptions = {\n sessionId,\n json: options.json ?? false,\n showAbstract: options.abstract ?? false,\n };\n\n if (options.abstractLength) {\n result.abstractLength = parseInt(options.abstractLength, 10);\n }\n\n if (options.limit) {\n result.limit = parseInt(options.limit, 10);\n }\n\n if (options.offset) {\n result.offset = parseInt(options.offset, 10);\n }\n\n if (options.fields) {\n result.fields = options.fields.split(',').map((f) => f.trim());\n }\n\n if (options.db) {\n result.providers = parseProviderNames(options.db);\n }\n\n // Parse filters\n const filter: ExportFilter = {};\n let hasFilter = false;\n\n if (options.filterYear) {\n const parts = options.filterYear.split('-');\n if (parts.length === 2) {\n const from = parseInt(parts[0]!, 10);\n const to = parseInt(parts[1]!, 10);\n if (!Number.isNaN(from)) filter.yearFrom = from;\n if (!Number.isNaN(to)) filter.yearTo = to;\n hasFilter = true;\n } else if (parts.length === 1) {\n const year = parseInt(parts[0]!, 10);\n if (!Number.isNaN(year)) {\n filter.yearFrom = year;\n filter.yearTo = year;\n hasFilter = true;\n }\n }\n }\n\n if (options.filterTitle) {\n filter.titleKeywords = options.filterTitle.split(',').map((s) => s.trim()).filter(Boolean);\n hasFilter = true;\n }\n\n if (options.filterAbstract) {\n filter.abstractKeywords = options.filterAbstract.split(',').map((s) => s.trim()).filter(Boolean);\n hasFilter = true;\n }\n\n if (hasFilter) {\n result.filter = filter;\n }\n\n return result;\n}\n\nexport function validateResultsInput(options: ResultsCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n if (options.limit !== undefined && options.limit < 0) {\n return {\n valid: false,\n error: 'limit must be a non-negative number',\n };\n }\n\n if (options.offset !== undefined && options.offset < 0) {\n return {\n valid: false,\n error: 'offset must be a non-negative number',\n };\n }\n\n return { valid: true };\n}\n\nconst DEFAULT_TITLE_MAX_LENGTH = 70;\nconst DEFAULT_ABSTRACT_MAX_LENGTH = 300;\n\nfunction extractYear(publicationDate: string | undefined): number | null {\n if (!publicationDate) return null;\n const year = parseInt(publicationDate.substring(0, 4), 10);\n return Number.isNaN(year) ? null : year;\n}\n\nfunction truncateText(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n return text.substring(0, maxLength - 3) + '...';\n}\n\nfunction truncateTitle(title: string, maxLength: number = DEFAULT_TITLE_MAX_LENGTH): string {\n return truncateText(title, maxLength);\n}\n\nexport function formatResultsList(\n articles: Article[],\n options: FormatOptions\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push(`Results: ${options.sessionName} (${options.sessionId})`);\n\n if (articles.length === 0) {\n lines.push('No articles found.');\n return lines.join('\\n');\n }\n\n // Pagination info\n const offset = options.offset ?? 0;\n const startNum = offset + 1;\n const endNum = offset + articles.length;\n const articleWord = options.total === 1 ? 'article' : 'articles';\n\n let countInfo = `Showing ${startNum}-${endNum} of ${options.total} ${articleWord}`;\n if (options.filteredFrom !== undefined && options.filteredFrom !== options.total) {\n countInfo += ` (filtered from ${options.filteredFrom})`;\n }\n lines.push(countInfo);\n lines.push('');\n\n // Articles\n for (let i = 0; i < articles.length; i++) {\n const article = articles[i]!;\n const num = offset + i + 1;\n const year = extractYear(article.publicationDate);\n const yearStr = year !== null ? String(year) : '----';\n const title = truncateTitle(article.title);\n\n lines.push(`${num.toString().padStart(2)}. [${yearStr}] ${title}`);\n\n if (article.journal) {\n lines.push(` ${article.journal}`);\n }\n\n if (article.doi) {\n lines.push(` DOI: ${article.doi}`);\n }\n\n if (options.showAbstract) {\n lines.push('');\n if (article.abstract && article.abstract.trim() !== '') {\n const maxLength = options.abstractLength ?? DEFAULT_ABSTRACT_MAX_LENGTH;\n const truncatedAbstract = truncateText(article.abstract, maxLength);\n lines.push(` Abstract: ${truncatedAbstract}`);\n } else {\n lines.push(' (No abstract available)');\n }\n }\n\n lines.push('');\n }\n\n return lines.join('\\n').trimEnd();\n}\n\nfunction addYearField(articles: Article[]): (Article & { year: number | null })[] {\n return articles.map((article) => ({\n ...article,\n year: extractYear(article.publicationDate),\n }));\n}\n\nexport function formatResultsJson(articles: Article[]): string {\n const articlesWithYear = addYearField(articles);\n return JSON.stringify(articlesWithYear, null, 2);\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"results.js","sources":["../../../src/cli/commands/results.ts"],"sourcesContent":["/**\n * Results command - display articles from a session in the terminal.\n */\nimport type { Article } from '../../providers/base/types.js';\nimport type { ExportFilter } from './export.js';\n\nexport interface ResultsCommandOptions {\n sessionId: string;\n limit?: number;\n offset?: number;\n json: boolean;\n fields?: string[];\n query?: string;\n filter?: ExportFilter;\n showAbstract: boolean;\n abstractLength?: number;\n}\n\nexport interface CommandLineOptions {\n limit?: string | undefined;\n offset?: string | undefined;\n json?: boolean | undefined;\n fields?: string | undefined;\n query?: string | undefined;\n filterYear?: string | undefined;\n filterTitle?: string | undefined;\n filterAbstract?: string | undefined;\n abstract?: boolean | undefined;\n abstractLength?: string | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\nexport interface FormatOptions {\n sessionId: string;\n sessionName: string;\n total: number;\n offset?: number | undefined;\n filteredFrom?: number | undefined;\n showAbstract?: boolean | undefined;\n abstractLength?: number | undefined;\n}\n\nexport function parseResultsOptions(\n sessionId: string,\n options: CommandLineOptions\n): ResultsCommandOptions {\n const result: ResultsCommandOptions = {\n sessionId,\n json: options.json ?? false,\n showAbstract: options.abstract ?? false,\n };\n\n if (options.abstractLength) {\n result.abstractLength = parseInt(options.abstractLength, 10);\n }\n\n if (options.limit) {\n result.limit = parseInt(options.limit, 10);\n }\n\n if (options.offset) {\n result.offset = parseInt(options.offset, 10);\n }\n\n if (options.fields) {\n result.fields = options.fields.split(',').map((f) => f.trim());\n }\n\n // Handle -q / --query\n if (options.query !== undefined) {\n result.query = options.query;\n }\n\n // Parse legacy filters\n const filter: ExportFilter = {};\n let hasFilter = false;\n\n if (options.filterYear) {\n const parts = options.filterYear.split('-');\n if (parts.length === 2) {\n const from = parseInt(parts[0]!, 10);\n const to = parseInt(parts[1]!, 10);\n if (!Number.isNaN(from)) filter.yearFrom = from;\n if (!Number.isNaN(to)) filter.yearTo = to;\n hasFilter = true;\n } else if (parts.length === 1) {\n const year = parseInt(parts[0]!, 10);\n if (!Number.isNaN(year)) {\n filter.yearFrom = year;\n filter.yearTo = year;\n hasFilter = true;\n }\n }\n }\n\n if (options.filterTitle) {\n filter.titleKeywords = options.filterTitle.split(',').map((s) => s.trim()).filter(Boolean);\n hasFilter = true;\n }\n\n if (options.filterAbstract) {\n filter.abstractKeywords = options.filterAbstract.split(',').map((s) => s.trim()).filter(Boolean);\n hasFilter = true;\n }\n\n if (hasFilter) {\n result.filter = filter;\n }\n\n return result;\n}\n\nexport function validateResultsInput(options: ResultsCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n if (options.limit !== undefined && options.limit < 0) {\n return {\n valid: false,\n error: 'limit must be a non-negative number',\n };\n }\n\n if (options.offset !== undefined && options.offset < 0) {\n return {\n valid: false,\n error: 'offset must be a non-negative number',\n };\n }\n\n // -q and legacy filter flags are mutually exclusive\n if (options.query !== undefined && options.query !== '' && options.filter) {\n return {\n valid: false,\n error: 'Cannot use -q/--query together with --filter-year, --filter-title, or --filter-abstract. Use -q only.',\n };\n }\n\n return { valid: true };\n}\n\nconst DEFAULT_TITLE_MAX_LENGTH = 70;\nconst DEFAULT_ABSTRACT_MAX_LENGTH = 300;\n\nfunction extractYear(publicationDate: string | undefined): number | null {\n if (!publicationDate) return null;\n const year = parseInt(publicationDate.substring(0, 4), 10);\n return Number.isNaN(year) ? null : year;\n}\n\nfunction truncateText(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n return text.substring(0, maxLength - 3) + '...';\n}\n\nfunction truncateTitle(title: string, maxLength: number = DEFAULT_TITLE_MAX_LENGTH): string {\n return truncateText(title, maxLength);\n}\n\nexport function formatResultsList(\n articles: Article[],\n options: FormatOptions\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push(`Results: ${options.sessionName} (${options.sessionId})`);\n\n if (articles.length === 0) {\n lines.push('No articles found.');\n return lines.join('\\n');\n }\n\n // Pagination info\n const offset = options.offset ?? 0;\n const startNum = offset + 1;\n const endNum = offset + articles.length;\n const articleWord = options.total === 1 ? 'article' : 'articles';\n\n let countInfo = `Showing ${startNum}-${endNum} of ${options.total} ${articleWord}`;\n if (options.filteredFrom !== undefined && options.filteredFrom !== options.total) {\n countInfo += ` (filtered from ${options.filteredFrom})`;\n }\n lines.push(countInfo);\n lines.push('');\n\n // Articles\n for (let i = 0; i < articles.length; i++) {\n const article = articles[i]!;\n const num = offset + i + 1;\n const year = extractYear(article.publicationDate);\n const yearStr = year !== null ? String(year) : '----';\n const title = truncateTitle(article.title);\n\n lines.push(`${num.toString().padStart(2)}. [${yearStr}] ${title}`);\n\n if (article.journal) {\n lines.push(` ${article.journal}`);\n }\n\n if (article.doi) {\n lines.push(` DOI: ${article.doi}`);\n }\n\n if (options.showAbstract) {\n lines.push('');\n if (article.abstract && article.abstract.trim() !== '') {\n const maxLength = options.abstractLength ?? DEFAULT_ABSTRACT_MAX_LENGTH;\n const truncatedAbstract = truncateText(article.abstract, maxLength);\n lines.push(` Abstract: ${truncatedAbstract}`);\n } else {\n lines.push(' (No abstract available)');\n }\n }\n\n lines.push('');\n }\n\n // Tip: show query filter hint when not already filtering\n if (options.filteredFrom === undefined) {\n lines.push('Tip: Use -q to filter: results SESSION -q \"author:smith year:2023\"');\n }\n lines.push('Tip: Use check to verify coverage: check SESSION --file known-dois.txt');\n\n return lines.join('\\n').trimEnd();\n}\n\nfunction addYearField(articles: Article[]): (Article & { year: number | null })[] {\n return articles.map((article) => ({\n ...article,\n year: extractYear(article.publicationDate),\n }));\n}\n\nexport function formatResultsJson(articles: Article[]): string {\n const articlesWithYear = addYearField(articles);\n return JSON.stringify(articlesWithYear, null, 2);\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"search-executor.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/search-executor.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAEV,QAAQ,EACR,YAAY,EAEb,MAAM,+BAA+B,CAAC;AAwBvC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAIrE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACnG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,aAAa,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;CACnD;AAOD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAOhF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,MAAM,GACb,QAAQ,GAAG,IAAI,CA2DjB;AAyCD;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,EAC7B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,UAAO,GAClB,OAAO,CAAC,qBAAqB,CAAC,CAuVhC;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,oBAAoB,EAC7B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,WAAW,EAAE,CAAC,CAuExB;AAGD;;GAEG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,oBAAoB,EAC7B,MAAM,EAAE,MAAM,EACd,SAAS,SAAI,GACZ,OAAO,CAAC,aAAa,EAAE,CAAC,CAoF1B"}
1
+ {"version":3,"file":"search-executor.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/search-executor.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAEV,QAAQ,EACR,YAAY,EAEb,MAAM,+BAA+B,CAAC;AAyBvC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAIrE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACnG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,aAAa,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;CACnD;AAOD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAOhF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,MAAM,GACb,QAAQ,GAAG,IAAI,CA2DjB;AA2CD;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,EAC7B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,UAAO,GAClB,OAAO,CAAC,qBAAqB,CAAC,CAwVhC;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,oBAAoB,EAC7B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,WAAW,EAAE,CAAC,CAwExB;AAGD;;GAEG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,oBAAoB,EAC7B,MAAM,EAAE,MAAM,EACd,SAAS,SAAI,GACZ,OAAO,CAAC,aAAa,EAAE,CAAC,CAqF1B"}
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { createHash } from "node:crypto";
4
4
  import { isProviderError } from "../../providers/base/types.js";
5
5
  import { parseQueryString } from "../../query/parser.js";
6
+ import { resolveForProvider } from "../../query/resolver.js";
6
7
  import "../../query/validator.js";
7
8
  import { createSession, updateDatabaseStatus, updateSessionStatus } from "../../session/manager.js";
8
9
  import { MultiProviderProgress } from "../utils/progress.js";
@@ -89,15 +90,16 @@ function createProviderInstance(name, config) {
89
90
  }
90
91
  }
91
92
  function translateQueryForProvider(ast, provider) {
93
+ const resolved = resolveForProvider(ast, provider);
92
94
  switch (provider) {
93
95
  case "pubmed":
94
- return translateQuery$3(ast);
96
+ return translateQuery$3(resolved);
95
97
  case "eric":
96
- return translateQuery$2(ast);
98
+ return translateQuery$2(resolved);
97
99
  case "arxiv":
98
- return translateQuery$1(ast);
100
+ return translateQuery$1(resolved);
99
101
  case "scopus":
100
- return translateQuery(ast);
102
+ return translateQuery(resolved);
101
103
  default:
102
104
  throw new Error(`No translator for provider '${provider}'`);
103
105
  }
@@ -121,13 +123,14 @@ async function executeSearch(options, sessionsDir, config, showProgress = true)
121
123
  name: options.sessionName ?? "direct-query",
122
124
  blocks: [
123
125
  {
126
+ id: "direct",
124
127
  field: "all",
125
128
  terms: { keywords: [options.directQuery] },
126
129
  operator: "AND"
127
130
  }
128
131
  ],
129
132
  filters: {},
130
- overrides: {}
133
+ providers: {}
131
134
  };
132
135
  queryContent = stringify({
133
136
  name: ast.name,
@@ -377,13 +380,14 @@ async function executeCountOnly(options, config) {
377
380
  name: options.sessionName ?? "direct-query",
378
381
  blocks: [
379
382
  {
383
+ id: "direct",
380
384
  field: "all",
381
385
  terms: { keywords: [options.directQuery] },
382
386
  operator: "AND"
383
387
  }
384
388
  ],
385
389
  filters: {},
386
- overrides: {}
390
+ providers: {}
387
391
  };
388
392
  } else if (options.queryFile) {
389
393
  const queryContent = await readFile(options.queryFile, "utf-8");
@@ -432,13 +436,14 @@ async function executePreview(options, config, maxTitles = 5) {
432
436
  name: options.sessionName ?? "direct-query",
433
437
  blocks: [
434
438
  {
439
+ id: "direct",
435
440
  field: "all",
436
441
  terms: { keywords: [options.directQuery] },
437
442
  operator: "AND"
438
443
  }
439
444
  ],
440
445
  filters: {},
441
- overrides: {}
446
+ providers: {}
442
447
  };
443
448
  } else if (options.queryFile) {
444
449
  const queryContent = await readFile(options.queryFile, "utf-8");