@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.
- package/CHANGELOG.md +7 -0
- package/README.md +10 -5
- package/TECHNICAL-NOTE.md +8 -4
- package/dist/{chunk-YZJKE5UX.js → chunk-PTLDSHRC.js} +2412 -553
- package/dist/chunk-PTLDSHRC.js.map +1 -0
- package/dist/cli.js +280 -506
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +45 -7
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli.ts +75 -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/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/cross-repo-linker.ts +13 -1
- package/src/output/table-output.ts +29 -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-targets.ts +459 -229
- package/src/trace/evidence.ts +305 -46
- package/src/trace/selectors.ts +483 -0
- package/src/trace/trace-engine.ts +90 -83
- package/src/types.ts +27 -1
- package/dist/chunk-YZJKE5UX.js.map +0 -1
|
@@ -2,48 +2,36 @@ import type { Db } from '../db/connection.js';
|
|
|
2
2
|
import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
|
|
3
3
|
import type { OperationTarget } from '../linker/service-resolver.js';
|
|
4
4
|
import type { DynamicMode } from '../types.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
alias?: string;
|
|
36
|
-
destination?: string;
|
|
37
|
-
servicePath?: string;
|
|
38
|
-
sourceKind: string;
|
|
39
|
-
repoName: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface Templates {
|
|
43
|
-
servicePath?: string;
|
|
44
|
-
operationPath?: string;
|
|
45
|
-
alias?: string;
|
|
46
|
-
destination?: string;
|
|
5
|
+
import { uniqueIdentityDerivations } from './001-dynamic-identity.js';
|
|
6
|
+
import {
|
|
7
|
+
dynamicReferenceProvenance,
|
|
8
|
+
dynamicReferenceRows,
|
|
9
|
+
type DynamicReferenceRow,
|
|
10
|
+
} from './003-dynamic-references.js';
|
|
11
|
+
import type {
|
|
12
|
+
DynamicTargetAnalysis,
|
|
13
|
+
DynamicTargetCandidate,
|
|
14
|
+
DynamicTemplates,
|
|
15
|
+
DynamicVariableProvenance,
|
|
16
|
+
} from './000-dynamic-target-types.js';
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
DynamicTargetAnalysis,
|
|
20
|
+
DynamicTargetCandidate,
|
|
21
|
+
} from './000-dynamic-target-types.js';
|
|
22
|
+
|
|
23
|
+
type Templates = DynamicTemplates;
|
|
24
|
+
type VariableProvenance = DynamicVariableProvenance;
|
|
25
|
+
|
|
26
|
+
interface AnalysisInputs {
|
|
27
|
+
original: Templates;
|
|
28
|
+
effective: Templates;
|
|
29
|
+
required: string[];
|
|
30
|
+
requiredSources: Record<string, string[]>;
|
|
31
|
+
supplied: Record<string, string>;
|
|
32
|
+
order: string[];
|
|
33
|
+
callerRepo?: string;
|
|
34
|
+
callerRepoId?: number;
|
|
47
35
|
}
|
|
48
36
|
|
|
49
37
|
export function analyzeDynamicTargetCandidates(
|
|
@@ -53,32 +41,65 @@ export function analyzeDynamicTargetCandidates(
|
|
|
53
41
|
mode: DynamicMode,
|
|
54
42
|
maxCandidates: number,
|
|
55
43
|
): DynamicTargetAnalysis | undefined {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
db,
|
|
62
|
-
candidateTargets(db, evidence, workspaceId),
|
|
63
|
-
referenceRows(db, workspaceId),
|
|
64
|
-
templates,
|
|
65
|
-
order,
|
|
44
|
+
const inputs = analysisInputs(evidence);
|
|
45
|
+
if (inputs.required.length === 0) return undefined;
|
|
46
|
+
const targets = candidateTargets(db, evidence, workspaceId);
|
|
47
|
+
const references = dynamicReferenceRows(
|
|
48
|
+
db, workspaceId, inputs.callerRepoId, inputs.callerRepo,
|
|
66
49
|
);
|
|
67
|
-
const
|
|
68
|
-
|
|
50
|
+
const candidates = buildCandidates(db, targets, references, inputs);
|
|
51
|
+
applyUniqueIdentityEvidence(db, candidates, inputs);
|
|
52
|
+
finalizeCandidates(candidates, inputs.order);
|
|
53
|
+
const ranked = stableRank(candidates);
|
|
54
|
+
const inference = inferenceDecision(ranked);
|
|
55
|
+
applyModeState(ranked, mode, inference);
|
|
56
|
+
const viable = ranked.filter((candidate) => candidate.viable);
|
|
57
|
+
const rejected = ranked.filter((candidate) => candidate.rejected);
|
|
58
|
+
const shown = viable.slice(0, maxCandidates)
|
|
59
|
+
.map((candidate) => boundedCandidate(candidate, maxCandidates));
|
|
60
|
+
const shownRejected = rejected.slice(0, maxCandidates)
|
|
61
|
+
.map((candidate) => boundedCandidate(candidate, maxCandidates));
|
|
69
62
|
return {
|
|
70
63
|
mode,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
64
|
+
maxCandidates,
|
|
65
|
+
candidateCount: ranked.length,
|
|
66
|
+
viableCandidateCount: viable.length,
|
|
67
|
+
rejectedCandidateCount: rejected.length,
|
|
68
|
+
shownCandidateCount: shown.length,
|
|
69
|
+
omittedCandidateCount: Math.max(0, viable.length - shown.length),
|
|
70
|
+
shownRejectedCandidateCount: shownRejected.length,
|
|
71
|
+
omittedRejectedCandidateCount: Math.max(0, rejected.length - shownRejected.length),
|
|
72
|
+
missingVariables: inputs.required.filter((key) => inputs.supplied[key] === undefined),
|
|
73
|
+
requiredVariables: inputs.required,
|
|
74
|
+
suppliedVariables: inputs.supplied,
|
|
75
|
+
appliedSuppliedVariables: requiredSuppliedVariables(inputs),
|
|
76
|
+
substitutedSignals: inputs.effective,
|
|
77
|
+
candidates: shown,
|
|
78
|
+
shownCandidates: shown,
|
|
79
|
+
rejectedCandidates: shownRejected,
|
|
80
|
+
suggestedVarSets: suggestedVarSets(viable, inputs.order, maxCandidates),
|
|
78
81
|
inference,
|
|
79
82
|
};
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
function analysisInputs(evidence: Record<string, unknown>): AnalysisInputs {
|
|
86
|
+
const original = templatesFromEvidence(evidence, 'original');
|
|
87
|
+
const effective = templatesFromEvidence(evidence, 'effective');
|
|
88
|
+
const requiredSources = placeholderSources(original);
|
|
89
|
+
const required = Object.keys(requiredSources);
|
|
90
|
+
const supplied = stringRecord(evidence.suppliedRuntimeVariables);
|
|
91
|
+
return {
|
|
92
|
+
original,
|
|
93
|
+
effective,
|
|
94
|
+
required,
|
|
95
|
+
requiredSources,
|
|
96
|
+
supplied,
|
|
97
|
+
order: variableOrder(original, required),
|
|
98
|
+
callerRepo: stringValue(evidence.repo),
|
|
99
|
+
callerRepoId: numberValue(evidence.repoId),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
82
103
|
function candidateTargets(
|
|
83
104
|
db: Db,
|
|
84
105
|
evidence: Record<string, unknown>,
|
|
@@ -86,7 +107,7 @@ function candidateTargets(
|
|
|
86
107
|
): OperationTarget[] {
|
|
87
108
|
const embedded = rowsFromEvidence(evidence.candidates);
|
|
88
109
|
if (embedded.length > 0) return embedded;
|
|
89
|
-
const operationPath =
|
|
110
|
+
const operationPath = effectiveSignal(evidence, 'operationPath');
|
|
90
111
|
if (!operationPath || extractPlaceholders(operationPath).length > 0) return [];
|
|
91
112
|
return queryOperationTargets(db, operationPath, workspaceId);
|
|
92
113
|
}
|
|
@@ -103,7 +124,9 @@ function rowsFromEvidence(value: unknown): OperationTarget[] {
|
|
|
103
124
|
if (operationId === undefined || !repoName || !servicePath || !operationPath || !operationName) return [];
|
|
104
125
|
return [{
|
|
105
126
|
operationId,
|
|
127
|
+
repoId: numberValue(row.repoId),
|
|
106
128
|
repoName,
|
|
129
|
+
packageName: stringValue(row.packageName),
|
|
107
130
|
serviceName: stringValue(row.serviceName) ?? '',
|
|
108
131
|
qualifiedName: stringValue(row.qualifiedName) ?? '',
|
|
109
132
|
servicePath,
|
|
@@ -111,8 +134,6 @@ function rowsFromEvidence(value: unknown): OperationTarget[] {
|
|
|
111
134
|
operationName,
|
|
112
135
|
sourceFile: stringValue(row.sourceFile) ?? '',
|
|
113
136
|
sourceLine: numberValue(row.sourceLine) ?? 0,
|
|
114
|
-
repoId: numberValue(row.repoId),
|
|
115
|
-
packageName: stringValue(row.packageName),
|
|
116
137
|
score: numberValue(row.score) ?? 0,
|
|
117
138
|
reasons: stringArray(row.reasons),
|
|
118
139
|
}];
|
|
@@ -129,214 +150,392 @@ function queryOperationTargets(
|
|
|
129
150
|
`SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,
|
|
130
151
|
s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,
|
|
131
152
|
o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,
|
|
132
|
-
o.source_line sourceLine
|
|
133
|
-
|
|
134
|
-
JOIN repositories r ON r.id=s.repo_id
|
|
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
|
|
135
155
|
WHERE (? IS NULL OR r.workspace_id=?)
|
|
136
156
|
AND (o.operation_path IN (?,?) OR o.operation_name=?)
|
|
137
|
-
ORDER BY r.name,s.service_path,o.operation_name`,
|
|
138
|
-
).all(workspaceId, workspaceId, operationPath, `/${simple}`, simple)
|
|
139
|
-
return rows.
|
|
140
|
-
|
|
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,
|
|
141
171
|
repoId: numberValue(row.repoId),
|
|
142
|
-
repoName
|
|
172
|
+
repoName,
|
|
143
173
|
packageName: stringValue(row.packageName),
|
|
144
|
-
serviceName:
|
|
145
|
-
qualifiedName:
|
|
146
|
-
servicePath
|
|
147
|
-
operationPath
|
|
148
|
-
operationName
|
|
149
|
-
sourceFile:
|
|
150
|
-
sourceLine:
|
|
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,
|
|
151
181
|
score: 0.2,
|
|
152
182
|
reasons: ['operation_path_match'],
|
|
153
|
-
}
|
|
183
|
+
}];
|
|
154
184
|
}
|
|
155
185
|
|
|
156
|
-
function
|
|
186
|
+
function buildCandidates(
|
|
157
187
|
db: Db,
|
|
158
|
-
|
|
159
|
-
references:
|
|
160
|
-
|
|
161
|
-
order: string[],
|
|
188
|
+
targets: OperationTarget[],
|
|
189
|
+
references: DynamicReferenceRow[],
|
|
190
|
+
inputs: AnalysisInputs,
|
|
162
191
|
): DynamicTargetCandidate[] {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
192
|
+
return targets.map((target) => {
|
|
193
|
+
const state = emptyCandidate(target, inputs);
|
|
194
|
+
applyDirectSignal(state, inputs, 'operationPath', target.operationPath, 0.25);
|
|
195
|
+
applyDirectSignal(state, inputs, 'servicePath', target.servicePath, 0.35);
|
|
196
|
+
const matchingReferences = references.filter((reference) =>
|
|
197
|
+
reference.servicePath === target.servicePath);
|
|
198
|
+
applyReferenceSignal(state, inputs, matchingReferences, 'alias');
|
|
199
|
+
applyReferenceSignal(state, inputs, matchingReferences, 'destination');
|
|
200
|
+
if (hasResolvedImplementation(db, target.operationId))
|
|
201
|
+
addScore(state, 0.1, 'implementation_edge_resolved');
|
|
202
|
+
return state;
|
|
203
|
+
});
|
|
169
204
|
}
|
|
170
205
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
references: ReferenceRow[],
|
|
175
|
-
templates: Templates,
|
|
176
|
-
order: string[],
|
|
206
|
+
function emptyCandidate(
|
|
207
|
+
target: OperationTarget,
|
|
208
|
+
inputs: AnalysisInputs,
|
|
177
209
|
): DynamicTargetCandidate {
|
|
178
|
-
const state = emptyCandidate(candidate);
|
|
179
|
-
applyDirectTemplate(state, templates.operationPath, candidate.operationPath, 'operation_path');
|
|
180
|
-
applyDirectTemplate(state, templates.servicePath, candidate.servicePath, 'service_path');
|
|
181
|
-
const refs = references.filter((item) => item.servicePath === candidate.servicePath);
|
|
182
|
-
applyReferenceTemplate(state, templates.alias, refs, 'alias');
|
|
183
|
-
applyReferenceTemplate(state, templates.destination, refs, 'destination');
|
|
184
|
-
if (hasResolvedImplementation(db, candidate.operationId)) addScore(state, 0.1, 'implementation_edge_resolved');
|
|
185
|
-
state.missingVariables = order.filter((key) => state.derivedVariables[key] === undefined);
|
|
186
|
-
if (state.missingVariables.length === 0) addScore(state, 0.15, 'all_runtime_variables_derived');
|
|
187
|
-
else state.rejectedReasons.push('missing_required_runtime_variable');
|
|
188
|
-
state.score = Math.max(0, Math.min(1, state.score));
|
|
189
|
-
state.cli = state.missingVariables.length === 0 ? cliFor(state.derivedVariables, order) : undefined;
|
|
190
|
-
return state;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function emptyCandidate(candidate: OperationTarget): DynamicTargetCandidate {
|
|
194
210
|
return {
|
|
195
|
-
candidateOperationId:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
211
|
+
candidateOperationId: target.operationId,
|
|
212
|
+
repoId: target.repoId,
|
|
213
|
+
repoName: target.repoName,
|
|
214
|
+
packageName: target.packageName ?? undefined,
|
|
215
|
+
serviceName: target.serviceName,
|
|
216
|
+
qualifiedName: target.qualifiedName,
|
|
217
|
+
servicePath: target.servicePath,
|
|
218
|
+
operationPath: target.operationPath,
|
|
219
|
+
operationName: target.operationName,
|
|
220
|
+
sourceFile: target.sourceFile,
|
|
221
|
+
sourceLine: target.sourceLine,
|
|
222
|
+
originalTemplates: inputs.original,
|
|
223
|
+
effectiveValues: inputs.effective,
|
|
224
|
+
requiredVariables: inputs.required,
|
|
225
|
+
requiredVariableSources: inputs.requiredSources,
|
|
226
|
+
suppliedVariables: inputs.supplied,
|
|
227
|
+
completeVariables: { ...inputs.supplied },
|
|
201
228
|
derivedVariables: {},
|
|
202
229
|
derivedVariableSources: {},
|
|
230
|
+
derivationProvenance: {},
|
|
203
231
|
missingVariables: [],
|
|
204
|
-
|
|
205
|
-
|
|
232
|
+
conflicts: [],
|
|
233
|
+
score: Math.max(0.2, Number(target.score ?? 0)),
|
|
234
|
+
explicitSignalStrength: 0,
|
|
235
|
+
reasons: nonEmptyStrings(target.reasons, ['operation_path_match']),
|
|
206
236
|
rejectedReasons: [],
|
|
237
|
+
inferenceBlockReasons: [],
|
|
238
|
+
viable: true,
|
|
239
|
+
rejected: false,
|
|
240
|
+
selected: false,
|
|
241
|
+
exploratory: false,
|
|
207
242
|
};
|
|
208
243
|
}
|
|
209
244
|
|
|
210
|
-
function
|
|
245
|
+
function applyDirectSignal(
|
|
211
246
|
state: DynamicTargetCandidate,
|
|
212
|
-
|
|
247
|
+
inputs: AnalysisInputs,
|
|
248
|
+
kind: 'servicePath' | 'operationPath',
|
|
213
249
|
concrete: string,
|
|
214
|
-
|
|
250
|
+
score: number,
|
|
215
251
|
): void {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
if (!
|
|
219
|
-
state
|
|
252
|
+
const effective = inputs.effective[kind];
|
|
253
|
+
const original = inputs.original[kind];
|
|
254
|
+
if (effective && !matchTemplate(effective, concrete)) {
|
|
255
|
+
reject(state, `${signalCode(kind)}_contradicts_runtime_substitution`);
|
|
220
256
|
return;
|
|
221
257
|
}
|
|
222
|
-
|
|
223
|
-
|
|
258
|
+
if (!effective) return;
|
|
259
|
+
const suppliedKeys = extractPlaceholders(original)
|
|
260
|
+
.filter((key) => inputs.supplied[key] !== undefined);
|
|
261
|
+
state.explicitSignalStrength += suppliedKeys.length;
|
|
262
|
+
const matched = matchTemplate(original, concrete) ?? {};
|
|
263
|
+
for (const [key, value] of Object.entries(matched)) {
|
|
264
|
+
addDerivation(state, key, value, {
|
|
265
|
+
sourceKind: `${signalCode(kind)}_template`,
|
|
266
|
+
value,
|
|
267
|
+
rule: 'exact_template_match',
|
|
268
|
+
template: original,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
addScore(state, score, `${signalCode(kind)}_template_match`);
|
|
224
272
|
}
|
|
225
273
|
|
|
226
|
-
function
|
|
274
|
+
function applyReferenceSignal(
|
|
227
275
|
state: DynamicTargetCandidate,
|
|
228
|
-
|
|
229
|
-
|
|
276
|
+
inputs: AnalysisInputs,
|
|
277
|
+
references: DynamicReferenceRow[],
|
|
230
278
|
kind: 'alias' | 'destination',
|
|
231
279
|
): void {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
280
|
+
const original = inputs.original[kind];
|
|
281
|
+
const effective = inputs.effective[kind];
|
|
282
|
+
if (!original || extractPlaceholders(original).length === 0) return;
|
|
283
|
+
const values = references.flatMap((reference) => {
|
|
284
|
+
const concrete = kind === 'alias' ? reference.alias : reference.destination;
|
|
285
|
+
return isConcrete(concrete) ? [{ reference, concrete }] : [];
|
|
286
|
+
});
|
|
287
|
+
if (effective && extractPlaceholders(effective).length === 0
|
|
288
|
+
&& values.length > 0 && !values.some(({ concrete }) => concrete === effective)) {
|
|
289
|
+
reject(state, `${kind}_contradicts_runtime_substitution`);
|
|
290
|
+
}
|
|
291
|
+
let matchedSignal = false;
|
|
292
|
+
for (const { reference, concrete } of values) {
|
|
293
|
+
const matched = matchTemplate(original, concrete);
|
|
236
294
|
if (!matched) continue;
|
|
237
|
-
|
|
295
|
+
matchedSignal = true;
|
|
296
|
+
for (const [key, value] of Object.entries(matched)) {
|
|
297
|
+
addDerivation(
|
|
298
|
+
state, key, value,
|
|
299
|
+
dynamicReferenceProvenance(reference, kind, original, value),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (matchedSignal) {
|
|
304
|
+
state.explicitSignalStrength += extractPlaceholders(original)
|
|
305
|
+
.filter((key) => inputs.supplied[key] !== undefined).length;
|
|
238
306
|
addScore(state, 0.2, `${kind}_template_match`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function applyUniqueIdentityEvidence(
|
|
311
|
+
db: Db,
|
|
312
|
+
candidates: DynamicTargetCandidate[],
|
|
313
|
+
inputs: AnalysisInputs,
|
|
314
|
+
): void {
|
|
315
|
+
for (const derivation of uniqueIdentityDerivations(db, candidates, inputs.original)) {
|
|
316
|
+
const candidate = candidates.find((item) =>
|
|
317
|
+
item.candidateOperationId === derivation.operationId);
|
|
318
|
+
if (!candidate) continue;
|
|
319
|
+
addDerivation(candidate, derivation.key, derivation.value, derivation.provenance);
|
|
320
|
+
addScore(candidate, 0.2, 'exact_identity_template_match');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function addDerivation(
|
|
325
|
+
state: DynamicTargetCandidate,
|
|
326
|
+
key: string,
|
|
327
|
+
value: string,
|
|
328
|
+
provenance: VariableProvenance,
|
|
329
|
+
): void {
|
|
330
|
+
const priorProvenance = state.derivationProvenance[key] ?? [];
|
|
331
|
+
state.derivationProvenance[key] = uniqueProvenance([...priorProvenance, provenance]);
|
|
332
|
+
const supplied = state.suppliedVariables[key];
|
|
333
|
+
if (supplied !== undefined && supplied !== value) {
|
|
334
|
+
addConflict(state, key, [supplied, value], 'explicit_value_conflicts_with_derived_value');
|
|
239
335
|
return;
|
|
240
336
|
}
|
|
241
|
-
|
|
242
|
-
|
|
337
|
+
const prior = state.derivedVariables[key];
|
|
338
|
+
if (prior !== undefined && prior !== value) {
|
|
339
|
+
addConflict(state, key, [prior, value], 'conflicting_strong_derivations');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (supplied === undefined) state.derivedVariables[key] = value;
|
|
343
|
+
state.completeVariables[key] = supplied ?? value;
|
|
344
|
+
state.derivedVariableSources[key] ??= provenance;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function addConflict(
|
|
348
|
+
state: DynamicTargetCandidate,
|
|
349
|
+
key: string,
|
|
350
|
+
values: string[],
|
|
351
|
+
reason: string,
|
|
352
|
+
): void {
|
|
353
|
+
const sources = (state.derivationProvenance[key] ?? [])
|
|
354
|
+
.map((item) => item.sourceKind).sort();
|
|
355
|
+
state.conflicts.push({ key, values: [...new Set(values)].sort(), reason, sources });
|
|
356
|
+
reject(state, reason);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function uniqueProvenance(rows: VariableProvenance[]): VariableProvenance[] {
|
|
360
|
+
const sorted = [...rows].sort((left, right) =>
|
|
361
|
+
left.sourceKind.localeCompare(right.sourceKind)
|
|
362
|
+
|| String(left.matchedName ?? '').localeCompare(String(right.matchedName ?? ''))
|
|
363
|
+
|| left.value.localeCompare(right.value));
|
|
364
|
+
const seen = new Set<string>();
|
|
365
|
+
return sorted.filter((row) => {
|
|
366
|
+
const key = JSON.stringify(row);
|
|
367
|
+
if (seen.has(key)) return false;
|
|
368
|
+
seen.add(key);
|
|
369
|
+
return true;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function finalizeCandidates(candidates: DynamicTargetCandidate[], order: string[]): void {
|
|
374
|
+
for (const candidate of candidates) {
|
|
375
|
+
candidate.missingVariables = order.filter((key) =>
|
|
376
|
+
candidate.completeVariables[key] === undefined);
|
|
377
|
+
candidate.viable = candidate.rejectedReasons.length === 0;
|
|
378
|
+
candidate.rejected = !candidate.viable;
|
|
379
|
+
if (candidate.missingVariables.length === 0 && candidate.viable)
|
|
380
|
+
addScore(candidate, 0.15, 'all_runtime_variables_derived');
|
|
381
|
+
else if (candidate.missingVariables.length > 0)
|
|
382
|
+
addReason(candidate, 'missing_required_runtime_variable');
|
|
383
|
+
candidate.score = Math.max(0, Math.min(1, candidate.score));
|
|
384
|
+
candidate.cli = candidate.missingVariables.length === 0 && candidate.viable
|
|
385
|
+
? cliFor(candidate.completeVariables, order)
|
|
386
|
+
: undefined;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function stableRank(candidates: DynamicTargetCandidate[]): DynamicTargetCandidate[] {
|
|
391
|
+
return [...candidates].sort((left, right) =>
|
|
392
|
+
Number(right.viable) - Number(left.viable)
|
|
393
|
+
|| right.score - left.score
|
|
394
|
+
|| right.explicitSignalStrength - left.explicitSignalStrength
|
|
395
|
+
|| left.repoName.localeCompare(right.repoName)
|
|
396
|
+
|| String(left.packageName ?? '').localeCompare(String(right.packageName ?? ''))
|
|
397
|
+
|| left.servicePath.localeCompare(right.servicePath)
|
|
398
|
+
|| left.operationPath.localeCompare(right.operationPath)
|
|
399
|
+
|| left.operationName.localeCompare(right.operationName)
|
|
400
|
+
|| left.candidateOperationId - right.candidateOperationId);
|
|
243
401
|
}
|
|
244
402
|
|
|
245
403
|
function inferenceDecision(candidates: DynamicTargetCandidate[]): Record<string, unknown> {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
404
|
+
const viable = candidates.filter((candidate) => candidate.viable);
|
|
405
|
+
const first = viable[0];
|
|
406
|
+
const second = viable[1];
|
|
407
|
+
if (!first || first.missingVariables.length > 0)
|
|
408
|
+
return { status: 'unresolved', reason: 'missing_required_runtime_variable' };
|
|
409
|
+
if (first.score < 0.85)
|
|
410
|
+
return { status: 'unresolved', reason: 'candidate_score_below_inference_threshold' };
|
|
411
|
+
const scoreGap = second
|
|
412
|
+
? Number((first.score - second.score).toFixed(12))
|
|
413
|
+
: undefined;
|
|
414
|
+
if (second && scoreGap !== undefined && scoreGap <= 0.05) {
|
|
415
|
+
const reason = scoreGap === 0
|
|
416
|
+
? 'candidate_tied_with_equal_score'
|
|
417
|
+
: 'candidate_within_inference_margin';
|
|
418
|
+
for (const candidate of viable.filter((item) => first.score - item.score <= 0.05))
|
|
419
|
+
addInferenceBlock(candidate, reason);
|
|
420
|
+
return { status: 'ambiguous', reason, scoreGap, requiredMargin: 0.05 };
|
|
257
421
|
}
|
|
258
422
|
return {
|
|
259
423
|
status: 'resolved',
|
|
260
424
|
candidateOperationId: first.candidateOperationId,
|
|
261
|
-
inferredVariables: first.
|
|
425
|
+
inferredVariables: first.completeVariables,
|
|
262
426
|
score: first.score,
|
|
263
427
|
reasons: first.reasons,
|
|
264
428
|
};
|
|
265
429
|
}
|
|
266
430
|
|
|
431
|
+
function boundedCandidate(
|
|
432
|
+
candidate: DynamicTargetCandidate,
|
|
433
|
+
limit: number,
|
|
434
|
+
): DynamicTargetCandidate {
|
|
435
|
+
const derivationProvenance = Object.fromEntries(
|
|
436
|
+
Object.entries(candidate.derivationProvenance)
|
|
437
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
438
|
+
.map(([key, rows]) => [key, rows.slice(0, limit)]),
|
|
439
|
+
);
|
|
440
|
+
const conflicts = candidate.conflicts.slice(0, limit).map((conflict) => ({
|
|
441
|
+
...conflict,
|
|
442
|
+
sources: conflict.sources.slice(0, limit),
|
|
443
|
+
}));
|
|
444
|
+
return { ...candidate, derivationProvenance, conflicts };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function applyModeState(
|
|
448
|
+
candidates: DynamicTargetCandidate[],
|
|
449
|
+
mode: DynamicMode,
|
|
450
|
+
inference: Record<string, unknown>,
|
|
451
|
+
): void {
|
|
452
|
+
const selectedId = mode === 'infer' && inference.status === 'resolved'
|
|
453
|
+
? numberValue(inference.candidateOperationId)
|
|
454
|
+
: undefined;
|
|
455
|
+
for (const candidate of candidates) {
|
|
456
|
+
candidate.selected = selectedId === candidate.candidateOperationId;
|
|
457
|
+
candidate.exploratory = mode === 'candidates' && candidate.viable;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
267
461
|
function suggestedVarSets(
|
|
268
462
|
candidates: DynamicTargetCandidate[],
|
|
269
|
-
required: string[],
|
|
270
463
|
order: string[],
|
|
464
|
+
limit: number,
|
|
271
465
|
): Array<{ variables: Record<string, string>; cli: string }> {
|
|
272
466
|
const seen = new Set<string>();
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (seen.has(cli))
|
|
277
|
-
seen.add(cli);
|
|
278
|
-
|
|
279
|
-
|
|
467
|
+
const rows: Array<{ variables: Record<string, string>; cli: string }> = [];
|
|
468
|
+
for (const candidate of candidates) {
|
|
469
|
+
if (!candidate.cli || candidate.missingVariables.length > 0) continue;
|
|
470
|
+
if (seen.has(candidate.cli)) continue;
|
|
471
|
+
seen.add(candidate.cli);
|
|
472
|
+
rows.push({ variables: orderedVariables(candidate.completeVariables, order), cli: candidate.cli });
|
|
473
|
+
if (rows.length >= limit) break;
|
|
474
|
+
}
|
|
475
|
+
return rows;
|
|
280
476
|
}
|
|
281
477
|
|
|
282
|
-
function
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
FROM cds_requires req JOIN repositories r ON r.id=req.repo_id
|
|
287
|
-
WHERE (? IS NULL OR r.workspace_id=?)
|
|
288
|
-
UNION ALL
|
|
289
|
-
SELECT COALESCE(b.alias,b.alias_expr) alias,b.destination_expr destination,
|
|
290
|
-
b.service_path_expr servicePath,'service_binding' sourceKind,r.name repoName
|
|
291
|
-
FROM service_bindings b JOIN repositories r ON r.id=b.repo_id
|
|
292
|
-
WHERE (? IS NULL OR r.workspace_id=?)`,
|
|
293
|
-
).all(workspaceId, workspaceId, workspaceId, workspaceId) as Array<Record<string, unknown>>;
|
|
294
|
-
return rows.map((row) => ({
|
|
295
|
-
alias: stringValue(row.alias),
|
|
296
|
-
destination: stringValue(row.destination),
|
|
297
|
-
servicePath: stringValue(row.servicePath),
|
|
298
|
-
sourceKind: String(row.sourceKind),
|
|
299
|
-
repoName: String(row.repoName),
|
|
300
|
-
}));
|
|
478
|
+
function hasResolvedImplementation(db: Db, operationId: number): boolean {
|
|
479
|
+
return Boolean(db.prepare(
|
|
480
|
+
"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
|
+
).get(String(operationId)));
|
|
301
482
|
}
|
|
302
483
|
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
484
|
+
function templatesFromEvidence(
|
|
485
|
+
evidence: Record<string, unknown>,
|
|
486
|
+
phase: 'original' | 'effective',
|
|
487
|
+
): Templates {
|
|
488
|
+
return {
|
|
489
|
+
servicePath: substitutionSignal(evidence, 'servicePath', phase),
|
|
490
|
+
operationPath: substitutionSignal(evidence, 'operationPath', phase),
|
|
491
|
+
alias: substitutionSignal(evidence,
|
|
492
|
+
evidence.serviceAliasExpr !== undefined ? 'serviceAliasExpr' : 'serviceAlias', phase),
|
|
493
|
+
destination: substitutionSignal(evidence, 'destination', phase),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function substitutionSignal(
|
|
498
|
+
evidence: Record<string, unknown>,
|
|
499
|
+
key: string,
|
|
500
|
+
phase: 'original' | 'effective',
|
|
501
|
+
): string | undefined {
|
|
502
|
+
const substitution = record(record(evidence.runtimeSubstitutions)[key]);
|
|
503
|
+
return stringValue(substitution[phase]) ?? stringValue(evidence[key]);
|
|
313
504
|
}
|
|
314
505
|
|
|
315
|
-
function
|
|
506
|
+
function effectiveSignal(evidence: Record<string, unknown>, key: string): string | undefined {
|
|
507
|
+
return substitutionSignal(evidence, key, 'effective');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function placeholderSources(templates: Templates): Record<string, string[]> {
|
|
511
|
+
const sources: Record<string, string[]> = {};
|
|
512
|
+
for (const [kind, template] of Object.entries(templates)) {
|
|
513
|
+
if (typeof template !== 'string') continue;
|
|
514
|
+
for (const key of extractPlaceholders(template))
|
|
515
|
+
sources[key] = [...new Set([...(sources[key] ?? []), `${kind}:${template}`])].sort();
|
|
516
|
+
}
|
|
517
|
+
return Object.fromEntries(Object.entries(sources).sort(([left], [right]) =>
|
|
518
|
+
left.localeCompare(right)));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function variableOrder(templates: Templates, required: string[]): string[] {
|
|
316
522
|
const ordered = [
|
|
317
523
|
templates.servicePath,
|
|
318
524
|
templates.operationPath,
|
|
319
525
|
templates.alias,
|
|
320
526
|
templates.destination,
|
|
321
527
|
].flatMap((value) => extractPlaceholders(value));
|
|
322
|
-
return [...new Set([...ordered, ...
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function templatesFromEvidence(evidence: Record<string, unknown>): Templates {
|
|
326
|
-
return {
|
|
327
|
-
servicePath: stringValue(evidence.servicePath),
|
|
328
|
-
operationPath: stringValue(evidence.operationPath),
|
|
329
|
-
alias: stringValue(evidence.serviceAliasExpr ?? evidence.serviceAlias),
|
|
330
|
-
destination: stringValue(evidence.destination),
|
|
331
|
-
};
|
|
528
|
+
return [...new Set([...ordered, ...required])];
|
|
332
529
|
}
|
|
333
530
|
|
|
334
|
-
function matchTemplate(
|
|
531
|
+
function matchTemplate(
|
|
532
|
+
template: string | undefined,
|
|
533
|
+
concrete: string | undefined,
|
|
534
|
+
): Record<string, string> | undefined {
|
|
335
535
|
if (!template || !concrete) return undefined;
|
|
336
536
|
const keys = extractPlaceholders(template);
|
|
337
537
|
if (keys.length === 0) return template === concrete ? {} : undefined;
|
|
338
|
-
const
|
|
339
|
-
const match = regex.exec(concrete);
|
|
538
|
+
const match = new RegExp(`^${templateToPattern(template)}$`).exec(concrete);
|
|
340
539
|
if (!match) return undefined;
|
|
341
540
|
const values: Record<string, string> = {};
|
|
342
541
|
for (let index = 0; index < keys.length; index += 1) {
|
|
@@ -360,42 +559,70 @@ function templateToPattern(template: string): string {
|
|
|
360
559
|
return `${pattern}${escapeRegex(template.slice(lastIndex))}`;
|
|
361
560
|
}
|
|
362
561
|
|
|
363
|
-
function
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
562
|
+
function cliFor(variables: Record<string, string>, order: string[]): string {
|
|
563
|
+
return order.filter((key) => variables[key] !== undefined)
|
|
564
|
+
.map((key) => `--var ${shellArgument(`${key}=${variables[key]}`)}`).join(' ');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function shellArgument(value: string): string {
|
|
568
|
+
return /^[A-Za-z0-9_./:=+-]+$/.test(value)
|
|
569
|
+
? value
|
|
570
|
+
: `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function orderedVariables(
|
|
574
|
+
variables: Record<string, string>,
|
|
575
|
+
order: string[],
|
|
576
|
+
): Record<string, string> {
|
|
577
|
+
return Object.fromEntries(order.flatMap((key) =>
|
|
578
|
+
variables[key] === undefined ? [] : [[key, variables[key]]]));
|
|
376
579
|
}
|
|
377
580
|
|
|
378
581
|
function addScore(state: DynamicTargetCandidate, amount: number, reason: string): void {
|
|
379
582
|
state.score += amount;
|
|
583
|
+
addReason(state, reason);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function addReason(state: DynamicTargetCandidate, reason: string): void {
|
|
380
587
|
if (!state.reasons.includes(reason)) state.reasons.push(reason);
|
|
381
588
|
}
|
|
382
589
|
|
|
383
|
-
function
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return Boolean(row);
|
|
590
|
+
function reject(state: DynamicTargetCandidate, reason: string): void {
|
|
591
|
+
rejectReasonOnly(state, reason);
|
|
592
|
+
state.viable = false;
|
|
593
|
+
state.rejected = true;
|
|
388
594
|
}
|
|
389
595
|
|
|
390
|
-
function
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
596
|
+
function rejectReasonOnly(state: DynamicTargetCandidate, reason: string): void {
|
|
597
|
+
if (!state.rejectedReasons.includes(reason)) state.rejectedReasons.push(reason);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function addInferenceBlock(state: DynamicTargetCandidate, reason: string): void {
|
|
601
|
+
addReason(state, reason);
|
|
602
|
+
if (!state.inferenceBlockReasons.includes(reason))
|
|
603
|
+
state.inferenceBlockReasons.push(reason);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function requiredSuppliedVariables(inputs: AnalysisInputs): Record<string, string> {
|
|
607
|
+
return Object.fromEntries(inputs.required.flatMap((key) =>
|
|
608
|
+
inputs.supplied[key] === undefined ? [] : [[key, inputs.supplied[key]]]));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function signalCode(kind: 'servicePath' | 'operationPath'): string {
|
|
612
|
+
return kind === 'servicePath' ? 'service_path' : 'operation_path';
|
|
395
613
|
}
|
|
396
614
|
|
|
397
615
|
function record(value: unknown): Record<string, unknown> {
|
|
398
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
616
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
617
|
+
? value as Record<string, unknown>
|
|
618
|
+
: {};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function stringRecord(value: unknown): Record<string, string> {
|
|
622
|
+
const entries = Object.entries(record(value))
|
|
623
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
|
624
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
625
|
+
return Object.fromEntries(entries);
|
|
399
626
|
}
|
|
400
627
|
|
|
401
628
|
function stringValue(value: unknown): string | undefined {
|
|
@@ -407,7 +634,9 @@ function numberValue(value: unknown): number | undefined {
|
|
|
407
634
|
}
|
|
408
635
|
|
|
409
636
|
function stringArray(value: unknown): string[] {
|
|
410
|
-
return Array.isArray(value)
|
|
637
|
+
return Array.isArray(value)
|
|
638
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
639
|
+
: [];
|
|
411
640
|
}
|
|
412
641
|
|
|
413
642
|
function nonEmptyStrings(value: unknown, fallback: string[]): string[] {
|
|
@@ -416,7 +645,8 @@ function nonEmptyStrings(value: unknown, fallback: string[]): string[] {
|
|
|
416
645
|
}
|
|
417
646
|
|
|
418
647
|
function isConcrete(value: unknown): value is string {
|
|
419
|
-
return typeof value === 'string' && value.length > 0
|
|
648
|
+
return typeof value === 'string' && value.length > 0
|
|
649
|
+
&& extractPlaceholders(value).length === 0;
|
|
420
650
|
}
|
|
421
651
|
|
|
422
652
|
function escapeRegex(value: string): string {
|