@saptools/service-flow 0.1.50 → 0.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +16 -3
  3. package/TECHNICAL-NOTE.md +10 -1
  4. package/dist/{chunk-52OUS3MO.js → chunk-PTLDSHRC.js} +2663 -363
  5. package/dist/chunk-PTLDSHRC.js.map +1 -0
  6. package/dist/cli.js +318 -510
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +59 -17
  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.ts +97 -57
  13. package/src/db/connection.ts +1 -1
  14. package/src/db/migrations.ts +5 -3
  15. package/src/db/repositories.ts +120 -22
  16. package/src/db/schema.ts +7 -2
  17. package/src/index.ts +1 -1
  18. package/src/indexer/repository-indexer.ts +57 -29
  19. package/src/indexer/workspace-indexer.ts +84 -6
  20. package/src/linker/cross-repo-linker.ts +22 -2
  21. package/src/linker/service-resolver.ts +15 -4
  22. package/src/output/table-output.ts +56 -3
  23. package/src/parsers/cds-parser.ts +8 -2
  24. package/src/parsers/decorator-parser.ts +382 -48
  25. package/src/parsers/handler-registration-parser.ts +38 -21
  26. package/src/parsers/imported-wrapper-parser.ts +18 -5
  27. package/src/parsers/outbound-call-parser.ts +25 -8
  28. package/src/parsers/package-json-parser.ts +36 -11
  29. package/src/parsers/service-binding-parser-helpers.ts +8 -1
  30. package/src/parsers/service-binding-parser.ts +6 -3
  31. package/src/parsers/symbol-parser.ts +13 -3
  32. package/src/parsers/ts-project.ts +54 -0
  33. package/src/trace/000-dynamic-target-types.ts +84 -0
  34. package/src/trace/001-dynamic-identity.ts +280 -0
  35. package/src/trace/002-trace-diagnostics.ts +54 -0
  36. package/src/trace/003-dynamic-references.ts +82 -0
  37. package/src/trace/dynamic-branches.ts +45 -0
  38. package/src/trace/dynamic-targets.ts +654 -0
  39. package/src/trace/evidence.ts +391 -22
  40. package/src/trace/selectors.ts +483 -0
  41. package/src/trace/trace-engine.ts +101 -94
  42. package/src/types.ts +39 -1
  43. package/dist/chunk-52OUS3MO.js.map +0 -1
@@ -0,0 +1,654 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
3
+ import type { OperationTarget } from '../linker/service-resolver.js';
4
+ import type { DynamicMode } from '../types.js';
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;
35
+ }
36
+
37
+ export function analyzeDynamicTargetCandidates(
38
+ db: Db,
39
+ evidence: Record<string, unknown>,
40
+ workspaceId: number | undefined,
41
+ mode: DynamicMode,
42
+ maxCandidates: number,
43
+ ): DynamicTargetAnalysis | undefined {
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,
49
+ );
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));
62
+ return {
63
+ mode,
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),
81
+ inference,
82
+ };
83
+ }
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
+
103
+ function candidateTargets(
104
+ db: Db,
105
+ evidence: Record<string, unknown>,
106
+ workspaceId: number | undefined,
107
+ ): OperationTarget[] {
108
+ const embedded = rowsFromEvidence(evidence.candidates);
109
+ if (embedded.length > 0) return embedded;
110
+ const operationPath = effectiveSignal(evidence, 'operationPath');
111
+ if (!operationPath || extractPlaceholders(operationPath).length > 0) return [];
112
+ return queryOperationTargets(db, operationPath, workspaceId);
113
+ }
114
+
115
+ function rowsFromEvidence(value: unknown): OperationTarget[] {
116
+ if (!Array.isArray(value)) return [];
117
+ return value.flatMap((item): OperationTarget[] => {
118
+ const row = record(item);
119
+ const operationId = numberValue(row.operationId);
120
+ const repoName = stringValue(row.repoName);
121
+ const servicePath = stringValue(row.servicePath);
122
+ const operationPath = stringValue(row.operationPath);
123
+ const operationName = stringValue(row.operationName) ?? operationPath?.replace(/^\//, '');
124
+ if (operationId === undefined || !repoName || !servicePath || !operationPath || !operationName) return [];
125
+ return [{
126
+ operationId,
127
+ repoId: numberValue(row.repoId),
128
+ repoName,
129
+ packageName: stringValue(row.packageName),
130
+ serviceName: stringValue(row.serviceName) ?? '',
131
+ qualifiedName: stringValue(row.qualifiedName) ?? '',
132
+ servicePath,
133
+ operationPath,
134
+ operationName,
135
+ sourceFile: stringValue(row.sourceFile) ?? '',
136
+ sourceLine: numberValue(row.sourceLine) ?? 0,
137
+ score: numberValue(row.score) ?? 0,
138
+ reasons: stringArray(row.reasons),
139
+ }];
140
+ });
141
+ }
142
+
143
+ function queryOperationTargets(
144
+ db: Db,
145
+ operationPath: string,
146
+ workspaceId: number | undefined,
147
+ ): OperationTarget[] {
148
+ const simple = operationPath.replace(/^\//, '').split('.').at(-1) ?? operationPath;
149
+ const rows = db.prepare(
150
+ `SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,
151
+ s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,
152
+ o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,
153
+ o.source_line sourceLine FROM cds_operations o
154
+ JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
155
+ WHERE (? IS NULL OR r.workspace_id=?)
156
+ AND (o.operation_path IN (?,?) OR o.operation_name=?)
157
+ ORDER BY r.name,s.service_path,o.operation_name,o.id`,
158
+ ).all(workspaceId, workspaceId, operationPath, `/${simple}`, simple);
159
+ return rows.flatMap(operationTargetFromRow);
160
+ }
161
+
162
+ function operationTargetFromRow(row: Record<string, unknown>): OperationTarget[] {
163
+ const operationId = numberValue(row.operationId);
164
+ const repoName = stringValue(row.repoName);
165
+ const servicePath = stringValue(row.servicePath);
166
+ const operationPath = stringValue(row.operationPath);
167
+ const operationName = stringValue(row.operationName);
168
+ if (operationId === undefined || !repoName || !servicePath || !operationPath || !operationName) return [];
169
+ return [{
170
+ operationId,
171
+ repoId: numberValue(row.repoId),
172
+ repoName,
173
+ packageName: stringValue(row.packageName),
174
+ serviceName: stringValue(row.serviceName) ?? '',
175
+ qualifiedName: stringValue(row.qualifiedName) ?? '',
176
+ servicePath,
177
+ operationPath,
178
+ operationName,
179
+ sourceFile: stringValue(row.sourceFile) ?? '',
180
+ sourceLine: numberValue(row.sourceLine) ?? 0,
181
+ score: 0.2,
182
+ reasons: ['operation_path_match'],
183
+ }];
184
+ }
185
+
186
+ function buildCandidates(
187
+ db: Db,
188
+ targets: OperationTarget[],
189
+ references: DynamicReferenceRow[],
190
+ inputs: AnalysisInputs,
191
+ ): DynamicTargetCandidate[] {
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
+ });
204
+ }
205
+
206
+ function emptyCandidate(
207
+ target: OperationTarget,
208
+ inputs: AnalysisInputs,
209
+ ): DynamicTargetCandidate {
210
+ return {
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 },
228
+ derivedVariables: {},
229
+ derivedVariableSources: {},
230
+ derivationProvenance: {},
231
+ missingVariables: [],
232
+ conflicts: [],
233
+ score: Math.max(0.2, Number(target.score ?? 0)),
234
+ explicitSignalStrength: 0,
235
+ reasons: nonEmptyStrings(target.reasons, ['operation_path_match']),
236
+ rejectedReasons: [],
237
+ inferenceBlockReasons: [],
238
+ viable: true,
239
+ rejected: false,
240
+ selected: false,
241
+ exploratory: false,
242
+ };
243
+ }
244
+
245
+ function applyDirectSignal(
246
+ state: DynamicTargetCandidate,
247
+ inputs: AnalysisInputs,
248
+ kind: 'servicePath' | 'operationPath',
249
+ concrete: string,
250
+ score: number,
251
+ ): void {
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`);
256
+ return;
257
+ }
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`);
272
+ }
273
+
274
+ function applyReferenceSignal(
275
+ state: DynamicTargetCandidate,
276
+ inputs: AnalysisInputs,
277
+ references: DynamicReferenceRow[],
278
+ kind: 'alias' | 'destination',
279
+ ): void {
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);
294
+ if (!matched) continue;
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;
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');
335
+ return;
336
+ }
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);
401
+ }
402
+
403
+ function inferenceDecision(candidates: DynamicTargetCandidate[]): Record<string, unknown> {
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 };
421
+ }
422
+ return {
423
+ status: 'resolved',
424
+ candidateOperationId: first.candidateOperationId,
425
+ inferredVariables: first.completeVariables,
426
+ score: first.score,
427
+ reasons: first.reasons,
428
+ };
429
+ }
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
+
461
+ function suggestedVarSets(
462
+ candidates: DynamicTargetCandidate[],
463
+ order: string[],
464
+ limit: number,
465
+ ): Array<{ variables: Record<string, string>; cli: string }> {
466
+ const seen = new Set<string>();
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;
476
+ }
477
+
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)));
482
+ }
483
+
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]);
504
+ }
505
+
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[] {
522
+ const ordered = [
523
+ templates.servicePath,
524
+ templates.operationPath,
525
+ templates.alias,
526
+ templates.destination,
527
+ ].flatMap((value) => extractPlaceholders(value));
528
+ return [...new Set([...ordered, ...required])];
529
+ }
530
+
531
+ function matchTemplate(
532
+ template: string | undefined,
533
+ concrete: string | undefined,
534
+ ): Record<string, string> | undefined {
535
+ if (!template || !concrete) return undefined;
536
+ const keys = extractPlaceholders(template);
537
+ if (keys.length === 0) return template === concrete ? {} : undefined;
538
+ const match = new RegExp(`^${templateToPattern(template)}$`).exec(concrete);
539
+ if (!match) return undefined;
540
+ const values: Record<string, string> = {};
541
+ for (let index = 0; index < keys.length; index += 1) {
542
+ const key = keys[index];
543
+ const value = match[index + 1];
544
+ if (!key || value === undefined) return undefined;
545
+ if (values[key] !== undefined && values[key] !== value) return undefined;
546
+ values[key] = value;
547
+ }
548
+ return values;
549
+ }
550
+
551
+ function templateToPattern(template: string): string {
552
+ let pattern = '';
553
+ let lastIndex = 0;
554
+ for (const match of template.matchAll(/\$\{([^}]*)\}/g)) {
555
+ pattern += escapeRegex(template.slice(lastIndex, match.index));
556
+ pattern += '([^/]+?)';
557
+ lastIndex = (match.index ?? 0) + match[0].length;
558
+ }
559
+ return `${pattern}${escapeRegex(template.slice(lastIndex))}`;
560
+ }
561
+
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]]]));
579
+ }
580
+
581
+ function addScore(state: DynamicTargetCandidate, amount: number, reason: string): void {
582
+ state.score += amount;
583
+ addReason(state, reason);
584
+ }
585
+
586
+ function addReason(state: DynamicTargetCandidate, reason: string): void {
587
+ if (!state.reasons.includes(reason)) state.reasons.push(reason);
588
+ }
589
+
590
+ function reject(state: DynamicTargetCandidate, reason: string): void {
591
+ rejectReasonOnly(state, reason);
592
+ state.viable = false;
593
+ state.rejected = true;
594
+ }
595
+
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';
613
+ }
614
+
615
+ function record(value: unknown): Record<string, unknown> {
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);
626
+ }
627
+
628
+ function stringValue(value: unknown): string | undefined {
629
+ return typeof value === 'string' ? value : undefined;
630
+ }
631
+
632
+ function numberValue(value: unknown): number | undefined {
633
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
634
+ }
635
+
636
+ function stringArray(value: unknown): string[] {
637
+ return Array.isArray(value)
638
+ ? value.filter((item): item is string => typeof item === 'string')
639
+ : [];
640
+ }
641
+
642
+ function nonEmptyStrings(value: unknown, fallback: string[]): string[] {
643
+ const values = stringArray(value).filter((item) => item.length > 0);
644
+ return values.length > 0 ? values : fallback;
645
+ }
646
+
647
+ function isConcrete(value: unknown): value is string {
648
+ return typeof value === 'string' && value.length > 0
649
+ && extractPlaceholders(value).length === 0;
650
+ }
651
+
652
+ function escapeRegex(value: string): string {
653
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
654
+ }