@saptools/service-flow 0.1.69 → 0.1.72

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 (144) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +9 -5
  3. package/TECHNICAL-NOTE.md +17 -0
  4. package/dist/{chunk-3N3B5KHV.js → chunk-Z6D433R5.js} +8766 -6546
  5. package/dist/chunk-Z6D433R5.js.map +1 -0
  6. package/dist/cli.js +703 -160
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +53 -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 +371 -0
  13. package/src/cli/{002-doctor-lifecycle.ts → doctor-lifecycle.ts} +1 -1
  14. package/src/cli/{001-doctor-projection.ts → doctor-projection.ts} +1 -1
  15. package/src/cli/doctor.ts +9 -11
  16. package/src/cli.ts +6 -6
  17. package/src/db/{009-binding-fact-semantics.ts → binding-fact-semantics.ts} +2 -2
  18. package/src/db/{000-call-fact-repository.ts → call-fact-repository.ts} +12 -9
  19. package/src/db/{003-current-fact-semantics.ts → current-fact-semantics.ts} +8 -8
  20. package/src/db/event-fact-semantics.ts +347 -0
  21. package/src/db/event-surface-invalidation.ts +38 -0
  22. package/src/db/{002-fact-json-inventory.ts → fact-json-inventory.ts} +16 -0
  23. package/src/db/{001-fact-lifecycle.ts → fact-lifecycle.ts} +3 -3
  24. package/src/db/migrations.ts +12 -1
  25. package/src/db/{007-package-fact-semantics.ts → package-fact-semantics.ts} +6 -6
  26. package/src/db/{010-package-symbol-surface-semantics.ts → package-symbol-surface-semantics.ts} +2 -2
  27. package/src/db/{004-package-target-invalidation.ts → package-target-invalidation.ts} +80 -1
  28. package/src/db/{008-relative-fact-semantics.ts → relative-fact-semantics.ts} +3 -3
  29. package/src/db/{006-relative-symbol-resolution.ts → relative-symbol-resolution.ts} +1 -1
  30. package/src/db/repositories.ts +31 -5
  31. package/src/db/{005-schema-structure.ts → schema-structure.ts} +41 -1
  32. package/src/db/schema.ts +6 -2
  33. package/src/db/{011-symbol-call-semantics.ts → symbol-call-semantics.ts} +1 -1
  34. package/src/index.ts +3 -3
  35. package/src/indexer/repository-indexer.ts +50 -11
  36. package/src/indexer/workspace-indexer.ts +3 -3
  37. package/src/linker/{007-call-edge-insertion.ts → call-edge-insertion.ts} +2 -2
  38. package/src/linker/{002-call-evidence.ts → call-evidence.ts} +2 -2
  39. package/src/linker/cross-repo-linker.ts +30 -8
  40. package/src/linker/dynamic-edge-resolver.ts +1 -1
  41. package/src/linker/event-environment-link.ts +211 -0
  42. package/src/linker/event-shape-candidate-linker.ts +161 -0
  43. package/src/linker/{004-event-subscription-handler-linker.ts → event-subscription-handler-linker.ts} +124 -20
  44. package/src/linker/{006-event-template-link.ts → event-template-link.ts} +40 -6
  45. package/src/linker/external-http-target.ts +1 -1
  46. package/src/linker/helper-package-linker.ts +1 -1
  47. package/src/linker/{000-implementation-candidates.ts → implementation-candidates.ts} +1 -1
  48. package/src/linker/{001-implementation-evidence-projection.ts → implementation-evidence-projection.ts} +1 -1
  49. package/src/linker/odata-path-normalizer.ts +1 -1
  50. package/src/linker/{005-odata-path-structure.ts → odata-path-structure.ts} +1 -1
  51. package/src/linker/package-event-constant-resolver.ts +298 -0
  52. package/src/linker/{003-package-import-symbol-resolver.ts → package-import-symbol-resolver.ts} +3 -3
  53. package/src/linker/service-resolver.ts +2 -2
  54. package/src/output/{001-compact-json-output.ts → compact-json-output.ts} +1 -1
  55. package/src/output/table-output.ts +13 -1
  56. package/src/parsers/{006-binding-identity.ts → binding-identity.ts} +3 -3
  57. package/src/parsers/{011-binding-lexical-scope.ts → binding-lexical-scope.ts} +1 -1
  58. package/src/parsers/{021-binding-visibility.ts → binding-visibility.ts} +1 -1
  59. package/src/parsers/decorator-parser.ts +9 -53
  60. package/src/parsers/environment-declarations.ts +327 -0
  61. package/src/parsers/event-call-analysis.ts +242 -0
  62. package/src/parsers/event-environment-reference.ts +231 -0
  63. package/src/parsers/event-loop-registration.ts +132 -0
  64. package/src/parsers/event-name-import-resolution.ts +243 -0
  65. package/src/parsers/event-receiver-analysis.ts +404 -0
  66. package/src/parsers/{005-event-subscription-facts.ts → event-subscription-facts.ts} +6 -2
  67. package/src/parsers/generated-constants-parser.ts +80 -14
  68. package/src/parsers/{016-local-symbol-reference.ts → local-symbol-reference.ts} +2 -2
  69. package/src/parsers/operation-path-analysis.ts +1 -1
  70. package/src/parsers/{023-outbound-call-classifier.ts → outbound-call-classifier.ts} +30 -127
  71. package/src/parsers/outbound-call-parser.ts +15 -3
  72. package/src/parsers/{022-outbound-expression-analysis.ts → outbound-expression-analysis.ts} +4 -4
  73. package/src/parsers/{018-package-commonjs-syntax.ts → package-commonjs-syntax.ts} +1 -1
  74. package/src/parsers/{012-package-fact-contract.ts → package-fact-contract.ts} +3 -3
  75. package/src/parsers/{010-package-public-surface-analysis.ts → package-public-surface-analysis.ts} +2 -2
  76. package/src/parsers/{003-package-public-surface.ts → package-public-surface.ts} +3 -3
  77. package/src/parsers/{008-package-surface-publication.ts → package-surface-publication.ts} +1 -1
  78. package/src/parsers/{001-query-entity-resolution.ts → query-entity-resolution.ts} +1 -1
  79. package/src/parsers/{015-service-binding-collector.ts → service-binding-collector.ts} +3 -3
  80. package/src/parsers/{014-service-binding-helper-flow.ts → service-binding-helper-flow.ts} +2 -2
  81. package/src/parsers/service-binding-parser-helpers.ts +1 -1
  82. package/src/parsers/service-binding-parser.ts +2 -2
  83. package/src/parsers/{007-source-fact-reconciliation.ts → source-fact-reconciliation.ts} +3 -3
  84. package/src/parsers/{020-stable-local-value.ts → stable-local-value.ts} +32 -11
  85. package/src/parsers/string-constant-lookups.ts +358 -0
  86. package/src/parsers/{009-symbol-call-facts.ts → symbol-call-facts.ts} +3 -3
  87. package/src/parsers/{017-symbol-derived-contexts.ts → symbol-derived-contexts.ts} +2 -2
  88. package/src/parsers/symbol-parser.ts +6 -6
  89. package/src/trace/{021-compact-decision-normalization.ts → compact-decision-normalization.ts} +2 -2
  90. package/src/trace/{020-compact-field-projection.ts → compact-field-projection.ts} +3 -3
  91. package/src/trace/{024-compact-observation-decision.ts → compact-observation-decision.ts} +3 -3
  92. package/src/trace/{016-compact-projector.ts → compact-projector.ts} +4 -4
  93. package/src/trace/{018-compact-trace.ts → compact-trace.ts} +3 -3
  94. package/src/trace/{006-contextual-projection.ts → contextual-projection.ts} +1 -1
  95. package/src/trace/{008-contextual-runtime-state.ts → contextual-runtime-state.ts} +1 -1
  96. package/src/trace/{001-dynamic-identity.ts → dynamic-identity.ts} +2 -2
  97. package/src/trace/{003-dynamic-references.ts → dynamic-references.ts} +2 -2
  98. package/src/trace/dynamic-targets.ts +6 -6
  99. package/src/trace/{030-event-runtime-resolution.ts → event-runtime-resolution.ts} +26 -5
  100. package/src/trace/event-shape-candidate-trace.ts +172 -0
  101. package/src/trace/{011-event-subscriber-traversal.ts → event-subscriber-traversal.ts} +20 -8
  102. package/src/trace/evidence.ts +9 -2
  103. package/src/trace/implementation-hints.ts +2 -2
  104. package/src/trace/{005-implementation-selection.ts → implementation-selection.ts} +2 -2
  105. package/src/trace/{031-local-call-expansion.ts → local-call-expansion.ts} +3 -3
  106. package/src/trace/{009-selected-handler-provenance.ts → selected-handler-provenance.ts} +1 -1
  107. package/src/trace/selectors.ts +1 -1
  108. package/src/trace/{017-trace-context.ts → trace-context.ts} +2 -2
  109. package/src/trace/{015-trace-edge-recorder.ts → trace-edge-recorder.ts} +2 -2
  110. package/src/trace/{019-trace-edge-semantics.ts → trace-edge-semantics.ts} +3 -3
  111. package/src/trace/trace-engine.ts +10 -10
  112. package/src/trace/{022-trace-fact-preflight.ts → trace-fact-preflight.ts} +2 -2
  113. package/src/trace/{025-trace-implementation-scope.ts → trace-implementation-scope.ts} +1 -1
  114. package/src/trace/{028-trace-operation-execution.ts → trace-operation-execution.ts} +10 -10
  115. package/src/trace/{013-trace-root-scopes.ts → trace-root-scopes.ts} +3 -3
  116. package/src/trace/{027-trace-scope-execution.ts → trace-scope-execution.ts} +35 -43
  117. package/src/trace/{029-trace-start-implementation.ts → trace-start-implementation.ts} +7 -7
  118. package/src/trace/{026-trace-start-scope.ts → trace-start-scope.ts} +5 -5
  119. package/src/trace/{010-traversal-scope.ts → traversal-scope.ts} +1 -1
  120. package/src/types.ts +17 -1
  121. package/src/utils/event-skeleton.ts +207 -0
  122. package/src/version.ts +1 -1
  123. package/dist/chunk-3N3B5KHV.js.map +0 -1
  124. /package/src/cli/{000-clean.ts → clean.ts} +0 -0
  125. /package/src/cli/{003-doctor-package-resolution.ts → doctor-package-resolution.ts} +0 -0
  126. /package/src/cli/{001-index-summary.ts → index-summary.ts} +0 -0
  127. /package/src/db/{014-binding-helper-provenance.ts → binding-helper-provenance.ts} +0 -0
  128. /package/src/db/{012-binding-reference-proof.ts → binding-reference-proof.ts} +0 -0
  129. /package/src/db/{013-index-publication-failure.ts → index-publication-failure.ts} +0 -0
  130. /package/src/output/{000-stdout-policy.ts → stdout-policy.ts} +0 -0
  131. /package/src/parsers/{019-binding-assignment-targets.ts → binding-assignment-targets.ts} +0 -0
  132. /package/src/parsers/{000-direct-query-execution.ts → direct-query-execution.ts} +0 -0
  133. /package/src/parsers/{013-executable-body-eligibility.ts → executable-body-eligibility.ts} +0 -0
  134. /package/src/parsers/{004-fact-identity.ts → fact-identity.ts} +0 -0
  135. /package/src/parsers/{002-symbol-import-bindings.ts → symbol-import-bindings.ts} +0 -0
  136. /package/src/trace/{014-compact-contract.ts → compact-contract.ts} +0 -0
  137. /package/src/trace/{004-dynamic-candidate-sources.ts → dynamic-candidate-sources.ts} +0 -0
  138. /package/src/trace/{000-dynamic-target-types.ts → dynamic-target-types.ts} +0 -0
  139. /package/src/trace/{007-implementation-start-diagnostic.ts → implementation-start-diagnostic.ts} +0 -0
  140. /package/src/trace/{023-nested-event-scopes.ts → nested-event-scopes.ts} +0 -0
  141. /package/src/trace/{002-trace-diagnostics.ts → trace-diagnostics.ts} +0 -0
  142. /package/src/trace/{012-trace-graph-lookups.ts → trace-graph-lookups.ts} +0 -0
  143. /package/src/utils/{000-bounded-projection.ts → bounded-projection.ts} +0 -0
  144. /package/src/utils/{001-placeholders.ts → placeholders.ts} +0 -0
@@ -0,0 +1,327 @@
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_ALLOWLIST = ['SHARD_CODE'] as const;
7
+
8
+ export type EventEnvironmentKey =
9
+ typeof EVENT_ENVIRONMENT_KEY_ALLOWLIST[number];
10
+ export type EnvironmentDeclarationProvenance =
11
+ | 'env_declaration_manifest'
12
+ | 'env_declaration_mta'
13
+ | 'env_declaration_dotenv'
14
+ | 'env_declaration_dev';
15
+
16
+ export interface EnvironmentDeclaration {
17
+ key: EventEnvironmentKey;
18
+ value: string;
19
+ provenance: EnvironmentDeclarationProvenance;
20
+ sourceFile: string;
21
+ startOffset: number;
22
+ endOffset: number;
23
+ }
24
+
25
+ export interface EnvironmentDeclarationsFact {
26
+ schema: typeof ENVIRONMENT_DECLARATIONS_SCHEMA;
27
+ status: 'complete' | 'ambiguous' | 'not_applicable' | 'incomplete';
28
+ reason: string | null;
29
+ recordCap: typeof ENVIRONMENT_DECLARATION_RECORD_CAP;
30
+ total: number;
31
+ shown: number;
32
+ omitted: number;
33
+ declarations: EnvironmentDeclaration[];
34
+ }
35
+
36
+ const allowedKeys = new Set<string>(EVENT_ENVIRONMENT_KEY_ALLOWLIST);
37
+ const allowedProvenance = new Set<EnvironmentDeclarationProvenance>([
38
+ 'env_declaration_manifest',
39
+ 'env_declaration_mta',
40
+ 'env_declaration_dotenv',
41
+ 'env_declaration_dev',
42
+ ]);
43
+ const environmentValueLimit = 512;
44
+ const dynamicEnvironmentValue = /\$\{|\$\(|~\{|\(\(/;
45
+
46
+ function hasControlCharacter(value: string): boolean {
47
+ for (const character of value) {
48
+ const code = character.charCodeAt(0);
49
+ if (code <= 31 || code === 127) return true;
50
+ }
51
+ return false;
52
+ }
53
+
54
+ function record(value: unknown): Record<string, unknown> | undefined {
55
+ return value && typeof value === 'object' && !Array.isArray(value)
56
+ ? value as Record<string, unknown> : undefined;
57
+ }
58
+
59
+ function parseJson(value: unknown): unknown {
60
+ if (typeof value !== 'string') return value;
61
+ try {
62
+ return JSON.parse(value) as unknown;
63
+ } catch {
64
+ return undefined;
65
+ }
66
+ }
67
+
68
+ function scalarValue(raw: string): string | undefined {
69
+ const value = raw.trim();
70
+ if (!value || /^[{[*&!>|]/.test(value)) return undefined;
71
+ if ((value.startsWith('"') && value.endsWith('"'))
72
+ || (value.startsWith("'") && value.endsWith("'")))
73
+ return value.slice(1, -1);
74
+ return /[\r\n]/.test(value) ? undefined : value;
75
+ }
76
+
77
+ function declaration(
78
+ key: string,
79
+ value: unknown,
80
+ provenance: EnvironmentDeclarationProvenance,
81
+ sourceFile: string,
82
+ startOffset: number,
83
+ ): EnvironmentDeclaration[] {
84
+ if (!allowedKeys.has(key) || typeof value !== 'string'
85
+ || !value || value.length > environmentValueLimit
86
+ || hasControlCharacter(value)
87
+ || dynamicEnvironmentValue.test(value)) return [];
88
+ return [{
89
+ key: key as EventEnvironmentKey,
90
+ value,
91
+ provenance,
92
+ sourceFile,
93
+ startOffset,
94
+ endOffset: startOffset + value.length,
95
+ }];
96
+ }
97
+
98
+ function valueOffset(
99
+ text: string,
100
+ value: string,
101
+ after: number,
102
+ ): number {
103
+ const direct = text.indexOf(value, Math.max(0, after));
104
+ return direct >= 0 ? direct : Math.max(0, after);
105
+ }
106
+
107
+ function jsonDeclarations(
108
+ filePath: string,
109
+ text: string,
110
+ ): EnvironmentDeclaration[] {
111
+ let parsed: unknown;
112
+ try {
113
+ parsed = JSON.parse(text) as unknown;
114
+ } catch {
115
+ return [];
116
+ }
117
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
118
+ return [];
119
+ const env = (parsed as Record<string, unknown>).env;
120
+ if (!env || typeof env !== 'object' || Array.isArray(env)) return [];
121
+ return EVENT_ENVIRONMENT_KEY_ALLOWLIST.flatMap((key) => {
122
+ const value = (env as Record<string, unknown>)[key];
123
+ const keyOffset = text.indexOf(`"${key}"`);
124
+ const offset = typeof value === 'string'
125
+ ? valueOffset(text, value, keyOffset + key.length + 2) : keyOffset;
126
+ return declaration(
127
+ key, value, 'env_declaration_dev', filePath, Math.max(0, offset),
128
+ );
129
+ });
130
+ }
131
+
132
+ function dotenvDeclarations(
133
+ filePath: string,
134
+ text: string,
135
+ ): EnvironmentDeclaration[] {
136
+ const values: EnvironmentDeclaration[] = [];
137
+ let offset = 0;
138
+ for (const line of text.split(/\n/)) {
139
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/
140
+ .exec(line);
141
+ const value = match?.[2] === undefined
142
+ ? undefined : scalarValue(match[2]);
143
+ if (match?.[1] && value !== undefined)
144
+ values.push(...declaration(
145
+ match[1], value, 'env_declaration_dotenv', filePath,
146
+ offset + valueOffset(line, value, line.indexOf('=') + 1),
147
+ ));
148
+ offset += line.length + 1;
149
+ }
150
+ return values;
151
+ }
152
+
153
+ function indentation(value: string): number {
154
+ return /^\s*/.exec(value)?.[0].length ?? 0;
155
+ }
156
+
157
+ function yamlDeclarations(
158
+ filePath: string,
159
+ text: string,
160
+ provenance: EnvironmentDeclarationProvenance,
161
+ ): EnvironmentDeclaration[] {
162
+ const values: EnvironmentDeclaration[] = [];
163
+ let envIndent: number | undefined;
164
+ let offset = 0;
165
+ for (const line of text.split(/\n/)) {
166
+ const trimmed = line.trim();
167
+ const indent = indentation(line);
168
+ if (/^env\s*:\s*(?:#.*)?$/.test(trimmed)) envIndent = indent;
169
+ else if (envIndent !== undefined && trimmed && !trimmed.startsWith('#')) {
170
+ if (indent <= envIndent) envIndent = undefined;
171
+ else {
172
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*?)\s*(?:#.*)?$/
173
+ .exec(trimmed);
174
+ const value = match?.[2] === undefined
175
+ ? undefined : scalarValue(match[2]);
176
+ if (match?.[1] && value !== undefined)
177
+ values.push(...declaration(
178
+ match[1], value, provenance, filePath,
179
+ offset + valueOffset(line, value, line.indexOf(':') + 1),
180
+ ));
181
+ }
182
+ }
183
+ offset += line.length + 1;
184
+ }
185
+ return values;
186
+ }
187
+
188
+ function snapshotDeclarations(
189
+ filePath: string,
190
+ text: string,
191
+ ): EnvironmentDeclaration[] {
192
+ const name = filePath.split('/').at(-1);
193
+ if (name === 'nodemon.json') return jsonDeclarations(filePath, text);
194
+ if (name === '.env') return dotenvDeclarations(filePath, text);
195
+ if (name === 'manifest.yml')
196
+ return yamlDeclarations(filePath, text, 'env_declaration_manifest');
197
+ return name === 'mta.yaml'
198
+ ? yamlDeclarations(filePath, text, 'env_declaration_mta') : [];
199
+ }
200
+
201
+ function compareDeclaration(
202
+ left: EnvironmentDeclaration,
203
+ right: EnvironmentDeclaration,
204
+ ): number {
205
+ const leftKey = `${left.key}\0${left.sourceFile}\0${left.startOffset}`;
206
+ const rightKey = `${right.key}\0${right.sourceFile}\0${right.startOffset}`;
207
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
208
+ }
209
+
210
+ function parsedDeclaration(
211
+ value: unknown,
212
+ ): EnvironmentDeclaration | undefined {
213
+ const item = record(value);
214
+ if (!item || typeof item.key !== 'string'
215
+ || !allowedKeys.has(item.key)
216
+ || typeof item.value !== 'string' || item.value.length === 0
217
+ || item.value.length > environmentValueLimit
218
+ || hasControlCharacter(item.value)
219
+ || dynamicEnvironmentValue.test(item.value)
220
+ || typeof item.provenance !== 'string'
221
+ || !allowedProvenance.has(
222
+ item.provenance as EnvironmentDeclarationProvenance,
223
+ )
224
+ || typeof item.sourceFile !== 'string' || item.sourceFile.length === 0
225
+ || !Number.isInteger(item.startOffset) || Number(item.startOffset) < 0
226
+ || !Number.isInteger(item.endOffset)
227
+ || Number(item.endOffset) <= Number(item.startOffset)) return undefined;
228
+ return item as unknown as EnvironmentDeclaration;
229
+ }
230
+
231
+ function countsValid(
232
+ item: Record<string, unknown>,
233
+ declarations: EnvironmentDeclaration[],
234
+ ): boolean {
235
+ return item.recordCap === ENVIRONMENT_DECLARATION_RECORD_CAP
236
+ && Number.isInteger(item.total) && Number(item.total) >= 0
237
+ && Number.isInteger(item.shown) && Number(item.shown) >= 0
238
+ && Number.isInteger(item.omitted) && Number(item.omitted) >= 0
239
+ && Number(item.shown) + Number(item.omitted) === Number(item.total)
240
+ && Number(item.shown) === declarations.length
241
+ && declarations.length <= ENVIRONMENT_DECLARATION_RECORD_CAP;
242
+ }
243
+
244
+ function statusValid(
245
+ item: Record<string, unknown>,
246
+ declarations: EnvironmentDeclaration[],
247
+ ): boolean {
248
+ const distinct = new Map<string, Set<string>>();
249
+ for (const value of declarations) {
250
+ const values = distinct.get(value.key) ?? new Set<string>();
251
+ values.add(value.value);
252
+ distinct.set(value.key, values);
253
+ }
254
+ const ambiguous = [...distinct.values()].some((values) => values.size > 1);
255
+ if (item.status === 'not_applicable')
256
+ return item.reason === null && item.total === 0;
257
+ if (item.status === 'complete')
258
+ return item.reason === null && item.total === item.shown
259
+ && Number(item.total) > 0 && !ambiguous;
260
+ if (item.status === 'ambiguous')
261
+ return item.reason === 'environment_declaration_values_conflict'
262
+ && ambiguous && item.omitted === 0;
263
+ return item.status === 'incomplete'
264
+ && item.reason === 'environment_declaration_record_cap_exceeded'
265
+ && Number(item.omitted) > 0;
266
+ }
267
+
268
+ export function parseEnvironmentDeclarationsFact(
269
+ value: unknown,
270
+ ): EnvironmentDeclarationsFact | undefined {
271
+ const item = record(parseJson(value));
272
+ if (!item || item.schema !== ENVIRONMENT_DECLARATIONS_SCHEMA
273
+ || !Array.isArray(item.declarations)) return undefined;
274
+ const declarations = item.declarations.flatMap((entry) => {
275
+ const parsed = parsedDeclaration(entry);
276
+ return parsed ? [parsed] : [];
277
+ });
278
+ if (declarations.length !== item.declarations.length
279
+ || !countsValid(item, declarations)
280
+ || !statusValid(item, declarations)) return undefined;
281
+ const identities = declarations.map((entry) =>
282
+ `${entry.key}\0${entry.sourceFile}\0${entry.startOffset}\0${entry.endOffset}`);
283
+ if (new Set(identities).size !== identities.length) return undefined;
284
+ return { ...item, declarations } as unknown as EnvironmentDeclarationsFact;
285
+ }
286
+
287
+ export function collectEnvironmentDeclarations(
288
+ sources: RepositorySourceContext,
289
+ ): EnvironmentDeclarationsFact {
290
+ const all = sources.entries().flatMap((snapshot) =>
291
+ snapshotDeclarations(snapshot.filePath, snapshot.text))
292
+ .sort(compareDeclaration);
293
+ const values = new Set(all.map((item) => `${item.key}\0${item.value}`));
294
+ const ambiguous = EVENT_ENVIRONMENT_KEY_ALLOWLIST.some((key) =>
295
+ [...values].filter((value) => value.startsWith(`${key}\0`)).length > 1);
296
+ const declarations = all.slice(0, ENVIRONMENT_DECLARATION_RECORD_CAP);
297
+ return {
298
+ schema: ENVIRONMENT_DECLARATIONS_SCHEMA,
299
+ status: ambiguous
300
+ ? 'ambiguous'
301
+ : all.length > ENVIRONMENT_DECLARATION_RECORD_CAP
302
+ ? 'incomplete'
303
+ : all.length > 0 ? 'complete' : 'not_applicable',
304
+ reason: ambiguous
305
+ ? 'environment_declaration_values_conflict'
306
+ : all.length > ENVIRONMENT_DECLARATION_RECORD_CAP
307
+ ? 'environment_declaration_record_cap_exceeded' : null,
308
+ recordCap: ENVIRONMENT_DECLARATION_RECORD_CAP,
309
+ total: all.length,
310
+ shown: declarations.length,
311
+ omitted: Math.max(0, all.length - declarations.length),
312
+ declarations,
313
+ };
314
+ }
315
+
316
+ export function emptyEnvironmentDeclarations(): EnvironmentDeclarationsFact {
317
+ return {
318
+ schema: ENVIRONMENT_DECLARATIONS_SCHEMA,
319
+ status: 'not_applicable',
320
+ reason: null,
321
+ recordCap: ENVIRONMENT_DECLARATION_RECORD_CAP,
322
+ total: 0,
323
+ shown: 0,
324
+ omitted: 0,
325
+ declarations: [],
326
+ };
327
+ }
@@ -0,0 +1,242 @@
1
+ import ts from 'typescript';
2
+ import type { OutboundCallFact } from '../types.js';
3
+ import {
4
+ CDS_LIFECYCLE_EVENTS,
5
+ resolveExpression,
6
+ type ExpressionResolution,
7
+ } from './outbound-expression-analysis.js';
8
+ import {
9
+ createEventReceiverIndex,
10
+ proveEventReceiver,
11
+ type EventReceiverIndex,
12
+ type EventReceiverProof,
13
+ } from './event-receiver-analysis.js';
14
+ import {
15
+ collectStringConstantLookups,
16
+ resolveStringConstant,
17
+ type StaticStringConstant,
18
+ type StaticStringLookupResult,
19
+ type StringConstantLookups,
20
+ } from './string-constant-lookups.js';
21
+ import type {
22
+ ImportedEventNameResult,
23
+ ImportedEventNameResolver,
24
+ } from './event-name-import-resolution.js';
25
+ import type {
26
+ SymbolImportReference,
27
+ } from './symbol-import-bindings.js';
28
+ import {
29
+ deriveEventSkeleton,
30
+ type EventSkeletonFact,
31
+ } from '../utils/event-skeleton.js';
32
+ import type {
33
+ EventEnvironmentReferenceResolver,
34
+ } from './event-environment-reference.js';
35
+
36
+ type EventFact = Pick<OutboundCallFact,
37
+ 'callType' | 'serviceVariableName' | 'eventNameExpr'
38
+ | 'eventSkeleton' | 'confidence' | 'unresolvedReason'>;
39
+
40
+ export interface EventCallAnalysisContext {
41
+ source: ts.SourceFile;
42
+ receivers: EventReceiverIndex;
43
+ constants: StringConstantLookups;
44
+ importedConstant?: ImportedEventNameResolver;
45
+ environmentReference?: EventEnvironmentReferenceResolver;
46
+ }
47
+
48
+ export type EventCallAnalysis =
49
+ | { status: 'classified'; fact: EventFact; evidence: Record<string, unknown> }
50
+ | { status: 'excluded' }
51
+ | { status: 'unclassified' };
52
+
53
+ interface EventNameState {
54
+ eventName: string;
55
+ resolved: ExpressionResolution;
56
+ unresolvedReason?: string;
57
+ constant?: StaticStringConstant;
58
+ packageImportReference?: SymbolImportReference;
59
+ }
60
+
61
+ function eventNameReason(
62
+ resolved: ExpressionResolution,
63
+ ): string | undefined {
64
+ if (resolved.status === 'static') return undefined;
65
+ return resolved.value !== undefined && resolved.placeholderKeys.length > 0
66
+ ? 'dynamic_event_name_identifier'
67
+ : 'dynamic_event_name_unsupported_expression';
68
+ }
69
+
70
+ function eventNameState(
71
+ node: ts.CallExpression,
72
+ context: EventCallAnalysisContext,
73
+ ): EventNameState | undefined {
74
+ const expression = node.arguments[0];
75
+ if (expression) {
76
+ const local = resolveStringConstant(expression, context.constants);
77
+ const lookup = local.status === 'not_found'
78
+ ? context.importedConstant?.(expression) ?? local
79
+ : local;
80
+ if (lookup.status === 'resolved') return {
81
+ eventName: lookup.constant.value,
82
+ resolved: {
83
+ status: 'static',
84
+ sourceKind: lookup.constant.kind,
85
+ value: lookup.constant.value,
86
+ rawExpression: expression.getText(node.getSourceFile()),
87
+ placeholderKeys: [],
88
+ evidence: [`event_name_${lookup.constant.kind}`],
89
+ },
90
+ constant: lookup.constant,
91
+ };
92
+ if (lookup.status === 'refused') return {
93
+ eventName: expression.getText(node.getSourceFile()),
94
+ resolved: {
95
+ status: 'dynamic',
96
+ sourceKind: 'dynamic_expression',
97
+ rawExpression: expression.getText(node.getSourceFile()),
98
+ placeholderKeys: [],
99
+ evidence: [lookup.reason],
100
+ },
101
+ unresolvedReason: lookup.reason,
102
+ packageImportReference: packageReference(lookup),
103
+ };
104
+ }
105
+ const resolved = resolveExpression(expression, node, 'operation_path');
106
+ const eventName = resolved.value
107
+ ?? resolved.rawExpression
108
+ ?? expression?.getText(node.getSourceFile());
109
+ return eventName ? {
110
+ eventName,
111
+ resolved,
112
+ unresolvedReason: eventNameReason(resolved),
113
+ } : undefined;
114
+ }
115
+
116
+ function packageReference(
117
+ value: ImportedEventNameResult | StaticStringLookupResult,
118
+ ): SymbolImportReference | undefined {
119
+ return 'packageImportReference' in value
120
+ ? value.packageImportReference : undefined;
121
+ }
122
+
123
+ function eventConfidence(
124
+ receiver: EventReceiverProof,
125
+ eventReason: string | undefined,
126
+ ): number {
127
+ const receiverConfidence = receiver.receiverClassification === 'unproven'
128
+ ? 0.2 : receiver.receiverClassification === 'name_fallback' ? 0.5 : 0.8;
129
+ const nameConfidence = !eventReason
130
+ ? 0.8 : eventReason === 'dynamic_event_name_identifier' ? 0.6 : 0.3;
131
+ return Math.min(receiverConfidence, nameConfidence);
132
+ }
133
+
134
+ function excludedEvent(
135
+ method: string,
136
+ receiver: EventReceiverProof,
137
+ state: EventNameState,
138
+ ): boolean {
139
+ if (state.resolved.status !== 'static') return false;
140
+ if (receiver.effectiveReceiver === 'cds'
141
+ && CDS_LIFECYCLE_EVENTS.has(state.eventName)) return true;
142
+ return method === 'on' && state.eventName === 'error';
143
+ }
144
+
145
+ function eventEvidence(
146
+ method: string,
147
+ receiver: EventReceiverProof,
148
+ state: EventNameState,
149
+ ): Record<string, unknown> {
150
+ return {
151
+ receiver: receiver.receiver,
152
+ rootReceiver: receiver.rootReceiver,
153
+ classifier: method === 'on'
154
+ ? 'cap_service_event_subscription' : 'cap_service_event_emit',
155
+ receiverClassification: receiver.receiverClassification,
156
+ receiverProof: receiver.receiverProof,
157
+ consideredBindingSites: receiver.consideredBindingSites,
158
+ eventNameUnresolvedReason: state.unresolvedReason,
159
+ eventNameConstantImportBinding: state.packageImportReference,
160
+ eventNameConstantSourceExpression: state.packageImportReference
161
+ ? state.eventName : undefined,
162
+ ...(state.constant ? {
163
+ eventNameConstant: {
164
+ sourceKind: state.constant.kind,
165
+ sourceFile: state.constant.sourceFile,
166
+ declarationStartOffset: state.constant.declarationStartOffset,
167
+ declarationEndOffset: state.constant.declarationEndOffset,
168
+ },
169
+ } : {}),
170
+ ...(state.resolved.status === 'static' ? {} : {
171
+ eventNameStatus: state.resolved.status,
172
+ eventNameSourceKind: state.resolved.sourceKind,
173
+ eventNamePlaceholderKeys: state.resolved.placeholderKeys,
174
+ }),
175
+ };
176
+ }
177
+
178
+ export function createEventCallAnalysisContext(
179
+ source: ts.SourceFile,
180
+ compatibilityNames: Set<string>,
181
+ importedConstant?: ImportedEventNameResolver,
182
+ environmentReference?: EventEnvironmentReferenceResolver,
183
+ ): EventCallAnalysisContext {
184
+ return {
185
+ source,
186
+ receivers: createEventReceiverIndex(source, compatibilityNames),
187
+ constants: collectStringConstantLookups(source),
188
+ importedConstant,
189
+ environmentReference,
190
+ };
191
+ }
192
+
193
+ function eventSkeleton(
194
+ node: ts.CallExpression,
195
+ eventName: string,
196
+ resolver: EventEnvironmentReferenceResolver | undefined,
197
+ ): EventSkeletonFact | undefined {
198
+ const expression = node.arguments[0];
199
+ if (!expression || !ts.isTemplateExpression(expression)) return undefined;
200
+ const skeleton = deriveEventSkeleton(eventName);
201
+ if (!skeleton || !resolver) return skeleton;
202
+ return {
203
+ ...skeleton,
204
+ environmentBindings: expression.templateSpans.flatMap((span) => {
205
+ const reference = resolver(span.expression);
206
+ return reference ? [{
207
+ ...reference,
208
+ sourceKey: span.expression.getText(node.getSourceFile()).trim(),
209
+ }] : [];
210
+ }),
211
+ };
212
+ }
213
+
214
+ export function analyzeEventCall(
215
+ node: ts.CallExpression,
216
+ expression: ts.PropertyAccessExpression,
217
+ context: EventCallAnalysisContext,
218
+ ): EventCallAnalysis {
219
+ const method = expression.name.text;
220
+ const state = eventNameState(node, context);
221
+ if (!state) return { status: 'unclassified' };
222
+ const receiver = proveEventReceiver(
223
+ expression.expression, node, context.receivers,
224
+ );
225
+ if (excludedEvent(method, receiver, state)) return { status: 'excluded' };
226
+ const unresolvedReason = receiver.unresolvedReason
227
+ ?? state.unresolvedReason;
228
+ return {
229
+ status: 'classified',
230
+ fact: {
231
+ callType: method === 'on' ? 'async_subscribe' : 'async_emit',
232
+ serviceVariableName: receiver.effectiveReceiver,
233
+ eventNameExpr: state.eventName,
234
+ eventSkeleton: eventSkeleton(
235
+ node, state.eventName, context.environmentReference,
236
+ ),
237
+ confidence: eventConfidence(receiver, state.unresolvedReason),
238
+ unresolvedReason,
239
+ },
240
+ evidence: eventEvidence(method, receiver, state),
241
+ };
242
+ }