@saptools/service-flow 0.1.51 → 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 (40) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +10 -5
  3. package/TECHNICAL-NOTE.md +8 -4
  4. package/dist/{chunk-YZJKE5UX.js → chunk-PTLDSHRC.js} +2412 -553
  5. package/dist/chunk-PTLDSHRC.js.map +1 -0
  6. package/dist/cli.js +280 -506
  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.ts +75 -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/indexer/repository-indexer.ts +57 -29
  18. package/src/indexer/workspace-indexer.ts +84 -6
  19. package/src/linker/cross-repo-linker.ts +13 -1
  20. package/src/output/table-output.ts +29 -3
  21. package/src/parsers/cds-parser.ts +8 -2
  22. package/src/parsers/decorator-parser.ts +382 -48
  23. package/src/parsers/handler-registration-parser.ts +38 -21
  24. package/src/parsers/imported-wrapper-parser.ts +18 -5
  25. package/src/parsers/outbound-call-parser.ts +25 -8
  26. package/src/parsers/package-json-parser.ts +36 -11
  27. package/src/parsers/service-binding-parser-helpers.ts +8 -1
  28. package/src/parsers/service-binding-parser.ts +6 -3
  29. package/src/parsers/symbol-parser.ts +13 -3
  30. package/src/parsers/ts-project.ts +54 -0
  31. package/src/trace/000-dynamic-target-types.ts +84 -0
  32. package/src/trace/001-dynamic-identity.ts +280 -0
  33. package/src/trace/002-trace-diagnostics.ts +54 -0
  34. package/src/trace/003-dynamic-references.ts +82 -0
  35. package/src/trace/dynamic-targets.ts +459 -229
  36. package/src/trace/evidence.ts +305 -46
  37. package/src/trace/selectors.ts +483 -0
  38. package/src/trace/trace-engine.ts +90 -83
  39. package/src/types.ts +27 -1
  40. package/dist/chunk-YZJKE5UX.js.map +0 -1
@@ -1,5 +1,468 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/db/repositories.ts
4
+ function upsertWorkspace(db, rootPath, dbPath) {
5
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6
+ db.prepare(
7
+ "INSERT INTO workspaces(root_path,db_path,created_at,updated_at) VALUES(?,?,?,?) ON CONFLICT(root_path) DO UPDATE SET db_path=excluded.db_path,updated_at=excluded.updated_at"
8
+ ).run(rootPath, dbPath, now, now);
9
+ return Number(
10
+ db.prepare("SELECT id FROM workspaces WHERE root_path=?").get(rootPath)?.id
11
+ );
12
+ }
13
+ function getWorkspace(db, rootPath) {
14
+ return db.prepare("SELECT * FROM workspaces WHERE root_path=?").get(rootPath);
15
+ }
16
+ function upsertRepository(db, workspaceId, r) {
17
+ db.prepare(
18
+ `INSERT INTO repositories(workspace_id,name,absolute_path,relative_path,package_name,package_version,dependencies_json,kind,is_git_repo) VALUES(?,?,?,?,?,?,?,?,?) ON CONFLICT(workspace_id,absolute_path) DO UPDATE SET name=excluded.name,relative_path=excluded.relative_path,package_name=excluded.package_name,package_version=excluded.package_version,dependencies_json=excluded.dependencies_json,kind=excluded.kind`
19
+ ).run(
20
+ workspaceId,
21
+ r.name,
22
+ r.absolutePath,
23
+ r.relativePath,
24
+ r.packageName,
25
+ r.packageVersion,
26
+ JSON.stringify(r.dependencies ?? {}),
27
+ r.kind ?? "unknown",
28
+ r.isGitRepo ? 1 : 0
29
+ );
30
+ return Number(
31
+ db.prepare(
32
+ "SELECT id FROM repositories WHERE workspace_id=? AND absolute_path=?"
33
+ ).get(workspaceId, r.absolutePath)?.id
34
+ );
35
+ }
36
+ function listRepositories(db, workspaceId) {
37
+ return db.prepare("SELECT * FROM repositories WHERE (? IS NULL OR workspace_id=?) ORDER BY name,absolute_path,id").all(workspaceId, workspaceId);
38
+ }
39
+ function reposByName(db, name, workspaceId) {
40
+ return db.prepare(`SELECT * FROM repositories
41
+ WHERE (? IS NULL OR workspace_id=?) AND (name=? OR package_name=?)
42
+ ORDER BY name,absolute_path,id`).all(workspaceId, workspaceId, name, name);
43
+ }
44
+ function clearRepoFacts(db, repoId) {
45
+ for (const t of [
46
+ "cds_requires",
47
+ "cds_services",
48
+ "handler_classes",
49
+ "outbound_calls",
50
+ "symbol_calls",
51
+ "handler_registrations",
52
+ "service_bindings",
53
+ "symbols",
54
+ "diagnostics",
55
+ "files"
56
+ ])
57
+ db.prepare(`DELETE FROM ${t} WHERE repo_id=?`).run(repoId);
58
+ db.prepare("DELETE FROM search_index WHERE repo=?").run(String(repoId));
59
+ }
60
+ function insertRequires(db, repoId, rows2) {
61
+ const stmt = db.prepare(
62
+ "INSERT INTO cds_requires(repo_id,alias,kind,model,destination,service_path,request_timeout,raw_json) VALUES(?,?,?,?,?,?,?,?)"
63
+ );
64
+ for (const r of rows2)
65
+ stmt.run(
66
+ repoId,
67
+ r.alias,
68
+ r.kind,
69
+ r.model,
70
+ r.destination,
71
+ r.servicePath,
72
+ r.requestTimeout,
73
+ r.rawJson
74
+ );
75
+ }
76
+ function insertService(db, repoId, s) {
77
+ const id = Number(
78
+ db.prepare(
79
+ "INSERT INTO cds_services(repo_id,namespace,service_name,qualified_name,service_path,is_extend,source_file,source_line,extension_local_ref,extension_imported_symbol,extension_local_alias,extension_module_specifier,extension_import_kind) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?) RETURNING id"
80
+ ).get(
81
+ repoId,
82
+ s.namespace,
83
+ s.serviceName,
84
+ s.qualifiedName,
85
+ s.servicePath,
86
+ s.isExtend ? 1 : 0,
87
+ s.sourceFile,
88
+ s.sourceLine,
89
+ s.extension?.localReference,
90
+ s.extension?.importedSymbol,
91
+ s.extension?.localAlias,
92
+ s.extension?.moduleSpecifier,
93
+ s.extension?.importKind
94
+ )?.id
95
+ );
96
+ const stmt = db.prepare(
97
+ "INSERT INTO cds_operations(service_id,operation_type,operation_name,operation_path,params_json,return_type,source_file,source_line,provenance,base_operation_id) VALUES(?,?,?,?,?,?,?,?,?,?)"
98
+ );
99
+ db.prepare(
100
+ "INSERT INTO search_index(kind,name,path,repo) VALUES(?,?,?,?)"
101
+ ).run("service", s.qualifiedName, s.servicePath, String(repoId));
102
+ for (const o of s.operations)
103
+ stmt.run(
104
+ id,
105
+ o.operationType,
106
+ o.operationName,
107
+ o.operationPath,
108
+ o.paramsJson,
109
+ o.returnType,
110
+ o.sourceFile,
111
+ o.sourceLine,
112
+ o.provenance ?? "direct",
113
+ o.baseOperationId ?? null
114
+ );
115
+ const search = db.prepare(
116
+ "INSERT INTO search_index(kind,name,path,repo) VALUES(?,?,?,?)"
117
+ );
118
+ for (const o of s.operations)
119
+ search.run("operation", o.operationName, o.operationPath, String(repoId));
120
+ return id;
121
+ }
122
+ function insertHandler(db, repoId, h) {
123
+ const sid = insertHandlerClassSymbol(db, repoId, h);
124
+ const hid = Number(
125
+ db.prepare(
126
+ "INSERT INTO handler_classes(repo_id,symbol_id,class_name,source_file,source_line) VALUES(?,?,?,?,?) RETURNING id"
127
+ ).get(repoId, sid, h.className, h.sourceFile, h.sourceLine)?.id
128
+ );
129
+ const stmt = db.prepare(
130
+ "INSERT INTO handler_methods(handler_class_id,method_name,decorator_kind,decorator_value,decorator_raw_expression,decorator_resolution_json,source_file,source_line) VALUES(?,?,?,?,?,?,?,?)"
131
+ );
132
+ for (const m of h.methods)
133
+ stmt.run(
134
+ hid,
135
+ m.methodName,
136
+ m.decoratorKind,
137
+ m.decoratorValue,
138
+ m.decoratorRawExpression,
139
+ JSON.stringify(canonicalHandlerMethodResolution(m)),
140
+ m.sourceFile,
141
+ m.sourceLine
142
+ );
143
+ insertHandlerIndexDiagnostic(db, repoId, h);
144
+ return hid;
145
+ }
146
+ function insertHandlerClassSymbol(db, repoId, h) {
147
+ const classEvidence = {
148
+ hasHandlerDecorator: h.hasHandlerDecorator ?? false,
149
+ classDecoratorNames: h.classDecoratorNames ?? [],
150
+ observedDecoratorNames: h.observedDecoratorNames ?? [],
151
+ unsupportedDecoratorNames: h.unsupportedDecoratorNames ?? [],
152
+ unsupportedMethods: h.methods.filter((method) => !handlerMethodIsExecutable(method)).map((method) => ({
153
+ methodName: method.methodName,
154
+ decoratorKind: method.decoratorKind,
155
+ sourceFile: method.sourceFile,
156
+ sourceLine: method.sourceLine,
157
+ reason: method.decoratorResolution.unresolvedReason
158
+ }))
159
+ };
160
+ return Number(
161
+ db.prepare(
162
+ "INSERT INTO symbols(repo_id,kind,name,qualified_name,exported,start_line,end_line,source_file,evidence_json) VALUES(?,?,?,?,?,?,?,?,?) RETURNING id"
163
+ ).get(
164
+ repoId,
165
+ "class",
166
+ h.className,
167
+ h.className,
168
+ 1,
169
+ h.sourceLine,
170
+ h.sourceLine,
171
+ h.sourceFile,
172
+ JSON.stringify(classEvidence)
173
+ )?.id
174
+ );
175
+ }
176
+ function insertHandlerIndexDiagnostic(db, repoId, h) {
177
+ if (!h.hasHandlerDecorator) return;
178
+ const hasExecutable = h.methods.some(handlerMethodIsExecutable);
179
+ const unsupported = h.methods.filter((method) => !handlerMethodIsExecutable(method));
180
+ if (hasExecutable && unsupported.length === 0) return;
181
+ const code = hasExecutable ? "handler_decorators_not_indexed" : "handler_methods_not_indexed";
182
+ const names = unsupported.map((method) => method.decoratorKind).sort();
183
+ const detail = names.length > 0 ? ` Unsupported decorators: ${[...new Set(names)].join(", ")}.` : "";
184
+ db.prepare(
185
+ "INSERT INTO diagnostics(repo_id,severity,code,message,source_file,source_line) VALUES(?,?,?,?,?,?)"
186
+ ).run(
187
+ repoId,
188
+ "warning",
189
+ code,
190
+ hasExecutable ? `Handler class ${h.className} contains methods that were not indexed.${detail}` : `Handler class ${h.className} has no indexed executable methods; use a supported CAP handler decorator and re-index.${detail}`,
191
+ h.sourceFile,
192
+ h.sourceLine
193
+ );
194
+ }
195
+ function canonicalHandlerMethodResolution(method) {
196
+ const handlerKind = method.handlerKind ?? method.decoratorResolution.handlerKind ?? legacyHandlerKind(method.decoratorKind);
197
+ const executable = method.executable ?? method.decoratorResolution.executable ?? (handlerKind === "operation" || handlerKind === "event" || handlerKind === "entity_lifecycle");
198
+ return {
199
+ ...method.decoratorResolution,
200
+ handlerKind,
201
+ executable,
202
+ lifecyclePhase: method.lifecyclePhase ?? method.decoratorResolution.lifecyclePhase,
203
+ lifecycleEvent: method.lifecycleEvent ?? method.decoratorResolution.lifecycleEvent
204
+ };
205
+ }
206
+ function handlerMethodIsExecutable(method) {
207
+ return canonicalHandlerMethodResolution(method).executable === true;
208
+ }
209
+ function legacyHandlerKind(kind) {
210
+ if (kind === "Event") return "event";
211
+ if (["Action", "Func", "On"].includes(kind)) return "operation";
212
+ return "unsupported_decorator";
213
+ }
214
+ function insertRegistrations(db, repoId, rows2) {
215
+ const stmt = db.prepare(
216
+ "INSERT INTO handler_registrations(repo_id,handler_class_id,class_name,import_source,registration_file,registration_line,registration_kind,confidence) VALUES(?,?,?,?,?,?,?,?)"
217
+ );
218
+ for (const r of rows2) {
219
+ const handlerClass = r.className ? db.prepare(
220
+ "SELECT id FROM handler_classes WHERE repo_id=? AND class_name=? ORDER BY id"
221
+ ).all(repoId, r.className) : [];
222
+ stmt.run(
223
+ repoId,
224
+ handlerClass.length === 1 ? handlerClass[0]?.id : null,
225
+ r.className,
226
+ r.importSource,
227
+ r.registrationFile,
228
+ r.registrationLine,
229
+ r.registrationKind,
230
+ r.confidence
231
+ );
232
+ }
233
+ }
234
+ function insertBindings(db, repoId, rows2) {
235
+ const stmt = db.prepare(
236
+ "INSERT INTO service_bindings(repo_id,symbol_id,variable_name,alias,alias_expr,destination_expr,service_path_expr,is_dynamic,placeholders_json,source_file,source_line,helper_chain_json) VALUES(?,(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1),?,?,?,?,?,?,?,?,?,?)"
237
+ );
238
+ for (const r of rows2)
239
+ stmt.run(
240
+ repoId,
241
+ repoId,
242
+ r.sourceFile,
243
+ r.sourceLine,
244
+ r.sourceLine,
245
+ r.variableName,
246
+ r.alias,
247
+ r.aliasExpr,
248
+ r.destinationExpr,
249
+ r.servicePathExpr,
250
+ r.isDynamic ? 1 : 0,
251
+ JSON.stringify(r.placeholders),
252
+ r.sourceFile,
253
+ r.sourceLine,
254
+ r.helperChain ? JSON.stringify(r.helperChain) : null
255
+ );
256
+ }
257
+ function insertExecutableSymbols(db, repoId, rows2) {
258
+ const stmt = db.prepare("INSERT INTO symbols(repo_id,file_id,kind,name,qualified_name,exported,start_line,end_line,start_offset,end_offset,source_file,exported_name,evidence_json) VALUES(?,(SELECT id FROM files WHERE repo_id=? AND relative_path=?),?,?,?,?,?,?,?,?,?,?,?)");
259
+ for (const r of rows2) stmt.run(repoId, repoId, r.sourceFile, r.kind, r.localName, r.qualifiedName, r.exported ? 1 : 0, r.startLine, r.endLine, r.startOffset, r.endOffset, r.sourceFile, r.exportedName, r.importExportEvidence ? JSON.stringify(r.importExportEvidence) : null);
260
+ }
261
+ function insertSymbolCalls(db, repoId, rows2) {
262
+ const callerStmt = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1");
263
+ const insertStmt = db.prepare("INSERT INTO symbol_calls(repo_id,caller_symbol_id,callee_symbol_id,callee_expression,import_source,source_file,source_line,status,confidence,evidence_json,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)");
264
+ for (const r of rows2) {
265
+ const caller = callerStmt.get(repoId, r.sourceFile, r.callerQualifiedName);
266
+ const target = resolveSymbolCallTarget(db, repoId, r);
267
+ insertStmt.run(repoId, caller?.id, target.id, r.calleeExpression, r.importSource, r.sourceFile, r.sourceLine, target.status, 0.8, JSON.stringify({ ...r.evidence, candidateStrategy: target.strategy, candidateCount: target.candidateCount }), target.reason);
268
+ }
269
+ }
270
+ function isRelativeImportedSymbolCall(r) {
271
+ return Boolean(r.importSource?.startsWith("."));
272
+ }
273
+ function resolveSymbolCallTarget(db, repoId, r) {
274
+ const evidence = r.evidence;
275
+ const localRows = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND (name=? OR qualified_name=?) ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName);
276
+ if (localRows.length === 1) return { id: localRows[0]?.id ?? null, status: "resolved", reason: null, strategy: "same_file_exact", candidateCount: 1 };
277
+ if (localRows.length > 1) return { id: null, status: "ambiguous", reason: "Multiple same-file symbol targets matched exactly", strategy: "same_file_exact", candidateCount: localRows.length };
278
+ if (evidence.relation === "class_instance_method" && isRelativeImportedSymbolCall(r)) {
279
+ const classRows = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file<>? AND qualified_name=? ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName);
280
+ if (classRows.length === 1) return { id: classRows[0]?.id ?? null, status: "resolved", reason: null, strategy: "relative_import_class_instance_method", candidateCount: 1 };
281
+ if (classRows.length > 1) return { id: null, status: "ambiguous", reason: "Multiple relative class instance method targets matched exactly", strategy: "relative_import_class_instance_method", candidateCount: classRows.length };
282
+ }
283
+ const rows2 = db.prepare("SELECT id,kind,evidence_json evidenceJson FROM symbols WHERE repo_id=? AND source_file<>? AND exported=1 AND (exported_name=? OR name=? OR qualified_name=?) ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName);
284
+ if (evidence.relation === "relative_import_proxy_member" && rows2.length > 1) {
285
+ const objectMapRows = rows2.filter((row) => String(row.evidenceJson ?? "").includes("exported_object_shorthand") || String(row.evidenceJson ?? "").includes("exported_object_literal"));
286
+ if (objectMapRows.length > 0) {
287
+ const concrete = rows2.find((row) => row.kind !== "object_alias") ?? objectMapRows[0];
288
+ return { id: concrete?.id ?? null, status: "resolved", reason: null, strategy: "proxy_member_exported_object_map", candidateCount: rows2.length };
289
+ }
290
+ return { id: null, status: "ambiguous", reason: "Proxy member target requires explicit factory/module/type evidence; global member name is ambiguous", strategy: "proxy_member_no_global_name_fallback", candidateCount: rows2.length };
291
+ }
292
+ if (rows2.length === 1) return { id: rows2[0]?.id ?? null, status: "resolved", reason: null, strategy: evidence.relation === "relative_import_proxy_member" ? "proxy_member_unique_exported_candidate" : "relative_import_exported_exact", candidateCount: 1 };
293
+ if (rows2.length > 1) return { id: null, status: "ambiguous", reason: "Multiple exported symbol targets matched exactly", strategy: "exported_exact", candidateCount: rows2.length };
294
+ return { id: null, status: "unresolved", reason: "No local symbol target matched exactly", strategy: evidence.relation === "relative_import_proxy_member" ? "proxy_member_no_global_name_fallback" : "exact_symbol_match", candidateCount: 0 };
295
+ }
296
+ function insertCalls(db, repoId, rows2) {
297
+ const stmt = db.prepare(
298
+ "INSERT INTO outbound_calls(repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,event_name_expr,payload_summary,source_file,source_line,confidence,unresolved_reason,local_service_name,local_service_lookup,alias_chain_json,evidence_json,external_target_kind,external_target_id,external_target_label,external_target_dynamic,service_binding_id) VALUES(?,COALESCE((SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1),(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1)),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
299
+ );
300
+ for (const r of rows2) {
301
+ const binding = resolvePersistedBinding(db, repoId, r);
302
+ const evidence = {
303
+ ...r.evidence ?? {},
304
+ serviceBindingResolution: binding.evidence
305
+ };
306
+ stmt.run(
307
+ repoId,
308
+ repoId,
309
+ r.sourceFile,
310
+ r.sourceSymbolQualifiedName,
311
+ repoId,
312
+ r.sourceFile,
313
+ r.sourceLine,
314
+ r.sourceLine,
315
+ r.callType,
316
+ r.method,
317
+ r.operationPathExpr,
318
+ r.queryEntity,
319
+ r.eventNameExpr,
320
+ r.payloadSummary,
321
+ r.sourceFile,
322
+ r.sourceLine,
323
+ r.confidence,
324
+ r.unresolvedReason ?? binding.unresolvedReason,
325
+ r.localServiceName,
326
+ r.localServiceLookup,
327
+ r.aliasChain ? JSON.stringify(r.aliasChain) : null,
328
+ JSON.stringify(evidence),
329
+ r.externalTarget?.kind ?? null,
330
+ r.externalTarget?.stableId ?? null,
331
+ r.externalTarget?.label ?? null,
332
+ r.externalTarget?.dynamic ? 1 : 0,
333
+ binding.bindingId
334
+ );
335
+ }
336
+ }
337
+ function resolvePersistedBinding(db, repoId, call) {
338
+ if (!call.serviceVariableName)
339
+ return { bindingId: null, evidence: { status: "not_applicable", candidateCount: 0 } };
340
+ const candidates = bindingCandidates(db, repoId, call);
341
+ const prior = candidates.filter((candidate) => candidate.sourceLine <= call.sourceLine);
342
+ const families = new Set(prior.map(bindingSignature));
343
+ if (prior.length > 0 && families.size === 1) {
344
+ const selected = prior.at(-1);
345
+ return {
346
+ bindingId: selected?.id ?? null,
347
+ evidence: bindingEvidence("selected", prior, selected)
348
+ };
349
+ }
350
+ if (prior.length > 1) {
351
+ return {
352
+ bindingId: null,
353
+ unresolvedReason: "ambiguous_service_binding_candidates",
354
+ evidence: bindingEvidence("ambiguous", prior)
355
+ };
356
+ }
357
+ if (candidates.length > 0) {
358
+ return {
359
+ bindingId: null,
360
+ unresolvedReason: "service_binding_declared_after_call",
361
+ evidence: bindingEvidence("rejected_future_binding", candidates)
362
+ };
363
+ }
364
+ return {
365
+ bindingId: null,
366
+ evidence: bindingEvidence("unrecoverable", [])
367
+ };
368
+ }
369
+ function bindingCandidates(db, repoId, call) {
370
+ const ownerId = callSymbolId(db, repoId, call);
371
+ const rows2 = db.prepare(`
372
+ SELECT id,symbol_id symbolId,variable_name variableName,alias,alias_expr aliasExpr,
373
+ destination_expr destinationExpr,service_path_expr servicePathExpr,
374
+ source_file sourceFile,source_line sourceLine,helper_chain_json helperChainJson
375
+ FROM service_bindings
376
+ WHERE repo_id=? AND variable_name=? AND source_file=?
377
+ AND (? IS NULL OR symbol_id IS NULL OR symbol_id=?)
378
+ ORDER BY source_line,id
379
+ `).all(
380
+ repoId,
381
+ call.serviceVariableName,
382
+ call.sourceFile,
383
+ ownerId,
384
+ ownerId
385
+ );
386
+ return rows2.flatMap((row) => {
387
+ if (typeof row.id !== "number" || typeof row.variableName !== "string" || typeof row.sourceFile !== "string" || typeof row.sourceLine !== "number")
388
+ return [];
389
+ return [{
390
+ id: row.id,
391
+ symbolId: nullableNumber(row.symbolId),
392
+ variableName: row.variableName,
393
+ alias: nullableString(row.alias),
394
+ aliasExpr: nullableString(row.aliasExpr),
395
+ destinationExpr: nullableString(row.destinationExpr),
396
+ servicePathExpr: nullableString(row.servicePathExpr),
397
+ sourceFile: row.sourceFile,
398
+ sourceLine: row.sourceLine,
399
+ helperChainJson: nullableString(row.helperChainJson)
400
+ }];
401
+ });
402
+ }
403
+ function nullableString(value) {
404
+ return value === null || typeof value === "string" ? value : void 0;
405
+ }
406
+ function nullableNumber(value) {
407
+ return value === null || typeof value === "number" ? value : void 0;
408
+ }
409
+ function callSymbolId(db, repoId, call) {
410
+ const row = db.prepare(`
411
+ SELECT id FROM symbols
412
+ WHERE repo_id=? AND source_file=?
413
+ AND ((? IS NOT NULL AND qualified_name=?)
414
+ OR (start_line<=? AND end_line>=?))
415
+ ORDER BY CASE WHEN qualified_name=? THEN 0 ELSE 1 END,
416
+ (end_line-start_line),id
417
+ LIMIT 1
418
+ `).get(
419
+ repoId,
420
+ call.sourceFile,
421
+ call.sourceSymbolQualifiedName,
422
+ call.sourceSymbolQualifiedName,
423
+ call.sourceLine,
424
+ call.sourceLine,
425
+ call.sourceSymbolQualifiedName
426
+ );
427
+ return typeof row?.id === "number" ? row.id : void 0;
428
+ }
429
+ function bindingEvidence(status, candidates, selected) {
430
+ return {
431
+ status,
432
+ candidateCount: candidates.length,
433
+ selectedBindingId: selected?.id,
434
+ sourceOrderRule: "binding_source_line_must_not_follow_call",
435
+ candidates: candidates.map((candidate) => ({
436
+ bindingId: candidate.id,
437
+ symbolId: candidate.symbolId,
438
+ variableName: candidate.variableName,
439
+ alias: candidate.alias,
440
+ aliasExpr: candidate.aliasExpr,
441
+ destinationExpr: candidate.destinationExpr,
442
+ servicePathExpr: candidate.servicePathExpr,
443
+ sourceFile: candidate.sourceFile,
444
+ sourceLine: candidate.sourceLine,
445
+ helperChain: parseBindingChain(candidate.helperChainJson)
446
+ }))
447
+ };
448
+ }
449
+ function bindingSignature(candidate) {
450
+ return JSON.stringify([
451
+ candidate.alias,
452
+ candidate.aliasExpr,
453
+ candidate.destinationExpr,
454
+ candidate.servicePathExpr
455
+ ]);
456
+ }
457
+ function parseBindingChain(value) {
458
+ if (!value) return void 0;
459
+ try {
460
+ return JSON.parse(value);
461
+ } catch {
462
+ return void 0;
463
+ }
464
+ }
465
+
3
466
  // src/discovery/discover-repositories.ts
4
467
  import fs from "fs/promises";
5
468
  import path2 from "path";
@@ -74,6 +537,9 @@ async function discoverRepositories(rootPath, ignore) {
74
537
  // src/parsers/package-json-parser.ts
75
538
  import fs2 from "fs/promises";
76
539
  import path3 from "path";
540
+ function emptyPackageFacts() {
541
+ return { dependencies: {}, cdsRequires: [], scripts: {} };
542
+ }
77
543
  function recordOfString(value) {
78
544
  if (!value || typeof value !== "object") return {};
79
545
  return Object.fromEntries(
@@ -100,22 +566,31 @@ function readRequires(cds) {
100
566
  ];
101
567
  });
102
568
  }
103
- async function parsePackageJson(repoPath) {
569
+ async function parsePackageJson(repoPath, options = {}) {
570
+ return (await loadPackageJsonSnapshot(repoPath, options)).facts;
571
+ }
572
+ async function loadPackageJsonSnapshot(repoPath, options = {}) {
104
573
  try {
105
574
  const raw = await fs2.readFile(path3.join(repoPath, "package.json"), "utf8");
106
575
  const json = JSON.parse(raw);
107
576
  return {
108
- packageName: typeof json.name === "string" ? json.name : void 0,
109
- packageVersion: typeof json.version === "string" ? json.version : void 0,
110
- dependencies: {
111
- ...recordOfString(json.dependencies),
112
- ...recordOfString(json.devDependencies)
113
- },
114
- cdsRequires: readRequires(json.cds),
115
- scripts: recordOfString(json.scripts)
577
+ rawText: raw,
578
+ facts: {
579
+ packageName: typeof json.name === "string" ? json.name : void 0,
580
+ packageVersion: typeof json.version === "string" ? json.version : void 0,
581
+ dependencies: {
582
+ ...recordOfString(json.dependencies),
583
+ ...recordOfString(json.devDependencies)
584
+ },
585
+ cdsRequires: readRequires(json.cds),
586
+ scripts: recordOfString(json.scripts)
587
+ }
116
588
  };
117
- } catch {
118
- return { dependencies: {}, cdsRequires: [], scripts: {} };
589
+ } catch (error) {
590
+ const missing = typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
591
+ if (!options.strict || options.allowMissing && missing)
592
+ return { facts: emptyPackageFacts(), rawText: "" };
593
+ throw error;
119
594
  }
120
595
  }
121
596
 
@@ -259,9 +734,9 @@ function operationsFromBody(text, maskedBody, bodyOffset, filePath) {
259
734
  sourceLine: lineOf(text, bodyOffset + (m.index ?? 0))
260
735
  }));
261
736
  }
262
- async function parseCdsFile(repoPath, filePath) {
737
+ async function parseCdsFile(repoPath, filePath, context) {
263
738
  const absolute = path4.join(repoPath, filePath);
264
- const text = await fs3.readFile(absolute, "utf8");
739
+ const text = context?.get(filePath)?.text ?? await fs3.readFile(absolute, "utf8");
265
740
  const masked = maskCommentsAndStrings(text);
266
741
  const namespace = /namespace\s+([\w.]+)\s*;/.exec(masked)?.[1];
267
742
  const services = [];
@@ -311,8 +786,8 @@ async function parseCdsFile(repoPath, filePath) {
311
786
  }
312
787
 
313
788
  // src/parsers/decorator-parser.ts
314
- import fs4 from "fs/promises";
315
- import path5 from "path";
789
+ import fs5 from "fs/promises";
790
+ import path6 from "path";
316
791
  import ts2 from "typescript";
317
792
 
318
793
  // src/linker/operation-decorator-normalizer.ts
@@ -356,7 +831,34 @@ function normalizeDecoratorOperationSignal(value, raw, candidateOperation) {
356
831
  }
357
832
 
358
833
  // src/parsers/ts-project.ts
834
+ import fs4 from "fs/promises";
835
+ import path5 from "path";
359
836
  import ts from "typescript";
837
+ async function loadRepositorySourceContext(repoPath, filePaths, instrumentation) {
838
+ const snapshots = /* @__PURE__ */ new Map();
839
+ for (const inputPath of filePaths) {
840
+ const filePath = normalizePath(inputPath);
841
+ await instrumentation?.onSourceRead?.(repoPath, filePath);
842
+ const text = await fs4.readFile(path5.join(repoPath, filePath), "utf8");
843
+ let ast;
844
+ snapshots.set(filePath, {
845
+ repoPath,
846
+ filePath,
847
+ text,
848
+ sizeBytes: Buffer.byteLength(text),
849
+ sourceFile: () => {
850
+ if (ast) return ast;
851
+ instrumentation?.onAstCreated?.(repoPath, filePath);
852
+ ast = createSourceFile(filePath, text);
853
+ return ast;
854
+ }
855
+ });
856
+ }
857
+ return {
858
+ get: (filePath) => snapshots.get(normalizePath(filePath)),
859
+ entries: () => [...snapshots.values()]
860
+ };
861
+ }
360
862
  function createSourceFile(filePath, text) {
361
863
  return ts.createSourceFile(
362
864
  filePath,
@@ -368,6 +870,22 @@ function createSourceFile(filePath, text) {
368
870
  }
369
871
 
370
872
  // src/parsers/decorator-parser.ts
873
+ var OPERATION_DECORATORS = /* @__PURE__ */ new Set(["Action", "Func", "On"]);
874
+ var EVENT_DECORATORS = /* @__PURE__ */ new Set(["Event"]);
875
+ var LIFECYCLE_DECORATORS = /* @__PURE__ */ new Map([
876
+ ["BeforeCreate", { phase: "before", event: "CREATE" }],
877
+ ["OnCreate", { phase: "on", event: "CREATE" }],
878
+ ["AfterCreate", { phase: "after", event: "CREATE" }],
879
+ ["BeforeRead", { phase: "before", event: "READ" }],
880
+ ["OnRead", { phase: "on", event: "READ" }],
881
+ ["AfterRead", { phase: "after", event: "READ" }],
882
+ ["BeforeUpdate", { phase: "before", event: "UPDATE" }],
883
+ ["OnUpdate", { phase: "on", event: "UPDATE" }],
884
+ ["AfterUpdate", { phase: "after", event: "UPDATE" }],
885
+ ["BeforeDelete", { phase: "before", event: "DELETE" }],
886
+ ["OnDelete", { phase: "on", event: "DELETE" }],
887
+ ["AfterDelete", { phase: "after", event: "DELETE" }]
888
+ ]);
371
889
  function line(sf, pos) {
372
890
  return sf.getLineAndCharacterOfPosition(pos).line + 1;
373
891
  }
@@ -382,6 +900,12 @@ function firstArg(d) {
382
900
  const e = d.expression;
383
901
  return ts2.isCallExpression(e) ? e.arguments[0] : void 0;
384
902
  }
903
+ function decoratorArguments(d) {
904
+ return ts2.isCallExpression(d.expression) ? d.expression.arguments : void 0;
905
+ }
906
+ function methodName(name) {
907
+ return ts2.isIdentifier(name) || ts2.isStringLiteralLike(name) || ts2.isNumericLiteral(name) ? name.text : name.getText();
908
+ }
385
909
  function unwrapExpression(expression) {
386
910
  let current = expression;
387
911
  while (ts2.isParenthesizedExpression(current) || ts2.isAsExpression(current) || ts2.isTypeAssertionExpression(current) || ts2.isSatisfiesExpression(current)) current = current.expression;
@@ -419,9 +943,12 @@ function collectStringLookups(source) {
419
943
  const lookups = {
420
944
  identifiers: /* @__PURE__ */ new Map(),
421
945
  enumMembers: /* @__PURE__ */ new Map(),
422
- objectProperties: /* @__PURE__ */ new Map()
946
+ objectProperties: /* @__PURE__ */ new Map(),
947
+ capDecoratorNames: /* @__PURE__ */ new Map(),
948
+ capDecoratorNamespaces: /* @__PURE__ */ new Set()
423
949
  };
424
950
  for (const statement of source.statements) {
951
+ collectCapDecoratorImports(statement, lookups);
425
952
  if (ts2.isEnumDeclaration(statement)) collectEnumMembers(statement, lookups);
426
953
  if (!ts2.isVariableStatement(statement) || !(statement.declarationList.flags & ts2.NodeFlags.Const)) continue;
427
954
  for (const declaration of statement.declarationList.declarations) {
@@ -433,75 +960,277 @@ function collectStringLookups(source) {
433
960
  }
434
961
  return lookups;
435
962
  }
436
- function unresolved(rawExpression, reason) {
437
- return { rawExpression, resolutionKind: "unresolved", unresolvedReason: reason };
963
+ function collectCapDecoratorImports(statement, lookups) {
964
+ if (!ts2.isImportDeclaration(statement) || !ts2.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "cds-routing-handlers") return;
965
+ const clause = statement.importClause;
966
+ if (!clause || clause.isTypeOnly) return;
967
+ const bindings = clause.namedBindings;
968
+ if (bindings && ts2.isNamedImports(bindings)) {
969
+ for (const element of bindings.elements) {
970
+ if (element.isTypeOnly) continue;
971
+ lookups.capDecoratorNames.set(
972
+ element.name.text,
973
+ element.propertyName?.text ?? element.name.text
974
+ );
975
+ }
976
+ }
977
+ if (bindings && ts2.isNamespaceImport(bindings))
978
+ lookups.capDecoratorNamespaces.add(bindings.name.text);
979
+ }
980
+ function capDecoratorName(decorator, lookups) {
981
+ const expression = ts2.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
982
+ if (ts2.isIdentifier(expression))
983
+ return lookups.capDecoratorNames.get(expression.text);
984
+ if (ts2.isPropertyAccessExpression(expression) && ts2.isIdentifier(expression.expression) && lookups.capDecoratorNamespaces.has(expression.expression.text))
985
+ return expression.name.text;
986
+ return void 0;
987
+ }
988
+ function unresolved(rawExpression, reason, argumentExpression) {
989
+ return {
990
+ rawExpression,
991
+ argumentExpression,
992
+ resolutionKind: "unresolved",
993
+ unresolvedReason: reason
994
+ };
995
+ }
996
+ function resolved2(rawExpression, argumentExpression, resolvedValue, resolutionKind) {
997
+ return { rawExpression, argumentExpression, resolvedValue, resolutionKind };
438
998
  }
439
999
  function generatedConstant(rawExpression) {
440
1000
  const match = /(?:^|\.)(Action[A-Z][\w$]*|Func[A-Z][\w$]*)\.name$/.exec(rawExpression.trim());
441
1001
  return match?.[1] ? generatedOperationNameFromConstant(match[1]) : void 0;
442
1002
  }
443
- function resolveDecoratorArgument(argument, lookups) {
444
- if (!argument) return unresolved("", "decorator_argument_missing");
445
- const rawExpression = argument.getText();
1003
+ function resolveDecoratorArgument(argument, lookups, rawExpression) {
1004
+ if (!argument) return unresolved(rawExpression, "decorator_argument_missing");
1005
+ const argumentExpression = argument.getText();
446
1006
  const expression = unwrapExpression(argument);
447
1007
  if (ts2.isStringLiteralLike(expression))
448
- return { rawExpression, resolvedValue: expression.text, resolutionKind: "literal" };
1008
+ return resolved2(rawExpression, argumentExpression, expression.text, "literal");
449
1009
  if (ts2.isIdentifier(expression)) {
450
1010
  const value = lookups.identifiers.get(expression.text);
451
- return value === void 0 ? unresolved(rawExpression, "identifier_not_resolved_to_local_const_string") : { rawExpression, resolvedValue: value, resolutionKind: "const_identifier" };
1011
+ return value === void 0 ? unresolved(rawExpression, "identifier_not_resolved_to_local_const_string", argumentExpression) : resolved2(rawExpression, argumentExpression, value, "const_identifier");
452
1012
  }
453
1013
  if (ts2.isPropertyAccessExpression(expression) && ts2.isIdentifier(expression.expression)) {
454
1014
  const key = `${expression.expression.text}.${expression.name.text}`;
455
1015
  const enumValue = lookups.enumMembers.get(key);
456
1016
  if (enumValue !== void 0)
457
- return { rawExpression, resolvedValue: enumValue, resolutionKind: "enum_member" };
1017
+ return resolved2(rawExpression, argumentExpression, enumValue, "enum_member");
458
1018
  const objectValue2 = lookups.objectProperties.get(key);
459
1019
  if (objectValue2 !== void 0)
460
- return { rawExpression, resolvedValue: objectValue2, resolutionKind: "const_object_property" };
1020
+ return resolved2(rawExpression, argumentExpression, objectValue2, "const_object_property");
461
1021
  }
462
- const generatedValue = generatedConstant(rawExpression);
1022
+ const generatedValue = generatedConstant(argumentExpression);
463
1023
  if (generatedValue !== void 0)
464
- return {
1024
+ return resolved2(
465
1025
  rawExpression,
466
- resolvedValue: generatedValue,
467
- resolutionKind: "generated_constant_name"
468
- };
1026
+ argumentExpression,
1027
+ generatedValue,
1028
+ "generated_constant_name"
1029
+ );
469
1030
  if (ts2.isPropertyAccessExpression(expression))
470
- return unresolved(rawExpression, "property_access_not_resolved_to_local_string");
471
- return unresolved(rawExpression, "unsupported_decorator_expression");
1031
+ return unresolved(
1032
+ rawExpression,
1033
+ "property_access_not_resolved_to_local_string",
1034
+ argumentExpression
1035
+ );
1036
+ return unresolved(rawExpression, "unsupported_decorator_expression", argumentExpression);
1037
+ }
1038
+ function classificationFor(name) {
1039
+ if (OPERATION_DECORATORS.has(name))
1040
+ return { handlerKind: "operation", executable: true };
1041
+ if (EVENT_DECORATORS.has(name))
1042
+ return { handlerKind: "event", executable: true };
1043
+ const lifecycle = LIFECYCLE_DECORATORS.get(name);
1044
+ if (!lifecycle) return void 0;
1045
+ return {
1046
+ handlerKind: "entity_lifecycle",
1047
+ executable: true,
1048
+ lifecyclePhase: lifecycle.phase,
1049
+ lifecycleEvent: lifecycle.event
1050
+ };
1051
+ }
1052
+ function lifecycleLikePhase(name) {
1053
+ if (/^On[A-Z]/.test(name)) return "on";
1054
+ if (/^Before[A-Z]/.test(name)) return "before";
1055
+ if (/^After[A-Z]/.test(name)) return "after";
1056
+ return void 0;
1057
+ }
1058
+ function withClassification(resolution, classification) {
1059
+ return {
1060
+ ...resolution,
1061
+ handlerKind: classification.handlerKind,
1062
+ executable: classification.executable,
1063
+ lifecyclePhase: classification.lifecyclePhase,
1064
+ lifecycleEvent: classification.lifecycleEvent
1065
+ };
1066
+ }
1067
+ function withDecoratorEvidence(resolution, decorator, resolvedDecoratorKind) {
1068
+ return {
1069
+ ...resolution,
1070
+ decoratorExpression: decorator.expression.getText(),
1071
+ resolvedDecoratorKind,
1072
+ decoratorImportSource: resolvedDecoratorKind ? "cds-routing-handlers" : void 0
1073
+ };
1074
+ }
1075
+ function lifecycleDecoratorResolution(decorator, lookups, classification) {
1076
+ const rawExpression = decorator.expression.getText();
1077
+ const args = decoratorArguments(decorator);
1078
+ if (args?.length === 0)
1079
+ return {
1080
+ classification,
1081
+ resolution: withClassification({
1082
+ rawExpression,
1083
+ resolutionKind: "lifecycle_implicit"
1084
+ }, classification)
1085
+ };
1086
+ const resolution = args?.length === 1 ? resolveDecoratorArgument(args[0], lookups, rawExpression) : unresolved(rawExpression, args ? "unsupported_lifecycle_argument_count" : "lifecycle_decorator_call_required");
1087
+ const unsupported = { ...classification, handlerKind: "unsupported_lifecycle", executable: false };
1088
+ const unsupportedResolution = args?.length === 1 ? { ...resolution, unresolvedReason: "lifecycle_decorator_arguments_not_supported" } : resolution;
1089
+ return {
1090
+ classification: unsupported,
1091
+ resolution: withClassification(unsupportedResolution, unsupported)
1092
+ };
1093
+ }
1094
+ function unsupportedLifecycleResolution(decorator, phase) {
1095
+ const classification = {
1096
+ handlerKind: "unsupported_lifecycle",
1097
+ executable: false,
1098
+ lifecyclePhase: phase
1099
+ };
1100
+ const resolution = unresolved(
1101
+ decorator.expression.getText(),
1102
+ "lifecycle_decorator_not_allowlisted"
1103
+ );
1104
+ return { classification, resolution: withClassification(resolution, classification) };
1105
+ }
1106
+ function unsupportedDecoratorResolution(decorator, reason, phase) {
1107
+ const classification = {
1108
+ handlerKind: phase ? "unsupported_lifecycle" : "unsupported_decorator",
1109
+ executable: false,
1110
+ lifecyclePhase: phase
1111
+ };
1112
+ return {
1113
+ classification,
1114
+ resolution: withClassification(
1115
+ unresolved(decorator.expression.getText(), reason),
1116
+ classification
1117
+ )
1118
+ };
1119
+ }
1120
+ function parseMethodDecorator(decorator, lookups, handlerClass, allowLifecycle) {
1121
+ const decoratorKind = callName(decorator);
1122
+ const importedKind = capDecoratorName(decorator, lookups);
1123
+ const resolvedKind = importedKind ?? decoratorKind;
1124
+ const base = classificationFor(resolvedKind);
1125
+ const phase = lifecycleLikePhase(resolvedKind);
1126
+ const isLifecycle = base?.handlerKind === "entity_lifecycle" || Boolean(phase && !base);
1127
+ if (!handlerClass && isLifecycle) return void 0;
1128
+ const parsed = isLifecycle && (!allowLifecycle || !importedKind) ? unsupportedDecoratorResolution(
1129
+ decorator,
1130
+ "lifecycle_decorator_import_not_supported",
1131
+ phase
1132
+ ) : base?.handlerKind === "entity_lifecycle" ? lifecycleDecoratorResolution(decorator, lookups, base) : phase && !base ? unsupportedLifecycleResolution(decorator, phase) : !base && handlerClass ? unsupportedDecoratorResolution(decorator, "decorator_not_allowlisted") : {
1133
+ classification: base,
1134
+ resolution: resolveDecoratorArgument(
1135
+ firstArg(decorator),
1136
+ lookups,
1137
+ firstArg(decorator)?.getText() ?? decorator.expression.getText()
1138
+ )
1139
+ };
1140
+ if (!parsed.classification) return void 0;
1141
+ return {
1142
+ classification: parsed.classification,
1143
+ resolution: parsed.resolution,
1144
+ decoratorKind,
1145
+ importedKind
1146
+ };
1147
+ }
1148
+ function methodDecoratorFact(method, decorator, lookups, source, filePath, handlerClass, allowLifecycle) {
1149
+ const parsed = parseMethodDecorator(decorator, lookups, handlerClass, allowLifecycle);
1150
+ if (!parsed) return void 0;
1151
+ const classification = method.body ? parsed.classification : { ...parsed.classification, executable: false };
1152
+ const resolution = method.body ? parsed.resolution : { ...parsed.resolution, unresolvedReason: "handler_method_body_missing" };
1153
+ const argumentExpression = firstArg(decorator)?.getText();
1154
+ return {
1155
+ methodName: methodName(method.name),
1156
+ decoratorKind: parsed.decoratorKind,
1157
+ decoratorValue: parsed.resolution.resolvedValue,
1158
+ decoratorRawExpression: argumentExpression ?? decorator.expression.getText(),
1159
+ handlerKind: classification.handlerKind,
1160
+ executable: classification.executable,
1161
+ lifecyclePhase: classification.lifecyclePhase,
1162
+ lifecycleEvent: classification.lifecycleEvent,
1163
+ decoratorResolution: withDecoratorEvidence(
1164
+ withClassification(resolution, classification),
1165
+ decorator,
1166
+ parsed.importedKind
1167
+ ),
1168
+ sourceFile: normalizePath(filePath),
1169
+ sourceLine: line(source, method.getStart())
1170
+ };
1171
+ }
1172
+ function unique(values) {
1173
+ return [...new Set(values)];
1174
+ }
1175
+ function parseClassMethods(node, lookups, source, filePath, handlerClass, allowLifecycle) {
1176
+ const methods = [];
1177
+ const observed = [];
1178
+ const unsupported = [];
1179
+ for (const method of node.members.filter(ts2.isMethodDeclaration)) {
1180
+ for (const decorator of decs(method)) {
1181
+ observed.push(callName(decorator));
1182
+ const fact = methodDecoratorFact(
1183
+ method,
1184
+ decorator,
1185
+ lookups,
1186
+ source,
1187
+ filePath,
1188
+ handlerClass,
1189
+ allowLifecycle
1190
+ );
1191
+ if (fact) methods.push(fact);
1192
+ if (!fact?.executable) unsupported.push(callName(decorator));
1193
+ }
1194
+ }
1195
+ return {
1196
+ methods,
1197
+ observedDecoratorNames: unique(observed),
1198
+ unsupportedDecoratorNames: unique(unsupported)
1199
+ };
1200
+ }
1201
+ function parseHandlerClass(node, lookups, source, filePath) {
1202
+ const classDecoratorNames = unique(decs(node).map(callName));
1203
+ const classDecorators = decs(node);
1204
+ const hasImportedHandler = classDecorators.some((decorator) => capDecoratorName(decorator, lookups) === "Handler");
1205
+ const hasHandlerDecorator = hasImportedHandler || classDecoratorNames.includes("Handler");
1206
+ const parsed = parseClassMethods(
1207
+ node,
1208
+ lookups,
1209
+ source,
1210
+ filePath,
1211
+ hasHandlerDecorator,
1212
+ hasImportedHandler
1213
+ );
1214
+ if (!hasHandlerDecorator && parsed.methods.length === 0) return void 0;
1215
+ return {
1216
+ className: node.name?.text ?? "AnonymousHandler",
1217
+ sourceFile: normalizePath(filePath),
1218
+ sourceLine: line(source, node.getStart()),
1219
+ ...parsed,
1220
+ hasHandlerDecorator,
1221
+ classDecoratorNames
1222
+ };
472
1223
  }
473
- async function parseDecorators(repoPath, filePath) {
474
- const text = await fs4.readFile(path5.join(repoPath, filePath), "utf8");
475
- const sf = createSourceFile(filePath, text);
1224
+ async function parseDecorators(repoPath, filePath, context) {
1225
+ const snapshot = context?.get(filePath);
1226
+ const text = snapshot?.text ?? await fs5.readFile(path6.join(repoPath, filePath), "utf8");
1227
+ const sf = snapshot?.sourceFile() ?? createSourceFile(filePath, text);
476
1228
  const lookups = collectStringLookups(sf);
477
1229
  const handlers = [];
478
1230
  function visit(node) {
479
1231
  if (ts2.isClassDeclaration(node)) {
480
- const className = node.name?.text ?? "AnonymousHandler";
481
- const hasHandler = decs(node).some((d) => callName(d) === "Handler");
482
- const methods = node.members.filter(ts2.isMethodDeclaration).flatMap(
483
- (m) => decs(m).filter(
484
- (d) => ["Func", "Action", "On", "Event"].includes(callName(d))
485
- ).map((d) => {
486
- const decoratorResolution = resolveDecoratorArgument(firstArg(d), lookups);
487
- return {
488
- methodName: m.name.getText(),
489
- decoratorKind: callName(d),
490
- decoratorValue: decoratorResolution.resolvedValue,
491
- decoratorRawExpression: decoratorResolution.rawExpression,
492
- decoratorResolution,
493
- sourceFile: normalizePath(filePath),
494
- sourceLine: line(sf, m.getStart())
495
- };
496
- })
497
- );
498
- if (hasHandler || methods.length > 0)
499
- handlers.push({
500
- className,
501
- sourceFile: normalizePath(filePath),
502
- sourceLine: line(sf, node.getStart()),
503
- methods
504
- });
1232
+ const handler = parseHandlerClass(node, lookups, sf, filePath);
1233
+ if (handler) handlers.push(handler);
505
1234
  }
506
1235
  ts2.forEachChild(node, visit);
507
1236
  }
@@ -511,8 +1240,8 @@ async function parseDecorators(repoPath, filePath) {
511
1240
 
512
1241
  // src/parsers/handler-registration-parser.ts
513
1242
  import fsSync from "fs";
514
- import fs5 from "fs/promises";
515
- import path6 from "path";
1243
+ import fs6 from "fs/promises";
1244
+ import path7 from "path";
516
1245
  import ts3 from "typescript";
517
1246
  var MAX_EXPORT_DEPTH = 5;
518
1247
  function lineOf2(sourceFile, node) {
@@ -529,15 +1258,37 @@ function importSourceFor(identifier, imports) {
529
1258
  const evidence = imports.get(identifier);
530
1259
  return evidence ? `${evidence.source}#${evidence.importedName}` : void 0;
531
1260
  }
532
- async function parseHandlerRegistrations(repoPath, filePath) {
533
- const absolutePath = path6.join(repoPath, filePath);
534
- const text = await fs5.readFile(absolutePath, "utf8");
535
- const sourceFile = ts3.createSourceFile(filePath, text, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
1261
+ async function parseHandlerRegistrations(repoPath, filePath, context) {
1262
+ const absolutePath = path7.join(repoPath, filePath);
1263
+ const snapshot = context?.get(filePath);
1264
+ const text = snapshot?.text ?? await fs6.readFile(absolutePath, "utf8");
1265
+ const sourceFile = snapshot?.sourceFile() ?? ts3.createSourceFile(
1266
+ filePath,
1267
+ text,
1268
+ ts3.ScriptTarget.Latest,
1269
+ true,
1270
+ ts3.ScriptKind.TS
1271
+ );
536
1272
  const imports = collectImports(sourceFile);
537
- const localArrays = collectLocalArrays(sourceFile, imports, /* @__PURE__ */ new Map(), repoPath, filePath);
1273
+ const localArrays = collectLocalArrays(
1274
+ sourceFile,
1275
+ imports,
1276
+ /* @__PURE__ */ new Map(),
1277
+ repoPath,
1278
+ filePath,
1279
+ context
1280
+ );
538
1281
  const out = [];
539
1282
  function emitFromExpression(expression, call) {
540
- const classes = resolveArrayExpression(expression, localArrays, imports, repoPath, filePath, /* @__PURE__ */ new Set());
1283
+ const classes = resolveArrayExpression(
1284
+ expression,
1285
+ localArrays,
1286
+ imports,
1287
+ repoPath,
1288
+ filePath,
1289
+ /* @__PURE__ */ new Set(),
1290
+ context
1291
+ );
541
1292
  for (const cls of classes) {
542
1293
  out.push({
543
1294
  className: cls.className,
@@ -598,70 +1349,86 @@ function collectImports(sourceFile) {
598
1349
  }
599
1350
  return imports;
600
1351
  }
601
- function collectLocalArrays(sourceFile, imports, seed, repoPath = "", fromFile = "") {
1352
+ function collectLocalArrays(sourceFile, imports, seed, repoPath = "", fromFile = "", context) {
602
1353
  const arrays = new Map(seed);
603
1354
  for (const statement of sourceFile.statements) {
604
1355
  if (ts3.isVariableStatement(statement)) {
605
1356
  for (const decl of statement.declarationList.declarations) {
606
1357
  if (ts3.isIdentifier(decl.name) && decl.initializer && ts3.isArrayLiteralExpression(decl.initializer)) {
607
- arrays.set(decl.name.text, resolveArrayLiteral(decl.initializer, arrays, imports, repoPath, fromFile, /* @__PURE__ */ new Set()));
1358
+ arrays.set(decl.name.text, resolveArrayLiteral(
1359
+ decl.initializer,
1360
+ arrays,
1361
+ imports,
1362
+ repoPath,
1363
+ fromFile,
1364
+ /* @__PURE__ */ new Set(),
1365
+ context
1366
+ ));
608
1367
  }
609
1368
  }
610
1369
  }
611
1370
  }
612
1371
  return arrays;
613
1372
  }
614
- function resolveArrayExpression(expr, arrays, imports, repoPath, fromFile, seen) {
615
- if (ts3.isArrayLiteralExpression(expr)) return resolveArrayLiteral(expr, arrays, imports, repoPath, fromFile, seen);
1373
+ function resolveArrayExpression(expr, arrays, imports, repoPath, fromFile, seen, context) {
1374
+ if (ts3.isArrayLiteralExpression(expr)) return resolveArrayLiteral(expr, arrays, imports, repoPath, fromFile, seen, context);
616
1375
  if (ts3.isIdentifier(expr)) {
617
1376
  const local = arrays.get(expr.text);
618
1377
  if (local) return local;
619
1378
  const evidence = imports.get(expr.text);
620
- if (evidence && isRelative(evidence.source)) return resolveImportedArray(repoPath, fromFile, evidence, seen);
1379
+ if (evidence && isRelative(evidence.source)) return resolveImportedArray(repoPath, fromFile, evidence, seen, context);
621
1380
  if (evidence) return [{ className: evidence.importedName === "default" ? expr.text : evidence.importedName, importSource: `${evidence.source}#${evidence.importedName}` }];
622
1381
  }
623
1382
  return [];
624
1383
  }
625
- function resolveArrayLiteral(array, arrays, imports, repoPath, fromFile, seen) {
1384
+ function resolveArrayLiteral(array, arrays, imports, repoPath, fromFile, seen, context) {
626
1385
  const out = [];
627
1386
  for (const element of array.elements) {
628
- if (ts3.isSpreadElement(element)) out.push(...resolveArrayExpression(element.expression, arrays, imports, repoPath, fromFile, seen));
1387
+ if (ts3.isSpreadElement(element)) out.push(...resolveArrayExpression(element.expression, arrays, imports, repoPath, fromFile, seen, context));
629
1388
  else if (ts3.isIdentifier(element)) out.push({ className: element.text, importSource: importSourceFor(element.text, imports) });
630
1389
  }
631
1390
  return out;
632
1391
  }
633
- function resolveImportedArray(repoPath, fromFile, evidence, seen) {
1392
+ function resolveImportedArray(repoPath, fromFile, evidence, seen, context) {
634
1393
  const moduleFile = resolveRelativeModule(repoPath, fromFile, evidence.source);
635
1394
  if (!moduleFile) return [];
636
1395
  const key = `${moduleFile}:${evidence.importedName}`;
637
1396
  if (seen.has(key) || seen.size > MAX_EXPORT_DEPTH) return [];
638
1397
  seen.add(key);
639
- const exports = readExports(repoPath, moduleFile, seen);
1398
+ const exports = readExports(repoPath, moduleFile, seen, context);
640
1399
  if (evidence.importedName === "default") return exports.defaultArray ?? [];
641
1400
  return exports.arrays.get(evidence.importedName) ?? exports.arrays.get(exports.aliases.get(evidence.importedName) ?? evidence.importedName) ?? [];
642
1401
  }
643
1402
  function resolveRelativeModule(repoPath, fromFile, specifier) {
644
- const base = path6.resolve(repoPath, path6.dirname(fromFile), specifier);
645
- for (const candidate of [base, `${base}.ts`, `${base}.js`, path6.join(base, "index.ts"), path6.join(base, "index.js")]) {
1403
+ const base = path7.resolve(repoPath, path7.dirname(fromFile), specifier);
1404
+ for (const candidate of [base, `${base}.ts`, `${base}.js`, path7.join(base, "index.ts"), path7.join(base, "index.js")]) {
646
1405
  try {
647
1406
  const stat = fsSync.statSync(candidate);
648
- if (stat.isFile()) return normalizePath(path6.relative(repoPath, candidate));
1407
+ if (stat.isFile()) return normalizePath(path7.relative(repoPath, candidate));
649
1408
  } catch {
650
1409
  }
651
1410
  }
652
1411
  return void 0;
653
1412
  }
654
- function readExports(repoPath, filePath, seen) {
655
- const absolute = path6.join(repoPath, filePath);
1413
+ function readExports(repoPath, filePath, seen, context) {
1414
+ const absolute = path7.join(repoPath, filePath);
656
1415
  let text;
1416
+ const snapshot = context?.get(filePath);
657
1417
  try {
658
- text = fsSync.readFileSync(absolute, "utf8");
1418
+ text = snapshot?.text ?? fsSync.readFileSync(absolute, "utf8");
659
1419
  } catch {
660
1420
  return { arrays: /* @__PURE__ */ new Map(), aliases: /* @__PURE__ */ new Map() };
661
1421
  }
662
- const sourceFile = ts3.createSourceFile(filePath, text, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
1422
+ const sourceFile = snapshot?.sourceFile() ?? ts3.createSourceFile(filePath, text, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
663
1423
  const imports = collectImports(sourceFile);
664
- const arrays = collectLocalArrays(sourceFile, imports, /* @__PURE__ */ new Map(), repoPath, filePath);
1424
+ const arrays = collectLocalArrays(
1425
+ sourceFile,
1426
+ imports,
1427
+ /* @__PURE__ */ new Map(),
1428
+ repoPath,
1429
+ filePath,
1430
+ context
1431
+ );
665
1432
  const aliases = /* @__PURE__ */ new Map();
666
1433
  let defaultArray;
667
1434
  for (const statement of sourceFile.statements) {
@@ -672,7 +1439,13 @@ function readExports(repoPath, filePath, seen) {
672
1439
  const local = element.propertyName?.text ?? element.name.text;
673
1440
  aliases.set(element.name.text, local);
674
1441
  if (module && isRelative(module)) {
675
- const imported = resolveImportedArray(repoPath, filePath, { source: module, importedName: local }, seen);
1442
+ const imported = resolveImportedArray(
1443
+ repoPath,
1444
+ filePath,
1445
+ { source: module, importedName: local },
1446
+ seen,
1447
+ context
1448
+ );
676
1449
  if (imported.length > 0) arrays.set(element.name.text, imported);
677
1450
  }
678
1451
  }
@@ -704,12 +1477,12 @@ function summarizeExpression(text) {
704
1477
  }
705
1478
 
706
1479
  // src/parsers/service-binding-parser.ts
707
- import path8 from "path";
1480
+ import path9 from "path";
708
1481
  import ts5 from "typescript";
709
1482
 
710
1483
  // src/parsers/service-binding-parser-helpers.ts
711
- import fs6 from "fs/promises";
712
- import path7 from "path";
1484
+ import fs7 from "fs/promises";
1485
+ import path8 from "path";
713
1486
  import ts4 from "typescript";
714
1487
  function lineOf3(sf, node) {
715
1488
  return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
@@ -795,9 +1568,11 @@ function findConnectInExpression(expr) {
795
1568
  visit(expr);
796
1569
  return found;
797
1570
  }
798
- async function readSource(abs) {
1571
+ async function readSource(abs, context, filePath) {
1572
+ const snapshot = filePath ? context?.get(filePath) : void 0;
1573
+ if (snapshot) return snapshot.sourceFile();
799
1574
  try {
800
- const text = await fs6.readFile(abs, "utf8");
1575
+ const text = await fs7.readFile(abs, "utf8");
801
1576
  return ts4.createSourceFile(abs, text, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
802
1577
  } catch {
803
1578
  return void 0;
@@ -805,12 +1580,12 @@ async function readSource(abs) {
805
1580
  }
806
1581
  async function resolveImport(repoPath, fromFile, spec) {
807
1582
  if (!spec.startsWith(".")) return void 0;
808
- const rawBase = path7.resolve(repoPath, path7.dirname(fromFile), spec);
809
- const parsed = path7.parse(rawBase);
810
- const base = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"].includes(parsed.ext) ? path7.join(parsed.dir, parsed.name) : rawBase;
811
- for (const candidate of [base, `${base}.ts`, `${base}.js`, path7.join(base, "index.ts"), path7.join(base, "index.js")]) {
812
- const stat = await fs6.stat(candidate).catch(() => void 0);
813
- if (stat?.isFile()) return normalizePath(path7.relative(repoPath, candidate));
1583
+ const rawBase = path8.resolve(repoPath, path8.dirname(fromFile), spec);
1584
+ const parsed = path8.parse(rawBase);
1585
+ const base = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"].includes(parsed.ext) ? path8.join(parsed.dir, parsed.name) : rawBase;
1586
+ for (const candidate of [base, `${base}.ts`, `${base}.js`, path8.join(base, "index.ts"), path8.join(base, "index.js")]) {
1587
+ const stat = await fs7.stat(candidate).catch(() => void 0);
1588
+ if (stat?.isFile()) return normalizePath(path8.relative(repoPath, candidate));
814
1589
  }
815
1590
  return void 0;
816
1591
  }
@@ -918,8 +1693,8 @@ function exportedLocalNames(sf) {
918
1693
  }
919
1694
  return exports;
920
1695
  }
921
- async function helperBindings(repoPath, filePath) {
922
- const sf = await readSource(path8.join(repoPath, filePath));
1696
+ async function helperBindings(repoPath, filePath, context) {
1697
+ const sf = await readSource(path9.join(repoPath, filePath), context, filePath);
923
1698
  if (!sf) return [];
924
1699
  const sourceFileAst = sf;
925
1700
  const out = [];
@@ -979,8 +1754,8 @@ async function helperBindings(repoPath, filePath) {
979
1754
  }
980
1755
  return out;
981
1756
  }
982
- async function parseServiceBindings(repoPath, filePath) {
983
- const sf = await readSource(path8.join(repoPath, filePath));
1757
+ async function parseServiceBindings(repoPath, filePath, context) {
1758
+ const sf = await readSource(path9.join(repoPath, filePath), context, filePath);
984
1759
  if (!sf) return [];
985
1760
  const sourceFileAst = sf;
986
1761
  const out = [];
@@ -1019,7 +1794,7 @@ async function parseServiceBindings(repoPath, filePath) {
1019
1794
  if (!helperCache.has(imp.sourceFile))
1020
1795
  helperCache.set(
1021
1796
  imp.sourceFile,
1022
- await helperBindings(repoPath, imp.sourceFile)
1797
+ await helperBindings(repoPath, imp.sourceFile, context)
1023
1798
  );
1024
1799
  return (helperCache.get(imp.sourceFile) ?? []).filter((h) => h.exportedName === imp.exportedName).map((helper) => ({ imp, helper }));
1025
1800
  }
@@ -1069,28 +1844,28 @@ async function parseServiceBindings(repoPath, filePath) {
1069
1844
  });
1070
1845
  else if (ts5.isIdentifier(call.expression)) {
1071
1846
  const localDirect = localDirectHelpers.get(call.expression.text);
1072
- const resolved2 = localDirect ? { helper: localDirect, imp: void 0 } : await importedHelper(call.expression.text);
1073
- if (resolved2)
1847
+ const resolved3 = localDirect ? { helper: localDirect, imp: void 0 } : await importedHelper(call.expression.text);
1848
+ if (resolved3)
1074
1849
  out.push({
1075
1850
  variableName: targetName,
1076
- alias: resolved2.helper.alias,
1077
- aliasExpr: resolved2.helper.aliasExpr,
1078
- destinationExpr: resolved2.helper.destinationExpr,
1079
- servicePathExpr: resolved2.helper.servicePathExpr,
1080
- isDynamic: resolved2.helper.isDynamic,
1081
- placeholders: resolved2.helper.placeholders,
1851
+ alias: resolved3.helper.alias,
1852
+ aliasExpr: resolved3.helper.aliasExpr,
1853
+ destinationExpr: resolved3.helper.destinationExpr,
1854
+ servicePathExpr: resolved3.helper.servicePathExpr,
1855
+ isDynamic: resolved3.helper.isDynamic,
1856
+ placeholders: resolved3.helper.placeholders,
1082
1857
  sourceFile: normalizePath(filePath),
1083
1858
  sourceLine: lineOf3(sourceFileAst, node),
1084
1859
  helperChain: [
1085
- ...resolved2.helper.helperChain ?? [],
1860
+ ...resolved3.helper.helperChain ?? [],
1086
1861
  {
1087
1862
  callerVariable: targetName,
1088
1863
  ...aliasKind === "assignment" ? { assignedFrom: call.expression.text, aliasKind, scopeRule: "same-file-source-order" } : {},
1089
1864
  importedHelper: call.expression.text,
1090
- importSource: resolved2.imp?.sourceFile,
1091
- exportedSymbol: resolved2.imp?.exportedName ?? resolved2.helper.exportedName,
1092
- helperSourceFile: resolved2.helper.sourceFile,
1093
- helperSourceLine: resolved2.helper.sourceLine
1865
+ importSource: resolved3.imp?.sourceFile,
1866
+ exportedSymbol: resolved3.imp?.exportedName ?? resolved3.helper.exportedName,
1867
+ helperSourceFile: resolved3.helper.sourceFile,
1868
+ helperSourceLine: resolved3.helper.sourceLine
1094
1869
  }
1095
1870
  ]
1096
1871
  });
@@ -1119,28 +1894,28 @@ async function parseServiceBindings(repoPath, filePath) {
1119
1894
  const helpers = objectHelperVariables.get(unwrapped.expression.text) ?? [];
1120
1895
  const matches2 = helpers.filter((row) => row.helper.returnedProperty === unwrapped.name.text);
1121
1896
  if (matches2.length !== 1) return false;
1122
- const resolved2 = matches2[0];
1897
+ const resolved3 = matches2[0];
1123
1898
  out.push({
1124
1899
  variableName: targetName,
1125
- alias: resolved2.helper.alias,
1126
- aliasExpr: resolved2.helper.aliasExpr,
1127
- destinationExpr: resolved2.helper.destinationExpr,
1128
- servicePathExpr: resolved2.helper.servicePathExpr,
1129
- isDynamic: resolved2.helper.isDynamic,
1130
- placeholders: resolved2.helper.placeholders,
1900
+ alias: resolved3.helper.alias,
1901
+ aliasExpr: resolved3.helper.aliasExpr,
1902
+ destinationExpr: resolved3.helper.destinationExpr,
1903
+ servicePathExpr: resolved3.helper.servicePathExpr,
1904
+ isDynamic: resolved3.helper.isDynamic,
1905
+ placeholders: resolved3.helper.placeholders,
1131
1906
  sourceFile: normalizePath(filePath),
1132
1907
  sourceLine: lineOf3(sourceFileAst, node),
1133
1908
  helperChain: [
1134
- ...resolved2.helper.helperChain ?? [],
1909
+ ...resolved3.helper.helperChain ?? [],
1135
1910
  {
1136
1911
  callerVariable: targetName,
1137
1912
  sourceVariable: unwrapped.expression.text,
1138
1913
  returnedProperty: unwrapped.name.text,
1139
1914
  assignedFromProperty: unwrapped.getText(sourceFileAst),
1140
- importSource: resolved2.imp?.sourceFile,
1141
- exportedSymbol: resolved2.imp?.exportedName,
1142
- helperSourceFile: resolved2.helper.sourceFile,
1143
- helperSourceLine: resolved2.helper.sourceLine
1915
+ importSource: resolved3.imp?.sourceFile,
1916
+ exportedSymbol: resolved3.imp?.exportedName,
1917
+ helperSourceFile: resolved3.helper.sourceFile,
1918
+ helperSourceLine: resolved3.helper.sourceLine
1144
1919
  }
1145
1920
  ]
1146
1921
  });
@@ -1157,18 +1932,18 @@ async function parseServiceBindings(repoPath, filePath) {
1157
1932
  const propertyName3 = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1158
1933
  const matches2 = helpers.filter((row) => row.helper.returnedProperty === propertyName3);
1159
1934
  if (matches2.length !== 1) continue;
1160
- const resolved2 = matches2[0];
1935
+ const resolved3 = matches2[0];
1161
1936
  out.push({
1162
1937
  variableName: el.name.text,
1163
- alias: resolved2.helper.alias,
1164
- aliasExpr: resolved2.helper.aliasExpr,
1165
- destinationExpr: resolved2.helper.destinationExpr,
1166
- servicePathExpr: resolved2.helper.servicePathExpr,
1167
- isDynamic: resolved2.helper.isDynamic,
1168
- placeholders: resolved2.helper.placeholders,
1938
+ alias: resolved3.helper.alias,
1939
+ aliasExpr: resolved3.helper.aliasExpr,
1940
+ destinationExpr: resolved3.helper.destinationExpr,
1941
+ servicePathExpr: resolved3.helper.servicePathExpr,
1942
+ isDynamic: resolved3.helper.isDynamic,
1943
+ placeholders: resolved3.helper.placeholders,
1169
1944
  sourceFile: normalizePath(filePath),
1170
1945
  sourceLine: lineOf3(sourceFileAst, decl),
1171
- helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName3, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
1946
+ helperChain: [...resolved3.helper.helperChain ?? [], { callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName3, importSource: resolved3.imp?.sourceFile, exportedSymbol: resolved3.imp?.exportedName, helperSourceFile: resolved3.helper.sourceFile, helperSourceLine: resolved3.helper.sourceLine }]
1172
1947
  });
1173
1948
  }
1174
1949
  }
@@ -1190,18 +1965,18 @@ async function parseServiceBindings(repoPath, filePath) {
1190
1965
  if (!propertyName3 || !targetName) continue;
1191
1966
  const matches2 = helpers.filter((row) => row.helper.returnedProperty === propertyName3);
1192
1967
  if (matches2.length !== 1) continue;
1193
- const resolved2 = matches2[0];
1968
+ const resolved3 = matches2[0];
1194
1969
  out.push({
1195
1970
  variableName: targetName,
1196
- alias: resolved2.helper.alias,
1197
- aliasExpr: resolved2.helper.aliasExpr,
1198
- destinationExpr: resolved2.helper.destinationExpr,
1199
- servicePathExpr: resolved2.helper.servicePathExpr,
1200
- isDynamic: resolved2.helper.isDynamic,
1201
- placeholders: resolved2.helper.placeholders,
1971
+ alias: resolved3.helper.alias,
1972
+ aliasExpr: resolved3.helper.aliasExpr,
1973
+ destinationExpr: resolved3.helper.destinationExpr,
1974
+ servicePathExpr: resolved3.helper.servicePathExpr,
1975
+ isDynamic: resolved3.helper.isDynamic,
1976
+ placeholders: resolved3.helper.placeholders,
1202
1977
  sourceFile: normalizePath(filePath),
1203
1978
  sourceLine: lineOf3(sourceFileAst, node),
1204
- helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName3, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
1979
+ helperChain: [...resolved3.helper.helperChain ?? [], { callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName3, importSource: resolved3.imp?.sourceFile, exportedSymbol: resolved3.imp?.exportedName, helperSourceFile: resolved3.helper.sourceFile, helperSourceLine: resolved3.helper.sourceLine }]
1205
1980
  });
1206
1981
  }
1207
1982
  }
@@ -1400,8 +2175,8 @@ function collectClassHelpers(sf) {
1400
2175
  }
1401
2176
 
1402
2177
  // src/parsers/outbound-call-parser.ts
1403
- import fs7 from "fs/promises";
1404
- import path10 from "path";
2178
+ import fs8 from "fs/promises";
2179
+ import path11 from "path";
1405
2180
  import ts8 from "typescript";
1406
2181
 
1407
2182
  // src/linker/external-http-target.ts
@@ -1419,8 +2194,8 @@ function redactUrl(value) {
1419
2194
  url.username = "";
1420
2195
  url.password = "";
1421
2196
  for (const key of [...url.searchParams.keys()]) url.searchParams.set(key, sensitiveKeys.has(key.toLowerCase()) ? "<redacted>" : "<redacted>");
1422
- const path11 = `${url.pathname}${url.search ? url.search : ""}`;
1423
- return value.startsWith("/") ? path11 : `${url.origin}${path11}`;
2197
+ const path12 = `${url.pathname}${url.search ? url.search : ""}`;
2198
+ return value.startsWith("/") ? path12 : `${url.origin}${path12}`;
1424
2199
  } catch {
1425
2200
  return value.replace(/([?&][^=;&]*(?:token|key|password|secret|cookie|authorization)[^=;&]*=)[^&]*/gi, "$1<redacted>");
1426
2201
  }
@@ -1454,9 +2229,9 @@ function safeParse(value) {
1454
2229
  }
1455
2230
 
1456
2231
  // src/linker/odata-path-normalizer.ts
1457
- function normalizeODataOperationInvocationPath(path11) {
1458
- if (path11 === void 0) return void 0;
1459
- const raw = path11.trim();
2232
+ function normalizeODataOperationInvocationPath(path12) {
2233
+ if (path12 === void 0) return void 0;
2234
+ const raw = path12.trim();
1460
2235
  if (!raw) return void 0;
1461
2236
  const rejected = (reason) => ({ rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false, invocationArgumentPlaceholderKeys: [], normalizationRejectedReason: reason });
1462
2237
  const open = raw.indexOf("(");
@@ -1479,8 +2254,8 @@ function normalizeODataOperationInvocationPath(path11) {
1479
2254
  normalizationReason: "balanced_top_level_operation_invocation"
1480
2255
  };
1481
2256
  }
1482
- function classifyODataPathIntent(path11, method) {
1483
- const rawPath = (path11 ?? "").trim();
2257
+ function classifyODataPathIntent(path12, method) {
2258
+ const rawPath = (path12 ?? "").trim();
1484
2259
  const normalizedMethod = (method ?? "GET").trim().toUpperCase() || "GET";
1485
2260
  const queryIndex = rawPath.indexOf("?");
1486
2261
  const pathWithoutQuery = queryIndex >= 0 ? rawPath.slice(0, queryIndex) : rawPath;
@@ -1528,8 +2303,8 @@ function classifyODataPathIntent(path11, method) {
1528
2303
  if (/^[A-Z][A-Za-z0-9_]*$/.test(firstSegment)) return { ...base, kind: "entity_candidate", reason: "uppercase_collection_segment_without_indexed_entity_evidence" };
1529
2304
  return { ...base, kind: "unknown", reason: "get_path_has_no_query_key_or_navigation_signal" };
1530
2305
  }
1531
- function entitySegmentFromPath(path11) {
1532
- const first = path11.replace(/^\//, "").split("/")[0]?.trim();
2306
+ function entitySegmentFromPath(path12) {
2307
+ const first = path12.replace(/^\//, "").split("/")[0]?.trim();
1533
2308
  if (!first) return void 0;
1534
2309
  const open = first.indexOf("(");
1535
2310
  const entity = (open >= 0 ? first.slice(0, open) : first).trim();
@@ -1677,7 +2452,7 @@ function topLevelQueryIndex(text) {
1677
2452
  }
1678
2453
 
1679
2454
  // src/parsers/imported-wrapper-parser.ts
1680
- import path9 from "path";
2455
+ import path10 from "path";
1681
2456
  import ts7 from "typescript";
1682
2457
 
1683
2458
  // src/parsers/operation-path-analysis.ts
@@ -1686,8 +2461,8 @@ var maxAliasDepth = 6;
1686
2461
  function analyzeOperationPath(expression, use, method = "POST") {
1687
2462
  if (!expression) return emptyAnalysis();
1688
2463
  const state = collectExpressionState(expression, use, 0, /* @__PURE__ */ new Set());
1689
- const paths = unique(state.paths.map(normalizeRawPath));
1690
- const normalized = unique(paths.flatMap((value) => normalizedCandidate(value, method)));
2464
+ const paths = unique2(state.paths.map(normalizeRawPath));
2465
+ const normalized = unique2(paths.flatMap((value) => normalizedCandidate(value, method)));
1691
2466
  const status = pathStatus(paths, state.placeholders, state.dynamic);
1692
2467
  const runtimeIdentifier = state.dynamic.at(-1)?.expression;
1693
2468
  return {
@@ -1696,8 +2471,8 @@ function analyzeOperationPath(expression, use, method = "POST") {
1696
2471
  normalizedOperationPath: status === "static" && normalized.length === 1 ? normalized[0] : void 0,
1697
2472
  candidateRawPaths: paths,
1698
2473
  candidateNormalizedOperationPaths: normalized,
1699
- placeholderKeys: unique([...state.placeholders, ...runtimeIdentifier ? [runtimeIdentifier] : []]),
1700
- sourceKind: unique(state.sourceKinds).join("+") || "unknown",
2474
+ placeholderKeys: unique2([...state.placeholders, ...runtimeIdentifier ? [runtimeIdentifier] : []]),
2475
+ sourceKind: unique2(state.sourceKinds).join("+") || "unknown",
1701
2476
  candidateIdentifier: ts6.isIdentifier(expression) ? expression.text : void 0,
1702
2477
  runtimeIdentifier,
1703
2478
  dynamicReassignments: state.dynamic,
@@ -1828,8 +2603,8 @@ function templateState(expression) {
1828
2603
  sourceKinds: ["template_with_placeholders"]
1829
2604
  };
1830
2605
  }
1831
- function staticState(path11, sourceKind) {
1832
- return { paths: [path11], placeholders: [], dynamic: [], sourceKinds: [sourceKind] };
2606
+ function staticState(path12, sourceKind) {
2607
+ return { paths: [path12], placeholders: [], dynamic: [], sourceKinds: [sourceKind] };
1833
2608
  }
1834
2609
  function dynamicState(expression, sourceKind = "dynamic_expression") {
1835
2610
  return {
@@ -1898,7 +2673,7 @@ function sourceLine(node) {
1898
2673
  const source = node.getSourceFile();
1899
2674
  return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
1900
2675
  }
1901
- function unique(values) {
2676
+ function unique2(values) {
1902
2677
  return [...new Set(values)].sort();
1903
2678
  }
1904
2679
  function emptyAnalysis() {
@@ -1914,7 +2689,7 @@ function emptyAnalysis() {
1914
2689
  }
1915
2690
 
1916
2691
  // src/parsers/imported-wrapper-parser.ts
1917
- async function parseImportedWrapperCalls(repoPath, filePath, source, serviceBindings) {
2692
+ async function parseImportedWrapperCalls(repoPath, filePath, source, serviceBindings, context) {
1918
2693
  const imports = await importsFor(repoPath, filePath, source);
1919
2694
  const importedByLocal = new Map(imports.filter((item) => item.sourceFile).map((item) => [item.localName, item]));
1920
2695
  const calls = collectImportedCalls(source, importedByLocal);
@@ -1924,7 +2699,7 @@ async function parseImportedWrapperCalls(repoPath, filePath, source, serviceBind
1924
2699
  if (!ts7.isIdentifier(call.expression)) continue;
1925
2700
  const imported = importedByLocal.get(call.expression.text);
1926
2701
  if (!imported?.sourceFile) continue;
1927
- const spec = await loadWrapperSpec(repoPath, imported, cache, 0);
2702
+ const spec = await loadWrapperSpec(repoPath, imported, cache, 0, context);
1928
2703
  const fact = spec ? wrapperCallFact(source, filePath, call, spec, serviceBindings) : void 0;
1929
2704
  if (fact) out.push(fact);
1930
2705
  }
@@ -1939,23 +2714,43 @@ function collectImportedCalls(source, imports) {
1939
2714
  visit(source);
1940
2715
  return calls;
1941
2716
  }
1942
- async function loadWrapperSpec(repoPath, imported, cache, depth) {
2717
+ async function loadWrapperSpec(repoPath, imported, cache, depth, context) {
1943
2718
  if (!imported.sourceFile || depth > 5) return void 0;
1944
2719
  const key = `${imported.sourceFile}#${imported.exportedName}`;
1945
2720
  const existing = cache.get(key);
1946
2721
  if (existing) return existing;
1947
- const pending = inspectWrapper(repoPath, imported.sourceFile, imported.exportedName, cache, depth);
2722
+ const pending = inspectWrapper(
2723
+ repoPath,
2724
+ imported.sourceFile,
2725
+ imported.exportedName,
2726
+ cache,
2727
+ depth,
2728
+ context
2729
+ );
1948
2730
  cache.set(key, pending);
1949
2731
  return pending;
1950
2732
  }
1951
- async function inspectWrapper(repoPath, sourceFile, exportedName, cache, depth) {
1952
- const source = await readSource(path9.join(repoPath, sourceFile));
2733
+ async function inspectWrapper(repoPath, sourceFile, exportedName, cache, depth, context) {
2734
+ const source = await readSource(
2735
+ path10.join(repoPath, sourceFile),
2736
+ context,
2737
+ sourceFile
2738
+ );
1953
2739
  if (!source) return void 0;
1954
2740
  const named = findFunction(source, exportedName);
1955
2741
  if (!named) return void 0;
1956
2742
  const direct = directSendSpec(source, sourceFile, named.name, named.fn);
1957
2743
  if (direct) return direct;
1958
- return nestedSendSpec(repoPath, sourceFile, source, named.name, named.fn, cache, depth);
2744
+ return nestedSendSpec(
2745
+ repoPath,
2746
+ sourceFile,
2747
+ source,
2748
+ named.name,
2749
+ named.fn,
2750
+ cache,
2751
+ depth,
2752
+ context
2753
+ );
1959
2754
  }
1960
2755
  function directSendSpec(source, sourceFile, name, fn) {
1961
2756
  const params = parameterNames(fn);
@@ -1974,11 +2769,11 @@ function directSendSpec(source, sourceFile, name, fn) {
1974
2769
  const clientIndex = params.indexOf(receiver);
1975
2770
  const pathIndex = pathName ? params.indexOf(pathName) : -1;
1976
2771
  if (clientIndex < 0 || pathIndex < 0) return void 0;
1977
- const methodName = methodExpr && ts7.isIdentifier(methodExpr) ? methodExpr.text : void 0;
1978
- const methodIndex = methodName ? params.indexOf(methodName) : -1;
2772
+ const methodName2 = methodExpr && ts7.isIdentifier(methodExpr) ? methodExpr.text : void 0;
2773
+ const methodIndex = methodName2 ? params.indexOf(methodName2) : -1;
1979
2774
  return { clientIndex, pathIndex, methodIndex: methodIndex >= 0 ? methodIndex : void 0, methodLiteral: literal(methodExpr), sourceFile, sourceLine: lineOf3(source, fn), chain: [name] };
1980
2775
  }
1981
- async function nestedSendSpec(repoPath, sourceFile, source, name, fn, cache, depth) {
2776
+ async function nestedSendSpec(repoPath, sourceFile, source, name, fn, cache, depth, context) {
1982
2777
  const imports = await importsFor(repoPath, sourceFile, source);
1983
2778
  const byLocal = new Map(imports.filter((item) => item.sourceFile).map((item) => [item.localName, item]));
1984
2779
  const calls = [];
@@ -1988,7 +2783,7 @@ async function nestedSendSpec(repoPath, sourceFile, source, name, fn, cache, dep
1988
2783
  if (calls.length !== 1) return void 0;
1989
2784
  const call = calls[0];
1990
2785
  const imported = call && ts7.isIdentifier(call.expression) ? byLocal.get(call.expression.text) : void 0;
1991
- const nested = imported ? await loadWrapperSpec(repoPath, imported, cache, depth + 1) : void 0;
2786
+ const nested = imported ? await loadWrapperSpec(repoPath, imported, cache, depth + 1, context) : void 0;
1992
2787
  if (!call || !nested) return void 0;
1993
2788
  const params = parameterNames(fn);
1994
2789
  const clientIndex = mappedParameterIndex(call.arguments[nested.clientIndex], params);
@@ -2235,8 +3030,8 @@ function resolveExpression(expr, use, policy, depth = 0, seen = /* @__PURE__ */
2235
3030
  if (!binding.declaration || !binding.initializer || !binding.immutable) return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: binding.evidence, constName: expr.text };
2236
3031
  if (seen.has(binding.declaration)) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_cycle_detected"], constName: expr.text };
2237
3032
  seen.add(binding.declaration);
2238
- const resolved2 = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
2239
- return { ...resolved2, sourceKind: "const_alias", rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved2.evidence] };
3033
+ const resolved3 = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
3034
+ return { ...resolved3, sourceKind: "const_alias", rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved3.evidence] };
2240
3035
  }
2241
3036
  return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${ts8.SyntaxKind[expr.kind] ?? "expression"}`] };
2242
3037
  }
@@ -2260,10 +3055,10 @@ function destinationExpressionShape(expr) {
2260
3055
  return ts8.SyntaxKind[expr.kind] ?? "expression";
2261
3056
  }
2262
3057
  function staticConditionalCandidates(expr, initializers) {
2263
- const resolved2 = expr && ts8.isIdentifier(expr) && initializers.has(expr.text) ? initializers.get(expr.text) : expr;
2264
- if (!resolved2 || !ts8.isConditionalExpression(resolved2)) return void 0;
2265
- const left = staticExpressionText(resolved2.whenTrue, initializers);
2266
- const right = staticExpressionText(resolved2.whenFalse, initializers);
3058
+ const resolved3 = expr && ts8.isIdentifier(expr) && initializers.has(expr.text) ? initializers.get(expr.text) : expr;
3059
+ if (!resolved3 || !ts8.isConditionalExpression(resolved3)) return void 0;
3060
+ const left = staticExpressionText(resolved3.whenTrue, initializers);
3061
+ const right = staticExpressionText(resolved3.whenFalse, initializers);
2267
3062
  if (!left || !right) return void 0;
2268
3063
  return [.../* @__PURE__ */ new Set([left, right])];
2269
3064
  }
@@ -2311,15 +3106,15 @@ function hasTemplatePlaceholder(value) {
2311
3106
  return /\$\{|%7B|%7D/i.test(value);
2312
3107
  }
2313
3108
  function urlTargetFromExpression(expr, use) {
2314
- const resolved2 = resolveExpression(expr, use, "external");
2315
- if (resolved2.status === "static" && resolved2.value && !hasTemplatePlaceholder(resolved2.value)) return { kind: "static_url", expression: resolved2.value, dynamic: false, sourceKind: resolved2.sourceKind };
2316
- if (expr) return { kind: "url_expression", dynamic: true, expression: `${resolved2.sourceKind}:${resolved2.placeholderKeys.join("|")}`, expressionShape: resolved2.sourceKind, placeholderKeys: resolved2.placeholderKeys };
3109
+ const resolved3 = resolveExpression(expr, use, "external");
3110
+ if (resolved3.status === "static" && resolved3.value && !hasTemplatePlaceholder(resolved3.value)) return { kind: "static_url", expression: resolved3.value, dynamic: false, sourceKind: resolved3.sourceKind };
3111
+ if (expr) return { kind: "url_expression", dynamic: true, expression: `${resolved3.sourceKind}:${resolved3.placeholderKeys.join("|")}`, expressionShape: resolved3.sourceKind, placeholderKeys: resolved3.placeholderKeys };
2317
3112
  return { kind: "unknown", dynamic: false };
2318
3113
  }
2319
3114
  function destinationTargetFromExpression(expr, use) {
2320
- const resolved2 = resolveExpression(expr, use, "external");
2321
- const text = resolved2.value;
2322
- if (resolved2.status === "static" && text && !hasTemplatePlaceholder(text)) return { kind: "destination", expression: text, dynamic: false, sourceKind: resolved2.sourceKind };
3115
+ const resolved3 = resolveExpression(expr, use, "external");
3116
+ const text = resolved3.value;
3117
+ if (resolved3.status === "static" && text && !hasTemplatePlaceholder(text)) return { kind: "destination", expression: text, dynamic: false, sourceKind: resolved3.sourceKind };
2323
3118
  const candidates = staticConditionalCandidates(expr, /* @__PURE__ */ new Map());
2324
3119
  if (candidates) return { kind: "destination", dynamic: true, expressionShape: "conditional", candidateLiterals: candidates };
2325
3120
  const shape = destinationExpressionShape(expr);
@@ -2419,9 +3214,9 @@ function collectWrapperSpecs(source) {
2419
3214
  const pathProp = propertyInitializer(objectArg, "path");
2420
3215
  const methodProp = propertyInitializer(objectArg, "method");
2421
3216
  const pathName = pathProp && ts8.isIdentifier(pathProp) ? pathProp.text : void 0;
2422
- const methodName = methodProp && ts8.isIdentifier(methodProp) ? methodProp.text : void 0;
3217
+ const methodName2 = methodProp && ts8.isIdentifier(methodProp) ? methodProp.text : void 0;
2423
3218
  const methodLiteral = resolveExpression(methodProp, node, "literal").value;
2424
- if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
3219
+ if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName2, methodLiteral, start: node.getStart(source), end: node.getEnd() });
2425
3220
  }
2426
3221
  }
2427
3222
  if (ts8.isCallExpression(node) && ts8.isIdentifier(node.expression) && specs.has(node.expression.text)) {
@@ -2558,15 +3353,37 @@ function containsSupportedOutboundCall(node) {
2558
3353
  const end = node.getEnd();
2559
3354
  return classifyOutboundCallsInSource(source, source.fileName).some((call) => call.node.getStart(source) >= start && call.node.getEnd() <= end);
2560
3355
  }
2561
- async function parseOutboundCalls(repoPath, filePath) {
2562
- const text = await fs7.readFile(path10.join(repoPath, filePath), "utf8");
2563
- const source = ts8.createSourceFile(filePath, text, ts8.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts8.ScriptKind.TS : ts8.ScriptKind.JS);
2564
- const bindingNames = new Set((await parseServiceBindings(repoPath, filePath)).map((binding) => binding.variableName));
2565
- const importedWrappers = await parseImportedWrapperCalls(repoPath, filePath, source, bindingNames);
2566
- return [...classifyOutboundCallsInSource(source, filePath).map((call) => call.fact), ...importedWrappers, ...parseLocalServiceCalls(text, filePath)];
2567
- }
2568
- function parseLocalServiceCalls(text, filePath) {
2569
- const source = ts8.createSourceFile(filePath, text, ts8.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts8.ScriptKind.TS : ts8.ScriptKind.JS);
3356
+ async function parseOutboundCalls(repoPath, filePath, context) {
3357
+ const snapshot = context?.get(filePath);
3358
+ const text = snapshot?.text ?? await fs8.readFile(path11.join(repoPath, filePath), "utf8");
3359
+ const source = snapshot?.sourceFile() ?? ts8.createSourceFile(
3360
+ filePath,
3361
+ text,
3362
+ ts8.ScriptTarget.Latest,
3363
+ true,
3364
+ filePath.endsWith(".ts") ? ts8.ScriptKind.TS : ts8.ScriptKind.JS
3365
+ );
3366
+ const bindingNames = new Set((await parseServiceBindings(
3367
+ repoPath,
3368
+ filePath,
3369
+ context
3370
+ )).map((binding) => binding.variableName));
3371
+ const importedWrappers = await parseImportedWrapperCalls(
3372
+ repoPath,
3373
+ filePath,
3374
+ source,
3375
+ bindingNames,
3376
+ context
3377
+ );
3378
+ return [...classifyOutboundCallsInSource(source, filePath).map((call) => call.fact), ...importedWrappers, ...parseLocalServiceCalls(text, filePath, source)];
3379
+ }
3380
+ function parseLocalServiceCalls(text, filePath, source = ts8.createSourceFile(
3381
+ filePath,
3382
+ text,
3383
+ ts8.ScriptTarget.Latest,
3384
+ true,
3385
+ filePath.endsWith(".ts") ? ts8.ScriptKind.TS : ts8.ScriptKind.JS
3386
+ )) {
2570
3387
  const aliases = /* @__PURE__ */ new Map();
2571
3388
  const calls = [];
2572
3389
  const visit = (node) => {
@@ -3277,7 +4094,7 @@ function linkImplementations(db, workspaceId, generation) {
3277
4094
  const duplicateFamilies = duplicatePackageFamilies(accepted);
3278
4095
  const duplicatePackageAmbiguous = duplicateFamilies.length > 0 && !accepted.some(hasDirectOwnershipEvidence);
3279
4096
  const selected = duplicatePackageAmbiguous ? accepted : winners;
3280
- const unique2 = !duplicatePackageAmbiguous && winners.length === 1 ? winners[0] : void 0;
4097
+ const unique3 = !duplicatePackageAmbiguous && winners.length === 1 ? winners[0] : void 0;
3281
4098
  const ambiguityReasons = duplicatePackageAmbiguous ? ["duplicate_package_name_candidates"] : winners.length > 1 ? ["multiple_equal_score_implementation_candidates"] : [];
3282
4099
  const evidence = {
3283
4100
  servicePath: operation.servicePath,
@@ -3291,16 +4108,16 @@ function linkImplementations(db, workspaceId, generation) {
3291
4108
  candidateFamilies: duplicateFamilies,
3292
4109
  candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1))
3293
4110
  };
3294
- const evidenceWithHints = unique2 ? evidence : { ...evidence, implementationHintSuggestions: implementationHintSuggestions(evidence) };
4111
+ const evidenceWithHints = unique3 ? evidence : { ...evidence, implementationHintSuggestions: implementationHintSuggestions(evidence) };
3295
4112
  if (accepted.length === 0) {
3296
4113
  db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", "unresolved", "operation", graphId(operation.operationId), "handler_method_candidates", candidates.map((row) => graphId(row.methodId)).join(","), 0, JSON.stringify(evidenceWithHints), 0, "No implementation candidate passed policy", generation);
3297
4114
  edgeCount += 1;
3298
4115
  unresolvedCount += 1;
3299
4116
  continue;
3300
4117
  }
3301
- db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", unique2 ? "resolved" : "ambiguous", "operation", graphId(operation.operationId), unique2 ? "handler_method" : "handler_method_candidates", unique2 ? graphId(unique2.methodId) : selected.map((row) => graphId(row.methodId)).join(","), unique2 ? 0.95 : 0.5, JSON.stringify(evidenceWithHints), 0, unique2 ? null : "Ambiguous registered handler implementation candidates", generation);
4118
+ db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", unique3 ? "resolved" : "ambiguous", "operation", graphId(operation.operationId), unique3 ? "handler_method" : "handler_method_candidates", unique3 ? graphId(unique3.methodId) : selected.map((row) => graphId(row.methodId)).join(","), unique3 ? 0.95 : 0.5, JSON.stringify(evidenceWithHints), 0, unique3 ? null : "Ambiguous registered handler implementation candidates", generation);
3302
4119
  edgeCount += 1;
3303
- if (unique2) resolvedCount += 1;
4120
+ if (unique3) resolvedCount += 1;
3304
4121
  else ambiguousCount += 1;
3305
4122
  }
3306
4123
  return { edgeCount, resolvedCount, ambiguousCount, unresolvedCount };
@@ -3443,7 +4260,7 @@ function implementationCandidates(db, workspaceId, operation) {
3443
4260
  CASE WHEN appRepo.id=handlerRepo.id THEN 1 ELSE 0 END sameRepoRegistration,
3444
4261
  CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id AND localService.service_path=?) THEN 1 ELSE 0 END localServicePathMatch,
3445
4262
  CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id) THEN 1 ELSE 0 END applicationHasLocalServices,
3446
- CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg JOIN handler_classes localClass ON ((localReg.handler_class_id IS NOT NULL AND localClass.id=localReg.handler_class_id) OR (localReg.handler_class_id IS NULL AND localClass.class_name=localReg.class_name AND localClass.repo_id=localReg.repo_id)) JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id WHERE localReg.repo_id=appRepo.id AND (localMethod.decorator_value=? OR localMethod.decorator_value=? OR localMethod.method_name=? OR localMethod.decorator_raw_expression LIKE ?)) THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
4263
+ CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg JOIN handler_classes localClass ON ((localReg.handler_class_id IS NOT NULL AND localClass.id=localReg.handler_class_id) OR (localReg.handler_class_id IS NULL AND localClass.class_name=localReg.class_name AND localClass.repo_id=localReg.repo_id)) JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id WHERE localReg.repo_id=appRepo.id AND COALESCE(json_extract(localMethod.decorator_resolution_json,'$.handlerKind'),CASE WHEN localMethod.decorator_kind='Event' THEN 'event' WHEN localMethod.decorator_kind IN ('Action','Func','On') THEN 'operation' ELSE 'unsupported' END)='operation' AND COALESCE(json_extract(localMethod.decorator_resolution_json,'$.executable'),CASE WHEN localMethod.decorator_kind IN ('Action','Func','On') THEN 1 ELSE 0 END)=1 AND (localMethod.decorator_value=? OR localMethod.decorator_value=? OR localMethod.method_name=? OR localMethod.decorator_raw_expression LIKE ?)) THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
3447
4264
  CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END appDependsOnModel,
3448
4265
  CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=CAST(handlerRepo.id AS TEXT)) THEN 1 ELSE 0 END appDependsOnHandler,
3449
4266
  CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(handlerRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END handlerDependsOnModel
@@ -3475,6 +4292,13 @@ function implementationCandidates(db, workspaceId, operation) {
3475
4292
  )
3476
4293
  JOIN repositories appRepo ON appRepo.id=hr.repo_id
3477
4294
  WHERE appRepo.workspace_id=?
4295
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
4296
+ CASE WHEN hm.decorator_kind='Event' THEN 'event'
4297
+ WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
4298
+ ELSE 'unsupported' END)='operation'
4299
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
4300
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On')
4301
+ THEN 1 ELSE 0 END)=1
3478
4302
  AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=? OR hm.decorator_raw_expression LIKE ?)`).all(
3479
4303
  operation.modelRepoId,
3480
4304
  operation.modelRepo,
@@ -3603,6 +4427,11 @@ function candidateEvidence(candidate, rank) {
3603
4427
  };
3604
4428
  }
3605
4429
  function implementationMethodSignal(row, operation) {
4430
+ const resolution = objectJson(row.decoratorResolutionJson) ?? {};
4431
+ if (resolution.handlerKind && resolution.handlerKind !== "operation")
4432
+ return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ["non_operation_handler_kind"] };
4433
+ if (resolution.executable === false)
4434
+ return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ["handler_method_not_executable"] };
3606
4435
  const op = normalizedOperationName(String(operation.operationPath ?? operation.operationName ?? ""));
3607
4436
  const decorator = normalizeDecoratorOperationSignal(typeof row.decoratorValue === "string" ? row.decoratorValue : void 0, typeof row.decoratorRawExpression === "string" ? row.decoratorRawExpression : void 0, op);
3608
4437
  if (decorator.status === "resolved" && decorator.operationName === op) return { matches: true, contradicted: false, acceptedReasons: ["decorator targets operation"], rejectedReasons: [] };
@@ -3632,91 +4461,730 @@ function parseVars(values) {
3632
4461
  }
3633
4462
  return out;
3634
4463
  }
3635
- function ambiguousStartDiagnostic(requested, candidates, message) {
3636
- const serviceSuggestions = [...new Set(candidates.flatMap((row) => typeof row.servicePath === "string" ? [`--service ${row.servicePath}`] : []))].sort();
4464
+ function selectorRepoNotFoundDiagnostic(requested) {
3637
4465
  return {
3638
4466
  severity: "warning",
3639
- code: "trace_start_ambiguous",
3640
- message,
3641
- normalizedSelectorValue: requested,
3642
- resolutionStage: "operation",
3643
- resolutionStatus: "ambiguous_operation",
3644
- candidates,
3645
- serviceSuggestions,
3646
- selectorSuggestions: fullSelectorSuggestions(candidates)
4467
+ code: "selector_repo_not_found",
4468
+ message: `No indexed repository matched selector: ${requested}`,
4469
+ selectorKind: "repo",
4470
+ requestedRepository: requested
3647
4471
  };
3648
4472
  }
3649
- function fullSelectorSuggestions(candidates) {
3650
- const includeRepo = new Set(candidates.map((row) => row.repoName)).size > 1;
3651
- return [...new Set(candidates.flatMap((row) => {
3652
- if (typeof row.servicePath !== "string" || typeof row.operationPath !== "string") return [];
3653
- const repoSelector = includeRepo && typeof row.repoName === "string" ? `--repo ${row.repoName} ` : "";
3654
- return [
3655
- `${repoSelector}--service ${row.servicePath} --path ${row.operationPath}`
3656
- ];
3657
- }))].sort();
3658
- }
3659
-
3660
- // src/trace/dynamic-targets.ts
3661
- function analyzeDynamicTargetCandidates(db, evidence, workspaceId, mode, maxCandidates) {
3662
- const templates = templatesFromEvidence(evidence);
3663
- const missingVariables = allMissingVariables(evidence, templates);
3664
- if (missingVariables.length === 0) return void 0;
3665
- const order = variableOrder(templates, missingVariables);
3666
- const candidates = rankedCandidates(
3667
- db,
3668
- candidateTargets(db, evidence, workspaceId),
3669
- referenceRows(db, workspaceId),
3670
- templates,
3671
- order
3672
- );
3673
- const inference = inferenceDecision(candidates);
3674
- const shownCandidates = candidates.slice(0, maxCandidates);
4473
+ function selectorRepoAmbiguousDiagnostic(requested, candidates) {
4474
+ const uniqueName = (value) => candidates.filter((candidate) => candidate.name === value).length === 1;
4475
+ const uniquePackage = (value) => candidates.filter((candidate) => candidate.packageName === value).length === 1;
4476
+ const suggestions = candidates.flatMap((candidate) => {
4477
+ if (uniqueName(candidate.name)) return [`--repo ${candidate.name}`];
4478
+ if (candidate.packageName && uniquePackage(candidate.packageName))
4479
+ return [`--repo ${candidate.packageName}`];
4480
+ return [];
4481
+ });
3675
4482
  return {
3676
- mode,
3677
- candidateCount: candidates.length,
3678
- shownCandidateCount: shownCandidates.length,
3679
- omittedCandidateCount: Math.max(0, candidates.length - shownCandidates.length),
3680
- missingVariables,
4483
+ severity: "warning",
4484
+ code: "selector_repo_ambiguous",
4485
+ message: `Repository selector matched multiple indexed repositories: ${requested}`,
4486
+ selectorKind: "repo",
4487
+ requestedRepository: requested,
3681
4488
  candidates,
3682
- shownCandidates,
3683
- suggestedVarSets: suggestedVarSets(candidates, missingVariables, order),
3684
- inference
4489
+ selectorSuggestions: [...new Set(suggestions)].sort(),
4490
+ remediation: suggestions.length > 0 ? "Use one of the unique --repo selectors shown." : "Repository names and package names must be unique before this selector can be traced safely."
3685
4491
  };
3686
4492
  }
3687
- function candidateTargets(db, evidence, workspaceId) {
3688
- const embedded = rowsFromEvidence(evidence.candidates);
3689
- if (embedded.length > 0) return embedded;
3690
- const operationPath = stringValue4(evidence.operationPath);
3691
- if (!operationPath || extractPlaceholders(operationPath).length > 0) return [];
3692
- return queryOperationTargets(db, operationPath, workspaceId);
3693
- }
3694
- function rowsFromEvidence(value) {
3695
- if (!Array.isArray(value)) return [];
3696
- return value.flatMap((item) => {
3697
- const row = record(item);
3698
- const operationId = numberValue(row.operationId);
3699
- const repoName = stringValue4(row.repoName);
3700
- const servicePath = stringValue4(row.servicePath);
3701
- const operationPath = stringValue4(row.operationPath);
3702
- const operationName = stringValue4(row.operationName) ?? operationPath?.replace(/^\//, "");
3703
- if (operationId === void 0 || !repoName || !servicePath || !operationPath || !operationName) return [];
3704
- return [{
3705
- operationId,
3706
- repoName,
3707
- serviceName: stringValue4(row.serviceName) ?? "",
3708
- qualifiedName: stringValue4(row.qualifiedName) ?? "",
3709
- servicePath,
3710
- operationPath,
3711
- operationName,
3712
- sourceFile: stringValue4(row.sourceFile) ?? "",
3713
- sourceLine: numberValue(row.sourceLine) ?? 0,
3714
- repoId: numberValue(row.repoId),
3715
- packageName: stringValue4(row.packageName),
3716
- score: numberValue(row.score) ?? 0,
3717
- reasons: stringArray(row.reasons)
3718
- }];
3719
- });
4493
+ function selectorNotFoundDiagnostic(start) {
4494
+ const serviceOnly = start.servicePath && !start.operation && !start.operationPath && !start.handler;
4495
+ return {
4496
+ severity: "warning",
4497
+ code: "trace_start_not_found",
4498
+ message: serviceOnly ? "Service-only trace requires --operation or --path and will not broaden to the whole workspace" : "No handler source matched the requested trace start selector",
4499
+ selectorKind: start.handler ? "handler" : start.operation || start.operationPath ? "operation" : start.servicePath ? "service" : void 0
4500
+ };
4501
+ }
4502
+ function sourceScopeForSelector(db, repoId, start, workspaceId) {
4503
+ if (start.handler) {
4504
+ const classRows = handlerClassRows(db, repoId, start.handler, workspaceId);
4505
+ if (classRows.length > 0) return handlerClassScope(classRows, start.handler);
4506
+ const methodRows = handlerMethodRows(db, repoId, start.handler, workspaceId);
4507
+ if (methodRows.length > 0)
4508
+ return handlerMethodScope(methodRows, repoId, start.handler);
4509
+ }
4510
+ const operation = normalizeOperation(start.operation ?? start.operationPath);
4511
+ if (!operation) return void 0;
4512
+ const operationRows = operationHandlerRows(
4513
+ db,
4514
+ repoId,
4515
+ operation,
4516
+ start.servicePath,
4517
+ workspaceId
4518
+ );
4519
+ if (operationRows.length > 0)
4520
+ return operationHandlerScope(operationRows, repoId, operation);
4521
+ if (!start.servicePath) return void 0;
4522
+ const implementationRows = implementationHandlerRows(
4523
+ db,
4524
+ repoId,
4525
+ start.servicePath,
4526
+ operation,
4527
+ workspaceId
4528
+ );
4529
+ return implementationRows.length > 0 ? executableScope(implementationRows, repoId) : void 0;
4530
+ }
4531
+ function handlerClassRows(db, repoId, handler, workspaceId) {
4532
+ return db.prepare(`SELECT hc.id handlerClassId,hc.repo_id repoId,
4533
+ r.name repoName,hc.class_name className,
4534
+ hc.source_file sourceFile,hc.source_line sourceLine,hm.id methodId,
4535
+ sym.id symbolId,COALESCE(classSym.evidence_json,
4536
+ (SELECT fallback.evidence_json FROM symbols fallback
4537
+ WHERE fallback.repo_id=hc.repo_id AND fallback.kind='class'
4538
+ AND fallback.name=hc.class_name AND fallback.source_file=hc.source_file
4539
+ ORDER BY fallback.id LIMIT 1)) classEvidence
4540
+ FROM handler_classes hc
4541
+ JOIN repositories r ON r.id=hc.repo_id
4542
+ LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
4543
+ AND sym.source_file=hc.source_file
4544
+ AND sym.kind='method'
4545
+ AND substr(sym.qualified_name,1,length(hc.class_name)+1)=hc.class_name || '.'
4546
+ AND (NOT EXISTS (SELECT 1 FROM handler_methods declared
4547
+ WHERE declared.handler_class_id=hc.id
4548
+ AND declared.method_name=sym.name)
4549
+ OR EXISTS (SELECT 1 FROM handler_methods executable
4550
+ WHERE executable.handler_class_id=hc.id
4551
+ AND executable.method_name=sym.name
4552
+ AND COALESCE(json_extract(executable.decorator_resolution_json,
4553
+ '$.executable'),CASE WHEN executable.decorator_kind IN
4554
+ ('Action','Func','On','Event') THEN 1 ELSE 0 END)=1))
4555
+ LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id
4556
+ AND hm.method_name=sym.name
4557
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
4558
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On','Event')
4559
+ THEN 1 ELSE 0 END)=1
4560
+ LEFT JOIN symbols classSym ON classSym.id=hc.symbol_id
4561
+ WHERE (? IS NULL OR r.workspace_id=?)
4562
+ AND (? IS NULL OR hc.repo_id=?) AND hc.class_name=?
4563
+ ORDER BY hc.repo_id,hc.id,hm.id`).all(
4564
+ workspaceId,
4565
+ workspaceId,
4566
+ repoId,
4567
+ repoId,
4568
+ handler
4569
+ );
4570
+ }
4571
+ function handlerMethodRows(db, repoId, handler, workspaceId) {
4572
+ return db.prepare(`SELECT hc.id handlerClassId,hc.repo_id repoId,
4573
+ r.name repoName,hc.class_name className,hc.source_file sourceFile,
4574
+ hm.source_line sourceLine,hm.id methodId,sym.id symbolId
4575
+ FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id
4576
+ JOIN repositories r ON r.id=hc.repo_id
4577
+ LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
4578
+ AND sym.source_file=hc.source_file
4579
+ AND sym.qualified_name=hc.class_name || '.' || hm.method_name
4580
+ AND sym.start_line=hm.source_line
4581
+ WHERE (? IS NULL OR r.workspace_id=?)
4582
+ AND (? IS NULL OR hc.repo_id=?) AND hm.method_name=?
4583
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
4584
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On','Event')
4585
+ THEN 1 ELSE 0 END)=1
4586
+ ORDER BY hc.repo_id,hc.id,hm.id`).all(
4587
+ workspaceId,
4588
+ workspaceId,
4589
+ repoId,
4590
+ repoId,
4591
+ handler
4592
+ );
4593
+ }
4594
+ function operationHandlerRows(db, repoId, operation, servicePath, workspaceId) {
4595
+ return db.prepare(`SELECT DISTINCT hc.id handlerClassId,hc.repo_id repoId,
4596
+ r.name repoName,hc.class_name className,hc.source_file sourceFile,
4597
+ hm.source_line sourceLine,hm.id methodId,sym.id symbolId
4598
+ FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id
4599
+ JOIN repositories r ON r.id=hc.repo_id
4600
+ LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
4601
+ AND sym.source_file=hc.source_file
4602
+ AND sym.qualified_name=hc.class_name || '.' || hm.method_name
4603
+ AND sym.start_line=hm.source_line
4604
+ WHERE (? IS NULL OR r.workspace_id=?) AND (? IS NULL OR hc.repo_id=?)
4605
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
4606
+ CASE WHEN hm.decorator_kind='Event' THEN 'event'
4607
+ WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
4608
+ ELSE 'unsupported' END)='operation'
4609
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
4610
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On')
4611
+ THEN 1 ELSE 0 END)=1
4612
+ AND (hm.decorator_value=? OR hm.method_name=?)
4613
+ AND (? IS NULL OR EXISTS (
4614
+ SELECT 1 FROM cds_services svc JOIN cds_operations op ON op.service_id=svc.id
4615
+ WHERE svc.repo_id=hc.repo_id AND svc.service_path=?
4616
+ AND (op.operation_path=? OR op.operation_name=?)))
4617
+ ORDER BY hc.repo_id,hc.id,hm.id`).all(
4618
+ workspaceId,
4619
+ workspaceId,
4620
+ repoId,
4621
+ repoId,
4622
+ operation,
4623
+ operation,
4624
+ servicePath,
4625
+ servicePath,
4626
+ operation,
4627
+ operation
4628
+ );
4629
+ }
4630
+ function operationHandlerScope(rows2, fallbackRepoId, requested) {
4631
+ const candidates = handlerSelectorCandidates(rows2, "method");
4632
+ if (candidates.length < 2) return executableScope(rows2, fallbackRepoId);
4633
+ const classes = /* @__PURE__ */ new Map();
4634
+ for (const candidate of candidates) {
4635
+ const key = `${String(candidate.repoName)}:${String(candidate.className)}`;
4636
+ classes.set(key, /* @__PURE__ */ new Set([
4637
+ ...classes.get(key) ?? [],
4638
+ String(candidate.handlerClassId)
4639
+ ]));
4640
+ }
4641
+ const suggestions = candidates.flatMap((candidate) => {
4642
+ if (typeof candidate.repoName !== "string" || typeof candidate.className !== "string") return [];
4643
+ const key = `${candidate.repoName}:${candidate.className}`;
4644
+ return classes.get(key)?.size === 1 ? [`--repo ${candidate.repoName} --handler ${candidate.className}`] : [];
4645
+ });
4646
+ return { diagnostics: [{
4647
+ severity: "warning",
4648
+ code: "trace_start_ambiguous",
4649
+ message: "Operation selector matched multiple handler-only executable scopes",
4650
+ selectorKind: "operation",
4651
+ normalizedSelectorValue: requested,
4652
+ resolutionStage: "handler",
4653
+ resolutionStatus: "ambiguous_handler_operation",
4654
+ candidates,
4655
+ selectorSuggestions: [...new Set(suggestions)].sort(),
4656
+ remediation: "Select one handler class explicitly; no operation was chosen automatically."
4657
+ }] };
4658
+ }
4659
+ function implementationHandlerRows(db, repoId, servicePath, operation, workspaceId) {
4660
+ return db.prepare(`SELECT DISTINCT hc.repo_id repoId,
4661
+ hc.source_file sourceFile,hm.id methodId,sym.id symbolId
4662
+ FROM cds_services svc JOIN cds_operations op ON op.service_id=svc.id
4663
+ JOIN repositories r ON r.id=svc.repo_id
4664
+ JOIN graph_edges edge ON edge.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER'
4665
+ AND edge.status='resolved' AND edge.from_kind='operation'
4666
+ AND edge.from_id=CAST(op.id AS TEXT)
4667
+ JOIN handler_methods hm ON hm.id=CAST(edge.to_id AS INTEGER)
4668
+ JOIN handler_classes hc ON hc.id=hm.handler_class_id
4669
+ LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
4670
+ AND sym.source_file=hc.source_file
4671
+ AND sym.qualified_name=hc.class_name || '.' || hm.method_name
4672
+ AND sym.start_line=hm.source_line
4673
+ WHERE (? IS NULL OR r.workspace_id=?)
4674
+ AND (? IS NULL OR svc.repo_id=?) AND svc.service_path=?
4675
+ AND (op.operation_path=? OR op.operation_name=?)
4676
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
4677
+ CASE WHEN hm.decorator_kind='Event' THEN 'event'
4678
+ WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
4679
+ ELSE 'unsupported' END)='operation'
4680
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
4681
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On')
4682
+ THEN 1 ELSE 0 END)=1
4683
+ ORDER BY hc.repo_id,hc.id,hm.id`).all(
4684
+ workspaceId,
4685
+ workspaceId,
4686
+ repoId,
4687
+ repoId,
4688
+ servicePath,
4689
+ operation,
4690
+ operation
4691
+ );
4692
+ }
4693
+ function handlerClassScope(rows2, requested) {
4694
+ const ambiguity = handlerSelectorAmbiguity(rows2, requested, "class");
4695
+ if (ambiguity) return { diagnostics: [ambiguity] };
4696
+ const executable = rows2.filter((row) => typeof row.symbolId === "number");
4697
+ const repoId = numericValue(rows2[0]?.repoId);
4698
+ if (executable.length > 0) {
4699
+ const scope = executableScope(executable, repoId);
4700
+ const warning = executable.some((row) => typeof row.methodId === "number") ? handlerDecoratorsNotIndexedDiagnostic(rows2[0]) : handlerMethodsNotIndexedDiagnostic(rows2[0]);
4701
+ return warning ? { ...scope, diagnostics: [warning] } : scope;
4702
+ }
4703
+ const first = rows2[0];
4704
+ return {
4705
+ repoId,
4706
+ diagnostics: [handlerMethodsNotIndexedDiagnostic(first)]
4707
+ };
4708
+ }
4709
+ function handlerMethodScope(rows2, fallbackRepoId, requested) {
4710
+ const ambiguity = handlerSelectorAmbiguity(rows2, requested, "method");
4711
+ return ambiguity ? { diagnostics: [ambiguity] } : executableScope(rows2, fallbackRepoId);
4712
+ }
4713
+ function handlerSelectorAmbiguity(rows2, requested, matchKind) {
4714
+ const candidates = handlerSelectorCandidates(rows2, matchKind);
4715
+ if (candidates.length < 2) return void 0;
4716
+ const repoCounts = /* @__PURE__ */ new Map();
4717
+ for (const candidate of candidates) {
4718
+ if (typeof candidate.repoName !== "string") continue;
4719
+ repoCounts.set(
4720
+ candidate.repoName,
4721
+ (repoCounts.get(candidate.repoName) ?? 0) + 1
4722
+ );
4723
+ }
4724
+ const suggestions = candidates.flatMap((candidate) => {
4725
+ const repoName = typeof candidate.repoName === "string" ? candidate.repoName : void 0;
4726
+ if (repoName && repoCounts.get(repoName) === 1)
4727
+ return [`--repo ${repoName} --handler ${requested}`];
4728
+ if (matchKind === "method" && typeof candidate.className === "string")
4729
+ return [`${repoName ? `--repo ${repoName} ` : ""}--handler ${candidate.className}`];
4730
+ return [];
4731
+ });
4732
+ return {
4733
+ severity: "warning",
4734
+ code: "trace_start_ambiguous",
4735
+ message: "Handler selector matched multiple executable scopes and was not selected automatically",
4736
+ selectorKind: "handler",
4737
+ requestedHandler: requested,
4738
+ resolutionStage: "handler",
4739
+ resolutionStatus: "ambiguous_handler",
4740
+ candidates,
4741
+ selectorSuggestions: [...new Set(suggestions)].sort(),
4742
+ remediation: suggestions.length > 0 ? "Use one of the scoped handler selectors shown." : "No current CLI selector uniquely identifies these duplicate handler classes."
4743
+ };
4744
+ }
4745
+ function handlerSelectorCandidates(rows2, matchKind) {
4746
+ const candidates = /* @__PURE__ */ new Map();
4747
+ for (const row of rows2) {
4748
+ const identity = matchKind === "class" ? `class:${String(row.handlerClassId)}` : `method:${String(row.repoId)}:${String(row.symbolId ?? row.methodId)}`;
4749
+ candidates.set(identity, {
4750
+ handlerClassId: row.handlerClassId,
4751
+ repoId: row.repoId,
4752
+ repoName: row.repoName,
4753
+ className: row.className,
4754
+ sourceFile: row.sourceFile,
4755
+ sourceLine: row.sourceLine,
4756
+ matchKind
4757
+ });
4758
+ }
4759
+ return [...candidates.values()].sort((left, right) => String(left.repoName ?? "").localeCompare(String(right.repoName ?? "")) || String(left.className ?? "").localeCompare(String(right.className ?? "")) || String(left.sourceFile ?? "").localeCompare(String(right.sourceFile ?? "")));
4760
+ }
4761
+ function executableScope(rows2, fallbackRepoId) {
4762
+ const files = rows2.flatMap((row) => row.sourceFile ? [row.sourceFile] : []);
4763
+ const symbols = rows2.flatMap((row) => typeof row.symbolId === "number" ? [row.symbolId] : []);
4764
+ if (files.length === 0 || symbols.length === 0) return { repoId: fallbackRepoId };
4765
+ return {
4766
+ files: new Set(files),
4767
+ symbols: new Set(symbols),
4768
+ repoId: numericValue(rows2[0]?.repoId) ?? fallbackRepoId
4769
+ };
4770
+ }
4771
+ function handlerMethodsNotIndexedDiagnostic(row) {
4772
+ return {
4773
+ severity: "warning",
4774
+ code: "handler_methods_not_indexed",
4775
+ message: `Handler class ${row?.className ?? "unknown"} has no indexed executable methods`,
4776
+ selectorKind: "handler",
4777
+ className: row?.className,
4778
+ sourceFile: row?.sourceFile,
4779
+ sourceLine: row?.sourceLine,
4780
+ observedDecoratorNames: stringEvidenceArray(
4781
+ row?.classEvidence,
4782
+ "observedDecoratorNames"
4783
+ ),
4784
+ unsupportedDecoratorNames: stringEvidenceArray(
4785
+ row?.classEvidence,
4786
+ "unsupportedDecoratorNames"
4787
+ ),
4788
+ remediation: "Use a supported CAP handler decorator on at least one class method and re-index the workspace."
4789
+ };
4790
+ }
4791
+ function handlerDecoratorsNotIndexedDiagnostic(row) {
4792
+ const names = stringEvidenceArray(
4793
+ row?.classEvidence,
4794
+ "unsupportedDecoratorNames"
4795
+ );
4796
+ const methods = arrayEvidence(row?.classEvidence, "unsupportedMethods");
4797
+ if (names.length === 0 && methods.length === 0) return void 0;
4798
+ return {
4799
+ severity: "warning",
4800
+ code: "handler_decorators_not_indexed",
4801
+ message: `Handler class ${row?.className ?? "unknown"} contains methods that were not indexed`,
4802
+ selectorKind: "handler",
4803
+ className: row?.className,
4804
+ sourceFile: row?.sourceFile,
4805
+ sourceLine: row?.sourceLine,
4806
+ unsupportedDecoratorNames: names,
4807
+ unsupportedMethods: methods,
4808
+ remediation: "Use a supported CAP handler decorator shape and re-index the workspace."
4809
+ };
4810
+ }
4811
+ function evidenceRecord(evidenceJson) {
4812
+ if (!evidenceJson) return {};
4813
+ try {
4814
+ const parsed = JSON.parse(evidenceJson);
4815
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4816
+ } catch {
4817
+ return {};
4818
+ }
4819
+ }
4820
+ function stringEvidenceArray(evidenceJson, key) {
4821
+ const value = evidenceRecord(evidenceJson)[key];
4822
+ return Array.isArray(value) ? [...new Set(value.filter((item) => typeof item === "string"))].sort() : [];
4823
+ }
4824
+ function arrayEvidence(evidenceJson, key) {
4825
+ const value = evidenceRecord(evidenceJson)[key];
4826
+ return Array.isArray(value) ? value.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
4827
+ }
4828
+ function numericValue(value) {
4829
+ return typeof value === "number" ? value : void 0;
4830
+ }
4831
+ function normalizeOperation(value) {
4832
+ if (!value) return void 0;
4833
+ return value.startsWith("/") ? value.slice(1) : value;
4834
+ }
4835
+ function ambiguousStartDiagnostic(requested, candidates, message) {
4836
+ const serviceSuggestions = [...new Set(candidates.flatMap((row) => typeof row.servicePath === "string" ? [`--service ${row.servicePath}`] : []))].sort();
4837
+ return {
4838
+ severity: "warning",
4839
+ code: "trace_start_ambiguous",
4840
+ message,
4841
+ normalizedSelectorValue: requested,
4842
+ resolutionStage: "operation",
4843
+ resolutionStatus: "ambiguous_operation",
4844
+ candidates,
4845
+ serviceSuggestions,
4846
+ selectorSuggestions: fullSelectorSuggestions(candidates)
4847
+ };
4848
+ }
4849
+ function fullSelectorSuggestions(candidates) {
4850
+ const includeRepo = new Set(candidates.map((row) => row.repoName)).size > 1;
4851
+ return [...new Set(candidates.flatMap((row) => {
4852
+ if (typeof row.servicePath !== "string" || typeof row.operationPath !== "string") return [];
4853
+ const repoSelector = includeRepo && typeof row.repoName === "string" ? `--repo ${row.repoName} ` : "";
4854
+ return [
4855
+ `${repoSelector}--service ${row.servicePath} --path ${row.operationPath}`
4856
+ ];
4857
+ }))].sort();
4858
+ }
4859
+
4860
+ // src/trace/001-dynamic-identity.ts
4861
+ function uniqueIdentityDerivations(db, candidates, templates) {
4862
+ const identities = workspaceIdentities(db, candidates);
4863
+ const proposals = candidates.flatMap((candidate) => {
4864
+ const owner = implementationOwner(db, candidate.candidateOperationId);
4865
+ const identity = identities.find((item) => item.repoId === candidate.repoId);
4866
+ return ownerAgrees(candidate, owner) && identity ? identityProposals(candidate, identity, templates) : [];
4867
+ });
4868
+ const matches2 = workspaceIdentityMatches(identities, templates);
4869
+ const competing = competingIdentityKeys(matches2);
4870
+ const duplicates = duplicateNormalizedIdentities(identities);
4871
+ return proposals.filter((proposal) => !competing.has(`${proposal.key}:${proposal.value}`)).filter((proposal) => !duplicates.has(proposal.normalizedIdentity)).map((proposal) => ({
4872
+ operationId: proposal.operationId,
4873
+ key: proposal.key,
4874
+ value: proposal.value,
4875
+ provenance: proposal.provenance
4876
+ }));
4877
+ }
4878
+ function identityProposals(candidate, identity, templates) {
4879
+ const routeTemplates = [templates.alias, templates.destination].filter((value) => Boolean(value));
4880
+ const identities = [
4881
+ { name: identity.packageName, sourceKind: "package_identity", npmPackage: true },
4882
+ { name: identity.repoName, sourceKind: "repository_identity", npmPackage: false }
4883
+ ].filter((item) => Boolean(item.name));
4884
+ const proposals = routeTemplates.flatMap((template) => identities.flatMap((identity2) => proposalForIdentity(
4885
+ candidate,
4886
+ template,
4887
+ identity2.name,
4888
+ identity2.sourceKind,
4889
+ identity2.npmPackage
4890
+ )));
4891
+ return deduplicateProposals(proposals);
4892
+ }
4893
+ function proposalForIdentity(candidate, template, identity, sourceKind, npmPackage) {
4894
+ const match = matchIdentityTemplate(template, identity, npmPackage);
4895
+ if (!match) return [];
4896
+ return [{
4897
+ operationId: candidate.candidateOperationId,
4898
+ key: match.key,
4899
+ value: match.value,
4900
+ normalizedIdentity: match.normalizedIdentity,
4901
+ provenance: {
4902
+ sourceKind,
4903
+ value: match.value,
4904
+ rule: "exact_normalized_identity_template_match",
4905
+ template,
4906
+ matchedName: identity,
4907
+ normalizedForm: match.normalizedIdentity,
4908
+ sourceRepo: candidate.repoName
4909
+ }
4910
+ }];
4911
+ }
4912
+ function matchIdentityTemplate(template, identity, npmPackage) {
4913
+ const matches2 = [...template.matchAll(/\$\{([^}]*)\}/g)];
4914
+ if (matches2.length !== 1 || !matches2[0]?.[1]) return void 0;
4915
+ const placeholder = matches2[0][0];
4916
+ const sentinel = "dynamicplaceholdertoken";
4917
+ const normalizedTemplate = normalizeIdentity(template.replace(placeholder, sentinel));
4918
+ const [prefix, suffix, extra] = normalizedTemplate.split(sentinel);
4919
+ if (!prefix || !suffix || extra !== void 0) return void 0;
4920
+ const normalizedIdentity = normalizeIdentity(identity, npmPackage);
4921
+ const match = new RegExp(`^${escapeRegex(prefix)}([a-z0-9]+)${escapeRegex(suffix)}$`).exec(normalizedIdentity);
4922
+ if (!match?.[1]) return void 0;
4923
+ return { key: matches2[0][1].trim(), value: match[1], normalizedIdentity };
4924
+ }
4925
+ function competingIdentityKeys(proposals) {
4926
+ const owners = /* @__PURE__ */ new Map();
4927
+ for (const proposal of proposals) {
4928
+ const key = `${proposal.key}:${proposal.value}`;
4929
+ owners.set(key, /* @__PURE__ */ new Set([...owners.get(key) ?? [], proposal.ownerKey]));
4930
+ }
4931
+ return new Set([...owners.entries()].filter(([, repos]) => repos.size > 1).map(([key]) => key));
4932
+ }
4933
+ function duplicateNormalizedIdentities(identities) {
4934
+ const owners = /* @__PURE__ */ new Map();
4935
+ for (const identity of identities) {
4936
+ for (const [name, npmPackage] of [
4937
+ [identity.packageName, true],
4938
+ [identity.repoName, false]
4939
+ ]) {
4940
+ if (!name) continue;
4941
+ const normalized = normalizeIdentity(name, npmPackage);
4942
+ owners.set(normalized, /* @__PURE__ */ new Set([
4943
+ ...owners.get(normalized) ?? [],
4944
+ `repository:${identity.repoId}`
4945
+ ]));
4946
+ }
4947
+ }
4948
+ return new Set([...owners.entries()].filter(([, repos]) => repos.size > 1).map(([identity]) => identity));
4949
+ }
4950
+ function workspaceIdentityMatches(identities, templates) {
4951
+ const routeTemplates = [templates.alias, templates.destination].filter((value) => Boolean(value));
4952
+ return identities.flatMap((identity) => {
4953
+ const names = [
4954
+ { name: identity.packageName, npmPackage: true },
4955
+ { name: identity.repoName, npmPackage: false }
4956
+ ];
4957
+ return routeTemplates.flatMap((template) => names.flatMap(({ name, npmPackage }) => {
4958
+ if (!name) return [];
4959
+ const match = matchIdentityTemplate(template, name, npmPackage);
4960
+ return match ? [{
4961
+ ownerKey: `repository:${identity.repoId}`,
4962
+ key: match.key,
4963
+ value: match.value,
4964
+ normalizedIdentity: match.normalizedIdentity
4965
+ }] : [];
4966
+ }));
4967
+ });
4968
+ }
4969
+ function workspaceIdentities(db, candidates) {
4970
+ const repoIds = [...new Set(candidates.flatMap((candidate) => candidate.repoId === void 0 ? [] : [candidate.repoId]))].sort((a, b) => a - b);
4971
+ if (repoIds.length === 0) return [];
4972
+ const placeholders3 = repoIds.map(() => "?").join(",");
4973
+ const rows2 = db.prepare(`SELECT id repoId,name repoName,package_name packageName
4974
+ FROM repositories WHERE workspace_id IN (
4975
+ SELECT DISTINCT workspace_id FROM repositories WHERE id IN (${placeholders3})
4976
+ ) ORDER BY workspace_id,name,absolute_path,id`).all(...repoIds);
4977
+ return rows2.flatMap((row) => {
4978
+ const repoId = numberValue(row.repoId);
4979
+ const repoName = stringValue4(row.repoName);
4980
+ return repoId === void 0 || !repoName ? [] : [{
4981
+ repoId,
4982
+ repoName,
4983
+ packageName: stringValue4(row.packageName)
4984
+ }];
4985
+ });
4986
+ }
4987
+ function deduplicateProposals(rows2) {
4988
+ const sorted = [...rows2].sort((left, right) => left.operationId - right.operationId || left.key.localeCompare(right.key) || left.value.localeCompare(right.value) || left.provenance.sourceKind.localeCompare(right.provenance.sourceKind));
4989
+ const seen = /* @__PURE__ */ new Set();
4990
+ return sorted.filter((row) => {
4991
+ const key = [row.operationId, row.key, row.value, row.normalizedIdentity].join(":");
4992
+ if (seen.has(key)) return false;
4993
+ seen.add(key);
4994
+ return true;
4995
+ });
4996
+ }
4997
+ function ownerAgrees(candidate, owner) {
4998
+ return candidate.repoId !== void 0 && owner?.repoId !== void 0 && owner.repoId === candidate.repoId;
4999
+ }
5000
+ function implementationOwner(db, operationId) {
5001
+ const rows2 = db.prepare(
5002
+ `SELECT r.id repoId
5003
+ FROM graph_edges e JOIN handler_methods hm ON hm.id=CAST(e.to_id AS INTEGER)
5004
+ JOIN handler_classes hc ON hc.id=hm.handler_class_id
5005
+ JOIN repositories r ON r.id=hc.repo_id
5006
+ WHERE e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status='resolved'
5007
+ AND e.from_kind='operation' AND e.from_id=?
5008
+ ORDER BY r.id,hm.id,e.id`
5009
+ ).all(String(operationId));
5010
+ if (rows2.length !== 1) return void 0;
5011
+ const row = rows2[0];
5012
+ if (!row) return void 0;
5013
+ return {
5014
+ repoId: numberValue(row.repoId)
5015
+ };
5016
+ }
5017
+ function normalizeIdentity(value, npmPackage = false) {
5018
+ const unscoped = npmPackage && /^@[^/]+\/[^/]+$/.test(value) ? value.slice(value.indexOf("/") + 1) : value;
5019
+ return unscoped.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
5020
+ }
5021
+ function stringValue4(value) {
5022
+ return typeof value === "string" ? value : void 0;
5023
+ }
5024
+ function numberValue(value) {
5025
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
5026
+ }
5027
+ function escapeRegex(value) {
5028
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5029
+ }
5030
+
5031
+ // src/trace/003-dynamic-references.ts
5032
+ function dynamicReferenceRows(db, workspaceId, callerRepoId, callerRepo) {
5033
+ if (callerRepoId === void 0 && callerRepo === void 0) return [];
5034
+ const rows2 = db.prepare(
5035
+ `SELECT COALESCE(b.alias,b.alias_expr) alias,b.destination_expr destination,
5036
+ b.service_path_expr servicePath,'service_binding' sourceKind,r.name repoName,
5037
+ b.source_file sourceFile,b.source_line sourceLine,0 sourcePriority
5038
+ FROM service_bindings b JOIN repositories r ON r.id=b.repo_id
5039
+ WHERE (? IS NULL OR r.workspace_id=?)
5040
+ AND ((? IS NOT NULL AND r.id=?) OR (? IS NULL AND r.name=?))
5041
+ UNION ALL
5042
+ SELECT req.alias,req.destination,req.service_path,'cds_require',r.name,
5043
+ 'package.json',1,1 FROM cds_requires req JOIN repositories r ON r.id=req.repo_id
5044
+ WHERE (? IS NULL OR r.workspace_id=?)
5045
+ AND ((? IS NOT NULL AND r.id=?) OR (? IS NULL AND r.name=?))
5046
+ ORDER BY sourcePriority,repoName,sourceFile,sourceLine`
5047
+ ).all(
5048
+ workspaceId,
5049
+ workspaceId,
5050
+ callerRepoId,
5051
+ callerRepoId,
5052
+ callerRepoId,
5053
+ callerRepo,
5054
+ workspaceId,
5055
+ workspaceId,
5056
+ callerRepoId,
5057
+ callerRepoId,
5058
+ callerRepoId,
5059
+ callerRepo
5060
+ );
5061
+ return rows2.flatMap(referenceFromRow);
5062
+ }
5063
+ function dynamicReferenceProvenance(reference, kind, template, value) {
5064
+ return {
5065
+ sourceKind: `${reference.sourceKind}.${kind}`,
5066
+ value,
5067
+ rule: "exact_indexed_reference_template_match",
5068
+ template,
5069
+ sourceRepo: reference.repoName,
5070
+ sourceFile: reference.sourceFile,
5071
+ sourceLine: reference.sourceLine
5072
+ };
5073
+ }
5074
+ function referenceFromRow(row) {
5075
+ const sourceKind = row.sourceKind;
5076
+ const repoName = stringValue5(row.repoName);
5077
+ if (sourceKind !== "service_binding" && sourceKind !== "cds_require" || !repoName) return [];
5078
+ return [{
5079
+ alias: stringValue5(row.alias),
5080
+ destination: stringValue5(row.destination),
5081
+ servicePath: stringValue5(row.servicePath),
5082
+ sourceKind,
5083
+ repoName,
5084
+ sourceFile: stringValue5(row.sourceFile),
5085
+ sourceLine: numberValue2(row.sourceLine)
5086
+ }];
5087
+ }
5088
+ function stringValue5(value) {
5089
+ return typeof value === "string" ? value : void 0;
5090
+ }
5091
+ function numberValue2(value) {
5092
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
5093
+ }
5094
+
5095
+ // src/trace/dynamic-targets.ts
5096
+ function analyzeDynamicTargetCandidates(db, evidence, workspaceId, mode, maxCandidates) {
5097
+ const inputs = analysisInputs(evidence);
5098
+ if (inputs.required.length === 0) return void 0;
5099
+ const targets = candidateTargets(db, evidence, workspaceId);
5100
+ const references = dynamicReferenceRows(
5101
+ db,
5102
+ workspaceId,
5103
+ inputs.callerRepoId,
5104
+ inputs.callerRepo
5105
+ );
5106
+ const candidates = buildCandidates(db, targets, references, inputs);
5107
+ applyUniqueIdentityEvidence(db, candidates, inputs);
5108
+ finalizeCandidates(candidates, inputs.order);
5109
+ const ranked = stableRank(candidates);
5110
+ const inference = inferenceDecision(ranked);
5111
+ applyModeState(ranked, mode, inference);
5112
+ const viable = ranked.filter((candidate) => candidate.viable);
5113
+ const rejected = ranked.filter((candidate) => candidate.rejected);
5114
+ const shown = viable.slice(0, maxCandidates).map((candidate) => boundedCandidate(candidate, maxCandidates));
5115
+ const shownRejected = rejected.slice(0, maxCandidates).map((candidate) => boundedCandidate(candidate, maxCandidates));
5116
+ return {
5117
+ mode,
5118
+ maxCandidates,
5119
+ candidateCount: ranked.length,
5120
+ viableCandidateCount: viable.length,
5121
+ rejectedCandidateCount: rejected.length,
5122
+ shownCandidateCount: shown.length,
5123
+ omittedCandidateCount: Math.max(0, viable.length - shown.length),
5124
+ shownRejectedCandidateCount: shownRejected.length,
5125
+ omittedRejectedCandidateCount: Math.max(0, rejected.length - shownRejected.length),
5126
+ missingVariables: inputs.required.filter((key) => inputs.supplied[key] === void 0),
5127
+ requiredVariables: inputs.required,
5128
+ suppliedVariables: inputs.supplied,
5129
+ appliedSuppliedVariables: requiredSuppliedVariables(inputs),
5130
+ substitutedSignals: inputs.effective,
5131
+ candidates: shown,
5132
+ shownCandidates: shown,
5133
+ rejectedCandidates: shownRejected,
5134
+ suggestedVarSets: suggestedVarSets(viable, inputs.order, maxCandidates),
5135
+ inference
5136
+ };
5137
+ }
5138
+ function analysisInputs(evidence) {
5139
+ const original = templatesFromEvidence(evidence, "original");
5140
+ const effective = templatesFromEvidence(evidence, "effective");
5141
+ const requiredSources = placeholderSources(original);
5142
+ const required = Object.keys(requiredSources);
5143
+ const supplied = stringRecord(evidence.suppliedRuntimeVariables);
5144
+ return {
5145
+ original,
5146
+ effective,
5147
+ required,
5148
+ requiredSources,
5149
+ supplied,
5150
+ order: variableOrder(original, required),
5151
+ callerRepo: stringValue6(evidence.repo),
5152
+ callerRepoId: numberValue3(evidence.repoId)
5153
+ };
5154
+ }
5155
+ function candidateTargets(db, evidence, workspaceId) {
5156
+ const embedded = rowsFromEvidence(evidence.candidates);
5157
+ if (embedded.length > 0) return embedded;
5158
+ const operationPath = effectiveSignal(evidence, "operationPath");
5159
+ if (!operationPath || extractPlaceholders(operationPath).length > 0) return [];
5160
+ return queryOperationTargets(db, operationPath, workspaceId);
5161
+ }
5162
+ function rowsFromEvidence(value) {
5163
+ if (!Array.isArray(value)) return [];
5164
+ return value.flatMap((item) => {
5165
+ const row = record(item);
5166
+ const operationId = numberValue3(row.operationId);
5167
+ const repoName = stringValue6(row.repoName);
5168
+ const servicePath = stringValue6(row.servicePath);
5169
+ const operationPath = stringValue6(row.operationPath);
5170
+ const operationName = stringValue6(row.operationName) ?? operationPath?.replace(/^\//, "");
5171
+ if (operationId === void 0 || !repoName || !servicePath || !operationPath || !operationName) return [];
5172
+ return [{
5173
+ operationId,
5174
+ repoId: numberValue3(row.repoId),
5175
+ repoName,
5176
+ packageName: stringValue6(row.packageName),
5177
+ serviceName: stringValue6(row.serviceName) ?? "",
5178
+ qualifiedName: stringValue6(row.qualifiedName) ?? "",
5179
+ servicePath,
5180
+ operationPath,
5181
+ operationName,
5182
+ sourceFile: stringValue6(row.sourceFile) ?? "",
5183
+ sourceLine: numberValue3(row.sourceLine) ?? 0,
5184
+ score: numberValue3(row.score) ?? 0,
5185
+ reasons: stringArray(row.reasons)
5186
+ }];
5187
+ });
3720
5188
  }
3721
5189
  function queryOperationTargets(db, operationPath, workspaceId) {
3722
5190
  const simple = operationPath.replace(/^\//, "").split(".").at(-1) ?? operationPath;
@@ -3724,169 +5192,291 @@ function queryOperationTargets(db, operationPath, workspaceId) {
3724
5192
  `SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,
3725
5193
  s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,
3726
5194
  o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,
3727
- o.source_line sourceLine
3728
- FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
3729
- JOIN repositories r ON r.id=s.repo_id
5195
+ o.source_line sourceLine FROM cds_operations o
5196
+ JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
3730
5197
  WHERE (? IS NULL OR r.workspace_id=?)
3731
5198
  AND (o.operation_path IN (?,?) OR o.operation_name=?)
3732
- ORDER BY r.name,s.service_path,o.operation_name`
5199
+ ORDER BY r.name,s.service_path,o.operation_name,o.id`
3733
5200
  ).all(workspaceId, workspaceId, operationPath, `/${simple}`, simple);
3734
- return rows2.map((row) => ({
3735
- operationId: Number(row.operationId),
3736
- repoId: numberValue(row.repoId),
3737
- repoName: String(row.repoName),
3738
- packageName: stringValue4(row.packageName),
3739
- serviceName: String(row.serviceName),
3740
- qualifiedName: String(row.qualifiedName),
3741
- servicePath: String(row.servicePath),
3742
- operationPath: String(row.operationPath),
3743
- operationName: String(row.operationName),
3744
- sourceFile: String(row.sourceFile),
3745
- sourceLine: Number(row.sourceLine),
5201
+ return rows2.flatMap(operationTargetFromRow);
5202
+ }
5203
+ function operationTargetFromRow(row) {
5204
+ const operationId = numberValue3(row.operationId);
5205
+ const repoName = stringValue6(row.repoName);
5206
+ const servicePath = stringValue6(row.servicePath);
5207
+ const operationPath = stringValue6(row.operationPath);
5208
+ const operationName = stringValue6(row.operationName);
5209
+ if (operationId === void 0 || !repoName || !servicePath || !operationPath || !operationName) return [];
5210
+ return [{
5211
+ operationId,
5212
+ repoId: numberValue3(row.repoId),
5213
+ repoName,
5214
+ packageName: stringValue6(row.packageName),
5215
+ serviceName: stringValue6(row.serviceName) ?? "",
5216
+ qualifiedName: stringValue6(row.qualifiedName) ?? "",
5217
+ servicePath,
5218
+ operationPath,
5219
+ operationName,
5220
+ sourceFile: stringValue6(row.sourceFile) ?? "",
5221
+ sourceLine: numberValue3(row.sourceLine) ?? 0,
3746
5222
  score: 0.2,
3747
5223
  reasons: ["operation_path_match"]
3748
- }));
5224
+ }];
5225
+ }
5226
+ function buildCandidates(db, targets, references, inputs) {
5227
+ return targets.map((target) => {
5228
+ const state = emptyCandidate(target, inputs);
5229
+ applyDirectSignal(state, inputs, "operationPath", target.operationPath, 0.25);
5230
+ applyDirectSignal(state, inputs, "servicePath", target.servicePath, 0.35);
5231
+ const matchingReferences = references.filter((reference) => reference.servicePath === target.servicePath);
5232
+ applyReferenceSignal(state, inputs, matchingReferences, "alias");
5233
+ applyReferenceSignal(state, inputs, matchingReferences, "destination");
5234
+ if (hasResolvedImplementation(db, target.operationId))
5235
+ addScore(state, 0.1, "implementation_edge_resolved");
5236
+ return state;
5237
+ });
3749
5238
  }
3750
- function rankedCandidates(db, candidates, references, templates, order) {
3751
- const ranked = candidates.map((candidate) => candidateEvidence2(db, candidate, references, templates, order));
3752
- return ranked.sort((a, b) => b.score - a.score || a.repoName.localeCompare(b.repoName) || a.servicePath.localeCompare(b.servicePath));
3753
- }
3754
- function candidateEvidence2(db, candidate, references, templates, order) {
3755
- const state = emptyCandidate(candidate);
3756
- applyDirectTemplate(state, templates.operationPath, candidate.operationPath, "operation_path");
3757
- applyDirectTemplate(state, templates.servicePath, candidate.servicePath, "service_path");
3758
- const refs = references.filter((item) => item.servicePath === candidate.servicePath);
3759
- applyReferenceTemplate(state, templates.alias, refs, "alias");
3760
- applyReferenceTemplate(state, templates.destination, refs, "destination");
3761
- if (hasResolvedImplementation(db, candidate.operationId)) addScore(state, 0.1, "implementation_edge_resolved");
3762
- state.missingVariables = order.filter((key) => state.derivedVariables[key] === void 0);
3763
- if (state.missingVariables.length === 0) addScore(state, 0.15, "all_runtime_variables_derived");
3764
- else state.rejectedReasons.push("missing_required_runtime_variable");
3765
- state.score = Math.max(0, Math.min(1, state.score));
3766
- state.cli = state.missingVariables.length === 0 ? cliFor(state.derivedVariables, order) : void 0;
3767
- return state;
3768
- }
3769
- function emptyCandidate(candidate) {
5239
+ function emptyCandidate(target, inputs) {
3770
5240
  return {
3771
- candidateOperationId: candidate.operationId,
3772
- repoName: candidate.repoName,
3773
- packageName: candidate.packageName ?? void 0,
3774
- servicePath: candidate.servicePath,
3775
- operationPath: candidate.operationPath,
3776
- operationName: candidate.operationName,
5241
+ candidateOperationId: target.operationId,
5242
+ repoId: target.repoId,
5243
+ repoName: target.repoName,
5244
+ packageName: target.packageName ?? void 0,
5245
+ serviceName: target.serviceName,
5246
+ qualifiedName: target.qualifiedName,
5247
+ servicePath: target.servicePath,
5248
+ operationPath: target.operationPath,
5249
+ operationName: target.operationName,
5250
+ sourceFile: target.sourceFile,
5251
+ sourceLine: target.sourceLine,
5252
+ originalTemplates: inputs.original,
5253
+ effectiveValues: inputs.effective,
5254
+ requiredVariables: inputs.required,
5255
+ requiredVariableSources: inputs.requiredSources,
5256
+ suppliedVariables: inputs.supplied,
5257
+ completeVariables: { ...inputs.supplied },
3777
5258
  derivedVariables: {},
3778
5259
  derivedVariableSources: {},
5260
+ derivationProvenance: {},
3779
5261
  missingVariables: [],
3780
- score: Math.max(0.2, Number(candidate.score ?? 0)),
3781
- reasons: nonEmptyStrings(candidate.reasons, ["operation_path_match"]),
3782
- rejectedReasons: []
5262
+ conflicts: [],
5263
+ score: Math.max(0.2, Number(target.score ?? 0)),
5264
+ explicitSignalStrength: 0,
5265
+ reasons: nonEmptyStrings(target.reasons, ["operation_path_match"]),
5266
+ rejectedReasons: [],
5267
+ inferenceBlockReasons: [],
5268
+ viable: true,
5269
+ rejected: false,
5270
+ selected: false,
5271
+ exploratory: false
3783
5272
  };
3784
5273
  }
3785
- function applyDirectTemplate(state, template, concrete, kind) {
3786
- const matched = matchTemplate(template, concrete);
3787
- if (!template) return;
3788
- if (!matched) {
3789
- state.rejectedReasons.push(`${kind}_template_mismatch`);
5274
+ function applyDirectSignal(state, inputs, kind, concrete, score) {
5275
+ const effective = inputs.effective[kind];
5276
+ const original = inputs.original[kind];
5277
+ if (effective && !matchTemplate(effective, concrete)) {
5278
+ reject(state, `${signalCode(kind)}_contradicts_runtime_substitution`);
3790
5279
  return;
3791
5280
  }
3792
- mergeDerived(state, matched, `${kind}_template`);
3793
- addScore(state, kind === "service_path" ? 0.35 : 0.25, `${kind}_template_match`);
5281
+ if (!effective) return;
5282
+ const suppliedKeys = extractPlaceholders(original).filter((key) => inputs.supplied[key] !== void 0);
5283
+ state.explicitSignalStrength += suppliedKeys.length;
5284
+ const matched = matchTemplate(original, concrete) ?? {};
5285
+ for (const [key, value] of Object.entries(matched)) {
5286
+ addDerivation(state, key, value, {
5287
+ sourceKind: `${signalCode(kind)}_template`,
5288
+ value,
5289
+ rule: "exact_template_match",
5290
+ template: original
5291
+ });
5292
+ }
5293
+ addScore(state, score, `${signalCode(kind)}_template_match`);
3794
5294
  }
3795
- function applyReferenceTemplate(state, template, refs, kind) {
3796
- if (!template || extractPlaceholders(template).length === 0) return;
3797
- for (const ref of refs) {
3798
- const concrete = kind === "alias" ? ref.alias : ref.destination;
3799
- const matched = isConcrete(concrete) ? matchTemplate(template, concrete) : void 0;
5295
+ function applyReferenceSignal(state, inputs, references, kind) {
5296
+ const original = inputs.original[kind];
5297
+ const effective = inputs.effective[kind];
5298
+ if (!original || extractPlaceholders(original).length === 0) return;
5299
+ const values = references.flatMap((reference) => {
5300
+ const concrete = kind === "alias" ? reference.alias : reference.destination;
5301
+ return isConcrete(concrete) ? [{ reference, concrete }] : [];
5302
+ });
5303
+ if (effective && extractPlaceholders(effective).length === 0 && values.length > 0 && !values.some(({ concrete }) => concrete === effective)) {
5304
+ reject(state, `${kind}_contradicts_runtime_substitution`);
5305
+ }
5306
+ let matchedSignal = false;
5307
+ for (const { reference, concrete } of values) {
5308
+ const matched = matchTemplate(original, concrete);
3800
5309
  if (!matched) continue;
3801
- mergeDerived(state, matched, `${ref.sourceKind}.${kind}`);
5310
+ matchedSignal = true;
5311
+ for (const [key, value] of Object.entries(matched)) {
5312
+ addDerivation(
5313
+ state,
5314
+ key,
5315
+ value,
5316
+ dynamicReferenceProvenance(reference, kind, original, value)
5317
+ );
5318
+ }
5319
+ }
5320
+ if (matchedSignal) {
5321
+ state.explicitSignalStrength += extractPlaceholders(original).filter((key) => inputs.supplied[key] !== void 0).length;
3802
5322
  addScore(state, 0.2, `${kind}_template_match`);
5323
+ }
5324
+ }
5325
+ function applyUniqueIdentityEvidence(db, candidates, inputs) {
5326
+ for (const derivation of uniqueIdentityDerivations(db, candidates, inputs.original)) {
5327
+ const candidate = candidates.find((item) => item.candidateOperationId === derivation.operationId);
5328
+ if (!candidate) continue;
5329
+ addDerivation(candidate, derivation.key, derivation.value, derivation.provenance);
5330
+ addScore(candidate, 0.2, "exact_identity_template_match");
5331
+ }
5332
+ }
5333
+ function addDerivation(state, key, value, provenance) {
5334
+ const priorProvenance = state.derivationProvenance[key] ?? [];
5335
+ state.derivationProvenance[key] = uniqueProvenance([...priorProvenance, provenance]);
5336
+ const supplied = state.suppliedVariables[key];
5337
+ if (supplied !== void 0 && supplied !== value) {
5338
+ addConflict(state, key, [supplied, value], "explicit_value_conflicts_with_derived_value");
5339
+ return;
5340
+ }
5341
+ const prior = state.derivedVariables[key];
5342
+ if (prior !== void 0 && prior !== value) {
5343
+ addConflict(state, key, [prior, value], "conflicting_strong_derivations");
3803
5344
  return;
3804
5345
  }
3805
- if (refs.some((ref) => isConcrete(kind === "alias" ? ref.alias : ref.destination)))
3806
- state.rejectedReasons.push(`${kind}_template_mismatch`);
5346
+ if (supplied === void 0) state.derivedVariables[key] = value;
5347
+ state.completeVariables[key] = supplied ?? value;
5348
+ state.derivedVariableSources[key] ??= provenance;
5349
+ }
5350
+ function addConflict(state, key, values, reason) {
5351
+ const sources = (state.derivationProvenance[key] ?? []).map((item) => item.sourceKind).sort();
5352
+ state.conflicts.push({ key, values: [...new Set(values)].sort(), reason, sources });
5353
+ reject(state, reason);
5354
+ }
5355
+ function uniqueProvenance(rows2) {
5356
+ const sorted = [...rows2].sort((left, right) => left.sourceKind.localeCompare(right.sourceKind) || String(left.matchedName ?? "").localeCompare(String(right.matchedName ?? "")) || left.value.localeCompare(right.value));
5357
+ const seen = /* @__PURE__ */ new Set();
5358
+ return sorted.filter((row) => {
5359
+ const key = JSON.stringify(row);
5360
+ if (seen.has(key)) return false;
5361
+ seen.add(key);
5362
+ return true;
5363
+ });
5364
+ }
5365
+ function finalizeCandidates(candidates, order) {
5366
+ for (const candidate of candidates) {
5367
+ candidate.missingVariables = order.filter((key) => candidate.completeVariables[key] === void 0);
5368
+ candidate.viable = candidate.rejectedReasons.length === 0;
5369
+ candidate.rejected = !candidate.viable;
5370
+ if (candidate.missingVariables.length === 0 && candidate.viable)
5371
+ addScore(candidate, 0.15, "all_runtime_variables_derived");
5372
+ else if (candidate.missingVariables.length > 0)
5373
+ addReason(candidate, "missing_required_runtime_variable");
5374
+ candidate.score = Math.max(0, Math.min(1, candidate.score));
5375
+ candidate.cli = candidate.missingVariables.length === 0 && candidate.viable ? cliFor(candidate.completeVariables, order) : void 0;
5376
+ }
5377
+ }
5378
+ function stableRank(candidates) {
5379
+ return [...candidates].sort((left, right) => Number(right.viable) - Number(left.viable) || right.score - left.score || right.explicitSignalStrength - left.explicitSignalStrength || left.repoName.localeCompare(right.repoName) || String(left.packageName ?? "").localeCompare(String(right.packageName ?? "")) || left.servicePath.localeCompare(right.servicePath) || left.operationPath.localeCompare(right.operationPath) || left.operationName.localeCompare(right.operationName) || left.candidateOperationId - right.candidateOperationId);
3807
5380
  }
3808
5381
  function inferenceDecision(candidates) {
3809
- const complete = candidates.filter((candidate) => candidate.missingVariables.length === 0 && candidate.rejectedReasons.length === 0);
3810
- const first = complete[0];
3811
- const second = complete[1];
3812
- if (!first) return { status: "unresolved", reason: "missing_required_runtime_variable" };
3813
- if (first.score < 0.85) return { status: "unresolved", reason: "candidate_score_below_inference_threshold" };
3814
- if (second && first.score - second.score <= 0.05) {
3815
- for (const candidate of complete.filter((item) => first.score - item.score <= 0.05))
3816
- candidate.rejectedReasons.push("candidate_tied_with_equal_score");
3817
- return { status: "ambiguous", reason: "candidate_tied_with_equal_score" };
5382
+ const viable = candidates.filter((candidate) => candidate.viable);
5383
+ const first = viable[0];
5384
+ const second = viable[1];
5385
+ if (!first || first.missingVariables.length > 0)
5386
+ return { status: "unresolved", reason: "missing_required_runtime_variable" };
5387
+ if (first.score < 0.85)
5388
+ return { status: "unresolved", reason: "candidate_score_below_inference_threshold" };
5389
+ const scoreGap = second ? Number((first.score - second.score).toFixed(12)) : void 0;
5390
+ if (second && scoreGap !== void 0 && scoreGap <= 0.05) {
5391
+ const reason = scoreGap === 0 ? "candidate_tied_with_equal_score" : "candidate_within_inference_margin";
5392
+ for (const candidate of viable.filter((item) => first.score - item.score <= 0.05))
5393
+ addInferenceBlock(candidate, reason);
5394
+ return { status: "ambiguous", reason, scoreGap, requiredMargin: 0.05 };
3818
5395
  }
3819
5396
  return {
3820
5397
  status: "resolved",
3821
5398
  candidateOperationId: first.candidateOperationId,
3822
- inferredVariables: first.derivedVariables,
5399
+ inferredVariables: first.completeVariables,
3823
5400
  score: first.score,
3824
5401
  reasons: first.reasons
3825
5402
  };
3826
5403
  }
3827
- function suggestedVarSets(candidates, required, order) {
5404
+ function boundedCandidate(candidate, limit) {
5405
+ const derivationProvenance = Object.fromEntries(
5406
+ Object.entries(candidate.derivationProvenance).sort(([left], [right]) => left.localeCompare(right)).map(([key, rows2]) => [key, rows2.slice(0, limit)])
5407
+ );
5408
+ const conflicts = candidate.conflicts.slice(0, limit).map((conflict) => ({
5409
+ ...conflict,
5410
+ sources: conflict.sources.slice(0, limit)
5411
+ }));
5412
+ return { ...candidate, derivationProvenance, conflicts };
5413
+ }
5414
+ function applyModeState(candidates, mode, inference) {
5415
+ const selectedId = mode === "infer" && inference.status === "resolved" ? numberValue3(inference.candidateOperationId) : void 0;
5416
+ for (const candidate of candidates) {
5417
+ candidate.selected = selectedId === candidate.candidateOperationId;
5418
+ candidate.exploratory = mode === "candidates" && candidate.viable;
5419
+ }
5420
+ }
5421
+ function suggestedVarSets(candidates, order, limit) {
3828
5422
  const seen = /* @__PURE__ */ new Set();
3829
- return candidates.flatMap((candidate) => {
3830
- if (required.some((key) => candidate.derivedVariables[key] === void 0)) return [];
3831
- const cli = cliFor(candidate.derivedVariables, order);
3832
- if (seen.has(cli)) return [];
3833
- seen.add(cli);
3834
- return [{ variables: candidate.derivedVariables, cli }];
3835
- });
5423
+ const rows2 = [];
5424
+ for (const candidate of candidates) {
5425
+ if (!candidate.cli || candidate.missingVariables.length > 0) continue;
5426
+ if (seen.has(candidate.cli)) continue;
5427
+ seen.add(candidate.cli);
5428
+ rows2.push({ variables: orderedVariables(candidate.completeVariables, order), cli: candidate.cli });
5429
+ if (rows2.length >= limit) break;
5430
+ }
5431
+ return rows2;
3836
5432
  }
3837
- function referenceRows(db, workspaceId) {
3838
- const rows2 = db.prepare(
3839
- `SELECT req.alias alias,req.destination destination,req.service_path servicePath,
3840
- 'cds_require' sourceKind,r.name repoName
3841
- FROM cds_requires req JOIN repositories r ON r.id=req.repo_id
3842
- WHERE (? IS NULL OR r.workspace_id=?)
3843
- UNION ALL
3844
- SELECT COALESCE(b.alias,b.alias_expr) alias,b.destination_expr destination,
3845
- b.service_path_expr servicePath,'service_binding' sourceKind,r.name repoName
3846
- FROM service_bindings b JOIN repositories r ON r.id=b.repo_id
3847
- WHERE (? IS NULL OR r.workspace_id=?)`
3848
- ).all(workspaceId, workspaceId, workspaceId, workspaceId);
3849
- return rows2.map((row) => ({
3850
- alias: stringValue4(row.alias),
3851
- destination: stringValue4(row.destination),
3852
- servicePath: stringValue4(row.servicePath),
3853
- sourceKind: String(row.sourceKind),
3854
- repoName: String(row.repoName)
3855
- }));
5433
+ function hasResolvedImplementation(db, operationId) {
5434
+ return Boolean(db.prepare(
5435
+ "SELECT 1 FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status='resolved' AND from_kind='operation' AND from_id=? LIMIT 1"
5436
+ ).get(String(operationId)));
3856
5437
  }
3857
- function allMissingVariables(evidence, templates) {
3858
- const fromSubstitutions = Object.values(record(evidence.runtimeSubstitutions)).flatMap((value) => stringArray(record(value).missing));
3859
- const fromTemplates = [
3860
- templates.servicePath,
3861
- templates.operationPath,
3862
- templates.alias,
3863
- templates.destination
3864
- ].flatMap((value) => extractPlaceholders(value));
3865
- return [.../* @__PURE__ */ new Set([...fromSubstitutions, ...fromTemplates])].sort();
5438
+ function templatesFromEvidence(evidence, phase) {
5439
+ return {
5440
+ servicePath: substitutionSignal(evidence, "servicePath", phase),
5441
+ operationPath: substitutionSignal(evidence, "operationPath", phase),
5442
+ alias: substitutionSignal(
5443
+ evidence,
5444
+ evidence.serviceAliasExpr !== void 0 ? "serviceAliasExpr" : "serviceAlias",
5445
+ phase
5446
+ ),
5447
+ destination: substitutionSignal(evidence, "destination", phase)
5448
+ };
3866
5449
  }
3867
- function variableOrder(templates, missingVariables) {
5450
+ function substitutionSignal(evidence, key, phase) {
5451
+ const substitution = record(record(evidence.runtimeSubstitutions)[key]);
5452
+ return stringValue6(substitution[phase]) ?? stringValue6(evidence[key]);
5453
+ }
5454
+ function effectiveSignal(evidence, key) {
5455
+ return substitutionSignal(evidence, key, "effective");
5456
+ }
5457
+ function placeholderSources(templates) {
5458
+ const sources = {};
5459
+ for (const [kind, template] of Object.entries(templates)) {
5460
+ if (typeof template !== "string") continue;
5461
+ for (const key of extractPlaceholders(template))
5462
+ sources[key] = [.../* @__PURE__ */ new Set([...sources[key] ?? [], `${kind}:${template}`])].sort();
5463
+ }
5464
+ return Object.fromEntries(Object.entries(sources).sort(([left], [right]) => left.localeCompare(right)));
5465
+ }
5466
+ function variableOrder(templates, required) {
3868
5467
  const ordered = [
3869
5468
  templates.servicePath,
3870
5469
  templates.operationPath,
3871
5470
  templates.alias,
3872
5471
  templates.destination
3873
5472
  ].flatMap((value) => extractPlaceholders(value));
3874
- return [.../* @__PURE__ */ new Set([...ordered, ...missingVariables])];
3875
- }
3876
- function templatesFromEvidence(evidence) {
3877
- return {
3878
- servicePath: stringValue4(evidence.servicePath),
3879
- operationPath: stringValue4(evidence.operationPath),
3880
- alias: stringValue4(evidence.serviceAliasExpr ?? evidence.serviceAlias),
3881
- destination: stringValue4(evidence.destination)
3882
- };
5473
+ return [.../* @__PURE__ */ new Set([...ordered, ...required])];
3883
5474
  }
3884
5475
  function matchTemplate(template, concrete) {
3885
5476
  if (!template || !concrete) return void 0;
3886
5477
  const keys = extractPlaceholders(template);
3887
5478
  if (keys.length === 0) return template === concrete ? {} : void 0;
3888
- const regex = new RegExp(`^${templateToPattern(template)}$`);
3889
- const match = regex.exec(concrete);
5479
+ const match = new RegExp(`^${templateToPattern(template)}$`).exec(concrete);
3890
5480
  if (!match) return void 0;
3891
5481
  const values = {};
3892
5482
  for (let index = 0; index < keys.length; index += 1) {
@@ -3902,42 +5492,58 @@ function templateToPattern(template) {
3902
5492
  let pattern = "";
3903
5493
  let lastIndex = 0;
3904
5494
  for (const match of template.matchAll(/\$\{([^}]*)\}/g)) {
3905
- pattern += escapeRegex(template.slice(lastIndex, match.index));
5495
+ pattern += escapeRegex2(template.slice(lastIndex, match.index));
3906
5496
  pattern += "([^/]+?)";
3907
5497
  lastIndex = (match.index ?? 0) + match[0].length;
3908
5498
  }
3909
- return `${pattern}${escapeRegex(template.slice(lastIndex))}`;
5499
+ return `${pattern}${escapeRegex2(template.slice(lastIndex))}`;
3910
5500
  }
3911
- function mergeDerived(state, values, sourceKind) {
3912
- for (const [key, value] of Object.entries(values)) {
3913
- if (state.derivedVariables[key] !== void 0 && state.derivedVariables[key] !== value) {
3914
- state.rejectedReasons.push("template_variable_conflict");
3915
- continue;
3916
- }
3917
- state.derivedVariables[key] = value;
3918
- state.derivedVariableSources[key] = { sourceKind, value };
3919
- }
5501
+ function cliFor(variables, order) {
5502
+ return order.filter((key) => variables[key] !== void 0).map((key) => `--var ${shellArgument(`${key}=${variables[key]}`)}`).join(" ");
5503
+ }
5504
+ function shellArgument(value) {
5505
+ return /^[A-Za-z0-9_./:=+-]+$/.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
5506
+ }
5507
+ function orderedVariables(variables, order) {
5508
+ return Object.fromEntries(order.flatMap((key) => variables[key] === void 0 ? [] : [[key, variables[key]]]));
3920
5509
  }
3921
5510
  function addScore(state, amount, reason) {
3922
5511
  state.score += amount;
5512
+ addReason(state, reason);
5513
+ }
5514
+ function addReason(state, reason) {
3923
5515
  if (!state.reasons.includes(reason)) state.reasons.push(reason);
3924
5516
  }
3925
- function hasResolvedImplementation(db, operationId) {
3926
- const row = db.prepare(
3927
- "SELECT 1 FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status='resolved' AND from_kind='operation' AND from_id=? LIMIT 1"
3928
- ).get(String(operationId));
3929
- return Boolean(row);
5517
+ function reject(state, reason) {
5518
+ rejectReasonOnly(state, reason);
5519
+ state.viable = false;
5520
+ state.rejected = true;
3930
5521
  }
3931
- function cliFor(variables, order) {
3932
- return order.filter((key) => variables[key] !== void 0).map((key) => `--var ${key}=${variables[key]}`).join(" ");
5522
+ function rejectReasonOnly(state, reason) {
5523
+ if (!state.rejectedReasons.includes(reason)) state.rejectedReasons.push(reason);
5524
+ }
5525
+ function addInferenceBlock(state, reason) {
5526
+ addReason(state, reason);
5527
+ if (!state.inferenceBlockReasons.includes(reason))
5528
+ state.inferenceBlockReasons.push(reason);
5529
+ }
5530
+ function requiredSuppliedVariables(inputs) {
5531
+ return Object.fromEntries(inputs.required.flatMap((key) => inputs.supplied[key] === void 0 ? [] : [[key, inputs.supplied[key]]]));
5532
+ }
5533
+ function signalCode(kind) {
5534
+ return kind === "servicePath" ? "service_path" : "operation_path";
3933
5535
  }
3934
5536
  function record(value) {
3935
5537
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
3936
5538
  }
3937
- function stringValue4(value) {
5539
+ function stringRecord(value) {
5540
+ const entries = Object.entries(record(value)).filter((entry) => typeof entry[1] === "string").sort(([left], [right]) => left.localeCompare(right));
5541
+ return Object.fromEntries(entries);
5542
+ }
5543
+ function stringValue6(value) {
3938
5544
  return typeof value === "string" ? value : void 0;
3939
5545
  }
3940
- function numberValue(value) {
5546
+ function numberValue3(value) {
3941
5547
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3942
5548
  }
3943
5549
  function stringArray(value) {
@@ -3950,7 +5556,7 @@ function nonEmptyStrings(value, fallback) {
3950
5556
  function isConcrete(value) {
3951
5557
  return typeof value === "string" && value.length > 0 && extractPlaceholders(value).length === 0;
3952
5558
  }
3953
- function escapeRegex(value) {
5559
+ function escapeRegex2(value) {
3954
5560
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3955
5561
  }
3956
5562
 
@@ -3963,6 +5569,8 @@ function baseTraceEvidence(row, call, persistedEvidence, contextualEvidence) {
3963
5569
  persistedGraphEdgeId: row.id > 0 ? row.id : void 0,
3964
5570
  outboundCallId: call.id,
3965
5571
  callSite: { sourceFile: call.source_file, sourceLine: call.source_line },
5572
+ callType: call.call_type,
5573
+ repoId: call.repo_id,
3966
5574
  sourceFile: call.source_file,
3967
5575
  sourceLine: call.source_line,
3968
5576
  file: call.source_file,
@@ -3973,43 +5581,123 @@ function baseTraceEvidence(row, call, persistedEvidence, contextualEvidence) {
3973
5581
  };
3974
5582
  }
3975
5583
  function runtimeResolution(db, row, evidence, options, workspaceId, contextualUnresolvedReason) {
3976
- const substituted = evidenceWithRuntimeVariables(evidence, options.vars);
3977
5584
  const dynamicMode = options.dynamicMode ?? "strict";
5585
+ const candidateCap = positiveCandidateCap(options.maxDynamicCandidates);
5586
+ if (!isDynamicRemoteOperationEdge(row, evidence))
5587
+ return unchangedRuntimeResolution(
5588
+ row,
5589
+ boundDynamicEvidence(evidence, candidateCap),
5590
+ contextualUnresolvedReason
5591
+ );
5592
+ const substituted = evidenceWithRuntimeVariables(evidence, options.vars);
3978
5593
  const analysis = analyzeDynamicTargetCandidates(
3979
5594
  db,
3980
5595
  substituted,
3981
5596
  workspaceId,
3982
5597
  dynamicMode,
3983
- positiveCandidateCap(options.maxDynamicCandidates)
5598
+ candidateCap
5599
+ );
5600
+ const enriched = boundDynamicEvidence(
5601
+ analysis ? evidenceWithDynamicAnalysis(substituted, analysis) : substituted,
5602
+ candidateCap
3984
5603
  );
3985
- const enriched = analysis ? evidenceWithDynamicAnalysis(substituted, analysis) : substituted;
5604
+ if (analysis && analysis.candidateCount > 0 && analysis.viableCandidateCount === 0 && Object.keys(analysis.appliedSuppliedVariables).length > 0)
5605
+ return noCandidateRuntimeResolution(row, enriched);
3986
5606
  if (dynamicMode === "infer") {
3987
5607
  const inferred = inferredTarget(analysis);
3988
- if (inferred) {
3989
- const resolvedRow = { ...row, status: "resolved", to_kind: "operation", to_id: String(inferred.operationId), unresolved_reason: void 0, confidence: inferred.score };
3990
- const resolution2 = { status: "resolved", target: inferred, candidates: [], reasons: inferred.reasons };
3991
- return { row: resolvedRow, evidence: withEffectiveResolution(enriched, resolvedRow, void 0, resolution2), target: inferred };
3992
- }
5608
+ if (inferred)
5609
+ return resolvedRuntimeResolution(row, enriched, inferred, inferred.reasons);
3993
5610
  }
3994
- if (!isRemoteRuntimeCandidate(row, evidence, options.vars)) {
5611
+ if (!hasApplicableRuntimeVariables(evidence, options.vars)) {
3995
5612
  const unresolvedReason2 = contextualUnresolvedReason ?? row.unresolved_reason;
3996
5613
  const withSections = withEffectiveResolution(enriched, row, unresolvedReason2);
3997
5614
  return { row, evidence: withSections, unresolvedReason: unresolvedReason2 };
3998
5615
  }
3999
5616
  const resolution = resolveRuntimeOperation(db, enriched, workspaceId);
4000
- if (resolution.target) {
4001
- const resolvedRow = { ...row, status: "resolved", to_kind: "operation", to_id: String(resolution.target.operationId), unresolved_reason: void 0, confidence: Math.max(0, Math.min(1, resolution.target.score)) };
4002
- return { row: resolvedRow, evidence: withEffectiveResolution(enriched, resolvedRow, void 0, resolution), target: resolution.target };
4003
- }
5617
+ if (resolution.target)
5618
+ return resolvedRuntimeResolution(
5619
+ row,
5620
+ enriched,
5621
+ resolution.target,
5622
+ resolution.reasons
5623
+ );
5624
+ if (analysis && analysis.viableCandidateCount === 0 && Object.keys(analysis.appliedSuppliedVariables).length > 0)
5625
+ return noCandidateRuntimeResolution(row, enriched);
4004
5626
  const unresolvedReason = runtimeUnresolvedReason(resolution);
4005
5627
  return { row, evidence: withEffectiveResolution(enriched, row, unresolvedReason, resolution), unresolvedReason };
4006
5628
  }
5629
+ function unchangedRuntimeResolution(row, evidence, contextualUnresolvedReason) {
5630
+ const unresolvedReason = contextualUnresolvedReason ?? row.unresolved_reason;
5631
+ return {
5632
+ row,
5633
+ evidence: withEffectiveResolution(evidence, row, unresolvedReason),
5634
+ unresolvedReason
5635
+ };
5636
+ }
5637
+ function noCandidateRuntimeResolution(row, evidence) {
5638
+ const unresolvedReason = "No candidate remained after runtime substitution";
5639
+ return {
5640
+ row,
5641
+ evidence: withEffectiveResolution(evidence, row, unresolvedReason),
5642
+ unresolvedReason
5643
+ };
5644
+ }
5645
+ function resolvedRuntimeResolution(row, evidence, target, reasons) {
5646
+ const resolvedRow = {
5647
+ ...row,
5648
+ status: "resolved",
5649
+ to_kind: "operation",
5650
+ to_id: String(target.operationId),
5651
+ unresolved_reason: void 0,
5652
+ confidence: Math.max(0, Math.min(1, target.score))
5653
+ };
5654
+ const resolution = {
5655
+ status: "resolved",
5656
+ target,
5657
+ candidates: [],
5658
+ reasons
5659
+ };
5660
+ return {
5661
+ row: resolvedRow,
5662
+ evidence: withEffectiveResolution(evidence, resolvedRow, void 0, resolution),
5663
+ target
5664
+ };
5665
+ }
4007
5666
  function runtimeVariableDiagnostic(edges) {
5667
+ const totals = runtimeDiagnosticTotals(edges);
5668
+ const missingVariables = [...totals.missing].sort();
5669
+ if (missingVariables.length === 0) return void 0;
5670
+ const shownSuggestions = totals.candidateSuggestions.slice(0, totals.maxCandidates);
5671
+ const shownRejected = totals.rejectedCandidates.slice(0, totals.maxCandidates);
5672
+ const shownCandidateCount = shownSuggestions.length;
5673
+ return {
5674
+ severity: "warning",
5675
+ code: "trace_runtime_variables_missing",
5676
+ message: `Runtime variables are required to resolve dynamic trace targets: ${missingVariables.join(", ")}`,
5677
+ missingVariables,
5678
+ suggestions: missingVariables.map((key) => `--var ${key}=<value>`),
5679
+ candidateCount: totals.candidateCount,
5680
+ viableCandidateCount: totals.viableCandidateCount,
5681
+ rejectedCandidateCount: totals.rejectedCandidateCount,
5682
+ shownCandidateCount,
5683
+ omittedCandidateCount: Math.max(0, totals.viableCandidateCount - shownCandidateCount),
5684
+ maxDynamicCandidates: totals.maxCandidates,
5685
+ shownRejectedCandidateCount: shownRejected.length,
5686
+ omittedRejectedCandidateCount: Math.max(0, totals.rejectedCandidateCount - shownRejected.length),
5687
+ candidateSuggestions: shownSuggestions,
5688
+ rejectedCandidates: shownRejected,
5689
+ suggestedVarSets: uniqueCliRows(totals.suggestedVarSets).slice(0, totals.maxCandidates),
5690
+ copyableExamples: copyableExamples(totals.suggestedVarSets, totals.candidateCount, totals.maxCandidates)
5691
+ };
5692
+ }
5693
+ function runtimeDiagnosticTotals(edges) {
4008
5694
  const missing = /* @__PURE__ */ new Set();
4009
5695
  let candidateCount = 0;
4010
- let shownCandidateCount = 0;
4011
- let omittedCandidateCount = 0;
5696
+ let viableCandidateCount = 0;
5697
+ let rejectedCandidateCount = 0;
5698
+ let maxCandidates = 5;
4012
5699
  const candidateSuggestions = [];
5700
+ const rejectedCandidates = [];
4013
5701
  const suggestedVarSets2 = [];
4014
5702
  for (const edge of edges) {
4015
5703
  const effective = parseObject(edge.evidence.effectiveResolution);
@@ -4020,47 +5708,90 @@ function runtimeVariableDiagnostic(edges) {
4020
5708
  for (const value of Object.values(substitutions))
4021
5709
  for (const key of value.missing ?? []) missing.add(key);
4022
5710
  const exploration = parseObject(edge.evidence.dynamicTargetExploration);
5711
+ maxCandidates = positiveCandidateCap(numeric(exploration.maxCandidates) || maxCandidates);
4023
5712
  candidateCount += numeric(exploration.candidateCount);
4024
- shownCandidateCount += numeric(exploration.shownCandidateCount);
4025
- omittedCandidateCount += numeric(exploration.omittedCandidateCount);
4026
- candidateSuggestions.push(...recordArray(edge.evidence.dynamicTargetCandidateSuggestions));
4027
- suggestedVarSets2.push(...recordArray(exploration.suggestedVarSets));
5713
+ viableCandidateCount += numeric(exploration.viableCandidateCount);
5714
+ rejectedCandidateCount += numeric(exploration.rejectedCandidateCount);
5715
+ appendBounded(
5716
+ candidateSuggestions,
5717
+ recordArray(edge.evidence.dynamicTargetCandidateSuggestions),
5718
+ maxCandidates
5719
+ );
5720
+ appendBounded(
5721
+ rejectedCandidates,
5722
+ recordArray(exploration.rejectedCandidates),
5723
+ maxCandidates
5724
+ );
5725
+ appendBounded(
5726
+ suggestedVarSets2,
5727
+ recordArray(exploration.suggestedVarSets),
5728
+ maxCandidates
5729
+ );
4028
5730
  }
4029
- const missingVariables = [...missing].sort();
4030
- if (missingVariables.length === 0) return void 0;
4031
5731
  return {
4032
- severity: "warning",
4033
- code: "trace_runtime_variables_missing",
4034
- message: `Runtime variables are required to resolve dynamic trace targets: ${missingVariables.join(", ")}`,
4035
- missingVariables,
4036
- suggestions: missingVariables.map((key) => `--var ${key}=<value>`),
5732
+ missing,
4037
5733
  candidateCount,
4038
- shownCandidateCount,
4039
- omittedCandidateCount,
4040
- candidateSuggestions: candidateSuggestions.slice(0, 5),
4041
- suggestedVarSets: uniqueCliRows(suggestedVarSets2).slice(0, 5),
4042
- copyableExamples: [
4043
- ...uniqueCliRows(suggestedVarSets2).slice(0, 3).flatMap((row) => typeof row.cli === "string" ? [row.cli] : []),
4044
- ...candidateCount > 0 ? ["--dynamic-mode candidates --max-dynamic-candidates 20"] : []
4045
- ]
5734
+ viableCandidateCount,
5735
+ rejectedCandidateCount,
5736
+ maxCandidates,
5737
+ candidateSuggestions,
5738
+ rejectedCandidates,
5739
+ suggestedVarSets: suggestedVarSets2
4046
5740
  };
4047
5741
  }
5742
+ function runtimeNoCandidateDiagnostics(edges) {
5743
+ const seen = /* @__PURE__ */ new Set();
5744
+ return edges.flatMap((edge) => {
5745
+ const exploration = parseObject(edge.evidence.dynamicTargetExploration);
5746
+ const suppliedVariables = parseObject(exploration.suppliedVariables);
5747
+ const appliedSuppliedVariables = parseObject(
5748
+ exploration.appliedSuppliedVariables
5749
+ );
5750
+ if (numeric(exploration.viableCandidateCount) !== 0 || Object.keys(appliedSuppliedVariables).length === 0) return [];
5751
+ const callSite = parseObject(edge.evidence.callSite);
5752
+ const key = JSON.stringify([callSite, suppliedVariables]);
5753
+ if (seen.has(key)) return [];
5754
+ seen.add(key);
5755
+ const maxCandidates = positiveCandidateCap(numeric(exploration.maxCandidates));
5756
+ return [{
5757
+ severity: "warning",
5758
+ code: "no_candidate_after_runtime_substitution",
5759
+ message: "No dynamic target candidate remained after applying runtime variables",
5760
+ suppliedVariables,
5761
+ appliedSuppliedVariables,
5762
+ substitutedSignals: parseObject(exploration.substitutedSignals),
5763
+ candidateCount: numeric(exploration.candidateCount),
5764
+ viableCandidateCount: 0,
5765
+ rejectedCandidateCount: numeric(exploration.rejectedCandidateCount),
5766
+ shownCandidateCount: numeric(exploration.shownCandidateCount),
5767
+ omittedCandidateCount: numeric(exploration.omittedCandidateCount),
5768
+ shownRejectedCandidateCount: numeric(
5769
+ exploration.shownRejectedCandidateCount
5770
+ ),
5771
+ omittedRejectedCandidateCount: numeric(
5772
+ exploration.omittedRejectedCandidateCount
5773
+ ),
5774
+ rejectedCandidates: recordArray(exploration.rejectedCandidates).slice(0, maxCandidates),
5775
+ callSite
5776
+ }];
5777
+ });
5778
+ }
4048
5779
  function edgeTarget(row, evidence) {
4049
5780
  const effective = parseObject(evidence.effectiveResolution);
4050
- const targetServicePath = stringValue5(effective.targetServicePath ?? evidence.targetServicePath);
4051
- const targetOperationPath = stringValue5(effective.targetOperationPath ?? evidence.targetOperationPath);
5781
+ const targetServicePath = stringValue7(effective.targetServicePath ?? evidence.targetServicePath);
5782
+ const targetOperationPath = stringValue7(effective.targetOperationPath ?? evidence.targetOperationPath);
4052
5783
  if (targetServicePath && targetOperationPath) return `${targetServicePath}${targetOperationPath}`;
4053
5784
  const runtimeCandidate = evidence.runtimeResolvedCandidate;
4054
5785
  if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath) return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
4055
- const servicePath = stringValue5(evidence.servicePath);
4056
- const operationPath = stringValue5(evidence.operationPath);
4057
- const targetOperation = stringValue5(evidence.targetOperation);
4058
- const targetRepo = stringValue5(evidence.targetRepo) ?? "";
5786
+ const servicePath = stringValue7(evidence.servicePath);
5787
+ const operationPath = stringValue7(evidence.operationPath);
5788
+ const targetOperation = stringValue7(evidence.targetOperation);
5789
+ const targetRepo = stringValue7(evidence.targetRepo) ?? "";
4059
5790
  if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return `Entity: ${row.to_id || "unknown"}`;
4060
- if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return stringValue5(evidence.remoteQueryTarget) ?? `Remote query: ${row.to_id || "unknown"}`;
5791
+ if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return stringValue7(evidence.remoteQueryTarget) ?? `Remote query: ${row.to_id || "unknown"}`;
4061
5792
  if (row.edge_type === "HANDLER_CALLS_EXTERNAL_HTTP") {
4062
5793
  const target = parseObject(evidence.externalTarget);
4063
- return stringValue5(target.label) ?? `External endpoint: ${row.to_id || "unknown"}`;
5794
+ return stringValue7(target.label) ?? `External endpoint: ${row.to_id || "unknown"}`;
4064
5795
  }
4065
5796
  if (servicePath && operationPath) return `${servicePath}${operationPath}`;
4066
5797
  return targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
@@ -4099,39 +5830,102 @@ function withEffectiveResolution(evidence, row, unresolvedReason, resolution) {
4099
5830
  return { ...rest, effectiveResolution: current, linker: { status: current.status, confidence: current.confidence, reason: unresolvedReason, edgeType: current.edgeType } };
4100
5831
  }
4101
5832
  function resolveRuntimeOperation(db, evidence, workspaceId) {
4102
- const servicePath = stringValue5(evidence.servicePath);
4103
- const rawOperationPath = stringValue5(evidence.operationPath);
5833
+ const servicePath = stringValue7(evidence.servicePath);
5834
+ const rawOperationPath = stringValue7(evidence.operationPath);
4104
5835
  const normalized = normalizeODataOperationInvocationPath(rawOperationPath);
4105
- const operationPath = normalized?.wasInvocation ? normalized.normalizedOperationPath : stringValue5(evidence.normalizedOperationPath) ?? rawOperationPath;
4106
- const alias = stringValue5(evidence.serviceAliasExpr ?? evidence.serviceAlias);
4107
- const destination = stringValue5(evidence.destination);
5836
+ const operationPath = normalized?.wasInvocation ? normalized.normalizedOperationPath : stringValue7(evidence.normalizedOperationPath) ?? rawOperationPath;
5837
+ const alias = stringValue7(evidence.serviceAliasExpr ?? evidence.serviceAlias);
5838
+ const destination = stringValue7(evidence.destination);
4108
5839
  return resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: true }, workspaceId);
4109
5840
  }
4110
5841
  function evidenceWithRuntimeVariables(evidence, vars) {
4111
5842
  const substitutions = runtimeSubstitutions(evidence, vars ?? {});
4112
- const next = { ...evidence, runtimeSubstitutions: substitutions };
5843
+ const suppliedRuntimeVariables = Object.fromEntries(
5844
+ Object.entries(vars ?? {}).sort(([left], [right]) => left.localeCompare(right))
5845
+ );
5846
+ const next = {
5847
+ ...evidence,
5848
+ runtimeSubstitutions: substitutions,
5849
+ suppliedRuntimeVariables
5850
+ };
4113
5851
  for (const [key, value] of Object.entries(substitutions)) if (value.effective) next[key] = value.effective;
4114
5852
  const missing = Object.values(substitutions).flatMap((value) => value.missing);
4115
5853
  if (missing.length > 0) next.missingRuntimeVariables = [...new Set(missing)].sort();
4116
- if (Object.keys(vars ?? {}).length > 0) next.runtimeVariablesApplied = true;
5854
+ if (Object.keys(suppliedRuntimeVariables).length > 0) next.runtimeVariablesApplied = true;
4117
5855
  return next;
4118
5856
  }
4119
5857
  function evidenceWithDynamicAnalysis(evidence, analysis) {
5858
+ const persistedCandidates = recordArray(evidence.candidates);
5859
+ const persistedScores = recordArray(evidence.candidateScores);
4120
5860
  return {
4121
5861
  ...evidence,
5862
+ candidates: persistedCandidates.slice(0, analysis.maxCandidates),
5863
+ candidateScores: persistedScores.slice(0, analysis.maxCandidates),
5864
+ persistedCandidateCount: persistedCandidates.length,
5865
+ persistedCandidateOmittedCount: Math.max(
5866
+ 0,
5867
+ persistedCandidates.length - analysis.maxCandidates
5868
+ ),
4122
5869
  dynamicTargetExploration: {
4123
5870
  mode: analysis.mode,
5871
+ maxCandidates: analysis.maxCandidates,
4124
5872
  missingVariables: analysis.missingVariables,
5873
+ requiredVariables: analysis.requiredVariables,
5874
+ suppliedVariables: analysis.suppliedVariables,
5875
+ appliedSuppliedVariables: analysis.appliedSuppliedVariables,
5876
+ substitutedSignals: analysis.substitutedSignals,
4125
5877
  candidateCount: analysis.candidateCount,
5878
+ viableCandidateCount: analysis.viableCandidateCount,
5879
+ rejectedCandidateCount: analysis.rejectedCandidateCount,
4126
5880
  shownCandidateCount: analysis.shownCandidateCount,
4127
5881
  omittedCandidateCount: analysis.omittedCandidateCount,
5882
+ shownRejectedCandidateCount: analysis.shownRejectedCandidateCount,
5883
+ omittedRejectedCandidateCount: analysis.omittedRejectedCandidateCount,
5884
+ rejectedCandidates: analysis.rejectedCandidates,
4128
5885
  suggestedVarSets: analysis.suggestedVarSets
4129
5886
  },
4130
5887
  dynamicTargetCandidates: analysis.candidates,
4131
5888
  dynamicTargetCandidateSuggestions: analysis.shownCandidates,
5889
+ rejectedCandidates: analysis.rejectedCandidates,
4132
5890
  dynamicTargetInference: analysis.inference
4133
5891
  };
4134
5892
  }
5893
+ var boundedDynamicListKeys = /* @__PURE__ */ new Set([
5894
+ "candidates",
5895
+ "candidateScores",
5896
+ "dynamicTargetCandidates",
5897
+ "dynamicTargetCandidateSuggestions",
5898
+ "candidateSuggestions",
5899
+ "suggestedVarSets",
5900
+ "rejectedCandidates",
5901
+ "rejectedCandidateSuggestions",
5902
+ "copyableExamples",
5903
+ "conflicts"
5904
+ ]);
5905
+ function boundDynamicEvidence(evidence, limit) {
5906
+ const candidateCount = numeric(evidence.persistedCandidateCount) || (Array.isArray(evidence.candidates) ? evidence.candidates.length : 0);
5907
+ const projected = boundDynamicValue(evidence, limit);
5908
+ const next = parseObject(projected);
5909
+ if (candidateCount === 0) return next;
5910
+ return {
5911
+ ...next,
5912
+ persistedCandidateCount: candidateCount,
5913
+ persistedCandidateOmittedCount: Math.max(0, candidateCount - limit)
5914
+ };
5915
+ }
5916
+ function boundDynamicValue(value, limit, key, parentKey) {
5917
+ if (Array.isArray(value)) {
5918
+ const bounded = Boolean(key && (boundedDynamicListKeys.has(key) || parentKey === "derivationProvenance" || parentKey === "conflicts" && key === "sources"));
5919
+ const input = bounded ? value.slice(0, limit) : value;
5920
+ const projected = input.map((item) => boundDynamicValue(item, limit, void 0, key ?? parentKey));
5921
+ return projected;
5922
+ }
5923
+ if (!value || typeof value !== "object") return value;
5924
+ return Object.fromEntries(Object.entries(value).map(([childKey, child]) => [
5925
+ childKey,
5926
+ boundDynamicValue(child, limit, childKey, key ?? parentKey)
5927
+ ]));
5928
+ }
4135
5929
  function runtimeSubstitutions(evidence, vars) {
4136
5930
  const substitutions = {};
4137
5931
  for (const key of ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"]) {
@@ -4141,15 +5935,18 @@ function runtimeSubstitutions(evidence, vars) {
4141
5935
  return substitutions;
4142
5936
  }
4143
5937
  function substitutionValue(evidence, key) {
4144
- const value = stringValue5(evidence[key]);
5938
+ const value = stringValue7(evidence[key]);
4145
5939
  if (key !== "operationPath") return value;
4146
5940
  const normalized = normalizeODataOperationInvocationPath(value);
4147
5941
  return normalized?.wasInvocation ? normalized.normalizedOperationPath : value;
4148
5942
  }
4149
- function isRemoteRuntimeCandidate(row, evidence, vars) {
4150
- if (!vars || Object.keys(vars).length === 0) return false;
5943
+ function isDynamicRemoteOperationEdge(row, evidence) {
5944
+ if (evidence.callType !== "remote_action") return false;
4151
5945
  if (!["dynamic", "ambiguous", "unresolved"].includes(String(row.status ?? ""))) return false;
4152
- if (!["DYNAMIC_EDGE_CANDIDATE", "UNRESOLVED_EDGE", "REMOTE_CALL_RESOLVES_TO_OPERATION"].includes(row.edge_type)) return false;
5946
+ return ["DYNAMIC_EDGE_CANDIDATE", "UNRESOLVED_EDGE", "REMOTE_CALL_RESOLVES_TO_OPERATION"].includes(row.edge_type);
5947
+ }
5948
+ function hasApplicableRuntimeVariables(evidence, vars) {
5949
+ if (!vars || Object.keys(vars).length === 0) return false;
4153
5950
  return ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"].some((key) => hasRuntimeVariable(evidence[key], vars));
4154
5951
  }
4155
5952
  function inferredTarget(analysis) {
@@ -4168,11 +5965,12 @@ function targetFromCandidate(candidate) {
4168
5965
  servicePath: candidate.servicePath,
4169
5966
  operationPath: candidate.operationPath,
4170
5967
  operationName: candidate.operationName,
5968
+ repoId: candidate.repoId,
4171
5969
  packageName: candidate.packageName,
4172
5970
  score: candidate.score,
4173
5971
  reasons: candidate.reasons,
4174
- sourceFile: "",
4175
- sourceLine: 0
5972
+ sourceFile: candidate.sourceFile,
5973
+ sourceLine: candidate.sourceLine
4176
5974
  };
4177
5975
  }
4178
5976
  function hasRuntimeVariable(value, vars) {
@@ -4186,7 +5984,7 @@ function runtimeUnresolvedReason(resolution) {
4186
5984
  function parseObject(value) {
4187
5985
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
4188
5986
  }
4189
- function stringValue5(value) {
5987
+ function stringValue7(value) {
4190
5988
  return typeof value === "string" ? value : void 0;
4191
5989
  }
4192
5990
  function numeric(value) {
@@ -4195,6 +5993,10 @@ function numeric(value) {
4195
5993
  function recordArray(value) {
4196
5994
  return Array.isArray(value) ? value.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
4197
5995
  }
5996
+ function appendBounded(target, values, limit) {
5997
+ const remaining = Math.max(0, limit - target.length);
5998
+ if (remaining > 0) target.push(...values.slice(0, remaining));
5999
+ }
4198
6000
  function uniqueCliRows(rows2) {
4199
6001
  const seen = /* @__PURE__ */ new Set();
4200
6002
  return rows2.filter((row) => {
@@ -4204,6 +6006,11 @@ function uniqueCliRows(rows2) {
4204
6006
  return true;
4205
6007
  });
4206
6008
  }
6009
+ function copyableExamples(suggestedVarSets2, candidateCount, limit) {
6010
+ const variableExamples = uniqueCliRows(suggestedVarSets2).flatMap((row) => typeof row.cli === "string" ? [row.cli] : []);
6011
+ const exploration = candidateCount > 0 ? ["--dynamic-mode candidates --max-dynamic-candidates 20"] : [];
6012
+ return [...variableExamples, ...exploration].slice(0, limit);
6013
+ }
4207
6014
  function positiveCandidateCap(value) {
4208
6015
  return Number.isFinite(value) && Number(value) > 0 ? Math.floor(Number(value)) : 5;
4209
6016
  }
@@ -4221,9 +6028,9 @@ function dynamicCandidateBranches(depth, call, evidence) {
4221
6028
  exploratory: true,
4222
6029
  dynamicMode: String(exploration.mode ?? "candidates"),
4223
6030
  selected: false,
4224
- omittedCandidateCount: numericValue(exploration.omittedCandidateCount)
6031
+ omittedCandidateCount: numericValue2(exploration.omittedCandidateCount)
4225
6032
  },
4226
- confidence: numericValue(candidate.score),
6033
+ confidence: numericValue2(candidate.score),
4227
6034
  unresolvedReason: "Exploratory dynamic target candidate; provide runtime variables to select it"
4228
6035
  }));
4229
6036
  }
@@ -4233,25 +6040,62 @@ function objectRecord(value) {
4233
6040
  function recordArray2(value) {
4234
6041
  return Array.isArray(value) ? value.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
4235
6042
  }
4236
- function numericValue(value) {
6043
+ function numericValue2(value) {
4237
6044
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
4238
6045
  }
4239
6046
 
6047
+ // src/trace/002-trace-diagnostics.ts
6048
+ function loadTraceDiagnostics(db, repoId, includeWorkspaceDiagnostics, workspaceId) {
6049
+ if (repoId === void 0 && !includeWorkspaceDiagnostics) return [];
6050
+ return db.prepare(`SELECT d.repo_id repoId,d.severity,d.code,d.message,
6051
+ d.source_file sourceFile,d.source_line sourceLine
6052
+ FROM diagnostics d LEFT JOIN repositories r ON r.id=d.repo_id
6053
+ WHERE (? IS NULL OR d.repo_id=?)
6054
+ AND (? IS NULL OR d.repo_id IS NULL OR r.workspace_id=?)
6055
+ ORDER BY severity,code,COALESCE(source_file,''),
6056
+ COALESCE(source_line,0),d.id`).all(
6057
+ repoId,
6058
+ repoId,
6059
+ workspaceId,
6060
+ workspaceId
6061
+ );
6062
+ }
6063
+ function prependTraceDiagnostic(diagnostics, diagnostic) {
6064
+ const duplicate = diagnostics.findIndex((item) => sameDiagnosticLocation(item, diagnostic));
6065
+ if (duplicate >= 0) diagnostics.splice(duplicate, 1);
6066
+ diagnostics.unshift(diagnostic);
6067
+ }
6068
+ function sameDiagnosticLocation(left, right) {
6069
+ if (left.code !== right.code) return false;
6070
+ const leftFile = stringValue8(left.sourceFile);
6071
+ const rightFile = stringValue8(right.sourceFile);
6072
+ if (leftFile || rightFile)
6073
+ return leftFile === rightFile && numericValue3(left.sourceLine) === numericValue3(right.sourceLine);
6074
+ return String(left.message ?? "") === String(right.message ?? "");
6075
+ }
6076
+ function stringValue8(value) {
6077
+ return typeof value === "string" ? value : void 0;
6078
+ }
6079
+ function numericValue3(value) {
6080
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
6081
+ }
6082
+
4240
6083
  // src/trace/trace-engine.ts
4241
- function normalizeOperation(value) {
6084
+ function normalizeOperation2(value) {
4242
6085
  if (!value) return void 0;
4243
6086
  return value.startsWith("/") ? value.slice(1) : value;
4244
6087
  }
4245
6088
  function positiveDepth(value) {
4246
6089
  return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
4247
6090
  }
4248
- function operationStartScope(db, repoId, start, hintOptions) {
4249
- const requested = normalizeOperation(start.operationPath ?? start.operation);
6091
+ function operationStartScope(db, repoId, start, hintOptions, workspaceId) {
6092
+ const requested = normalizeOperation2(start.operationPath ?? start.operation);
4250
6093
  if (!requested) return void 0;
4251
6094
  const rows2 = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_path operationPath,o.source_file sourceFile,o.source_line sourceLine,s.service_path servicePath,r.id repoId,r.name repoName
4252
6095
  FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
4253
- WHERE (? IS NULL OR r.id=?) AND (? IS NULL OR s.service_path=?) AND (o.operation_name=? OR o.operation_path=? OR o.operation_path=?)
4254
- ORDER BY r.name,s.service_path,o.operation_name,o.id`).all(repoId, repoId, start.servicePath, start.servicePath, requested, requested, requested.startsWith("/") ? requested : `/${requested}`);
6096
+ WHERE (? IS NULL OR r.workspace_id=?) AND (? IS NULL OR r.id=?)
6097
+ AND (? IS NULL OR s.service_path=?) AND (o.operation_name=? OR o.operation_path=? OR o.operation_path=?)
6098
+ ORDER BY r.name,s.service_path,o.operation_name,o.id`).all(workspaceId, workspaceId, repoId, repoId, start.servicePath, start.servicePath, requested, requested, requested.startsWith("/") ? requested : `/${requested}`);
4255
6099
  if (rows2.length === 0) return void 0;
4256
6100
  const repoCount = new Set(rows2.map((row) => String(row.repoName))).size;
4257
6101
  const serviceCount = new Set(rows2.map((row) => `${String(row.repoName)}:${String(row.servicePath)}`)).size;
@@ -4282,54 +6126,34 @@ function implementationRejectionReasons(evidence) {
4282
6126
  const reasons = candidates.flatMap((candidate) => Array.isArray(candidate.rejectedReasons) ? candidate.rejectedReasons.filter((reason) => typeof reason === "string") : []);
4283
6127
  return [...new Set(reasons)].sort();
4284
6128
  }
4285
- function sourceFilesForStart(db, repoId, start) {
4286
- const handler = start.handler;
4287
- const operation = normalizeOperation(start.operation ?? start.operationPath);
4288
- if (!handler && !operation) return void 0;
4289
- const rows2 = db.prepare(
4290
- `SELECT DISTINCT hc.source_file sourceFile,s.id symbolId
4291
- FROM handler_classes hc LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name
4292
- WHERE (? IS NULL OR hc.repo_id=?) AND (? IS NULL OR hc.class_name=? OR hm.method_name=?)
4293
- AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)
4294
- AND (? IS NULL OR EXISTS (SELECT 1 FROM cds_services s JOIN cds_operations o ON o.service_id=s.id WHERE s.repo_id=hc.repo_id AND s.service_path=? AND (? IS NULL OR o.operation_path=? OR o.operation_name=? OR hm.decorator_value=? OR hm.method_name=?)))`
4295
- ).all(
4296
- repoId,
4297
- repoId,
4298
- handler,
4299
- handler,
4300
- handler,
4301
- operation,
4302
- operation,
4303
- operation,
4304
- start.servicePath,
4305
- start.servicePath,
4306
- operation,
4307
- operation,
4308
- operation,
4309
- operation,
4310
- operation
6129
+ function sourceFilesForStart(db, repoId, start, workspaceId) {
6130
+ return sourceScopeForSelector(db, repoId, start, workspaceId);
6131
+ }
6132
+ function startScope(db, start, hintOptions, workspaceId) {
6133
+ const repos = start.repo ? reposByName(db, start.repo, workspaceId).map((row) => ({
6134
+ id: row.id,
6135
+ name: row.name,
6136
+ packageName: row.package_name ?? void 0
6137
+ })) : [];
6138
+ if (start.repo && repos.length === 0) return {
6139
+ selectorMatched: false,
6140
+ startDiagnostics: [selectorRepoNotFoundDiagnostic(start.repo)]
6141
+ };
6142
+ if (start.repo && repos.length > 1) return {
6143
+ selectorMatched: false,
6144
+ startDiagnostics: [selectorRepoAmbiguousDiagnostic(start.repo, repos)]
6145
+ };
6146
+ const repo = repos[0];
6147
+ const operationScope = operationStartScope(
6148
+ db,
6149
+ repo?.id,
6150
+ start,
6151
+ hintOptions,
6152
+ workspaceId
4311
6153
  );
4312
- if (rows2.length > 0) return { files: new Set(rows2.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(rows2.map((row) => Number(row.symbolId)).filter(Boolean)), repoId };
4313
- if (start.servicePath && operation) {
4314
- const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId
4315
- FROM cds_services s JOIN cds_operations o ON o.service_id=s.id
4316
- JOIN graph_edges e ON e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status='resolved' AND e.from_kind='operation' AND e.from_id=CAST(o.id AS TEXT)
4317
- JOIN handler_methods hm ON hm.id=CAST(e.to_id AS INTEGER)
4318
- JOIN handler_classes hc ON hc.id=hm.handler_class_id
4319
- LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id AND sym.source_file=hc.source_file AND sym.name=hm.method_name
4320
- WHERE (? IS NULL OR s.repo_id=?) AND s.service_path=? AND (o.operation_path=? OR o.operation_name=?)`).all(repoId, repoId, start.servicePath, operation, operation);
4321
- if (implRows.length > 0) return { files: new Set(implRows.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(implRows.map((row) => Number(row.symbolId)).filter(Boolean)), repoId };
4322
- }
4323
- return void 0;
4324
- }
4325
- function startScope(db, start, hintOptions) {
4326
- const repo = start.repo ? db.prepare(
4327
- "SELECT id,name FROM repositories WHERE name=? OR package_name=?"
4328
- ).get(start.repo, start.repo) : void 0;
4329
- if (start.repo && !repo) return { repo, selectorMatched: false };
4330
- const operationScope = operationStartScope(db, repo?.id, start, hintOptions);
4331
6154
  const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === "operation" || d.resolutionStage === "implementation");
4332
- const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start);
6155
+ const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start, workspaceId);
6156
+ const terminalSelectorScope = Boolean(sourceScope?.diagnostics?.length && !sourceScope.files);
4333
6157
  const sourceFiles = sourceScope?.files;
4334
6158
  const hasSelector = Boolean(
4335
6159
  start.handler ?? start.operation ?? start.operationPath ?? start.servicePath
@@ -4341,9 +6165,9 @@ function startScope(db, start, hintOptions) {
4341
6165
  executionRepoId: sourceScope?.repoId ?? repo?.id,
4342
6166
  sourceFiles,
4343
6167
  symbolIds: sourceScope?.symbols,
4344
- selectorMatched: !terminalOperationScope && (!hasSelector || sourceFiles !== void 0),
6168
+ selectorMatched: !terminalOperationScope && !terminalSelectorScope && (!hasSelector || sourceFiles !== void 0),
4345
6169
  startOperationId: operationScope?.operationId,
4346
- startDiagnostics: operationScope?.diagnostics
6170
+ startDiagnostics: operationScope?.diagnostics?.length ? operationScope.diagnostics : sourceScope?.diagnostics
4347
6171
  };
4348
6172
  }
4349
6173
  function handlerFilesForOperation(db, operationId) {
@@ -4352,12 +6176,23 @@ function handlerFilesForOperation(db, operationId) {
4352
6176
  FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?`
4353
6177
  ).get(operationId);
4354
6178
  if (!op) return /* @__PURE__ */ new Set();
4355
- const operation = normalizeOperation(op.operationPath ?? op.operationName);
6179
+ const operation = normalizeOperation2(op.operationPath ?? op.operationName);
4356
6180
  const rows2 = db.prepare(
4357
6181
  `SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId FROM handler_classes hc
4358
6182
  JOIN handler_methods hm ON hm.handler_class_id=hc.id
4359
- LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id AND sym.source_file=hc.source_file AND sym.name=hm.method_name
4360
- WHERE hc.repo_id=? AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`
6183
+ LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
6184
+ AND sym.source_file=hc.source_file
6185
+ AND sym.qualified_name=hc.class_name || '.' || hm.method_name
6186
+ AND sym.start_line=hm.source_line
6187
+ WHERE hc.repo_id=?
6188
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
6189
+ CASE WHEN hm.decorator_kind='Event' THEN 'event'
6190
+ WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
6191
+ ELSE 'unsupported' END)='operation'
6192
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
6193
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On')
6194
+ THEN 1 ELSE 0 END)=1
6195
+ AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`
4361
6196
  ).all(op.repoId, operation, operation, op.operationName);
4362
6197
  return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
4363
6198
  }
@@ -4372,7 +6207,9 @@ function handlerMethodNode(db, methodId) {
4372
6207
  function implementationScope(db, operationId) {
4373
6208
  const edge = implementationEdge(db, operationId);
4374
6209
  if (!edge || edge.status !== "resolved") return { files: /* @__PURE__ */ new Set(), edge };
4375
- const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?").get(edge.to_id);
6210
+ const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.qualified_name=hc.class_name || '.' || hm.method_name AND s.start_line=hm.source_line WHERE hm.id=?").get(edge.to_id);
6211
+ if (!row || typeof row.symbolId !== "number")
6212
+ return { repoId: row?.repoId, files: /* @__PURE__ */ new Set(), edge };
4376
6213
  return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
4377
6214
  }
4378
6215
  function implementationMethodIdFromHint(edge, options) {
@@ -4408,8 +6245,8 @@ function contextImplementationMethodId(edge, callerRepoId, remoteEvidence, hintO
4408
6245
  return { blocksAutomatic: false, evidence: hinted.evidence.reason === "no_scoped_hint_matched_edge" ? hinted.evidence : { status: "tied", tieReason: scores.length > 1 ? "duplicate_helper_implementation_candidates" : "no_unique_materially_stronger_candidate", candidateScores: scores } };
4409
6246
  }
4410
6247
  function handlerScope(db, methodId) {
4411
- const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?").get(methodId);
4412
- if (!row) return void 0;
6248
+ const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.qualified_name=hc.class_name || '.' || hm.method_name AND s.start_line=hm.source_line WHERE hm.id=?").get(methodId);
6249
+ if (!row || typeof row.symbolId !== "number") return void 0;
4413
6250
  return { repoId: row.repoId, files: new Set(row.sourceFile ? [row.sourceFile] : []), symbolId: row.symbolId };
4414
6251
  }
4415
6252
  function traceEdgeType(call, row) {
@@ -4616,20 +6453,22 @@ function contextualRuntimeResolution(db, call, binding, workspaceId, persistedRo
4616
6453
  }
4617
6454
  function trace(db, start, options) {
4618
6455
  const hintOptions = { implementationRepo: options.implementationRepo, implementationHints: options.implementationHints };
4619
- const scope = startScope(db, start, hintOptions);
4620
- const diagnostics = db.prepare(
4621
- "SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)"
4622
- ).all(scope.repo?.id, scope.repo?.id);
4623
- const stale = db.prepare("SELECT name,graph_stale_reason reason FROM repositories WHERE graph_stale_reason IS NOT NULL AND (? IS NULL OR id=?)").all(scope.repo?.id, scope.repo?.id);
6456
+ const scope = startScope(db, start, hintOptions, options.workspaceId);
6457
+ const hasSelector = Boolean(start.repo || start.handler || start.operation || start.operationPath || start.servicePath);
6458
+ const diagnosticRepoId = scope.executionRepoId ?? scope.repo?.id;
6459
+ const diagnostics = loadTraceDiagnostics(
6460
+ db,
6461
+ diagnosticRepoId,
6462
+ !hasSelector,
6463
+ options.workspaceId
6464
+ );
6465
+ const stale = diagnosticRepoId !== void 0 || !hasSelector ? db.prepare("SELECT name,graph_stale_reason reason FROM repositories WHERE graph_stale_reason IS NOT NULL AND (? IS NULL OR id=?) AND (? IS NULL OR workspace_id=?) ORDER BY name,id").all(diagnosticRepoId, diagnosticRepoId, options.workspaceId, options.workspaceId) : [];
4624
6466
  for (const row of stale)
4625
- diagnostics.unshift({ severity: "warning", code: "graph_stale", message: `Graph is stale for ${row.name ?? "repository"}: ${row.reason ?? "facts_changed"}. Run service-flow link.` });
4626
- for (const diagnostic of scope.startDiagnostics ?? []) diagnostics.unshift(diagnostic);
6467
+ prependTraceDiagnostic(diagnostics, { severity: "warning", code: "graph_stale", message: `Graph is stale for ${row.name ?? "repository"}: ${row.reason ?? "facts_changed"}. Run service-flow link.` });
6468
+ for (const diagnostic of scope.startDiagnostics ?? [])
6469
+ prependTraceDiagnostic(diagnostics, diagnostic);
4627
6470
  if (!scope.selectorMatched && !scope.startDiagnostics?.length)
4628
- diagnostics.unshift({
4629
- severity: "warning",
4630
- code: "trace_start_not_found",
4631
- message: start.servicePath && !start.operation && !start.operationPath && !start.handler ? "Service-only trace requires --operation or --path and will not broaden to the whole workspace" : "No handler source matched the requested trace start selector"
4632
- });
6471
+ prependTraceDiagnostic(diagnostics, selectorNotFoundDiagnostic(start));
4633
6472
  const maxDepth = positiveDepth(options.depth);
4634
6473
  const edges = [];
4635
6474
  const nodes = /* @__PURE__ */ new Map();
@@ -4658,8 +6497,8 @@ function trace(db, start, options) {
4658
6497
  if (seenScopes.has(key)) continue;
4659
6498
  seenScopes.add(key);
4660
6499
  const calls = db.prepare(
4661
- `SELECT c.*,r.name repoName FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?) ORDER BY c.source_file,c.source_line`
4662
- ).all(current.repoId, current.repoId);
6500
+ `SELECT c.*,r.name repoName FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?) AND (? IS NULL OR r.workspace_id=?) ORDER BY c.source_file,c.source_line`
6501
+ ).all(current.repoId, current.repoId, options.workspaceId, options.workspaceId);
4663
6502
  const filtered = calls.filter(
4664
6503
  (c) => (!current.symbolIds || current.symbolIds.has(Number(c.source_symbol_id))) && (!current.files || current.files.has(String(c.source_file))) && includeCall(String(c.call_type), options)
4665
6504
  );
@@ -4738,7 +6577,7 @@ function trace(db, start, options) {
4738
6577
  if (implementation.edge) {
4739
6578
  const implEvidence = JSON.parse(String(implementation.edge.evidence_json || "{}"));
4740
6579
  const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence.implementationHintSuggestions);
4741
- if (hintDiagnostic) diagnostics.unshift(hintDiagnostic);
6580
+ if (hintDiagnostic) prependTraceDiagnostic(diagnostics, hintDiagnostic);
4742
6581
  const handlerNode = implementation.edge.status === "resolved" ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
4743
6582
  const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
4744
6583
  if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
@@ -4784,16 +6623,35 @@ function trace(db, start, options) {
4784
6623
  }
4785
6624
  }
4786
6625
  const runtimeDiagnostic = runtimeVariableDiagnostic(edges);
4787
- if (runtimeDiagnostic) diagnostics.unshift(runtimeDiagnostic);
6626
+ if (runtimeDiagnostic) prependTraceDiagnostic(diagnostics, runtimeDiagnostic);
6627
+ for (const diagnostic of runtimeNoCandidateDiagnostics(edges))
6628
+ prependTraceDiagnostic(diagnostics, diagnostic);
4788
6629
  return { start, nodes: [...nodes.values()], edges, diagnostics };
4789
6630
  }
4790
6631
 
4791
6632
  export {
6633
+ upsertWorkspace,
6634
+ getWorkspace,
6635
+ upsertRepository,
6636
+ listRepositories,
6637
+ reposByName,
6638
+ clearRepoFacts,
6639
+ insertRequires,
6640
+ insertService,
6641
+ insertHandler,
6642
+ handlerMethodIsExecutable,
6643
+ insertRegistrations,
6644
+ insertBindings,
6645
+ insertExecutableSymbols,
6646
+ insertSymbolCalls,
6647
+ insertCalls,
4792
6648
  normalizePath,
4793
6649
  stripQuotes,
4794
6650
  discoverRepositories,
4795
6651
  parsePackageJson,
6652
+ loadPackageJsonSnapshot,
4796
6653
  parseCdsFile,
6654
+ loadRepositorySourceContext,
4797
6655
  parseDecorators,
4798
6656
  parseHandlerRegistrations,
4799
6657
  redactText,
@@ -4810,6 +6668,7 @@ export {
4810
6668
  implementationHintSuggestions,
4811
6669
  linkWorkspace,
4812
6670
  parseVars,
6671
+ selectorRepoAmbiguousDiagnostic,
4813
6672
  trace
4814
6673
  };
4815
- //# sourceMappingURL=chunk-YZJKE5UX.js.map
6674
+ //# sourceMappingURL=chunk-PTLDSHRC.js.map