@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
@@ -0,0 +1,308 @@
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 RouteImplementationEvidence {
9
+ routeRepoId?: number;
10
+ handlerRepo?: string;
11
+ operationProvenance?: string;
12
+ baseOperationId?: number;
13
+ edgeStatus?: string;
14
+ }
15
+
16
+ interface IdentityProposal {
17
+ operationId: number;
18
+ key: string;
19
+ value: string;
20
+ normalizedIdentity: string;
21
+ provenance: DynamicVariableProvenance;
22
+ }
23
+
24
+ interface RepositoryIdentity {
25
+ repoId: number;
26
+ repoName: string;
27
+ packageName?: string;
28
+ }
29
+
30
+ interface WorkspaceIdentityMatch {
31
+ ownerKey: string;
32
+ key: string;
33
+ value: string;
34
+ normalizedIdentity: string;
35
+ }
36
+
37
+ export interface IdentityDerivation {
38
+ operationId: number;
39
+ key: string;
40
+ value: string;
41
+ provenance: DynamicVariableProvenance;
42
+ }
43
+
44
+ export function uniqueIdentityDerivations(
45
+ db: Db,
46
+ candidates: DynamicTargetCandidate[],
47
+ templates: DynamicTemplates,
48
+ ): IdentityDerivation[] {
49
+ const identities = workspaceIdentities(db, candidates);
50
+ const proposals = candidates.flatMap((candidate) => {
51
+ const implementation = routeImplementationEvidence(
52
+ db, candidate.candidateOperationId,
53
+ );
54
+ const identity = identities.find((item) => item.repoId === candidate.repoId);
55
+ if (!identity || !implementation || !routeOwnerAgrees(candidate, implementation))
56
+ return [];
57
+ return identityProposals(candidate, identity, templates, implementation);
58
+ });
59
+ const matches = workspaceIdentityMatches(identities, templates);
60
+ const competing = competingIdentityKeys(matches);
61
+ const duplicates = duplicateNormalizedIdentities(identities);
62
+ return proposals
63
+ .filter((proposal) => !competing.has(`${proposal.key}:${proposal.value}`))
64
+ .filter((proposal) => !duplicates.has(proposal.normalizedIdentity))
65
+ .map((proposal) => ({
66
+ operationId: proposal.operationId,
67
+ key: proposal.key,
68
+ value: proposal.value,
69
+ provenance: proposal.provenance,
70
+ }));
71
+ }
72
+
73
+ function identityProposals(
74
+ candidate: DynamicTargetCandidate,
75
+ identity: RepositoryIdentity,
76
+ templates: DynamicTemplates,
77
+ implementation: RouteImplementationEvidence,
78
+ ): IdentityProposal[] {
79
+ const routeTemplates = [templates.alias, templates.destination]
80
+ .filter((value): value is string => Boolean(value));
81
+ const identities = [
82
+ { name: identity.packageName, sourceKind: 'package_identity', npmPackage: true },
83
+ { name: identity.repoName, sourceKind: 'repository_identity', npmPackage: false },
84
+ ].filter((item): item is {
85
+ name: string; sourceKind: string; npmPackage: boolean;
86
+ } => Boolean(item.name));
87
+ const proposals = routeTemplates.flatMap((template) =>
88
+ identities.flatMap((identity) =>
89
+ proposalForIdentity(
90
+ candidate, template, identity.name, identity.sourceKind,
91
+ identity.npmPackage, implementation,
92
+ )));
93
+ return deduplicateProposals(proposals);
94
+ }
95
+
96
+ function proposalForIdentity(
97
+ candidate: DynamicTargetCandidate,
98
+ template: string,
99
+ identity: string,
100
+ sourceKind: string,
101
+ npmPackage: boolean,
102
+ implementation: RouteImplementationEvidence,
103
+ ): IdentityProposal[] {
104
+ const match = matchIdentityTemplate(template, identity, npmPackage);
105
+ if (!match) return [];
106
+ return [{
107
+ operationId: candidate.candidateOperationId,
108
+ key: match.key,
109
+ value: match.value,
110
+ normalizedIdentity: match.normalizedIdentity,
111
+ provenance: {
112
+ sourceKind,
113
+ value: match.value,
114
+ rule: 'exact_normalized_identity_template_match',
115
+ template,
116
+ matchedName: identity,
117
+ normalizedForm: match.normalizedIdentity,
118
+ sourceRepo: candidate.repoName,
119
+ routeOwner: candidate.repoName,
120
+ candidateOperationId: candidate.candidateOperationId,
121
+ effectiveBaseOperationId: implementation.baseOperationId,
122
+ candidateOperationProvenance: implementation.operationProvenance,
123
+ implementationEdgeStatus: implementation.edgeStatus,
124
+ implementationHandlerRepo: implementation.handlerRepo,
125
+ },
126
+ }];
127
+ }
128
+
129
+ function matchIdentityTemplate(
130
+ template: string,
131
+ identity: string,
132
+ npmPackage: boolean,
133
+ ): { key: string; value: string; normalizedIdentity: string } | undefined {
134
+ const matches = [...template.matchAll(/\$\{([^}]*)\}/g)];
135
+ if (matches.length !== 1 || !matches[0]?.[1]) return undefined;
136
+ const placeholder = matches[0][0];
137
+ const sentinel = 'dynamicplaceholdertoken';
138
+ const normalizedTemplate = normalizeIdentity(template.replace(placeholder, sentinel));
139
+ const [prefix, suffix, extra] = normalizedTemplate.split(sentinel);
140
+ if (!prefix || !suffix || extra !== undefined) return undefined;
141
+ const normalizedIdentity = normalizeIdentity(identity, npmPackage);
142
+ const match = new RegExp(`^${escapeRegex(prefix)}([a-z0-9]+)${escapeRegex(suffix)}$`)
143
+ .exec(normalizedIdentity);
144
+ if (!match?.[1]) return undefined;
145
+ return { key: matches[0][1].trim(), value: match[1], normalizedIdentity };
146
+ }
147
+
148
+ function competingIdentityKeys(
149
+ proposals: Array<{ key: string; value: string; ownerKey: string }>,
150
+ ): Set<string> {
151
+ const owners = new Map<string, Set<string>>();
152
+ for (const proposal of proposals) {
153
+ const key = `${proposal.key}:${proposal.value}`;
154
+ owners.set(key, new Set([...(owners.get(key) ?? []), proposal.ownerKey]));
155
+ }
156
+ return new Set([...owners.entries()]
157
+ .filter(([, repos]) => repos.size > 1)
158
+ .map(([key]) => key));
159
+ }
160
+
161
+ function duplicateNormalizedIdentities(
162
+ identities: RepositoryIdentity[],
163
+ ): Set<string> {
164
+ const owners = new Map<string, Set<string>>();
165
+ for (const identity of identities) {
166
+ for (const [name, npmPackage] of [
167
+ [identity.packageName, true],
168
+ [identity.repoName, false],
169
+ ] as const) {
170
+ if (!name) continue;
171
+ const normalized = normalizeIdentity(name, npmPackage);
172
+ owners.set(normalized, new Set([
173
+ ...(owners.get(normalized) ?? []),
174
+ `repository:${identity.repoId}`,
175
+ ]));
176
+ }
177
+ }
178
+ return new Set([...owners.entries()]
179
+ .filter(([, repos]) => repos.size > 1)
180
+ .map(([identity]) => identity));
181
+ }
182
+
183
+ function workspaceIdentityMatches(
184
+ identities: RepositoryIdentity[],
185
+ templates: DynamicTemplates,
186
+ ): WorkspaceIdentityMatch[] {
187
+ const routeTemplates = [templates.alias, templates.destination]
188
+ .filter((value): value is string => Boolean(value));
189
+ return identities.flatMap((identity) => {
190
+ const names: Array<{ name?: string; npmPackage: boolean }> = [
191
+ { name: identity.packageName, npmPackage: true },
192
+ { name: identity.repoName, npmPackage: false },
193
+ ];
194
+ return routeTemplates.flatMap((template) =>
195
+ names.flatMap(({ name, npmPackage }) => {
196
+ if (!name) return [];
197
+ const match = matchIdentityTemplate(template, name, npmPackage);
198
+ return match ? [{
199
+ ownerKey: `repository:${identity.repoId}`,
200
+ key: match.key,
201
+ value: match.value,
202
+ normalizedIdentity: match.normalizedIdentity,
203
+ }] : [];
204
+ }));
205
+ });
206
+ }
207
+
208
+ function workspaceIdentities(
209
+ db: Db,
210
+ candidates: DynamicTargetCandidate[],
211
+ ): RepositoryIdentity[] {
212
+ const repoIds = [...new Set(candidates.flatMap((candidate) =>
213
+ candidate.repoId === undefined ? [] : [candidate.repoId]))].sort((a, b) => a - b);
214
+ if (repoIds.length === 0) return [];
215
+ const placeholders = repoIds.map(() => '?').join(',');
216
+ const rows = db.prepare(`SELECT id repoId,name repoName,package_name packageName
217
+ FROM repositories WHERE workspace_id IN (
218
+ SELECT DISTINCT workspace_id FROM repositories WHERE id IN (${placeholders})
219
+ ) ORDER BY workspace_id,name,absolute_path,id`).all(...repoIds);
220
+ return rows.flatMap((row): RepositoryIdentity[] => {
221
+ const repoId = numberValue(row.repoId);
222
+ const repoName = stringValue(row.repoName);
223
+ return repoId === undefined || !repoName ? [] : [{
224
+ repoId,
225
+ repoName,
226
+ packageName: stringValue(row.packageName),
227
+ }];
228
+ });
229
+ }
230
+
231
+ function deduplicateProposals(rows: IdentityProposal[]): IdentityProposal[] {
232
+ const sorted = [...rows].sort((left, right) =>
233
+ left.operationId - right.operationId
234
+ || left.key.localeCompare(right.key)
235
+ || left.value.localeCompare(right.value)
236
+ || left.provenance.sourceKind.localeCompare(right.provenance.sourceKind));
237
+ const seen = new Set<string>();
238
+ return sorted.filter((row) => {
239
+ const key = [row.operationId, row.key, row.value, row.normalizedIdentity].join(':');
240
+ if (seen.has(key)) return false;
241
+ seen.add(key);
242
+ return true;
243
+ });
244
+ }
245
+
246
+ function routeOwnerAgrees(
247
+ candidate: DynamicTargetCandidate,
248
+ implementation: RouteImplementationEvidence | undefined,
249
+ ): boolean {
250
+ return candidate.repoId !== undefined
251
+ && implementation?.routeRepoId === candidate.repoId
252
+ && implementation.edgeStatus === 'resolved'
253
+ && candidate.viable
254
+ && candidate.reasons.includes('service_path_template_match');
255
+ }
256
+
257
+ function routeImplementationEvidence(
258
+ db: Db,
259
+ operationId: number,
260
+ ): RouteImplementationEvidence | undefined {
261
+ const rows = db.prepare(
262
+ `SELECT s.repo_id routeRepoId,o.provenance operationProvenance,
263
+ o.base_operation_id baseOperationId,e.status edgeStatus,
264
+ r.name handlerRepo
265
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
266
+ JOIN graph_edges e ON e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER'
267
+ AND e.from_kind='operation' AND e.from_id=CAST(o.id AS TEXT)
268
+ JOIN handler_methods hm ON hm.id=CAST(e.to_id AS INTEGER)
269
+ JOIN handler_classes hc ON hc.id=hm.handler_class_id
270
+ JOIN repositories r ON r.id=hc.repo_id
271
+ WHERE e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status='resolved'
272
+ AND o.id=?
273
+ ORDER BY e.id,hm.id`,
274
+ ).all(String(operationId));
275
+ if (rows.length !== 1) return undefined;
276
+ const row = rows[0];
277
+ if (!row) return undefined;
278
+ return {
279
+ routeRepoId: numberValue(row.routeRepoId),
280
+ handlerRepo: stringValue(row.handlerRepo),
281
+ operationProvenance: stringValue(row.operationProvenance),
282
+ baseOperationId: numberValue(row.baseOperationId),
283
+ edgeStatus: stringValue(row.edgeStatus),
284
+ };
285
+ }
286
+
287
+ function normalizeIdentity(value: string, npmPackage = false): string {
288
+ const unscoped = npmPackage && /^@[^/]+\/[^/]+$/.test(value)
289
+ ? value.slice(value.indexOf('/') + 1)
290
+ : value;
291
+ return unscoped
292
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
293
+ .toLowerCase()
294
+ .replace(/[^a-z0-9]+/g, '_')
295
+ .replace(/^_+|_+$/g, '');
296
+ }
297
+
298
+ function stringValue(value: unknown): string | undefined {
299
+ return typeof value === 'string' ? value : undefined;
300
+ }
301
+
302
+ function numberValue(value: unknown): number | undefined {
303
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
304
+ }
305
+
306
+ function escapeRegex(value: string): string {
307
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
308
+ }
@@ -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,283 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import {
3
+ projectBounded,
4
+ type BoundedProjection,
5
+ } from '../utils/000-bounded-projection.js';
6
+ import type { DynamicVariableProvenance } from './000-dynamic-target-types.js';
7
+
8
+ export interface DynamicReferenceRow {
9
+ bindingId?: number;
10
+ alias?: string;
11
+ aliasExpr?: string;
12
+ destination?: string;
13
+ servicePath?: string;
14
+ sourceKind: 'service_binding' | 'cds_require';
15
+ selection: 'selected_binding' | 'selected_binding_require' | 'fallback';
16
+ repoName: string;
17
+ sourceFile?: string;
18
+ sourceLine?: number;
19
+ helperChain?: unknown;
20
+ }
21
+
22
+ export interface DynamicRoutingContext {
23
+ outboundCallId?: number;
24
+ callerRepoId?: number;
25
+ callerRepo?: string;
26
+ selectedBindingId?: number;
27
+ bindingResolutionStatus: string;
28
+ selectedBinding?: DynamicReferenceRow;
29
+ bindingAlternatives: Array<Record<string, unknown>>;
30
+ bindingAlternativeCount: number;
31
+ shownBindingAlternativeCount: number;
32
+ omittedBindingAlternativeCount: number;
33
+ references: DynamicReferenceRow[];
34
+ fallbackUsed: boolean;
35
+ }
36
+
37
+ interface SelectedDynamicReference extends DynamicReferenceRow {
38
+ outboundCallId: number;
39
+ callerRepoId: number;
40
+ }
41
+
42
+ export function dynamicRoutingContext(
43
+ db: Db,
44
+ workspaceId: number | undefined,
45
+ evidence: Record<string, unknown>,
46
+ ): DynamicRoutingContext {
47
+ const selected = selectedBinding(db, workspaceId, evidence);
48
+ const persisted = persistedBindingResolution(evidence);
49
+ const alternatives = boundedAlternatives(
50
+ persisted.candidates,
51
+ persisted.candidateCount,
52
+ );
53
+ if (selected) {
54
+ const requires = exactRequireReferences(db, workspaceId, selected);
55
+ return {
56
+ ...contextBase(selected, persisted.status, alternatives),
57
+ selectedBinding: selected,
58
+ references: [selected, ...requires],
59
+ fallbackUsed: false,
60
+ };
61
+ }
62
+ const callerRepoId = numberValue(evidence.repoId);
63
+ const callerRepo = stringValue(evidence.repo);
64
+ return {
65
+ outboundCallId: numberValue(evidence.outboundCallId ?? evidence.callId),
66
+ callerRepoId,
67
+ callerRepo,
68
+ bindingResolutionStatus: persisted.status,
69
+ bindingAlternatives: alternatives.items,
70
+ bindingAlternativeCount: alternatives.totalCount,
71
+ shownBindingAlternativeCount: alternatives.shownCount,
72
+ omittedBindingAlternativeCount: alternatives.omittedCount,
73
+ references: fallbackReferences(db, workspaceId, callerRepoId, callerRepo),
74
+ fallbackUsed: true,
75
+ };
76
+ }
77
+
78
+ export function dynamicReferenceProvenance(
79
+ reference: DynamicReferenceRow,
80
+ kind: 'alias' | 'destination',
81
+ template: string,
82
+ value: string,
83
+ ): DynamicVariableProvenance {
84
+ const sourceKind = reference.selection === 'selected_binding'
85
+ ? `selected_binding.${kind}`
86
+ : reference.selection === 'selected_binding_require'
87
+ ? `selected_binding_require.${kind}`
88
+ : `${reference.sourceKind}.${kind}`;
89
+ return {
90
+ sourceKind,
91
+ value,
92
+ rule: 'exact_indexed_reference_template_match',
93
+ template,
94
+ sourceRepo: reference.repoName,
95
+ sourceFile: reference.sourceFile,
96
+ sourceLine: reference.sourceLine,
97
+ selection: reference.selection,
98
+ bindingId: reference.bindingId,
99
+ };
100
+ }
101
+
102
+ function contextBase(
103
+ selected: SelectedDynamicReference,
104
+ status: string,
105
+ alternatives: ReturnType<typeof boundedAlternatives>,
106
+ ): Omit<DynamicRoutingContext, 'selectedBinding' | 'references' | 'fallbackUsed'> {
107
+ return {
108
+ outboundCallId: selected.outboundCallId,
109
+ callerRepoId: selected.callerRepoId,
110
+ callerRepo: selected.repoName,
111
+ selectedBindingId: selected.bindingId,
112
+ bindingResolutionStatus: status,
113
+ bindingAlternatives: alternatives.items,
114
+ bindingAlternativeCount: alternatives.totalCount,
115
+ shownBindingAlternativeCount: alternatives.shownCount,
116
+ omittedBindingAlternativeCount: alternatives.omittedCount,
117
+ };
118
+ }
119
+
120
+ function selectedBinding(
121
+ db: Db,
122
+ workspaceId: number | undefined,
123
+ evidence: Record<string, unknown>,
124
+ ): SelectedDynamicReference | undefined {
125
+ const callId = numberValue(evidence.outboundCallId ?? evidence.callId);
126
+ if (callId === undefined) return undefined;
127
+ const row = db.prepare(`SELECT c.id outboundCallId,c.repo_id callerRepoId,r.name repoName,
128
+ b.id bindingId,b.alias,b.alias_expr aliasExpr,b.destination_expr destination,
129
+ b.service_path_expr servicePath,b.source_file sourceFile,b.source_line sourceLine,
130
+ b.helper_chain_json helperChainJson
131
+ FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
132
+ JOIN service_bindings b ON b.id=c.service_binding_id AND b.repo_id=c.repo_id
133
+ WHERE c.id=? AND (? IS NULL OR r.workspace_id=?)`).get(
134
+ callId, workspaceId, workspaceId,
135
+ );
136
+ return selectedReferenceFromRow(row);
137
+ }
138
+
139
+ function selectedReferenceFromRow(
140
+ row: Record<string, unknown> | undefined,
141
+ ): SelectedDynamicReference | undefined {
142
+ const outboundCallId = numberValue(row?.outboundCallId);
143
+ const callerRepoId = numberValue(row?.callerRepoId);
144
+ const reference = referenceFromRow(
145
+ row, 'service_binding', 'selected_binding',
146
+ )[0];
147
+ return reference && outboundCallId !== undefined && callerRepoId !== undefined
148
+ ? { ...reference, outboundCallId, callerRepoId }
149
+ : undefined;
150
+ }
151
+
152
+ function exactRequireReferences(
153
+ db: Db,
154
+ workspaceId: number | undefined,
155
+ selected: SelectedDynamicReference,
156
+ ): DynamicReferenceRow[] {
157
+ if (!(selected.aliasExpr ?? selected.alias)) return [];
158
+ const rows = db.prepare(`SELECT req.alias,req.destination,req.service_path servicePath,
159
+ r.name repoName,'package.json' sourceFile,1 sourceLine
160
+ FROM cds_requires req JOIN repositories r ON r.id=req.repo_id
161
+ WHERE req.repo_id=? AND (? IS NULL OR r.workspace_id=?)
162
+ ORDER BY req.alias,req.id`).all(
163
+ selected.callerRepoId, workspaceId, workspaceId,
164
+ );
165
+ return rows.flatMap((row) => referenceFromRow(
166
+ row, 'cds_require', 'selected_binding_require', selected.bindingId,
167
+ ));
168
+ }
169
+
170
+ function fallbackReferences(
171
+ db: Db,
172
+ workspaceId: number | undefined,
173
+ callerRepoId: number | undefined,
174
+ callerRepo: string | undefined,
175
+ ): DynamicReferenceRow[] {
176
+ if (callerRepoId === undefined && callerRepo === undefined) return [];
177
+ const rows = db.prepare(`SELECT b.id bindingId,COALESCE(b.alias,b.alias_expr) alias,
178
+ b.alias_expr aliasExpr,b.destination_expr destination,b.service_path_expr servicePath,
179
+ 'service_binding' sourceKind,r.name repoName,b.source_file sourceFile,
180
+ b.source_line sourceLine,b.helper_chain_json helperChainJson,0 sourcePriority
181
+ FROM service_bindings b JOIN repositories r ON r.id=b.repo_id
182
+ WHERE (? IS NULL OR r.workspace_id=?)
183
+ AND ((? IS NOT NULL AND r.id=?) OR (? IS NULL AND r.name=?))
184
+ UNION ALL
185
+ SELECT NULL,req.alias,req.alias,req.destination,req.service_path,
186
+ 'cds_require',r.name,'package.json',1,NULL,1
187
+ FROM cds_requires req JOIN repositories r ON r.id=req.repo_id
188
+ WHERE (? IS NULL OR r.workspace_id=?)
189
+ AND ((? IS NOT NULL AND r.id=?) OR (? IS NULL AND r.name=?))
190
+ ORDER BY sourcePriority,repoName,sourceFile,sourceLine`).all(
191
+ workspaceId, workspaceId, callerRepoId, callerRepoId, callerRepoId, callerRepo,
192
+ workspaceId, workspaceId, callerRepoId, callerRepoId, callerRepoId, callerRepo,
193
+ );
194
+ return rows.flatMap((row) => {
195
+ const sourceKind = row.sourceKind;
196
+ return sourceKind === 'service_binding' || sourceKind === 'cds_require'
197
+ ? referenceFromRow(row, sourceKind, 'fallback')
198
+ : [];
199
+ });
200
+ }
201
+
202
+ function referenceFromRow(
203
+ row: Record<string, unknown> | undefined,
204
+ sourceKind: DynamicReferenceRow['sourceKind'],
205
+ selection: DynamicReferenceRow['selection'],
206
+ bindingId = numberValue(row?.bindingId),
207
+ ): DynamicReferenceRow[] {
208
+ const repoName = stringValue(row?.repoName);
209
+ if (!repoName) return [];
210
+ return [{
211
+ bindingId,
212
+ alias: stringValue(row?.alias),
213
+ aliasExpr: stringValue(row?.aliasExpr),
214
+ destination: stringValue(row?.destination),
215
+ servicePath: stringValue(row?.servicePath),
216
+ sourceKind,
217
+ selection,
218
+ repoName,
219
+ sourceFile: stringValue(row?.sourceFile),
220
+ sourceLine: numberValue(row?.sourceLine),
221
+ helperChain: parsedJson(row?.helperChainJson),
222
+ }];
223
+ }
224
+
225
+ function persistedBindingResolution(evidence: Record<string, unknown>): {
226
+ status: string;
227
+ candidates: Array<Record<string, unknown>>;
228
+ candidateCount: number;
229
+ } {
230
+ const outbound = record(evidence.outboundEvidence);
231
+ const resolution = record(outbound.serviceBindingResolution);
232
+ return {
233
+ status: stringValue(resolution.status) ?? 'unknown',
234
+ candidates: recordArray(resolution.candidates),
235
+ candidateCount: numberValue(resolution.candidateCount) ?? 0,
236
+ };
237
+ }
238
+
239
+ function boundedAlternatives(
240
+ rows: Array<Record<string, unknown>>,
241
+ reportedCount: number,
242
+ ): BoundedProjection<Record<string, unknown>> {
243
+ const projection = projectBounded(rows, (left, right) =>
244
+ Number(left.bindingId ?? 0) - Number(right.bindingId ?? 0)
245
+ || String(left.sourceFile ?? '').localeCompare(String(right.sourceFile ?? ''))
246
+ || Number(left.sourceLine ?? 0) - Number(right.sourceLine ?? 0));
247
+ const totalCount = Math.max(reportedCount, projection.totalCount);
248
+ return {
249
+ ...projection,
250
+ totalCount,
251
+ omittedCount: Math.max(0, totalCount - projection.shownCount),
252
+ };
253
+ }
254
+
255
+ function parsedJson(value: unknown): unknown {
256
+ if (typeof value !== 'string' || value.length === 0) return undefined;
257
+ try {
258
+ return JSON.parse(value) as unknown;
259
+ } catch {
260
+ return undefined;
261
+ }
262
+ }
263
+
264
+ function record(value: unknown): Record<string, unknown> {
265
+ return value && typeof value === 'object' && !Array.isArray(value)
266
+ ? value as Record<string, unknown>
267
+ : {};
268
+ }
269
+
270
+ function recordArray(value: unknown): Array<Record<string, unknown>> {
271
+ return Array.isArray(value)
272
+ ? value.filter((item): item is Record<string, unknown> =>
273
+ Boolean(item && typeof item === 'object' && !Array.isArray(item)))
274
+ : [];
275
+ }
276
+
277
+ function stringValue(value: unknown): string | undefined {
278
+ return typeof value === 'string' ? value : undefined;
279
+ }
280
+
281
+ function numberValue(value: unknown): number | undefined {
282
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
283
+ }