@saptools/service-flow 0.1.53 → 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 +6 -0
- package/README.md +2 -0
- package/TECHNICAL-NOTE.md +7 -0
- package/dist/{chunk-LFH7C46B.js → chunk-ERIZHM5C.js} +535 -126
- package/dist/chunk-ERIZHM5C.js.map +1 -0
- package/dist/cli.js +13 -10
- 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 +3 -7
- package/src/linker/000-implementation-candidates.ts +35 -2
- package/src/linker/001-implementation-evidence-projection.ts +78 -6
- package/src/output/table-output.ts +11 -2
- package/src/trace/008-contextual-runtime-state.ts +294 -0
- package/src/trace/009-selected-handler-provenance.ts +186 -0
- package/src/trace/evidence.ts +103 -31
- package/src/trace/trace-engine.ts +67 -82
- package/src/utils/000-bounded-projection.ts +20 -15
- package/dist/chunk-LFH7C46B.js.map +0 -1
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
stableProjectionValue,
|
|
2
|
+
projectBoundedInOrder,
|
|
4
3
|
type BoundedProjection,
|
|
5
4
|
} from '../utils/000-bounded-projection.js';
|
|
6
5
|
|
|
@@ -54,7 +53,8 @@ function boundDoctorValue(value: unknown): unknown {
|
|
|
54
53
|
output[key] = boundDoctorValue(child);
|
|
55
54
|
continue;
|
|
56
55
|
}
|
|
57
|
-
|
|
56
|
+
// Doctor producers already query or assemble deterministic semantic order.
|
|
57
|
+
const projection = projectBoundedInOrder(child.map(boundDoctorValue));
|
|
58
58
|
output[key] = projection.items;
|
|
59
59
|
addProjectionMetadata(output, input, key, projection);
|
|
60
60
|
}
|
|
@@ -127,10 +127,6 @@ function projectionStem(key: string): string {
|
|
|
127
127
|
return stems[key] ?? 'repository';
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
function compareDiagnostic(left: unknown, right: unknown): number {
|
|
131
|
-
return stableProjectionValue(left).localeCompare(stableProjectionValue(right));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
130
|
function numericValue(value: unknown): number {
|
|
135
131
|
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
136
132
|
}
|
|
@@ -4,6 +4,8 @@ import { normalizeDecoratorOperationSignal, normalizedOperationName } from './op
|
|
|
4
4
|
import {
|
|
5
5
|
boundedImplementationEvidence,
|
|
6
6
|
boundedImplementationTargetIds,
|
|
7
|
+
displayImplementationCandidates,
|
|
8
|
+
selectedHandlerProvenance,
|
|
7
9
|
} from './001-implementation-evidence-projection.js';
|
|
8
10
|
|
|
9
11
|
interface ImplementationCandidate extends Record<string, unknown> {
|
|
@@ -107,6 +109,7 @@ function implementationDecision(
|
|
|
107
109
|
: winners.length > 1 ? ['multiple_equal_score_implementation_candidates'] : [];
|
|
108
110
|
const evidence = implementationEvidence(
|
|
109
111
|
operation, implementationContext, candidates, duplicateFamilies, ambiguityReasons,
|
|
112
|
+
unique,
|
|
110
113
|
);
|
|
111
114
|
const hintProjection = implementationHintSuggestionProjection(evidence);
|
|
112
115
|
return {
|
|
@@ -174,6 +177,7 @@ function implementationEvidence(
|
|
|
174
177
|
candidates: ImplementationCandidate[],
|
|
175
178
|
duplicateFamilies: Array<Record<string, unknown>>,
|
|
176
179
|
ambiguityReasons: string[],
|
|
180
|
+
selected: ImplementationCandidate | undefined,
|
|
177
181
|
): Record<string, unknown> {
|
|
178
182
|
return {
|
|
179
183
|
servicePath: operation.servicePath,
|
|
@@ -191,7 +195,13 @@ function implementationEvidence(
|
|
|
191
195
|
implementationOperationId: context.operationId,
|
|
192
196
|
ambiguityReasons,
|
|
193
197
|
candidateFamilies: duplicateFamilies,
|
|
194
|
-
|
|
198
|
+
selectedHandler: selected
|
|
199
|
+
? selectedHandlerProvenance(selectedHandlerSource(selected))
|
|
200
|
+
: undefined,
|
|
201
|
+
candidates: displayImplementationCandidates(
|
|
202
|
+
candidates.map((candidate, index) => candidateEvidence(candidate, index + 1)),
|
|
203
|
+
selected?.methodId,
|
|
204
|
+
),
|
|
195
205
|
};
|
|
196
206
|
}
|
|
197
207
|
|
|
@@ -350,7 +360,7 @@ function implementationCandidates(
|
|
|
350
360
|
hm.id methodId,hm.method_name methodName,hm.decorator_value decoratorValue,
|
|
351
361
|
hm.decorator_raw_expression decoratorRawExpression,
|
|
352
362
|
hm.decorator_resolution_json decoratorResolutionJson,hc.id classId,
|
|
353
|
-
hc.class_name className,hc.source_file sourceFile,
|
|
363
|
+
hc.class_name className,hc.source_file sourceFile,hm.source_line sourceLine,
|
|
354
364
|
hr.id registrationId,hr.handler_class_id registrationHandlerClassId,
|
|
355
365
|
hr.class_name registrationClassName,hr.repo_id applicationRepoId,
|
|
356
366
|
hr.registration_file registrationFile,hr.registration_line registrationLine,
|
|
@@ -526,6 +536,7 @@ function ownershipScore(
|
|
|
526
536
|
function candidateEvidence(candidate: ImplementationCandidate, rank: number): Record<string, unknown> {
|
|
527
537
|
return {
|
|
528
538
|
rank,
|
|
539
|
+
rankKind: 'discovery_score',
|
|
529
540
|
score: candidate.score,
|
|
530
541
|
accepted: candidate.accepted,
|
|
531
542
|
acceptedReasons: candidate.acceptedReasons,
|
|
@@ -580,6 +591,28 @@ function candidateEvidence(candidate: ImplementationCandidate, rank: number): Re
|
|
|
580
591
|
};
|
|
581
592
|
}
|
|
582
593
|
|
|
594
|
+
function selectedHandlerSource(candidate: ImplementationCandidate): {
|
|
595
|
+
methodId: number;
|
|
596
|
+
className?: string;
|
|
597
|
+
methodName?: string;
|
|
598
|
+
repositoryId?: number;
|
|
599
|
+
repositoryName?: string;
|
|
600
|
+
repositoryPackageName?: string;
|
|
601
|
+
sourceFile?: string;
|
|
602
|
+
sourceLine?: number;
|
|
603
|
+
} {
|
|
604
|
+
return {
|
|
605
|
+
methodId: candidate.methodId,
|
|
606
|
+
className: stringValue(candidate.className),
|
|
607
|
+
methodName: stringValue(candidate.methodName),
|
|
608
|
+
repositoryId: numberValue(candidate.handlerRepoId),
|
|
609
|
+
repositoryName: stringValue(candidate.handlerRepo),
|
|
610
|
+
repositoryPackageName: stringValue(candidate.handlerPackage),
|
|
611
|
+
sourceFile: stringValue(candidate.sourceFile),
|
|
612
|
+
sourceLine: numberValue(candidate.sourceLine),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
583
616
|
function implementationMethodSignal(
|
|
584
617
|
row: Record<string, unknown>,
|
|
585
618
|
operation: Record<string, unknown>,
|
|
@@ -1,14 +1,91 @@
|
|
|
1
1
|
import {
|
|
2
2
|
projectBounded,
|
|
3
|
+
projectBoundedInOrder,
|
|
3
4
|
type BoundedProjection,
|
|
4
5
|
} from '../utils/000-bounded-projection.js';
|
|
5
6
|
|
|
7
|
+
export interface SelectedHandlerSource {
|
|
8
|
+
methodId: number;
|
|
9
|
+
className?: string;
|
|
10
|
+
methodName?: string;
|
|
11
|
+
repositoryId?: number;
|
|
12
|
+
repositoryName?: string;
|
|
13
|
+
repositoryPackageName?: string;
|
|
14
|
+
sourceFile?: string;
|
|
15
|
+
sourceLine?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SelectedHandlerProvenance {
|
|
19
|
+
status: 'selected';
|
|
20
|
+
accepted: true;
|
|
21
|
+
graphTargetId: string;
|
|
22
|
+
methodId: number;
|
|
23
|
+
className?: string;
|
|
24
|
+
methodName?: string;
|
|
25
|
+
repository?: {
|
|
26
|
+
id?: number;
|
|
27
|
+
name?: string;
|
|
28
|
+
packageName?: string;
|
|
29
|
+
};
|
|
30
|
+
sourceFile?: string;
|
|
31
|
+
sourceLine?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function selectedHandlerProvenance(
|
|
35
|
+
source: SelectedHandlerSource,
|
|
36
|
+
): SelectedHandlerProvenance {
|
|
37
|
+
return {
|
|
38
|
+
status: 'selected',
|
|
39
|
+
accepted: true,
|
|
40
|
+
graphTargetId: String(source.methodId),
|
|
41
|
+
methodId: source.methodId,
|
|
42
|
+
className: source.className,
|
|
43
|
+
methodName: source.methodName,
|
|
44
|
+
repository: {
|
|
45
|
+
id: source.repositoryId,
|
|
46
|
+
name: source.repositoryName,
|
|
47
|
+
packageName: source.repositoryPackageName,
|
|
48
|
+
},
|
|
49
|
+
sourceFile: source.sourceFile,
|
|
50
|
+
sourceLine: source.sourceLine,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function displayImplementationCandidates(
|
|
55
|
+
candidates: Array<Record<string, unknown>>,
|
|
56
|
+
selectedMethodId: number | undefined,
|
|
57
|
+
): Array<Record<string, unknown>> {
|
|
58
|
+
return [...candidates]
|
|
59
|
+
.sort((left, right) => compareDisplayCandidate(
|
|
60
|
+
left, right, selectedMethodId,
|
|
61
|
+
))
|
|
62
|
+
.map((candidate, index) => ({
|
|
63
|
+
...candidate,
|
|
64
|
+
discoveryRank: candidate.rank,
|
|
65
|
+
displayRank: index + 1,
|
|
66
|
+
selected: selectedMethodId !== undefined
|
|
67
|
+
&& Number(candidate.methodId) === selectedMethodId,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function compareDisplayCandidate(
|
|
72
|
+
left: Record<string, unknown>,
|
|
73
|
+
right: Record<string, unknown>,
|
|
74
|
+
selectedMethodId: number | undefined,
|
|
75
|
+
): number {
|
|
76
|
+
return Number(Number(right.methodId) === selectedMethodId)
|
|
77
|
+
- Number(Number(left.methodId) === selectedMethodId)
|
|
78
|
+
|| Number(right.accepted === true) - Number(left.accepted === true)
|
|
79
|
+
|| numberValue(left.rank) - numberValue(right.rank)
|
|
80
|
+
|| compareTargetCandidates(left, right);
|
|
81
|
+
}
|
|
82
|
+
|
|
6
83
|
export function boundedImplementationEvidence(
|
|
7
84
|
evidence: Record<string, unknown>,
|
|
8
85
|
targetCandidateCount: number,
|
|
9
86
|
): Record<string, unknown> {
|
|
10
87
|
const candidates = recordArray(evidence.candidates);
|
|
11
|
-
const candidateProjection =
|
|
88
|
+
const candidateProjection = projectBoundedInOrder(candidates);
|
|
12
89
|
const families = recordArray(evidence.candidateFamilies);
|
|
13
90
|
const familyProjection = projectBounded(families, compareFamilies);
|
|
14
91
|
const hints = recordArray(evidence.implementationHintSuggestions);
|
|
@@ -80,11 +157,6 @@ function compareTargetCandidates(left: Record<string, unknown>, right: Record<st
|
|
|
80
157
|
|| Number(left.methodId ?? 0) - Number(right.methodId ?? 0);
|
|
81
158
|
}
|
|
82
159
|
|
|
83
|
-
function compareCandidateEvidence(left: Record<string, unknown>, right: Record<string, unknown>): number {
|
|
84
|
-
return Number(left.rank ?? 0) - Number(right.rank ?? 0)
|
|
85
|
-
|| compareTargetCandidates(left, right);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
160
|
function compareFamilies(left: Record<string, unknown>, right: Record<string, unknown>): number {
|
|
89
161
|
return String(left.packageName ?? '').localeCompare(String(right.packageName ?? ''))
|
|
90
162
|
|| String(left.reason ?? '').localeCompare(String(right.reason ?? ''));
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
|
|
3
|
+
import { resolveOperation, type OperationResolution } from '../linker/service-resolver.js';
|
|
4
|
+
import { boundedContextCandidates } from './006-contextual-projection.js';
|
|
5
|
+
|
|
6
|
+
export interface ContextBinding {
|
|
7
|
+
bindingId?: number;
|
|
8
|
+
alias?: string;
|
|
9
|
+
aliasExpr?: string;
|
|
10
|
+
destinationExpr?: string;
|
|
11
|
+
servicePathExpr?: string;
|
|
12
|
+
requireServicePath?: string;
|
|
13
|
+
requireDestination?: string;
|
|
14
|
+
effectiveServicePath?: string;
|
|
15
|
+
effectiveDestination?: string;
|
|
16
|
+
sourceFile?: string;
|
|
17
|
+
sourceLine?: number;
|
|
18
|
+
source: string;
|
|
19
|
+
callerArgument?: string;
|
|
20
|
+
callerProperty?: string;
|
|
21
|
+
calleeParameter?: string;
|
|
22
|
+
calleeObjectProperty?: string;
|
|
23
|
+
calleeLocalDestructuredIdentifier?: string;
|
|
24
|
+
parameterPropertyAliasKind?: unknown;
|
|
25
|
+
parameterPropertyAliasLine?: unknown;
|
|
26
|
+
calleeReceiver: string;
|
|
27
|
+
callerSite?: { sourceFile?: string; sourceLine?: number };
|
|
28
|
+
calleeSite?: { sourceFile?: string; sourceLine?: number };
|
|
29
|
+
resolutionStatus?: 'selected' | 'ambiguous';
|
|
30
|
+
bindingCandidates?: Array<Record<string, unknown>>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ContextualCall {
|
|
34
|
+
id: number;
|
|
35
|
+
call_type: string;
|
|
36
|
+
operation_path_expr?: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface PersistedGraphRow {
|
|
40
|
+
status?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ContextualGraphRow {
|
|
44
|
+
id: number;
|
|
45
|
+
edge_type: string;
|
|
46
|
+
from_id: string;
|
|
47
|
+
to_kind: string;
|
|
48
|
+
to_id: string;
|
|
49
|
+
confidence: number;
|
|
50
|
+
evidence_json: string;
|
|
51
|
+
status: 'resolved';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ContextualResolutionCategory =
|
|
55
|
+
| 'none'
|
|
56
|
+
| 'dynamic_missing'
|
|
57
|
+
| 'ambiguous_binding'
|
|
58
|
+
| 'ambiguous_operation'
|
|
59
|
+
| 'no_matching_operation'
|
|
60
|
+
| 'other_blocker';
|
|
61
|
+
|
|
62
|
+
export interface ContextualRuntimeState {
|
|
63
|
+
category: ContextualResolutionCategory;
|
|
64
|
+
message?: string;
|
|
65
|
+
missingVariables?: string[];
|
|
66
|
+
resolutionStatus?: string;
|
|
67
|
+
phase?: 'before_runtime_substitution';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ContextualRuntimeResolution {
|
|
71
|
+
row?: ContextualGraphRow;
|
|
72
|
+
evidence?: Record<string, unknown>;
|
|
73
|
+
state: ContextualRuntimeState;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function dynamicMissingReason(keys: readonly string[]): string {
|
|
77
|
+
const missing = normalizedKeys(keys);
|
|
78
|
+
return missing.length > 0
|
|
79
|
+
? `Dynamic target is missing runtime variables: ${missing.join(', ')}`
|
|
80
|
+
: 'Dynamic target still requires runtime variables';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isStructuralContextualBlocker(
|
|
84
|
+
state: ContextualRuntimeState | undefined,
|
|
85
|
+
): boolean {
|
|
86
|
+
return state?.category === 'ambiguous_binding'
|
|
87
|
+
|| state?.category === 'ambiguous_operation'
|
|
88
|
+
|| state?.category === 'no_matching_operation'
|
|
89
|
+
|| state?.category === 'other_blocker';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function contextualRuntimeResolution(
|
|
93
|
+
db: Db,
|
|
94
|
+
call: ContextualCall,
|
|
95
|
+
binding: ContextBinding | undefined,
|
|
96
|
+
workspaceId: number | undefined,
|
|
97
|
+
persistedRows: PersistedGraphRow[] = [],
|
|
98
|
+
): ContextualRuntimeResolution {
|
|
99
|
+
if (!binding || call.call_type !== 'remote_action'
|
|
100
|
+
|| call.operation_path_expr === undefined || call.operation_path_expr === null)
|
|
101
|
+
return { state: { category: 'none' } };
|
|
102
|
+
if (binding.resolutionStatus === 'ambiguous')
|
|
103
|
+
return ambiguousBindingResolution(binding);
|
|
104
|
+
return selectedBindingResolution(db, call, binding, workspaceId, persistedRows);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ambiguousBindingResolution(
|
|
108
|
+
binding: ContextBinding,
|
|
109
|
+
): ContextualRuntimeResolution {
|
|
110
|
+
const candidates = boundedContextCandidates(binding.bindingCandidates ?? []);
|
|
111
|
+
const state: ContextualRuntimeState = {
|
|
112
|
+
category: 'ambiguous_binding',
|
|
113
|
+
message: 'Ambiguous contextual service binding candidates',
|
|
114
|
+
resolutionStatus: 'ambiguous',
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
evidence: {
|
|
118
|
+
contextualServiceBindingAttempted: true,
|
|
119
|
+
contextualBinding: {
|
|
120
|
+
source: binding.source,
|
|
121
|
+
status: 'tied',
|
|
122
|
+
candidates: candidates.candidates,
|
|
123
|
+
candidateCount: candidates.candidateCount,
|
|
124
|
+
shownCandidateCount: candidates.shownCandidateCount,
|
|
125
|
+
omittedCandidateCount: candidates.omittedCandidateCount,
|
|
126
|
+
},
|
|
127
|
+
contextualResolutionStatus: 'ambiguous',
|
|
128
|
+
contextualCandidateCount: candidates.candidateCount,
|
|
129
|
+
contextualPreSubstitutionState: historicalState(state),
|
|
130
|
+
},
|
|
131
|
+
state,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function selectedBindingResolution(
|
|
136
|
+
db: Db,
|
|
137
|
+
call: ContextualCall,
|
|
138
|
+
binding: ContextBinding,
|
|
139
|
+
workspaceId: number | undefined,
|
|
140
|
+
persistedRows: PersistedGraphRow[],
|
|
141
|
+
): ContextualRuntimeResolution {
|
|
142
|
+
const normalized = normalizeODataOperationInvocationPath(
|
|
143
|
+
String(call.operation_path_expr),
|
|
144
|
+
);
|
|
145
|
+
const operationPath = normalized?.normalizedOperationPath
|
|
146
|
+
?? withLeadingSlash(String(call.operation_path_expr));
|
|
147
|
+
const servicePath = binding.effectiveServicePath
|
|
148
|
+
?? binding.servicePathExpr ?? binding.requireServicePath;
|
|
149
|
+
const destination = binding.effectiveDestination
|
|
150
|
+
?? binding.destinationExpr ?? binding.requireDestination;
|
|
151
|
+
const resolution = resolveOperation(db, {
|
|
152
|
+
servicePath,
|
|
153
|
+
operationPath,
|
|
154
|
+
alias: binding.aliasExpr ?? binding.alias,
|
|
155
|
+
destination,
|
|
156
|
+
hasExplicitOverride: true,
|
|
157
|
+
isDynamic: false,
|
|
158
|
+
}, workspaceId);
|
|
159
|
+
const state = stateForResolution(resolution);
|
|
160
|
+
const evidence = contextualEvidence(
|
|
161
|
+
binding, normalized, operationPath, servicePath, destination, resolution, state,
|
|
162
|
+
);
|
|
163
|
+
if (!resolution.target) return { evidence, state };
|
|
164
|
+
const resolvedEvidence = {
|
|
165
|
+
...evidence,
|
|
166
|
+
contextualServiceBindingSelected: true,
|
|
167
|
+
targetRepo: resolution.target.repoName,
|
|
168
|
+
targetServicePath: resolution.target.servicePath,
|
|
169
|
+
targetOperationPath: resolution.target.operationPath,
|
|
170
|
+
targetOperation: resolution.target.operationName,
|
|
171
|
+
};
|
|
172
|
+
if (persistedRows.some((row) => row.status === 'resolved'))
|
|
173
|
+
return { evidence: { ...resolvedEvidence, contextualPreservedPersistedResolvedEdge: true }, state };
|
|
174
|
+
return {
|
|
175
|
+
row: {
|
|
176
|
+
id: -call.id,
|
|
177
|
+
edge_type: 'REMOTE_CALL_RESOLVES_TO_OPERATION',
|
|
178
|
+
from_id: String(call.id),
|
|
179
|
+
to_kind: 'operation',
|
|
180
|
+
to_id: String(resolution.target.operationId),
|
|
181
|
+
confidence: resolution.target.score,
|
|
182
|
+
evidence_json: JSON.stringify(resolvedEvidence),
|
|
183
|
+
status: 'resolved',
|
|
184
|
+
},
|
|
185
|
+
evidence: resolvedEvidence,
|
|
186
|
+
state,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function contextualEvidence(
|
|
191
|
+
binding: ContextBinding,
|
|
192
|
+
normalized: ReturnType<typeof normalizeODataOperationInvocationPath>,
|
|
193
|
+
operationPath: string,
|
|
194
|
+
servicePath: string | undefined,
|
|
195
|
+
destination: string | undefined,
|
|
196
|
+
resolution: OperationResolution,
|
|
197
|
+
state: ContextualRuntimeState,
|
|
198
|
+
): Record<string, unknown> {
|
|
199
|
+
const candidates = boundedContextCandidates(resolution.candidates);
|
|
200
|
+
return {
|
|
201
|
+
contextualServiceBindingAttempted: true,
|
|
202
|
+
contextualBinding: bindingEvidence(binding),
|
|
203
|
+
operationPath,
|
|
204
|
+
rawOperationPath: normalized?.rawOperationPath,
|
|
205
|
+
normalizedOperationPath: normalized?.wasInvocation
|
|
206
|
+
? normalized.normalizedOperationPath : undefined,
|
|
207
|
+
invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length
|
|
208
|
+
? normalized.invocationArgumentPlaceholderKeys : undefined,
|
|
209
|
+
servicePath,
|
|
210
|
+
serviceAlias: binding.alias,
|
|
211
|
+
serviceAliasExpr: binding.aliasExpr,
|
|
212
|
+
destination,
|
|
213
|
+
requireServicePath: binding.requireServicePath,
|
|
214
|
+
requireDestination: binding.requireDestination,
|
|
215
|
+
effectiveServicePath: binding.effectiveServicePath,
|
|
216
|
+
effectiveDestination: binding.effectiveDestination,
|
|
217
|
+
contextualResolutionStatus: resolution.status,
|
|
218
|
+
contextualCandidateCount: candidates.candidateCount,
|
|
219
|
+
shownContextualCandidateCount: candidates.shownCandidateCount,
|
|
220
|
+
omittedContextualCandidateCount: candidates.omittedCandidateCount,
|
|
221
|
+
candidates: candidates.candidates,
|
|
222
|
+
contextualResolutionReasons: resolution.reasons,
|
|
223
|
+
resolutionReasons: resolution.reasons,
|
|
224
|
+
contextualPreSubstitutionState: historicalState(state),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function bindingEvidence(binding: ContextBinding): Record<string, unknown> {
|
|
229
|
+
return {
|
|
230
|
+
source: binding.source,
|
|
231
|
+
callerArgument: binding.callerArgument,
|
|
232
|
+
callerProperty: binding.callerProperty,
|
|
233
|
+
calleeParameter: binding.calleeParameter,
|
|
234
|
+
calleeReceiver: binding.calleeReceiver,
|
|
235
|
+
callerSite: binding.callerSite,
|
|
236
|
+
calleeSite: binding.calleeSite,
|
|
237
|
+
bindingSourceFile: binding.sourceFile,
|
|
238
|
+
bindingSourceLine: binding.sourceLine,
|
|
239
|
+
alias: binding.alias,
|
|
240
|
+
aliasExpr: binding.aliasExpr,
|
|
241
|
+
requireServicePath: binding.requireServicePath,
|
|
242
|
+
requireDestination: binding.requireDestination,
|
|
243
|
+
effectiveServicePath: binding.effectiveServicePath,
|
|
244
|
+
effectiveDestination: binding.effectiveDestination,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function stateForResolution(
|
|
249
|
+
resolution: OperationResolution,
|
|
250
|
+
): ContextualRuntimeState {
|
|
251
|
+
if (resolution.status === 'resolved') return { category: 'none' };
|
|
252
|
+
if (resolution.status === 'dynamic') {
|
|
253
|
+
const missingVariables = missingVariableKeys(resolution.reasons);
|
|
254
|
+
return {
|
|
255
|
+
category: 'dynamic_missing',
|
|
256
|
+
message: dynamicMissingReason(missingVariables),
|
|
257
|
+
missingVariables,
|
|
258
|
+
resolutionStatus: resolution.status,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (resolution.status === 'ambiguous') return {
|
|
262
|
+
category: 'ambiguous_operation',
|
|
263
|
+
message: 'Ambiguous contextual operation candidates',
|
|
264
|
+
resolutionStatus: resolution.status,
|
|
265
|
+
};
|
|
266
|
+
return {
|
|
267
|
+
category: resolution.status === 'unresolved'
|
|
268
|
+
? 'no_matching_operation' : 'other_blocker',
|
|
269
|
+
message: resolution.status === 'unresolved'
|
|
270
|
+
? 'No contextual operation candidate matched'
|
|
271
|
+
: 'Contextual operation resolution is blocked',
|
|
272
|
+
resolutionStatus: resolution.status,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function historicalState(
|
|
277
|
+
state: ContextualRuntimeState,
|
|
278
|
+
): ContextualRuntimeState {
|
|
279
|
+
return { ...state, phase: 'before_runtime_substitution' };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function missingVariableKeys(reasons: string[]): string[] {
|
|
283
|
+
return normalizedKeys(reasons.flatMap((reason) =>
|
|
284
|
+
reason.startsWith('missing_variable:')
|
|
285
|
+
? [reason.slice('missing_variable:'.length)] : []));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function normalizedKeys(keys: readonly string[]): string[] {
|
|
289
|
+
return [...new Set(keys.filter((key) => key.length > 0))].sort();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function withLeadingSlash(value: string): string {
|
|
293
|
+
return value.startsWith('/') ? value : `/${value}`;
|
|
294
|
+
}
|