@saptools/service-flow 0.1.53 → 0.1.54

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.
@@ -0,0 +1,186 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import {
3
+ selectedHandlerProvenance,
4
+ type SelectedHandlerProvenance,
5
+ type SelectedHandlerSource,
6
+ } from '../linker/001-implementation-evidence-projection.js';
7
+
8
+ export interface HandlerMethodNode extends Record<string, unknown> {
9
+ id: string;
10
+ kind: 'handler_method';
11
+ label: string;
12
+ methodId: number;
13
+ methodName: string;
14
+ className: string;
15
+ sourceFile: string;
16
+ sourceLine: number;
17
+ repoId: number;
18
+ repoName: string;
19
+ packageName?: string;
20
+ }
21
+
22
+ export interface SelectedHandlerEvidence {
23
+ handler?: HandlerMethodNode;
24
+ evidence: Record<string, unknown>;
25
+ diagnostic?: Record<string, unknown>;
26
+ unresolvedReason?: string;
27
+ }
28
+
29
+ export function handlerMethodNode(
30
+ db: Db,
31
+ methodId: string,
32
+ ): HandlerMethodNode | undefined {
33
+ const row = db.prepare(`SELECT hm.id methodId,hm.method_name methodName,
34
+ hm.source_line sourceLine,hc.class_name className,hc.source_file sourceFile,
35
+ r.name repoName,r.id repoId,r.package_name packageName
36
+ FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id
37
+ JOIN repositories r ON r.id=hc.repo_id WHERE hm.id=?`).get(methodId) as
38
+ | Record<string, unknown>
39
+ | undefined;
40
+ return handlerNodeFromRow(row);
41
+ }
42
+
43
+ export function withSelectedHandlerProvenance(
44
+ evidence: Record<string, unknown>,
45
+ targetId: string,
46
+ handler: HandlerMethodNode | undefined,
47
+ ): SelectedHandlerEvidence {
48
+ if (!handler) return unavailableHandlerEvidence(evidence, targetId);
49
+ const selected = selectedHandlerProvenance(handlerSource(handler));
50
+ const stored = record(evidence.selectedHandler);
51
+ const mismatchedFields = storedSelectedHandlerMismatches(
52
+ stored, selected, targetId,
53
+ );
54
+ if (mismatchedFields.length === 0)
55
+ return { handler, evidence: { ...evidence, selectedHandler: selected } };
56
+ return {
57
+ handler,
58
+ evidence: {
59
+ ...evidence,
60
+ selectedHandler: selected,
61
+ selectedHandlerProvenanceAudit: {
62
+ status: 'mismatch',
63
+ graphTargetId: targetId,
64
+ mismatchedFields,
65
+ persistedSelectedHandler: stored,
66
+ },
67
+ },
68
+ diagnostic: {
69
+ severity: 'warning',
70
+ code: 'selected_handler_provenance_mismatch',
71
+ message: 'Persisted selected handler provenance did not match the resolved graph target',
72
+ graphTargetId: targetId,
73
+ mismatchedFields,
74
+ sourceFile: handler.sourceFile,
75
+ sourceLine: handler.sourceLine,
76
+ },
77
+ };
78
+ }
79
+
80
+ function unavailableHandlerEvidence(
81
+ evidence: Record<string, unknown>,
82
+ targetId: string,
83
+ ): SelectedHandlerEvidence {
84
+ return {
85
+ evidence: {
86
+ ...evidence,
87
+ selectedHandlerProvenanceAudit: {
88
+ status: 'unavailable',
89
+ graphTargetId: targetId,
90
+ reason: 'handler_target_not_indexed',
91
+ },
92
+ },
93
+ diagnostic: {
94
+ severity: 'warning',
95
+ code: 'selected_handler_target_not_found',
96
+ message: 'Resolved implementation target is not an indexed handler method',
97
+ graphTargetId: targetId,
98
+ },
99
+ unresolvedReason: 'Resolved implementation target is not an indexed handler method',
100
+ };
101
+ }
102
+
103
+ function handlerNodeFromRow(
104
+ row: Record<string, unknown> | undefined,
105
+ ): HandlerMethodNode | undefined {
106
+ const methodId = numberValue(row?.methodId);
107
+ const methodName = stringValue(row?.methodName);
108
+ const className = stringValue(row?.className);
109
+ const sourceFile = stringValue(row?.sourceFile);
110
+ const sourceLine = numberValue(row?.sourceLine);
111
+ const repoId = numberValue(row?.repoId);
112
+ const repoName = stringValue(row?.repoName);
113
+ if (methodId === undefined || !methodName || !className || !sourceFile
114
+ || sourceLine === undefined || repoId === undefined || !repoName)
115
+ return undefined;
116
+ return {
117
+ id: `handler_method:${methodId}`,
118
+ kind: 'handler_method',
119
+ label: `${repoName}:${className}.${methodName}`,
120
+ methodId,
121
+ methodName,
122
+ className,
123
+ sourceFile,
124
+ sourceLine,
125
+ repoId,
126
+ repoName,
127
+ packageName: stringValue(row?.packageName),
128
+ };
129
+ }
130
+
131
+ function handlerSource(node: HandlerMethodNode): SelectedHandlerSource {
132
+ return {
133
+ methodId: node.methodId,
134
+ methodName: node.methodName,
135
+ className: node.className,
136
+ repositoryId: node.repoId,
137
+ repositoryName: node.repoName,
138
+ repositoryPackageName: node.packageName,
139
+ sourceFile: node.sourceFile,
140
+ sourceLine: node.sourceLine,
141
+ };
142
+ }
143
+
144
+ function storedSelectedHandlerMismatches(
145
+ stored: Record<string, unknown>,
146
+ selected: SelectedHandlerProvenance,
147
+ targetId: string,
148
+ ): string[] {
149
+ if (Object.keys(stored).length === 0) return [];
150
+ const expectedRepository = record(stored.repository);
151
+ const selectedRepository = selected.repository ?? {};
152
+ return [
153
+ mismatch('graphTargetId', stored.graphTargetId, targetId),
154
+ mismatch('methodId', stored.methodId, selected.methodId),
155
+ mismatch('className', stored.className, selected.className),
156
+ mismatch('methodName', stored.methodName, selected.methodName),
157
+ mismatch('sourceFile', stored.sourceFile, selected.sourceFile),
158
+ mismatch('sourceLine', stored.sourceLine, selected.sourceLine),
159
+ mismatch('repository.id', expectedRepository.id, selectedRepository.id),
160
+ mismatch('repository.name', expectedRepository.name, selectedRepository.name),
161
+ mismatch('repository.packageName', expectedRepository.packageName,
162
+ selectedRepository.packageName),
163
+ ].flatMap((field) => field ? [field] : []);
164
+ }
165
+
166
+ function mismatch(
167
+ field: string,
168
+ stored: unknown,
169
+ actual: unknown,
170
+ ): string | undefined {
171
+ return stored === undefined || stored === actual ? undefined : field;
172
+ }
173
+
174
+ function record(value: unknown): Record<string, unknown> {
175
+ return value && typeof value === 'object' && !Array.isArray(value)
176
+ ? value as Record<string, unknown>
177
+ : {};
178
+ }
179
+
180
+ function stringValue(value: unknown): string | undefined {
181
+ return typeof value === 'string' ? value : undefined;
182
+ }
183
+
184
+ function numberValue(value: unknown): number | undefined {
185
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
186
+ }
@@ -5,6 +5,11 @@ import { resolveOperation, type OperationTarget } from '../linker/service-resolv
5
5
  import type { DynamicMode } from '../types.js';
6
6
  import { analyzeDynamicTargetCandidates, type DynamicTargetAnalysis, type DynamicTargetCandidate } from './dynamic-targets.js';
7
7
  import { boundCandidateLikeEvidence } from '../utils/000-bounded-projection.js';
8
+ import {
9
+ dynamicMissingReason,
10
+ isStructuralContextualBlocker,
11
+ type ContextualRuntimeState,
12
+ } from './008-contextual-runtime-state.js';
8
13
 
9
14
  export interface TraceGraphRow extends Record<string, unknown> {
10
15
  id: number;
@@ -35,6 +40,12 @@ interface RuntimeDiagnosticTotals {
35
40
  rejectedCandidates: Record<string, unknown>[];
36
41
  suggestedVarSets: Record<string, unknown>[];
37
42
  }
43
+ interface RuntimeResolutionResult {
44
+ row: TraceGraphRow;
45
+ evidence: Record<string, unknown>;
46
+ target?: OperationTarget;
47
+ unresolvedReason?: string;
48
+ }
38
49
 
39
50
  export function baseTraceEvidence(
40
51
  row: TraceGraphRow,
@@ -67,8 +78,8 @@ export function runtimeResolution(
67
78
  evidence: Record<string, unknown>,
68
79
  options: { vars?: Record<string, string>; dynamicMode?: DynamicMode; maxDynamicCandidates?: number },
69
80
  workspaceId: number | undefined,
70
- contextualUnresolvedReason?: string,
71
- ): { row: TraceGraphRow; evidence: Record<string, unknown>; target?: OperationTarget; unresolvedReason?: string } {
81
+ contextualState?: ContextualRuntimeState,
82
+ ): RuntimeResolutionResult {
72
83
  const dynamicMode = options.dynamicMode ?? 'strict';
73
84
  const candidateCap = positiveCandidateCap(options.maxDynamicCandidates);
74
85
  const boundedEvidence = boundCandidateLikeEvidence(evidence, candidateCap);
@@ -76,7 +87,7 @@ export function runtimeResolution(
76
87
  return unchangedRuntimeResolution(
77
88
  row,
78
89
  boundDynamicEvidence(boundedEvidence, candidateCap),
79
- contextualUnresolvedReason,
90
+ contextualState,
80
91
  );
81
92
  const substituted = evidenceWithRuntimeVariables(boundedEvidence, options.vars);
82
93
  const analysis = analyzeDynamicTargetCandidates(
@@ -86,47 +97,83 @@ export function runtimeResolution(
86
97
  analysis ? evidenceWithDynamicAnalysis(substituted, analysis) : substituted,
87
98
  candidateCap,
88
99
  );
100
+ const appliedRuntimeValues = hasApplicableRuntimeVariables(evidence, options.vars);
101
+ const analyzed = analyzedRuntimeResolution(
102
+ row, enriched, analysis, dynamicMode, appliedRuntimeValues, contextualState,
103
+ );
104
+ if (analyzed) return analyzed;
105
+ if (!appliedRuntimeValues) {
106
+ const unresolvedReason = contextualReason(contextualState) ?? row.unresolved_reason;
107
+ const withSections = withEffectiveResolution(
108
+ enriched, row, unresolvedReason, undefined, contextualState,
109
+ );
110
+ return { row, evidence: withSections, unresolvedReason };
111
+ }
112
+ return resolveSuppliedRuntimeOperation(db, row, enriched, workspaceId, contextualState);
113
+ }
114
+
115
+ function analyzedRuntimeResolution(
116
+ row: TraceGraphRow,
117
+ evidence: Record<string, unknown>,
118
+ analysis: DynamicTargetAnalysis | undefined,
119
+ dynamicMode: DynamicMode,
120
+ appliedRuntimeValues: boolean,
121
+ contextualState: ContextualRuntimeState | undefined,
122
+ ): RuntimeResolutionResult | undefined {
89
123
  if (analysis && analysis.viableCandidateCount === 0
90
124
  && Object.keys(analysis.appliedSuppliedVariables).length > 0)
91
- return noCandidateRuntimeResolution(row, enriched);
92
- if (dynamicMode === 'infer') {
93
- const inferred = inferredTarget(analysis);
94
- if (inferred)
95
- return resolvedRuntimeResolution(row, enriched, inferred, inferred.reasons);
96
- }
97
- if (analysis && analysis.missingVariables.length > 0) {
98
- const unresolvedReason = contextualUnresolvedReason
99
- ?? row.unresolved_reason
100
- ?? 'Dynamic target still requires runtime variables';
125
+ return noCandidateRuntimeResolution(row, evidence, contextualState);
126
+ const inferred = dynamicMode === 'infer' ? inferredTarget(analysis) : undefined;
127
+ if (inferred && !isStructuralContextualBlocker(contextualState))
128
+ return resolvedRuntimeResolution(row, evidence, inferred, inferred.reasons);
129
+ if (analysis && analysis.missingVariables.length > 0 && appliedRuntimeValues) {
130
+ const unresolvedReason = dynamicMissingReason(analysis.missingVariables);
101
131
  return {
102
132
  row,
103
- evidence: withEffectiveResolution(enriched, row, unresolvedReason),
133
+ evidence: withEffectiveResolution(
134
+ evidence, row, unresolvedReason, undefined, contextualState,
135
+ ),
104
136
  unresolvedReason,
105
137
  };
106
138
  }
107
- if (!hasApplicableRuntimeVariables(evidence, options.vars)) {
108
- const unresolvedReason = contextualUnresolvedReason ?? row.unresolved_reason;
109
- const withSections = withEffectiveResolution(enriched, row, unresolvedReason);
110
- return { row, evidence: withSections, unresolvedReason };
111
- }
112
- const resolution = resolveRuntimeOperation(db, enriched, workspaceId);
139
+ return isStructuralContextualBlocker(contextualState)
140
+ ? unchangedRuntimeResolution(row, evidence, contextualState)
141
+ : undefined;
142
+ }
143
+
144
+ function resolveSuppliedRuntimeOperation(
145
+ db: Db,
146
+ row: TraceGraphRow,
147
+ evidence: Record<string, unknown>,
148
+ workspaceId: number | undefined,
149
+ contextualState: ContextualRuntimeState | undefined,
150
+ ): RuntimeResolutionResult {
151
+ const resolution = resolveRuntimeOperation(db, evidence, workspaceId);
113
152
  if (resolution.target)
114
153
  return resolvedRuntimeResolution(
115
- row, enriched, resolution.target, resolution.reasons,
154
+ row, evidence, resolution.target, resolution.reasons,
116
155
  );
117
156
  const unresolvedReason = runtimeUnresolvedReason(resolution);
118
- return { row, evidence: withEffectiveResolution(enriched, row, unresolvedReason, resolution), unresolvedReason };
157
+ return {
158
+ row,
159
+ evidence: withEffectiveResolution(
160
+ evidence, row, unresolvedReason, resolution, contextualState,
161
+ ),
162
+ unresolvedReason,
163
+ };
119
164
  }
120
165
 
121
166
  function unchangedRuntimeResolution(
122
167
  row: TraceGraphRow,
123
168
  evidence: Record<string, unknown>,
124
- contextualUnresolvedReason: string | undefined,
125
- ): ReturnType<typeof runtimeResolution> {
126
- const unresolvedReason = contextualUnresolvedReason ?? row.unresolved_reason;
169
+ contextualState: ContextualRuntimeState | undefined,
170
+ ): RuntimeResolutionResult {
171
+ const unresolvedReason = contextualReason(contextualState) ?? row.unresolved_reason;
127
172
  return {
128
173
  row,
129
- evidence: withEffectiveResolution(evidence, row, unresolvedReason),
174
+ evidence: withEffectiveResolution(
175
+ evidence, row, unresolvedReason, undefined, contextualState,
176
+ ),
130
177
  unresolvedReason,
131
178
  };
132
179
  }
@@ -134,11 +181,14 @@ function unchangedRuntimeResolution(
134
181
  function noCandidateRuntimeResolution(
135
182
  row: TraceGraphRow,
136
183
  evidence: Record<string, unknown>,
137
- ): ReturnType<typeof runtimeResolution> {
184
+ contextualState: ContextualRuntimeState | undefined,
185
+ ): RuntimeResolutionResult {
138
186
  const unresolvedReason = 'No candidate remained after runtime substitution';
139
187
  return {
140
188
  row,
141
- evidence: withEffectiveResolution(evidence, row, unresolvedReason),
189
+ evidence: withEffectiveResolution(
190
+ evidence, row, unresolvedReason, undefined, contextualState,
191
+ ),
142
192
  unresolvedReason,
143
193
  };
144
194
  }
@@ -148,7 +198,7 @@ function resolvedRuntimeResolution(
148
198
  evidence: Record<string, unknown>,
149
199
  target: OperationTarget,
150
200
  reasons: string[],
151
- ): ReturnType<typeof runtimeResolution> {
201
+ ): RuntimeResolutionResult {
152
202
  const resolvedRow = {
153
203
  ...row,
154
204
  status: 'resolved',
@@ -328,6 +378,7 @@ function effectiveResolution(
328
378
  evidence: Record<string, unknown>,
329
379
  unresolvedReason: string | undefined,
330
380
  resolution?: ReturnType<typeof resolveRuntimeOperation>,
381
+ contextualState?: ContextualRuntimeState,
331
382
  ): Record<string, unknown> {
332
383
  const target = resolution?.target;
333
384
  return {
@@ -342,6 +393,8 @@ function effectiveResolution(
342
393
  reasons: resolution?.reasons ?? evidence.resolutionReasons,
343
394
  unresolvedReason,
344
395
  edgeType: target ? 'REMOTE_CALL_RESOLVES_TO_OPERATION' : row.edge_type,
396
+ contextualBlocker: isStructuralContextualBlocker(contextualState)
397
+ ? contextualState : undefined,
345
398
  };
346
399
  }
347
400
 
@@ -350,11 +403,30 @@ function withEffectiveResolution(
350
403
  row: TraceGraphRow,
351
404
  unresolvedReason: string | undefined,
352
405
  resolution?: ReturnType<typeof resolveRuntimeOperation>,
406
+ contextualState?: ContextualRuntimeState,
353
407
  ): Record<string, unknown> {
354
- const current = effectiveResolution(row, evidence, unresolvedReason, resolution);
408
+ const current = effectiveResolution(
409
+ row, evidence, unresolvedReason, resolution, contextualState,
410
+ );
355
411
  const rest = { ...evidence };
356
412
  delete rest.runtimeResolvedCandidate;
357
- return { ...rest, effectiveResolution: current, linker: { status: current.status, confidence: current.confidence, reason: unresolvedReason, edgeType: current.edgeType } };
413
+ return {
414
+ ...rest,
415
+ effectiveResolution: current,
416
+ linker: {
417
+ status: current.status,
418
+ confidence: current.confidence,
419
+ reason: unresolvedReason,
420
+ edgeType: current.edgeType,
421
+ contextualBlocker: current.contextualBlocker,
422
+ },
423
+ };
424
+ }
425
+
426
+ function contextualReason(
427
+ state: ContextualRuntimeState | undefined,
428
+ ): string | undefined {
429
+ return state?.category === 'none' ? undefined : state?.message;
358
430
  }
359
431
 
360
432
  function resolveRuntimeOperation(db: Db, evidence: Record<string, unknown>, workspaceId: number | undefined): ReturnType<typeof resolveOperation> {
@@ -1,8 +1,6 @@
1
1
  import type { Db } from '../db/connection.js';
2
2
  import { reposByName } from '../db/repositories.js';
3
3
  import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
4
- import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
5
- import { resolveOperation } from '../linker/service-resolver.js';
6
4
  import type { ImplementationHint, TraceEdge, TraceOptions, TraceResult, TraceStart } from '../types.js';
7
5
  import { baseTraceEvidence, edgeTarget, runtimeNoCandidateDiagnostics, runtimeResolution, runtimeVariableDiagnostic, type TraceGraphRow } from './evidence.js';
8
6
  import { dynamicCandidateBranches } from './dynamic-branches.js';
@@ -15,8 +13,16 @@ import {
15
13
  contextualImplementationSelection,
16
14
  hintedImplementationSelection,
17
15
  } from './005-implementation-selection.js';
18
- import { boundedContextCandidates } from './006-contextual-projection.js';
19
16
  import { implementationStartDiagnostic } from './007-implementation-start-diagnostic.js';
17
+ import {
18
+ contextualRuntimeResolution,
19
+ type ContextBinding,
20
+ } from './008-contextual-runtime-state.js';
21
+ import {
22
+ handlerMethodNode,
23
+ withSelectedHandlerProvenance,
24
+ type SelectedHandlerEvidence,
25
+ } from './009-selected-handler-provenance.js';
20
26
  import { boundCandidateLikeEvidence } from '../utils/000-bounded-projection.js';
21
27
  import {
22
28
  ambiguousStartDiagnostic,
@@ -60,32 +66,6 @@ interface GraphRow extends Record<string, unknown> {
60
66
  unresolved_reason?: string;
61
67
  status?: string;
62
68
  }
63
- interface ContextBinding {
64
- bindingId?: number;
65
- alias?: string;
66
- aliasExpr?: string;
67
- destinationExpr?: string;
68
- servicePathExpr?: string;
69
- requireServicePath?: string;
70
- requireDestination?: string;
71
- effectiveServicePath?: string;
72
- effectiveDestination?: string;
73
- sourceFile?: string;
74
- sourceLine?: number;
75
- source: string;
76
- callerArgument?: string;
77
- callerProperty?: string;
78
- calleeParameter?: string;
79
- calleeObjectProperty?: string;
80
- calleeLocalDestructuredIdentifier?: string;
81
- parameterPropertyAliasKind?: unknown;
82
- parameterPropertyAliasLine?: unknown;
83
- calleeReceiver: string;
84
- callerSite?: { sourceFile?: string; sourceLine?: number };
85
- calleeSite?: { sourceFile?: string; sourceLine?: number };
86
- resolutionStatus?: 'selected' | 'ambiguous';
87
- bindingCandidates?: Array<Record<string, unknown>>;
88
- }
89
69
  interface ImplementationHintOptions {
90
70
  implementationRepo?: string;
91
71
  implementationHints?: ImplementationHint[];
@@ -219,11 +199,6 @@ function handlerFilesForOperation(db: Db, operationId: string): Set<string> {
219
199
  function implementationEdge(db: Db, operationId: string): GraphRow | undefined {
220
200
  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) as GraphRow | undefined;
221
201
  }
222
- function handlerMethodNode(db: Db, methodId: string): Record<string, unknown> | undefined {
223
- 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) as Record<string, unknown> | undefined;
224
- if (!row) return undefined;
225
- return { id: `handler_method:${methodId}`, kind: 'handler_method', label: `${String(row.repoName)}:${String(row.className)}.${String(row.methodName)}`, ...row };
226
- }
227
202
  function implementationScope(db: Db, operationId: string): { repoId?: number; files: Set<string>; symbolId?: number; edge?: GraphRow } {
228
203
  const edge = implementationEdge(db, operationId);
229
204
  if (!edge || edge.status !== 'resolved') return { files: new Set(), edge };
@@ -436,40 +411,6 @@ function contextForSymbolCall(db: Db, symbolCall: Record<string, unknown>, calle
436
411
  });
437
412
  return next;
438
413
  }
439
- function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBinding | undefined, workspaceId: number | undefined, persistedRows: GraphRow[] = []): { row?: GraphRow; evidence?: Record<string, unknown>; unresolvedReason?: string } {
440
- if (!binding || String(call.call_type) !== 'remote_action' || call.operation_path_expr === undefined || call.operation_path_expr === null) return {};
441
- if (binding.resolutionStatus === 'ambiguous') {
442
- const candidates = boundedContextCandidates(binding.bindingCandidates ?? []);
443
- return {
444
- evidence: {
445
- contextualServiceBindingAttempted: true,
446
- contextualBinding: {
447
- source: binding.source,
448
- status: 'tied',
449
- candidates: candidates.candidates,
450
- candidateCount: candidates.candidateCount,
451
- shownCandidateCount: candidates.shownCandidateCount,
452
- omittedCandidateCount: candidates.omittedCandidateCount,
453
- },
454
- contextualResolutionStatus: 'ambiguous',
455
- contextualCandidateCount: candidates.candidateCount,
456
- },
457
- unresolvedReason: 'Ambiguous contextual service binding candidates',
458
- };
459
- }
460
- const normalized = normalizeODataOperationInvocationPath(String(call.operation_path_expr));
461
- const op = normalized?.normalizedOperationPath ?? (String(call.operation_path_expr).startsWith('/') ? String(call.operation_path_expr) : `/${String(call.operation_path_expr)}`);
462
- const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
463
- const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
464
- const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
465
- const candidates = boundedContextCandidates(resolution.candidates);
466
- const evidence: Record<string, unknown> = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, callerSite: binding.callerSite, calleeSite: binding.calleeSite, 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 : undefined, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : undefined, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: candidates.candidateCount, shownContextualCandidateCount: candidates.shownCandidateCount, omittedContextualCandidateCount: candidates.omittedCandidateCount, candidates: candidates.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
467
- 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' };
468
- const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
469
- const persistedResolved = persistedRows.find((item) => item.status === 'resolved');
470
- if (persistedResolved) return { row: undefined, evidence: { ...resolvedEvidence, contextualPreservedPersistedResolvedEdge: true }, unresolvedReason: undefined };
471
- 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: undefined };
472
- }
473
414
  export function trace(
474
415
  db: Db,
475
416
  start: TraceStart,
@@ -512,11 +453,27 @@ export function trace(
512
453
  );
513
454
  if (impl.edge && (impl.edge.status === 'resolved' || startSelection.methodId)) {
514
455
  const selectedMethodId = impl.edge.status === 'resolved' ? impl.edge.to_id : startSelection.methodId;
515
- const implEvidence = { ...parseEvidence(impl.edge.evidence_json), startResolution: { strategy: 'indexed_operation_graph', matchedOperationId: scope.startOperationId, implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, selectedHandlerMethodId: selectedMethodId }, implementationSelection: startSelection.methodId ? startSelection.evidence : undefined };
516
- const handlerNode = selectedMethodId ? handlerMethodNode(db, selectedMethodId) : undefined;
517
- if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
456
+ const implEvidence = {
457
+ ...parseEvidence(impl.edge.evidence_json),
458
+ startResolution: {
459
+ strategy: 'indexed_operation_graph',
460
+ matchedOperationId: scope.startOperationId,
461
+ implementationEdgeId: impl.edge.id,
462
+ implementationStatus: impl.edge.status,
463
+ selectedHandlerMethodId: selectedMethodId,
464
+ },
465
+ implementationSelection: startSelection.methodId
466
+ ? startSelection.evidence : undefined,
467
+ };
468
+ const selected: SelectedHandlerEvidence = selectedMethodId
469
+ ? withSelectedHandlerProvenance(
470
+ implEvidence, selectedMethodId, handlerMethodNode(db, selectedMethodId),
471
+ )
472
+ : { evidence: implEvidence };
473
+ if (selected.diagnostic) prependTraceDiagnostic(diagnostics, selected.diagnostic);
474
+ if (selected.handler) nodes.set(String(selected.handler.id), selected.handler);
518
475
  seenEdges.add(Number(impl.edge.id));
519
- edges.push({ step: 1, type: 'operation_implemented_by_handler', from: op?.label ? String(op.label) : `operation:${scope.startOperationId}`, 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' || startSelection.methodId ? undefined : String(impl.edge.unresolved_reason ?? impl.edge.status) });
476
+ edges.push({ step: 1, type: 'operation_implemented_by_handler', from: op?.label ? String(op.label) : `operation:${scope.startOperationId}`, to: selected.handler?.label ? String(selected.handler.label) : `${impl.edge.to_kind}:${impl.edge.to_id}`, evidence: selected.evidence, confidence: Number(impl.edge.confidence ?? 0), unresolvedReason: selected.unresolvedReason ?? (impl.edge.status === 'resolved' || startSelection.methodId ? undefined : String(impl.edge.unresolved_reason ?? impl.edge.status)) });
520
477
  }
521
478
  }
522
479
  const seenScopes = new Set<string>();
@@ -581,7 +538,7 @@ export function trace(
581
538
  vars: options.vars,
582
539
  dynamicMode: options.dynamicMode ?? 'strict',
583
540
  maxDynamicCandidates: options.maxDynamicCandidates,
584
- }, workspaceIdForCall(db, String(call.id)), contextual.unresolvedReason);
541
+ }, workspaceIdForCall(db, String(call.id)), contextual.state);
585
542
  const evidence = effective.evidence;
586
543
  const effectiveRow = effective.row;
587
544
  const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
@@ -611,31 +568,59 @@ export function trace(
611
568
  hintOptions,
612
569
  );
613
570
  const contextMethodId = contextSelection.methodId;
614
- const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : undefined;
571
+ let selectedHandlerAvailable = true;
615
572
  if (implementation.edge) {
616
573
  const implEvidence = parseEvidence(implementation.edge.evidence_json);
617
574
  const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence);
618
575
  if (hintDiagnostic) prependTraceDiagnostic(diagnostics, hintDiagnostic);
619
- const handlerNode = implementation.edge.status === 'resolved' ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
620
- const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
621
- if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
576
+ const selectedMethodId = implementation.edge.status === 'resolved'
577
+ ? implementation.edge.to_id : contextMethodId;
578
+ const selectionEvidence = contextMethodId
579
+ ? {
580
+ ...implEvidence,
581
+ contextualImplementationSelected:
582
+ contextSelection.evidence.strategy !== 'implementation_repo_hint',
583
+ contextualImplementation: contextSelection.evidence,
584
+ implementationSelection: contextSelection.evidence,
585
+ }
586
+ : {
587
+ ...implEvidence,
588
+ contextualImplementation: contextSelection.evidence,
589
+ implementationSelection: contextSelection.evidence,
590
+ };
591
+ const selected: SelectedHandlerEvidence = selectedMethodId
592
+ ? withSelectedHandlerProvenance(
593
+ selectionEvidence,
594
+ selectedMethodId,
595
+ handlerMethodNode(db, selectedMethodId),
596
+ )
597
+ : { evidence: selectionEvidence };
598
+ selectedHandlerAvailable = !selected.unresolvedReason;
599
+ if (selected.diagnostic) prependTraceDiagnostic(diagnostics, selected.diagnostic);
600
+ const implTo = selected.handler?.label
601
+ ? String(selected.handler.label)
602
+ : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
603
+ if (selected.handler) nodes.set(String(selected.handler.id), selected.handler);
622
604
  edges.push({
623
605
  step: current.depth,
624
606
  type: 'operation_implemented_by_handler',
625
607
  from: to,
626
608
  to: implTo,
627
- evidence: contextMethodId
628
- ? { ...implEvidence, contextualImplementationSelected: contextSelection.evidence.strategy !== 'implementation_repo_hint', contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence }
629
- : { ...implEvidence, contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence },
609
+ evidence: selected.evidence,
630
610
  confidence: Number(implementation.edge.confidence ?? 0),
631
- unresolvedReason: implementation.edge.status === 'resolved' || contextMethodId ? undefined : String(implementation.edge.unresolved_reason ?? implementation.edge.status),
611
+ unresolvedReason: selected.unresolvedReason
612
+ ?? (implementation.edge.status === 'resolved' || contextMethodId
613
+ ? undefined
614
+ : String(implementation.edge.unresolved_reason ?? implementation.edge.status)),
632
615
  });
633
616
  }
634
617
  if (current.depth >= maxDepth) continue;
635
618
  const contextScope = contextMethodId ? handlerScope(db, contextMethodId) : undefined;
636
619
  const files = contextScope?.files ?? (implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id));
637
620
  const symbolIds = contextScope?.symbolId ? new Set([contextScope.symbolId]) : implementation.symbolId ? new Set([implementation.symbolId]) : undefined;
638
- if ((implementation.edge?.status === 'resolved' || contextScope) && files.size > 0) {
621
+ if (selectedHandlerAvailable
622
+ && (implementation.edge?.status === 'resolved' || contextScope)
623
+ && files.size > 0) {
639
624
  const targetRepoId = contextScope?.repoId ?? implementation.repoId ?? (db
640
625
  .prepare(
641
626
  'SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?',