@saptools/service-flow 0.1.70 → 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 (55) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +9 -5
  3. package/TECHNICAL-NOTE.md +13 -0
  4. package/dist/{chunk-GSLFY6J2.js → chunk-Z6D433R5.js} +8696 -6476
  5. package/dist/chunk-Z6D433R5.js.map +1 -0
  6. package/dist/cli.js +685 -142
  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/doctor.ts +4 -6
  14. package/src/cli.ts +1 -1
  15. package/src/db/call-fact-repository.ts +6 -3
  16. package/src/db/current-fact-semantics.ts +5 -5
  17. package/src/db/event-fact-semantics.ts +347 -0
  18. package/src/db/event-surface-invalidation.ts +38 -0
  19. package/src/db/fact-json-inventory.ts +16 -0
  20. package/src/db/migrations.ts +12 -1
  21. package/src/db/package-target-invalidation.ts +79 -0
  22. package/src/db/repositories.ts +28 -2
  23. package/src/db/schema-structure.ts +41 -1
  24. package/src/db/schema.ts +6 -2
  25. package/src/indexer/repository-indexer.ts +45 -6
  26. package/src/linker/cross-repo-linker.ts +25 -3
  27. package/src/linker/event-environment-link.ts +211 -0
  28. package/src/linker/event-shape-candidate-linker.ts +161 -0
  29. package/src/linker/event-subscription-handler-linker.ts +123 -19
  30. package/src/linker/event-template-link.ts +40 -6
  31. package/src/linker/package-event-constant-resolver.ts +298 -0
  32. package/src/output/table-output.ts +13 -1
  33. package/src/parsers/decorator-parser.ts +9 -53
  34. package/src/parsers/environment-declarations.ts +327 -0
  35. package/src/parsers/event-call-analysis.ts +242 -0
  36. package/src/parsers/event-environment-reference.ts +231 -0
  37. package/src/parsers/event-loop-registration.ts +132 -0
  38. package/src/parsers/event-name-import-resolution.ts +243 -0
  39. package/src/parsers/event-receiver-analysis.ts +404 -0
  40. package/src/parsers/event-subscription-facts.ts +4 -0
  41. package/src/parsers/generated-constants-parser.ts +80 -14
  42. package/src/parsers/outbound-call-classifier.ts +27 -124
  43. package/src/parsers/outbound-call-parser.ts +13 -1
  44. package/src/parsers/outbound-expression-analysis.ts +2 -2
  45. package/src/parsers/stable-local-value.ts +30 -9
  46. package/src/parsers/string-constant-lookups.ts +358 -0
  47. package/src/trace/event-runtime-resolution.ts +24 -3
  48. package/src/trace/event-shape-candidate-trace.ts +172 -0
  49. package/src/trace/event-subscriber-traversal.ts +19 -7
  50. package/src/trace/evidence.ts +7 -0
  51. package/src/trace/trace-scope-execution.ts +21 -29
  52. package/src/types.ts +17 -1
  53. package/src/utils/event-skeleton.ts +207 -0
  54. package/src/version.ts +1 -1
  55. package/dist/chunk-GSLFY6J2.js.map +0 -1
@@ -12,7 +12,8 @@ const requiredColumns = {
12
12
  repositories: [
13
13
  'id', 'workspace_id', 'name', 'absolute_path', 'relative_path',
14
14
  'package_name', 'package_version', 'dependencies_json',
15
- 'package_public_surface_json', 'kind', 'is_git_repo', 'last_indexed_at',
15
+ 'package_public_surface_json', 'environment_declarations_json',
16
+ 'kind', 'is_git_repo', 'last_indexed_at',
16
17
  'index_status', 'error_count', 'fingerprint', 'fact_generation',
17
18
  'graph_generation', 'graph_stale_reason', 'graph_stale_at',
18
19
  'fact_analyzer_version',
@@ -66,6 +67,7 @@ const requiredColumns = {
66
67
  outbound_calls: [
67
68
  'id', 'repo_id', 'source_symbol_id', 'service_binding_id', 'call_type',
68
69
  'method', 'operation_path_expr', 'query_entity', 'event_name_expr',
70
+ 'event_skeleton_signature', 'event_skeleton_json',
69
71
  'payload_summary', 'source_file', 'source_line',
70
72
  'call_site_start_offset', 'call_site_end_offset', 'evidence_json',
71
73
  'confidence', 'unresolved_reason', 'local_service_name',
@@ -79,6 +81,13 @@ const requiredColumns = {
79
81
  'call_site_end_offset', 'call_role', 'status', 'evidence_json',
80
82
  'confidence', 'unresolved_reason',
81
83
  ],
84
+ generated_constants: [
85
+ 'id', 'repo_id', 'source_file', 'source_line', 'name',
86
+ 'container_name', 'member_name', 'value', 'constant_kind', 'exported',
87
+ 'stable', 'resolution_status', 'unresolved_reason',
88
+ 'declaration_start_offset', 'declaration_end_offset',
89
+ 'value_start_offset', 'value_end_offset',
90
+ ],
82
91
  graph_edges: [
83
92
  'id', 'workspace_id', 'edge_type', 'status', 'from_kind', 'from_id',
84
93
  'to_kind', 'to_id', 'confidence', 'evidence_json', 'is_dynamic',
@@ -167,6 +176,33 @@ function bindingIndexInvalid(
167
176
  && metadata?.unique === unique && metadata.partial === partial ? 0 : 1;
168
177
  }
169
178
 
179
+ function tableIndexInvalid(
180
+ db: Db,
181
+ table: string,
182
+ name: string,
183
+ columns: readonly string[],
184
+ unique: number,
185
+ ): number {
186
+ const metadata = db.prepare(`PRAGMA index_list(${table})`)
187
+ .all().find((item) => item.name === name);
188
+ return metadata?.unique === unique
189
+ && indexColumns(db, name).join('\0') === columns.join('\0') ? 0 : 1;
190
+ }
191
+
192
+ function eventSurfaceIndexesInvalid(db: Db): number {
193
+ return tableIndexInvalid(
194
+ db, 'outbound_calls', 'idx_outbound_event_skeleton',
195
+ ['event_skeleton_signature', 'call_type', 'repo_id'], 0,
196
+ ) + tableIndexInvalid(
197
+ db, 'generated_constants', 'idx_generated_constant_name',
198
+ ['repo_id', 'source_file', 'name'], 0,
199
+ ) + tableIndexInvalid(
200
+ db, 'generated_constants', 'uq_generated_constant_site',
201
+ ['repo_id', 'source_file', 'name', 'declaration_start_offset',
202
+ 'declaration_end_offset'], 1,
203
+ );
204
+ }
205
+
170
206
  function exactBindingIndexInvalid(db: Db): number {
171
207
  const row = db.prepare(`SELECT sql FROM sqlite_master
172
208
  WHERE type='index' AND name='uq_service_binding_exact_site'`).get();
@@ -197,5 +233,9 @@ export function invalidSchemaStructureCategories(
197
233
  exactBindingIndexInvalid(db)
198
234
  + bindingIndexInvalid(db, 'idx_service_binding_site', 0, 0),
199
235
  ),
236
+ ...category(
237
+ 'schema_event_surface_index_invalid',
238
+ eventSurfaceIndexesInvalid(db),
239
+ ),
200
240
  ];
201
241
  }
package/src/db/schema.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export const schemaTablesSql = `
2
2
  CREATE TABLE IF NOT EXISTS workspaces (id INTEGER PRIMARY KEY, root_path TEXT UNIQUE NOT NULL, db_path TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
3
- CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', package_public_surface_json TEXT, kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, fingerprint TEXT, fact_generation INTEGER NOT NULL DEFAULT 0, graph_generation INTEGER NOT NULL DEFAULT 0, graph_stale_reason TEXT, graph_stale_at TEXT, fact_analyzer_version TEXT, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
3
+ CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', package_public_surface_json TEXT, environment_declarations_json TEXT DEFAULT '{"schema":"service-flow/environment-declarations@1","status":"not_applicable","reason":null,"recordCap":32,"total":0,"shown":0,"omitted":0,"declarations":[]}', kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, fingerprint TEXT, fact_generation INTEGER NOT NULL DEFAULT 0, graph_generation INTEGER NOT NULL DEFAULT 0, graph_stale_reason TEXT, graph_stale_at TEXT, fact_analyzer_version TEXT, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
4
4
  CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, relative_path TEXT NOT NULL, extension TEXT NOT NULL, sha256 TEXT NOT NULL, size_bytes INTEGER NOT NULL, last_indexed_at TEXT NOT NULL, UNIQUE(repo_id, relative_path), FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
5
5
  CREATE TABLE IF NOT EXISTS cds_requires (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, alias TEXT NOT NULL, kind TEXT, model TEXT, destination TEXT, service_path TEXT, request_timeout INTEGER, raw_json TEXT NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
6
6
  CREATE TABLE IF NOT EXISTS cds_services (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, namespace TEXT, service_name TEXT NOT NULL, qualified_name TEXT NOT NULL, service_path TEXT NOT NULL, is_extend INTEGER NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, extension_local_ref TEXT, extension_imported_symbol TEXT, extension_local_alias TEXT, extension_module_specifier TEXT, extension_import_kind TEXT, extension_base_service_id INTEGER, extension_base_status TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(extension_base_service_id) REFERENCES cds_services(id) ON DELETE SET NULL);
@@ -10,7 +10,8 @@ CREATE TABLE IF NOT EXISTS handler_classes (id INTEGER PRIMARY KEY, repo_id INTE
10
10
  CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_class_id INTEGER NOT NULL, method_name TEXT NOT NULL, decorator_kind TEXT NOT NULL, decorator_value TEXT, decorator_raw_expression TEXT NOT NULL, decorator_resolution_json TEXT NOT NULL DEFAULT '{}', source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE CASCADE);
11
11
  CREATE TABLE IF NOT EXISTS handler_registrations (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, handler_class_id INTEGER, class_name TEXT, import_source TEXT, registration_file TEXT NOT NULL, registration_line INTEGER NOT NULL, registration_kind TEXT NOT NULL, confidence REAL NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE SET NULL);
12
12
  CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, variable_name TEXT NOT NULL, alias TEXT, alias_expr TEXT, destination_expr TEXT, service_path_expr TEXT, is_dynamic INTEGER NOT NULL, placeholders_json TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, binding_site_start_offset INTEGER, binding_site_end_offset INTEGER, owner_resolution TEXT NOT NULL DEFAULT 'legacy_unknown' CHECK(owner_resolution IN ('owned_exact','ownerless_file_scope','legacy_unknown')), helper_chain_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
13
- CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, call_site_start_offset INTEGER, call_site_end_offset INTEGER, confidence REAL NOT NULL, unresolved_reason TEXT, local_service_name TEXT, local_service_lookup TEXT, alias_chain_json TEXT, evidence_json TEXT, external_target_kind TEXT, external_target_id TEXT, external_target_label TEXT, external_target_dynamic INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
13
+ CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, event_skeleton_signature TEXT, event_skeleton_json TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, call_site_start_offset INTEGER, call_site_end_offset INTEGER, confidence REAL NOT NULL, unresolved_reason TEXT, local_service_name TEXT, local_service_lookup TEXT, alias_chain_json TEXT, evidence_json TEXT, external_target_kind TEXT, external_target_id TEXT, external_target_label TEXT, external_target_dynamic INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
14
+ CREATE TABLE IF NOT EXISTS generated_constants (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, name TEXT NOT NULL, container_name TEXT, member_name TEXT, value TEXT, constant_kind TEXT NOT NULL, exported INTEGER NOT NULL, stable INTEGER NOT NULL, resolution_status TEXT NOT NULL CHECK(resolution_status IN ('resolved','refused')), unresolved_reason TEXT, declaration_start_offset INTEGER NOT NULL, declaration_end_offset INTEGER NOT NULL, value_start_offset INTEGER NOT NULL, value_end_offset INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
14
15
  CREATE TABLE IF NOT EXISTS symbol_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, caller_symbol_id INTEGER NOT NULL, callee_symbol_id INTEGER, callee_expression TEXT NOT NULL, import_source TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, call_site_start_offset INTEGER, call_site_end_offset INTEGER, call_role TEXT NOT NULL DEFAULT 'legacy_unknown', status TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, unresolved_reason TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(caller_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE, FOREIGN KEY(callee_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
15
16
  CREATE TABLE IF NOT EXISTS graph_edges (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, edge_type TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'unresolved', from_kind TEXT NOT NULL, from_id TEXT NOT NULL, to_kind TEXT NOT NULL, to_id TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, is_dynamic INTEGER NOT NULL, unresolved_reason TEXT, generation INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
16
17
  CREATE TABLE IF NOT EXISTS index_runs (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, repo_count INTEGER NOT NULL, file_count INTEGER NOT NULL, diagnostic_count INTEGER NOT NULL, error_message TEXT, owner_pid INTEGER, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
@@ -24,6 +25,9 @@ CREATE INDEX IF NOT EXISTS idx_extension_import ON cds_services(extension_module
24
25
  CREATE INDEX IF NOT EXISTS idx_operation_name ON cds_operations(operation_name, operation_path);
25
26
  CREATE INDEX IF NOT EXISTS idx_operation_base ON cds_operations(base_operation_id);
26
27
  CREATE INDEX IF NOT EXISTS idx_calls_repo ON outbound_calls(repo_id, call_type);
28
+ CREATE INDEX IF NOT EXISTS idx_outbound_event_skeleton ON outbound_calls(event_skeleton_signature,call_type,repo_id);
29
+ CREATE INDEX IF NOT EXISTS idx_generated_constant_name ON generated_constants(repo_id,source_file,name);
30
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_generated_constant_site ON generated_constants(repo_id,source_file,name,declaration_start_offset,declaration_end_offset);
27
31
  CREATE INDEX IF NOT EXISTS idx_symbol_calls_caller ON symbol_calls(repo_id, caller_symbol_id);
28
32
  CREATE INDEX IF NOT EXISTS idx_outbound_call_site ON outbound_calls(repo_id, source_file, call_site_start_offset, call_site_end_offset, call_type);
29
33
  CREATE INDEX IF NOT EXISTS idx_symbol_call_site_role ON symbol_calls(repo_id, source_file, call_site_start_offset, call_site_end_offset, call_role);
@@ -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';
@@ -54,7 +58,19 @@ import {
54
58
  type RepositorySourceContext,
55
59
  type SourceContextInstrumentation,
56
60
  } from '../parsers/ts-project.js';
57
- import type { CdsServiceFact, HandlerClassFact, HandlerRegistrationFact, OutboundCallFact, PackageFacts, ServiceBindingFact, ExecutableSymbolFact, SymbolCallFact } from '../types.js';
61
+ import {
62
+ createImportedEventNameResolver,
63
+ } from '../parsers/event-name-import-resolution.js';
64
+ import {
65
+ collectEnvironmentDeclarations,
66
+ type EnvironmentDeclarationsFact,
67
+ } from '../parsers/environment-declarations.js';
68
+ import {
69
+ createEventEnvironmentReferenceResolver,
70
+ } from '../parsers/event-environment-reference.js';
71
+ import { invalidateEventSurfaceFacts } from
72
+ '../db/event-surface-invalidation.js';
73
+ import type { CdsServiceFact, GeneratedConstantFact, HandlerClassFact, HandlerRegistrationFact, OutboundCallFact, PackageFacts, ServiceBindingFact, ExecutableSymbolFact, SymbolCallFact } from '../types.js';
58
74
  export interface IndexRepoResult {
59
75
  fileCount: number;
60
76
  diagnosticCount: number;
@@ -68,6 +84,7 @@ interface ParsedFacts {
68
84
  calls: OutboundCallFact[];
69
85
  symbols: ExecutableSymbolFact[];
70
86
  symbolCalls: SymbolCallFact[];
87
+ generatedConstants: GeneratedConstantFact[];
71
88
  fileRecords: Array<{ relativePath: string; extension: string; sha256: string; sizeBytes: number }>;
72
89
  }
73
90
  export interface PreparedRepositoryIndex extends IndexRepoResult {
@@ -77,6 +94,7 @@ export interface PreparedRepositoryIndex extends IndexRepoResult {
77
94
  kind?: string;
78
95
  parsed?: ParsedFacts;
79
96
  packagePublicSurface?: PackagePublicSurfaceFact;
97
+ environmentDeclarations?: EnvironmentDeclarationsFact;
80
98
  }
81
99
  export async function indexRepository(
82
100
  db: Db,
@@ -139,6 +157,7 @@ export async function prepareRepositoryIndex(
139
157
  kind: await classifyRepository(repo.absolute_path, packageFacts),
140
158
  parsed,
141
159
  packagePublicSurface: packageSurface.surface,
160
+ environmentDeclarations: collectEnvironmentDeclarations(sources),
142
161
  fileCount: sourceFiles.length,
143
162
  diagnosticCount: parsed.handlers.filter((handler) =>
144
163
  handler.hasHandlerDecorator
@@ -154,20 +173,27 @@ export function publishPreparedRepositoryIndex(
154
173
  ): void {
155
174
  if (prepared.skipped) return;
156
175
  if (!prepared.packageFacts || !prepared.parsed || !prepared.fingerprint
157
- || !prepared.kind || !prepared.packagePublicSurface)
176
+ || !prepared.kind || !prepared.packagePublicSurface
177
+ || !prepared.environmentDeclarations)
158
178
  throw new Error('Prepared repository index is missing publication facts');
159
179
  const now = new Date().toISOString();
160
180
  const repoId = prepared.repo.id;
181
+ const environmentJson = JSON.stringify(prepared.environmentDeclarations);
161
182
  invalidatePackageTargetFacts(
162
183
  db, repoId, prepared.packageFacts.packageName, invalidations,
163
184
  );
185
+ invalidateEventSurfaceFacts(
186
+ db, repoId, prepared.parsed.calls, environmentJson,
187
+ );
164
188
  db.prepare(`UPDATE repositories SET package_name=?, package_version=?,
165
- dependencies_json=?,package_public_surface_json=?,kind=?,index_status=?
189
+ dependencies_json=?,package_public_surface_json=?,
190
+ environment_declarations_json=?,kind=?,index_status=?
166
191
  WHERE id=?`).run(
167
192
  prepared.packageFacts.packageName,
168
193
  prepared.packageFacts.packageVersion,
169
194
  JSON.stringify(prepared.packageFacts.dependencies),
170
195
  JSON.stringify(prepared.packagePublicSurface),
196
+ environmentJson,
171
197
  prepared.kind,
172
198
  'indexing',
173
199
  repoId,
@@ -183,6 +209,7 @@ export function publishPreparedRepositoryIndex(
183
209
  insertRegistrations(db, repoId, prepared.parsed.registrations);
184
210
  insertBindings(db, repoId, prepared.parsed.bindings);
185
211
  insertCalls(db, repoId, prepared.parsed.calls);
212
+ insertGeneratedConstants(db, repoId, prepared.parsed.generatedConstants);
186
213
  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
214
  }
188
215
 
@@ -243,14 +270,21 @@ async function parseAllSourceFacts(
243
270
  root: string,
244
271
  sources: RepositorySourceContext,
245
272
  ): Promise<ParsedFacts> {
246
- const facts: ParsedFacts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], fileRecords: [] };
273
+ const facts: ParsedFacts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], generatedConstants: [], fileRecords: [] };
247
274
  for (const snapshot of sources.entries()) {
248
275
  const file = snapshot.filePath;
249
276
  facts.fileRecords.push({ relativePath: normalizePath(file), extension: path.extname(file), sha256: sha256Text(snapshot.text), sizeBytes: snapshot.sizeBytes });
250
277
  if (file.endsWith('.cds')) facts.services.push(...(await parseCdsFile(root, file, sources)));
251
278
  if (/\.[jt]s$/.test(file)) {
252
279
  const source = snapshot.sourceFile();
253
- const classified = classifyOutboundCallsInSource(source, file);
280
+ facts.generatedConstants.push(...generatedConstantFacts(source, file));
281
+ const classified = classifyOutboundCallsInSource(source, file, {
282
+ importedEventNameResolver: createImportedEventNameResolver(
283
+ sources, source, file,
284
+ ),
285
+ eventEnvironmentReferenceResolver:
286
+ createEventEnvironmentReferenceResolver(sources, source, file),
287
+ });
254
288
  facts.handlers.push(...(await parseDecorators(root, file, sources)));
255
289
  facts.registrations.push(...(await parseHandlerRegistrations(root, file, sources)));
256
290
  const bindings = await parseServiceBindings(root, file, sources);
@@ -280,12 +314,17 @@ async function findSourceFiles(root: string): Promise<string[]> {
280
314
  const rel = prefix ? `${prefix}/${e.name}` : e.name;
281
315
  if (e.isDirectory()) {
282
316
  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);
317
+ } else if (isRepositoryFactInput(e.name)
318
+ && !isDefaultTestFile(rel)) out.push(rel);
284
319
  }
285
320
  }
286
321
  await walk(root);
287
322
  return out.sort();
288
323
  }
324
+ function isRepositoryFactInput(name: string): boolean {
325
+ return /\.(cds|ts|js)$/.test(name)
326
+ || ['nodemon.json', '.env', 'mta.yaml', 'manifest.yml'].includes(name);
327
+ }
289
328
  function isDefaultTestFile(relativeFile: string): boolean {
290
329
  const parts = relativeFile.split('/');
291
330
  if (parts.some((part) => ['test', 'tests', '__tests__'].includes(part))) return true;
@@ -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,13 @@ export interface LinkWorkspaceResult {
23
26
  subscriptionHandlerAmbiguousCount: number;
24
27
  subscriptionHandlerUnresolvedCount: number;
25
28
  subscriptionHandlerMissingAssociationCount: number;
29
+ eventShapeCandidateCount: number;
26
30
  }
27
31
  export function linkWorkspace(db: Db, workspaceId: number, vars: Record<string, string> = {}): LinkWorkspaceResult {
28
32
  return db.transaction(() => {
29
33
  assertWorkspaceLinkable(db, workspaceId);
30
34
  linkPackageImportSymbolCalls(db, workspaceId);
35
+ linkPackageEventConstants(db, workspaceId);
31
36
  assertWorkspaceLinkable(db, workspaceId, 'terminal');
32
37
  const generation = nextGraphGeneration(db, workspaceId);
33
38
  db.prepare('DELETE FROM graph_edges WHERE workspace_id=?').run(workspaceId);
@@ -35,10 +40,13 @@ export function linkWorkspace(db: Db, workspaceId: number, vars: Record<string,
35
40
  const subscriptions = linkEventSubscriptionHandlers(
36
41
  db, workspaceId, generation, vars,
37
42
  );
43
+ const eventShapes = linkEventShapeCandidates(
44
+ db, workspaceId, generation,
45
+ );
38
46
  const impl = linkCanonicalImplementations(db, workspaceId, generation);
39
47
  const callSummary = linkCalls(db, workspaceId, vars, generation);
40
48
  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 };
49
+ 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 };
42
50
  });
43
51
  }
44
52
  function nextGraphGeneration(db: Db, workspaceId: number): number {
@@ -54,7 +62,8 @@ type CallLinkSummary = Omit<LinkWorkspaceResult,
54
62
  | 'subscriptionHandlerResolvedCount'
55
63
  | 'subscriptionHandlerAmbiguousCount'
56
64
  | 'subscriptionHandlerUnresolvedCount'
57
- | 'subscriptionHandlerMissingAssociationCount'>;
65
+ | 'subscriptionHandlerMissingAssociationCount'
66
+ | 'eventShapeCandidateCount'>;
58
67
 
59
68
  function linkCalls(db: Db, workspaceId: number, vars: Record<string, string>, generation: number): CallLinkSummary {
60
69
  let edgeCount = 0;
@@ -65,7 +74,20 @@ function linkCalls(db: Db, workspaceId: number, vars: Record<string, string>, ge
65
74
  let ambiguousCount = 0;
66
75
  let dynamicCount = 0;
67
76
  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>>;
77
+ const calls = db.prepare(`SELECT c.*,r.name repoName,
78
+ r.environment_declarations_json environmentDeclarationsJson,
79
+ b.id selectedBindingId,b.alias,b.alias_expr aliasExpr,
80
+ b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,
81
+ b.is_dynamic isDynamic,b.placeholders_json placeholdersJson,
82
+ b.source_file bindingSourceFile,b.source_line bindingSourceLine,
83
+ b.helper_chain_json helperChainJson,req.service_path requireServicePath,
84
+ req.destination requireDestination
85
+ FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
86
+ LEFT JOIN service_bindings b ON b.id=c.service_binding_id
87
+ LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias
88
+ WHERE r.workspace_id=?`).all(
89
+ workspaceId,
90
+ ) as Array<Record<string, unknown>>;
69
91
  for (const call of calls) {
70
92
  const result = insertCallEdge(db, workspaceId, call, vars, generation);
71
93
  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
+ }