@saptools/service-flow 0.1.51 → 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +16 -14
  3. package/TECHNICAL-NOTE.md +18 -6
  4. package/dist/{chunk-YZJKE5UX.js → chunk-LFH7C46B.js} +4290 -1261
  5. package/dist/chunk-LFH7C46B.js.map +1 -0
  6. package/dist/cli.js +435 -515
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +45 -7
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/000-clean.ts +82 -0
  12. package/src/cli/001-doctor-projection.ts +140 -0
  13. package/src/cli/doctor.ts +20 -3
  14. package/src/cli.ts +75 -57
  15. package/src/db/connection.ts +1 -1
  16. package/src/db/migrations.ts +5 -3
  17. package/src/db/repositories.ts +130 -24
  18. package/src/db/schema.ts +7 -2
  19. package/src/indexer/repository-indexer.ts +57 -29
  20. package/src/indexer/workspace-indexer.ts +84 -6
  21. package/src/linker/000-implementation-candidates.ts +641 -0
  22. package/src/linker/001-implementation-evidence-projection.ts +119 -0
  23. package/src/linker/002-call-evidence.ts +226 -0
  24. package/src/linker/cross-repo-linker.ts +27 -441
  25. package/src/linker/dynamic-edge-resolver.ts +35 -0
  26. package/src/linker/external-http-target.ts +24 -3
  27. package/src/linker/helper-package-linker.ts +18 -2
  28. package/src/linker/service-resolver.ts +45 -2
  29. package/src/output/doctor-output.ts +13 -4
  30. package/src/output/table-output.ts +33 -5
  31. package/src/parsers/cds-parser.ts +8 -2
  32. package/src/parsers/decorator-parser.ts +382 -48
  33. package/src/parsers/handler-registration-parser.ts +38 -21
  34. package/src/parsers/imported-wrapper-parser.ts +18 -5
  35. package/src/parsers/outbound-call-parser.ts +25 -8
  36. package/src/parsers/package-json-parser.ts +36 -11
  37. package/src/parsers/service-binding-parser-helpers.ts +8 -1
  38. package/src/parsers/service-binding-parser.ts +6 -3
  39. package/src/parsers/symbol-parser.ts +13 -3
  40. package/src/parsers/ts-project.ts +54 -0
  41. package/src/trace/000-dynamic-target-types.ts +96 -0
  42. package/src/trace/001-dynamic-identity.ts +308 -0
  43. package/src/trace/002-trace-diagnostics.ts +54 -0
  44. package/src/trace/003-dynamic-references.ts +283 -0
  45. package/src/trace/004-dynamic-candidate-sources.ts +155 -0
  46. package/src/trace/005-implementation-selection.ts +187 -0
  47. package/src/trace/006-contextual-projection.ts +30 -0
  48. package/src/trace/007-implementation-start-diagnostic.ts +61 -0
  49. package/src/trace/dynamic-targets.ts +582 -306
  50. package/src/trace/evidence.ts +331 -46
  51. package/src/trace/implementation-hints.ts +148 -8
  52. package/src/trace/selectors.ts +551 -3
  53. package/src/trace/trace-engine.ts +129 -135
  54. package/src/types.ts +27 -1
  55. package/src/utils/000-bounded-projection.ts +161 -0
  56. package/dist/chunk-YZJKE5UX.js.map +0 -1
@@ -1,4 +1,28 @@
1
+ import type { Db } from '../db/connection.js';
1
2
  import type { TraceStart } from '../types.js';
3
+ import {
4
+ projectBounded,
5
+ type BoundedProjection,
6
+ } from '../utils/000-bounded-projection.js';
7
+
8
+ export interface SelectorSourceScope {
9
+ files?: Set<string>;
10
+ symbols?: Set<number>;
11
+ repoId?: number;
12
+ diagnostics?: Array<Record<string, unknown>>;
13
+ }
14
+
15
+ interface HandlerSelectorRow {
16
+ handlerClassId?: number | null;
17
+ repoId?: number | null;
18
+ repoName?: string | null;
19
+ className?: string | null;
20
+ sourceFile?: string | null;
21
+ sourceLine?: number | null;
22
+ methodId?: number | null;
23
+ symbolId?: number | null;
24
+ classEvidence?: string | null;
25
+ }
2
26
  export function parseVars(
3
27
  values: string[] | undefined
4
28
  ): Record<string, string> {
@@ -18,6 +42,498 @@ export function startLabel(start: TraceStart): string {
18
42
  .filter(Boolean)
19
43
  .join(' ');
20
44
  }
45
+
46
+ export function selectorRepoNotFoundDiagnostic(
47
+ requested: string,
48
+ ): Record<string, unknown> {
49
+ return {
50
+ severity: 'warning',
51
+ code: 'selector_repo_not_found',
52
+ message: `No indexed repository matched selector: ${requested}`,
53
+ selectorKind: 'repo',
54
+ requestedRepository: requested,
55
+ };
56
+ }
57
+
58
+ export function selectorRepoAmbiguousDiagnostic(
59
+ requested: string,
60
+ candidates: Array<{ id: number; name: string; packageName?: string }>,
61
+ ): Record<string, unknown> {
62
+ const uniqueName = (value: string): boolean => candidates
63
+ .filter((candidate) => candidate.name === value).length === 1;
64
+ const uniquePackage = (value: string): boolean => candidates
65
+ .filter((candidate) => candidate.packageName === value).length === 1;
66
+ const suggestions = candidates.flatMap((candidate) => {
67
+ if (uniqueName(candidate.name)) return [`--repo ${candidate.name}`];
68
+ if (candidate.packageName && uniquePackage(candidate.packageName))
69
+ return [`--repo ${candidate.packageName}`];
70
+ return [];
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
+ );
79
+ return {
80
+ severity: 'warning',
81
+ code: 'selector_repo_ambiguous',
82
+ message: `Repository selector matched multiple indexed repositories: ${requested}`,
83
+ selectorKind: 'repo',
84
+ requestedRepository: requested,
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,
93
+ remediation: suggestions.length > 0
94
+ ? 'Use one of the unique --repo selectors shown.'
95
+ : 'Repository names and package names must be unique before this selector can be traced safely.',
96
+ };
97
+ }
98
+
99
+ export function selectorNotFoundDiagnostic(
100
+ start: TraceStart,
101
+ ): Record<string, unknown> {
102
+ const serviceOnly = start.servicePath && !start.operation
103
+ && !start.operationPath && !start.handler;
104
+ return {
105
+ severity: 'warning',
106
+ code: 'trace_start_not_found',
107
+ message: serviceOnly
108
+ ? 'Service-only trace requires --operation or --path and will not broaden to the whole workspace'
109
+ : 'No handler source matched the requested trace start selector',
110
+ selectorKind: start.handler ? 'handler' : start.operation || start.operationPath
111
+ ? 'operation' : start.servicePath ? 'service' : undefined,
112
+ };
113
+ }
114
+
115
+ export function sourceScopeForSelector(
116
+ db: Db,
117
+ repoId: number | undefined,
118
+ start: TraceStart,
119
+ workspaceId?: number,
120
+ ): SelectorSourceScope | undefined {
121
+ if (start.handler) {
122
+ const classRows = handlerClassRows(db, repoId, start.handler, workspaceId);
123
+ if (classRows.length > 0) return handlerClassScope(classRows, start.handler);
124
+ const methodRows = handlerMethodRows(db, repoId, start.handler, workspaceId);
125
+ if (methodRows.length > 0)
126
+ return handlerMethodScope(methodRows, repoId, start.handler);
127
+ }
128
+ const operation = normalizeOperation(start.operation ?? start.operationPath);
129
+ if (!operation) return undefined;
130
+ const operationRows = operationHandlerRows(
131
+ db, repoId, operation, start.servicePath, workspaceId,
132
+ );
133
+ if (operationRows.length > 0)
134
+ return operationHandlerScope(operationRows, repoId, operation);
135
+ if (!start.servicePath) return undefined;
136
+ const implementationRows = implementationHandlerRows(
137
+ db, repoId, start.servicePath, operation, workspaceId,
138
+ );
139
+ return implementationRows.length > 0
140
+ ? executableScope(implementationRows, repoId)
141
+ : undefined;
142
+ }
143
+
144
+ function handlerClassRows(
145
+ db: Db,
146
+ repoId: number | undefined,
147
+ handler: string,
148
+ workspaceId: number | undefined,
149
+ ): HandlerSelectorRow[] {
150
+ return db.prepare(`SELECT hc.id handlerClassId,hc.repo_id repoId,
151
+ r.name repoName,hc.class_name className,
152
+ hc.source_file sourceFile,hc.source_line sourceLine,hm.id methodId,
153
+ sym.id symbolId,COALESCE(classSym.evidence_json,
154
+ (SELECT fallback.evidence_json FROM symbols fallback
155
+ WHERE fallback.repo_id=hc.repo_id AND fallback.kind='class'
156
+ AND fallback.name=hc.class_name AND fallback.source_file=hc.source_file
157
+ ORDER BY fallback.id LIMIT 1)) classEvidence
158
+ FROM handler_classes hc
159
+ JOIN repositories r ON r.id=hc.repo_id
160
+ LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
161
+ AND sym.source_file=hc.source_file
162
+ AND sym.kind='method'
163
+ AND substr(sym.qualified_name,1,length(hc.class_name)+1)=hc.class_name || '.'
164
+ AND (NOT EXISTS (SELECT 1 FROM handler_methods declared
165
+ WHERE declared.handler_class_id=hc.id
166
+ AND declared.method_name=sym.name)
167
+ OR EXISTS (SELECT 1 FROM handler_methods executable
168
+ WHERE executable.handler_class_id=hc.id
169
+ AND executable.method_name=sym.name
170
+ AND COALESCE(json_extract(executable.decorator_resolution_json,
171
+ '$.executable'),CASE WHEN executable.decorator_kind IN
172
+ ('Action','Func','On','Event') THEN 1 ELSE 0 END)=1))
173
+ LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id
174
+ AND hm.method_name=sym.name
175
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
176
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On','Event')
177
+ THEN 1 ELSE 0 END)=1
178
+ LEFT JOIN symbols classSym ON classSym.id=hc.symbol_id
179
+ WHERE (? IS NULL OR r.workspace_id=?)
180
+ AND (? IS NULL OR hc.repo_id=?) AND hc.class_name=?
181
+ ORDER BY hc.repo_id,hc.id,hm.id`).all(
182
+ workspaceId, workspaceId, repoId, repoId, handler,
183
+ ) as HandlerSelectorRow[];
184
+ }
185
+
186
+ function handlerMethodRows(
187
+ db: Db,
188
+ repoId: number | undefined,
189
+ handler: string,
190
+ workspaceId: number | undefined,
191
+ ): HandlerSelectorRow[] {
192
+ return db.prepare(`SELECT hc.id handlerClassId,hc.repo_id repoId,
193
+ r.name repoName,hc.class_name className,hc.source_file sourceFile,
194
+ hm.source_line sourceLine,hm.id methodId,sym.id symbolId
195
+ FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id
196
+ JOIN repositories r ON r.id=hc.repo_id
197
+ LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
198
+ AND sym.source_file=hc.source_file
199
+ AND sym.qualified_name=hc.class_name || '.' || hm.method_name
200
+ AND sym.start_line=hm.source_line
201
+ WHERE (? IS NULL OR r.workspace_id=?)
202
+ AND (? IS NULL OR hc.repo_id=?) AND hm.method_name=?
203
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
204
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On','Event')
205
+ THEN 1 ELSE 0 END)=1
206
+ ORDER BY hc.repo_id,hc.id,hm.id`).all(
207
+ workspaceId, workspaceId, repoId, repoId, handler,
208
+ ) as HandlerSelectorRow[];
209
+ }
210
+
211
+ function operationHandlerRows(
212
+ db: Db,
213
+ repoId: number | undefined,
214
+ operation: string,
215
+ servicePath: string | undefined,
216
+ workspaceId: number | undefined,
217
+ ): HandlerSelectorRow[] {
218
+ return db.prepare(`SELECT DISTINCT hc.id handlerClassId,hc.repo_id repoId,
219
+ r.name repoName,hc.class_name className,hc.source_file sourceFile,
220
+ hm.source_line sourceLine,hm.id methodId,sym.id symbolId
221
+ FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id
222
+ JOIN repositories r ON r.id=hc.repo_id
223
+ LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
224
+ AND sym.source_file=hc.source_file
225
+ AND sym.qualified_name=hc.class_name || '.' || hm.method_name
226
+ AND sym.start_line=hm.source_line
227
+ WHERE (? IS NULL OR r.workspace_id=?) AND (? IS NULL OR hc.repo_id=?)
228
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
229
+ CASE WHEN hm.decorator_kind='Event' THEN 'event'
230
+ WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
231
+ ELSE 'unsupported' END)='operation'
232
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
233
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On')
234
+ THEN 1 ELSE 0 END)=1
235
+ AND (hm.decorator_value=? OR hm.method_name=?)
236
+ AND (? IS NULL OR EXISTS (
237
+ SELECT 1 FROM cds_services svc JOIN cds_operations op ON op.service_id=svc.id
238
+ WHERE svc.repo_id=hc.repo_id AND svc.service_path=?
239
+ AND (op.operation_path=? OR op.operation_name=?)))
240
+ ORDER BY hc.repo_id,hc.id,hm.id`).all(
241
+ workspaceId, workspaceId, repoId, repoId,
242
+ operation, operation, servicePath, servicePath,
243
+ operation, operation,
244
+ ) as HandlerSelectorRow[];
245
+ }
246
+
247
+ function operationHandlerScope(
248
+ rows: HandlerSelectorRow[],
249
+ fallbackRepoId: number | undefined,
250
+ requested: string,
251
+ ): SelectorSourceScope {
252
+ const candidates = handlerSelectorCandidates(rows, 'method');
253
+ if (candidates.length < 2) return executableScope(rows, fallbackRepoId);
254
+ const classes = new Map<string, Set<string>>();
255
+ for (const candidate of candidates) {
256
+ const key = `${String(candidate.repoName)}:${String(candidate.className)}`;
257
+ classes.set(key, new Set([
258
+ ...(classes.get(key) ?? []),
259
+ String(candidate.handlerClassId),
260
+ ]));
261
+ }
262
+ const suggestions = candidates.flatMap((candidate) => {
263
+ if (typeof candidate.repoName !== 'string'
264
+ || typeof candidate.className !== 'string') return [];
265
+ const key = `${candidate.repoName}:${candidate.className}`;
266
+ return classes.get(key)?.size === 1
267
+ ? [`--repo ${candidate.repoName} --handler ${candidate.className}`]
268
+ : [];
269
+ });
270
+ const projection = boundedSelectorCandidates(candidates);
271
+ const suggestionProjection = boundedSelectorSuggestions(suggestions);
272
+ return { diagnostics: [{
273
+ severity: 'warning',
274
+ code: 'trace_start_ambiguous',
275
+ message: 'Operation selector matched multiple handler-only executable scopes',
276
+ selectorKind: 'operation',
277
+ normalizedSelectorValue: requested,
278
+ resolutionStage: 'handler',
279
+ resolutionStatus: 'ambiguous_handler_operation',
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,
288
+ remediation: 'Select one handler class explicitly; no operation was chosen automatically.',
289
+ }] };
290
+ }
291
+
292
+ function implementationHandlerRows(
293
+ db: Db,
294
+ repoId: number | undefined,
295
+ servicePath: string,
296
+ operation: string,
297
+ workspaceId: number | undefined,
298
+ ): HandlerSelectorRow[] {
299
+ return db.prepare(`SELECT DISTINCT hc.repo_id repoId,
300
+ hc.source_file sourceFile,hm.id methodId,sym.id symbolId
301
+ FROM cds_services svc JOIN cds_operations op ON op.service_id=svc.id
302
+ JOIN repositories r ON r.id=svc.repo_id
303
+ JOIN graph_edges edge ON edge.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER'
304
+ AND edge.status='resolved' AND edge.from_kind='operation'
305
+ AND edge.from_id=CAST(op.id AS TEXT)
306
+ JOIN handler_methods hm ON hm.id=CAST(edge.to_id AS INTEGER)
307
+ JOIN handler_classes hc ON hc.id=hm.handler_class_id
308
+ LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
309
+ AND sym.source_file=hc.source_file
310
+ AND sym.qualified_name=hc.class_name || '.' || hm.method_name
311
+ AND sym.start_line=hm.source_line
312
+ WHERE (? IS NULL OR r.workspace_id=?)
313
+ AND (? IS NULL OR svc.repo_id=?) AND svc.service_path=?
314
+ AND (op.operation_path=? OR op.operation_name=?)
315
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
316
+ CASE WHEN hm.decorator_kind='Event' THEN 'event'
317
+ WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
318
+ ELSE 'unsupported' END)='operation'
319
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
320
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On')
321
+ THEN 1 ELSE 0 END)=1
322
+ ORDER BY hc.repo_id,hc.id,hm.id`).all(
323
+ workspaceId, workspaceId, repoId, repoId,
324
+ servicePath, operation, operation,
325
+ ) as HandlerSelectorRow[];
326
+ }
327
+
328
+ function handlerClassScope(
329
+ rows: HandlerSelectorRow[],
330
+ requested: string,
331
+ ): SelectorSourceScope {
332
+ const ambiguity = handlerSelectorAmbiguity(rows, requested, 'class');
333
+ if (ambiguity) return { diagnostics: [ambiguity] };
334
+ const executable = rows.filter((row) => typeof row.symbolId === 'number');
335
+ const repoId = numericValue(rows[0]?.repoId);
336
+ if (executable.length > 0) {
337
+ const scope = executableScope(executable, repoId);
338
+ const warning = executable.some((row) => typeof row.methodId === 'number')
339
+ ? handlerDecoratorsNotIndexedDiagnostic(rows[0])
340
+ : handlerMethodsNotIndexedDiagnostic(rows[0]);
341
+ return warning ? { ...scope, diagnostics: [warning] } : scope;
342
+ }
343
+ const first = rows[0];
344
+ return {
345
+ repoId,
346
+ diagnostics: [handlerMethodsNotIndexedDiagnostic(first)],
347
+ };
348
+ }
349
+
350
+ function handlerMethodScope(
351
+ rows: HandlerSelectorRow[],
352
+ fallbackRepoId: number | undefined,
353
+ requested: string,
354
+ ): SelectorSourceScope {
355
+ const ambiguity = handlerSelectorAmbiguity(rows, requested, 'method');
356
+ return ambiguity
357
+ ? { diagnostics: [ambiguity] }
358
+ : executableScope(rows, fallbackRepoId);
359
+ }
360
+
361
+ function handlerSelectorAmbiguity(
362
+ rows: HandlerSelectorRow[],
363
+ requested: string,
364
+ matchKind: 'class' | 'method',
365
+ ): Record<string, unknown> | undefined {
366
+ const candidates = handlerSelectorCandidates(rows, matchKind);
367
+ if (candidates.length < 2) return undefined;
368
+ const repoCounts = new Map<string, number>();
369
+ for (const candidate of candidates) {
370
+ if (typeof candidate.repoName !== 'string') continue;
371
+ repoCounts.set(
372
+ candidate.repoName,
373
+ (repoCounts.get(candidate.repoName) ?? 0) + 1,
374
+ );
375
+ }
376
+ const suggestions = candidates.flatMap((candidate) => {
377
+ const repoName = typeof candidate.repoName === 'string'
378
+ ? candidate.repoName
379
+ : undefined;
380
+ if (repoName && repoCounts.get(repoName) === 1)
381
+ return [`--repo ${repoName} --handler ${requested}`];
382
+ if (matchKind === 'method' && typeof candidate.className === 'string')
383
+ return [`${repoName ? `--repo ${repoName} ` : ''}--handler ${candidate.className}`];
384
+ return [];
385
+ });
386
+ const projection = boundedSelectorCandidates(candidates);
387
+ const suggestionProjection = boundedSelectorSuggestions(suggestions);
388
+ return {
389
+ severity: 'warning',
390
+ code: 'trace_start_ambiguous',
391
+ message: 'Handler selector matched multiple executable scopes and was not selected automatically',
392
+ selectorKind: 'handler',
393
+ requestedHandler: requested,
394
+ resolutionStage: 'handler',
395
+ resolutionStatus: 'ambiguous_handler',
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,
404
+ remediation: suggestions.length > 0
405
+ ? 'Use one of the scoped handler selectors shown.'
406
+ : 'No current CLI selector uniquely identifies these duplicate handler classes.',
407
+ };
408
+ }
409
+
410
+ function handlerSelectorCandidates(
411
+ rows: HandlerSelectorRow[],
412
+ matchKind: 'class' | 'method',
413
+ ): Array<Record<string, unknown>> {
414
+ const candidates = new Map<string, Record<string, unknown>>();
415
+ for (const row of rows) {
416
+ const identity = matchKind === 'class'
417
+ ? `class:${String(row.handlerClassId)}`
418
+ : `method:${String(row.repoId)}:${String(row.symbolId ?? row.methodId)}`;
419
+ candidates.set(identity, {
420
+ handlerClassId: row.handlerClassId,
421
+ repoId: row.repoId,
422
+ repoName: row.repoName,
423
+ className: row.className,
424
+ sourceFile: row.sourceFile,
425
+ sourceLine: row.sourceLine,
426
+ matchKind,
427
+ });
428
+ }
429
+ return [...candidates.values()].sort((left, right) =>
430
+ String(left.repoName ?? '').localeCompare(String(right.repoName ?? ''))
431
+ || String(left.className ?? '').localeCompare(String(right.className ?? ''))
432
+ || String(left.sourceFile ?? '').localeCompare(String(right.sourceFile ?? '')));
433
+ }
434
+
435
+ function executableScope(
436
+ rows: HandlerSelectorRow[],
437
+ fallbackRepoId: number | undefined,
438
+ ): SelectorSourceScope {
439
+ const files = rows.flatMap((row) => row.sourceFile ? [row.sourceFile] : []);
440
+ const symbols = rows.flatMap((row) => typeof row.symbolId === 'number'
441
+ ? [row.symbolId] : []);
442
+ if (files.length === 0 || symbols.length === 0) return { repoId: fallbackRepoId };
443
+ return {
444
+ files: new Set(files),
445
+ symbols: new Set(symbols),
446
+ repoId: numericValue(rows[0]?.repoId) ?? fallbackRepoId,
447
+ };
448
+ }
449
+
450
+ function handlerMethodsNotIndexedDiagnostic(
451
+ row: HandlerSelectorRow | undefined,
452
+ ): Record<string, unknown> {
453
+ return {
454
+ severity: 'warning',
455
+ code: 'handler_methods_not_indexed',
456
+ message: `Handler class ${row?.className ?? 'unknown'} has no indexed executable methods`,
457
+ selectorKind: 'handler',
458
+ className: row?.className,
459
+ sourceFile: row?.sourceFile,
460
+ sourceLine: row?.sourceLine,
461
+ observedDecoratorNames: stringEvidenceArray(
462
+ row?.classEvidence, 'observedDecoratorNames',
463
+ ),
464
+ unsupportedDecoratorNames: stringEvidenceArray(
465
+ row?.classEvidence, 'unsupportedDecoratorNames',
466
+ ),
467
+ remediation: 'Use a supported CAP handler decorator on at least one class method and re-index the workspace.',
468
+ };
469
+ }
470
+
471
+ function handlerDecoratorsNotIndexedDiagnostic(
472
+ row: HandlerSelectorRow | undefined,
473
+ ): Record<string, unknown> | undefined {
474
+ const names = stringEvidenceArray(
475
+ row?.classEvidence, 'unsupportedDecoratorNames',
476
+ );
477
+ const methods = arrayEvidence(row?.classEvidence, 'unsupportedMethods');
478
+ if (names.length === 0 && methods.length === 0) return undefined;
479
+ return {
480
+ severity: 'warning',
481
+ code: 'handler_decorators_not_indexed',
482
+ message: `Handler class ${row?.className ?? 'unknown'} contains methods that were not indexed`,
483
+ selectorKind: 'handler',
484
+ className: row?.className,
485
+ sourceFile: row?.sourceFile,
486
+ sourceLine: row?.sourceLine,
487
+ unsupportedDecoratorNames: names,
488
+ unsupportedMethods: methods,
489
+ remediation: 'Use a supported CAP handler decorator shape and re-index the workspace.',
490
+ };
491
+ }
492
+
493
+ function evidenceRecord(
494
+ evidenceJson: string | null | undefined,
495
+ ): Record<string, unknown> {
496
+ if (!evidenceJson) return {};
497
+ try {
498
+ const parsed = JSON.parse(evidenceJson) as unknown;
499
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
500
+ ? parsed as Record<string, unknown>
501
+ : {};
502
+ } catch {
503
+ return {};
504
+ }
505
+ }
506
+
507
+ function stringEvidenceArray(
508
+ evidenceJson: string | null | undefined,
509
+ key: string,
510
+ ): string[] {
511
+ const value = evidenceRecord(evidenceJson)[key];
512
+ return Array.isArray(value)
513
+ ? [...new Set(value.filter((item): item is string =>
514
+ typeof item === 'string'))].sort()
515
+ : [];
516
+ }
517
+
518
+ function arrayEvidence(
519
+ evidenceJson: string | null | undefined,
520
+ key: string,
521
+ ): Array<Record<string, unknown>> {
522
+ const value = evidenceRecord(evidenceJson)[key];
523
+ return Array.isArray(value)
524
+ ? value.filter((item): item is Record<string, unknown> =>
525
+ Boolean(item && typeof item === 'object' && !Array.isArray(item)))
526
+ : [];
527
+ }
528
+
529
+ function numericValue(value: number | null | undefined): number | undefined {
530
+ return typeof value === 'number' ? value : undefined;
531
+ }
532
+
533
+ function normalizeOperation(value: string | undefined): string | undefined {
534
+ if (!value) return undefined;
535
+ return value.startsWith('/') ? value.slice(1) : value;
536
+ }
21
537
  export function ambiguousStartDiagnostic(
22
538
  requested: string,
23
539
  candidates: Array<Record<string, unknown>>,
@@ -27,6 +543,9 @@ export function ambiguousStartDiagnostic(
27
543
  .flatMap((row) => typeof row.servicePath === 'string'
28
544
  ? [`--service ${row.servicePath}`]
29
545
  : []))].sort();
546
+ const projection = boundedSelectorCandidates(candidates);
547
+ const serviceProjection = boundedSelectorSuggestions(serviceSuggestions);
548
+ const selectorProjection = boundedSelectorSuggestions(fullSelectorSuggestions(candidates));
30
549
  return {
31
550
  severity: 'warning',
32
551
  code: 'trace_start_ambiguous',
@@ -34,11 +553,40 @@ export function ambiguousStartDiagnostic(
34
553
  normalizedSelectorValue: requested,
35
554
  resolutionStage: 'operation',
36
555
  resolutionStatus: 'ambiguous_operation',
37
- candidates,
38
- serviceSuggestions,
39
- 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,
40
568
  };
41
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
+ }
42
590
  function fullSelectorSuggestions(
43
591
  candidates: Array<Record<string, unknown>>,
44
592
  ): string[] {