@saptools/service-flow 0.1.43 → 0.1.45

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.
@@ -0,0 +1,210 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import { extractPlaceholders, substituteVariables, type RuntimeSubstitution } from '../linker/dynamic-edge-resolver.js';
3
+ import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
4
+ import { resolveOperation, type OperationTarget } from '../linker/service-resolver.js';
5
+
6
+ export interface TraceGraphRow extends Record<string, unknown> {
7
+ id: number;
8
+ edge_type: string;
9
+ from_id: string;
10
+ to_kind: string;
11
+ to_id: string;
12
+ confidence: number;
13
+ evidence_json: string;
14
+ unresolved_reason?: string;
15
+ status?: string;
16
+ }
17
+
18
+ interface Candidate {
19
+ servicePath?: string;
20
+ operationPath?: string;
21
+ repoName?: string;
22
+ operationName?: string;
23
+ score?: number;
24
+ }
25
+
26
+ export function baseTraceEvidence(
27
+ row: TraceGraphRow,
28
+ call: Record<string, unknown>,
29
+ persistedEvidence: Record<string, unknown>,
30
+ contextualEvidence: Record<string, unknown> | undefined,
31
+ ): Record<string, unknown> {
32
+ const evidence = { ...persistedEvidence, ...(contextualEvidence ?? {}) };
33
+ return {
34
+ ...evidence,
35
+ graphEdgeId: row.id,
36
+ persistedGraphEdgeId: row.id > 0 ? row.id : undefined,
37
+ outboundCallId: call.id,
38
+ callSite: { sourceFile: call.source_file, sourceLine: call.source_line },
39
+ sourceFile: call.source_file,
40
+ sourceLine: call.source_line,
41
+ file: call.source_file,
42
+ line: call.source_line,
43
+ persistedTarget: { kind: row.to_kind, id: row.to_id },
44
+ contextualResolutionParticipated: Boolean(contextualEvidence?.contextualServiceBindingAttempted),
45
+ persistedResolution: persistedResolution(row),
46
+ };
47
+ }
48
+
49
+ export function runtimeResolution(
50
+ db: Db,
51
+ row: TraceGraphRow,
52
+ evidence: Record<string, unknown>,
53
+ vars: Record<string, string> | undefined,
54
+ workspaceId: number | undefined,
55
+ ): { row: TraceGraphRow; evidence: Record<string, unknown>; target?: OperationTarget; unresolvedReason?: string } {
56
+ const substituted = evidenceWithRuntimeVariables(evidence, vars);
57
+ if (!isRemoteRuntimeCandidate(row, evidence, vars)) {
58
+ const withSections = withEffectiveResolution(substituted, row, row.unresolved_reason);
59
+ return { row, evidence: withSections, unresolvedReason: row.unresolved_reason };
60
+ }
61
+ const resolution = resolveRuntimeOperation(db, substituted, workspaceId);
62
+ if (resolution.target) {
63
+ const resolvedRow = { ...row, status: 'resolved', to_kind: 'operation', to_id: String(resolution.target.operationId), unresolved_reason: undefined, confidence: Math.max(0, Math.min(1, resolution.target.score)) };
64
+ return { row: resolvedRow, evidence: withEffectiveResolution(substituted, resolvedRow, undefined, resolution), target: resolution.target };
65
+ }
66
+ const unresolvedReason = runtimeUnresolvedReason(resolution);
67
+ return { row, evidence: withEffectiveResolution(substituted, row, unresolvedReason, resolution), unresolvedReason };
68
+ }
69
+
70
+ export function runtimeVariableDiagnostic(edges: Array<{ evidence: Record<string, unknown> }>): Record<string, unknown> | undefined {
71
+ const missing = new Set<string>();
72
+ for (const edge of edges) {
73
+ const substitutions = edge.evidence.runtimeSubstitutions;
74
+ if (!substitutions || typeof substitutions !== 'object' || Array.isArray(substitutions)) continue;
75
+ for (const value of Object.values(substitutions as Record<string, RuntimeSubstitution>))
76
+ for (const key of value.missing ?? []) missing.add(key);
77
+ }
78
+ const missingVariables = [...missing].sort();
79
+ if (missingVariables.length === 0) return undefined;
80
+ return {
81
+ severity: 'warning',
82
+ code: 'trace_runtime_variables_missing',
83
+ message: `Runtime variables are required to resolve dynamic trace targets: ${missingVariables.join(', ')}`,
84
+ missingVariables,
85
+ suggestions: missingVariables.map((key) => `--var ${key}=<value>`),
86
+ };
87
+ }
88
+
89
+ export function edgeTarget(row: TraceGraphRow, evidence: Record<string, unknown>): string {
90
+ const effective = parseObject(evidence.effectiveResolution);
91
+ const targetServicePath = stringValue(effective.targetServicePath ?? evidence.targetServicePath);
92
+ const targetOperationPath = stringValue(effective.targetOperationPath ?? evidence.targetOperationPath);
93
+ if (targetServicePath && targetOperationPath) return `${targetServicePath}${targetOperationPath}`;
94
+ const runtimeCandidate = evidence.runtimeResolvedCandidate as Candidate | undefined;
95
+ if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath) return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
96
+ const servicePath = stringValue(evidence.servicePath);
97
+ const operationPath = stringValue(evidence.operationPath);
98
+ const targetOperation = stringValue(evidence.targetOperation);
99
+ const targetRepo = stringValue(evidence.targetRepo) ?? '';
100
+ if (row.edge_type === 'HANDLER_RUNS_DB_QUERY') return `Entity: ${row.to_id || 'unknown'}`;
101
+ if (row.edge_type === 'HANDLER_RUNS_REMOTE_QUERY') return stringValue(evidence.remoteQueryTarget) ?? `Remote query: ${row.to_id || 'unknown'}`;
102
+ if (row.edge_type === 'HANDLER_CALLS_EXTERNAL_HTTP') {
103
+ const target = parseObject(evidence.externalTarget);
104
+ return stringValue(target.label) ?? `External endpoint: ${row.to_id || 'unknown'}`;
105
+ }
106
+ if (servicePath && operationPath) return `${servicePath}${operationPath}`;
107
+ return targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
108
+ }
109
+
110
+ function persistedResolution(row: TraceGraphRow): Record<string, unknown> {
111
+ return {
112
+ status: row.status,
113
+ targetKind: row.to_kind,
114
+ targetId: row.to_id,
115
+ edgeId: row.id > 0 ? row.id : undefined,
116
+ confidence: row.confidence,
117
+ unresolvedReason: row.unresolved_reason,
118
+ edgeType: row.edge_type,
119
+ };
120
+ }
121
+
122
+ function effectiveResolution(
123
+ row: TraceGraphRow,
124
+ evidence: Record<string, unknown>,
125
+ unresolvedReason: string | undefined,
126
+ resolution?: ReturnType<typeof resolveRuntimeOperation>,
127
+ ): Record<string, unknown> {
128
+ const target = resolution?.target;
129
+ return {
130
+ status: target ? 'resolved' : row.status,
131
+ targetKind: target ? 'operation' : row.to_kind,
132
+ targetId: target ? String(target.operationId) : row.to_id,
133
+ targetRepo: target?.repoName ?? evidence.targetRepo,
134
+ targetServicePath: target?.servicePath ?? evidence.targetServicePath,
135
+ targetOperationPath: target?.operationPath ?? evidence.targetOperationPath,
136
+ targetOperation: target?.operationName ?? evidence.targetOperation,
137
+ confidence: target?.score ?? row.confidence,
138
+ reasons: resolution?.reasons ?? evidence.resolutionReasons,
139
+ unresolvedReason,
140
+ edgeType: target ? 'REMOTE_CALL_RESOLVES_TO_OPERATION' : row.edge_type,
141
+ };
142
+ }
143
+
144
+ function withEffectiveResolution(
145
+ evidence: Record<string, unknown>,
146
+ row: TraceGraphRow,
147
+ unresolvedReason: string | undefined,
148
+ resolution?: ReturnType<typeof resolveRuntimeOperation>,
149
+ ): Record<string, unknown> {
150
+ const current = effectiveResolution(row, evidence, unresolvedReason, resolution);
151
+ const rest = { ...evidence };
152
+ delete rest.runtimeResolvedCandidate;
153
+ return { ...rest, effectiveResolution: current, linker: { status: current.status, confidence: current.confidence, reason: unresolvedReason, edgeType: current.edgeType } };
154
+ }
155
+
156
+ function resolveRuntimeOperation(db: Db, evidence: Record<string, unknown>, workspaceId: number | undefined): ReturnType<typeof resolveOperation> {
157
+ const servicePath = stringValue(evidence.servicePath);
158
+ const rawOperationPath = stringValue(evidence.operationPath);
159
+ const normalized = normalizeODataOperationInvocationPath(rawOperationPath);
160
+ const operationPath = normalized?.wasInvocation
161
+ ? normalized.normalizedOperationPath
162
+ : stringValue(evidence.normalizedOperationPath) ?? rawOperationPath;
163
+ const alias = stringValue(evidence.serviceAliasExpr ?? evidence.serviceAlias);
164
+ const destination = stringValue(evidence.destination);
165
+ return resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: true }, workspaceId);
166
+ }
167
+
168
+ function evidenceWithRuntimeVariables(evidence: Record<string, unknown>, vars: Record<string, string> | undefined): Record<string, unknown> {
169
+ const substitutions = runtimeSubstitutions(evidence, vars ?? {});
170
+ const next: Record<string, unknown> = { ...evidence, runtimeSubstitutions: substitutions };
171
+ for (const [key, value] of Object.entries(substitutions)) if (value.effective) next[key] = value.effective;
172
+ const missing = Object.values(substitutions).flatMap((value) => value.missing);
173
+ if (missing.length > 0) next.missingRuntimeVariables = [...new Set(missing)].sort();
174
+ if (Object.keys(vars ?? {}).length > 0) next.runtimeVariablesApplied = true;
175
+ return next;
176
+ }
177
+
178
+ function runtimeSubstitutions(evidence: Record<string, unknown>, vars: Record<string, string>): Record<string, RuntimeSubstitution> {
179
+ const substitutions: Record<string, RuntimeSubstitution> = {};
180
+ for (const key of ['servicePath', 'operationPath', 'serviceAliasExpr', 'serviceAlias', 'destination']) {
181
+ const substitution = substituteVariables(stringValue(evidence[key]), vars);
182
+ if (substitution.placeholders.length > 0) substitutions[key] = substitution;
183
+ }
184
+ return substitutions;
185
+ }
186
+
187
+ function isRemoteRuntimeCandidate(row: TraceGraphRow, evidence: Record<string, unknown>, vars: Record<string, string> | undefined): boolean {
188
+ if (!vars || Object.keys(vars).length === 0) return false;
189
+ if (!['dynamic', 'ambiguous', 'unresolved'].includes(String(row.status ?? ''))) return false;
190
+ if (!['DYNAMIC_EDGE_CANDIDATE', 'UNRESOLVED_EDGE', 'REMOTE_CALL_RESOLVES_TO_OPERATION'].includes(row.edge_type)) return false;
191
+ return ['servicePath', 'operationPath', 'serviceAliasExpr', 'serviceAlias', 'destination'].some((key) => hasRuntimeVariable(evidence[key], vars));
192
+ }
193
+
194
+ function hasRuntimeVariable(value: unknown, vars: Record<string, string>): boolean {
195
+ return typeof value === 'string' && extractPlaceholders(value).some((key) => Object.hasOwn(vars, key));
196
+ }
197
+
198
+ function runtimeUnresolvedReason(resolution: ReturnType<typeof resolveRuntimeOperation>): string {
199
+ if (resolution.status === 'dynamic') return `Dynamic target is missing runtime variables: ${resolution.reasons.join(', ')}`;
200
+ if (resolution.status === 'ambiguous') return 'Ambiguous runtime operation candidates';
201
+ return 'No runtime operation candidate matched substituted service and operation path';
202
+ }
203
+
204
+ function parseObject(value: unknown): Record<string, unknown> {
205
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
206
+ }
207
+
208
+ function stringValue(value: unknown): string | undefined {
209
+ return typeof value === 'string' ? value : undefined;
210
+ }
@@ -0,0 +1,151 @@
1
+ import type { ImplementationHint } from '../types.js';
2
+
3
+ interface Candidate {
4
+ accepted?: boolean;
5
+ methodId?: number;
6
+ sourceFile?: string;
7
+ handlerPackage?: { name?: string; packageName?: string };
8
+ modelPackage?: { name?: string; packageName?: string };
9
+ servicePath?: string;
10
+ operationPath?: string;
11
+ }
12
+
13
+ interface EdgeEvidence {
14
+ servicePath?: string;
15
+ operationPath?: string;
16
+ ambiguityReasons?: string[];
17
+ candidateFamilies?: Array<{ packageName?: string }>;
18
+ candidates?: Candidate[];
19
+ modelPackage?: { name?: string; packageName?: string };
20
+ }
21
+
22
+ export interface ImplementationSelection {
23
+ methodId?: string;
24
+ blocksAutomatic: boolean;
25
+ evidence: Record<string, unknown>;
26
+ }
27
+
28
+ export function parseImplementationHint(value: string): ImplementationHint {
29
+ const hint: Partial<ImplementationHint> = {};
30
+ for (const part of value.split(',')) {
31
+ const separator = part.indexOf('=');
32
+ if (separator <= 0 || separator === part.length - 1) throw new Error(`Invalid implementation hint field: ${part}`);
33
+ assignHintField(hint, part.slice(0, separator).trim(), part.slice(separator + 1).trim());
34
+ }
35
+ if (!hint.implementationRepo) throw new Error('Scoped implementation hint requires an implementation repo selection');
36
+ return { ...hint, implementationRepo: hint.implementationRepo };
37
+ }
38
+
39
+ export function selectImplementation(
40
+ rawEvidence: Record<string, unknown>,
41
+ hints: ImplementationHint[] | undefined,
42
+ legacyRepo: string | undefined,
43
+ ): ImplementationSelection {
44
+ const evidence = asEvidence(rawEvidence);
45
+ const scoped = hints ?? [];
46
+ const matchingHints = scoped.filter((hint) => hintMatchesEdge(hint, evidence));
47
+ if (matchingHints.length === 0) {
48
+ if (legacyRepo) return selectCandidate(evidence, legacyHint(legacyRepo), 'implementation_repo_hint');
49
+ const reason = scoped.length > 0 ? 'no_scoped_hint_matched_edge' : 'no_implementation_hint_supplied';
50
+ return { blocksAutomatic: false, evidence: { status: 'not_matched', reason, strategy: 'scoped_implementation_hint' } };
51
+ }
52
+ if (matchingHints.length > 1) {
53
+ return {
54
+ blocksAutomatic: true,
55
+ evidence: {
56
+ status: 'tied',
57
+ reason: 'multiple_scoped_hints_matched_edge',
58
+ strategy: 'scoped_implementation_hint',
59
+ matchedHints: matchingHints,
60
+ candidateCount: matchingHints.length,
61
+ },
62
+ };
63
+ }
64
+ const hint = matchingHints[0];
65
+ return hint ? selectCandidate(evidence, hint, 'scoped_implementation_hint') : { blocksAutomatic: false, evidence: { status: 'not_matched' } };
66
+ }
67
+
68
+ export function implementationHintDiagnostic(selection: ImplementationSelection): Record<string, unknown> | undefined {
69
+ if (!selection.blocksAutomatic || selection.methodId) return undefined;
70
+ return {
71
+ severity: 'warning',
72
+ code: 'implementation_hint_mismatch',
73
+ message: 'Implementation hint did not select exactly one viable candidate',
74
+ hintStatus: selection.evidence.status,
75
+ candidateCount: selection.evidence.candidateCount,
76
+ implementationSelection: selection.evidence,
77
+ };
78
+ }
79
+
80
+ function assignHintField(hint: Partial<ImplementationHint>, key: string, value: string): void {
81
+ if (key === 'service' || key === 'servicePath') hint.servicePath = value;
82
+ else if (key === 'operation' || key === 'operationPath') hint.operationPath = value;
83
+ else if (key === 'package' || key === 'packageName') hint.packageName = value;
84
+ else if (key === 'repository' || key === 'repositoryName') hint.repositoryName = value;
85
+ else if (key === 'family' || key === 'candidateFamily') hint.candidateFamily = value;
86
+ else if (key === 'repo' || key === 'implementationRepo' || key === 'select') hint.implementationRepo = value;
87
+ else throw new Error(`Unknown implementation hint field: ${key}`);
88
+ }
89
+
90
+ function selectCandidate(evidence: EdgeEvidence, hint: ImplementationHint, strategy: string): ImplementationSelection {
91
+ const matches = (evidence.candidates ?? []).filter((candidate) =>
92
+ candidate.accepted && candidateMatchesRepo(candidate, hint.implementationRepo));
93
+ const selected = matches.length === 1 ? matches[0] : undefined;
94
+ if (!selected || selected.methodId === undefined) {
95
+ return {
96
+ blocksAutomatic: true,
97
+ evidence: {
98
+ status: matches.length > 1 ? 'tied' : 'not_matched',
99
+ reason: matches.length > 1 ? 'hint_matched_multiple_candidates' : 'hint_matched_zero_candidates',
100
+ strategy,
101
+ matchedHint: hint,
102
+ selectedRepo: hint.implementationRepo,
103
+ candidateCount: matches.length,
104
+ },
105
+ };
106
+ }
107
+ return {
108
+ methodId: String(selected.methodId),
109
+ blocksAutomatic: false,
110
+ evidence: {
111
+ status: 'selected',
112
+ guided: true,
113
+ strategy,
114
+ matchedHint: hint,
115
+ selectedRepo: hint.implementationRepo,
116
+ selectedMethodId: selected.methodId,
117
+ ambiguityReason: evidence.ambiguityReasons?.[0],
118
+ },
119
+ };
120
+ }
121
+
122
+ function hintMatchesEdge(hint: ImplementationHint, evidence: EdgeEvidence): boolean {
123
+ const model = evidence.modelPackage ?? evidence.candidates?.[0]?.modelPackage;
124
+ const familyNames = new Set([
125
+ ...(evidence.candidateFamilies ?? []).flatMap((family) => family.packageName ? [family.packageName] : []),
126
+ ...(evidence.candidates ?? []).flatMap((candidate) => candidate.handlerPackage?.packageName ? [candidate.handlerPackage.packageName] : []),
127
+ ]);
128
+ return matches(hint.servicePath, evidence.servicePath ?? evidence.candidates?.[0]?.servicePath)
129
+ && matches(hint.operationPath, evidence.operationPath ?? evidence.candidates?.[0]?.operationPath)
130
+ && matches(hint.packageName, model?.packageName)
131
+ && matches(hint.repositoryName, model?.name)
132
+ && (!hint.candidateFamily || familyNames.has(hint.candidateFamily));
133
+ }
134
+
135
+ function candidateMatchesRepo(candidate: Candidate, value: string): boolean {
136
+ return candidate.handlerPackage?.name === value
137
+ || candidate.handlerPackage?.packageName === value
138
+ || candidate.sourceFile?.startsWith(value) === true;
139
+ }
140
+
141
+ function matches(expected: string | undefined, actual: string | undefined): boolean {
142
+ return expected === undefined || expected === actual;
143
+ }
144
+
145
+ function legacyHint(implementationRepo: string): ImplementationHint {
146
+ return { implementationRepo };
147
+ }
148
+
149
+ function asEvidence(value: Record<string, unknown>): EdgeEvidence {
150
+ return value as EdgeEvidence;
151
+ }