@saptools/service-flow 0.1.12 → 0.1.14

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.14
4
+
5
+ - Made local symbol-call indexing opt-in: CAP DSL, request helpers, package namespace/CommonJS calls, global runtime APIs, and service-client transport helpers are filtered unless indexed local or relative-import evidence makes the edge actionable.
6
+ - Expanded local DB query entity extraction for `SELECT.one(Entity)`, `UPSERT.into(Entity)`, `UPDATE.entity(Entity)`, static element access such as `this.model['Books']`, and clearer dynamic-query warning reasons.
7
+ - Persisted unknown DB query graph targets as semantic `db_entity:unknown` terminal nodes with source `callId` and parser-warning evidence, so fresh relinks no longer store numeric call ids as DB targets.
8
+ - Classified unresolved local service-client `.send`, `.emit`, `.publish`, and `.on` calls as terminal transport/client calls when the model does not declare a matching operation, while preserving real declared operations.
9
+ - Tightened doctor default local-service warnings and added compact `doctor --strict` symbol-call and DB-query quality aggregates with capped top unresolved examples.
10
+ - Added neutral regression fixtures for symbol-call noise, DB query forms, and local service-client methods.
11
+
12
+ ## 0.1.13
13
+
14
+ - Added implementation-context fallback for local `cds.services.*` calls so helper packages can resolve model-package operations only when handler/dependency/registration evidence ties the caller repository to the target operation.
15
+ - Preserved same-repository local service resolution as the strongest path and added explicit evidence/reasons for implementation-context ownership, rejected candidates, and candidate-without-caller-ownership cases.
16
+ - Replaced fragile CAP DB query string extraction with AST traversal for chained `cds.run(SELECT/INSERT/UPDATE/DELETE...)` forms, including multiline `columns(...).where(...)` chains and `this.EntityName` targets.
17
+ - Kept genuinely dynamic DB queries terminal while exposing parser-warning evidence and rendering unknown targets as `Entity: unknown` instead of raw numeric call ids in table/Mermaid output.
18
+ - Further reduced symbol-call noise by filtering built-in collection/string methods, logger calls, global built-ins, third-party package property calls, and unindexed `this.container.method()` calls while retaining indexed local helpers and relative object-literal helper imports.
19
+ - Added doctor aggregate visibility for local service calls resolved by implementation context and calls left unresolved because candidates lack caller ownership.
20
+ - Updated README and technical notes to clarify generated constants remain low-level parser output rather than persisted graph facts.
21
+
3
22
  ## 0.1.12
4
23
 
5
24
  - Resolved same-repository local CAP service calls by qualified CDS name, simple service name, and service path, with explicit local transport and alias-chain evidence.
package/README.md CHANGED
@@ -69,6 +69,7 @@ npm install @saptools/service-flow
69
69
  - Fresh databases include foreign keys for key graph, run, and diagnostic tables. Migrated legacy stores that still lack that metadata are reported by doctor with `legacy_schema_weaker_foreign_keys`; rebuild into a fresh database if strict structural parity is required.
70
70
  - Parser warnings describe analysis completeness, while routing status describes graph behavior. A terminal DB edge can remain terminal while still exposing parser warning evidence about an unknown entity.
71
71
 
72
+
72
73
  ## 🚀 Quick Start
73
74
 
74
75
  ```bash
@@ -152,11 +153,11 @@ target handler up to `--depth` instead of showing only calls in the first file.
152
153
 
153
154
  `service-flow trace` starts from the selected handler method symbol, renders outbound calls owned by that symbol, and follows conservative local helper-call facts. Supported helper edges include same-file functions, `this.method()` calls, and exactly mapped relative imports/exports that resolve to an indexed executable symbol. Calls from unrelated functions in the same source file are not included merely because the file path matches.
154
155
 
155
- Local CAP calls through `cds.services.<Service>.<operation>()`, bracket service lookups, and simple aliases are indexed as local operation calls. Linking stays within the same repository by default and matches the target operation by exact qualified CDS service name, exact simple service name, exact service path, or an unambiguous service-path suffix. Entity accessors such as `cds.services.db.entities(...)` are treated as entity metadata access, not operation calls.
156
+ Local CAP calls through `cds.services.<Service>.<operation>()`, bracket service lookups, and simple aliases are indexed as local operation calls. Linking first stays within the same repository and matches the target operation by exact qualified CDS service name, exact simple service name, exact service path, or an unambiguous service-path suffix. If no same-repository service exists, the linker can use implementation-context evidence to resolve model-package operations for helper packages: a resolved/ambiguous implementation candidate, registration package, or dependency/import edge must tie the caller repository to the model operation. Name-only global matches are preserved as unresolved candidate evidence rather than guessed links. Entity accessors such as `cds.services.db.entities(...)` are treated as entity metadata access, not operation calls.
156
157
 
157
158
  Conservative local symbol traversal intentionally excludes decorators, built-ins such as `JSON.parse`, collection methods, third-party APIs, and arbitrary property chains unless the callee can plausibly resolve to an indexed local symbol. Named export lists such as `export { loadTemplate as publicLoadTemplate }` are indexed with the public exported name so relative imports can resolve. One-level object-literal helpers are indexed as symbols named like `cacheHelper.getConfiguration`; nested object literals are not yet expanded beyond the first helper level. `parseGeneratedConstants` remains a public low-level parser export for callers that need it, but generated constants are not persisted as graph facts in this patch; linking uses the deterministic decorator normalizer described above.
158
159
  JSON output includes typed nodes for calls, operations, database entities,
159
- external destinations, and unresolved/dynamic candidates when edges exist.
160
+ external destinations, and unresolved/dynamic candidates when edges exist. Chained CAP DB queries inside `cds.run(...)` are parsed with TypeScript AST evidence for `SELECT`, `INSERT`, `UPDATE`, and `DELETE` forms. When the query target is genuinely dynamic, graph status remains terminal and JSON retains `parserWarning` evidence, while table and Mermaid render the target as `Entity: unknown` rather than a numeric call id.
160
161
 
161
162
  ```bash
162
163
  service-flow trace --workspace /path/to/workspace --repo facade-service --operation doWork
@@ -287,7 +288,7 @@ service-flow clean --workspace /path/to/workspace
287
288
  | Local data access | `cds.run(SELECT...)` and local entity query evidence |
288
289
  | Async channels | Event Mesh-style `emit`, `publish`, and `on` facts |
289
290
  | External calls | Cloud SDK-style HTTP/destination calls and external edge evidence |
290
- | Generated constants | constants used to resolve service paths, operation paths, and thin helper wrappers |
291
+ | Generated constants | low-level `parseGeneratedConstants` parser output for integrations; not persisted as first-class graph facts in this patch |
291
292
 
292
293
  ---
293
294
 
package/TECHNICAL-NOTE.md CHANGED
@@ -1,4 +1,4 @@
1
- # Service Flow 0.1.8 Resolution Notes
1
+ # Service Flow Resolution Notes
2
2
 
3
3
  - Imported helper bindings: TypeScript imports are resolved for relative modules. When a caller assigns `const client = await connectToService()`, the analyzer follows the imported symbol to an exported helper that returns `cds.connect.to(...)` and persists caller-variable evidence plus the helper source/export chain.
4
4
  - Candidate ranking: operation-path matches start as weak candidates. A resolved operation edge requires a strong signal such as exact service path, CDS alias/destination context, or explicit dynamic variable overrides. Otherwise candidates are preserved in edge evidence as ambiguous or unresolved.
@@ -6,6 +6,16 @@
6
6
  - Trace cycle safety: trace queues carry repository IDs, visited scope keys are independent of depth, graph edge IDs are emitted once, and revisiting an already-seen downstream operation scope creates a cycle marker instead of recursive expansion.
7
7
  - SQLite reliability: the package uses a persistent SQLite connection per opened database, bound parameters, transactions, WAL, busy timeouts, read-only openings for query commands, and connection-local foreign-key enforcement. Native driver loading failures produce an actionable startup error before output rendering.
8
8
 
9
+ ## 0.1.14 audit follow-up notes
10
+
11
+ - Local CAP service calls keep same-repository service ownership as the strongest resolution path. When the caller repository does not own the CDS service model, the linker searches workspace operations by service identity and operation name/path, then requires caller ownership evidence from implementation edges, ambiguous implementation candidates, registration packages, or resolved dependency/import edges before resolving. Candidate operations without caller evidence are retained with `local_service_candidate_without_caller_ownership` rather than guessed.
12
+ - Trace traversal can use local-call context to choose the caller repository's handler from an otherwise ambiguous global implementation edge. This is scoped to the local call and does not rewrite the global `OPERATION_IMPLEMENTED_BY_HANDLER` ambiguity.
13
+ - CAP DB entity extraction for local `cds.run(...)` calls uses TypeScript AST traversal across chained query expressions, including `SELECT.from(Entity)`, `SELECT.one.from(Entity)`, `SELECT.one(Entity)`, `INSERT.into(Entity)`, `UPSERT.into(Entity)`, `UPDATE(Entity)`, `UPDATE.entity(Entity)`, `DELETE.from(Entity)`, and static element access such as `this.model['Books']`. Dynamic or unknowable query targets remain terminal graph edges with parser-warning evidence such as `dynamic_entity_expression`.
14
+ - Fresh relinks persist unknown DB query targets semantically as `to_kind = db_entity` and `to_id = unknown`; no schema migration is required for this row-level graph change. JSON, table, and Mermaid output use `Entity: unknown`, while evidence keeps the source `callId` and parser warning for machines.
15
+ - Symbol-call indexing is opt-in. Same-file symbols, indexed `this.method()` calls, exact relative import/export matches, and exported object-literal helper methods are kept; CAP DSL, request helpers, package namespace/CommonJS calls, global runtime APIs, and generic transport helpers are filtered unless local indexed evidence makes the edge actionable.
16
+ - `doctor --strict` reports compact parser-quality aggregates for symbol calls and local DB query known/unknown ratios; default doctor stays focused on actionable warnings.
17
+ - Generated constants remain low-level parser output through `parseGeneratedConstants`. They are not persisted as graph facts; implementation linking only uses deterministic decorator normalization for common generated action/function names.
18
+
9
19
  ## 0.1.4 trace-correctness additions
10
20
 
11
21
  - Helper exports are normalized through a public-to-local export map, so `export { helper }` and `export { helper as publicHelper }` both resolve to the local declaration that contains the `cds.connect.to(...)` call.
@@ -555,22 +555,56 @@ function matchingParen(text, open) {
555
555
  }
556
556
  return -1;
557
557
  }
558
- function argumentForCall(expr, marker) {
559
- const idx = expr.indexOf(marker);
560
- if (idx < 0) return void 0;
561
- const open = expr.indexOf("(", idx + marker.length);
562
- if (open < 0) return void 0;
563
- const close = matchingParen(expr, open);
564
- return close > open ? expr.slice(open + 1, close).trim() : void 0;
565
- }
566
- function entityFromArg(arg) {
567
- if (!arg) return void 0;
568
- const first = arg.split(",")[0]?.trim();
569
- if (!first) return void 0;
570
- return stripQuotes(first).replace(/^this\./, "");
558
+ function entityFromExpression(expr) {
559
+ if (!expr) return void 0;
560
+ if (ts4.isIdentifier(expr) || ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
561
+ if (ts4.isPropertyAccessExpression(expr) && expr.expression.kind === ts4.SyntaxKind.ThisKeyword) return expr.name.text;
562
+ if (ts4.isElementAccessExpression(expr) && expr.argumentExpression && (ts4.isStringLiteral(expr.argumentExpression) || ts4.isNoSubstitutionTemplateLiteral(expr.argumentExpression))) return expr.argumentExpression.text;
563
+ return void 0;
564
+ }
565
+ function expressionName(expr) {
566
+ if (ts4.isIdentifier(expr)) return expr.text;
567
+ if (ts4.isPropertyAccessExpression(expr)) return `${expressionName(expr.expression)}.${expr.name.text}`;
568
+ return expr.getText();
569
+ }
570
+ function variableInitializers(source) {
571
+ const initializers = /* @__PURE__ */ new Map();
572
+ const visit = (node) => {
573
+ if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer && (node.parent.flags & ts4.NodeFlags.Const) !== 0) initializers.set(node.name.text, node.initializer);
574
+ ts4.forEachChild(node, visit);
575
+ };
576
+ visit(source);
577
+ return initializers;
578
+ }
579
+ function queryEntityFromAst(expr, initializers = /* @__PURE__ */ new Map()) {
580
+ if (ts4.isParenthesizedExpression(expr) || ts4.isAwaitExpression(expr)) return queryEntityFromAst(expr.expression, initializers);
581
+ if (ts4.isIdentifier(expr) && initializers.has(expr.text)) return queryEntityFromAst(initializers.get(expr.text), initializers);
582
+ if (ts4.isCallExpression(expr)) {
583
+ const name = expressionName(expr.expression);
584
+ if (name === "cds.run") return queryEntityFromAst(expr.arguments[0], initializers);
585
+ if (["SELECT.one.from", "SELECT.from", "SELECT.one", "INSERT.into", "UPSERT.into", "DELETE.from", "UPDATE.entity"].includes(name)) return entityFromExpression(expr.arguments[0]);
586
+ if (name === "UPDATE") return entityFromExpression(expr.arguments[0]);
587
+ const receiver = ts4.isPropertyAccessExpression(expr.expression) ? expr.expression.expression : void 0;
588
+ if (receiver) return queryEntityFromAst(receiver, initializers);
589
+ }
590
+ return void 0;
571
591
  }
572
592
  function extractQueryEntity(expr) {
573
- return entityFromArg(argumentForCall(expr, "SELECT.one.from")) ?? entityFromArg(argumentForCall(expr, "SELECT.from")) ?? entityFromArg(argumentForCall(expr, "INSERT.into")) ?? entityFromArg(argumentForCall(expr, "UPDATE")) ?? entityFromArg(argumentForCall(expr, "DELETE.from"));
593
+ const source = ts4.createSourceFile("query.ts", `const __query = (${expr});`, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
594
+ const initializers = variableInitializers(source);
595
+ let found;
596
+ const visit = (node) => {
597
+ if (found) return;
598
+ if (ts4.isParenthesizedExpression(node)) found = queryEntityFromAst(node.expression, initializers);
599
+ ts4.forEachChild(node, visit);
600
+ };
601
+ visit(source);
602
+ return found;
603
+ }
604
+ function queryWarning(expr) {
605
+ if (/^\s*[`'"]/.test(expr)) return "raw_sql_or_cql_expression";
606
+ if (/^\s*\w+\s*$/.test(expr)) return "query_variable_without_static_initializer";
607
+ return "dynamic_entity_expression";
574
608
  }
575
609
  async function parseOutboundCalls(repoPath, filePath) {
576
610
  const text = await fs6.readFile(path7.join(repoPath, filePath), "utf8");
@@ -603,7 +637,7 @@ async function parseOutboundCalls(repoPath, filePath) {
603
637
  sourceFile: normalizePath(filePath),
604
638
  sourceLine: lineOf3(text, m.index ?? 0),
605
639
  confidence: entity ? 0.9 : 0.55,
606
- unresolvedReason: entity ? void 0 : "Could not resolve CAP query target entity from nested expression"
640
+ unresolvedReason: entity ? void 0 : queryWarning(expr)
607
641
  });
608
642
  }
609
643
  for (const m of text.matchAll(
@@ -651,7 +685,8 @@ function parseLocalServiceCalls(text, filePath) {
651
685
  aliasChain: parsed.chain,
652
686
  sourceFile: normalizePath(filePath),
653
687
  sourceLine: lineOf3(text, node.getStart(source)),
654
- confidence: 0.9
688
+ confidence: 0.9,
689
+ unresolvedReason: ["send", "emit", "publish", "on"].includes(parsed.operation) ? "transport_client_method" : void 0
655
690
  });
656
691
  }
657
692
  ts4.forEachChild(node, visit);
@@ -1074,7 +1109,7 @@ function substituteVariables(template, vars) {
1074
1109
  // src/linker/service-resolver.ts
1075
1110
  function rows(db, operationPath, workspaceId) {
1076
1111
  return db.prepare(
1077
- `SELECT o.id operationId,r.name repoName,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score,'' reasons
1112
+ `SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score,'' reasons
1078
1113
  FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
1079
1114
  WHERE (? IS NULL OR r.workspace_id=?) AND (o.operation_path=? OR o.operation_name=?) ORDER BY r.name,s.service_path,o.operation_name`
1080
1115
  ).all(
@@ -1098,11 +1133,21 @@ function resolveOperation(db, signals, workspaceId) {
1098
1133
  candidates: [],
1099
1134
  reasons: ["missing_operation_path"]
1100
1135
  };
1101
- const candidates = rows(db, signals.operationPath, workspaceId).filter((c) => matchesLocalRepo(db, c.operationId, signals.repoId)).map((c) => ({
1136
+ const allCandidates = rows(db, signals.operationPath, workspaceId).map((c) => ({
1102
1137
  ...c,
1103
1138
  score: 0.2,
1104
1139
  reasons: ["operation_path_match"]
1105
1140
  }));
1141
+ let candidates = allCandidates.filter((c) => matchesLocalRepo(db, c.operationId, signals.repoId));
1142
+ if (candidates.length === 0 && signals.repoId !== void 0 && signals.serviceName) {
1143
+ candidates = implementationContextCandidates(db, allCandidates, signals.repoId, signals.serviceName);
1144
+ if (candidates.length === 0)
1145
+ return {
1146
+ status: "unresolved",
1147
+ candidates: allCandidates.filter((c) => serviceMatches(c, signals.serviceName)),
1148
+ reasons: allCandidates.some((c) => serviceMatches(c, signals.serviceName)) ? ["local_service_candidate_without_caller_ownership"] : ["no_operation_candidates"]
1149
+ };
1150
+ }
1106
1151
  if (candidates.length === 0)
1107
1152
  return {
1108
1153
  status: "unresolved",
@@ -1173,6 +1218,34 @@ function resolveOperation(db, signals, workspaceId) {
1173
1218
  reasons: ["candidate_score_below_resolution_threshold"]
1174
1219
  };
1175
1220
  }
1221
+ function serviceMatches(candidate, serviceName) {
1222
+ if (!serviceName) return false;
1223
+ const simple = serviceName.split(".").at(-1) ?? serviceName;
1224
+ return candidate.qualifiedName === serviceName || candidate.serviceName === serviceName || candidate.serviceName === simple || candidate.servicePath === serviceName || candidate.servicePath === `/${serviceName}` || candidate.servicePath === `/${simple}` || candidate.servicePath.endsWith(`/${simple}`);
1225
+ }
1226
+ function implementationContextCandidates(db, candidates, callerRepoId, serviceName) {
1227
+ const matching = candidates.filter((candidate) => serviceMatches(candidate, serviceName));
1228
+ const owned = matching.map((candidate) => ownershipReason(db, candidate, callerRepoId)).filter((item) => Boolean(item));
1229
+ if (owned.length === 0) return [];
1230
+ const direct = owned.filter((item) => item.reason !== "caller_depends_on_model_package");
1231
+ const chosen = direct.length > 0 ? direct : owned.length === 1 ? owned : [];
1232
+ return chosen.map((item) => ({ ...item.candidate, score: 0.95, reasons: [...item.candidate.reasons, "implementation_context_caller_ownership", item.reason] }));
1233
+ }
1234
+ function ownershipReason(db, candidate, callerRepoId) {
1235
+ const edge = db.prepare("SELECT status,evidence_json,to_id 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 LIMIT 1").get(String(candidate.operationId));
1236
+ if (edge?.status === "resolved") {
1237
+ const row = db.prepare("SELECT hc.repo_id repoId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id WHERE hm.id=?").get(edge.to_id);
1238
+ if (row?.repoId === callerRepoId) return { candidate, reason: "resolved_implementation_handler_repo_matches_caller" };
1239
+ }
1240
+ if (edge?.evidence_json) {
1241
+ const evidence = JSON.parse(edge.evidence_json);
1242
+ const hit = evidence.candidates?.find((item) => item.accepted && (Number(item.handlerPackage?.id) === callerRepoId || Number(item.applicationPackage?.id) === callerRepoId));
1243
+ if (hit) return { candidate, reason: edge.status === "ambiguous" ? "ambiguous_implementation_candidate_repo_matches_caller" : "registration_package_matches_caller" };
1244
+ }
1245
+ const dep = db.prepare("SELECT 1 FROM graph_edges WHERE edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND status='resolved' AND from_kind='repo' AND from_id=? AND to_id=?").get(String(callerRepoId), String(candidate.repoId));
1246
+ if (dep) return { candidate, reason: "caller_depends_on_model_package" };
1247
+ return void 0;
1248
+ }
1176
1249
  function matchesLocalRepo(db, operationId, repoId) {
1177
1250
  if (repoId === void 0) return true;
1178
1251
  const row = db.prepare("SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?").get(operationId);
@@ -1227,8 +1300,8 @@ function linkWorkspace(db, workspaceId, vars = {}) {
1227
1300
  const generation = nextGraphGeneration(db, workspaceId);
1228
1301
  db.prepare("DELETE FROM graph_edges WHERE workspace_id=?").run(workspaceId);
1229
1302
  const deps = linkHelperPackages(db, workspaceId, generation);
1230
- const callSummary = linkCalls(db, workspaceId, vars, generation);
1231
1303
  const impl = linkImplementations(db, workspaceId, generation);
1304
+ const callSummary = linkCalls(db, workspaceId, vars, generation);
1232
1305
  db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
1233
1306
  return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount, implementationUnresolvedCount: impl.unresolvedCount };
1234
1307
  });
@@ -1265,6 +1338,10 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
1265
1338
  const isOperationCall = callType.startsWith("remote") || callType === "local_service_call";
1266
1339
  const resolution = isOperationCall ? resolveOperation(db, { servicePath, operationPath: op, serviceName: call.local_service_name, repoId: callType === "local_service_call" ? Number(call.repo_id) : void 0, alias: applyVariables(call.aliasExpr ?? call.alias, vars), destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 || callType === "local_service_call" }, workspaceId) : { status: "unresolved", candidates: [], reasons: [] };
1267
1340
  const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0);
1341
+ if (callType === "local_service_call" && call.unresolved_reason === "transport_client_method" && !resolution.target && resolution.candidates.length === 0) {
1342
+ db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "HANDLER_CALLS_EXTERNAL_HTTP", "terminal", "call", String(call.id), "external", String(op || "transport_client_method"), Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, classification: "transport_client_method" }), 0, generation);
1343
+ return { status: "terminal" };
1344
+ }
1268
1345
  if (resolution.target) {
1269
1346
  db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, callType === "local_service_call" ? "LOCAL_CALL_RESOLVES_TO_OPERATION" : "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify(evidence), isDynamic ? 1 : 0, generation);
1270
1347
  return { status: "resolved" };
@@ -1272,11 +1349,13 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
1272
1349
  const edgeType = callType === "local_db_query" ? "HANDLER_RUNS_DB_QUERY" : callType === "external_http" ? "HANDLER_CALLS_EXTERNAL_HTTP" : callType === "async_emit" ? "HANDLER_EMITS_EVENT" : callType === "async_subscribe" ? "EVENT_CONSUMED_BY_HANDLER" : resolution.status === "dynamic" ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE";
1273
1350
  const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
1274
1351
  const unresolvedReason = status === "terminal" ? null : String(call.unresolved_reason ?? (resolution.status === "ambiguous" ? "Ambiguous operation candidates require a strong service signal" : resolution.status === "dynamic" ? `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ["missing runtime variables"]).join(", ")}` : "No indexed target operation matched"));
1275
- 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, edgeType, status, "call", String(call.id), callType.startsWith("async_") ? "event" : "external", String(call.event_name_expr ?? call.query_entity ?? op ?? call.id), Number(call.confidence ?? 0.2), JSON.stringify(evidence), isDynamic || resolution.status === "dynamic" ? 1 : 0, unresolvedReason, generation);
1352
+ const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : "external";
1353
+ const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : String(call.event_name_expr ?? op ?? call.id);
1354
+ 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, edgeType, status, "call", String(call.id), targetKind, targetId, Number(call.confidence ?? 0.2), JSON.stringify(evidence), isDynamic || resolution.status === "dynamic" ? 1 : 0, unresolvedReason, generation);
1276
1355
  return { status };
1277
1356
  }
1278
1357
  function callEvidence(call, resolution, servicePath, op, destination) {
1279
- 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, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: call.alias_chain_json ? JSON.parse(String(call.alias_chain_json)) : void 0, transport: call.call_type === "local_service_call" ? "local" : void 0, 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 };
1358
+ return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, callId: call.id, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: call.alias_chain_json ? JSON.parse(String(call.alias_chain_json)) : void 0, transport: call.call_type === "local_service_call" ? "local" : void 0, 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 };
1280
1359
  }
1281
1360
  function linkImplementations(db, workspaceId, generation) {
1282
1361
  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);
@@ -1633,6 +1712,17 @@ function implementationScope(db, operationId) {
1633
1712
  const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?").get(edge.to_id);
1634
1713
  return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
1635
1714
  }
1715
+ function contextImplementationMethodId(edge, callerRepoId) {
1716
+ if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return void 0;
1717
+ const evidence = JSON.parse(String(edge.evidence_json || "{}"));
1718
+ const candidate = evidence.candidates?.find((item) => item.accepted && (Number(item.handlerPackage?.id) === callerRepoId || Number(item.applicationPackage?.id) === callerRepoId));
1719
+ return candidate?.methodId === void 0 ? void 0 : String(candidate.methodId);
1720
+ }
1721
+ function handlerScope(db, methodId) {
1722
+ const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?").get(methodId);
1723
+ if (!row) return void 0;
1724
+ return { repoId: row.repoId, files: new Set(row.sourceFile ? [row.sourceFile] : []), symbolId: row.symbolId };
1725
+ }
1636
1726
  function includeCall(type, options) {
1637
1727
  if (!options.includeDb && type === "local_db_query") return false;
1638
1728
  if (!options.includeExternal && type === "external_http") return false;
@@ -1716,6 +1806,7 @@ function edgeTarget(row, evidence) {
1716
1806
  const operationPath = typeof evidence.operationPath === "string" ? evidence.operationPath : void 0;
1717
1807
  const targetOperation = typeof evidence.targetOperation === "string" ? evidence.targetOperation : void 0;
1718
1808
  const targetRepo = typeof evidence.targetRepo === "string" ? evidence.targetRepo : "";
1809
+ if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return `Entity: ${row.to_id || "unknown"}`;
1719
1810
  return servicePath && operationPath ? `${servicePath}${operationPath}` : targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
1720
1811
  }
1721
1812
  function trace(db, start, options) {
@@ -1813,7 +1904,7 @@ function trace(db, start, options) {
1813
1904
  nodes.set(targetNode, opNode ?? {
1814
1905
  id: targetNode,
1815
1906
  kind: effectiveRow.to_kind,
1816
- label: effectiveRow.to_id
1907
+ label: effectiveRow.to_kind === "db_entity" ? `Entity: ${effectiveRow.to_id || "unknown"}` : effectiveRow.to_id
1817
1908
  });
1818
1909
  const to = edgeTarget(effectiveRow, evidence);
1819
1910
  edges.push({
@@ -1827,9 +1918,11 @@ function trace(db, start, options) {
1827
1918
  });
1828
1919
  if (effectiveRow.to_kind === "operation") {
1829
1920
  const implementation = implementationScope(db, effectiveRow.to_id);
1921
+ const contextMethodId = contextImplementationMethodId(implementation.edge, current.repoId);
1922
+ const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : void 0;
1830
1923
  if (implementation.edge) {
1831
1924
  const implEvidence = JSON.parse(String(implementation.edge.evidence_json || "{}"));
1832
- const handlerNode = implementation.edge.status === "resolved" ? handlerMethodNode(db, implementation.edge.to_id) : void 0;
1925
+ const handlerNode = implementation.edge.status === "resolved" ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
1833
1926
  const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
1834
1927
  if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
1835
1928
  edges.push({
@@ -1843,10 +1936,11 @@ function trace(db, start, options) {
1843
1936
  });
1844
1937
  }
1845
1938
  if (current.depth >= maxDepth) continue;
1846
- const files = implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id);
1847
- const symbolIds = implementation.symbolId ? /* @__PURE__ */ new Set([implementation.symbolId]) : void 0;
1848
- if (implementation.edge?.status === "resolved" && files.size > 0) {
1849
- const targetRepoId = implementation.repoId ?? db.prepare(
1939
+ const contextScope = contextMethodId ? handlerScope(db, contextMethodId) : void 0;
1940
+ const files = contextScope?.files ?? (implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id));
1941
+ const symbolIds = contextScope?.symbolId ? /* @__PURE__ */ new Set([contextScope.symbolId]) : implementation.symbolId ? /* @__PURE__ */ new Set([implementation.symbolId]) : void 0;
1942
+ if ((implementation.edge?.status === "resolved" || contextScope) && files.size > 0) {
1943
+ const targetRepoId = contextScope?.repoId ?? implementation.repoId ?? db.prepare(
1850
1944
  "SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
1851
1945
  ).get(effectiveRow.to_id)?.repoId;
1852
1946
  const nextKey = `${targetRepoId ?? "*"}:${[...symbolIds ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${[...files].sort().join(",")}`;
@@ -1893,4 +1987,4 @@ export {
1893
1987
  linkWorkspace,
1894
1988
  trace
1895
1989
  };
1896
- //# sourceMappingURL=chunk-MXYYARUP.js.map
1990
+ //# sourceMappingURL=chunk-UPXFMLUY.js.map