@saptools/service-flow 0.1.51 → 0.1.53

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 +13 -0
  2. package/README.md +16 -14
  3. package/TECHNICAL-NOTE.md +18 -6
  4. package/dist/{chunk-YZJKE5UX.js → chunk-LFH7C46B.js} +4290 -1261
  5. package/dist/chunk-LFH7C46B.js.map +1 -0
  6. package/dist/cli.js +435 -515
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +45 -7
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/000-clean.ts +82 -0
  12. package/src/cli/001-doctor-projection.ts +140 -0
  13. package/src/cli/doctor.ts +20 -3
  14. package/src/cli.ts +75 -57
  15. package/src/db/connection.ts +1 -1
  16. package/src/db/migrations.ts +5 -3
  17. package/src/db/repositories.ts +130 -24
  18. package/src/db/schema.ts +7 -2
  19. package/src/indexer/repository-indexer.ts +57 -29
  20. package/src/indexer/workspace-indexer.ts +84 -6
  21. package/src/linker/000-implementation-candidates.ts +641 -0
  22. package/src/linker/001-implementation-evidence-projection.ts +119 -0
  23. package/src/linker/002-call-evidence.ts +226 -0
  24. package/src/linker/cross-repo-linker.ts +27 -441
  25. package/src/linker/dynamic-edge-resolver.ts +35 -0
  26. package/src/linker/external-http-target.ts +24 -3
  27. package/src/linker/helper-package-linker.ts +18 -2
  28. package/src/linker/service-resolver.ts +45 -2
  29. package/src/output/doctor-output.ts +13 -4
  30. package/src/output/table-output.ts +33 -5
  31. package/src/parsers/cds-parser.ts +8 -2
  32. package/src/parsers/decorator-parser.ts +382 -48
  33. package/src/parsers/handler-registration-parser.ts +38 -21
  34. package/src/parsers/imported-wrapper-parser.ts +18 -5
  35. package/src/parsers/outbound-call-parser.ts +25 -8
  36. package/src/parsers/package-json-parser.ts +36 -11
  37. package/src/parsers/service-binding-parser-helpers.ts +8 -1
  38. package/src/parsers/service-binding-parser.ts +6 -3
  39. package/src/parsers/symbol-parser.ts +13 -3
  40. package/src/parsers/ts-project.ts +54 -0
  41. package/src/trace/000-dynamic-target-types.ts +96 -0
  42. package/src/trace/001-dynamic-identity.ts +308 -0
  43. package/src/trace/002-trace-diagnostics.ts +54 -0
  44. package/src/trace/003-dynamic-references.ts +283 -0
  45. package/src/trace/004-dynamic-candidate-sources.ts +155 -0
  46. package/src/trace/005-implementation-selection.ts +187 -0
  47. package/src/trace/006-contextual-projection.ts +30 -0
  48. package/src/trace/007-implementation-start-diagnostic.ts +61 -0
  49. package/src/trace/dynamic-targets.ts +582 -306
  50. package/src/trace/evidence.ts +331 -46
  51. package/src/trace/implementation-hints.ts +148 -8
  52. package/src/trace/selectors.ts +551 -3
  53. package/src/trace/trace-engine.ts +129 -135
  54. package/src/types.ts +27 -1
  55. package/src/utils/000-bounded-projection.ts +161 -0
  56. package/dist/chunk-YZJKE5UX.js.map +0 -1
@@ -0,0 +1,119 @@
1
+ import {
2
+ projectBounded,
3
+ type BoundedProjection,
4
+ } from '../utils/000-bounded-projection.js';
5
+
6
+ export function boundedImplementationEvidence(
7
+ evidence: Record<string, unknown>,
8
+ targetCandidateCount: number,
9
+ ): Record<string, unknown> {
10
+ const candidates = recordArray(evidence.candidates);
11
+ const candidateProjection = projectBounded(candidates, compareCandidateEvidence);
12
+ const families = recordArray(evidence.candidateFamilies);
13
+ const familyProjection = projectBounded(families, compareFamilies);
14
+ const hints = recordArray(evidence.implementationHintSuggestions);
15
+ const hintProjection = projectBounded(hints, compareHints);
16
+ const hintCount = Math.max(
17
+ numberValue(evidence.implementationHintSuggestionCount),
18
+ hintProjection.totalCount,
19
+ );
20
+ const targets = Math.max(0, targetCandidateCount);
21
+ return {
22
+ ...evidence,
23
+ candidates: candidateProjection.items.map(boundedCandidateEvidence),
24
+ candidateCount: candidateProjection.totalCount,
25
+ shownCandidateCount: candidateProjection.shownCount,
26
+ omittedCandidateCount: candidateProjection.omittedCount,
27
+ candidateFamilies: familyProjection.items.map(boundedFamilyEvidence),
28
+ candidateFamilyCount: familyProjection.totalCount,
29
+ shownCandidateFamilyCount: familyProjection.shownCount,
30
+ omittedCandidateFamilyCount: familyProjection.omittedCount,
31
+ implementationHintSuggestions: hintProjection.items,
32
+ implementationHintSuggestionCount: hintCount,
33
+ shownImplementationHintSuggestionCount: hintProjection.shownCount,
34
+ omittedImplementationHintSuggestionCount: Math.max(0, hintCount - hintProjection.shownCount),
35
+ candidateTargetCount: targets,
36
+ shownCandidateTargetCount: Math.min(targets, candidateProjection.shownCount),
37
+ omittedCandidateTargetCount: Math.max(0, targets - candidateProjection.shownCount),
38
+ };
39
+ }
40
+
41
+ export function boundedImplementationTargetIds(
42
+ candidates: Array<Record<string, unknown>>,
43
+ ): BoundedProjection<string> {
44
+ const projection = projectBounded(candidates, compareTargetCandidates);
45
+ return {
46
+ totalCount: projection.totalCount,
47
+ shownCount: projection.shownCount,
48
+ omittedCount: projection.omittedCount,
49
+ items: projection.items.map((candidate) => String(candidate.methodId ?? '')),
50
+ };
51
+ }
52
+
53
+ function boundedCandidateEvidence(candidate: Record<string, unknown>): Record<string, unknown> {
54
+ const registrations = recordArray(candidate.registrations);
55
+ const projection = projectBounded(registrations, compareRegistrations);
56
+ return {
57
+ ...candidate,
58
+ registrations: projection.items,
59
+ registrationCount: projection.totalCount,
60
+ shownRegistrationCount: projection.shownCount,
61
+ omittedRegistrationCount: projection.omittedCount,
62
+ };
63
+ }
64
+
65
+ function boundedFamilyEvidence(family: Record<string, unknown>): Record<string, unknown> {
66
+ const repositories = stringArray(family.repositories);
67
+ const projection = projectBounded(repositories, (left, right) => left.localeCompare(right));
68
+ return {
69
+ ...family,
70
+ repositories: projection.items,
71
+ repositoryCount: projection.totalCount,
72
+ shownRepositoryCount: projection.shownCount,
73
+ omittedRepositoryCount: projection.omittedCount,
74
+ };
75
+ }
76
+
77
+ function compareTargetCandidates(left: Record<string, unknown>, right: Record<string, unknown>): number {
78
+ return Number(right.score ?? 0) - Number(left.score ?? 0)
79
+ || String(left.className ?? '').localeCompare(String(right.className ?? ''))
80
+ || Number(left.methodId ?? 0) - Number(right.methodId ?? 0);
81
+ }
82
+
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
+ function compareFamilies(left: Record<string, unknown>, right: Record<string, unknown>): number {
89
+ return String(left.packageName ?? '').localeCompare(String(right.packageName ?? ''))
90
+ || String(left.reason ?? '').localeCompare(String(right.reason ?? ''));
91
+ }
92
+
93
+ function compareHints(left: Record<string, unknown>, right: Record<string, unknown>): number {
94
+ return String(left.cli ?? '').localeCompare(String(right.cli ?? ''))
95
+ || String(left.implementationRepo ?? '').localeCompare(String(right.implementationRepo ?? ''));
96
+ }
97
+
98
+ function compareRegistrations(left: Record<string, unknown>, right: Record<string, unknown>): number {
99
+ return String(left.file ?? '').localeCompare(String(right.file ?? ''))
100
+ || Number(left.line ?? 0) - Number(right.line ?? 0)
101
+ || Number(left.id ?? 0) - Number(right.id ?? 0);
102
+ }
103
+
104
+ function recordArray(value: unknown): Array<Record<string, unknown>> {
105
+ return Array.isArray(value)
106
+ ? value.filter((item): item is Record<string, unknown> =>
107
+ Boolean(item && typeof item === 'object' && !Array.isArray(item)))
108
+ : [];
109
+ }
110
+
111
+ function stringArray(value: unknown): string[] {
112
+ return Array.isArray(value)
113
+ ? value.filter((item): item is string => typeof item === 'string')
114
+ : [];
115
+ }
116
+
117
+ function numberValue(value: unknown): number {
118
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
119
+ }
@@ -0,0 +1,226 @@
1
+ import type {
2
+ NormalizedODataOperationPath,
3
+ ODataPathIntent,
4
+ } from './odata-path-normalizer.js';
5
+ import {
6
+ boundCandidateLikeEvidence,
7
+ projectBounded,
8
+ type BoundedProjection,
9
+ } from '../utils/000-bounded-projection.js';
10
+
11
+ export interface LinkedOperationResolution {
12
+ target?: {
13
+ repoName?: string;
14
+ servicePath?: string;
15
+ operationPath?: string;
16
+ operationName?: string;
17
+ };
18
+ candidates: unknown[];
19
+ status: string;
20
+ reasons: string[];
21
+ }
22
+
23
+ export function linkedCallEvidence(
24
+ call: Record<string, unknown>,
25
+ resolution: LinkedOperationResolution,
26
+ servicePath: string | undefined,
27
+ operationPath: string | undefined,
28
+ destination: string | undefined,
29
+ normalized: NormalizedODataOperationPath | undefined,
30
+ intent: ODataPathIntent | undefined,
31
+ ): Record<string, unknown> {
32
+ const candidates = boundedCallCandidates(resolution.candidates);
33
+ return {
34
+ ...callLocationEvidence(call),
35
+ ...selectedBindingEvidence(call),
36
+ ...routingEvidence(call, servicePath, operationPath, destination, normalized, intent),
37
+ ...candidateEvidence(candidates, resolution),
38
+ outboundEvidence: boundCandidateLikeEvidence(objectJson(call.evidence_json) ?? {}),
39
+ analysisCompleteness: call.unresolved_reason ? 'partial' : 'complete',
40
+ parserWarning: call.unresolved_reason
41
+ ? { code: 'parser_warning', message: call.unresolved_reason }
42
+ : undefined,
43
+ };
44
+ }
45
+
46
+ export function ambiguousPathCandidates(
47
+ pathAnalysis: Record<string, unknown>,
48
+ ): BoundedProjection<string> {
49
+ const values = Array.isArray(pathAnalysis.candidateRawPaths)
50
+ ? pathAnalysis.candidateRawPaths.filter((value): value is string =>
51
+ typeof value === 'string')
52
+ : [];
53
+ return projectBounded(values, (left, right) => left.localeCompare(right));
54
+ }
55
+
56
+ export function objectJson(value: unknown): Record<string, unknown> | undefined {
57
+ const parsed = parseJson(value);
58
+ return isRecord(parsed) ? parsed : undefined;
59
+ }
60
+
61
+ export function objectValue(value: unknown): Record<string, unknown> | undefined {
62
+ return isRecord(value) ? value : undefined;
63
+ }
64
+
65
+ function callLocationEvidence(call: Record<string, unknown>): Record<string, unknown> {
66
+ return {
67
+ sourceFile: call.source_file,
68
+ sourceLine: call.source_line,
69
+ file: call.source_file,
70
+ line: call.source_line,
71
+ callId: call.id,
72
+ repo: call.repoName,
73
+ };
74
+ }
75
+
76
+ function selectedBindingEvidence(call: Record<string, unknown>): Record<string, unknown> {
77
+ if (!call.selectedBindingId) return {};
78
+ return {
79
+ selectedBindingId: call.selectedBindingId,
80
+ selectedBinding: {
81
+ bindingId: call.selectedBindingId,
82
+ alias: call.alias,
83
+ aliasExpr: call.aliasExpr,
84
+ destinationExpr: call.destinationExpr,
85
+ servicePathExpr: call.servicePathExpr,
86
+ sourceFile: call.bindingSourceFile,
87
+ sourceLine: call.bindingSourceLine,
88
+ helperChain: parseJson(call.helperChainJson),
89
+ },
90
+ };
91
+ }
92
+
93
+ function routingEvidence(
94
+ call: Record<string, unknown>,
95
+ servicePath: string | undefined,
96
+ operationPath: string | undefined,
97
+ destination: string | undefined,
98
+ normalized: NormalizedODataOperationPath | undefined,
99
+ intent: ODataPathIntent | undefined,
100
+ ): Record<string, unknown> {
101
+ const routingPlaceholderKeys = placeholderKeys([
102
+ servicePath,
103
+ destination,
104
+ stringValue(call.aliasExpr),
105
+ stringValue(call.alias),
106
+ ]);
107
+ return {
108
+ serviceAlias: call.alias,
109
+ serviceAliasExpr: call.aliasExpr,
110
+ destination,
111
+ servicePath,
112
+ operationPath,
113
+ rawOperationPath: normalized?.wasInvocation
114
+ ? normalized.rawOperationPath
115
+ : intent?.rawPath,
116
+ normalizedOperationPath: normalized?.wasInvocation
117
+ ? normalized.normalizedOperationPath
118
+ : undefined,
119
+ invocationArguments: normalized?.wasInvocation
120
+ ? normalized.invocationArguments
121
+ : undefined,
122
+ invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length
123
+ ? normalized.invocationArgumentPlaceholderKeys
124
+ : undefined,
125
+ routingPlaceholderKeys: routingPlaceholderKeys.length
126
+ ? routingPlaceholderKeys
127
+ : undefined,
128
+ odataOperationNormalizationReason: normalized?.normalizationReason,
129
+ odataOperationNormalizationRejectedReason: normalized?.normalizationRejectedReason,
130
+ localServiceName: call.local_service_name,
131
+ localServiceLookup: call.local_service_lookup,
132
+ aliasChain: parseJson(call.alias_chain_json),
133
+ transport: call.call_type === 'local_service_call' ? 'local' : undefined,
134
+ helperChain: parseJson(call.helperChainJson),
135
+ odataPathIntent: intent,
136
+ queryStringPresent: intent?.hasQueryString || undefined,
137
+ queryPlaceholderKeys: intent?.placeholderKeys.length
138
+ ? intent.placeholderKeys
139
+ : undefined,
140
+ bindingHasDynamicExpression: Boolean(Number(call.isDynamic ?? 0)) || undefined,
141
+ };
142
+ }
143
+
144
+ function candidateEvidence(
145
+ candidates: ReturnType<typeof boundedCallCandidates>,
146
+ resolution: LinkedOperationResolution,
147
+ ): Record<string, unknown> {
148
+ return {
149
+ targetRepo: resolution.target?.repoName,
150
+ targetServicePath: resolution.target?.servicePath,
151
+ targetOperationPath: resolution.target?.operationPath,
152
+ targetOperation: resolution.target?.operationName,
153
+ candidates: candidates.items,
154
+ candidateScores: compactCandidateScores(candidates.items),
155
+ candidateCount: candidates.totalCount,
156
+ shownCandidateCount: candidates.shownCount,
157
+ omittedCandidateCount: candidates.omittedCount,
158
+ candidateScoreCount: candidates.totalCount,
159
+ shownCandidateScoreCount: candidates.shownCount,
160
+ omittedCandidateScoreCount: candidates.omittedCount,
161
+ resolutionStatus: resolution.status,
162
+ resolutionReasons: resolution.reasons,
163
+ };
164
+ }
165
+
166
+ function boundedCallCandidates(
167
+ candidates: unknown[],
168
+ ): BoundedProjection<Record<string, unknown>> {
169
+ const rows = candidates.flatMap((candidate): Array<Record<string, unknown>> => {
170
+ const row = objectValue(candidate);
171
+ return row ? [row] : [];
172
+ });
173
+ return projectBounded(rows, compareCallCandidate);
174
+ }
175
+
176
+ function compareCallCandidate(
177
+ left: Record<string, unknown>,
178
+ right: Record<string, unknown>,
179
+ ): number {
180
+ return Number(right.score ?? 0) - Number(left.score ?? 0)
181
+ || String(left.repoName ?? '').localeCompare(String(right.repoName ?? ''))
182
+ || String(left.servicePath ?? '').localeCompare(String(right.servicePath ?? ''))
183
+ || String(left.operationPath ?? '').localeCompare(String(right.operationPath ?? ''))
184
+ || Number(left.operationId ?? 0) - Number(right.operationId ?? 0);
185
+ }
186
+
187
+ function compactCandidateScores(
188
+ candidates: Array<Record<string, unknown>>,
189
+ ): Array<Record<string, unknown>> {
190
+ return candidates.map((candidate) => ({
191
+ repo: candidate.repoName,
192
+ servicePath: candidate.servicePath,
193
+ operationPath: candidate.operationPath,
194
+ score: candidate.score,
195
+ reasons: Array.isArray(candidate.reasons)
196
+ ? candidate.reasons.filter((reason): reason is string =>
197
+ typeof reason === 'string')
198
+ : ['operation_path_match'],
199
+ }));
200
+ }
201
+
202
+ function placeholderKeys(values: Array<string | undefined>): string[] {
203
+ const keys = values.flatMap((value) => [...(value ?? '')
204
+ .matchAll(/\$\{([^}]*)\}/g)]
205
+ .map((match) => (match[1] ?? '').trim())
206
+ .filter(Boolean));
207
+ return [...new Set(keys)].sort();
208
+ }
209
+
210
+ function parseJson(value: unknown): unknown {
211
+ if (!value) return undefined;
212
+ try {
213
+ const parsed: unknown = JSON.parse(String(value));
214
+ return parsed;
215
+ } catch {
216
+ return undefined;
217
+ }
218
+ }
219
+
220
+ function stringValue(value: unknown): string | undefined {
221
+ return typeof value === 'string' ? value : undefined;
222
+ }
223
+
224
+ function isRecord(value: unknown): value is Record<string, unknown> {
225
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
226
+ }