@ninjaxtools/slopdex 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -346,6 +346,13 @@ function pythonIsMethod(node) {
346
346
  const definition = node.parent?.type === "decorated_definition" ? node.parent : node;
347
347
  return definition.parent?.parent?.type === "class_definition";
348
348
  }
349
+ function pythonDocstring(body, content) {
350
+ const statement = body?.namedChildren[0];
351
+ if (statement?.type !== "expression_statement") return null;
352
+ const value = statement.namedChildren[0];
353
+ if (value?.type !== "string" && value?.type !== "concatenated_string") return null;
354
+ return content.slice(value.startIndex, value.endIndex);
355
+ }
349
356
  function goReceiver(node) {
350
357
  let type = node.childForFieldName("receiver")?.namedChildren[0]?.childForFieldName("type");
351
358
  while (type?.type === "pointer_type") type = type.namedChildren[0] ?? null;
@@ -359,12 +366,14 @@ function collectNativeCallables(root, content, language) {
359
366
  if ((!body || body.isMissing) && !node.hasError) return false;
360
367
  const sourceNode = language === "python" && node.parent?.type === "decorated_definition" ? node.parent : node;
361
368
  const header = content.slice(node.startIndex, body?.startIndex ?? node.endIndex).trimEnd();
369
+ const documentation = language === "python" && body ? pythonDocstring(body, content) : null;
362
370
  candidates.push({
363
371
  node: sourceNode,
364
372
  name,
365
373
  kind,
366
374
  scope,
367
- signature: bound ? `${name} = ${header}` : header
375
+ signature: bound ? `${name} = ${header}` : header,
376
+ ...documentation ? { documentation } : {}
368
377
  });
369
378
  if (body) walk2(body, [...scope, name]);
370
379
  return true;
@@ -789,7 +798,7 @@ function extractCallables(relativePath, content) {
789
798
  const errors = parseDiagnostics(tree.rootNode, content, relativePath, language, candidates);
790
799
  const occurrences = /* @__PURE__ */ new Map();
791
800
  const callables = [];
792
- for (const { node, name, kind, scope, signature: candidateSignature } of candidates) {
801
+ for (const { node, name, kind, scope, signature: candidateSignature, documentation } of candidates) {
793
802
  const qualifiedName = [...scope, name].join(".");
794
803
  const baseIdentity = `${relativePath}\0${qualifiedName}\0${kind}`;
795
804
  const occurrence = occurrences.get(baseIdentity) ?? 0;
@@ -804,6 +813,8 @@ function extractCallables(relativePath, content) {
804
813
  `kind: ${kind}`,
805
814
  `symbol: ${qualifiedName}`,
806
815
  signature ? `signature: ${signature}` : null,
816
+ documentation ? `documentation:
817
+ ${documentation}` : null,
807
818
  "source:",
808
819
  source
809
820
  ].filter((value) => value !== null).join("\n");
@@ -1105,6 +1116,13 @@ var IndexDatabase = class {
1105
1116
  this.#setMetadata("generation", String(expectedGeneration + 1));
1106
1117
  });
1107
1118
  }
1119
+ disableSummaries() {
1120
+ if (!this.summariesEnabled()) return;
1121
+ this.#transaction(() => {
1122
+ this.#setMetadata("summaries_enabled", "false");
1123
+ this.#setMetadata("generation", String(this.getGeneration() + 1));
1124
+ });
1125
+ }
1108
1126
  getWorkingTreeFiles() {
1109
1127
  return this.#db.prepare(`
1110
1128
  SELECT path, previous_path AS previousPath
@@ -1988,7 +2006,7 @@ var CodeIndex = class {
1988
2006
  async #attachEmbeddings(files, signal) {
1989
2007
  if (this.#database.summariesEnabled()) {
1990
2008
  if (JSON.stringify(this.#database.summaryProfile()) !== JSON.stringify(this.summaryProvider.profile)) {
1991
- throw new CodeIndexError("Summary provider or model differs from this index; run use-summaries (useSummaries() in the library) with the new provider first.");
2009
+ throw new CodeIndexError("Summary provider or model differs from this index; run summaries enable (useSummaries() in the library) with the new provider first.");
1992
2010
  }
1993
2011
  await this.#attachSummaries(files, signal);
1994
2012
  }
@@ -2087,8 +2105,12 @@ var CodeIndex = class {
2087
2105
  }))), this.summaryProvider.profile, generation);
2088
2106
  return { summariesCreated, summariesEnabled: true };
2089
2107
  }
2108
+ disableSummaries() {
2109
+ this.#database.disableSummaries();
2110
+ return { summariesCreated: 0, summariesEnabled: false };
2111
+ }
2090
2112
  async searchSummary(options) {
2091
- if (!this.#database.summariesEnabled()) throw new CodeIndexError("Summaries are not enabled; run use-summaries first.");
2113
+ if (!this.#database.summariesEnabled()) throw new CodeIndexError("Summaries are not enabled; run summaries enable first.");
2092
2114
  if (!options.query.trim()) throw new CodeIndexError("query must not be empty.");
2093
2115
  const limit = options.limit ?? 10;
2094
2116
  assertPositiveInteger(limit, "limit");
@@ -2225,7 +2247,7 @@ function normalizeProfile(profile) {
2225
2247
  provider: profile.provider,
2226
2248
  model: profile.model,
2227
2249
  dimensions: profile.dimensions,
2228
- strategyVersion: profile.strategyVersion ?? "callable-v1"
2250
+ strategyVersion: profile.strategyVersion ?? "callable-v2"
2229
2251
  };
2230
2252
  }
2231
2253
  function emptyStats(checkpoint) {
@@ -2602,7 +2624,7 @@ var JinaEmbeddingProvider = class {
2602
2624
  provider: "jina",
2603
2625
  model,
2604
2626
  dimensions,
2605
- strategyVersion: "callable-v1:code-query-passage"
2627
+ strategyVersion: "callable-v2:code-query-passage"
2606
2628
  };
2607
2629
  this.#url = options.baseUrl ?? "https://api.jina.ai/v1/embeddings";
2608
2630
  }
@@ -2652,7 +2674,7 @@ var OpenAIEmbeddingProvider = class {
2652
2674
  const model = options.model ?? "text-embedding-3-large";
2653
2675
  const dimensions = options.dimensions ?? 3072;
2654
2676
  if (!Number.isInteger(dimensions) || dimensions < 1) throw new Error("dimensions must be a positive integer.");
2655
- this.profile = { provider: "openai", model, dimensions, strategyVersion: "callable-v1" };
2677
+ this.profile = { provider: "openai", model, dimensions, strategyVersion: "callable-v2" };
2656
2678
  this.#url = `${(options.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "")}/embeddings`;
2657
2679
  }
2658
2680
  embedDocuments(inputs, options) {
@@ -2918,8 +2940,9 @@ var parsed = (() => {
2918
2940
  }
2919
2941
  })();
2920
2942
  var [command, ...positionals] = parsed.positionals;
2943
+ var summariesAction = command === "summaries" ? positionals[0] : void 0;
2921
2944
  if (parsed.values.version) {
2922
- const version = true ? "0.9.0" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
2945
+ const version = true ? "0.10.0" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
2923
2946
  process.stdout.write(`${version}
2924
2947
  `);
2925
2948
  process.exit(0);
@@ -2962,7 +2985,7 @@ async function main() {
2962
2985
  if (command === "index-errors") {
2963
2986
  const indexPath = path10.resolve(parsed.values.index ?? config.indexPath ?? path10.join(rootDir, ".slopdex/index.sqlite"));
2964
2987
  const errors = existsSync(indexPath) ? readIndexErrors(indexPath) : [];
2965
- if (outputFormat("json") === "summary") {
2988
+ if (outputFormat("summary") === "summary") {
2966
2989
  process.stdout.write(errors.length === 0 ? "No indexing errors.\n" : `${errors.map(
2967
2990
  (error) => `${error.path}${error.startLine === null ? "" : `:${error.startLine}:${error.startColumn}`} ${error.qualifiedName ? `:: ${error.qualifiedName} ` : ""}[${error.code}]
2968
2991
  ${error.message}`
@@ -2988,12 +3011,15 @@ async function main() {
2988
3011
  const updateTarget = command === "update-git" ? parsed.values.target ?? "HEAD" : "HEAD";
2989
3012
  const { summaryProvider: _summaryProvider, ...refreshOptions } = indexOptions;
2990
3013
  const updateStats = await ensureIndexUpdated(
2991
- command === "use-summaries" ? refreshOptions : indexOptions,
3014
+ command === "summaries" && summariesAction === "enable" ? refreshOptions : indexOptions,
2992
3015
  "index",
2993
3016
  updateTarget,
2994
3017
  parsed.values["rebuild-on-divergence"],
2995
3018
  parsed.values["force-reindex"],
2996
- parsed.values["no-reindex"]
3019
+ parsed.values["no-reindex"],
3020
+ command === "summaries" && summariesAction === "disable" ? (refreshIndex2) => {
3021
+ refreshIndex2.disableSummaries();
3022
+ } : void 0
2997
3023
  );
2998
3024
  const index = new CodeIndex(indexOptions);
2999
3025
  try {
@@ -3013,22 +3039,23 @@ async function main() {
3013
3039
  printJson(updateStats);
3014
3040
  break;
3015
3041
  }
3016
- case "use-summaries":
3017
- printJson(await index.useSummaries());
3042
+ case "summaries":
3043
+ printJson(summariesAction === "enable" ? await index.useSummaries() : index.disableSummaries());
3018
3044
  break;
3019
3045
  case "search":
3020
3046
  case "search-summary": {
3021
3047
  const query = positionals.join(" ").trim();
3022
3048
  if (!query) throw new CodeIndexError(`${command} requires a query.`);
3023
3049
  const threshold = similarityThreshold();
3050
+ const nameRegex = qualifiedNameRegex();
3024
3051
  const results = await (command === "search-summary" ? index.searchSummary.bind(index) : index.similaritySearch.bind(index))({
3025
3052
  query,
3026
- ...parsed.values.regexp !== void 0 ? { nameRegex: parsed.values.regexp } : {},
3053
+ ...nameRegex !== void 0 ? { nameRegex } : {},
3027
3054
  limit: numberOption(parsed.values.limit, 10, "limit"),
3028
3055
  minSimilarity: threshold.min,
3029
3056
  ...threshold.max !== void 0 ? { maxSimilarity: threshold.max } : {}
3030
3057
  });
3031
- const format = outputFormat("json");
3058
+ const format = outputFormat("summary");
3032
3059
  if (format === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
3033
3060
  if (format === "summary") {
3034
3061
  process.stdout.write(`${command === "search-summary" ? results.map((match) => `${formatSimilaritySummary([match])}
@@ -3049,10 +3076,9 @@ async function main() {
3049
3076
  limit: positiveIntegerOption(parsed.values.limit, 50, "limit"),
3050
3077
  minSimilarity: threshold.min,
3051
3078
  ...threshold.max !== void 0 ? { maxSimilarity: threshold.max } : {},
3052
- minLines: minimumLines(),
3053
- ...parsed.values.regex !== void 0 ? { nameRegex: parsed.values.regex } : {}
3079
+ minLines: minimumLines()
3054
3080
  });
3055
- const format = outputFormat("json");
3081
+ const format = outputFormat("summary");
3056
3082
  if (format === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
3057
3083
  if (format === "summary") process.stdout.write(`${formatCohesionSummary(report)}
3058
3084
  `);
@@ -3109,8 +3135,7 @@ async function runCrossSearch(source, sourceOptions, provider) {
3109
3135
  ...threshold.max !== void 0 ? { maxSimilarity: threshold.max } : {},
3110
3136
  includeSymmetricDuplicates: parsed.values["include-symmetric-duplicates"],
3111
3137
  crossFileOnly: parsed.values["cross-file-only"],
3112
- minLines: minimumLines(),
3113
- ...parsed.values.regex !== void 0 ? { nameRegex: parsed.values.regex } : {}
3138
+ minLines: minimumLines()
3114
3139
  };
3115
3140
  try {
3116
3141
  if (format === "clusters") {
@@ -3137,13 +3162,14 @@ async function runCrossSearch(source, sourceOptions, provider) {
3137
3162
  target?.close();
3138
3163
  }
3139
3164
  }
3140
- async function ensureIndexUpdated(options, label, target, rebuildOnDivergence, forceRebuild, noReindex) {
3165
+ async function ensureIndexUpdated(options, label, target, rebuildOnDivergence, forceRebuild, noReindex, beforeRefresh) {
3141
3166
  diagnosticIndexes.add(resolveIndexPath(options));
3142
3167
  const initialized = await initializeMissingIndex(options, label, target, noReindex);
3143
3168
  if (initialized) return initialized;
3144
3169
  try {
3145
3170
  const index = new CodeIndex(options);
3146
3171
  try {
3172
+ beforeRefresh?.(index);
3147
3173
  return await refreshIndex(index, label, target, rebuildOnDivergence, noReindex);
3148
3174
  } finally {
3149
3175
  index.close();
@@ -3274,19 +3300,24 @@ function numberOption(value, defaultValue, name) {
3274
3300
  return parsedValue;
3275
3301
  }
3276
3302
  function validateInvocation() {
3277
- if (parsed.values.regexp !== void 0) {
3303
+ const nameRegex = qualifiedNameRegex();
3304
+ if (nameRegex !== void 0) {
3278
3305
  if (!["search", "search-summary", "cross-search", "cohesion"].includes(command)) {
3279
- throw new CodeIndexError("-e/--regexp is only available for search, search-summary, cross-search, and cohesion.");
3306
+ throw new CodeIndexError("-e/--regexp/--regex is only available for search, search-summary, cross-search, and cohesion.");
3280
3307
  }
3281
- compileNameRegex(parsed.values.regexp, "-e/--regexp value");
3308
+ compileNameRegex(nameRegex, parsed.values.regex !== void 0 ? "--regex value" : "-e/--regexp value");
3282
3309
  }
3283
3310
  switch (command) {
3284
3311
  case "index-errors":
3285
- if (outputFormat("json") === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
3312
+ if (outputFormat("summary") === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
3286
3313
  return;
3287
3314
  case "status":
3288
3315
  case "update-git":
3289
- case "use-summaries":
3316
+ return;
3317
+ case "summaries":
3318
+ if (positionals.length !== 1 || summariesAction !== "enable" && summariesAction !== "disable") {
3319
+ throw new CodeIndexError("summaries requires enable or disable.");
3320
+ }
3290
3321
  return;
3291
3322
  case "update-files":
3292
3323
  if (positionals.length === 0) throw new CodeIndexError("update-files requires at least one path.");
@@ -3299,7 +3330,7 @@ function validateInvocation() {
3299
3330
  if (!positionals.join(" ").trim()) throw new CodeIndexError(`${command} requires a query.`);
3300
3331
  validateLimit(10);
3301
3332
  similarityThreshold();
3302
- if (outputFormat("json") === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
3333
+ if (outputFormat("summary") === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
3303
3334
  return;
3304
3335
  }
3305
3336
  case "cross-search":
@@ -3313,16 +3344,14 @@ function validateInvocation() {
3313
3344
  similarityThreshold();
3314
3345
  outputFormat("clusters");
3315
3346
  minimumLines();
3316
- functionNameRegex();
3317
3347
  crossSearchSourceFilter();
3318
3348
  return;
3319
3349
  case "cohesion":
3320
3350
  positiveIntegerOption(parsed.values.limit, 50, "limit");
3321
3351
  positiveIntegerOption(parsed.values.neighbors, 20, "neighbors");
3322
3352
  cohesionThreshold();
3323
- if (outputFormat("json") === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
3353
+ if (outputFormat("summary") === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
3324
3354
  minimumLines();
3325
- functionNameRegex();
3326
3355
  crossSearchSourceFilter();
3327
3356
  return;
3328
3357
  default:
@@ -3368,15 +3397,19 @@ function minimumLines() {
3368
3397
  if (!Number.isInteger(value) || value < 1) throw new CodeIndexError("min-lines must be a positive integer.");
3369
3398
  return value;
3370
3399
  }
3371
- function functionNameRegex() {
3372
- return compileNameRegex(parsed.values.regex, "--regex value");
3400
+ function qualifiedNameRegex() {
3401
+ if (parsed.values.regexp !== void 0 && parsed.values.regex !== void 0 && parsed.values.regexp !== parsed.values.regex) {
3402
+ throw new CodeIndexError("-e/--regexp and --regex are aliases and cannot use different values.");
3403
+ }
3404
+ return parsed.values.regexp ?? parsed.values.regex;
3373
3405
  }
3374
3406
  function crossSearchSourceFilter() {
3375
3407
  const changedSince = parsed.values["changed-since"];
3376
3408
  const uncommitted = parsed.values.uncommitted;
3409
+ const nameRegex = qualifiedNameRegex();
3377
3410
  const restrictions = {
3378
3411
  ...parsed.values["source-path"] ? { path: parsed.values["source-path"] } : {},
3379
- ...parsed.values.regexp !== void 0 ? { nameRegex: parsed.values.regexp } : {}
3412
+ ...nameRegex !== void 0 ? { nameRegex } : {}
3380
3413
  };
3381
3414
  if (changedSince) return { type: "changed-since", commit: changedSince, ...uncommitted ? { uncommitted: true } : {}, ...restrictions };
3382
3415
  if (uncommitted) return { type: "uncommitted", ...restrictions };
@@ -3439,7 +3472,7 @@ Commands:
3439
3472
  delete-files <path...> Remove specific files from the index
3440
3473
  update-git Index a Git snapshot plus working-tree changes
3441
3474
  search <query> Search functions by semantic similarity
3442
- use-summaries Generate purpose summaries and enable automatic updates
3475
+ summaries <enable|disable> Enable or disable automatic purpose summaries
3443
3476
  search-summary <query> Search functions using summary embeddings
3444
3477
  cross-search Find nearest functions for each source function
3445
3478
  cohesion Rank related functions separated across the repository
@@ -3543,13 +3576,13 @@ Options:
3543
3576
  --limit <number> Search result limit
3544
3577
  --neighbors <number> Semantic neighbors per function for cohesion (default: 20)
3545
3578
  --threshold <number|range> Show similarities at/above a value or within a range
3546
- --format <json|summary|clusters> Output format (search: json; cross-search: clusters)
3579
+ --format <json|summary|clusters> Output format (default: summary; cross-search: clusters)
3547
3580
  --include-source Include function source in cohesion JSON
3548
3581
  --include-symmetric-duplicates Show both directions of same-index matches
3549
3582
  --cross-file-only Exclude matches from the source file
3550
3583
  --min-lines <number> Minimum callable length for cross-search (default: 2)
3551
3584
  -e, --regexp <regex> Filter qualified symbols (analysis: sources only)
3552
- --regex <regex> Filter both analysis sources and matching candidates
3585
+ --regex <regex> Alias for -e/--regexp
3553
3586
  --changed-since <commit> Search added, modified, or moved functions
3554
3587
  --uncommitted Search functions from uncommitted files
3555
3588
  --source-path <path> Restrict cross-search sources to a file or directory
@@ -3577,7 +3610,7 @@ Other Examples:
3577
3610
  slopdex search "validate an authenticated session" --limit 10
3578
3611
 
3579
3612
  Enable purpose summaries, then search by their meaning:
3580
- slopdex use-summaries
3613
+ slopdex summaries enable
3581
3614
  slopdex search-summary "maintain the repository index" --format summary
3582
3615
 
3583
3616
  Review functions under a path against the whole codebase:
@@ -3592,10 +3625,10 @@ Other Examples:
3592
3625
  Filter semantic search results by qualified symbol before applying the limit:
3593
3626
  slopdex search "validate session" -e '^Session\\.' --limit 10
3594
3627
 
3595
- -e/--regexp uses a case-sensitive JavaScript regex on qualified names. For cross-search
3628
+ -e/--regexp/--regex uses a case-sensitive JavaScript regex on qualified names. For cross-search
3596
3629
  and cohesion it filters sources only; targets keep their normal eligibility rules.
3597
3630
  With --changed-since and --uncommitted, sources must be changed since the commit and
3598
- belong to an uncommitted file. --source-path and -e further narrow that intersection.
3631
+ belong to an uncommitted file. --source-path and the regex filter further narrow that intersection.
3599
3632
  `);
3600
3633
  }
3601
3634
  //# sourceMappingURL=cli.js.map