@saptools/service-flow 0.1.51 → 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 (40) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +10 -5
  3. package/TECHNICAL-NOTE.md +8 -4
  4. package/dist/{chunk-YZJKE5UX.js → chunk-PTLDSHRC.js} +2412 -553
  5. package/dist/chunk-PTLDSHRC.js.map +1 -0
  6. package/dist/cli.js +280 -506
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +45 -7
  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 +75 -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/indexer/repository-indexer.ts +57 -29
  18. package/src/indexer/workspace-indexer.ts +84 -6
  19. package/src/linker/cross-repo-linker.ts +13 -1
  20. package/src/output/table-output.ts +29 -3
  21. package/src/parsers/cds-parser.ts +8 -2
  22. package/src/parsers/decorator-parser.ts +382 -48
  23. package/src/parsers/handler-registration-parser.ts +38 -21
  24. package/src/parsers/imported-wrapper-parser.ts +18 -5
  25. package/src/parsers/outbound-call-parser.ts +25 -8
  26. package/src/parsers/package-json-parser.ts +36 -11
  27. package/src/parsers/service-binding-parser-helpers.ts +8 -1
  28. package/src/parsers/service-binding-parser.ts +6 -3
  29. package/src/parsers/symbol-parser.ts +13 -3
  30. package/src/parsers/ts-project.ts +54 -0
  31. package/src/trace/000-dynamic-target-types.ts +84 -0
  32. package/src/trace/001-dynamic-identity.ts +280 -0
  33. package/src/trace/002-trace-diagnostics.ts +54 -0
  34. package/src/trace/003-dynamic-references.ts +82 -0
  35. package/src/trace/dynamic-targets.ts +459 -229
  36. package/src/trace/evidence.ts +305 -46
  37. package/src/trace/selectors.ts +483 -0
  38. package/src/trace/trace-engine.ts +90 -83
  39. package/src/types.ts +27 -1
  40. package/dist/chunk-YZJKE5UX.js.map +0 -1
@@ -24,6 +24,16 @@ interface Candidate {
24
24
  operationName?: string;
25
25
  score?: number;
26
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
+ }
27
37
 
28
38
  export function baseTraceEvidence(
29
39
  row: TraceGraphRow,
@@ -38,6 +48,8 @@ export function baseTraceEvidence(
38
48
  persistedGraphEdgeId: row.id > 0 ? row.id : undefined,
39
49
  outboundCallId: call.id,
40
50
  callSite: { sourceFile: call.source_file, sourceLine: call.source_line },
51
+ callType: call.call_type,
52
+ repoId: call.repo_id,
41
53
  sourceFile: call.source_file,
42
54
  sourceLine: call.source_line,
43
55
  file: call.source_file,
@@ -56,44 +68,137 @@ export function runtimeResolution(
56
68
  workspaceId: number | undefined,
57
69
  contextualUnresolvedReason?: string,
58
70
  ): { row: TraceGraphRow; evidence: Record<string, unknown>; target?: OperationTarget; unresolvedReason?: string } {
59
- const substituted = evidenceWithRuntimeVariables(evidence, options.vars);
60
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);
61
80
  const analysis = analyzeDynamicTargetCandidates(
62
- db,
63
- substituted,
64
- workspaceId,
65
- dynamicMode,
66
- positiveCandidateCap(options.maxDynamicCandidates),
81
+ db, substituted, workspaceId, dynamicMode, candidateCap,
67
82
  );
68
- const enriched = analysis ? evidenceWithDynamicAnalysis(substituted, analysis) : substituted;
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);
69
91
  if (dynamicMode === 'infer') {
70
92
  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
- }
93
+ if (inferred)
94
+ return resolvedRuntimeResolution(row, enriched, inferred, inferred.reasons);
76
95
  }
77
- if (!isRemoteRuntimeCandidate(row, evidence, options.vars)) {
96
+ if (!hasApplicableRuntimeVariables(evidence, options.vars)) {
78
97
  const unresolvedReason = contextualUnresolvedReason ?? row.unresolved_reason;
79
98
  const withSections = withEffectiveResolution(enriched, row, unresolvedReason);
80
99
  return { row, evidence: withSections, unresolvedReason };
81
100
  }
82
101
  const resolution = resolveRuntimeOperation(db, enriched, workspaceId);
83
- if (resolution.target) {
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)) };
85
- return { row: resolvedRow, evidence: withEffectiveResolution(enriched, resolvedRow, undefined, resolution), target: resolution.target };
86
- }
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);
87
109
  const unresolvedReason = runtimeUnresolvedReason(resolution);
88
110
  return { row, evidence: withEffectiveResolution(enriched, row, unresolvedReason, resolution), unresolvedReason };
89
111
  }
90
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
+ };
163
+ }
164
+
91
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 {
92
195
  const missing = new Set<string>();
93
196
  let candidateCount = 0;
94
- let shownCandidateCount = 0;
95
- let omittedCandidateCount = 0;
197
+ let viableCandidateCount = 0;
198
+ let rejectedCandidateCount = 0;
199
+ let maxCandidates = 5;
96
200
  const candidateSuggestions: Record<string, unknown>[] = [];
201
+ const rejectedCandidates: Record<string, unknown>[] = [];
97
202
  const suggestedVarSets: Record<string, unknown>[] = [];
98
203
  for (const edge of edges) {
99
204
  const effective = parseObject(edge.evidence.effectiveResolution);
@@ -104,33 +209,79 @@ export function runtimeVariableDiagnostic(edges: Array<{ evidence: Record<string
104
209
  for (const value of Object.values(substitutions as Record<string, RuntimeSubstitution>))
105
210
  for (const key of value.missing ?? []) missing.add(key);
106
211
  const exploration = parseObject(edge.evidence.dynamicTargetExploration);
212
+ maxCandidates = positiveCandidateCap(numeric(exploration.maxCandidates) || maxCandidates);
107
213
  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));
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
+ );
112
231
  }
113
- const missingVariables = [...missing].sort();
114
- if (missingVariables.length === 0) return undefined;
115
232
  return {
116
- severity: 'warning',
117
- code: 'trace_runtime_variables_missing',
118
- message: `Runtime variables are required to resolve dynamic trace targets: ${missingVariables.join(', ')}`,
119
- missingVariables,
120
- suggestions: missingVariables.map((key) => `--var ${key}=<value>`),
233
+ missing,
121
234
  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
- ],
235
+ viableCandidateCount,
236
+ rejectedCandidateCount,
237
+ maxCandidates,
238
+ candidateSuggestions,
239
+ rejectedCandidates,
240
+ suggestedVarSets,
131
241
  };
132
242
  }
133
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
+
134
285
  export function edgeTarget(row: TraceGraphRow, evidence: Record<string, unknown>): string {
135
286
  const effective = parseObject(evidence.effectiveResolution);
136
287
  const targetServicePath = stringValue(effective.targetServicePath ?? evidence.targetServicePath);
@@ -212,11 +363,18 @@ function resolveRuntimeOperation(db: Db, evidence: Record<string, unknown>, work
212
363
 
213
364
  function evidenceWithRuntimeVariables(evidence: Record<string, unknown>, vars: Record<string, string> | undefined): Record<string, unknown> {
214
365
  const substitutions = runtimeSubstitutions(evidence, vars ?? {});
215
- 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
+ };
216
374
  for (const [key, value] of Object.entries(substitutions)) if (value.effective) next[key] = value.effective;
217
375
  const missing = Object.values(substitutions).flatMap((value) => value.missing);
218
376
  if (missing.length > 0) next.missingRuntimeVariables = [...new Set(missing)].sort();
219
- if (Object.keys(vars ?? {}).length > 0) next.runtimeVariablesApplied = true;
377
+ if (Object.keys(suppliedRuntimeVariables).length > 0) next.runtimeVariablesApplied = true;
220
378
  return next;
221
379
  }
222
380
 
@@ -224,22 +382,93 @@ function evidenceWithDynamicAnalysis(
224
382
  evidence: Record<string, unknown>,
225
383
  analysis: DynamicTargetAnalysis,
226
384
  ): Record<string, unknown> {
385
+ const persistedCandidates = recordArray(evidence.candidates);
386
+ const persistedScores = recordArray(evidence.candidateScores);
227
387
  return {
228
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
+ ),
229
396
  dynamicTargetExploration: {
230
397
  mode: analysis.mode,
398
+ maxCandidates: analysis.maxCandidates,
231
399
  missingVariables: analysis.missingVariables,
400
+ requiredVariables: analysis.requiredVariables,
401
+ suppliedVariables: analysis.suppliedVariables,
402
+ appliedSuppliedVariables: analysis.appliedSuppliedVariables,
403
+ substitutedSignals: analysis.substitutedSignals,
232
404
  candidateCount: analysis.candidateCount,
405
+ viableCandidateCount: analysis.viableCandidateCount,
406
+ rejectedCandidateCount: analysis.rejectedCandidateCount,
233
407
  shownCandidateCount: analysis.shownCandidateCount,
234
408
  omittedCandidateCount: analysis.omittedCandidateCount,
409
+ shownRejectedCandidateCount: analysis.shownRejectedCandidateCount,
410
+ omittedRejectedCandidateCount: analysis.omittedRejectedCandidateCount,
411
+ rejectedCandidates: analysis.rejectedCandidates,
235
412
  suggestedVarSets: analysis.suggestedVarSets,
236
413
  },
237
414
  dynamicTargetCandidates: analysis.candidates,
238
415
  dynamicTargetCandidateSuggestions: analysis.shownCandidates,
416
+ rejectedCandidates: analysis.rejectedCandidates,
239
417
  dynamicTargetInference: analysis.inference,
240
418
  };
241
419
  }
242
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
+
243
472
  function runtimeSubstitutions(evidence: Record<string, unknown>, vars: Record<string, string>): Record<string, RuntimeSubstitution> {
244
473
  const substitutions: Record<string, RuntimeSubstitution> = {};
245
474
  for (const key of ['servicePath', 'operationPath', 'serviceAliasExpr', 'serviceAlias', 'destination']) {
@@ -259,10 +488,21 @@ function substitutionValue(
259
488
  return normalized?.wasInvocation ? normalized.normalizedOperationPath : value;
260
489
  }
261
490
 
262
- function isRemoteRuntimeCandidate(row: TraceGraphRow, evidence: Record<string, unknown>, vars: Record<string, string> | undefined): boolean {
263
- 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;
264
496
  if (!['dynamic', 'ambiguous', 'unresolved'].includes(String(row.status ?? ''))) return false;
265
- 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;
266
506
  return ['servicePath', 'operationPath', 'serviceAliasExpr', 'serviceAlias', 'destination'].some((key) => hasRuntimeVariable(evidence[key], vars));
267
507
  }
268
508
 
@@ -283,11 +523,12 @@ function targetFromCandidate(candidate: DynamicTargetCandidate): OperationTarget
283
523
  servicePath: candidate.servicePath,
284
524
  operationPath: candidate.operationPath,
285
525
  operationName: candidate.operationName,
526
+ repoId: candidate.repoId,
286
527
  packageName: candidate.packageName,
287
528
  score: candidate.score,
288
529
  reasons: candidate.reasons,
289
- sourceFile: '',
290
- sourceLine: 0,
530
+ sourceFile: candidate.sourceFile,
531
+ sourceLine: candidate.sourceLine,
291
532
  };
292
533
  }
293
534
 
@@ -320,6 +561,11 @@ function recordArray(value: unknown): Record<string, unknown>[] {
320
561
  : [];
321
562
  }
322
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
+
323
569
  function uniqueCliRows(rows: Record<string, unknown>[]): Record<string, unknown>[] {
324
570
  const seen = new Set<string>();
325
571
  return rows.filter((row) => {
@@ -330,6 +576,19 @@ function uniqueCliRows(rows: Record<string, unknown>[]): Record<string, unknown>
330
576
  });
331
577
  }
332
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
+
333
592
  function positiveCandidateCap(value: number | undefined): number {
334
593
  return Number.isFinite(value) && Number(value) > 0 ? Math.floor(Number(value)) : 5;
335
594
  }