@ncukondo/search-hub 0.12.2 → 0.13.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 (90) hide show
  1. package/dist/cli/commands/diff.js +2 -2
  2. package/dist/cli/commands/diff.js.map +1 -1
  3. package/dist/cli/commands/query/init.d.ts +5 -0
  4. package/dist/cli/commands/query/init.d.ts.map +1 -1
  5. package/dist/cli/commands/query/init.js +9 -1
  6. package/dist/cli/commands/query/init.js.map +1 -1
  7. package/dist/cli/commands/query/translate.d.ts.map +1 -1
  8. package/dist/cli/commands/query/translate.js +5 -0
  9. package/dist/cli/commands/query/translate.js.map +1 -1
  10. package/dist/cli/commands/query/validate.d.ts +22 -1
  11. package/dist/cli/commands/query/validate.d.ts.map +1 -1
  12. package/dist/cli/commands/query/validate.js +65 -22
  13. package/dist/cli/commands/query/validate.js.map +1 -1
  14. package/dist/cli/commands/review/extract.d.ts.map +1 -1
  15. package/dist/cli/commands/review/extract.js +1 -2
  16. package/dist/cli/commands/review/extract.js.map +1 -1
  17. package/dist/cli/commands/review/finalize.d.ts.map +1 -1
  18. package/dist/cli/commands/review/finalize.js +1 -2
  19. package/dist/cli/commands/review/finalize.js.map +1 -1
  20. package/dist/cli/commands/review/init.d.ts.map +1 -1
  21. package/dist/cli/commands/review/init.js +2 -5
  22. package/dist/cli/commands/review/init.js.map +1 -1
  23. package/dist/cli/commands/review/merge.d.ts.map +1 -1
  24. package/dist/cli/commands/review/merge.js +1 -2
  25. package/dist/cli/commands/review/merge.js.map +1 -1
  26. package/dist/cli/index.d.ts.map +1 -1
  27. package/dist/cli/index.js +81 -7
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/cli/suggestions/index.d.ts.map +1 -1
  30. package/dist/cli/suggestions/index.js +10 -0
  31. package/dist/cli/suggestions/index.js.map +1 -1
  32. package/dist/cli/suggestions/rules.d.ts.map +1 -1
  33. package/dist/cli/suggestions/rules.js +21 -8
  34. package/dist/cli/suggestions/rules.js.map +1 -1
  35. package/dist/cli/suggestions/types.d.ts +11 -0
  36. package/dist/cli/suggestions/types.d.ts.map +1 -1
  37. package/dist/index.js +5 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/providers/arxiv/translator.d.ts.map +1 -1
  40. package/dist/providers/arxiv/translator.js +5 -2
  41. package/dist/providers/arxiv/translator.js.map +1 -1
  42. package/dist/providers/base/types.d.ts +2 -0
  43. package/dist/providers/base/types.d.ts.map +1 -1
  44. package/dist/providers/base/types.js.map +1 -1
  45. package/dist/providers/base/warnings.d.ts +14 -0
  46. package/dist/providers/base/warnings.d.ts.map +1 -0
  47. package/dist/providers/base/warnings.js +33 -0
  48. package/dist/providers/base/warnings.js.map +1 -0
  49. package/dist/providers/eric/translator.d.ts.map +1 -1
  50. package/dist/providers/eric/translator.js +5 -2
  51. package/dist/providers/eric/translator.js.map +1 -1
  52. package/dist/providers/pubmed/translator.d.ts.map +1 -1
  53. package/dist/providers/pubmed/translator.js +5 -2
  54. package/dist/providers/pubmed/translator.js.map +1 -1
  55. package/dist/providers/scopus/translator.d.ts.map +1 -1
  56. package/dist/providers/scopus/translator.js +22 -5
  57. package/dist/providers/scopus/translator.js.map +1 -1
  58. package/dist/query/__test-helpers__/mock-mesh-client.d.ts +12 -0
  59. package/dist/query/__test-helpers__/mock-mesh-client.d.ts.map +1 -0
  60. package/dist/query/index.d.ts +4 -0
  61. package/dist/query/index.d.ts.map +1 -1
  62. package/dist/query/json-schema.d.ts +3 -0
  63. package/dist/query/json-schema.d.ts.map +1 -0
  64. package/dist/query/json-schema.js +48 -0
  65. package/dist/query/json-schema.js.map +1 -0
  66. package/dist/query/mesh-lookup.d.ts +47 -0
  67. package/dist/query/mesh-lookup.d.ts.map +1 -0
  68. package/dist/query/mesh-lookup.js +151 -0
  69. package/dist/query/mesh-lookup.js.map +1 -0
  70. package/dist/query/parser.js +1 -1
  71. package/dist/query/parser.js.map +1 -1
  72. package/dist/query/types.d.ts +2 -2
  73. package/dist/query/types.d.ts.map +1 -1
  74. package/dist/query/validator.d.ts +5 -5
  75. package/dist/query/validator.d.ts.map +1 -1
  76. package/dist/query/validator.js +5 -2
  77. package/dist/query/validator.js.map +1 -1
  78. package/dist/query/vocab-cache.d.ts +15 -0
  79. package/dist/query/vocab-cache.d.ts.map +1 -0
  80. package/dist/query/vocab-cache.js +44 -0
  81. package/dist/query/vocab-cache.js.map +1 -0
  82. package/dist/query/vocab-validator.d.ts +71 -0
  83. package/dist/query/vocab-validator.d.ts.map +1 -0
  84. package/dist/query/vocab-validator.js +153 -0
  85. package/dist/query/vocab-validator.js.map +1 -0
  86. package/dist/utils/levenshtein.d.ts +6 -0
  87. package/dist/utils/levenshtein.d.ts.map +1 -0
  88. package/dist/utils/levenshtein.js +21 -0
  89. package/dist/utils/levenshtein.js.map +1 -0
  90. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"translator.js","sources":["../../../src/providers/pubmed/translator.ts"],"sourcesContent":["/**\n * PubMed query translator.\n * Converts QueryAST to PubMed E-utilities search syntax.\n */\n\nimport type { QueryAST, FieldType, QueryBlock, Filters } from '../../query/types';\nimport type { TranslatedQuery } from '../base/types';\n\n/**\n * Field type to PubMed qualifier mapping.\n */\nconst FIELD_QUALIFIERS: Record<FieldType, string> = {\n title: 'ti',\n abstract: 'ab',\n title_abstract: 'tiab',\n author: 'au',\n keyword: 'mh',\n all: 'all',\n};\n\n/**\n * Language code to PubMed language name mapping.\n */\nconst LANGUAGE_NAMES: Record<string, string> = {\n en: 'english',\n ja: 'japanese',\n de: 'german',\n fr: 'french',\n es: 'spanish',\n it: 'italian',\n pt: 'portuguese',\n zh: 'chinese',\n ko: 'korean',\n ru: 'russian',\n};\n\n/**\n * Quote a term if it contains spaces and is not already quoted.\n */\nfunction quoteTerm(term: string): string {\n // Already quoted\n if (term.startsWith('\"') && term.endsWith('\"')) {\n return term;\n }\n // Contains spaces - needs quoting\n if (term.includes(' ')) {\n return `\"${term}\"`;\n }\n return term;\n}\n\n/**\n * Translate a single term with field qualifier.\n */\nfunction translateTerm(term: string, qualifier: string): string {\n const quoted = quoteTerm(term);\n return `${quoted}[${qualifier}]`;\n}\n\n/**\n * Translate exclude terms to NOT clause.\n */\nfunction translateExcludeTerms(exclude: string[], qualifier: string): string | null {\n if (exclude.length === 0) {\n return null;\n }\n\n const excludeTerms = exclude.map((term) => translateTerm(term, qualifier));\n\n if (excludeTerms.length === 1) {\n return `NOT ${excludeTerms[0]}`;\n }\n return `NOT (${excludeTerms.join(' OR ')})`;\n}\n\n/**\n * Translate a query block to PubMed syntax.\n * Returns an object with the main query part and optional NOT clause.\n */\nfunction translateBlock(block: QueryBlock): { query: string; notClause: string | null } {\n const qualifier = FIELD_QUALIFIERS[block.field];\n const terms: string[] = [];\n\n // Translate keywords\n for (const keyword of block.terms.keywords) {\n terms.push(translateTerm(keyword, qualifier));\n }\n\n // Translate MeSH terms (always use [mh] regardless of field)\n if (block.terms.mesh) {\n for (const meshTerm of block.terms.mesh) {\n terms.push(translateTerm(meshTerm, 'mh'));\n }\n }\n\n // Build query part\n let query = '';\n if (terms.length === 1) {\n query = `(${terms[0]})`;\n } else if (terms.length > 1) {\n query = `(${terms.join(` ${block.operator} `)})`;\n }\n\n // Translate exclude terms\n const notClause = block.terms.exclude\n ? translateExcludeTerms(block.terms.exclude, qualifier)\n : null;\n\n return { query, notClause };\n}\n\n/**\n * Translate date filters to PubMed syntax.\n */\nfunction translateDateFilters(filters: Filters): string | null {\n const yearFrom = filters.yearFrom ?? 1900;\n const yearTo = filters.yearTo ?? 3000;\n\n if (filters.yearFrom !== undefined || filters.yearTo !== undefined) {\n return `${yearFrom}:${yearTo}[dp]`;\n }\n return null;\n}\n\n/**\n * Translate language filters to PubMed syntax.\n */\nfunction translateLanguageFilters(languages: string[]): string | null {\n if (languages.length === 0) {\n return null;\n }\n\n const langTerms = languages.map((code) => {\n const langName = LANGUAGE_NAMES[code] ?? code;\n return `${langName}[la]`;\n });\n\n if (langTerms.length === 1) {\n return langTerms[0]!;\n }\n return `(${langTerms.join(' OR ')})`;\n}\n\n/**\n * Translate publication type filters to PubMed syntax.\n */\nfunction translatePublicationTypeFilters(\n pubTypes: Filters['publicationTypes']\n): string[] {\n const filters: string[] = [];\n\n if (!pubTypes) {\n return filters;\n }\n\n // Include filters\n if (pubTypes.include && pubTypes.include.length > 0) {\n const includeTerms = pubTypes.include.map(\n (pt) => `\"${pt.toLowerCase()}\"[pt]`\n );\n if (includeTerms.length === 1) {\n filters.push(includeTerms[0]!);\n } else {\n filters.push(`(${includeTerms.join(' OR ')})`);\n }\n }\n\n // Exclude filters - single grouped NOT clause\n if (pubTypes.exclude && pubTypes.exclude.length > 0) {\n const excludeTerms = pubTypes.exclude.map((pt) => `${pt.toLowerCase()}[pt]`);\n if (excludeTerms.length === 1) {\n filters.push(`NOT ${excludeTerms[0]}`);\n } else {\n filters.push(`NOT (${excludeTerms.join(\" OR \")})`);\n }\n }\n\n return filters;\n}\n\n/**\n * Merge global filters with provider-specific overrides.\n */\nfunction mergeFilters(global: Filters, overrides?: Filters): Filters {\n if (!overrides) {\n return global;\n }\n\n return {\n yearFrom: overrides.yearFrom ?? global.yearFrom,\n yearTo: overrides.yearTo ?? global.yearTo,\n languages: overrides.languages ?? global.languages,\n publicationTypes: overrides.publicationTypes\n ? {\n include:\n overrides.publicationTypes.include ??\n global.publicationTypes?.include,\n exclude: [\n ...(global.publicationTypes?.exclude ?? []),\n ...(overrides.publicationTypes.exclude ?? []),\n ],\n }\n : global.publicationTypes,\n };\n}\n\n/**\n * Translate a QueryAST to PubMed search syntax.\n */\nexport function translateQuery(ast: QueryAST): TranslatedQuery {\n // Merge filters with PubMed-specific overrides\n const pubmedOverride = ast.overrides.pubmed;\n const filters = mergeFilters(ast.filters, pubmedOverride?.filters);\n\n // Translate query blocks\n const blockResults = ast.blocks.map((block) => translateBlock(block));\n\n // Collect query parts and NOT clauses\n const blockStrings = blockResults\n .map((r) => r.query)\n .filter((s) => s.length > 0);\n const blockNotClauses = blockResults\n .map((r) => r.notClause)\n .filter((s): s is string => s !== null);\n\n // Build the main query\n const parts: string[] = [];\n\n // Add query blocks (AND'd together)\n if (blockStrings.length > 0) {\n parts.push(blockStrings.join(' AND '));\n }\n\n // Add date filter\n const dateFilter = translateDateFilters(filters);\n if (dateFilter) {\n parts.push(dateFilter);\n }\n\n // Add language filter\n if (filters.languages && filters.languages.length > 0) {\n const langFilter = translateLanguageFilters(filters.languages);\n if (langFilter) {\n parts.push(langFilter);\n }\n }\n\n // Add publication type filters\n const pubTypeFilters = translatePublicationTypeFilters(filters.publicationTypes);\n parts.push(...pubTypeFilters);\n\n // Add block-level NOT clauses (from exclude terms)\n parts.push(...blockNotClauses);\n\n // Separate NOT clauses from AND-joined parts\n // PubMed treats NOT as a standalone binary operator, not AND NOT\n const notParts = parts.filter((p) => p.startsWith('NOT '));\n const andParts = parts.filter((p) => !p.startsWith('NOT '));\n\n const andSection = andParts.join(' AND ');\n const notSection = notParts.join(' ');\n let native: string;\n if (andSection && notSection) {\n native = andSection + ' ' + notSection;\n } else if (notSection) {\n native = notSection;\n } else {\n native = andSection;\n }\n\n return {\n native,\n originalAst: ast,\n provider: 'pubmed',\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"translator.js","sources":["../../../src/providers/pubmed/translator.ts"],"sourcesContent":["/**\n * PubMed query translator.\n * Converts QueryAST to PubMed E-utilities search syntax.\n */\n\nimport type { QueryAST, FieldType, QueryBlock, Filters } from '../../query/types';\nimport type { TranslatedQuery } from '../base/types';\nimport { collectUnsupportedVocabWarnings } from '../base/warnings';\n\n/**\n * Field type to PubMed qualifier mapping.\n */\nconst FIELD_QUALIFIERS: Record<FieldType, string> = {\n title: 'ti',\n abstract: 'ab',\n title_abstract: 'tiab',\n author: 'au',\n keyword: 'mh',\n all: 'all',\n};\n\n/**\n * Language code to PubMed language name mapping.\n */\nconst LANGUAGE_NAMES: Record<string, string> = {\n en: 'english',\n ja: 'japanese',\n de: 'german',\n fr: 'french',\n es: 'spanish',\n it: 'italian',\n pt: 'portuguese',\n zh: 'chinese',\n ko: 'korean',\n ru: 'russian',\n};\n\n/**\n * Quote a term if it contains spaces and is not already quoted.\n */\nfunction quoteTerm(term: string): string {\n // Already quoted\n if (term.startsWith('\"') && term.endsWith('\"')) {\n return term;\n }\n // Contains spaces - needs quoting\n if (term.includes(' ')) {\n return `\"${term}\"`;\n }\n return term;\n}\n\n/**\n * Translate a single term with field qualifier.\n */\nfunction translateTerm(term: string, qualifier: string): string {\n const quoted = quoteTerm(term);\n return `${quoted}[${qualifier}]`;\n}\n\n/**\n * Translate exclude terms to NOT clause.\n */\nfunction translateExcludeTerms(exclude: string[], qualifier: string): string | null {\n if (exclude.length === 0) {\n return null;\n }\n\n const excludeTerms = exclude.map((term) => translateTerm(term, qualifier));\n\n if (excludeTerms.length === 1) {\n return `NOT ${excludeTerms[0]}`;\n }\n return `NOT (${excludeTerms.join(' OR ')})`;\n}\n\n/**\n * Translate a query block to PubMed syntax.\n * Returns an object with the main query part and optional NOT clause.\n */\nfunction translateBlock(block: QueryBlock): { query: string; notClause: string | null } {\n const qualifier = FIELD_QUALIFIERS[block.field];\n const terms: string[] = [];\n\n // Translate keywords\n for (const keyword of block.terms.keywords ?? []) {\n terms.push(translateTerm(keyword, qualifier));\n }\n\n // Translate MeSH terms (always use [mh] regardless of field)\n if (block.terms.mesh) {\n for (const meshTerm of block.terms.mesh) {\n terms.push(translateTerm(meshTerm, 'mh'));\n }\n }\n\n // Build query part\n let query = '';\n if (terms.length === 1) {\n query = `(${terms[0]})`;\n } else if (terms.length > 1) {\n query = `(${terms.join(` ${block.operator} `)})`;\n }\n\n // Translate exclude terms\n const notClause = block.terms.exclude\n ? translateExcludeTerms(block.terms.exclude, qualifier)\n : null;\n\n return { query, notClause };\n}\n\n/**\n * Translate date filters to PubMed syntax.\n */\nfunction translateDateFilters(filters: Filters): string | null {\n const yearFrom = filters.yearFrom ?? 1900;\n const yearTo = filters.yearTo ?? 3000;\n\n if (filters.yearFrom !== undefined || filters.yearTo !== undefined) {\n return `${yearFrom}:${yearTo}[dp]`;\n }\n return null;\n}\n\n/**\n * Translate language filters to PubMed syntax.\n */\nfunction translateLanguageFilters(languages: string[]): string | null {\n if (languages.length === 0) {\n return null;\n }\n\n const langTerms = languages.map((code) => {\n const langName = LANGUAGE_NAMES[code] ?? code;\n return `${langName}[la]`;\n });\n\n if (langTerms.length === 1) {\n return langTerms[0]!;\n }\n return `(${langTerms.join(' OR ')})`;\n}\n\n/**\n * Translate publication type filters to PubMed syntax.\n */\nfunction translatePublicationTypeFilters(\n pubTypes: Filters['publicationTypes']\n): string[] {\n const filters: string[] = [];\n\n if (!pubTypes) {\n return filters;\n }\n\n // Include filters\n if (pubTypes.include && pubTypes.include.length > 0) {\n const includeTerms = pubTypes.include.map(\n (pt) => `\"${pt.toLowerCase()}\"[pt]`\n );\n if (includeTerms.length === 1) {\n filters.push(includeTerms[0]!);\n } else {\n filters.push(`(${includeTerms.join(' OR ')})`);\n }\n }\n\n // Exclude filters - single grouped NOT clause\n if (pubTypes.exclude && pubTypes.exclude.length > 0) {\n const excludeTerms = pubTypes.exclude.map((pt) => `${pt.toLowerCase()}[pt]`);\n if (excludeTerms.length === 1) {\n filters.push(`NOT ${excludeTerms[0]}`);\n } else {\n filters.push(`NOT (${excludeTerms.join(\" OR \")})`);\n }\n }\n\n return filters;\n}\n\n/**\n * Merge global filters with provider-specific overrides.\n */\nfunction mergeFilters(global: Filters, overrides?: Filters): Filters {\n if (!overrides) {\n return global;\n }\n\n return {\n yearFrom: overrides.yearFrom ?? global.yearFrom,\n yearTo: overrides.yearTo ?? global.yearTo,\n languages: overrides.languages ?? global.languages,\n publicationTypes: overrides.publicationTypes\n ? {\n include:\n overrides.publicationTypes.include ??\n global.publicationTypes?.include,\n exclude: [\n ...(global.publicationTypes?.exclude ?? []),\n ...(overrides.publicationTypes.exclude ?? []),\n ],\n }\n : global.publicationTypes,\n };\n}\n\n/**\n * Translate a QueryAST to PubMed search syntax.\n */\nexport function translateQuery(ast: QueryAST): TranslatedQuery {\n // Merge filters with PubMed-specific overrides\n const pubmedOverride = ast.overrides.pubmed;\n const filters = mergeFilters(ast.filters, pubmedOverride?.filters);\n\n // Translate query blocks\n const blockResults = ast.blocks.map((block) => translateBlock(block));\n\n // Collect query parts and NOT clauses\n const blockStrings = blockResults\n .map((r) => r.query)\n .filter((s) => s.length > 0);\n const blockNotClauses = blockResults\n .map((r) => r.notClause)\n .filter((s): s is string => s !== null);\n\n // Build the main query\n const parts: string[] = [];\n\n // Add query blocks (AND'd together)\n if (blockStrings.length > 0) {\n parts.push(blockStrings.join(' AND '));\n }\n\n // Add date filter\n const dateFilter = translateDateFilters(filters);\n if (dateFilter) {\n parts.push(dateFilter);\n }\n\n // Add language filter\n if (filters.languages && filters.languages.length > 0) {\n const langFilter = translateLanguageFilters(filters.languages);\n if (langFilter) {\n parts.push(langFilter);\n }\n }\n\n // Add publication type filters\n const pubTypeFilters = translatePublicationTypeFilters(filters.publicationTypes);\n parts.push(...pubTypeFilters);\n\n // Add block-level NOT clauses (from exclude terms)\n parts.push(...blockNotClauses);\n\n // Separate NOT clauses from AND-joined parts\n // PubMed treats NOT as a standalone binary operator, not AND NOT\n const notParts = parts.filter((p) => p.startsWith('NOT '));\n const andParts = parts.filter((p) => !p.startsWith('NOT '));\n\n const andSection = andParts.join(' AND ');\n const notSection = notParts.join(' ');\n let native: string;\n if (andSection && notSection) {\n native = andSection + ' ' + notSection;\n } else if (notSection) {\n native = notSection;\n } else {\n native = andSection;\n }\n\n // Collect warnings for unsupported controlled vocabulary\n // PubMed supports mesh but not emtree or eric\n const warnings = collectUnsupportedVocabWarnings(ast.blocks, 'PubMed', new Set(['mesh']));\n\n return {\n native,\n originalAst: ast,\n provider: 'pubmed',\n ...(warnings.length > 0 ? { warnings } : {}),\n };\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/translator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAiD,MAAM,mBAAmB,CAAC;AACjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAwHrD;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,QAAQ,GAAG,eAAe,CAuB7D"}
1
+ {"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/translator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAiD,MAAM,mBAAmB,CAAC;AACjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AA4IrD;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,QAAQ,GAAG,eAAe,CA8B7D"}
@@ -1,3 +1,4 @@
1
+ import { collectUnsupportedVocabWarnings } from "../base/warnings.js";
1
2
  const FIELD_MAP = {
2
3
  title: "TITLE",
3
4
  abstract: "ABS",
@@ -42,10 +43,24 @@ function quoteTerm(term) {
42
43
  }
43
44
  function translateBlock(block) {
44
45
  const field = FIELD_MAP[block.field];
45
- const terms = block.terms.keywords.map(quoteTerm);
46
46
  const operator = block.operator;
47
- const termsStr = terms.join(` ${operator} `);
48
- const query = `${field}(${termsStr})`;
47
+ const parts = [];
48
+ const keywords = (block.terms.keywords ?? []).map(quoteTerm);
49
+ if (keywords.length > 0) {
50
+ parts.push(`${field}(${keywords.join(` ${operator} `)})`);
51
+ }
52
+ const emtree = (block.terms.emtree ?? []).map(quoteTerm);
53
+ if (emtree.length > 0) {
54
+ parts.push(`INDEXTERMS(${emtree.join(` ${operator} `)})`);
55
+ }
56
+ let query;
57
+ if (parts.length === 0) {
58
+ query = "";
59
+ } else if (parts.length === 1) {
60
+ query = parts[0];
61
+ } else {
62
+ query = parts.join(` ${operator} `);
63
+ }
49
64
  let notClause = null;
50
65
  if (block.terms.exclude && block.terms.exclude.length > 0) {
51
66
  const excludeTerms = block.terms.exclude.map(quoteTerm);
@@ -74,16 +89,18 @@ function translateFilters(filters, scopusOverrides) {
74
89
  }
75
90
  function translateQuery(ast) {
76
91
  const blockResults = ast.blocks.map(translateBlock);
77
- const blockParts = blockResults.map((r) => r.query);
92
+ const blockParts = blockResults.map((r) => r.query).filter((s) => s.length > 0);
78
93
  const notClauses = blockResults.map((r) => r.notClause).filter((s) => s !== null);
79
94
  const scopusOverrides = ast.overrides.scopus;
80
95
  const filterParts = translateFilters(ast.filters, scopusOverrides);
81
96
  const allParts = [...blockParts, ...notClauses, ...filterParts];
82
97
  const native = allParts.join(" AND ");
98
+ const warnings = collectUnsupportedVocabWarnings(ast.blocks, "Scopus", /* @__PURE__ */ new Set(["emtree"]));
83
99
  return {
84
100
  native,
85
101
  originalAst: ast,
86
- provider: "scopus"
102
+ provider: "scopus",
103
+ ...warnings.length > 0 ? { warnings } : {}
87
104
  };
88
105
  }
89
106
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"translator.js","sources":["../../../src/providers/scopus/translator.ts"],"sourcesContent":["/**\n * Scopus Query Translator\n *\n * Translates QueryAST to Scopus search syntax.\n */\n\nimport type { QueryAST, FieldType, QueryBlock, Filters, OverrideBlock } from '../../query/types';\nimport type { TranslatedQuery } from '../base/types';\n\n/**\n * Field function mappings for Scopus.\n */\nconst FIELD_MAP: Record<FieldType, string> = {\n title: 'TITLE',\n abstract: 'ABS',\n title_abstract: 'TITLE-ABS-KEY',\n author: 'AUTH',\n keyword: 'KEY',\n all: 'ALL',\n};\n\n/**\n * Source type code mappings for Scopus.\n */\nconst SOURCE_TYPE_MAP: Record<string, string> = {\n journal: 'j',\n conference: 'p',\n book: 'b',\n 'book series': 'k',\n 'trade journal': 'd',\n};\n\n/**\n * Language code mappings for Scopus.\n */\nconst LANGUAGE_MAP: Record<string, string> = {\n en: 'english',\n de: 'german',\n fr: 'french',\n es: 'spanish',\n it: 'italian',\n ja: 'japanese',\n zh: 'chinese',\n ko: 'korean',\n pt: 'portuguese',\n ru: 'russian',\n};\n\n/**\n * Check if a term needs to be quoted (contains spaces and isn't already quoted).\n */\nfunction needsQuotes(term: string): boolean {\n if (term.startsWith('\"') && term.endsWith('\"')) {\n return false;\n }\n if (term.startsWith('{') && term.endsWith('}')) {\n return false;\n }\n return term.includes(' ');\n}\n\n/**\n * Quote a term if it contains spaces.\n */\nfunction quoteTerm(term: string): string {\n if (needsQuotes(term)) {\n return `\"${term}\"`;\n }\n return term;\n}\n\n/**\n * Translate a single query block to Scopus syntax.\n * Returns an object with the main query part and optional NOT clause.\n */\nfunction translateBlock(block: QueryBlock): { query: string; notClause: string | null } {\n const field = FIELD_MAP[block.field];\n const terms = block.terms.keywords.map(quoteTerm);\n const operator = block.operator;\n\n const termsStr = terms.join(` ${operator} `);\n const query = `${field}(${termsStr})`;\n\n // Translate exclude terms (without AND prefix - will be added during join)\n let notClause: string | null = null;\n if (block.terms.exclude && block.terms.exclude.length > 0) {\n const excludeTerms = block.terms.exclude.map(quoteTerm);\n const excludeStr = excludeTerms.join(' OR ');\n notClause = `NOT ${field}(${excludeStr})`;\n }\n\n return { query, notClause };\n}\n\n/**\n * Translate filters to Scopus syntax.\n */\nfunction translateFilters(filters: Filters, scopusOverrides?: OverrideBlock): string[] {\n const parts: string[] = [];\n\n // Year filters\n if (filters.yearFrom !== undefined) {\n parts.push(`PUBYEAR > ${filters.yearFrom - 1}`);\n }\n if (filters.yearTo !== undefined) {\n parts.push(`PUBYEAR < ${filters.yearTo + 1}`);\n }\n\n // Language filter\n if (filters.languages && filters.languages.length > 0) {\n const languages = filters.languages\n .map(code => LANGUAGE_MAP[code] || code)\n .join(' OR ');\n parts.push(`LANGUAGE(${languages})`);\n }\n\n // Source type filter from overrides\n if (scopusOverrides?.sourceTypes && scopusOverrides.sourceTypes.length > 0) {\n const sourceTypes = scopusOverrides.sourceTypes\n .map(type => SOURCE_TYPE_MAP[type] || type)\n .join(' OR ');\n parts.push(`SRCTYPE(${sourceTypes})`);\n }\n\n return parts;\n}\n\n/**\n * Translate a QueryAST to Scopus search syntax.\n */\nexport function translateQuery(ast: QueryAST): TranslatedQuery {\n // Translate query blocks\n const blockResults = ast.blocks.map(translateBlock);\n\n // Collect query parts and NOT clauses\n const blockParts = blockResults.map((r) => r.query);\n const notClauses = blockResults\n .map((r) => r.notClause)\n .filter((s): s is string => s !== null);\n\n // Translate filters\n const scopusOverrides = ast.overrides.scopus;\n const filterParts = translateFilters(ast.filters, scopusOverrides);\n\n // Build native query: blocks AND NOT(excludes) AND filters\n const allParts: string[] = [...blockParts, ...notClauses, ...filterParts];\n const native = allParts.join(' AND ');\n\n return {\n native,\n originalAst: ast,\n provider: 'scopus',\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"translator.js","sources":["../../../src/providers/scopus/translator.ts"],"sourcesContent":["/**\n * Scopus Query Translator\n *\n * Translates QueryAST to Scopus search syntax.\n */\n\nimport type { QueryAST, FieldType, QueryBlock, Filters, OverrideBlock } from '../../query/types';\nimport type { TranslatedQuery } from '../base/types';\nimport { collectUnsupportedVocabWarnings } from '../base/warnings';\n\n/**\n * Field function mappings for Scopus.\n */\nconst FIELD_MAP: Record<FieldType, string> = {\n title: 'TITLE',\n abstract: 'ABS',\n title_abstract: 'TITLE-ABS-KEY',\n author: 'AUTH',\n keyword: 'KEY',\n all: 'ALL',\n};\n\n/**\n * Source type code mappings for Scopus.\n */\nconst SOURCE_TYPE_MAP: Record<string, string> = {\n journal: 'j',\n conference: 'p',\n book: 'b',\n 'book series': 'k',\n 'trade journal': 'd',\n};\n\n/**\n * Language code mappings for Scopus.\n */\nconst LANGUAGE_MAP: Record<string, string> = {\n en: 'english',\n de: 'german',\n fr: 'french',\n es: 'spanish',\n it: 'italian',\n ja: 'japanese',\n zh: 'chinese',\n ko: 'korean',\n pt: 'portuguese',\n ru: 'russian',\n};\n\n/**\n * Check if a term needs to be quoted (contains spaces and isn't already quoted).\n */\nfunction needsQuotes(term: string): boolean {\n if (term.startsWith('\"') && term.endsWith('\"')) {\n return false;\n }\n if (term.startsWith('{') && term.endsWith('}')) {\n return false;\n }\n return term.includes(' ');\n}\n\n/**\n * Quote a term if it contains spaces.\n */\nfunction quoteTerm(term: string): string {\n if (needsQuotes(term)) {\n return `\"${term}\"`;\n }\n return term;\n}\n\n/**\n * Translate a single query block to Scopus syntax.\n * Returns an object with the main query part and optional NOT clause.\n */\nfunction translateBlock(block: QueryBlock): { query: string; notClause: string | null } {\n const field = FIELD_MAP[block.field];\n const operator = block.operator;\n const parts: string[] = [];\n\n // Translate keywords\n const keywords = (block.terms.keywords ?? []).map(quoteTerm);\n if (keywords.length > 0) {\n parts.push(`${field}(${keywords.join(` ${operator} `)})`);\n }\n\n // Translate Emtree terms (always use INDEXTERMS)\n const emtree = (block.terms.emtree ?? []).map(quoteTerm);\n if (emtree.length > 0) {\n parts.push(`INDEXTERMS(${emtree.join(` ${operator} `)})`);\n }\n\n // Combine parts (empty string when no supported terms)\n let query: string;\n if (parts.length === 0) {\n query = '';\n } else if (parts.length === 1) {\n query = parts[0]!;\n } else {\n query = parts.join(` ${operator} `);\n }\n\n // Translate exclude terms (without AND prefix - will be added during join)\n let notClause: string | null = null;\n if (block.terms.exclude && block.terms.exclude.length > 0) {\n const excludeTerms = block.terms.exclude.map(quoteTerm);\n const excludeStr = excludeTerms.join(' OR ');\n notClause = `NOT ${field}(${excludeStr})`;\n }\n\n return { query, notClause };\n}\n\n/**\n * Translate filters to Scopus syntax.\n */\nfunction translateFilters(filters: Filters, scopusOverrides?: OverrideBlock): string[] {\n const parts: string[] = [];\n\n // Year filters\n if (filters.yearFrom !== undefined) {\n parts.push(`PUBYEAR > ${filters.yearFrom - 1}`);\n }\n if (filters.yearTo !== undefined) {\n parts.push(`PUBYEAR < ${filters.yearTo + 1}`);\n }\n\n // Language filter\n if (filters.languages && filters.languages.length > 0) {\n const languages = filters.languages\n .map(code => LANGUAGE_MAP[code] || code)\n .join(' OR ');\n parts.push(`LANGUAGE(${languages})`);\n }\n\n // Source type filter from overrides\n if (scopusOverrides?.sourceTypes && scopusOverrides.sourceTypes.length > 0) {\n const sourceTypes = scopusOverrides.sourceTypes\n .map(type => SOURCE_TYPE_MAP[type] || type)\n .join(' OR ');\n parts.push(`SRCTYPE(${sourceTypes})`);\n }\n\n return parts;\n}\n\n/**\n * Translate a QueryAST to Scopus search syntax.\n */\nexport function translateQuery(ast: QueryAST): TranslatedQuery {\n // Translate query blocks\n const blockResults = ast.blocks.map(translateBlock);\n\n // Collect query parts (filter empty blocks) and NOT clauses\n const blockParts = blockResults\n .map((r) => r.query)\n .filter((s) => s.length > 0);\n const notClauses = blockResults\n .map((r) => r.notClause)\n .filter((s): s is string => s !== null);\n\n // Translate filters\n const scopusOverrides = ast.overrides.scopus;\n const filterParts = translateFilters(ast.filters, scopusOverrides);\n\n // Build native query: blocks AND NOT(excludes) AND filters\n const allParts: string[] = [...blockParts, ...notClauses, ...filterParts];\n const native = allParts.join(' AND ');\n\n // Collect warnings for unsupported controlled vocabulary\n // Scopus supports emtree but not mesh or eric\n const warnings = collectUnsupportedVocabWarnings(ast.blocks, 'Scopus', new Set(['emtree']));\n\n return {\n native,\n originalAst: ast,\n provider: 'scopus',\n ...(warnings.length > 0 ? { warnings } : {}),\n };\n}\n"],"names":[],"mappings":";AAaA,MAAM,YAAuC;AAAA,EAC3C,OAAO;AAAA,EACP,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,KAAK;AACP;AAKA,MAAM,kBAA0C;AAAA,EAC9C,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,eAAe;AAAA,EACf,iBAAiB;AACnB;AAKA,MAAM,eAAuC;AAAA,EAC3C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAKA,SAAS,YAAY,MAAuB;AAC1C,MAAI,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,GAAG;AAC9C,WAAO;AAAA,EACT;AACA,MAAI,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,GAAG;AAC9C,WAAO;AAAA,EACT;AACA,SAAO,KAAK,SAAS,GAAG;AAC1B;AAKA,SAAS,UAAU,MAAsB;AACvC,MAAI,YAAY,IAAI,GAAG;AACrB,WAAO,IAAI,IAAI;AAAA,EACjB;AACA,SAAO;AACT;AAMA,SAAS,eAAe,OAAgE;AACtF,QAAM,QAAQ,UAAU,MAAM,KAAK;AACnC,QAAM,WAAW,MAAM;AACvB,QAAM,QAAkB,CAAA;AAGxB,QAAM,YAAY,MAAM,MAAM,YAAY,CAAA,GAAI,IAAI,SAAS;AAC3D,MAAI,SAAS,SAAS,GAAG;AACvB,UAAM,KAAK,GAAG,KAAK,IAAI,SAAS,KAAK,IAAI,QAAQ,GAAG,CAAC,GAAG;AAAA,EAC1D;AAGA,QAAM,UAAU,MAAM,MAAM,UAAU,CAAA,GAAI,IAAI,SAAS;AACvD,MAAI,OAAO,SAAS,GAAG;AACrB,UAAM,KAAK,cAAc,OAAO,KAAK,IAAI,QAAQ,GAAG,CAAC,GAAG;AAAA,EAC1D;AAGA,MAAI;AACJ,MAAI,MAAM,WAAW,GAAG;AACtB,YAAQ;AAAA,EACV,WAAW,MAAM,WAAW,GAAG;AAC7B,YAAQ,MAAM,CAAC;AAAA,EACjB,OAAO;AACL,YAAQ,MAAM,KAAK,IAAI,QAAQ,GAAG;AAAA,EACpC;AAGA,MAAI,YAA2B;AAC/B,MAAI,MAAM,MAAM,WAAW,MAAM,MAAM,QAAQ,SAAS,GAAG;AACzD,UAAM,eAAe,MAAM,MAAM,QAAQ,IAAI,SAAS;AACtD,UAAM,aAAa,aAAa,KAAK,MAAM;AAC3C,gBAAY,OAAO,KAAK,IAAI,UAAU;AAAA,EACxC;AAEA,SAAO,EAAE,OAAO,UAAA;AAClB;AAKA,SAAS,iBAAiB,SAAkB,iBAA2C;AACrF,QAAM,QAAkB,CAAA;AAGxB,MAAI,QAAQ,aAAa,QAAW;AAClC,UAAM,KAAK,aAAa,QAAQ,WAAW,CAAC,EAAE;AAAA,EAChD;AACA,MAAI,QAAQ,WAAW,QAAW;AAChC,UAAM,KAAK,aAAa,QAAQ,SAAS,CAAC,EAAE;AAAA,EAC9C;AAGA,MAAI,QAAQ,aAAa,QAAQ,UAAU,SAAS,GAAG;AACrD,UAAM,YAAY,QAAQ,UACvB,IAAI,CAAA,SAAQ,aAAa,IAAI,KAAK,IAAI,EACtC,KAAK,MAAM;AACd,UAAM,KAAK,YAAY,SAAS,GAAG;AAAA,EACrC;AAGA,MAAI,iBAAiB,eAAe,gBAAgB,YAAY,SAAS,GAAG;AAC1E,UAAM,cAAc,gBAAgB,YACjC,IAAI,CAAA,SAAQ,gBAAgB,IAAI,KAAK,IAAI,EACzC,KAAK,MAAM;AACd,UAAM,KAAK,WAAW,WAAW,GAAG;AAAA,EACtC;AAEA,SAAO;AACT;AAKO,SAAS,eAAe,KAAgC;AAE7D,QAAM,eAAe,IAAI,OAAO,IAAI,cAAc;AAGlD,QAAM,aAAa,aAChB,IAAI,CAAC,MAAM,EAAE,KAAK,EAClB,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC7B,QAAM,aAAa,aAChB,IAAI,CAAC,MAAM,EAAE,SAAS,EACtB,OAAO,CAAC,MAAmB,MAAM,IAAI;AAGxC,QAAM,kBAAkB,IAAI,UAAU;AACtC,QAAM,cAAc,iBAAiB,IAAI,SAAS,eAAe;AAGjE,QAAM,WAAqB,CAAC,GAAG,YAAY,GAAG,YAAY,GAAG,WAAW;AACxE,QAAM,SAAS,SAAS,KAAK,OAAO;AAIpC,QAAM,WAAW,gCAAgC,IAAI,QAAQ,8BAAc,IAAI,CAAC,QAAQ,CAAC,CAAC;AAE1F,SAAO;AAAA,IACL;AAAA,IACA,aAAa;AAAA,IACb,UAAU;AAAA,IACV,GAAI,SAAS,SAAS,IAAI,EAAE,SAAA,IAAa,CAAA;AAAA,EAAC;AAE9C;"}
@@ -0,0 +1,12 @@
1
+ import { MeSHLookupClient } from '../mesh-lookup.js';
2
+ /**
3
+ * Create a mock MeSH lookup client for testing.
4
+ *
5
+ * @param results - Map of term to lookup result
6
+ * @returns A mock MeSHLookupClient
7
+ */
8
+ export declare function createMockMeSHClient(results: Map<string, {
9
+ found: boolean;
10
+ suggestions?: string[];
11
+ }>): MeSHLookupClient;
12
+ //# sourceMappingURL=mock-mesh-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-mesh-client.d.ts","sourceRoot":"","sources":["../../../src/query/__test-helpers__/mock-mesh-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,GAC/D,gBAAgB,CAWlB"}
@@ -9,4 +9,8 @@
9
9
  export type { FieldType, Operator, ProviderName, TermBlock, QueryBlock, Filters, PublicationTypeFilter, OverrideBlock, QueryAST, } from './types.js';
10
10
  export { parseQueryFile, parseQueryString } from './parser.js';
11
11
  export { validateQueryFile, formatValidationErrors, ValidationError, fieldTypeSchema, termBlockSchema, queryBlockSchema, filtersSchema, overrideBlockSchema, queryFileSchema, } from './validator.js';
12
+ export { MeSHLookupClient } from './mesh-lookup.js';
13
+ export type { MeSHLookupResult } from './mesh-lookup.js';
14
+ export { extractControlledVocabTerms, validateControlledVocab, } from './vocab-validator.js';
15
+ export type { VocabTerm, VocabTermError, VocabTermResult, VocabValidationResult, } from './vocab-validator.js';
12
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,YAAY,EACV,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,UAAU,EACV,OAAO,EACP,qBAAqB,EACrB,aAAa,EACb,QAAQ,GACT,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/D,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EAEf,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,eAAe,GAChB,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,YAAY,EACV,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,UAAU,EACV,OAAO,EACP,qBAAqB,EACrB,aAAa,EACb,QAAQ,GACT,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/D,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EAEf,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,qBAAqB,GACtB,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Generate a JSON Schema from the query file Zod schema. */
2
+ export declare function generateQueryJSONSchema(): Record<string, unknown>;
3
+ //# sourceMappingURL=json-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-schema.d.ts","sourceRoot":"","sources":["../../src/query/json-schema.ts"],"names":[],"mappings":"AAsEA,6DAA6D;AAC7D,wBAAgB,uBAAuB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIjE"}
@@ -0,0 +1,48 @@
1
+ import * as z from "zod";
2
+ import { publicationTypeFilterSchema, operatorSchema, fieldTypeSchema } from "./validator.js";
3
+ const filtersInputSchema = z.object({
4
+ year_from: z.number().int().optional(),
5
+ year_to: z.number().int().optional(),
6
+ language: z.array(z.string()).optional(),
7
+ publication_types: publicationTypeFilterSchema.optional()
8
+ }).optional();
9
+ const overrideBlockInputSchema = z.object({
10
+ filters: filtersInputSchema,
11
+ categories: z.array(z.string()).optional(),
12
+ source_types: z.array(z.string()).optional()
13
+ }).optional();
14
+ const termBlockInputSchema = z.object({
15
+ keywords: z.array(z.string()).min(1).optional(),
16
+ mesh: z.array(z.string()).optional(),
17
+ emtree: z.array(z.string()).optional(),
18
+ eric: z.array(z.string()).optional(),
19
+ exclude: z.array(z.string()).optional()
20
+ });
21
+ const queryBlockInputSchema = z.object({
22
+ field: fieldTypeSchema,
23
+ terms: termBlockInputSchema,
24
+ operator: operatorSchema
25
+ });
26
+ const queryFileInputSchema = z.object({
27
+ name: z.string().min(1),
28
+ description: z.string().optional(),
29
+ query: z.array(queryBlockInputSchema).min(1),
30
+ filters: filtersInputSchema,
31
+ overrides: z.object({
32
+ pubmed: overrideBlockInputSchema,
33
+ scopus: overrideBlockInputSchema,
34
+ eric: overrideBlockInputSchema,
35
+ arxiv: overrideBlockInputSchema,
36
+ wos: overrideBlockInputSchema,
37
+ embase: overrideBlockInputSchema
38
+ }).optional()
39
+ });
40
+ function generateQueryJSONSchema() {
41
+ return z.toJSONSchema(queryFileInputSchema, {
42
+ target: "draft-2020-12"
43
+ });
44
+ }
45
+ export {
46
+ generateQueryJSONSchema
47
+ };
48
+ //# sourceMappingURL=json-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-schema.js","sources":["../../src/query/json-schema.ts"],"sourcesContent":["/**\n * JSON Schema generation for query YAML files.\n *\n * Defines an input-only schema (without transforms) that mirrors the structure\n * of queryFileSchema from validator.ts. This is necessary because Zod v4's\n * z.toJSONSchema() cannot handle .transform() calls.\n *\n * The generated JSON Schema enables editor autocompletion and validation\n * via the yaml-language-server $schema comment.\n */\nimport * as z from 'zod';\nimport {\n fieldTypeSchema,\n operatorSchema,\n publicationTypeFilterSchema,\n} from './validator.js';\n\n/** Filters input schema (without transform) */\nconst filtersInputSchema = z\n .object({\n year_from: z.number().int().optional(),\n year_to: z.number().int().optional(),\n language: z.array(z.string()).optional(),\n publication_types: publicationTypeFilterSchema.optional(),\n })\n .optional();\n\n/** Override block input schema (without transform) */\nconst overrideBlockInputSchema = z\n .object({\n filters: filtersInputSchema,\n categories: z.array(z.string()).optional(),\n source_types: z.array(z.string()).optional(),\n })\n .optional();\n\n/** Term block input schema (without refine) */\nconst termBlockInputSchema = z.object({\n keywords: z.array(z.string()).min(1).optional(),\n mesh: z.array(z.string()).optional(),\n emtree: z.array(z.string()).optional(),\n eric: z.array(z.string()).optional(),\n exclude: z.array(z.string()).optional(),\n});\n\n/** Query block input schema */\nconst queryBlockInputSchema = z.object({\n field: fieldTypeSchema,\n terms: termBlockInputSchema,\n operator: operatorSchema,\n});\n\n/** Query file input schema (without transform) - mirrors queryFileSchema input */\nconst queryFileInputSchema = z.object({\n name: z.string().min(1),\n description: z.string().optional(),\n query: z.array(queryBlockInputSchema).min(1),\n filters: filtersInputSchema,\n overrides: z\n .object({\n pubmed: overrideBlockInputSchema,\n scopus: overrideBlockInputSchema,\n eric: overrideBlockInputSchema,\n arxiv: overrideBlockInputSchema,\n wos: overrideBlockInputSchema,\n embase: overrideBlockInputSchema,\n })\n .optional(),\n});\n\n/** Generate a JSON Schema from the query file Zod schema. */\nexport function generateQueryJSONSchema(): Record<string, unknown> {\n return z.toJSONSchema(queryFileInputSchema, {\n target: 'draft-2020-12',\n });\n}\n"],"names":[],"mappings":";;AAkBA,MAAM,qBAAqB,EACxB,OAAO;AAAA,EACN,WAAW,EAAE,OAAA,EAAS,IAAA,EAAM,SAAA;AAAA,EAC5B,SAAS,EAAE,OAAA,EAAS,IAAA,EAAM,SAAA;AAAA,EAC1B,UAAU,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AAAA,EAC9B,mBAAmB,4BAA4B,SAAA;AACjD,CAAC,EACA,SAAA;AAGH,MAAM,2BAA2B,EAC9B,OAAO;AAAA,EACN,SAAS;AAAA,EACT,YAAY,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AAAA,EAChC,cAAc,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AACpC,CAAC,EACA,SAAA;AAGH,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,UAAU,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,IAAI,CAAC,EAAE,SAAA;AAAA,EACrC,MAAM,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AAAA,EAC1B,QAAQ,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AAAA,EAC5B,MAAM,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AAAA,EAC1B,SAAS,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AAC/B,CAAC;AAGD,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,OAAO;AAAA,EACP,OAAO;AAAA,EACP,UAAU;AACZ,CAAC;AAGD,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,MAAM,EAAE,SAAS,IAAI,CAAC;AAAA,EACtB,aAAa,EAAE,OAAA,EAAS,SAAA;AAAA,EACxB,OAAO,EAAE,MAAM,qBAAqB,EAAE,IAAI,CAAC;AAAA,EAC3C,SAAS;AAAA,EACT,WAAW,EACR,OAAO;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,IACP,KAAK;AAAA,IACL,QAAQ;AAAA,EAAA,CACT,EACA,SAAA;AACL,CAAC;AAGM,SAAS,0BAAmD;AACjE,SAAO,EAAE,aAAa,sBAAsB;AAAA,IAC1C,QAAQ;AAAA,EAAA,CACT;AACH;"}
@@ -0,0 +1,47 @@
1
+ import { RateLimiter } from '../providers/base/rate-limiter.js';
2
+ import { VocabCache } from './vocab-cache.js';
3
+ /**
4
+ * Result of a MeSH term lookup.
5
+ */
6
+ export interface MeSHLookupResult {
7
+ /** The term that was looked up */
8
+ term: string;
9
+ /** Whether the term was found as a valid MeSH heading */
10
+ found: boolean;
11
+ /** Suggested terms if the lookup term was not found */
12
+ suggestions?: string[];
13
+ }
14
+ /**
15
+ * Client for the NLM MeSH Lookup API.
16
+ */
17
+ export declare class MeSHLookupClient {
18
+ private readonly rateLimiter;
19
+ private readonly timeoutMs;
20
+ private readonly cache;
21
+ constructor(options?: {
22
+ rateLimiter?: RateLimiter;
23
+ timeoutMs?: number;
24
+ cache?: VocabCache;
25
+ });
26
+ /**
27
+ * Look up a single MeSH term.
28
+ *
29
+ * Tries multiple match strategies in order:
30
+ * 1. exact — exact match
31
+ * 2. startsWith (full term) — prefix match
32
+ * 2b. startsWith (truncated) — suffix typo recovery (1-3 chars removed)
33
+ * 2c. startsWith (word1 + word2 prefix) — multi-word progressive prefix (max 3 calls)
34
+ * 3. contains (full term) — substring match
35
+ * 4. startsWith (first word, limit=25) — re-ranked by Levenshtein distance
36
+ *
37
+ * Returns on the first strategy that produces results.
38
+ * Results are cached when a VocabCache is provided.
39
+ */
40
+ lookupTerm(term: string): Promise<MeSHLookupResult>;
41
+ /**
42
+ * Look up multiple MeSH terms.
43
+ */
44
+ lookupTerms(terms: string[]): Promise<MeSHLookupResult[]>;
45
+ private fetchLookup;
46
+ }
47
+ //# sourceMappingURL=mesh-lookup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mesh-lookup.d.ts","sourceRoot":"","sources":["../../src/query/mesh-lookup.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,KAAK,EAAE,OAAO,CAAC;IACf,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAOD;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA0B;IACtD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;gBAEnC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,WAAW,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,UAAU,CAAA;KAAE;IAM3F;;;;;;;;;;;;;OAaG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA4GzD;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;YAQjD,WAAW;CAoC1B"}
@@ -0,0 +1,151 @@
1
+ import { levenshteinDistance } from "../utils/levenshtein.js";
2
+ const MESH_LOOKUP_BASE_URL = "https://id.nlm.nih.gov/mesh/lookup/term";
3
+ const DEFAULT_TIMEOUT_MS = 1e4;
4
+ class MeSHLookupClient {
5
+ rateLimiter;
6
+ timeoutMs;
7
+ cache;
8
+ constructor(options) {
9
+ this.rateLimiter = options?.rateLimiter;
10
+ this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
11
+ this.cache = options?.cache;
12
+ }
13
+ /**
14
+ * Look up a single MeSH term.
15
+ *
16
+ * Tries multiple match strategies in order:
17
+ * 1. exact — exact match
18
+ * 2. startsWith (full term) — prefix match
19
+ * 2b. startsWith (truncated) — suffix typo recovery (1-3 chars removed)
20
+ * 2c. startsWith (word1 + word2 prefix) — multi-word progressive prefix (max 3 calls)
21
+ * 3. contains (full term) — substring match
22
+ * 4. startsWith (first word, limit=25) — re-ranked by Levenshtein distance
23
+ *
24
+ * Returns on the first strategy that produces results.
25
+ * Results are cached when a VocabCache is provided.
26
+ */
27
+ async lookupTerm(term) {
28
+ if (this.cache) {
29
+ const cached = this.cache.get("mesh", term);
30
+ if (cached) {
31
+ return cached;
32
+ }
33
+ }
34
+ const exactResults = await this.fetchLookup(term, "exact", 1);
35
+ if (exactResults.length > 0) {
36
+ const result2 = { term, found: true };
37
+ this.cache?.set("mesh", term, result2);
38
+ return result2;
39
+ }
40
+ const startsWithResults = await this.fetchLookup(term, "startswith", 5);
41
+ if (startsWithResults.length > 0) {
42
+ const result2 = {
43
+ term,
44
+ found: false,
45
+ suggestions: startsWithResults.map((s) => s.label)
46
+ };
47
+ this.cache?.set("mesh", term, result2);
48
+ return result2;
49
+ }
50
+ if (term.length > 3) {
51
+ for (let len = term.length - 1; len >= Math.max(term.length - 3, 3); len--) {
52
+ const truncated = term.slice(0, len);
53
+ const truncatedResults = await this.fetchLookup(truncated, "startswith", 5);
54
+ if (truncatedResults.length > 0) {
55
+ const result2 = {
56
+ term,
57
+ found: false,
58
+ suggestions: truncatedResults.map((s) => s.label)
59
+ };
60
+ this.cache?.set("mesh", term, result2);
61
+ return result2;
62
+ }
63
+ }
64
+ }
65
+ const words = term.split(/\s+/);
66
+ if (words.length >= 2 && words[1].length > 3) {
67
+ const startN = Math.min(words[1].length - 4, words[1].length - 1);
68
+ const endN = 3;
69
+ let iterations = 0;
70
+ for (let n = startN; n >= endN && iterations < 3; n--, iterations++) {
71
+ const prefix = words[0] + " " + words[1].slice(0, n);
72
+ const prefixResults = await this.fetchLookup(prefix, "startswith", 5);
73
+ if (prefixResults.length > 0) {
74
+ const result2 = {
75
+ term,
76
+ found: false,
77
+ suggestions: prefixResults.map((s) => s.label)
78
+ };
79
+ this.cache?.set("mesh", term, result2);
80
+ return result2;
81
+ }
82
+ }
83
+ }
84
+ const containsResults = await this.fetchLookup(term, "contains", 5);
85
+ if (containsResults.length > 0) {
86
+ const result2 = {
87
+ term,
88
+ found: false,
89
+ suggestions: containsResults.map((s) => s.label)
90
+ };
91
+ this.cache?.set("mesh", term, result2);
92
+ return result2;
93
+ }
94
+ if (words.length > 1) {
95
+ const firstWord = words[0];
96
+ const firstWordResults = await this.fetchLookup(firstWord, "startswith", 25);
97
+ if (firstWordResults.length > 0) {
98
+ const ranked = firstWordResults.map((s) => ({
99
+ label: s.label,
100
+ distance: levenshteinDistance(term.toLowerCase(), s.label.toLowerCase())
101
+ })).sort((a, b) => a.distance - b.distance).slice(0, 5).map((s) => s.label);
102
+ const result2 = { term, found: false, suggestions: ranked };
103
+ this.cache?.set("mesh", term, result2);
104
+ return result2;
105
+ }
106
+ }
107
+ const result = { term, found: false };
108
+ this.cache?.set("mesh", term, result);
109
+ return result;
110
+ }
111
+ /**
112
+ * Look up multiple MeSH terms.
113
+ */
114
+ async lookupTerms(terms) {
115
+ const results = [];
116
+ for (const term of terms) {
117
+ results.push(await this.lookupTerm(term));
118
+ }
119
+ return results;
120
+ }
121
+ async fetchLookup(label, match, limit) {
122
+ if (this.rateLimiter) {
123
+ await this.rateLimiter.acquire();
124
+ }
125
+ const params = new URLSearchParams({
126
+ label,
127
+ match,
128
+ limit: String(limit)
129
+ });
130
+ const url = `${MESH_LOOKUP_BASE_URL}?${params.toString()}`;
131
+ let response;
132
+ try {
133
+ response = await fetch(url, {
134
+ signal: AbortSignal.timeout(this.timeoutMs)
135
+ });
136
+ } catch (error) {
137
+ const message = error instanceof Error ? error.message : "Unknown error";
138
+ throw new Error(`MeSH lookup failed: ${message}`);
139
+ }
140
+ if (!response.ok) {
141
+ throw new Error(
142
+ `MeSH lookup failed: HTTP ${response.status} ${response.statusText}`
143
+ );
144
+ }
145
+ return await response.json();
146
+ }
147
+ }
148
+ export {
149
+ MeSHLookupClient
150
+ };
151
+ //# sourceMappingURL=mesh-lookup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mesh-lookup.js","sources":["../../src/query/mesh-lookup.ts"],"sourcesContent":["/**\n * MeSH Lookup API client.\n *\n * Validates MeSH (Medical Subject Headings) terms against the NLM MeSH Lookup API.\n * No API key required.\n *\n * API docs: https://id.nlm.nih.gov/mesh/lookup/term\n */\n\nimport type { RateLimiter } from '../providers/base/rate-limiter.js';\nimport type { VocabCache } from './vocab-cache.js';\nimport { levenshteinDistance } from '../utils/levenshtein.js';\n\nconst MESH_LOOKUP_BASE_URL = 'https://id.nlm.nih.gov/mesh/lookup/term';\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * Result of a MeSH term lookup.\n */\nexport interface MeSHLookupResult {\n /** The term that was looked up */\n term: string;\n /** Whether the term was found as a valid MeSH heading */\n found: boolean;\n /** Suggested terms if the lookup term was not found */\n suggestions?: string[];\n}\n\ninterface MeSHApiEntry {\n resource: string;\n label: string;\n}\n\n/**\n * Client for the NLM MeSH Lookup API.\n */\nexport class MeSHLookupClient {\n private readonly rateLimiter: RateLimiter | undefined;\n private readonly timeoutMs: number;\n private readonly cache: VocabCache | undefined;\n\n constructor(options?: { rateLimiter?: RateLimiter; timeoutMs?: number; cache?: VocabCache }) {\n this.rateLimiter = options?.rateLimiter;\n this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.cache = options?.cache;\n }\n\n /**\n * Look up a single MeSH term.\n *\n * Tries multiple match strategies in order:\n * 1. exact — exact match\n * 2. startsWith (full term) — prefix match\n * 2b. startsWith (truncated) — suffix typo recovery (1-3 chars removed)\n * 2c. startsWith (word1 + word2 prefix) — multi-word progressive prefix (max 3 calls)\n * 3. contains (full term) — substring match\n * 4. startsWith (first word, limit=25) — re-ranked by Levenshtein distance\n *\n * Returns on the first strategy that produces results.\n * Results are cached when a VocabCache is provided.\n */\n async lookupTerm(term: string): Promise<MeSHLookupResult> {\n // Check cache first\n if (this.cache) {\n const cached = this.cache.get('mesh', term);\n if (cached) {\n return cached;\n }\n }\n\n // 1. Try exact match first\n const exactResults = await this.fetchLookup(term, 'exact', 1);\n\n if (exactResults.length > 0) {\n const result: MeSHLookupResult = { term, found: true };\n this.cache?.set('mesh', term, result);\n return result;\n }\n\n // 2. Try startsWith (full term) for suggestions\n const startsWithResults = await this.fetchLookup(term, 'startswith', 5);\n\n if (startsWithResults.length > 0) {\n const result: MeSHLookupResult = {\n term,\n found: false,\n suggestions: startsWithResults.map((s) => s.label),\n };\n this.cache?.set('mesh', term, result);\n return result;\n }\n\n // 2b. Try startsWith with progressively shorter input (handles suffix typos)\n if (term.length > 3) {\n for (let len = term.length - 1; len >= Math.max(term.length - 3, 3); len--) {\n const truncated = term.slice(0, len);\n const truncatedResults = await this.fetchLookup(truncated, 'startswith', 5);\n if (truncatedResults.length > 0) {\n const result: MeSHLookupResult = {\n term,\n found: false,\n suggestions: truncatedResults.map((s) => s.label),\n };\n this.cache?.set('mesh', term, result);\n return result;\n }\n }\n }\n\n // 2c. Multi-word progressive prefix: try word1 + word2.slice(0, N)\n const words = term.split(/\\s+/);\n if (words.length >= 2 && words[1]!.length > 3) {\n const startN = Math.min(words[1]!.length - 4, words[1]!.length - 1);\n const endN = 3;\n let iterations = 0;\n for (let n = startN; n >= endN && iterations < 3; n--, iterations++) {\n const prefix = words[0]! + ' ' + words[1]!.slice(0, n);\n const prefixResults = await this.fetchLookup(prefix, 'startswith', 5);\n if (prefixResults.length > 0) {\n const result: MeSHLookupResult = {\n term,\n found: false,\n suggestions: prefixResults.map((s) => s.label),\n };\n this.cache?.set('mesh', term, result);\n return result;\n }\n }\n }\n\n // 3. Try contains (full term) for typos and variant spellings\n const containsResults = await this.fetchLookup(term, 'contains', 5);\n\n if (containsResults.length > 0) {\n const result: MeSHLookupResult = {\n term,\n found: false,\n suggestions: containsResults.map((s) => s.label),\n };\n this.cache?.set('mesh', term, result);\n return result;\n }\n\n // 4. Try startsWith with first word only (for multi-word terms)\n // Fetch up to 25 results and re-rank by Levenshtein distance\n if (words.length > 1) {\n const firstWord = words[0]!;\n const firstWordResults = await this.fetchLookup(firstWord, 'startswith', 25);\n\n if (firstWordResults.length > 0) {\n const ranked = firstWordResults\n .map((s) => ({\n label: s.label,\n distance: levenshteinDistance(term.toLowerCase(), s.label.toLowerCase()),\n }))\n .sort((a, b) => a.distance - b.distance)\n .slice(0, 5)\n .map((s) => s.label);\n const result: MeSHLookupResult = { term, found: false, suggestions: ranked };\n this.cache?.set('mesh', term, result);\n return result;\n }\n }\n\n const result: MeSHLookupResult = { term, found: false };\n this.cache?.set('mesh', term, result);\n return result;\n }\n\n /**\n * Look up multiple MeSH terms.\n */\n async lookupTerms(terms: string[]): Promise<MeSHLookupResult[]> {\n const results: MeSHLookupResult[] = [];\n for (const term of terms) {\n results.push(await this.lookupTerm(term));\n }\n return results;\n }\n\n private async fetchLookup(\n label: string,\n match: 'exact' | 'startswith' | 'contains',\n limit: number\n ): Promise<MeSHApiEntry[]> {\n if (this.rateLimiter) {\n await this.rateLimiter.acquire();\n }\n\n const params = new URLSearchParams({\n label,\n match,\n limit: String(limit),\n });\n\n const url = `${MESH_LOOKUP_BASE_URL}?${params.toString()}`;\n\n let response: Response;\n try {\n response = await fetch(url, {\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Unknown error';\n throw new Error(`MeSH lookup failed: ${message}`);\n }\n\n if (!response.ok) {\n throw new Error(\n `MeSH lookup failed: HTTP ${response.status} ${response.statusText}`\n );\n }\n\n return (await response.json()) as MeSHApiEntry[];\n }\n}\n"],"names":["result"],"mappings":"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"}
@@ -12,7 +12,7 @@ async function parseQueryFile(filePath) {
12
12
  function detectShortKeywords(ast, threshold = 3) {
13
13
  const shortKeywords = /* @__PURE__ */ new Set();
14
14
  for (const block of ast.blocks) {
15
- for (const keyword of block.terms.keywords) {
15
+ for (const keyword of block.terms.keywords ?? []) {
16
16
  if (keyword.length <= threshold) {
17
17
  shortKeywords.add(keyword);
18
18
  }
@@ -1 +1 @@
1
- {"version":3,"file":"parser.js","sources":["../../src/query/parser.ts"],"sourcesContent":["/**\n * Query YAML Parser\n *\n * Parses YAML query files into validated QueryAST.\n * See spec/models/query-dsl.md for the full specification.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { parse } from 'yaml';\nimport type { QueryAST } from './types.js';\nimport { validateQueryFile } from './validator.js';\n\n/**\n * Parse a YAML string into a validated QueryAST.\n *\n * @param yaml - YAML string to parse\n * @returns Validated QueryAST\n * @throws Error if YAML is invalid or doesn't match schema\n */\nexport function parseQueryString(yaml: string): QueryAST {\n const data = parse(yaml);\n return validateQueryFile(data);\n}\n\n/**\n * Parse a YAML file into a validated QueryAST.\n *\n * @param filePath - Path to the YAML file\n * @returns Promise resolving to validated QueryAST\n * @throws Error if file doesn't exist, YAML is invalid, or doesn't match schema\n */\nexport async function parseQueryFile(filePath: string): Promise<QueryAST> {\n const content = await readFile(filePath, 'utf-8');\n return parseQueryString(content);\n}\n\n\n/**\n * Detect short keywords (potential acronyms) in a query.\n *\n * Short keywords (3 characters or fewer by default) may match unrelated\n * acronyms in different fields, producing noisy results.\n *\n * @param ast - Parsed QueryAST\n * @param threshold - Maximum length to consider \"short\" (default: 3)\n * @returns Array of unique short keywords found\n */\nexport function detectShortKeywords(ast: QueryAST, threshold = 3): string[] {\n const shortKeywords = new Set<string>();\n\n for (const block of ast.blocks) {\n for (const keyword of block.terms.keywords) {\n if (keyword.length <= threshold) {\n shortKeywords.add(keyword);\n }\n }\n }\n\n return Array.from(shortKeywords);\n}\n"],"names":[],"mappings":";;;AAmBO,SAAS,iBAAiB,MAAwB;AACvD,QAAM,OAAO,MAAM,IAAI;AACvB,SAAO,kBAAkB,IAAI;AAC/B;AASA,eAAsB,eAAe,UAAqC;AACxE,QAAM,UAAU,MAAM,SAAS,UAAU,OAAO;AAChD,SAAO,iBAAiB,OAAO;AACjC;AAaO,SAAS,oBAAoB,KAAe,YAAY,GAAa;AAC1E,QAAM,oCAAoB,IAAA;AAE1B,aAAW,SAAS,IAAI,QAAQ;AAC9B,eAAW,WAAW,MAAM,MAAM,UAAU;AAC1C,UAAI,QAAQ,UAAU,WAAW;AAC/B,sBAAc,IAAI,OAAO;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,aAAa;AACjC;"}
1
+ {"version":3,"file":"parser.js","sources":["../../src/query/parser.ts"],"sourcesContent":["/**\n * Query YAML Parser\n *\n * Parses YAML query files into validated QueryAST.\n * See spec/models/query-dsl.md for the full specification.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { parse } from 'yaml';\nimport type { QueryAST } from './types.js';\nimport { validateQueryFile } from './validator.js';\n\n/**\n * Parse a YAML string into a validated QueryAST.\n *\n * @param yaml - YAML string to parse\n * @returns Validated QueryAST\n * @throws Error if YAML is invalid or doesn't match schema\n */\nexport function parseQueryString(yaml: string): QueryAST {\n const data = parse(yaml);\n return validateQueryFile(data);\n}\n\n/**\n * Parse a YAML file into a validated QueryAST.\n *\n * @param filePath - Path to the YAML file\n * @returns Promise resolving to validated QueryAST\n * @throws Error if file doesn't exist, YAML is invalid, or doesn't match schema\n */\nexport async function parseQueryFile(filePath: string): Promise<QueryAST> {\n const content = await readFile(filePath, 'utf-8');\n return parseQueryString(content);\n}\n\n\n/**\n * Detect short keywords (potential acronyms) in a query.\n *\n * Short keywords (3 characters or fewer by default) may match unrelated\n * acronyms in different fields, producing noisy results.\n *\n * @param ast - Parsed QueryAST\n * @param threshold - Maximum length to consider \"short\" (default: 3)\n * @returns Array of unique short keywords found\n */\nexport function detectShortKeywords(ast: QueryAST, threshold = 3): string[] {\n const shortKeywords = new Set<string>();\n\n for (const block of ast.blocks) {\n for (const keyword of block.terms.keywords ?? []) {\n if (keyword.length <= threshold) {\n shortKeywords.add(keyword);\n }\n }\n }\n\n return Array.from(shortKeywords);\n}\n"],"names":[],"mappings":";;;AAmBO,SAAS,iBAAiB,MAAwB;AACvD,QAAM,OAAO,MAAM,IAAI;AACvB,SAAO,kBAAkB,IAAI;AAC/B;AASA,eAAsB,eAAe,UAAqC;AACxE,QAAM,UAAU,MAAM,SAAS,UAAU,OAAO;AAChD,SAAO,iBAAiB,OAAO;AACjC;AAaO,SAAS,oBAAoB,KAAe,YAAY,GAAa;AAC1E,QAAM,oCAAoB,IAAA;AAE1B,aAAW,SAAS,IAAI,QAAQ;AAC9B,eAAW,WAAW,MAAM,MAAM,YAAY,CAAA,GAAI;AAChD,UAAI,QAAQ,UAAU,WAAW;AAC/B,sBAAc,IAAI,OAAO;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,aAAa;AACjC;"}
@@ -15,10 +15,10 @@ export type Operator = 'AND' | 'OR';
15
15
  */
16
16
  export interface TermBlock {
17
17
  /** Free-text keywords (supported by all databases) */
18
- keywords: string[];
18
+ keywords?: string[] | undefined;
19
19
  /** MeSH terms (PubMed only) */
20
20
  mesh?: string[] | undefined;
21
- /** Emtree terms (Embase only) */
21
+ /** Emtree terms (Embase/Scopus) */
22
22
  emtree?: string[] | undefined;
23
23
  /** ERIC Descriptors (ERIC only) */
24
24
  eric?: string[] | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/query/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,YAAY,EAAE,CAAC;AAE7B;;;GAGG;AACH,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,UAAU,GACV,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,KAAK,CAAC;AAEV;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,IAAI,CAAC;AAEpC;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,+BAA+B;IAC/B,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC5B,iCAAiC;IACjC,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC9B,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC5B,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,kCAAkC;IAClC,KAAK,EAAE,SAAS,CAAC;IACjB,mBAAmB;IACnB,KAAK,EAAE,SAAS,CAAC;IACjB,6CAA6C;IAC7C,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC/B,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACjC,+BAA+B;IAC/B,gBAAgB,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC;CACtD;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAClC,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CACpC;AAED;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,uBAAuB;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,4CAA4C;IAC5C,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,qBAAqB;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,GAAG,SAAS,CAAC,CAAC,CAAC;CACrE"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/query/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,YAAY,EAAE,CAAC;AAE7B;;;GAGG;AACH,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,UAAU,GACV,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,KAAK,CAAC;AAEV;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,IAAI,CAAC;AAEpC;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAChC,+BAA+B;IAC/B,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC5B,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC9B,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC5B,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,kCAAkC;IAClC,KAAK,EAAE,SAAS,CAAC;IACjB,mBAAmB;IACnB,KAAK,EAAE,SAAS,CAAC;IACjB,6CAA6C;IAC7C,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC/B,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACjC,+BAA+B;IAC/B,gBAAgB,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC;CACtD;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAClC,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CACpC;AAED;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,uBAAuB;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,4CAA4C;IAC5C,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,qBAAqB;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,GAAG,SAAS,CAAC,CAAC,CAAC;CACrE"}
@@ -15,7 +15,7 @@ export declare const fieldTypeSchema: z.ZodEnum<{
15
15
  * Schema for term block containing search terms.
16
16
  */
17
17
  export declare const termBlockSchema: z.ZodObject<{
18
- keywords: z.ZodArray<z.ZodString>;
18
+ keywords: z.ZodOptional<z.ZodArray<z.ZodString>>;
19
19
  mesh: z.ZodOptional<z.ZodArray<z.ZodString>>;
20
20
  emtree: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
21
  eric: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -41,7 +41,7 @@ export declare const queryBlockSchema: z.ZodObject<{
41
41
  all: "all";
42
42
  }>;
43
43
  terms: z.ZodObject<{
44
- keywords: z.ZodArray<z.ZodString>;
44
+ keywords: z.ZodOptional<z.ZodArray<z.ZodString>>;
45
45
  mesh: z.ZodOptional<z.ZodArray<z.ZodString>>;
46
46
  emtree: z.ZodOptional<z.ZodArray<z.ZodString>>;
47
47
  eric: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -156,7 +156,7 @@ export declare const queryFileSchema: z.ZodPipe<z.ZodObject<{
156
156
  all: "all";
157
157
  }>;
158
158
  terms: z.ZodObject<{
159
- keywords: z.ZodArray<z.ZodString>;
159
+ keywords: z.ZodOptional<z.ZodArray<z.ZodString>>;
160
160
  mesh: z.ZodOptional<z.ZodArray<z.ZodString>>;
161
161
  emtree: z.ZodOptional<z.ZodArray<z.ZodString>>;
162
162
  eric: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -422,7 +422,7 @@ export declare const queryFileSchema: z.ZodPipe<z.ZodObject<{
422
422
  blocks: {
423
423
  field: "title" | "abstract" | "title_abstract" | "author" | "keyword" | "all";
424
424
  terms: {
425
- keywords: string[];
425
+ keywords?: string[] | undefined;
426
426
  mesh?: string[] | undefined;
427
427
  emtree?: string[] | undefined;
428
428
  eric?: string[] | undefined;
@@ -524,7 +524,7 @@ export declare const queryFileSchema: z.ZodPipe<z.ZodObject<{
524
524
  query: {
525
525
  field: "title" | "abstract" | "title_abstract" | "author" | "keyword" | "all";
526
526
  terms: {
527
- keywords: string[];
527
+ keywords?: string[] | undefined;
528
528
  mesh?: string[] | undefined;
529
529
  emtree?: string[] | undefined;
530
530
  eric?: string[] | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/query/validator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;GAEG;AACH,eAAO,MAAM,eAAe;;;;;;;EAO1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,eAAe;;;;;;iBAM1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,cAAc;;;EAAwB,CAAC;AAEpD;;GAEG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;iBAI3B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,2BAA2B;;;iBAGtC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;GAcrB,CAAC;AAEN;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwB3B,CAAC;AAEN;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;;;EAO7B,CAAC;AAiBH;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAcvB,CAAC;AAEN;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAEzD;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,KAAK;aAEtB,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EAC5B,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,GAAG,eAAe,EAAE,CAWvE"}
1
+ {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/query/validator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;GAEG;AACH,eAAO,MAAM,eAAe;;;;;;;EAO1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,eAAe;;;;;;iBAS3B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc;;;EAAwB,CAAC;AAEpD;;GAEG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;iBAI3B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,2BAA2B;;;iBAGtC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;GAcrB,CAAC;AAEN;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwB3B,CAAC;AAEN;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;;;EAO7B,CAAC;AAiBH;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAcvB,CAAC;AAEN;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAEzD;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,KAAK;aAEtB,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EAC5B,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,GAAG,eAAe,EAAE,CAWvE"}
@@ -8,12 +8,15 @@ const fieldTypeSchema = z.enum([
8
8
  "all"
9
9
  ]);
10
10
  const termBlockSchema = z.object({
11
- keywords: z.array(z.string()).min(1),
11
+ keywords: z.array(z.string()).min(1).optional(),
12
12
  mesh: z.array(z.string()).optional(),
13
13
  emtree: z.array(z.string()).optional(),
14
14
  eric: z.array(z.string()).optional(),
15
15
  exclude: z.array(z.string()).optional()
16
- });
16
+ }).refine(
17
+ (data) => data.keywords?.length || data.mesh?.length || data.emtree?.length || data.eric?.length,
18
+ { message: "At least one of keywords, mesh, emtree, or eric is required" }
19
+ );
17
20
  const operatorSchema = z.enum(["AND", "OR"]);
18
21
  const queryBlockSchema = z.object({
19
22
  field: fieldTypeSchema,