@saptools/service-flow 0.1.52 → 0.1.54
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 +12 -0
- package/README.md +11 -12
- package/TECHNICAL-NOTE.md +18 -3
- package/dist/{chunk-PTLDSHRC.js → chunk-ERIZHM5C.js} +2646 -1067
- package/dist/chunk-ERIZHM5C.js.map +1 -0
- package/dist/cli.js +162 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/001-doctor-projection.ts +136 -0
- package/src/cli/doctor.ts +20 -3
- package/src/db/repositories.ts +10 -2
- package/src/linker/000-implementation-candidates.ts +674 -0
- package/src/linker/001-implementation-evidence-projection.ts +191 -0
- package/src/linker/002-call-evidence.ts +226 -0
- package/src/linker/cross-repo-linker.ts +27 -453
- package/src/linker/dynamic-edge-resolver.ts +35 -0
- package/src/linker/external-http-target.ts +24 -3
- package/src/linker/helper-package-linker.ts +18 -2
- package/src/linker/service-resolver.ts +45 -2
- package/src/output/doctor-output.ts +13 -4
- package/src/output/table-output.ts +15 -4
- package/src/trace/000-dynamic-target-types.ts +12 -0
- package/src/trace/001-dynamic-identity.ts +45 -17
- package/src/trace/003-dynamic-references.ts +236 -35
- package/src/trace/004-dynamic-candidate-sources.ts +155 -0
- package/src/trace/005-implementation-selection.ts +187 -0
- package/src/trace/006-contextual-projection.ts +30 -0
- package/src/trace/007-implementation-start-diagnostic.ts +61 -0
- package/src/trace/008-contextual-runtime-state.ts +294 -0
- package/src/trace/009-selected-handler-provenance.ts +186 -0
- package/src/trace/dynamic-targets.ts +205 -159
- package/src/trace/evidence.ts +132 -34
- package/src/trace/implementation-hints.ts +148 -8
- package/src/trace/selectors.ts +74 -9
- package/src/trace/trace-engine.ts +97 -125
- package/src/utils/000-bounded-projection.ts +166 -0
- package/dist/chunk-PTLDSHRC.js.map +0 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
projectBounded,
|
|
3
|
+
projectBoundedInOrder,
|
|
4
|
+
type BoundedProjection,
|
|
5
|
+
} from '../utils/000-bounded-projection.js';
|
|
6
|
+
|
|
7
|
+
export interface SelectedHandlerSource {
|
|
8
|
+
methodId: number;
|
|
9
|
+
className?: string;
|
|
10
|
+
methodName?: string;
|
|
11
|
+
repositoryId?: number;
|
|
12
|
+
repositoryName?: string;
|
|
13
|
+
repositoryPackageName?: string;
|
|
14
|
+
sourceFile?: string;
|
|
15
|
+
sourceLine?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SelectedHandlerProvenance {
|
|
19
|
+
status: 'selected';
|
|
20
|
+
accepted: true;
|
|
21
|
+
graphTargetId: string;
|
|
22
|
+
methodId: number;
|
|
23
|
+
className?: string;
|
|
24
|
+
methodName?: string;
|
|
25
|
+
repository?: {
|
|
26
|
+
id?: number;
|
|
27
|
+
name?: string;
|
|
28
|
+
packageName?: string;
|
|
29
|
+
};
|
|
30
|
+
sourceFile?: string;
|
|
31
|
+
sourceLine?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function selectedHandlerProvenance(
|
|
35
|
+
source: SelectedHandlerSource,
|
|
36
|
+
): SelectedHandlerProvenance {
|
|
37
|
+
return {
|
|
38
|
+
status: 'selected',
|
|
39
|
+
accepted: true,
|
|
40
|
+
graphTargetId: String(source.methodId),
|
|
41
|
+
methodId: source.methodId,
|
|
42
|
+
className: source.className,
|
|
43
|
+
methodName: source.methodName,
|
|
44
|
+
repository: {
|
|
45
|
+
id: source.repositoryId,
|
|
46
|
+
name: source.repositoryName,
|
|
47
|
+
packageName: source.repositoryPackageName,
|
|
48
|
+
},
|
|
49
|
+
sourceFile: source.sourceFile,
|
|
50
|
+
sourceLine: source.sourceLine,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function displayImplementationCandidates(
|
|
55
|
+
candidates: Array<Record<string, unknown>>,
|
|
56
|
+
selectedMethodId: number | undefined,
|
|
57
|
+
): Array<Record<string, unknown>> {
|
|
58
|
+
return [...candidates]
|
|
59
|
+
.sort((left, right) => compareDisplayCandidate(
|
|
60
|
+
left, right, selectedMethodId,
|
|
61
|
+
))
|
|
62
|
+
.map((candidate, index) => ({
|
|
63
|
+
...candidate,
|
|
64
|
+
discoveryRank: candidate.rank,
|
|
65
|
+
displayRank: index + 1,
|
|
66
|
+
selected: selectedMethodId !== undefined
|
|
67
|
+
&& Number(candidate.methodId) === selectedMethodId,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function compareDisplayCandidate(
|
|
72
|
+
left: Record<string, unknown>,
|
|
73
|
+
right: Record<string, unknown>,
|
|
74
|
+
selectedMethodId: number | undefined,
|
|
75
|
+
): number {
|
|
76
|
+
return Number(Number(right.methodId) === selectedMethodId)
|
|
77
|
+
- Number(Number(left.methodId) === selectedMethodId)
|
|
78
|
+
|| Number(right.accepted === true) - Number(left.accepted === true)
|
|
79
|
+
|| numberValue(left.rank) - numberValue(right.rank)
|
|
80
|
+
|| compareTargetCandidates(left, right);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function boundedImplementationEvidence(
|
|
84
|
+
evidence: Record<string, unknown>,
|
|
85
|
+
targetCandidateCount: number,
|
|
86
|
+
): Record<string, unknown> {
|
|
87
|
+
const candidates = recordArray(evidence.candidates);
|
|
88
|
+
const candidateProjection = projectBoundedInOrder(candidates);
|
|
89
|
+
const families = recordArray(evidence.candidateFamilies);
|
|
90
|
+
const familyProjection = projectBounded(families, compareFamilies);
|
|
91
|
+
const hints = recordArray(evidence.implementationHintSuggestions);
|
|
92
|
+
const hintProjection = projectBounded(hints, compareHints);
|
|
93
|
+
const hintCount = Math.max(
|
|
94
|
+
numberValue(evidence.implementationHintSuggestionCount),
|
|
95
|
+
hintProjection.totalCount,
|
|
96
|
+
);
|
|
97
|
+
const targets = Math.max(0, targetCandidateCount);
|
|
98
|
+
return {
|
|
99
|
+
...evidence,
|
|
100
|
+
candidates: candidateProjection.items.map(boundedCandidateEvidence),
|
|
101
|
+
candidateCount: candidateProjection.totalCount,
|
|
102
|
+
shownCandidateCount: candidateProjection.shownCount,
|
|
103
|
+
omittedCandidateCount: candidateProjection.omittedCount,
|
|
104
|
+
candidateFamilies: familyProjection.items.map(boundedFamilyEvidence),
|
|
105
|
+
candidateFamilyCount: familyProjection.totalCount,
|
|
106
|
+
shownCandidateFamilyCount: familyProjection.shownCount,
|
|
107
|
+
omittedCandidateFamilyCount: familyProjection.omittedCount,
|
|
108
|
+
implementationHintSuggestions: hintProjection.items,
|
|
109
|
+
implementationHintSuggestionCount: hintCount,
|
|
110
|
+
shownImplementationHintSuggestionCount: hintProjection.shownCount,
|
|
111
|
+
omittedImplementationHintSuggestionCount: Math.max(0, hintCount - hintProjection.shownCount),
|
|
112
|
+
candidateTargetCount: targets,
|
|
113
|
+
shownCandidateTargetCount: Math.min(targets, candidateProjection.shownCount),
|
|
114
|
+
omittedCandidateTargetCount: Math.max(0, targets - candidateProjection.shownCount),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function boundedImplementationTargetIds(
|
|
119
|
+
candidates: Array<Record<string, unknown>>,
|
|
120
|
+
): BoundedProjection<string> {
|
|
121
|
+
const projection = projectBounded(candidates, compareTargetCandidates);
|
|
122
|
+
return {
|
|
123
|
+
totalCount: projection.totalCount,
|
|
124
|
+
shownCount: projection.shownCount,
|
|
125
|
+
omittedCount: projection.omittedCount,
|
|
126
|
+
items: projection.items.map((candidate) => String(candidate.methodId ?? '')),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function boundedCandidateEvidence(candidate: Record<string, unknown>): Record<string, unknown> {
|
|
131
|
+
const registrations = recordArray(candidate.registrations);
|
|
132
|
+
const projection = projectBounded(registrations, compareRegistrations);
|
|
133
|
+
return {
|
|
134
|
+
...candidate,
|
|
135
|
+
registrations: projection.items,
|
|
136
|
+
registrationCount: projection.totalCount,
|
|
137
|
+
shownRegistrationCount: projection.shownCount,
|
|
138
|
+
omittedRegistrationCount: projection.omittedCount,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function boundedFamilyEvidence(family: Record<string, unknown>): Record<string, unknown> {
|
|
143
|
+
const repositories = stringArray(family.repositories);
|
|
144
|
+
const projection = projectBounded(repositories, (left, right) => left.localeCompare(right));
|
|
145
|
+
return {
|
|
146
|
+
...family,
|
|
147
|
+
repositories: projection.items,
|
|
148
|
+
repositoryCount: projection.totalCount,
|
|
149
|
+
shownRepositoryCount: projection.shownCount,
|
|
150
|
+
omittedRepositoryCount: projection.omittedCount,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function compareTargetCandidates(left: Record<string, unknown>, right: Record<string, unknown>): number {
|
|
155
|
+
return Number(right.score ?? 0) - Number(left.score ?? 0)
|
|
156
|
+
|| String(left.className ?? '').localeCompare(String(right.className ?? ''))
|
|
157
|
+
|| Number(left.methodId ?? 0) - Number(right.methodId ?? 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function compareFamilies(left: Record<string, unknown>, right: Record<string, unknown>): number {
|
|
161
|
+
return String(left.packageName ?? '').localeCompare(String(right.packageName ?? ''))
|
|
162
|
+
|| String(left.reason ?? '').localeCompare(String(right.reason ?? ''));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function compareHints(left: Record<string, unknown>, right: Record<string, unknown>): number {
|
|
166
|
+
return String(left.cli ?? '').localeCompare(String(right.cli ?? ''))
|
|
167
|
+
|| String(left.implementationRepo ?? '').localeCompare(String(right.implementationRepo ?? ''));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function compareRegistrations(left: Record<string, unknown>, right: Record<string, unknown>): number {
|
|
171
|
+
return String(left.file ?? '').localeCompare(String(right.file ?? ''))
|
|
172
|
+
|| Number(left.line ?? 0) - Number(right.line ?? 0)
|
|
173
|
+
|| Number(left.id ?? 0) - Number(right.id ?? 0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function recordArray(value: unknown): Array<Record<string, unknown>> {
|
|
177
|
+
return Array.isArray(value)
|
|
178
|
+
? value.filter((item): item is Record<string, unknown> =>
|
|
179
|
+
Boolean(item && typeof item === 'object' && !Array.isArray(item)))
|
|
180
|
+
: [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function stringArray(value: unknown): string[] {
|
|
184
|
+
return Array.isArray(value)
|
|
185
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
186
|
+
: [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function numberValue(value: unknown): number {
|
|
190
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
191
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NormalizedODataOperationPath,
|
|
3
|
+
ODataPathIntent,
|
|
4
|
+
} from './odata-path-normalizer.js';
|
|
5
|
+
import {
|
|
6
|
+
boundCandidateLikeEvidence,
|
|
7
|
+
projectBounded,
|
|
8
|
+
type BoundedProjection,
|
|
9
|
+
} from '../utils/000-bounded-projection.js';
|
|
10
|
+
|
|
11
|
+
export interface LinkedOperationResolution {
|
|
12
|
+
target?: {
|
|
13
|
+
repoName?: string;
|
|
14
|
+
servicePath?: string;
|
|
15
|
+
operationPath?: string;
|
|
16
|
+
operationName?: string;
|
|
17
|
+
};
|
|
18
|
+
candidates: unknown[];
|
|
19
|
+
status: string;
|
|
20
|
+
reasons: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function linkedCallEvidence(
|
|
24
|
+
call: Record<string, unknown>,
|
|
25
|
+
resolution: LinkedOperationResolution,
|
|
26
|
+
servicePath: string | undefined,
|
|
27
|
+
operationPath: string | undefined,
|
|
28
|
+
destination: string | undefined,
|
|
29
|
+
normalized: NormalizedODataOperationPath | undefined,
|
|
30
|
+
intent: ODataPathIntent | undefined,
|
|
31
|
+
): Record<string, unknown> {
|
|
32
|
+
const candidates = boundedCallCandidates(resolution.candidates);
|
|
33
|
+
return {
|
|
34
|
+
...callLocationEvidence(call),
|
|
35
|
+
...selectedBindingEvidence(call),
|
|
36
|
+
...routingEvidence(call, servicePath, operationPath, destination, normalized, intent),
|
|
37
|
+
...candidateEvidence(candidates, resolution),
|
|
38
|
+
outboundEvidence: boundCandidateLikeEvidence(objectJson(call.evidence_json) ?? {}),
|
|
39
|
+
analysisCompleteness: call.unresolved_reason ? 'partial' : 'complete',
|
|
40
|
+
parserWarning: call.unresolved_reason
|
|
41
|
+
? { code: 'parser_warning', message: call.unresolved_reason }
|
|
42
|
+
: undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function ambiguousPathCandidates(
|
|
47
|
+
pathAnalysis: Record<string, unknown>,
|
|
48
|
+
): BoundedProjection<string> {
|
|
49
|
+
const values = Array.isArray(pathAnalysis.candidateRawPaths)
|
|
50
|
+
? pathAnalysis.candidateRawPaths.filter((value): value is string =>
|
|
51
|
+
typeof value === 'string')
|
|
52
|
+
: [];
|
|
53
|
+
return projectBounded(values, (left, right) => left.localeCompare(right));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function objectJson(value: unknown): Record<string, unknown> | undefined {
|
|
57
|
+
const parsed = parseJson(value);
|
|
58
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function objectValue(value: unknown): Record<string, unknown> | undefined {
|
|
62
|
+
return isRecord(value) ? value : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function callLocationEvidence(call: Record<string, unknown>): Record<string, unknown> {
|
|
66
|
+
return {
|
|
67
|
+
sourceFile: call.source_file,
|
|
68
|
+
sourceLine: call.source_line,
|
|
69
|
+
file: call.source_file,
|
|
70
|
+
line: call.source_line,
|
|
71
|
+
callId: call.id,
|
|
72
|
+
repo: call.repoName,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function selectedBindingEvidence(call: Record<string, unknown>): Record<string, unknown> {
|
|
77
|
+
if (!call.selectedBindingId) return {};
|
|
78
|
+
return {
|
|
79
|
+
selectedBindingId: call.selectedBindingId,
|
|
80
|
+
selectedBinding: {
|
|
81
|
+
bindingId: call.selectedBindingId,
|
|
82
|
+
alias: call.alias,
|
|
83
|
+
aliasExpr: call.aliasExpr,
|
|
84
|
+
destinationExpr: call.destinationExpr,
|
|
85
|
+
servicePathExpr: call.servicePathExpr,
|
|
86
|
+
sourceFile: call.bindingSourceFile,
|
|
87
|
+
sourceLine: call.bindingSourceLine,
|
|
88
|
+
helperChain: parseJson(call.helperChainJson),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function routingEvidence(
|
|
94
|
+
call: Record<string, unknown>,
|
|
95
|
+
servicePath: string | undefined,
|
|
96
|
+
operationPath: string | undefined,
|
|
97
|
+
destination: string | undefined,
|
|
98
|
+
normalized: NormalizedODataOperationPath | undefined,
|
|
99
|
+
intent: ODataPathIntent | undefined,
|
|
100
|
+
): Record<string, unknown> {
|
|
101
|
+
const routingPlaceholderKeys = placeholderKeys([
|
|
102
|
+
servicePath,
|
|
103
|
+
destination,
|
|
104
|
+
stringValue(call.aliasExpr),
|
|
105
|
+
stringValue(call.alias),
|
|
106
|
+
]);
|
|
107
|
+
return {
|
|
108
|
+
serviceAlias: call.alias,
|
|
109
|
+
serviceAliasExpr: call.aliasExpr,
|
|
110
|
+
destination,
|
|
111
|
+
servicePath,
|
|
112
|
+
operationPath,
|
|
113
|
+
rawOperationPath: normalized?.wasInvocation
|
|
114
|
+
? normalized.rawOperationPath
|
|
115
|
+
: intent?.rawPath,
|
|
116
|
+
normalizedOperationPath: normalized?.wasInvocation
|
|
117
|
+
? normalized.normalizedOperationPath
|
|
118
|
+
: undefined,
|
|
119
|
+
invocationArguments: normalized?.wasInvocation
|
|
120
|
+
? normalized.invocationArguments
|
|
121
|
+
: undefined,
|
|
122
|
+
invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length
|
|
123
|
+
? normalized.invocationArgumentPlaceholderKeys
|
|
124
|
+
: undefined,
|
|
125
|
+
routingPlaceholderKeys: routingPlaceholderKeys.length
|
|
126
|
+
? routingPlaceholderKeys
|
|
127
|
+
: undefined,
|
|
128
|
+
odataOperationNormalizationReason: normalized?.normalizationReason,
|
|
129
|
+
odataOperationNormalizationRejectedReason: normalized?.normalizationRejectedReason,
|
|
130
|
+
localServiceName: call.local_service_name,
|
|
131
|
+
localServiceLookup: call.local_service_lookup,
|
|
132
|
+
aliasChain: parseJson(call.alias_chain_json),
|
|
133
|
+
transport: call.call_type === 'local_service_call' ? 'local' : undefined,
|
|
134
|
+
helperChain: parseJson(call.helperChainJson),
|
|
135
|
+
odataPathIntent: intent,
|
|
136
|
+
queryStringPresent: intent?.hasQueryString || undefined,
|
|
137
|
+
queryPlaceholderKeys: intent?.placeholderKeys.length
|
|
138
|
+
? intent.placeholderKeys
|
|
139
|
+
: undefined,
|
|
140
|
+
bindingHasDynamicExpression: Boolean(Number(call.isDynamic ?? 0)) || undefined,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function candidateEvidence(
|
|
145
|
+
candidates: ReturnType<typeof boundedCallCandidates>,
|
|
146
|
+
resolution: LinkedOperationResolution,
|
|
147
|
+
): Record<string, unknown> {
|
|
148
|
+
return {
|
|
149
|
+
targetRepo: resolution.target?.repoName,
|
|
150
|
+
targetServicePath: resolution.target?.servicePath,
|
|
151
|
+
targetOperationPath: resolution.target?.operationPath,
|
|
152
|
+
targetOperation: resolution.target?.operationName,
|
|
153
|
+
candidates: candidates.items,
|
|
154
|
+
candidateScores: compactCandidateScores(candidates.items),
|
|
155
|
+
candidateCount: candidates.totalCount,
|
|
156
|
+
shownCandidateCount: candidates.shownCount,
|
|
157
|
+
omittedCandidateCount: candidates.omittedCount,
|
|
158
|
+
candidateScoreCount: candidates.totalCount,
|
|
159
|
+
shownCandidateScoreCount: candidates.shownCount,
|
|
160
|
+
omittedCandidateScoreCount: candidates.omittedCount,
|
|
161
|
+
resolutionStatus: resolution.status,
|
|
162
|
+
resolutionReasons: resolution.reasons,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function boundedCallCandidates(
|
|
167
|
+
candidates: unknown[],
|
|
168
|
+
): BoundedProjection<Record<string, unknown>> {
|
|
169
|
+
const rows = candidates.flatMap((candidate): Array<Record<string, unknown>> => {
|
|
170
|
+
const row = objectValue(candidate);
|
|
171
|
+
return row ? [row] : [];
|
|
172
|
+
});
|
|
173
|
+
return projectBounded(rows, compareCallCandidate);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function compareCallCandidate(
|
|
177
|
+
left: Record<string, unknown>,
|
|
178
|
+
right: Record<string, unknown>,
|
|
179
|
+
): number {
|
|
180
|
+
return Number(right.score ?? 0) - Number(left.score ?? 0)
|
|
181
|
+
|| String(left.repoName ?? '').localeCompare(String(right.repoName ?? ''))
|
|
182
|
+
|| String(left.servicePath ?? '').localeCompare(String(right.servicePath ?? ''))
|
|
183
|
+
|| String(left.operationPath ?? '').localeCompare(String(right.operationPath ?? ''))
|
|
184
|
+
|| Number(left.operationId ?? 0) - Number(right.operationId ?? 0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function compactCandidateScores(
|
|
188
|
+
candidates: Array<Record<string, unknown>>,
|
|
189
|
+
): Array<Record<string, unknown>> {
|
|
190
|
+
return candidates.map((candidate) => ({
|
|
191
|
+
repo: candidate.repoName,
|
|
192
|
+
servicePath: candidate.servicePath,
|
|
193
|
+
operationPath: candidate.operationPath,
|
|
194
|
+
score: candidate.score,
|
|
195
|
+
reasons: Array.isArray(candidate.reasons)
|
|
196
|
+
? candidate.reasons.filter((reason): reason is string =>
|
|
197
|
+
typeof reason === 'string')
|
|
198
|
+
: ['operation_path_match'],
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function placeholderKeys(values: Array<string | undefined>): string[] {
|
|
203
|
+
const keys = values.flatMap((value) => [...(value ?? '')
|
|
204
|
+
.matchAll(/\$\{([^}]*)\}/g)]
|
|
205
|
+
.map((match) => (match[1] ?? '').trim())
|
|
206
|
+
.filter(Boolean));
|
|
207
|
+
return [...new Set(keys)].sort();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseJson(value: unknown): unknown {
|
|
211
|
+
if (!value) return undefined;
|
|
212
|
+
try {
|
|
213
|
+
const parsed: unknown = JSON.parse(String(value));
|
|
214
|
+
return parsed;
|
|
215
|
+
} catch {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function stringValue(value: unknown): string | undefined {
|
|
221
|
+
return typeof value === 'string' ? value : undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
225
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
226
|
+
}
|