@wrongstack/tools 0.68.0 → 0.77.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.
@@ -1,6 +1,29 @@
1
- import { d as Symbol, F as FileMeta, e as SymbolKind, f as SymbolLang, c as SearchResult, b as IndexStats, R as Ref } from '../codebase-stats-tool-C8ApERbn.js';
2
- export { a as FileSymbols, I as IndexResult, S as SCHEMA_VERSION, g as codebaseIndexTool, h as codebaseSearchTool, i as codebaseStatsTool } from '../codebase-stats-tool-C8ApERbn.js';
3
- import '@wrongstack/core';
1
+ import { I as IndexResult, d as Symbol, F as FileMeta, e as SymbolKind, f as SymbolLang, c as SearchResult, b as IndexStats, R as Ref } from '../background-indexer-C70RD7LU.js';
2
+ export { a as FileSymbols, S as SCHEMA_VERSION, g as cancelPendingReindexes, h as codebaseIndexTool, i as codebaseSearchTool, j as codebaseStatsTool, k as enqueueReindex, l as getIndexState, m as isIndexReady, n as isIndexableFile, o as isIndexing, p as onIndexStateChange, r as runStartupIndex } from '../background-indexer-C70RD7LU.js';
3
+ import { Context } from '@wrongstack/core';
4
+
5
+ /**
6
+ * Main indexing orchestrator.
7
+ *
8
+ * Given a project root and a list of files:
9
+ * 1. Parse each file with the appropriate parser (TS, Go, Python, Rust, JSON, YAML)
10
+ * 2. Delete old symbols for changed/deleted files
11
+ * 3. Insert new symbols
12
+ * 4. Update file metadata
13
+ * 5. Return index statistics
14
+ */
15
+
16
+ interface IndexerOptions {
17
+ projectRoot: string;
18
+ files?: string[];
19
+ force?: boolean;
20
+ langs?: string[];
21
+ ignore?: string[];
22
+ /** Override the index directory (default: the global per-project dir). */
23
+ indexDir?: string;
24
+ }
25
+ /** Run a full or incremental index and return statistics. */
26
+ declare function runIndexer(_ctx: Context, opts: IndexerOptions): Promise<IndexResult>;
4
27
 
5
28
  /**
6
29
  * SQLite storage layer for the codebase index.
@@ -50,6 +73,14 @@ declare class IndexStore {
50
73
  id: number;
51
74
  text: string;
52
75
  }>;
76
+ /**
77
+ * Largest symbol id currently in the table (0 when empty). New ids must be
78
+ * allocated from this, NOT from `COUNT(*)`: incremental reindexes delete a
79
+ * changed file's rows, so the row count drops below the max id and a
80
+ * count-based id would collide with a surviving row (UNIQUE constraint on
81
+ * `symbols.id`). Ids may have gaps — that is fine.
82
+ */
83
+ getMaxSymbolId(): number;
53
84
  getStats(): IndexStats;
54
85
  setLastIndexed(ts: number): void;
55
86
  clearAll(): void;
@@ -137,4 +168,4 @@ declare function lspKindToInternalKind(k: number): SymbolKind | null;
137
168
  */
138
169
  declare function internalKindToLspKind(k: SymbolKind): number | null;
139
170
 
140
- export { FileMeta, IndexStats, IndexStore, SearchResult, Symbol, SymbolKind, SymbolLang, buildBm25Index, buildIndexableText, codebaseIndexDirOverride, internalKindToLspKind, lspKindToInternalKind, resolveIndexDir, tokenise };
171
+ export { FileMeta, IndexResult, IndexStats, IndexStore, SearchResult, Symbol, SymbolKind, SymbolLang, buildBm25Index, buildIndexableText, codebaseIndexDirOverride, internalKindToLspKind, lspKindToInternalKind, resolveIndexDir, runIndexer, tokenise };
@@ -1,4 +1,4 @@
1
- import * as fs2 from 'node:fs/promises';
1
+ import * as fs3 from 'node:fs/promises';
2
2
  import * as path4 from 'node:path';
3
3
  import { resolveWstackPaths, compileGlob } from '@wrongstack/core';
4
4
  import { createRequire } from 'node:module';
@@ -290,6 +290,17 @@ var IndexStore = class {
290
290
  ({ id, text }) => ({ id, text })
291
291
  );
292
292
  }
293
+ /**
294
+ * Largest symbol id currently in the table (0 when empty). New ids must be
295
+ * allocated from this, NOT from `COUNT(*)`: incremental reindexes delete a
296
+ * changed file's rows, so the row count drops below the max id and a
297
+ * count-based id would collide with a surviving row (UNIQUE constraint on
298
+ * `symbols.id`). Ids may have gaps — that is fine.
299
+ */
300
+ getMaxSymbolId() {
301
+ const rows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
302
+ return rows[0]?.m ?? 0;
303
+ }
293
304
  // ─── Stats ───────────────────────────────────────────────────────────────────
294
305
  getStats() {
295
306
  const sizeBytes = this.sizeBytes();
@@ -1608,8 +1619,181 @@ function makeSymbol2(opts) {
1608
1619
  text: `${opts.name} ${opts.signature}`.trim()
1609
1620
  };
1610
1621
  }
1622
+ function globBody(glob) {
1623
+ return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
1624
+ }
1625
+ function compileGitignore(lines) {
1626
+ const rules = [];
1627
+ for (const raw of lines) {
1628
+ let line = raw.replace(/\r$/, "");
1629
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
1630
+ line = line.trim();
1631
+ let negated = false;
1632
+ if (line.startsWith("!")) {
1633
+ negated = true;
1634
+ line = line.slice(1);
1635
+ }
1636
+ let dirOnly = false;
1637
+ if (line.endsWith("/")) {
1638
+ dirOnly = true;
1639
+ line = line.slice(0, -1);
1640
+ }
1641
+ if (!line) continue;
1642
+ const anchored = line.startsWith("/") || line.includes("/");
1643
+ if (line.startsWith("/")) line = line.slice(1);
1644
+ const body = globBody(line);
1645
+ const prefix = anchored ? "^" : "(?:^|.*/)";
1646
+ rules.push({
1647
+ eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
1648
+ under: new RegExp(`${prefix}${body}/.*$`),
1649
+ negated,
1650
+ dirOnly
1651
+ });
1652
+ }
1653
+ return (relPath, isDir) => {
1654
+ const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
1655
+ let ignored = false;
1656
+ for (const r of rules) {
1657
+ const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
1658
+ if (re.test(p)) ignored = !r.negated;
1659
+ }
1660
+ return ignored;
1661
+ };
1662
+ }
1663
+ async function loadGitignoreMatcher(projectRoot) {
1664
+ let lines = [];
1665
+ try {
1666
+ const raw = await fs3.readFile(path4.join(projectRoot, ".gitignore"), "utf8");
1667
+ lines = raw.split("\n");
1668
+ } catch {
1669
+ }
1670
+ return compileGitignore(lines);
1671
+ }
1672
+
1673
+ // src/codebase-index/background-indexer.ts
1674
+ var _ready = false;
1675
+ var _indexing = false;
1676
+ var _currentFile = 0;
1677
+ var _totalFiles = 0;
1678
+ var _lastError = null;
1679
+ function isIndexReady() {
1680
+ return _ready;
1681
+ }
1682
+ function setIndexReady() {
1683
+ _ready = true;
1684
+ }
1685
+ function isIndexing() {
1686
+ return _indexing;
1687
+ }
1688
+ function getIndexState() {
1689
+ return {
1690
+ ready: _ready,
1691
+ indexing: _indexing,
1692
+ currentFile: _currentFile,
1693
+ totalFiles: _totalFiles,
1694
+ lastError: _lastError
1695
+ };
1696
+ }
1697
+ var _listeners = [];
1698
+ function onIndexStateChange(listener) {
1699
+ _listeners.push(listener);
1700
+ return () => {
1701
+ _listeners = _listeners.filter((l) => l !== listener);
1702
+ };
1703
+ }
1704
+ function emitState() {
1705
+ const state = getIndexState();
1706
+ for (const l of _listeners) l(state);
1707
+ }
1708
+ function _setIndexProgress(current, total) {
1709
+ _currentFile = current;
1710
+ _totalFiles = total;
1711
+ emitState();
1712
+ }
1713
+ function stubCtx(projectRoot) {
1714
+ return {
1715
+ projectRoot,
1716
+ cwd: projectRoot,
1717
+ messages: [],
1718
+ todos: [],
1719
+ readFiles: /* @__PURE__ */ new Set(),
1720
+ fileMtimes: /* @__PURE__ */ new Map()
1721
+ };
1722
+ }
1723
+ var chain = Promise.resolve();
1724
+ function withMutex(job) {
1725
+ const run = chain.then(job, job);
1726
+ chain = run.then(
1727
+ () => void 0,
1728
+ () => void 0
1729
+ );
1730
+ return run;
1731
+ }
1732
+ var DEFAULT_DEBOUNCE_MS = 400;
1733
+ var debounceTimers = /* @__PURE__ */ new Map();
1734
+ function debounceKey(indexDir, file) {
1735
+ return `${indexDir ?? ""}|${file}`;
1736
+ }
1737
+ function isIndexableFile(filePath) {
1738
+ return detectLang(filePath) !== null;
1739
+ }
1740
+ async function runStartupIndex(opts) {
1741
+ _indexing = true;
1742
+ _currentFile = 0;
1743
+ _totalFiles = 0;
1744
+ _lastError = null;
1745
+ emitState();
1746
+ try {
1747
+ const result = await withMutex(
1748
+ () => runIndexer(stubCtx(opts.projectRoot), {
1749
+ projectRoot: opts.projectRoot,
1750
+ indexDir: opts.indexDir,
1751
+ force: opts.force
1752
+ })
1753
+ );
1754
+ _ready = true;
1755
+ return result;
1756
+ } catch (err) {
1757
+ _lastError = err instanceof Error ? err.message : String(err);
1758
+ _ready = true;
1759
+ throw err;
1760
+ } finally {
1761
+ _indexing = false;
1762
+ emitState();
1763
+ }
1764
+ }
1765
+ function enqueueReindex(opts) {
1766
+ const files = opts.files.filter(isIndexableFile);
1767
+ if (files.length === 0) return;
1768
+ const ms = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
1769
+ for (const file of files) {
1770
+ const key = debounceKey(opts.indexDir, file);
1771
+ const existing = debounceTimers.get(key);
1772
+ if (existing) clearTimeout(existing);
1773
+ const timer = setTimeout(() => {
1774
+ debounceTimers.delete(key);
1775
+ void withMutex(
1776
+ () => runIndexer(stubCtx(opts.projectRoot), {
1777
+ projectRoot: opts.projectRoot,
1778
+ files: [file],
1779
+ indexDir: opts.indexDir
1780
+ })
1781
+ ).catch((err) => opts.onError?.(err));
1782
+ }, ms);
1783
+ timer.unref?.();
1784
+ debounceTimers.set(key, timer);
1785
+ }
1786
+ }
1787
+ function cancelPendingReindexes() {
1788
+ for (const t of debounceTimers.values()) clearTimeout(t);
1789
+ debounceTimers.clear();
1790
+ }
1611
1791
 
1612
1792
  // src/codebase-index/indexer.ts
1793
+ var YIELD_EVERY_N = 50;
1794
+ function yieldEventLoop() {
1795
+ return new Promise((resolve2) => setImmediate(resolve2));
1796
+ }
1613
1797
  var DEFAULT_IGNORE = [
1614
1798
  "node_modules",
1615
1799
  ".git",
@@ -1621,7 +1805,7 @@ var DEFAULT_IGNORE = [
1621
1805
  "__snapshots__",
1622
1806
  ".nyc_output"
1623
1807
  ];
1624
- async function findSourceFiles(projectRoot, ignore) {
1808
+ async function findSourceFiles(projectRoot, ignore, isGitIgnored) {
1625
1809
  const results = [];
1626
1810
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
1627
1811
  const globs = [
@@ -1639,17 +1823,19 @@ async function findSourceFiles(projectRoot, ignore) {
1639
1823
  const walk = async (dir) => {
1640
1824
  let entries;
1641
1825
  try {
1642
- entries = await fs2.readdir(dir, { withFileTypes: true });
1826
+ entries = await fs3.readdir(dir, { withFileTypes: true });
1643
1827
  } catch {
1644
1828
  return;
1645
1829
  }
1646
1830
  for (const e of entries) {
1647
1831
  if (ignoreSet.has(e.name)) continue;
1648
1832
  const full = path4.join(dir, e.name);
1833
+ const rel = path4.relative(projectRoot, full).replace(/\\/g, "/");
1649
1834
  if (e.isDirectory()) {
1835
+ if (isGitIgnored(rel, true)) continue;
1650
1836
  await walk(full);
1651
1837
  } else if (e.isFile()) {
1652
- const rel = path4.relative(projectRoot, full).replace(/\\/g, "/");
1838
+ if (isGitIgnored(rel, false)) continue;
1653
1839
  const ext = path4.extname(e.name);
1654
1840
  for (const { ext: extName, pat } of globs) {
1655
1841
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
@@ -1692,11 +1878,12 @@ async function runIndexer(_ctx, opts) {
1692
1878
  const langStats = {};
1693
1879
  let filesIndexed = 0;
1694
1880
  let symbolsIndexed = 0;
1881
+ const isGitIgnored = await loadGitignoreMatcher(projectRoot);
1695
1882
  let files;
1696
1883
  if (opts.files && opts.files.length > 0) {
1697
- files = opts.files.map((f) => path4.resolve(projectRoot, f));
1884
+ files = opts.files.map((f) => path4.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path4.relative(projectRoot, f).replace(/\\/g, "/"), false));
1698
1885
  } else {
1699
- files = await findSourceFiles(projectRoot, ignore);
1886
+ files = await findSourceFiles(projectRoot, ignore, isGitIgnored);
1700
1887
  }
1701
1888
  if (langs && langs.length > 0) {
1702
1889
  const langSet = new Set(langs);
@@ -1710,10 +1897,15 @@ async function runIndexer(_ctx, opts) {
1710
1897
  if (!force) {
1711
1898
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
1712
1899
  }
1713
- for (const file of files) {
1900
+ for (let fi = 0; fi < files.length; fi++) {
1901
+ const file = files[fi];
1902
+ _setIndexProgress(fi + 1, files.length);
1903
+ if (fi > 0 && fi % YIELD_EVERY_N === 0) {
1904
+ await yieldEventLoop();
1905
+ }
1714
1906
  let stat2;
1715
1907
  try {
1716
- stat2 = await fs2.stat(file);
1908
+ stat2 = await fs3.stat(file);
1717
1909
  } catch {
1718
1910
  store.deleteFile(file);
1719
1911
  continue;
@@ -1728,11 +1920,11 @@ async function runIndexer(_ctx, opts) {
1728
1920
  filesIndexed++;
1729
1921
  continue;
1730
1922
  }
1731
- store.deleteSymbolsForFile(file);
1732
1923
  store.deleteRefsForFile(file);
1924
+ store.deleteSymbolsForFile(file);
1733
1925
  let content;
1734
1926
  try {
1735
- content = await fs2.readFile(file, "utf8");
1927
+ content = await fs3.readFile(file, "utf8");
1736
1928
  } catch (e) {
1737
1929
  errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
1738
1930
  continue;
@@ -1755,7 +1947,7 @@ async function runIndexer(_ctx, opts) {
1755
1947
  filesIndexed++;
1756
1948
  continue;
1757
1949
  }
1758
- const nextId = store.getStats().totalSymbols + 1;
1950
+ const nextId = store.getMaxSymbolId() + 1;
1759
1951
  const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
1760
1952
  store.insertSymbols(symbolsWithIds, nextId);
1761
1953
  const count = symbolsWithIds.length;
@@ -1782,7 +1974,7 @@ async function runIndexer(_ctx, opts) {
1782
1974
  }
1783
1975
  for (const [file_] of existingMeta) {
1784
1976
  try {
1785
- await fs2.stat(file_);
1977
+ await fs3.stat(file_);
1786
1978
  } catch {
1787
1979
  store.deleteFile(file_);
1788
1980
  }
@@ -1824,12 +2016,23 @@ var codebaseIndexTool = {
1824
2016
  }
1825
2017
  },
1826
2018
  async execute(input, ctx) {
2019
+ if (isIndexing()) {
2020
+ return {
2021
+ filesIndexed: 0,
2022
+ symbolsIndexed: 0,
2023
+ langStats: {},
2024
+ durationMs: 0,
2025
+ errors: [],
2026
+ note: "A full index is already in progress. Retry codebase-index after it completes (check codebase-stats)."
2027
+ };
2028
+ }
1827
2029
  const result = await runIndexer(ctx, {
1828
2030
  projectRoot: ctx.projectRoot,
1829
2031
  force: input.force ?? false,
1830
2032
  langs: input.langs,
1831
2033
  indexDir: codebaseIndexDirOverride(ctx)
1832
2034
  });
2035
+ setIndexReady();
1833
2036
  return result;
1834
2037
  }
1835
2038
  };
@@ -1965,6 +2168,31 @@ var codebaseSearchTool = {
1965
2168
  required: ["query"]
1966
2169
  },
1967
2170
  async execute(input, ctx) {
2171
+ const state = getIndexState();
2172
+ if (!state.ready) {
2173
+ return {
2174
+ results: [],
2175
+ total: 0,
2176
+ query: input.query,
2177
+ indexStatus: state.indexing ? `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.` : "Index not yet built. The codebase is being indexed at startup \u2014 search will be available shortly."
2178
+ };
2179
+ }
2180
+ if (state.indexing) {
2181
+ return {
2182
+ results: [],
2183
+ total: 0,
2184
+ query: input.query,
2185
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
2186
+ };
2187
+ }
2188
+ if (state.lastError) {
2189
+ return {
2190
+ results: [],
2191
+ total: 0,
2192
+ query: input.query,
2193
+ indexStatus: `Index build failed: ${state.lastError}. Try /codebase-reindex.`
2194
+ };
2195
+ }
1968
2196
  const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
1969
2197
  try {
1970
2198
  const limit = Math.min(input.limit ?? 20, 100);
@@ -2022,6 +2250,32 @@ var codebaseStatsTool = {
2022
2250
  additionalProperties: false
2023
2251
  },
2024
2252
  async execute(_input, ctx) {
2253
+ const idxState = getIndexState();
2254
+ if (!idxState.ready) {
2255
+ return {
2256
+ totalSymbols: 0,
2257
+ totalFiles: 0,
2258
+ byLang: {},
2259
+ byKind: {},
2260
+ lastIndexed: null,
2261
+ sizeBytes: 0,
2262
+ indexPath: "",
2263
+ version: SCHEMA_VERSION,
2264
+ indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
2265
+ };
2266
+ }
2267
+ if (idxState.indexing) {
2268
+ const store2 = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
2269
+ try {
2270
+ const stats = store2.getStats();
2271
+ return {
2272
+ ...stats,
2273
+ indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
2274
+ };
2275
+ } finally {
2276
+ store2.close();
2277
+ }
2278
+ }
2025
2279
  const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
2026
2280
  try {
2027
2281
  const stats = store.getStats();
@@ -2041,6 +2295,6 @@ var codebaseStatsTool = {
2041
2295
  }
2042
2296
  };
2043
2297
 
2044
- export { IndexStore, SCHEMA_VERSION, buildBm25Index, buildIndexableText, codebaseIndexDirOverride, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, internalKindToLspKind, lspKindToInternalKind, resolveIndexDir, tokenise };
2298
+ export { IndexStore, SCHEMA_VERSION, buildBm25Index, buildIndexableText, cancelPendingReindexes, codebaseIndexDirOverride, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, enqueueReindex, getIndexState, internalKindToLspKind, isIndexReady, isIndexableFile, isIndexing, lspKindToInternalKind, onIndexStateChange, resolveIndexDir, runIndexer, runStartupIndex, tokenise };
2045
2299
  //# sourceMappingURL=index.js.map
2046
2300
  //# sourceMappingURL=index.js.map