@saptools/service-flow 0.1.9 → 0.1.10

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.10
4
+
5
+ - Persist unresolved `OPERATION_IMPLEMENTED_BY_HANDLER` audit edges when implementation candidates exist but all are rejected, including ranked candidate evidence and rejected reasons.
6
+ - Added a conservative helper-owned implementation path for unique registered helper handlers that implement model-oriented CDS operations without direct package dependency evidence, while keeping multiple helper matches ambiguous and local service-path contradictions rejected.
7
+ - Trace output now includes operation-to-handler implementation hops and terminal handler nodes in JSON/table/Mermaid-compatible edge data, including runtime-resolved operation targets.
8
+ - Doctor now reports rejected implementation candidates and strict remote-target implementation coverage gaps without making entity-only services noisy by default.
9
+ - Updated Node compatibility errors, package metadata, and README wording so current behavior is not described with stale release-specific version strings.
10
+
3
11
  ## 0.1.9
4
12
 
5
13
  - Fixed implementation dependency matching by binding graph repository ids as text when comparing against `graph_edges.from_id` and `graph_edges.to_id`, restoring cross-package application-to-model and application-to-handler evidence under `node:sqlite`.
package/README.md CHANGED
@@ -47,12 +47,12 @@ npm install @saptools/service-flow
47
47
  ```
48
48
 
49
49
  > [!NOTE]
50
- > Requires **Node.js ≥ 24.0.0** for the bundled `node:sqlite` runtime. Version 0.1.8 uses a persistent SQLite driver (`node:sqlite` in supported Node builds) for bound parameters, transactions, WAL, busy timeouts, and read-only query commands. The analyzer is static: it reads files and package metadata, but it does not start CAP services, connect to SAP BTP, or execute application code.
50
+ > Requires **Node.js ≥ 24.0.0** for the bundled `node:sqlite` runtime. The CLI uses a persistent SQLite driver (`node:sqlite` in supported Node builds) for bound parameters, transactions, WAL, busy timeouts, and read-only query commands. The analyzer is static: it reads files and package metadata, but it does not start CAP services, connect to SAP BTP, or execute application code.
51
51
 
52
52
  ---
53
53
 
54
54
 
55
- ### Correctness notes for 0.1.8
55
+ ### Correctness notes
56
56
 
57
57
  - Runtime `--var` values are considered only for dynamic, ambiguous, or unresolved **remote** graph edges whose alias, destination, service path, or operation path expressions contain supplied placeholders. Local database, external HTTP, event, and already resolved static edges keep their persisted status, target, reason, and confidence. Partial substitutions remain dynamic and report the missing placeholder names.
58
58
  - `trace` and `graph` both accept repeatable `--var key=value` options. Effective substitutions are rendered in trace evidence without mutating the persisted graph. Confidence values are bounded to `[0, 1]`.
@@ -60,7 +60,7 @@ npm install @saptools/service-flow
60
60
  - Helper-package dependency edges prefer exact indexed package names. Duplicate package-name candidates are persisted as ambiguous evidence rather than silently selecting one repository.
61
61
  - Handler registration parsing is AST-based for common `createCombinedHandler({ handler: ... })` forms: direct arrays, arrays assembled with spreads, non-`handlers` array names, aliased class imports, default-imported arrays, named exported arrays, and safe relative re-exports. Class-level rows keep registration file/line and import evidence.
62
62
  - Implementation edges require both operation compatibility and registration evidence. Same-repository registrations do not need a self-dependency edge; cross-package matches use registration or handler-package dependencies on the model package. Duplicate strong candidates are stored as ambiguous implementation edges.
63
- - Traces prefer persisted `OPERATION_IMPLEMENTED_BY_HANDLER` edges after static or runtime remote operation resolution, then fall back to same-repository decorator matching for simple projects.
63
+ - Traces render persisted `OPERATION_IMPLEMENTED_BY_HANDLER` hops after static or runtime remote operation resolution, including terminal handler nodes and ambiguous or unresolved implementation evidence when traversal cannot continue.
64
64
  - Repository fingerprints include source content, package name/version, dependencies and devDependencies, scripts, normalized `cds.requires` (including nested credentials), package file content, and the analyzer version. Metadata-only changes therefore trigger reindexing.
65
65
  - Index publication is designed around the last-good snapshot: failed parse or persistence attempts are recorded as diagnostics and must not be mixed with older graph facts. After indexing changes, relink before relying on graph/trace output; doctor reports stale or inconsistent stores where detectable.
66
66
  - Source discovery, file reads, hashing, parsing, and publication are all inside the repository-level protected indexing flow. A failed read keeps the previous fingerprint and facts, marks the repository failed, and records a `source_read_failed` diagnostic; a later successful index clears superseded read-failure diagnostics during fact publication.
@@ -1177,7 +1177,7 @@ function linkWorkspace(db, workspaceId, vars = {}) {
1177
1177
  const callSummary = linkCalls(db, workspaceId, vars, generation);
1178
1178
  const impl = linkImplementations(db, workspaceId, generation);
1179
1179
  db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
1180
- return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount };
1180
+ return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount, implementationUnresolvedCount: impl.unresolvedCount };
1181
1181
  });
1182
1182
  }
1183
1183
  function nextGraphGeneration(db, workspaceId) {
@@ -1225,14 +1225,15 @@ function callEvidence(call, resolution, servicePath, op, destination) {
1225
1225
  return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, targetRepo: resolution.target?.repoName, targetOperation: resolution.target?.operationName, helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0, candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
1226
1226
  }
1227
1227
  function linkImplementations(db, workspaceId, generation) {
1228
- const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,r.package_name modelPackage FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=?`).all(workspaceId);
1228
+ const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,r.package_name modelPackage,r.kind modelKind FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=?`).all(workspaceId);
1229
1229
  let edgeCount = 0;
1230
1230
  let resolvedCount = 0;
1231
1231
  let ambiguousCount = 0;
1232
+ let unresolvedCount = 0;
1232
1233
  for (const operation of operations) {
1233
1234
  const candidates = rankedImplementationCandidates(db, workspaceId, operation);
1235
+ if (candidates.length === 0) continue;
1234
1236
  const accepted = candidates.filter((candidate) => candidate.accepted);
1235
- if (accepted.length === 0) continue;
1236
1237
  const topScore = accepted[0]?.score ?? 0;
1237
1238
  const winners = accepted.filter((candidate) => candidate.score === topScore);
1238
1239
  const unique = winners.length === 1 ? winners[0] : void 0;
@@ -1243,12 +1244,18 @@ function linkImplementations(db, workspaceId, generation) {
1243
1244
  modelPackage: { id: operation.modelRepoId, name: operation.modelRepo, packageName: operation.modelPackage },
1244
1245
  candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1))
1245
1246
  };
1247
+ if (accepted.length === 0) {
1248
+ db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", "unresolved", "operation", graphId(operation.operationId), "handler_method_candidates", candidates.map((row) => graphId(row.methodId)).join(","), 0, JSON.stringify(evidence), 0, "No implementation candidate passed policy", generation);
1249
+ edgeCount += 1;
1250
+ unresolvedCount += 1;
1251
+ continue;
1252
+ }
1246
1253
  db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", unique ? "resolved" : "ambiguous", "operation", graphId(operation.operationId), unique ? "handler_method" : "handler_method_candidates", unique ? graphId(unique.methodId) : winners.map((row) => graphId(row.methodId)).join(","), unique ? 0.95 : 0.5, JSON.stringify(evidence), 0, unique ? null : "Ambiguous registered handler implementation candidates", generation);
1247
1254
  edgeCount += 1;
1248
1255
  if (unique) resolvedCount += 1;
1249
1256
  else ambiguousCount += 1;
1250
1257
  }
1251
- return { edgeCount, resolvedCount, ambiguousCount };
1258
+ return { edgeCount, resolvedCount, ambiguousCount, unresolvedCount };
1252
1259
  }
1253
1260
  function rankedImplementationCandidates(db, workspaceId, operation) {
1254
1261
  const rows2 = implementationCandidates(db, workspaceId, operation);
@@ -1275,6 +1282,7 @@ function implementationCandidates(db, workspaceId, operation) {
1275
1282
  ? modelRepoId,
1276
1283
  ? modelRepo,
1277
1284
  ? modelPackage,
1285
+ ? modelKind,
1278
1286
  ? servicePath,
1279
1287
  ? operationPath,
1280
1288
  ? operationName,
@@ -1283,6 +1291,7 @@ function implementationCandidates(db, workspaceId, operation) {
1283
1291
  CASE WHEN appRepo.id=handlerRepo.id THEN 1 ELSE 0 END sameRepoRegistration,
1284
1292
  CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id AND localService.service_path=?) THEN 1 ELSE 0 END localServicePathMatch,
1285
1293
  CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id) THEN 1 ELSE 0 END applicationHasLocalServices,
1294
+ CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg JOIN handler_classes localClass ON (localClass.id=localReg.handler_class_id OR localClass.class_name=localReg.class_name) JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id WHERE localReg.repo_id=appRepo.id AND (localMethod.decorator_value=? OR localMethod.decorator_value=? OR localMethod.method_name=?)) THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
1286
1295
  CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END appDependsOnModel,
1287
1296
  CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=CAST(handlerRepo.id AS TEXT)) THEN 1 ELSE 0 END appDependsOnHandler,
1288
1297
  CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(handlerRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END handlerDependsOnModel
@@ -1296,12 +1305,16 @@ function implementationCandidates(db, workspaceId, operation) {
1296
1305
  operation.modelRepoId,
1297
1306
  operation.modelRepo,
1298
1307
  operation.modelPackage,
1308
+ operation.modelKind,
1299
1309
  operation.servicePath,
1300
1310
  operation.operationPath,
1301
1311
  operation.operationName,
1302
1312
  operation.modelRepoId,
1303
1313
  operation.modelRepoId,
1304
1314
  operation.servicePath,
1315
+ normalizedOperation(String(operation.operationPath ?? "")),
1316
+ operation.operationName,
1317
+ operation.operationName,
1305
1318
  modelRepoGraphId,
1306
1319
  modelRepoGraphId,
1307
1320
  workspaceId,
@@ -1319,9 +1332,15 @@ function scoreImplementationCandidate(row, operation) {
1319
1332
  const localServicePathMatch = flag(row.localServicePathMatch);
1320
1333
  const applicationHasLocalServices = flag(row.applicationHasLocalServices);
1321
1334
  const appDependsOnModel = flag(row.appDependsOnModel);
1335
+ const applicationHasLocalRegistrationForOperation = flag(row.applicationHasLocalRegistrationForOperation);
1322
1336
  const appDependsOnHandler = flag(row.appDependsOnHandler);
1323
1337
  const handlerDependsOnModel = flag(row.handlerDependsOnModel);
1324
1338
  const importSource = typeof row.importSource === "string" && row.importSource.length > 0;
1339
+ const sameRepoRegistration = flag(row.sameRepoRegistration);
1340
+ const modelOriented = row.modelKind === "cap-db-model" || !applicationHasLocalRegistrationForOperation;
1341
+ const methodMatches = true;
1342
+ const registeredAndLinked = sameRepoRegistration && importSource;
1343
+ const helperOwned = modelOriented && methodMatches && registeredAndLinked && sameRepoRegistration && !applicationHasLocalServices && !modelIsApplicationRepo && !modelIsHandlerRepo && !localServicePathMatch && !appDependsOnModel && !appDependsOnHandler && !handlerDependsOnModel;
1325
1344
  if (modelIsApplicationRepo) {
1326
1345
  score += 100;
1327
1346
  acceptedReasons.push("model package equals registration package");
@@ -1348,6 +1367,10 @@ function scoreImplementationCandidate(row, operation) {
1348
1367
  score += 20;
1349
1368
  acceptedReasons.push("handler package depends on model package");
1350
1369
  }
1370
+ if (helperOwned) {
1371
+ score += 60;
1372
+ acceptedReasons.push("unique registered helper implementation for model-only operation");
1373
+ }
1351
1374
  if (importSource) {
1352
1375
  score += 10;
1353
1376
  acceptedReasons.push("registration imports handler class");
@@ -1355,8 +1378,8 @@ function scoreImplementationCandidate(row, operation) {
1355
1378
  const hasOwnership = modelIsApplicationRepo || modelIsHandlerRepo;
1356
1379
  const hasCrossPackage = appDependsOnModel && (modelIsHandlerRepo || appDependsOnHandler || !importSource);
1357
1380
  const contradicted = applicationHasLocalServices && !localServicePathMatch && !appDependsOnModel && !hasOwnership;
1358
- if (!hasOwnership && !localServicePathMatch && !hasCrossPackage) rejectedReasons.push("missing direct ownership, exact local service path, or validated cross-package dependency evidence");
1359
- const accepted = !contradicted && (hasOwnership || localServicePathMatch || hasCrossPackage || handlerDependsOnModel);
1381
+ if (!hasOwnership && !localServicePathMatch && !hasCrossPackage && !helperOwned) rejectedReasons.push("missing direct ownership, exact local service path, or validated cross-package dependency evidence");
1382
+ const accepted = !contradicted && (hasOwnership || localServicePathMatch || hasCrossPackage || handlerDependsOnModel || helperOwned);
1360
1383
  if (!accepted && rejectedReasons.length === 0) rejectedReasons.push("candidate did not meet implementation ownership policy");
1361
1384
  return { ...row, methodId: Number(row.methodId), score, accepted, acceptedReasons, rejectedReasons };
1362
1385
  }
@@ -1383,6 +1406,7 @@ function candidateEvidence(candidate, rank) {
1383
1406
  directOwnership: { modelIsApplicationRepo: flag(candidate.modelIsApplicationRepo), modelIsHandlerRepo: flag(candidate.modelIsHandlerRepo) },
1384
1407
  localServicePathMatch: flag(candidate.localServicePathMatch),
1385
1408
  applicationHasLocalServices: flag(candidate.applicationHasLocalServices),
1409
+ applicationHasLocalRegistrationForOperation: flag(candidate.applicationHasLocalRegistrationForOperation),
1386
1410
  appDependsOnModel: flag(candidate.appDependsOnModel),
1387
1411
  appDependsOnHandler: flag(candidate.appDependsOnHandler),
1388
1412
  handlerDependsOnModel: flag(candidate.handlerDependsOnModel),
@@ -1435,9 +1459,17 @@ function sourceFilesForStart(db, repoId, start) {
1435
1459
  operation,
1436
1460
  operation
1437
1461
  );
1438
- if (start.servicePath && rows2.length === 0) return void 0;
1439
- if (rows2.length === 0) return void 0;
1440
- return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
1462
+ if (rows2.length > 0) return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
1463
+ if (start.servicePath && operation) {
1464
+ const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile
1465
+ FROM cds_services s JOIN cds_operations o ON o.service_id=s.id
1466
+ JOIN graph_edges e ON e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status='resolved' AND e.from_kind='operation' AND e.from_id=CAST(o.id AS TEXT)
1467
+ JOIN handler_methods hm ON hm.id=CAST(e.to_id AS INTEGER)
1468
+ JOIN handler_classes hc ON hc.id=hm.handler_class_id
1469
+ WHERE (? IS NULL OR s.repo_id=?) AND s.service_path=? AND (o.operation_path=? OR o.operation_name=?)`).all(repoId, repoId, start.servicePath, operation, operation);
1470
+ if (implRows.length > 0) return new Set(implRows.map((row) => row.sourceFile).filter(Boolean));
1471
+ }
1472
+ return void 0;
1441
1473
  }
1442
1474
  function startScope(db, start) {
1443
1475
  const repo = start.repo ? db.prepare(
@@ -1470,11 +1502,19 @@ function handlerFilesForOperation(db, operationId) {
1470
1502
  ).all(op.repoId, operation, operation, op.operationName);
1471
1503
  return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
1472
1504
  }
1505
+ function implementationEdge(db, operationId) {
1506
+ return db.prepare("SELECT * FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND from_kind='operation' AND from_id=? ORDER BY CASE status WHEN 'resolved' THEN 0 WHEN 'ambiguous' THEN 1 ELSE 2 END,id LIMIT 1").get(operationId);
1507
+ }
1508
+ function handlerMethodNode(db, methodId) {
1509
+ const row = db.prepare(`SELECT hm.id methodId,hm.method_name methodName,hm.decorator_value decoratorValue,hm.source_line sourceLine,hc.class_name className,hc.source_file sourceFile,r.name repoName,r.id repoId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id JOIN repositories r ON r.id=hc.repo_id WHERE hm.id=?`).get(methodId);
1510
+ if (!row) return void 0;
1511
+ return { id: `handler_method:${methodId}`, kind: "handler_method", label: `${String(row.repoName)}:${String(row.className)}.${String(row.methodName)}`, ...row };
1512
+ }
1473
1513
  function implementationScope(db, operationId) {
1474
- const edge = db.prepare("SELECT * FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status='resolved' AND from_kind='operation' AND from_id=? ORDER BY id LIMIT 1").get(operationId);
1475
- if (!edge) return { files: /* @__PURE__ */ new Set() };
1514
+ const edge = implementationEdge(db, operationId);
1515
+ if (!edge || edge.status !== "resolved") return { files: /* @__PURE__ */ new Set(), edge };
1476
1516
  const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id WHERE hm.id=?").get(edge.to_id);
1477
- return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []) };
1517
+ return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), edge };
1478
1518
  }
1479
1519
  function includeCall(type, options) {
1480
1520
  if (!options.includeDb && type === "local_db_query") return false;
@@ -1573,6 +1613,22 @@ function trace(db, start, options) {
1573
1613
  const edges = [];
1574
1614
  const nodes = /* @__PURE__ */ new Map();
1575
1615
  const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, depth: 1 }] : [];
1616
+ if (start.servicePath && (start.operation ?? start.operationPath)) {
1617
+ const startOperation = normalizeOperation(start.operation ?? start.operationPath);
1618
+ const row = db.prepare(`SELECT o.id operationId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE s.service_path=? AND (o.operation_path=? OR o.operation_name=?) ORDER BY o.id LIMIT 1`).get(start.servicePath, startOperation, startOperation);
1619
+ if (row?.operationId !== void 0) {
1620
+ const opId = String(row.operationId);
1621
+ const op = operationNode(db, opId);
1622
+ const impl = implementationScope(db, opId);
1623
+ if (op) nodes.set(String(op.id), op);
1624
+ if (impl.edge) {
1625
+ const implEvidence = JSON.parse(String(impl.edge.evidence_json || "{}"));
1626
+ const handlerNode = impl.edge.status === "resolved" ? handlerMethodNode(db, impl.edge.to_id) : void 0;
1627
+ if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
1628
+ edges.push({ step: 1, type: "operation_implemented_by_handler", from: op?.label ? String(op.label) : `${start.servicePath}/${startOperation ?? ""}`, to: handlerNode?.label ? String(handlerNode.label) : `${impl.edge.to_kind}:${impl.edge.to_id}`, evidence: implEvidence, confidence: Number(impl.edge.confidence ?? 0), unresolvedReason: impl.edge.status === "resolved" ? void 0 : String(impl.edge.unresolved_reason ?? impl.edge.status) });
1629
+ }
1630
+ }
1631
+ }
1576
1632
  const seenScopes = /* @__PURE__ */ new Set();
1577
1633
  const seenEdges = /* @__PURE__ */ new Set();
1578
1634
  while (queue.length > 0) {
@@ -1628,10 +1684,26 @@ function trace(db, start, options) {
1628
1684
  confidence: Number(effectiveRow.confidence ?? call.confidence),
1629
1685
  unresolvedReason: effective.unresolvedReason
1630
1686
  });
1631
- if (effectiveRow.to_kind === "operation" && current.depth < maxDepth) {
1687
+ if (effectiveRow.to_kind === "operation") {
1632
1688
  const implementation = implementationScope(db, effectiveRow.to_id);
1689
+ if (implementation.edge) {
1690
+ const implEvidence = JSON.parse(String(implementation.edge.evidence_json || "{}"));
1691
+ const handlerNode = implementation.edge.status === "resolved" ? handlerMethodNode(db, implementation.edge.to_id) : void 0;
1692
+ const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
1693
+ if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
1694
+ edges.push({
1695
+ step: current.depth + 1,
1696
+ type: "operation_implemented_by_handler",
1697
+ from: to,
1698
+ to: implTo,
1699
+ evidence: implEvidence,
1700
+ confidence: Number(implementation.edge.confidence ?? 0),
1701
+ unresolvedReason: implementation.edge.status === "resolved" ? void 0 : String(implementation.edge.unresolved_reason ?? implementation.edge.status)
1702
+ });
1703
+ }
1704
+ if (current.depth >= maxDepth) continue;
1633
1705
  const files = implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id);
1634
- if (files.size > 0) {
1706
+ if (implementation.edge?.status === "resolved" && files.size > 0) {
1635
1707
  const targetRepoId = implementation.repoId ?? db.prepare(
1636
1708
  "SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
1637
1709
  ).get(effectiveRow.to_id)?.repoId;
@@ -1678,4 +1750,4 @@ export {
1678
1750
  linkWorkspace,
1679
1751
  trace
1680
1752
  };
1681
- //# sourceMappingURL=chunk-HQMAFSRR.js.map
1753
+ //# sourceMappingURL=chunk-LQT67AU5.js.map