@saptools/service-flow 0.1.49 → 0.1.51

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.
@@ -2,6 +2,8 @@ import type { Db } from '../db/connection.js';
2
2
  import { extractPlaceholders, substituteVariables, type RuntimeSubstitution } from '../linker/dynamic-edge-resolver.js';
3
3
  import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
4
4
  import { resolveOperation, type OperationTarget } from '../linker/service-resolver.js';
5
+ import type { DynamicMode } from '../types.js';
6
+ import { analyzeDynamicTargetCandidates, type DynamicTargetAnalysis, type DynamicTargetCandidate } from './dynamic-targets.js';
5
7
 
6
8
  export interface TraceGraphRow extends Record<string, unknown> {
7
9
  id: number;
@@ -50,27 +52,49 @@ export function runtimeResolution(
50
52
  db: Db,
51
53
  row: TraceGraphRow,
52
54
  evidence: Record<string, unknown>,
53
- vars: Record<string, string> | undefined,
55
+ options: { vars?: Record<string, string>; dynamicMode?: DynamicMode; maxDynamicCandidates?: number },
54
56
  workspaceId: number | undefined,
55
57
  contextualUnresolvedReason?: string,
56
58
  ): { row: TraceGraphRow; evidence: Record<string, unknown>; target?: OperationTarget; unresolvedReason?: string } {
57
- const substituted = evidenceWithRuntimeVariables(evidence, vars);
58
- if (!isRemoteRuntimeCandidate(row, evidence, vars)) {
59
+ const substituted = evidenceWithRuntimeVariables(evidence, options.vars);
60
+ const dynamicMode = options.dynamicMode ?? 'strict';
61
+ const analysis = analyzeDynamicTargetCandidates(
62
+ db,
63
+ substituted,
64
+ workspaceId,
65
+ dynamicMode,
66
+ positiveCandidateCap(options.maxDynamicCandidates),
67
+ );
68
+ const enriched = analysis ? evidenceWithDynamicAnalysis(substituted, analysis) : substituted;
69
+ if (dynamicMode === 'infer') {
70
+ const inferred = inferredTarget(analysis);
71
+ if (inferred) {
72
+ const resolvedRow = { ...row, status: 'resolved', to_kind: 'operation', to_id: String(inferred.operationId), unresolved_reason: undefined, confidence: inferred.score };
73
+ const resolution = { status: 'resolved' as const, target: inferred, candidates: [], reasons: inferred.reasons };
74
+ return { row: resolvedRow, evidence: withEffectiveResolution(enriched, resolvedRow, undefined, resolution), target: inferred };
75
+ }
76
+ }
77
+ if (!isRemoteRuntimeCandidate(row, evidence, options.vars)) {
59
78
  const unresolvedReason = contextualUnresolvedReason ?? row.unresolved_reason;
60
- const withSections = withEffectiveResolution(substituted, row, unresolvedReason);
79
+ const withSections = withEffectiveResolution(enriched, row, unresolvedReason);
61
80
  return { row, evidence: withSections, unresolvedReason };
62
81
  }
63
- const resolution = resolveRuntimeOperation(db, substituted, workspaceId);
82
+ const resolution = resolveRuntimeOperation(db, enriched, workspaceId);
64
83
  if (resolution.target) {
65
84
  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)) };
66
- return { row: resolvedRow, evidence: withEffectiveResolution(substituted, resolvedRow, undefined, resolution), target: resolution.target };
85
+ return { row: resolvedRow, evidence: withEffectiveResolution(enriched, resolvedRow, undefined, resolution), target: resolution.target };
67
86
  }
68
87
  const unresolvedReason = runtimeUnresolvedReason(resolution);
69
- return { row, evidence: withEffectiveResolution(substituted, row, unresolvedReason, resolution), unresolvedReason };
88
+ return { row, evidence: withEffectiveResolution(enriched, row, unresolvedReason, resolution), unresolvedReason };
70
89
  }
71
90
 
72
91
  export function runtimeVariableDiagnostic(edges: Array<{ evidence: Record<string, unknown> }>): Record<string, unknown> | undefined {
73
92
  const missing = new Set<string>();
93
+ let candidateCount = 0;
94
+ let shownCandidateCount = 0;
95
+ let omittedCandidateCount = 0;
96
+ const candidateSuggestions: Record<string, unknown>[] = [];
97
+ const suggestedVarSets: Record<string, unknown>[] = [];
74
98
  for (const edge of edges) {
75
99
  const effective = parseObject(edge.evidence.effectiveResolution);
76
100
  if (!['dynamic', 'unresolved', 'ambiguous'].includes(String(effective.status ?? '')))
@@ -79,6 +103,12 @@ export function runtimeVariableDiagnostic(edges: Array<{ evidence: Record<string
79
103
  if (!substitutions || typeof substitutions !== 'object' || Array.isArray(substitutions)) continue;
80
104
  for (const value of Object.values(substitutions as Record<string, RuntimeSubstitution>))
81
105
  for (const key of value.missing ?? []) missing.add(key);
106
+ const exploration = parseObject(edge.evidence.dynamicTargetExploration);
107
+ candidateCount += numeric(exploration.candidateCount);
108
+ shownCandidateCount += numeric(exploration.shownCandidateCount);
109
+ omittedCandidateCount += numeric(exploration.omittedCandidateCount);
110
+ candidateSuggestions.push(...recordArray(edge.evidence.dynamicTargetCandidateSuggestions));
111
+ suggestedVarSets.push(...recordArray(exploration.suggestedVarSets));
82
112
  }
83
113
  const missingVariables = [...missing].sort();
84
114
  if (missingVariables.length === 0) return undefined;
@@ -88,6 +118,16 @@ export function runtimeVariableDiagnostic(edges: Array<{ evidence: Record<string
88
118
  message: `Runtime variables are required to resolve dynamic trace targets: ${missingVariables.join(', ')}`,
89
119
  missingVariables,
90
120
  suggestions: missingVariables.map((key) => `--var ${key}=<value>`),
121
+ candidateCount,
122
+ shownCandidateCount,
123
+ omittedCandidateCount,
124
+ candidateSuggestions: candidateSuggestions.slice(0, 5),
125
+ suggestedVarSets: uniqueCliRows(suggestedVarSets).slice(0, 5),
126
+ copyableExamples: [
127
+ ...uniqueCliRows(suggestedVarSets).slice(0, 3).flatMap((row) =>
128
+ typeof row.cli === 'string' ? [row.cli] : []),
129
+ ...(candidateCount > 0 ? ['--dynamic-mode candidates --max-dynamic-candidates 20'] : []),
130
+ ],
91
131
  };
92
132
  }
93
133
 
@@ -180,6 +220,26 @@ function evidenceWithRuntimeVariables(evidence: Record<string, unknown>, vars: R
180
220
  return next;
181
221
  }
182
222
 
223
+ function evidenceWithDynamicAnalysis(
224
+ evidence: Record<string, unknown>,
225
+ analysis: DynamicTargetAnalysis,
226
+ ): Record<string, unknown> {
227
+ return {
228
+ ...evidence,
229
+ dynamicTargetExploration: {
230
+ mode: analysis.mode,
231
+ missingVariables: analysis.missingVariables,
232
+ candidateCount: analysis.candidateCount,
233
+ shownCandidateCount: analysis.shownCandidateCount,
234
+ omittedCandidateCount: analysis.omittedCandidateCount,
235
+ suggestedVarSets: analysis.suggestedVarSets,
236
+ },
237
+ dynamicTargetCandidates: analysis.candidates,
238
+ dynamicTargetCandidateSuggestions: analysis.shownCandidates,
239
+ dynamicTargetInference: analysis.inference,
240
+ };
241
+ }
242
+
183
243
  function runtimeSubstitutions(evidence: Record<string, unknown>, vars: Record<string, string>): Record<string, RuntimeSubstitution> {
184
244
  const substitutions: Record<string, RuntimeSubstitution> = {};
185
245
  for (const key of ['servicePath', 'operationPath', 'serviceAliasExpr', 'serviceAlias', 'destination']) {
@@ -206,6 +266,31 @@ function isRemoteRuntimeCandidate(row: TraceGraphRow, evidence: Record<string, u
206
266
  return ['servicePath', 'operationPath', 'serviceAliasExpr', 'serviceAlias', 'destination'].some((key) => hasRuntimeVariable(evidence[key], vars));
207
267
  }
208
268
 
269
+ function inferredTarget(analysis: DynamicTargetAnalysis | undefined): OperationTarget | undefined {
270
+ if (analysis?.inference.status !== 'resolved') return undefined;
271
+ const id = Number(analysis.inference.candidateOperationId);
272
+ const candidate = analysis.candidates.find((item) => item.candidateOperationId === id);
273
+ if (!candidate) return undefined;
274
+ return targetFromCandidate(candidate);
275
+ }
276
+
277
+ function targetFromCandidate(candidate: DynamicTargetCandidate): OperationTarget {
278
+ return {
279
+ operationId: candidate.candidateOperationId,
280
+ repoName: candidate.repoName,
281
+ serviceName: '',
282
+ qualifiedName: '',
283
+ servicePath: candidate.servicePath,
284
+ operationPath: candidate.operationPath,
285
+ operationName: candidate.operationName,
286
+ packageName: candidate.packageName,
287
+ score: candidate.score,
288
+ reasons: candidate.reasons,
289
+ sourceFile: '',
290
+ sourceLine: 0,
291
+ };
292
+ }
293
+
209
294
  function hasRuntimeVariable(value: unknown, vars: Record<string, string>): boolean {
210
295
  return typeof value === 'string' && extractPlaceholders(value).some((key) => Object.hasOwn(vars, key));
211
296
  }
@@ -223,3 +308,28 @@ function parseObject(value: unknown): Record<string, unknown> {
223
308
  function stringValue(value: unknown): string | undefined {
224
309
  return typeof value === 'string' ? value : undefined;
225
310
  }
311
+
312
+ function numeric(value: unknown): number {
313
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
314
+ }
315
+
316
+ function recordArray(value: unknown): Record<string, unknown>[] {
317
+ return Array.isArray(value)
318
+ ? value.filter((item): item is Record<string, unknown> =>
319
+ Boolean(item && typeof item === 'object' && !Array.isArray(item)))
320
+ : [];
321
+ }
322
+
323
+ function uniqueCliRows(rows: Record<string, unknown>[]): Record<string, unknown>[] {
324
+ const seen = new Set<string>();
325
+ return rows.filter((row) => {
326
+ const cli = typeof row.cli === 'string' ? row.cli : JSON.stringify(row);
327
+ if (seen.has(cli)) return false;
328
+ seen.add(cli);
329
+ return true;
330
+ });
331
+ }
332
+
333
+ function positiveCandidateCap(value: number | undefined): number {
334
+ return Number.isFinite(value) && Number(value) > 0 ? Math.floor(Number(value)) : 5;
335
+ }
@@ -18,3 +18,39 @@ export function startLabel(start: TraceStart): string {
18
18
  .filter(Boolean)
19
19
  .join(' ');
20
20
  }
21
+ export function ambiguousStartDiagnostic(
22
+ requested: string,
23
+ candidates: Array<Record<string, unknown>>,
24
+ message: string,
25
+ ): Record<string, unknown> {
26
+ const serviceSuggestions = [...new Set(candidates
27
+ .flatMap((row) => typeof row.servicePath === 'string'
28
+ ? [`--service ${row.servicePath}`]
29
+ : []))].sort();
30
+ return {
31
+ severity: 'warning',
32
+ code: 'trace_start_ambiguous',
33
+ message,
34
+ normalizedSelectorValue: requested,
35
+ resolutionStage: 'operation',
36
+ resolutionStatus: 'ambiguous_operation',
37
+ candidates,
38
+ serviceSuggestions,
39
+ selectorSuggestions: fullSelectorSuggestions(candidates),
40
+ };
41
+ }
42
+ function fullSelectorSuggestions(
43
+ candidates: Array<Record<string, unknown>>,
44
+ ): string[] {
45
+ const includeRepo = new Set(candidates.map((row) => row.repoName)).size > 1;
46
+ return [...new Set(candidates.flatMap((row) => {
47
+ if (typeof row.servicePath !== 'string'
48
+ || typeof row.operationPath !== 'string') return [];
49
+ const repoSelector = includeRepo && typeof row.repoName === 'string'
50
+ ? `--repo ${row.repoName} `
51
+ : '';
52
+ return [
53
+ `${repoSelector}--service ${row.servicePath} --path ${row.operationPath}`,
54
+ ];
55
+ }))].sort();
56
+ }
@@ -2,9 +2,11 @@ import type { Db } from '../db/connection.js';
2
2
  import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
3
3
  import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
4
4
  import { resolveOperation } from '../linker/service-resolver.js';
5
- import type { ImplementationHint, TraceEdge, TraceResult, TraceStart } from '../types.js';
5
+ import type { ImplementationHint, TraceEdge, TraceOptions, TraceResult, TraceStart } from '../types.js';
6
6
  import { baseTraceEvidence, edgeTarget, runtimeResolution, runtimeVariableDiagnostic, type TraceGraphRow } from './evidence.js';
7
+ import { dynamicCandidateBranches } from './dynamic-branches.js';
7
8
  import { implementationHintDiagnostic, selectImplementation, type ImplementationSelection } from './implementation-hints.js';
9
+ import { ambiguousStartDiagnostic } from './selectors.js';
8
10
  interface RepoRef {
9
11
  id: number;
10
12
  name: string;
@@ -120,26 +122,6 @@ function implementationRejectionReasons(evidence: Record<string, unknown>): stri
120
122
  : []);
121
123
  return [...new Set(reasons)].sort();
122
124
  }
123
- function ambiguousStartDiagnostic(
124
- requested: string,
125
- candidates: Array<Record<string, unknown>>,
126
- message: string,
127
- ): Record<string, unknown> {
128
- const serviceSuggestions = [...new Set(candidates
129
- .flatMap((row) => typeof row.servicePath === 'string'
130
- ? [`--service ${row.servicePath}`]
131
- : []))].sort();
132
- return {
133
- severity: 'warning',
134
- code: 'trace_start_ambiguous',
135
- message,
136
- normalizedSelectorValue: requested,
137
- resolutionStage: 'operation',
138
- resolutionStatus: 'ambiguous_operation',
139
- candidates,
140
- serviceSuggestions,
141
- };
142
- }
143
125
  function sourceFilesForStart(
144
126
  db: Db,
145
127
  repoId: number | undefined,
@@ -326,6 +308,7 @@ function operationNode(db: Db, operationId: string): Record<string, unknown> | u
326
308
  if (!row) return undefined;
327
309
  return { id: `operation:${operationId}`, kind: 'operation', label: `${String(row.repoName)}:${String(row.servicePath)}${String(row.operationPath)}`, ...row };
328
310
  }
311
+
329
312
  function workspaceIdForCall(db: Db, callId: string): number | undefined {
330
313
  return (db.prepare('SELECT r.workspace_id workspaceId FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE c.id=?').get(callId) as { workspaceId?: number } | undefined)?.workspaceId;
331
314
  }
@@ -505,15 +488,7 @@ function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBind
505
488
  export function trace(
506
489
  db: Db,
507
490
  start: TraceStart,
508
- options: {
509
- depth: number;
510
- vars?: Record<string, string>;
511
- includeExternal?: boolean;
512
- includeDb?: boolean;
513
- includeAsync?: boolean;
514
- implementationRepo?: string;
515
- implementationHints?: ImplementationHint[];
516
- },
491
+ options: TraceOptions,
517
492
  ): TraceResult {
518
493
  const hintOptions = { implementationRepo: options.implementationRepo, implementationHints: options.implementationHints };
519
494
  const scope = startScope(db, start, hintOptions);
@@ -614,7 +589,11 @@ export function trace(
614
589
  String(row.evidence_json || '{}'),
615
590
  ) as Record<string, unknown>;
616
591
  const rawEvidence = baseTraceEvidence(row as TraceGraphRow, call, persistedEvidence, contextual.evidence);
617
- const effective = runtimeResolution(db, row as TraceGraphRow, rawEvidence, options.vars, workspaceIdForCall(db, String(call.id)), contextual.unresolvedReason);
592
+ const effective = runtimeResolution(db, row as TraceGraphRow, rawEvidence, {
593
+ vars: options.vars,
594
+ dynamicMode: options.dynamicMode ?? 'strict',
595
+ maxDynamicCandidates: options.maxDynamicCandidates,
596
+ }, workspaceIdForCall(db, String(call.id)), contextual.unresolvedReason);
618
597
  const evidence = effective.evidence;
619
598
  const effectiveRow = effective.row;
620
599
  const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
@@ -634,6 +613,8 @@ export function trace(
634
613
  confidence: Number(effectiveRow.confidence ?? call.confidence),
635
614
  unresolvedReason: effective.unresolvedReason,
636
615
  });
616
+ if ((options.dynamicMode ?? 'strict') === 'candidates')
617
+ edges.push(...dynamicCandidateBranches(current.depth, call, evidence));
637
618
  if (effectiveRow.to_kind === 'operation') {
638
619
  const implementation = implementationScope(db, effectiveRow.to_id);
639
620
  const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence, hintOptions);
package/src/types.ts CHANGED
@@ -101,6 +101,18 @@ export interface HandlerMethodFact {
101
101
  decoratorKind: string;
102
102
  decoratorValue?: string;
103
103
  decoratorRawExpression: string;
104
+ decoratorResolution: {
105
+ rawExpression: string;
106
+ resolvedValue?: string;
107
+ resolutionKind:
108
+ | 'literal'
109
+ | 'const_identifier'
110
+ | 'enum_member'
111
+ | 'const_object_property'
112
+ | 'generated_constant_name'
113
+ | 'unresolved';
114
+ unresolvedReason?: string;
115
+ };
104
116
  sourceFile: string;
105
117
  sourceLine: number;
106
118
  }
@@ -187,6 +199,18 @@ export interface ImplementationHint {
187
199
  candidateFamily?: string;
188
200
  implementationRepo: string;
189
201
  }
202
+ export type DynamicMode = 'strict' | 'candidates' | 'infer';
203
+ export interface TraceOptions {
204
+ depth: number;
205
+ vars?: Record<string, string>;
206
+ includeExternal?: boolean;
207
+ includeDb?: boolean;
208
+ includeAsync?: boolean;
209
+ implementationRepo?: string;
210
+ implementationHints?: ImplementationHint[];
211
+ dynamicMode?: DynamicMode;
212
+ maxDynamicCandidates?: number;
213
+ }
190
214
  export interface TraceEdge {
191
215
  step: number;
192
216
  type: string;