@saptools/service-flow 0.1.51 → 0.1.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +16 -14
  3. package/TECHNICAL-NOTE.md +18 -6
  4. package/dist/{chunk-YZJKE5UX.js → chunk-LFH7C46B.js} +4290 -1261
  5. package/dist/chunk-LFH7C46B.js.map +1 -0
  6. package/dist/cli.js +435 -515
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +45 -7
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/000-clean.ts +82 -0
  12. package/src/cli/001-doctor-projection.ts +140 -0
  13. package/src/cli/doctor.ts +20 -3
  14. package/src/cli.ts +75 -57
  15. package/src/db/connection.ts +1 -1
  16. package/src/db/migrations.ts +5 -3
  17. package/src/db/repositories.ts +130 -24
  18. package/src/db/schema.ts +7 -2
  19. package/src/indexer/repository-indexer.ts +57 -29
  20. package/src/indexer/workspace-indexer.ts +84 -6
  21. package/src/linker/000-implementation-candidates.ts +641 -0
  22. package/src/linker/001-implementation-evidence-projection.ts +119 -0
  23. package/src/linker/002-call-evidence.ts +226 -0
  24. package/src/linker/cross-repo-linker.ts +27 -441
  25. package/src/linker/dynamic-edge-resolver.ts +35 -0
  26. package/src/linker/external-http-target.ts +24 -3
  27. package/src/linker/helper-package-linker.ts +18 -2
  28. package/src/linker/service-resolver.ts +45 -2
  29. package/src/output/doctor-output.ts +13 -4
  30. package/src/output/table-output.ts +33 -5
  31. package/src/parsers/cds-parser.ts +8 -2
  32. package/src/parsers/decorator-parser.ts +382 -48
  33. package/src/parsers/handler-registration-parser.ts +38 -21
  34. package/src/parsers/imported-wrapper-parser.ts +18 -5
  35. package/src/parsers/outbound-call-parser.ts +25 -8
  36. package/src/parsers/package-json-parser.ts +36 -11
  37. package/src/parsers/service-binding-parser-helpers.ts +8 -1
  38. package/src/parsers/service-binding-parser.ts +6 -3
  39. package/src/parsers/symbol-parser.ts +13 -3
  40. package/src/parsers/ts-project.ts +54 -0
  41. package/src/trace/000-dynamic-target-types.ts +96 -0
  42. package/src/trace/001-dynamic-identity.ts +308 -0
  43. package/src/trace/002-trace-diagnostics.ts +54 -0
  44. package/src/trace/003-dynamic-references.ts +283 -0
  45. package/src/trace/004-dynamic-candidate-sources.ts +155 -0
  46. package/src/trace/005-implementation-selection.ts +187 -0
  47. package/src/trace/006-contextual-projection.ts +30 -0
  48. package/src/trace/007-implementation-start-diagnostic.ts +61 -0
  49. package/src/trace/dynamic-targets.ts +582 -306
  50. package/src/trace/evidence.ts +331 -46
  51. package/src/trace/implementation-hints.ts +148 -8
  52. package/src/trace/selectors.ts +551 -3
  53. package/src/trace/trace-engine.ts +129 -135
  54. package/src/types.ts +27 -1
  55. package/src/utils/000-bounded-projection.ts +161 -0
  56. package/dist/chunk-YZJKE5UX.js.map +0 -1
@@ -1,49 +1,43 @@
1
1
  import type { Db } from '../db/connection.js';
2
- import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
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
- export interface DynamicTargetCandidate {
7
- candidateOperationId: number;
8
- repoName: string;
9
- packageName?: string;
10
- servicePath: string;
11
- operationPath: string;
12
- operationName: string;
13
- derivedVariables: Record<string, string>;
14
- derivedVariableSources: Record<string, Record<string, unknown>>;
15
- missingVariables: string[];
16
- score: number;
17
- reasons: string[];
18
- rejectedReasons: string[];
19
- cli?: string;
20
- }
21
-
22
- export interface DynamicTargetAnalysis {
23
- mode: DynamicMode;
24
- candidateCount: number;
25
- shownCandidateCount: number;
26
- omittedCandidateCount: number;
27
- missingVariables: string[];
28
- candidates: DynamicTargetCandidate[];
29
- shownCandidates: DynamicTargetCandidate[];
30
- suggestedVarSets: Array<{ variables: Record<string, string>; cli: string }>;
31
- inference: Record<string, unknown>;
32
- }
33
-
34
- interface ReferenceRow {
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;
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 templates = templatesFromEvidence(evidence);
57
- const missingVariables = allMissingVariables(evidence, templates);
58
- if (missingVariables.length === 0) return undefined;
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
- candidateTargets(db, evidence, workspaceId),
63
- referenceRows(db, workspaceId),
64
- templates,
65
- order,
54
+ inputs.effective.operationPath,
55
+ inputs.original.operationPath,
56
+ evidence.candidates,
57
+ workspaceId,
58
+ inputs.routing.outboundCallId !== undefined,
66
59
  );
67
- const inference = inferenceDecision(candidates);
68
- const shownCandidates = candidates.slice(0, maxCandidates);
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
- candidateCount: candidates.length,
72
- shownCandidateCount: shownCandidates.length,
73
- omittedCandidateCount: Math.max(0, candidates.length - shownCandidates.length),
74
- missingVariables,
75
- candidates,
76
- shownCandidates,
77
- suggestedVarSets: suggestedVarSets(candidates, missingVariables, order),
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 candidateTargets(
100
+ function analysisInputs(
83
101
  db: Db,
84
102
  evidence: Record<string, unknown>,
85
103
  workspaceId: number | undefined,
86
- ): OperationTarget[] {
87
- const embedded = rowsFromEvidence(evidence.candidates);
88
- if (embedded.length > 0) return embedded;
89
- const operationPath = stringValue(evidence.operationPath);
90
- if (!operationPath || extractPlaceholders(operationPath).length > 0) return [];
91
- return queryOperationTargets(db, operationPath, workspaceId);
92
- }
93
-
94
- function rowsFromEvidence(value: unknown): OperationTarget[] {
95
- if (!Array.isArray(value)) return [];
96
- return value.flatMap((item): OperationTarget[] => {
97
- const row = record(item);
98
- const operationId = numberValue(row.operationId);
99
- const repoName = stringValue(row.repoName);
100
- const servicePath = stringValue(row.servicePath);
101
- const operationPath = stringValue(row.operationPath);
102
- const operationName = stringValue(row.operationName) ?? operationPath?.replace(/^\//, '');
103
- if (operationId === undefined || !repoName || !servicePath || !operationPath || !operationName) return [];
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 queryOperationTargets(
124
+ function buildCandidates(
123
125
  db: Db,
124
- operationPath: string,
125
- workspaceId: number | undefined,
126
- ): OperationTarget[] {
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
- const ranked = candidates.map((candidate) =>
164
- candidateEvidence(db, candidate, references, templates, order));
165
- return ranked.sort((a, b) =>
166
- b.score - a.score
167
- || a.repoName.localeCompare(b.repoName)
168
- || a.servicePath.localeCompare(b.servicePath));
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 candidateEvidence(
172
- db: Db,
173
- candidate: OperationTarget,
174
- references: ReferenceRow[],
175
- templates: Templates,
176
- order: string[],
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: candidate.operationId,
196
- repoName: candidate.repoName,
197
- packageName: candidate.packageName ?? undefined,
198
- servicePath: candidate.servicePath,
199
- operationPath: candidate.operationPath,
200
- operationName: candidate.operationName,
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
- score: Math.max(0.2, Number(candidate.score ?? 0)),
205
- reasons: nonEmptyStrings(candidate.reasons, ['operation_path_match']),
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 applyDirectTemplate(
217
+ function applyDirectSignal(
211
218
  state: DynamicTargetCandidate,
212
- template: string | undefined,
219
+ inputs: AnalysisInputs,
220
+ kind: 'servicePath' | 'operationPath',
213
221
  concrete: string,
214
- kind: 'operation_path' | 'service_path',
222
+ score: number,
215
223
  ): void {
216
- const matched = matchTemplate(template, concrete);
217
- if (!template) return;
218
- if (!matched) {
219
- state.rejectedReasons.push(`${kind}_template_mismatch`);
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
- mergeDerived(state, matched, `${kind}_template`);
223
- addScore(state, kind === 'service_path' ? 0.35 : 0.25, `${kind}_template_match`);
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 applyReferenceTemplate(
256
+ function applyReferenceSignal(
227
257
  state: DynamicTargetCandidate,
228
- template: string | undefined,
229
- refs: ReferenceRow[],
258
+ inputs: AnalysisInputs,
259
+ references: DynamicReferenceRow[],
230
260
  kind: 'alias' | 'destination',
231
261
  ): void {
232
- if (!template || extractPlaceholders(template).length === 0) return;
233
- for (const ref of refs) {
234
- const concrete = kind === 'alias' ? ref.alias : ref.destination;
235
- const matched = isConcrete(concrete) ? matchTemplate(template, concrete) : undefined;
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
- mergeDerived(state, matched, `${ref.sourceKind}.${kind}`);
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 (refs.some((ref) => isConcrete(kind === 'alias' ? ref.alias : ref.destination)))
242
- state.rejectedReasons.push(`${kind}_template_mismatch`);
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 complete = candidates.filter((candidate) =>
247
- candidate.missingVariables.length === 0
248
- && candidate.rejectedReasons.length === 0);
249
- const first = complete[0];
250
- const second = complete[1];
251
- if (!first) return { status: 'unresolved', reason: 'missing_required_runtime_variable' };
252
- if (first.score < 0.85) return { status: 'unresolved', reason: 'candidate_score_below_inference_threshold' };
253
- if (second && first.score - second.score <= 0.05) {
254
- for (const candidate of complete.filter((item) => first.score - item.score <= 0.05))
255
- candidate.rejectedReasons.push('candidate_tied_with_equal_score');
256
- return { status: 'ambiguous', reason: 'candidate_tied_with_equal_score' };
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.derivedVariables,
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
- ): Array<{ variables: Record<string, string>; cli: string }> {
493
+ limit: number,
494
+ ): ReturnType<typeof projectBounded<{ variables: Record<string, string>; cli: string }>> {
272
495
  const seen = new Set<string>();
273
- return candidates.flatMap((candidate) => {
274
- if (required.some((key) => candidate.derivedVariables[key] === undefined)) return [];
275
- const cli = cliFor(candidate.derivedVariables, order);
276
- if (seen.has(cli)) return [];
277
- seen.add(cli);
278
- return [{ variables: candidate.derivedVariables, cli }];
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 referenceRows(db: Db, workspaceId: number | undefined): ReferenceRow[] {
283
- const rows = db.prepare(
284
- `SELECT req.alias alias,req.destination destination,req.service_path servicePath,
285
- 'cds_require' sourceKind,r.name repoName
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
- }));
301
- }
302
-
303
- function allMissingVariables(evidence: Record<string, unknown>, templates: Templates): string[] {
304
- const fromSubstitutions = Object.values(record(evidence.runtimeSubstitutions))
305
- .flatMap((value) => stringArray(record(value).missing));
306
- const fromTemplates = [
307
- templates.servicePath,
308
- templates.operationPath,
309
- templates.alias,
310
- templates.destination,
311
- ].flatMap((value) => extractPlaceholders(value));
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 variableOrder(templates: Templates, missingVariables: string[]): string[] {
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, ...missingVariables])];
565
+ return [...new Set([...ordered, ...required])];
323
566
  }
324
567
 
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
- };
568
+ function referenceMatchesCandidate(
569
+ reference: DynamicReferenceRow,
570
+ servicePath: string,
571
+ ): boolean {
572
+ return matchRuntimeTemplate(reference.servicePath, servicePath) !== undefined;
332
573
  }
333
574
 
334
- function matchTemplate(template: string | undefined, concrete: string | undefined): Record<string, string> | undefined {
335
- if (!template || !concrete) return undefined;
336
- const keys = extractPlaceholders(template);
337
- if (keys.length === 0) return template === concrete ? {} : undefined;
338
- const regex = new RegExp(`^${templateToPattern(template)}$`);
339
- const match = regex.exec(concrete);
340
- if (!match) return undefined;
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 templateToPattern(template: string): string {
353
- let pattern = '';
354
- let lastIndex = 0;
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 mergeDerived(
364
- state: DynamicTargetCandidate,
365
- values: Record<string, string>,
366
- sourceKind: string,
367
- ): void {
368
- for (const [key, value] of Object.entries(values)) {
369
- if (state.derivedVariables[key] !== undefined && state.derivedVariables[key] !== value) {
370
- state.rejectedReasons.push('template_variable_conflict');
371
- continue;
372
- }
373
- state.derivedVariables[key] = value;
374
- state.derivedVariableSources[key] = { sourceKind, value };
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 hasResolvedImplementation(db: Db, operationId: number): boolean {
384
- const row = db.prepare(
385
- "SELECT 1 FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status='resolved' AND from_kind='operation' AND from_id=? LIMIT 1",
386
- ).get(String(operationId));
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 cliFor(variables: Record<string, string>, order: string[]): string {
391
- return order
392
- .filter((key) => variables[key] !== undefined)
393
- .map((key) => `--var ${key}=${variables[key]}`)
394
- .join(' ');
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) ? value as Record<string, unknown> : {};
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) ? value.filter((item): item is string => typeof item === 'string') : [];
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 && extractPlaceholders(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
  }