@saptools/service-flow 0.1.70 → 0.1.73

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 (61) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +23 -6
  3. package/TECHNICAL-NOTE.md +24 -2
  4. package/dist/{chunk-GSLFY6J2.js → chunk-32WOTGTS.js} +12061 -9288
  5. package/dist/chunk-32WOTGTS.js.map +1 -0
  6. package/dist/cli.js +854 -161
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +54 -14
  9. package/dist/index.js +2 -23
  10. package/dist/index.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/cli/doctor-event-quality.ts +446 -0
  13. package/src/cli/doctor.ts +4 -6
  14. package/src/cli.ts +35 -13
  15. package/src/config/workspace-config.ts +15 -0
  16. package/src/db/call-fact-repository.ts +6 -3
  17. package/src/db/current-fact-semantics.ts +5 -29
  18. package/src/db/event-fact-semantics.ts +546 -0
  19. package/src/db/event-site-semantics.ts +62 -0
  20. package/src/db/event-surface-invalidation.ts +38 -0
  21. package/src/db/fact-json-inventory.ts +16 -0
  22. package/src/db/fact-lifecycle.ts +70 -12
  23. package/src/db/migrations.ts +15 -1
  24. package/src/db/package-target-invalidation.ts +80 -0
  25. package/src/db/repositories.ts +28 -2
  26. package/src/db/schema-structure.ts +41 -1
  27. package/src/db/schema.ts +6 -2
  28. package/src/indexer/repository-indexer.ts +93 -10
  29. package/src/indexer/workspace-indexer.ts +13 -3
  30. package/src/linker/cross-repo-linker.ts +27 -3
  31. package/src/linker/event-environment-link.ts +211 -0
  32. package/src/linker/event-shape-candidate-linker.ts +204 -0
  33. package/src/linker/event-subscription-handler-linker.ts +220 -25
  34. package/src/linker/event-template-link.ts +40 -6
  35. package/src/linker/package-event-constant-resolver.ts +302 -0
  36. package/src/output/repository-inspection.ts +11 -0
  37. package/src/output/table-output.ts +13 -1
  38. package/src/parsers/decorator-parser.ts +9 -53
  39. package/src/parsers/environment-declarations.ts +404 -0
  40. package/src/parsers/event-call-analysis.ts +370 -0
  41. package/src/parsers/event-environment-reference.ts +242 -0
  42. package/src/parsers/event-loop-registration.ts +132 -0
  43. package/src/parsers/event-name-import-resolution.ts +243 -0
  44. package/src/parsers/event-receiver-analysis.ts +394 -0
  45. package/src/parsers/event-subscription-facts.ts +4 -0
  46. package/src/parsers/generated-constants-parser.ts +80 -14
  47. package/src/parsers/outbound-call-classifier.ts +27 -124
  48. package/src/parsers/outbound-call-parser.ts +13 -1
  49. package/src/parsers/outbound-expression-analysis.ts +2 -2
  50. package/src/parsers/stable-local-value.ts +42 -10
  51. package/src/parsers/string-constant-lookups.ts +408 -0
  52. package/src/trace/edge-target.ts +65 -0
  53. package/src/trace/event-runtime-resolution.ts +24 -3
  54. package/src/trace/event-shape-candidate-trace.ts +172 -0
  55. package/src/trace/event-subscriber-traversal.ts +90 -8
  56. package/src/trace/evidence.ts +7 -28
  57. package/src/trace/trace-scope-execution.ts +22 -30
  58. package/src/types.ts +19 -1
  59. package/src/utils/event-skeleton.ts +207 -0
  60. package/src/version.ts +1 -1
  61. package/dist/chunk-GSLFY6J2.js.map +0 -1
@@ -0,0 +1,132 @@
1
+ import ts from 'typescript';
2
+ import {
3
+ collectStringConstantLookups,
4
+ type StaticStringConstant,
5
+ } from './string-constant-lookups.js';
6
+ import { resolveBinding } from './query-entity-resolution.js';
7
+ import { lexicalIdentifierDeclaration } from './symbol-import-bindings.js';
8
+
9
+ const loopValueCap = 32;
10
+ const loopExpressionLimit = 256;
11
+
12
+ function unwrap(expression: ts.Expression): ts.Expression {
13
+ if (ts.isParenthesizedExpression(expression)
14
+ || ts.isAsExpression(expression)
15
+ || ts.isSatisfiesExpression(expression)
16
+ || ts.isTypeAssertionExpression(expression))
17
+ return unwrap(expression.expression);
18
+ return expression;
19
+ }
20
+
21
+ function stringArray(expression: ts.Expression): string[] | undefined {
22
+ const value = unwrap(expression);
23
+ if (!ts.isArrayLiteralExpression(value)) return undefined;
24
+ const strings = value.elements.flatMap((element) => {
25
+ const item = ts.isSpreadElement(element) ? undefined : unwrap(element);
26
+ return item && ts.isStringLiteralLike(item) ? [item.text] : [];
27
+ });
28
+ return strings.length === value.elements.length ? strings : undefined;
29
+ }
30
+
31
+ function identifierArray(
32
+ identifier: ts.Identifier,
33
+ ): string[] | undefined {
34
+ const binding = resolveBinding(identifier, identifier);
35
+ return binding.immutable && binding.initializer
36
+ ? stringArray(binding.initializer) : undefined;
37
+ }
38
+
39
+ function orderedValues(values: StaticStringConstant[]): string[] {
40
+ return [...values]
41
+ .sort((left, right) =>
42
+ left.declarationStartOffset - right.declarationStartOffset)
43
+ .map((item) => item.value);
44
+ }
45
+
46
+ function objectValues(
47
+ expression: ts.CallExpression,
48
+ source: ts.SourceFile,
49
+ ): string[] | undefined {
50
+ if (!ts.isPropertyAccessExpression(expression.expression))
51
+ return undefined;
52
+ const root = expression.expression.expression;
53
+ if (!ts.isIdentifier(root) || root.text !== 'Object'
54
+ || lexicalIdentifierDeclaration(root)
55
+ || expression.expression.name.text !== 'values'
56
+ || expression.arguments.length !== 1) return undefined;
57
+ const argument = expression.arguments[0];
58
+ if (!argument || !ts.isIdentifier(argument)) return undefined;
59
+ const lookups = collectStringConstantLookups(source);
60
+ const prefix = `${argument.text}.`;
61
+ const refused = [...lookups.refusedMembers.keys()].some((key) =>
62
+ key.startsWith(prefix));
63
+ const values = [
64
+ ...lookups.enumMembers.values(),
65
+ ...lookups.objectProperties.values(),
66
+ ].filter((item) => item.key.startsWith(prefix));
67
+ return !refused && values.length > 0 ? orderedValues(values) : undefined;
68
+ }
69
+
70
+ function staticCollectionValues(
71
+ expression: ts.Expression,
72
+ source: ts.SourceFile,
73
+ ): string[] | undefined {
74
+ const value = unwrap(expression);
75
+ if (ts.isArrayLiteralExpression(value)) return stringArray(value);
76
+ if (ts.isIdentifier(value)) return identifierArray(value);
77
+ return ts.isCallExpression(value) ? objectValues(value, source) : undefined;
78
+ }
79
+
80
+ function enclosingForEach(
81
+ node: ts.Node,
82
+ ): ts.CallExpression | undefined {
83
+ let current: ts.Node | undefined = node.parent;
84
+ while (current && !ts.isSourceFile(current)) {
85
+ if ((ts.isArrowFunction(current) || ts.isFunctionExpression(current))
86
+ && ts.isCallExpression(current.parent)
87
+ && current.parent.arguments.includes(current)
88
+ && ts.isPropertyAccessExpression(current.parent.expression)
89
+ && current.parent.expression.name.text === 'forEach')
90
+ return current.parent;
91
+ if (ts.isFunctionLike(current)) return undefined;
92
+ current = current.parent;
93
+ }
94
+ return undefined;
95
+ }
96
+
97
+ function boundedExpression(
98
+ expression: ts.Expression,
99
+ source: ts.SourceFile,
100
+ ): string {
101
+ return expression.getText(source).slice(0, loopExpressionLimit);
102
+ }
103
+
104
+ export function eventLoopRegistrationEvidence(
105
+ node: ts.CallExpression,
106
+ source: ts.SourceFile,
107
+ ): Record<string, unknown> {
108
+ const loop = enclosingForEach(node);
109
+ if (!loop || !ts.isPropertyAccessExpression(loop.expression)) return {};
110
+ const collection = loop.expression.expression;
111
+ const values = staticCollectionValues(collection, source);
112
+ if (!values) return {
113
+ subscriptionRegisteredInLoop: true,
114
+ subscriptionLoopRegistrationStatus: 'unresolved',
115
+ subscriptionLoopUnresolvedReason:
116
+ 'subscription_loop_collection_not_statically_enumerable',
117
+ subscriptionLoopCollectionExpression:
118
+ boundedExpression(collection, source),
119
+ };
120
+ const shown = values.slice(0, loopValueCap);
121
+ return {
122
+ subscriptionRegisteredInLoop: true,
123
+ subscriptionLoopRegistrationStatus: 'enumerated',
124
+ subscriptionLoopCollectionExpression:
125
+ boundedExpression(collection, source),
126
+ subscriptionLoopRegistrationCount: values.length,
127
+ subscriptionLoopValues: shown,
128
+ shownSubscriptionLoopValueCount: shown.length,
129
+ omittedSubscriptionLoopValueCount:
130
+ Math.max(0, values.length - shown.length),
131
+ };
132
+ }
@@ -0,0 +1,243 @@
1
+ import { posix } from 'node:path';
2
+ import ts from 'typescript';
3
+ import { normalizePath } from '../utils/path-utils.js';
4
+ import {
5
+ collectStringConstantLookups,
6
+ type StaticStringConstant,
7
+ type StaticStringLookupResult,
8
+ type StringConstantLookups,
9
+ } from './string-constant-lookups.js';
10
+ import {
11
+ collectSymbolImportBindings,
12
+ derivedMemberImportReference,
13
+ lexicalIdentifierDeclaration,
14
+ type SymbolImportBinding,
15
+ type SymbolImportReference,
16
+ } from './symbol-import-bindings.js';
17
+ import type { RepositorySourceContext } from './ts-project.js';
18
+
19
+ interface ImportedMemberRequest {
20
+ binding: SymbolImportBinding;
21
+ containerName: string;
22
+ memberName?: string;
23
+ }
24
+
25
+ export type ImportedEventNameResult =
26
+ | { status: 'resolved'; constant: StaticStringConstant }
27
+ | { status: 'not_found' }
28
+ | {
29
+ status: 'refused';
30
+ reason: string;
31
+ packageImportReference?: SymbolImportReference;
32
+ };
33
+ export type ImportedEventNameResolver = (
34
+ expression: ts.Expression,
35
+ ) => ImportedEventNameResult;
36
+
37
+ function bindingFor(
38
+ identifier: ts.Identifier,
39
+ bindings: readonly SymbolImportBinding[],
40
+ ): SymbolImportBinding | undefined {
41
+ const declaration = lexicalIdentifierDeclaration(identifier);
42
+ if (!declaration) return undefined;
43
+ const start = declaration.getStart(identifier.getSourceFile());
44
+ const end = declaration.getEnd();
45
+ const matches = bindings.filter((binding) =>
46
+ binding.localName === identifier.text
47
+ && binding.bindingSiteStartOffset === start
48
+ && binding.bindingSiteEndOffset === end);
49
+ return matches.length === 1 ? matches[0] : undefined;
50
+ }
51
+
52
+ function directMemberRequest(
53
+ expression: ts.PropertyAccessExpression,
54
+ bindings: readonly SymbolImportBinding[],
55
+ ): ImportedMemberRequest | undefined {
56
+ if (!ts.isIdentifier(expression.expression)) return undefined;
57
+ const binding = bindingFor(expression.expression, bindings);
58
+ if (!binding || binding.typeOnly || binding.importedName === null)
59
+ return undefined;
60
+ return {
61
+ binding,
62
+ containerName: binding.importedName,
63
+ memberName: expression.name.text,
64
+ };
65
+ }
66
+
67
+ function namespaceMemberRequest(
68
+ expression: ts.PropertyAccessExpression,
69
+ bindings: readonly SymbolImportBinding[],
70
+ ): ImportedMemberRequest | undefined {
71
+ const container = expression.expression;
72
+ if (!ts.isPropertyAccessExpression(container)
73
+ || !ts.isIdentifier(container.expression)) return undefined;
74
+ const binding = bindingFor(container.expression, bindings);
75
+ if (!binding || binding.typeOnly
76
+ || !['esm_namespace', 'cjs_namespace'].includes(binding.bindingKind))
77
+ return undefined;
78
+ return {
79
+ binding,
80
+ containerName: container.name.text,
81
+ memberName: expression.name.text,
82
+ };
83
+ }
84
+
85
+ function importedMemberRequest(
86
+ expression: ts.Expression,
87
+ bindings: readonly SymbolImportBinding[],
88
+ ): ImportedMemberRequest | undefined {
89
+ if (ts.isIdentifier(expression)) {
90
+ const binding = bindingFor(expression, bindings);
91
+ return binding && !binding.typeOnly && binding.importedName
92
+ ? { binding, containerName: binding.importedName }
93
+ : undefined;
94
+ }
95
+ if (!ts.isPropertyAccessExpression(expression)
96
+ || expression.questionDotToken) return undefined;
97
+ return directMemberRequest(expression, bindings)
98
+ ?? namespaceMemberRequest(expression, bindings);
99
+ }
100
+
101
+ function rootIdentifier(
102
+ expression: ts.Expression,
103
+ ): ts.Identifier | undefined {
104
+ if (ts.isIdentifier(expression)) return expression;
105
+ if (ts.isPropertyAccessExpression(expression)
106
+ || ts.isElementAccessExpression(expression))
107
+ return rootIdentifier(expression.expression);
108
+ return undefined;
109
+ }
110
+
111
+ function importedAliasBinding(
112
+ identifier: ts.Identifier,
113
+ bindings: readonly SymbolImportBinding[],
114
+ ): SymbolImportBinding | undefined {
115
+ const direct = bindingFor(identifier, bindings);
116
+ if (direct) return direct;
117
+ const declaration = lexicalIdentifierDeclaration(identifier);
118
+ if (!declaration || !ts.isVariableDeclaration(declaration.parent))
119
+ return undefined;
120
+ const initializer = declaration.parent.initializer;
121
+ return initializer && ts.isIdentifier(initializer)
122
+ ? bindingFor(initializer, bindings) : undefined;
123
+ }
124
+
125
+ function unsupportedImportedContainer(
126
+ expression: ts.Expression,
127
+ bindings: readonly SymbolImportBinding[],
128
+ ): boolean {
129
+ const root = rootIdentifier(expression);
130
+ return Boolean(root && importedAliasBinding(root, bindings));
131
+ }
132
+
133
+ function constantImportReference(
134
+ request: ImportedMemberRequest,
135
+ ): SymbolImportReference | undefined {
136
+ const binding = request.binding;
137
+ if (!request.memberName) {
138
+ if (!binding.importedName) return undefined;
139
+ return {
140
+ ...binding,
141
+ referenceShape: 'identifier',
142
+ referencedMemberName: null,
143
+ requestedPublicName: binding.importedName,
144
+ };
145
+ }
146
+ if (binding.bindingKind !== 'esm_namespace'
147
+ && binding.bindingKind !== 'cjs_namespace')
148
+ return derivedMemberImportReference(binding, request.memberName);
149
+ const requestedPublicName =
150
+ `${request.containerName}.${request.memberName}`;
151
+ return {
152
+ ...binding,
153
+ referenceShape: 'namespace_member',
154
+ referencedMemberName: requestedPublicName,
155
+ requestedPublicName,
156
+ };
157
+ }
158
+
159
+ function relativeCandidates(
160
+ callerFile: string,
161
+ specifier: string,
162
+ ): string[] {
163
+ const base = normalizePath(posix.normalize(
164
+ posix.join(posix.dirname(callerFile), specifier),
165
+ ));
166
+ if (/\.[jt]s$/.test(base)) return [base];
167
+ return [
168
+ `${base}.ts`, `${base}.js`, `${base}/index.ts`, `${base}/index.js`,
169
+ ];
170
+ }
171
+
172
+ function targetLookups(
173
+ context: RepositorySourceContext,
174
+ callerFile: string,
175
+ request: ImportedMemberRequest,
176
+ ): StringConstantLookups[] {
177
+ if (request.binding.moduleKind !== 'relative') return [];
178
+ return relativeCandidates(
179
+ callerFile, request.binding.rawModuleSpecifier,
180
+ ).flatMap((candidate) => {
181
+ const snapshot = context.get(candidate);
182
+ return snapshot
183
+ ? [collectStringConstantLookups(snapshot.sourceFile())] : [];
184
+ });
185
+ }
186
+
187
+ function targetResult(
188
+ lookups: readonly StringConstantLookups[],
189
+ request: ImportedMemberRequest,
190
+ ): StaticStringLookupResult {
191
+ if (lookups.length !== 1) return {
192
+ status: 'refused',
193
+ reason: 'event_name_constant_container_ambiguous',
194
+ };
195
+ const key = request.memberName
196
+ ? `${request.containerName}.${request.memberName}`
197
+ : request.containerName;
198
+ const constant = request.memberName
199
+ ? lookups[0]?.enumMembers.get(key)
200
+ ?? lookups[0]?.objectProperties.get(key)
201
+ : lookups[0]?.identifiers.get(key);
202
+ if (constant && constant.exported && constant.stable)
203
+ return { status: 'resolved', constant };
204
+ const refusal = lookups[0]?.refusedMembers.get(key);
205
+ if (refusal) return { status: 'refused', reason: refusal.reason };
206
+ if (constant && !constant.exported) return {
207
+ status: 'refused',
208
+ reason: 'event_name_constant_container_not_exported',
209
+ };
210
+ return {
211
+ status: 'refused',
212
+ reason: 'event_name_constant_member_not_string',
213
+ };
214
+ }
215
+
216
+ export function createImportedEventNameResolver(
217
+ context: RepositorySourceContext,
218
+ source: ts.SourceFile,
219
+ sourceFile: string,
220
+ ): ImportedEventNameResolver {
221
+ const bindings = collectSymbolImportBindings(source);
222
+ return (expression): ImportedEventNameResult => {
223
+ const request = importedMemberRequest(expression, bindings);
224
+ if (!request) return unsupportedImportedContainer(expression, bindings)
225
+ ? {
226
+ status: 'refused',
227
+ reason: 'event_name_constant_container_ambiguous',
228
+ }
229
+ : { status: 'not_found' };
230
+ if (request.binding.moduleKind !== 'relative') {
231
+ const packageImportReference = constantImportReference(request);
232
+ return packageImportReference ? {
233
+ status: 'refused',
234
+ reason: 'event_name_constant_resolution_pending',
235
+ packageImportReference,
236
+ } : {
237
+ status: 'refused',
238
+ reason: 'event_name_constant_container_ambiguous',
239
+ };
240
+ }
241
+ return targetResult(targetLookups(context, sourceFile, request), request);
242
+ };
243
+ }