@saptools/service-flow 0.1.51 → 0.1.53

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