@saptools/service-flow 0.1.52 → 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 (36) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +9 -12
  3. package/TECHNICAL-NOTE.md +11 -3
  4. package/dist/{chunk-PTLDSHRC.js → chunk-LFH7C46B.js} +2214 -1044
  5. package/dist/chunk-LFH7C46B.js.map +1 -0
  6. package/dist/cli.js +157 -11
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.js +1 -1
  9. package/package.json +1 -1
  10. package/src/cli/001-doctor-projection.ts +140 -0
  11. package/src/cli/doctor.ts +20 -3
  12. package/src/db/repositories.ts +10 -2
  13. package/src/linker/000-implementation-candidates.ts +641 -0
  14. package/src/linker/001-implementation-evidence-projection.ts +119 -0
  15. package/src/linker/002-call-evidence.ts +226 -0
  16. package/src/linker/cross-repo-linker.ts +27 -453
  17. package/src/linker/dynamic-edge-resolver.ts +35 -0
  18. package/src/linker/external-http-target.ts +24 -3
  19. package/src/linker/helper-package-linker.ts +18 -2
  20. package/src/linker/service-resolver.ts +45 -2
  21. package/src/output/doctor-output.ts +13 -4
  22. package/src/output/table-output.ts +4 -2
  23. package/src/trace/000-dynamic-target-types.ts +12 -0
  24. package/src/trace/001-dynamic-identity.ts +45 -17
  25. package/src/trace/003-dynamic-references.ts +236 -35
  26. package/src/trace/004-dynamic-candidate-sources.ts +155 -0
  27. package/src/trace/005-implementation-selection.ts +187 -0
  28. package/src/trace/006-contextual-projection.ts +30 -0
  29. package/src/trace/007-implementation-start-diagnostic.ts +61 -0
  30. package/src/trace/dynamic-targets.ts +205 -159
  31. package/src/trace/evidence.ts +35 -9
  32. package/src/trace/implementation-hints.ts +148 -8
  33. package/src/trace/selectors.ts +74 -9
  34. package/src/trace/trace-engine.ts +39 -52
  35. package/src/utils/000-bounded-projection.ts +161 -0
  36. package/dist/chunk-PTLDSHRC.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import type { Db } from '../db/connection.js';
2
+ import { projectBounded } from '../utils/000-bounded-projection.js';
2
3
 
3
4
  interface RepoDependencyRow {
4
5
  id: number;
@@ -34,6 +35,10 @@ export function linkHelperPackages(db: Db, workspaceId: number, generation: numb
34
35
  if (result.candidates.length === 0) continue;
35
36
  const status = result.candidates.length === 1 ? 'resolved' : 'ambiguous';
36
37
  const helper = status === 'resolved' ? result.candidates[0] : undefined;
38
+ const projection = projectBounded(result.candidates, (left, right) =>
39
+ left.name.localeCompare(right.name)
40
+ || String(left.package_name ?? '').localeCompare(String(right.package_name ?? ''))
41
+ || left.id - right.id);
37
42
  db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)').run(
38
43
  workspaceId,
39
44
  'REPO_IMPORTS_HELPER_PACKAGE',
@@ -41,9 +46,20 @@ export function linkHelperPackages(db: Db, workspaceId: number, generation: numb
41
46
  'repo',
42
47
  String(repo.id),
43
48
  helper ? 'repo' : 'repo_candidates',
44
- helper ? String(helper.id) : result.candidates.map((candidate) => candidate.id).join(','),
49
+ helper ? String(helper.id) : projection.items.map((candidate) => candidate.id).join(','),
45
50
  helper ? 1 : 0.5,
46
- JSON.stringify({ dependency: dep, candidates: result.candidates.map((candidate) => ({ id: candidate.id, name: candidate.name, packageName: candidate.package_name })), match: result.strategy }),
51
+ JSON.stringify({
52
+ dependency: dep,
53
+ candidates: projection.items.map((candidate) => ({
54
+ id: candidate.id,
55
+ name: candidate.name,
56
+ packageName: candidate.package_name,
57
+ })),
58
+ candidateCount: projection.totalCount,
59
+ shownCandidateCount: projection.shownCount,
60
+ omittedCandidateCount: projection.omittedCount,
61
+ match: result.strategy,
62
+ }),
47
63
  0,
48
64
  helper ? null : 'Ambiguous dependency package candidates',
49
65
  generation,
@@ -1,4 +1,5 @@
1
1
  import type { Db } from '../db/connection.js';
2
+ import { canonicalImplementationEvidence } from './000-implementation-candidates.js';
2
3
  export interface OperationTarget {
3
4
  operationId: number;
4
5
  repoName: string;
@@ -209,8 +210,11 @@ function ownershipReason(db: Db, candidate: OperationTarget, callerRepoId: numbe
209
210
  if (row?.repoId === callerRepoId) return { candidate, reason: 'resolved_implementation_handler_repo_matches_caller' };
210
211
  }
211
212
  if (edge?.evidence_json) {
212
- const evidence = JSON.parse(edge.evidence_json) as { candidates?: Array<{ accepted?: boolean; handlerPackage?: { id?: number }; applicationPackage?: { id?: number } }> };
213
- const hit = evidence.candidates?.find((item) => item.accepted && (Number(item.handlerPackage?.id) === callerRepoId || Number(item.applicationPackage?.id) === callerRepoId));
213
+ const stored = parsedRecord(edge.evidence_json);
214
+ const evidence = canonicalImplementationEvidence(db, candidate.operationId) ?? stored;
215
+ const hit = implementationEvidenceCandidates(evidence).find((item) =>
216
+ item.accepted && (item.handlerRepoId === callerRepoId
217
+ || item.applicationRepoId === callerRepoId));
214
218
  if (hit) return { candidate, reason: edge.status === 'ambiguous' ? 'ambiguous_implementation_candidate_repo_matches_caller' : 'registration_package_matches_caller' };
215
219
  }
216
220
  const dep = db.prepare("SELECT 1 FROM graph_edges WHERE edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND status='resolved' AND from_kind='repo' AND from_id=? AND to_id=?").get(String(callerRepoId), String(candidate.repoId));
@@ -218,6 +222,45 @@ function ownershipReason(db: Db, candidate: OperationTarget, callerRepoId: numbe
218
222
  return undefined;
219
223
  }
220
224
 
225
+ function implementationEvidenceCandidates(
226
+ evidence: Record<string, unknown>,
227
+ ): Array<{ accepted: boolean; handlerRepoId?: number; applicationRepoId?: number }> {
228
+ const candidates = evidence.candidates;
229
+ if (!Array.isArray(candidates)) return [];
230
+ return candidates.flatMap((candidate) => {
231
+ if (!isRecord(candidate)) return [];
232
+ const row = candidate;
233
+ const handler = recordValue(row.handlerPackage);
234
+ const application = recordValue(row.applicationPackage);
235
+ return [{
236
+ accepted: row.accepted === true,
237
+ handlerRepoId: numberValue(handler.id),
238
+ applicationRepoId: numberValue(application.id),
239
+ }];
240
+ });
241
+ }
242
+
243
+ function recordValue(value: unknown): Record<string, unknown> {
244
+ return isRecord(value) ? value : {};
245
+ }
246
+
247
+ function parsedRecord(value: string): Record<string, unknown> {
248
+ try {
249
+ const parsed: unknown = JSON.parse(value);
250
+ return recordValue(parsed);
251
+ } catch {
252
+ return {};
253
+ }
254
+ }
255
+
256
+ function isRecord(value: unknown): value is Record<string, unknown> {
257
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
258
+ }
259
+
260
+ function numberValue(value: unknown): number | undefined {
261
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
262
+ }
263
+
221
264
  function matchesLocalRepo(db: Db, operationId: number, repoId: number | undefined): boolean {
222
265
  if (repoId === undefined) return true;
223
266
  const row = db.prepare('SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?').get(operationId) as { repoId?: number } | undefined;
@@ -56,8 +56,11 @@ function compactMessage(diagnostic: Diagnostic): string {
56
56
 
57
57
  function suggestedHintLines(diagnostic: Diagnostic): string[] {
58
58
  const direct = cliHints(diagnostic.suggestedHints);
59
- if (direct.length > 0) return cappedHints(direct);
60
- return cappedHints(cliHintsFromSuggestions(diagnostic.implementationHintSuggestions));
59
+ const omitted = numericValue(diagnostic.omittedImplementationHintSuggestionCount);
60
+ if (direct.length > 0) return cappedHints(direct, omitted);
61
+ return cappedHints(
62
+ cliHintsFromSuggestions(diagnostic.implementationHintSuggestions), omitted,
63
+ );
61
64
  }
62
65
 
63
66
  function cliHints(value: unknown): string[] {
@@ -76,13 +79,19 @@ function isRecord(value: unknown): value is Record<string, unknown> {
76
79
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
77
80
  }
78
81
 
79
- function cappedHints(hints: string[]): string[] {
82
+ function cappedHints(hints: string[], omitted: number): string[] {
80
83
  const unique = [...new Set(hints)];
81
84
  const shown = unique.slice(0, 3);
82
- if (unique.length > shown.length) shown.push(`... ${unique.length - shown.length} more hint(s) available in --format json`);
85
+ const remaining = Math.max(0, unique.length - shown.length) + omitted;
86
+ if (remaining > 0)
87
+ shown.push(`... ${remaining} additional hint(s) omitted; use a scoped --implementation-hint`);
83
88
  return shown;
84
89
  }
85
90
 
91
+ function numericValue(value: unknown): number {
92
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
93
+ }
94
+
86
95
  function cleanDoctorMessage(): string {
87
96
  return `${pc.green('No diagnostics recorded')}\n`;
88
97
  }
@@ -60,8 +60,10 @@ function hintLines(evidence: Record<string, unknown>): string[] {
60
60
  : []);
61
61
  const unique = [...new Set(hints)];
62
62
  const shown = unique.slice(0, 3).map((hint) => `try ${hint}`);
63
- if (unique.length > shown.length)
64
- shown.push(`... ${unique.length - shown.length} more hint(s) available in JSON`);
63
+ const omitted = numberValue(evidence.omittedImplementationHintSuggestionCount);
64
+ const remaining = Math.max(0, unique.length - shown.length) + omitted;
65
+ if (remaining > 0)
66
+ shown.push(`... ${remaining} additional hint(s) omitted; use a scoped --implementation-hint`);
65
67
  return [...dynamicLines, ...shown];
66
68
  }
67
69
 
@@ -47,8 +47,16 @@ export interface DynamicTargetCandidate {
47
47
  derivedVariables: Record<string, string>;
48
48
  derivedVariableSources: Record<string, DynamicVariableProvenance>;
49
49
  derivationProvenance: Record<string, DynamicVariableProvenance[]>;
50
+ derivationProvenanceCounts?: Record<string, {
51
+ provenanceCount: number;
52
+ shownProvenanceCount: number;
53
+ omittedProvenanceCount: number;
54
+ }>;
50
55
  missingVariables: string[];
51
56
  conflicts: DynamicVariableConflict[];
57
+ conflictCount?: number;
58
+ shownConflictCount?: number;
59
+ omittedConflictCount?: number;
52
60
  score: number;
53
61
  explicitSignalStrength: number;
54
62
  reasons: string[];
@@ -80,5 +88,9 @@ export interface DynamicTargetAnalysis {
80
88
  shownCandidates: DynamicTargetCandidate[];
81
89
  rejectedCandidates: DynamicTargetCandidate[];
82
90
  suggestedVarSets: Array<{ variables: Record<string, string>; cli: string }>;
91
+ suggestedVarSetCount: number;
92
+ shownSuggestedVarSetCount: number;
93
+ omittedSuggestedVarSetCount: number;
83
94
  inference: Record<string, unknown>;
95
+ routingContext?: Record<string, unknown>;
84
96
  }
@@ -5,8 +5,12 @@ import type {
5
5
  DynamicVariableProvenance,
6
6
  } from './000-dynamic-target-types.js';
7
7
 
8
- interface ImplementationOwner {
9
- repoId?: number;
8
+ interface RouteImplementationEvidence {
9
+ routeRepoId?: number;
10
+ handlerRepo?: string;
11
+ operationProvenance?: string;
12
+ baseOperationId?: number;
13
+ edgeStatus?: string;
10
14
  }
11
15
 
12
16
  interface IdentityProposal {
@@ -44,11 +48,13 @@ export function uniqueIdentityDerivations(
44
48
  ): IdentityDerivation[] {
45
49
  const identities = workspaceIdentities(db, candidates);
46
50
  const proposals = candidates.flatMap((candidate) => {
47
- const owner = implementationOwner(db, candidate.candidateOperationId);
51
+ const implementation = routeImplementationEvidence(
52
+ db, candidate.candidateOperationId,
53
+ );
48
54
  const identity = identities.find((item) => item.repoId === candidate.repoId);
49
- return ownerAgrees(candidate, owner) && identity
50
- ? identityProposals(candidate, identity, templates)
51
- : [];
55
+ if (!identity || !implementation || !routeOwnerAgrees(candidate, implementation))
56
+ return [];
57
+ return identityProposals(candidate, identity, templates, implementation);
52
58
  });
53
59
  const matches = workspaceIdentityMatches(identities, templates);
54
60
  const competing = competingIdentityKeys(matches);
@@ -68,6 +74,7 @@ function identityProposals(
68
74
  candidate: DynamicTargetCandidate,
69
75
  identity: RepositoryIdentity,
70
76
  templates: DynamicTemplates,
77
+ implementation: RouteImplementationEvidence,
71
78
  ): IdentityProposal[] {
72
79
  const routeTemplates = [templates.alias, templates.destination]
73
80
  .filter((value): value is string => Boolean(value));
@@ -81,7 +88,7 @@ function identityProposals(
81
88
  identities.flatMap((identity) =>
82
89
  proposalForIdentity(
83
90
  candidate, template, identity.name, identity.sourceKind,
84
- identity.npmPackage,
91
+ identity.npmPackage, implementation,
85
92
  )));
86
93
  return deduplicateProposals(proposals);
87
94
  }
@@ -92,6 +99,7 @@ function proposalForIdentity(
92
99
  identity: string,
93
100
  sourceKind: string,
94
101
  npmPackage: boolean,
102
+ implementation: RouteImplementationEvidence,
95
103
  ): IdentityProposal[] {
96
104
  const match = matchIdentityTemplate(template, identity, npmPackage);
97
105
  if (!match) return [];
@@ -108,6 +116,12 @@ function proposalForIdentity(
108
116
  matchedName: identity,
109
117
  normalizedForm: match.normalizedIdentity,
110
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,
111
125
  },
112
126
  }];
113
127
  }
@@ -229,30 +243,44 @@ function deduplicateProposals(rows: IdentityProposal[]): IdentityProposal[] {
229
243
  });
230
244
  }
231
245
 
232
- function ownerAgrees(
246
+ function routeOwnerAgrees(
233
247
  candidate: DynamicTargetCandidate,
234
- owner: ImplementationOwner | undefined,
248
+ implementation: RouteImplementationEvidence | undefined,
235
249
  ): boolean {
236
250
  return candidate.repoId !== undefined
237
- && owner?.repoId !== undefined
238
- && owner.repoId === candidate.repoId;
251
+ && implementation?.routeRepoId === candidate.repoId
252
+ && implementation.edgeStatus === 'resolved'
253
+ && candidate.viable
254
+ && candidate.reasons.includes('service_path_template_match');
239
255
  }
240
256
 
241
- function implementationOwner(db: Db, operationId: number): ImplementationOwner | undefined {
257
+ function routeImplementationEvidence(
258
+ db: Db,
259
+ operationId: number,
260
+ ): RouteImplementationEvidence | undefined {
242
261
  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)
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)
245
269
  JOIN handler_classes hc ON hc.id=hm.handler_class_id
246
270
  JOIN repositories r ON r.id=hc.repo_id
247
271
  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`,
272
+ AND o.id=?
273
+ ORDER BY e.id,hm.id`,
250
274
  ).all(String(operationId));
251
275
  if (rows.length !== 1) return undefined;
252
276
  const row = rows[0];
253
277
  if (!row) return undefined;
254
278
  return {
255
- repoId: numberValue(row.repoId),
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),
256
284
  };
257
285
  }
258
286
 
@@ -1,43 +1,78 @@
1
1
  import type { Db } from '../db/connection.js';
2
+ import {
3
+ projectBounded,
4
+ type BoundedProjection,
5
+ } from '../utils/000-bounded-projection.js';
2
6
  import type { DynamicVariableProvenance } from './000-dynamic-target-types.js';
3
7
 
4
8
  export interface DynamicReferenceRow {
9
+ bindingId?: number;
5
10
  alias?: string;
11
+ aliasExpr?: string;
6
12
  destination?: string;
7
13
  servicePath?: string;
8
14
  sourceKind: 'service_binding' | 'cds_require';
15
+ selection: 'selected_binding' | 'selected_binding_require' | 'fallback';
9
16
  repoName: string;
10
17
  sourceFile?: string;
11
18
  sourceLine?: number;
19
+ helperChain?: unknown;
12
20
  }
13
21
 
14
- export function dynamicReferenceRows(
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(
15
43
  db: Db,
16
44
  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,
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,
39
52
  );
40
- return rows.flatMap(referenceFromRow);
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
+ };
41
76
  }
42
77
 
43
78
  export function dynamicReferenceProvenance(
@@ -46,33 +81,199 @@ export function dynamicReferenceProvenance(
46
81
  template: string,
47
82
  value: string,
48
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}`;
49
89
  return {
50
- sourceKind: `${reference.sourceKind}.${kind}`,
90
+ sourceKind,
51
91
  value,
52
92
  rule: 'exact_indexed_reference_template_match',
53
93
  template,
54
94
  sourceRepo: reference.repoName,
55
95
  sourceFile: reference.sourceFile,
56
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,
57
117
  };
58
118
  }
59
119
 
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 [];
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 [];
65
210
  return [{
66
- alias: stringValue(row.alias),
67
- destination: stringValue(row.destination),
68
- servicePath: stringValue(row.servicePath),
211
+ bindingId,
212
+ alias: stringValue(row?.alias),
213
+ aliasExpr: stringValue(row?.aliasExpr),
214
+ destination: stringValue(row?.destination),
215
+ servicePath: stringValue(row?.servicePath),
69
216
  sourceKind,
217
+ selection,
70
218
  repoName,
71
- sourceFile: stringValue(row.sourceFile),
72
- sourceLine: numberValue(row.sourceLine),
219
+ sourceFile: stringValue(row?.sourceFile),
220
+ sourceLine: numberValue(row?.sourceLine),
221
+ helperChain: parsedJson(row?.helperChainJson),
73
222
  }];
74
223
  }
75
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
+
76
277
  function stringValue(value: unknown): string | undefined {
77
278
  return typeof value === 'string' ? value : undefined;
78
279
  }