@saptools/service-flow 0.1.67 → 0.1.69

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 (101) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +27 -9
  3. package/TECHNICAL-NOTE.md +36 -0
  4. package/dist/chunk-3N3B5KHV.js +19596 -0
  5. package/dist/chunk-3N3B5KHV.js.map +1 -0
  6. package/dist/cli.js +2645 -521
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +67 -2
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/001-index-summary.ts +22 -0
  12. package/src/cli/003-doctor-package-resolution.ts +68 -0
  13. package/src/cli/doctor.ts +25 -14
  14. package/src/cli.ts +151 -87
  15. package/src/db/000-call-fact-repository.ts +499 -340
  16. package/src/db/001-fact-lifecycle.ts +60 -30
  17. package/src/db/002-fact-json-inventory.ts +169 -0
  18. package/src/db/003-current-fact-semantics.ts +699 -0
  19. package/src/db/004-package-target-invalidation.ts +183 -0
  20. package/src/db/005-schema-structure.ts +201 -0
  21. package/src/db/006-relative-symbol-resolution.ts +464 -0
  22. package/src/db/007-package-fact-semantics.ts +573 -0
  23. package/src/db/008-relative-fact-semantics.ts +210 -0
  24. package/src/db/009-binding-fact-semantics.ts +352 -0
  25. package/src/db/010-package-symbol-surface-semantics.ts +320 -0
  26. package/src/db/011-symbol-call-semantics.ts +144 -0
  27. package/src/db/012-binding-reference-proof.ts +268 -0
  28. package/src/db/013-index-publication-failure.ts +91 -0
  29. package/src/db/014-binding-helper-provenance.ts +17 -0
  30. package/src/db/migrations.ts +16 -3
  31. package/src/db/repositories.ts +130 -6
  32. package/src/db/schema.ts +4 -2
  33. package/src/index.ts +12 -0
  34. package/src/indexer/cds-extension-resolver.ts +27 -3
  35. package/src/indexer/repository-indexer.ts +135 -13
  36. package/src/indexer/workspace-indexer.ts +237 -34
  37. package/src/linker/003-package-import-symbol-resolver.ts +363 -131
  38. package/src/linker/004-event-subscription-handler-linker.ts +34 -11
  39. package/src/linker/005-odata-path-structure.ts +371 -0
  40. package/src/linker/006-event-template-link.ts +72 -0
  41. package/src/linker/007-call-edge-insertion.ts +568 -0
  42. package/src/linker/cross-repo-linker.ts +4 -166
  43. package/src/linker/odata-path-normalizer.ts +273 -180
  44. package/src/linker/service-resolver.ts +197 -77
  45. package/src/parsers/000-direct-query-execution.ts +11 -0
  46. package/src/parsers/002-symbol-import-bindings.ts +516 -0
  47. package/src/parsers/003-package-public-surface.ts +661 -0
  48. package/src/parsers/004-fact-identity.ts +108 -0
  49. package/src/parsers/005-event-subscription-facts.ts +281 -0
  50. package/src/parsers/006-binding-identity.ts +348 -0
  51. package/src/parsers/007-source-fact-reconciliation.ts +105 -0
  52. package/src/parsers/008-package-surface-publication.ts +82 -0
  53. package/src/parsers/009-symbol-call-facts.ts +528 -0
  54. package/src/parsers/010-package-public-surface-analysis.ts +352 -0
  55. package/src/parsers/011-binding-lexical-scope.ts +583 -0
  56. package/src/parsers/012-package-fact-contract.ts +306 -0
  57. package/src/parsers/013-executable-body-eligibility.ts +35 -0
  58. package/src/parsers/014-service-binding-helper-flow.ts +306 -0
  59. package/src/parsers/015-service-binding-collector.ts +693 -0
  60. package/src/parsers/016-local-symbol-reference.ts +261 -0
  61. package/src/parsers/017-symbol-derived-contexts.ts +268 -0
  62. package/src/parsers/018-package-commonjs-syntax.ts +142 -0
  63. package/src/parsers/019-binding-assignment-targets.ts +76 -0
  64. package/src/parsers/020-stable-local-value.ts +217 -0
  65. package/src/parsers/021-binding-visibility.ts +168 -0
  66. package/src/parsers/022-outbound-expression-analysis.ts +700 -0
  67. package/src/parsers/023-outbound-call-classifier.ts +692 -0
  68. package/src/parsers/operation-path-analysis.ts +6 -1
  69. package/src/parsers/outbound-call-parser.ts +162 -512
  70. package/src/parsers/package-json-parser.ts +45 -3
  71. package/src/parsers/service-binding-parser-helpers.ts +86 -15
  72. package/src/parsers/service-binding-parser.ts +147 -597
  73. package/src/parsers/symbol-parser.ts +513 -352
  74. package/src/trace/002-trace-diagnostics.ts +36 -1
  75. package/src/trace/007-implementation-start-diagnostic.ts +1 -0
  76. package/src/trace/011-event-subscriber-traversal.ts +100 -8
  77. package/src/trace/013-trace-root-scopes.ts +2 -3
  78. package/src/trace/014-compact-contract.ts +6 -0
  79. package/src/trace/015-trace-edge-recorder.ts +61 -4
  80. package/src/trace/016-compact-projector.ts +15 -17
  81. package/src/trace/019-trace-edge-semantics.ts +6 -10
  82. package/src/trace/020-compact-field-projection.ts +122 -38
  83. package/src/trace/021-compact-decision-normalization.ts +171 -0
  84. package/src/trace/022-trace-fact-preflight.ts +21 -0
  85. package/src/trace/023-nested-event-scopes.ts +23 -0
  86. package/src/trace/024-compact-observation-decision.ts +81 -0
  87. package/src/trace/025-trace-implementation-scope.ts +123 -0
  88. package/src/trace/026-trace-start-scope.ts +336 -0
  89. package/src/trace/027-trace-scope-execution.ts +566 -0
  90. package/src/trace/028-trace-operation-execution.ts +336 -0
  91. package/src/trace/029-trace-start-implementation.ts +172 -0
  92. package/src/trace/030-event-runtime-resolution.ts +151 -0
  93. package/src/trace/031-local-call-expansion.ts +37 -0
  94. package/src/trace/implementation-hints.ts +9 -6
  95. package/src/trace/selectors.ts +1 -0
  96. package/src/trace/trace-engine.ts +122 -624
  97. package/src/types.ts +57 -0
  98. package/src/utils/001-placeholders.ts +188 -10
  99. package/src/version.ts +1 -1
  100. package/dist/chunk-ZQABU7MR.js +0 -12151
  101. package/dist/chunk-ZQABU7MR.js.map +0 -1
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import ts from 'typescript';
2
2
 
3
3
  type CallType = 'remote_action' | 'remote_query' | 'remote_entity_read' | 'remote_entity_mutation' | 'remote_entity_delete' | 'remote_entity_media' | 'remote_entity_candidate' | 'local_db_query' | 'external_http' | 'async_emit' | 'async_subscribe' | 'local_service_call' | 'unknown';
4
+ type EdgeType = 'REPO_HAS_SERVICE' | 'SERVICE_HAS_OPERATION' | 'OPERATION_IMPLEMENTED_BY_HANDLER' | 'HANDLER_REGISTERED_BY_SERVER' | 'HANDLER_CALLS_LOCAL_FUNCTION' | 'HANDLER_USES_SERVICE_ALIAS' | 'HANDLER_CALLS_REMOTE_OPERATION' | 'REMOTE_CALL_RESOLVES_TO_OPERATION' | 'LOCAL_CALL_RESOLVES_TO_OPERATION' | 'HANDLER_RUNS_DB_QUERY' | 'HANDLER_RUNS_REMOTE_QUERY' | 'HANDLER_ACCESSES_REMOTE_ENTITY' | 'HANDLER_CALLS_EXTERNAL_HTTP' | 'HANDLER_CALLS_TRANSPORT_METHOD' | 'HANDLER_EMITS_EVENT' | 'EVENT_CONSUMED_BY_HANDLER' | 'EVENT_SUBSCRIPTION_HANDLED_BY' | 'REPO_IMPORTS_HELPER_PACKAGE' | 'HELPER_PACKAGE_PROVIDES_HANDLER' | 'DYNAMIC_EDGE_CANDIDATE' | 'UNRESOLVED_EDGE';
4
5
  interface DiscoveredRepository {
5
6
  name: string;
6
7
  absolutePath: string;
@@ -109,8 +110,34 @@ interface ServiceBindingFact {
109
110
  placeholders: string[];
110
111
  sourceFile: string;
111
112
  sourceLine: number;
113
+ bindingSiteStartOffset?: number;
114
+ bindingSiteEndOffset?: number;
115
+ sourceSymbolQualifiedName?: string;
116
+ ownerResolution?: ServiceBindingOwnerResolution;
112
117
  helperChain?: Array<Record<string, unknown>>;
113
118
  }
119
+ type ServiceBindingOwnerResolution = 'owned_exact' | 'ownerless_file_scope' | 'legacy_unknown';
120
+ type ServiceBindingReferenceStatus = 'resolved_exact' | 'ambiguous' | 'unresolved' | 'not_applicable';
121
+ type ServiceBindingReferenceReason = 'binding_not_found' | 'binding_declared_after_call' | 'binding_scope_ambiguous' | 'scope_chain_limit_exceeded' | 'unsupported_reaching_assignment' | 'unsupported_var_binding' | 'binding_flow_unsupported';
122
+ interface LexicalScopeFact {
123
+ kind: 'source_file' | 'module_block' | 'function' | 'class' | 'loop' | 'case_block' | 'block' | 'catch';
124
+ startOffset: number;
125
+ endOffset: number;
126
+ }
127
+ interface ServiceBindingReference {
128
+ status: ServiceBindingReferenceStatus;
129
+ variableName?: string;
130
+ bindingSourceFile?: string;
131
+ bindingSiteStartOffset?: number;
132
+ bindingSiteEndOffset?: number;
133
+ resolutionStrategy?: 'lexical_declaration' | 'lexical_alias_declaration' | 'deterministic_reaching_assignment' | 'single_hop_helper_return';
134
+ lexicalScopeChain?: LexicalScopeFact[];
135
+ bindingScopeIndex?: number;
136
+ scopeChainTotal: number;
137
+ scopeChainShown: number;
138
+ scopeChainOmitted: number;
139
+ reason?: ServiceBindingReferenceReason;
140
+ }
114
141
  interface OutboundCallFact {
115
142
  callType: CallType;
116
143
  sourceSymbolQualifiedName?: string;
@@ -127,6 +154,7 @@ interface OutboundCallFact {
127
154
  sourceLine: number;
128
155
  callSiteStartOffset?: number;
129
156
  callSiteEndOffset?: number;
157
+ serviceBindingReference?: ServiceBindingReference;
130
158
  confidence: number;
131
159
  unresolvedReason?: string;
132
160
  evidence?: Record<string, unknown>;
@@ -137,6 +165,33 @@ interface OutboundCallFact {
137
165
  dynamic: boolean;
138
166
  };
139
167
  }
168
+ interface ExecutableSymbolFact {
169
+ kind: string;
170
+ localName: string;
171
+ exportedName?: string;
172
+ qualifiedName: string;
173
+ sourceFile: string;
174
+ startLine: number;
175
+ endLine: number;
176
+ startOffset: number;
177
+ endOffset: number;
178
+ exported: boolean;
179
+ importExportEvidence?: Record<string, unknown>;
180
+ }
181
+ type SymbolCallRole = 'ordinary_call' | 'event_subscribe_handler' | 'legacy_unknown';
182
+ interface SymbolCallFact {
183
+ callerQualifiedName: string;
184
+ calleeExpression: string;
185
+ calleeLocalName?: string;
186
+ receiverLocalName?: string;
187
+ importSource?: string;
188
+ sourceFile: string;
189
+ sourceLine: number;
190
+ callSiteStartOffset?: number;
191
+ callSiteEndOffset?: number;
192
+ callRole: Exclude<SymbolCallRole, 'legacy_unknown'>;
193
+ evidence: Record<string, unknown>;
194
+ }
140
195
  interface GeneratedConstantFact {
141
196
  name: string;
142
197
  value: string;
@@ -215,7 +270,12 @@ declare function parseHandlerRegistrations(repoPath: string, filePath: string, c
215
270
 
216
271
  declare function parseServiceBindings(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<ServiceBindingFact[]>;
217
272
 
218
- declare function parseOutboundCalls(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<OutboundCallFact[]>;
273
+ interface ClassifiedOutboundCall {
274
+ fact: OutboundCallFact;
275
+ node: ts.CallExpression;
276
+ }
277
+
278
+ declare function parseOutboundCalls(repoPath: string, filePath: string, context?: RepositorySourceContext, classified?: readonly ClassifiedOutboundCall[], preparedBindings?: readonly ServiceBindingFact[]): Promise<OutboundCallFact[]>;
219
279
 
220
280
  declare function parseGeneratedConstants(repoPath: string, filePath: string): Promise<GeneratedConstantFact[]>;
221
281
 
@@ -334,6 +394,7 @@ interface CompactDecisionV1 {
334
394
  implementationStrategy?: string;
335
395
  implementationGuided?: boolean;
336
396
  implementationContextual?: boolean;
397
+ tiedCandidateRepos?: CompactReferenceGroupV1;
337
398
  eventMatchStrategy?: string;
338
399
  dispatchCertainty?: string;
339
400
  eventSubscriptionCount?: number;
@@ -354,6 +415,10 @@ interface CompactEdgeDetailsV1 {
354
415
  }
355
416
  interface CompactDiagnosticDetailsV1 {
356
417
  reasonCode?: string;
418
+ tiedCandidateRepos?: CompactReferenceGroupV1;
419
+ selectorKind?: string;
420
+ selectorSuggestions?: CompactReferenceGroupV1;
421
+ invalidFactCategories?: CompactReferenceGroupV1;
357
422
  missingVariableNames?: string[];
358
423
  missingVariableCount?: number;
359
424
  shownMissingVariableCount?: number;
@@ -465,4 +530,4 @@ declare function parseImplementationHint(value: string): ImplementationHint;
465
530
  declare function redactText(text: string): string;
466
531
  declare function redactValue(value: unknown): unknown;
467
532
 
468
- export { type CompactDecisionV1, type CompactDiagnosticDetailsV1, type CompactDiagnosticRowV1, type CompactEdgeDetailsV1, type CompactEdgeRowV1, type CompactGraphV1, type CompactHintV1, type CompactNodeRowV1, type CompactQueryV1, type CompactReferenceGroupV1, type CompactReferencesV1, type CompactSourceContext, type CompactStartV1, type CompactStatus, type CompactStatusCountsV1, type CompactTraceExecution, type DynamicMode, type ImplementationHint, type RuntimeSubstitution, type TraceOptions, applyVariables, compactTrace, discoverRepositories, extractPlaceholders, linkWorkspace, parseCdsFile, parseDecorators, parseGeneratedConstants, parseHandlerRegistrations, parseImplementationHint, parseOutboundCalls, parsePackageJson, parseServiceBindings, redactText, redactValue, substituteVariables, trace, traceAndCompact };
533
+ export { type CallType, type CompactDecisionV1, type CompactDiagnosticDetailsV1, type CompactDiagnosticRowV1, type CompactEdgeDetailsV1, type CompactEdgeRowV1, type CompactGraphV1, type CompactHintV1, type CompactNodeRowV1, type CompactQueryV1, type CompactReferenceGroupV1, type CompactReferencesV1, type CompactSourceContext, type CompactStartV1, type CompactStatus, type CompactStatusCountsV1, type CompactTraceExecution, type Db, type DynamicMode, type EdgeType, type ExecutableSymbolFact, type ImplementationHint, type OutboundCallFact, type RuntimeSubstitution, type SymbolCallFact, type SymbolCallRole, type TraceEdge, type TraceOptions, type TraceResult, type TraceStart, applyVariables, compactTrace, discoverRepositories, extractPlaceholders, linkWorkspace, parseCdsFile, parseDecorators, parseGeneratedConstants, parseHandlerRegistrations, parseImplementationHint, parseOutboundCalls, parsePackageJson, parseServiceBindings, redactText, redactValue, substituteVariables, trace, traceAndCompact };
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  substituteVariables,
20
20
  trace,
21
21
  traceAndCompact
22
- } from "./chunk-ZQABU7MR.js";
22
+ } from "./chunk-3N3B5KHV.js";
23
23
 
24
24
  // src/parsers/generated-constants-parser.ts
25
25
  import fs from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saptools/service-flow",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
4
4
  "description": "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -0,0 +1,22 @@
1
+ import type {
2
+ IndexWorkspaceSummary,
3
+ } from '../indexer/workspace-indexer.js';
4
+
5
+ export interface IndexCommandOutcome {
6
+ stdout: string;
7
+ exitCode: 0 | 1;
8
+ }
9
+
10
+ export function indexCommandOutcome(
11
+ summary: IndexWorkspaceSummary,
12
+ ): IndexCommandOutcome {
13
+ const failed = summary.failedCount > 0
14
+ ? `, failed ${summary.failedCount} (${
15
+ summary.failedRepos.map(({ name, code }) => `${name}: ${code}`).join(', ')
16
+ })`
17
+ : '';
18
+ return {
19
+ stdout: `Indexed ${summary.indexedCount} repositories, skipped ${summary.skippedCount}${failed}, ${summary.fileCount} files, ${summary.diagnosticCount} diagnostics\n`,
20
+ exitCode: summary.failedCount > 0 ? 1 : 0,
21
+ };
22
+ }
@@ -0,0 +1,68 @@
1
+ import type { Db } from '../db/connection.js';
2
+
3
+ type Diagnostic = Record<string, unknown>;
4
+
5
+ function pendingPredicate(alias: string): string {
6
+ return `${alias}.status='unresolved'
7
+ AND ${alias}.callee_symbol_id IS NULL
8
+ AND ${alias}.unresolved_reason='package_resolution_pending'
9
+ AND json_extract(${alias}.evidence_json,'$.relation')='package_import'
10
+ AND json_extract(${alias}.evidence_json,
11
+ '$.importBinding.moduleKind')='package'
12
+ AND json_extract(${alias}.evidence_json,
13
+ '$.candidateStrategy')='package_import_pending'
14
+ AND json_extract(${alias}.evidence_json,'$.candidateCount')=0
15
+ AND json_extract(${alias}.evidence_json,'$.eligibleCandidateCount')=0
16
+ AND json_extract(${alias}.evidence_json,'$.selectedCandidateCount')=0
17
+ AND json_extract(${alias}.evidence_json,'$.candidateSetComplete')=0`;
18
+ }
19
+
20
+ function count(db: Db, sql: string): number {
21
+ return Number(db.prepare(sql).get()?.count ?? 0);
22
+ }
23
+
24
+ export function packagePendingDiagnostics(db: Db): Diagnostic[] {
25
+ const pending = count(db, `SELECT COUNT(*) count FROM symbol_calls sc
26
+ WHERE ${pendingPredicate('sc')}`);
27
+ if (pending === 0) return [];
28
+ const stale = count(db, `SELECT COUNT(*) count FROM repositories
29
+ WHERE graph_stale_reason IS NOT NULL`);
30
+ return [{
31
+ severity: 'warning',
32
+ code: 'package_import_resolution_pending',
33
+ message: 'Package-import facts await workspace linking; terminal package-resolution quality is deferred.',
34
+ packageResolutionState: 'pre_link_pending',
35
+ pendingPackageImportCount: pending,
36
+ graphState: 'stale',
37
+ staleRepositoryCount: stale,
38
+ requiredAction: 'relink',
39
+ remediation: 'service-flow link --workspace /workspace --force',
40
+ }];
41
+ }
42
+
43
+ export function symbolCallQuality(db: Db): Diagnostic {
44
+ const terminal = `NOT (${pendingPredicate('sc')})`;
45
+ const row = db.prepare(`SELECT COUNT(*) total,
46
+ SUM(CASE WHEN sc.status='resolved' THEN 1 ELSE 0 END) resolved,
47
+ SUM(CASE WHEN sc.status='unresolved' THEN 1 ELSE 0 END) unresolved
48
+ FROM symbol_calls sc WHERE ${terminal}`).get();
49
+ const top = db.prepare(`SELECT sc.callee_expression calleeExpression,
50
+ COUNT(*) count FROM symbol_calls sc
51
+ WHERE sc.status='unresolved' AND ${terminal}
52
+ GROUP BY sc.callee_expression
53
+ ORDER BY count DESC,sc.callee_expression COLLATE BINARY LIMIT 5`).all();
54
+ const total = Number(row?.total ?? 0);
55
+ const unresolved = Number(row?.unresolved ?? 0);
56
+ const ratio = total === 0 ? 0 : Number((unresolved / total).toFixed(4));
57
+ return {
58
+ severity: ratio > 0.05 ? 'warning' : 'info',
59
+ code: 'strict_symbol_call_quality',
60
+ message: 'Terminal symbol-call quality aggregate',
61
+ total,
62
+ resolved: Number(row?.resolved ?? 0),
63
+ unresolved,
64
+ unresolvedRatio: ratio,
65
+ unresolvedRatioThreshold: 0.05,
66
+ topUnresolvedCallees: top,
67
+ };
68
+ }
package/src/cli/doctor.ts CHANGED
@@ -7,6 +7,10 @@ import {
7
7
  analyzerVersionDiagnostics,
8
8
  schemaDriftDiagnostics,
9
9
  } from './002-doctor-lifecycle.js';
10
+ import {
11
+ packagePendingDiagnostics,
12
+ symbolCallQuality,
13
+ } from './003-doctor-package-resolution.js';
10
14
  export { linkUpgradeWarnings } from './002-doctor-lifecycle.js';
11
15
 
12
16
  type Diagnostic = Record<string, unknown>;
@@ -17,13 +21,23 @@ interface DoctorOptions {
17
21
 
18
22
  export function doctorDiagnostics(db: Db, strict: boolean, options: DoctorOptions = {}): Diagnostic[] {
19
23
  const lifecycle = factLifecycleDiagnostic(db, options.workspaceId);
20
- if (lifecycle?.code === 'schema_upgrade_required'
21
- || lifecycle?.code === 'unsupported_future_schema')
22
- return boundDoctorDiagnostics([lifecycle]);
24
+ if (lifecycle) return boundDoctorDiagnostics([lifecycle]);
25
+ const globalLifecycle = options.workspaceId === undefined
26
+ ? undefined : factLifecycleDiagnostic(db);
27
+ if (globalLifecycle) return boundDoctorDiagnostics([
28
+ ...schemaDriftDiagnostics(db, strict, options.workspaceId),
29
+ ...analyzerVersionDiagnostics(db, strict, options.workspaceId),
30
+ {
31
+ severity: 'warning',
32
+ code: 'workspace_detail_checks_deferred',
33
+ message: 'Selected-workspace lifecycle is valid, but JSON-dependent global detail checks were deferred because another workspace requires reindexing.',
34
+ remediation: 'Run doctor for the affected workspace after force index and link.',
35
+ },
36
+ ]);
23
37
  const diagnostics = db.prepare('SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id').all() as Diagnostic[];
24
38
  return boundDoctorDiagnostics([
25
- ...(lifecycle ? [lifecycle] : []),
26
39
  ...diagnostics,
40
+ ...packagePendingDiagnostics(db),
27
41
  ...healthDiagnostics(db, strict),
28
42
  ...remoteTargetWithoutImplementationDiagnostics(db, strict, Boolean(options.detail)),
29
43
  ...localServiceDiagnostics(db, strict),
@@ -170,15 +184,6 @@ function jsonEvidenceQuality(db: Db): Diagnostic[] {
170
184
  ];
171
185
  }
172
186
 
173
- function symbolCallQuality(db: Db): Diagnostic {
174
- const row = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN status='resolved' THEN 1 ELSE 0 END) resolved, SUM(CASE WHEN status='unresolved' THEN 1 ELSE 0 END) unresolved FROM symbol_calls").get() as Record<string, unknown>;
175
- const top = db.prepare("SELECT callee_expression calleeExpression,COUNT(*) count FROM symbol_calls WHERE status='unresolved' GROUP BY callee_expression ORDER BY count DESC,callee_expression LIMIT 5").all() as Diagnostic[];
176
- const total = Number(row.total ?? 0);
177
- const unresolved = Number(row.unresolved ?? 0);
178
- const ratio = total === 0 ? 0 : Number((unresolved / total).toFixed(4));
179
- return { severity: ratio > 0.05 ? 'warning' : 'info', code: 'strict_symbol_call_quality', message: 'Symbol-call quality aggregate', total, resolved: Number(row.resolved ?? 0), unresolved, unresolvedRatio: ratio, unresolvedRatioThreshold: 0.05, topUnresolvedCallees: top };
180
- }
181
-
182
187
  function dbQueryQuality(db: Db): Diagnostic {
183
188
  const row = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN query_entity IS NOT NULL THEN 1 ELSE 0 END) known, SUM(CASE WHEN query_entity IS NULL THEN 1 ELSE 0 END) unknown FROM outbound_calls WHERE call_type='local_db_query'").get() as Record<string, unknown>;
184
189
  const total = Number(row.total ?? 0);
@@ -418,8 +423,13 @@ function serviceBindingQuality(db: Db, detail: boolean): Diagnostic {
418
423
  function bindingCategory(row: Diagnostic): string {
419
424
  const evidence = parseObject(row.evidenceJson);
420
425
  const resolution = parseObject(evidence.serviceBindingResolution);
426
+ const reference = parseObject(evidence.serviceBindingReference);
421
427
  if (resolution.status === 'rejected_future_binding') return 'direct_binding_missing';
422
- if (resolution.status === 'ambiguous') return 'ambiguous_binding_candidates';
428
+ if (reference.reason === 'binding_declared_after_call')
429
+ return 'direct_binding_missing';
430
+ if (resolution.status === 'ambiguous' || reference.status === 'ambiguous'
431
+ || reference.reason === 'unsupported_reaching_assignment')
432
+ return 'ambiguous_binding_candidates';
423
433
  const receiver = evidence.receiver;
424
434
  const symbolEvidence = parseObject(row.symbolEvidenceJson);
425
435
  if (symbolHasReceiverParameter(symbolEvidence, receiver))
@@ -448,6 +458,7 @@ function bindingExample(row: Diagnostic): Diagnostic {
448
458
  receiver: evidence.receiver,
449
459
  unresolvedReason: row.unresolvedReason,
450
460
  bindingResolution: evidence.serviceBindingResolution,
461
+ bindingReference: evidence.serviceBindingReference,
451
462
  };
452
463
  }
453
464
 
package/src/cli.ts CHANGED
@@ -41,6 +41,7 @@ import type {
41
41
  TraceStart,
42
42
  } from './types.js';
43
43
  import { cleanWorkspaceState } from './cli/000-clean.js';
44
+ import { indexCommandOutcome } from './cli/001-index-summary.js';
44
45
 
45
46
  const stdout = createStdoutWriter(process.stdout, fail);
46
47
  const TRACE_FORMATS = ['table', 'json', 'mermaid', 'compact-json'] as const;
@@ -245,14 +246,16 @@ function runGraphCommand(opts: GraphCommandOptions): Promise<void> {
245
246
  });
246
247
  }
247
248
 
248
- export function createProgram(): Command {
249
- const program = new Command();
250
- program
249
+ function configuredProgram(): Command {
250
+ return new Command()
251
251
  .name('service-flow')
252
252
  .description(
253
253
  'Trace SAP CAP service-to-service flows across multi-repository workspaces',
254
254
  )
255
255
  .version(VERSION);
256
+ }
257
+
258
+ function registerInitCommand(program: Command): void {
256
259
  program
257
260
  .command('init')
258
261
  .argument('<workspace>')
@@ -262,6 +265,9 @@ export function createProgram(): Command {
262
265
  (workspace: string, opts: { db?: string; ignore?: string[] }) =>
263
266
  void init(workspace, opts).catch(fail),
264
267
  );
268
+ }
269
+
270
+ function registerIndexCommand(program: Command): void {
265
271
  program
266
272
  .command('index')
267
273
  .option('--workspace <path>')
@@ -274,11 +280,14 @@ export function createProgram(): Command {
274
280
  repo: opts.repo,
275
281
  force: Boolean(opts.force),
276
282
  });
277
- writeStdout(
278
- `Indexed ${r.indexedCount} repositories, skipped ${r.skippedCount}, ${r.fileCount} files, ${r.diagnosticCount} diagnostics\n`,
279
- );
283
+ const outcome = indexCommandOutcome(r);
284
+ writeStdout(outcome.stdout);
285
+ if (outcome.exitCode !== 0) process.exitCode = outcome.exitCode;
280
286
  }).catch(fail),
281
287
  );
288
+ }
289
+
290
+ function registerLinkCommand(program: Command): void {
282
291
  program
283
292
  .command('link')
284
293
  .option('--workspace <path>')
@@ -293,6 +302,9 @@ export function createProgram(): Command {
293
302
  );
294
303
  }).catch(fail),
295
304
  );
305
+ }
306
+
307
+ function registerTraceCommand(program: Command): void {
296
308
  program
297
309
  .command('trace')
298
310
  .option('--workspace <path>')
@@ -312,23 +324,91 @@ export function createProgram(): Command {
312
324
  .option('--dynamic-mode <mode>', 'strict|candidates|infer', 'strict')
313
325
  .option('--max-dynamic-candidates <n>', 'maximum dynamic candidates to show', '5')
314
326
  .action((opts: TraceCommandOptions) => void runTraceCommand(opts).catch(fail));
327
+ }
328
+
329
+ function listRepositoriesCommand(
330
+ opts: { workspace?: string },
331
+ ): 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
+ ),
341
+ ));
342
+ }
343
+
344
+ function listServicesCommand(
345
+ opts: { workspace?: string; repo?: string },
346
+ ): Promise<void> {
347
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
348
+ const selection = opts.repo
349
+ ? selectRepository(db, opts.repo, workspaceId) : {};
350
+ if (selection.diagnostic) {
351
+ writeStdout(renderJson([selection.diagnostic]));
352
+ return;
353
+ }
354
+ const repo = selection.repo;
355
+ const rows = db.prepare(
356
+ 'SELECT r.name repo,s.service_path servicePath,s.qualified_name qualifiedName FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (? IS NULL OR s.repo_id=?) ORDER BY r.name,s.service_path',
357
+ ).all(workspaceId, repo?.id, repo?.id);
358
+ writeStdout(renderJson(rows));
359
+ });
360
+ }
361
+
362
+ function listOperationsCommand(
363
+ opts: { workspace?: string; repo?: string; service?: string },
364
+ ): Promise<void> {
365
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
366
+ const selection = opts.repo
367
+ ? selectRepository(db, opts.repo, workspaceId) : {};
368
+ if (selection.diagnostic) {
369
+ writeStdout(renderJson([selection.diagnostic]));
370
+ return;
371
+ }
372
+ const repo = selection.repo;
373
+ const rows = db.prepare(
374
+ 'SELECT r.name repo,s.service_path servicePath,o.operation_name operation,o.operation_path path 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 (? IS NULL OR s.repo_id=?) AND (? IS NULL OR s.service_path=?)',
375
+ ).all(
376
+ workspaceId, repo?.id, repo?.id, opts.service, opts.service,
377
+ );
378
+ writeStdout(renderJson(rows));
379
+ });
380
+ }
381
+
382
+ function listCallsCommand(
383
+ opts: { workspace?: string; repo?: string; operation?: string },
384
+ ): Promise<void> {
385
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
386
+ const selection = opts.repo
387
+ ? selectRepository(db, opts.repo, workspaceId) : {};
388
+ if (selection.diagnostic) {
389
+ writeStdout(renderJson([selection.diagnostic]));
390
+ return;
391
+ }
392
+ const repo = selection.repo;
393
+ const rows = db.prepare(
394
+ 'SELECT r.name repo,c.call_type type,c.operation_path_expr path,c.source_file file,c.source_line line FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE r.workspace_id=? AND (? IS NULL OR c.repo_id=?) AND (? IS NULL OR c.operation_path_expr=? OR c.operation_path_expr=? OR c.payload_summary LIKE ?)',
395
+ ).all(
396
+ workspaceId, repo?.id, repo?.id, opts.operation, opts.operation,
397
+ opts.operation ? `/${opts.operation}` : undefined,
398
+ opts.operation ? `%${opts.operation}%` : undefined,
399
+ );
400
+ writeStdout(renderJson(rows));
401
+ });
402
+ }
403
+
404
+ function registerListCommands(program: Command): void {
315
405
  const list = program.command('list');
316
406
  list
317
407
  .command('repos')
318
408
  .option('--workspace <path>')
319
409
  .action(
320
410
  (opts: { workspace?: string }) =>
321
- void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) =>
322
- writeStdout(
323
- renderJson(
324
- listRepositories(db, workspaceId).map((r) => ({
325
- name: r.name,
326
- kind: r.kind,
327
- packageName: r.package_name,
328
- })),
329
- ),
330
- ),
331
- ).catch(fail),
411
+ void listRepositoriesCommand(opts).catch(fail),
332
412
  );
333
413
  list
334
414
  .command('services')
@@ -336,22 +416,7 @@ export function createProgram(): Command {
336
416
  .option('--repo <name>')
337
417
  .action(
338
418
  (opts: { workspace?: string; repo?: string }) =>
339
- void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
340
- const selection = opts.repo
341
- ? selectRepository(db, opts.repo, workspaceId)
342
- : {};
343
- if (selection.diagnostic) {
344
- writeStdout(renderJson([selection.diagnostic]));
345
- return;
346
- }
347
- const repo = selection.repo;
348
- const rows = db
349
- .prepare(
350
- 'SELECT r.name repo,s.service_path servicePath,s.qualified_name qualifiedName FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (? IS NULL OR s.repo_id=?) ORDER BY r.name,s.service_path',
351
- )
352
- .all(workspaceId, repo?.id, repo?.id);
353
- writeStdout(renderJson(rows));
354
- }).catch(fail),
419
+ void listServicesCommand(opts).catch(fail),
355
420
  );
356
421
  list
357
422
  .command('operations')
@@ -360,22 +425,7 @@ export function createProgram(): Command {
360
425
  .option('--service <path>')
361
426
  .action(
362
427
  (opts: { workspace?: string; repo?: string; service?: string }) =>
363
- void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
364
- const selection = opts.repo
365
- ? selectRepository(db, opts.repo, workspaceId)
366
- : {};
367
- if (selection.diagnostic) {
368
- writeStdout(renderJson([selection.diagnostic]));
369
- return;
370
- }
371
- const repo = selection.repo;
372
- const rows = db
373
- .prepare(
374
- 'SELECT r.name repo,s.service_path servicePath,o.operation_name operation,o.operation_path path 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 (? IS NULL OR s.repo_id=?) AND (? IS NULL OR s.service_path=?)',
375
- )
376
- .all(workspaceId, repo?.id, repo?.id, opts.service, opts.service);
377
- writeStdout(renderJson(rows));
378
- }).catch(fail),
428
+ void listOperationsCommand(opts).catch(fail),
379
429
  );
380
430
  list
381
431
  .command('calls')
@@ -384,31 +434,11 @@ export function createProgram(): Command {
384
434
  .option('--operation <name>')
385
435
  .action(
386
436
  (opts: { workspace?: string; repo?: string; operation?: string }) =>
387
- void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
388
- const selection = opts.repo
389
- ? selectRepository(db, opts.repo, workspaceId)
390
- : {};
391
- if (selection.diagnostic) {
392
- writeStdout(renderJson([selection.diagnostic]));
393
- return;
394
- }
395
- const repo = selection.repo;
396
- const rows = db
397
- .prepare(
398
- 'SELECT r.name repo,c.call_type type,c.operation_path_expr path,c.source_file file,c.source_line line FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE r.workspace_id=? AND (? IS NULL OR c.repo_id=?) AND (? IS NULL OR c.operation_path_expr=? OR c.operation_path_expr=? OR c.payload_summary LIKE ?)',
399
- )
400
- .all(
401
- workspaceId,
402
- repo?.id,
403
- repo?.id,
404
- opts.operation,
405
- opts.operation,
406
- opts.operation ? `/${opts.operation}` : undefined,
407
- opts.operation ? `%${opts.operation}%` : undefined,
408
- );
409
- writeStdout(renderJson(rows));
410
- }).catch(fail),
437
+ void listCallsCommand(opts).catch(fail),
411
438
  );
439
+ }
440
+
441
+ function registerGraphCommand(program: Command): void {
412
442
  program
413
443
  .command('graph')
414
444
  .option('--workspace <path>')
@@ -423,6 +453,33 @@ export function createProgram(): Command {
423
453
  .option('--dynamic-mode <mode>', 'strict|candidates|infer', 'strict')
424
454
  .option('--max-dynamic-candidates <n>', 'maximum dynamic candidates to show', '5')
425
455
  .action((opts: GraphCommandOptions) => void runGraphCommand(opts).catch(fail));
456
+ }
457
+
458
+ function inspectRepositoryCommand(
459
+ name: string,
460
+ opts: { workspace?: string },
461
+ ): Promise<void> {
462
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
463
+ const selection = selectRepository(db, name, workspaceId);
464
+ writeStdout(renderJson(
465
+ selection.repo ?? selection.diagnostic ?? { error: 'repo not found' },
466
+ ));
467
+ });
468
+ }
469
+
470
+ function inspectOperationCommand(
471
+ selector: string,
472
+ opts: { workspace?: string },
473
+ ): Promise<void> {
474
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
475
+ const rows = db.prepare(
476
+ '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
+ ).all(workspaceId, selector, selector);
478
+ writeStdout(renderJson(rows));
479
+ });
480
+ }
481
+
482
+ function registerInspectCommands(program: Command): void {
426
483
  const inspect = program.command('inspect');
427
484
  inspect
428
485
  .command('repo')
@@ -430,12 +487,7 @@ export function createProgram(): Command {
430
487
  .option('--workspace <path>')
431
488
  .action(
432
489
  (name: string, opts: { workspace?: string }) =>
433
- void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
434
- const selection = selectRepository(db, name, workspaceId);
435
- writeStdout(renderJson(
436
- selection.repo ?? selection.diagnostic ?? { error: 'repo not found' },
437
- ));
438
- }).catch(fail),
490
+ void inspectRepositoryCommand(name, opts).catch(fail),
439
491
  );
440
492
  inspect
441
493
  .command('operation')
@@ -443,15 +495,11 @@ export function createProgram(): Command {
443
495
  .option('--workspace <path>')
444
496
  .action(
445
497
  (selector: string, opts: { workspace?: string }) =>
446
- void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
447
- const rows = db
448
- .prepare(
449
- '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=?)',
450
- )
451
- .all(workspaceId, selector, selector);
452
- writeStdout(renderJson(rows));
453
- }).catch(fail),
498
+ void inspectOperationCommand(selector, opts).catch(fail),
454
499
  );
500
+ }
501
+
502
+ function registerDoctorCommand(program: Command): void {
455
503
  program
456
504
  .command('doctor')
457
505
  .option('--workspace <path>')
@@ -467,6 +515,9 @@ export function createProgram(): Command {
467
515
  writeStdout(renderDoctorDiagnostics(allDiagnostics, opts.format));
468
516
  }).catch(fail),
469
517
  );
518
+ }
519
+
520
+ function registerCleanCommand(program: Command): void {
470
521
  program
471
522
  .command('clean')
472
523
  .option('--workspace <path>')
@@ -479,6 +530,19 @@ export function createProgram(): Command {
479
530
  writeStdout('Cleaned service-flow state\n');
480
531
  })().catch(fail),
481
532
  );
533
+ }
534
+
535
+ export function createProgram(): Command {
536
+ const program = configuredProgram();
537
+ registerInitCommand(program);
538
+ registerIndexCommand(program);
539
+ registerLinkCommand(program);
540
+ registerTraceCommand(program);
541
+ registerListCommands(program);
542
+ registerGraphCommand(program);
543
+ registerInspectCommands(program);
544
+ registerDoctorCommand(program);
545
+ registerCleanCommand(program);
482
546
  return program;
483
547
  }
484
548
  function collect(value: string, previous: string[]): string[] {