@saptools/service-flow 0.1.50 → 0.1.52

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