@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.
- package/CHANGELOG.md +14 -0
- package/README.md +16 -3
- package/TECHNICAL-NOTE.md +10 -1
- package/dist/{chunk-52OUS3MO.js → chunk-PTLDSHRC.js} +2663 -363
- package/dist/chunk-PTLDSHRC.js.map +1 -0
- package/dist/cli.js +318 -510
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +59 -17
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli.ts +97 -57
- package/src/db/connection.ts +1 -1
- package/src/db/migrations.ts +5 -3
- package/src/db/repositories.ts +120 -22
- package/src/db/schema.ts +7 -2
- package/src/index.ts +1 -1
- package/src/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/cross-repo-linker.ts +22 -2
- package/src/linker/service-resolver.ts +15 -4
- package/src/output/table-output.ts +56 -3
- package/src/parsers/cds-parser.ts +8 -2
- package/src/parsers/decorator-parser.ts +382 -48
- package/src/parsers/handler-registration-parser.ts +38 -21
- package/src/parsers/imported-wrapper-parser.ts +18 -5
- package/src/parsers/outbound-call-parser.ts +25 -8
- package/src/parsers/package-json-parser.ts +36 -11
- package/src/parsers/service-binding-parser-helpers.ts +8 -1
- package/src/parsers/service-binding-parser.ts +6 -3
- package/src/parsers/symbol-parser.ts +13 -3
- package/src/parsers/ts-project.ts +54 -0
- package/src/trace/000-dynamic-target-types.ts +84 -0
- package/src/trace/001-dynamic-identity.ts +280 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +82 -0
- package/src/trace/dynamic-branches.ts +45 -0
- package/src/trace/dynamic-targets.ts +654 -0
- package/src/trace/evidence.ts +391 -22
- package/src/trace/selectors.ts +483 -0
- package/src/trace/trace-engine.ts +101 -94
- package/src/types.ts +39 -1
- package/dist/chunk-52OUS3MO.js.map +0 -1
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import type {
|
|
3
|
+
DynamicTargetCandidate,
|
|
4
|
+
DynamicTemplates,
|
|
5
|
+
DynamicVariableProvenance,
|
|
6
|
+
} from './000-dynamic-target-types.js';
|
|
7
|
+
|
|
8
|
+
interface ImplementationOwner {
|
|
9
|
+
repoId?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface IdentityProposal {
|
|
13
|
+
operationId: number;
|
|
14
|
+
key: string;
|
|
15
|
+
value: string;
|
|
16
|
+
normalizedIdentity: string;
|
|
17
|
+
provenance: DynamicVariableProvenance;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface RepositoryIdentity {
|
|
21
|
+
repoId: number;
|
|
22
|
+
repoName: string;
|
|
23
|
+
packageName?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface WorkspaceIdentityMatch {
|
|
27
|
+
ownerKey: string;
|
|
28
|
+
key: string;
|
|
29
|
+
value: string;
|
|
30
|
+
normalizedIdentity: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface IdentityDerivation {
|
|
34
|
+
operationId: number;
|
|
35
|
+
key: string;
|
|
36
|
+
value: string;
|
|
37
|
+
provenance: DynamicVariableProvenance;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function uniqueIdentityDerivations(
|
|
41
|
+
db: Db,
|
|
42
|
+
candidates: DynamicTargetCandidate[],
|
|
43
|
+
templates: DynamicTemplates,
|
|
44
|
+
): IdentityDerivation[] {
|
|
45
|
+
const identities = workspaceIdentities(db, candidates);
|
|
46
|
+
const proposals = candidates.flatMap((candidate) => {
|
|
47
|
+
const owner = implementationOwner(db, candidate.candidateOperationId);
|
|
48
|
+
const identity = identities.find((item) => item.repoId === candidate.repoId);
|
|
49
|
+
return ownerAgrees(candidate, owner) && identity
|
|
50
|
+
? identityProposals(candidate, identity, templates)
|
|
51
|
+
: [];
|
|
52
|
+
});
|
|
53
|
+
const matches = workspaceIdentityMatches(identities, templates);
|
|
54
|
+
const competing = competingIdentityKeys(matches);
|
|
55
|
+
const duplicates = duplicateNormalizedIdentities(identities);
|
|
56
|
+
return proposals
|
|
57
|
+
.filter((proposal) => !competing.has(`${proposal.key}:${proposal.value}`))
|
|
58
|
+
.filter((proposal) => !duplicates.has(proposal.normalizedIdentity))
|
|
59
|
+
.map((proposal) => ({
|
|
60
|
+
operationId: proposal.operationId,
|
|
61
|
+
key: proposal.key,
|
|
62
|
+
value: proposal.value,
|
|
63
|
+
provenance: proposal.provenance,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function identityProposals(
|
|
68
|
+
candidate: DynamicTargetCandidate,
|
|
69
|
+
identity: RepositoryIdentity,
|
|
70
|
+
templates: DynamicTemplates,
|
|
71
|
+
): IdentityProposal[] {
|
|
72
|
+
const routeTemplates = [templates.alias, templates.destination]
|
|
73
|
+
.filter((value): value is string => Boolean(value));
|
|
74
|
+
const identities = [
|
|
75
|
+
{ name: identity.packageName, sourceKind: 'package_identity', npmPackage: true },
|
|
76
|
+
{ name: identity.repoName, sourceKind: 'repository_identity', npmPackage: false },
|
|
77
|
+
].filter((item): item is {
|
|
78
|
+
name: string; sourceKind: string; npmPackage: boolean;
|
|
79
|
+
} => Boolean(item.name));
|
|
80
|
+
const proposals = routeTemplates.flatMap((template) =>
|
|
81
|
+
identities.flatMap((identity) =>
|
|
82
|
+
proposalForIdentity(
|
|
83
|
+
candidate, template, identity.name, identity.sourceKind,
|
|
84
|
+
identity.npmPackage,
|
|
85
|
+
)));
|
|
86
|
+
return deduplicateProposals(proposals);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function proposalForIdentity(
|
|
90
|
+
candidate: DynamicTargetCandidate,
|
|
91
|
+
template: string,
|
|
92
|
+
identity: string,
|
|
93
|
+
sourceKind: string,
|
|
94
|
+
npmPackage: boolean,
|
|
95
|
+
): IdentityProposal[] {
|
|
96
|
+
const match = matchIdentityTemplate(template, identity, npmPackage);
|
|
97
|
+
if (!match) return [];
|
|
98
|
+
return [{
|
|
99
|
+
operationId: candidate.candidateOperationId,
|
|
100
|
+
key: match.key,
|
|
101
|
+
value: match.value,
|
|
102
|
+
normalizedIdentity: match.normalizedIdentity,
|
|
103
|
+
provenance: {
|
|
104
|
+
sourceKind,
|
|
105
|
+
value: match.value,
|
|
106
|
+
rule: 'exact_normalized_identity_template_match',
|
|
107
|
+
template,
|
|
108
|
+
matchedName: identity,
|
|
109
|
+
normalizedForm: match.normalizedIdentity,
|
|
110
|
+
sourceRepo: candidate.repoName,
|
|
111
|
+
},
|
|
112
|
+
}];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function matchIdentityTemplate(
|
|
116
|
+
template: string,
|
|
117
|
+
identity: string,
|
|
118
|
+
npmPackage: boolean,
|
|
119
|
+
): { key: string; value: string; normalizedIdentity: string } | undefined {
|
|
120
|
+
const matches = [...template.matchAll(/\$\{([^}]*)\}/g)];
|
|
121
|
+
if (matches.length !== 1 || !matches[0]?.[1]) return undefined;
|
|
122
|
+
const placeholder = matches[0][0];
|
|
123
|
+
const sentinel = 'dynamicplaceholdertoken';
|
|
124
|
+
const normalizedTemplate = normalizeIdentity(template.replace(placeholder, sentinel));
|
|
125
|
+
const [prefix, suffix, extra] = normalizedTemplate.split(sentinel);
|
|
126
|
+
if (!prefix || !suffix || extra !== undefined) return undefined;
|
|
127
|
+
const normalizedIdentity = normalizeIdentity(identity, npmPackage);
|
|
128
|
+
const match = new RegExp(`^${escapeRegex(prefix)}([a-z0-9]+)${escapeRegex(suffix)}$`)
|
|
129
|
+
.exec(normalizedIdentity);
|
|
130
|
+
if (!match?.[1]) return undefined;
|
|
131
|
+
return { key: matches[0][1].trim(), value: match[1], normalizedIdentity };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function competingIdentityKeys(
|
|
135
|
+
proposals: Array<{ key: string; value: string; ownerKey: string }>,
|
|
136
|
+
): Set<string> {
|
|
137
|
+
const owners = new Map<string, Set<string>>();
|
|
138
|
+
for (const proposal of proposals) {
|
|
139
|
+
const key = `${proposal.key}:${proposal.value}`;
|
|
140
|
+
owners.set(key, new Set([...(owners.get(key) ?? []), proposal.ownerKey]));
|
|
141
|
+
}
|
|
142
|
+
return new Set([...owners.entries()]
|
|
143
|
+
.filter(([, repos]) => repos.size > 1)
|
|
144
|
+
.map(([key]) => key));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function duplicateNormalizedIdentities(
|
|
148
|
+
identities: RepositoryIdentity[],
|
|
149
|
+
): Set<string> {
|
|
150
|
+
const owners = new Map<string, Set<string>>();
|
|
151
|
+
for (const identity of identities) {
|
|
152
|
+
for (const [name, npmPackage] of [
|
|
153
|
+
[identity.packageName, true],
|
|
154
|
+
[identity.repoName, false],
|
|
155
|
+
] as const) {
|
|
156
|
+
if (!name) continue;
|
|
157
|
+
const normalized = normalizeIdentity(name, npmPackage);
|
|
158
|
+
owners.set(normalized, new Set([
|
|
159
|
+
...(owners.get(normalized) ?? []),
|
|
160
|
+
`repository:${identity.repoId}`,
|
|
161
|
+
]));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return new Set([...owners.entries()]
|
|
165
|
+
.filter(([, repos]) => repos.size > 1)
|
|
166
|
+
.map(([identity]) => identity));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function workspaceIdentityMatches(
|
|
170
|
+
identities: RepositoryIdentity[],
|
|
171
|
+
templates: DynamicTemplates,
|
|
172
|
+
): WorkspaceIdentityMatch[] {
|
|
173
|
+
const routeTemplates = [templates.alias, templates.destination]
|
|
174
|
+
.filter((value): value is string => Boolean(value));
|
|
175
|
+
return identities.flatMap((identity) => {
|
|
176
|
+
const names: Array<{ name?: string; npmPackage: boolean }> = [
|
|
177
|
+
{ name: identity.packageName, npmPackage: true },
|
|
178
|
+
{ name: identity.repoName, npmPackage: false },
|
|
179
|
+
];
|
|
180
|
+
return routeTemplates.flatMap((template) =>
|
|
181
|
+
names.flatMap(({ name, npmPackage }) => {
|
|
182
|
+
if (!name) return [];
|
|
183
|
+
const match = matchIdentityTemplate(template, name, npmPackage);
|
|
184
|
+
return match ? [{
|
|
185
|
+
ownerKey: `repository:${identity.repoId}`,
|
|
186
|
+
key: match.key,
|
|
187
|
+
value: match.value,
|
|
188
|
+
normalizedIdentity: match.normalizedIdentity,
|
|
189
|
+
}] : [];
|
|
190
|
+
}));
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function workspaceIdentities(
|
|
195
|
+
db: Db,
|
|
196
|
+
candidates: DynamicTargetCandidate[],
|
|
197
|
+
): RepositoryIdentity[] {
|
|
198
|
+
const repoIds = [...new Set(candidates.flatMap((candidate) =>
|
|
199
|
+
candidate.repoId === undefined ? [] : [candidate.repoId]))].sort((a, b) => a - b);
|
|
200
|
+
if (repoIds.length === 0) return [];
|
|
201
|
+
const placeholders = repoIds.map(() => '?').join(',');
|
|
202
|
+
const rows = db.prepare(`SELECT id repoId,name repoName,package_name packageName
|
|
203
|
+
FROM repositories WHERE workspace_id IN (
|
|
204
|
+
SELECT DISTINCT workspace_id FROM repositories WHERE id IN (${placeholders})
|
|
205
|
+
) ORDER BY workspace_id,name,absolute_path,id`).all(...repoIds);
|
|
206
|
+
return rows.flatMap((row): RepositoryIdentity[] => {
|
|
207
|
+
const repoId = numberValue(row.repoId);
|
|
208
|
+
const repoName = stringValue(row.repoName);
|
|
209
|
+
return repoId === undefined || !repoName ? [] : [{
|
|
210
|
+
repoId,
|
|
211
|
+
repoName,
|
|
212
|
+
packageName: stringValue(row.packageName),
|
|
213
|
+
}];
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function deduplicateProposals(rows: IdentityProposal[]): IdentityProposal[] {
|
|
218
|
+
const sorted = [...rows].sort((left, right) =>
|
|
219
|
+
left.operationId - right.operationId
|
|
220
|
+
|| left.key.localeCompare(right.key)
|
|
221
|
+
|| left.value.localeCompare(right.value)
|
|
222
|
+
|| left.provenance.sourceKind.localeCompare(right.provenance.sourceKind));
|
|
223
|
+
const seen = new Set<string>();
|
|
224
|
+
return sorted.filter((row) => {
|
|
225
|
+
const key = [row.operationId, row.key, row.value, row.normalizedIdentity].join(':');
|
|
226
|
+
if (seen.has(key)) return false;
|
|
227
|
+
seen.add(key);
|
|
228
|
+
return true;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function ownerAgrees(
|
|
233
|
+
candidate: DynamicTargetCandidate,
|
|
234
|
+
owner: ImplementationOwner | undefined,
|
|
235
|
+
): boolean {
|
|
236
|
+
return candidate.repoId !== undefined
|
|
237
|
+
&& owner?.repoId !== undefined
|
|
238
|
+
&& owner.repoId === candidate.repoId;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function implementationOwner(db: Db, operationId: number): ImplementationOwner | undefined {
|
|
242
|
+
const rows = db.prepare(
|
|
243
|
+
`SELECT r.id repoId
|
|
244
|
+
FROM graph_edges e JOIN handler_methods hm ON hm.id=CAST(e.to_id AS INTEGER)
|
|
245
|
+
JOIN handler_classes hc ON hc.id=hm.handler_class_id
|
|
246
|
+
JOIN repositories r ON r.id=hc.repo_id
|
|
247
|
+
WHERE e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status='resolved'
|
|
248
|
+
AND e.from_kind='operation' AND e.from_id=?
|
|
249
|
+
ORDER BY r.id,hm.id,e.id`,
|
|
250
|
+
).all(String(operationId));
|
|
251
|
+
if (rows.length !== 1) return undefined;
|
|
252
|
+
const row = rows[0];
|
|
253
|
+
if (!row) return undefined;
|
|
254
|
+
return {
|
|
255
|
+
repoId: numberValue(row.repoId),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function normalizeIdentity(value: string, npmPackage = false): string {
|
|
260
|
+
const unscoped = npmPackage && /^@[^/]+\/[^/]+$/.test(value)
|
|
261
|
+
? value.slice(value.indexOf('/') + 1)
|
|
262
|
+
: value;
|
|
263
|
+
return unscoped
|
|
264
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
265
|
+
.toLowerCase()
|
|
266
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
267
|
+
.replace(/^_+|_+$/g, '');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function stringValue(value: unknown): string | undefined {
|
|
271
|
+
return typeof value === 'string' ? value : undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function numberValue(value: unknown): number | undefined {
|
|
275
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function escapeRegex(value: string): string {
|
|
279
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
280
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
|
|
3
|
+
export function loadTraceDiagnostics(
|
|
4
|
+
db: Db,
|
|
5
|
+
repoId: number | undefined,
|
|
6
|
+
includeWorkspaceDiagnostics: boolean,
|
|
7
|
+
workspaceId?: number,
|
|
8
|
+
): Array<Record<string, unknown>> {
|
|
9
|
+
if (repoId === undefined && !includeWorkspaceDiagnostics) return [];
|
|
10
|
+
return db.prepare(`SELECT d.repo_id repoId,d.severity,d.code,d.message,
|
|
11
|
+
d.source_file sourceFile,d.source_line sourceLine
|
|
12
|
+
FROM diagnostics d LEFT JOIN repositories r ON r.id=d.repo_id
|
|
13
|
+
WHERE (? IS NULL OR d.repo_id=?)
|
|
14
|
+
AND (? IS NULL OR d.repo_id IS NULL OR r.workspace_id=?)
|
|
15
|
+
ORDER BY severity,code,COALESCE(source_file,''),
|
|
16
|
+
COALESCE(source_line,0),d.id`).all(
|
|
17
|
+
repoId, repoId, workspaceId, workspaceId,
|
|
18
|
+
) as Array<
|
|
19
|
+
Record<string, unknown>
|
|
20
|
+
>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function prependTraceDiagnostic(
|
|
24
|
+
diagnostics: Array<Record<string, unknown>>,
|
|
25
|
+
diagnostic: Record<string, unknown>,
|
|
26
|
+
): void {
|
|
27
|
+
const duplicate = diagnostics.findIndex((item) =>
|
|
28
|
+
sameDiagnosticLocation(item, diagnostic));
|
|
29
|
+
if (duplicate >= 0) diagnostics.splice(duplicate, 1);
|
|
30
|
+
diagnostics.unshift(diagnostic);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sameDiagnosticLocation(
|
|
34
|
+
left: Record<string, unknown>,
|
|
35
|
+
right: Record<string, unknown>,
|
|
36
|
+
): boolean {
|
|
37
|
+
if (left.code !== right.code) return false;
|
|
38
|
+
const leftFile = stringValue(left.sourceFile);
|
|
39
|
+
const rightFile = stringValue(right.sourceFile);
|
|
40
|
+
if (leftFile || rightFile)
|
|
41
|
+
return leftFile === rightFile
|
|
42
|
+
&& numericValue(left.sourceLine) === numericValue(right.sourceLine);
|
|
43
|
+
return String(left.message ?? '') === String(right.message ?? '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stringValue(value: unknown): string | undefined {
|
|
47
|
+
return typeof value === 'string' ? value : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function numericValue(value: unknown): number | undefined {
|
|
51
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
52
|
+
? value
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import type { DynamicVariableProvenance } from './000-dynamic-target-types.js';
|
|
3
|
+
|
|
4
|
+
export interface DynamicReferenceRow {
|
|
5
|
+
alias?: string;
|
|
6
|
+
destination?: string;
|
|
7
|
+
servicePath?: string;
|
|
8
|
+
sourceKind: 'service_binding' | 'cds_require';
|
|
9
|
+
repoName: string;
|
|
10
|
+
sourceFile?: string;
|
|
11
|
+
sourceLine?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function dynamicReferenceRows(
|
|
15
|
+
db: Db,
|
|
16
|
+
workspaceId: number | undefined,
|
|
17
|
+
callerRepoId: number | undefined,
|
|
18
|
+
callerRepo: string | undefined,
|
|
19
|
+
): DynamicReferenceRow[] {
|
|
20
|
+
if (callerRepoId === undefined && callerRepo === undefined) return [];
|
|
21
|
+
const rows = db.prepare(
|
|
22
|
+
`SELECT COALESCE(b.alias,b.alias_expr) alias,b.destination_expr destination,
|
|
23
|
+
b.service_path_expr servicePath,'service_binding' sourceKind,r.name repoName,
|
|
24
|
+
b.source_file sourceFile,b.source_line sourceLine,0 sourcePriority
|
|
25
|
+
FROM service_bindings b JOIN repositories r ON r.id=b.repo_id
|
|
26
|
+
WHERE (? IS NULL OR r.workspace_id=?)
|
|
27
|
+
AND ((? IS NOT NULL AND r.id=?) OR (? IS NULL AND r.name=?))
|
|
28
|
+
UNION ALL
|
|
29
|
+
SELECT req.alias,req.destination,req.service_path,'cds_require',r.name,
|
|
30
|
+
'package.json',1,1 FROM cds_requires req JOIN repositories r ON r.id=req.repo_id
|
|
31
|
+
WHERE (? IS NULL OR r.workspace_id=?)
|
|
32
|
+
AND ((? IS NOT NULL AND r.id=?) OR (? IS NULL AND r.name=?))
|
|
33
|
+
ORDER BY sourcePriority,repoName,sourceFile,sourceLine`,
|
|
34
|
+
).all(
|
|
35
|
+
workspaceId, workspaceId,
|
|
36
|
+
callerRepoId, callerRepoId, callerRepoId, callerRepo,
|
|
37
|
+
workspaceId, workspaceId,
|
|
38
|
+
callerRepoId, callerRepoId, callerRepoId, callerRepo,
|
|
39
|
+
);
|
|
40
|
+
return rows.flatMap(referenceFromRow);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function dynamicReferenceProvenance(
|
|
44
|
+
reference: DynamicReferenceRow,
|
|
45
|
+
kind: 'alias' | 'destination',
|
|
46
|
+
template: string,
|
|
47
|
+
value: string,
|
|
48
|
+
): DynamicVariableProvenance {
|
|
49
|
+
return {
|
|
50
|
+
sourceKind: `${reference.sourceKind}.${kind}`,
|
|
51
|
+
value,
|
|
52
|
+
rule: 'exact_indexed_reference_template_match',
|
|
53
|
+
template,
|
|
54
|
+
sourceRepo: reference.repoName,
|
|
55
|
+
sourceFile: reference.sourceFile,
|
|
56
|
+
sourceLine: reference.sourceLine,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function referenceFromRow(row: Record<string, unknown>): DynamicReferenceRow[] {
|
|
61
|
+
const sourceKind = row.sourceKind;
|
|
62
|
+
const repoName = stringValue(row.repoName);
|
|
63
|
+
if ((sourceKind !== 'service_binding' && sourceKind !== 'cds_require')
|
|
64
|
+
|| !repoName) return [];
|
|
65
|
+
return [{
|
|
66
|
+
alias: stringValue(row.alias),
|
|
67
|
+
destination: stringValue(row.destination),
|
|
68
|
+
servicePath: stringValue(row.servicePath),
|
|
69
|
+
sourceKind,
|
|
70
|
+
repoName,
|
|
71
|
+
sourceFile: stringValue(row.sourceFile),
|
|
72
|
+
sourceLine: numberValue(row.sourceLine),
|
|
73
|
+
}];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stringValue(value: unknown): string | undefined {
|
|
77
|
+
return typeof value === 'string' ? value : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function numberValue(value: unknown): number | undefined {
|
|
81
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
82
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TraceEdge } from '../types.js';
|
|
2
|
+
|
|
3
|
+
interface DynamicBranchCall {
|
|
4
|
+
repoName: string;
|
|
5
|
+
source_file: string;
|
|
6
|
+
source_line: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function dynamicCandidateBranches(
|
|
10
|
+
depth: number,
|
|
11
|
+
call: DynamicBranchCall,
|
|
12
|
+
evidence: Record<string, unknown>,
|
|
13
|
+
): TraceEdge[] {
|
|
14
|
+
const exploration = objectRecord(evidence.dynamicTargetExploration);
|
|
15
|
+
return recordArray(evidence.dynamicTargetCandidateSuggestions).map((candidate) => ({
|
|
16
|
+
step: depth,
|
|
17
|
+
type: 'dynamic_candidate_branch',
|
|
18
|
+
from: `${call.repoName}:${call.source_file}:${call.source_line}`,
|
|
19
|
+
to: `${String(candidate.servicePath ?? '')}${String(candidate.operationPath ?? '')}`,
|
|
20
|
+
evidence: {
|
|
21
|
+
...candidate,
|
|
22
|
+
exploratory: true,
|
|
23
|
+
dynamicMode: String(exploration.mode ?? 'candidates'),
|
|
24
|
+
selected: false,
|
|
25
|
+
omittedCandidateCount: numericValue(exploration.omittedCandidateCount),
|
|
26
|
+
},
|
|
27
|
+
confidence: numericValue(candidate.score),
|
|
28
|
+
unresolvedReason: 'Exploratory dynamic target candidate; provide runtime variables to select it',
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function objectRecord(value: unknown): Record<string, unknown> {
|
|
33
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function recordArray(value: unknown): Record<string, unknown>[] {
|
|
37
|
+
return Array.isArray(value)
|
|
38
|
+
? value.filter((item): item is Record<string, unknown> =>
|
|
39
|
+
Boolean(item && typeof item === 'object' && !Array.isArray(item)))
|
|
40
|
+
: [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function numericValue(value: unknown): number {
|
|
44
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
45
|
+
}
|