@neuralsea/workspace-indexer 0.3.5 → 0.4.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.
@@ -2548,63 +2548,6 @@ var RepoIndexer = class {
2548
2548
  }
2549
2549
  };
2550
2550
 
2551
- // src/profiles.ts
2552
- var DEFAULT_PROFILES = {
2553
- search: {
2554
- name: "search",
2555
- k: 10,
2556
- weights: { vector: 0.65, lexical: 0.35, recency: 0 },
2557
- expand: { adjacentChunks: 0, followImports: 0, includeFileSynopsis: false },
2558
- candidates: { vectorK: 25, lexicalK: 25, maxMergedCandidates: 60 }
2559
- },
2560
- refactor: {
2561
- name: "refactor",
2562
- k: 15,
2563
- weights: { vector: 0.55, lexical: 0.35, recency: 0.1 },
2564
- expand: { adjacentChunks: 1, followImports: 2, includeFileSynopsis: true },
2565
- candidates: { vectorK: 60, lexicalK: 40, maxMergedCandidates: 140 }
2566
- },
2567
- review: {
2568
- name: "review",
2569
- k: 20,
2570
- weights: { vector: 0.45, lexical: 0.35, recency: 0.2 },
2571
- expand: { adjacentChunks: 1, followImports: 1, includeFileSynopsis: true },
2572
- candidates: { vectorK: 80, lexicalK: 60, maxMergedCandidates: 180 }
2573
- },
2574
- architecture: {
2575
- name: "architecture",
2576
- k: 20,
2577
- weights: { vector: 0.7, lexical: 0.2, recency: 0.1 },
2578
- expand: { adjacentChunks: 0, followImports: 3, includeFileSynopsis: true },
2579
- candidates: { vectorK: 120, lexicalK: 40, maxMergedCandidates: 220 }
2580
- },
2581
- rca: {
2582
- name: "rca",
2583
- k: 25,
2584
- weights: { vector: 0.5, lexical: 0.25, recency: 0.25 },
2585
- expand: { adjacentChunks: 2, followImports: 1, includeFileSynopsis: true },
2586
- candidates: { vectorK: 140, lexicalK: 80, maxMergedCandidates: 260 }
2587
- },
2588
- custom: {
2589
- name: "custom",
2590
- k: 10,
2591
- weights: { vector: 0.65, lexical: 0.35, recency: 0 },
2592
- expand: { adjacentChunks: 0, followImports: 0, includeFileSynopsis: false },
2593
- candidates: { vectorK: 25, lexicalK: 25, maxMergedCandidates: 60 }
2594
- }
2595
- };
2596
- function deepMergeProfile(base, patch) {
2597
- if (!patch) return base;
2598
- const merged = {
2599
- ...base,
2600
- ...patch,
2601
- weights: { ...base.weights, ...patch.weights ?? {} },
2602
- expand: { ...base.expand, ...patch.expand ?? {} },
2603
- candidates: { ...base.candidates, ...patch.candidates ?? {} }
2604
- };
2605
- return merged;
2606
- }
2607
-
2608
2551
  // src/indexer/repoDiscovery.ts
2609
2552
  import fs10 from "fs";
2610
2553
  import path13 from "path";
@@ -2767,9 +2710,163 @@ function mergeIndexerConfig(target, patch) {
2767
2710
  }
2768
2711
  }
2769
2712
 
2770
- // src/store/workspaceStore.ts
2713
+ // src/store/workspace/db.ts
2714
+ import Database3 from "better-sqlite3";
2715
+ import fs11 from "fs";
2716
+ import path14 from "path";
2717
+ function detectFts5Support(db) {
2718
+ try {
2719
+ const rows = db.prepare(`PRAGMA compile_options`).all();
2720
+ if (rows.some((r) => String(r.compile_options ?? "").includes("ENABLE_FTS5"))) return true;
2721
+ } catch {
2722
+ }
2723
+ try {
2724
+ db.exec(`
2725
+ CREATE VIRTUAL TABLE IF NOT EXISTS __fts5_probe USING fts5(x);
2726
+ DROP TABLE __fts5_probe;
2727
+ `);
2728
+ return true;
2729
+ } catch {
2730
+ return false;
2731
+ }
2732
+ }
2733
+ var BetterSqlite3Adapter = class {
2734
+ db;
2735
+ capabilities;
2736
+ constructor(dbPath) {
2737
+ this.db = new Database3(dbPath);
2738
+ this.capabilities = { supportsFts5: detectFts5Support(this.db) };
2739
+ }
2740
+ pragma(sql) {
2741
+ this.db.pragma(sql);
2742
+ }
2743
+ exec(sql) {
2744
+ this.db.exec(sql);
2745
+ }
2746
+ prepare(sql) {
2747
+ return this.db.prepare(sql);
2748
+ }
2749
+ transaction(fn) {
2750
+ return this.db.transaction(fn);
2751
+ }
2752
+ close() {
2753
+ this.db.close();
2754
+ }
2755
+ };
2756
+ var betterSqlite3Adapter = {
2757
+ open(dbPath) {
2758
+ fs11.mkdirSync(path14.dirname(dbPath), { recursive: true });
2759
+ const db = new BetterSqlite3Adapter(dbPath);
2760
+ db.pragma("journal_mode = WAL");
2761
+ return db;
2762
+ }
2763
+ };
2764
+
2765
+ // src/store/workspace/sqlJsAdapter.ts
2771
2766
  import fs12 from "fs";
2772
2767
  import path15 from "path";
2768
+ function detectFts5Support2(db) {
2769
+ try {
2770
+ db.exec(`
2771
+ CREATE VIRTUAL TABLE IF NOT EXISTS __fts5_probe USING fts5(x);
2772
+ DROP TABLE __fts5_probe;
2773
+ `);
2774
+ return true;
2775
+ } catch {
2776
+ return false;
2777
+ }
2778
+ }
2779
+ var SqlJsStatement = class {
2780
+ constructor(stmt) {
2781
+ this.stmt = stmt;
2782
+ }
2783
+ run(...args) {
2784
+ this.stmt.run(args);
2785
+ return void 0;
2786
+ }
2787
+ get(...args) {
2788
+ this.stmt.bind(args);
2789
+ const hasRow = this.stmt.step();
2790
+ if (!hasRow) {
2791
+ this.stmt.reset();
2792
+ return void 0;
2793
+ }
2794
+ const row = this.stmt.getAsObject();
2795
+ this.stmt.reset();
2796
+ return row;
2797
+ }
2798
+ all(...args) {
2799
+ this.stmt.bind(args);
2800
+ const rows = [];
2801
+ while (this.stmt.step()) rows.push(this.stmt.getAsObject());
2802
+ this.stmt.reset();
2803
+ return rows;
2804
+ }
2805
+ };
2806
+ var SqlJsDbAdapter = class {
2807
+ constructor(db, dbPath) {
2808
+ this.db = db;
2809
+ this.dbPath = dbPath;
2810
+ this.capabilities = { supportsFts5: detectFts5Support2(db) };
2811
+ }
2812
+ capabilities;
2813
+ pragma(sql) {
2814
+ this.exec(`PRAGMA ${sql}`);
2815
+ }
2816
+ exec(sql) {
2817
+ this.db.exec(sql);
2818
+ }
2819
+ prepare(sql) {
2820
+ return new SqlJsStatement(this.db.prepare(sql));
2821
+ }
2822
+ transaction(fn) {
2823
+ return () => {
2824
+ this.db.exec("BEGIN");
2825
+ try {
2826
+ const out = fn();
2827
+ this.db.exec("COMMIT");
2828
+ return out;
2829
+ } catch (e) {
2830
+ try {
2831
+ this.db.exec("ROLLBACK");
2832
+ } catch {
2833
+ }
2834
+ throw e;
2835
+ }
2836
+ };
2837
+ }
2838
+ close() {
2839
+ if (this.dbPath && this.dbPath !== ":memory:") {
2840
+ fs12.mkdirSync(path15.dirname(this.dbPath), { recursive: true });
2841
+ const bytes = this.db.export();
2842
+ fs12.writeFileSync(this.dbPath, Buffer.from(bytes));
2843
+ }
2844
+ this.db.close();
2845
+ }
2846
+ };
2847
+ async function sqlJsAdapter(opts = {}) {
2848
+ let init;
2849
+ try {
2850
+ const mod = await import("./sql-wasm-LT2WFO7Q.js");
2851
+ init = mod?.default ?? mod;
2852
+ } catch (e) {
2853
+ throw new Error(`sqlJsAdapter requires optional dependency 'sql.js' (install it to use this adapter): ${String(e?.message ?? e)}`);
2854
+ }
2855
+ const SQL = await init({ locateFile: opts.locateFile, wasmBinary: opts.wasmBinary });
2856
+ return {
2857
+ open(dbPath) {
2858
+ const abs = path15.resolve(dbPath);
2859
+ const exists = fs12.existsSync(abs);
2860
+ const bytes = exists ? new Uint8Array(fs12.readFileSync(abs)) : void 0;
2861
+ const db = bytes ? new SQL.Database(bytes) : new SQL.Database();
2862
+ return new SqlJsDbAdapter(db, abs);
2863
+ }
2864
+ };
2865
+ }
2866
+
2867
+ // src/store/workspaceStore.ts
2868
+ import fs14 from "fs";
2869
+ import path17 from "path";
2773
2870
 
2774
2871
  // src/store/workspace/unitOfWork.ts
2775
2872
  var UnitOfWork = class {
@@ -2989,32 +3086,11 @@ var RepoLinksRepository = class {
2989
3086
  };
2990
3087
 
2991
3088
  // src/store/workspace/factory.ts
2992
- import fs11 from "fs";
2993
- import path14 from "path";
3089
+ import fs13 from "fs";
3090
+ import path16 from "path";
2994
3091
 
2995
- // src/store/workspace/db.ts
2996
- import Database3 from "better-sqlite3";
2997
- var BetterSqlite3Adapter = class {
2998
- db;
2999
- constructor(dbPath) {
3000
- this.db = new Database3(dbPath);
3001
- }
3002
- pragma(sql) {
3003
- this.db.pragma(sql);
3004
- }
3005
- exec(sql) {
3006
- this.db.exec(sql);
3007
- }
3008
- prepare(sql) {
3009
- return this.db.prepare(sql);
3010
- }
3011
- transaction(fn) {
3012
- return this.db.transaction(fn);
3013
- }
3014
- close() {
3015
- this.db.close();
3016
- }
3017
- };
3092
+ // src/store/workspace/fts5.sql
3093
+ var fts5_default = "CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(\n id UNINDEXED,\n repo_id UNINDEXED,\n repo_root UNINDEXED,\n path,\n language,\n kind,\n text,\n tokenize='unicode61'\n);\n\n";
3018
3094
 
3019
3095
  // src/store/workspace/fts.ts
3020
3096
  var NoopFtsStrategy = class {
@@ -3038,18 +3114,7 @@ var Fts5Strategy = class {
3038
3114
  enabled = true;
3039
3115
  ins = null;
3040
3116
  init(db) {
3041
- db.exec(`
3042
- CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
3043
- id UNINDEXED,
3044
- repo_id UNINDEXED,
3045
- repo_root UNINDEXED,
3046
- path,
3047
- language,
3048
- kind,
3049
- text,
3050
- tokenize='unicode61'
3051
- );
3052
- `);
3117
+ db.exec(fts5_default);
3053
3118
  }
3054
3119
  clearRepo(repoId) {
3055
3120
  this.db.prepare(`DELETE FROM chunks_fts WHERE repo_id = ?`).run(repoId);
@@ -3133,109 +3198,26 @@ var WorkspaceMigrator = class {
3133
3198
  }
3134
3199
  };
3135
3200
 
3201
+ // src/store/workspace/baseSchema.sql
3202
+ var baseSchema_default = "CREATE TABLE IF NOT EXISTS meta (\n k TEXT PRIMARY KEY,\n v TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS repos (\n repo_id TEXT PRIMARY KEY,\n repo_root TEXT NOT NULL,\n head_commit TEXT NOT NULL,\n head_branch TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_repos_root ON repos(repo_root);\n\nCREATE TABLE IF NOT EXISTS files (\n repo_id TEXT NOT NULL,\n path TEXT NOT NULL,\n hash TEXT NOT NULL,\n mtime INTEGER NOT NULL,\n language TEXT NOT NULL,\n size INTEGER NOT NULL,\n PRIMARY KEY(repo_id, path)\n);\n\nCREATE INDEX IF NOT EXISTS idx_files_repo ON files(repo_id);\n\nCREATE TABLE IF NOT EXISTS chunks (\n id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n repo_root TEXT NOT NULL,\n path TEXT NOT NULL,\n language TEXT NOT NULL,\n kind TEXT NOT NULL DEFAULT 'chunk',\n start_line INTEGER NOT NULL,\n end_line INTEGER NOT NULL,\n content_hash TEXT NOT NULL,\n tokens INTEGER NOT NULL,\n file_mtime INTEGER NOT NULL,\n text TEXT NOT NULL,\n embedding BLOB NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_chunks_repo_path ON chunks(repo_id, path);\nCREATE INDEX IF NOT EXISTS idx_chunks_kind_repo_path ON chunks(kind, repo_id, path);\n\nCREATE TABLE IF NOT EXISTS edges (\n repo_id TEXT NOT NULL,\n from_path TEXT NOT NULL,\n kind TEXT NOT NULL,\n value TEXT NOT NULL,\n PRIMARY KEY(repo_id, from_path, kind, value)\n);\n\nCREATE INDEX IF NOT EXISTS idx_edges_repo_from ON edges(repo_id, from_path);\n\nCREATE TABLE IF NOT EXISTS symbols (\n id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n repo_root TEXT NOT NULL,\n path TEXT NOT NULL,\n language TEXT NOT NULL,\n name TEXT NOT NULL,\n kind TEXT NOT NULL,\n start_line INTEGER NOT NULL,\n start_char INTEGER NOT NULL,\n end_line INTEGER NOT NULL,\n end_char INTEGER NOT NULL,\n container_name TEXT NOT NULL DEFAULT '',\n detail TEXT NOT NULL DEFAULT ''\n);\n\nCREATE INDEX IF NOT EXISTS idx_symbols_repo_path ON symbols(repo_id, path);\nCREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);\n\nCREATE TABLE IF NOT EXISTS symbol_edges (\n repo_id TEXT NOT NULL,\n from_id TEXT NOT NULL,\n to_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n from_path TEXT NOT NULL,\n to_path TEXT NOT NULL,\n PRIMARY KEY(repo_id, from_id, to_id, kind)\n);\n\nCREATE INDEX IF NOT EXISTS idx_symbol_edges_from ON symbol_edges(repo_id, from_id);\nCREATE INDEX IF NOT EXISTS idx_symbol_edges_paths ON symbol_edges(repo_id, from_path);\n\n";
3203
+
3136
3204
  // src/store/workspace/factory.ts
3137
- function createWorkspaceDb(dbPath) {
3138
- fs11.mkdirSync(path14.dirname(dbPath), { recursive: true });
3139
- const db = new BetterSqlite3Adapter(dbPath);
3140
- db.pragma("journal_mode = WAL");
3141
- return db;
3205
+ function createWorkspaceDb(dbPath, opts = {}) {
3206
+ fs13.mkdirSync(path16.dirname(dbPath), { recursive: true });
3207
+ return (opts.db ?? betterSqlite3Adapter).open(dbPath);
3142
3208
  }
3143
3209
  function createWorkspaceBaseSchema(db) {
3144
- db.exec(`
3145
- CREATE TABLE IF NOT EXISTS meta (
3146
- k TEXT PRIMARY KEY,
3147
- v TEXT NOT NULL
3148
- );
3149
-
3150
- CREATE TABLE IF NOT EXISTS repos (
3151
- repo_id TEXT PRIMARY KEY,
3152
- repo_root TEXT NOT NULL,
3153
- head_commit TEXT NOT NULL,
3154
- head_branch TEXT NOT NULL,
3155
- updated_at INTEGER NOT NULL
3156
- );
3157
-
3158
- CREATE UNIQUE INDEX IF NOT EXISTS idx_repos_root ON repos(repo_root);
3159
-
3160
- CREATE TABLE IF NOT EXISTS files (
3161
- repo_id TEXT NOT NULL,
3162
- path TEXT NOT NULL,
3163
- hash TEXT NOT NULL,
3164
- mtime INTEGER NOT NULL,
3165
- language TEXT NOT NULL,
3166
- size INTEGER NOT NULL,
3167
- PRIMARY KEY(repo_id, path)
3168
- );
3169
-
3170
- CREATE INDEX IF NOT EXISTS idx_files_repo ON files(repo_id);
3171
-
3172
- CREATE TABLE IF NOT EXISTS chunks (
3173
- id TEXT PRIMARY KEY,
3174
- repo_id TEXT NOT NULL,
3175
- repo_root TEXT NOT NULL,
3176
- path TEXT NOT NULL,
3177
- language TEXT NOT NULL,
3178
- kind TEXT NOT NULL DEFAULT 'chunk',
3179
- start_line INTEGER NOT NULL,
3180
- end_line INTEGER NOT NULL,
3181
- content_hash TEXT NOT NULL,
3182
- tokens INTEGER NOT NULL,
3183
- file_mtime INTEGER NOT NULL,
3184
- text TEXT NOT NULL,
3185
- embedding BLOB NOT NULL
3186
- );
3187
-
3188
- CREATE INDEX IF NOT EXISTS idx_chunks_repo_path ON chunks(repo_id, path);
3189
- CREATE INDEX IF NOT EXISTS idx_chunks_kind_repo_path ON chunks(kind, repo_id, path);
3190
-
3191
- CREATE TABLE IF NOT EXISTS edges (
3192
- repo_id TEXT NOT NULL,
3193
- from_path TEXT NOT NULL,
3194
- kind TEXT NOT NULL,
3195
- value TEXT NOT NULL,
3196
- PRIMARY KEY(repo_id, from_path, kind, value)
3197
- );
3198
-
3199
- CREATE INDEX IF NOT EXISTS idx_edges_repo_from ON edges(repo_id, from_path);
3200
-
3201
- CREATE TABLE IF NOT EXISTS symbols (
3202
- id TEXT PRIMARY KEY,
3203
- repo_id TEXT NOT NULL,
3204
- repo_root TEXT NOT NULL,
3205
- path TEXT NOT NULL,
3206
- language TEXT NOT NULL,
3207
- name TEXT NOT NULL,
3208
- kind TEXT NOT NULL,
3209
- start_line INTEGER NOT NULL,
3210
- start_char INTEGER NOT NULL,
3211
- end_line INTEGER NOT NULL,
3212
- end_char INTEGER NOT NULL,
3213
- container_name TEXT NOT NULL DEFAULT '',
3214
- detail TEXT NOT NULL DEFAULT ''
3215
- );
3216
-
3217
- CREATE INDEX IF NOT EXISTS idx_symbols_repo_path ON symbols(repo_id, path);
3218
- CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
3219
-
3220
- CREATE TABLE IF NOT EXISTS symbol_edges (
3221
- repo_id TEXT NOT NULL,
3222
- from_id TEXT NOT NULL,
3223
- to_id TEXT NOT NULL,
3224
- kind TEXT NOT NULL,
3225
- from_path TEXT NOT NULL,
3226
- to_path TEXT NOT NULL,
3227
- PRIMARY KEY(repo_id, from_id, to_id, kind)
3228
- );
3229
-
3230
- CREATE INDEX IF NOT EXISTS idx_symbol_edges_from ON symbol_edges(repo_id, from_id);
3231
- CREATE INDEX IF NOT EXISTS idx_symbol_edges_paths ON symbol_edges(repo_id, from_path);
3232
- `);
3210
+ db.exec(baseSchema_default);
3233
3211
  }
3234
3212
  function createWorkspaceFts(db, meta, opts = {}) {
3235
3213
  if (opts.fts === "off") {
3236
3214
  meta.set("fts", "0");
3237
3215
  return new NoopFtsStrategy();
3238
3216
  }
3217
+ if (!db.capabilities.supportsFts5) {
3218
+ meta.set("fts", "0");
3219
+ return new NoopFtsStrategy();
3220
+ }
3239
3221
  try {
3240
3222
  const fts = new Fts5Strategy(db);
3241
3223
  fts.init(db);
@@ -3252,16 +3234,31 @@ function migrateWorkspaceDb(db, meta) {
3252
3234
  }
3253
3235
 
3254
3236
  // src/store/workspaceStore.ts
3237
+ function createWorkspaceStore(dbPath, opts = {}) {
3238
+ return new WorkspaceStore(dbPath, opts);
3239
+ }
3240
+ async function defaultWorkspaceDbFactory() {
3241
+ try {
3242
+ return await sqlJsAdapter();
3243
+ } catch {
3244
+ return betterSqlite3Adapter;
3245
+ }
3246
+ }
3247
+ async function createWorkspaceStoreAsync(dbPath, opts = {}) {
3248
+ const dbFactory = opts.db ? await Promise.resolve(opts.db) : await defaultWorkspaceDbFactory();
3249
+ return new WorkspaceStore(dbPath, { ...opts, db: dbFactory });
3250
+ }
3255
3251
  var WorkspaceStore = class {
3256
3252
  constructor(dbPath, opts = {}) {
3257
3253
  this.dbPath = dbPath;
3258
3254
  this.opts = opts;
3259
- this.db = createWorkspaceDb(dbPath);
3255
+ this.db = createWorkspaceDb(dbPath, { db: opts.db });
3260
3256
  this.uow = new UnitOfWork(this.db);
3261
3257
  createWorkspaceBaseSchema(this.db);
3262
3258
  this.meta = new MetaRepository(this.db);
3263
3259
  migrateWorkspaceDb(this.db, this.meta);
3264
3260
  const fts = createWorkspaceFts(this.db, this.meta, opts);
3261
+ this.ftsEnabledInternal = fts.enabled;
3265
3262
  this.repoHeads = new RepoHeadsRepository(this.db);
3266
3263
  this.files = new FilesRepository(this.db);
3267
3264
  this.edges = new EdgesRepository(this.db);
@@ -3271,6 +3268,7 @@ var WorkspaceStore = class {
3271
3268
  }
3272
3269
  db;
3273
3270
  uow;
3271
+ ftsEnabledInternal;
3274
3272
  meta;
3275
3273
  repoHeads;
3276
3274
  files;
@@ -3279,6 +3277,9 @@ var WorkspaceStore = class {
3279
3277
  symbols;
3280
3278
  chunks;
3281
3279
  opts;
3280
+ get ftsEnabled() {
3281
+ return this.ftsEnabledInternal;
3282
+ }
3282
3283
  setMeta(k, v) {
3283
3284
  this.meta.set(k, v);
3284
3285
  }
@@ -3362,9 +3363,9 @@ var WorkspaceStore = class {
3362
3363
  * The chunk boundaries are approximate; the stored row includes start/end line.
3363
3364
  */
3364
3365
  getChunkTextFallback(row) {
3365
- const abs = path15.join(row.repo_root, row.path.split("/").join(path15.sep));
3366
+ const abs = path17.join(row.repo_root, row.path.split("/").join(path17.sep));
3366
3367
  try {
3367
- const raw = fs12.readFileSync(abs, "utf8");
3368
+ const raw = fs14.readFileSync(abs, "utf8");
3368
3369
  const lines = raw.split(/\r?\n/);
3369
3370
  const start = Math.max(1, row.start_line);
3370
3371
  const end = Math.max(start, row.end_line);
@@ -3834,11 +3835,11 @@ ${hint}`);
3834
3835
  }
3835
3836
 
3836
3837
  // src/indexer/workspaceLinker.ts
3837
- import fs13 from "fs";
3838
- import path16 from "path";
3838
+ import fs15 from "fs";
3839
+ import path18 from "path";
3839
3840
  function readText(absPath) {
3840
3841
  try {
3841
- return fs13.readFileSync(absPath, "utf8");
3842
+ return fs15.readFileSync(absPath, "utf8");
3842
3843
  } catch {
3843
3844
  return null;
3844
3845
  }
@@ -3868,7 +3869,7 @@ var NestedRepoLinkStrategy = class {
3868
3869
  for (const child of sorted) {
3869
3870
  for (const parent of sorted) {
3870
3871
  if (child.repoId === parent.repoId) continue;
3871
- if (child.absRoot.startsWith(parent.absRoot + path16.sep)) {
3872
+ if (child.absRoot.startsWith(parent.absRoot + path18.sep)) {
3872
3873
  out.push({
3873
3874
  fromRepoId: child.repoId,
3874
3875
  toRepoId: parent.repoId,
@@ -3888,7 +3889,7 @@ var NpmDependencyLinkStrategy = class {
3888
3889
  const out = [];
3889
3890
  const depSections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
3890
3891
  for (const r of ctx.repos) {
3891
- const pkg = readJson(path16.join(r.absRoot, "package.json"));
3892
+ const pkg = readJson(path18.join(r.absRoot, "package.json"));
3892
3893
  if (!pkg) continue;
3893
3894
  for (const sec of depSections) {
3894
3895
  const deps = pkg?.[sec];
@@ -3906,13 +3907,13 @@ var NpmDependencyLinkStrategy = class {
3906
3907
  }
3907
3908
  };
3908
3909
  function parseGoModule(absRepoRoot) {
3909
- const raw = readText(path16.join(absRepoRoot, "go.mod"));
3910
+ const raw = readText(path18.join(absRepoRoot, "go.mod"));
3910
3911
  if (!raw) return null;
3911
3912
  const m = raw.match(/^\s*module\s+(.+)\s*$/m);
3912
3913
  return m ? String(m[1]).trim() : null;
3913
3914
  }
3914
3915
  function parseGoRequires(absRepoRoot) {
3915
- const raw = readText(path16.join(absRepoRoot, "go.mod"));
3916
+ const raw = readText(path18.join(absRepoRoot, "go.mod"));
3916
3917
  if (!raw) return [];
3917
3918
  const out = [];
3918
3919
  for (const line of raw.split(/\r?\n/)) {
@@ -3952,13 +3953,13 @@ function walkFiles(root, opts, onFile) {
3952
3953
  if (depth > maxDepth) return;
3953
3954
  let ents = [];
3954
3955
  try {
3955
- ents = fs13.readdirSync(dir, { withFileTypes: true });
3956
+ ents = fs15.readdirSync(dir, { withFileTypes: true });
3956
3957
  } catch {
3957
3958
  return;
3958
3959
  }
3959
3960
  for (const e of ents) {
3960
3961
  if (seen >= maxFiles) return;
3961
- const abs = path16.join(dir, e.name);
3962
+ const abs = path18.join(dir, e.name);
3962
3963
  if (opts.shouldVisit && !opts.shouldVisit(abs, e)) continue;
3963
3964
  if (e.isDirectory()) {
3964
3965
  if (isSkippableDir(e.name)) continue;
@@ -3982,7 +3983,7 @@ function collectVsCodeLanguagesForRepo(absRepoRoot) {
3982
3983
  shouldVisit: (_abs, dirent) => !(dirent.isDirectory() && isSkippableDir(dirent.name))
3983
3984
  },
3984
3985
  (absPath) => {
3985
- if (path16.basename(absPath) !== "package.json") return;
3986
+ if (path18.basename(absPath) !== "package.json") return;
3986
3987
  const pkg = readJson(absPath);
3987
3988
  const langs = pkg?.contributes?.languages;
3988
3989
  if (!Array.isArray(langs)) return;
@@ -4011,7 +4012,7 @@ function repoUsedExtensions(absRepoRoot, exts) {
4011
4012
  shouldVisit: (_abs, dirent) => !(dirent.isDirectory() && isSkippableDir(dirent.name))
4012
4013
  },
4013
4014
  (absPath) => {
4014
- const ext = path16.extname(absPath).toLowerCase();
4015
+ const ext = path18.extname(absPath).toLowerCase();
4015
4016
  if (!ext) return;
4016
4017
  if (exts.has(ext)) used.add(ext);
4017
4018
  }
@@ -4083,12 +4084,12 @@ var WorkspaceLinker = class _WorkspaceLinker {
4083
4084
  const repos = repoRoots.map((repoRoot) => ({
4084
4085
  repoRoot,
4085
4086
  repoId: repoIdFromRoot(repoRoot),
4086
- absRoot: path16.resolve(repoRoot)
4087
+ absRoot: path18.resolve(repoRoot)
4087
4088
  }));
4088
4089
  const npmNameToRepoId = /* @__PURE__ */ new Map();
4089
4090
  const goModuleToRepoId = /* @__PURE__ */ new Map();
4090
4091
  for (const r of repos) {
4091
- const pkg = readJson(path16.join(r.absRoot, "package.json"));
4092
+ const pkg = readJson(path18.join(r.absRoot, "package.json"));
4092
4093
  const name = typeof pkg?.name === "string" ? pkg.name : null;
4093
4094
  if (name) npmNameToRepoId.set(name, r.repoId);
4094
4095
  const mod = parseGoModule(r.absRoot);
@@ -4132,14 +4133,265 @@ async function linkWorkspaceRepos(args) {
4132
4133
  return { repos: ctx.repos, links };
4133
4134
  }
4134
4135
 
4136
+ // src/profiles.ts
4137
+ var DEFAULT_PROFILES = {
4138
+ search: {
4139
+ name: "search",
4140
+ k: 10,
4141
+ weights: { vector: 0.65, lexical: 0.35, recency: 0 },
4142
+ expand: { adjacentChunks: 0, followImports: 0, includeFileSynopsis: false },
4143
+ candidates: { vectorK: 25, lexicalK: 25, maxMergedCandidates: 60 }
4144
+ },
4145
+ refactor: {
4146
+ name: "refactor",
4147
+ k: 15,
4148
+ weights: { vector: 0.55, lexical: 0.35, recency: 0.1 },
4149
+ expand: { adjacentChunks: 1, followImports: 2, includeFileSynopsis: true },
4150
+ candidates: { vectorK: 60, lexicalK: 40, maxMergedCandidates: 140 }
4151
+ },
4152
+ review: {
4153
+ name: "review",
4154
+ k: 20,
4155
+ weights: { vector: 0.45, lexical: 0.35, recency: 0.2 },
4156
+ expand: { adjacentChunks: 1, followImports: 1, includeFileSynopsis: true },
4157
+ candidates: { vectorK: 80, lexicalK: 60, maxMergedCandidates: 180 }
4158
+ },
4159
+ architecture: {
4160
+ name: "architecture",
4161
+ k: 20,
4162
+ weights: { vector: 0.7, lexical: 0.2, recency: 0.1 },
4163
+ expand: { adjacentChunks: 0, followImports: 3, includeFileSynopsis: true },
4164
+ candidates: { vectorK: 120, lexicalK: 40, maxMergedCandidates: 220 }
4165
+ },
4166
+ rca: {
4167
+ name: "rca",
4168
+ k: 25,
4169
+ weights: { vector: 0.5, lexical: 0.25, recency: 0.25 },
4170
+ expand: { adjacentChunks: 2, followImports: 1, includeFileSynopsis: true },
4171
+ candidates: { vectorK: 140, lexicalK: 80, maxMergedCandidates: 260 }
4172
+ },
4173
+ custom: {
4174
+ name: "custom",
4175
+ k: 10,
4176
+ weights: { vector: 0.65, lexical: 0.35, recency: 0 },
4177
+ expand: { adjacentChunks: 0, followImports: 0, includeFileSynopsis: false },
4178
+ candidates: { vectorK: 25, lexicalK: 25, maxMergedCandidates: 60 }
4179
+ }
4180
+ };
4181
+ function deepMergeProfile(base, patch) {
4182
+ if (!patch) return base;
4183
+ const merged = {
4184
+ ...base,
4185
+ ...patch,
4186
+ weights: { ...base.weights, ...patch.weights ?? {} },
4187
+ expand: { ...base.expand, ...patch.expand ?? {} },
4188
+ candidates: { ...base.candidates, ...patch.candidates ?? {} }
4189
+ };
4190
+ return merged;
4191
+ }
4192
+
4135
4193
  // src/indexer/workspaceIndexer.ts
4136
- import path17 from "path";
4194
+ import path20 from "path";
4195
+
4196
+ // src/indexer/workspaceRetrieveCandidates.ts
4197
+ import path19 from "path";
4198
+ function resolveWorkspaceProfile(config, opts) {
4199
+ const name = opts?.profile ?? "search";
4200
+ const base = DEFAULT_PROFILES[name] ?? DEFAULT_PROFILES.search;
4201
+ const configPatch = config.profiles?.[name] ?? {};
4202
+ const merged1 = deepMergeProfile(base, configPatch);
4203
+ const merged2 = deepMergeProfile(merged1, opts?.profileOverrides);
4204
+ const w = merged2.weights;
4205
+ const sum = Math.max(1e-6, w.vector + w.lexical + w.recency);
4206
+ merged2.weights = { vector: w.vector / sum, lexical: w.lexical / sum, recency: w.recency / sum };
4207
+ return merged2;
4208
+ }
4137
4209
  function halfLifeDaysForProfile(profileName) {
4138
4210
  if (profileName === "rca") return 7;
4139
4211
  if (profileName === "review") return 14;
4140
4212
  if (profileName === "refactor") return 21;
4141
4213
  return 30;
4142
4214
  }
4215
+ function buildWorkspaceLexByRepoRoot(args) {
4216
+ const { workspaceStore, repos, query, lexicalK, repoFilters } = args;
4217
+ const ftq = ftsQueryFromText(query);
4218
+ if (!ftq) return { lexByRepoRoot: /* @__PURE__ */ new Map(), count: 0 };
4219
+ const allowRoots = repoFilters ? new Set(repoFilters.map((r) => path19.resolve(r))) : null;
4220
+ const repoIds = allowRoots ? repos.filter((r) => allowRoots.has(path19.resolve(r.repoRoot))).map((r) => r.repoId) : void 0;
4221
+ const rows = workspaceStore.searchFts(ftq, lexicalK, repoIds);
4222
+ const lexByRepoRoot = /* @__PURE__ */ new Map();
4223
+ for (const r of rows) {
4224
+ const row = workspaceStore.getChunkById(r.id);
4225
+ if (!row) continue;
4226
+ const rootKey = path19.resolve(row.repo_root);
4227
+ const arr = lexByRepoRoot.get(rootKey) ?? [];
4228
+ arr.push({ id: r.id, score: bm25ToScore01(r.bm25) });
4229
+ lexByRepoRoot.set(rootKey, arr);
4230
+ }
4231
+ return { lexByRepoRoot, count: rows.length };
4232
+ }
4233
+ async function collectWorkspaceCandidates(args) {
4234
+ const { repos, qVec, query, vectorK, lexicalK, profile, opts, lexByRepoRoot, canUseWorkspaceLex } = args;
4235
+ const repoFilters = opts.filters?.repoRoots;
4236
+ const langFilter = opts.filters?.language;
4237
+ const pathPrefix = opts.filters?.pathPrefix;
4238
+ const candidates = [];
4239
+ let vecCount = 0;
4240
+ let lexCount = 0;
4241
+ for (const repo of repos) {
4242
+ if (repoFilters && !repoFilters.includes(repo.repoRoot)) continue;
4243
+ let includePaths = opts.scope?.includePaths?.slice();
4244
+ if (opts.scope?.changedOnly) {
4245
+ try {
4246
+ const changed = await listChangedFiles(repo.repoRoot, opts.scope.baseRef ?? "HEAD~1");
4247
+ includePaths = includePaths ? includePaths.filter((p) => changed.includes(p)) : changed;
4248
+ } catch {
4249
+ }
4250
+ }
4251
+ const [vHits, lHits] = await Promise.all([
4252
+ repo.vectorCandidates(qVec, vectorK, includePaths),
4253
+ canUseWorkspaceLex ? Promise.resolve(lexByRepoRoot?.get(path19.resolve(repo.repoRoot)) ?? []) : repo.lexicalCandidates(query, lexicalK, includePaths)
4254
+ ]);
4255
+ vecCount += vHits.length;
4256
+ if (!canUseWorkspaceLex) lexCount += lHits.length;
4257
+ const m = /* @__PURE__ */ new Map();
4258
+ for (const vh of vHits) {
4259
+ const id = vh.id;
4260
+ const vector01 = vectorCosineToScore01(vh.score);
4261
+ m.set(id, { repo, id, vector01, combined: 0 });
4262
+ }
4263
+ for (const lh of lHits) {
4264
+ const id = lh.id;
4265
+ const prev = m.get(id);
4266
+ if (prev) prev.lexical01 = lh.score;
4267
+ else m.set(id, { repo, id, lexical01: lh.score, combined: 0 });
4268
+ }
4269
+ const halfLife = halfLifeDaysForProfile(profile.name);
4270
+ for (const c of m.values()) {
4271
+ const meta = repo.getChunkMeta(c.id);
4272
+ if (!meta) continue;
4273
+ if (langFilter && meta.language !== langFilter) continue;
4274
+ if (pathPrefix && !meta.path.startsWith(pathPrefix)) continue;
4275
+ c.recency01 = profile.weights.recency > 0 ? recencyScore(meta.fileMtimeMs, halfLife) : 0;
4276
+ let kindFactor = 1;
4277
+ if (meta.kind === "synopsis" && profile.name === "search") kindFactor = 0.85;
4278
+ if (meta.kind === "synopsis" && profile.name === "architecture") kindFactor = 1.05;
4279
+ const v = c.vector01 ?? 0;
4280
+ const l = c.lexical01 ?? 0;
4281
+ const r = c.recency01 ?? 0;
4282
+ c.combined = clamp(kindFactor * (profile.weights.vector * v + profile.weights.lexical * l + profile.weights.recency * r), 0, 1);
4283
+ candidates.push(c);
4284
+ }
4285
+ }
4286
+ return { candidates, vecCount, lexCount };
4287
+ }
4288
+ function rankWorkspaceCandidates(args) {
4289
+ const { candidates, maxMerged, k } = args;
4290
+ candidates.sort((a, b) => b.combined - a.combined);
4291
+ const merged = candidates.slice(0, maxMerged);
4292
+ const top = merged.slice(0, k);
4293
+ const hits = top.map((c) => {
4294
+ const meta = c.repo.getChunkMeta(c.id);
4295
+ const preview = makePreview(c.repo.getChunkText(c.id));
4296
+ return {
4297
+ score: c.combined,
4298
+ scoreBreakdown: { vector: c.vector01, lexical: c.lexical01, recency: c.recency01 },
4299
+ chunk: { ...meta, preview }
4300
+ };
4301
+ });
4302
+ return { merged, hits };
4303
+ }
4304
+
4305
+ // src/indexer/workspaceRetrieveContext.ts
4306
+ async function warmSymbolGraphForHits(repos, hits) {
4307
+ const byRepo = /* @__PURE__ */ new Map();
4308
+ for (const h of hits) {
4309
+ const s = byRepo.get(h.chunk.repoRoot) ?? /* @__PURE__ */ new Set();
4310
+ s.add(h.chunk.path);
4311
+ byRepo.set(h.chunk.repoRoot, s);
4312
+ }
4313
+ for (const [repoRoot, paths] of byRepo) {
4314
+ const repo = repos.find((r) => r.repoRoot === repoRoot);
4315
+ if (!repo) continue;
4316
+ await repo.warmSymbolGraphEdges(Array.from(paths), { maxFiles: 6 });
4317
+ }
4318
+ }
4319
+ async function fetchGraphNeighborFiles(args) {
4320
+ const { graphStore, repos, hits, profile, workspaceRoot, emitProgress } = args;
4321
+ if (!graphStore?.neighborFiles) return [];
4322
+ const seeds = [];
4323
+ const seen = /* @__PURE__ */ new Set();
4324
+ for (const h of hits) {
4325
+ const repo = repos.find((r) => r.repoRoot === h.chunk.repoRoot);
4326
+ if (!repo) continue;
4327
+ const key = `${repo.repoId}:${h.chunk.path}`;
4328
+ if (seen.has(key)) continue;
4329
+ seen.add(key);
4330
+ seeds.push({ repoId: repo.repoId, path: h.chunk.path });
4331
+ if (seeds.length >= 4) break;
4332
+ }
4333
+ if (seeds.length === 0) return [];
4334
+ const startedAt = Date.now();
4335
+ emitProgress({ type: "workspace/retrieve/graph/start", workspaceRoot, seeds: seeds.length });
4336
+ try {
4337
+ const neighbors = await graphStore.neighborFiles({
4338
+ seeds,
4339
+ limit: profile.name === "architecture" ? 16 : 10,
4340
+ kinds: ["definition", "reference", "implementation", "typeDefinition"]
4341
+ });
4342
+ emitProgress({ type: "workspace/retrieve/graph/done", workspaceRoot, neighbors: neighbors.length, ms: Date.now() - startedAt });
4343
+ return neighbors;
4344
+ } catch {
4345
+ return [];
4346
+ }
4347
+ }
4348
+ async function buildContextBlocks(args) {
4349
+ const { repos, hits, graphNeighborFiles, profile } = args;
4350
+ const contextBlocks = [];
4351
+ const seenKey = /* @__PURE__ */ new Set();
4352
+ const addBlock = (repoRoot, filePath, startLine, endLine, text, reason) => {
4353
+ const key = `${repoRoot}:${filePath}:${startLine}:${endLine}:${text.length}:${reason}`;
4354
+ if (seenKey.has(key)) return;
4355
+ seenKey.add(key);
4356
+ if (!text.trim()) return;
4357
+ contextBlocks.push({ repoRoot, path: filePath, startLine, endLine, text, reason });
4358
+ };
4359
+ try {
4360
+ const byRepoId = /* @__PURE__ */ new Map();
4361
+ for (const r of repos) byRepoId.set(r.repoId, r);
4362
+ for (const n of graphNeighborFiles.slice(0, 10)) {
4363
+ const repo = byRepoId.get(n.repoId);
4364
+ if (!repo) continue;
4365
+ const chunkId = await repo.getRepresentativeChunkIdForFile(n.path, true);
4366
+ if (!chunkId) continue;
4367
+ const meta = repo.getChunkMeta(chunkId);
4368
+ if (!meta) continue;
4369
+ const text = repo.getChunkText(chunkId);
4370
+ addBlock(meta.repoRoot, meta.path, meta.startLine, meta.endLine, text, `graph neighbor (${n.weight})`);
4371
+ }
4372
+ } catch {
4373
+ }
4374
+ for (const h of hits) {
4375
+ const repo = repos.find((r) => r.repoRoot === h.chunk.repoRoot);
4376
+ if (!repo) continue;
4377
+ const hitText = repo.getChunkText(h.chunk.id);
4378
+ addBlock(h.chunk.repoRoot, h.chunk.path, h.chunk.startLine, h.chunk.endLine, hitText, "primary hit");
4379
+ const expanded = await repo.expandContext(h.chunk.id, {
4380
+ adjacentChunks: profile.expand.adjacentChunks ?? 0,
4381
+ followImports: profile.expand.followImports ?? 0,
4382
+ includeFileSynopsis: profile.expand.includeFileSynopsis ?? false
4383
+ });
4384
+ for (const ex of expanded) {
4385
+ const meta = repo.getChunkMeta(ex.id);
4386
+ if (!meta) continue;
4387
+ const text = repo.getChunkText(ex.id);
4388
+ addBlock(meta.repoRoot, meta.path, meta.startLine, meta.endLine, text, ex.reason);
4389
+ }
4390
+ }
4391
+ return contextBlocks;
4392
+ }
4393
+
4394
+ // src/indexer/workspaceIndexer.ts
4143
4395
  var WorkspaceIndexer = class {
4144
4396
  constructor(workspaceRoot, embedder, config = {}) {
4145
4397
  this.workspaceRoot = workspaceRoot;
@@ -4147,23 +4399,30 @@ var WorkspaceIndexer = class {
4147
4399
  this.config = { ...config };
4148
4400
  if (!this.config.cacheDir) this.config.cacheDir = defaultCacheDir();
4149
4401
  this.progress = asProgressSink(this.config.progress);
4150
- const wsId = sha256Hex(path17.resolve(this.workspaceRoot)).slice(0, 16);
4151
- const dbPath = path17.join(this.config.cacheDir, "workspace", wsId, "workspace.sqlite");
4152
- this.workspaceStore = new WorkspaceStore(dbPath);
4153
- this.workspaceStore.setMeta("workspaceRoot", path17.resolve(this.workspaceRoot));
4402
+ const wsId = sha256Hex(path20.resolve(this.workspaceRoot)).slice(0, 16);
4403
+ this.workspaceDbPath = path20.join(this.config.cacheDir, "workspace", wsId, "workspace.sqlite");
4154
4404
  }
4155
4405
  repos = [];
4156
4406
  config;
4157
4407
  progress = asProgressSink();
4158
4408
  workspaceStore = null;
4159
4409
  graphStore = null;
4410
+ workspaceDbPath;
4160
4411
  emitProgress(event) {
4161
4412
  try {
4162
4413
  this.progress?.emit(event);
4163
4414
  } catch {
4164
4415
  }
4165
4416
  }
4417
+ async ensureWorkspaceStore() {
4418
+ if (this.workspaceStore) return this.workspaceStore;
4419
+ const ws = await createWorkspaceStoreAsync(this.workspaceDbPath, { db: this.config.workspace?.db });
4420
+ ws.setMeta("workspaceRoot", path20.resolve(this.workspaceRoot));
4421
+ this.workspaceStore = ws;
4422
+ return ws;
4423
+ }
4166
4424
  async open() {
4425
+ await this.ensureWorkspaceStore();
4167
4426
  if (!this.graphStore && this.config.workspace?.graph?.provider === "neo4j") {
4168
4427
  try {
4169
4428
  const n = this.config.workspace.graph.neo4j;
@@ -4235,195 +4494,49 @@ var WorkspaceIndexer = class {
4235
4494
  getRepoIndexers() {
4236
4495
  return this.repos.slice();
4237
4496
  }
4238
- resolveProfile(opts) {
4239
- const name = opts?.profile ?? "search";
4240
- const base = DEFAULT_PROFILES[name] ?? DEFAULT_PROFILES.search;
4241
- const configPatch = this.config.profiles?.[name] ?? {};
4242
- const merged1 = deepMergeProfile(base, configPatch);
4243
- const merged2 = deepMergeProfile(merged1, opts?.profileOverrides);
4244
- const w = merged2.weights;
4245
- const sum = Math.max(1e-6, w.vector + w.lexical + w.recency);
4246
- merged2.weights = { vector: w.vector / sum, lexical: w.lexical / sum, recency: w.recency / sum };
4247
- return merged2;
4248
- }
4249
4497
  async retrieve(query, opts = {}) {
4250
4498
  if (this.repos.length === 0) await this.open();
4251
- const profile = this.resolveProfile(opts);
4499
+ const profile = resolveWorkspaceProfile(this.config, opts);
4252
4500
  const startedAt = Date.now();
4253
4501
  this.emitProgress({ type: "workspace/retrieve/start", workspaceRoot: this.workspaceRoot, profile: profile.name, query });
4254
4502
  const qVec = (await this.embedder.embed([query]))[0];
4255
4503
  const vectorK = profile.candidates?.vectorK ?? Math.max(profile.k * 3, 30);
4256
4504
  const lexicalK = profile.candidates?.lexicalK ?? Math.max(profile.k * 3, 30);
4257
4505
  const maxMerged = profile.candidates?.maxMergedCandidates ?? Math.max(profile.k * 8, 120);
4258
- const repoFilters = opts.filters?.repoRoots;
4259
- const langFilter = opts.filters?.language;
4260
- const pathPrefix = opts.filters?.pathPrefix;
4261
- const candidates = [];
4262
- let vecCount = 0;
4263
- let lexCount = 0;
4264
- const canUseWorkspaceLex = !!this.workspaceStore && this.config.storage?.ftsMode !== "off" && !opts.scope?.includePaths && !opts.scope?.changedOnly;
4265
- const workspaceLexByRepoRoot = /* @__PURE__ */ new Map();
4266
- if (canUseWorkspaceLex && profile.weights.lexical > 0) {
4267
- const ftq = ftsQueryFromText(query);
4268
- const allowRoots = repoFilters ? new Set(repoFilters.map((r) => path17.resolve(r))) : null;
4269
- const repoIds = allowRoots ? this.repos.filter((r) => allowRoots.has(path17.resolve(r.repoRoot))).map((r) => r.repoId) : void 0;
4270
- if (ftq) {
4271
- const rows = this.workspaceStore.searchFts(ftq, lexicalK, repoIds);
4272
- lexCount += rows.length;
4273
- for (const r of rows) {
4274
- const row = this.workspaceStore.getChunkById(r.id);
4275
- if (!row) continue;
4276
- const rootKey = path17.resolve(row.repo_root);
4277
- const arr = workspaceLexByRepoRoot.get(rootKey) ?? [];
4278
- arr.push({ id: r.id, score: bm25ToScore01(r.bm25) });
4279
- workspaceLexByRepoRoot.set(rootKey, arr);
4280
- }
4281
- }
4282
- }
4283
- for (const repo of this.repos) {
4284
- if (repoFilters && !repoFilters.includes(repo.repoRoot)) continue;
4285
- let includePaths = opts.scope?.includePaths?.slice();
4286
- if (opts.scope?.changedOnly) {
4287
- try {
4288
- const changed = await listChangedFiles(repo.repoRoot, opts.scope.baseRef ?? "HEAD~1");
4289
- includePaths = includePaths ? includePaths.filter((p) => changed.includes(p)) : changed;
4290
- } catch {
4291
- }
4292
- }
4293
- const [vHits, lHits] = await Promise.all([
4294
- repo.vectorCandidates(qVec, vectorK, includePaths),
4295
- canUseWorkspaceLex ? Promise.resolve(workspaceLexByRepoRoot.get(path17.resolve(repo.repoRoot)) ?? []) : repo.lexicalCandidates(query, lexicalK, includePaths)
4296
- ]);
4297
- vecCount += vHits.length;
4298
- if (!canUseWorkspaceLex) lexCount += lHits.length;
4299
- const m = /* @__PURE__ */ new Map();
4300
- for (const vh of vHits) {
4301
- const id = vh.id;
4302
- const vector01 = vectorCosineToScore01(vh.score);
4303
- m.set(id, { repo, id, vector01, combined: 0 });
4304
- }
4305
- for (const lh of lHits) {
4306
- const id = lh.id;
4307
- const prev = m.get(id);
4308
- if (prev) prev.lexical01 = lh.score;
4309
- else m.set(id, { repo, id, lexical01: lh.score, combined: 0 });
4310
- }
4311
- const halfLife = halfLifeDaysForProfile(profile.name);
4312
- for (const c of m.values()) {
4313
- const meta = repo.getChunkMeta(c.id);
4314
- if (!meta) continue;
4315
- if (langFilter && meta.language !== langFilter) continue;
4316
- if (pathPrefix && !meta.path.startsWith(pathPrefix)) continue;
4317
- c.recency01 = profile.weights.recency > 0 ? recencyScore(meta.fileMtimeMs, halfLife) : 0;
4318
- let kindFactor = 1;
4319
- if (meta.kind === "synopsis" && profile.name === "search") kindFactor = 0.85;
4320
- if (meta.kind === "synopsis" && profile.name === "architecture") kindFactor = 1.05;
4321
- const v = c.vector01 ?? 0;
4322
- const l = c.lexical01 ?? 0;
4323
- const r = c.recency01 ?? 0;
4324
- c.combined = clamp(
4325
- kindFactor * (profile.weights.vector * v + profile.weights.lexical * l + profile.weights.recency * r),
4326
- 0,
4327
- 1
4328
- );
4329
- candidates.push(c);
4330
- }
4331
- }
4332
- candidates.sort((a, b) => b.combined - a.combined);
4333
- const merged = candidates.slice(0, maxMerged);
4334
- const top = merged.slice(0, profile.k);
4335
- const hits = top.map((c) => {
4336
- const meta = c.repo.getChunkMeta(c.id);
4337
- const preview = makePreview(c.repo.getChunkText(c.id));
4338
- return {
4339
- score: c.combined,
4340
- scoreBreakdown: { vector: c.vector01, lexical: c.lexical01, recency: c.recency01 },
4341
- chunk: { ...meta, preview }
4342
- };
4506
+ const canUseWorkspaceLex = !!this.workspaceStore && this.workspaceStore.ftsEnabled && this.config.storage?.ftsMode !== "off" && !opts.scope?.includePaths && !opts.scope?.changedOnly;
4507
+ const { lexByRepoRoot, count: workspaceLexCount } = canUseWorkspaceLex && profile.weights.lexical > 0 && this.workspaceStore ? buildWorkspaceLexByRepoRoot({
4508
+ workspaceStore: this.workspaceStore,
4509
+ repos: this.repos,
4510
+ query,
4511
+ lexicalK,
4512
+ repoFilters: opts.filters?.repoRoots
4513
+ }) : { lexByRepoRoot: void 0, count: 0 };
4514
+ const { candidates, vecCount, lexCount } = await collectWorkspaceCandidates({
4515
+ repos: this.repos,
4516
+ qVec,
4517
+ query,
4518
+ vectorK,
4519
+ lexicalK,
4520
+ profile,
4521
+ opts,
4522
+ lexByRepoRoot,
4523
+ canUseWorkspaceLex
4343
4524
  });
4525
+ const { merged, hits } = rankWorkspaceCandidates({ candidates, maxMerged, k: profile.k });
4526
+ const totalLexCount = (canUseWorkspaceLex ? workspaceLexCount : 0) + lexCount;
4344
4527
  try {
4345
- const byRepo = /* @__PURE__ */ new Map();
4346
- for (const h of hits) {
4347
- const s = byRepo.get(h.chunk.repoRoot) ?? /* @__PURE__ */ new Set();
4348
- s.add(h.chunk.path);
4349
- byRepo.set(h.chunk.repoRoot, s);
4350
- }
4351
- for (const [repoRoot, paths] of byRepo) {
4352
- const repo = this.repos.find((r) => r.repoRoot === repoRoot);
4353
- if (!repo) continue;
4354
- await repo.warmSymbolGraphEdges(Array.from(paths), { maxFiles: 6 });
4355
- }
4528
+ await warmSymbolGraphForHits(this.repos, hits);
4356
4529
  } catch {
4357
4530
  }
4358
- let graphNeighborFiles = [];
4359
- try {
4360
- if (this.graphStore?.neighborFiles) {
4361
- const seeds = [];
4362
- const seen = /* @__PURE__ */ new Set();
4363
- for (const h of hits) {
4364
- const repo = this.repos.find((r) => r.repoRoot === h.chunk.repoRoot);
4365
- if (!repo) continue;
4366
- const key = `${repo.repoId}:${h.chunk.path}`;
4367
- if (seen.has(key)) continue;
4368
- seen.add(key);
4369
- seeds.push({ repoId: repo.repoId, path: h.chunk.path });
4370
- if (seeds.length >= 4) break;
4371
- }
4372
- if (seeds.length > 0) {
4373
- const gs = Date.now();
4374
- this.emitProgress({ type: "workspace/retrieve/graph/start", workspaceRoot: this.workspaceRoot, seeds: seeds.length });
4375
- graphNeighborFiles = await this.graphStore.neighborFiles({
4376
- seeds,
4377
- limit: profile.name === "architecture" ? 16 : 10,
4378
- kinds: ["definition", "reference", "implementation", "typeDefinition"]
4379
- });
4380
- this.emitProgress({ type: "workspace/retrieve/graph/done", workspaceRoot: this.workspaceRoot, neighbors: graphNeighborFiles.length, ms: Date.now() - gs });
4381
- }
4382
- }
4383
- } catch {
4384
- graphNeighborFiles = [];
4385
- }
4386
- const contextBlocks = [];
4387
- const seenKey = /* @__PURE__ */ new Set();
4388
- const addBlock = (repoRoot, path19, startLine, endLine, text, reason) => {
4389
- const key = `${repoRoot}:${path19}:${startLine}:${endLine}:${text.length}:${reason}`;
4390
- if (seenKey.has(key)) return;
4391
- seenKey.add(key);
4392
- if (!text.trim()) return;
4393
- contextBlocks.push({ repoRoot, path: path19, startLine, endLine, text, reason });
4394
- };
4395
- try {
4396
- const byRepoId = /* @__PURE__ */ new Map();
4397
- for (const r of this.repos) byRepoId.set(r.repoId, r);
4398
- for (const n of graphNeighborFiles.slice(0, 10)) {
4399
- const repo = byRepoId.get(n.repoId);
4400
- if (!repo) continue;
4401
- const chunkId = await repo.getRepresentativeChunkIdForFile(n.path, true);
4402
- if (!chunkId) continue;
4403
- const meta = repo.getChunkMeta(chunkId);
4404
- if (!meta) continue;
4405
- const text = repo.getChunkText(chunkId);
4406
- addBlock(meta.repoRoot, meta.path, meta.startLine, meta.endLine, text, `graph neighbor (${n.weight})`);
4407
- }
4408
- } catch {
4409
- }
4410
- for (const h of hits) {
4411
- const repo = this.repos.find((r) => r.repoRoot === h.chunk.repoRoot);
4412
- if (!repo) continue;
4413
- const text = repo.getChunkText(h.chunk.id);
4414
- addBlock(h.chunk.repoRoot, h.chunk.path, h.chunk.startLine, h.chunk.endLine, text, "primary hit");
4415
- const expanded = await repo.expandContext(h.chunk.id, {
4416
- adjacentChunks: profile.expand.adjacentChunks ?? 0,
4417
- followImports: profile.expand.followImports ?? 0,
4418
- includeFileSynopsis: profile.expand.includeFileSynopsis ?? false
4419
- });
4420
- for (const ex of expanded) {
4421
- const meta = repo.getChunkMeta(ex.id);
4422
- if (!meta) continue;
4423
- const t = repo.getChunkText(ex.id);
4424
- addBlock(meta.repoRoot, meta.path, meta.startLine, meta.endLine, t, ex.reason);
4425
- }
4426
- }
4531
+ const graphNeighborFiles = await fetchGraphNeighborFiles({
4532
+ graphStore: this.graphStore,
4533
+ repos: this.repos,
4534
+ hits,
4535
+ profile,
4536
+ workspaceRoot: this.workspaceRoot,
4537
+ emitProgress: (e) => this.emitProgress(e)
4538
+ });
4539
+ const contextBlocks = await buildContextBlocks({ repos: this.repos, hits, graphNeighborFiles, profile });
4427
4540
  const bundle = {
4428
4541
  hits,
4429
4542
  context: contextBlocks,
@@ -4432,7 +4545,7 @@ var WorkspaceIndexer = class {
4432
4545
  reposSearched: this.repos.length,
4433
4546
  candidates: {
4434
4547
  vector: vecCount,
4435
- lexical: lexCount,
4548
+ lexical: totalLexCount,
4436
4549
  merged: merged.length,
4437
4550
  returned: hits.length
4438
4551
  }
@@ -4444,7 +4557,7 @@ var WorkspaceIndexer = class {
4444
4557
  profile: profile.name,
4445
4558
  ms: Date.now() - startedAt,
4446
4559
  hits: hits.length,
4447
- candidates: { vector: vecCount, lexical: lexCount, merged: merged.length }
4560
+ candidates: { vector: vecCount, lexical: totalLexCount, merged: merged.length }
4448
4561
  });
4449
4562
  return bundle;
4450
4563
  }
@@ -4581,11 +4694,11 @@ var HashEmbeddingsProvider = class {
4581
4694
  };
4582
4695
 
4583
4696
  // src/config.ts
4584
- import fs14 from "fs";
4585
- import path18 from "path";
4697
+ import fs16 from "fs";
4698
+ import path21 from "path";
4586
4699
  function loadConfigFile(filePath) {
4587
- const abs = path18.resolve(filePath);
4588
- const raw = fs14.readFileSync(abs, "utf8");
4700
+ const abs = path21.resolve(filePath);
4701
+ const raw = fs16.readFileSync(abs, "utf8");
4589
4702
  const json = JSON.parse(raw);
4590
4703
  const cfg = { ...json };
4591
4704
  if (json.redact?.patterns && Array.isArray(json.redact.patterns)) {
@@ -4615,11 +4728,13 @@ export {
4615
4728
  NoopAnnIndex,
4616
4729
  createAnnIndex,
4617
4730
  RepoIndexer,
4618
- DEFAULT_PROFILES,
4619
- deepMergeProfile,
4620
4731
  discoverGitRepos,
4621
4732
  pickRepoOverride,
4622
4733
  mergeIndexerConfig,
4734
+ betterSqlite3Adapter,
4735
+ sqlJsAdapter,
4736
+ createWorkspaceStore,
4737
+ createWorkspaceStoreAsync,
4623
4738
  WorkspaceStore,
4624
4739
  Neo4jGraphStore,
4625
4740
  createNeo4jGraphStore,
@@ -4629,6 +4744,8 @@ export {
4629
4744
  VsCodeContributesLanguageLinkStrategy,
4630
4745
  WorkspaceLinker,
4631
4746
  linkWorkspaceRepos,
4747
+ DEFAULT_PROFILES,
4748
+ deepMergeProfile,
4632
4749
  WorkspaceIndexer,
4633
4750
  OllamaEmbeddingsProvider,
4634
4751
  OpenAIEmbeddingsProvider,