@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,404 @@
1
+ import type { RepositorySourceContext } from './ts-project.js';
2
+
3
+ export const ENVIRONMENT_DECLARATIONS_SCHEMA =
4
+ 'service-flow/environment-declarations@1';
5
+ export const ENVIRONMENT_DECLARATION_RECORD_CAP = 32;
6
+ export const EVENT_ENVIRONMENT_KEY_CAP = 16;
7
+ export const DEFAULT_EVENT_ENVIRONMENT_KEYS = ['SHARD_CODE'] as const;
8
+ export const EVENT_ENVIRONMENT_KEY_ALLOWLIST =
9
+ DEFAULT_EVENT_ENVIRONMENT_KEYS;
10
+
11
+ export type EventEnvironmentKey = string;
12
+ export type EnvironmentDeclarationProvenance =
13
+ | 'env_declaration_manifest'
14
+ | 'env_declaration_mta'
15
+ | 'env_declaration_dotenv'
16
+ | 'env_declaration_dev';
17
+
18
+ export interface EnvironmentDeclaration {
19
+ key: EventEnvironmentKey;
20
+ value: string;
21
+ provenance: EnvironmentDeclarationProvenance;
22
+ sourceFile: string;
23
+ startOffset: number;
24
+ endOffset: number;
25
+ }
26
+
27
+ export interface EnvironmentDeclarationsFact {
28
+ schema: typeof ENVIRONMENT_DECLARATIONS_SCHEMA;
29
+ allowedKeys: string[];
30
+ status: 'complete' | 'ambiguous' | 'not_applicable' | 'incomplete';
31
+ reason: string | null;
32
+ recordCap: typeof ENVIRONMENT_DECLARATION_RECORD_CAP;
33
+ total: number;
34
+ shown: number;
35
+ omitted: number;
36
+ declarations: EnvironmentDeclaration[];
37
+ }
38
+
39
+ const allowedProvenance = new Set<EnvironmentDeclarationProvenance>([
40
+ 'env_declaration_manifest',
41
+ 'env_declaration_mta',
42
+ 'env_declaration_dotenv',
43
+ 'env_declaration_dev',
44
+ ]);
45
+ const environmentValueLimit = 512;
46
+ const dynamicEnvironmentValue = /\$\{|\$\(|~\{|\(\(/;
47
+ const environmentKeyGrammar = /^[A-Z_][A-Z0-9_]{0,63}$/;
48
+
49
+ export function validEventEnvironmentKey(value: string): boolean {
50
+ return environmentKeyGrammar.test(value);
51
+ }
52
+
53
+ export function normalizeEventEnvironmentKeys(
54
+ values: readonly string[] = DEFAULT_EVENT_ENVIRONMENT_KEYS,
55
+ ): string[] {
56
+ const unique = [...new Set(values)].sort((left, right) =>
57
+ left < right ? -1 : left > right ? 1 : 0);
58
+ if (unique.length === 0 || unique.length > EVENT_ENVIRONMENT_KEY_CAP
59
+ || !unique.every(validEventEnvironmentKey))
60
+ throw new Error('invalid_event_environment_keys');
61
+ return unique;
62
+ }
63
+
64
+ function hasControlCharacter(value: string): boolean {
65
+ for (const character of value) {
66
+ const code = character.charCodeAt(0);
67
+ if (code <= 31 || code === 127) return true;
68
+ }
69
+ return false;
70
+ }
71
+
72
+ function record(value: unknown): Record<string, unknown> | undefined {
73
+ return value && typeof value === 'object' && !Array.isArray(value)
74
+ ? value as Record<string, unknown> : undefined;
75
+ }
76
+
77
+ function parseJson(value: unknown): unknown {
78
+ if (typeof value !== 'string') return value;
79
+ try {
80
+ return JSON.parse(value) as unknown;
81
+ } catch {
82
+ return undefined;
83
+ }
84
+ }
85
+
86
+ function scalarValue(raw: string): string | undefined {
87
+ const value = raw.trim();
88
+ if (!value || /^[{[*&!>|]/.test(value)) return undefined;
89
+ if ((value.startsWith('"') && value.endsWith('"'))
90
+ || (value.startsWith("'") && value.endsWith("'")))
91
+ return value.slice(1, -1);
92
+ return /[\r\n]/.test(value) ? undefined : value;
93
+ }
94
+
95
+ function declaration(
96
+ key: string,
97
+ value: unknown,
98
+ provenance: EnvironmentDeclarationProvenance,
99
+ sourceFile: string,
100
+ startOffset: number,
101
+ allowedKeys: ReadonlySet<string>,
102
+ ): EnvironmentDeclaration[] {
103
+ if (!allowedKeys.has(key) || typeof value !== 'string'
104
+ || !value || value.length > environmentValueLimit
105
+ || hasControlCharacter(value)
106
+ || dynamicEnvironmentValue.test(value)) return [];
107
+ return [{
108
+ key: key as EventEnvironmentKey,
109
+ value,
110
+ provenance,
111
+ sourceFile,
112
+ startOffset,
113
+ endOffset: startOffset + value.length,
114
+ }];
115
+ }
116
+
117
+ function valueOffset(
118
+ text: string,
119
+ value: string,
120
+ after: number,
121
+ ): number {
122
+ const direct = text.indexOf(value, Math.max(0, after));
123
+ return direct >= 0 ? direct : Math.max(0, after);
124
+ }
125
+
126
+ function jsonDeclarations(
127
+ filePath: string,
128
+ text: string,
129
+ keys: readonly string[],
130
+ allowedKeys: ReadonlySet<string>,
131
+ ): EnvironmentDeclaration[] {
132
+ let parsed: unknown;
133
+ try {
134
+ parsed = JSON.parse(text) as unknown;
135
+ } catch {
136
+ return [];
137
+ }
138
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
139
+ return [];
140
+ const env = (parsed as Record<string, unknown>).env;
141
+ if (!env || typeof env !== 'object' || Array.isArray(env)) return [];
142
+ return keys.flatMap((key) => {
143
+ const value = (env as Record<string, unknown>)[key];
144
+ const keyOffset = text.indexOf(`"${key}"`);
145
+ const offset = typeof value === 'string'
146
+ ? valueOffset(text, value, keyOffset + key.length + 2) : keyOffset;
147
+ return declaration(
148
+ key, value, 'env_declaration_dev', filePath, Math.max(0, offset),
149
+ allowedKeys,
150
+ );
151
+ });
152
+ }
153
+
154
+ function dotenvDeclarations(
155
+ filePath: string,
156
+ text: string,
157
+ allowedKeys: ReadonlySet<string>,
158
+ ): EnvironmentDeclaration[] {
159
+ const values: EnvironmentDeclaration[] = [];
160
+ let offset = 0;
161
+ for (const line of text.split(/\n/)) {
162
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/
163
+ .exec(line);
164
+ const value = match?.[2] === undefined
165
+ ? undefined : scalarValue(match[2]);
166
+ if (match?.[1] && value !== undefined)
167
+ values.push(...declaration(
168
+ match[1], value, 'env_declaration_dotenv', filePath,
169
+ offset + valueOffset(line, value, line.indexOf('=') + 1),
170
+ allowedKeys,
171
+ ));
172
+ offset += line.length + 1;
173
+ }
174
+ return values;
175
+ }
176
+
177
+ function indentation(value: string): number {
178
+ return /^\s*/.exec(value)?.[0].length ?? 0;
179
+ }
180
+
181
+ function yamlDeclarations(
182
+ filePath: string,
183
+ text: string,
184
+ provenance: EnvironmentDeclarationProvenance,
185
+ allowedKeys: ReadonlySet<string>,
186
+ ): EnvironmentDeclaration[] {
187
+ const values: EnvironmentDeclaration[] = [];
188
+ let envIndent: number | undefined;
189
+ let offset = 0;
190
+ for (const line of text.split(/\n/)) {
191
+ const trimmed = line.trim();
192
+ const indent = indentation(line);
193
+ if (/^env\s*:\s*(?:#.*)?$/.test(trimmed)) envIndent = indent;
194
+ else if (envIndent !== undefined && trimmed && !trimmed.startsWith('#')) {
195
+ if (indent <= envIndent) envIndent = undefined;
196
+ else {
197
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*?)\s*(?:#.*)?$/
198
+ .exec(trimmed);
199
+ const value = match?.[2] === undefined
200
+ ? undefined : scalarValue(match[2]);
201
+ if (match?.[1] && value !== undefined)
202
+ values.push(...declaration(
203
+ match[1], value, provenance, filePath,
204
+ offset + valueOffset(line, value, line.indexOf(':') + 1),
205
+ allowedKeys,
206
+ ));
207
+ }
208
+ }
209
+ offset += line.length + 1;
210
+ }
211
+ return values;
212
+ }
213
+
214
+ function snapshotDeclarations(
215
+ filePath: string,
216
+ text: string,
217
+ keys: readonly string[],
218
+ allowedKeys: ReadonlySet<string>,
219
+ ): EnvironmentDeclaration[] {
220
+ const name = filePath.split('/').at(-1);
221
+ if (name === 'nodemon.json')
222
+ return jsonDeclarations(filePath, text, keys, allowedKeys);
223
+ if (name === '.env')
224
+ return dotenvDeclarations(filePath, text, allowedKeys);
225
+ if (name === 'manifest.yml')
226
+ return yamlDeclarations(
227
+ filePath, text, 'env_declaration_manifest', allowedKeys,
228
+ );
229
+ return name === 'mta.yaml'
230
+ ? yamlDeclarations(filePath, text, 'env_declaration_mta', allowedKeys) : [];
231
+ }
232
+
233
+ function compareDeclaration(
234
+ left: EnvironmentDeclaration,
235
+ right: EnvironmentDeclaration,
236
+ ): number {
237
+ const leftKey = `${left.key}\0${left.sourceFile}\0${left.startOffset}`;
238
+ const rightKey = `${right.key}\0${right.sourceFile}\0${right.startOffset}`;
239
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
240
+ }
241
+
242
+ function declarationIdentityValid(
243
+ item: Record<string, unknown>,
244
+ allowedKeys: ReadonlySet<string>,
245
+ ): boolean {
246
+ return typeof item.key === 'string'
247
+ && allowedKeys.has(item.key)
248
+ && typeof item.provenance === 'string'
249
+ && allowedProvenance.has(
250
+ item.provenance as EnvironmentDeclarationProvenance,
251
+ );
252
+ }
253
+
254
+ function declarationValueValid(item: Record<string, unknown>): boolean {
255
+ return typeof item.value === 'string'
256
+ && item.value.length > 0
257
+ && item.value.length <= environmentValueLimit
258
+ && !hasControlCharacter(item.value)
259
+ && !dynamicEnvironmentValue.test(item.value);
260
+ }
261
+
262
+ function declarationLocationValid(item: Record<string, unknown>): boolean {
263
+ return typeof item.sourceFile === 'string'
264
+ && item.sourceFile.length > 0
265
+ && Number.isInteger(item.startOffset)
266
+ && Number(item.startOffset) >= 0
267
+ && Number.isInteger(item.endOffset)
268
+ && Number(item.endOffset) > Number(item.startOffset);
269
+ }
270
+
271
+ function parsedDeclaration(
272
+ value: unknown,
273
+ allowedKeys: ReadonlySet<string>,
274
+ ): EnvironmentDeclaration | undefined {
275
+ const item = record(value);
276
+ if (!item || !declarationIdentityValid(item, allowedKeys)
277
+ || !declarationValueValid(item)
278
+ || !declarationLocationValid(item)) return undefined;
279
+ return item as unknown as EnvironmentDeclaration;
280
+ }
281
+
282
+ function countsValid(
283
+ item: Record<string, unknown>,
284
+ declarations: EnvironmentDeclaration[],
285
+ ): boolean {
286
+ return item.recordCap === ENVIRONMENT_DECLARATION_RECORD_CAP
287
+ && Number.isInteger(item.total) && Number(item.total) >= 0
288
+ && Number.isInteger(item.shown) && Number(item.shown) >= 0
289
+ && Number.isInteger(item.omitted) && Number(item.omitted) >= 0
290
+ && Number(item.shown) + Number(item.omitted) === Number(item.total)
291
+ && Number(item.shown) === declarations.length
292
+ && declarations.length <= ENVIRONMENT_DECLARATION_RECORD_CAP;
293
+ }
294
+
295
+ function statusValid(
296
+ item: Record<string, unknown>,
297
+ declarations: EnvironmentDeclaration[],
298
+ ): boolean {
299
+ const distinct = new Map<string, Set<string>>();
300
+ for (const value of declarations) {
301
+ const values = distinct.get(value.key) ?? new Set<string>();
302
+ values.add(value.value);
303
+ distinct.set(value.key, values);
304
+ }
305
+ const ambiguous = [...distinct.values()].some((values) => values.size > 1);
306
+ if (item.status === 'not_applicable')
307
+ return item.reason === null && item.total === 0;
308
+ if (item.status === 'complete')
309
+ return item.reason === null && item.total === item.shown
310
+ && Number(item.total) > 0 && !ambiguous;
311
+ if (item.status === 'ambiguous')
312
+ return item.reason === 'environment_declaration_values_conflict'
313
+ && ambiguous && item.omitted === 0;
314
+ return item.status === 'incomplete'
315
+ && item.reason === 'environment_declaration_record_cap_exceeded'
316
+ && Number(item.omitted) > 0;
317
+ }
318
+
319
+ function parsedAllowedKeys(value: unknown): string[] | undefined {
320
+ if (!Array.isArray(value)
321
+ || !value.every((item): item is string => typeof item === 'string'))
322
+ return undefined;
323
+ try {
324
+ const normalized = normalizeEventEnvironmentKeys(value);
325
+ return normalized.length === value.length
326
+ && normalized.every((item, index) => item === value[index])
327
+ ? normalized : undefined;
328
+ } catch {
329
+ return undefined;
330
+ }
331
+ }
332
+
333
+ export function parseEnvironmentDeclarationsFact(
334
+ value: unknown,
335
+ ): EnvironmentDeclarationsFact | undefined {
336
+ const item = record(parseJson(value));
337
+ if (!item || item.schema !== ENVIRONMENT_DECLARATIONS_SCHEMA
338
+ || !Array.isArray(item.declarations)) return undefined;
339
+ const keys = parsedAllowedKeys(item.allowedKeys);
340
+ if (!keys) return undefined;
341
+ const allowedKeys = new Set(keys);
342
+ const declarations = item.declarations.flatMap((entry) => {
343
+ const parsed = parsedDeclaration(entry, allowedKeys);
344
+ return parsed ? [parsed] : [];
345
+ });
346
+ if (declarations.length !== item.declarations.length
347
+ || !countsValid(item, declarations)
348
+ || !statusValid(item, declarations)) return undefined;
349
+ const identities = declarations.map((entry) =>
350
+ `${entry.key}\0${entry.sourceFile}\0${entry.startOffset}\0${entry.endOffset}`);
351
+ if (new Set(identities).size !== identities.length) return undefined;
352
+ return { ...item, declarations } as unknown as EnvironmentDeclarationsFact;
353
+ }
354
+
355
+ export function collectEnvironmentDeclarations(
356
+ sources: RepositorySourceContext,
357
+ configuredKeys: readonly string[] = DEFAULT_EVENT_ENVIRONMENT_KEYS,
358
+ ): EnvironmentDeclarationsFact {
359
+ const keys = normalizeEventEnvironmentKeys(configuredKeys);
360
+ const allowedKeys = new Set(keys);
361
+ const all = sources.entries().flatMap((snapshot) =>
362
+ snapshotDeclarations(
363
+ snapshot.filePath, snapshot.text, keys, allowedKeys,
364
+ ))
365
+ .sort(compareDeclaration);
366
+ const values = new Set(all.map((item) => `${item.key}\0${item.value}`));
367
+ const ambiguous = keys.some((key) =>
368
+ [...values].filter((value) => value.startsWith(`${key}\0`)).length > 1);
369
+ const declarations = all.slice(0, ENVIRONMENT_DECLARATION_RECORD_CAP);
370
+ return {
371
+ schema: ENVIRONMENT_DECLARATIONS_SCHEMA,
372
+ allowedKeys: keys,
373
+ status: ambiguous
374
+ ? 'ambiguous'
375
+ : all.length > ENVIRONMENT_DECLARATION_RECORD_CAP
376
+ ? 'incomplete'
377
+ : all.length > 0 ? 'complete' : 'not_applicable',
378
+ reason: ambiguous
379
+ ? 'environment_declaration_values_conflict'
380
+ : all.length > ENVIRONMENT_DECLARATION_RECORD_CAP
381
+ ? 'environment_declaration_record_cap_exceeded' : null,
382
+ recordCap: ENVIRONMENT_DECLARATION_RECORD_CAP,
383
+ total: all.length,
384
+ shown: declarations.length,
385
+ omitted: Math.max(0, all.length - declarations.length),
386
+ declarations,
387
+ };
388
+ }
389
+
390
+ export function emptyEnvironmentDeclarations(
391
+ configuredKeys: readonly string[] = DEFAULT_EVENT_ENVIRONMENT_KEYS,
392
+ ): EnvironmentDeclarationsFact {
393
+ return {
394
+ schema: ENVIRONMENT_DECLARATIONS_SCHEMA,
395
+ allowedKeys: normalizeEventEnvironmentKeys(configuredKeys),
396
+ status: 'not_applicable',
397
+ reason: null,
398
+ recordCap: ENVIRONMENT_DECLARATION_RECORD_CAP,
399
+ total: 0,
400
+ shown: 0,
401
+ omitted: 0,
402
+ declarations: [],
403
+ };
404
+ }