@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
package/src/trace/evidence.ts
CHANGED
|
@@ -4,6 +4,12 @@ import { normalizeODataOperationInvocationPath } from '../linker/odata-path-norm
|
|
|
4
4
|
import { resolveOperation, type OperationTarget } from '../linker/service-resolver.js';
|
|
5
5
|
import type { DynamicMode } from '../types.js';
|
|
6
6
|
import { analyzeDynamicTargetCandidates, type DynamicTargetAnalysis, type DynamicTargetCandidate } from './dynamic-targets.js';
|
|
7
|
+
import { boundCandidateLikeEvidence } from '../utils/000-bounded-projection.js';
|
|
8
|
+
import {
|
|
9
|
+
dynamicMissingReason,
|
|
10
|
+
isStructuralContextualBlocker,
|
|
11
|
+
type ContextualRuntimeState,
|
|
12
|
+
} from './008-contextual-runtime-state.js';
|
|
7
13
|
|
|
8
14
|
export interface TraceGraphRow extends Record<string, unknown> {
|
|
9
15
|
id: number;
|
|
@@ -34,6 +40,12 @@ interface RuntimeDiagnosticTotals {
|
|
|
34
40
|
rejectedCandidates: Record<string, unknown>[];
|
|
35
41
|
suggestedVarSets: Record<string, unknown>[];
|
|
36
42
|
}
|
|
43
|
+
interface RuntimeResolutionResult {
|
|
44
|
+
row: TraceGraphRow;
|
|
45
|
+
evidence: Record<string, unknown>;
|
|
46
|
+
target?: OperationTarget;
|
|
47
|
+
unresolvedReason?: string;
|
|
48
|
+
}
|
|
37
49
|
|
|
38
50
|
export function baseTraceEvidence(
|
|
39
51
|
row: TraceGraphRow,
|
|
@@ -66,17 +78,18 @@ export function runtimeResolution(
|
|
|
66
78
|
evidence: Record<string, unknown>,
|
|
67
79
|
options: { vars?: Record<string, string>; dynamicMode?: DynamicMode; maxDynamicCandidates?: number },
|
|
68
80
|
workspaceId: number | undefined,
|
|
69
|
-
|
|
70
|
-
):
|
|
81
|
+
contextualState?: ContextualRuntimeState,
|
|
82
|
+
): RuntimeResolutionResult {
|
|
71
83
|
const dynamicMode = options.dynamicMode ?? 'strict';
|
|
72
84
|
const candidateCap = positiveCandidateCap(options.maxDynamicCandidates);
|
|
85
|
+
const boundedEvidence = boundCandidateLikeEvidence(evidence, candidateCap);
|
|
73
86
|
if (!isDynamicRemoteOperationEdge(row, evidence))
|
|
74
87
|
return unchangedRuntimeResolution(
|
|
75
88
|
row,
|
|
76
|
-
boundDynamicEvidence(
|
|
77
|
-
|
|
89
|
+
boundDynamicEvidence(boundedEvidence, candidateCap),
|
|
90
|
+
contextualState,
|
|
78
91
|
);
|
|
79
|
-
const substituted = evidenceWithRuntimeVariables(
|
|
92
|
+
const substituted = evidenceWithRuntimeVariables(boundedEvidence, options.vars);
|
|
80
93
|
const analysis = analyzeDynamicTargetCandidates(
|
|
81
94
|
db, substituted, workspaceId, dynamicMode, candidateCap,
|
|
82
95
|
);
|
|
@@ -84,41 +97,83 @@ export function runtimeResolution(
|
|
|
84
97
|
analysis ? evidenceWithDynamicAnalysis(substituted, analysis) : substituted,
|
|
85
98
|
candidateCap,
|
|
86
99
|
);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const unresolvedReason = contextualUnresolvedReason ?? row.unresolved_reason;
|
|
98
|
-
const withSections = withEffectiveResolution(enriched, row, unresolvedReason);
|
|
100
|
+
const appliedRuntimeValues = hasApplicableRuntimeVariables(evidence, options.vars);
|
|
101
|
+
const analyzed = analyzedRuntimeResolution(
|
|
102
|
+
row, enriched, analysis, dynamicMode, appliedRuntimeValues, contextualState,
|
|
103
|
+
);
|
|
104
|
+
if (analyzed) return analyzed;
|
|
105
|
+
if (!appliedRuntimeValues) {
|
|
106
|
+
const unresolvedReason = contextualReason(contextualState) ?? row.unresolved_reason;
|
|
107
|
+
const withSections = withEffectiveResolution(
|
|
108
|
+
enriched, row, unresolvedReason, undefined, contextualState,
|
|
109
|
+
);
|
|
99
110
|
return { row, evidence: withSections, unresolvedReason };
|
|
100
111
|
}
|
|
101
|
-
|
|
112
|
+
return resolveSuppliedRuntimeOperation(db, row, enriched, workspaceId, contextualState);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function analyzedRuntimeResolution(
|
|
116
|
+
row: TraceGraphRow,
|
|
117
|
+
evidence: Record<string, unknown>,
|
|
118
|
+
analysis: DynamicTargetAnalysis | undefined,
|
|
119
|
+
dynamicMode: DynamicMode,
|
|
120
|
+
appliedRuntimeValues: boolean,
|
|
121
|
+
contextualState: ContextualRuntimeState | undefined,
|
|
122
|
+
): RuntimeResolutionResult | undefined {
|
|
123
|
+
if (analysis && analysis.viableCandidateCount === 0
|
|
124
|
+
&& Object.keys(analysis.appliedSuppliedVariables).length > 0)
|
|
125
|
+
return noCandidateRuntimeResolution(row, evidence, contextualState);
|
|
126
|
+
const inferred = dynamicMode === 'infer' ? inferredTarget(analysis) : undefined;
|
|
127
|
+
if (inferred && !isStructuralContextualBlocker(contextualState))
|
|
128
|
+
return resolvedRuntimeResolution(row, evidence, inferred, inferred.reasons);
|
|
129
|
+
if (analysis && analysis.missingVariables.length > 0 && appliedRuntimeValues) {
|
|
130
|
+
const unresolvedReason = dynamicMissingReason(analysis.missingVariables);
|
|
131
|
+
return {
|
|
132
|
+
row,
|
|
133
|
+
evidence: withEffectiveResolution(
|
|
134
|
+
evidence, row, unresolvedReason, undefined, contextualState,
|
|
135
|
+
),
|
|
136
|
+
unresolvedReason,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return isStructuralContextualBlocker(contextualState)
|
|
140
|
+
? unchangedRuntimeResolution(row, evidence, contextualState)
|
|
141
|
+
: undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveSuppliedRuntimeOperation(
|
|
145
|
+
db: Db,
|
|
146
|
+
row: TraceGraphRow,
|
|
147
|
+
evidence: Record<string, unknown>,
|
|
148
|
+
workspaceId: number | undefined,
|
|
149
|
+
contextualState: ContextualRuntimeState | undefined,
|
|
150
|
+
): RuntimeResolutionResult {
|
|
151
|
+
const resolution = resolveRuntimeOperation(db, evidence, workspaceId);
|
|
102
152
|
if (resolution.target)
|
|
103
153
|
return resolvedRuntimeResolution(
|
|
104
|
-
row,
|
|
154
|
+
row, evidence, resolution.target, resolution.reasons,
|
|
105
155
|
);
|
|
106
|
-
if (analysis && analysis.viableCandidateCount === 0
|
|
107
|
-
&& Object.keys(analysis.appliedSuppliedVariables).length > 0)
|
|
108
|
-
return noCandidateRuntimeResolution(row, enriched);
|
|
109
156
|
const unresolvedReason = runtimeUnresolvedReason(resolution);
|
|
110
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
row,
|
|
159
|
+
evidence: withEffectiveResolution(
|
|
160
|
+
evidence, row, unresolvedReason, resolution, contextualState,
|
|
161
|
+
),
|
|
162
|
+
unresolvedReason,
|
|
163
|
+
};
|
|
111
164
|
}
|
|
112
165
|
|
|
113
166
|
function unchangedRuntimeResolution(
|
|
114
167
|
row: TraceGraphRow,
|
|
115
168
|
evidence: Record<string, unknown>,
|
|
116
|
-
|
|
117
|
-
):
|
|
118
|
-
const unresolvedReason =
|
|
169
|
+
contextualState: ContextualRuntimeState | undefined,
|
|
170
|
+
): RuntimeResolutionResult {
|
|
171
|
+
const unresolvedReason = contextualReason(contextualState) ?? row.unresolved_reason;
|
|
119
172
|
return {
|
|
120
173
|
row,
|
|
121
|
-
evidence: withEffectiveResolution(
|
|
174
|
+
evidence: withEffectiveResolution(
|
|
175
|
+
evidence, row, unresolvedReason, undefined, contextualState,
|
|
176
|
+
),
|
|
122
177
|
unresolvedReason,
|
|
123
178
|
};
|
|
124
179
|
}
|
|
@@ -126,11 +181,14 @@ function unchangedRuntimeResolution(
|
|
|
126
181
|
function noCandidateRuntimeResolution(
|
|
127
182
|
row: TraceGraphRow,
|
|
128
183
|
evidence: Record<string, unknown>,
|
|
129
|
-
|
|
184
|
+
contextualState: ContextualRuntimeState | undefined,
|
|
185
|
+
): RuntimeResolutionResult {
|
|
130
186
|
const unresolvedReason = 'No candidate remained after runtime substitution';
|
|
131
187
|
return {
|
|
132
188
|
row,
|
|
133
|
-
evidence: withEffectiveResolution(
|
|
189
|
+
evidence: withEffectiveResolution(
|
|
190
|
+
evidence, row, unresolvedReason, undefined, contextualState,
|
|
191
|
+
),
|
|
134
192
|
unresolvedReason,
|
|
135
193
|
};
|
|
136
194
|
}
|
|
@@ -140,7 +198,7 @@ function resolvedRuntimeResolution(
|
|
|
140
198
|
evidence: Record<string, unknown>,
|
|
141
199
|
target: OperationTarget,
|
|
142
200
|
reasons: string[],
|
|
143
|
-
):
|
|
201
|
+
): RuntimeResolutionResult {
|
|
144
202
|
const resolvedRow = {
|
|
145
203
|
...row,
|
|
146
204
|
status: 'resolved',
|
|
@@ -320,6 +378,7 @@ function effectiveResolution(
|
|
|
320
378
|
evidence: Record<string, unknown>,
|
|
321
379
|
unresolvedReason: string | undefined,
|
|
322
380
|
resolution?: ReturnType<typeof resolveRuntimeOperation>,
|
|
381
|
+
contextualState?: ContextualRuntimeState,
|
|
323
382
|
): Record<string, unknown> {
|
|
324
383
|
const target = resolution?.target;
|
|
325
384
|
return {
|
|
@@ -334,6 +393,8 @@ function effectiveResolution(
|
|
|
334
393
|
reasons: resolution?.reasons ?? evidence.resolutionReasons,
|
|
335
394
|
unresolvedReason,
|
|
336
395
|
edgeType: target ? 'REMOTE_CALL_RESOLVES_TO_OPERATION' : row.edge_type,
|
|
396
|
+
contextualBlocker: isStructuralContextualBlocker(contextualState)
|
|
397
|
+
? contextualState : undefined,
|
|
337
398
|
};
|
|
338
399
|
}
|
|
339
400
|
|
|
@@ -342,11 +403,30 @@ function withEffectiveResolution(
|
|
|
342
403
|
row: TraceGraphRow,
|
|
343
404
|
unresolvedReason: string | undefined,
|
|
344
405
|
resolution?: ReturnType<typeof resolveRuntimeOperation>,
|
|
406
|
+
contextualState?: ContextualRuntimeState,
|
|
345
407
|
): Record<string, unknown> {
|
|
346
|
-
const current = effectiveResolution(
|
|
408
|
+
const current = effectiveResolution(
|
|
409
|
+
row, evidence, unresolvedReason, resolution, contextualState,
|
|
410
|
+
);
|
|
347
411
|
const rest = { ...evidence };
|
|
348
412
|
delete rest.runtimeResolvedCandidate;
|
|
349
|
-
return {
|
|
413
|
+
return {
|
|
414
|
+
...rest,
|
|
415
|
+
effectiveResolution: current,
|
|
416
|
+
linker: {
|
|
417
|
+
status: current.status,
|
|
418
|
+
confidence: current.confidence,
|
|
419
|
+
reason: unresolvedReason,
|
|
420
|
+
edgeType: current.edgeType,
|
|
421
|
+
contextualBlocker: current.contextualBlocker,
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function contextualReason(
|
|
427
|
+
state: ContextualRuntimeState | undefined,
|
|
428
|
+
): string | undefined {
|
|
429
|
+
return state?.category === 'none' ? undefined : state?.message;
|
|
350
430
|
}
|
|
351
431
|
|
|
352
432
|
function resolveRuntimeOperation(db: Db, evidence: Record<string, unknown>, workspaceId: number | undefined): ReturnType<typeof resolveOperation> {
|
|
@@ -384,14 +464,23 @@ function evidenceWithDynamicAnalysis(
|
|
|
384
464
|
): Record<string, unknown> {
|
|
385
465
|
const persistedCandidates = recordArray(evidence.candidates);
|
|
386
466
|
const persistedScores = recordArray(evidence.candidateScores);
|
|
467
|
+
const persistedCandidateCount = numeric(evidence.persistedCandidateCount)
|
|
468
|
+
|| numeric(evidence.candidateCount)
|
|
469
|
+
|| persistedCandidates.length;
|
|
470
|
+
const persistedScoreCount = numeric(evidence.candidateScoreCount)
|
|
471
|
+
|| persistedScores.length;
|
|
387
472
|
return {
|
|
388
473
|
...evidence,
|
|
389
474
|
candidates: persistedCandidates.slice(0, analysis.maxCandidates),
|
|
390
475
|
candidateScores: persistedScores.slice(0, analysis.maxCandidates),
|
|
391
|
-
persistedCandidateCount
|
|
476
|
+
persistedCandidateCount,
|
|
392
477
|
persistedCandidateOmittedCount: Math.max(
|
|
393
478
|
0,
|
|
394
|
-
persistedCandidates.length
|
|
479
|
+
persistedCandidateCount - Math.min(persistedCandidates.length, analysis.maxCandidates),
|
|
480
|
+
),
|
|
481
|
+
persistedCandidateScoreCount: persistedScoreCount,
|
|
482
|
+
persistedCandidateScoreOmittedCount: Math.max(
|
|
483
|
+
0, persistedScoreCount - Math.min(persistedScores.length, analysis.maxCandidates),
|
|
395
484
|
),
|
|
396
485
|
dynamicTargetExploration: {
|
|
397
486
|
mode: analysis.mode,
|
|
@@ -410,9 +499,16 @@ function evidenceWithDynamicAnalysis(
|
|
|
410
499
|
omittedRejectedCandidateCount: analysis.omittedRejectedCandidateCount,
|
|
411
500
|
rejectedCandidates: analysis.rejectedCandidates,
|
|
412
501
|
suggestedVarSets: analysis.suggestedVarSets,
|
|
502
|
+
suggestedVarSetCount: analysis.suggestedVarSetCount,
|
|
503
|
+
shownSuggestedVarSetCount: analysis.shownSuggestedVarSetCount,
|
|
504
|
+
omittedSuggestedVarSetCount: analysis.omittedSuggestedVarSetCount,
|
|
505
|
+
routingContext: analysis.routingContext,
|
|
413
506
|
},
|
|
414
507
|
dynamicTargetCandidates: analysis.candidates,
|
|
415
508
|
dynamicTargetCandidateSuggestions: analysis.shownCandidates,
|
|
509
|
+
dynamicTargetCandidateSuggestionCount: analysis.viableCandidateCount,
|
|
510
|
+
shownDynamicTargetCandidateSuggestionCount: analysis.shownCandidateCount,
|
|
511
|
+
omittedDynamicTargetCandidateSuggestionCount: analysis.omittedCandidateCount,
|
|
416
512
|
rejectedCandidates: analysis.rejectedCandidates,
|
|
417
513
|
dynamicTargetInference: analysis.inference,
|
|
418
514
|
};
|
|
@@ -429,6 +525,7 @@ const boundedDynamicListKeys = new Set([
|
|
|
429
525
|
'rejectedCandidateSuggestions',
|
|
430
526
|
'copyableExamples',
|
|
431
527
|
'conflicts',
|
|
528
|
+
'bindingAlternatives',
|
|
432
529
|
]);
|
|
433
530
|
|
|
434
531
|
function boundDynamicEvidence(
|
|
@@ -436,6 +533,7 @@ function boundDynamicEvidence(
|
|
|
436
533
|
limit: number,
|
|
437
534
|
): Record<string, unknown> {
|
|
438
535
|
const candidateCount = numeric(evidence.persistedCandidateCount)
|
|
536
|
+
|| numeric(evidence.candidateCount)
|
|
439
537
|
|| (Array.isArray(evidence.candidates) ? evidence.candidates.length : 0);
|
|
440
538
|
const projected = boundDynamicValue(evidence, limit);
|
|
441
539
|
const next = parseObject(projected);
|
|
@@ -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[] {
|