@ninjaxtools/slopdex 0.8.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) {
@@ -2907,6 +2929,7 @@ var parsed = (() => {
2907
2929
  "force-reindex": { type: "boolean", default: false },
2908
2930
  "no-reindex": { type: "boolean", default: false },
2909
2931
  "ignore-errors": { type: "boolean", default: false },
2932
+ version: { type: "boolean", default: false },
2910
2933
  help: { type: "boolean", short: "h", default: false }
2911
2934
  }
2912
2935
  });
@@ -2917,6 +2940,13 @@ var parsed = (() => {
2917
2940
  }
2918
2941
  })();
2919
2942
  var [command, ...positionals] = parsed.positionals;
2943
+ var summariesAction = command === "summaries" ? positionals[0] : void 0;
2944
+ if (parsed.values.version) {
2945
+ const version = true ? "0.10.0" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
2946
+ process.stdout.write(`${version}
2947
+ `);
2948
+ process.exit(0);
2949
+ }
2920
2950
  var diagnosticIndexes = /* @__PURE__ */ new Set();
2921
2951
  process.on("exit", () => {
2922
2952
  if (parsed.values["ignore-errors"]) return;
@@ -2955,7 +2985,7 @@ async function main() {
2955
2985
  if (command === "index-errors") {
2956
2986
  const indexPath = path10.resolve(parsed.values.index ?? config.indexPath ?? path10.join(rootDir, ".slopdex/index.sqlite"));
2957
2987
  const errors = existsSync(indexPath) ? readIndexErrors(indexPath) : [];
2958
- if (outputFormat("json") === "summary") {
2988
+ if (outputFormat("summary") === "summary") {
2959
2989
  process.stdout.write(errors.length === 0 ? "No indexing errors.\n" : `${errors.map(
2960
2990
  (error) => `${error.path}${error.startLine === null ? "" : `:${error.startLine}:${error.startColumn}`} ${error.qualifiedName ? `:: ${error.qualifiedName} ` : ""}[${error.code}]
2961
2991
  ${error.message}`
@@ -2981,12 +3011,15 @@ async function main() {
2981
3011
  const updateTarget = command === "update-git" ? parsed.values.target ?? "HEAD" : "HEAD";
2982
3012
  const { summaryProvider: _summaryProvider, ...refreshOptions } = indexOptions;
2983
3013
  const updateStats = await ensureIndexUpdated(
2984
- command === "use-summaries" ? refreshOptions : indexOptions,
3014
+ command === "summaries" && summariesAction === "enable" ? refreshOptions : indexOptions,
2985
3015
  "index",
2986
3016
  updateTarget,
2987
3017
  parsed.values["rebuild-on-divergence"],
2988
3018
  parsed.values["force-reindex"],
2989
- parsed.values["no-reindex"]
3019
+ parsed.values["no-reindex"],
3020
+ command === "summaries" && summariesAction === "disable" ? (refreshIndex2) => {
3021
+ refreshIndex2.disableSummaries();
3022
+ } : void 0
2990
3023
  );
2991
3024
  const index = new CodeIndex(indexOptions);
2992
3025
  try {
@@ -3006,22 +3039,23 @@ async function main() {
3006
3039
  printJson(updateStats);
3007
3040
  break;
3008
3041
  }
3009
- case "use-summaries":
3010
- printJson(await index.useSummaries());
3042
+ case "summaries":
3043
+ printJson(summariesAction === "enable" ? await index.useSummaries() : index.disableSummaries());
3011
3044
  break;
3012
3045
  case "search":
3013
3046
  case "search-summary": {
3014
3047
  const query = positionals.join(" ").trim();
3015
3048
  if (!query) throw new CodeIndexError(`${command} requires a query.`);
3016
3049
  const threshold = similarityThreshold();
3050
+ const nameRegex = qualifiedNameRegex();
3017
3051
  const results = await (command === "search-summary" ? index.searchSummary.bind(index) : index.similaritySearch.bind(index))({
3018
3052
  query,
3019
- ...parsed.values.regexp !== void 0 ? { nameRegex: parsed.values.regexp } : {},
3053
+ ...nameRegex !== void 0 ? { nameRegex } : {},
3020
3054
  limit: numberOption(parsed.values.limit, 10, "limit"),
3021
3055
  minSimilarity: threshold.min,
3022
3056
  ...threshold.max !== void 0 ? { maxSimilarity: threshold.max } : {}
3023
3057
  });
3024
- const format = outputFormat("json");
3058
+ const format = outputFormat("summary");
3025
3059
  if (format === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
3026
3060
  if (format === "summary") {
3027
3061
  process.stdout.write(`${command === "search-summary" ? results.map((match) => `${formatSimilaritySummary([match])}
@@ -3042,10 +3076,9 @@ async function main() {
3042
3076
  limit: positiveIntegerOption(parsed.values.limit, 50, "limit"),
3043
3077
  minSimilarity: threshold.min,
3044
3078
  ...threshold.max !== void 0 ? { maxSimilarity: threshold.max } : {},
3045
- minLines: minimumLines(),
3046
- ...parsed.values.regex !== void 0 ? { nameRegex: parsed.values.regex } : {}
3079
+ minLines: minimumLines()
3047
3080
  });
3048
- const format = outputFormat("json");
3081
+ const format = outputFormat("summary");
3049
3082
  if (format === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
3050
3083
  if (format === "summary") process.stdout.write(`${formatCohesionSummary(report)}
3051
3084
  `);
@@ -3102,8 +3135,7 @@ async function runCrossSearch(source, sourceOptions, provider) {
3102
3135
  ...threshold.max !== void 0 ? { maxSimilarity: threshold.max } : {},
3103
3136
  includeSymmetricDuplicates: parsed.values["include-symmetric-duplicates"],
3104
3137
  crossFileOnly: parsed.values["cross-file-only"],
3105
- minLines: minimumLines(),
3106
- ...parsed.values.regex !== void 0 ? { nameRegex: parsed.values.regex } : {}
3138
+ minLines: minimumLines()
3107
3139
  };
3108
3140
  try {
3109
3141
  if (format === "clusters") {
@@ -3130,13 +3162,14 @@ async function runCrossSearch(source, sourceOptions, provider) {
3130
3162
  target?.close();
3131
3163
  }
3132
3164
  }
3133
- async function ensureIndexUpdated(options, label, target, rebuildOnDivergence, forceRebuild, noReindex) {
3165
+ async function ensureIndexUpdated(options, label, target, rebuildOnDivergence, forceRebuild, noReindex, beforeRefresh) {
3134
3166
  diagnosticIndexes.add(resolveIndexPath(options));
3135
3167
  const initialized = await initializeMissingIndex(options, label, target, noReindex);
3136
3168
  if (initialized) return initialized;
3137
3169
  try {
3138
3170
  const index = new CodeIndex(options);
3139
3171
  try {
3172
+ beforeRefresh?.(index);
3140
3173
  return await refreshIndex(index, label, target, rebuildOnDivergence, noReindex);
3141
3174
  } finally {
3142
3175
  index.close();
@@ -3267,19 +3300,24 @@ function numberOption(value, defaultValue, name) {
3267
3300
  return parsedValue;
3268
3301
  }
3269
3302
  function validateInvocation() {
3270
- if (parsed.values.regexp !== void 0) {
3303
+ const nameRegex = qualifiedNameRegex();
3304
+ if (nameRegex !== void 0) {
3271
3305
  if (!["search", "search-summary", "cross-search", "cohesion"].includes(command)) {
3272
- 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.");
3273
3307
  }
3274
- compileNameRegex(parsed.values.regexp, "-e/--regexp value");
3308
+ compileNameRegex(nameRegex, parsed.values.regex !== void 0 ? "--regex value" : "-e/--regexp value");
3275
3309
  }
3276
3310
  switch (command) {
3277
3311
  case "index-errors":
3278
- 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.");
3279
3313
  return;
3280
3314
  case "status":
3281
3315
  case "update-git":
3282
- 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
+ }
3283
3321
  return;
3284
3322
  case "update-files":
3285
3323
  if (positionals.length === 0) throw new CodeIndexError("update-files requires at least one path.");
@@ -3292,7 +3330,7 @@ function validateInvocation() {
3292
3330
  if (!positionals.join(" ").trim()) throw new CodeIndexError(`${command} requires a query.`);
3293
3331
  validateLimit(10);
3294
3332
  similarityThreshold();
3295
- 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.");
3296
3334
  return;
3297
3335
  }
3298
3336
  case "cross-search":
@@ -3306,16 +3344,14 @@ function validateInvocation() {
3306
3344
  similarityThreshold();
3307
3345
  outputFormat("clusters");
3308
3346
  minimumLines();
3309
- functionNameRegex();
3310
3347
  crossSearchSourceFilter();
3311
3348
  return;
3312
3349
  case "cohesion":
3313
3350
  positiveIntegerOption(parsed.values.limit, 50, "limit");
3314
3351
  positiveIntegerOption(parsed.values.neighbors, 20, "neighbors");
3315
3352
  cohesionThreshold();
3316
- 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.");
3317
3354
  minimumLines();
3318
- functionNameRegex();
3319
3355
  crossSearchSourceFilter();
3320
3356
  return;
3321
3357
  default:
@@ -3361,15 +3397,19 @@ function minimumLines() {
3361
3397
  if (!Number.isInteger(value) || value < 1) throw new CodeIndexError("min-lines must be a positive integer.");
3362
3398
  return value;
3363
3399
  }
3364
- function functionNameRegex() {
3365
- 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;
3366
3405
  }
3367
3406
  function crossSearchSourceFilter() {
3368
3407
  const changedSince = parsed.values["changed-since"];
3369
3408
  const uncommitted = parsed.values.uncommitted;
3409
+ const nameRegex = qualifiedNameRegex();
3370
3410
  const restrictions = {
3371
3411
  ...parsed.values["source-path"] ? { path: parsed.values["source-path"] } : {},
3372
- ...parsed.values.regexp !== void 0 ? { nameRegex: parsed.values.regexp } : {}
3412
+ ...nameRegex !== void 0 ? { nameRegex } : {}
3373
3413
  };
3374
3414
  if (changedSince) return { type: "changed-since", commit: changedSince, ...uncommitted ? { uncommitted: true } : {}, ...restrictions };
3375
3415
  if (uncommitted) return { type: "uncommitted", ...restrictions };
@@ -3432,7 +3472,7 @@ Commands:
3432
3472
  delete-files <path...> Remove specific files from the index
3433
3473
  update-git Index a Git snapshot plus working-tree changes
3434
3474
  search <query> Search functions by semantic similarity
3435
- use-summaries Generate purpose summaries and enable automatic updates
3475
+ summaries <enable|disable> Enable or disable automatic purpose summaries
3436
3476
  search-summary <query> Search functions using summary embeddings
3437
3477
  cross-search Find nearest functions for each source function
3438
3478
  cohesion Rank related functions separated across the repository
@@ -3520,6 +3560,7 @@ Reading Analysis Output:
3520
3560
  Higher means more of a file's related affinity lies outside its folder
3521
3561
 
3522
3562
  Options:
3563
+ --version Show the package version
3523
3564
  --root <path> Repository root (default: current directory)
3524
3565
  --config <path> Config file (default: .slopdex/config.json)
3525
3566
  --index <path> SQLite index path
@@ -3535,13 +3576,13 @@ Options:
3535
3576
  --limit <number> Search result limit
3536
3577
  --neighbors <number> Semantic neighbors per function for cohesion (default: 20)
3537
3578
  --threshold <number|range> Show similarities at/above a value or within a range
3538
- --format <json|summary|clusters> Output format (search: json; cross-search: clusters)
3579
+ --format <json|summary|clusters> Output format (default: summary; cross-search: clusters)
3539
3580
  --include-source Include function source in cohesion JSON
3540
3581
  --include-symmetric-duplicates Show both directions of same-index matches
3541
3582
  --cross-file-only Exclude matches from the source file
3542
3583
  --min-lines <number> Minimum callable length for cross-search (default: 2)
3543
3584
  -e, --regexp <regex> Filter qualified symbols (analysis: sources only)
3544
- --regex <regex> Filter both analysis sources and matching candidates
3585
+ --regex <regex> Alias for -e/--regexp
3545
3586
  --changed-since <commit> Search added, modified, or moved functions
3546
3587
  --uncommitted Search functions from uncommitted files
3547
3588
  --source-path <path> Restrict cross-search sources to a file or directory
@@ -3569,7 +3610,7 @@ Other Examples:
3569
3610
  slopdex search "validate an authenticated session" --limit 10
3570
3611
 
3571
3612
  Enable purpose summaries, then search by their meaning:
3572
- slopdex use-summaries
3613
+ slopdex summaries enable
3573
3614
  slopdex search-summary "maintain the repository index" --format summary
3574
3615
 
3575
3616
  Review functions under a path against the whole codebase:
@@ -3584,10 +3625,10 @@ Other Examples:
3584
3625
  Filter semantic search results by qualified symbol before applying the limit:
3585
3626
  slopdex search "validate session" -e '^Session\\.' --limit 10
3586
3627
 
3587
- -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
3588
3629
  and cohesion it filters sources only; targets keep their normal eligibility rules.
3589
3630
  With --changed-since and --uncommitted, sources must be changed since the commit and
3590
- 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.
3591
3632
  `);
3592
3633
  }
3593
3634
  //# sourceMappingURL=cli.js.map