@saptools/service-flow 0.1.45 → 0.1.47

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.
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  stripQuotes,
18
18
  substituteVariables,
19
19
  trace
20
- } from "./chunk-BXSCE5CR.js";
20
+ } from "./chunk-EGY2A4AT.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.45",
3
+ "version": "0.1.47",
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": {
package/src/cli/doctor.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Db } from '../db/connection.js';
2
2
  import { classifyODataPathIntent, normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
3
+ import { implementationHintSuggestions } from '../trace/implementation-hints.js';
3
4
  import { ANALYZER_VERSION } from '../version.js';
4
5
 
5
6
  type Diagnostic = Record<string, unknown>;
@@ -198,6 +199,7 @@ function implementationEdgeCategories(db: Db, detail: boolean): Array<Diagnostic
198
199
  ...item,
199
200
  servicePathPattern: pathPattern(servicePaths),
200
201
  suggestedAction: categoryAction(String(item.category)),
202
+ suggestedHints: suggestedHints(item.examples),
201
203
  examples: item.examples.slice(0, 3),
202
204
  expandedExamples: detail ? item.examples : undefined,
203
205
  }));
@@ -211,12 +213,27 @@ function addImplementationCategory(grouped: Map<string, Diagnostic & { count: nu
211
213
  const baseOperation = String(row.baseOperation ?? row.operationName ?? evidence.operationName ?? 'unknown');
212
214
  const key = [category, baseOperation, reason, family].join('\0');
213
215
  const current = grouped.get(key) ?? { category, baseOperation, reason, candidateFamily: family, count: 0, servicePaths: [], examples: [] };
216
+ const hintSuggestions = implementationSuggestions(evidence);
214
217
  current.count += 1;
215
218
  current.servicePaths.push(String(row.servicePath ?? evidence.servicePath ?? ''));
216
- current.examples.push({ servicePath: row.servicePath, operation: row.operationName, status: row.status, reason: row.unresolvedReason });
219
+ current.examples.push({ servicePath: row.servicePath, operation: row.operationName, status: row.status, reason: row.unresolvedReason, implementationHintSuggestions: hintSuggestions });
217
220
  grouped.set(key, current);
218
221
  }
219
222
 
223
+ function implementationSuggestions(evidence: Diagnostic): Diagnostic[] | undefined {
224
+ const persisted = asRecords(evidence.implementationHintSuggestions);
225
+ const suggestions = persisted.length ? persisted : implementationHintSuggestions(evidence);
226
+ return suggestions.length ? suggestions : undefined;
227
+ }
228
+
229
+ function suggestedHints(examples: Diagnostic[]): string[] | undefined {
230
+ const hints = examples.flatMap((example) =>
231
+ asRecords(example.implementationHintSuggestions)
232
+ .flatMap((suggestion) => typeof suggestion.cli === 'string' ? [String(suggestion.cli)] : []));
233
+ const unique = [...new Set(hints)].slice(0, 3);
234
+ return unique.length ? unique : undefined;
235
+ }
236
+
220
237
  function missingParameterMetadataCategory(db: Db, detail = false): Diagnostic & { count: number } {
221
238
  const examples = db.prepare(`SELECT sc.source_file sourceFile,sc.source_line sourceLine,sc.callee_expression calleeExpression
222
239
  FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
@@ -6,6 +6,7 @@ import { resolveOperation } from './service-resolver.js';
6
6
  import { linkHelperPackages } from './helper-package-linker.js';
7
7
  import { normalizeDecoratorOperationSignal, normalizedOperationName } from './operation-decorator-normalizer.js';
8
8
  import { externalHttpTarget } from './external-http-target.js';
9
+ import { implementationHintSuggestions } from '../trace/implementation-hints.js';
9
10
  export interface LinkWorkspaceResult {
10
11
  edgeCount: number;
11
12
  unresolvedCount: number;
@@ -201,13 +202,14 @@ function linkImplementations(db: Db, workspaceId: number, generation: number): {
201
202
  candidateFamilies: duplicateFamilies,
202
203
  candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1)),
203
204
  };
205
+ const evidenceWithHints = unique ? evidence : { ...evidence, implementationHintSuggestions: implementationHintSuggestions(evidence) };
204
206
  if (accepted.length === 0) {
205
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, 'OPERATION_IMPLEMENTED_BY_HANDLER', 'unresolved', 'operation', graphId(operation.operationId), 'handler_method_candidates', candidates.map((row) => graphId(row.methodId)).join(','), 0, JSON.stringify(evidence), 0, 'No implementation candidate passed policy', generation);
207
+ db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, 'OPERATION_IMPLEMENTED_BY_HANDLER', 'unresolved', 'operation', graphId(operation.operationId), 'handler_method_candidates', candidates.map((row) => graphId(row.methodId)).join(','), 0, JSON.stringify(evidenceWithHints), 0, 'No implementation candidate passed policy', generation);
206
208
  edgeCount += 1;
207
209
  unresolvedCount += 1;
208
210
  continue;
209
211
  }
210
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, 'OPERATION_IMPLEMENTED_BY_HANDLER', unique ? 'resolved' : 'ambiguous', 'operation', graphId(operation.operationId), unique ? 'handler_method' : 'handler_method_candidates', unique ? graphId(unique.methodId) : selected.map((row) => graphId(row.methodId)).join(','), unique ? 0.95 : 0.5, JSON.stringify(evidence), 0, unique ? null : 'Ambiguous registered handler implementation candidates', generation);
212
+ db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, 'OPERATION_IMPLEMENTED_BY_HANDLER', unique ? 'resolved' : 'ambiguous', 'operation', graphId(operation.operationId), unique ? 'handler_method' : 'handler_method_candidates', unique ? graphId(unique.methodId) : selected.map((row) => graphId(row.methodId)).join(','), unique ? 0.95 : 0.5, JSON.stringify(evidenceWithHints), 0, unique ? null : 'Ambiguous registered handler implementation candidates', generation);
211
213
  edgeCount += 1;
212
214
  if (unique) resolvedCount += 1;
213
215
  else ambiguousCount += 1;
@@ -15,7 +15,22 @@ export function renderTraceTable(result: TraceResult): string {
15
15
  const lines = ['Step Type From To Evidence'];
16
16
  for (const e of result.edges) {
17
17
  lines.push(`${String(e.step).padEnd(5)} ${e.type.padEnd(20)} ${e.from.slice(0, 34).padEnd(35)} ${e.to.slice(0, 35).padEnd(36)} ${location(e.evidence)}`);
18
+ const hint = firstHint(e.evidence);
19
+ if (e.unresolvedReason && hint) lines.push(` try ${hint}`);
18
20
  }
19
- if (result.diagnostics.length > 0) lines.push('', 'Diagnostics:', ...result.diagnostics.map((d) => `${String(d.severity ?? 'info')} ${String(d.code ?? 'diagnostic')} ${String(d.message ?? '')}`));
21
+ if (result.diagnostics.length > 0) lines.push('', 'Diagnostics:', ...result.diagnostics.flatMap(diagnosticLines));
20
22
  return `${lines.join('\n')}\n`;
21
23
  }
24
+
25
+ function diagnosticLines(diagnostic: Record<string, unknown>): string[] {
26
+ const first = `${String(diagnostic.severity ?? 'info')} ${String(diagnostic.code ?? 'diagnostic')} ${String(diagnostic.message ?? '')}`;
27
+ const hint = firstHint(diagnostic);
28
+ return hint ? [first, ` try ${hint}`] : [first];
29
+ }
30
+
31
+ function firstHint(evidence: Record<string, unknown>): string | undefined {
32
+ const suggestions = evidence.implementationHintSuggestions;
33
+ if (!Array.isArray(suggestions)) return undefined;
34
+ const first = suggestions.find((item): item is Record<string, unknown> => Boolean(item) && typeof item === 'object');
35
+ return typeof first?.cli === 'string' ? first.cli : undefined;
36
+ }
@@ -65,7 +65,10 @@ export function selectImplementation(
65
65
  return hint ? selectCandidate(evidence, hint, 'scoped_implementation_hint') : { blocksAutomatic: false, evidence: { status: 'not_matched' } };
66
66
  }
67
67
 
68
- export function implementationHintDiagnostic(selection: ImplementationSelection): Record<string, unknown> | undefined {
68
+ export function implementationHintDiagnostic(
69
+ selection: ImplementationSelection,
70
+ suggestions?: unknown,
71
+ ): Record<string, unknown> | undefined {
69
72
  if (!selection.blocksAutomatic || selection.methodId) return undefined;
70
73
  return {
71
74
  severity: 'warning',
@@ -73,10 +76,41 @@ export function implementationHintDiagnostic(selection: ImplementationSelection)
73
76
  message: 'Implementation hint did not select exactly one viable candidate',
74
77
  hintStatus: selection.evidence.status,
75
78
  candidateCount: selection.evidence.candidateCount,
79
+ implementationHintSuggestions: Array.isArray(suggestions) && suggestions.length > 0 ? suggestions : undefined,
76
80
  implementationSelection: selection.evidence,
77
81
  };
78
82
  }
79
83
 
84
+ export function implementationHintSuggestions(rawEvidence: Record<string, unknown>): Array<Record<string, unknown>> {
85
+ const evidence = asEvidence(rawEvidence);
86
+ const accepted = (evidence.candidates ?? []).filter((candidate) => candidate.accepted);
87
+ if (accepted.length < 2) return [];
88
+ const repos = selectableRepositories(accepted);
89
+ return accepted
90
+ .flatMap((candidate) => {
91
+ const repo = candidate.handlerPackage?.name;
92
+ if (!repo || !repos.includes(repo)) return [];
93
+ const hint = suggestionHint(evidence, candidate, repo);
94
+ return [{
95
+ servicePath: hint.servicePath,
96
+ operationPath: hint.operationPath,
97
+ ambiguityReason: evidence.ambiguityReasons?.[0],
98
+ candidateFamily: hint.candidateFamily,
99
+ selectableImplementationRepositories: repos,
100
+ implementationRepo: repo,
101
+ hint,
102
+ cli: `--implementation-hint ${hintString(hint)}`,
103
+ }];
104
+ });
105
+ }
106
+
107
+ function selectableRepositories(candidates: Candidate[]): string[] {
108
+ const repos = new Set(candidates.flatMap((candidate) => candidate.handlerPackage?.name ? [candidate.handlerPackage.name] : []));
109
+ return [...repos]
110
+ .filter((repo) => candidates.filter((candidate) => candidateMatchesRepo(candidate, repo)).length === 1)
111
+ .sort();
112
+ }
113
+
80
114
  function assignHintField(hint: Partial<ImplementationHint>, key: string, value: string): void {
81
115
  if (key === 'service' || key === 'servicePath') hint.servicePath = value;
82
116
  else if (key === 'operation' || key === 'operationPath') hint.operationPath = value;
@@ -119,6 +153,44 @@ function selectCandidate(evidence: EdgeEvidence, hint: ImplementationHint, strat
119
153
  };
120
154
  }
121
155
 
156
+ function suggestionHint(evidence: EdgeEvidence, candidate: Candidate, repo: string): ImplementationHint {
157
+ const servicePath = evidence.servicePath ?? candidate.servicePath;
158
+ const operationPath = evidence.operationPath ?? candidate.operationPath;
159
+ const family = usefulCandidateFamily(evidence, candidate);
160
+ return {
161
+ ...(servicePath ? { servicePath } : {}),
162
+ ...(operationPath ? { operationPath } : {}),
163
+ ...(evidence.modelPackage?.packageName ? { packageName: evidence.modelPackage.packageName } : {}),
164
+ ...(evidence.modelPackage?.name ? { repositoryName: evidence.modelPackage.name } : {}),
165
+ ...(family ? { candidateFamily: family } : {}),
166
+ implementationRepo: repo,
167
+ };
168
+ }
169
+
170
+ function usefulCandidateFamily(evidence: EdgeEvidence, candidate: Candidate): string | undefined {
171
+ const family = candidate.handlerPackage?.packageName;
172
+ if (!family) return undefined;
173
+ if ((evidence.candidateFamilies ?? []).some((item) => item.packageName === family)) return family;
174
+ const acceptedFamilies = new Set(
175
+ (evidence.candidates ?? [])
176
+ .filter((item) => item.accepted)
177
+ .flatMap((item) => item.handlerPackage?.packageName ? [item.handlerPackage.packageName] : []),
178
+ );
179
+ return acceptedFamilies.size > 1 ? family : undefined;
180
+ }
181
+
182
+ function hintString(hint: ImplementationHint): string {
183
+ const fields = [
184
+ ['service', hint.servicePath],
185
+ ['operation', hint.operationPath],
186
+ ['package', hint.packageName],
187
+ ['repository', hint.repositoryName],
188
+ ['family', hint.candidateFamily],
189
+ ['repo', hint.implementationRepo],
190
+ ];
191
+ return fields.flatMap(([key, value]) => value ? [`${key}=${value}`] : []).join(',');
192
+ }
193
+
122
194
  function hintMatchesEdge(hint: ImplementationHint, evidence: EdgeEvidence): boolean {
123
195
  const model = evidence.modelPackage ?? evidence.candidates?.[0]?.modelPackage;
124
196
  const familyNames = new Set([
@@ -98,8 +98,8 @@ function operationStartScope(db: Db, repoId: number | undefined, start: TraceSta
98
98
  }
99
99
  if (impl.edge) {
100
100
  const evidence = parseEvidence(impl.edge.evidence_json);
101
- const hintDiagnostic = implementationHintDiagnostic(hinted);
102
- const diagnostics = [{ severity: 'warning', code: impl.edge.status === 'ambiguous' ? 'trace_start_ambiguous' : 'trace_start_implementation_unresolved', message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? 'unresolved')}`, resolutionStage: 'implementation', resolutionStatus: impl.edge.status === 'ambiguous' ? 'ambiguous_implementation' : 'rejected_implementation', implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, implementationAmbiguityReasons: evidence.ambiguityReasons, candidates: evidence.candidates }];
101
+ const hintDiagnostic = implementationHintDiagnostic(hinted, evidence.implementationHintSuggestions);
102
+ const diagnostics = [{ severity: 'warning', code: impl.edge.status === 'ambiguous' ? 'trace_start_ambiguous' : 'trace_start_implementation_unresolved', message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? 'unresolved')}`, resolutionStage: 'implementation', resolutionStatus: impl.edge.status === 'ambiguous' ? 'ambiguous_implementation' : 'rejected_implementation', implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, implementationAmbiguityReasons: evidence.ambiguityReasons, implementationHintSuggestions: evidence.implementationHintSuggestions, candidates: evidence.candidates }];
103
103
  return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
104
104
  }
105
105
  return { operationId, diagnostics: [{ severity: 'warning', code: 'trace_start_implementation_unresolved', message: 'Indexed operation matched but no implementation candidate exists', resolutionStage: 'implementation', resolutionStatus: 'operation_without_implementation' }] };
@@ -547,9 +547,9 @@ export function trace(
547
547
  const contextMethodId = contextSelection.methodId;
548
548
  const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : undefined;
549
549
  if (implementation.edge) {
550
- const hintDiagnostic = implementationHintDiagnostic(contextSelection);
551
- if (hintDiagnostic) diagnostics.unshift(hintDiagnostic);
552
550
  const implEvidence = JSON.parse(String(implementation.edge.evidence_json || '{}')) as Record<string, unknown>;
551
+ const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence.implementationHintSuggestions);
552
+ if (hintDiagnostic) diagnostics.unshift(hintDiagnostic);
553
553
  const handlerNode = implementation.edge.status === 'resolved' ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
554
554
  const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
555
555
  if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);