@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/CHANGELOG.md +7 -0
- package/README.md +0 -36
- package/dist/{chunk-BXSCE5CR.js → chunk-EGY2A4AT.js} +175 -114
- package/dist/chunk-EGY2A4AT.js.map +1 -0
- package/dist/cli.js +30 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/doctor.ts +18 -1
- package/src/linker/cross-repo-linker.ts +4 -2
- package/src/output/table-output.ts +16 -1
- package/src/trace/implementation-hints.ts +73 -1
- package/src/trace/trace-engine.ts +4 -4
- package/dist/chunk-BXSCE5CR.js.map +0 -1
package/dist/index.js
CHANGED
package/package.json
CHANGED
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(
|
|
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(
|
|
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.
|
|
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(
|
|
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);
|