@openez-graph/cli 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to OpenEZ Graph are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.11.1] - 2026-08-06
9
+
10
+ ### Fixed
11
+
12
+ - Writing phase performance bottleneck: symbol, import, and wikilink node upserts batched per file instead of N individual queries
13
+ - Embedding provider and model now logged at indexing startup for visibility
14
+ - Progress logs added for post-writing phases (embedding batches, FTS rebuild, call edge resolution)
15
+
8
16
  ## [0.11.0] - 2026-08-06
9
17
 
10
18
  ### Added
@@ -146,6 +154,7 @@ Remediation release — index/graph correctness, data protection, and web flow f
146
154
  - Error handling and validation for import path extraction
147
155
  - CLI npm packaging
148
156
 
157
+ [0.11.1]: https://github.com/asta-nguyen/openez-graph/compare/v0.11.0...v0.11.1
149
158
  [0.11.0]: https://github.com/asta-nguyen/openez-graph/compare/v0.10.0...v0.11.0
150
159
  [0.10.0]: https://github.com/asta-nguyen/openez-graph/compare/fbcad4f...HEAD
151
160
  [0.9.1]: https://github.com/asta-nguyen/openez-graph/compare/fbcad4f...HEAD
package/dist/cli.cjs CHANGED
@@ -12594,6 +12594,36 @@ function createWorkspaceRepository(rootPath) {
12594
12594
  }
12595
12595
  return ids;
12596
12596
  },
12597
+ async upsertGraphNodesBatch(inputs) {
12598
+ if (inputs.length === 0) return [];
12599
+ const now = (/* @__PURE__ */ new Date()).toISOString();
12600
+ const BATCH = 500;
12601
+ const results = [];
12602
+ for (let i = 0; i < inputs.length; i += BATCH) {
12603
+ const batch = inputs.slice(i, i + BATCH);
12604
+ const placeholders = batch.map(() => "(?, ?, ?, ?, ?, ?, ?)").join(",");
12605
+ const params = [];
12606
+ for (const item of batch) {
12607
+ const id = import_node_crypto2.default.randomUUID();
12608
+ params.push(
12609
+ id,
12610
+ item.type,
12611
+ item.label,
12612
+ item.refId ?? null,
12613
+ item.metadata ?? "{}",
12614
+ now,
12615
+ now
12616
+ );
12617
+ }
12618
+ const rows = native.prepare(
12619
+ `INSERT INTO graph_nodes (id, type, label, ref_id, metadata, created_at, updated_at) VALUES ${placeholders}
12620
+ ON CONFLICT(type, label) WHERE type != 'symbol' DO UPDATE SET ref_id = COALESCE(excluded.ref_id, graph_nodes.ref_id), metadata = excluded.metadata, updated_at = excluded.updated_at
12621
+ RETURNING id, label`
12622
+ ).all(...params);
12623
+ results.push(...rows.map((r) => ({ label: r.label, id: String(r.id) })));
12624
+ }
12625
+ return results;
12626
+ },
12597
12627
  async getGraphNode(id) {
12598
12628
  const row = native.prepare("SELECT * FROM graph_nodes WHERE id = ?").get(id);
12599
12629
  return row ? mapNodeRow(row) : null;
@@ -30293,13 +30323,7 @@ function walkTree(root, config2, lines) {
30293
30323
  endLine: endRow,
30294
30324
  ...receiver ? { receiver } : {}
30295
30325
  });
30296
- extractCallsInNode(
30297
- node,
30298
- config2,
30299
- fullName,
30300
- calledIdentifiers,
30301
- callExpressions
30302
- );
30326
+ extractCallsInNode(node, config2, fullName, calledIdentifiers, callExpressions);
30303
30327
  const isContextNode = symbolRule.establishesContext || config2.contextNodeTypes.has(node.type);
30304
30328
  if (isContextNode) {
30305
30329
  const contextName = symbolRule.extractContextName ? symbolRule.extractContextName(node) ?? fullName : fullName;
@@ -30324,9 +30348,7 @@ function walkTree(root, config2, lines) {
30324
30348
  }
30325
30349
  function extractCallsInNode(symbolNode, config2, callerName, calledIdentifiers, callExpressions) {
30326
30350
  const nestedSymbolTypes = config2.symbolRules.map((r) => r.nodeType);
30327
- const nestedSymbols = symbolNode.descendantsOfType(nestedSymbolTypes).filter(
30328
- (n) => !(n.startIndex === symbolNode.startIndex && n.endIndex === symbolNode.endIndex)
30329
- );
30351
+ const nestedSymbols = symbolNode.descendantsOfType(nestedSymbolTypes).filter((n) => !(n.startIndex === symbolNode.startIndex && n.endIndex === symbolNode.endIndex));
30330
30352
  const callNodes = symbolNode.descendantsOfType(config2.callRule.nodeType);
30331
30353
  for (const callNode of callNodes) {
30332
30354
  const insideNested = nestedSymbols.some(
@@ -281073,7 +281095,12 @@ async function writeEmbeddingsToRepo(repo, chunkRows, provider) {
281073
281095
  const BATCH_SIZE = 50;
281074
281096
  let totalWritten = 0;
281075
281097
  let failedBatches = 0;
281098
+ const totalBatches = Math.ceil(toEmbed.length / BATCH_SIZE);
281076
281099
  for (let i = 0; i < toEmbed.length; i += BATCH_SIZE) {
281100
+ const batchNum = Math.floor(i / BATCH_SIZE) + 1;
281101
+ if (batchNum % 10 === 0 || batchNum === totalBatches) {
281102
+ process.stdout.write(`\r[embedding] batch ${batchNum}/${totalBatches} (${totalWritten} written)`);
281103
+ }
281077
281104
  const batch = toEmbed.slice(i, i + BATCH_SIZE);
281078
281105
  try {
281079
281106
  const vectors = await provider.embed(
@@ -281118,6 +281145,9 @@ async function writeEmbeddingsToRepo(repo, chunkRows, provider) {
281118
281145
  );
281119
281146
  }
281120
281147
  }
281148
+ if (totalBatches > 0) {
281149
+ process.stdout.write("\n");
281150
+ }
281121
281151
  return { written: totalWritten + reusedWritten, failedBatches };
281122
281152
  }
281123
281153
  async function parseInline(tasks, onProgress) {
@@ -281161,6 +281191,15 @@ async function indexWorkspace(input) {
281161
281191
  const excludeGlobs = workspace.excludeGlobs || configuredWorkspace?.exclude.join("\n") || "";
281162
281192
  const embeddingProvider = await getEmbeddingProvider();
281163
281193
  const runMode = input.mode ?? "incremental";
281194
+ if (embeddingProvider) {
281195
+ process.stdout.write(
281196
+ `[embedding] provider=${embeddingProvider.provider} model=${embeddingProvider.model}
281197
+ `
281198
+ );
281199
+ } else {
281200
+ process.stdout.write(`[embedding] disabled (no provider configured)
281201
+ `);
281202
+ }
281164
281203
  const reportProgress = async (message, progress) => {
281165
281204
  await input.onProgress?.({ message, progress });
281166
281205
  };
@@ -281358,6 +281397,8 @@ async function indexWorkspace(input) {
281358
281397
  const chunkNodeIds = await repo.insertGraphNodesBatch(chunkNodeInputs);
281359
281398
  const edges = [];
281360
281399
  const reusedSymbolIds = /* @__PURE__ */ new Set();
281400
+ const newSymbolInputs = [];
281401
+ const pendingSymbolEdges = [];
281361
281402
  for (const [ci, chunkId] of chunkIds.entries()) {
281362
281403
  const chunkNodeId = chunkNodeIds[ci];
281363
281404
  edges.push({ fromNodeId: fileNodeId, toNodeId: chunkNodeId, type: "contains" });
@@ -281366,7 +281407,7 @@ async function indexWorkspace(input) {
281366
281407
  if (symbolName) {
281367
281408
  const fileSymbolKey = `${file.relativePath}\0${symbolName}`;
281368
281409
  let symbolNodeId = symbolNodeIdsByFileAndName.get(fileSymbolKey);
281369
- if (!symbolNodeId) {
281410
+ if (symbolNodeIdsByFileAndName.has(fileSymbolKey) === false) {
281370
281411
  const existingSymbolId = fileExistingSymbols.get(symbolName);
281371
281412
  if (existingSymbolId) {
281372
281413
  repo.updateSymbolNode(
@@ -281381,7 +281422,7 @@ async function indexWorkspace(input) {
281381
281422
  );
281382
281423
  symbolNodeId = existingSymbolId;
281383
281424
  } else {
281384
- symbolNodeId = await repo.upsertGraphNode({
281425
+ newSymbolInputs.push({
281385
281426
  type: "symbol",
281386
281427
  label: symbolName,
281387
281428
  refId: chunkId,
@@ -281390,14 +281431,43 @@ async function indexWorkspace(input) {
281390
281431
  filePath: file.relativePath,
281391
281432
  language: indexed.language,
281392
281433
  parser: indexed.parser
281393
- })
281434
+ }),
281435
+ _chunkIndex: ci
281394
281436
  });
281437
+ symbolNodeId = "";
281395
281438
  }
281396
281439
  symbolNodeIdsByFileAndName.set(fileSymbolKey, symbolNodeId);
281440
+ } else {
281441
+ symbolNodeId = symbolNodeIdsByFileAndName.get(fileSymbolKey) ?? "";
281397
281442
  }
281398
281443
  reusedSymbolIds.add(symbolNodeId);
281444
+ const definesEdgeIdx = edges.length;
281399
281445
  edges.push({ fromNodeId: fileNodeId, toNodeId: symbolNodeId, type: "defines" });
281446
+ const representedByEdgeIdx = edges.length;
281400
281447
  edges.push({ fromNodeId: symbolNodeId, toNodeId: chunkNodeId, type: "represented_by" });
281448
+ if (symbolNodeId === "") {
281449
+ pendingSymbolEdges.push({ label: symbolName, definesEdgeIdx, representedByEdgeIdx });
281450
+ }
281451
+ }
281452
+ }
281453
+ if (newSymbolInputs.length > 0) {
281454
+ const symbolIds = await repo.insertGraphNodesBatch(
281455
+ newSymbolInputs.map(({ _chunkIndex, ...rest }) => rest)
281456
+ );
281457
+ for (let si = 0; si < newSymbolInputs.length; si++) {
281458
+ const input2 = newSymbolInputs[si];
281459
+ const fileSymbolKey = `${file.relativePath}\0${input2.label}`;
281460
+ const newId = symbolIds[si];
281461
+ symbolNodeIdsByFileAndName.set(fileSymbolKey, newId);
281462
+ reusedSymbolIds.add(newId);
281463
+ }
281464
+ for (const pending of pendingSymbolEdges) {
281465
+ const newId = symbolNodeIdsByFileAndName.get(
281466
+ `${file.relativePath}\0${pending.label}`
281467
+ );
281468
+ if (!newId) continue;
281469
+ edges[pending.definesEdgeIdx].toNodeId = newId;
281470
+ edges[pending.representedByEdgeIdx].fromNodeId = newId;
281401
281471
  }
281402
281472
  }
281403
281473
  const staleSymbolIds = [];
@@ -281409,6 +281479,8 @@ async function indexWorkspace(input) {
281409
281479
  if (staleSymbolIds.length > 0) {
281410
281480
  repo.deleteGraphNodesByIds(staleSymbolIds);
281411
281481
  }
281482
+ const batchNodeInputs = [];
281483
+ const importEdgeInputs = [];
281412
281484
  for (const importPath of indexed.importPaths) {
281413
281485
  if (typeof importPath !== "string" || importPath.length === 0) continue;
281414
281486
  const resolvedImportPath = workspaceFileResolver?.resolveImport(
@@ -281417,25 +281489,43 @@ async function indexWorkspace(input) {
281417
281489
  indexed.language ?? void 0
281418
281490
  );
281419
281491
  if (!resolvedImportPath) continue;
281420
- const targetNodeId = await repo.upsertGraphNode({
281492
+ batchNodeInputs.push({
281421
281493
  type: "file",
281422
281494
  label: resolvedImportPath,
281423
281495
  metadata: JSON.stringify({ path: resolvedImportPath })
281424
281496
  });
281425
- edges.push({
281426
- fromNodeId: fileNodeId,
281427
- toNodeId: targetNodeId,
281428
- type: "imports",
281429
- metadata: JSON.stringify({ importPath })
281430
- });
281497
+ importEdgeInputs.push({ importPath, resolvedImportPath });
281431
281498
  }
281432
281499
  for (const link of indexed.wikilinks) {
281433
- const entityNodeId = await repo.upsertGraphNode({
281500
+ batchNodeInputs.push({
281434
281501
  type: "entity",
281435
281502
  label: link,
281436
281503
  metadata: "{}"
281437
281504
  });
281438
- edges.push({ fromNodeId: fileNodeId, toNodeId: entityNodeId, type: "mentions" });
281505
+ }
281506
+ if (batchNodeInputs.length > 0) {
281507
+ const upsertedNodes = await repo.upsertGraphNodesBatch(batchNodeInputs);
281508
+ const nodeByLabel = /* @__PURE__ */ new Map();
281509
+ for (const node of upsertedNodes) {
281510
+ nodeByLabel.set(node.label, node.id);
281511
+ }
281512
+ for (const { importPath, resolvedImportPath } of importEdgeInputs) {
281513
+ const targetNodeId = nodeByLabel.get(resolvedImportPath);
281514
+ if (targetNodeId) {
281515
+ edges.push({
281516
+ fromNodeId: fileNodeId,
281517
+ toNodeId: targetNodeId,
281518
+ type: "imports",
281519
+ metadata: JSON.stringify({ importPath })
281520
+ });
281521
+ }
281522
+ }
281523
+ for (const link of indexed.wikilinks) {
281524
+ const entityNodeId = nodeByLabel.get(link);
281525
+ if (entityNodeId) {
281526
+ edges.push({ fromNodeId: fileNodeId, toNodeId: entityNodeId, type: "mentions" });
281527
+ }
281528
+ }
281439
281529
  }
281440
281530
  const edgeSet = /* @__PURE__ */ new Set();
281441
281531
  const dedupedEdges = edges.filter((e) => {
@@ -281463,7 +281553,11 @@ async function indexWorkspace(input) {
281463
281553
  filesUpdated += 1;
281464
281554
  }
281465
281555
  });
281466
- if (allChunkRowsForEmbeddings.length > 0) {
281556
+ if (allChunkRowsForEmbeddings.length > 0 && embeddingProvider) {
281557
+ await reportProgress(
281558
+ `Embedding ${allChunkRowsForEmbeddings.length} chunks via ${embeddingProvider.provider}/${embeddingProvider.model}...`,
281559
+ 90
281560
+ );
281467
281561
  const embeddingResult = await writeEmbeddingsToRepo(
281468
281562
  repo,
281469
281563
  allChunkRowsForEmbeddings,
@@ -281473,10 +281567,12 @@ async function indexWorkspace(input) {
281473
281567
  embeddingFailures += embeddingResult.failedBatches;
281474
281568
  }
281475
281569
  if (bulkWriteMode) {
281570
+ await reportProgress("Rebuilding FTS index...", 93);
281476
281571
  repo.restoreFtsTriggers();
281477
281572
  repo.setOptimizedWriteMode(false);
281478
281573
  bulkWriteMode = false;
281479
281574
  }
281575
+ await reportProgress("Resolving call edges...", 95);
281480
281576
  const globalSymbolNodes = await repo.loadAllSymbolNodes();
281481
281577
  const insertedCallEdges = /* @__PURE__ */ new Set();
281482
281578
  const callEdges = [];
@@ -296995,7 +297091,7 @@ __export(mcp_bridge_exports, {
296995
297091
  startMcpServer: () => startMcpServer
296996
297092
  });
296997
297093
  async function startMcpServer(defaultPath, version4) {
296998
- await createAndStartMcpServer({ defaultPath, version: version4, build: "cc56c89-dirty" });
297094
+ await createAndStartMcpServer({ defaultPath, version: version4, build: "ebc5064-dirty" });
296999
297095
  }
297000
297096
  var init_mcp_bridge = __esm({
297001
297097
  "src/mcp-bridge.ts"() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openez-graph/cli",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Local-first code intelligence engine — index, query, and graph your codebase with zero config. SQLite-only, MCP-first.",
5
5
  "license": "MIT",
6
6
  "author": "Asta Nguyen",