@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
@@ -1,4 +1,5 @@
1
1
  import type { ImplementationHint } from '../types.js';
2
+ import { projectBounded } from '../utils/000-bounded-projection.js';
2
3
 
3
4
  interface Candidate {
4
5
  accepted?: boolean;
@@ -25,6 +26,13 @@ export interface ImplementationSelection {
25
26
  evidence: Record<string, unknown>;
26
27
  }
27
28
 
29
+ export interface ImplementationHintSuggestionProjection {
30
+ suggestions: Array<Record<string, unknown>>;
31
+ suggestionCount: number;
32
+ shownSuggestionCount: number;
33
+ omittedSuggestionCount: number;
34
+ }
35
+
28
36
  export function parseImplementationHint(value: string): ImplementationHint {
29
37
  const hint: Partial<ImplementationHint> = {};
30
38
  for (const part of value.split(',')) {
@@ -40,8 +48,9 @@ export function selectImplementation(
40
48
  rawEvidence: Record<string, unknown>,
41
49
  hints: ImplementationHint[] | undefined,
42
50
  legacyRepo: string | undefined,
51
+ canonicalEvidence?: Record<string, unknown>,
43
52
  ): ImplementationSelection {
44
- const evidence = asEvidence(rawEvidence);
53
+ const evidence = asEvidence(canonicalEvidence ?? rawEvidence);
45
54
  const scoped = hints ?? [];
46
55
  const matchingHints = scoped.filter((hint) => hintMatchesEdge(hint, evidence));
47
56
  if (matchingHints.length === 0) {
@@ -50,14 +59,18 @@ export function selectImplementation(
50
59
  return { blocksAutomatic: false, evidence: { status: 'not_matched', reason, strategy: 'scoped_implementation_hint' } };
51
60
  }
52
61
  if (matchingHints.length > 1) {
62
+ const projection = projectBounded(matchingHints, compareHints);
53
63
  return {
54
64
  blocksAutomatic: true,
55
65
  evidence: {
56
66
  status: 'tied',
57
67
  reason: 'multiple_scoped_hints_matched_edge',
58
68
  strategy: 'scoped_implementation_hint',
59
- matchedHints: matchingHints,
69
+ matchedHints: projection.items,
60
70
  candidateCount: matchingHints.length,
71
+ matchedHintCount: projection.totalCount,
72
+ shownMatchedHintCount: projection.shownCount,
73
+ omittedMatchedHintCount: projection.omittedCount,
61
74
  },
62
75
  };
63
76
  }
@@ -67,26 +80,48 @@ export function selectImplementation(
67
80
 
68
81
  export function implementationHintDiagnostic(
69
82
  selection: ImplementationSelection,
70
- suggestions?: unknown,
83
+ suggestionEvidence?: unknown,
71
84
  ): Record<string, unknown> | undefined {
72
85
  if (!selection.blocksAutomatic || selection.methodId) return undefined;
86
+ const suggestions = projectedSuggestions(suggestionEvidence);
73
87
  return {
74
88
  severity: 'warning',
75
89
  code: 'implementation_hint_mismatch',
76
90
  message: 'Implementation hint did not select exactly one viable candidate',
77
91
  hintStatus: selection.evidence.status,
78
92
  candidateCount: selection.evidence.candidateCount,
79
- implementationHintSuggestions: Array.isArray(suggestions) && suggestions.length > 0 ? suggestions : undefined,
93
+ implementationHintSuggestions: suggestions.suggestions.length > 0
94
+ ? suggestions.suggestions
95
+ : undefined,
96
+ implementationHintSuggestionCount: suggestions.suggestionCount,
97
+ shownImplementationHintSuggestionCount: suggestions.shownSuggestionCount,
98
+ omittedImplementationHintSuggestionCount: suggestions.omittedSuggestionCount,
80
99
  implementationSelection: selection.evidence,
81
100
  };
82
101
  }
83
102
 
84
103
  export function implementationHintSuggestions(rawEvidence: Record<string, unknown>): Array<Record<string, unknown>> {
104
+ return implementationHintSuggestionProjection(rawEvidence).suggestions;
105
+ }
106
+
107
+ export function implementationHintSuggestionProjection(
108
+ rawEvidence: Record<string, unknown>,
109
+ ): ImplementationHintSuggestionProjection {
85
110
  const evidence = asEvidence(rawEvidence);
86
111
  const accepted = (evidence.candidates ?? []).filter((candidate) => candidate.accepted);
87
- if (accepted.length < 2) return [];
112
+ if (accepted.length < 2) {
113
+ return {
114
+ suggestions: [],
115
+ suggestionCount: 0,
116
+ shownSuggestionCount: 0,
117
+ omittedSuggestionCount: 0,
118
+ };
119
+ }
88
120
  const repos = selectableRepositories(accepted);
89
- return accepted
121
+ const repositoryProjection = projectBounded(
122
+ repos, (left, right) => left.localeCompare(right),
123
+ );
124
+ const suggestions = accepted
90
125
  .flatMap((candidate) => {
91
126
  const repo = candidate.handlerPackage?.name;
92
127
  if (!repo || !repos.includes(repo)) return [];
@@ -96,12 +131,71 @@ export function implementationHintSuggestions(rawEvidence: Record<string, unknow
96
131
  operationPath: hint.operationPath,
97
132
  ambiguityReason: evidence.ambiguityReasons?.[0],
98
133
  candidateFamily: hint.candidateFamily,
99
- selectableImplementationRepositories: repos,
134
+ selectableImplementationRepositories: repositoryProjection.items,
135
+ selectableImplementationRepositoryCount: repositoryProjection.totalCount,
136
+ shownSelectableImplementationRepositoryCount:
137
+ repositoryProjection.shownCount,
138
+ omittedSelectableImplementationRepositoryCount:
139
+ repositoryProjection.omittedCount,
100
140
  implementationRepo: repo,
101
141
  hint,
102
142
  cli: `--implementation-hint ${hintString(hint)}`,
103
143
  }];
104
144
  });
145
+ const projection = projectBounded(suggestions, compareSuggestion);
146
+ return {
147
+ suggestions: projection.items,
148
+ suggestionCount: projection.totalCount,
149
+ shownSuggestionCount: projection.shownCount,
150
+ omittedSuggestionCount: projection.omittedCount,
151
+ };
152
+ }
153
+
154
+ function projectedSuggestions(value: unknown): ImplementationHintSuggestionProjection {
155
+ const evidence = objectRecord(value);
156
+ const values = Array.isArray(value)
157
+ ? recordSuggestions(value)
158
+ : recordSuggestions(evidence.implementationHintSuggestions);
159
+ const projection = projectBounded(values, compareSuggestion);
160
+ const total = Math.max(
161
+ numericValue(evidence.implementationHintSuggestionCount),
162
+ projection.totalCount,
163
+ );
164
+ return {
165
+ suggestions: projection.items,
166
+ suggestionCount: total,
167
+ shownSuggestionCount: projection.shownCount,
168
+ omittedSuggestionCount: Math.max(0, total - projection.shownCount),
169
+ };
170
+ }
171
+
172
+ function compareHints(left: ImplementationHint, right: ImplementationHint): number {
173
+ return hintString(left).localeCompare(hintString(right));
174
+ }
175
+
176
+ function compareSuggestion(
177
+ left: Record<string, unknown>,
178
+ right: Record<string, unknown>,
179
+ ): number {
180
+ return String(left.cli ?? '').localeCompare(String(right.cli ?? ''))
181
+ || String(left.implementationRepo ?? '').localeCompare(
182
+ String(right.implementationRepo ?? ''),
183
+ );
184
+ }
185
+
186
+ function objectRecord(value: unknown): Record<string, unknown> {
187
+ return isRecord(value) ? value : {};
188
+ }
189
+
190
+ function recordSuggestions(value: unknown): Array<Record<string, unknown>> {
191
+ return Array.isArray(value)
192
+ ? value.filter((item): item is Record<string, unknown> =>
193
+ Boolean(item && typeof item === 'object' && !Array.isArray(item)))
194
+ : [];
195
+ }
196
+
197
+ function numericValue(value: unknown): number {
198
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
105
199
  }
106
200
 
107
201
  function selectableRepositories(candidates: Candidate[]): string[] {
@@ -219,5 +313,51 @@ function legacyHint(implementationRepo: string): ImplementationHint {
219
313
  }
220
314
 
221
315
  function asEvidence(value: Record<string, unknown>): EdgeEvidence {
222
- return value as EdgeEvidence;
316
+ return {
317
+ servicePath: stringValue(value.servicePath),
318
+ operationPath: stringValue(value.operationPath),
319
+ ambiguityReasons: stringArray(value.ambiguityReasons),
320
+ candidateFamilies: candidateFamilies(value.candidateFamilies),
321
+ candidates: candidates(value.candidates),
322
+ modelPackage: packageValue(value.modelPackage),
323
+ };
324
+ }
325
+
326
+ function candidates(value: unknown): Candidate[] {
327
+ return recordSuggestions(value).map((candidate) => ({
328
+ accepted: candidate.accepted === true,
329
+ methodId: numericValue(candidate.methodId) || undefined,
330
+ sourceFile: stringValue(candidate.sourceFile),
331
+ handlerPackage: packageValue(candidate.handlerPackage),
332
+ modelPackage: packageValue(candidate.modelPackage),
333
+ servicePath: stringValue(candidate.servicePath),
334
+ operationPath: stringValue(candidate.operationPath),
335
+ }));
336
+ }
337
+
338
+ function candidateFamilies(value: unknown): Array<{ packageName?: string }> {
339
+ return recordSuggestions(value).map((family) => ({
340
+ packageName: stringValue(family.packageName),
341
+ }));
342
+ }
343
+
344
+ function packageValue(value: unknown): { name?: string; packageName?: string } | undefined {
345
+ const candidate = objectRecord(value);
346
+ const name = stringValue(candidate.name);
347
+ const packageName = stringValue(candidate.packageName);
348
+ return name || packageName ? { name, packageName } : undefined;
349
+ }
350
+
351
+ function stringArray(value: unknown): string[] {
352
+ return Array.isArray(value)
353
+ ? value.filter((item): item is string => typeof item === 'string')
354
+ : [];
355
+ }
356
+
357
+ function stringValue(value: unknown): string | undefined {
358
+ return typeof value === 'string' ? value : undefined;
359
+ }
360
+
361
+ function isRecord(value: unknown): value is Record<string, unknown> {
362
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
223
363
  }