@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.
- package/CHANGELOG.md +6 -0
- package/README.md +9 -12
- package/TECHNICAL-NOTE.md +11 -3
- package/dist/{chunk-PTLDSHRC.js → chunk-LFH7C46B.js} +2214 -1044
- package/dist/chunk-LFH7C46B.js.map +1 -0
- package/dist/cli.js +157 -11
- 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 +140 -0
- package/src/cli/doctor.ts +20 -3
- package/src/db/repositories.ts +10 -2
- package/src/linker/000-implementation-candidates.ts +641 -0
- package/src/linker/001-implementation-evidence-projection.ts +119 -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 +4 -2
- 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/dynamic-targets.ts +205 -159
- package/src/trace/evidence.ts +35 -9
- package/src/trace/implementation-hints.ts +148 -8
- package/src/trace/selectors.ts +74 -9
- package/src/trace/trace-engine.ts +39 -52
- package/src/utils/000-bounded-projection.ts +161 -0
- package/dist/chunk-PTLDSHRC.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImplementationHint } from '../types.js';
|
|
2
|
+
import { projectBounded } from '../utils/000-bounded-projection.js';
|
|
2
3
|
|
|
3
4
|
interface Candidate {
|
|
4
5
|
accepted?: boolean;
|
|
@@ -25,6 +26,13 @@ export interface ImplementationSelection {
|
|
|
25
26
|
evidence: Record<string, unknown>;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
export interface ImplementationHintSuggestionProjection {
|
|
30
|
+
suggestions: Array<Record<string, unknown>>;
|
|
31
|
+
suggestionCount: number;
|
|
32
|
+
shownSuggestionCount: number;
|
|
33
|
+
omittedSuggestionCount: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
export function parseImplementationHint(value: string): ImplementationHint {
|
|
29
37
|
const hint: Partial<ImplementationHint> = {};
|
|
30
38
|
for (const part of value.split(',')) {
|
|
@@ -40,8 +48,9 @@ export function selectImplementation(
|
|
|
40
48
|
rawEvidence: Record<string, unknown>,
|
|
41
49
|
hints: ImplementationHint[] | undefined,
|
|
42
50
|
legacyRepo: string | undefined,
|
|
51
|
+
canonicalEvidence?: Record<string, unknown>,
|
|
43
52
|
): ImplementationSelection {
|
|
44
|
-
const evidence = asEvidence(rawEvidence);
|
|
53
|
+
const evidence = asEvidence(canonicalEvidence ?? rawEvidence);
|
|
45
54
|
const scoped = hints ?? [];
|
|
46
55
|
const matchingHints = scoped.filter((hint) => hintMatchesEdge(hint, evidence));
|
|
47
56
|
if (matchingHints.length === 0) {
|
|
@@ -50,14 +59,18 @@ export function selectImplementation(
|
|
|
50
59
|
return { blocksAutomatic: false, evidence: { status: 'not_matched', reason, strategy: 'scoped_implementation_hint' } };
|
|
51
60
|
}
|
|
52
61
|
if (matchingHints.length > 1) {
|
|
62
|
+
const projection = projectBounded(matchingHints, compareHints);
|
|
53
63
|
return {
|
|
54
64
|
blocksAutomatic: true,
|
|
55
65
|
evidence: {
|
|
56
66
|
status: 'tied',
|
|
57
67
|
reason: 'multiple_scoped_hints_matched_edge',
|
|
58
68
|
strategy: 'scoped_implementation_hint',
|
|
59
|
-
matchedHints:
|
|
69
|
+
matchedHints: projection.items,
|
|
60
70
|
candidateCount: matchingHints.length,
|
|
71
|
+
matchedHintCount: projection.totalCount,
|
|
72
|
+
shownMatchedHintCount: projection.shownCount,
|
|
73
|
+
omittedMatchedHintCount: projection.omittedCount,
|
|
61
74
|
},
|
|
62
75
|
};
|
|
63
76
|
}
|
|
@@ -67,26 +80,48 @@ export function selectImplementation(
|
|
|
67
80
|
|
|
68
81
|
export function implementationHintDiagnostic(
|
|
69
82
|
selection: ImplementationSelection,
|
|
70
|
-
|
|
83
|
+
suggestionEvidence?: unknown,
|
|
71
84
|
): Record<string, unknown> | undefined {
|
|
72
85
|
if (!selection.blocksAutomatic || selection.methodId) return undefined;
|
|
86
|
+
const suggestions = projectedSuggestions(suggestionEvidence);
|
|
73
87
|
return {
|
|
74
88
|
severity: 'warning',
|
|
75
89
|
code: 'implementation_hint_mismatch',
|
|
76
90
|
message: 'Implementation hint did not select exactly one viable candidate',
|
|
77
91
|
hintStatus: selection.evidence.status,
|
|
78
92
|
candidateCount: selection.evidence.candidateCount,
|
|
79
|
-
implementationHintSuggestions:
|
|
93
|
+
implementationHintSuggestions: suggestions.suggestions.length > 0
|
|
94
|
+
? suggestions.suggestions
|
|
95
|
+
: undefined,
|
|
96
|
+
implementationHintSuggestionCount: suggestions.suggestionCount,
|
|
97
|
+
shownImplementationHintSuggestionCount: suggestions.shownSuggestionCount,
|
|
98
|
+
omittedImplementationHintSuggestionCount: suggestions.omittedSuggestionCount,
|
|
80
99
|
implementationSelection: selection.evidence,
|
|
81
100
|
};
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
export function implementationHintSuggestions(rawEvidence: Record<string, unknown>): Array<Record<string, unknown>> {
|
|
104
|
+
return implementationHintSuggestionProjection(rawEvidence).suggestions;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function implementationHintSuggestionProjection(
|
|
108
|
+
rawEvidence: Record<string, unknown>,
|
|
109
|
+
): ImplementationHintSuggestionProjection {
|
|
85
110
|
const evidence = asEvidence(rawEvidence);
|
|
86
111
|
const accepted = (evidence.candidates ?? []).filter((candidate) => candidate.accepted);
|
|
87
|
-
if (accepted.length < 2)
|
|
112
|
+
if (accepted.length < 2) {
|
|
113
|
+
return {
|
|
114
|
+
suggestions: [],
|
|
115
|
+
suggestionCount: 0,
|
|
116
|
+
shownSuggestionCount: 0,
|
|
117
|
+
omittedSuggestionCount: 0,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
88
120
|
const repos = selectableRepositories(accepted);
|
|
89
|
-
|
|
121
|
+
const repositoryProjection = projectBounded(
|
|
122
|
+
repos, (left, right) => left.localeCompare(right),
|
|
123
|
+
);
|
|
124
|
+
const suggestions = accepted
|
|
90
125
|
.flatMap((candidate) => {
|
|
91
126
|
const repo = candidate.handlerPackage?.name;
|
|
92
127
|
if (!repo || !repos.includes(repo)) return [];
|
|
@@ -96,12 +131,71 @@ export function implementationHintSuggestions(rawEvidence: Record<string, unknow
|
|
|
96
131
|
operationPath: hint.operationPath,
|
|
97
132
|
ambiguityReason: evidence.ambiguityReasons?.[0],
|
|
98
133
|
candidateFamily: hint.candidateFamily,
|
|
99
|
-
selectableImplementationRepositories:
|
|
134
|
+
selectableImplementationRepositories: repositoryProjection.items,
|
|
135
|
+
selectableImplementationRepositoryCount: repositoryProjection.totalCount,
|
|
136
|
+
shownSelectableImplementationRepositoryCount:
|
|
137
|
+
repositoryProjection.shownCount,
|
|
138
|
+
omittedSelectableImplementationRepositoryCount:
|
|
139
|
+
repositoryProjection.omittedCount,
|
|
100
140
|
implementationRepo: repo,
|
|
101
141
|
hint,
|
|
102
142
|
cli: `--implementation-hint ${hintString(hint)}`,
|
|
103
143
|
}];
|
|
104
144
|
});
|
|
145
|
+
const projection = projectBounded(suggestions, compareSuggestion);
|
|
146
|
+
return {
|
|
147
|
+
suggestions: projection.items,
|
|
148
|
+
suggestionCount: projection.totalCount,
|
|
149
|
+
shownSuggestionCount: projection.shownCount,
|
|
150
|
+
omittedSuggestionCount: projection.omittedCount,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function projectedSuggestions(value: unknown): ImplementationHintSuggestionProjection {
|
|
155
|
+
const evidence = objectRecord(value);
|
|
156
|
+
const values = Array.isArray(value)
|
|
157
|
+
? recordSuggestions(value)
|
|
158
|
+
: recordSuggestions(evidence.implementationHintSuggestions);
|
|
159
|
+
const projection = projectBounded(values, compareSuggestion);
|
|
160
|
+
const total = Math.max(
|
|
161
|
+
numericValue(evidence.implementationHintSuggestionCount),
|
|
162
|
+
projection.totalCount,
|
|
163
|
+
);
|
|
164
|
+
return {
|
|
165
|
+
suggestions: projection.items,
|
|
166
|
+
suggestionCount: total,
|
|
167
|
+
shownSuggestionCount: projection.shownCount,
|
|
168
|
+
omittedSuggestionCount: Math.max(0, total - projection.shownCount),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function compareHints(left: ImplementationHint, right: ImplementationHint): number {
|
|
173
|
+
return hintString(left).localeCompare(hintString(right));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function compareSuggestion(
|
|
177
|
+
left: Record<string, unknown>,
|
|
178
|
+
right: Record<string, unknown>,
|
|
179
|
+
): number {
|
|
180
|
+
return String(left.cli ?? '').localeCompare(String(right.cli ?? ''))
|
|
181
|
+
|| String(left.implementationRepo ?? '').localeCompare(
|
|
182
|
+
String(right.implementationRepo ?? ''),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function objectRecord(value: unknown): Record<string, unknown> {
|
|
187
|
+
return isRecord(value) ? value : {};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function recordSuggestions(value: unknown): Array<Record<string, unknown>> {
|
|
191
|
+
return Array.isArray(value)
|
|
192
|
+
? value.filter((item): item is Record<string, unknown> =>
|
|
193
|
+
Boolean(item && typeof item === 'object' && !Array.isArray(item)))
|
|
194
|
+
: [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function numericValue(value: unknown): number {
|
|
198
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
105
199
|
}
|
|
106
200
|
|
|
107
201
|
function selectableRepositories(candidates: Candidate[]): string[] {
|
|
@@ -219,5 +313,51 @@ function legacyHint(implementationRepo: string): ImplementationHint {
|
|
|
219
313
|
}
|
|
220
314
|
|
|
221
315
|
function asEvidence(value: Record<string, unknown>): EdgeEvidence {
|
|
222
|
-
return
|
|
316
|
+
return {
|
|
317
|
+
servicePath: stringValue(value.servicePath),
|
|
318
|
+
operationPath: stringValue(value.operationPath),
|
|
319
|
+
ambiguityReasons: stringArray(value.ambiguityReasons),
|
|
320
|
+
candidateFamilies: candidateFamilies(value.candidateFamilies),
|
|
321
|
+
candidates: candidates(value.candidates),
|
|
322
|
+
modelPackage: packageValue(value.modelPackage),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function candidates(value: unknown): Candidate[] {
|
|
327
|
+
return recordSuggestions(value).map((candidate) => ({
|
|
328
|
+
accepted: candidate.accepted === true,
|
|
329
|
+
methodId: numericValue(candidate.methodId) || undefined,
|
|
330
|
+
sourceFile: stringValue(candidate.sourceFile),
|
|
331
|
+
handlerPackage: packageValue(candidate.handlerPackage),
|
|
332
|
+
modelPackage: packageValue(candidate.modelPackage),
|
|
333
|
+
servicePath: stringValue(candidate.servicePath),
|
|
334
|
+
operationPath: stringValue(candidate.operationPath),
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function candidateFamilies(value: unknown): Array<{ packageName?: string }> {
|
|
339
|
+
return recordSuggestions(value).map((family) => ({
|
|
340
|
+
packageName: stringValue(family.packageName),
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function packageValue(value: unknown): { name?: string; packageName?: string } | undefined {
|
|
345
|
+
const candidate = objectRecord(value);
|
|
346
|
+
const name = stringValue(candidate.name);
|
|
347
|
+
const packageName = stringValue(candidate.packageName);
|
|
348
|
+
return name || packageName ? { name, packageName } : undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function stringArray(value: unknown): string[] {
|
|
352
|
+
return Array.isArray(value)
|
|
353
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
354
|
+
: [];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function stringValue(value: unknown): string | undefined {
|
|
358
|
+
return typeof value === 'string' ? value : undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
362
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
223
363
|
}
|
package/src/trace/selectors.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Db } from '../db/connection.js';
|
|
2
2
|
import type { TraceStart } from '../types.js';
|
|
3
|
+
import {
|
|
4
|
+
projectBounded,
|
|
5
|
+
type BoundedProjection,
|
|
6
|
+
} from '../utils/000-bounded-projection.js';
|
|
3
7
|
|
|
4
8
|
export interface SelectorSourceScope {
|
|
5
9
|
files?: Set<string>;
|
|
@@ -65,14 +69,27 @@ export function selectorRepoAmbiguousDiagnostic(
|
|
|
65
69
|
return [`--repo ${candidate.packageName}`];
|
|
66
70
|
return [];
|
|
67
71
|
});
|
|
72
|
+
const candidateProjection = projectBounded(candidates, (left, right) =>
|
|
73
|
+
left.name.localeCompare(right.name)
|
|
74
|
+
|| String(left.packageName ?? '').localeCompare(String(right.packageName ?? ''))
|
|
75
|
+
|| left.id - right.id);
|
|
76
|
+
const suggestionProjection = projectBounded(
|
|
77
|
+
[...new Set(suggestions)], (left, right) => left.localeCompare(right),
|
|
78
|
+
);
|
|
68
79
|
return {
|
|
69
80
|
severity: 'warning',
|
|
70
81
|
code: 'selector_repo_ambiguous',
|
|
71
82
|
message: `Repository selector matched multiple indexed repositories: ${requested}`,
|
|
72
83
|
selectorKind: 'repo',
|
|
73
84
|
requestedRepository: requested,
|
|
74
|
-
candidates,
|
|
75
|
-
|
|
85
|
+
candidates: candidateProjection.items,
|
|
86
|
+
candidateCount: candidateProjection.totalCount,
|
|
87
|
+
shownCandidateCount: candidateProjection.shownCount,
|
|
88
|
+
omittedCandidateCount: candidateProjection.omittedCount,
|
|
89
|
+
selectorSuggestions: suggestionProjection.items,
|
|
90
|
+
selectorSuggestionCount: suggestionProjection.totalCount,
|
|
91
|
+
shownSelectorSuggestionCount: suggestionProjection.shownCount,
|
|
92
|
+
omittedSelectorSuggestionCount: suggestionProjection.omittedCount,
|
|
76
93
|
remediation: suggestions.length > 0
|
|
77
94
|
? 'Use one of the unique --repo selectors shown.'
|
|
78
95
|
: 'Repository names and package names must be unique before this selector can be traced safely.',
|
|
@@ -250,6 +267,8 @@ function operationHandlerScope(
|
|
|
250
267
|
? [`--repo ${candidate.repoName} --handler ${candidate.className}`]
|
|
251
268
|
: [];
|
|
252
269
|
});
|
|
270
|
+
const projection = boundedSelectorCandidates(candidates);
|
|
271
|
+
const suggestionProjection = boundedSelectorSuggestions(suggestions);
|
|
253
272
|
return { diagnostics: [{
|
|
254
273
|
severity: 'warning',
|
|
255
274
|
code: 'trace_start_ambiguous',
|
|
@@ -258,8 +277,14 @@ function operationHandlerScope(
|
|
|
258
277
|
normalizedSelectorValue: requested,
|
|
259
278
|
resolutionStage: 'handler',
|
|
260
279
|
resolutionStatus: 'ambiguous_handler_operation',
|
|
261
|
-
candidates,
|
|
262
|
-
|
|
280
|
+
candidates: projection.items,
|
|
281
|
+
candidateCount: projection.totalCount,
|
|
282
|
+
shownCandidateCount: projection.shownCount,
|
|
283
|
+
omittedCandidateCount: projection.omittedCount,
|
|
284
|
+
selectorSuggestions: suggestionProjection.items,
|
|
285
|
+
selectorSuggestionCount: suggestionProjection.totalCount,
|
|
286
|
+
shownSelectorSuggestionCount: suggestionProjection.shownCount,
|
|
287
|
+
omittedSelectorSuggestionCount: suggestionProjection.omittedCount,
|
|
263
288
|
remediation: 'Select one handler class explicitly; no operation was chosen automatically.',
|
|
264
289
|
}] };
|
|
265
290
|
}
|
|
@@ -358,6 +383,8 @@ function handlerSelectorAmbiguity(
|
|
|
358
383
|
return [`${repoName ? `--repo ${repoName} ` : ''}--handler ${candidate.className}`];
|
|
359
384
|
return [];
|
|
360
385
|
});
|
|
386
|
+
const projection = boundedSelectorCandidates(candidates);
|
|
387
|
+
const suggestionProjection = boundedSelectorSuggestions(suggestions);
|
|
361
388
|
return {
|
|
362
389
|
severity: 'warning',
|
|
363
390
|
code: 'trace_start_ambiguous',
|
|
@@ -366,8 +393,14 @@ function handlerSelectorAmbiguity(
|
|
|
366
393
|
requestedHandler: requested,
|
|
367
394
|
resolutionStage: 'handler',
|
|
368
395
|
resolutionStatus: 'ambiguous_handler',
|
|
369
|
-
candidates,
|
|
370
|
-
|
|
396
|
+
candidates: projection.items,
|
|
397
|
+
candidateCount: projection.totalCount,
|
|
398
|
+
shownCandidateCount: projection.shownCount,
|
|
399
|
+
omittedCandidateCount: projection.omittedCount,
|
|
400
|
+
selectorSuggestions: suggestionProjection.items,
|
|
401
|
+
selectorSuggestionCount: suggestionProjection.totalCount,
|
|
402
|
+
shownSelectorSuggestionCount: suggestionProjection.shownCount,
|
|
403
|
+
omittedSelectorSuggestionCount: suggestionProjection.omittedCount,
|
|
371
404
|
remediation: suggestions.length > 0
|
|
372
405
|
? 'Use one of the scoped handler selectors shown.'
|
|
373
406
|
: 'No current CLI selector uniquely identifies these duplicate handler classes.',
|
|
@@ -510,6 +543,9 @@ export function ambiguousStartDiagnostic(
|
|
|
510
543
|
.flatMap((row) => typeof row.servicePath === 'string'
|
|
511
544
|
? [`--service ${row.servicePath}`]
|
|
512
545
|
: []))].sort();
|
|
546
|
+
const projection = boundedSelectorCandidates(candidates);
|
|
547
|
+
const serviceProjection = boundedSelectorSuggestions(serviceSuggestions);
|
|
548
|
+
const selectorProjection = boundedSelectorSuggestions(fullSelectorSuggestions(candidates));
|
|
513
549
|
return {
|
|
514
550
|
severity: 'warning',
|
|
515
551
|
code: 'trace_start_ambiguous',
|
|
@@ -517,11 +553,40 @@ export function ambiguousStartDiagnostic(
|
|
|
517
553
|
normalizedSelectorValue: requested,
|
|
518
554
|
resolutionStage: 'operation',
|
|
519
555
|
resolutionStatus: 'ambiguous_operation',
|
|
520
|
-
candidates,
|
|
521
|
-
|
|
522
|
-
|
|
556
|
+
candidates: projection.items,
|
|
557
|
+
candidateCount: projection.totalCount,
|
|
558
|
+
shownCandidateCount: projection.shownCount,
|
|
559
|
+
omittedCandidateCount: projection.omittedCount,
|
|
560
|
+
serviceSuggestions: serviceProjection.items,
|
|
561
|
+
serviceSuggestionCount: serviceProjection.totalCount,
|
|
562
|
+
shownServiceSuggestionCount: serviceProjection.shownCount,
|
|
563
|
+
omittedServiceSuggestionCount: serviceProjection.omittedCount,
|
|
564
|
+
selectorSuggestions: selectorProjection.items,
|
|
565
|
+
selectorSuggestionCount: selectorProjection.totalCount,
|
|
566
|
+
shownSelectorSuggestionCount: selectorProjection.shownCount,
|
|
567
|
+
omittedSelectorSuggestionCount: selectorProjection.omittedCount,
|
|
523
568
|
};
|
|
524
569
|
}
|
|
570
|
+
|
|
571
|
+
function boundedSelectorCandidates(
|
|
572
|
+
candidates: Array<Record<string, unknown>>,
|
|
573
|
+
): BoundedProjection<Record<string, unknown>> {
|
|
574
|
+
return projectBounded(candidates, (left, right) =>
|
|
575
|
+
String(left.repoName ?? '').localeCompare(String(right.repoName ?? ''))
|
|
576
|
+
|| String(left.servicePath ?? '').localeCompare(String(right.servicePath ?? ''))
|
|
577
|
+
|| String(left.className ?? '').localeCompare(String(right.className ?? ''))
|
|
578
|
+
|| String(left.sourceFile ?? '').localeCompare(String(right.sourceFile ?? ''))
|
|
579
|
+
|| Number(left.sourceLine ?? 0) - Number(right.sourceLine ?? 0)
|
|
580
|
+
|| Number(left.handlerClassId ?? left.operationId ?? 0)
|
|
581
|
+
- Number(right.handlerClassId ?? right.operationId ?? 0));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function boundedSelectorSuggestions(
|
|
585
|
+
suggestions: string[],
|
|
586
|
+
): BoundedProjection<string> {
|
|
587
|
+
return projectBounded([...new Set(suggestions)], (left, right) =>
|
|
588
|
+
left.localeCompare(right));
|
|
589
|
+
}
|
|
525
590
|
function fullSelectorSuggestions(
|
|
526
591
|
candidates: Array<Record<string, unknown>>,
|
|
527
592
|
): string[] {
|
|
@@ -10,7 +10,14 @@ import {
|
|
|
10
10
|
loadTraceDiagnostics,
|
|
11
11
|
prependTraceDiagnostic,
|
|
12
12
|
} from './002-trace-diagnostics.js';
|
|
13
|
-
import { implementationHintDiagnostic
|
|
13
|
+
import { implementationHintDiagnostic } from './implementation-hints.js';
|
|
14
|
+
import {
|
|
15
|
+
contextualImplementationSelection,
|
|
16
|
+
hintedImplementationSelection,
|
|
17
|
+
} from './005-implementation-selection.js';
|
|
18
|
+
import { boundedContextCandidates } from './006-contextual-projection.js';
|
|
19
|
+
import { implementationStartDiagnostic } from './007-implementation-start-diagnostic.js';
|
|
20
|
+
import { boundCandidateLikeEvidence } from '../utils/000-bounded-projection.js';
|
|
14
21
|
import {
|
|
15
22
|
ambiguousStartDiagnostic,
|
|
16
23
|
selectorNotFoundDiagnostic,
|
|
@@ -110,31 +117,21 @@ function operationStartScope(db: Db, repoId: number | undefined, start: TraceSta
|
|
|
110
117
|
const operationId = String(rows[0]?.operationId);
|
|
111
118
|
const impl = implementationScope(db, operationId);
|
|
112
119
|
if (impl.edge?.status === 'resolved' && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? new Set([impl.symbolId]) : undefined, repoId: impl.repoId, operationId, diagnostics: [] };
|
|
113
|
-
const hinted =
|
|
120
|
+
const hinted = hintedImplementationSelection(
|
|
121
|
+
db, impl.edge, operationId, hintOptions,
|
|
122
|
+
);
|
|
114
123
|
if (hinted.methodId) {
|
|
115
124
|
const hintedScope = handlerScope(db, hinted.methodId);
|
|
116
125
|
if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? new Set([hintedScope.symbolId]) : undefined, repoId: hintedScope.repoId, operationId, diagnostics: [] };
|
|
117
126
|
}
|
|
118
127
|
if (impl.edge) {
|
|
119
128
|
const evidence = parseEvidence(impl.edge.evidence_json);
|
|
120
|
-
const hintDiagnostic = implementationHintDiagnostic(hinted, evidence
|
|
121
|
-
const diagnostics = [
|
|
129
|
+
const hintDiagnostic = implementationHintDiagnostic(hinted, evidence);
|
|
130
|
+
const diagnostics = [implementationStartDiagnostic(impl.edge, evidence)];
|
|
122
131
|
return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
|
|
123
132
|
}
|
|
124
133
|
return { operationId, diagnostics: [{ severity: 'warning', code: 'trace_start_implementation_unresolved', message: 'Indexed operation matched but no implementation candidate exists', resolutionStage: 'implementation', resolutionStatus: 'operation_without_implementation' }] };
|
|
125
134
|
}
|
|
126
|
-
function implementationRejectionReasons(evidence: Record<string, unknown>): string[] {
|
|
127
|
-
const candidates = Array.isArray(evidence.candidates)
|
|
128
|
-
? evidence.candidates.filter((item): item is Record<string, unknown> =>
|
|
129
|
-
Boolean(item && typeof item === 'object' && !Array.isArray(item)))
|
|
130
|
-
: [];
|
|
131
|
-
const reasons = candidates.flatMap((candidate) =>
|
|
132
|
-
Array.isArray(candidate.rejectedReasons)
|
|
133
|
-
? candidate.rejectedReasons.filter((reason): reason is string =>
|
|
134
|
-
typeof reason === 'string')
|
|
135
|
-
: []);
|
|
136
|
-
return [...new Set(reasons)].sort();
|
|
137
|
-
}
|
|
138
135
|
function sourceFilesForStart(
|
|
139
136
|
db: Db,
|
|
140
137
|
repoId: number | undefined,
|
|
@@ -235,29 +232,6 @@ function implementationScope(db: Db, operationId: string): { repoId?: number; fi
|
|
|
235
232
|
return { repoId: row?.repoId, files: new Set(), edge };
|
|
236
233
|
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
|
|
237
234
|
}
|
|
238
|
-
function implementationMethodIdFromHint(edge: GraphRow | undefined, options: ImplementationHintOptions): ImplementationSelection {
|
|
239
|
-
if (!edge || edge.status !== 'ambiguous') return { blocksAutomatic: false, evidence: { status: 'not_applicable' } };
|
|
240
|
-
return selectImplementation(parseEvidence(edge.evidence_json), options.implementationHints, options.implementationRepo);
|
|
241
|
-
}
|
|
242
|
-
function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId: number | undefined, remoteEvidence: Record<string, unknown>, hintOptions: ImplementationHintOptions): ImplementationSelection {
|
|
243
|
-
const hinted = implementationMethodIdFromHint(edge, hintOptions);
|
|
244
|
-
if (hinted.methodId) return hinted;
|
|
245
|
-
if (hinted.blocksAutomatic) return hinted;
|
|
246
|
-
if (!edge || edge.status !== 'ambiguous' || callerRepoId === undefined) return hinted;
|
|
247
|
-
const evidence = JSON.parse(String(edge.evidence_json || '{}')) as { candidates?: Array<{ accepted?: boolean; methodId?: number; handlerPackage?: { id?: number; name?: string }; applicationPackage?: { id?: number; name?: string }; reasons?: string[]; score?: number }> };
|
|
248
|
-
const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
|
|
249
|
-
const reasons: string[] = [];
|
|
250
|
-
let score = Number(item.score ?? 0);
|
|
251
|
-
if (Number(item.handlerPackage?.id) === callerRepoId) { score += 10; reasons.push('handler_package_matches_caller_repository'); }
|
|
252
|
-
if (Number(item.applicationPackage?.id) === callerRepoId) { score += 10; reasons.push('registration_package_matches_caller_repository'); }
|
|
253
|
-
if (typeof remoteEvidence.effectiveServicePath === 'string' || typeof remoteEvidence.effectiveDestination === 'string' || typeof remoteEvidence.effectiveAlias === 'string') { score += 1; reasons.push('remote_call_context_available'); }
|
|
254
|
-
return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
|
|
255
|
-
}).sort((a, b) => b.score - a.score);
|
|
256
|
-
if (scores.length === 0) return { blocksAutomatic: false, evidence: { status: 'not_applicable', candidateScores: [] } };
|
|
257
|
-
const [first, second] = scores;
|
|
258
|
-
if (first && first.methodId !== undefined && first.score > 0 && (!second || first.score > second.score)) return { methodId: String(first.methodId), blocksAutomatic: false, evidence: { status: 'selected', selectedMethodId: first.methodId, candidateScores: scores } };
|
|
259
|
-
return { blocksAutomatic: false, evidence: hinted.evidence.reason === 'no_scoped_hint_matched_edge' ? hinted.evidence : { status: 'tied', tieReason: scores.length > 1 ? 'duplicate_helper_implementation_candidates' : 'no_unique_materially_stronger_candidate', candidateScores: scores } };
|
|
260
|
-
}
|
|
261
235
|
function handlerScope(db: Db, methodId: string): { repoId?: number; files: Set<string>; symbolId?: number } | undefined {
|
|
262
236
|
const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.qualified_name=hc.class_name || '.' || hm.method_name AND s.start_line=hm.source_line WHERE hm.id=?").get(methodId) as { repoId?: number; sourceFile?: string; symbolId?: number } | undefined;
|
|
263
237
|
if (!row || typeof row.symbolId !== 'number') return undefined;
|
|
@@ -317,11 +291,15 @@ function workspaceIdForCall(db: Db, callId: string): number | undefined {
|
|
|
317
291
|
function parseEvidence(value: unknown): Record<string, unknown> {
|
|
318
292
|
try {
|
|
319
293
|
const parsed = JSON.parse(String(value || '{}')) as unknown;
|
|
320
|
-
return
|
|
294
|
+
return isEvidenceRecord(parsed) ? boundCandidateLikeEvidence(parsed) : {};
|
|
321
295
|
} catch {
|
|
322
296
|
return {};
|
|
323
297
|
}
|
|
324
298
|
}
|
|
299
|
+
|
|
300
|
+
function isEvidenceRecord(value: unknown): value is Record<string, unknown> {
|
|
301
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
302
|
+
}
|
|
325
303
|
function receiverFromEvidence(value: unknown): string | undefined {
|
|
326
304
|
const evidence = parseEvidence(value);
|
|
327
305
|
return typeof evidence.receiver === 'string' ? evidence.receiver : undefined;
|
|
@@ -461,16 +439,20 @@ function contextForSymbolCall(db: Db, symbolCall: Record<string, unknown>, calle
|
|
|
461
439
|
function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBinding | undefined, workspaceId: number | undefined, persistedRows: GraphRow[] = []): { row?: GraphRow; evidence?: Record<string, unknown>; unresolvedReason?: string } {
|
|
462
440
|
if (!binding || String(call.call_type) !== 'remote_action' || call.operation_path_expr === undefined || call.operation_path_expr === null) return {};
|
|
463
441
|
if (binding.resolutionStatus === 'ambiguous') {
|
|
442
|
+
const candidates = boundedContextCandidates(binding.bindingCandidates ?? []);
|
|
464
443
|
return {
|
|
465
444
|
evidence: {
|
|
466
445
|
contextualServiceBindingAttempted: true,
|
|
467
446
|
contextualBinding: {
|
|
468
447
|
source: binding.source,
|
|
469
448
|
status: 'tied',
|
|
470
|
-
candidates:
|
|
449
|
+
candidates: candidates.candidates,
|
|
450
|
+
candidateCount: candidates.candidateCount,
|
|
451
|
+
shownCandidateCount: candidates.shownCandidateCount,
|
|
452
|
+
omittedCandidateCount: candidates.omittedCandidateCount,
|
|
471
453
|
},
|
|
472
454
|
contextualResolutionStatus: 'ambiguous',
|
|
473
|
-
contextualCandidateCount:
|
|
455
|
+
contextualCandidateCount: candidates.candidateCount,
|
|
474
456
|
},
|
|
475
457
|
unresolvedReason: 'Ambiguous contextual service binding candidates',
|
|
476
458
|
};
|
|
@@ -480,7 +462,8 @@ function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBind
|
|
|
480
462
|
const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
|
|
481
463
|
const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
|
|
482
464
|
const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
|
|
483
|
-
const
|
|
465
|
+
const candidates = boundedContextCandidates(resolution.candidates);
|
|
466
|
+
const evidence: Record<string, unknown> = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, callerSite: binding.callerSite, calleeSite: binding.calleeSite, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine, alias: binding.alias, aliasExpr: binding.aliasExpr, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination }, operationPath: op, rawOperationPath: normalized?.rawOperationPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : undefined, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : undefined, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: candidates.candidateCount, shownContextualCandidateCount: candidates.shownCandidateCount, omittedContextualCandidateCount: candidates.omittedCandidateCount, candidates: candidates.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
|
|
484
467
|
if (!resolution.target) return { evidence, unresolvedReason: resolution.status === 'ambiguous' ? 'Ambiguous contextual operation candidates' : resolution.status === 'dynamic' ? `Dynamic contextual target is missing runtime variables: ${resolution.reasons.join(', ')}` : 'No contextual operation candidate matched' };
|
|
485
468
|
const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
|
|
486
469
|
const persistedResolved = persistedRows.find((item) => item.status === 'resolved');
|
|
@@ -524,7 +507,9 @@ export function trace(
|
|
|
524
507
|
const op = operationNode(db, scope.startOperationId);
|
|
525
508
|
const impl = implementationScope(db, scope.startOperationId);
|
|
526
509
|
if (op) nodes.set(String(op.id), op);
|
|
527
|
-
const startSelection =
|
|
510
|
+
const startSelection = hintedImplementationSelection(
|
|
511
|
+
db, impl.edge, scope.startOperationId, hintOptions,
|
|
512
|
+
);
|
|
528
513
|
if (impl.edge && (impl.edge.status === 'resolved' || startSelection.methodId)) {
|
|
529
514
|
const selectedMethodId = impl.edge.status === 'resolved' ? impl.edge.to_id : startSelection.methodId;
|
|
530
515
|
const implEvidence = { ...parseEvidence(impl.edge.evidence_json), startResolution: { strategy: 'indexed_operation_graph', matchedOperationId: scope.startOperationId, implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, selectedHandlerMethodId: selectedMethodId }, implementationSelection: startSelection.methodId ? startSelection.evidence : undefined };
|
|
@@ -564,7 +549,7 @@ export function trace(
|
|
|
564
549
|
const nextKey = `${nextRepoId}:${[...nextSymbols].join(',')}:${[...nextFiles].join(',')}`;
|
|
565
550
|
const calleeNode = symbolNode(db, Number(symbolCall.callee_symbol_id));
|
|
566
551
|
if (calleeNode) nodes.set(String(calleeNode.id), calleeNode);
|
|
567
|
-
const evidence = { ...(
|
|
552
|
+
const evidence = { ...parseEvidence(symbolCall.evidence_json), sourceFile: symbolCall.source_file, sourceLine: symbolCall.source_line, calleeSymbolId: symbolCall.callee_symbol_id, calleeSymbolName: calleeNode?.symbolName, calleeSymbolFile: calleeNode?.sourceFile, resolutionStatus: symbolCall.status };
|
|
568
553
|
edges.push({ step: current.depth, type: 'local_symbol_call', from: String(symbolCall.callee_expression), to: calleeNode?.label ? String(calleeNode.label) : `symbol:${String(symbolCall.callee_symbol_id)}`, evidence, confidence: Number(symbolCall.confidence ?? 0.8), unresolvedReason: String(symbolCall.status) === 'resolved' ? undefined : symbolCall.unresolved_reason ? String(symbolCall.unresolved_reason) : undefined });
|
|
569
554
|
if (seenScopes.has(nextKey)) edges.push({ step: current.depth, type: 'cycle', from: String(symbolCall.callee_expression), to: nextKey, evidence: { cycle: true, symbolCallId: symbolCall.id }, confidence: 1, unresolvedReason: 'Cycle detected; downstream symbol already visited' });
|
|
570
555
|
else queue.push({ repoId: nextRepoId, files: nextFiles, symbolIds: nextSymbols, depth: current.depth + 1, context: contextForSymbolCall(db, symbolCall, callerBindings) });
|
|
@@ -590,9 +575,7 @@ export function trace(
|
|
|
590
575
|
for (const row of graphRows) {
|
|
591
576
|
if (seenEdges.has(Number(row.id))) continue;
|
|
592
577
|
seenEdges.add(Number(row.id));
|
|
593
|
-
const persistedEvidence =
|
|
594
|
-
String(row.evidence_json || '{}'),
|
|
595
|
-
) as Record<string, unknown>;
|
|
578
|
+
const persistedEvidence = parseEvidence(row.evidence_json);
|
|
596
579
|
const rawEvidence = baseTraceEvidence(row as TraceGraphRow, call, persistedEvidence, contextual.evidence);
|
|
597
580
|
const effective = runtimeResolution(db, row as TraceGraphRow, rawEvidence, {
|
|
598
581
|
vars: options.vars,
|
|
@@ -618,16 +601,20 @@ export function trace(
|
|
|
618
601
|
confidence: Number(effectiveRow.confidence ?? call.confidence),
|
|
619
602
|
unresolvedReason: effective.unresolvedReason,
|
|
620
603
|
});
|
|
621
|
-
if ((options.dynamicMode ?? 'strict') === 'candidates'
|
|
604
|
+
if ((options.dynamicMode ?? 'strict') === 'candidates'
|
|
605
|
+
&& effectiveRow.status !== 'resolved')
|
|
622
606
|
edges.push(...dynamicCandidateBranches(current.depth, call, evidence));
|
|
623
607
|
if (effectiveRow.to_kind === 'operation') {
|
|
624
608
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
625
|
-
const contextSelection =
|
|
609
|
+
const contextSelection = contextualImplementationSelection(
|
|
610
|
+
db, implementation.edge, effectiveRow.to_id, current.repoId, evidence,
|
|
611
|
+
hintOptions,
|
|
612
|
+
);
|
|
626
613
|
const contextMethodId = contextSelection.methodId;
|
|
627
614
|
const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : undefined;
|
|
628
615
|
if (implementation.edge) {
|
|
629
|
-
const implEvidence =
|
|
630
|
-
const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence
|
|
616
|
+
const implEvidence = parseEvidence(implementation.edge.evidence_json);
|
|
617
|
+
const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence);
|
|
631
618
|
if (hintDiagnostic) prependTraceDiagnostic(diagnostics, hintDiagnostic);
|
|
632
619
|
const handlerNode = implementation.edge.status === 'resolved' ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
|
|
633
620
|
const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
|