@saptools/service-flow 0.1.53 → 0.1.55
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 +11 -0
- package/README.md +12 -5
- package/TECHNICAL-NOTE.md +13 -0
- package/dist/{chunk-LFH7C46B.js → chunk-GXYVIHJ5.js} +635 -135
- package/dist/chunk-GXYVIHJ5.js.map +1 -0
- package/dist/cli.js +71 -26
- 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/cli.ts +24 -16
- package/src/linker/000-implementation-candidates.ts +35 -2
- package/src/linker/001-implementation-evidence-projection.ts +78 -6
- package/src/output/000-stdout-policy.ts +65 -0
- package/src/output/table-output.ts +11 -2
- package/src/parsers/outbound-call-parser.ts +116 -11
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import {
|
|
3
|
+
selectedHandlerProvenance,
|
|
4
|
+
type SelectedHandlerProvenance,
|
|
5
|
+
type SelectedHandlerSource,
|
|
6
|
+
} from '../linker/001-implementation-evidence-projection.js';
|
|
7
|
+
|
|
8
|
+
export interface HandlerMethodNode extends Record<string, unknown> {
|
|
9
|
+
id: string;
|
|
10
|
+
kind: 'handler_method';
|
|
11
|
+
label: string;
|
|
12
|
+
methodId: number;
|
|
13
|
+
methodName: string;
|
|
14
|
+
className: string;
|
|
15
|
+
sourceFile: string;
|
|
16
|
+
sourceLine: number;
|
|
17
|
+
repoId: number;
|
|
18
|
+
repoName: string;
|
|
19
|
+
packageName?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SelectedHandlerEvidence {
|
|
23
|
+
handler?: HandlerMethodNode;
|
|
24
|
+
evidence: Record<string, unknown>;
|
|
25
|
+
diagnostic?: Record<string, unknown>;
|
|
26
|
+
unresolvedReason?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function handlerMethodNode(
|
|
30
|
+
db: Db,
|
|
31
|
+
methodId: string,
|
|
32
|
+
): HandlerMethodNode | undefined {
|
|
33
|
+
const row = db.prepare(`SELECT hm.id methodId,hm.method_name methodName,
|
|
34
|
+
hm.source_line sourceLine,hc.class_name className,hc.source_file sourceFile,
|
|
35
|
+
r.name repoName,r.id repoId,r.package_name packageName
|
|
36
|
+
FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id
|
|
37
|
+
JOIN repositories r ON r.id=hc.repo_id WHERE hm.id=?`).get(methodId) as
|
|
38
|
+
| Record<string, unknown>
|
|
39
|
+
| undefined;
|
|
40
|
+
return handlerNodeFromRow(row);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function withSelectedHandlerProvenance(
|
|
44
|
+
evidence: Record<string, unknown>,
|
|
45
|
+
targetId: string,
|
|
46
|
+
handler: HandlerMethodNode | undefined,
|
|
47
|
+
): SelectedHandlerEvidence {
|
|
48
|
+
if (!handler) return unavailableHandlerEvidence(evidence, targetId);
|
|
49
|
+
const selected = selectedHandlerProvenance(handlerSource(handler));
|
|
50
|
+
const stored = record(evidence.selectedHandler);
|
|
51
|
+
const mismatchedFields = storedSelectedHandlerMismatches(
|
|
52
|
+
stored, selected, targetId,
|
|
53
|
+
);
|
|
54
|
+
if (mismatchedFields.length === 0)
|
|
55
|
+
return { handler, evidence: { ...evidence, selectedHandler: selected } };
|
|
56
|
+
return {
|
|
57
|
+
handler,
|
|
58
|
+
evidence: {
|
|
59
|
+
...evidence,
|
|
60
|
+
selectedHandler: selected,
|
|
61
|
+
selectedHandlerProvenanceAudit: {
|
|
62
|
+
status: 'mismatch',
|
|
63
|
+
graphTargetId: targetId,
|
|
64
|
+
mismatchedFields,
|
|
65
|
+
persistedSelectedHandler: stored,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
diagnostic: {
|
|
69
|
+
severity: 'warning',
|
|
70
|
+
code: 'selected_handler_provenance_mismatch',
|
|
71
|
+
message: 'Persisted selected handler provenance did not match the resolved graph target',
|
|
72
|
+
graphTargetId: targetId,
|
|
73
|
+
mismatchedFields,
|
|
74
|
+
sourceFile: handler.sourceFile,
|
|
75
|
+
sourceLine: handler.sourceLine,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function unavailableHandlerEvidence(
|
|
81
|
+
evidence: Record<string, unknown>,
|
|
82
|
+
targetId: string,
|
|
83
|
+
): SelectedHandlerEvidence {
|
|
84
|
+
return {
|
|
85
|
+
evidence: {
|
|
86
|
+
...evidence,
|
|
87
|
+
selectedHandlerProvenanceAudit: {
|
|
88
|
+
status: 'unavailable',
|
|
89
|
+
graphTargetId: targetId,
|
|
90
|
+
reason: 'handler_target_not_indexed',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
diagnostic: {
|
|
94
|
+
severity: 'warning',
|
|
95
|
+
code: 'selected_handler_target_not_found',
|
|
96
|
+
message: 'Resolved implementation target is not an indexed handler method',
|
|
97
|
+
graphTargetId: targetId,
|
|
98
|
+
},
|
|
99
|
+
unresolvedReason: 'Resolved implementation target is not an indexed handler method',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handlerNodeFromRow(
|
|
104
|
+
row: Record<string, unknown> | undefined,
|
|
105
|
+
): HandlerMethodNode | undefined {
|
|
106
|
+
const methodId = numberValue(row?.methodId);
|
|
107
|
+
const methodName = stringValue(row?.methodName);
|
|
108
|
+
const className = stringValue(row?.className);
|
|
109
|
+
const sourceFile = stringValue(row?.sourceFile);
|
|
110
|
+
const sourceLine = numberValue(row?.sourceLine);
|
|
111
|
+
const repoId = numberValue(row?.repoId);
|
|
112
|
+
const repoName = stringValue(row?.repoName);
|
|
113
|
+
if (methodId === undefined || !methodName || !className || !sourceFile
|
|
114
|
+
|| sourceLine === undefined || repoId === undefined || !repoName)
|
|
115
|
+
return undefined;
|
|
116
|
+
return {
|
|
117
|
+
id: `handler_method:${methodId}`,
|
|
118
|
+
kind: 'handler_method',
|
|
119
|
+
label: `${repoName}:${className}.${methodName}`,
|
|
120
|
+
methodId,
|
|
121
|
+
methodName,
|
|
122
|
+
className,
|
|
123
|
+
sourceFile,
|
|
124
|
+
sourceLine,
|
|
125
|
+
repoId,
|
|
126
|
+
repoName,
|
|
127
|
+
packageName: stringValue(row?.packageName),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handlerSource(node: HandlerMethodNode): SelectedHandlerSource {
|
|
132
|
+
return {
|
|
133
|
+
methodId: node.methodId,
|
|
134
|
+
methodName: node.methodName,
|
|
135
|
+
className: node.className,
|
|
136
|
+
repositoryId: node.repoId,
|
|
137
|
+
repositoryName: node.repoName,
|
|
138
|
+
repositoryPackageName: node.packageName,
|
|
139
|
+
sourceFile: node.sourceFile,
|
|
140
|
+
sourceLine: node.sourceLine,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function storedSelectedHandlerMismatches(
|
|
145
|
+
stored: Record<string, unknown>,
|
|
146
|
+
selected: SelectedHandlerProvenance,
|
|
147
|
+
targetId: string,
|
|
148
|
+
): string[] {
|
|
149
|
+
if (Object.keys(stored).length === 0) return [];
|
|
150
|
+
const expectedRepository = record(stored.repository);
|
|
151
|
+
const selectedRepository = selected.repository ?? {};
|
|
152
|
+
return [
|
|
153
|
+
mismatch('graphTargetId', stored.graphTargetId, targetId),
|
|
154
|
+
mismatch('methodId', stored.methodId, selected.methodId),
|
|
155
|
+
mismatch('className', stored.className, selected.className),
|
|
156
|
+
mismatch('methodName', stored.methodName, selected.methodName),
|
|
157
|
+
mismatch('sourceFile', stored.sourceFile, selected.sourceFile),
|
|
158
|
+
mismatch('sourceLine', stored.sourceLine, selected.sourceLine),
|
|
159
|
+
mismatch('repository.id', expectedRepository.id, selectedRepository.id),
|
|
160
|
+
mismatch('repository.name', expectedRepository.name, selectedRepository.name),
|
|
161
|
+
mismatch('repository.packageName', expectedRepository.packageName,
|
|
162
|
+
selectedRepository.packageName),
|
|
163
|
+
].flatMap((field) => field ? [field] : []);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function mismatch(
|
|
167
|
+
field: string,
|
|
168
|
+
stored: unknown,
|
|
169
|
+
actual: unknown,
|
|
170
|
+
): string | undefined {
|
|
171
|
+
return stored === undefined || stored === actual ? undefined : field;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function record(value: unknown): Record<string, unknown> {
|
|
175
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
176
|
+
? value as Record<string, unknown>
|
|
177
|
+
: {};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function stringValue(value: unknown): string | undefined {
|
|
181
|
+
return typeof value === 'string' ? value : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function numberValue(value: unknown): number | undefined {
|
|
185
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
186
|
+
}
|