@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
package/src/cli.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  createWorkspaceConfig,
5
5
  loadWorkspaceConfig,
6
6
  saveWorkspaceConfig,
7
+ type WorkspaceConfig,
7
8
  } from './config/workspace-config.js';
8
9
  import { openDatabase, openReadOnlyDatabase, type Db } from './db/connection.js';
9
10
  import {
@@ -20,6 +21,7 @@ import { classifyRepository } from './discovery/classify-repository.js';
20
21
  import { indexWorkspace } from './indexer/workspace-indexer.js';
21
22
  import { linkWorkspace } from './linker/cross-repo-linker.js';
22
23
  import { doctorDiagnostics, linkUpgradeWarnings } from './cli/doctor.js';
24
+ import { factLifecycleDiagnostic } from './db/fact-lifecycle.js';
23
25
  import { trace } from './trace/trace-engine.js';
24
26
  import { compactTrace } from './trace/compact-trace.js';
25
27
  import {
@@ -33,6 +35,9 @@ import { renderDoctorDiagnostics } from './output/doctor-output.js';
33
35
  import { renderMermaid } from './output/mermaid-output.js';
34
36
  import { createStdoutWriter } from './output/stdout-policy.js';
35
37
  import { renderCompactJson } from './output/compact-json-output.js';
38
+ import {
39
+ projectRepositoryInspection,
40
+ } from './output/repository-inspection.js';
36
41
  import { VERSION } from './version.js';
37
42
  import type {
38
43
  DynamicMode,
@@ -122,6 +127,7 @@ async function withWorkspace<T>(
122
127
  db: ReturnType<typeof openDatabase>,
123
128
  workspaceId: number,
124
129
  rootPath: string,
130
+ config: WorkspaceConfig,
125
131
  ) => Promise<T> | T,
126
132
  ): Promise<T> {
127
133
  const config = await loadWorkspaceConfig(workspace);
@@ -130,7 +136,7 @@ async function withWorkspace<T>(
130
136
  const row = getWorkspace(db, config.rootPath);
131
137
  const workspaceId =
132
138
  row?.id ?? upsertWorkspace(db, config.rootPath, config.dbPath);
133
- return await fn(db, workspaceId, config.rootPath);
139
+ return await fn(db, workspaceId, config.rootPath, config);
134
140
  } finally {
135
141
  db.close();
136
142
  }
@@ -275,10 +281,11 @@ function registerIndexCommand(program: Command): void {
275
281
  .option('--force')
276
282
  .action(
277
283
  (opts: { workspace?: string; repo?: string; force?: boolean }) =>
278
- void withWorkspace(opts.workspace, async (db, workspaceId) => {
284
+ void withWorkspace(opts.workspace, async (db, workspaceId, _root, config) => {
279
285
  const r = await indexWorkspace(db, workspaceId, {
280
286
  repo: opts.repo,
281
287
  force: Boolean(opts.force),
288
+ eventEnvironmentKeys: config.eventEnvironmentKeys,
282
289
  });
283
290
  const outcome = indexCommandOutcome(r);
284
291
  writeStdout(outcome.stdout);
@@ -298,7 +305,7 @@ function registerLinkCommand(program: Command): void {
298
305
  const r = linkWorkspace(db, workspaceId);
299
306
  const upgradeWarnings = linkUpgradeWarnings(db, workspaceId);
300
307
  writeStdout(
301
- `${upgradeWarnings.length ? `Warnings: ${upgradeWarnings.map((item) => String(item.code)).join(', ')}. Run service-flow doctor --strict for remediation.\n` : ''}Linked ${r.edgeCount} edges: ${r.remoteResolvedCount} remote operation calls resolved, ${r.localResolvedCount} local operation calls resolved, ${r.unresolvedCount} unresolved operation calls, ${r.ambiguousCount} ambiguous operation calls, ${r.dynamicCount} dynamic operation calls, ${r.terminalCount} terminal call edges, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved, ${r.subscriptionHandlerResolvedCount} subscription handlers resolved, ${r.subscriptionHandlerAmbiguousCount} subscription handlers ambiguous, ${r.subscriptionHandlerUnresolvedCount} subscription handlers unresolved, ${r.subscriptionHandlerMissingAssociationCount} subscription handler associations missing\n`,
308
+ `${upgradeWarnings.length ? `Warnings: ${upgradeWarnings.map((item) => String(item.code)).join(', ')}. Run service-flow doctor --strict for remediation.\n` : ''}Linked ${r.edgeCount} edges: ${r.remoteResolvedCount} remote operation calls resolved, ${r.localResolvedCount} local operation calls resolved, ${r.unresolvedCount} unresolved operation calls, ${r.ambiguousCount} ambiguous operation calls, ${r.dynamicCount} dynamic operation calls, ${r.terminalCount} terminal call edges, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved, ${r.subscriptionHandlerResolvedCount} subscription handlers resolved, ${r.subscriptionHandlerAmbiguousCount} subscription handlers ambiguous, ${r.subscriptionHandlerUnresolvedCount} subscription handlers unresolved, ${r.subscriptionHandlerMissingAssociationCount} subscription handler associations missing, ${r.eventShapeCandidateCount} event shape candidates, ${r.eventShapeCandidateOmittedCount} event shape candidates omitted by the link cap\n`,
302
309
  );
303
310
  }).catch(fail),
304
311
  );
@@ -329,22 +336,23 @@ function registerTraceCommand(program: Command): void {
329
336
  function listRepositoriesCommand(
330
337
  opts: { workspace?: string },
331
338
  ): Promise<void> {
332
- return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) =>
333
- writeStdout(
334
- renderJson(
335
- listRepositories(db, workspaceId).map((repo) => ({
336
- name: repo.name,
337
- kind: repo.kind,
338
- packageName: repo.package_name,
339
- })),
340
- ),
339
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
340
+ if (writeLifecycleBlock(db, workspaceId)) return;
341
+ writeStdout(renderJson(
342
+ listRepositories(db, workspaceId).map((repo) => ({
343
+ name: repo.name,
344
+ kind: repo.kind,
345
+ packageName: repo.package_name,
346
+ })),
341
347
  ));
348
+ });
342
349
  }
343
350
 
344
351
  function listServicesCommand(
345
352
  opts: { workspace?: string; repo?: string },
346
353
  ): Promise<void> {
347
354
  return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
355
+ if (writeLifecycleBlock(db, workspaceId)) return;
348
356
  const selection = opts.repo
349
357
  ? selectRepository(db, opts.repo, workspaceId) : {};
350
358
  if (selection.diagnostic) {
@@ -363,6 +371,7 @@ function listOperationsCommand(
363
371
  opts: { workspace?: string; repo?: string; service?: string },
364
372
  ): Promise<void> {
365
373
  return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
374
+ if (writeLifecycleBlock(db, workspaceId)) return;
366
375
  const selection = opts.repo
367
376
  ? selectRepository(db, opts.repo, workspaceId) : {};
368
377
  if (selection.diagnostic) {
@@ -383,6 +392,7 @@ function listCallsCommand(
383
392
  opts: { workspace?: string; repo?: string; operation?: string },
384
393
  ): Promise<void> {
385
394
  return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
395
+ if (writeLifecycleBlock(db, workspaceId)) return;
386
396
  const selection = opts.repo
387
397
  ? selectRepository(db, opts.repo, workspaceId) : {};
388
398
  if (selection.diagnostic) {
@@ -460,9 +470,12 @@ function inspectRepositoryCommand(
460
470
  opts: { workspace?: string },
461
471
  ): Promise<void> {
462
472
  return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
473
+ if (writeLifecycleBlock(db, workspaceId)) return;
463
474
  const selection = selectRepository(db, name, workspaceId);
464
475
  writeStdout(renderJson(
465
- selection.repo ?? selection.diagnostic ?? { error: 'repo not found' },
476
+ selection.repo
477
+ ? projectRepositoryInspection(selection.repo)
478
+ : selection.diagnostic ?? { error: 'repo not found' },
466
479
  ));
467
480
  });
468
481
  }
@@ -472,6 +485,7 @@ function inspectOperationCommand(
472
485
  opts: { workspace?: string },
473
486
  ): Promise<void> {
474
487
  return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
488
+ if (writeLifecycleBlock(db, workspaceId)) return;
475
489
  const rows = db.prepare(
476
490
  'SELECT o.* FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (o.operation_name=? OR o.operation_path=?)',
477
491
  ).all(workspaceId, selector, selector);
@@ -479,6 +493,14 @@ function inspectOperationCommand(
479
493
  });
480
494
  }
481
495
 
496
+ function writeLifecycleBlock(db: Db, workspaceId: number): boolean {
497
+ const diagnostic = factLifecycleDiagnostic(db, workspaceId);
498
+ if (!diagnostic) return false;
499
+ writeStdout(renderJson([diagnostic]));
500
+ process.exitCode = 1;
501
+ return true;
502
+ }
503
+
482
504
  function registerInspectCommands(program: Command): void {
483
505
  const inspect = program.command('inspect');
484
506
  inspect
@@ -7,10 +7,24 @@ import {
7
7
  DEFAULT_DB_FILE,
8
8
  DEFAULT_IGNORES,
9
9
  } from './defaults.js';
10
+ import {
11
+ DEFAULT_EVENT_ENVIRONMENT_KEYS,
12
+ EVENT_ENVIRONMENT_KEY_CAP,
13
+ normalizeEventEnvironmentKeys,
14
+ validEventEnvironmentKey,
15
+ } from '../parsers/environment-declarations.js';
16
+ const environmentKeys = z.array(
17
+ z.string().refine(validEventEnvironmentKey),
18
+ ).min(1).max(EVENT_ENVIRONMENT_KEY_CAP).transform(
19
+ normalizeEventEnvironmentKeys,
20
+ );
10
21
  const schema = z.object({
11
22
  rootPath: z.string(),
12
23
  dbPath: z.string(),
13
24
  ignore: z.array(z.string()),
25
+ eventEnvironmentKeys: environmentKeys.default(
26
+ [...DEFAULT_EVENT_ENVIRONMENT_KEYS],
27
+ ),
14
28
  createdAt: z.string(),
15
29
  updatedAt: z.string(),
16
30
  });
@@ -55,6 +69,7 @@ export function createWorkspaceConfig(
55
69
  rootPath: root,
56
70
  dbPath: path.resolve(dbPath ?? defaultDbPath(root)),
57
71
  ignore,
72
+ eventEnvironmentKeys: [...DEFAULT_EVENT_ENVIRONMENT_KEYS],
58
73
  createdAt: now,
59
74
  updatedAt: now,
60
75
  };
@@ -400,11 +400,12 @@ export function insertCalls(
400
400
  function outboundCallInsertStatement(db: Db): Statement {
401
401
  return db.prepare(`INSERT INTO outbound_calls(
402
402
  repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,
403
- event_name_expr,payload_summary,source_file,source_line,call_site_start_offset,
403
+ event_name_expr,event_skeleton_signature,event_skeleton_json,
404
+ payload_summary,source_file,source_line,call_site_start_offset,
404
405
  call_site_end_offset,confidence,unresolved_reason,local_service_name,
405
406
  local_service_lookup,alias_chain_json,evidence_json,external_target_kind,
406
407
  external_target_id,external_target_label,external_target_dynamic,service_binding_id
407
- ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
408
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
408
409
  }
409
410
 
410
411
  function insertOutboundCall(
@@ -424,7 +425,9 @@ function insertOutboundCall(
424
425
  stmt.run(
425
426
  repoId, sourceSymbolId,
426
427
  call.callType, call.method, call.operationPathExpr, call.queryEntity,
427
- call.eventNameExpr, call.payloadSummary, call.sourceFile, call.sourceLine,
428
+ call.eventNameExpr, call.eventSkeleton?.signature ?? null,
429
+ call.eventSkeleton ? JSON.stringify(call.eventSkeleton) : null,
430
+ call.payloadSummary, call.sourceFile, call.sourceLine,
428
431
  call.callSiteStartOffset, call.callSiteEndOffset, call.confidence,
429
432
  call.unresolvedReason,
430
433
  call.localServiceName, call.localServiceLookup,
@@ -10,9 +10,9 @@ import {
10
10
  import {
11
11
  invalidBindingFactCategories,
12
12
  } from './binding-fact-semantics.js';
13
- import {
14
- invalidSymbolFactCategories,
15
- } from './symbol-call-semantics.js';
13
+ import { invalidSymbolFactCategories } from './symbol-call-semantics.js';
14
+ import { invalidEventFactCategories } from './event-fact-semantics.js';
15
+ import { eventSiteCategories } from './event-site-semantics.js';
16
16
 
17
17
  export type PackageFactPhase = 'pre_package' | 'terminal';
18
18
 
@@ -258,31 +258,6 @@ function duplicateSymbolCallCategories(
258
258
  return category('symbol_call_site_duplicate', duplicate);
259
259
  }
260
260
 
261
- function eventNameCategories(
262
- db: Db,
263
- workspaceId?: number,
264
- ): FactSemanticCategoryCount[] {
265
- const invalid = count(db, `SELECT COUNT(*) count
266
- FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
267
- WHERE ${currentRepositoryPredicate()}
268
- AND fact.call_type IN ('async_emit','async_subscribe')
269
- AND (typeof(fact.event_name_expr)<>'text'
270
- OR length(fact.event_name_expr)=0)`, workspaceId);
271
- const duplicate = count(db, `SELECT COUNT(*) count FROM (
272
- SELECT fact.repo_id,fact.source_file,fact.call_site_start_offset,
273
- fact.call_site_end_offset,COUNT(*) duplicate_count
274
- FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
275
- WHERE ${currentRepositoryPredicate()}
276
- AND fact.call_type='async_subscribe'
277
- GROUP BY fact.repo_id,fact.source_file,fact.call_site_start_offset,
278
- fact.call_site_end_offset HAVING COUNT(*)<>1
279
- )`, workspaceId);
280
- return [
281
- ...category('event_name_invalid', invalid),
282
- ...category('async_subscription_site_duplicate', duplicate),
283
- ];
284
- }
285
-
286
261
  function outboundOwnerCount(db: Db, workspaceId?: number): number {
287
262
  return count(db, `WITH eligible AS (
288
263
  SELECT fact.id fact_id,s.id symbol_id,
@@ -685,7 +660,8 @@ export function invalidFactSemanticCategories(
685
660
  ...operationCategories(db, workspaceId),
686
661
  ...callSpanCategories(db, workspaceId),
687
662
  ...duplicateSymbolCallCategories(db, workspaceId),
688
- ...eventNameCategories(db, workspaceId),
663
+ ...eventSiteCategories(db, workspaceId),
664
+ ...invalidEventFactCategories(db, workspaceId, phase),
689
665
  ...ownerCategories(db, workspaceId),
690
666
  ...bindingCategories(db, workspaceId),
691
667
  ...invalidBindingFactCategories(db, workspaceId),