@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
@@ -7,6 +7,7 @@ import {
7
7
  insertCalls,
8
8
  insertExecutableSymbols,
9
9
  insertHandler,
10
+ insertGeneratedConstants,
10
11
  handlerMethodIsExecutable,
11
12
  insertRegistrations,
12
13
  insertSymbolCalls,
@@ -23,6 +24,9 @@ import {
23
24
  parseOutboundCalls,
24
25
  } from '../parsers/outbound-call-parser.js';
25
26
  import { parseExecutableSymbols } from '../parsers/symbol-parser.js';
27
+ import {
28
+ generatedConstantFacts,
29
+ } from '../parsers/generated-constants-parser.js';
26
30
  import {
27
31
  loadPackageJsonSnapshot,
28
32
  } from '../parsers/package-json-parser.js';
@@ -52,9 +56,22 @@ import {
52
56
  import {
53
57
  loadRepositorySourceContext,
54
58
  type RepositorySourceContext,
59
+ type SourceFileSnapshot,
55
60
  type SourceContextInstrumentation,
56
61
  } from '../parsers/ts-project.js';
57
- import type { CdsServiceFact, HandlerClassFact, HandlerRegistrationFact, OutboundCallFact, PackageFacts, ServiceBindingFact, ExecutableSymbolFact, SymbolCallFact } from '../types.js';
62
+ import {
63
+ createImportedEventNameResolver,
64
+ } from '../parsers/event-name-import-resolution.js';
65
+ import {
66
+ collectEnvironmentDeclarations,
67
+ type EnvironmentDeclarationsFact,
68
+ } from '../parsers/environment-declarations.js';
69
+ import {
70
+ createEventEnvironmentReferenceResolver,
71
+ } from '../parsers/event-environment-reference.js';
72
+ import { invalidateEventSurfaceFacts } from
73
+ '../db/event-surface-invalidation.js';
74
+ import type { CdsServiceFact, GeneratedConstantFact, HandlerClassFact, HandlerRegistrationFact, OutboundCallFact, PackageFacts, ServiceBindingFact, ExecutableSymbolFact, SymbolCallFact } from '../types.js';
58
75
  export interface IndexRepoResult {
59
76
  fileCount: number;
60
77
  diagnosticCount: number;
@@ -68,6 +85,7 @@ interface ParsedFacts {
68
85
  calls: OutboundCallFact[];
69
86
  symbols: ExecutableSymbolFact[];
70
87
  symbolCalls: SymbolCallFact[];
88
+ generatedConstants: GeneratedConstantFact[];
71
89
  fileRecords: Array<{ relativePath: string; extension: string; sha256: string; sizeBytes: number }>;
72
90
  }
73
91
  export interface PreparedRepositoryIndex extends IndexRepoResult {
@@ -77,6 +95,7 @@ export interface PreparedRepositoryIndex extends IndexRepoResult {
77
95
  kind?: string;
78
96
  parsed?: ParsedFacts;
79
97
  packagePublicSurface?: PackagePublicSurfaceFact;
98
+ environmentDeclarations?: EnvironmentDeclarationsFact;
80
99
  }
81
100
  export async function indexRepository(
82
101
  db: Db,
@@ -110,6 +129,7 @@ export async function prepareRepositoryIndex(
110
129
  repo: RepoRow,
111
130
  force: boolean,
112
131
  instrumentation?: SourceContextInstrumentation,
132
+ eventEnvironmentKeys?: readonly string[],
113
133
  ): Promise<PreparedRepositoryIndex> {
114
134
  const sourceFiles = await findSourceFiles(repo.absolute_path);
115
135
  const packageSnapshot = await loadPackageJsonSnapshot(repo.absolute_path, {
@@ -120,11 +140,17 @@ export async function prepareRepositoryIndex(
120
140
  const sources = await loadRepositorySourceContext(
121
141
  repo.absolute_path, sourceFiles, instrumentation,
122
142
  );
143
+ const environmentDeclarations = collectEnvironmentDeclarations(
144
+ sources, eventEnvironmentKeys,
145
+ );
123
146
  const fingerprint = repositoryFingerprint(
124
- sources, packageFacts, packageSnapshot.rawText,
147
+ sources, packageFacts, packageSnapshot.rawText, environmentDeclarations,
125
148
  );
126
149
  if (!force && repo.fingerprint === fingerprint) return { repo, fileCount: 0, diagnosticCount: 0, skipped: true };
127
- const parsedFacts = await parseAllSourceFacts(repo.absolute_path, sources);
150
+ const parsedFacts = await parseAllSourceFacts(
151
+ repo.absolute_path, sources, environmentDeclarations,
152
+ eventEnvironmentKeys,
153
+ );
128
154
  const packageSurface = analyzeRepositoryPackageSurface(
129
155
  packageFacts, packageSnapshot.manifest, sources,
130
156
  );
@@ -139,6 +165,7 @@ export async function prepareRepositoryIndex(
139
165
  kind: await classifyRepository(repo.absolute_path, packageFacts),
140
166
  parsed,
141
167
  packagePublicSurface: packageSurface.surface,
168
+ environmentDeclarations,
142
169
  fileCount: sourceFiles.length,
143
170
  diagnosticCount: parsed.handlers.filter((handler) =>
144
171
  handler.hasHandlerDecorator
@@ -154,20 +181,27 @@ export function publishPreparedRepositoryIndex(
154
181
  ): void {
155
182
  if (prepared.skipped) return;
156
183
  if (!prepared.packageFacts || !prepared.parsed || !prepared.fingerprint
157
- || !prepared.kind || !prepared.packagePublicSurface)
184
+ || !prepared.kind || !prepared.packagePublicSurface
185
+ || !prepared.environmentDeclarations)
158
186
  throw new Error('Prepared repository index is missing publication facts');
159
187
  const now = new Date().toISOString();
160
188
  const repoId = prepared.repo.id;
189
+ const environmentJson = JSON.stringify(prepared.environmentDeclarations);
161
190
  invalidatePackageTargetFacts(
162
191
  db, repoId, prepared.packageFacts.packageName, invalidations,
163
192
  );
193
+ invalidateEventSurfaceFacts(
194
+ db, repoId, prepared.parsed.calls, environmentJson,
195
+ );
164
196
  db.prepare(`UPDATE repositories SET package_name=?, package_version=?,
165
- dependencies_json=?,package_public_surface_json=?,kind=?,index_status=?
197
+ dependencies_json=?,package_public_surface_json=?,
198
+ environment_declarations_json=?,kind=?,index_status=?
166
199
  WHERE id=?`).run(
167
200
  prepared.packageFacts.packageName,
168
201
  prepared.packageFacts.packageVersion,
169
202
  JSON.stringify(prepared.packageFacts.dependencies),
170
203
  JSON.stringify(prepared.packagePublicSurface),
204
+ environmentJson,
171
205
  prepared.kind,
172
206
  'indexing',
173
207
  repoId,
@@ -183,6 +217,7 @@ export function publishPreparedRepositoryIndex(
183
217
  insertRegistrations(db, repoId, prepared.parsed.registrations);
184
218
  insertBindings(db, repoId, prepared.parsed.bindings);
185
219
  insertCalls(db, repoId, prepared.parsed.calls);
220
+ insertGeneratedConstants(db, repoId, prepared.parsed.generatedConstants);
186
221
  db.prepare("UPDATE repositories SET last_indexed_at=?, index_status='indexed', error_count=0, fingerprint=?, fact_generation=COALESCE(fact_generation,0)+1, graph_stale_reason='facts_changed', graph_stale_at=?, fact_analyzer_version=? WHERE id=?").run(now, prepared.fingerprint, now, ANALYZER_VERSION, repoId);
187
222
  }
188
223
 
@@ -242,15 +277,31 @@ export function recordIndexFailure(db: Db, repoId: number, error: unknown): void
242
277
  async function parseAllSourceFacts(
243
278
  root: string,
244
279
  sources: RepositorySourceContext,
280
+ environmentDeclarations: EnvironmentDeclarationsFact,
281
+ eventEnvironmentKeys?: readonly string[],
245
282
  ): Promise<ParsedFacts> {
246
- const facts: ParsedFacts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], fileRecords: [] };
283
+ const facts: ParsedFacts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], generatedConstants: [], fileRecords: [] };
247
284
  for (const snapshot of sources.entries()) {
248
285
  const file = snapshot.filePath;
249
- facts.fileRecords.push({ relativePath: normalizePath(file), extension: path.extname(file), sha256: sha256Text(snapshot.text), sizeBytes: snapshot.sizeBytes });
286
+ facts.fileRecords.push({
287
+ relativePath: normalizePath(file),
288
+ extension: path.extname(file),
289
+ sha256: sourceFactHash(snapshot, environmentDeclarations),
290
+ sizeBytes: snapshot.sizeBytes,
291
+ });
250
292
  if (file.endsWith('.cds')) facts.services.push(...(await parseCdsFile(root, file, sources)));
251
293
  if (/\.[jt]s$/.test(file)) {
252
294
  const source = snapshot.sourceFile();
253
- const classified = classifyOutboundCallsInSource(source, file);
295
+ facts.generatedConstants.push(...generatedConstantFacts(source, file));
296
+ const classified = classifyOutboundCallsInSource(source, file, {
297
+ importedEventNameResolver: createImportedEventNameResolver(
298
+ sources, source, file,
299
+ ),
300
+ eventEnvironmentReferenceResolver:
301
+ createEventEnvironmentReferenceResolver(
302
+ sources, source, file, eventEnvironmentKeys,
303
+ ),
304
+ });
254
305
  facts.handlers.push(...(await parseDecorators(root, file, sources)));
255
306
  facts.registrations.push(...(await parseHandlerRegistrations(root, file, sources)));
256
307
  const bindings = await parseServiceBindings(root, file, sources);
@@ -280,12 +331,17 @@ async function findSourceFiles(root: string): Promise<string[]> {
280
331
  const rel = prefix ? `${prefix}/${e.name}` : e.name;
281
332
  if (e.isDirectory()) {
282
333
  if (!['node_modules', 'dist', 'gen', 'coverage', '.git'].includes(e.name)) await walk(path.join(dir, e.name), rel);
283
- } else if (/\.(cds|ts|js)$/.test(e.name) && !isDefaultTestFile(rel)) out.push(rel);
334
+ } else if (isRepositoryFactInput(e.name)
335
+ && !isDefaultTestFile(rel)) out.push(rel);
284
336
  }
285
337
  }
286
338
  await walk(root);
287
339
  return out.sort();
288
340
  }
341
+ function isRepositoryFactInput(name: string): boolean {
342
+ return /\.(cds|ts|js)$/.test(name)
343
+ || ['nodemon.json', '.env', 'mta.yaml', 'manifest.yml'].includes(name);
344
+ }
289
345
  function isDefaultTestFile(relativeFile: string): boolean {
290
346
  const parts = relativeFile.split('/');
291
347
  if (parts.some((part) => ['test', 'tests', '__tests__'].includes(part))) return true;
@@ -295,6 +351,7 @@ function repositoryFingerprint(
295
351
  sources: RepositorySourceContext,
296
352
  facts: PackageFacts,
297
353
  packageJsonText: string,
354
+ environmentDeclarations: EnvironmentDeclarationsFact,
298
355
  ): string {
299
356
  const normalizedFacts = {
300
357
  analyzerVersion: ANALYZER_VERSION,
@@ -304,10 +361,36 @@ function repositoryFingerprint(
304
361
  cdsRequires: [...facts.cdsRequires].sort((a, b) => a.alias.localeCompare(b.alias)),
305
362
  scripts: Object.fromEntries(Object.entries(facts.scripts).sort()),
306
363
  includeTests: false,
364
+ eventEnvironmentKeys: environmentDeclarations.allowedKeys,
307
365
  packageJsonHash: sha256Text(packageJsonText),
308
366
  };
309
367
  const entries: string[] = [`facts:${JSON.stringify(normalizedFacts)}`];
310
368
  for (const snapshot of sources.entries())
311
- entries.push(`${snapshot.filePath}:${sha256Text(snapshot.text)}`);
369
+ entries.push(
370
+ `${snapshot.filePath}:${
371
+ sourceFactHash(snapshot, environmentDeclarations)}`,
372
+ );
312
373
  return sha256Text(entries.join('\n'));
313
374
  }
375
+
376
+ function sourceFactHash(
377
+ snapshot: SourceFileSnapshot,
378
+ environmentDeclarations: EnvironmentDeclarationsFact,
379
+ ): string {
380
+ if (!isEnvironmentFactInput(snapshot.filePath))
381
+ return sha256Text(snapshot.text);
382
+ const declarations = environmentDeclarations.declarations.filter(
383
+ (item) => item.sourceFile === snapshot.filePath,
384
+ );
385
+ return sha256Text(JSON.stringify({
386
+ allowedKeys: environmentDeclarations.allowedKeys,
387
+ declarations,
388
+ }));
389
+ }
390
+
391
+ function isEnvironmentFactInput(filePath: string): boolean {
392
+ const name = filePath.split('/').at(-1);
393
+ return ['nodemon.json', '.env', 'mta.yaml', 'manifest.yml'].includes(
394
+ name ?? '',
395
+ );
396
+ }
@@ -104,7 +104,12 @@ export interface IndexWorkspaceSummary {
104
104
  export async function indexWorkspace(
105
105
  db: Db,
106
106
  workspaceId: number,
107
- options: { repo?: string; force: boolean; injectDerivedMaterializationFailure?: boolean },
107
+ options: {
108
+ repo?: string;
109
+ force: boolean;
110
+ eventEnvironmentKeys?: readonly string[];
111
+ injectDerivedMaterializationFailure?: boolean;
112
+ },
108
113
  ): Promise<IndexWorkspaceSummary> {
109
114
  const repos = selectedRepositories(db, workspaceId, options.repo);
110
115
  const runId = claimIndexRun(db, workspaceId, repos.length);
@@ -112,7 +117,9 @@ export async function indexWorkspace(
112
117
  fileCount: 0, diagnosticCount: 0, skippedCount: 0, rows: [],
113
118
  };
114
119
  try {
115
- await prepareRepositories(repos, options.force, state);
120
+ await prepareRepositories(
121
+ repos, options.force, state, options.eventEnvironmentKeys,
122
+ );
116
123
  return publishPreparedWorkspaceRows(
117
124
  db, workspaceId, runId, state.rows, options,
118
125
  );
@@ -168,10 +175,13 @@ async function prepareRepositories(
168
175
  repos: readonly IndexRepository[],
169
176
  force: boolean,
170
177
  state: PreparationState,
178
+ eventEnvironmentKeys?: readonly string[],
171
179
  ): Promise<void> {
172
180
  for (const repo of repos) {
173
181
  state.activeRepoId = repo.id;
174
- const result = await prepareRepositoryIndex(repo, force);
182
+ const result = await prepareRepositoryIndex(
183
+ repo, force, undefined, eventEnvironmentKeys,
184
+ );
175
185
  state.rows.push(result);
176
186
  state.fileCount += result.fileCount;
177
187
  state.diagnosticCount += result.diagnosticCount;
@@ -4,7 +4,10 @@ import { linkImplementations as linkCanonicalImplementations } from './implement
4
4
  import { linkPackageImportSymbolCalls } from './package-import-symbol-resolver.js';
5
5
  import { linkEventSubscriptionHandlers } from './event-subscription-handler-linker.js';
6
6
  import { insertCallEdge } from './call-edge-insertion.js';
7
+ import { linkEventShapeCandidates } from './event-shape-candidate-linker.js';
7
8
  import { assertWorkspaceLinkable } from '../db/fact-lifecycle.js';
9
+ import { linkPackageEventConstants } from
10
+ './package-event-constant-resolver.js';
8
11
  export interface LinkWorkspaceResult {
9
12
  edgeCount: number;
10
13
  unresolvedCount: number;
@@ -23,11 +26,14 @@ export interface LinkWorkspaceResult {
23
26
  subscriptionHandlerAmbiguousCount: number;
24
27
  subscriptionHandlerUnresolvedCount: number;
25
28
  subscriptionHandlerMissingAssociationCount: number;
29
+ eventShapeCandidateCount: number;
30
+ eventShapeCandidateOmittedCount: number;
26
31
  }
27
32
  export function linkWorkspace(db: Db, workspaceId: number, vars: Record<string, string> = {}): LinkWorkspaceResult {
28
33
  return db.transaction(() => {
29
34
  assertWorkspaceLinkable(db, workspaceId);
30
35
  linkPackageImportSymbolCalls(db, workspaceId);
36
+ linkPackageEventConstants(db, workspaceId);
31
37
  assertWorkspaceLinkable(db, workspaceId, 'terminal');
32
38
  const generation = nextGraphGeneration(db, workspaceId);
33
39
  db.prepare('DELETE FROM graph_edges WHERE workspace_id=?').run(workspaceId);
@@ -35,10 +41,13 @@ export function linkWorkspace(db: Db, workspaceId: number, vars: Record<string,
35
41
  const subscriptions = linkEventSubscriptionHandlers(
36
42
  db, workspaceId, generation, vars,
37
43
  );
44
+ const eventShapes = linkEventShapeCandidates(
45
+ db, workspaceId, generation,
46
+ );
38
47
  const impl = linkCanonicalImplementations(db, workspaceId, generation);
39
48
  const callSummary = linkCalls(db, workspaceId, vars, generation);
40
49
  db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
41
- return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount + subscriptions.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount, implementationUnresolvedCount: impl.unresolvedCount, subscriptionHandlerResolvedCount: subscriptions.resolvedCount, subscriptionHandlerAmbiguousCount: subscriptions.ambiguousCount, subscriptionHandlerUnresolvedCount: subscriptions.unresolvedCount, subscriptionHandlerMissingAssociationCount: subscriptions.missingAssociationCount };
50
+ return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount + subscriptions.edgeCount + eventShapes.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount, implementationUnresolvedCount: impl.unresolvedCount, subscriptionHandlerResolvedCount: subscriptions.resolvedCount, subscriptionHandlerAmbiguousCount: subscriptions.ambiguousCount, subscriptionHandlerUnresolvedCount: subscriptions.unresolvedCount, subscriptionHandlerMissingAssociationCount: subscriptions.missingAssociationCount, eventShapeCandidateCount: eventShapes.edgeCount, eventShapeCandidateOmittedCount: eventShapes.omittedCount };
42
51
  });
43
52
  }
44
53
  function nextGraphGeneration(db: Db, workspaceId: number): number {
@@ -54,7 +63,9 @@ type CallLinkSummary = Omit<LinkWorkspaceResult,
54
63
  | 'subscriptionHandlerResolvedCount'
55
64
  | 'subscriptionHandlerAmbiguousCount'
56
65
  | 'subscriptionHandlerUnresolvedCount'
57
- | 'subscriptionHandlerMissingAssociationCount'>;
66
+ | 'subscriptionHandlerMissingAssociationCount'
67
+ | 'eventShapeCandidateCount'
68
+ | 'eventShapeCandidateOmittedCount'>;
58
69
 
59
70
  function linkCalls(db: Db, workspaceId: number, vars: Record<string, string>, generation: number): CallLinkSummary {
60
71
  let edgeCount = 0;
@@ -65,7 +76,20 @@ function linkCalls(db: Db, workspaceId: number, vars: Record<string, string>, ge
65
76
  let ambiguousCount = 0;
66
77
  let dynamicCount = 0;
67
78
  let terminalCount = 0;
68
- const calls = db.prepare(`SELECT c.*,r.name repoName,b.id selectedBindingId,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.is_dynamic isDynamic,b.placeholders_json placeholdersJson,b.source_file bindingSourceFile,b.source_line bindingSourceLine,b.helper_chain_json helperChainJson,req.service_path requireServicePath,req.destination requireDestination FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id LEFT JOIN service_bindings b ON b.id=c.service_binding_id LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias WHERE r.workspace_id=?`).all(workspaceId) as Array<Record<string, unknown>>;
79
+ const calls = db.prepare(`SELECT c.*,r.name repoName,
80
+ r.environment_declarations_json environmentDeclarationsJson,
81
+ b.id selectedBindingId,b.alias,b.alias_expr aliasExpr,
82
+ b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,
83
+ b.is_dynamic isDynamic,b.placeholders_json placeholdersJson,
84
+ b.source_file bindingSourceFile,b.source_line bindingSourceLine,
85
+ b.helper_chain_json helperChainJson,req.service_path requireServicePath,
86
+ req.destination requireDestination
87
+ FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
88
+ LEFT JOIN service_bindings b ON b.id=c.service_binding_id
89
+ LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias
90
+ WHERE r.workspace_id=?`).all(
91
+ workspaceId,
92
+ ) as Array<Record<string, unknown>>;
69
93
  for (const call of calls) {
70
94
  const result = insertCallEdge(db, workspaceId, call, vars, generation);
71
95
  edgeCount += 1;
@@ -0,0 +1,211 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import {
3
+ applyEventEnvironmentTransforms,
4
+ type EventEnvironmentReference,
5
+ } from '../parsers/event-environment-reference.js';
6
+ import {
7
+ parseEnvironmentDeclarationsFact,
8
+ type EnvironmentDeclarationsFact,
9
+ } from '../parsers/environment-declarations.js';
10
+ import {
11
+ eventTemplateVariables,
12
+ parseEventSkeletonFact,
13
+ } from '../utils/event-skeleton.js';
14
+
15
+ export interface EventEnvironmentResolution {
16
+ status: 'resolved' | 'ambiguous' | 'unresolved' | 'not_applicable';
17
+ variables: Record<string, string>;
18
+ reason?: string;
19
+ provenance: Array<Record<string, unknown>>;
20
+ }
21
+
22
+ export interface SubscriptionEnvironmentTarget {
23
+ consumerRepoId?: number;
24
+ consumerRepoName?: string;
25
+ resolution: EventEnvironmentResolution;
26
+ collisionCount: number;
27
+ }
28
+
29
+ function record(value: unknown): Record<string, unknown> | undefined {
30
+ return value && typeof value === 'object' && !Array.isArray(value)
31
+ ? value as Record<string, unknown> : undefined;
32
+ }
33
+
34
+ function parsedJson(value: unknown): unknown {
35
+ if (typeof value !== 'string') return value;
36
+ try {
37
+ return JSON.parse(value) as unknown;
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ }
42
+
43
+ function bindingEnvironmentValues(
44
+ binding: EventEnvironmentReference,
45
+ fact: EnvironmentDeclarationsFact,
46
+ ): string[] {
47
+ if (!binding.environmentKey) return [];
48
+ return [...new Set(fact.declarations
49
+ .filter((item) => item.key === binding.environmentKey)
50
+ .map((item) => item.value))];
51
+ }
52
+
53
+ function bindingResolution(
54
+ binding: EventEnvironmentReference,
55
+ fact: EnvironmentDeclarationsFact,
56
+ ): { value?: string; reason?: string; ambiguous?: boolean } {
57
+ if (binding.status === 'refused')
58
+ return { reason: binding.reason ?? 'event_environment_reference_refused' };
59
+ if (fact.status === 'incomplete') return {
60
+ reason: 'event_environment_declarations_incomplete',
61
+ };
62
+ const values = bindingEnvironmentValues(binding, fact);
63
+ if (fact.status === 'ambiguous' || values.length > 1) return {
64
+ reason: 'event_environment_declaration_ambiguous', ambiguous: true,
65
+ };
66
+ const value = values[0];
67
+ if (value === undefined)
68
+ return { reason: 'event_environment_declaration_missing' };
69
+ return {
70
+ value: applyEventEnvironmentTransforms(value, binding.transforms),
71
+ };
72
+ }
73
+
74
+ function provenance(
75
+ binding: EventEnvironmentReference,
76
+ fact: EnvironmentDeclarationsFact,
77
+ ): Record<string, unknown> {
78
+ const rank = new Map([
79
+ ['env_declaration_mta', 0],
80
+ ['env_declaration_manifest', 1],
81
+ ['env_declaration_dotenv', 2],
82
+ ['env_declaration_dev', 3],
83
+ ]);
84
+ const declaration = fact.declarations.filter((item) =>
85
+ item.key === binding.environmentKey).sort((left, right) =>
86
+ (rank.get(left.provenance) ?? 99) - (rank.get(right.provenance) ?? 99)
87
+ || (left.sourceFile < right.sourceFile
88
+ ? -1 : left.sourceFile > right.sourceFile ? 1 : 0)
89
+ || left.startOffset - right.startOffset)[0];
90
+ return {
91
+ sourceKey: binding.sourceKey,
92
+ environmentKey: binding.environmentKey,
93
+ transforms: binding.transforms,
94
+ declarationProvenance: declaration?.provenance,
95
+ declarationSourceFile: declaration?.sourceFile,
96
+ declarationStartOffset: declaration?.startOffset,
97
+ declarationEndOffset: declaration?.endOffset,
98
+ };
99
+ }
100
+
101
+ export function resolveEventEnvironment(
102
+ skeletonValue: unknown,
103
+ environmentValue: unknown,
104
+ variables: Record<string, string>,
105
+ ): EventEnvironmentResolution {
106
+ const skeleton = parseEventSkeletonFact(skeletonValue);
107
+ if (!skeleton || skeleton.environmentBindings.length === 0) return {
108
+ status: 'not_applicable',
109
+ variables: eventTemplateVariables(skeleton, variables),
110
+ provenance: [],
111
+ };
112
+ const fact = parseEnvironmentDeclarationsFact(environmentValue);
113
+ if (!fact) return {
114
+ status: 'unresolved', variables,
115
+ reason: 'event_environment_declarations_invalid', provenance: [],
116
+ };
117
+ const expanded = eventTemplateVariables(skeleton, variables);
118
+ const outcomes = skeleton.environmentBindings.map((binding) =>
119
+ Object.hasOwn(expanded, binding.sourceKey)
120
+ ? {} : bindingResolution(binding, fact));
121
+ for (let index = 0; index < outcomes.length; index += 1) {
122
+ const binding = skeleton.environmentBindings[index];
123
+ const outcome = outcomes[index];
124
+ if (binding?.sourceKey && outcome?.value !== undefined
125
+ && !Object.hasOwn(expanded, binding.sourceKey))
126
+ expanded[binding.sourceKey] = outcome.value;
127
+ }
128
+ const ambiguous = outcomes.find((outcome) => outcome.ambiguous);
129
+ const unresolved = outcomes.find((outcome) => outcome.reason);
130
+ return {
131
+ status: ambiguous ? 'ambiguous'
132
+ : unresolved ? 'unresolved' : 'resolved',
133
+ variables: expanded,
134
+ reason: ambiguous?.reason ?? unresolved?.reason,
135
+ provenance: skeleton.environmentBindings.map((binding) =>
136
+ provenance(binding, fact)),
137
+ };
138
+ }
139
+
140
+ function dependencies(value: unknown): Record<string, string> {
141
+ const parsed = record(parsedJson(value));
142
+ if (!parsed) return {};
143
+ return Object.fromEntries(Object.entries(parsed).flatMap(([key, item]) =>
144
+ typeof item === 'string' ? [[key, item]] : []));
145
+ }
146
+
147
+ function consumerRows(
148
+ db: Db,
149
+ workspaceId: number,
150
+ packageName: string,
151
+ ): Array<Record<string, unknown>> {
152
+ return db.prepare(`SELECT id,name,dependencies_json dependenciesJson,
153
+ environment_declarations_json environmentJson
154
+ FROM repositories WHERE workspace_id=?
155
+ ORDER BY name COLLATE BINARY,id`).all(workspaceId)
156
+ .filter((row) => Object.hasOwn(
157
+ dependencies(row.dependenciesJson), packageName,
158
+ ));
159
+ }
160
+
161
+ function collisionCounts(
162
+ targets: readonly SubscriptionEnvironmentTarget[],
163
+ ): Map<string, number> {
164
+ const counts = new Map<string, number>();
165
+ for (const target of targets) {
166
+ const values = Object.entries(target.resolution.variables)
167
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
168
+ const key = JSON.stringify(values);
169
+ counts.set(key, (counts.get(key) ?? 0) + 1);
170
+ }
171
+ return counts;
172
+ }
173
+
174
+ export function subscriptionEnvironmentTargets(
175
+ db: Db,
176
+ workspaceId: number,
177
+ packageName: string | undefined,
178
+ skeletonValue: unknown,
179
+ ownEnvironmentValue: unknown,
180
+ variables: Record<string, string>,
181
+ ): SubscriptionEnvironmentTarget[] {
182
+ const skeleton = parseEventSkeletonFact(skeletonValue);
183
+ if (!skeleton || skeleton.environmentBindings.length === 0
184
+ || !packageName) return [{
185
+ resolution: resolveEventEnvironment(
186
+ skeletonValue, ownEnvironmentValue, variables,
187
+ ),
188
+ collisionCount: 1,
189
+ }];
190
+ const rows = consumerRows(db, workspaceId, packageName);
191
+ const provisional = rows.map((row): SubscriptionEnvironmentTarget => ({
192
+ consumerRepoId: Number(row.id),
193
+ consumerRepoName: String(row.name),
194
+ resolution: resolveEventEnvironment(
195
+ skeletonValue, row.environmentJson, variables,
196
+ ),
197
+ collisionCount: 1,
198
+ }));
199
+ if (provisional.length === 0) return [{
200
+ resolution: resolveEventEnvironment(
201
+ skeletonValue, ownEnvironmentValue, variables,
202
+ ),
203
+ collisionCount: 1,
204
+ }];
205
+ const counts = collisionCounts(provisional);
206
+ return provisional.map((target) => {
207
+ const key = JSON.stringify(Object.entries(target.resolution.variables)
208
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
209
+ return { ...target, collisionCount: counts.get(key) ?? 1 };
210
+ });
211
+ }