@ncukondo/search-hub 0.10.0 → 0.12.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 (36) hide show
  1. package/dist/cli/commands/merge.d.ts +82 -0
  2. package/dist/cli/commands/merge.d.ts.map +1 -0
  3. package/dist/cli/commands/merge.js +221 -0
  4. package/dist/cli/commands/merge.js.map +1 -0
  5. package/dist/cli/commands/resume.d.ts.map +1 -1
  6. package/dist/cli/commands/resume.js +7 -0
  7. package/dist/cli/commands/resume.js.map +1 -1
  8. package/dist/cli/commands/review/extract.d.ts +3 -1
  9. package/dist/cli/commands/review/extract.d.ts.map +1 -1
  10. package/dist/cli/commands/review/extract.js +75 -45
  11. package/dist/cli/commands/review/extract.js.map +1 -1
  12. package/dist/cli/commands/review/mark.d.ts.map +1 -1
  13. package/dist/cli/commands/review/mark.js +69 -22
  14. package/dist/cli/commands/review/mark.js.map +1 -1
  15. package/dist/cli/commands/review/merge.d.ts.map +1 -1
  16. package/dist/cli/commands/review/merge.js +8 -2
  17. package/dist/cli/commands/review/merge.js.map +1 -1
  18. package/dist/cli/commands/review/types.d.ts +4 -0
  19. package/dist/cli/commands/review/types.d.ts.map +1 -1
  20. package/dist/cli/commands/review/types.js +10 -3
  21. package/dist/cli/commands/review/types.js.map +1 -1
  22. package/dist/cli/commands/status.js +1 -1
  23. package/dist/cli/commands/status.js.map +1 -1
  24. package/dist/cli/index.d.ts.map +1 -1
  25. package/dist/cli/index.js +143 -4
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/cli/suggestions/rules.d.ts.map +1 -1
  28. package/dist/cli/suggestions/rules.js +20 -2
  29. package/dist/cli/suggestions/rules.js.map +1 -1
  30. package/dist/cli/suggestions/types.d.ts +6 -0
  31. package/dist/cli/suggestions/types.d.ts.map +1 -1
  32. package/dist/session/types.d.ts +14 -1
  33. package/dist/session/types.d.ts.map +1 -1
  34. package/dist/session/types.js +7 -0
  35. package/dist/session/types.js.map +1 -0
  36. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -23,11 +23,12 @@ import { computeSummary, formatSummaryJson, formatSummary } from "./commands/sum
23
23
  import { parseResultsOptions, validateResultsInput, formatResultsJson, formatResultsList } from "./commands/results.js";
24
24
  import { loadNotes, formatAllSessionNotes, formatNotesList, addNote, addAssessment } from "./commands/notes.js";
25
25
  import { computeDiff, computeQueryDiff, formatDiffJson, formatDiff } from "./commands/diff.js";
26
+ import { validateMergeSources, mergeArticles, formatMergeJson, formatMergeOutput, createMergedSession } from "./commands/merge.js";
26
27
  import { executeReviewInit } from "./commands/review/init.js";
27
28
  import { executeReviewStatus, formatStatusOutput } from "./commands/review/status.js";
28
29
  import { executeReviewList, formatListOutput } from "./commands/review/list.js";
29
30
  import { executeReviewExtract } from "./commands/review/extract.js";
30
- import { executeReviewMerge, formatMergeOutput } from "./commands/review/merge.js";
31
+ import { executeReviewMerge, formatMergeOutput as formatMergeOutput$1 } from "./commands/review/merge.js";
31
32
  import { executeReviewMark } from "./commands/review/mark.js";
32
33
  import { executeReviewExport, formatExportOutput } from "./commands/review/export.js";
33
34
  import { executeReviewFinalize, formatFinalizeOutput } from "./commands/review/finalize.js";
@@ -295,7 +296,13 @@ Examples:
295
296
  $ search-hub search --db pubmed --query "diabetes[tiab]" # Direct query
296
297
  $ search-hub search ./query.yaml --dry-run # Preview translations
297
298
  $ search-hub search ./query.yaml --count-only # Get hit counts only
298
- $ search-hub search ./query.yaml --max-results 100 # Limit results`).action(
299
+ $ search-hub search ./query.yaml --max-results 100 # Limit results
300
+
301
+ Query features (use "query init" to see full template):
302
+ filters: year_from, year_to, language, publication_types
303
+ exclude: NOT terms per block (terms.exclude)
304
+ mesh/eric: controlled vocabulary (terms.mesh, terms.eric)
305
+ overrides: per-database settings (pubmed, scopus, eric, arxiv)`).action(
299
306
  async (queryFile, options) => {
300
307
  const globalOpts = program.opts();
301
308
  try {
@@ -967,6 +974,135 @@ Query Refinement Workflow:
967
974
  } else {
968
975
  if (!globalOpts.quiet) {
969
976
  console.log(formatDiff(diff, sessionId1, sessionId2, showFilter, formatOptions));
977
+ const suggestion = formatSuggestion(getSuggestion({
978
+ command: "diff",
979
+ sessionId: sessionId2,
980
+ diffSession1Id: sessionId1,
981
+ diffAddedCount: diff.added.length,
982
+ diffRemovedCount: diff.removed.length
983
+ }));
984
+ if (suggestion) {
985
+ console.log(suggestion);
986
+ }
987
+ }
988
+ }
989
+ process.exitCode = EXIT_CODES.SUCCESS;
990
+ } catch (error) {
991
+ if (!globalOpts.quiet) {
992
+ console.error(
993
+ "Error:",
994
+ error instanceof Error ? error.message : error
995
+ );
996
+ }
997
+ process.exitCode = EXIT_CODES.SESSION_ERROR;
998
+ }
999
+ }
1000
+ );
1001
+ program.command("merge").description("Merge results from multiple search sessions").argument("<session-ids...>", "two or more session IDs to merge").option("--name <string>", "name for merged session").option("--dry-run", "show what would be merged without creating session").option("--json", "output as JSON").addHelpText("after", `
1002
+ Examples:
1003
+ $ search-hub merge session-v4 session-v9 # Merge two sessions
1004
+ $ search-hub merge session-v4 session-v9 --name combined # Merge with custom name
1005
+ $ search-hub merge session-a session-b session-c # Merge three sessions
1006
+ $ search-hub merge session-v4 session-v9 --dry-run # Preview merge`).action(
1007
+ async (sessionIds, options) => {
1008
+ const globalOpts = program.opts();
1009
+ try {
1010
+ if (sessionIds.length < 2) {
1011
+ if (!globalOpts.quiet) {
1012
+ console.error("Error: At least two session IDs are required for merge");
1013
+ }
1014
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
1015
+ return;
1016
+ }
1017
+ const sessionsDir = await getSessionsDir(globalOpts);
1018
+ const sessions = /* @__PURE__ */ new Map();
1019
+ for (const sessionId of sessionIds) {
1020
+ try {
1021
+ const session = await loadSession(sessionId, sessionsDir);
1022
+ sessions.set(sessionId, session);
1023
+ } catch (error) {
1024
+ if (!globalOpts.quiet) {
1025
+ console.error(
1026
+ `Error loading session '${sessionId}': ${error instanceof Error ? error.message : "Failed to load session"}`
1027
+ );
1028
+ }
1029
+ process.exitCode = EXIT_CODES.SESSION_ERROR;
1030
+ return;
1031
+ }
1032
+ }
1033
+ const validation = validateMergeSources(sessions);
1034
+ if (!validation.valid) {
1035
+ if (!globalOpts.quiet) {
1036
+ console.error(`Error: ${validation.error}`);
1037
+ }
1038
+ process.exitCode = EXIT_CODES.SESSION_ERROR;
1039
+ return;
1040
+ }
1041
+ const sessionArticles = /* @__PURE__ */ new Map();
1042
+ for (const [sessionId, session] of sessions) {
1043
+ const articles = await loadSessionArticles(session, sessionId, sessionsDir);
1044
+ sessionArticles.set(sessionId, articles);
1045
+ }
1046
+ const mergeResult = mergeArticles(sessionArticles);
1047
+ const sources = [...sessions.entries()].map(([id, session]) => ({
1048
+ id,
1049
+ name: session.name,
1050
+ count: mergeResult.perSession.get(id) ?? 0
1051
+ }));
1052
+ const byProviderCounts = /* @__PURE__ */ new Map();
1053
+ for (const [provider, articles] of mergeResult.byProvider) {
1054
+ byProviderCounts.set(provider, articles.length);
1055
+ }
1056
+ const firstSession = sessions.values().next().value;
1057
+ const mergeName = options?.name ?? (firstSession ? firstSession.name + "-merged" : "merged");
1058
+ if (options?.dryRun) {
1059
+ const outputData2 = {
1060
+ sessionId: "(dry-run)",
1061
+ totalBefore: mergeResult.totalBefore,
1062
+ totalAfter: mergeResult.totalAfter,
1063
+ duplicatesRemoved: mergeResult.duplicatesRemoved,
1064
+ sources,
1065
+ byProvider: byProviderCounts
1066
+ };
1067
+ if (options.json) {
1068
+ console.log(formatMergeJson(outputData2));
1069
+ } else if (!globalOpts.quiet) {
1070
+ console.log(formatMergeOutput(outputData2));
1071
+ }
1072
+ process.exitCode = EXIT_CODES.SUCCESS;
1073
+ return;
1074
+ }
1075
+ const sessionSources = [...sessions.entries()].map(([id, session]) => ({
1076
+ id,
1077
+ name: session.name
1078
+ }));
1079
+ const mergedSession = await createMergedSession({
1080
+ name: mergeName,
1081
+ sources: sessionSources,
1082
+ byProvider: mergeResult.byProvider,
1083
+ totalRetrieved: mergeResult.totalAfter,
1084
+ sessionsDir,
1085
+ sourceSessionIds: sessionIds
1086
+ });
1087
+ const outputData = {
1088
+ sessionId: mergedSession.id,
1089
+ totalBefore: mergeResult.totalBefore,
1090
+ totalAfter: mergeResult.totalAfter,
1091
+ duplicatesRemoved: mergeResult.duplicatesRemoved,
1092
+ sources,
1093
+ byProvider: byProviderCounts
1094
+ };
1095
+ if (options?.json) {
1096
+ console.log(formatMergeJson(outputData));
1097
+ } else if (!globalOpts.quiet) {
1098
+ console.log(formatMergeOutput(outputData));
1099
+ const suggestion = getSuggestion({
1100
+ command: "merge",
1101
+ sessionId: mergedSession.id
1102
+ });
1103
+ const suggestionText = formatSuggestion(suggestion);
1104
+ if (suggestionText) {
1105
+ console.log(suggestionText);
970
1106
  }
971
1107
  }
972
1108
  process.exitCode = EXIT_CODES.SUCCESS;
@@ -1258,7 +1394,7 @@ Examples:
1258
1394
  process.exitCode = EXIT_CODES.SESSION_ERROR;
1259
1395
  }
1260
1396
  });
1261
- reviewCommand.command("extract").description("Extract subset to for-review/<name>/review.yaml for distributed review").requiredOption("--session <id>", "session ID").requiredOption("--name <name>", "name for the review subset (output: for-review/<name>/review.yaml)").option("--filter <types>", "filter by status (comma-separated): pending, incomplete, uncertain, agreed-include, agreed-exclude, conflicting, finalized").option("--sort <method>", "sort method: year, title, random, none", "none").option("--limit <n>", "limit number of articles").option("--offset <n>", "skip first n articles").option("--seed <n>", "random seed for reproducible sorting").option("--basis <type>", "basis for review: title or abstract (outputs work file format)").option("--reviewer <id>", 'reviewer identifier (e.g., "ai:claude")').action(async (options) => {
1397
+ reviewCommand.command("extract").description("Extract subset to for-review/<name>/review.yaml for distributed review").requiredOption("--session <id>", "session ID").requiredOption("--name <name>", "name for the review subset (output: for-review/<name>/review.yaml)").option("--filter <types>", "filter by status (comma-separated): pending, incomplete, uncertain, agreed-include, agreed-exclude, conflicting, finalized").option("--sort <method>", "sort method: year, title, random, none", "none").option("--limit <n>", "limit number of articles").option("--offset <n>", "skip first n articles").option("--seed <n>", "random seed for reproducible sorting").option("--basis <type>", "basis for review: title, abstract, or fulltext").option("--reviewer <id>", 'reviewer identifier (e.g., "ai:claude")').option("--finalize", "extract for final decision (includes reviewHistory and finalDecision)").action(async (options) => {
1262
1398
  const globalOpts = program.opts();
1263
1399
  try {
1264
1400
  const validSorts = ["year", "title", "random", "none"];
@@ -1307,6 +1443,9 @@ Examples:
1307
1443
  }
1308
1444
  extractOptions.basis = options.basis;
1309
1445
  }
1446
+ if (options.finalize) {
1447
+ extractOptions.finalize = true;
1448
+ }
1310
1449
  const result = await executeReviewExtract(extractOptions, sessionsDir);
1311
1450
  if (!globalOpts.quiet) {
1312
1451
  console.log(`Extracted ${result.extractedCount} of ${result.totalMatching} articles to ${result.outputPath}`);
@@ -1340,7 +1479,7 @@ Examples:
1340
1479
  };
1341
1480
  const result = await executeReviewMerge(mergeOptions, sessionsDir);
1342
1481
  if (!globalOpts.quiet) {
1343
- console.log(formatMergeOutput(result, options.dryRun));
1482
+ console.log(formatMergeOutput$1(result, options.dryRun));
1344
1483
  if (!options.dryRun) {
1345
1484
  const statusResult = await executeReviewStatus({ sessionId: options.session }, sessionsDir);
1346
1485
  const suggestion = formatSuggestion(getSuggestion({