@neuralsea/workspace-indexer 0.3.2 → 0.3.4

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.
@@ -1621,6 +1621,16 @@ var RepoFileIndexer = class {
1621
1621
  return;
1622
1622
  }
1623
1623
  this.emit({ type: "repo/index/file/start", repoRoot: this.repoRoot, path: posixRelPath });
1624
+ const stage = (stageName, stageStartedAt) => {
1625
+ this.emit({
1626
+ type: "repo/index/file/stage",
1627
+ repoRoot: this.repoRoot,
1628
+ path: posixRelPath,
1629
+ stage: stageName,
1630
+ ms: Date.now() - stageStartedAt
1631
+ });
1632
+ };
1633
+ const readStartedAt = Date.now();
1624
1634
  const abs = path9.join(this.repoRoot, fromPosixPath(posixRelPath));
1625
1635
  let stat;
1626
1636
  try {
@@ -1644,16 +1654,20 @@ var RepoFileIndexer = class {
1644
1654
  }
1645
1655
  const raw = buf.toString("utf8");
1646
1656
  const redacted = this.applyRedactions(raw);
1657
+ stage("read", readStartedAt);
1647
1658
  const fileHash = sha256Hex(redacted);
1648
1659
  const prev = this.store.getFileHash(posixRelPath);
1649
1660
  if (prev === fileHash) {
1650
1661
  this.emit({ type: "repo/index/file/skip", repoRoot: this.repoRoot, path: posixRelPath, reason: "unchanged" });
1651
1662
  return;
1652
1663
  }
1664
+ const chunkStartedAt = Date.now();
1653
1665
  const { language, chunks } = chunkSource(posixRelPath, redacted, this.config.chunk);
1666
+ stage("chunk", chunkStartedAt);
1654
1667
  let imports = [];
1655
1668
  let exports = [];
1656
1669
  if (language === "typescript" || language === "javascript") {
1670
+ const relationsStartedAt = Date.now();
1657
1671
  const rel = extractTsRelations(posixRelPath, redacted);
1658
1672
  imports = rel.imports;
1659
1673
  exports = rel.exports;
@@ -1661,12 +1675,14 @@ var RepoFileIndexer = class {
1661
1675
  this.store.setEdges(posixRelPath, "export", rel.exports);
1662
1676
  this.workspaceStore?.setEdges(this.repoId, posixRelPath, "import", rel.imports);
1663
1677
  this.workspaceStore?.setEdges(this.repoId, posixRelPath, "export", rel.exports);
1678
+ stage("relations", relationsStartedAt);
1664
1679
  } else {
1665
1680
  this.store.setEdges(posixRelPath, "import", []);
1666
1681
  this.store.setEdges(posixRelPath, "export", []);
1667
1682
  this.workspaceStore?.setEdges(this.repoId, posixRelPath, "import", []);
1668
1683
  this.workspaceStore?.setEdges(this.repoId, posixRelPath, "export", []);
1669
1684
  }
1685
+ const synopsisStartedAt = Date.now();
1670
1686
  const synopsisText = buildSynopsis(posixRelPath, language, redacted);
1671
1687
  const synopsis = synopsisText.trim() ? [
1672
1688
  {
@@ -1691,14 +1707,18 @@ var RepoFileIndexer = class {
1691
1707
  }
1692
1708
  ] : [];
1693
1709
  const combined = [...synopsis, ...headerChunk, ...chunks.map((c) => ({ ...c, kind: "chunk" }))];
1710
+ stage("synopsis", synopsisStartedAt);
1711
+ const embedStartedAt = Date.now();
1694
1712
  const embedTexts = [];
1695
1713
  const embedPlan = [];
1696
1714
  const embeddings = combined.map(() => null);
1715
+ let cached2 = 0;
1697
1716
  for (let i = 0; i < combined.length; i++) {
1698
1717
  const ch = combined[i];
1699
- const cached2 = this.embeddingCache.get(this.embedder.id, ch.contentHash);
1700
- if (cached2) {
1701
- embeddings[i] = cached2;
1718
+ const cachedEmb = this.embeddingCache.get(this.embedder.id, ch.contentHash);
1719
+ if (cachedEmb) {
1720
+ embeddings[i] = cachedEmb;
1721
+ cached2++;
1702
1722
  continue;
1703
1723
  }
1704
1724
  embedTexts.push(
@@ -1730,11 +1750,22 @@ ${ch.text}`
1730
1750
  this.embeddingCache.put(this.embedder.id, plan.contentHash, vecs[j]);
1731
1751
  }
1732
1752
  }
1753
+ this.emit({
1754
+ type: "repo/index/file/embed/done",
1755
+ repoRoot: this.repoRoot,
1756
+ path: posixRelPath,
1757
+ chunks: combined.length,
1758
+ cached: cached2,
1759
+ computed: embedTexts.length,
1760
+ ms: Date.now() - embedStartedAt
1761
+ });
1762
+ stage("embed", embedStartedAt);
1733
1763
  const fileMtime = stat.mtimeMs;
1734
1764
  const ftsMode = this.config.storage.ftsMode;
1735
1765
  const storeText = this.config.storage.storeText;
1736
1766
  const oldChunkIds = this.store.listChunksForFile(posixRelPath).map((r) => r.id);
1737
1767
  const points = [];
1768
+ const storeStartedAt = Date.now();
1738
1769
  const rows = combined.map((ch, i) => {
1739
1770
  const id = sha256Hex(
1740
1771
  `${this.repoId}:${posixRelPath}:${ch.kind}:${ch.startLine}:${ch.endLine}:${ch.contentHash}`
@@ -1762,7 +1793,19 @@ ${ch.text}`
1762
1793
  this.store.replaceChunksForFile(posixRelPath, rows);
1763
1794
  this.workspaceStore?.upsertFile(this.repoId, posixRelPath, fileHash, fileMtime, language, stat.size);
1764
1795
  this.workspaceStore?.replaceChunksForFile(this.repoId, this.repoRoot, posixRelPath, rows);
1796
+ stage("store", storeStartedAt);
1797
+ const symbolsStartedAt = Date.now();
1765
1798
  const symbolOut = await this.indexSymbolsIfEnabled(posixRelPath, language, redacted, fileHash);
1799
+ this.emit({
1800
+ type: "repo/index/file/symbols",
1801
+ repoRoot: this.repoRoot,
1802
+ path: posixRelPath,
1803
+ symbols: symbolOut?.symbols?.length ?? 0,
1804
+ edges: symbolOut?.edges?.length ?? 0,
1805
+ ms: Date.now() - symbolsStartedAt
1806
+ });
1807
+ stage("symbols", symbolsStartedAt);
1808
+ const graphStartedAt = Date.now();
1766
1809
  const head = this.getHead();
1767
1810
  if (this.graphStore && head) {
1768
1811
  await this.graphStore.replaceFileGraph({
@@ -1778,7 +1821,10 @@ ${ch.text}`
1778
1821
  symbolEdges: (symbolOut?.edges ?? []).map((e) => ({ fromId: e.fromId, toId: e.toId, kind: e.kind }))
1779
1822
  }).catch(() => void 0);
1780
1823
  }
1824
+ stage("graph", graphStartedAt);
1825
+ const vectorStartedAt = Date.now();
1781
1826
  await this.ensureVectorUpToDate(points, oldChunkIds);
1827
+ stage("vector", vectorStartedAt);
1782
1828
  this.emit({
1783
1829
  type: "repo/index/file/done",
1784
1830
  repoRoot: this.repoRoot,
@@ -2389,11 +2435,14 @@ var RepoIndexer = class {
2389
2435
  const uniq3 = Array.from(new Set(posixPaths)).slice(0, maxFiles);
2390
2436
  for (const p of uniq3) {
2391
2437
  if (opts?.signal?.aborted) return;
2438
+ const startedAt = Date.now();
2439
+ this.emitProgress({ type: "repo/symbolGraph/expand/start", repoRoot: this.repoRoot, path: p });
2392
2440
  const abs = path12.join(this.repoRoot, p.split("/").join(path12.sep));
2393
2441
  let text = "";
2394
2442
  try {
2395
2443
  text = fs9.readFileSync(abs, "utf8");
2396
2444
  } catch {
2445
+ this.emitProgress({ type: "repo/symbolGraph/expand/done", repoRoot: this.repoRoot, path: p, edges: 0, ms: Date.now() - startedAt });
2397
2446
  continue;
2398
2447
  }
2399
2448
  const contentHash = this.store.getFileHash(p) ?? void 0;
@@ -2411,6 +2460,7 @@ var RepoIndexer = class {
2411
2460
  fromPath: p,
2412
2461
  edges: normalized.map((e) => ({ fromId: e.fromId, toId: e.toId, kind: e.kind, toPath: e.toPath }))
2413
2462
  }).catch(() => void 0);
2463
+ this.emitProgress({ type: "repo/symbolGraph/expand/done", repoRoot: this.repoRoot, path: p, edges: normalized.length, ms: Date.now() - startedAt });
2414
2464
  }
2415
2465
  }
2416
2466
  async watch() {
@@ -3362,8 +3412,8 @@ var Neo4jGraphStore = class {
3362
3412
  const stmts = [
3363
3413
  `CREATE CONSTRAINT ${Repo}_repo_id IF NOT EXISTS FOR (r:${Repo}) REQUIRE r.repo_id IS UNIQUE`,
3364
3414
  `CREATE CONSTRAINT ${Symbol}_id IF NOT EXISTS FOR (s:${Symbol}) REQUIRE s.id IS UNIQUE`,
3365
- `CREATE CONSTRAINT ${External}_key IF NOT EXISTS FOR (e:${External}) REQUIRE (e.repo_id, e.kind, e.value) IS NODE KEY`,
3366
- `CREATE CONSTRAINT ${File}_key IF NOT EXISTS FOR (f:${File}) REQUIRE (f.repo_id, f.path) IS NODE KEY`
3415
+ `CREATE CONSTRAINT ${External}_key IF NOT EXISTS FOR (e:${External}) REQUIRE e.key IS UNIQUE`,
3416
+ `CREATE CONSTRAINT ${File}_key IF NOT EXISTS FOR (f:${File}) REQUIRE f.key IS UNIQUE`
3367
3417
  ];
3368
3418
  for (const q of stmts) {
3369
3419
  try {
@@ -3489,10 +3539,18 @@ var Neo4jGraphStore = class {
3489
3539
  await s.run(
3490
3540
  `MERGE (r:${Repo} {repo_id:$repoId})
3491
3541
  SET r.repo_root=$repoRoot, r.commit=$commit, r.branch=$branch, r.updated_at=timestamp()
3492
- MERGE (f:${File} {repo_id:$repoId, path:$path})
3493
- SET f.repo_root=$repoRoot, f.language=$language, f.updated_at=timestamp()
3542
+ MERGE (f:${File} {key:$fileKey})
3543
+ SET f.repo_id=$repoId, f.path=$path, f.repo_root=$repoRoot, f.language=$language, f.updated_at=timestamp()
3494
3544
  MERGE (r)-[:HAS_FILE]->(f)`,
3495
- { repoId: update.repoId, repoRoot: update.repoRoot, commit: update.commit, branch: update.branch, path: update.path, language: update.language }
3545
+ {
3546
+ repoId: update.repoId,
3547
+ repoRoot: update.repoRoot,
3548
+ commit: update.commit,
3549
+ branch: update.branch,
3550
+ path: update.path,
3551
+ language: update.language,
3552
+ fileKey: `${update.repoId}:${update.path}`
3553
+ }
3496
3554
  );
3497
3555
  const fileEdges = [
3498
3556
  ...update.imports.map((v) => ({ kind: "import", value: v })),
@@ -3502,9 +3560,14 @@ var Neo4jGraphStore = class {
3502
3560
  await s.run(
3503
3561
  `MATCH (f:${File} {repo_id:$repoId, path:$path})
3504
3562
  UNWIND $edges AS e
3505
- MERGE (x:${External} {repo_id:$repoId, kind:e.kind, value:e.value})
3563
+ MERGE (x:${External} {key:e.key})
3564
+ SET x.repo_id=$repoId, x.kind=e.kind, x.value=e.value
3506
3565
  MERGE (f)-[:FILE_EDGE {kind:e.kind}]->(x)`,
3507
- { repoId: update.repoId, path: update.path, edges: fileEdges }
3566
+ {
3567
+ repoId: update.repoId,
3568
+ path: update.path,
3569
+ edges: fileEdges.map((e) => ({ ...e, key: `${update.repoId}:${e.kind}:${e.value}` }))
3570
+ }
3508
3571
  ).catch(() => void 0);
3509
3572
  await s.run(
3510
3573
  `MATCH (f:${File} {repo_id:$repoId, path:$path})-[r:FILE_EDGE]->(x:${External})
@@ -4223,11 +4286,14 @@ var WorkspaceIndexer = class {
4223
4286
  if (seeds.length >= 4) break;
4224
4287
  }
4225
4288
  if (seeds.length > 0) {
4289
+ const gs = Date.now();
4290
+ this.emitProgress({ type: "workspace/retrieve/graph/start", workspaceRoot: this.workspaceRoot, seeds: seeds.length });
4226
4291
  graphNeighborFiles = await this.graphStore.neighborFiles({
4227
4292
  seeds,
4228
4293
  limit: profile.name === "architecture" ? 16 : 10,
4229
4294
  kinds: ["definition", "reference", "implementation", "typeDefinition"]
4230
4295
  });
4296
+ this.emitProgress({ type: "workspace/retrieve/graph/done", workspaceRoot: this.workspaceRoot, neighbors: graphNeighborFiles.length, ms: Date.now() - gs });
4231
4297
  }
4232
4298
  }
4233
4299
  } catch {
package/dist/cli.cjs CHANGED
@@ -1604,6 +1604,16 @@ var RepoFileIndexer = class {
1604
1604
  return;
1605
1605
  }
1606
1606
  this.emit({ type: "repo/index/file/start", repoRoot: this.repoRoot, path: posixRelPath });
1607
+ const stage = (stageName, stageStartedAt) => {
1608
+ this.emit({
1609
+ type: "repo/index/file/stage",
1610
+ repoRoot: this.repoRoot,
1611
+ path: posixRelPath,
1612
+ stage: stageName,
1613
+ ms: Date.now() - stageStartedAt
1614
+ });
1615
+ };
1616
+ const readStartedAt = Date.now();
1607
1617
  const abs = import_node_path9.default.join(this.repoRoot, fromPosixPath(posixRelPath));
1608
1618
  let stat;
1609
1619
  try {
@@ -1627,16 +1637,20 @@ var RepoFileIndexer = class {
1627
1637
  }
1628
1638
  const raw = buf.toString("utf8");
1629
1639
  const redacted = this.applyRedactions(raw);
1640
+ stage("read", readStartedAt);
1630
1641
  const fileHash = sha256Hex(redacted);
1631
1642
  const prev = this.store.getFileHash(posixRelPath);
1632
1643
  if (prev === fileHash) {
1633
1644
  this.emit({ type: "repo/index/file/skip", repoRoot: this.repoRoot, path: posixRelPath, reason: "unchanged" });
1634
1645
  return;
1635
1646
  }
1647
+ const chunkStartedAt = Date.now();
1636
1648
  const { language, chunks } = chunkSource(posixRelPath, redacted, this.config.chunk);
1649
+ stage("chunk", chunkStartedAt);
1637
1650
  let imports = [];
1638
1651
  let exports2 = [];
1639
1652
  if (language === "typescript" || language === "javascript") {
1653
+ const relationsStartedAt = Date.now();
1640
1654
  const rel = extractTsRelations(posixRelPath, redacted);
1641
1655
  imports = rel.imports;
1642
1656
  exports2 = rel.exports;
@@ -1644,12 +1658,14 @@ var RepoFileIndexer = class {
1644
1658
  this.store.setEdges(posixRelPath, "export", rel.exports);
1645
1659
  this.workspaceStore?.setEdges(this.repoId, posixRelPath, "import", rel.imports);
1646
1660
  this.workspaceStore?.setEdges(this.repoId, posixRelPath, "export", rel.exports);
1661
+ stage("relations", relationsStartedAt);
1647
1662
  } else {
1648
1663
  this.store.setEdges(posixRelPath, "import", []);
1649
1664
  this.store.setEdges(posixRelPath, "export", []);
1650
1665
  this.workspaceStore?.setEdges(this.repoId, posixRelPath, "import", []);
1651
1666
  this.workspaceStore?.setEdges(this.repoId, posixRelPath, "export", []);
1652
1667
  }
1668
+ const synopsisStartedAt = Date.now();
1653
1669
  const synopsisText = buildSynopsis(posixRelPath, language, redacted);
1654
1670
  const synopsis = synopsisText.trim() ? [
1655
1671
  {
@@ -1674,14 +1690,18 @@ var RepoFileIndexer = class {
1674
1690
  }
1675
1691
  ] : [];
1676
1692
  const combined = [...synopsis, ...headerChunk, ...chunks.map((c) => ({ ...c, kind: "chunk" }))];
1693
+ stage("synopsis", synopsisStartedAt);
1694
+ const embedStartedAt = Date.now();
1677
1695
  const embedTexts = [];
1678
1696
  const embedPlan = [];
1679
1697
  const embeddings = combined.map(() => null);
1698
+ let cached2 = 0;
1680
1699
  for (let i = 0; i < combined.length; i++) {
1681
1700
  const ch = combined[i];
1682
- const cached2 = this.embeddingCache.get(this.embedder.id, ch.contentHash);
1683
- if (cached2) {
1684
- embeddings[i] = cached2;
1701
+ const cachedEmb = this.embeddingCache.get(this.embedder.id, ch.contentHash);
1702
+ if (cachedEmb) {
1703
+ embeddings[i] = cachedEmb;
1704
+ cached2++;
1685
1705
  continue;
1686
1706
  }
1687
1707
  embedTexts.push(
@@ -1713,11 +1733,22 @@ ${ch.text}`
1713
1733
  this.embeddingCache.put(this.embedder.id, plan.contentHash, vecs[j]);
1714
1734
  }
1715
1735
  }
1736
+ this.emit({
1737
+ type: "repo/index/file/embed/done",
1738
+ repoRoot: this.repoRoot,
1739
+ path: posixRelPath,
1740
+ chunks: combined.length,
1741
+ cached: cached2,
1742
+ computed: embedTexts.length,
1743
+ ms: Date.now() - embedStartedAt
1744
+ });
1745
+ stage("embed", embedStartedAt);
1716
1746
  const fileMtime = stat.mtimeMs;
1717
1747
  const ftsMode = this.config.storage.ftsMode;
1718
1748
  const storeText = this.config.storage.storeText;
1719
1749
  const oldChunkIds = this.store.listChunksForFile(posixRelPath).map((r) => r.id);
1720
1750
  const points = [];
1751
+ const storeStartedAt = Date.now();
1721
1752
  const rows = combined.map((ch, i) => {
1722
1753
  const id = sha256Hex(
1723
1754
  `${this.repoId}:${posixRelPath}:${ch.kind}:${ch.startLine}:${ch.endLine}:${ch.contentHash}`
@@ -1745,7 +1776,19 @@ ${ch.text}`
1745
1776
  this.store.replaceChunksForFile(posixRelPath, rows);
1746
1777
  this.workspaceStore?.upsertFile(this.repoId, posixRelPath, fileHash, fileMtime, language, stat.size);
1747
1778
  this.workspaceStore?.replaceChunksForFile(this.repoId, this.repoRoot, posixRelPath, rows);
1779
+ stage("store", storeStartedAt);
1780
+ const symbolsStartedAt = Date.now();
1748
1781
  const symbolOut = await this.indexSymbolsIfEnabled(posixRelPath, language, redacted, fileHash);
1782
+ this.emit({
1783
+ type: "repo/index/file/symbols",
1784
+ repoRoot: this.repoRoot,
1785
+ path: posixRelPath,
1786
+ symbols: symbolOut?.symbols?.length ?? 0,
1787
+ edges: symbolOut?.edges?.length ?? 0,
1788
+ ms: Date.now() - symbolsStartedAt
1789
+ });
1790
+ stage("symbols", symbolsStartedAt);
1791
+ const graphStartedAt = Date.now();
1749
1792
  const head = this.getHead();
1750
1793
  if (this.graphStore && head) {
1751
1794
  await this.graphStore.replaceFileGraph({
@@ -1761,7 +1804,10 @@ ${ch.text}`
1761
1804
  symbolEdges: (symbolOut?.edges ?? []).map((e) => ({ fromId: e.fromId, toId: e.toId, kind: e.kind }))
1762
1805
  }).catch(() => void 0);
1763
1806
  }
1807
+ stage("graph", graphStartedAt);
1808
+ const vectorStartedAt = Date.now();
1764
1809
  await this.ensureVectorUpToDate(points, oldChunkIds);
1810
+ stage("vector", vectorStartedAt);
1765
1811
  this.emit({
1766
1812
  type: "repo/index/file/done",
1767
1813
  repoRoot: this.repoRoot,
@@ -2401,11 +2447,14 @@ var RepoIndexer = class {
2401
2447
  const uniq3 = Array.from(new Set(posixPaths)).slice(0, maxFiles);
2402
2448
  for (const p of uniq3) {
2403
2449
  if (opts?.signal?.aborted) return;
2450
+ const startedAt = Date.now();
2451
+ this.emitProgress({ type: "repo/symbolGraph/expand/start", repoRoot: this.repoRoot, path: p });
2404
2452
  const abs = import_node_path12.default.join(this.repoRoot, p.split("/").join(import_node_path12.default.sep));
2405
2453
  let text = "";
2406
2454
  try {
2407
2455
  text = import_node_fs9.default.readFileSync(abs, "utf8");
2408
2456
  } catch {
2457
+ this.emitProgress({ type: "repo/symbolGraph/expand/done", repoRoot: this.repoRoot, path: p, edges: 0, ms: Date.now() - startedAt });
2409
2458
  continue;
2410
2459
  }
2411
2460
  const contentHash = this.store.getFileHash(p) ?? void 0;
@@ -2423,6 +2472,7 @@ var RepoIndexer = class {
2423
2472
  fromPath: p,
2424
2473
  edges: normalized.map((e) => ({ fromId: e.fromId, toId: e.toId, kind: e.kind, toPath: e.toPath }))
2425
2474
  }).catch(() => void 0);
2475
+ this.emitProgress({ type: "repo/symbolGraph/expand/done", repoRoot: this.repoRoot, path: p, edges: normalized.length, ms: Date.now() - startedAt });
2426
2476
  }
2427
2477
  }
2428
2478
  async watch() {
@@ -3375,8 +3425,8 @@ var Neo4jGraphStore = class {
3375
3425
  const stmts = [
3376
3426
  `CREATE CONSTRAINT ${Repo}_repo_id IF NOT EXISTS FOR (r:${Repo}) REQUIRE r.repo_id IS UNIQUE`,
3377
3427
  `CREATE CONSTRAINT ${Symbol2}_id IF NOT EXISTS FOR (s:${Symbol2}) REQUIRE s.id IS UNIQUE`,
3378
- `CREATE CONSTRAINT ${External}_key IF NOT EXISTS FOR (e:${External}) REQUIRE (e.repo_id, e.kind, e.value) IS NODE KEY`,
3379
- `CREATE CONSTRAINT ${File}_key IF NOT EXISTS FOR (f:${File}) REQUIRE (f.repo_id, f.path) IS NODE KEY`
3428
+ `CREATE CONSTRAINT ${External}_key IF NOT EXISTS FOR (e:${External}) REQUIRE e.key IS UNIQUE`,
3429
+ `CREATE CONSTRAINT ${File}_key IF NOT EXISTS FOR (f:${File}) REQUIRE f.key IS UNIQUE`
3380
3430
  ];
3381
3431
  for (const q of stmts) {
3382
3432
  try {
@@ -3502,10 +3552,18 @@ var Neo4jGraphStore = class {
3502
3552
  await s.run(
3503
3553
  `MERGE (r:${Repo} {repo_id:$repoId})
3504
3554
  SET r.repo_root=$repoRoot, r.commit=$commit, r.branch=$branch, r.updated_at=timestamp()
3505
- MERGE (f:${File} {repo_id:$repoId, path:$path})
3506
- SET f.repo_root=$repoRoot, f.language=$language, f.updated_at=timestamp()
3555
+ MERGE (f:${File} {key:$fileKey})
3556
+ SET f.repo_id=$repoId, f.path=$path, f.repo_root=$repoRoot, f.language=$language, f.updated_at=timestamp()
3507
3557
  MERGE (r)-[:HAS_FILE]->(f)`,
3508
- { repoId: update.repoId, repoRoot: update.repoRoot, commit: update.commit, branch: update.branch, path: update.path, language: update.language }
3558
+ {
3559
+ repoId: update.repoId,
3560
+ repoRoot: update.repoRoot,
3561
+ commit: update.commit,
3562
+ branch: update.branch,
3563
+ path: update.path,
3564
+ language: update.language,
3565
+ fileKey: `${update.repoId}:${update.path}`
3566
+ }
3509
3567
  );
3510
3568
  const fileEdges = [
3511
3569
  ...update.imports.map((v) => ({ kind: "import", value: v })),
@@ -3515,9 +3573,14 @@ var Neo4jGraphStore = class {
3515
3573
  await s.run(
3516
3574
  `MATCH (f:${File} {repo_id:$repoId, path:$path})
3517
3575
  UNWIND $edges AS e
3518
- MERGE (x:${External} {repo_id:$repoId, kind:e.kind, value:e.value})
3576
+ MERGE (x:${External} {key:e.key})
3577
+ SET x.repo_id=$repoId, x.kind=e.kind, x.value=e.value
3519
3578
  MERGE (f)-[:FILE_EDGE {kind:e.kind}]->(x)`,
3520
- { repoId: update.repoId, path: update.path, edges: fileEdges }
3579
+ {
3580
+ repoId: update.repoId,
3581
+ path: update.path,
3582
+ edges: fileEdges.map((e) => ({ ...e, key: `${update.repoId}:${e.kind}:${e.value}` }))
3583
+ }
3521
3584
  ).catch(() => void 0);
3522
3585
  await s.run(
3523
3586
  `MATCH (f:${File} {repo_id:$repoId, path:$path})-[r:FILE_EDGE]->(x:${External})
@@ -4235,11 +4298,14 @@ var WorkspaceIndexer = class {
4235
4298
  if (seeds.length >= 4) break;
4236
4299
  }
4237
4300
  if (seeds.length > 0) {
4301
+ const gs = Date.now();
4302
+ this.emitProgress({ type: "workspace/retrieve/graph/start", workspaceRoot: this.workspaceRoot, seeds: seeds.length });
4238
4303
  graphNeighborFiles = await this.graphStore.neighborFiles({
4239
4304
  seeds,
4240
4305
  limit: profile.name === "architecture" ? 16 : 10,
4241
4306
  kinds: ["definition", "reference", "implementation", "typeDefinition"]
4242
4307
  });
4308
+ this.emitProgress({ type: "workspace/retrieve/graph/done", workspaceRoot: this.workspaceRoot, neighbors: graphNeighborFiles.length, ms: Date.now() - gs });
4243
4309
  }
4244
4310
  }
4245
4311
  } catch {
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  OpenAIEmbeddingsProvider,
6
6
  WorkspaceIndexer,
7
7
  loadConfigFile
8
- } from "./chunk-GGL3XTMV.js";
8
+ } from "./chunk-ENM3P2PS.js";
9
9
 
10
10
  // src/cli.ts
11
11
  import yargs from "yargs";
package/dist/index.cjs CHANGED
@@ -2325,8 +2325,8 @@ var Neo4jGraphStore = class {
2325
2325
  const stmts = [
2326
2326
  `CREATE CONSTRAINT ${Repo}_repo_id IF NOT EXISTS FOR (r:${Repo}) REQUIRE r.repo_id IS UNIQUE`,
2327
2327
  `CREATE CONSTRAINT ${Symbol2}_id IF NOT EXISTS FOR (s:${Symbol2}) REQUIRE s.id IS UNIQUE`,
2328
- `CREATE CONSTRAINT ${External}_key IF NOT EXISTS FOR (e:${External}) REQUIRE (e.repo_id, e.kind, e.value) IS NODE KEY`,
2329
- `CREATE CONSTRAINT ${File}_key IF NOT EXISTS FOR (f:${File}) REQUIRE (f.repo_id, f.path) IS NODE KEY`
2328
+ `CREATE CONSTRAINT ${External}_key IF NOT EXISTS FOR (e:${External}) REQUIRE e.key IS UNIQUE`,
2329
+ `CREATE CONSTRAINT ${File}_key IF NOT EXISTS FOR (f:${File}) REQUIRE f.key IS UNIQUE`
2330
2330
  ];
2331
2331
  for (const q of stmts) {
2332
2332
  try {
@@ -2452,10 +2452,18 @@ var Neo4jGraphStore = class {
2452
2452
  await s.run(
2453
2453
  `MERGE (r:${Repo} {repo_id:$repoId})
2454
2454
  SET r.repo_root=$repoRoot, r.commit=$commit, r.branch=$branch, r.updated_at=timestamp()
2455
- MERGE (f:${File} {repo_id:$repoId, path:$path})
2456
- SET f.repo_root=$repoRoot, f.language=$language, f.updated_at=timestamp()
2455
+ MERGE (f:${File} {key:$fileKey})
2456
+ SET f.repo_id=$repoId, f.path=$path, f.repo_root=$repoRoot, f.language=$language, f.updated_at=timestamp()
2457
2457
  MERGE (r)-[:HAS_FILE]->(f)`,
2458
- { repoId: update.repoId, repoRoot: update.repoRoot, commit: update.commit, branch: update.branch, path: update.path, language: update.language }
2458
+ {
2459
+ repoId: update.repoId,
2460
+ repoRoot: update.repoRoot,
2461
+ commit: update.commit,
2462
+ branch: update.branch,
2463
+ path: update.path,
2464
+ language: update.language,
2465
+ fileKey: `${update.repoId}:${update.path}`
2466
+ }
2459
2467
  );
2460
2468
  const fileEdges = [
2461
2469
  ...update.imports.map((v) => ({ kind: "import", value: v })),
@@ -2465,9 +2473,14 @@ var Neo4jGraphStore = class {
2465
2473
  await s.run(
2466
2474
  `MATCH (f:${File} {repo_id:$repoId, path:$path})
2467
2475
  UNWIND $edges AS e
2468
- MERGE (x:${External} {repo_id:$repoId, kind:e.kind, value:e.value})
2476
+ MERGE (x:${External} {key:e.key})
2477
+ SET x.repo_id=$repoId, x.kind=e.kind, x.value=e.value
2469
2478
  MERGE (f)-[:FILE_EDGE {kind:e.kind}]->(x)`,
2470
- { repoId: update.repoId, path: update.path, edges: fileEdges }
2479
+ {
2480
+ repoId: update.repoId,
2481
+ path: update.path,
2482
+ edges: fileEdges.map((e) => ({ ...e, key: `${update.repoId}:${e.kind}:${e.value}` }))
2483
+ }
2471
2484
  ).catch(() => void 0);
2472
2485
  await s.run(
2473
2486
  `MATCH (f:${File} {repo_id:$repoId, path:$path})-[r:FILE_EDGE]->(x:${External})
@@ -3395,6 +3408,16 @@ var RepoFileIndexer = class {
3395
3408
  return;
3396
3409
  }
3397
3410
  this.emit({ type: "repo/index/file/start", repoRoot: this.repoRoot, path: posixRelPath });
3411
+ const stage = (stageName, stageStartedAt) => {
3412
+ this.emit({
3413
+ type: "repo/index/file/stage",
3414
+ repoRoot: this.repoRoot,
3415
+ path: posixRelPath,
3416
+ stage: stageName,
3417
+ ms: Date.now() - stageStartedAt
3418
+ });
3419
+ };
3420
+ const readStartedAt = Date.now();
3398
3421
  const abs = import_node_path14.default.join(this.repoRoot, fromPosixPath(posixRelPath));
3399
3422
  let stat;
3400
3423
  try {
@@ -3418,16 +3441,20 @@ var RepoFileIndexer = class {
3418
3441
  }
3419
3442
  const raw = buf.toString("utf8");
3420
3443
  const redacted = this.applyRedactions(raw);
3444
+ stage("read", readStartedAt);
3421
3445
  const fileHash = sha256Hex(redacted);
3422
3446
  const prev = this.store.getFileHash(posixRelPath);
3423
3447
  if (prev === fileHash) {
3424
3448
  this.emit({ type: "repo/index/file/skip", repoRoot: this.repoRoot, path: posixRelPath, reason: "unchanged" });
3425
3449
  return;
3426
3450
  }
3451
+ const chunkStartedAt = Date.now();
3427
3452
  const { language, chunks } = chunkSource(posixRelPath, redacted, this.config.chunk);
3453
+ stage("chunk", chunkStartedAt);
3428
3454
  let imports = [];
3429
3455
  let exports2 = [];
3430
3456
  if (language === "typescript" || language === "javascript") {
3457
+ const relationsStartedAt = Date.now();
3431
3458
  const rel = extractTsRelations(posixRelPath, redacted);
3432
3459
  imports = rel.imports;
3433
3460
  exports2 = rel.exports;
@@ -3435,12 +3462,14 @@ var RepoFileIndexer = class {
3435
3462
  this.store.setEdges(posixRelPath, "export", rel.exports);
3436
3463
  this.workspaceStore?.setEdges(this.repoId, posixRelPath, "import", rel.imports);
3437
3464
  this.workspaceStore?.setEdges(this.repoId, posixRelPath, "export", rel.exports);
3465
+ stage("relations", relationsStartedAt);
3438
3466
  } else {
3439
3467
  this.store.setEdges(posixRelPath, "import", []);
3440
3468
  this.store.setEdges(posixRelPath, "export", []);
3441
3469
  this.workspaceStore?.setEdges(this.repoId, posixRelPath, "import", []);
3442
3470
  this.workspaceStore?.setEdges(this.repoId, posixRelPath, "export", []);
3443
3471
  }
3472
+ const synopsisStartedAt = Date.now();
3444
3473
  const synopsisText = buildSynopsis(posixRelPath, language, redacted);
3445
3474
  const synopsis = synopsisText.trim() ? [
3446
3475
  {
@@ -3465,14 +3494,18 @@ var RepoFileIndexer = class {
3465
3494
  }
3466
3495
  ] : [];
3467
3496
  const combined = [...synopsis, ...headerChunk, ...chunks.map((c) => ({ ...c, kind: "chunk" }))];
3497
+ stage("synopsis", synopsisStartedAt);
3498
+ const embedStartedAt = Date.now();
3468
3499
  const embedTexts = [];
3469
3500
  const embedPlan = [];
3470
3501
  const embeddings = combined.map(() => null);
3502
+ let cached2 = 0;
3471
3503
  for (let i = 0; i < combined.length; i++) {
3472
3504
  const ch = combined[i];
3473
- const cached2 = this.embeddingCache.get(this.embedder.id, ch.contentHash);
3474
- if (cached2) {
3475
- embeddings[i] = cached2;
3505
+ const cachedEmb = this.embeddingCache.get(this.embedder.id, ch.contentHash);
3506
+ if (cachedEmb) {
3507
+ embeddings[i] = cachedEmb;
3508
+ cached2++;
3476
3509
  continue;
3477
3510
  }
3478
3511
  embedTexts.push(
@@ -3504,11 +3537,22 @@ ${ch.text}`
3504
3537
  this.embeddingCache.put(this.embedder.id, plan.contentHash, vecs[j]);
3505
3538
  }
3506
3539
  }
3540
+ this.emit({
3541
+ type: "repo/index/file/embed/done",
3542
+ repoRoot: this.repoRoot,
3543
+ path: posixRelPath,
3544
+ chunks: combined.length,
3545
+ cached: cached2,
3546
+ computed: embedTexts.length,
3547
+ ms: Date.now() - embedStartedAt
3548
+ });
3549
+ stage("embed", embedStartedAt);
3507
3550
  const fileMtime = stat.mtimeMs;
3508
3551
  const ftsMode = this.config.storage.ftsMode;
3509
3552
  const storeText = this.config.storage.storeText;
3510
3553
  const oldChunkIds = this.store.listChunksForFile(posixRelPath).map((r) => r.id);
3511
3554
  const points = [];
3555
+ const storeStartedAt = Date.now();
3512
3556
  const rows = combined.map((ch, i) => {
3513
3557
  const id = sha256Hex(
3514
3558
  `${this.repoId}:${posixRelPath}:${ch.kind}:${ch.startLine}:${ch.endLine}:${ch.contentHash}`
@@ -3536,7 +3580,19 @@ ${ch.text}`
3536
3580
  this.store.replaceChunksForFile(posixRelPath, rows);
3537
3581
  this.workspaceStore?.upsertFile(this.repoId, posixRelPath, fileHash, fileMtime, language, stat.size);
3538
3582
  this.workspaceStore?.replaceChunksForFile(this.repoId, this.repoRoot, posixRelPath, rows);
3583
+ stage("store", storeStartedAt);
3584
+ const symbolsStartedAt = Date.now();
3539
3585
  const symbolOut = await this.indexSymbolsIfEnabled(posixRelPath, language, redacted, fileHash);
3586
+ this.emit({
3587
+ type: "repo/index/file/symbols",
3588
+ repoRoot: this.repoRoot,
3589
+ path: posixRelPath,
3590
+ symbols: symbolOut?.symbols?.length ?? 0,
3591
+ edges: symbolOut?.edges?.length ?? 0,
3592
+ ms: Date.now() - symbolsStartedAt
3593
+ });
3594
+ stage("symbols", symbolsStartedAt);
3595
+ const graphStartedAt = Date.now();
3540
3596
  const head = this.getHead();
3541
3597
  if (this.graphStore && head) {
3542
3598
  await this.graphStore.replaceFileGraph({
@@ -3552,7 +3608,10 @@ ${ch.text}`
3552
3608
  symbolEdges: (symbolOut?.edges ?? []).map((e) => ({ fromId: e.fromId, toId: e.toId, kind: e.kind }))
3553
3609
  }).catch(() => void 0);
3554
3610
  }
3611
+ stage("graph", graphStartedAt);
3612
+ const vectorStartedAt = Date.now();
3555
3613
  await this.ensureVectorUpToDate(points, oldChunkIds);
3614
+ stage("vector", vectorStartedAt);
3556
3615
  this.emit({
3557
3616
  type: "repo/index/file/done",
3558
3617
  repoRoot: this.repoRoot,
@@ -4163,11 +4222,14 @@ var RepoIndexer = class {
4163
4222
  const uniq3 = Array.from(new Set(posixPaths)).slice(0, maxFiles);
4164
4223
  for (const p of uniq3) {
4165
4224
  if (opts?.signal?.aborted) return;
4225
+ const startedAt = Date.now();
4226
+ this.emitProgress({ type: "repo/symbolGraph/expand/start", repoRoot: this.repoRoot, path: p });
4166
4227
  const abs = import_node_path17.default.join(this.repoRoot, p.split("/").join(import_node_path17.default.sep));
4167
4228
  let text = "";
4168
4229
  try {
4169
4230
  text = import_node_fs12.default.readFileSync(abs, "utf8");
4170
4231
  } catch {
4232
+ this.emitProgress({ type: "repo/symbolGraph/expand/done", repoRoot: this.repoRoot, path: p, edges: 0, ms: Date.now() - startedAt });
4171
4233
  continue;
4172
4234
  }
4173
4235
  const contentHash = this.store.getFileHash(p) ?? void 0;
@@ -4185,6 +4247,7 @@ var RepoIndexer = class {
4185
4247
  fromPath: p,
4186
4248
  edges: normalized.map((e) => ({ fromId: e.fromId, toId: e.toId, kind: e.kind, toPath: e.toPath }))
4187
4249
  }).catch(() => void 0);
4250
+ this.emitProgress({ type: "repo/symbolGraph/expand/done", repoRoot: this.repoRoot, path: p, edges: normalized.length, ms: Date.now() - startedAt });
4188
4251
  }
4189
4252
  }
4190
4253
  async watch() {
@@ -4771,11 +4834,14 @@ var WorkspaceIndexer = class {
4771
4834
  if (seeds.length >= 4) break;
4772
4835
  }
4773
4836
  if (seeds.length > 0) {
4837
+ const gs = Date.now();
4838
+ this.emitProgress({ type: "workspace/retrieve/graph/start", workspaceRoot: this.workspaceRoot, seeds: seeds.length });
4774
4839
  graphNeighborFiles = await this.graphStore.neighborFiles({
4775
4840
  seeds,
4776
4841
  limit: profile.name === "architecture" ? 16 : 10,
4777
4842
  kinds: ["definition", "reference", "implementation", "typeDefinition"]
4778
4843
  });
4844
+ this.emitProgress({ type: "workspace/retrieve/graph/done", workspaceRoot: this.workspaceRoot, neighbors: graphNeighborFiles.length, ms: Date.now() - gs });
4779
4845
  }
4780
4846
  }
4781
4847
  } catch {
package/dist/index.d.cts CHANGED
@@ -253,6 +253,15 @@ type IndexerProgressEvent = {
253
253
  lexical: number;
254
254
  merged: number;
255
255
  };
256
+ } | {
257
+ type: "workspace/retrieve/graph/start";
258
+ workspaceRoot: string;
259
+ seeds: number;
260
+ } | {
261
+ type: "workspace/retrieve/graph/done";
262
+ workspaceRoot: string;
263
+ ms: number;
264
+ neighbors: number;
256
265
  } | {
257
266
  type: "repo/open";
258
267
  repoRoot: string;
@@ -267,11 +276,32 @@ type IndexerProgressEvent = {
267
276
  type: "repo/index/file/start";
268
277
  repoRoot: string;
269
278
  path: string;
279
+ } | {
280
+ type: "repo/index/file/stage";
281
+ repoRoot: string;
282
+ path: string;
283
+ stage: "read" | "chunk" | "relations" | "synopsis" | "embed" | "store" | "symbols" | "graph" | "vector";
284
+ ms?: number;
270
285
  } | {
271
286
  type: "repo/index/file/skip";
272
287
  repoRoot: string;
273
288
  path: string;
274
289
  reason: string;
290
+ } | {
291
+ type: "repo/index/file/embed/done";
292
+ repoRoot: string;
293
+ path: string;
294
+ chunks: number;
295
+ cached: number;
296
+ computed: number;
297
+ ms: number;
298
+ } | {
299
+ type: "repo/index/file/symbols";
300
+ repoRoot: string;
301
+ path: string;
302
+ symbols: number;
303
+ edges: number;
304
+ ms: number;
275
305
  } | {
276
306
  type: "repo/index/file/done";
277
307
  repoRoot: string;
@@ -306,6 +336,16 @@ type IndexerProgressEvent = {
306
336
  type: "repo/vector/flush";
307
337
  repoRoot: string;
308
338
  kind: string;
339
+ } | {
340
+ type: "repo/symbolGraph/expand/start";
341
+ repoRoot: string;
342
+ path: string;
343
+ } | {
344
+ type: "repo/symbolGraph/expand/done";
345
+ repoRoot: string;
346
+ path: string;
347
+ edges: number;
348
+ ms: number;
309
349
  } | {
310
350
  type: "repo/watch/start";
311
351
  repoRoot: string;
package/dist/index.d.ts CHANGED
@@ -253,6 +253,15 @@ type IndexerProgressEvent = {
253
253
  lexical: number;
254
254
  merged: number;
255
255
  };
256
+ } | {
257
+ type: "workspace/retrieve/graph/start";
258
+ workspaceRoot: string;
259
+ seeds: number;
260
+ } | {
261
+ type: "workspace/retrieve/graph/done";
262
+ workspaceRoot: string;
263
+ ms: number;
264
+ neighbors: number;
256
265
  } | {
257
266
  type: "repo/open";
258
267
  repoRoot: string;
@@ -267,11 +276,32 @@ type IndexerProgressEvent = {
267
276
  type: "repo/index/file/start";
268
277
  repoRoot: string;
269
278
  path: string;
279
+ } | {
280
+ type: "repo/index/file/stage";
281
+ repoRoot: string;
282
+ path: string;
283
+ stage: "read" | "chunk" | "relations" | "synopsis" | "embed" | "store" | "symbols" | "graph" | "vector";
284
+ ms?: number;
270
285
  } | {
271
286
  type: "repo/index/file/skip";
272
287
  repoRoot: string;
273
288
  path: string;
274
289
  reason: string;
290
+ } | {
291
+ type: "repo/index/file/embed/done";
292
+ repoRoot: string;
293
+ path: string;
294
+ chunks: number;
295
+ cached: number;
296
+ computed: number;
297
+ ms: number;
298
+ } | {
299
+ type: "repo/index/file/symbols";
300
+ repoRoot: string;
301
+ path: string;
302
+ symbols: number;
303
+ edges: number;
304
+ ms: number;
275
305
  } | {
276
306
  type: "repo/index/file/done";
277
307
  repoRoot: string;
@@ -306,6 +336,16 @@ type IndexerProgressEvent = {
306
336
  type: "repo/vector/flush";
307
337
  repoRoot: string;
308
338
  kind: string;
339
+ } | {
340
+ type: "repo/symbolGraph/expand/start";
341
+ repoRoot: string;
342
+ path: string;
343
+ } | {
344
+ type: "repo/symbolGraph/expand/done";
345
+ repoRoot: string;
346
+ path: string;
347
+ edges: number;
348
+ ms: number;
309
349
  } | {
310
350
  type: "repo/watch/start";
311
351
  repoRoot: string;
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  mergeIndexerConfig,
28
28
  pickRepoOverride,
29
29
  stableSymbolId
30
- } from "./chunk-GGL3XTMV.js";
30
+ } from "./chunk-ENM3P2PS.js";
31
31
 
32
32
  // src/symbolGraph/vscodeProvider.ts
33
33
  import path2 from "path";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuralsea/workspace-indexer",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Local-first multi-repo workspace indexer (semantic embeddings + git-aware incremental updates + hybrid retrieval profiles) for AI agents.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",