@saptools/service-flow 0.1.52 → 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.
- package/CHANGELOG.md +6 -0
- package/README.md +9 -12
- package/TECHNICAL-NOTE.md +11 -3
- package/dist/{chunk-PTLDSHRC.js → chunk-LFH7C46B.js} +2214 -1044
- package/dist/chunk-LFH7C46B.js.map +1 -0
- package/dist/cli.js +157 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/001-doctor-projection.ts +140 -0
- package/src/cli/doctor.ts +20 -3
- package/src/db/repositories.ts +10 -2
- package/src/linker/000-implementation-candidates.ts +641 -0
- package/src/linker/001-implementation-evidence-projection.ts +119 -0
- package/src/linker/002-call-evidence.ts +226 -0
- package/src/linker/cross-repo-linker.ts +27 -453
- package/src/linker/dynamic-edge-resolver.ts +35 -0
- package/src/linker/external-http-target.ts +24 -3
- package/src/linker/helper-package-linker.ts +18 -2
- package/src/linker/service-resolver.ts +45 -2
- package/src/output/doctor-output.ts +13 -4
- package/src/output/table-output.ts +4 -2
- package/src/trace/000-dynamic-target-types.ts +12 -0
- package/src/trace/001-dynamic-identity.ts +45 -17
- package/src/trace/003-dynamic-references.ts +236 -35
- package/src/trace/004-dynamic-candidate-sources.ts +155 -0
- package/src/trace/005-implementation-selection.ts +187 -0
- package/src/trace/006-contextual-projection.ts +30 -0
- package/src/trace/007-implementation-start-diagnostic.ts +61 -0
- package/src/trace/dynamic-targets.ts +205 -159
- package/src/trace/evidence.ts +35 -9
- package/src/trace/implementation-hints.ts +148 -8
- package/src/trace/selectors.ts +74 -9
- package/src/trace/trace-engine.ts +39 -52
- package/src/utils/000-bounded-projection.ts +161 -0
- package/dist/chunk-PTLDSHRC.js.map +0 -1
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import type { Db } from '../db/connection.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
applyVariables,
|
|
4
|
+
extractPlaceholders,
|
|
5
|
+
matchRuntimeTemplate,
|
|
6
|
+
} from '../linker/dynamic-edge-resolver.js';
|
|
7
|
+
import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
|
|
3
8
|
import type { OperationTarget } from '../linker/service-resolver.js';
|
|
4
9
|
import type { DynamicMode } from '../types.js';
|
|
10
|
+
import { dynamicCandidateTargets } from './004-dynamic-candidate-sources.js';
|
|
11
|
+
import { projectBounded } from '../utils/000-bounded-projection.js';
|
|
5
12
|
import { uniqueIdentityDerivations } from './001-dynamic-identity.js';
|
|
6
13
|
import {
|
|
7
14
|
dynamicReferenceProvenance,
|
|
8
|
-
|
|
15
|
+
dynamicRoutingContext,
|
|
9
16
|
type DynamicReferenceRow,
|
|
17
|
+
type DynamicRoutingContext,
|
|
10
18
|
} from './003-dynamic-references.js';
|
|
11
19
|
import type {
|
|
12
20
|
DynamicTargetAnalysis,
|
|
@@ -14,15 +22,12 @@ import type {
|
|
|
14
22
|
DynamicTemplates,
|
|
15
23
|
DynamicVariableProvenance,
|
|
16
24
|
} from './000-dynamic-target-types.js';
|
|
17
|
-
|
|
18
25
|
export type {
|
|
19
26
|
DynamicTargetAnalysis,
|
|
20
27
|
DynamicTargetCandidate,
|
|
21
28
|
} from './000-dynamic-target-types.js';
|
|
22
|
-
|
|
23
29
|
type Templates = DynamicTemplates;
|
|
24
30
|
type VariableProvenance = DynamicVariableProvenance;
|
|
25
|
-
|
|
26
31
|
interface AnalysisInputs {
|
|
27
32
|
original: Templates;
|
|
28
33
|
effective: Templates;
|
|
@@ -32,6 +37,7 @@ interface AnalysisInputs {
|
|
|
32
37
|
order: string[];
|
|
33
38
|
callerRepo?: string;
|
|
34
39
|
callerRepoId?: number;
|
|
40
|
+
routing: DynamicRoutingContext;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export function analyzeDynamicTargetCandidates(
|
|
@@ -41,13 +47,17 @@ export function analyzeDynamicTargetCandidates(
|
|
|
41
47
|
mode: DynamicMode,
|
|
42
48
|
maxCandidates: number,
|
|
43
49
|
): DynamicTargetAnalysis | undefined {
|
|
44
|
-
const inputs = analysisInputs(evidence);
|
|
50
|
+
const inputs = analysisInputs(db, evidence, workspaceId);
|
|
45
51
|
if (inputs.required.length === 0) return undefined;
|
|
46
|
-
const targets =
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
const targets = dynamicCandidateTargets(
|
|
53
|
+
db,
|
|
54
|
+
inputs.effective.operationPath,
|
|
55
|
+
inputs.original.operationPath,
|
|
56
|
+
evidence.candidates,
|
|
57
|
+
workspaceId,
|
|
58
|
+
inputs.routing.outboundCallId !== undefined,
|
|
49
59
|
);
|
|
50
|
-
const candidates = buildCandidates(db, targets, references, inputs);
|
|
60
|
+
const candidates = buildCandidates(db, targets, inputs.routing.references, inputs);
|
|
51
61
|
applyUniqueIdentityEvidence(db, candidates, inputs);
|
|
52
62
|
finalizeCandidates(candidates, inputs.order);
|
|
53
63
|
const ranked = stableRank(candidates);
|
|
@@ -59,6 +69,7 @@ export function analyzeDynamicTargetCandidates(
|
|
|
59
69
|
.map((candidate) => boundedCandidate(candidate, maxCandidates));
|
|
60
70
|
const shownRejected = rejected.slice(0, maxCandidates)
|
|
61
71
|
.map((candidate) => boundedCandidate(candidate, maxCandidates));
|
|
72
|
+
const suggestionProjection = suggestedVarSets(viable, inputs.order, maxCandidates);
|
|
62
73
|
return {
|
|
63
74
|
mode,
|
|
64
75
|
maxCandidates,
|
|
@@ -77,17 +88,26 @@ export function analyzeDynamicTargetCandidates(
|
|
|
77
88
|
candidates: shown,
|
|
78
89
|
shownCandidates: shown,
|
|
79
90
|
rejectedCandidates: shownRejected,
|
|
80
|
-
suggestedVarSets:
|
|
91
|
+
suggestedVarSets: suggestionProjection.items,
|
|
92
|
+
suggestedVarSetCount: suggestionProjection.totalCount,
|
|
93
|
+
shownSuggestedVarSetCount: suggestionProjection.shownCount,
|
|
94
|
+
omittedSuggestedVarSetCount: suggestionProjection.omittedCount,
|
|
81
95
|
inference,
|
|
96
|
+
routingContext: routingEvidence(inputs.routing),
|
|
82
97
|
};
|
|
83
98
|
}
|
|
84
99
|
|
|
85
|
-
function analysisInputs(
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
function analysisInputs(
|
|
101
|
+
db: Db,
|
|
102
|
+
evidence: Record<string, unknown>,
|
|
103
|
+
workspaceId: number | undefined,
|
|
104
|
+
): AnalysisInputs {
|
|
105
|
+
const routing = dynamicRoutingContext(db, workspaceId, evidence);
|
|
106
|
+
const supplied = stringRecord(evidence.suppliedRuntimeVariables);
|
|
107
|
+
const original = templatesFromEvidence(evidence, routing);
|
|
108
|
+
const effective = effectiveTemplates(original, supplied);
|
|
88
109
|
const requiredSources = placeholderSources(original);
|
|
89
110
|
const required = Object.keys(requiredSources);
|
|
90
|
-
const supplied = stringRecord(evidence.suppliedRuntimeVariables);
|
|
91
111
|
return {
|
|
92
112
|
original,
|
|
93
113
|
effective,
|
|
@@ -95,94 +115,12 @@ function analysisInputs(evidence: Record<string, unknown>): AnalysisInputs {
|
|
|
95
115
|
requiredSources,
|
|
96
116
|
supplied,
|
|
97
117
|
order: variableOrder(original, required),
|
|
98
|
-
callerRepo: stringValue(evidence.repo),
|
|
99
|
-
callerRepoId: numberValue(evidence.repoId),
|
|
118
|
+
callerRepo: routing.callerRepo ?? stringValue(evidence.repo),
|
|
119
|
+
callerRepoId: routing.callerRepoId ?? numberValue(evidence.repoId),
|
|
120
|
+
routing,
|
|
100
121
|
};
|
|
101
122
|
}
|
|
102
123
|
|
|
103
|
-
function candidateTargets(
|
|
104
|
-
db: Db,
|
|
105
|
-
evidence: Record<string, unknown>,
|
|
106
|
-
workspaceId: number | undefined,
|
|
107
|
-
): OperationTarget[] {
|
|
108
|
-
const embedded = rowsFromEvidence(evidence.candidates);
|
|
109
|
-
if (embedded.length > 0) return embedded;
|
|
110
|
-
const operationPath = effectiveSignal(evidence, 'operationPath');
|
|
111
|
-
if (!operationPath || extractPlaceholders(operationPath).length > 0) return [];
|
|
112
|
-
return queryOperationTargets(db, operationPath, workspaceId);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function rowsFromEvidence(value: unknown): OperationTarget[] {
|
|
116
|
-
if (!Array.isArray(value)) return [];
|
|
117
|
-
return value.flatMap((item): OperationTarget[] => {
|
|
118
|
-
const row = record(item);
|
|
119
|
-
const operationId = numberValue(row.operationId);
|
|
120
|
-
const repoName = stringValue(row.repoName);
|
|
121
|
-
const servicePath = stringValue(row.servicePath);
|
|
122
|
-
const operationPath = stringValue(row.operationPath);
|
|
123
|
-
const operationName = stringValue(row.operationName) ?? operationPath?.replace(/^\//, '');
|
|
124
|
-
if (operationId === undefined || !repoName || !servicePath || !operationPath || !operationName) return [];
|
|
125
|
-
return [{
|
|
126
|
-
operationId,
|
|
127
|
-
repoId: numberValue(row.repoId),
|
|
128
|
-
repoName,
|
|
129
|
-
packageName: stringValue(row.packageName),
|
|
130
|
-
serviceName: stringValue(row.serviceName) ?? '',
|
|
131
|
-
qualifiedName: stringValue(row.qualifiedName) ?? '',
|
|
132
|
-
servicePath,
|
|
133
|
-
operationPath,
|
|
134
|
-
operationName,
|
|
135
|
-
sourceFile: stringValue(row.sourceFile) ?? '',
|
|
136
|
-
sourceLine: numberValue(row.sourceLine) ?? 0,
|
|
137
|
-
score: numberValue(row.score) ?? 0,
|
|
138
|
-
reasons: stringArray(row.reasons),
|
|
139
|
-
}];
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function queryOperationTargets(
|
|
144
|
-
db: Db,
|
|
145
|
-
operationPath: string,
|
|
146
|
-
workspaceId: number | undefined,
|
|
147
|
-
): OperationTarget[] {
|
|
148
|
-
const simple = operationPath.replace(/^\//, '').split('.').at(-1) ?? operationPath;
|
|
149
|
-
const rows = db.prepare(
|
|
150
|
-
`SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,
|
|
151
|
-
s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,
|
|
152
|
-
o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,
|
|
153
|
-
o.source_line sourceLine FROM cds_operations o
|
|
154
|
-
JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
|
|
155
|
-
WHERE (? IS NULL OR r.workspace_id=?)
|
|
156
|
-
AND (o.operation_path IN (?,?) OR o.operation_name=?)
|
|
157
|
-
ORDER BY r.name,s.service_path,o.operation_name,o.id`,
|
|
158
|
-
).all(workspaceId, workspaceId, operationPath, `/${simple}`, simple);
|
|
159
|
-
return rows.flatMap(operationTargetFromRow);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function operationTargetFromRow(row: Record<string, unknown>): OperationTarget[] {
|
|
163
|
-
const operationId = numberValue(row.operationId);
|
|
164
|
-
const repoName = stringValue(row.repoName);
|
|
165
|
-
const servicePath = stringValue(row.servicePath);
|
|
166
|
-
const operationPath = stringValue(row.operationPath);
|
|
167
|
-
const operationName = stringValue(row.operationName);
|
|
168
|
-
if (operationId === undefined || !repoName || !servicePath || !operationPath || !operationName) return [];
|
|
169
|
-
return [{
|
|
170
|
-
operationId,
|
|
171
|
-
repoId: numberValue(row.repoId),
|
|
172
|
-
repoName,
|
|
173
|
-
packageName: stringValue(row.packageName),
|
|
174
|
-
serviceName: stringValue(row.serviceName) ?? '',
|
|
175
|
-
qualifiedName: stringValue(row.qualifiedName) ?? '',
|
|
176
|
-
servicePath,
|
|
177
|
-
operationPath,
|
|
178
|
-
operationName,
|
|
179
|
-
sourceFile: stringValue(row.sourceFile) ?? '',
|
|
180
|
-
sourceLine: numberValue(row.sourceLine) ?? 0,
|
|
181
|
-
score: 0.2,
|
|
182
|
-
reasons: ['operation_path_match'],
|
|
183
|
-
}];
|
|
184
|
-
}
|
|
185
|
-
|
|
186
124
|
function buildCandidates(
|
|
187
125
|
db: Db,
|
|
188
126
|
targets: OperationTarget[],
|
|
@@ -194,15 +132,49 @@ function buildCandidates(
|
|
|
194
132
|
applyDirectSignal(state, inputs, 'operationPath', target.operationPath, 0.25);
|
|
195
133
|
applyDirectSignal(state, inputs, 'servicePath', target.servicePath, 0.35);
|
|
196
134
|
const matchingReferences = references.filter((reference) =>
|
|
197
|
-
reference
|
|
198
|
-
|
|
199
|
-
|
|
135
|
+
referenceMatchesCandidate(reference, target.servicePath)
|
|
136
|
+
&& referenceMatchesSelectedAlias(reference, inputs.routing.selectedBinding));
|
|
137
|
+
const referencesForSignals = fallbackReferencesForCandidate(
|
|
138
|
+
state, matchingReferences, inputs.routing.fallbackUsed,
|
|
139
|
+
);
|
|
140
|
+
applyReferenceSignal(state, inputs, referencesForSignals, 'alias');
|
|
141
|
+
applyReferenceSignal(state, inputs, referencesForSignals, 'destination');
|
|
200
142
|
if (hasResolvedImplementation(db, target.operationId))
|
|
201
143
|
addScore(state, 0.1, 'implementation_edge_resolved');
|
|
202
144
|
return state;
|
|
203
145
|
});
|
|
204
146
|
}
|
|
205
147
|
|
|
148
|
+
function fallbackReferencesForCandidate(
|
|
149
|
+
state: DynamicTargetCandidate,
|
|
150
|
+
references: DynamicReferenceRow[],
|
|
151
|
+
fallbackUsed: boolean,
|
|
152
|
+
): DynamicReferenceRow[] {
|
|
153
|
+
if (!fallbackUsed) return references;
|
|
154
|
+
const unique = uniqueFallbackReferences(references);
|
|
155
|
+
if (unique.length <= 1) return unique;
|
|
156
|
+
addReason(state, 'fallback_reference_ambiguous');
|
|
157
|
+
addInferenceBlock(state, 'fallback_reference_ambiguous');
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function uniqueFallbackReferences(
|
|
162
|
+
references: DynamicReferenceRow[],
|
|
163
|
+
): DynamicReferenceRow[] {
|
|
164
|
+
const seen = new Set<string>();
|
|
165
|
+
return references.filter((reference) => {
|
|
166
|
+
const signature = [
|
|
167
|
+
reference.sourceKind,
|
|
168
|
+
reference.alias,
|
|
169
|
+
reference.destination,
|
|
170
|
+
reference.servicePath,
|
|
171
|
+
].join('\0');
|
|
172
|
+
if (seen.has(signature)) return false;
|
|
173
|
+
seen.add(signature);
|
|
174
|
+
return true;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
206
178
|
function emptyCandidate(
|
|
207
179
|
target: OperationTarget,
|
|
208
180
|
inputs: AnalysisInputs,
|
|
@@ -251,7 +223,7 @@ function applyDirectSignal(
|
|
|
251
223
|
): void {
|
|
252
224
|
const effective = inputs.effective[kind];
|
|
253
225
|
const original = inputs.original[kind];
|
|
254
|
-
if (effective && !
|
|
226
|
+
if (effective && !matchRuntimeTemplate(effective, concrete)) {
|
|
255
227
|
reject(state, `${signalCode(kind)}_contradicts_runtime_substitution`);
|
|
256
228
|
return;
|
|
257
229
|
}
|
|
@@ -259,13 +231,23 @@ function applyDirectSignal(
|
|
|
259
231
|
const suppliedKeys = extractPlaceholders(original)
|
|
260
232
|
.filter((key) => inputs.supplied[key] !== undefined);
|
|
261
233
|
state.explicitSignalStrength += suppliedKeys.length;
|
|
262
|
-
const matched =
|
|
234
|
+
const matched = matchRuntimeTemplate(original, concrete) ?? {};
|
|
235
|
+
const fromSelectedBinding = kind === 'servicePath'
|
|
236
|
+
&& inputs.routing.selectedBinding !== undefined;
|
|
263
237
|
for (const [key, value] of Object.entries(matched)) {
|
|
264
238
|
addDerivation(state, key, value, {
|
|
265
|
-
sourceKind:
|
|
239
|
+
sourceKind: fromSelectedBinding
|
|
240
|
+
? `selected_binding.${signalCode(kind)}_template`
|
|
241
|
+
: `${signalCode(kind)}_template`,
|
|
266
242
|
value,
|
|
267
|
-
rule:
|
|
243
|
+
rule: fromSelectedBinding
|
|
244
|
+
? 'exact_selected_binding_template_match'
|
|
245
|
+
: 'exact_template_match',
|
|
268
246
|
template: original,
|
|
247
|
+
sourceRepo: fromSelectedBinding ? inputs.routing.selectedBinding?.repoName : undefined,
|
|
248
|
+
sourceFile: fromSelectedBinding ? inputs.routing.selectedBinding?.sourceFile : undefined,
|
|
249
|
+
sourceLine: fromSelectedBinding ? inputs.routing.selectedBinding?.sourceLine : undefined,
|
|
250
|
+
selection: fromSelectedBinding ? 'selected_binding' : 'call_evidence',
|
|
269
251
|
});
|
|
270
252
|
}
|
|
271
253
|
addScore(state, score, `${signalCode(kind)}_template_match`);
|
|
@@ -290,7 +272,7 @@ function applyReferenceSignal(
|
|
|
290
272
|
}
|
|
291
273
|
let matchedSignal = false;
|
|
292
274
|
for (const { reference, concrete } of values) {
|
|
293
|
-
const matched =
|
|
275
|
+
const matched = matchRuntimeTemplate(original, concrete);
|
|
294
276
|
if (!matched) continue;
|
|
295
277
|
matchedSignal = true;
|
|
296
278
|
for (const [key, value] of Object.entries(matched)) {
|
|
@@ -406,6 +388,8 @@ function inferenceDecision(candidates: DynamicTargetCandidate[]): Record<string,
|
|
|
406
388
|
const second = viable[1];
|
|
407
389
|
if (!first || first.missingVariables.length > 0)
|
|
408
390
|
return { status: 'unresolved', reason: 'missing_required_runtime_variable' };
|
|
391
|
+
if (first.inferenceBlockReasons.length > 0)
|
|
392
|
+
return { status: 'unresolved', reason: first.inferenceBlockReasons[0] };
|
|
409
393
|
if (first.score < 0.85)
|
|
410
394
|
return { status: 'unresolved', reason: 'candidate_score_below_inference_threshold' };
|
|
411
395
|
const scoreGap = second
|
|
@@ -432,16 +416,61 @@ function boundedCandidate(
|
|
|
432
416
|
candidate: DynamicTargetCandidate,
|
|
433
417
|
limit: number,
|
|
434
418
|
): DynamicTargetCandidate {
|
|
435
|
-
const
|
|
419
|
+
const provenanceProjections = Object.fromEntries(
|
|
436
420
|
Object.entries(candidate.derivationProvenance)
|
|
437
421
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
438
|
-
.map(([key, rows]) => [key, rows
|
|
422
|
+
.map(([key, rows]) => [key, projectBounded(rows, compareProvenance, limit)]),
|
|
439
423
|
);
|
|
440
|
-
const
|
|
424
|
+
const derivationProvenance = Object.fromEntries(Object.entries(provenanceProjections)
|
|
425
|
+
.map(([key, projection]) => [key, projection.items]));
|
|
426
|
+
const derivationProvenanceCounts = Object.fromEntries(Object.entries(provenanceProjections)
|
|
427
|
+
.map(([key, projection]) => [key, {
|
|
428
|
+
provenanceCount: projection.totalCount,
|
|
429
|
+
shownProvenanceCount: projection.shownCount,
|
|
430
|
+
omittedProvenanceCount: projection.omittedCount,
|
|
431
|
+
}]));
|
|
432
|
+
const conflicts = projectBounded(candidate.conflicts, compareConflict, limit);
|
|
433
|
+
return {
|
|
434
|
+
...candidate,
|
|
435
|
+
derivationProvenance,
|
|
436
|
+
derivationProvenanceCounts,
|
|
437
|
+
conflicts: conflicts.items.map(boundedConflict),
|
|
438
|
+
conflictCount: conflicts.totalCount,
|
|
439
|
+
shownConflictCount: conflicts.shownCount,
|
|
440
|
+
omittedConflictCount: conflicts.omittedCount,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function compareProvenance(
|
|
445
|
+
left: DynamicVariableProvenance,
|
|
446
|
+
right: DynamicVariableProvenance,
|
|
447
|
+
): number {
|
|
448
|
+
return left.sourceKind.localeCompare(right.sourceKind)
|
|
449
|
+
|| String(left.matchedName ?? '').localeCompare(String(right.matchedName ?? ''))
|
|
450
|
+
|| left.value.localeCompare(right.value);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function compareConflict(
|
|
454
|
+
left: DynamicTargetCandidate['conflicts'][number],
|
|
455
|
+
right: DynamicTargetCandidate['conflicts'][number],
|
|
456
|
+
): number {
|
|
457
|
+
return left.key.localeCompare(right.key)
|
|
458
|
+
|| left.reason.localeCompare(right.reason)
|
|
459
|
+
|| left.values.join('\0').localeCompare(right.values.join('\0'));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function boundedConflict(
|
|
463
|
+
conflict: DynamicTargetCandidate['conflicts'][number],
|
|
464
|
+
): DynamicTargetCandidate['conflicts'][number] & Record<string, unknown> {
|
|
465
|
+
const sources = projectBounded(conflict.sources, (left, right) =>
|
|
466
|
+
left.localeCompare(right));
|
|
467
|
+
return {
|
|
441
468
|
...conflict,
|
|
442
|
-
sources:
|
|
443
|
-
|
|
444
|
-
|
|
469
|
+
sources: sources.items,
|
|
470
|
+
sourceCount: sources.totalCount,
|
|
471
|
+
shownSourceCount: sources.shownCount,
|
|
472
|
+
omittedSourceCount: sources.omittedCount,
|
|
473
|
+
};
|
|
445
474
|
}
|
|
446
475
|
|
|
447
476
|
function applyModeState(
|
|
@@ -462,7 +491,7 @@ function suggestedVarSets(
|
|
|
462
491
|
candidates: DynamicTargetCandidate[],
|
|
463
492
|
order: string[],
|
|
464
493
|
limit: number,
|
|
465
|
-
):
|
|
494
|
+
): ReturnType<typeof projectBounded<{ variables: Record<string, string>; cli: string }>> {
|
|
466
495
|
const seen = new Set<string>();
|
|
467
496
|
const rows: Array<{ variables: Record<string, string>; cli: string }> = [];
|
|
468
497
|
for (const candidate of candidates) {
|
|
@@ -470,9 +499,8 @@ function suggestedVarSets(
|
|
|
470
499
|
if (seen.has(candidate.cli)) continue;
|
|
471
500
|
seen.add(candidate.cli);
|
|
472
501
|
rows.push({ variables: orderedVariables(candidate.completeVariables, order), cli: candidate.cli });
|
|
473
|
-
if (rows.length >= limit) break;
|
|
474
502
|
}
|
|
475
|
-
return rows;
|
|
503
|
+
return projectBounded(rows, (left, right) => left.cli.localeCompare(right.cli), limit);
|
|
476
504
|
}
|
|
477
505
|
|
|
478
506
|
function hasResolvedImplementation(db: Db, operationId: number): boolean {
|
|
@@ -480,17 +508,30 @@ function hasResolvedImplementation(db: Db, operationId: number): boolean {
|
|
|
480
508
|
"SELECT 1 FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status='resolved' AND from_kind='operation' AND from_id=? LIMIT 1",
|
|
481
509
|
).get(String(operationId)));
|
|
482
510
|
}
|
|
483
|
-
|
|
484
511
|
function templatesFromEvidence(
|
|
485
512
|
evidence: Record<string, unknown>,
|
|
486
|
-
|
|
513
|
+
routing: DynamicRoutingContext,
|
|
487
514
|
): Templates {
|
|
515
|
+
const selected = routing.selectedBinding;
|
|
488
516
|
return {
|
|
489
|
-
servicePath: substitutionSignal(evidence, 'servicePath',
|
|
490
|
-
operationPath: substitutionSignal(evidence, 'operationPath',
|
|
491
|
-
alias: substitutionSignal(evidence,
|
|
492
|
-
evidence.serviceAliasExpr !== undefined ? 'serviceAliasExpr' : 'serviceAlias',
|
|
493
|
-
destination: substitutionSignal(evidence, 'destination',
|
|
517
|
+
servicePath: selected?.servicePath ?? substitutionSignal(evidence, 'servicePath', 'original'),
|
|
518
|
+
operationPath: substitutionSignal(evidence, 'operationPath', 'original'),
|
|
519
|
+
alias: selected?.aliasExpr ?? selected?.alias ?? substitutionSignal(evidence,
|
|
520
|
+
evidence.serviceAliasExpr !== undefined ? 'serviceAliasExpr' : 'serviceAlias', 'original'),
|
|
521
|
+
destination: selected?.destination ?? substitutionSignal(evidence, 'destination', 'original'),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function effectiveTemplates(
|
|
525
|
+
templates: Templates,
|
|
526
|
+
supplied: Record<string, string>,
|
|
527
|
+
): Templates {
|
|
528
|
+
const operationPath = applyVariables(templates.operationPath, supplied);
|
|
529
|
+
return {
|
|
530
|
+
servicePath: applyVariables(templates.servicePath, supplied),
|
|
531
|
+
operationPath: normalizeODataOperationInvocationPath(operationPath)?.normalizedOperationPath
|
|
532
|
+
?? operationPath,
|
|
533
|
+
alias: applyVariables(templates.alias, supplied),
|
|
534
|
+
destination: applyVariables(templates.destination, supplied),
|
|
494
535
|
};
|
|
495
536
|
}
|
|
496
537
|
|
|
@@ -503,10 +544,6 @@ function substitutionSignal(
|
|
|
503
544
|
return stringValue(substitution[phase]) ?? stringValue(evidence[key]);
|
|
504
545
|
}
|
|
505
546
|
|
|
506
|
-
function effectiveSignal(evidence: Record<string, unknown>, key: string): string | undefined {
|
|
507
|
-
return substitutionSignal(evidence, key, 'effective');
|
|
508
|
-
}
|
|
509
|
-
|
|
510
547
|
function placeholderSources(templates: Templates): Record<string, string[]> {
|
|
511
548
|
const sources: Record<string, string[]> = {};
|
|
512
549
|
for (const [kind, template] of Object.entries(templates)) {
|
|
@@ -528,35 +565,20 @@ function variableOrder(templates: Templates, required: string[]): string[] {
|
|
|
528
565
|
return [...new Set([...ordered, ...required])];
|
|
529
566
|
}
|
|
530
567
|
|
|
531
|
-
function
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
):
|
|
535
|
-
|
|
536
|
-
const keys = extractPlaceholders(template);
|
|
537
|
-
if (keys.length === 0) return template === concrete ? {} : undefined;
|
|
538
|
-
const match = new RegExp(`^${templateToPattern(template)}$`).exec(concrete);
|
|
539
|
-
if (!match) return undefined;
|
|
540
|
-
const values: Record<string, string> = {};
|
|
541
|
-
for (let index = 0; index < keys.length; index += 1) {
|
|
542
|
-
const key = keys[index];
|
|
543
|
-
const value = match[index + 1];
|
|
544
|
-
if (!key || value === undefined) return undefined;
|
|
545
|
-
if (values[key] !== undefined && values[key] !== value) return undefined;
|
|
546
|
-
values[key] = value;
|
|
547
|
-
}
|
|
548
|
-
return values;
|
|
568
|
+
function referenceMatchesCandidate(
|
|
569
|
+
reference: DynamicReferenceRow,
|
|
570
|
+
servicePath: string,
|
|
571
|
+
): boolean {
|
|
572
|
+
return matchRuntimeTemplate(reference.servicePath, servicePath) !== undefined;
|
|
549
573
|
}
|
|
550
574
|
|
|
551
|
-
function
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
559
|
-
return `${pattern}${escapeRegex(template.slice(lastIndex))}`;
|
|
575
|
+
function referenceMatchesSelectedAlias(
|
|
576
|
+
reference: DynamicReferenceRow,
|
|
577
|
+
selected: DynamicRoutingContext['selectedBinding'],
|
|
578
|
+
): boolean {
|
|
579
|
+
if (reference.selection !== 'selected_binding_require') return true;
|
|
580
|
+
const template = selected?.aliasExpr ?? selected?.alias;
|
|
581
|
+
return matchRuntimeTemplate(template, reference.alias) !== undefined;
|
|
560
582
|
}
|
|
561
583
|
|
|
562
584
|
function cliFor(variables: Record<string, string>, order: string[]): string {
|
|
@@ -608,6 +630,34 @@ function requiredSuppliedVariables(inputs: AnalysisInputs): Record<string, strin
|
|
|
608
630
|
inputs.supplied[key] === undefined ? [] : [[key, inputs.supplied[key]]]));
|
|
609
631
|
}
|
|
610
632
|
|
|
633
|
+
function routingEvidence(routing: DynamicRoutingContext): Record<string, unknown> {
|
|
634
|
+
const binding = routing.selectedBinding;
|
|
635
|
+
return {
|
|
636
|
+
outboundCallId: routing.outboundCallId,
|
|
637
|
+
callerRepoId: routing.callerRepoId,
|
|
638
|
+
callerRepo: routing.callerRepo,
|
|
639
|
+
selectedBindingId: routing.selectedBindingId,
|
|
640
|
+
bindingResolutionStatus: routing.bindingResolutionStatus,
|
|
641
|
+
selectedBinding: binding ? {
|
|
642
|
+
bindingId: binding.bindingId,
|
|
643
|
+
alias: binding.alias,
|
|
644
|
+
aliasExpr: binding.aliasExpr,
|
|
645
|
+
destination: binding.destination,
|
|
646
|
+
destinationExpr: binding.destination,
|
|
647
|
+
servicePath: binding.servicePath,
|
|
648
|
+
servicePathExpr: binding.servicePath,
|
|
649
|
+
sourceFile: binding.sourceFile,
|
|
650
|
+
sourceLine: binding.sourceLine,
|
|
651
|
+
helperChain: binding.helperChain,
|
|
652
|
+
} : undefined,
|
|
653
|
+
bindingAlternativeCount: routing.bindingAlternativeCount,
|
|
654
|
+
shownBindingAlternativeCount: routing.shownBindingAlternativeCount,
|
|
655
|
+
omittedBindingAlternativeCount: routing.omittedBindingAlternativeCount,
|
|
656
|
+
bindingAlternatives: routing.bindingAlternatives,
|
|
657
|
+
fallbackUsed: routing.fallbackUsed,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
611
661
|
function signalCode(kind: 'servicePath' | 'operationPath'): string {
|
|
612
662
|
return kind === 'servicePath' ? 'service_path' : 'operation_path';
|
|
613
663
|
}
|
|
@@ -648,7 +698,3 @@ function isConcrete(value: unknown): value is string {
|
|
|
648
698
|
return typeof value === 'string' && value.length > 0
|
|
649
699
|
&& extractPlaceholders(value).length === 0;
|
|
650
700
|
}
|
|
651
|
-
|
|
652
|
-
function escapeRegex(value: string): string {
|
|
653
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
654
|
-
}
|
package/src/trace/evidence.ts
CHANGED
|
@@ -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;
|
|
@@ -70,13 +71,14 @@ export function runtimeResolution(
|
|
|
70
71
|
): { row: TraceGraphRow; evidence: Record<string, unknown>; target?: OperationTarget; unresolvedReason?: string } {
|
|
71
72
|
const dynamicMode = options.dynamicMode ?? 'strict';
|
|
72
73
|
const candidateCap = positiveCandidateCap(options.maxDynamicCandidates);
|
|
74
|
+
const boundedEvidence = boundCandidateLikeEvidence(evidence, candidateCap);
|
|
73
75
|
if (!isDynamicRemoteOperationEdge(row, evidence))
|
|
74
76
|
return unchangedRuntimeResolution(
|
|
75
77
|
row,
|
|
76
|
-
boundDynamicEvidence(
|
|
78
|
+
boundDynamicEvidence(boundedEvidence, candidateCap),
|
|
77
79
|
contextualUnresolvedReason,
|
|
78
80
|
);
|
|
79
|
-
const substituted = evidenceWithRuntimeVariables(
|
|
81
|
+
const substituted = evidenceWithRuntimeVariables(boundedEvidence, options.vars);
|
|
80
82
|
const analysis = analyzeDynamicTargetCandidates(
|
|
81
83
|
db, substituted, workspaceId, dynamicMode, candidateCap,
|
|
82
84
|
);
|
|
@@ -84,8 +86,7 @@ export function runtimeResolution(
|
|
|
84
86
|
analysis ? evidenceWithDynamicAnalysis(substituted, analysis) : substituted,
|
|
85
87
|
candidateCap,
|
|
86
88
|
);
|
|
87
|
-
if (analysis && analysis.
|
|
88
|
-
&& analysis.viableCandidateCount === 0
|
|
89
|
+
if (analysis && analysis.viableCandidateCount === 0
|
|
89
90
|
&& Object.keys(analysis.appliedSuppliedVariables).length > 0)
|
|
90
91
|
return noCandidateRuntimeResolution(row, enriched);
|
|
91
92
|
if (dynamicMode === 'infer') {
|
|
@@ -93,6 +94,16 @@ export function runtimeResolution(
|
|
|
93
94
|
if (inferred)
|
|
94
95
|
return resolvedRuntimeResolution(row, enriched, inferred, inferred.reasons);
|
|
95
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
|
+
};
|
|
106
|
+
}
|
|
96
107
|
if (!hasApplicableRuntimeVariables(evidence, options.vars)) {
|
|
97
108
|
const unresolvedReason = contextualUnresolvedReason ?? row.unresolved_reason;
|
|
98
109
|
const withSections = withEffectiveResolution(enriched, row, unresolvedReason);
|
|
@@ -103,9 +114,6 @@ export function runtimeResolution(
|
|
|
103
114
|
return resolvedRuntimeResolution(
|
|
104
115
|
row, enriched, resolution.target, resolution.reasons,
|
|
105
116
|
);
|
|
106
|
-
if (analysis && analysis.viableCandidateCount === 0
|
|
107
|
-
&& Object.keys(analysis.appliedSuppliedVariables).length > 0)
|
|
108
|
-
return noCandidateRuntimeResolution(row, enriched);
|
|
109
117
|
const unresolvedReason = runtimeUnresolvedReason(resolution);
|
|
110
118
|
return { row, evidence: withEffectiveResolution(enriched, row, unresolvedReason, resolution), unresolvedReason };
|
|
111
119
|
}
|
|
@@ -384,14 +392,23 @@ function evidenceWithDynamicAnalysis(
|
|
|
384
392
|
): Record<string, unknown> {
|
|
385
393
|
const persistedCandidates = recordArray(evidence.candidates);
|
|
386
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;
|
|
387
400
|
return {
|
|
388
401
|
...evidence,
|
|
389
402
|
candidates: persistedCandidates.slice(0, analysis.maxCandidates),
|
|
390
403
|
candidateScores: persistedScores.slice(0, analysis.maxCandidates),
|
|
391
|
-
persistedCandidateCount
|
|
404
|
+
persistedCandidateCount,
|
|
392
405
|
persistedCandidateOmittedCount: Math.max(
|
|
393
406
|
0,
|
|
394
|
-
persistedCandidates.length
|
|
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),
|
|
395
412
|
),
|
|
396
413
|
dynamicTargetExploration: {
|
|
397
414
|
mode: analysis.mode,
|
|
@@ -410,9 +427,16 @@ function evidenceWithDynamicAnalysis(
|
|
|
410
427
|
omittedRejectedCandidateCount: analysis.omittedRejectedCandidateCount,
|
|
411
428
|
rejectedCandidates: analysis.rejectedCandidates,
|
|
412
429
|
suggestedVarSets: analysis.suggestedVarSets,
|
|
430
|
+
suggestedVarSetCount: analysis.suggestedVarSetCount,
|
|
431
|
+
shownSuggestedVarSetCount: analysis.shownSuggestedVarSetCount,
|
|
432
|
+
omittedSuggestedVarSetCount: analysis.omittedSuggestedVarSetCount,
|
|
433
|
+
routingContext: analysis.routingContext,
|
|
413
434
|
},
|
|
414
435
|
dynamicTargetCandidates: analysis.candidates,
|
|
415
436
|
dynamicTargetCandidateSuggestions: analysis.shownCandidates,
|
|
437
|
+
dynamicTargetCandidateSuggestionCount: analysis.viableCandidateCount,
|
|
438
|
+
shownDynamicTargetCandidateSuggestionCount: analysis.shownCandidateCount,
|
|
439
|
+
omittedDynamicTargetCandidateSuggestionCount: analysis.omittedCandidateCount,
|
|
416
440
|
rejectedCandidates: analysis.rejectedCandidates,
|
|
417
441
|
dynamicTargetInference: analysis.inference,
|
|
418
442
|
};
|
|
@@ -429,6 +453,7 @@ const boundedDynamicListKeys = new Set([
|
|
|
429
453
|
'rejectedCandidateSuggestions',
|
|
430
454
|
'copyableExamples',
|
|
431
455
|
'conflicts',
|
|
456
|
+
'bindingAlternatives',
|
|
432
457
|
]);
|
|
433
458
|
|
|
434
459
|
function boundDynamicEvidence(
|
|
@@ -436,6 +461,7 @@ function boundDynamicEvidence(
|
|
|
436
461
|
limit: number,
|
|
437
462
|
): Record<string, unknown> {
|
|
438
463
|
const candidateCount = numeric(evidence.persistedCandidateCount)
|
|
464
|
+
|| numeric(evidence.candidateCount)
|
|
439
465
|
|| (Array.isArray(evidence.candidates) ? evidence.candidates.length : 0);
|
|
440
466
|
const projected = boundDynamicValue(evidence, limit);
|
|
441
467
|
const next = parseObject(projected);
|