@wrongstack/plugins 0.318.0 → 0.319.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/index.js CHANGED
@@ -19000,7 +19000,10 @@ var state45 = {
19000
19000
  buildPromise: null,
19001
19001
  buildGeneration: 0,
19002
19002
  publishedGeneration: 0,
19003
- hookUnregister: null
19003
+ hookUnregister: null,
19004
+ deleteHookUnregister: null,
19005
+ lastPruneAt: 0,
19006
+ pruneInFlight: null
19004
19007
  };
19005
19008
  var DEFAULTS43 = {
19006
19009
  enabled: true,
@@ -19115,11 +19118,11 @@ var YIELD_EVERY_FILES = 64;
19115
19118
  function yieldEventLoop() {
19116
19119
  return new Promise((resolve28) => setImmediate(resolve28));
19117
19120
  }
19118
- function addFileToIndex(relPath, content, size, cfg) {
19121
+ function addFileToIndex(relPath, absPath, content, size, cfg) {
19119
19122
  if (!state45.index || content.includes("\0")) return;
19120
19123
  state45.bytesIndexed += content.length;
19121
19124
  const lines = content.split(/\r?\n/);
19122
- state45.index.files.set(relPath, { lines, size });
19125
+ state45.index.files.set(relPath, { lines, size, absPath });
19123
19126
  for (let i = 0; i < lines.length; i += 1) {
19124
19127
  const terms = tokenize(lines[i], cfg.minTokenLength);
19125
19128
  for (const term of terms) {
@@ -19145,7 +19148,7 @@ async function indexFileFromStats(absPath, relPath, stats, cfg) {
19145
19148
  } catch {
19146
19149
  return;
19147
19150
  }
19148
- addFileToIndex(relPath, content, stats.size, cfg);
19151
+ addFileToIndex(relPath, absPath, content, stats.size, cfg);
19149
19152
  }
19150
19153
  async function flushFileBatch(batch, cfg) {
19151
19154
  if (batch.length === 0) return;
@@ -19238,6 +19241,51 @@ async function ensureIndex(rootPath, cfg) {
19238
19241
  state45.cachedPath = null;
19239
19242
  }
19240
19243
  }
19244
+ var PRUNE_MIN_INTERVAL_MS = 3e4;
19245
+ function removeFileFromIndex(relPath, cfg) {
19246
+ const index = state45.index;
19247
+ if (!index) return;
19248
+ const entry = index.files.get(relPath);
19249
+ if (!entry) return;
19250
+ const terms = /* @__PURE__ */ new Set();
19251
+ for (const line of entry.lines) {
19252
+ for (const term of tokenize(line, cfg.minTokenLength)) terms.add(term);
19253
+ }
19254
+ for (const term of terms) {
19255
+ const postings = index.terms.get(term);
19256
+ if (!postings) continue;
19257
+ postings.delete(relPath);
19258
+ if (postings.size === 0) index.terms.delete(term);
19259
+ }
19260
+ index.files.delete(relPath);
19261
+ state45.fileCount = Math.max(0, state45.fileCount - 1);
19262
+ state45.bytesIndexed = Math.max(0, state45.bytesIndexed - entry.size);
19263
+ state45.termCount = index.terms.size;
19264
+ }
19265
+ async function pruneDeletedFiles(rootPath, cfg, opts) {
19266
+ const index = state45.index;
19267
+ if (!index || state45.cachedPath !== rootPath) return 0;
19268
+ if (state45.pruneInFlight) return state45.pruneInFlight;
19269
+ const now = Date.now();
19270
+ if (!opts?.force && now - state45.lastPruneAt < PRUNE_MIN_INTERVAL_MS) return 0;
19271
+ state45.lastPruneAt = now;
19272
+ const pass = (async () => {
19273
+ const missing = [];
19274
+ for (const [relPath, entry] of index.files) {
19275
+ try {
19276
+ await fs2.stat(entry.absPath);
19277
+ } catch {
19278
+ missing.push(relPath);
19279
+ }
19280
+ }
19281
+ for (const relPath of missing) removeFileFromIndex(relPath, cfg);
19282
+ return missing.length;
19283
+ })();
19284
+ state45.pruneInFlight = pass.finally(() => {
19285
+ state45.pruneInFlight = null;
19286
+ });
19287
+ return pass;
19288
+ }
19241
19289
  function compareRankedCandidates(a, b) {
19242
19290
  return b.score - a.score || a.path.localeCompare(b.path);
19243
19291
  }
@@ -19384,6 +19432,15 @@ var plugin49 = {
19384
19432
  }
19385
19433
  state45.hookUnregister = null;
19386
19434
  }
19435
+ if (state45.deleteHookUnregister) {
19436
+ try {
19437
+ state45.deleteHookUnregister();
19438
+ } catch {
19439
+ }
19440
+ state45.deleteHookUnregister = null;
19441
+ }
19442
+ state45.lastPruneAt = 0;
19443
+ state45.pruneInFlight = null;
19387
19444
  const cfg = readConfig45(api.config.extensions?.["semantic-search-indexer"]);
19388
19445
  api.tools.register({
19389
19446
  name: "semantic_search",
@@ -19493,6 +19550,17 @@ var plugin49 = {
19493
19550
  { background: true }
19494
19551
  );
19495
19552
  }
19553
+ if (typeof api.registerHook === "function") {
19554
+ state45.deleteHookUnregister = api.registerHook(
19555
+ "PostToolUse",
19556
+ "bash|exec|pwsh|delete_file|remove_file",
19557
+ (() => {
19558
+ const root = state45.cachedPath;
19559
+ if (root) void pruneDeletedFiles(root, cfg);
19560
+ }),
19561
+ { background: true }
19562
+ );
19563
+ }
19496
19564
  api.log.info("semantic-search-indexer plugin loaded", {
19497
19565
  version: "0.1.0",
19498
19566
  defaultLimit: cfg.defaultLimit,
@@ -19507,6 +19575,13 @@ var plugin49 = {
19507
19575
  }
19508
19576
  state45.hookUnregister = null;
19509
19577
  }
19578
+ if (state45.deleteHookUnregister) {
19579
+ try {
19580
+ state45.deleteHookUnregister();
19581
+ } catch {
19582
+ }
19583
+ state45.deleteHookUnregister = null;
19584
+ }
19510
19585
  const final = {
19511
19586
  fileCount: state45.fileCount,
19512
19587
  termCount: state45.termCount,
@@ -19524,6 +19599,8 @@ var plugin49 = {
19524
19599
  state45.buildPromise = null;
19525
19600
  state45.buildGeneration = 0;
19526
19601
  state45.publishedGeneration = 0;
19602
+ state45.lastPruneAt = 0;
19603
+ state45.pruneInFlight = null;
19527
19604
  api.log.info("semantic-search-indexer: teardown complete", { final });
19528
19605
  },
19529
19606
  async health() {
@@ -42,6 +42,32 @@ interface SemanticSearchConfig {
42
42
  maxFiles: number;
43
43
  }
44
44
  export declare function readConfig(raw: unknown): SemanticSearchConfig;
45
+ /**
46
+ * Remove one indexed file's entries (postings + file record) from the index.
47
+ * The stored lines re-derive exactly the terms that were indexed for the
48
+ * file, so each term's posting for it can be dropped and emptied posting
49
+ * maps pruned — no rebuild needed.
50
+ */
51
+ declare function removeFileFromIndex(relPath: string, cfg: SemanticSearchConfig): void;
52
+ /**
53
+ * Stat-scan the indexed files and drop entries whose file no longer exists —
54
+ * the delete-side counterpart of the write-path generation bump. Shell tools
55
+ * cannot be matched to exact deleted paths reliably, so instead of a full
56
+ * rebuild we prune exactly the dead entries. Throttled and single-flight;
57
+ * resolves with the number of entries removed.
58
+ */
59
+ declare function pruneDeletedFiles(rootPath: string, cfg: SemanticSearchConfig, opts?: {
60
+ force?: boolean;
61
+ }): Promise<number>;
45
62
  declare const plugin: Plugin;
63
+ /**
64
+ * Test seam: direct access to the incremental delete-prune. `force` bypasses
65
+ * the throttle window (production callers go through the PostToolUse hook).
66
+ */
67
+ export declare const semanticIndexerCoverage: {
68
+ removeFileFromIndex: typeof removeFileFromIndex;
69
+ pruneDeletedFiles: typeof pruneDeletedFiles;
70
+ readConfig: typeof readConfig;
71
+ };
46
72
  export default plugin;
47
73
  //# sourceMappingURL=index.d.ts.map
@@ -16,7 +16,10 @@ var state = {
16
16
  buildPromise: null,
17
17
  buildGeneration: 0,
18
18
  publishedGeneration: 0,
19
- hookUnregister: null
19
+ hookUnregister: null,
20
+ deleteHookUnregister: null,
21
+ lastPruneAt: 0,
22
+ pruneInFlight: null
20
23
  };
21
24
  var DEFAULTS = {
22
25
  enabled: true,
@@ -131,11 +134,11 @@ var YIELD_EVERY_FILES = 64;
131
134
  function yieldEventLoop() {
132
135
  return new Promise((resolve2) => setImmediate(resolve2));
133
136
  }
134
- function addFileToIndex(relPath, content, size, cfg) {
137
+ function addFileToIndex(relPath, absPath, content, size, cfg) {
135
138
  if (!state.index || content.includes("\0")) return;
136
139
  state.bytesIndexed += content.length;
137
140
  const lines = content.split(/\r?\n/);
138
- state.index.files.set(relPath, { lines, size });
141
+ state.index.files.set(relPath, { lines, size, absPath });
139
142
  for (let i = 0; i < lines.length; i += 1) {
140
143
  const terms = tokenize(lines[i], cfg.minTokenLength);
141
144
  for (const term of terms) {
@@ -161,7 +164,7 @@ async function indexFileFromStats(absPath, relPath, stats, cfg) {
161
164
  } catch {
162
165
  return;
163
166
  }
164
- addFileToIndex(relPath, content, stats.size, cfg);
167
+ addFileToIndex(relPath, absPath, content, stats.size, cfg);
165
168
  }
166
169
  async function flushFileBatch(batch, cfg) {
167
170
  if (batch.length === 0) return;
@@ -254,6 +257,51 @@ async function ensureIndex(rootPath, cfg) {
254
257
  state.cachedPath = null;
255
258
  }
256
259
  }
260
+ var PRUNE_MIN_INTERVAL_MS = 3e4;
261
+ function removeFileFromIndex(relPath, cfg) {
262
+ const index = state.index;
263
+ if (!index) return;
264
+ const entry = index.files.get(relPath);
265
+ if (!entry) return;
266
+ const terms = /* @__PURE__ */ new Set();
267
+ for (const line of entry.lines) {
268
+ for (const term of tokenize(line, cfg.minTokenLength)) terms.add(term);
269
+ }
270
+ for (const term of terms) {
271
+ const postings = index.terms.get(term);
272
+ if (!postings) continue;
273
+ postings.delete(relPath);
274
+ if (postings.size === 0) index.terms.delete(term);
275
+ }
276
+ index.files.delete(relPath);
277
+ state.fileCount = Math.max(0, state.fileCount - 1);
278
+ state.bytesIndexed = Math.max(0, state.bytesIndexed - entry.size);
279
+ state.termCount = index.terms.size;
280
+ }
281
+ async function pruneDeletedFiles(rootPath, cfg, opts) {
282
+ const index = state.index;
283
+ if (!index || state.cachedPath !== rootPath) return 0;
284
+ if (state.pruneInFlight) return state.pruneInFlight;
285
+ const now = Date.now();
286
+ if (!opts?.force && now - state.lastPruneAt < PRUNE_MIN_INTERVAL_MS) return 0;
287
+ state.lastPruneAt = now;
288
+ const pass = (async () => {
289
+ const missing = [];
290
+ for (const [relPath, entry] of index.files) {
291
+ try {
292
+ await fs.stat(entry.absPath);
293
+ } catch {
294
+ missing.push(relPath);
295
+ }
296
+ }
297
+ for (const relPath of missing) removeFileFromIndex(relPath, cfg);
298
+ return missing.length;
299
+ })();
300
+ state.pruneInFlight = pass.finally(() => {
301
+ state.pruneInFlight = null;
302
+ });
303
+ return pass;
304
+ }
257
305
  function compareRankedCandidates(a, b) {
258
306
  return b.score - a.score || a.path.localeCompare(b.path);
259
307
  }
@@ -400,6 +448,15 @@ var plugin = {
400
448
  }
401
449
  state.hookUnregister = null;
402
450
  }
451
+ if (state.deleteHookUnregister) {
452
+ try {
453
+ state.deleteHookUnregister();
454
+ } catch {
455
+ }
456
+ state.deleteHookUnregister = null;
457
+ }
458
+ state.lastPruneAt = 0;
459
+ state.pruneInFlight = null;
403
460
  const cfg = readConfig(api.config.extensions?.["semantic-search-indexer"]);
404
461
  api.tools.register({
405
462
  name: "semantic_search",
@@ -509,6 +566,17 @@ var plugin = {
509
566
  { background: true }
510
567
  );
511
568
  }
569
+ if (typeof api.registerHook === "function") {
570
+ state.deleteHookUnregister = api.registerHook(
571
+ "PostToolUse",
572
+ "bash|exec|pwsh|delete_file|remove_file",
573
+ (() => {
574
+ const root = state.cachedPath;
575
+ if (root) void pruneDeletedFiles(root, cfg);
576
+ }),
577
+ { background: true }
578
+ );
579
+ }
512
580
  api.log.info("semantic-search-indexer plugin loaded", {
513
581
  version: "0.1.0",
514
582
  defaultLimit: cfg.defaultLimit,
@@ -523,6 +591,13 @@ var plugin = {
523
591
  }
524
592
  state.hookUnregister = null;
525
593
  }
594
+ if (state.deleteHookUnregister) {
595
+ try {
596
+ state.deleteHookUnregister();
597
+ } catch {
598
+ }
599
+ state.deleteHookUnregister = null;
600
+ }
526
601
  const final = {
527
602
  fileCount: state.fileCount,
528
603
  termCount: state.termCount,
@@ -540,6 +615,8 @@ var plugin = {
540
615
  state.buildPromise = null;
541
616
  state.buildGeneration = 0;
542
617
  state.publishedGeneration = 0;
618
+ state.lastPruneAt = 0;
619
+ state.pruneInFlight = null;
543
620
  api.log.info("semantic-search-indexer: teardown complete", { final });
544
621
  },
545
622
  async health() {
@@ -556,8 +633,14 @@ var plugin = {
556
633
  };
557
634
  }
558
635
  };
636
+ var semanticIndexerCoverage = {
637
+ removeFileFromIndex,
638
+ pruneDeletedFiles,
639
+ readConfig
640
+ };
559
641
  var semantic_search_indexer_default = plugin;
560
642
  export {
561
643
  semantic_search_indexer_default as default,
562
- readConfig
644
+ readConfig,
645
+ semanticIndexerCoverage
563
646
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/plugins",
3
- "version": "0.318.0",
3
+ "version": "0.319.0",
4
4
  "description": "Official WrongStack collection of focused plugins for code quality, security, observability, planning, and agent coordination",
5
5
  "license": "MIT",
6
6
  "author": "ECOSTACK TECHNOLOGY OÜ",
@@ -303,10 +303,10 @@
303
303
  "vitest": "^4.1.11"
304
304
  },
305
305
  "dependencies": {
306
- "@wrongstack/core": "0.318.0",
307
- "@wrongstack/primitives": "0.318.0",
308
- "@wrongstack/plugin-sdk": "0.318.0",
309
- "@wrongstack/tools": "0.318.0"
306
+ "@wrongstack/core": "0.319.0",
307
+ "@wrongstack/primitives": "0.319.0",
308
+ "@wrongstack/plugin-sdk": "0.319.0",
309
+ "@wrongstack/tools": "0.319.0"
310
310
  },
311
311
  "scripts": {
312
312
  "build": "node ../../scripts/build-package.mjs",