@saptools/service-flow 0.1.23 → 0.1.25

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.25
4
+
5
+ - Make class-instance symbol-call indexing conservative so only same-file and relatively imported helper classes are traced, while built-in collection, date, URL, error, typed-array, promise, and abort-controller instances are ignored.
6
+ - Persist identifier and one-level destructured object parameter metadata for executable symbols, including class methods, so contextual service-client binding propagation can map helper arguments to callee receiver names.
7
+ - Propagate contextual service bindings through positional helper arguments and destructured object helper parameters with auditable caller and callee evidence.
8
+ - Refine unresolved operation diagnostics when indexed candidates exist but service context is absent or the resolution score is below threshold.
9
+
10
+ ## 0.1.24
11
+
12
+ - Add conservative class instance method symbol-call resolution for same-file and relatively imported helper classes, with auditable class-instance evidence.
13
+ - Propagate service-client binding context through resolved local symbol calls for explicit positional and object-literal helper arguments during trace rendering.
14
+ - Harden one-hop higher-order send wrapper literal path propagation for returned async closures and expose wrapper definition evidence.
15
+ - Refine strict doctor categories for no-binding remote actions and split ambiguous versus unresolved implementation diagnostics with capped examples.
16
+
17
+ ## 0.1.23
18
+
19
+ - Retrospective note: preserve same-file late-assignment service binding extraction for connected clients, `cds.connect.to(...)` assignments, identity aliases, and simple transaction aliases before outbound `send(...)` calls.
20
+ - Retrospective note: preserve explicit object destructuring from safe helper-returned client objects so binding rows remain available for downstream remote action linking.
21
+
3
22
  ## 0.1.22
4
23
 
5
24
  - Propagate conservative same-file identity aliases of connected service clients, including typed, `as`, `satisfies`, helper-returned, and transitive aliases, while preserving `.tx()` alias evidence.
@@ -642,28 +642,25 @@ function collectWrapperSpecs(source) {
642
642
  const specs = /* @__PURE__ */ new Map();
643
643
  const scanFunction = (name, fn) => {
644
644
  const params = fn.parameters.map((param) => ts4.isIdentifier(param.name) ? param.name.text : void 0);
645
- let found;
645
+ const closure = returnedClosure(fn) ?? fn;
646
+ const sends = [];
646
647
  const visit = (node) => {
647
- if (found) return;
648
- if (node !== fn && (ts4.isFunctionDeclaration(node) || ts4.isArrowFunction(node) || ts4.isFunctionExpression(node))) {
649
- ts4.forEachChild(node, visit);
650
- return;
651
- }
652
648
  if (ts4.isCallExpression(node) && ts4.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send" && ts4.isIdentifier(node.expression.expression)) {
653
649
  const objectArg = node.arguments[0];
654
650
  if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
655
651
  const pathProp = objectArg.properties.find((property) => ts4.isShorthandPropertyAssignment(property) && property.name.text === "path");
656
- if (pathProp) found = { client: node.expression.expression.text, path: pathProp.name.text, method: objectPropertyText(objectArg, "method") };
652
+ if (pathProp) sends.push({ client: node.expression.expression.text, path: pathProp.name.text, method: objectPropertyText(objectArg, "method") });
657
653
  }
658
654
  }
659
655
  ts4.forEachChild(node, visit);
660
656
  };
661
- visit(fn);
662
- if (!found) return;
657
+ visit(closure);
658
+ if (sends.length !== 1) return;
659
+ const found = sends[0];
663
660
  const clientIndex = params.indexOf(found.client);
664
661
  const pathIndex = params.indexOf(found.path);
665
662
  const methodIndex = found.method && params.includes(found.method) ? params.indexOf(found.method) : void 0;
666
- if (clientIndex >= 0 && pathIndex >= 0) specs.set(name, { clientIndex, pathIndex, methodIndex });
663
+ if (clientIndex >= 0 && pathIndex >= 0) specs.set(name, { clientIndex, pathIndex, methodIndex, definitionLine: lineOf3(source.text, fn.getStart(source)) });
667
664
  };
668
665
  for (const stmt of source.statements) {
669
666
  if (ts4.isFunctionDeclaration(stmt) && stmt.name) scanFunction(stmt.name.text, stmt);
@@ -673,6 +670,16 @@ function collectWrapperSpecs(source) {
673
670
  }
674
671
  return specs;
675
672
  }
673
+ function returnedClosure(fn) {
674
+ const body = fn.body;
675
+ if (!body) return void 0;
676
+ if (ts4.isArrowFunction(fn) && !ts4.isBlock(body)) return ts4.isArrowFunction(body) || ts4.isFunctionExpression(body) ? body : void 0;
677
+ if (!ts4.isBlock(body)) return void 0;
678
+ const returns = body.statements.filter(ts4.isReturnStatement);
679
+ if (returns.length !== 1) return void 0;
680
+ const expr = returns[0]?.expression;
681
+ return expr && (ts4.isArrowFunction(expr) || ts4.isFunctionExpression(expr)) ? expr : void 0;
682
+ }
676
683
  function classifyOutboundCallsInSource(source, filePath) {
677
684
  const calls = [];
678
685
  const sourceFile = normalizePath(filePath);
@@ -691,22 +698,24 @@ function classifyOutboundCallsInSource(source, filePath) {
691
698
  const entity = arg ? queryEntityFromAst(arg, initializers) : void 0;
692
699
  const payload = arg?.getText(source) ?? "";
693
700
  add(node, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) });
694
- } else if (ts4.isPropertyAccessExpression(expr) && expr.name.text === "send" && ts4.isIdentifier(expr.expression)) {
701
+ } else if (ts4.isPropertyAccessExpression(expr) && expr.name.text === "send" && (ts4.isIdentifier(expr.expression) || ts4.isPropertyAccessExpression(expr.expression))) {
695
702
  const objectArg = node.arguments[0];
696
703
  if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
697
- const receiver = expr.expression.text;
704
+ const receiver = receiverName(expr.expression);
698
705
  const query = objectPropertyText(objectArg, "query");
699
706
  const op = objectPropertyText(objectArg, "path") ?? objectPropertyText(objectArg, "event");
700
707
  const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
701
708
  add(node, { callType: query ? "remote_query" : "remote_action", serviceVariableName: receiver, method: stripQuotes(objectPropertyText(objectArg, "method") ?? "POST"), operationPathExpr: op && !shorthandPath ? `/${stripQuotes(op).replace(/^\//, "")}` : void 0, queryEntity: query ? extractQueryEntity(query) : void 0, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason: !query && shorthandPath ? "dynamic_operation_path_identifier" : void 0 }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, parserWarning: shorthandPath ? "dynamic_operation_path_identifier" : void 0 });
702
709
  }
703
- } else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text)) {
704
- const spec = wrapperSpecs.get(expr.expression.text);
705
- const clientArg = spec ? expr.arguments[spec.clientIndex] : void 0;
706
- const pathArg = spec ? expr.arguments[spec.pathIndex] : void 0;
707
- const methodArg = spec?.methodIndex === void 0 ? void 0 : expr.arguments[spec.methodIndex];
710
+ } else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts4.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
711
+ const wrapperName = ts4.isIdentifier(expr) ? expr.text : ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) ? expr.expression.text : "";
712
+ const wrapperArgs = ts4.isIdentifier(expr) ? node.arguments : ts4.isCallExpression(expr) ? expr.arguments : node.arguments;
713
+ const spec = wrapperSpecs.get(wrapperName);
714
+ const clientArg = spec ? wrapperArgs[spec.clientIndex] : void 0;
715
+ const pathArg = spec ? wrapperArgs[spec.pathIndex] : void 0;
716
+ const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
708
717
  if (spec && clientArg && ts4.isIdentifier(clientArg) && isStringLike(pathArg)) {
709
- add(node, { callType: "remote_action", serviceVariableName: clientArg.text, method: stripQuotes(methodArg?.getText(source) ?? "POST"), operationPathExpr: `/${pathArg.text.replace(/^\//, "")}`, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75 }, { receiver: clientArg.text, classifier: "local_wrapper_literal_path", wrapperFunction: expr.expression.text, literalCallerArgumentDetected: true });
718
+ add(node, { callType: "remote_action", serviceVariableName: clientArg.text, method: stripQuotes(methodArg?.getText(source) ?? "POST"), operationPathExpr: `/${pathArg.text.replace(/^\//, "")}`, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75 }, { receiver: clientArg.text, classifier: "higher_order_wrapper_literal_path", wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, literalCallerArgumentDetected: true });
710
719
  }
711
720
  } else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
712
721
  const receiver = receiverName(expr.expression);
@@ -1758,13 +1767,21 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
1758
1767
  }
1759
1768
  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";
1760
1769
  const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
1761
- 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"));
1770
+ const unresolvedReason = status === "terminal" ? null : String(call.unresolved_reason ?? unresolvedOperationReason(resolution));
1762
1771
  const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : "external";
1763
1772
  const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : callType === "remote_action" ? op ? `Remote action: ${op}` : call.unresolved_reason === "dynamic_operation_path_identifier" ? "Remote action: dynamic path" : "Remote action: unknown path" : String(call.event_name_expr ?? op ?? call.id);
1764
1773
  const graphLevelDynamic = edgeType === "DYNAMIC_EDGE_CANDIDATE" && resolution.status === "dynamic";
1765
1774
  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), graphLevelDynamic ? 1 : 0, unresolvedReason, generation);
1766
1775
  return { status, callType };
1767
1776
  }
1777
+ function unresolvedOperationReason(resolution) {
1778
+ if (resolution.status === "dynamic") return `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ["missing runtime variables"]).join(", ")}`;
1779
+ if (resolution.candidates.length === 0) return "No indexed target operation matched";
1780
+ if (resolution.reasons.includes("operation_path_only_has_no_strong_target_signal")) return "Operation candidates found but no strong service signal is available";
1781
+ if (resolution.reasons.includes("candidate_score_below_resolution_threshold")) return "Operation candidates found but resolution score is below threshold";
1782
+ if (resolution.status === "ambiguous") return "Ambiguous operation candidates require a strong service signal";
1783
+ return "Operation candidates found but resolution could not select a target";
1784
+ }
1768
1785
  function parseJson(value) {
1769
1786
  if (!value) return void 0;
1770
1787
  try {
@@ -2237,6 +2254,84 @@ function runtimeResolution(db, row, evidence, vars) {
2237
2254
  const unresolvedReason = resolution.status === "dynamic" ? `Dynamic target is missing runtime variables: ${resolution.reasons.join(", ")}` : resolution.status === "ambiguous" ? "Ambiguous runtime operation candidates" : "No runtime operation candidate matched substituted service and operation path";
2238
2255
  return { row, evidence: nextEvidence, unresolvedReason };
2239
2256
  }
2257
+ function parseEvidence(value) {
2258
+ try {
2259
+ const parsed = JSON.parse(String(value || "{}"));
2260
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
2261
+ } catch {
2262
+ return {};
2263
+ }
2264
+ }
2265
+ function receiverFromEvidence(value) {
2266
+ const evidence = parseEvidence(value);
2267
+ return typeof evidence.receiver === "string" ? evidence.receiver : void 0;
2268
+ }
2269
+ function knownBindingsForCalls(db, calls) {
2270
+ const map = /* @__PURE__ */ new Map();
2271
+ for (const call of calls) {
2272
+ const receiver = receiverFromEvidence(call.evidence_json);
2273
+ const bindingId = Number(call.service_binding_id ?? 0);
2274
+ if (!receiver || !bindingId) continue;
2275
+ const row = db.prepare("SELECT id,alias,alias_expr aliasExpr,destination_expr destinationExpr,service_path_expr servicePathExpr,source_file sourceFile,source_line sourceLine FROM service_bindings WHERE id=?").get(bindingId);
2276
+ if (row) map.set(receiver, { ...row, bindingId, source: "local_service_binding", calleeReceiver: receiver });
2277
+ }
2278
+ return map;
2279
+ }
2280
+ function knownBindingsForScope(db, repoId, symbolIds, files) {
2281
+ const map = /* @__PURE__ */ new Map();
2282
+ if (repoId === void 0) return map;
2283
+ const rows2 = db.prepare("SELECT id,variable_name variableName,alias,alias_expr aliasExpr,destination_expr destinationExpr,service_path_expr servicePathExpr,source_file sourceFile,source_line sourceLine FROM service_bindings WHERE repo_id=?").all(repoId);
2284
+ for (const row of rows2) {
2285
+ if (!row.variableName) continue;
2286
+ if (files && !files.has(String(row.sourceFile))) continue;
2287
+ if (symbolIds && symbolIds.size > 0) {
2288
+ const owner = db.prepare("SELECT id FROM symbols WHERE id IN (" + [...symbolIds].map(() => "?").join(",") + ") AND source_file=? AND start_line<=? AND end_line>=? LIMIT 1").get(...symbolIds, row.sourceFile, row.sourceLine, row.sourceLine);
2289
+ if (!owner) continue;
2290
+ }
2291
+ map.set(row.variableName, { ...row, bindingId: Number(row.id), source: "local_service_binding", calleeReceiver: row.variableName });
2292
+ }
2293
+ return map;
2294
+ }
2295
+ function contextForSymbolCall(db, symbolCall, callerBindings) {
2296
+ const next = /* @__PURE__ */ new Map();
2297
+ if (callerBindings.size === 0) return next;
2298
+ const callEvidence2 = parseEvidence(symbolCall.evidence_json);
2299
+ const callee = db.prepare("SELECT evidence_json evidenceJson FROM symbols WHERE id=?").get(symbolCall.callee_symbol_id);
2300
+ const calleeEvidence = parseEvidence(callee?.evidenceJson);
2301
+ const params = Array.isArray(calleeEvidence.parameters) ? calleeEvidence.parameters.filter((item) => typeof item === "string") : [];
2302
+ const parameterBindings = Array.isArray(calleeEvidence.parameterBindings) ? calleeEvidence.parameterBindings.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
2303
+ const args = Array.isArray(callEvidence2.callArguments) ? callEvidence2.callArguments : [];
2304
+ args.forEach((arg, index) => {
2305
+ const paramBinding = parameterBindings.find((binding) => binding.index === index);
2306
+ const param = paramBinding?.kind === "identifier" && typeof paramBinding.name === "string" ? paramBinding.name : params[index];
2307
+ if (arg.kind === "identifier" && typeof arg.name === "string") {
2308
+ const binding = callerBindings.get(arg.name);
2309
+ if (binding && param) next.set(param, { ...binding, source: "local_symbol_argument", callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
2310
+ }
2311
+ if (arg.kind === "object_literal" && Array.isArray(arg.properties)) {
2312
+ for (const prop of arg.properties) {
2313
+ if (typeof prop.property !== "string" || typeof prop.argument !== "string") continue;
2314
+ const binding = callerBindings.get(prop.argument);
2315
+ if (!binding) continue;
2316
+ const destructured = paramBinding?.kind === "object_pattern" && Array.isArray(paramBinding.properties) ? paramBinding.properties.find((item) => item.property === prop.property && typeof item.local === "string") : void 0;
2317
+ if (destructured && typeof destructured.local === "string") next.set(destructured.local, { ...binding, source: "local_symbol_destructured_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: String(index), calleeReceiver: destructured.local });
2318
+ else if (param) next.set(`${param}.${prop.property}`, { ...binding, source: "local_symbol_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
2319
+ }
2320
+ }
2321
+ });
2322
+ return next;
2323
+ }
2324
+ function contextualRuntimeResolution(db, call, binding, workspaceId) {
2325
+ if (!binding || String(call.call_type) !== "remote_action" || call.operation_path_expr === void 0 || call.operation_path_expr === null) return {};
2326
+ const op = normalizeODataOperationInvocationPathForTrace(String(call.operation_path_expr));
2327
+ const resolution = resolveOperation(db, { servicePath: binding.servicePathExpr, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination: binding.destinationExpr, hasExplicitOverride: true, isDynamic: false }, workspaceId);
2328
+ const evidence = { contextualServiceBindingSelected: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine }, operationPath: op, servicePath: binding.servicePathExpr, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination: binding.destinationExpr, contextualResolutionStatus: resolution.status, candidates: resolution.candidates, resolutionReasons: resolution.reasons };
2329
+ if (!resolution.target) return { evidence, unresolvedReason: resolution.status === "ambiguous" ? "Ambiguous contextual operation candidates" : "No contextual operation candidate matched" };
2330
+ return { row: { id: -Number(call.id), edge_type: "REMOTE_CALL_RESOLVES_TO_OPERATION", from_id: String(call.id), to_kind: "operation", to_id: String(resolution.target.operationId), confidence: resolution.target.score, evidence_json: JSON.stringify(evidence), status: "resolved" }, evidence: { ...evidence, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName }, unresolvedReason: void 0 };
2331
+ }
2332
+ function normalizeODataOperationInvocationPathForTrace(raw) {
2333
+ return raw.startsWith("/") ? raw : `/${raw}`;
2334
+ }
2240
2335
  function edgeTarget(row, evidence) {
2241
2336
  const runtimeCandidate = evidence.runtimeResolvedCandidate;
2242
2337
  if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath)
@@ -2269,7 +2364,7 @@ function trace(db, start, options) {
2269
2364
  const maxDepth = positiveDepth(options.depth);
2270
2365
  const edges = [];
2271
2366
  const nodes = /* @__PURE__ */ new Map();
2272
- const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1 }] : [];
2367
+ const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1, context: /* @__PURE__ */ new Map() }] : [];
2273
2368
  if (start.servicePath && (start.operation ?? start.operationPath)) {
2274
2369
  const startOperation = normalizeOperation(start.operation ?? start.operationPath);
2275
2370
  const startRows = db.prepare(`SELECT o.id operationId,r.name repoName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE (? IS NULL OR s.repo_id=?) AND s.service_path=? AND (o.operation_path=? OR o.operation_name=?) ORDER BY r.name,o.id LIMIT 2`).all(scope.repo?.id, scope.repo?.id, start.servicePath, startOperation, startOperation);
@@ -2302,6 +2397,7 @@ function trace(db, start, options) {
2302
2397
  const filtered = calls.filter(
2303
2398
  (c) => (!current.symbolIds || current.symbolIds.has(Number(c.source_symbol_id))) && (!current.files || current.files.has(String(c.source_file))) && includeCall(String(c.call_type), options)
2304
2399
  );
2400
+ const callerBindings = new Map([...current.context ?? /* @__PURE__ */ new Map(), ...knownBindingsForScope(db, current.repoId, current.symbolIds, current.files), ...knownBindingsForCalls(db, filtered)]);
2305
2401
  if (current.symbolIds && current.symbolIds.size > 0 && current.depth < maxDepth) {
2306
2402
  const symbolRows = db.prepare(`SELECT sc.*,s.repo_id calleeRepoId,s.source_file calleeFile FROM symbol_calls sc LEFT JOIN symbols s ON s.id=sc.callee_symbol_id WHERE sc.caller_symbol_id IN (${[...current.symbolIds].map(() => "?").join(",")}) ORDER BY sc.source_file,sc.source_line`).all(...current.symbolIds);
2307
2403
  for (const symbolCall of symbolRows) {
@@ -2315,7 +2411,7 @@ function trace(db, start, options) {
2315
2411
  const evidence = { ...JSON.parse(String(symbolCall.evidence_json || "{}")), sourceFile: symbolCall.source_file, sourceLine: symbolCall.source_line, calleeSymbolId: symbolCall.callee_symbol_id, calleeSymbolName: calleeNode?.symbolName, calleeSymbolFile: calleeNode?.sourceFile, resolutionStatus: symbolCall.status };
2316
2412
  edges.push({ step: current.depth, type: "local_symbol_call", from: String(symbolCall.callee_expression), to: calleeNode?.label ? String(calleeNode.label) : `symbol:${String(symbolCall.callee_symbol_id)}`, evidence, confidence: Number(symbolCall.confidence ?? 0.8), unresolvedReason: String(symbolCall.status) === "resolved" ? void 0 : symbolCall.unresolved_reason ? String(symbolCall.unresolved_reason) : void 0 });
2317
2413
  if (seenScopes.has(nextKey)) edges.push({ step: current.depth, type: "cycle", from: String(symbolCall.callee_expression), to: nextKey, evidence: { cycle: true, symbolCallId: symbolCall.id }, confidence: 1, unresolvedReason: "Cycle detected; downstream symbol already visited" });
2318
- else queue.push({ repoId: nextRepoId, files: nextFiles, symbolIds: nextSymbols, depth: current.depth + 1 });
2414
+ else queue.push({ repoId: nextRepoId, files: nextFiles, symbolIds: nextSymbols, depth: current.depth + 1, context: contextForSymbolCall(db, symbolCall, callerBindings) });
2319
2415
  }
2320
2416
  }
2321
2417
  const graph = graphForCalls(
@@ -2332,11 +2428,12 @@ function trace(db, start, options) {
2332
2428
  line: call.source_line,
2333
2429
  callType: call.call_type
2334
2430
  });
2335
- const graphRows = graph.get(Number(call.id)) ?? [];
2431
+ const contextual = contextualRuntimeResolution(db, call, (current.context ?? /* @__PURE__ */ new Map()).get(receiverFromEvidence(call.evidence_json) ?? ""), workspaceIdForCall(db, String(call.id)));
2432
+ const graphRows = contextual.row ? [contextual.row] : graph.get(Number(call.id)) ?? [];
2336
2433
  for (const row of graphRows) {
2337
2434
  if (seenEdges.has(Number(row.id))) continue;
2338
2435
  seenEdges.add(Number(row.id));
2339
- const rawEvidence = JSON.parse(
2436
+ const rawEvidence = contextual.evidence && row.id < 0 ? contextual.evidence : JSON.parse(
2340
2437
  String(row.evidence_json || "{}")
2341
2438
  );
2342
2439
  const effective = runtimeResolution(db, row, rawEvidence, options.vars);
@@ -2433,4 +2530,4 @@ export {
2433
2530
  linkWorkspace,
2434
2531
  trace
2435
2532
  };
2436
- //# sourceMappingURL=chunk-LCXDOSWL.js.map
2533
+ //# sourceMappingURL=chunk-HS746OHV.js.map