@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
|
@@ -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) :
|
|
49
|
+
helper ? String(helper.id) : projection.items.map((candidate) => candidate.id).join(','),
|
|
45
50
|
helper ? 1 : 0.5,
|
|
46
|
-
JSON.stringify({
|
|
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
|
|
213
|
-
const
|
|
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
|
-
|
|
60
|
-
return cappedHints(
|
|
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
|
-
|
|
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
|
}
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import type { TraceResult } from '../types.js';
|
|
2
2
|
|
|
3
3
|
function location(evidence: Record<string, unknown>): string {
|
|
4
|
+
const selected = isRecord(evidence.selectedHandler)
|
|
5
|
+
? evidence.selectedHandler : undefined;
|
|
6
|
+
const selectedFile = selected?.sourceFile;
|
|
7
|
+
const selectedLine = selected?.sourceLine;
|
|
8
|
+
if (selectedFile || selectedLine)
|
|
9
|
+
return `${String(selectedFile ?? '')}:${String(selectedLine ?? '')}`;
|
|
4
10
|
const file = evidence.file ?? evidence.sourceFile ?? evidence.handlerSourceFile ?? evidence.operationSourceFile ?? evidence.registrationSourceFile;
|
|
5
11
|
const line = evidence.line ?? evidence.sourceLine ?? evidence.handlerSourceLine ?? evidence.operationSourceLine ?? evidence.registrationSourceLine;
|
|
6
12
|
if (file || line) return `${String(file ?? '')}:${String(line ?? '')}`;
|
|
7
13
|
const candidates = evidence.candidates;
|
|
14
|
+
if (Array.isArray(candidates) && candidates.some((candidate) =>
|
|
15
|
+
isRecord(candidate) && candidate.methodId !== undefined)) return ':';
|
|
8
16
|
if (Array.isArray(candidates) && candidates.length > 0) {
|
|
9
|
-
const first = candidates
|
|
10
|
-
|
|
17
|
+
const first = candidates.find(isRecord);
|
|
18
|
+
if (first)
|
|
19
|
+
return `${String(first.sourceFile ?? '')}:${String(first.sourceLine ?? '')}`;
|
|
11
20
|
}
|
|
12
21
|
return ':';
|
|
13
22
|
}
|
|
@@ -60,8 +69,10 @@ function hintLines(evidence: Record<string, unknown>): string[] {
|
|
|
60
69
|
: []);
|
|
61
70
|
const unique = [...new Set(hints)];
|
|
62
71
|
const shown = unique.slice(0, 3).map((hint) => `try ${hint}`);
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
const omitted = numberValue(evidence.omittedImplementationHintSuggestionCount);
|
|
73
|
+
const remaining = Math.max(0, unique.length - shown.length) + omitted;
|
|
74
|
+
if (remaining > 0)
|
|
75
|
+
shown.push(`... ${remaining} additional hint(s) omitted; use a scoped --implementation-hint`);
|
|
65
76
|
return [...dynamicLines, ...shown];
|
|
66
77
|
}
|
|
67
78
|
|
|
@@ -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
|
|
9
|
-
|
|
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
|
|
51
|
+
const implementation = routeImplementationEvidence(
|
|
52
|
+
db, candidate.candidateOperationId,
|
|
53
|
+
);
|
|
48
54
|
const identity = identities.find((item) => item.repoId === candidate.repoId);
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
246
|
+
function routeOwnerAgrees(
|
|
233
247
|
candidate: DynamicTargetCandidate,
|
|
234
|
-
|
|
248
|
+
implementation: RouteImplementationEvidence | undefined,
|
|
235
249
|
): boolean {
|
|
236
250
|
return candidate.repoId !== undefined
|
|
237
|
-
&&
|
|
238
|
-
&&
|
|
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
|
|
257
|
+
function routeImplementationEvidence(
|
|
258
|
+
db: Db,
|
|
259
|
+
operationId: number,
|
|
260
|
+
): RouteImplementationEvidence | undefined {
|
|
242
261
|
const rows = db.prepare(
|
|
243
|
-
`SELECT
|
|
244
|
-
|
|
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
|
|
249
|
-
ORDER BY
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
72
|
-
sourceLine: numberValue(row
|
|
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
|
}
|