@saptools/service-flow 0.1.52 → 0.1.54

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 (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +11 -12
  3. package/TECHNICAL-NOTE.md +18 -3
  4. package/dist/{chunk-PTLDSHRC.js → chunk-ERIZHM5C.js} +2646 -1067
  5. package/dist/chunk-ERIZHM5C.js.map +1 -0
  6. package/dist/cli.js +162 -13
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.js +1 -1
  9. package/package.json +1 -1
  10. package/src/cli/001-doctor-projection.ts +136 -0
  11. package/src/cli/doctor.ts +20 -3
  12. package/src/db/repositories.ts +10 -2
  13. package/src/linker/000-implementation-candidates.ts +674 -0
  14. package/src/linker/001-implementation-evidence-projection.ts +191 -0
  15. package/src/linker/002-call-evidence.ts +226 -0
  16. package/src/linker/cross-repo-linker.ts +27 -453
  17. package/src/linker/dynamic-edge-resolver.ts +35 -0
  18. package/src/linker/external-http-target.ts +24 -3
  19. package/src/linker/helper-package-linker.ts +18 -2
  20. package/src/linker/service-resolver.ts +45 -2
  21. package/src/output/doctor-output.ts +13 -4
  22. package/src/output/table-output.ts +15 -4
  23. package/src/trace/000-dynamic-target-types.ts +12 -0
  24. package/src/trace/001-dynamic-identity.ts +45 -17
  25. package/src/trace/003-dynamic-references.ts +236 -35
  26. package/src/trace/004-dynamic-candidate-sources.ts +155 -0
  27. package/src/trace/005-implementation-selection.ts +187 -0
  28. package/src/trace/006-contextual-projection.ts +30 -0
  29. package/src/trace/007-implementation-start-diagnostic.ts +61 -0
  30. package/src/trace/008-contextual-runtime-state.ts +294 -0
  31. package/src/trace/009-selected-handler-provenance.ts +186 -0
  32. package/src/trace/dynamic-targets.ts +205 -159
  33. package/src/trace/evidence.ts +132 -34
  34. package/src/trace/implementation-hints.ts +148 -8
  35. package/src/trace/selectors.ts +74 -9
  36. package/src/trace/trace-engine.ts +97 -125
  37. package/src/utils/000-bounded-projection.ts +166 -0
  38. package/dist/chunk-PTLDSHRC.js.map +0 -1
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  stripQuotes,
18
18
  substituteVariables,
19
19
  trace
20
- } from "./chunk-PTLDSHRC.js";
20
+ } from "./chunk-ERIZHM5C.js";
21
21
 
22
22
  // src/parsers/generated-constants-parser.ts
23
23
  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.52",
3
+ "version": "0.1.54",
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,136 @@
1
+ import {
2
+ projectBoundedInOrder,
3
+ type BoundedProjection,
4
+ } from '../utils/000-bounded-projection.js';
5
+
6
+ type Diagnostic = Record<string, unknown>;
7
+
8
+ const boundedDoctorArrayKeys = new Set([
9
+ 'candidates',
10
+ 'candidateScores',
11
+ 'candidateFamilies',
12
+ 'candidateEvidence',
13
+ 'candidatePaths',
14
+ 'candidateRawPaths',
15
+ 'candidateNormalizedOperationPaths',
16
+ 'normalizedCandidateOperations',
17
+ 'candidateLiterals',
18
+ 'bindingCandidates',
19
+ 'bindingAlternatives',
20
+ 'registrations',
21
+ 'implementationHintSuggestions',
22
+ 'selectableImplementationRepositories',
23
+ 'matchedHints',
24
+ 'candidateSuggestions',
25
+ 'dynamicTargetCandidates',
26
+ 'dynamicTargetCandidateSuggestions',
27
+ 'rejectedCandidates',
28
+ 'suggestedVarSets',
29
+ 'copyableExamples',
30
+ 'examples',
31
+ 'expandedExamples',
32
+ 'selectorSuggestions',
33
+ 'serviceSuggestions',
34
+ 'repositories',
35
+ ]);
36
+
37
+ export function boundDoctorDiagnostics(diagnostics: Diagnostic[]): Diagnostic[] {
38
+ return diagnostics.map(boundDoctorDiagnostic);
39
+ }
40
+
41
+ function boundDoctorDiagnostic(diagnostic: Diagnostic): Diagnostic {
42
+ const bounded = boundDoctorValue(diagnostic);
43
+ return isDiagnostic(bounded) ? bounded : {};
44
+ }
45
+
46
+ function boundDoctorValue(value: unknown): unknown {
47
+ if (Array.isArray(value)) return value.map(boundDoctorValue);
48
+ if (!isDiagnostic(value)) return value;
49
+ const input = value;
50
+ const output: Diagnostic = {};
51
+ for (const [key, child] of Object.entries(input)) {
52
+ if (!Array.isArray(child) || !boundedDoctorArrayKeys.has(key)) {
53
+ output[key] = boundDoctorValue(child);
54
+ continue;
55
+ }
56
+ // Doctor producers already query or assemble deterministic semantic order.
57
+ const projection = projectBoundedInOrder(child.map(boundDoctorValue));
58
+ output[key] = projection.items;
59
+ addProjectionMetadata(output, input, key, projection);
60
+ }
61
+ return output;
62
+ }
63
+
64
+ function isDiagnostic(value: unknown): value is Diagnostic {
65
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
66
+ }
67
+
68
+ function addProjectionMetadata(
69
+ output: Diagnostic,
70
+ input: Diagnostic,
71
+ key: string,
72
+ projection: BoundedProjection<unknown>,
73
+ ): void {
74
+ const names = projectionNames(key);
75
+ const total = Math.max(
76
+ numericValue(input[names.total]),
77
+ projection.totalCount,
78
+ siblingCollectionCount(input, key),
79
+ );
80
+ output[names.total] = total;
81
+ output[names.shown] = projection.shownCount;
82
+ output[names.omitted] = Math.max(0, total - projection.shownCount);
83
+ }
84
+
85
+ function siblingCollectionCount(input: Diagnostic, key: string): number {
86
+ if (key !== 'examples' || !Array.isArray(input.expandedExamples)) return 0;
87
+ return input.expandedExamples.length;
88
+ }
89
+
90
+ function projectionNames(key: string): { total: string; shown: string; omitted: string } {
91
+ const stem = projectionStem(key);
92
+ return {
93
+ total: `${stem}Count`,
94
+ shown: `shown${upperFirst(stem)}Count`,
95
+ omitted: `omitted${upperFirst(stem)}Count`,
96
+ };
97
+ }
98
+
99
+ function projectionStem(key: string): string {
100
+ const stems: Record<string, string> = {
101
+ candidates: 'candidate',
102
+ candidateScores: 'candidateScore',
103
+ candidateFamilies: 'candidateFamily',
104
+ candidateEvidence: 'candidateEvidence',
105
+ candidatePaths: 'candidatePath',
106
+ candidateRawPaths: 'candidateRawPath',
107
+ candidateNormalizedOperationPaths: 'candidateNormalizedOperationPath',
108
+ normalizedCandidateOperations: 'normalizedCandidateOperation',
109
+ candidateLiterals: 'candidateLiteral',
110
+ bindingCandidates: 'bindingCandidate',
111
+ bindingAlternatives: 'bindingAlternative',
112
+ registrations: 'registration',
113
+ implementationHintSuggestions: 'implementationHintSuggestion',
114
+ selectableImplementationRepositories: 'selectableImplementationRepository',
115
+ matchedHints: 'matchedHint',
116
+ candidateSuggestions: 'candidateSuggestion',
117
+ dynamicTargetCandidates: 'dynamicTargetCandidate',
118
+ dynamicTargetCandidateSuggestions: 'dynamicTargetCandidateSuggestion',
119
+ rejectedCandidates: 'rejectedCandidate',
120
+ suggestedVarSets: 'suggestedVarSet',
121
+ copyableExamples: 'copyableExample',
122
+ examples: 'example',
123
+ expandedExamples: 'expandedExample',
124
+ selectorSuggestions: 'selectorSuggestion',
125
+ serviceSuggestions: 'serviceSuggestion',
126
+ };
127
+ return stems[key] ?? 'repository';
128
+ }
129
+
130
+ function numericValue(value: unknown): number {
131
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
132
+ }
133
+
134
+ function upperFirst(value: string): string {
135
+ return value ? `${value[0]?.toUpperCase() ?? ''}${value.slice(1)}` : value;
136
+ }
package/src/cli/doctor.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Db } from '../db/connection.js';
2
2
  import { classifyODataPathIntent, normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
3
3
  import { implementationHintSuggestions } from '../trace/implementation-hints.js';
4
+ import { boundDoctorDiagnostics } from './001-doctor-projection.js';
4
5
  import { ANALYZER_VERSION } from '../version.js';
5
6
 
6
7
  type Diagnostic = Record<string, unknown>;
@@ -15,7 +16,7 @@ export function linkUpgradeWarnings(db: Db): Diagnostic[] {
15
16
 
16
17
  export function doctorDiagnostics(db: Db, strict: boolean, options: DoctorOptions = {}): Diagnostic[] {
17
18
  const diagnostics = db.prepare('SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id').all() as Diagnostic[];
18
- return [
19
+ return boundDoctorDiagnostics([
19
20
  ...diagnostics,
20
21
  ...healthDiagnostics(db, strict),
21
22
  ...remoteTargetWithoutImplementationDiagnostics(db, strict, Boolean(options.detail)),
@@ -23,7 +24,7 @@ export function doctorDiagnostics(db: Db, strict: boolean, options: DoctorOption
23
24
  ...schemaDriftDiagnostics(db, strict),
24
25
  ...analyzerVersionDiagnostics(db, strict),
25
26
  ...parserQualityDiagnostics(db, strict, options),
26
- ];
27
+ ]);
27
28
  }
28
29
 
29
30
  function healthDiagnostics(db: Db, strict: boolean): Diagnostic[] {
@@ -252,6 +253,10 @@ function addImplementationCategory(grouped: Map<string, Diagnostic & { count: nu
252
253
  const current = grouped.get(key) ?? { category, baseOperation, reason, candidateFamily: family, count: 0, servicePaths: [], examples: [] };
253
254
  const hintSuggestions = implementationSuggestions(evidence);
254
255
  const candidates = asRecords(evidence.candidates);
256
+ const candidateCount = Math.max(
257
+ numericValue(evidence.candidateCount),
258
+ candidates.length,
259
+ );
255
260
  current.count += 1;
256
261
  current.servicePaths.push(String(row.servicePath ?? evidence.servicePath ?? ''));
257
262
  current.examples.push({
@@ -259,7 +264,15 @@ function addImplementationCategory(grouped: Map<string, Diagnostic & { count: nu
259
264
  operation: row.operationName,
260
265
  status: row.status,
261
266
  reason: row.unresolvedReason,
262
- candidateCount: candidates.length,
267
+ candidateCount,
268
+ shownCandidateCount: Math.min(
269
+ candidateCount,
270
+ numericValue(evidence.shownCandidateCount) || candidates.length,
271
+ ),
272
+ omittedCandidateCount: Math.max(
273
+ 0,
274
+ candidateCount - (numericValue(evidence.shownCandidateCount) || candidates.length),
275
+ ),
263
276
  candidateEvidence: candidates.slice(0, 3),
264
277
  implementationHintSuggestions: hintSuggestions,
265
278
  });
@@ -655,6 +668,10 @@ function candidateCount(value: unknown): number {
655
668
  return Number(parseObject(value).candidateCount ?? 0);
656
669
  }
657
670
 
671
+ function numericValue(value: unknown): number {
672
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
673
+ }
674
+
658
675
  function parseObject(value: unknown): Diagnostic {
659
676
  if (value && typeof value === 'object' && !Array.isArray(value)) return value as Diagnostic;
660
677
  try {
@@ -1,4 +1,5 @@
1
1
  import type { Db } from './connection.js';
2
+ import { projectBounded } from '../utils/000-bounded-projection.js';
2
3
  import type {
3
4
  CdsRequire,
4
5
  CdsServiceFact,
@@ -596,12 +597,19 @@ function bindingEvidence(
596
597
  candidates: BindingCandidate[],
597
598
  selected?: BindingCandidate,
598
599
  ): Record<string, unknown> {
600
+ const projection = projectBounded(candidates, (left, right) =>
601
+ Number(right.id === selected?.id) - Number(left.id === selected?.id)
602
+ || left.sourceFile.localeCompare(right.sourceFile)
603
+ || left.sourceLine - right.sourceLine
604
+ || left.id - right.id);
599
605
  return {
600
606
  status,
601
- candidateCount: candidates.length,
607
+ candidateCount: projection.totalCount,
608
+ shownCandidateCount: projection.shownCount,
609
+ omittedCandidateCount: projection.omittedCount,
602
610
  selectedBindingId: selected?.id,
603
611
  sourceOrderRule: 'binding_source_line_must_not_follow_call',
604
- candidates: candidates.map((candidate) => ({
612
+ candidates: projection.items.map((candidate) => ({
605
613
  bindingId: candidate.id,
606
614
  symbolId: candidate.symbolId,
607
615
  variableName: candidate.variableName,