@saptools/service-flow 0.1.32 → 0.1.34

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.34
4
+
5
+ - Prefer indexed CDS operation evidence over heuristic remote-entity classification for service-client operation invocations, while keeping true collection, entity, delete, navigation, and media paths terminal.
6
+ - Added strict doctor collision diagnostics for terminal remote entity edges that look like operation invocations with indexed operation candidates.
7
+ - Persist repository fact analyzer versions and warn during link/strict doctor when force reindex is required after an analyzer upgrade.
8
+
9
+ ## 0.1.33
10
+
11
+ - Preserved persisted graph decisions and call-site evidence during trace and graph rendering while keeping contextual runtime resolution as enrichment.
12
+ - Classified OData entity reads, mutations, deletes, navigation, media streams, and uppercase entity candidates as terminal remote entity edges instead of unresolved operation candidates.
13
+ - Kept dynamic external HTTP destinations dynamic with stable synthetic ids, neutral labels, bounded safe candidates, and sanitized URL evidence.
14
+ - Bumped the SQLite schema capability to version 7 and added strict doctor diagnostics for legacy schema drift and reindex-required external metadata.
15
+ - Standardized terminal trace-start diagnostics so non-traversable starts return zero graph nodes and edges by default.
16
+
3
17
  ## 0.1.32
4
18
 
5
19
  - Hardened operation-first trace starts to fail closed on ambiguous, rejected, or non-executable implementation evidence.
package/README.md CHANGED
@@ -68,6 +68,10 @@ npm install @saptools/service-flow
68
68
  - Doctor treats a `running` index run as abandoned only after 60 minutes and includes the run id/start time. Active short-lived concurrent runs are not default warnings.
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
+ - Persisted graph rows take precedence during `trace` and `graph`: resolved call edges keep their `graph_edges.id`, `outbound_calls.id`, call-site file/line, outbound parser evidence, linker status/reason, and selected target evidence. Contextual runtime resolution can enrich evidence but does not replace an already resolved persisted target. Terminal start diagnostics such as ambiguous operations or rejected implementations return zero nodes and zero edges by default; candidates remain in structured diagnostics for automation.
72
+ - OData entity paths are conservative terminal remote entity edges. Reads, mutations, deletes, navigation paths, media-stream paths such as `/Documents(ID)/content`, and uppercase unknown entity-set candidates do not inflate unresolved operation counts. Lowercase action/function-style paths remain eligible for indexed operation resolution.
73
+ - External HTTP destinations are static only when a safe literal or local const literal proves the value. Identifier, property-read, function-call, and arbitrary destination expressions are dynamic with stable `destination:dynamic:<hash>` ids and neutral labels; conditional literal branches expose only safe candidate names.
74
+ - Schema version 7 uses an explicit reindex-required upgrade policy for legacy schema drift. `doctor --strict` reports `schema_legacy_columns_present`, `external_target_columns_missing_data`, and `reindex_required_after_upgrade` with remediation when relink alone cannot make an upgraded database equivalent to a fresh index.
71
75
 
72
76
 
73
77
  ## 🚀 Quick Start
package/TECHNICAL-NOTE.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # Service Flow Resolution Notes
2
2
 
3
3
 
4
+ ## 0.1.33 trace, entity, destination, and upgrade notes
5
+
6
+ - Trace and graph rendering use persisted resolved graph rows as authoritative base edges. Runtime/contextual resolution may add evidence such as substitutions or binding propagation, but persisted graph edge ids, outbound call ids, call-site file/line, parser evidence, linker status, selected target id, and target evidence remain present in effective edges.
7
+ - The trace-start machine contract is fail-closed: terminal start diagnostics produce zero graph nodes and zero graph edges by default. Candidate operations, implementation candidates, rejected edges, and selected ids are diagnostics-only evidence.
8
+ - Service-client OData entity paths are separated from action/function invocations. Entity reads, mutations, deletes, navigation, media stream calls, and uppercase entity-set candidates become terminal remote entity graph edges; lowercase operation-looking paths still go through operation resolution when indexed evidence exists.
9
+ - External HTTP destination extraction uses conservative static evaluation. Literals and safe local const literals are static; all other expressions stay dynamic unless a conditional has all-static branches, in which case only the candidate literals and sanitized expression shape are persisted.
10
+ - Schema version 7 follows an explicit reindex-required upgrade policy. Legacy external-target columns on `symbols` or missing queryable external metadata are strict doctor warnings with rebuild/reindex remediation instead of silent relink-only drift.
11
+
4
12
  ## 0.1.31 selector and external target notes
5
13
 
6
14
  Operation trace selectors are resolved from CDS operation facts before handler-source fallback. The start resolver scopes by repository and service path, requires disambiguation when multiple repositories or services match, and only queues traversal from a resolved implementation edge. Generated operation decorator constants are normalized in `operation-decorator-normalizer` so linker, trace fallback, diagnostics, and tests share one conservative implementation.
@@ -559,16 +559,27 @@ function classifyODataPathIntent(path9, method) {
559
559
  const placeholderKeys = [...new Set(extractTemplatePlaceholders(rawPath))];
560
560
  const base = { rawPath, method: normalizedMethod, pathWithoutQuery, queryString, hasQueryString: queryIndex >= 0, entitySegment, placeholderKeys };
561
561
  if (!rawPath || !rawPath.startsWith("/")) return { ...base, kind: "unknown", reason: "path_missing_or_not_absolute" };
562
- if (normalizedMethod !== "GET") return { ...base, kind: "operation_invocation", reason: "non_get_service_send_defaults_to_operation" };
562
+ const upperEntityLike = /^[A-Z][A-Za-z0-9_]*$/.test(entitySegment ?? firstSegment);
563
+ const mediaLike = ["content", "$value"].includes((segments.at(-1) ?? "").toLowerCase());
564
+ const invocation = normalizeODataOperationInvocationPath(pathWithoutQuery);
565
+ if (normalizedMethod !== "GET") {
566
+ if (invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "operation_invocation", reason: "non_get_balanced_top_level_operation_invocation" };
567
+ if (mediaLike) return { ...base, kind: "entity_media", reason: "non_get_entity_media_stream_path" };
568
+ if (hasNavigationSegments || firstSegment.includes("(")) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: "non_get_entity_path_shape" };
569
+ if (upperEntityLike) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: "non_get_entity_path_shape" };
570
+ return { ...base, kind: "operation_invocation", reason: "non_get_lowercase_path_may_be_operation" };
571
+ }
563
572
  if (queryIndex >= 0) {
564
573
  if (hasNavigationSegments) return { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_and_query_string" };
565
574
  if (looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "unknown", reason: "get_invocation_with_query_string_requires_indexed_operation_evidence" };
566
575
  return { ...base, kind: "entity_query", reason: "get_collection_path_has_query_string" };
567
576
  }
568
- if (hasNavigationSegments) return { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_segments" };
577
+ if (hasNavigationSegments) return mediaLike ? { ...base, kind: "entity_media", reason: "get_entity_media_stream_path" } : { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_segments" };
569
578
  if (firstSegment.includes("(")) {
579
+ if (invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "operation_invocation", reason: "get_balanced_top_level_operation_invocation" };
570
580
  return looksLikeLowerCamelInvocation(firstSegment) ? { ...base, kind: "operation_invocation", reason: "get_single_lower_camel_segment_has_top_level_invocation" } : { ...base, kind: "entity_key_read", reason: "get_entity_segment_has_key_predicate" };
571
581
  }
582
+ if (/^[A-Z][A-Za-z0-9_]*$/.test(firstSegment)) return { ...base, kind: "entity_candidate", reason: "uppercase_collection_segment_without_indexed_entity_evidence" };
572
583
  return { ...base, kind: "unknown", reason: "get_path_has_no_query_key_or_navigation_signal" };
573
584
  }
574
585
  function entitySegmentFromPath(path9) {
@@ -748,6 +759,11 @@ function externalHttpTarget(call) {
748
759
  const method = typeof call.method === "string" ? call.method : typeof target.method === "string" ? target.method : void 0;
749
760
  const kind = typeof target.kind === "string" ? target.kind : "unknown";
750
761
  const expression = typeof target.expression === "string" ? target.expression : void 0;
762
+ if (kind === "destination" && target.dynamic === true) {
763
+ const shape = typeof target.expressionShape === "string" ? target.expressionShape : "expression";
764
+ const candidates = Array.isArray(target.candidateLiterals) ? target.candidateLiterals.filter((item) => typeof item === "string") : [];
765
+ return { kind, toKind: "external_destination", toId: `destination:dynamic:${hash(`${shape}:${candidates.join("|")}`)}`, label: "External destination: dynamic destination", method, dynamic: true, expression: candidates.length ? `candidates:${candidates.join("|")}` : `shape:${shape}` };
766
+ }
751
767
  if (kind === "destination" && expression) return { kind, toKind: "external_destination", toId: `destination:${expression}`, label: `External destination: ${expression}`, method, dynamic: false, expression };
752
768
  if (kind === "static_url" && expression) {
753
769
  const redacted = redactUrl(expression);
@@ -850,6 +866,24 @@ function staticExpressionText(expr, initializers) {
850
866
  if (ts4.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
851
867
  return void 0;
852
868
  }
869
+ function destinationExpressionShape(expr) {
870
+ if (!expr) return void 0;
871
+ if (ts4.isIdentifier(expr)) return "identifier";
872
+ if (ts4.isPropertyAccessExpression(expr) || ts4.isElementAccessExpression(expr)) return "property_read";
873
+ if (ts4.isCallExpression(expr)) return "function_call";
874
+ if (ts4.isConditionalExpression(expr)) return "conditional";
875
+ if (ts4.isBinaryExpression(expr)) return "binary_expression";
876
+ if (ts4.isTemplateExpression(expr)) return "template_expression";
877
+ return ts4.SyntaxKind[expr.kind] ?? "expression";
878
+ }
879
+ function staticConditionalCandidates(expr, initializers) {
880
+ const resolved2 = expr && ts4.isIdentifier(expr) && initializers.has(expr.text) ? initializers.get(expr.text) : expr;
881
+ if (!resolved2 || !ts4.isConditionalExpression(resolved2)) return void 0;
882
+ const left = staticExpressionText(resolved2.whenTrue, initializers);
883
+ const right = staticExpressionText(resolved2.whenFalse, initializers);
884
+ if (!left || !right) return void 0;
885
+ return [.../* @__PURE__ */ new Set([left, right])];
886
+ }
853
887
  function propertyInitializer(object, key) {
854
888
  for (const property of object.properties) {
855
889
  if (ts4.isPropertyAssignment(property) && nameOfProperty(property.name) === key) return property.initializer;
@@ -870,7 +904,10 @@ function urlTargetFromExpression(expr, initializers) {
870
904
  function destinationTargetFromExpression(expr, initializers) {
871
905
  const text = staticExpressionText(expr, initializers);
872
906
  if (text) return { kind: "destination", expression: text, dynamic: false };
873
- if (expr && ts4.isIdentifier(expr)) return { kind: "destination", expression: expr.text, dynamic: true };
907
+ const candidates = staticConditionalCandidates(expr, initializers);
908
+ if (candidates) return { kind: "destination", dynamic: true, expressionShape: "conditional", candidateLiterals: candidates };
909
+ const shape = destinationExpressionShape(expr);
910
+ if (shape) return { kind: "destination", dynamic: true, expressionShape: shape };
874
911
  return void 0;
875
912
  }
876
913
  function externalHttpEvidence(node, source, initializers) {
@@ -1018,8 +1055,10 @@ function classifyOutboundCallsInSource(source, filePath) {
1018
1055
  const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
1019
1056
  const operationPathExpr = op && !shorthandPath ? `/${stripQuotes(op).replace(/^\//, "")}` : void 0;
1020
1057
  const intent = classifyODataPathIntent(operationPathExpr, method);
1058
+ const entityCallTypes = { entity_mutation: "remote_entity_mutation", entity_delete: "remote_entity_delete", entity_media: "remote_entity_media", entity_candidate: "remote_entity_candidate" };
1059
+ const entityCallType = entityCallTypes[intent.kind];
1021
1060
  const isODataQueryRead = method.toUpperCase() === "GET" && ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
1022
- add(node, { callType: query || isODataQueryRead ? "remote_query" : "remote_action", serviceVariableName: receiver, method, operationPathExpr, queryEntity: query ? extractQueryEntity(query) : isODataQueryRead ? intent.entitySegment : 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, odataPathIntent: operationPathExpr ? intent : void 0, parserWarning: shorthandPath ? "dynamic_operation_path_identifier" : void 0 });
1061
+ add(node, { callType: query ? "remote_query" : entityCallType ?? (isODataQueryRead ? "remote_query" : "remote_action"), serviceVariableName: receiver, method, operationPathExpr, queryEntity: query ? extractQueryEntity(query) : isODataQueryRead ? intent.entitySegment : 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, odataPathIntent: operationPathExpr ? intent : void 0, parserWarning: shorthandPath ? "dynamic_operation_path_identifier" : void 0 });
1023
1062
  }
1024
1063
  } else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts4.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
1025
1064
  const wrapperName = ts4.isIdentifier(expr) ? expr.text : ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) ? expr.expression.text : "";
@@ -2066,9 +2105,26 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
2066
2105
  const servicePath = applyVariables(call.servicePathExpr ?? call.requireServicePath, vars);
2067
2106
  const destination = call.destinationExpr ?? call.requireDestination;
2068
2107
  const isDynamic = Boolean(Number(call.isDynamic ?? 0));
2069
- const isOperationCall = callType === "remote_action" || callType === "local_service_call" || callType === "remote_query" && Boolean(op);
2108
+ const isRemoteEntityCall = callType.startsWith("remote_entity_");
2109
+ const operationLikeRemoteEntity = isRemoteEntityCall && Boolean(op) && !["entity_media", "entity_delete", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
2110
+ const isOperationCall = operationLikeRemoteEntity || (callType === "remote_action" || callType === "local_service_call" || callType === "remote_query" && Boolean(op));
2070
2111
  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: [] };
2071
2112
  const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0, normalized, intent);
2113
+ if (isRemoteEntityCall && (resolution.target || resolution.candidates.length > 0 || resolution.status === "dynamic")) {
2114
+ if (resolution.target) {
2115
+ 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, "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify({ ...evidence, operationEntityPrecedence: "indexed_operation_over_parser_entity" }), 0, generation);
2116
+ return { status: "resolved", callType };
2117
+ }
2118
+ const status2 = resolution.status === "dynamic" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : "unresolved";
2119
+ 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, status2 === "dynamic" ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE", status2, "call", String(call.id), "operation_candidate", op ? `Remote action: ${op}` : "Remote action: unknown path", Number(call.confidence ?? 0.2), JSON.stringify({ ...evidence, operationEntityPrecedence: "parser_entity_with_indexed_operation_candidates" }), status2 === "dynamic" ? 1 : 0, unresolvedOperationReason(resolution), generation);
2120
+ return { status: status2, callType };
2121
+ }
2122
+ if (isRemoteEntityCall) {
2123
+ const target = buildRemoteQueryTarget({ queryEntity: intent.entitySegment ?? call.query_entity, servicePath, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, parserWarning: evidence.parserWarning });
2124
+ const entityKind = callType.replace("remote_entity_", "remote_entity_");
2125
+ 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_ACCESSES_REMOTE_ENTITY", "terminal", "call", String(call.id), target.toKind, target.toId, Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, ...target.evidence, remoteEntityAccess: entityKind }), 0, generation);
2126
+ return { status: "terminal", callType };
2127
+ }
2072
2128
  if (callType === "remote_query" && (isEntityQueryIntent || !op) && !resolution.target) {
2073
2129
  const target = buildRemoteQueryTarget({ queryEntity: call.query_entity, servicePath, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, parserWarning: evidence.parserWarning });
2074
2130
  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_RUNS_REMOTE_QUERY", "terminal", "call", String(call.id), target.toKind, target.toId, Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, ...target.evidence }), 0, generation);
@@ -2673,7 +2729,7 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
2673
2729
  });
2674
2730
  return next;
2675
2731
  }
2676
- function contextualRuntimeResolution(db, call, binding, workspaceId) {
2732
+ function contextualRuntimeResolution(db, call, binding, workspaceId, persistedRows = []) {
2677
2733
  if (!binding || String(call.call_type) !== "remote_action" || call.operation_path_expr === void 0 || call.operation_path_expr === null) return {};
2678
2734
  const normalized = normalizeODataOperationInvocationPath(String(call.operation_path_expr));
2679
2735
  const op = normalized?.normalizedOperationPath ?? (String(call.operation_path_expr).startsWith("/") ? String(call.operation_path_expr) : `/${String(call.operation_path_expr)}`);
@@ -2683,6 +2739,8 @@ function contextualRuntimeResolution(db, call, binding, workspaceId) {
2683
2739
  const evidence = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine, alias: binding.alias, aliasExpr: binding.aliasExpr, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination }, operationPath: op, rawOperationPath: normalized?.rawOperationPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : void 0, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: resolution.candidates.length, candidates: resolution.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
2684
2740
  if (!resolution.target) return { evidence, unresolvedReason: resolution.status === "ambiguous" ? "Ambiguous contextual operation candidates" : resolution.status === "dynamic" ? `Dynamic contextual target is missing runtime variables: ${resolution.reasons.join(", ")}` : "No contextual operation candidate matched" };
2685
2741
  const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
2742
+ const persistedResolved = persistedRows.find((item) => item.status === "resolved");
2743
+ if (persistedResolved) return { row: void 0, evidence: { ...resolvedEvidence, contextualPreservedPersistedResolvedEdge: true }, unresolvedReason: void 0 };
2686
2744
  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(resolvedEvidence), status: "resolved" }, evidence: resolvedEvidence, unresolvedReason: void 0 };
2687
2745
  }
2688
2746
  function edgeTarget(row, evidence) {
@@ -2724,7 +2782,7 @@ function trace(db, start, options) {
2724
2782
  const nodes = /* @__PURE__ */ new Map();
2725
2783
  const seenEdges = /* @__PURE__ */ new Set();
2726
2784
  const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1, context: /* @__PURE__ */ new Map() }] : [];
2727
- if (scope.startOperationId) {
2785
+ if (scope.startOperationId && scope.selectorMatched) {
2728
2786
  const op = operationNode(db, scope.startOperationId);
2729
2787
  const impl = implementationScope(db, scope.startOperationId);
2730
2788
  if (op) nodes.set(String(op.id), op);
@@ -2781,15 +2839,16 @@ function trace(db, start, options) {
2781
2839
  line: call.source_line,
2782
2840
  callType: call.call_type
2783
2841
  });
2784
- const contextual = contextualRuntimeResolution(db, call, callerBindings.get(receiverFromEvidence(call.evidence_json) ?? ""), workspaceIdForCall(db, String(call.id)));
2785
- const graphRows = contextual.row ? [contextual.row] : graph.get(Number(call.id)) ?? [];
2842
+ const persistedRowsForCall = graph.get(Number(call.id)) ?? [];
2843
+ const contextual = contextualRuntimeResolution(db, call, callerBindings.get(receiverFromEvidence(call.evidence_json) ?? ""), workspaceIdForCall(db, String(call.id)), persistedRowsForCall);
2844
+ const graphRows = contextual.row ? [contextual.row] : persistedRowsForCall;
2786
2845
  for (const row of graphRows) {
2787
2846
  if (seenEdges.has(Number(row.id))) continue;
2788
2847
  seenEdges.add(Number(row.id));
2789
2848
  const persistedEvidence = JSON.parse(
2790
2849
  String(row.evidence_json || "{}")
2791
2850
  );
2792
- const rawEvidence = contextual.evidence ? { ...persistedEvidence, ...contextual.evidence } : persistedEvidence;
2851
+ const rawEvidence = { ...persistedEvidence, ...contextual.evidence ?? {}, graphEdgeId: row.id, persistedGraphEdgeId: row.id > 0 ? row.id : void 0, outboundCallId: call.id, callSite: { sourceFile: call.source_file, sourceLine: call.source_line }, sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, linker: { status: row.status, confidence: row.confidence, reason: row.unresolved_reason, edgeType: row.edge_type }, persistedTarget: { kind: row.to_kind, id: row.to_id }, contextualResolutionParticipated: Boolean(contextual.evidence?.contextualServiceBindingAttempted) };
2793
2852
  const effective = runtimeResolution(db, row, rawEvidence, options.vars);
2794
2853
  const evidence = effective.evidence;
2795
2854
  const effectiveRow = effective.row;
@@ -2884,4 +2943,4 @@ export {
2884
2943
  linkWorkspace,
2885
2944
  trace
2886
2945
  };
2887
- //# sourceMappingURL=chunk-VT5FVMOA.js.map
2946
+ //# sourceMappingURL=chunk-5SR4SFSU.js.map