@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.
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  stripQuotes,
18
18
  substituteVariables,
19
19
  trace
20
- } from "./chunk-LFH7C46B.js";
20
+ } from "./chunk-ERIZHM5C.js";
21
21
 
22
22
  // src/parsers/generated-constants-parser.ts
23
23
  import fs from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saptools/service-flow",
3
- "version": "0.1.53",
3
+ "version": "0.1.54",
4
4
  "description": "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -1,6 +1,5 @@
1
1
  import {
2
- projectBounded,
3
- stableProjectionValue,
2
+ projectBoundedInOrder,
4
3
  type BoundedProjection,
5
4
  } from '../utils/000-bounded-projection.js';
6
5
 
@@ -54,7 +53,8 @@ function boundDoctorValue(value: unknown): unknown {
54
53
  output[key] = boundDoctorValue(child);
55
54
  continue;
56
55
  }
57
- const projection = projectBounded(child.map(boundDoctorValue), compareDiagnostic);
56
+ // Doctor producers already query or assemble deterministic semantic order.
57
+ const projection = projectBoundedInOrder(child.map(boundDoctorValue));
58
58
  output[key] = projection.items;
59
59
  addProjectionMetadata(output, input, key, projection);
60
60
  }
@@ -127,10 +127,6 @@ function projectionStem(key: string): string {
127
127
  return stems[key] ?? 'repository';
128
128
  }
129
129
 
130
- function compareDiagnostic(left: unknown, right: unknown): number {
131
- return stableProjectionValue(left).localeCompare(stableProjectionValue(right));
132
- }
133
-
134
130
  function numericValue(value: unknown): number {
135
131
  return typeof value === 'number' && Number.isFinite(value) ? value : 0;
136
132
  }
@@ -4,6 +4,8 @@ import { normalizeDecoratorOperationSignal, normalizedOperationName } from './op
4
4
  import {
5
5
  boundedImplementationEvidence,
6
6
  boundedImplementationTargetIds,
7
+ displayImplementationCandidates,
8
+ selectedHandlerProvenance,
7
9
  } from './001-implementation-evidence-projection.js';
8
10
 
9
11
  interface ImplementationCandidate extends Record<string, unknown> {
@@ -107,6 +109,7 @@ function implementationDecision(
107
109
  : winners.length > 1 ? ['multiple_equal_score_implementation_candidates'] : [];
108
110
  const evidence = implementationEvidence(
109
111
  operation, implementationContext, candidates, duplicateFamilies, ambiguityReasons,
112
+ unique,
110
113
  );
111
114
  const hintProjection = implementationHintSuggestionProjection(evidence);
112
115
  return {
@@ -174,6 +177,7 @@ function implementationEvidence(
174
177
  candidates: ImplementationCandidate[],
175
178
  duplicateFamilies: Array<Record<string, unknown>>,
176
179
  ambiguityReasons: string[],
180
+ selected: ImplementationCandidate | undefined,
177
181
  ): Record<string, unknown> {
178
182
  return {
179
183
  servicePath: operation.servicePath,
@@ -191,7 +195,13 @@ function implementationEvidence(
191
195
  implementationOperationId: context.operationId,
192
196
  ambiguityReasons,
193
197
  candidateFamilies: duplicateFamilies,
194
- candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1)),
198
+ selectedHandler: selected
199
+ ? selectedHandlerProvenance(selectedHandlerSource(selected))
200
+ : undefined,
201
+ candidates: displayImplementationCandidates(
202
+ candidates.map((candidate, index) => candidateEvidence(candidate, index + 1)),
203
+ selected?.methodId,
204
+ ),
195
205
  };
196
206
  }
197
207
 
@@ -350,7 +360,7 @@ function implementationCandidates(
350
360
  hm.id methodId,hm.method_name methodName,hm.decorator_value decoratorValue,
351
361
  hm.decorator_raw_expression decoratorRawExpression,
352
362
  hm.decorator_resolution_json decoratorResolutionJson,hc.id classId,
353
- hc.class_name className,hc.source_file sourceFile,hc.source_line sourceLine,
363
+ hc.class_name className,hc.source_file sourceFile,hm.source_line sourceLine,
354
364
  hr.id registrationId,hr.handler_class_id registrationHandlerClassId,
355
365
  hr.class_name registrationClassName,hr.repo_id applicationRepoId,
356
366
  hr.registration_file registrationFile,hr.registration_line registrationLine,
@@ -526,6 +536,7 @@ function ownershipScore(
526
536
  function candidateEvidence(candidate: ImplementationCandidate, rank: number): Record<string, unknown> {
527
537
  return {
528
538
  rank,
539
+ rankKind: 'discovery_score',
529
540
  score: candidate.score,
530
541
  accepted: candidate.accepted,
531
542
  acceptedReasons: candidate.acceptedReasons,
@@ -580,6 +591,28 @@ function candidateEvidence(candidate: ImplementationCandidate, rank: number): Re
580
591
  };
581
592
  }
582
593
 
594
+ function selectedHandlerSource(candidate: ImplementationCandidate): {
595
+ methodId: number;
596
+ className?: string;
597
+ methodName?: string;
598
+ repositoryId?: number;
599
+ repositoryName?: string;
600
+ repositoryPackageName?: string;
601
+ sourceFile?: string;
602
+ sourceLine?: number;
603
+ } {
604
+ return {
605
+ methodId: candidate.methodId,
606
+ className: stringValue(candidate.className),
607
+ methodName: stringValue(candidate.methodName),
608
+ repositoryId: numberValue(candidate.handlerRepoId),
609
+ repositoryName: stringValue(candidate.handlerRepo),
610
+ repositoryPackageName: stringValue(candidate.handlerPackage),
611
+ sourceFile: stringValue(candidate.sourceFile),
612
+ sourceLine: numberValue(candidate.sourceLine),
613
+ };
614
+ }
615
+
583
616
  function implementationMethodSignal(
584
617
  row: Record<string, unknown>,
585
618
  operation: Record<string, unknown>,
@@ -1,14 +1,91 @@
1
1
  import {
2
2
  projectBounded,
3
+ projectBoundedInOrder,
3
4
  type BoundedProjection,
4
5
  } from '../utils/000-bounded-projection.js';
5
6
 
7
+ export interface SelectedHandlerSource {
8
+ methodId: number;
9
+ className?: string;
10
+ methodName?: string;
11
+ repositoryId?: number;
12
+ repositoryName?: string;
13
+ repositoryPackageName?: string;
14
+ sourceFile?: string;
15
+ sourceLine?: number;
16
+ }
17
+
18
+ export interface SelectedHandlerProvenance {
19
+ status: 'selected';
20
+ accepted: true;
21
+ graphTargetId: string;
22
+ methodId: number;
23
+ className?: string;
24
+ methodName?: string;
25
+ repository?: {
26
+ id?: number;
27
+ name?: string;
28
+ packageName?: string;
29
+ };
30
+ sourceFile?: string;
31
+ sourceLine?: number;
32
+ }
33
+
34
+ export function selectedHandlerProvenance(
35
+ source: SelectedHandlerSource,
36
+ ): SelectedHandlerProvenance {
37
+ return {
38
+ status: 'selected',
39
+ accepted: true,
40
+ graphTargetId: String(source.methodId),
41
+ methodId: source.methodId,
42
+ className: source.className,
43
+ methodName: source.methodName,
44
+ repository: {
45
+ id: source.repositoryId,
46
+ name: source.repositoryName,
47
+ packageName: source.repositoryPackageName,
48
+ },
49
+ sourceFile: source.sourceFile,
50
+ sourceLine: source.sourceLine,
51
+ };
52
+ }
53
+
54
+ export function displayImplementationCandidates(
55
+ candidates: Array<Record<string, unknown>>,
56
+ selectedMethodId: number | undefined,
57
+ ): Array<Record<string, unknown>> {
58
+ return [...candidates]
59
+ .sort((left, right) => compareDisplayCandidate(
60
+ left, right, selectedMethodId,
61
+ ))
62
+ .map((candidate, index) => ({
63
+ ...candidate,
64
+ discoveryRank: candidate.rank,
65
+ displayRank: index + 1,
66
+ selected: selectedMethodId !== undefined
67
+ && Number(candidate.methodId) === selectedMethodId,
68
+ }));
69
+ }
70
+
71
+ function compareDisplayCandidate(
72
+ left: Record<string, unknown>,
73
+ right: Record<string, unknown>,
74
+ selectedMethodId: number | undefined,
75
+ ): number {
76
+ return Number(Number(right.methodId) === selectedMethodId)
77
+ - Number(Number(left.methodId) === selectedMethodId)
78
+ || Number(right.accepted === true) - Number(left.accepted === true)
79
+ || numberValue(left.rank) - numberValue(right.rank)
80
+ || compareTargetCandidates(left, right);
81
+ }
82
+
6
83
  export function boundedImplementationEvidence(
7
84
  evidence: Record<string, unknown>,
8
85
  targetCandidateCount: number,
9
86
  ): Record<string, unknown> {
10
87
  const candidates = recordArray(evidence.candidates);
11
- const candidateProjection = projectBounded(candidates, compareCandidateEvidence);
88
+ const candidateProjection = projectBoundedInOrder(candidates);
12
89
  const families = recordArray(evidence.candidateFamilies);
13
90
  const familyProjection = projectBounded(families, compareFamilies);
14
91
  const hints = recordArray(evidence.implementationHintSuggestions);
@@ -80,11 +157,6 @@ function compareTargetCandidates(left: Record<string, unknown>, right: Record<st
80
157
  || Number(left.methodId ?? 0) - Number(right.methodId ?? 0);
81
158
  }
82
159
 
83
- function compareCandidateEvidence(left: Record<string, unknown>, right: Record<string, unknown>): number {
84
- return Number(left.rank ?? 0) - Number(right.rank ?? 0)
85
- || compareTargetCandidates(left, right);
86
- }
87
-
88
160
  function compareFamilies(left: Record<string, unknown>, right: Record<string, unknown>): number {
89
161
  return String(left.packageName ?? '').localeCompare(String(right.packageName ?? ''))
90
162
  || String(left.reason ?? '').localeCompare(String(right.reason ?? ''));
@@ -1,13 +1,22 @@
1
1
  import type { TraceResult } from '../types.js';
2
2
 
3
3
  function location(evidence: Record<string, unknown>): string {
4
+ const selected = isRecord(evidence.selectedHandler)
5
+ ? evidence.selectedHandler : undefined;
6
+ const selectedFile = selected?.sourceFile;
7
+ const selectedLine = selected?.sourceLine;
8
+ if (selectedFile || selectedLine)
9
+ return `${String(selectedFile ?? '')}:${String(selectedLine ?? '')}`;
4
10
  const file = evidence.file ?? evidence.sourceFile ?? evidence.handlerSourceFile ?? evidence.operationSourceFile ?? evidence.registrationSourceFile;
5
11
  const line = evidence.line ?? evidence.sourceLine ?? evidence.handlerSourceLine ?? evidence.operationSourceLine ?? evidence.registrationSourceLine;
6
12
  if (file || line) return `${String(file ?? '')}:${String(line ?? '')}`;
7
13
  const candidates = evidence.candidates;
14
+ if (Array.isArray(candidates) && candidates.some((candidate) =>
15
+ isRecord(candidate) && candidate.methodId !== undefined)) return ':';
8
16
  if (Array.isArray(candidates) && candidates.length > 0) {
9
- const first = candidates[0] as Record<string, unknown>;
10
- return `${String(first.sourceFile ?? '')}:${String(first.sourceLine ?? '')}`;
17
+ const first = candidates.find(isRecord);
18
+ if (first)
19
+ return `${String(first.sourceFile ?? '')}:${String(first.sourceLine ?? '')}`;
11
20
  }
12
21
  return ':';
13
22
  }
@@ -0,0 +1,294 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
3
+ import { resolveOperation, type OperationResolution } from '../linker/service-resolver.js';
4
+ import { boundedContextCandidates } from './006-contextual-projection.js';
5
+
6
+ export interface ContextBinding {
7
+ bindingId?: number;
8
+ alias?: string;
9
+ aliasExpr?: string;
10
+ destinationExpr?: string;
11
+ servicePathExpr?: string;
12
+ requireServicePath?: string;
13
+ requireDestination?: string;
14
+ effectiveServicePath?: string;
15
+ effectiveDestination?: string;
16
+ sourceFile?: string;
17
+ sourceLine?: number;
18
+ source: string;
19
+ callerArgument?: string;
20
+ callerProperty?: string;
21
+ calleeParameter?: string;
22
+ calleeObjectProperty?: string;
23
+ calleeLocalDestructuredIdentifier?: string;
24
+ parameterPropertyAliasKind?: unknown;
25
+ parameterPropertyAliasLine?: unknown;
26
+ calleeReceiver: string;
27
+ callerSite?: { sourceFile?: string; sourceLine?: number };
28
+ calleeSite?: { sourceFile?: string; sourceLine?: number };
29
+ resolutionStatus?: 'selected' | 'ambiguous';
30
+ bindingCandidates?: Array<Record<string, unknown>>;
31
+ }
32
+
33
+ interface ContextualCall {
34
+ id: number;
35
+ call_type: string;
36
+ operation_path_expr?: unknown;
37
+ }
38
+
39
+ interface PersistedGraphRow {
40
+ status?: string;
41
+ }
42
+
43
+ export interface ContextualGraphRow {
44
+ id: number;
45
+ edge_type: string;
46
+ from_id: string;
47
+ to_kind: string;
48
+ to_id: string;
49
+ confidence: number;
50
+ evidence_json: string;
51
+ status: 'resolved';
52
+ }
53
+
54
+ export type ContextualResolutionCategory =
55
+ | 'none'
56
+ | 'dynamic_missing'
57
+ | 'ambiguous_binding'
58
+ | 'ambiguous_operation'
59
+ | 'no_matching_operation'
60
+ | 'other_blocker';
61
+
62
+ export interface ContextualRuntimeState {
63
+ category: ContextualResolutionCategory;
64
+ message?: string;
65
+ missingVariables?: string[];
66
+ resolutionStatus?: string;
67
+ phase?: 'before_runtime_substitution';
68
+ }
69
+
70
+ export interface ContextualRuntimeResolution {
71
+ row?: ContextualGraphRow;
72
+ evidence?: Record<string, unknown>;
73
+ state: ContextualRuntimeState;
74
+ }
75
+
76
+ export function dynamicMissingReason(keys: readonly string[]): string {
77
+ const missing = normalizedKeys(keys);
78
+ return missing.length > 0
79
+ ? `Dynamic target is missing runtime variables: ${missing.join(', ')}`
80
+ : 'Dynamic target still requires runtime variables';
81
+ }
82
+
83
+ export function isStructuralContextualBlocker(
84
+ state: ContextualRuntimeState | undefined,
85
+ ): boolean {
86
+ return state?.category === 'ambiguous_binding'
87
+ || state?.category === 'ambiguous_operation'
88
+ || state?.category === 'no_matching_operation'
89
+ || state?.category === 'other_blocker';
90
+ }
91
+
92
+ export function contextualRuntimeResolution(
93
+ db: Db,
94
+ call: ContextualCall,
95
+ binding: ContextBinding | undefined,
96
+ workspaceId: number | undefined,
97
+ persistedRows: PersistedGraphRow[] = [],
98
+ ): ContextualRuntimeResolution {
99
+ if (!binding || call.call_type !== 'remote_action'
100
+ || call.operation_path_expr === undefined || call.operation_path_expr === null)
101
+ return { state: { category: 'none' } };
102
+ if (binding.resolutionStatus === 'ambiguous')
103
+ return ambiguousBindingResolution(binding);
104
+ return selectedBindingResolution(db, call, binding, workspaceId, persistedRows);
105
+ }
106
+
107
+ function ambiguousBindingResolution(
108
+ binding: ContextBinding,
109
+ ): ContextualRuntimeResolution {
110
+ const candidates = boundedContextCandidates(binding.bindingCandidates ?? []);
111
+ const state: ContextualRuntimeState = {
112
+ category: 'ambiguous_binding',
113
+ message: 'Ambiguous contextual service binding candidates',
114
+ resolutionStatus: 'ambiguous',
115
+ };
116
+ return {
117
+ evidence: {
118
+ contextualServiceBindingAttempted: true,
119
+ contextualBinding: {
120
+ source: binding.source,
121
+ status: 'tied',
122
+ candidates: candidates.candidates,
123
+ candidateCount: candidates.candidateCount,
124
+ shownCandidateCount: candidates.shownCandidateCount,
125
+ omittedCandidateCount: candidates.omittedCandidateCount,
126
+ },
127
+ contextualResolutionStatus: 'ambiguous',
128
+ contextualCandidateCount: candidates.candidateCount,
129
+ contextualPreSubstitutionState: historicalState(state),
130
+ },
131
+ state,
132
+ };
133
+ }
134
+
135
+ function selectedBindingResolution(
136
+ db: Db,
137
+ call: ContextualCall,
138
+ binding: ContextBinding,
139
+ workspaceId: number | undefined,
140
+ persistedRows: PersistedGraphRow[],
141
+ ): ContextualRuntimeResolution {
142
+ const normalized = normalizeODataOperationInvocationPath(
143
+ String(call.operation_path_expr),
144
+ );
145
+ const operationPath = normalized?.normalizedOperationPath
146
+ ?? withLeadingSlash(String(call.operation_path_expr));
147
+ const servicePath = binding.effectiveServicePath
148
+ ?? binding.servicePathExpr ?? binding.requireServicePath;
149
+ const destination = binding.effectiveDestination
150
+ ?? binding.destinationExpr ?? binding.requireDestination;
151
+ const resolution = resolveOperation(db, {
152
+ servicePath,
153
+ operationPath,
154
+ alias: binding.aliasExpr ?? binding.alias,
155
+ destination,
156
+ hasExplicitOverride: true,
157
+ isDynamic: false,
158
+ }, workspaceId);
159
+ const state = stateForResolution(resolution);
160
+ const evidence = contextualEvidence(
161
+ binding, normalized, operationPath, servicePath, destination, resolution, state,
162
+ );
163
+ if (!resolution.target) return { evidence, state };
164
+ const resolvedEvidence = {
165
+ ...evidence,
166
+ contextualServiceBindingSelected: true,
167
+ targetRepo: resolution.target.repoName,
168
+ targetServicePath: resolution.target.servicePath,
169
+ targetOperationPath: resolution.target.operationPath,
170
+ targetOperation: resolution.target.operationName,
171
+ };
172
+ if (persistedRows.some((row) => row.status === 'resolved'))
173
+ return { evidence: { ...resolvedEvidence, contextualPreservedPersistedResolvedEdge: true }, state };
174
+ return {
175
+ row: {
176
+ id: -call.id,
177
+ edge_type: 'REMOTE_CALL_RESOLVES_TO_OPERATION',
178
+ from_id: String(call.id),
179
+ to_kind: 'operation',
180
+ to_id: String(resolution.target.operationId),
181
+ confidence: resolution.target.score,
182
+ evidence_json: JSON.stringify(resolvedEvidence),
183
+ status: 'resolved',
184
+ },
185
+ evidence: resolvedEvidence,
186
+ state,
187
+ };
188
+ }
189
+
190
+ function contextualEvidence(
191
+ binding: ContextBinding,
192
+ normalized: ReturnType<typeof normalizeODataOperationInvocationPath>,
193
+ operationPath: string,
194
+ servicePath: string | undefined,
195
+ destination: string | undefined,
196
+ resolution: OperationResolution,
197
+ state: ContextualRuntimeState,
198
+ ): Record<string, unknown> {
199
+ const candidates = boundedContextCandidates(resolution.candidates);
200
+ return {
201
+ contextualServiceBindingAttempted: true,
202
+ contextualBinding: bindingEvidence(binding),
203
+ operationPath,
204
+ rawOperationPath: normalized?.rawOperationPath,
205
+ normalizedOperationPath: normalized?.wasInvocation
206
+ ? normalized.normalizedOperationPath : undefined,
207
+ invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length
208
+ ? normalized.invocationArgumentPlaceholderKeys : undefined,
209
+ servicePath,
210
+ serviceAlias: binding.alias,
211
+ serviceAliasExpr: binding.aliasExpr,
212
+ destination,
213
+ requireServicePath: binding.requireServicePath,
214
+ requireDestination: binding.requireDestination,
215
+ effectiveServicePath: binding.effectiveServicePath,
216
+ effectiveDestination: binding.effectiveDestination,
217
+ contextualResolutionStatus: resolution.status,
218
+ contextualCandidateCount: candidates.candidateCount,
219
+ shownContextualCandidateCount: candidates.shownCandidateCount,
220
+ omittedContextualCandidateCount: candidates.omittedCandidateCount,
221
+ candidates: candidates.candidates,
222
+ contextualResolutionReasons: resolution.reasons,
223
+ resolutionReasons: resolution.reasons,
224
+ contextualPreSubstitutionState: historicalState(state),
225
+ };
226
+ }
227
+
228
+ function bindingEvidence(binding: ContextBinding): Record<string, unknown> {
229
+ return {
230
+ source: binding.source,
231
+ callerArgument: binding.callerArgument,
232
+ callerProperty: binding.callerProperty,
233
+ calleeParameter: binding.calleeParameter,
234
+ calleeReceiver: binding.calleeReceiver,
235
+ callerSite: binding.callerSite,
236
+ calleeSite: binding.calleeSite,
237
+ bindingSourceFile: binding.sourceFile,
238
+ bindingSourceLine: binding.sourceLine,
239
+ alias: binding.alias,
240
+ aliasExpr: binding.aliasExpr,
241
+ requireServicePath: binding.requireServicePath,
242
+ requireDestination: binding.requireDestination,
243
+ effectiveServicePath: binding.effectiveServicePath,
244
+ effectiveDestination: binding.effectiveDestination,
245
+ };
246
+ }
247
+
248
+ function stateForResolution(
249
+ resolution: OperationResolution,
250
+ ): ContextualRuntimeState {
251
+ if (resolution.status === 'resolved') return { category: 'none' };
252
+ if (resolution.status === 'dynamic') {
253
+ const missingVariables = missingVariableKeys(resolution.reasons);
254
+ return {
255
+ category: 'dynamic_missing',
256
+ message: dynamicMissingReason(missingVariables),
257
+ missingVariables,
258
+ resolutionStatus: resolution.status,
259
+ };
260
+ }
261
+ if (resolution.status === 'ambiguous') return {
262
+ category: 'ambiguous_operation',
263
+ message: 'Ambiguous contextual operation candidates',
264
+ resolutionStatus: resolution.status,
265
+ };
266
+ return {
267
+ category: resolution.status === 'unresolved'
268
+ ? 'no_matching_operation' : 'other_blocker',
269
+ message: resolution.status === 'unresolved'
270
+ ? 'No contextual operation candidate matched'
271
+ : 'Contextual operation resolution is blocked',
272
+ resolutionStatus: resolution.status,
273
+ };
274
+ }
275
+
276
+ function historicalState(
277
+ state: ContextualRuntimeState,
278
+ ): ContextualRuntimeState {
279
+ return { ...state, phase: 'before_runtime_substitution' };
280
+ }
281
+
282
+ function missingVariableKeys(reasons: string[]): string[] {
283
+ return normalizedKeys(reasons.flatMap((reason) =>
284
+ reason.startsWith('missing_variable:')
285
+ ? [reason.slice('missing_variable:'.length)] : []));
286
+ }
287
+
288
+ function normalizedKeys(keys: readonly string[]): string[] {
289
+ return [...new Set(keys.filter((key) => key.length > 0))].sort();
290
+ }
291
+
292
+ function withLeadingSlash(value: string): string {
293
+ return value.startsWith('/') ? value : `/${value}`;
294
+ }