@saptools/service-flow 0.1.67 → 0.1.68

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 (80) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +20 -8
  3. package/TECHNICAL-NOTE.md +27 -0
  4. package/dist/{chunk-ZQABU7MR.js → chunk-AEM4JY22.js} +12676 -6714
  5. package/dist/chunk-AEM4JY22.js.map +1 -0
  6. package/dist/cli.js +2325 -413
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +61 -2
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/003-doctor-package-resolution.ts +68 -0
  12. package/src/cli/doctor.ts +25 -14
  13. package/src/db/000-call-fact-repository.ts +475 -342
  14. package/src/db/001-fact-lifecycle.ts +60 -30
  15. package/src/db/002-fact-json-inventory.ts +169 -0
  16. package/src/db/003-current-fact-semantics.ts +698 -0
  17. package/src/db/004-package-target-invalidation.ts +173 -0
  18. package/src/db/005-schema-structure.ts +201 -0
  19. package/src/db/006-relative-symbol-resolution.ts +443 -0
  20. package/src/db/007-package-fact-semantics.ts +573 -0
  21. package/src/db/008-relative-fact-semantics.ts +207 -0
  22. package/src/db/009-binding-fact-semantics.ts +347 -0
  23. package/src/db/010-package-symbol-surface-semantics.ts +320 -0
  24. package/src/db/011-symbol-call-semantics.ts +144 -0
  25. package/src/db/012-binding-reference-proof.ts +264 -0
  26. package/src/db/migrations.ts +16 -3
  27. package/src/db/repositories.ts +113 -6
  28. package/src/db/schema.ts +4 -2
  29. package/src/index.ts +12 -0
  30. package/src/indexer/repository-indexer.ts +72 -11
  31. package/src/indexer/workspace-indexer.ts +123 -32
  32. package/src/linker/003-package-import-symbol-resolver.ts +363 -131
  33. package/src/linker/004-event-subscription-handler-linker.ts +2 -0
  34. package/src/linker/005-odata-path-structure.ts +371 -0
  35. package/src/linker/cross-repo-linker.ts +2 -1
  36. package/src/linker/odata-path-normalizer.ts +273 -180
  37. package/src/linker/service-resolver.ts +197 -77
  38. package/src/parsers/002-symbol-import-bindings.ts +516 -0
  39. package/src/parsers/003-package-public-surface.ts +661 -0
  40. package/src/parsers/004-fact-identity.ts +108 -0
  41. package/src/parsers/005-event-subscription-facts.ts +281 -0
  42. package/src/parsers/006-binding-identity.ts +343 -0
  43. package/src/parsers/007-source-fact-reconciliation.ts +105 -0
  44. package/src/parsers/008-package-surface-publication.ts +82 -0
  45. package/src/parsers/009-symbol-call-facts.ts +528 -0
  46. package/src/parsers/010-package-public-surface-analysis.ts +352 -0
  47. package/src/parsers/011-binding-lexical-scope.ts +583 -0
  48. package/src/parsers/012-package-fact-contract.ts +306 -0
  49. package/src/parsers/013-executable-body-eligibility.ts +35 -0
  50. package/src/parsers/014-service-binding-helper-flow.ts +240 -0
  51. package/src/parsers/015-service-binding-collector.ts +693 -0
  52. package/src/parsers/016-local-symbol-reference.ts +261 -0
  53. package/src/parsers/017-symbol-derived-contexts.ts +268 -0
  54. package/src/parsers/018-package-commonjs-syntax.ts +142 -0
  55. package/src/parsers/019-binding-assignment-targets.ts +76 -0
  56. package/src/parsers/020-stable-local-value.ts +217 -0
  57. package/src/parsers/021-binding-visibility.ts +152 -0
  58. package/src/parsers/operation-path-analysis.ts +6 -1
  59. package/src/parsers/outbound-call-parser.ts +19 -6
  60. package/src/parsers/package-json-parser.ts +45 -3
  61. package/src/parsers/service-binding-parser-helpers.ts +86 -15
  62. package/src/parsers/service-binding-parser.ts +147 -597
  63. package/src/parsers/symbol-parser.ts +482 -353
  64. package/src/trace/002-trace-diagnostics.ts +36 -1
  65. package/src/trace/016-compact-projector.ts +16 -17
  66. package/src/trace/020-compact-field-projection.ts +48 -37
  67. package/src/trace/021-compact-decision-normalization.ts +105 -0
  68. package/src/trace/022-trace-fact-preflight.ts +21 -0
  69. package/src/trace/023-nested-event-scopes.ts +23 -0
  70. package/src/trace/024-compact-observation-decision.ts +76 -0
  71. package/src/trace/025-trace-implementation-scope.ts +123 -0
  72. package/src/trace/026-trace-start-scope.ts +335 -0
  73. package/src/trace/027-trace-scope-execution.ts +566 -0
  74. package/src/trace/028-trace-operation-execution.ts +336 -0
  75. package/src/trace/029-trace-start-implementation.ts +172 -0
  76. package/src/trace/trace-engine.ts +122 -624
  77. package/src/types.ts +56 -0
  78. package/src/utils/001-placeholders.ts +188 -10
  79. package/src/version.ts +1 -1
  80. package/dist/chunk-ZQABU7MR.js.map +0 -1
@@ -22,6 +22,17 @@ export interface OperationResolution {
22
22
  candidates: OperationTarget[];
23
23
  reasons: string[];
24
24
  }
25
+ interface OperationSignals {
26
+ servicePath?: string;
27
+ alias?: string;
28
+ destination?: string;
29
+ operationPath?: string;
30
+ serviceName?: string;
31
+ repoId?: number;
32
+ hasExplicitOverride?: boolean;
33
+ isDynamic?: boolean;
34
+ localServiceLookup?: string;
35
+ }
25
36
  function rows(
26
37
  db: Db,
27
38
  operationPath: string,
@@ -60,31 +71,30 @@ function operationMatches(candidate: OperationTarget, operationPath: string | un
60
71
  }
61
72
  export function resolveOperation(
62
73
  db: Db,
63
- signals: {
64
- servicePath?: string;
65
- alias?: string;
66
- destination?: string;
67
- operationPath?: string;
68
- serviceName?: string;
69
- repoId?: number;
70
- hasExplicitOverride?: boolean;
71
- isDynamic?: boolean;
72
- localServiceLookup?: string;
73
- },
74
+ signals: OperationSignals,
74
75
  workspaceId?: number,
75
76
  ): OperationResolution {
77
+ const early = earlyOperationResolution(db, signals, workspaceId);
78
+ if (early) return early;
79
+ const selected = selectOperationCandidates(db, signals, workspaceId);
80
+ if (!Array.isArray(selected)) return selected;
81
+ const candidates = scoreOperationCandidates(selected, signals);
82
+ return finalOperationResolution(candidates, signals);
83
+ }
84
+
85
+ function earlyOperationResolution(
86
+ db: Db,
87
+ signals: OperationSignals,
88
+ workspaceId: number | undefined,
89
+ ): OperationResolution | undefined {
76
90
  const missing = [signals.servicePath, signals.alias, signals.destination, signals.operationPath]
77
91
  .flatMap(extractPlaceholderKeys);
78
92
  if (missing.length > 0)
79
93
  return {
80
94
  status: 'dynamic',
81
- candidates: signals.operationPath
82
- ? rows(db, signals.operationPath, workspaceId).map((candidate) => ({
83
- ...candidate,
84
- score: 0.2,
85
- reasons: ['operation_path_match'],
86
- }))
87
- : [],
95
+ candidates: dynamicOperationCandidates(
96
+ db, signals.operationPath, workspaceId,
97
+ ),
88
98
  reasons: [...new Set(missing)].map((name) => `missing_variable:${name}`),
89
99
  };
90
100
  if (!signals.operationPath)
@@ -93,72 +103,184 @@ export function resolveOperation(
93
103
  candidates: [],
94
104
  reasons: ['missing_operation_path'],
95
105
  };
96
- const allCandidates = rows(db, signals.operationPath, workspaceId).map((c) => ({
106
+ return undefined;
107
+ }
108
+
109
+ function dynamicOperationCandidates(
110
+ db: Db,
111
+ operationPath: string | undefined,
112
+ workspaceId: number | undefined,
113
+ ): OperationTarget[] {
114
+ if (!operationPath || extractPlaceholderKeys(operationPath).length > 0)
115
+ return [];
116
+ return rows(db, operationPath, workspaceId).map((candidate) => ({
117
+ ...candidate,
118
+ score: 0.2,
119
+ reasons: ['operation_path_match'],
120
+ }));
121
+ }
122
+
123
+ function selectOperationCandidates(
124
+ db: Db,
125
+ signals: OperationSignals,
126
+ workspaceId?: number,
127
+ ): OperationTarget[] | OperationResolution {
128
+ const operationPath = signals.operationPath ?? '';
129
+ const allCandidates = rows(db, operationPath, workspaceId).map((c) => ({
97
130
  ...c,
98
131
  score: 0.2,
99
132
  reasons: ['operation_path_match'],
100
133
  }));
101
134
  let candidates = allCandidates.filter((c) => matchesLocalRepo(db, c.operationId, signals.repoId));
102
- if (candidates.length === 0 && signals.repoId !== undefined && signals.serviceName) {
103
- candidates = implementationContextCandidates(db, allCandidates, signals.repoId, signals.serviceName);
104
- if (candidates.length === 0)
105
- return {
106
- status: 'unresolved',
107
- candidates: allCandidates.filter((c) => serviceMatches(c, signals.serviceName)),
108
- reasons: allCandidates.some((c) => serviceMatches(c, signals.serviceName)) ? ['local_service_candidate_without_caller_ownership'] : ['no_operation_candidates'],
109
- };
110
- }
135
+ if (candidates.length === 0 && signals.repoId !== undefined
136
+ && signals.serviceName)
137
+ candidates = implementationContextCandidates(
138
+ db, allCandidates, signals.repoId, signals.serviceName,
139
+ );
140
+ if (candidates.length === 0 && signals.repoId !== undefined
141
+ && signals.serviceName)
142
+ return missingOwnedCandidate(allCandidates, signals.serviceName);
111
143
  if (candidates.length === 0)
112
144
  return {
113
145
  status: 'unresolved',
114
146
  candidates: [],
115
147
  reasons: ['no_operation_candidates'],
116
148
  };
117
- const hasStrongSignal = Boolean(
118
- signals.servicePath ||
119
- signals.serviceName ||
120
- signals.alias ||
121
- signals.destination ||
122
- signals.hasExplicitOverride,
123
- );
149
+ return candidates;
150
+ }
151
+
152
+ function missingOwnedCandidate(
153
+ candidates: OperationTarget[],
154
+ serviceName: string,
155
+ ): OperationResolution {
156
+ const matching = candidates.filter((candidate) =>
157
+ serviceMatches(candidate, serviceName));
158
+ return {
159
+ status: 'unresolved',
160
+ candidates: matching,
161
+ reasons: matching.length > 0
162
+ ? ['local_service_candidate_without_caller_ownership']
163
+ : ['no_operation_candidates'],
164
+ };
165
+ }
166
+
167
+ function scoreOperationCandidates(
168
+ candidates: OperationTarget[],
169
+ signals: OperationSignals,
170
+ ): OperationTarget[] {
124
171
  for (const c of candidates) {
125
- if (signals.servicePath && c.servicePath === signals.servicePath) {
126
- c.score += 0.75;
127
- c.reasons.push('exact_service_path');
128
- }
129
- if (signals.servicePath && c.servicePath !== signals.servicePath) {
130
- c.score -= 0.1;
131
- c.reasons.push('service_path_mismatch');
132
- }
133
- if (signals.serviceName) {
134
- const simple = signals.serviceName.split('.').at(-1) ?? signals.serviceName;
135
- if (c.qualifiedName === signals.serviceName) {
136
- c.score += 0.8;
137
- c.reasons.push('exact_local_qualified_service_name');
138
- } else if (c.serviceName === signals.serviceName || c.serviceName === simple) {
139
- c.score += 0.75;
140
- c.reasons.push('exact_local_simple_service_name');
141
- } else if (c.servicePath === signals.serviceName || c.servicePath === `/${signals.serviceName}` || c.servicePath === `/${simple}`) {
142
- c.score += 0.7;
143
- c.reasons.push('exact_local_service_path');
144
- } else if (c.servicePath.endsWith(`/${simple}`)) {
145
- c.score += candidates.filter((candidate) => candidate.servicePath.endsWith(`/${simple}`)).length === 1 ? 0.65 : 0.2;
146
- c.reasons.push('suffix_local_service_path');
147
- } else c.reasons.push('local_service_name_mismatch');
148
- }
149
- if (signals.hasExplicitOverride) {
150
- c.score += 0.2;
151
- c.reasons.push(signals.repoId !== undefined ? 'explicit_local_service_call' : 'explicit_dynamic_override');
152
- }
153
- if (signals.repoId !== undefined && candidates.length === 1 && signals.serviceName && c.reasons.includes('local_service_name_mismatch') && operationMatches(c, signals.operationPath)) {
154
- c.score = Math.max(c.score, 0.9);
155
- c.reasons.push('same_repo_unique_operation_path_with_lookup_mismatch');
156
- }
172
+ applyServicePathScore(c, signals.servicePath);
173
+ applyServiceNameScore(c, signals.serviceName, candidates);
174
+ applyOverrideScore(c, signals);
175
+ applyUniquePathRecovery(c, candidates, signals);
176
+ c.score = Math.max(0, Math.min(1, c.score));
157
177
  }
158
- for (const c of candidates) c.score = Math.max(0, Math.min(1, c.score));
159
178
  candidates.sort(
160
- (a, b) => b.score - a.score || a.repoName.localeCompare(b.repoName),
179
+ (a, b) => b.score - a.score || compareBinary(a.repoName, b.repoName),
180
+ );
181
+ return candidates;
182
+ }
183
+
184
+ function applyServicePathScore(
185
+ candidate: OperationTarget,
186
+ servicePath: string | undefined,
187
+ ): void {
188
+ if (!servicePath) return;
189
+ const exact = candidate.servicePath === servicePath;
190
+ candidate.score += exact ? 0.75 : -0.1;
191
+ candidate.reasons.push(exact ? 'exact_service_path' : 'service_path_mismatch');
192
+ }
193
+
194
+ function serviceNameScore(
195
+ candidate: OperationTarget,
196
+ serviceName: string,
197
+ candidates: readonly OperationTarget[],
198
+ ): { score: number; reason: string } {
199
+ const simple = serviceName.split('.').at(-1) ?? serviceName;
200
+ if (candidate.qualifiedName === serviceName)
201
+ return { score: 0.8, reason: 'exact_local_qualified_service_name' };
202
+ if (candidate.serviceName === serviceName || candidate.serviceName === simple)
203
+ return { score: 0.75, reason: 'exact_local_simple_service_name' };
204
+ if ([serviceName, `/${serviceName}`, `/${simple}`]
205
+ .includes(candidate.servicePath))
206
+ return { score: 0.7, reason: 'exact_local_service_path' };
207
+ if (candidate.servicePath.endsWith(`/${simple}`)) return {
208
+ score: candidates.filter((item) =>
209
+ item.servicePath.endsWith(`/${simple}`)).length === 1 ? 0.65 : 0.2,
210
+ reason: 'suffix_local_service_path',
211
+ };
212
+ return { score: 0, reason: 'local_service_name_mismatch' };
213
+ }
214
+
215
+ function applyServiceNameScore(
216
+ candidate: OperationTarget,
217
+ serviceName: string | undefined,
218
+ candidates: readonly OperationTarget[],
219
+ ): void {
220
+ if (!serviceName) return;
221
+ const signal = serviceNameScore(candidate, serviceName, candidates);
222
+ candidate.score += signal.score;
223
+ candidate.reasons.push(signal.reason);
224
+ }
225
+
226
+ function applyOverrideScore(
227
+ candidate: OperationTarget,
228
+ signals: OperationSignals,
229
+ ): void {
230
+ if (!signals.hasExplicitOverride) return;
231
+ candidate.score += 0.2;
232
+ candidate.reasons.push(signals.repoId !== undefined
233
+ ? 'explicit_local_service_call' : 'explicit_dynamic_override');
234
+ }
235
+
236
+ function applyUniquePathRecovery(
237
+ candidate: OperationTarget,
238
+ candidates: readonly OperationTarget[],
239
+ signals: OperationSignals,
240
+ ): void {
241
+ if (signals.repoId === undefined || candidates.length !== 1
242
+ || !signals.serviceName
243
+ || !candidate.reasons.includes('local_service_name_mismatch')
244
+ || !operationMatches(candidate, signals.operationPath)) return;
245
+ candidate.score = Math.max(candidate.score, 0.9);
246
+ candidate.reasons.push(
247
+ 'same_repo_unique_operation_path_with_lookup_mismatch',
161
248
  );
249
+ }
250
+
251
+ function strongSignal(signals: OperationSignals): boolean {
252
+ return Boolean(signals.servicePath || signals.serviceName || signals.alias
253
+ || signals.destination || signals.hasExplicitOverride);
254
+ }
255
+
256
+ function targetSignalMatches(
257
+ candidate: OperationTarget,
258
+ signals: OperationSignals,
259
+ ): boolean {
260
+ if (candidate.servicePath === signals.servicePath) return true;
261
+ if (!signals.serviceName) return false;
262
+ return !candidate.reasons.includes('local_service_name_mismatch')
263
+ || candidate.reasons.includes(
264
+ 'same_repo_unique_operation_path_with_lookup_mismatch',
265
+ );
266
+ }
267
+
268
+ function bestCandidateResolves(
269
+ best: OperationTarget | undefined,
270
+ second: OperationTarget | undefined,
271
+ signals: OperationSignals,
272
+ ): best is OperationTarget {
273
+ if (!best) return false;
274
+ return best.score >= 0.9
275
+ && targetSignalMatches(best, signals)
276
+ && operationMatches(best, signals.operationPath)
277
+ && (!second || best.score - second.score >= 0.25);
278
+ }
279
+
280
+ function finalOperationResolution(
281
+ candidates: OperationTarget[],
282
+ signals: OperationSignals,
283
+ ): OperationResolution {
162
284
  const best = candidates[0];
163
285
  const second = candidates[1];
164
286
  if (signals.isDynamic && !signals.hasExplicitOverride && !signals.servicePath)
@@ -167,19 +289,13 @@ export function resolveOperation(
167
289
  candidates,
168
290
  reasons: ['dynamic_target_without_override'],
169
291
  };
170
- if (!hasStrongSignal)
292
+ if (!strongSignal(signals))
171
293
  return {
172
294
  status: candidates.length > 1 ? 'ambiguous' : 'unresolved',
173
295
  candidates,
174
296
  reasons: ['operation_path_only_has_no_strong_target_signal'],
175
297
  };
176
- if (
177
- best &&
178
- best.score >= 0.9 &&
179
- (best.servicePath === signals.servicePath || Boolean(signals.serviceName && (!best.reasons.includes('local_service_name_mismatch') || best.reasons.includes('same_repo_unique_operation_path_with_lookup_mismatch')))) &&
180
- operationMatches(best, signals.operationPath) &&
181
- (!second || best.score - second.score >= 0.25)
182
- )
298
+ if (bestCandidateResolves(best, second, signals))
183
299
  return {
184
300
  status: 'resolved',
185
301
  target: best,
@@ -192,6 +308,10 @@ export function resolveOperation(
192
308
  reasons: ['candidate_score_below_resolution_threshold'],
193
309
  };
194
310
  }
311
+
312
+ function compareBinary(left: string, right: string): number {
313
+ return left < right ? -1 : left > right ? 1 : 0;
314
+ }
195
315
  function serviceMatches(candidate: OperationTarget, serviceName: string | undefined): boolean {
196
316
  if (!serviceName) return false;
197
317
  const simple = serviceName.split('.').at(-1) ?? serviceName;