@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +11 -12
  3. package/TECHNICAL-NOTE.md +18 -3
  4. package/dist/{chunk-PTLDSHRC.js → chunk-ERIZHM5C.js} +2646 -1067
  5. package/dist/chunk-ERIZHM5C.js.map +1 -0
  6. package/dist/cli.js +162 -13
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.js +1 -1
  9. package/package.json +1 -1
  10. package/src/cli/001-doctor-projection.ts +136 -0
  11. package/src/cli/doctor.ts +20 -3
  12. package/src/db/repositories.ts +10 -2
  13. package/src/linker/000-implementation-candidates.ts +674 -0
  14. package/src/linker/001-implementation-evidence-projection.ts +191 -0
  15. package/src/linker/002-call-evidence.ts +226 -0
  16. package/src/linker/cross-repo-linker.ts +27 -453
  17. package/src/linker/dynamic-edge-resolver.ts +35 -0
  18. package/src/linker/external-http-target.ts +24 -3
  19. package/src/linker/helper-package-linker.ts +18 -2
  20. package/src/linker/service-resolver.ts +45 -2
  21. package/src/output/doctor-output.ts +13 -4
  22. package/src/output/table-output.ts +15 -4
  23. package/src/trace/000-dynamic-target-types.ts +12 -0
  24. package/src/trace/001-dynamic-identity.ts +45 -17
  25. package/src/trace/003-dynamic-references.ts +236 -35
  26. package/src/trace/004-dynamic-candidate-sources.ts +155 -0
  27. package/src/trace/005-implementation-selection.ts +187 -0
  28. package/src/trace/006-contextual-projection.ts +30 -0
  29. package/src/trace/007-implementation-start-diagnostic.ts +61 -0
  30. package/src/trace/008-contextual-runtime-state.ts +294 -0
  31. package/src/trace/009-selected-handler-provenance.ts +186 -0
  32. package/src/trace/dynamic-targets.ts +205 -159
  33. package/src/trace/evidence.ts +132 -34
  34. package/src/trace/implementation-hints.ts +148 -8
  35. package/src/trace/selectors.ts +74 -9
  36. package/src/trace/trace-engine.ts +97 -125
  37. package/src/utils/000-bounded-projection.ts +166 -0
  38. package/dist/chunk-PTLDSHRC.js.map +0 -1
@@ -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
- contextualUnresolvedReason?: string,
70
- ): { row: TraceGraphRow; evidence: Record<string, unknown>; target?: OperationTarget; unresolvedReason?: string } {
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(evidence, candidateCap),
77
- contextualUnresolvedReason,
89
+ boundDynamicEvidence(boundedEvidence, candidateCap),
90
+ contextualState,
78
91
  );
79
- const substituted = evidenceWithRuntimeVariables(evidence, options.vars);
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
- if (analysis && analysis.candidateCount > 0
88
- && analysis.viableCandidateCount === 0
89
- && Object.keys(analysis.appliedSuppliedVariables).length > 0)
90
- return noCandidateRuntimeResolution(row, enriched);
91
- if (dynamicMode === 'infer') {
92
- const inferred = inferredTarget(analysis);
93
- if (inferred)
94
- return resolvedRuntimeResolution(row, enriched, inferred, inferred.reasons);
95
- }
96
- if (!hasApplicableRuntimeVariables(evidence, options.vars)) {
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
- const resolution = resolveRuntimeOperation(db, enriched, workspaceId);
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, enriched, resolution.target, resolution.reasons,
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 { row, evidence: withEffectiveResolution(enriched, row, unresolvedReason, resolution), unresolvedReason };
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
- contextualUnresolvedReason: string | undefined,
117
- ): ReturnType<typeof runtimeResolution> {
118
- const unresolvedReason = contextualUnresolvedReason ?? row.unresolved_reason;
169
+ contextualState: ContextualRuntimeState | undefined,
170
+ ): RuntimeResolutionResult {
171
+ const unresolvedReason = contextualReason(contextualState) ?? row.unresolved_reason;
119
172
  return {
120
173
  row,
121
- evidence: withEffectiveResolution(evidence, row, unresolvedReason),
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
- ): ReturnType<typeof runtimeResolution> {
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(evidence, row, unresolvedReason),
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
- ): ReturnType<typeof runtimeResolution> {
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(row, evidence, unresolvedReason, resolution);
408
+ const current = effectiveResolution(
409
+ row, evidence, unresolvedReason, resolution, contextualState,
410
+ );
347
411
  const rest = { ...evidence };
348
412
  delete rest.runtimeResolvedCandidate;
349
- return { ...rest, effectiveResolution: current, linker: { status: current.status, confidence: current.confidence, reason: unresolvedReason, edgeType: current.edgeType } };
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: persistedCandidates.length,
476
+ persistedCandidateCount,
392
477
  persistedCandidateOmittedCount: Math.max(
393
478
  0,
394
- persistedCandidates.length - analysis.maxCandidates,
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: matchingHints,
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
- suggestions?: unknown,
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: Array.isArray(suggestions) && suggestions.length > 0 ? suggestions : undefined,
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) return [];
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
- return accepted
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: repos,
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 value as EdgeEvidence;
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
  }
@@ -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
- selectorSuggestions: [...new Set(suggestions)].sort(),
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
- selectorSuggestions: [...new Set(suggestions)].sort(),
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
- selectorSuggestions: [...new Set(suggestions)].sort(),
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
- serviceSuggestions,
522
- selectorSuggestions: fullSelectorSuggestions(candidates),
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[] {