@saptools/service-flow 0.1.51 → 0.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +16 -14
- package/TECHNICAL-NOTE.md +18 -6
- package/dist/{chunk-YZJKE5UX.js → chunk-LFH7C46B.js} +4290 -1261
- package/dist/chunk-LFH7C46B.js.map +1 -0
- package/dist/cli.js +435 -515
- 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/001-doctor-projection.ts +140 -0
- package/src/cli/doctor.ts +20 -3
- 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 +130 -24
- 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/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 -441
- 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 +33 -5
- 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 +96 -0
- package/src/trace/001-dynamic-identity.ts +308 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +283 -0
- 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 +582 -306
- package/src/trace/evidence.ts +331 -46
- package/src/trace/implementation-hints.ts +148 -8
- package/src/trace/selectors.ts +551 -3
- package/src/trace/trace-engine.ts +129 -135
- package/src/types.ts +27 -1
- package/src/utils/000-bounded-projection.ts +161 -0
- package/dist/chunk-YZJKE5UX.js.map +0 -1
|
@@ -1,49 +1,43 @@
|
|
|
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';
|
|
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
|
-
|
|
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;
|
|
10
|
+
import { dynamicCandidateTargets } from './004-dynamic-candidate-sources.js';
|
|
11
|
+
import { projectBounded } from '../utils/000-bounded-projection.js';
|
|
12
|
+
import { uniqueIdentityDerivations } from './001-dynamic-identity.js';
|
|
13
|
+
import {
|
|
14
|
+
dynamicReferenceProvenance,
|
|
15
|
+
dynamicRoutingContext,
|
|
16
|
+
type DynamicReferenceRow,
|
|
17
|
+
type DynamicRoutingContext,
|
|
18
|
+
} from './003-dynamic-references.js';
|
|
19
|
+
import type {
|
|
20
|
+
DynamicTargetAnalysis,
|
|
21
|
+
DynamicTargetCandidate,
|
|
22
|
+
DynamicTemplates,
|
|
23
|
+
DynamicVariableProvenance,
|
|
24
|
+
} from './000-dynamic-target-types.js';
|
|
25
|
+
export type {
|
|
26
|
+
DynamicTargetAnalysis,
|
|
27
|
+
DynamicTargetCandidate,
|
|
28
|
+
} from './000-dynamic-target-types.js';
|
|
29
|
+
type Templates = DynamicTemplates;
|
|
30
|
+
type VariableProvenance = DynamicVariableProvenance;
|
|
31
|
+
interface AnalysisInputs {
|
|
32
|
+
original: Templates;
|
|
33
|
+
effective: Templates;
|
|
34
|
+
required: string[];
|
|
35
|
+
requiredSources: Record<string, string[]>;
|
|
36
|
+
supplied: Record<string, string>;
|
|
37
|
+
order: string[];
|
|
38
|
+
callerRepo?: string;
|
|
39
|
+
callerRepoId?: number;
|
|
40
|
+
routing: DynamicRoutingContext;
|
|
47
41
|
}
|
|
48
42
|
|
|
49
43
|
export function analyzeDynamicTargetCandidates(
|
|
@@ -53,349 +47,632 @@ export function analyzeDynamicTargetCandidates(
|
|
|
53
47
|
mode: DynamicMode,
|
|
54
48
|
maxCandidates: number,
|
|
55
49
|
): DynamicTargetAnalysis | undefined {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const order = variableOrder(templates, missingVariables);
|
|
60
|
-
const candidates = rankedCandidates(
|
|
50
|
+
const inputs = analysisInputs(db, evidence, workspaceId);
|
|
51
|
+
if (inputs.required.length === 0) return undefined;
|
|
52
|
+
const targets = dynamicCandidateTargets(
|
|
61
53
|
db,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
inputs.effective.operationPath,
|
|
55
|
+
inputs.original.operationPath,
|
|
56
|
+
evidence.candidates,
|
|
57
|
+
workspaceId,
|
|
58
|
+
inputs.routing.outboundCallId !== undefined,
|
|
66
59
|
);
|
|
67
|
-
const
|
|
68
|
-
|
|
60
|
+
const candidates = buildCandidates(db, targets, inputs.routing.references, inputs);
|
|
61
|
+
applyUniqueIdentityEvidence(db, candidates, inputs);
|
|
62
|
+
finalizeCandidates(candidates, inputs.order);
|
|
63
|
+
const ranked = stableRank(candidates);
|
|
64
|
+
const inference = inferenceDecision(ranked);
|
|
65
|
+
applyModeState(ranked, mode, inference);
|
|
66
|
+
const viable = ranked.filter((candidate) => candidate.viable);
|
|
67
|
+
const rejected = ranked.filter((candidate) => candidate.rejected);
|
|
68
|
+
const shown = viable.slice(0, maxCandidates)
|
|
69
|
+
.map((candidate) => boundedCandidate(candidate, maxCandidates));
|
|
70
|
+
const shownRejected = rejected.slice(0, maxCandidates)
|
|
71
|
+
.map((candidate) => boundedCandidate(candidate, maxCandidates));
|
|
72
|
+
const suggestionProjection = suggestedVarSets(viable, inputs.order, maxCandidates);
|
|
69
73
|
return {
|
|
70
74
|
mode,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
maxCandidates,
|
|
76
|
+
candidateCount: ranked.length,
|
|
77
|
+
viableCandidateCount: viable.length,
|
|
78
|
+
rejectedCandidateCount: rejected.length,
|
|
79
|
+
shownCandidateCount: shown.length,
|
|
80
|
+
omittedCandidateCount: Math.max(0, viable.length - shown.length),
|
|
81
|
+
shownRejectedCandidateCount: shownRejected.length,
|
|
82
|
+
omittedRejectedCandidateCount: Math.max(0, rejected.length - shownRejected.length),
|
|
83
|
+
missingVariables: inputs.required.filter((key) => inputs.supplied[key] === undefined),
|
|
84
|
+
requiredVariables: inputs.required,
|
|
85
|
+
suppliedVariables: inputs.supplied,
|
|
86
|
+
appliedSuppliedVariables: requiredSuppliedVariables(inputs),
|
|
87
|
+
substitutedSignals: inputs.effective,
|
|
88
|
+
candidates: shown,
|
|
89
|
+
shownCandidates: shown,
|
|
90
|
+
rejectedCandidates: shownRejected,
|
|
91
|
+
suggestedVarSets: suggestionProjection.items,
|
|
92
|
+
suggestedVarSetCount: suggestionProjection.totalCount,
|
|
93
|
+
shownSuggestedVarSetCount: suggestionProjection.shownCount,
|
|
94
|
+
omittedSuggestedVarSetCount: suggestionProjection.omittedCount,
|
|
78
95
|
inference,
|
|
96
|
+
routingContext: routingEvidence(inputs.routing),
|
|
79
97
|
};
|
|
80
98
|
}
|
|
81
99
|
|
|
82
|
-
function
|
|
100
|
+
function analysisInputs(
|
|
83
101
|
db: Db,
|
|
84
102
|
evidence: Record<string, unknown>,
|
|
85
103
|
workspaceId: number | undefined,
|
|
86
|
-
):
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return [{
|
|
105
|
-
operationId,
|
|
106
|
-
repoName,
|
|
107
|
-
serviceName: stringValue(row.serviceName) ?? '',
|
|
108
|
-
qualifiedName: stringValue(row.qualifiedName) ?? '',
|
|
109
|
-
servicePath,
|
|
110
|
-
operationPath,
|
|
111
|
-
operationName,
|
|
112
|
-
sourceFile: stringValue(row.sourceFile) ?? '',
|
|
113
|
-
sourceLine: numberValue(row.sourceLine) ?? 0,
|
|
114
|
-
repoId: numberValue(row.repoId),
|
|
115
|
-
packageName: stringValue(row.packageName),
|
|
116
|
-
score: numberValue(row.score) ?? 0,
|
|
117
|
-
reasons: stringArray(row.reasons),
|
|
118
|
-
}];
|
|
119
|
-
});
|
|
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);
|
|
109
|
+
const requiredSources = placeholderSources(original);
|
|
110
|
+
const required = Object.keys(requiredSources);
|
|
111
|
+
return {
|
|
112
|
+
original,
|
|
113
|
+
effective,
|
|
114
|
+
required,
|
|
115
|
+
requiredSources,
|
|
116
|
+
supplied,
|
|
117
|
+
order: variableOrder(original, required),
|
|
118
|
+
callerRepo: routing.callerRepo ?? stringValue(evidence.repo),
|
|
119
|
+
callerRepoId: routing.callerRepoId ?? numberValue(evidence.repoId),
|
|
120
|
+
routing,
|
|
121
|
+
};
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
function
|
|
124
|
+
function buildCandidates(
|
|
123
125
|
db: Db,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const simple = operationPath.replace(/^\//, '').split('.').at(-1) ?? operationPath;
|
|
128
|
-
const rows = db.prepare(
|
|
129
|
-
`SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,
|
|
130
|
-
s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,
|
|
131
|
-
o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,
|
|
132
|
-
o.source_line sourceLine
|
|
133
|
-
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
|
|
134
|
-
JOIN repositories r ON r.id=s.repo_id
|
|
135
|
-
WHERE (? IS NULL OR r.workspace_id=?)
|
|
136
|
-
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) as Array<Record<string, unknown>>;
|
|
139
|
-
return rows.map((row) => ({
|
|
140
|
-
operationId: Number(row.operationId),
|
|
141
|
-
repoId: numberValue(row.repoId),
|
|
142
|
-
repoName: String(row.repoName),
|
|
143
|
-
packageName: stringValue(row.packageName),
|
|
144
|
-
serviceName: String(row.serviceName),
|
|
145
|
-
qualifiedName: String(row.qualifiedName),
|
|
146
|
-
servicePath: String(row.servicePath),
|
|
147
|
-
operationPath: String(row.operationPath),
|
|
148
|
-
operationName: String(row.operationName),
|
|
149
|
-
sourceFile: String(row.sourceFile),
|
|
150
|
-
sourceLine: Number(row.sourceLine),
|
|
151
|
-
score: 0.2,
|
|
152
|
-
reasons: ['operation_path_match'],
|
|
153
|
-
}));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function rankedCandidates(
|
|
157
|
-
db: Db,
|
|
158
|
-
candidates: OperationTarget[],
|
|
159
|
-
references: ReferenceRow[],
|
|
160
|
-
templates: Templates,
|
|
161
|
-
order: string[],
|
|
126
|
+
targets: OperationTarget[],
|
|
127
|
+
references: DynamicReferenceRow[],
|
|
128
|
+
inputs: AnalysisInputs,
|
|
162
129
|
): DynamicTargetCandidate[] {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
130
|
+
return targets.map((target) => {
|
|
131
|
+
const state = emptyCandidate(target, inputs);
|
|
132
|
+
applyDirectSignal(state, inputs, 'operationPath', target.operationPath, 0.25);
|
|
133
|
+
applyDirectSignal(state, inputs, 'servicePath', target.servicePath, 0.35);
|
|
134
|
+
const matchingReferences = references.filter((reference) =>
|
|
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');
|
|
142
|
+
if (hasResolvedImplementation(db, target.operationId))
|
|
143
|
+
addScore(state, 0.1, 'implementation_edge_resolved');
|
|
144
|
+
return state;
|
|
145
|
+
});
|
|
169
146
|
}
|
|
170
147
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
|
|
178
|
+
function emptyCandidate(
|
|
179
|
+
target: OperationTarget,
|
|
180
|
+
inputs: AnalysisInputs,
|
|
177
181
|
): 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
182
|
return {
|
|
195
|
-
candidateOperationId:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
183
|
+
candidateOperationId: target.operationId,
|
|
184
|
+
repoId: target.repoId,
|
|
185
|
+
repoName: target.repoName,
|
|
186
|
+
packageName: target.packageName ?? undefined,
|
|
187
|
+
serviceName: target.serviceName,
|
|
188
|
+
qualifiedName: target.qualifiedName,
|
|
189
|
+
servicePath: target.servicePath,
|
|
190
|
+
operationPath: target.operationPath,
|
|
191
|
+
operationName: target.operationName,
|
|
192
|
+
sourceFile: target.sourceFile,
|
|
193
|
+
sourceLine: target.sourceLine,
|
|
194
|
+
originalTemplates: inputs.original,
|
|
195
|
+
effectiveValues: inputs.effective,
|
|
196
|
+
requiredVariables: inputs.required,
|
|
197
|
+
requiredVariableSources: inputs.requiredSources,
|
|
198
|
+
suppliedVariables: inputs.supplied,
|
|
199
|
+
completeVariables: { ...inputs.supplied },
|
|
201
200
|
derivedVariables: {},
|
|
202
201
|
derivedVariableSources: {},
|
|
202
|
+
derivationProvenance: {},
|
|
203
203
|
missingVariables: [],
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
conflicts: [],
|
|
205
|
+
score: Math.max(0.2, Number(target.score ?? 0)),
|
|
206
|
+
explicitSignalStrength: 0,
|
|
207
|
+
reasons: nonEmptyStrings(target.reasons, ['operation_path_match']),
|
|
206
208
|
rejectedReasons: [],
|
|
209
|
+
inferenceBlockReasons: [],
|
|
210
|
+
viable: true,
|
|
211
|
+
rejected: false,
|
|
212
|
+
selected: false,
|
|
213
|
+
exploratory: false,
|
|
207
214
|
};
|
|
208
215
|
}
|
|
209
216
|
|
|
210
|
-
function
|
|
217
|
+
function applyDirectSignal(
|
|
211
218
|
state: DynamicTargetCandidate,
|
|
212
|
-
|
|
219
|
+
inputs: AnalysisInputs,
|
|
220
|
+
kind: 'servicePath' | 'operationPath',
|
|
213
221
|
concrete: string,
|
|
214
|
-
|
|
222
|
+
score: number,
|
|
215
223
|
): void {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
if (!
|
|
219
|
-
state
|
|
224
|
+
const effective = inputs.effective[kind];
|
|
225
|
+
const original = inputs.original[kind];
|
|
226
|
+
if (effective && !matchRuntimeTemplate(effective, concrete)) {
|
|
227
|
+
reject(state, `${signalCode(kind)}_contradicts_runtime_substitution`);
|
|
220
228
|
return;
|
|
221
229
|
}
|
|
222
|
-
|
|
223
|
-
|
|
230
|
+
if (!effective) return;
|
|
231
|
+
const suppliedKeys = extractPlaceholders(original)
|
|
232
|
+
.filter((key) => inputs.supplied[key] !== undefined);
|
|
233
|
+
state.explicitSignalStrength += suppliedKeys.length;
|
|
234
|
+
const matched = matchRuntimeTemplate(original, concrete) ?? {};
|
|
235
|
+
const fromSelectedBinding = kind === 'servicePath'
|
|
236
|
+
&& inputs.routing.selectedBinding !== undefined;
|
|
237
|
+
for (const [key, value] of Object.entries(matched)) {
|
|
238
|
+
addDerivation(state, key, value, {
|
|
239
|
+
sourceKind: fromSelectedBinding
|
|
240
|
+
? `selected_binding.${signalCode(kind)}_template`
|
|
241
|
+
: `${signalCode(kind)}_template`,
|
|
242
|
+
value,
|
|
243
|
+
rule: fromSelectedBinding
|
|
244
|
+
? 'exact_selected_binding_template_match'
|
|
245
|
+
: 'exact_template_match',
|
|
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',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
addScore(state, score, `${signalCode(kind)}_template_match`);
|
|
224
254
|
}
|
|
225
255
|
|
|
226
|
-
function
|
|
256
|
+
function applyReferenceSignal(
|
|
227
257
|
state: DynamicTargetCandidate,
|
|
228
|
-
|
|
229
|
-
|
|
258
|
+
inputs: AnalysisInputs,
|
|
259
|
+
references: DynamicReferenceRow[],
|
|
230
260
|
kind: 'alias' | 'destination',
|
|
231
261
|
): void {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
262
|
+
const original = inputs.original[kind];
|
|
263
|
+
const effective = inputs.effective[kind];
|
|
264
|
+
if (!original || extractPlaceholders(original).length === 0) return;
|
|
265
|
+
const values = references.flatMap((reference) => {
|
|
266
|
+
const concrete = kind === 'alias' ? reference.alias : reference.destination;
|
|
267
|
+
return isConcrete(concrete) ? [{ reference, concrete }] : [];
|
|
268
|
+
});
|
|
269
|
+
if (effective && extractPlaceholders(effective).length === 0
|
|
270
|
+
&& values.length > 0 && !values.some(({ concrete }) => concrete === effective)) {
|
|
271
|
+
reject(state, `${kind}_contradicts_runtime_substitution`);
|
|
272
|
+
}
|
|
273
|
+
let matchedSignal = false;
|
|
274
|
+
for (const { reference, concrete } of values) {
|
|
275
|
+
const matched = matchRuntimeTemplate(original, concrete);
|
|
236
276
|
if (!matched) continue;
|
|
237
|
-
|
|
277
|
+
matchedSignal = true;
|
|
278
|
+
for (const [key, value] of Object.entries(matched)) {
|
|
279
|
+
addDerivation(
|
|
280
|
+
state, key, value,
|
|
281
|
+
dynamicReferenceProvenance(reference, kind, original, value),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (matchedSignal) {
|
|
286
|
+
state.explicitSignalStrength += extractPlaceholders(original)
|
|
287
|
+
.filter((key) => inputs.supplied[key] !== undefined).length;
|
|
238
288
|
addScore(state, 0.2, `${kind}_template_match`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function applyUniqueIdentityEvidence(
|
|
293
|
+
db: Db,
|
|
294
|
+
candidates: DynamicTargetCandidate[],
|
|
295
|
+
inputs: AnalysisInputs,
|
|
296
|
+
): void {
|
|
297
|
+
for (const derivation of uniqueIdentityDerivations(db, candidates, inputs.original)) {
|
|
298
|
+
const candidate = candidates.find((item) =>
|
|
299
|
+
item.candidateOperationId === derivation.operationId);
|
|
300
|
+
if (!candidate) continue;
|
|
301
|
+
addDerivation(candidate, derivation.key, derivation.value, derivation.provenance);
|
|
302
|
+
addScore(candidate, 0.2, 'exact_identity_template_match');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function addDerivation(
|
|
307
|
+
state: DynamicTargetCandidate,
|
|
308
|
+
key: string,
|
|
309
|
+
value: string,
|
|
310
|
+
provenance: VariableProvenance,
|
|
311
|
+
): void {
|
|
312
|
+
const priorProvenance = state.derivationProvenance[key] ?? [];
|
|
313
|
+
state.derivationProvenance[key] = uniqueProvenance([...priorProvenance, provenance]);
|
|
314
|
+
const supplied = state.suppliedVariables[key];
|
|
315
|
+
if (supplied !== undefined && supplied !== value) {
|
|
316
|
+
addConflict(state, key, [supplied, value], 'explicit_value_conflicts_with_derived_value');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const prior = state.derivedVariables[key];
|
|
320
|
+
if (prior !== undefined && prior !== value) {
|
|
321
|
+
addConflict(state, key, [prior, value], 'conflicting_strong_derivations');
|
|
239
322
|
return;
|
|
240
323
|
}
|
|
241
|
-
if (
|
|
242
|
-
|
|
324
|
+
if (supplied === undefined) state.derivedVariables[key] = value;
|
|
325
|
+
state.completeVariables[key] = supplied ?? value;
|
|
326
|
+
state.derivedVariableSources[key] ??= provenance;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function addConflict(
|
|
330
|
+
state: DynamicTargetCandidate,
|
|
331
|
+
key: string,
|
|
332
|
+
values: string[],
|
|
333
|
+
reason: string,
|
|
334
|
+
): void {
|
|
335
|
+
const sources = (state.derivationProvenance[key] ?? [])
|
|
336
|
+
.map((item) => item.sourceKind).sort();
|
|
337
|
+
state.conflicts.push({ key, values: [...new Set(values)].sort(), reason, sources });
|
|
338
|
+
reject(state, reason);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function uniqueProvenance(rows: VariableProvenance[]): VariableProvenance[] {
|
|
342
|
+
const sorted = [...rows].sort((left, right) =>
|
|
343
|
+
left.sourceKind.localeCompare(right.sourceKind)
|
|
344
|
+
|| String(left.matchedName ?? '').localeCompare(String(right.matchedName ?? ''))
|
|
345
|
+
|| left.value.localeCompare(right.value));
|
|
346
|
+
const seen = new Set<string>();
|
|
347
|
+
return sorted.filter((row) => {
|
|
348
|
+
const key = JSON.stringify(row);
|
|
349
|
+
if (seen.has(key)) return false;
|
|
350
|
+
seen.add(key);
|
|
351
|
+
return true;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function finalizeCandidates(candidates: DynamicTargetCandidate[], order: string[]): void {
|
|
356
|
+
for (const candidate of candidates) {
|
|
357
|
+
candidate.missingVariables = order.filter((key) =>
|
|
358
|
+
candidate.completeVariables[key] === undefined);
|
|
359
|
+
candidate.viable = candidate.rejectedReasons.length === 0;
|
|
360
|
+
candidate.rejected = !candidate.viable;
|
|
361
|
+
if (candidate.missingVariables.length === 0 && candidate.viable)
|
|
362
|
+
addScore(candidate, 0.15, 'all_runtime_variables_derived');
|
|
363
|
+
else if (candidate.missingVariables.length > 0)
|
|
364
|
+
addReason(candidate, 'missing_required_runtime_variable');
|
|
365
|
+
candidate.score = Math.max(0, Math.min(1, candidate.score));
|
|
366
|
+
candidate.cli = candidate.missingVariables.length === 0 && candidate.viable
|
|
367
|
+
? cliFor(candidate.completeVariables, order)
|
|
368
|
+
: undefined;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function stableRank(candidates: DynamicTargetCandidate[]): DynamicTargetCandidate[] {
|
|
373
|
+
return [...candidates].sort((left, right) =>
|
|
374
|
+
Number(right.viable) - Number(left.viable)
|
|
375
|
+
|| right.score - left.score
|
|
376
|
+
|| right.explicitSignalStrength - left.explicitSignalStrength
|
|
377
|
+
|| left.repoName.localeCompare(right.repoName)
|
|
378
|
+
|| String(left.packageName ?? '').localeCompare(String(right.packageName ?? ''))
|
|
379
|
+
|| left.servicePath.localeCompare(right.servicePath)
|
|
380
|
+
|| left.operationPath.localeCompare(right.operationPath)
|
|
381
|
+
|| left.operationName.localeCompare(right.operationName)
|
|
382
|
+
|| left.candidateOperationId - right.candidateOperationId);
|
|
243
383
|
}
|
|
244
384
|
|
|
245
385
|
function inferenceDecision(candidates: DynamicTargetCandidate[]): Record<string, unknown> {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
386
|
+
const viable = candidates.filter((candidate) => candidate.viable);
|
|
387
|
+
const first = viable[0];
|
|
388
|
+
const second = viable[1];
|
|
389
|
+
if (!first || first.missingVariables.length > 0)
|
|
390
|
+
return { status: 'unresolved', reason: 'missing_required_runtime_variable' };
|
|
391
|
+
if (first.inferenceBlockReasons.length > 0)
|
|
392
|
+
return { status: 'unresolved', reason: first.inferenceBlockReasons[0] };
|
|
393
|
+
if (first.score < 0.85)
|
|
394
|
+
return { status: 'unresolved', reason: 'candidate_score_below_inference_threshold' };
|
|
395
|
+
const scoreGap = second
|
|
396
|
+
? Number((first.score - second.score).toFixed(12))
|
|
397
|
+
: undefined;
|
|
398
|
+
if (second && scoreGap !== undefined && scoreGap <= 0.05) {
|
|
399
|
+
const reason = scoreGap === 0
|
|
400
|
+
? 'candidate_tied_with_equal_score'
|
|
401
|
+
: 'candidate_within_inference_margin';
|
|
402
|
+
for (const candidate of viable.filter((item) => first.score - item.score <= 0.05))
|
|
403
|
+
addInferenceBlock(candidate, reason);
|
|
404
|
+
return { status: 'ambiguous', reason, scoreGap, requiredMargin: 0.05 };
|
|
257
405
|
}
|
|
258
406
|
return {
|
|
259
407
|
status: 'resolved',
|
|
260
408
|
candidateOperationId: first.candidateOperationId,
|
|
261
|
-
inferredVariables: first.
|
|
409
|
+
inferredVariables: first.completeVariables,
|
|
262
410
|
score: first.score,
|
|
263
411
|
reasons: first.reasons,
|
|
264
412
|
};
|
|
265
413
|
}
|
|
266
414
|
|
|
415
|
+
function boundedCandidate(
|
|
416
|
+
candidate: DynamicTargetCandidate,
|
|
417
|
+
limit: number,
|
|
418
|
+
): DynamicTargetCandidate {
|
|
419
|
+
const provenanceProjections = Object.fromEntries(
|
|
420
|
+
Object.entries(candidate.derivationProvenance)
|
|
421
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
422
|
+
.map(([key, rows]) => [key, projectBounded(rows, compareProvenance, limit)]),
|
|
423
|
+
);
|
|
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 {
|
|
468
|
+
...conflict,
|
|
469
|
+
sources: sources.items,
|
|
470
|
+
sourceCount: sources.totalCount,
|
|
471
|
+
shownSourceCount: sources.shownCount,
|
|
472
|
+
omittedSourceCount: sources.omittedCount,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function applyModeState(
|
|
477
|
+
candidates: DynamicTargetCandidate[],
|
|
478
|
+
mode: DynamicMode,
|
|
479
|
+
inference: Record<string, unknown>,
|
|
480
|
+
): void {
|
|
481
|
+
const selectedId = mode === 'infer' && inference.status === 'resolved'
|
|
482
|
+
? numberValue(inference.candidateOperationId)
|
|
483
|
+
: undefined;
|
|
484
|
+
for (const candidate of candidates) {
|
|
485
|
+
candidate.selected = selectedId === candidate.candidateOperationId;
|
|
486
|
+
candidate.exploratory = mode === 'candidates' && candidate.viable;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
267
490
|
function suggestedVarSets(
|
|
268
491
|
candidates: DynamicTargetCandidate[],
|
|
269
|
-
required: string[],
|
|
270
492
|
order: string[],
|
|
271
|
-
|
|
493
|
+
limit: number,
|
|
494
|
+
): ReturnType<typeof projectBounded<{ variables: Record<string, string>; cli: string }>> {
|
|
272
495
|
const seen = new Set<string>();
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (seen.has(cli))
|
|
277
|
-
seen.add(cli);
|
|
278
|
-
|
|
279
|
-
}
|
|
496
|
+
const rows: Array<{ variables: Record<string, string>; cli: string }> = [];
|
|
497
|
+
for (const candidate of candidates) {
|
|
498
|
+
if (!candidate.cli || candidate.missingVariables.length > 0) continue;
|
|
499
|
+
if (seen.has(candidate.cli)) continue;
|
|
500
|
+
seen.add(candidate.cli);
|
|
501
|
+
rows.push({ variables: orderedVariables(candidate.completeVariables, order), cli: candidate.cli });
|
|
502
|
+
}
|
|
503
|
+
return projectBounded(rows, (left, right) => left.cli.localeCompare(right.cli), limit);
|
|
280
504
|
}
|
|
281
505
|
|
|
282
|
-
function
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
alias:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
templates.alias,
|
|
310
|
-
templates.destination,
|
|
311
|
-
|
|
312
|
-
return [...new Set([...fromSubstitutions, ...fromTemplates])].sort();
|
|
506
|
+
function hasResolvedImplementation(db: Db, operationId: number): boolean {
|
|
507
|
+
return Boolean(db.prepare(
|
|
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",
|
|
509
|
+
).get(String(operationId)));
|
|
510
|
+
}
|
|
511
|
+
function templatesFromEvidence(
|
|
512
|
+
evidence: Record<string, unknown>,
|
|
513
|
+
routing: DynamicRoutingContext,
|
|
514
|
+
): Templates {
|
|
515
|
+
const selected = routing.selectedBinding;
|
|
516
|
+
return {
|
|
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),
|
|
535
|
+
};
|
|
313
536
|
}
|
|
314
537
|
|
|
315
|
-
function
|
|
538
|
+
function substitutionSignal(
|
|
539
|
+
evidence: Record<string, unknown>,
|
|
540
|
+
key: string,
|
|
541
|
+
phase: 'original' | 'effective',
|
|
542
|
+
): string | undefined {
|
|
543
|
+
const substitution = record(record(evidence.runtimeSubstitutions)[key]);
|
|
544
|
+
return stringValue(substitution[phase]) ?? stringValue(evidence[key]);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function placeholderSources(templates: Templates): Record<string, string[]> {
|
|
548
|
+
const sources: Record<string, string[]> = {};
|
|
549
|
+
for (const [kind, template] of Object.entries(templates)) {
|
|
550
|
+
if (typeof template !== 'string') continue;
|
|
551
|
+
for (const key of extractPlaceholders(template))
|
|
552
|
+
sources[key] = [...new Set([...(sources[key] ?? []), `${kind}:${template}`])].sort();
|
|
553
|
+
}
|
|
554
|
+
return Object.fromEntries(Object.entries(sources).sort(([left], [right]) =>
|
|
555
|
+
left.localeCompare(right)));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function variableOrder(templates: Templates, required: string[]): string[] {
|
|
316
559
|
const ordered = [
|
|
317
560
|
templates.servicePath,
|
|
318
561
|
templates.operationPath,
|
|
319
562
|
templates.alias,
|
|
320
563
|
templates.destination,
|
|
321
564
|
].flatMap((value) => extractPlaceholders(value));
|
|
322
|
-
return [...new Set([...ordered, ...
|
|
565
|
+
return [...new Set([...ordered, ...required])];
|
|
323
566
|
}
|
|
324
567
|
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
destination: stringValue(evidence.destination),
|
|
331
|
-
};
|
|
568
|
+
function referenceMatchesCandidate(
|
|
569
|
+
reference: DynamicReferenceRow,
|
|
570
|
+
servicePath: string,
|
|
571
|
+
): boolean {
|
|
572
|
+
return matchRuntimeTemplate(reference.servicePath, servicePath) !== undefined;
|
|
332
573
|
}
|
|
333
574
|
|
|
334
|
-
function
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
const values: Record<string, string> = {};
|
|
342
|
-
for (let index = 0; index < keys.length; index += 1) {
|
|
343
|
-
const key = keys[index];
|
|
344
|
-
const value = match[index + 1];
|
|
345
|
-
if (!key || value === undefined) return undefined;
|
|
346
|
-
if (values[key] !== undefined && values[key] !== value) return undefined;
|
|
347
|
-
values[key] = value;
|
|
348
|
-
}
|
|
349
|
-
return values;
|
|
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;
|
|
350
582
|
}
|
|
351
583
|
|
|
352
|
-
function
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
for (const match of template.matchAll(/\$\{([^}]*)\}/g)) {
|
|
356
|
-
pattern += escapeRegex(template.slice(lastIndex, match.index));
|
|
357
|
-
pattern += '([^/]+?)';
|
|
358
|
-
lastIndex = (match.index ?? 0) + match[0].length;
|
|
359
|
-
}
|
|
360
|
-
return `${pattern}${escapeRegex(template.slice(lastIndex))}`;
|
|
584
|
+
function cliFor(variables: Record<string, string>, order: string[]): string {
|
|
585
|
+
return order.filter((key) => variables[key] !== undefined)
|
|
586
|
+
.map((key) => `--var ${shellArgument(`${key}=${variables[key]}`)}`).join(' ');
|
|
361
587
|
}
|
|
362
588
|
|
|
363
|
-
function
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
589
|
+
function shellArgument(value: string): string {
|
|
590
|
+
return /^[A-Za-z0-9_./:=+-]+$/.test(value)
|
|
591
|
+
? value
|
|
592
|
+
: `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function orderedVariables(
|
|
596
|
+
variables: Record<string, string>,
|
|
597
|
+
order: string[],
|
|
598
|
+
): Record<string, string> {
|
|
599
|
+
return Object.fromEntries(order.flatMap((key) =>
|
|
600
|
+
variables[key] === undefined ? [] : [[key, variables[key]]]));
|
|
376
601
|
}
|
|
377
602
|
|
|
378
603
|
function addScore(state: DynamicTargetCandidate, amount: number, reason: string): void {
|
|
379
604
|
state.score += amount;
|
|
605
|
+
addReason(state, reason);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function addReason(state: DynamicTargetCandidate, reason: string): void {
|
|
380
609
|
if (!state.reasons.includes(reason)) state.reasons.push(reason);
|
|
381
610
|
}
|
|
382
611
|
|
|
383
|
-
function
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return Boolean(row);
|
|
612
|
+
function reject(state: DynamicTargetCandidate, reason: string): void {
|
|
613
|
+
rejectReasonOnly(state, reason);
|
|
614
|
+
state.viable = false;
|
|
615
|
+
state.rejected = true;
|
|
388
616
|
}
|
|
389
617
|
|
|
390
|
-
function
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
618
|
+
function rejectReasonOnly(state: DynamicTargetCandidate, reason: string): void {
|
|
619
|
+
if (!state.rejectedReasons.includes(reason)) state.rejectedReasons.push(reason);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function addInferenceBlock(state: DynamicTargetCandidate, reason: string): void {
|
|
623
|
+
addReason(state, reason);
|
|
624
|
+
if (!state.inferenceBlockReasons.includes(reason))
|
|
625
|
+
state.inferenceBlockReasons.push(reason);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function requiredSuppliedVariables(inputs: AnalysisInputs): Record<string, string> {
|
|
629
|
+
return Object.fromEntries(inputs.required.flatMap((key) =>
|
|
630
|
+
inputs.supplied[key] === undefined ? [] : [[key, inputs.supplied[key]]]));
|
|
631
|
+
}
|
|
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
|
+
|
|
661
|
+
function signalCode(kind: 'servicePath' | 'operationPath'): string {
|
|
662
|
+
return kind === 'servicePath' ? 'service_path' : 'operation_path';
|
|
395
663
|
}
|
|
396
664
|
|
|
397
665
|
function record(value: unknown): Record<string, unknown> {
|
|
398
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
666
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
667
|
+
? value as Record<string, unknown>
|
|
668
|
+
: {};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function stringRecord(value: unknown): Record<string, string> {
|
|
672
|
+
const entries = Object.entries(record(value))
|
|
673
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
|
674
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
675
|
+
return Object.fromEntries(entries);
|
|
399
676
|
}
|
|
400
677
|
|
|
401
678
|
function stringValue(value: unknown): string | undefined {
|
|
@@ -407,7 +684,9 @@ function numberValue(value: unknown): number | undefined {
|
|
|
407
684
|
}
|
|
408
685
|
|
|
409
686
|
function stringArray(value: unknown): string[] {
|
|
410
|
-
return Array.isArray(value)
|
|
687
|
+
return Array.isArray(value)
|
|
688
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
689
|
+
: [];
|
|
411
690
|
}
|
|
412
691
|
|
|
413
692
|
function nonEmptyStrings(value: unknown, fallback: string[]): string[] {
|
|
@@ -416,9 +695,6 @@ function nonEmptyStrings(value: unknown, fallback: string[]): string[] {
|
|
|
416
695
|
}
|
|
417
696
|
|
|
418
697
|
function isConcrete(value: unknown): value is string {
|
|
419
|
-
return typeof value === 'string' && value.length > 0
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
function escapeRegex(value: string): string {
|
|
423
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
698
|
+
return typeof value === 'string' && value.length > 0
|
|
699
|
+
&& extractPlaceholders(value).length === 0;
|
|
424
700
|
}
|