@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.
- package/CHANGELOG.md +14 -0
- package/README.md +16 -3
- package/TECHNICAL-NOTE.md +10 -1
- package/dist/{chunk-52OUS3MO.js → chunk-PTLDSHRC.js} +2663 -363
- package/dist/chunk-PTLDSHRC.js.map +1 -0
- package/dist/cli.js +318 -510
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +59 -17
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli.ts +97 -57
- package/src/db/connection.ts +1 -1
- package/src/db/migrations.ts +5 -3
- package/src/db/repositories.ts +120 -22
- package/src/db/schema.ts +7 -2
- package/src/index.ts +1 -1
- package/src/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/cross-repo-linker.ts +22 -2
- package/src/linker/service-resolver.ts +15 -4
- package/src/output/table-output.ts +56 -3
- package/src/parsers/cds-parser.ts +8 -2
- package/src/parsers/decorator-parser.ts +382 -48
- package/src/parsers/handler-registration-parser.ts +38 -21
- package/src/parsers/imported-wrapper-parser.ts +18 -5
- package/src/parsers/outbound-call-parser.ts +25 -8
- package/src/parsers/package-json-parser.ts +36 -11
- package/src/parsers/service-binding-parser-helpers.ts +8 -1
- package/src/parsers/service-binding-parser.ts +6 -3
- package/src/parsers/symbol-parser.ts +13 -3
- package/src/parsers/ts-project.ts +54 -0
- package/src/trace/000-dynamic-target-types.ts +84 -0
- package/src/trace/001-dynamic-identity.ts +280 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +82 -0
- package/src/trace/dynamic-branches.ts +45 -0
- package/src/trace/dynamic-targets.ts +654 -0
- package/src/trace/evidence.ts +391 -22
- package/src/trace/selectors.ts +483 -0
- package/src/trace/trace-engine.ts +101 -94
- package/src/types.ts +39 -1
- package/dist/chunk-52OUS3MO.js.map +0 -1
package/src/trace/selectors.ts
CHANGED
|
@@ -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>>,
|