@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.
- package/CHANGELOG.md +14 -0
- package/README.md +16 -3
- package/TECHNICAL-NOTE.md +10 -1
- package/dist/{chunk-52OUS3MO.js → chunk-PTLDSHRC.js} +2663 -363
- package/dist/chunk-PTLDSHRC.js.map +1 -0
- package/dist/cli.js +318 -510
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +59 -17
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli.ts +97 -57
- package/src/db/connection.ts +1 -1
- package/src/db/migrations.ts +5 -3
- package/src/db/repositories.ts +120 -22
- package/src/db/schema.ts +7 -2
- package/src/index.ts +1 -1
- package/src/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/cross-repo-linker.ts +22 -2
- package/src/linker/service-resolver.ts +15 -4
- package/src/output/table-output.ts +56 -3
- package/src/parsers/cds-parser.ts +8 -2
- package/src/parsers/decorator-parser.ts +382 -48
- package/src/parsers/handler-registration-parser.ts +38 -21
- package/src/parsers/imported-wrapper-parser.ts +18 -5
- package/src/parsers/outbound-call-parser.ts +25 -8
- package/src/parsers/package-json-parser.ts +36 -11
- package/src/parsers/service-binding-parser-helpers.ts +8 -1
- package/src/parsers/service-binding-parser.ts +6 -3
- package/src/parsers/symbol-parser.ts +13 -3
- package/src/parsers/ts-project.ts +54 -0
- package/src/trace/000-dynamic-target-types.ts +84 -0
- package/src/trace/001-dynamic-identity.ts +280 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +82 -0
- package/src/trace/dynamic-branches.ts +45 -0
- package/src/trace/dynamic-targets.ts +654 -0
- package/src/trace/evidence.ts +391 -22
- package/src/trace/selectors.ts +483 -0
- package/src/trace/trace-engine.ts +101 -94
- package/src/types.ts +39 -1
- package/dist/chunk-52OUS3MO.js.map +0 -1
package/src/trace/evidence.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
58
|
-
|
|
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(
|
|
98
|
+
const withSections = withEffectiveResolution(enriched, row, unresolvedReason);
|
|
61
99
|
return { row, evidence: withSections, unresolvedReason };
|
|
62
100
|
}
|
|
63
|
-
const resolution = resolveRuntimeOperation(db,
|
|
64
|
-
if (resolution.target)
|
|
65
|
-
|
|
66
|
-
|
|
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(
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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(
|
|
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
|
|
203
|
-
|
|
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
|
-
|
|
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
|
+
}
|