@saptools/service-flow 0.1.68 → 0.1.69

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +11 -5
  3. package/TECHNICAL-NOTE.md +9 -0
  4. package/dist/{chunk-AEM4JY22.js → chunk-3N3B5KHV.js} +6983 -5500
  5. package/dist/chunk-3N3B5KHV.js.map +1 -0
  6. package/dist/cli.js +317 -105
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +7 -1
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/001-index-summary.ts +22 -0
  12. package/src/cli.ts +151 -87
  13. package/src/db/000-call-fact-repository.ts +45 -19
  14. package/src/db/003-current-fact-semantics.ts +1 -0
  15. package/src/db/004-package-target-invalidation.ts +10 -0
  16. package/src/db/006-relative-symbol-resolution.ts +28 -7
  17. package/src/db/008-relative-fact-semantics.ts +6 -3
  18. package/src/db/009-binding-fact-semantics.ts +5 -0
  19. package/src/db/012-binding-reference-proof.ts +4 -0
  20. package/src/db/013-index-publication-failure.ts +91 -0
  21. package/src/db/014-binding-helper-provenance.ts +17 -0
  22. package/src/db/repositories.ts +22 -5
  23. package/src/indexer/cds-extension-resolver.ts +27 -3
  24. package/src/indexer/repository-indexer.ts +66 -5
  25. package/src/indexer/workspace-indexer.ts +141 -29
  26. package/src/linker/004-event-subscription-handler-linker.ts +32 -11
  27. package/src/linker/006-event-template-link.ts +72 -0
  28. package/src/linker/007-call-edge-insertion.ts +568 -0
  29. package/src/linker/cross-repo-linker.ts +2 -165
  30. package/src/parsers/000-direct-query-execution.ts +11 -0
  31. package/src/parsers/006-binding-identity.ts +6 -1
  32. package/src/parsers/014-service-binding-helper-flow.ts +70 -4
  33. package/src/parsers/021-binding-visibility.ts +17 -1
  34. package/src/parsers/022-outbound-expression-analysis.ts +700 -0
  35. package/src/parsers/023-outbound-call-classifier.ts +692 -0
  36. package/src/parsers/outbound-call-parser.ts +146 -509
  37. package/src/parsers/symbol-parser.ts +37 -5
  38. package/src/trace/007-implementation-start-diagnostic.ts +1 -0
  39. package/src/trace/011-event-subscriber-traversal.ts +100 -8
  40. package/src/trace/013-trace-root-scopes.ts +2 -3
  41. package/src/trace/014-compact-contract.ts +6 -0
  42. package/src/trace/015-trace-edge-recorder.ts +61 -4
  43. package/src/trace/016-compact-projector.ts +2 -3
  44. package/src/trace/019-trace-edge-semantics.ts +6 -10
  45. package/src/trace/020-compact-field-projection.ts +74 -1
  46. package/src/trace/021-compact-decision-normalization.ts +75 -9
  47. package/src/trace/024-compact-observation-decision.ts +7 -2
  48. package/src/trace/026-trace-start-scope.ts +1 -0
  49. package/src/trace/027-trace-scope-execution.ts +33 -33
  50. package/src/trace/030-event-runtime-resolution.ts +151 -0
  51. package/src/trace/031-local-call-expansion.ts +37 -0
  52. package/src/trace/implementation-hints.ts +9 -6
  53. package/src/trace/selectors.ts +1 -0
  54. package/src/types.ts +2 -1
  55. package/src/version.ts +1 -1
  56. package/dist/chunk-AEM4JY22.js.map +0 -1
@@ -0,0 +1,568 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import {
3
+ ambiguousPathCandidates,
4
+ linkedCallEvidence,
5
+ objectJson,
6
+ objectValue,
7
+ } from './002-call-evidence.js';
8
+ import { applyVariables } from './dynamic-edge-resolver.js';
9
+ import { insertEventCallEdge } from './006-event-template-link.js';
10
+ import { externalHttpTarget } from './external-http-target.js';
11
+ import {
12
+ classifyODataPathIntent,
13
+ normalizeODataOperationInvocationPath,
14
+ type ODataPathIntent,
15
+ } from './odata-path-normalizer.js';
16
+ import { buildRemoteQueryTarget } from './remote-query-target.js';
17
+ import {
18
+ resolveOperation,
19
+ type OperationResolution,
20
+ } from './service-resolver.js';
21
+
22
+ export interface CallEdgeInsertionResult {
23
+ status: string;
24
+ callType: string;
25
+ }
26
+
27
+ interface CallPathContext {
28
+ intent: ODataPathIntent;
29
+ entityQueryIntent: boolean;
30
+ normalized: ReturnType<typeof normalizeODataOperationInvocationPath>;
31
+ operation?: string;
32
+ }
33
+
34
+ interface CallEdgeContext extends CallPathContext {
35
+ db: Db;
36
+ workspaceId: number;
37
+ call: Record<string, unknown>;
38
+ variables: Record<string, string>;
39
+ generation: number;
40
+ callType: string;
41
+ servicePath?: string;
42
+ destination?: string;
43
+ bindingDynamic: boolean;
44
+ remoteEntity: boolean;
45
+ indexedOperationCandidateCount: number;
46
+ resolution: OperationResolution;
47
+ evidence: Record<string, unknown>;
48
+ }
49
+
50
+ function callPathContext(
51
+ call: Record<string, unknown>,
52
+ variables: Record<string, string>,
53
+ ): CallPathContext {
54
+ const raw = applyVariables(String(call.operation_path_expr ?? ''), variables);
55
+ const intent = classifyODataPathIntent(
56
+ raw, call.method as string | undefined,
57
+ );
58
+ const entityQueryIntent = [
59
+ 'entity_query', 'entity_key_read', 'entity_navigation_query',
60
+ ].includes(intent.kind);
61
+ const resolutionPath = call.call_type === 'remote_query'
62
+ && entityQueryIntent ? intent.pathWithoutQuery : raw;
63
+ const normalized = normalizeODataOperationInvocationPath(resolutionPath);
64
+ return {
65
+ intent,
66
+ entityQueryIntent,
67
+ normalized,
68
+ operation: normalized?.normalizedOperationPath ?? resolutionPath,
69
+ };
70
+ }
71
+
72
+ function resolvedServicePath(
73
+ call: Record<string, unknown>,
74
+ variables: Record<string, string>,
75
+ ): string | undefined {
76
+ const expression = (call.servicePathExpr as string | undefined)
77
+ ?? (call.requireServicePath as string | undefined);
78
+ return applyVariables(expression, variables);
79
+ }
80
+
81
+ function resolvedDestination(
82
+ call: Record<string, unknown>,
83
+ variables: Record<string, string>,
84
+ ): string | undefined {
85
+ const expression = (call.destinationExpr as string | undefined)
86
+ ?? (call.requireDestination as string | undefined);
87
+ return expression ? applyVariables(expression, variables) : undefined;
88
+ }
89
+
90
+ function credibleOperationSignal(
91
+ path: CallPathContext,
92
+ indexedCandidateCount: number,
93
+ ): boolean {
94
+ if (path.normalized?.wasInvocation) return true;
95
+ return Boolean(path.intent.topLevelOperationNameCandidate)
96
+ && indexedCandidateCount > 0;
97
+ }
98
+
99
+ function strongEntitySignal(intent: ODataPathIntent): boolean {
100
+ if (['entity_media', 'entity_delete', 'entity_key_read',
101
+ 'entity_navigation_query'].includes(intent.kind)) return true;
102
+ return intent.kind === 'entity_mutation'
103
+ && (intent.hasEntityKeyPredicate || intent.hasNavigationSuffix);
104
+ }
105
+
106
+ function operationCallEligible(
107
+ callType: string,
108
+ remoteEntity: boolean,
109
+ path: CallPathContext,
110
+ indexedCandidateCount: number,
111
+ ): boolean {
112
+ if (remoteEntity)
113
+ return Boolean(path.operation)
114
+ && credibleOperationSignal(path, indexedCandidateCount)
115
+ && (!strongEntitySignal(path.intent) || indexedCandidateCount > 0);
116
+ if (callType === 'remote_action' || callType === 'local_service_call')
117
+ return true;
118
+ return callType === 'remote_query' && Boolean(path.operation);
119
+ }
120
+
121
+ function callResolution(
122
+ db: Db,
123
+ workspaceId: number,
124
+ call: Record<string, unknown>,
125
+ variables: Record<string, string>,
126
+ path: CallPathContext,
127
+ servicePath: string | undefined,
128
+ destination: string | undefined,
129
+ bindingDynamic: boolean,
130
+ remoteEntity: boolean,
131
+ ): OperationResolution {
132
+ const callType = String(call.call_type);
133
+ const count = operationCandidateCount(
134
+ db, workspaceId, path.operation, path.intent.topLevelOperationName,
135
+ );
136
+ if (!operationCallEligible(callType, remoteEntity, path, count))
137
+ return { status: 'unresolved', candidates: [], reasons: [] };
138
+ const aliasExpression = (call.aliasExpr as string | undefined)
139
+ ?? (call.alias as string | undefined);
140
+ return resolveOperation(db, {
141
+ servicePath,
142
+ operationPath: path.operation,
143
+ serviceName: call.local_service_name as string | undefined,
144
+ repoId: callType === 'local_service_call'
145
+ ? Number(call.repo_id) : undefined,
146
+ alias: applyVariables(aliasExpression, variables),
147
+ destination,
148
+ isDynamic: bindingDynamic,
149
+ hasExplicitOverride: Object.keys(variables).length > 0
150
+ || callType === 'local_service_call',
151
+ }, workspaceId);
152
+ }
153
+
154
+ function callEvidence(
155
+ call: Record<string, unknown>,
156
+ path: CallPathContext,
157
+ resolution: OperationResolution,
158
+ servicePath: string | undefined,
159
+ destination: string | undefined,
160
+ candidateCount: number,
161
+ ): Record<string, unknown> {
162
+ const callType = String(call.call_type);
163
+ return {
164
+ ...linkedCallEvidence(
165
+ call, resolution, servicePath, path.operation, destination,
166
+ path.normalized, path.intent,
167
+ ),
168
+ indexedOperationCandidateCount: candidateCount,
169
+ parserCallType: callType,
170
+ entityOperationPrecedence: operationPrecedence(
171
+ callType, path.intent, candidateCount, Boolean(resolution.target),
172
+ ),
173
+ };
174
+ }
175
+
176
+ function createCallEdgeContext(
177
+ db: Db,
178
+ workspaceId: number,
179
+ call: Record<string, unknown>,
180
+ variables: Record<string, string>,
181
+ generation: number,
182
+ ): CallEdgeContext {
183
+ const callType = String(call.call_type);
184
+ const path = callPathContext(call, variables);
185
+ const servicePath = resolvedServicePath(call, variables);
186
+ const destination = resolvedDestination(call, variables);
187
+ const bindingDynamic = Boolean(Number(call.isDynamic ?? 0));
188
+ const remoteEntity = callType.startsWith('remote_entity_');
189
+ const indexedOperationCandidateCount = operationCandidateCount(
190
+ db, workspaceId, path.operation, path.intent.topLevelOperationName,
191
+ );
192
+ const resolution = callResolution(
193
+ db, workspaceId, call, variables, path, servicePath, destination,
194
+ bindingDynamic, remoteEntity,
195
+ );
196
+ return {
197
+ db, workspaceId, call, variables, generation, callType, ...path,
198
+ servicePath, destination, bindingDynamic, remoteEntity,
199
+ indexedOperationCandidateCount, resolution,
200
+ evidence: callEvidence(
201
+ call, path, resolution, servicePath, destination,
202
+ indexedOperationCandidateCount,
203
+ ),
204
+ };
205
+ }
206
+
207
+ function insertAmbiguousPathEdge(
208
+ context: CallEdgeContext,
209
+ ): CallEdgeInsertionResult | undefined {
210
+ const analysis = objectValue(
211
+ objectJson(context.call.evidence_json)?.pathAnalysis,
212
+ );
213
+ if (context.callType !== 'remote_action'
214
+ || analysis?.status !== 'ambiguous') return undefined;
215
+ const paths = ambiguousPathCandidates(analysis);
216
+ const evidence = {
217
+ ...context.evidence,
218
+ ambiguousOperationPathCandidateCount: paths.totalCount,
219
+ shownAmbiguousOperationPathCandidateCount: paths.shownCount,
220
+ omittedAmbiguousOperationPathCandidateCount: paths.omittedCount,
221
+ };
222
+ context.db.prepare(`INSERT INTO graph_edges(
223
+ workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
224
+ confidence,evidence_json,is_dynamic,unresolved_reason,generation
225
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(
226
+ context.workspaceId, 'UNRESOLVED_EDGE', 'ambiguous', 'call',
227
+ String(context.call.id), 'operation_candidates', paths.items.join(','),
228
+ Number(context.call.confidence ?? 0.5), JSON.stringify(evidence), 0,
229
+ 'Ambiguous operation path candidates require explicit disambiguation',
230
+ context.generation,
231
+ );
232
+ return { status: 'ambiguous', callType: context.callType };
233
+ }
234
+
235
+ function resolvedOperationEdge(
236
+ context: CallEdgeContext,
237
+ evidence = context.evidence,
238
+ ): CallEdgeInsertionResult {
239
+ const target = context.resolution.target;
240
+ if (!target) throw new Error('resolved_operation_target_missing');
241
+ const edgeType = context.callType === 'local_service_call'
242
+ ? 'LOCAL_CALL_RESOLVES_TO_OPERATION'
243
+ : 'REMOTE_CALL_RESOLVES_TO_OPERATION';
244
+ context.db.prepare(`INSERT INTO graph_edges(
245
+ workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
246
+ confidence,evidence_json,is_dynamic,generation
247
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?)`).run(
248
+ context.workspaceId, edgeType, 'resolved', 'call',
249
+ String(context.call.id), 'operation', String(target.operationId),
250
+ target.score, JSON.stringify(evidence), 0, context.generation,
251
+ );
252
+ return { status: 'resolved', callType: context.callType };
253
+ }
254
+
255
+ function resolutionStatus(resolution: OperationResolution): string {
256
+ if (resolution.status === 'dynamic') return 'dynamic';
257
+ return resolution.status === 'ambiguous' ? 'ambiguous' : 'unresolved';
258
+ }
259
+
260
+ function remoteEntityOperationEdge(
261
+ context: CallEdgeContext,
262
+ ): CallEdgeInsertionResult {
263
+ if (context.resolution.target)
264
+ return resolvedOperationEdge(context, {
265
+ ...context.evidence,
266
+ operationEntityPrecedence: 'indexed_operation_over_parser_entity',
267
+ });
268
+ const status = resolutionStatus(context.resolution);
269
+ const precedence = context.resolution.candidates.length > 0
270
+ ? 'parser_entity_with_indexed_operation_candidates'
271
+ : 'parser_entity_operation_candidate_without_indexed_match';
272
+ context.db.prepare(`INSERT INTO graph_edges(
273
+ workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
274
+ confidence,evidence_json,is_dynamic,unresolved_reason,generation
275
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(
276
+ context.workspaceId,
277
+ status === 'dynamic' ? 'DYNAMIC_EDGE_CANDIDATE' : 'UNRESOLVED_EDGE',
278
+ status, 'call', String(context.call.id), 'operation_candidate',
279
+ context.operation
280
+ ? `Remote action: ${context.operation}` : 'Remote action: unknown path',
281
+ Number(context.call.confidence ?? 0.2),
282
+ JSON.stringify({
283
+ ...context.evidence, operationEntityPrecedence: precedence,
284
+ }),
285
+ status === 'dynamic' ? 1 : 0,
286
+ unresolvedOperationReason(context.resolution), context.generation,
287
+ );
288
+ return { status, callType: context.callType };
289
+ }
290
+
291
+ function remoteQueryTarget(
292
+ context: CallEdgeContext,
293
+ queryEntity: unknown,
294
+ ): ReturnType<typeof buildRemoteQueryTarget> {
295
+ return buildRemoteQueryTarget({
296
+ queryEntity,
297
+ servicePath: context.servicePath,
298
+ serviceAlias: context.call.alias,
299
+ serviceAliasExpr: context.call.aliasExpr,
300
+ destination: context.destination,
301
+ isDynamic: context.bindingDynamic,
302
+ parserWarning: context.evidence.parserWarning,
303
+ });
304
+ }
305
+
306
+ function insertRemoteEntityEdge(
307
+ context: CallEdgeContext,
308
+ ): CallEdgeInsertionResult | undefined {
309
+ if (!context.remoteEntity) return undefined;
310
+ const resolution = context.resolution;
311
+ if (resolution.target || resolution.candidates.length > 0
312
+ || resolution.status === 'dynamic')
313
+ return remoteEntityOperationEdge(context);
314
+ const target = remoteQueryTarget(
315
+ context, context.intent.entitySegment ?? context.call.query_entity,
316
+ );
317
+ context.db.prepare(`INSERT INTO graph_edges(
318
+ workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
319
+ confidence,evidence_json,is_dynamic,generation
320
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?)`).run(
321
+ context.workspaceId, 'HANDLER_ACCESSES_REMOTE_ENTITY', 'terminal',
322
+ 'call', String(context.call.id), target.toKind, target.toId,
323
+ Number(context.call.confidence ?? 0.5),
324
+ JSON.stringify({
325
+ ...context.evidence, ...target.evidence,
326
+ remoteEntityAccess: context.callType,
327
+ }),
328
+ 0, context.generation,
329
+ );
330
+ return { status: 'terminal', callType: context.callType };
331
+ }
332
+
333
+ function insertRemoteQueryEdge(
334
+ context: CallEdgeContext,
335
+ ): CallEdgeInsertionResult | undefined {
336
+ if (context.callType !== 'remote_query'
337
+ || !context.entityQueryIntent && context.operation
338
+ || context.resolution.target) return undefined;
339
+ const target = remoteQueryTarget(context, context.call.query_entity);
340
+ context.db.prepare(`INSERT INTO graph_edges(
341
+ workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
342
+ confidence,evidence_json,is_dynamic,generation
343
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?)`).run(
344
+ context.workspaceId, 'HANDLER_RUNS_REMOTE_QUERY', 'terminal', 'call',
345
+ String(context.call.id), target.toKind, target.toId,
346
+ Number(context.call.confidence ?? 0.5),
347
+ JSON.stringify({ ...context.evidence, ...target.evidence }),
348
+ 0, context.generation,
349
+ );
350
+ return { status: 'terminal', callType: context.callType };
351
+ }
352
+
353
+ function insertTransportEdge(
354
+ context: CallEdgeContext,
355
+ ): CallEdgeInsertionResult | undefined {
356
+ if (context.callType !== 'local_service_call'
357
+ || context.call.unresolved_reason !== 'transport_client_method'
358
+ || context.resolution.target
359
+ || context.resolution.candidates.length > 0) return undefined;
360
+ context.db.prepare(`INSERT INTO graph_edges(
361
+ workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
362
+ confidence,evidence_json,is_dynamic,generation
363
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?)`).run(
364
+ context.workspaceId, 'HANDLER_CALLS_TRANSPORT_METHOD', 'terminal',
365
+ 'call', String(context.call.id), 'transport_method',
366
+ String(context.operation || 'transport_client_method'),
367
+ Number(context.call.confidence ?? 0.5),
368
+ JSON.stringify({
369
+ ...context.evidence, classification: 'transport_client_method',
370
+ }),
371
+ 0, context.generation,
372
+ );
373
+ return { status: 'terminal', callType: context.callType };
374
+ }
375
+
376
+ function insertResolvedEdge(
377
+ context: CallEdgeContext,
378
+ ): CallEdgeInsertionResult | undefined {
379
+ return context.resolution.target
380
+ ? resolvedOperationEdge(context) : undefined;
381
+ }
382
+
383
+ function insertEventEdge(
384
+ context: CallEdgeContext,
385
+ ): CallEdgeInsertionResult | undefined {
386
+ if (context.callType !== 'async_emit'
387
+ && context.callType !== 'async_subscribe') return undefined;
388
+ return insertEventCallEdge(
389
+ context.db, context.workspaceId, context.generation, context.call,
390
+ context.variables, context.evidence,
391
+ );
392
+ }
393
+
394
+ function fallbackEdgeType(context: CallEdgeContext): string {
395
+ const fixed: Readonly<Record<string, string>> = {
396
+ local_db_query: 'HANDLER_RUNS_DB_QUERY',
397
+ external_http: 'HANDLER_CALLS_EXTERNAL_HTTP',
398
+ async_emit: 'HANDLER_EMITS_EVENT',
399
+ async_subscribe: 'EVENT_CONSUMED_BY_HANDLER',
400
+ };
401
+ return fixed[context.callType]
402
+ ?? (context.resolution.status === 'dynamic'
403
+ ? 'DYNAMIC_EDGE_CANDIDATE' : 'UNRESOLVED_EDGE');
404
+ }
405
+
406
+ function fallbackStatus(
407
+ edgeType: string,
408
+ resolution: OperationResolution,
409
+ ): string {
410
+ if (edgeType === 'DYNAMIC_EDGE_CANDIDATE') return 'dynamic';
411
+ if (resolution.status === 'ambiguous') return 'ambiguous';
412
+ return edgeType === 'UNRESOLVED_EDGE' ? 'unresolved' : 'terminal';
413
+ }
414
+
415
+ function fallbackTargetKind(
416
+ context: CallEdgeContext,
417
+ externalKind: string | undefined,
418
+ ): string {
419
+ if (context.callType === 'local_db_query') return 'db_entity';
420
+ if (context.callType.startsWith('async_')) return 'event';
421
+ if (context.callType === 'external_http')
422
+ return externalKind ?? 'external_endpoint';
423
+ return 'operation_candidate';
424
+ }
425
+
426
+ function remoteActionTarget(context: CallEdgeContext): string {
427
+ if (context.operation) return `Remote action: ${context.operation}`;
428
+ return context.call.unresolved_reason === 'dynamic_operation_path_identifier'
429
+ ? 'Remote action: dynamic path' : 'Remote action: unknown path';
430
+ }
431
+
432
+ function fallbackTargetId(
433
+ context: CallEdgeContext,
434
+ externalId: string | undefined,
435
+ ): string {
436
+ if (context.callType === 'local_db_query')
437
+ return String(context.call.query_entity ?? 'unknown');
438
+ if (context.callType === 'remote_action') return remoteActionTarget(context);
439
+ if (context.callType === 'external_http')
440
+ return String(externalId ?? 'unknown');
441
+ return String(
442
+ context.call.event_name_expr ?? context.operation ?? 'unknown',
443
+ );
444
+ }
445
+
446
+ function insertFallbackEdge(context: CallEdgeContext): CallEdgeInsertionResult {
447
+ const edgeType = fallbackEdgeType(context);
448
+ const status = fallbackStatus(edgeType, context.resolution);
449
+ const unresolvedReason = status === 'terminal' ? null : String(
450
+ context.call.unresolved_reason
451
+ ?? unresolvedOperationReason(context.resolution),
452
+ );
453
+ const external = context.callType === 'external_http'
454
+ ? externalHttpTarget(context.call) : undefined;
455
+ const targetKind = fallbackTargetKind(context, external?.toKind);
456
+ const targetId = fallbackTargetId(context, external?.toId);
457
+ const dynamic = edgeType === 'DYNAMIC_EDGE_CANDIDATE'
458
+ && context.resolution.status === 'dynamic';
459
+ const evidence = external
460
+ ? { ...context.evidence, externalTarget: external } : context.evidence;
461
+ context.db.prepare(`INSERT INTO graph_edges(
462
+ workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
463
+ confidence,evidence_json,is_dynamic,unresolved_reason,generation
464
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(
465
+ context.workspaceId, edgeType, status, 'call', String(context.call.id),
466
+ targetKind, targetId, Number(context.call.confidence ?? 0.2),
467
+ JSON.stringify(evidence), dynamic ? 1 : 0, unresolvedReason,
468
+ context.generation,
469
+ );
470
+ return { status, callType: context.callType };
471
+ }
472
+
473
+ export function insertCallEdge(
474
+ db: Db,
475
+ workspaceId: number,
476
+ call: Record<string, unknown>,
477
+ variables: Record<string, string>,
478
+ generation: number,
479
+ ): CallEdgeInsertionResult {
480
+ const context = createCallEdgeContext(
481
+ db, workspaceId, call, variables, generation,
482
+ );
483
+ const ambiguous = insertAmbiguousPathEdge(context);
484
+ if (ambiguous) return ambiguous;
485
+ const remoteEntity = insertRemoteEntityEdge(context);
486
+ if (remoteEntity) return remoteEntity;
487
+ const remoteQuery = insertRemoteQueryEdge(context);
488
+ if (remoteQuery) return remoteQuery;
489
+ const transport = insertTransportEdge(context);
490
+ if (transport) return transport;
491
+ const resolved = insertResolvedEdge(context);
492
+ if (resolved) return resolved;
493
+ const event = insertEventEdge(context);
494
+ return event ?? insertFallbackEdge(context);
495
+ }
496
+
497
+ function operationCandidateCount(
498
+ db: Db,
499
+ workspaceId: number,
500
+ operationPath: string | undefined,
501
+ operationName: string | undefined,
502
+ ): number {
503
+ if (!operationPath && !operationName) return 0;
504
+ const normalizedName = operationName
505
+ ?? operationPath?.replace(/^\//, '').split('.').at(-1);
506
+ const row = db.prepare(`SELECT COUNT(*) count FROM cds_operations o
507
+ JOIN cds_services s ON s.id=o.service_id
508
+ JOIN repositories r ON r.id=s.repo_id
509
+ WHERE r.workspace_id=? AND (
510
+ o.operation_path=? OR o.operation_path=? OR o.operation_name=?
511
+ )`).get(
512
+ workspaceId, operationPath,
513
+ normalizedName ? `/${normalizedName}` : operationPath,
514
+ normalizedName,
515
+ ) as { count?: number } | undefined;
516
+ return Number(row?.count ?? 0);
517
+ }
518
+
519
+ function operationPrecedence(
520
+ callType: string,
521
+ intent: ODataPathIntent,
522
+ indexedOperationCandidateCount: number,
523
+ resolvedOperation: boolean,
524
+ ): Record<string, unknown> {
525
+ if (resolvedOperation) return {
526
+ decision: 'operation',
527
+ reason: 'indexed_operation_with_strong_service_context',
528
+ indexedOperationCandidateCount,
529
+ };
530
+ if (callType === 'remote_action'
531
+ && intent.kind === 'operation_invocation') return {
532
+ decision: 'operation_candidate',
533
+ rejectionReason: indexedOperationCandidateCount > 0
534
+ ? 'indexed_candidates_lack_unique_strong_service_context'
535
+ : 'no_indexed_operation_candidate',
536
+ indexedOperationCandidateCount,
537
+ };
538
+ if (intent.kind.startsWith('entity_')) return {
539
+ decision: 'entity',
540
+ rejectionReason: indexedOperationCandidateCount > 0
541
+ ? 'entity_shape_has_precedence_without_resolved_operation_context'
542
+ : 'entity_shape_has_no_indexed_operation_evidence',
543
+ indexedOperationCandidateCount,
544
+ };
545
+ return {
546
+ decision: 'unresolved',
547
+ rejectionReason: 'path_has_no_safe_entity_or_operation_precedence',
548
+ indexedOperationCandidateCount,
549
+ };
550
+ }
551
+
552
+ function unresolvedOperationReason(resolution: OperationResolution): string {
553
+ if (resolution.status === 'dynamic')
554
+ return `Dynamic target requires runtime variable overrides: ${
555
+ (resolution.reasons.length
556
+ ? resolution.reasons : ['missing runtime variables']).join(', ')}`;
557
+ if (resolution.candidates.length === 0)
558
+ return 'No indexed target operation matched';
559
+ if (resolution.reasons.includes(
560
+ 'operation_path_only_has_no_strong_target_signal',
561
+ )) return 'Operation candidates found but no strong service signal is available';
562
+ if (resolution.reasons.includes(
563
+ 'candidate_score_below_resolution_threshold',
564
+ )) return 'Operation candidates found but resolution score is below threshold';
565
+ if (resolution.status === 'ambiguous')
566
+ return 'Ambiguous operation candidates require a strong service signal';
567
+ return 'Operation candidates found but resolution could not select a target';
568
+ }