@saptools/service-flow 0.1.50 → 0.1.52

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 (43) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +16 -3
  3. package/TECHNICAL-NOTE.md +10 -1
  4. package/dist/{chunk-52OUS3MO.js → chunk-PTLDSHRC.js} +2663 -363
  5. package/dist/chunk-PTLDSHRC.js.map +1 -0
  6. package/dist/cli.js +318 -510
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +59 -17
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/000-clean.ts +82 -0
  12. package/src/cli.ts +97 -57
  13. package/src/db/connection.ts +1 -1
  14. package/src/db/migrations.ts +5 -3
  15. package/src/db/repositories.ts +120 -22
  16. package/src/db/schema.ts +7 -2
  17. package/src/index.ts +1 -1
  18. package/src/indexer/repository-indexer.ts +57 -29
  19. package/src/indexer/workspace-indexer.ts +84 -6
  20. package/src/linker/cross-repo-linker.ts +22 -2
  21. package/src/linker/service-resolver.ts +15 -4
  22. package/src/output/table-output.ts +56 -3
  23. package/src/parsers/cds-parser.ts +8 -2
  24. package/src/parsers/decorator-parser.ts +382 -48
  25. package/src/parsers/handler-registration-parser.ts +38 -21
  26. package/src/parsers/imported-wrapper-parser.ts +18 -5
  27. package/src/parsers/outbound-call-parser.ts +25 -8
  28. package/src/parsers/package-json-parser.ts +36 -11
  29. package/src/parsers/service-binding-parser-helpers.ts +8 -1
  30. package/src/parsers/service-binding-parser.ts +6 -3
  31. package/src/parsers/symbol-parser.ts +13 -3
  32. package/src/parsers/ts-project.ts +54 -0
  33. package/src/trace/000-dynamic-target-types.ts +84 -0
  34. package/src/trace/001-dynamic-identity.ts +280 -0
  35. package/src/trace/002-trace-diagnostics.ts +54 -0
  36. package/src/trace/003-dynamic-references.ts +82 -0
  37. package/src/trace/dynamic-branches.ts +45 -0
  38. package/src/trace/dynamic-targets.ts +654 -0
  39. package/src/trace/evidence.ts +391 -22
  40. package/src/trace/selectors.ts +483 -0
  41. package/src/trace/trace-engine.ts +101 -94
  42. package/src/types.ts +39 -1
  43. package/dist/chunk-52OUS3MO.js.map +0 -1
@@ -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;
@@ -22,6 +24,16 @@ interface Candidate {
22
24
  operationName?: string;
23
25
  score?: number;
24
26
  }
27
+ interface RuntimeDiagnosticTotals {
28
+ missing: Set<string>;
29
+ candidateCount: number;
30
+ viableCandidateCount: number;
31
+ rejectedCandidateCount: number;
32
+ maxCandidates: number;
33
+ candidateSuggestions: Record<string, unknown>[];
34
+ rejectedCandidates: Record<string, unknown>[];
35
+ suggestedVarSets: Record<string, unknown>[];
36
+ }
25
37
 
26
38
  export function baseTraceEvidence(
27
39
  row: TraceGraphRow,
@@ -36,6 +48,8 @@ export function baseTraceEvidence(
36
48
  persistedGraphEdgeId: row.id > 0 ? row.id : undefined,
37
49
  outboundCallId: call.id,
38
50
  callSite: { sourceFile: call.source_file, sourceLine: call.source_line },
51
+ callType: call.call_type,
52
+ repoId: call.repo_id,
39
53
  sourceFile: call.source_file,
40
54
  sourceLine: call.source_line,
41
55
  file: call.source_file,
@@ -50,27 +64,142 @@ export function runtimeResolution(
50
64
  db: Db,
51
65
  row: TraceGraphRow,
52
66
  evidence: Record<string, unknown>,
53
- vars: Record<string, string> | undefined,
67
+ options: { vars?: Record<string, string>; dynamicMode?: DynamicMode; maxDynamicCandidates?: number },
54
68
  workspaceId: number | undefined,
55
69
  contextualUnresolvedReason?: string,
56
70
  ): { row: TraceGraphRow; evidence: Record<string, unknown>; target?: OperationTarget; unresolvedReason?: string } {
57
- const substituted = evidenceWithRuntimeVariables(evidence, vars);
58
- if (!isRemoteRuntimeCandidate(row, evidence, vars)) {
71
+ const dynamicMode = options.dynamicMode ?? 'strict';
72
+ const candidateCap = positiveCandidateCap(options.maxDynamicCandidates);
73
+ if (!isDynamicRemoteOperationEdge(row, evidence))
74
+ return unchangedRuntimeResolution(
75
+ row,
76
+ boundDynamicEvidence(evidence, candidateCap),
77
+ contextualUnresolvedReason,
78
+ );
79
+ const substituted = evidenceWithRuntimeVariables(evidence, options.vars);
80
+ const analysis = analyzeDynamicTargetCandidates(
81
+ db, substituted, workspaceId, dynamicMode, candidateCap,
82
+ );
83
+ const enriched = boundDynamicEvidence(
84
+ analysis ? evidenceWithDynamicAnalysis(substituted, analysis) : substituted,
85
+ candidateCap,
86
+ );
87
+ if (analysis && analysis.candidateCount > 0
88
+ && analysis.viableCandidateCount === 0
89
+ && Object.keys(analysis.appliedSuppliedVariables).length > 0)
90
+ return noCandidateRuntimeResolution(row, enriched);
91
+ if (dynamicMode === 'infer') {
92
+ const inferred = inferredTarget(analysis);
93
+ if (inferred)
94
+ return resolvedRuntimeResolution(row, enriched, inferred, inferred.reasons);
95
+ }
96
+ if (!hasApplicableRuntimeVariables(evidence, options.vars)) {
59
97
  const unresolvedReason = contextualUnresolvedReason ?? row.unresolved_reason;
60
- const withSections = withEffectiveResolution(substituted, row, unresolvedReason);
98
+ const withSections = withEffectiveResolution(enriched, row, unresolvedReason);
61
99
  return { row, evidence: withSections, unresolvedReason };
62
100
  }
63
- const resolution = resolveRuntimeOperation(db, substituted, workspaceId);
64
- if (resolution.target) {
65
- 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 };
67
- }
101
+ const resolution = resolveRuntimeOperation(db, enriched, workspaceId);
102
+ if (resolution.target)
103
+ return resolvedRuntimeResolution(
104
+ row, enriched, resolution.target, resolution.reasons,
105
+ );
106
+ if (analysis && analysis.viableCandidateCount === 0
107
+ && Object.keys(analysis.appliedSuppliedVariables).length > 0)
108
+ return noCandidateRuntimeResolution(row, enriched);
68
109
  const unresolvedReason = runtimeUnresolvedReason(resolution);
69
- return { row, evidence: withEffectiveResolution(substituted, row, unresolvedReason, resolution), unresolvedReason };
110
+ return { row, evidence: withEffectiveResolution(enriched, row, unresolvedReason, resolution), unresolvedReason };
111
+ }
112
+
113
+ function unchangedRuntimeResolution(
114
+ row: TraceGraphRow,
115
+ evidence: Record<string, unknown>,
116
+ contextualUnresolvedReason: string | undefined,
117
+ ): ReturnType<typeof runtimeResolution> {
118
+ const unresolvedReason = contextualUnresolvedReason ?? row.unresolved_reason;
119
+ return {
120
+ row,
121
+ evidence: withEffectiveResolution(evidence, row, unresolvedReason),
122
+ unresolvedReason,
123
+ };
124
+ }
125
+
126
+ function noCandidateRuntimeResolution(
127
+ row: TraceGraphRow,
128
+ evidence: Record<string, unknown>,
129
+ ): ReturnType<typeof runtimeResolution> {
130
+ const unresolvedReason = 'No candidate remained after runtime substitution';
131
+ return {
132
+ row,
133
+ evidence: withEffectiveResolution(evidence, row, unresolvedReason),
134
+ unresolvedReason,
135
+ };
136
+ }
137
+
138
+ function resolvedRuntimeResolution(
139
+ row: TraceGraphRow,
140
+ evidence: Record<string, unknown>,
141
+ target: OperationTarget,
142
+ reasons: string[],
143
+ ): ReturnType<typeof runtimeResolution> {
144
+ const resolvedRow = {
145
+ ...row,
146
+ status: 'resolved',
147
+ to_kind: 'operation',
148
+ to_id: String(target.operationId),
149
+ unresolved_reason: undefined,
150
+ confidence: Math.max(0, Math.min(1, target.score)),
151
+ };
152
+ const resolution = {
153
+ status: 'resolved' as const,
154
+ target,
155
+ candidates: [],
156
+ reasons,
157
+ };
158
+ return {
159
+ row: resolvedRow,
160
+ evidence: withEffectiveResolution(evidence, resolvedRow, undefined, resolution),
161
+ target,
162
+ };
70
163
  }
71
164
 
72
165
  export function runtimeVariableDiagnostic(edges: Array<{ evidence: Record<string, unknown> }>): Record<string, unknown> | undefined {
166
+ const totals = runtimeDiagnosticTotals(edges);
167
+ const missingVariables = [...totals.missing].sort();
168
+ if (missingVariables.length === 0) return undefined;
169
+ const shownSuggestions = totals.candidateSuggestions.slice(0, totals.maxCandidates);
170
+ const shownRejected = totals.rejectedCandidates.slice(0, totals.maxCandidates);
171
+ const shownCandidateCount = shownSuggestions.length;
172
+ return {
173
+ severity: 'warning',
174
+ code: 'trace_runtime_variables_missing',
175
+ message: `Runtime variables are required to resolve dynamic trace targets: ${missingVariables.join(', ')}`,
176
+ missingVariables,
177
+ suggestions: missingVariables.map((key) => `--var ${key}=<value>`),
178
+ candidateCount: totals.candidateCount,
179
+ viableCandidateCount: totals.viableCandidateCount,
180
+ rejectedCandidateCount: totals.rejectedCandidateCount,
181
+ shownCandidateCount,
182
+ omittedCandidateCount: Math.max(0, totals.viableCandidateCount - shownCandidateCount),
183
+ maxDynamicCandidates: totals.maxCandidates,
184
+ shownRejectedCandidateCount: shownRejected.length,
185
+ omittedRejectedCandidateCount: Math.max(0, totals.rejectedCandidateCount - shownRejected.length),
186
+ candidateSuggestions: shownSuggestions,
187
+ rejectedCandidates: shownRejected,
188
+ suggestedVarSets: uniqueCliRows(totals.suggestedVarSets).slice(0, totals.maxCandidates),
189
+ copyableExamples: copyableExamples(totals.suggestedVarSets, totals.candidateCount, totals.maxCandidates),
190
+ };
191
+ }
192
+
193
+ function runtimeDiagnosticTotals(
194
+ edges: Array<{ evidence: Record<string, unknown> }>): RuntimeDiagnosticTotals {
73
195
  const missing = new Set<string>();
196
+ let candidateCount = 0;
197
+ let viableCandidateCount = 0;
198
+ let rejectedCandidateCount = 0;
199
+ let maxCandidates = 5;
200
+ const candidateSuggestions: Record<string, unknown>[] = [];
201
+ const rejectedCandidates: Record<string, unknown>[] = [];
202
+ const suggestedVarSets: Record<string, unknown>[] = [];
74
203
  for (const edge of edges) {
75
204
  const effective = parseObject(edge.evidence.effectiveResolution);
76
205
  if (!['dynamic', 'unresolved', 'ambiguous'].includes(String(effective.status ?? '')))
@@ -79,18 +208,80 @@ export function runtimeVariableDiagnostic(edges: Array<{ evidence: Record<string
79
208
  if (!substitutions || typeof substitutions !== 'object' || Array.isArray(substitutions)) continue;
80
209
  for (const value of Object.values(substitutions as Record<string, RuntimeSubstitution>))
81
210
  for (const key of value.missing ?? []) missing.add(key);
211
+ const exploration = parseObject(edge.evidence.dynamicTargetExploration);
212
+ maxCandidates = positiveCandidateCap(numeric(exploration.maxCandidates) || maxCandidates);
213
+ candidateCount += numeric(exploration.candidateCount);
214
+ viableCandidateCount += numeric(exploration.viableCandidateCount);
215
+ rejectedCandidateCount += numeric(exploration.rejectedCandidateCount);
216
+ appendBounded(
217
+ candidateSuggestions,
218
+ recordArray(edge.evidence.dynamicTargetCandidateSuggestions),
219
+ maxCandidates,
220
+ );
221
+ appendBounded(
222
+ rejectedCandidates,
223
+ recordArray(exploration.rejectedCandidates),
224
+ maxCandidates,
225
+ );
226
+ appendBounded(
227
+ suggestedVarSets,
228
+ recordArray(exploration.suggestedVarSets),
229
+ maxCandidates,
230
+ );
82
231
  }
83
- const missingVariables = [...missing].sort();
84
- if (missingVariables.length === 0) return undefined;
85
232
  return {
86
- severity: 'warning',
87
- code: 'trace_runtime_variables_missing',
88
- message: `Runtime variables are required to resolve dynamic trace targets: ${missingVariables.join(', ')}`,
89
- missingVariables,
90
- suggestions: missingVariables.map((key) => `--var ${key}=<value>`),
233
+ missing,
234
+ candidateCount,
235
+ viableCandidateCount,
236
+ rejectedCandidateCount,
237
+ maxCandidates,
238
+ candidateSuggestions,
239
+ rejectedCandidates,
240
+ suggestedVarSets,
91
241
  };
92
242
  }
93
243
 
244
+ export function runtimeNoCandidateDiagnostics(
245
+ edges: Array<{ evidence: Record<string, unknown> }>,
246
+ ): Array<Record<string, unknown>> {
247
+ const seen = new Set<string>();
248
+ return edges.flatMap((edge) => {
249
+ const exploration = parseObject(edge.evidence.dynamicTargetExploration);
250
+ const suppliedVariables = parseObject(exploration.suppliedVariables);
251
+ const appliedSuppliedVariables = parseObject(
252
+ exploration.appliedSuppliedVariables,
253
+ );
254
+ if (numeric(exploration.viableCandidateCount) !== 0
255
+ || Object.keys(appliedSuppliedVariables).length === 0) return [];
256
+ const callSite = parseObject(edge.evidence.callSite);
257
+ const key = JSON.stringify([callSite, suppliedVariables]);
258
+ if (seen.has(key)) return [];
259
+ seen.add(key);
260
+ const maxCandidates = positiveCandidateCap(numeric(exploration.maxCandidates));
261
+ return [{
262
+ severity: 'warning',
263
+ code: 'no_candidate_after_runtime_substitution',
264
+ message: 'No dynamic target candidate remained after applying runtime variables',
265
+ suppliedVariables,
266
+ appliedSuppliedVariables,
267
+ substitutedSignals: parseObject(exploration.substitutedSignals),
268
+ candidateCount: numeric(exploration.candidateCount),
269
+ viableCandidateCount: 0,
270
+ rejectedCandidateCount: numeric(exploration.rejectedCandidateCount),
271
+ shownCandidateCount: numeric(exploration.shownCandidateCount),
272
+ omittedCandidateCount: numeric(exploration.omittedCandidateCount),
273
+ shownRejectedCandidateCount: numeric(
274
+ exploration.shownRejectedCandidateCount,
275
+ ),
276
+ omittedRejectedCandidateCount: numeric(
277
+ exploration.omittedRejectedCandidateCount,
278
+ ),
279
+ rejectedCandidates: recordArray(exploration.rejectedCandidates).slice(0, maxCandidates),
280
+ callSite,
281
+ }];
282
+ });
283
+ }
284
+
94
285
  export function edgeTarget(row: TraceGraphRow, evidence: Record<string, unknown>): string {
95
286
  const effective = parseObject(evidence.effectiveResolution);
96
287
  const targetServicePath = stringValue(effective.targetServicePath ?? evidence.targetServicePath);
@@ -172,14 +363,112 @@ function resolveRuntimeOperation(db: Db, evidence: Record<string, unknown>, work
172
363
 
173
364
  function evidenceWithRuntimeVariables(evidence: Record<string, unknown>, vars: Record<string, string> | undefined): Record<string, unknown> {
174
365
  const substitutions = runtimeSubstitutions(evidence, vars ?? {});
175
- const next: Record<string, unknown> = { ...evidence, runtimeSubstitutions: substitutions };
366
+ const suppliedRuntimeVariables = Object.fromEntries(
367
+ Object.entries(vars ?? {}).sort(([left], [right]) => left.localeCompare(right)),
368
+ );
369
+ const next: Record<string, unknown> = {
370
+ ...evidence,
371
+ runtimeSubstitutions: substitutions,
372
+ suppliedRuntimeVariables,
373
+ };
176
374
  for (const [key, value] of Object.entries(substitutions)) if (value.effective) next[key] = value.effective;
177
375
  const missing = Object.values(substitutions).flatMap((value) => value.missing);
178
376
  if (missing.length > 0) next.missingRuntimeVariables = [...new Set(missing)].sort();
179
- if (Object.keys(vars ?? {}).length > 0) next.runtimeVariablesApplied = true;
377
+ if (Object.keys(suppliedRuntimeVariables).length > 0) next.runtimeVariablesApplied = true;
180
378
  return next;
181
379
  }
182
380
 
381
+ function evidenceWithDynamicAnalysis(
382
+ evidence: Record<string, unknown>,
383
+ analysis: DynamicTargetAnalysis,
384
+ ): Record<string, unknown> {
385
+ const persistedCandidates = recordArray(evidence.candidates);
386
+ const persistedScores = recordArray(evidence.candidateScores);
387
+ return {
388
+ ...evidence,
389
+ candidates: persistedCandidates.slice(0, analysis.maxCandidates),
390
+ candidateScores: persistedScores.slice(0, analysis.maxCandidates),
391
+ persistedCandidateCount: persistedCandidates.length,
392
+ persistedCandidateOmittedCount: Math.max(
393
+ 0,
394
+ persistedCandidates.length - analysis.maxCandidates,
395
+ ),
396
+ dynamicTargetExploration: {
397
+ mode: analysis.mode,
398
+ maxCandidates: analysis.maxCandidates,
399
+ missingVariables: analysis.missingVariables,
400
+ requiredVariables: analysis.requiredVariables,
401
+ suppliedVariables: analysis.suppliedVariables,
402
+ appliedSuppliedVariables: analysis.appliedSuppliedVariables,
403
+ substitutedSignals: analysis.substitutedSignals,
404
+ candidateCount: analysis.candidateCount,
405
+ viableCandidateCount: analysis.viableCandidateCount,
406
+ rejectedCandidateCount: analysis.rejectedCandidateCount,
407
+ shownCandidateCount: analysis.shownCandidateCount,
408
+ omittedCandidateCount: analysis.omittedCandidateCount,
409
+ shownRejectedCandidateCount: analysis.shownRejectedCandidateCount,
410
+ omittedRejectedCandidateCount: analysis.omittedRejectedCandidateCount,
411
+ rejectedCandidates: analysis.rejectedCandidates,
412
+ suggestedVarSets: analysis.suggestedVarSets,
413
+ },
414
+ dynamicTargetCandidates: analysis.candidates,
415
+ dynamicTargetCandidateSuggestions: analysis.shownCandidates,
416
+ rejectedCandidates: analysis.rejectedCandidates,
417
+ dynamicTargetInference: analysis.inference,
418
+ };
419
+ }
420
+
421
+ const boundedDynamicListKeys = new Set([
422
+ 'candidates',
423
+ 'candidateScores',
424
+ 'dynamicTargetCandidates',
425
+ 'dynamicTargetCandidateSuggestions',
426
+ 'candidateSuggestions',
427
+ 'suggestedVarSets',
428
+ 'rejectedCandidates',
429
+ 'rejectedCandidateSuggestions',
430
+ 'copyableExamples',
431
+ 'conflicts',
432
+ ]);
433
+
434
+ function boundDynamicEvidence(
435
+ evidence: Record<string, unknown>,
436
+ limit: number,
437
+ ): Record<string, unknown> {
438
+ const candidateCount = numeric(evidence.persistedCandidateCount)
439
+ || (Array.isArray(evidence.candidates) ? evidence.candidates.length : 0);
440
+ const projected = boundDynamicValue(evidence, limit);
441
+ const next = parseObject(projected);
442
+ if (candidateCount === 0) return next;
443
+ return {
444
+ ...next,
445
+ persistedCandidateCount: candidateCount,
446
+ persistedCandidateOmittedCount: Math.max(0, candidateCount - limit),
447
+ };
448
+ }
449
+
450
+ function boundDynamicValue(
451
+ value: unknown,
452
+ limit: number,
453
+ key?: string,
454
+ parentKey?: string,
455
+ ): unknown {
456
+ if (Array.isArray(value)) {
457
+ const bounded = Boolean(key && (boundedDynamicListKeys.has(key)
458
+ || parentKey === 'derivationProvenance'
459
+ || parentKey === 'conflicts' && key === 'sources'));
460
+ const input = bounded ? value.slice(0, limit) : value;
461
+ const projected = input.map((item) =>
462
+ boundDynamicValue(item, limit, undefined, key ?? parentKey));
463
+ return projected;
464
+ }
465
+ if (!value || typeof value !== 'object') return value;
466
+ return Object.fromEntries(Object.entries(value).map(([childKey, child]) => [
467
+ childKey,
468
+ boundDynamicValue(child, limit, childKey, key ?? parentKey),
469
+ ]));
470
+ }
471
+
183
472
  function runtimeSubstitutions(evidence: Record<string, unknown>, vars: Record<string, string>): Record<string, RuntimeSubstitution> {
184
473
  const substitutions: Record<string, RuntimeSubstitution> = {};
185
474
  for (const key of ['servicePath', 'operationPath', 'serviceAliasExpr', 'serviceAlias', 'destination']) {
@@ -199,13 +488,50 @@ function substitutionValue(
199
488
  return normalized?.wasInvocation ? normalized.normalizedOperationPath : value;
200
489
  }
201
490
 
202
- function isRemoteRuntimeCandidate(row: TraceGraphRow, evidence: Record<string, unknown>, vars: Record<string, string> | undefined): boolean {
203
- if (!vars || Object.keys(vars).length === 0) return false;
491
+ function isDynamicRemoteOperationEdge(
492
+ row: TraceGraphRow,
493
+ evidence: Record<string, unknown>,
494
+ ): boolean {
495
+ if (evidence.callType !== 'remote_action') return false;
204
496
  if (!['dynamic', 'ambiguous', 'unresolved'].includes(String(row.status ?? ''))) return false;
205
- if (!['DYNAMIC_EDGE_CANDIDATE', 'UNRESOLVED_EDGE', 'REMOTE_CALL_RESOLVES_TO_OPERATION'].includes(row.edge_type)) return false;
497
+ return ['DYNAMIC_EDGE_CANDIDATE', 'UNRESOLVED_EDGE', 'REMOTE_CALL_RESOLVES_TO_OPERATION']
498
+ .includes(row.edge_type);
499
+ }
500
+
501
+ function hasApplicableRuntimeVariables(
502
+ evidence: Record<string, unknown>,
503
+ vars: Record<string, string> | undefined,
504
+ ): boolean {
505
+ if (!vars || Object.keys(vars).length === 0) return false;
206
506
  return ['servicePath', 'operationPath', 'serviceAliasExpr', 'serviceAlias', 'destination'].some((key) => hasRuntimeVariable(evidence[key], vars));
207
507
  }
208
508
 
509
+ function inferredTarget(analysis: DynamicTargetAnalysis | undefined): OperationTarget | undefined {
510
+ if (analysis?.inference.status !== 'resolved') return undefined;
511
+ const id = Number(analysis.inference.candidateOperationId);
512
+ const candidate = analysis.candidates.find((item) => item.candidateOperationId === id);
513
+ if (!candidate) return undefined;
514
+ return targetFromCandidate(candidate);
515
+ }
516
+
517
+ function targetFromCandidate(candidate: DynamicTargetCandidate): OperationTarget {
518
+ return {
519
+ operationId: candidate.candidateOperationId,
520
+ repoName: candidate.repoName,
521
+ serviceName: '',
522
+ qualifiedName: '',
523
+ servicePath: candidate.servicePath,
524
+ operationPath: candidate.operationPath,
525
+ operationName: candidate.operationName,
526
+ repoId: candidate.repoId,
527
+ packageName: candidate.packageName,
528
+ score: candidate.score,
529
+ reasons: candidate.reasons,
530
+ sourceFile: candidate.sourceFile,
531
+ sourceLine: candidate.sourceLine,
532
+ };
533
+ }
534
+
209
535
  function hasRuntimeVariable(value: unknown, vars: Record<string, string>): boolean {
210
536
  return typeof value === 'string' && extractPlaceholders(value).some((key) => Object.hasOwn(vars, key));
211
537
  }
@@ -223,3 +549,46 @@ function parseObject(value: unknown): Record<string, unknown> {
223
549
  function stringValue(value: unknown): string | undefined {
224
550
  return typeof value === 'string' ? value : undefined;
225
551
  }
552
+
553
+ function numeric(value: unknown): number {
554
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
555
+ }
556
+
557
+ function recordArray(value: unknown): Record<string, unknown>[] {
558
+ return Array.isArray(value)
559
+ ? value.filter((item): item is Record<string, unknown> =>
560
+ Boolean(item && typeof item === 'object' && !Array.isArray(item)))
561
+ : [];
562
+ }
563
+
564
+ function appendBounded<T>(target: T[], values: T[], limit: number): void {
565
+ const remaining = Math.max(0, limit - target.length);
566
+ if (remaining > 0) target.push(...values.slice(0, remaining));
567
+ }
568
+
569
+ function uniqueCliRows(rows: Record<string, unknown>[]): Record<string, unknown>[] {
570
+ const seen = new Set<string>();
571
+ return rows.filter((row) => {
572
+ const cli = typeof row.cli === 'string' ? row.cli : JSON.stringify(row);
573
+ if (seen.has(cli)) return false;
574
+ seen.add(cli);
575
+ return true;
576
+ });
577
+ }
578
+
579
+ function copyableExamples(
580
+ suggestedVarSets: Record<string, unknown>[],
581
+ candidateCount: number,
582
+ limit: number,
583
+ ): string[] {
584
+ const variableExamples = uniqueCliRows(suggestedVarSets).flatMap((row) =>
585
+ typeof row.cli === 'string' ? [row.cli] : []);
586
+ const exploration = candidateCount > 0
587
+ ? ['--dynamic-mode candidates --max-dynamic-candidates 20']
588
+ : [];
589
+ return [...variableExamples, ...exploration].slice(0, limit);
590
+ }
591
+
592
+ function positiveCandidateCap(value: number | undefined): number {
593
+ return Number.isFinite(value) && Number(value) > 0 ? Math.floor(Number(value)) : 5;
594
+ }