@saptools/service-flow 0.1.64 → 0.1.66

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 (42) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +67 -11
  3. package/TECHNICAL-NOTE.md +41 -1
  4. package/dist/{chunk-TBH33OYC.js → chunk-TOVX4WYH.js} +3765 -643
  5. package/dist/chunk-TOVX4WYH.js.map +1 -0
  6. package/dist/cli.js +231 -292
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +196 -1
  9. package/dist/index.js +7 -3
  10. package/dist/index.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/cli/002-doctor-lifecycle.ts +66 -0
  13. package/src/cli/doctor.ts +14 -27
  14. package/src/cli.ts +130 -102
  15. package/src/db/000-call-fact-repository.ts +537 -0
  16. package/src/db/001-fact-lifecycle.ts +111 -0
  17. package/src/db/migrations.ts +16 -1
  18. package/src/db/repositories.ts +1 -315
  19. package/src/db/schema.ts +4 -2
  20. package/src/index.ts +22 -0
  21. package/src/linker/004-event-subscription-handler-linker.ts +300 -0
  22. package/src/linker/cross-repo-linker.ts +23 -2
  23. package/src/output/001-compact-json-output.ts +6 -0
  24. package/src/parsers/imported-wrapper-parser.ts +4 -0
  25. package/src/parsers/outbound-call-parser.ts +11 -1
  26. package/src/parsers/symbol-parser.ts +110 -2
  27. package/src/trace/010-traversal-scope.ts +188 -0
  28. package/src/trace/011-event-subscriber-traversal.ts +366 -0
  29. package/src/trace/012-trace-graph-lookups.ts +74 -0
  30. package/src/trace/013-trace-root-scopes.ts +279 -0
  31. package/src/trace/014-compact-contract.ts +347 -0
  32. package/src/trace/015-trace-edge-recorder.ts +336 -0
  33. package/src/trace/016-compact-projector.ts +697 -0
  34. package/src/trace/017-trace-context.ts +378 -0
  35. package/src/trace/018-compact-trace.ts +87 -0
  36. package/src/trace/019-trace-edge-semantics.ts +328 -0
  37. package/src/trace/020-compact-field-projection.ts +387 -0
  38. package/src/trace/dynamic-branches.ts +35 -13
  39. package/src/trace/trace-engine.ts +271 -245
  40. package/src/types.ts +10 -0
  41. package/src/version.ts +1 -1
  42. package/dist/chunk-TBH33OYC.js.map +0 -1
@@ -1,6 +1,85 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/db/repositories.ts
3
+ // package.json
4
+ var package_default = {
5
+ name: "@saptools/service-flow",
6
+ version: "0.1.66",
7
+ description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
8
+ type: "module",
9
+ publishConfig: {
10
+ access: "public",
11
+ registry: "https://registry.npmjs.org/"
12
+ },
13
+ bin: {
14
+ "service-flow": "dist/cli.js"
15
+ },
16
+ main: "./dist/index.js",
17
+ types: "./dist/index.d.ts",
18
+ exports: {
19
+ ".": {
20
+ types: "./dist/index.d.ts",
21
+ import: "./dist/index.js"
22
+ }
23
+ },
24
+ files: [
25
+ "CHANGELOG.md",
26
+ "README.md",
27
+ "TECHNICAL-NOTE.md",
28
+ "dist",
29
+ "src"
30
+ ],
31
+ engines: {
32
+ node: ">=24.0.0"
33
+ },
34
+ scripts: {
35
+ build: "tsup",
36
+ typecheck: "tsc --noEmit",
37
+ lint: "eslint src tests",
38
+ test: "vitest run tests/unit",
39
+ "test:unit": "vitest run tests/unit",
40
+ "test:e2e": "vitest run tests/e2e",
41
+ "test:e2e:fake": "vitest run tests/e2e"
42
+ },
43
+ keywords: [
44
+ "sap",
45
+ "cap",
46
+ "cds",
47
+ "btp",
48
+ "cli",
49
+ "service-graph",
50
+ "call-graph",
51
+ "sqlite",
52
+ "saptools"
53
+ ],
54
+ author: "Dong Tran",
55
+ license: "MIT",
56
+ repository: {
57
+ type: "git",
58
+ url: "git+https://github.com/dongitran/saptools.git",
59
+ directory: "packages/service-flow"
60
+ },
61
+ homepage: "https://github.com/dongitran/saptools/tree/main/packages/service-flow#readme",
62
+ bugs: {
63
+ url: "https://github.com/dongitran/saptools/issues"
64
+ },
65
+ dependencies: {
66
+ commander: "13.1.0",
67
+ picocolors: "1.1.1",
68
+ typescript: "5.9.3",
69
+ zod: "4.4.3"
70
+ },
71
+ devDependencies: {
72
+ "@vitest/coverage-v8": "3.2.4",
73
+ tsup: "8.5.1",
74
+ vitest: "3.2.4"
75
+ }
76
+ };
77
+
78
+ // src/version.ts
79
+ var VERSION = package_default.version;
80
+ var ANALYZER_VERSION = "0.1.66-facts.1";
81
+
82
+ // src/db/000-call-fact-repository.ts
4
83
  import { posix } from "path";
5
84
 
6
85
  // src/utils/000-bounded-projection.ts
@@ -126,6 +205,406 @@ function upperFirst(value) {
126
205
  return value ? `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}` : value;
127
206
  }
128
207
 
208
+ // src/db/000-call-fact-repository.ts
209
+ function insertSymbolCalls(db, repoId, rows2) {
210
+ const callerStmt = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1");
211
+ const insertStmt = db.prepare("INSERT INTO symbol_calls(repo_id,caller_symbol_id,callee_symbol_id,callee_expression,import_source,source_file,source_line,call_site_start_offset,call_site_end_offset,call_role,status,confidence,evidence_json,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
212
+ for (const r of rows2) {
213
+ const caller = callerStmt.get(repoId, r.sourceFile, r.callerQualifiedName);
214
+ const target = resolveSymbolCallTarget(db, repoId, r);
215
+ insertStmt.run(
216
+ repoId,
217
+ caller?.id,
218
+ target.id,
219
+ r.calleeExpression,
220
+ r.importSource,
221
+ r.sourceFile,
222
+ r.sourceLine,
223
+ r.callSiteStartOffset,
224
+ r.callSiteEndOffset,
225
+ r.callRole,
226
+ target.status,
227
+ 0.8,
228
+ JSON.stringify({
229
+ ...r.evidence,
230
+ candidateStrategy: target.strategy,
231
+ candidateCount: target.candidateCount,
232
+ resolvedModulePath: target.resolvedModulePath
233
+ }),
234
+ target.reason
235
+ );
236
+ }
237
+ }
238
+ var stripExt = (value) => value.replace(/\.(ts|tsx|js|jsx|cds)$/, "");
239
+ function symbolTargetRows(rows2) {
240
+ return rows2.flatMap((row) => typeof row.id === "number" ? [{
241
+ id: row.id,
242
+ kind: typeof row.kind === "string" ? row.kind : void 0,
243
+ sourceFile: nullableString(row.sourceFile),
244
+ evidenceJson: nullableString(row.evidenceJson)
245
+ }] : []);
246
+ }
247
+ function relativeModuleTargets(callerSourceFile, importSource) {
248
+ const base = posix.dirname(callerSourceFile);
249
+ const joined = stripExt(posix.normalize(posix.join(base, importSource)));
250
+ return /* @__PURE__ */ new Set([joined, `${joined}/index`]);
251
+ }
252
+ function moduleRows(rows2, r) {
253
+ if (!r.importSource) return [];
254
+ const targets = relativeModuleTargets(r.sourceFile, r.importSource);
255
+ return rows2.filter((row) => typeof row.sourceFile === "string" && targets.has(stripExt(row.sourceFile)));
256
+ }
257
+ function resolvedSymbol(row, strategy, candidateCount, moduleScoped = false) {
258
+ return {
259
+ id: row.id,
260
+ status: "resolved",
261
+ reason: null,
262
+ strategy,
263
+ candidateCount,
264
+ resolvedModulePath: moduleScoped && row.sourceFile ? stripExt(row.sourceFile) : void 0
265
+ };
266
+ }
267
+ function exportedSymbolRows(db, repoId, r) {
268
+ return symbolTargetRows(db.prepare("SELECT id,kind,source_file sourceFile,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));
269
+ }
270
+ function isRelativeImportedSymbolCall(r) {
271
+ return Boolean(r.importSource?.startsWith("."));
272
+ }
273
+ function sameFileResolution(db, repoId, r, relation) {
274
+ const bareImport = relation === "relative_import" && isRelativeImportedSymbolCall(r) && !String(r.calleeLocalName).includes(".");
275
+ if (bareImport || relation === "relative_import_namespace_member" || relation === "package_import") return void 0;
276
+ const rows2 = symbolTargetRows(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));
277
+ if (rows2.length === 1 && rows2[0]) return resolvedSymbol(rows2[0], "same_file_exact", 1);
278
+ return rows2.length > 1 ? {
279
+ id: null,
280
+ status: "ambiguous",
281
+ reason: "Multiple same-file symbol targets matched exactly",
282
+ strategy: "same_file_exact",
283
+ candidateCount: rows2.length
284
+ } : void 0;
285
+ }
286
+ function classInstanceResolution(db, repoId, r, relation) {
287
+ if (relation !== "class_instance_method" || !isRelativeImportedSymbolCall(r))
288
+ return void 0;
289
+ const rows2 = symbolTargetRows(db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file<>? AND qualified_name=? ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName));
290
+ if (rows2.length === 1 && rows2[0])
291
+ return resolvedSymbol(rows2[0], "relative_import_class_instance_method", 1);
292
+ return rows2.length > 1 ? {
293
+ id: null,
294
+ status: "ambiguous",
295
+ reason: "Multiple relative class instance method targets matched exactly",
296
+ strategy: "relative_import_class_instance_method",
297
+ candidateCount: rows2.length
298
+ } : void 0;
299
+ }
300
+ function namespaceResolution(db, repoId, r, relation) {
301
+ if (relation !== "relative_import_namespace_member" || !isRelativeImportedSymbolCall(r)) return void 0;
302
+ const rows2 = moduleRows(exportedSymbolRows(db, repoId, r), r);
303
+ if (rows2.length === 1 && rows2[0])
304
+ return resolvedSymbol(rows2[0], "relative_import_namespace_member", 1, true);
305
+ if (rows2.length > 1) return {
306
+ id: null,
307
+ status: "ambiguous",
308
+ reason: "Multiple namespace member targets matched the imported module",
309
+ strategy: "relative_import_namespace_member",
310
+ candidateCount: rows2.length
311
+ };
312
+ return {
313
+ id: null,
314
+ status: "unresolved",
315
+ reason: "No namespace member target matched the imported module",
316
+ strategy: "relative_import_namespace_member",
317
+ candidateCount: 0
318
+ };
319
+ }
320
+ function proxyResolution(rows2, r, relation) {
321
+ if (relation !== "relative_import_proxy_member" || rows2.length <= 1) return void 0;
322
+ const mapped = rows2.filter(isExportedObjectMapping);
323
+ if (mapped.length > 0) {
324
+ const concrete = rows2.find((row) => row.kind !== "object_alias") ?? mapped[0];
325
+ return {
326
+ id: concrete?.id ?? null,
327
+ status: "resolved",
328
+ reason: null,
329
+ strategy: "proxy_member_exported_object_map",
330
+ candidateCount: rows2.length
331
+ };
332
+ }
333
+ const scoped = moduleRows(rows2, r);
334
+ if (scoped.length === 1 && scoped[0])
335
+ return resolvedSymbol(scoped[0], "relative_import_path_disambiguated", rows2.length, true);
336
+ return {
337
+ id: null,
338
+ status: "ambiguous",
339
+ reason: "Proxy member target requires explicit factory/module/type evidence; global member name is ambiguous",
340
+ strategy: "proxy_member_no_global_name_fallback",
341
+ candidateCount: rows2.length
342
+ };
343
+ }
344
+ function isExportedObjectMapping(row) {
345
+ const evidence = String(row.evidenceJson ?? "");
346
+ return evidence.includes("exported_object_shorthand") || evidence.includes("exported_object_literal");
347
+ }
348
+ function exportedResolution(rows2, r, relation) {
349
+ if (rows2.length === 1 && rows2[0]) return resolvedSymbol(
350
+ rows2[0],
351
+ relation === "relative_import_proxy_member" ? "proxy_member_unique_exported_candidate" : "relative_import_exported_exact",
352
+ 1,
353
+ moduleRows(rows2, r).length === 1
354
+ );
355
+ if (rows2.length <= 1) return void 0;
356
+ const scoped = isRelativeImportedSymbolCall(r) ? moduleRows(rows2, r) : [];
357
+ if (scoped.length === 1 && scoped[0])
358
+ return resolvedSymbol(scoped[0], "relative_import_path_disambiguated", rows2.length, true);
359
+ return {
360
+ id: null,
361
+ status: "ambiguous",
362
+ reason: "Multiple exported symbol targets matched exactly",
363
+ strategy: "exported_exact",
364
+ candidateCount: rows2.length
365
+ };
366
+ }
367
+ function accessorResolution(db, repoId, r, relation) {
368
+ if (relation !== "relative_import" || !isRelativeImportedSymbolCall(r) || !/^[^.]+\.[^.]+$/.test(String(r.calleeLocalName))) return void 0;
369
+ const methodRows = symbolTargetRows(db.prepare("SELECT id,kind,source_file sourceFile FROM symbols WHERE repo_id=? AND source_file<>? AND kind='method' AND qualified_name=? ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName));
370
+ const scoped = moduleRows(methodRows, r);
371
+ if (scoped.length === 1 && scoped[0]) return resolvedSymbol(
372
+ scoped[0],
373
+ "relative_import_static_accessor_instance_method",
374
+ 1,
375
+ true
376
+ );
377
+ return scoped.length > 1 ? {
378
+ id: null,
379
+ status: "ambiguous",
380
+ reason: "Multiple static-accessor instance method targets matched the imported module",
381
+ strategy: "relative_import_static_accessor_instance_method",
382
+ candidateCount: scoped.length
383
+ } : void 0;
384
+ }
385
+ function resolveSymbolCallTarget(db, repoId, r) {
386
+ const relation = r.evidence.relation;
387
+ const early = sameFileResolution(db, repoId, r, relation) ?? classInstanceResolution(db, repoId, r, relation) ?? namespaceResolution(db, repoId, r, relation);
388
+ if (early) return early;
389
+ const rows2 = relation === "package_import" ? [] : exportedSymbolRows(db, repoId, r);
390
+ const matched = proxyResolution(rows2, r, relation) ?? exportedResolution(rows2, r, relation) ?? accessorResolution(db, repoId, r, relation);
391
+ if (matched) return matched;
392
+ if (relation === "package_import") return {
393
+ id: null,
394
+ status: "unresolved",
395
+ reason: "Package import target resolution requires a post-publication workspace pass",
396
+ strategy: "package_import_unresolved",
397
+ candidateCount: 0
398
+ };
399
+ return {
400
+ id: null,
401
+ status: "unresolved",
402
+ reason: "No local symbol target matched exactly",
403
+ strategy: relation === "relative_import_proxy_member" ? "proxy_member_no_global_name_fallback" : "exact_symbol_match",
404
+ candidateCount: 0
405
+ };
406
+ }
407
+ function insertCalls(db, repoId, rows2) {
408
+ const stmt = outboundCallInsertStatement(db);
409
+ for (const row of rows2) insertOutboundCall(db, stmt, repoId, row);
410
+ }
411
+ function outboundCallInsertStatement(db) {
412
+ return db.prepare(`INSERT INTO outbound_calls(
413
+ repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,
414
+ event_name_expr,payload_summary,source_file,source_line,call_site_start_offset,
415
+ call_site_end_offset,confidence,unresolved_reason,local_service_name,
416
+ local_service_lookup,alias_chain_json,evidence_json,external_target_kind,
417
+ external_target_id,external_target_label,external_target_dynamic,service_binding_id
418
+ ) VALUES(
419
+ ?,COALESCE(
420
+ (SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1),
421
+ (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)
422
+ ),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
423
+ )`);
424
+ }
425
+ function insertOutboundCall(db, stmt, repoId, call) {
426
+ const binding = resolvePersistedBinding(db, repoId, call);
427
+ const external = externalTargetValues(call.externalTarget);
428
+ const evidence = {
429
+ ...call.evidence ?? {},
430
+ serviceBindingResolution: binding.evidence
431
+ };
432
+ stmt.run(
433
+ repoId,
434
+ repoId,
435
+ call.sourceFile,
436
+ call.sourceSymbolQualifiedName,
437
+ repoId,
438
+ call.sourceFile,
439
+ call.sourceLine,
440
+ call.sourceLine,
441
+ call.callType,
442
+ call.method,
443
+ call.operationPathExpr,
444
+ call.queryEntity,
445
+ call.eventNameExpr,
446
+ call.payloadSummary,
447
+ call.sourceFile,
448
+ call.sourceLine,
449
+ call.callSiteStartOffset,
450
+ call.callSiteEndOffset,
451
+ call.confidence,
452
+ call.unresolvedReason ?? binding.unresolvedReason,
453
+ call.localServiceName,
454
+ call.localServiceLookup,
455
+ serializedAliasChain(call.aliasChain),
456
+ JSON.stringify(evidence),
457
+ external.kind,
458
+ external.stableId,
459
+ external.label,
460
+ external.dynamic,
461
+ binding.bindingId
462
+ );
463
+ }
464
+ function serializedAliasChain(aliasChain) {
465
+ return aliasChain ? JSON.stringify(aliasChain) : null;
466
+ }
467
+ function externalTargetValues(target) {
468
+ if (!target) return { kind: null, stableId: null, label: null, dynamic: 0 };
469
+ return {
470
+ kind: target.kind,
471
+ stableId: target.stableId,
472
+ label: target.label,
473
+ dynamic: target.dynamic ? 1 : 0
474
+ };
475
+ }
476
+ function resolvePersistedBinding(db, repoId, call) {
477
+ if (!call.serviceVariableName)
478
+ return { bindingId: null, evidence: { status: "not_applicable", candidateCount: 0 } };
479
+ const candidates2 = bindingCandidates(db, repoId, call);
480
+ const prior = candidates2.filter((candidate) => candidate.sourceLine <= call.sourceLine);
481
+ const families = new Set(prior.map(bindingSignature));
482
+ if (prior.length > 0 && families.size === 1) {
483
+ const selected = prior.at(-1);
484
+ return {
485
+ bindingId: selected?.id ?? null,
486
+ evidence: bindingEvidence("selected", prior, selected)
487
+ };
488
+ }
489
+ if (prior.length > 1) {
490
+ return {
491
+ bindingId: null,
492
+ unresolvedReason: "ambiguous_service_binding_candidates",
493
+ evidence: bindingEvidence("ambiguous", prior)
494
+ };
495
+ }
496
+ if (candidates2.length > 0) {
497
+ return {
498
+ bindingId: null,
499
+ unresolvedReason: "service_binding_declared_after_call",
500
+ evidence: bindingEvidence("rejected_future_binding", candidates2)
501
+ };
502
+ }
503
+ return {
504
+ bindingId: null,
505
+ evidence: bindingEvidence("unrecoverable", [])
506
+ };
507
+ }
508
+ function bindingCandidates(db, repoId, call) {
509
+ const ownerId = callSymbolId(db, repoId, call);
510
+ const rows2 = db.prepare(`
511
+ SELECT id,symbol_id symbolId,variable_name variableName,alias,alias_expr aliasExpr,
512
+ destination_expr destinationExpr,service_path_expr servicePathExpr,
513
+ source_file sourceFile,source_line sourceLine,helper_chain_json helperChainJson
514
+ FROM service_bindings
515
+ WHERE repo_id=? AND variable_name=? AND source_file=?
516
+ AND (? IS NULL OR symbol_id IS NULL OR symbol_id=?)
517
+ ORDER BY source_line,id
518
+ `).all(
519
+ repoId,
520
+ call.serviceVariableName,
521
+ call.sourceFile,
522
+ ownerId,
523
+ ownerId
524
+ );
525
+ return rows2.flatMap((row) => {
526
+ if (typeof row.id !== "number" || typeof row.variableName !== "string" || typeof row.sourceFile !== "string" || typeof row.sourceLine !== "number")
527
+ return [];
528
+ return [{
529
+ id: row.id,
530
+ symbolId: nullableNumber(row.symbolId),
531
+ variableName: row.variableName,
532
+ alias: nullableString(row.alias),
533
+ aliasExpr: nullableString(row.aliasExpr),
534
+ destinationExpr: nullableString(row.destinationExpr),
535
+ servicePathExpr: nullableString(row.servicePathExpr),
536
+ sourceFile: row.sourceFile,
537
+ sourceLine: row.sourceLine,
538
+ helperChainJson: nullableString(row.helperChainJson)
539
+ }];
540
+ });
541
+ }
542
+ function nullableString(value) {
543
+ return value === null || typeof value === "string" ? value : void 0;
544
+ }
545
+ function nullableNumber(value) {
546
+ return value === null || typeof value === "number" ? value : void 0;
547
+ }
548
+ function callSymbolId(db, repoId, call) {
549
+ const row = db.prepare(`
550
+ SELECT id FROM symbols
551
+ WHERE repo_id=? AND source_file=?
552
+ AND ((? IS NOT NULL AND qualified_name=?)
553
+ OR (start_line<=? AND end_line>=?))
554
+ ORDER BY CASE WHEN qualified_name=? THEN 0 ELSE 1 END,
555
+ (end_line-start_line),id
556
+ LIMIT 1
557
+ `).get(
558
+ repoId,
559
+ call.sourceFile,
560
+ call.sourceSymbolQualifiedName,
561
+ call.sourceSymbolQualifiedName,
562
+ call.sourceLine,
563
+ call.sourceLine,
564
+ call.sourceSymbolQualifiedName
565
+ );
566
+ return typeof row?.id === "number" ? row.id : void 0;
567
+ }
568
+ function bindingEvidence(status, candidates2, selected) {
569
+ const projection = projectBounded(candidates2, (left, right) => Number(right.id === selected?.id) - Number(left.id === selected?.id) || left.sourceFile.localeCompare(right.sourceFile) || left.sourceLine - right.sourceLine || left.id - right.id);
570
+ return {
571
+ status,
572
+ candidateCount: projection.totalCount,
573
+ shownCandidateCount: projection.shownCount,
574
+ omittedCandidateCount: projection.omittedCount,
575
+ selectedBindingId: selected?.id,
576
+ sourceOrderRule: "binding_source_line_must_not_follow_call",
577
+ candidates: projection.items.map((candidate) => ({
578
+ bindingId: candidate.id,
579
+ symbolId: candidate.symbolId,
580
+ variableName: candidate.variableName,
581
+ alias: candidate.alias,
582
+ aliasExpr: candidate.aliasExpr,
583
+ destinationExpr: candidate.destinationExpr,
584
+ servicePathExpr: candidate.servicePathExpr,
585
+ sourceFile: candidate.sourceFile,
586
+ sourceLine: candidate.sourceLine,
587
+ helperChain: parseBindingChain(candidate.helperChainJson)
588
+ }))
589
+ };
590
+ }
591
+ function bindingSignature(candidate) {
592
+ return JSON.stringify([
593
+ candidate.alias,
594
+ candidate.aliasExpr,
595
+ candidate.destinationExpr,
596
+ candidate.servicePathExpr
597
+ ]);
598
+ }
599
+ function parseBindingChain(value) {
600
+ if (!value) return void 0;
601
+ try {
602
+ return JSON.parse(value);
603
+ } catch {
604
+ return void 0;
605
+ }
606
+ }
607
+
129
608
  // src/db/repositories.ts
130
609
  function upsertWorkspace(db, rootPath, dbPath) {
131
610
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -384,265 +863,6 @@ function insertExecutableSymbols(db, repoId, rows2) {
384
863
  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=?),?,?,?,?,?,?,?,?,?,?,?)");
385
864
  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);
386
865
  }
387
- function insertSymbolCalls(db, repoId, rows2) {
388
- const callerStmt = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1");
389
- 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(?,?,?,?,?,?,?,?,?,?,?)");
390
- for (const r of rows2) {
391
- const caller = callerStmt.get(repoId, r.sourceFile, r.callerQualifiedName);
392
- const target = resolveSymbolCallTarget(db, repoId, r);
393
- 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, resolvedModulePath: target.resolvedModulePath }), target.reason);
394
- }
395
- }
396
- var stripExt = (value) => value.replace(/\.(ts|tsx|js|jsx|cds)$/, "");
397
- function symbolTargetRows(rows2) {
398
- return rows2.flatMap((row) => typeof row.id === "number" ? [{ id: row.id, kind: typeof row.kind === "string" ? row.kind : void 0, sourceFile: nullableString(row.sourceFile), evidenceJson: nullableString(row.evidenceJson) }] : []);
399
- }
400
- function relativeModuleTargets(callerSourceFile, importSource) {
401
- const base = posix.dirname(callerSourceFile);
402
- const joined = stripExt(posix.normalize(posix.join(base, importSource)));
403
- return /* @__PURE__ */ new Set([joined, `${joined}/index`]);
404
- }
405
- function moduleRows(rows2, r) {
406
- if (!r.importSource) return [];
407
- const targets = relativeModuleTargets(r.sourceFile, r.importSource);
408
- return rows2.filter((row) => typeof row.sourceFile === "string" && targets.has(stripExt(row.sourceFile)));
409
- }
410
- function resolvedSymbol(row, strategy, candidateCount, moduleScoped = false) {
411
- return { id: row.id, status: "resolved", reason: null, strategy, candidateCount, resolvedModulePath: moduleScoped && row.sourceFile ? stripExt(row.sourceFile) : void 0 };
412
- }
413
- function exportedSymbolRows(db, repoId, r) {
414
- return symbolTargetRows(db.prepare("SELECT id,kind,source_file sourceFile,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));
415
- }
416
- function isRelativeImportedSymbolCall(r) {
417
- return Boolean(r.importSource?.startsWith("."));
418
- }
419
- function sameFileResolution(db, repoId, r, relation) {
420
- const bareImport = relation === "relative_import" && isRelativeImportedSymbolCall(r) && !String(r.calleeLocalName).includes(".");
421
- if (bareImport || relation === "relative_import_namespace_member" || relation === "package_import") return void 0;
422
- const rows2 = symbolTargetRows(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));
423
- if (rows2.length === 1 && rows2[0]) return resolvedSymbol(rows2[0], "same_file_exact", 1);
424
- return rows2.length > 1 ? { id: null, status: "ambiguous", reason: "Multiple same-file symbol targets matched exactly", strategy: "same_file_exact", candidateCount: rows2.length } : void 0;
425
- }
426
- function classInstanceResolution(db, repoId, r, relation) {
427
- if (relation !== "class_instance_method" || !isRelativeImportedSymbolCall(r)) return void 0;
428
- const rows2 = symbolTargetRows(db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file<>? AND qualified_name=? ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName));
429
- if (rows2.length === 1 && rows2[0]) return resolvedSymbol(rows2[0], "relative_import_class_instance_method", 1);
430
- return rows2.length > 1 ? { id: null, status: "ambiguous", reason: "Multiple relative class instance method targets matched exactly", strategy: "relative_import_class_instance_method", candidateCount: rows2.length } : void 0;
431
- }
432
- function namespaceResolution(db, repoId, r, relation) {
433
- if (relation !== "relative_import_namespace_member" || !isRelativeImportedSymbolCall(r)) return void 0;
434
- const rows2 = moduleRows(exportedSymbolRows(db, repoId, r), r);
435
- if (rows2.length === 1 && rows2[0]) return resolvedSymbol(rows2[0], "relative_import_namespace_member", 1, true);
436
- if (rows2.length > 1) return { id: null, status: "ambiguous", reason: "Multiple namespace member targets matched the imported module", strategy: "relative_import_namespace_member", candidateCount: rows2.length };
437
- return { id: null, status: "unresolved", reason: "No namespace member target matched the imported module", strategy: "relative_import_namespace_member", candidateCount: 0 };
438
- }
439
- function proxyResolution(rows2, r, relation) {
440
- if (relation !== "relative_import_proxy_member" || rows2.length <= 1) return void 0;
441
- const mapped = rows2.filter((row) => String(row.evidenceJson ?? "").includes("exported_object_shorthand") || String(row.evidenceJson ?? "").includes("exported_object_literal"));
442
- if (mapped.length > 0) {
443
- const concrete = rows2.find((row) => row.kind !== "object_alias") ?? mapped[0];
444
- return { id: concrete?.id ?? null, status: "resolved", reason: null, strategy: "proxy_member_exported_object_map", candidateCount: rows2.length };
445
- }
446
- const scoped = moduleRows(rows2, r);
447
- if (scoped.length === 1 && scoped[0]) return resolvedSymbol(scoped[0], "relative_import_path_disambiguated", rows2.length, true);
448
- 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 };
449
- }
450
- function exportedResolution(rows2, r, relation) {
451
- if (rows2.length === 1 && rows2[0]) return resolvedSymbol(rows2[0], relation === "relative_import_proxy_member" ? "proxy_member_unique_exported_candidate" : "relative_import_exported_exact", 1, moduleRows(rows2, r).length === 1);
452
- if (rows2.length <= 1) return void 0;
453
- const scoped = isRelativeImportedSymbolCall(r) ? moduleRows(rows2, r) : [];
454
- if (scoped.length === 1 && scoped[0]) return resolvedSymbol(scoped[0], "relative_import_path_disambiguated", rows2.length, true);
455
- return { id: null, status: "ambiguous", reason: "Multiple exported symbol targets matched exactly", strategy: "exported_exact", candidateCount: rows2.length };
456
- }
457
- function accessorResolution(db, repoId, r, relation) {
458
- if (relation !== "relative_import" || !isRelativeImportedSymbolCall(r) || !/^[^.]+\.[^.]+$/.test(String(r.calleeLocalName))) return void 0;
459
- const methodRows = symbolTargetRows(db.prepare("SELECT id,kind,source_file sourceFile FROM symbols WHERE repo_id=? AND source_file<>? AND kind='method' AND qualified_name=? ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName));
460
- const scoped = moduleRows(methodRows, r);
461
- if (scoped.length === 1 && scoped[0]) return resolvedSymbol(scoped[0], "relative_import_static_accessor_instance_method", 1, true);
462
- return scoped.length > 1 ? { id: null, status: "ambiguous", reason: "Multiple static-accessor instance method targets matched the imported module", strategy: "relative_import_static_accessor_instance_method", candidateCount: scoped.length } : void 0;
463
- }
464
- function resolveSymbolCallTarget(db, repoId, r) {
465
- const relation = r.evidence.relation;
466
- const early = sameFileResolution(db, repoId, r, relation) ?? classInstanceResolution(db, repoId, r, relation) ?? namespaceResolution(db, repoId, r, relation);
467
- if (early) return early;
468
- const rows2 = relation === "package_import" ? [] : exportedSymbolRows(db, repoId, r);
469
- const matched = proxyResolution(rows2, r, relation) ?? exportedResolution(rows2, r, relation) ?? accessorResolution(db, repoId, r, relation);
470
- if (matched) return matched;
471
- if (relation === "package_import") return { id: null, status: "unresolved", reason: "Package import target resolution requires a post-publication workspace pass", strategy: "package_import_unresolved", candidateCount: 0 };
472
- return { id: null, status: "unresolved", reason: "No local symbol target matched exactly", strategy: relation === "relative_import_proxy_member" ? "proxy_member_no_global_name_fallback" : "exact_symbol_match", candidateCount: 0 };
473
- }
474
- function insertCalls(db, repoId, rows2) {
475
- const stmt = db.prepare(
476
- "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)),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
477
- );
478
- for (const r of rows2) {
479
- const binding = resolvePersistedBinding(db, repoId, r);
480
- const evidence = {
481
- ...r.evidence ?? {},
482
- serviceBindingResolution: binding.evidence
483
- };
484
- stmt.run(
485
- repoId,
486
- repoId,
487
- r.sourceFile,
488
- r.sourceSymbolQualifiedName,
489
- repoId,
490
- r.sourceFile,
491
- r.sourceLine,
492
- r.sourceLine,
493
- r.callType,
494
- r.method,
495
- r.operationPathExpr,
496
- r.queryEntity,
497
- r.eventNameExpr,
498
- r.payloadSummary,
499
- r.sourceFile,
500
- r.sourceLine,
501
- r.confidence,
502
- r.unresolvedReason ?? binding.unresolvedReason,
503
- r.localServiceName,
504
- r.localServiceLookup,
505
- r.aliasChain ? JSON.stringify(r.aliasChain) : null,
506
- JSON.stringify(evidence),
507
- r.externalTarget?.kind ?? null,
508
- r.externalTarget?.stableId ?? null,
509
- r.externalTarget?.label ?? null,
510
- r.externalTarget?.dynamic ? 1 : 0,
511
- binding.bindingId
512
- );
513
- }
514
- }
515
- function resolvePersistedBinding(db, repoId, call) {
516
- if (!call.serviceVariableName)
517
- return { bindingId: null, evidence: { status: "not_applicable", candidateCount: 0 } };
518
- const candidates2 = bindingCandidates(db, repoId, call);
519
- const prior = candidates2.filter((candidate) => candidate.sourceLine <= call.sourceLine);
520
- const families = new Set(prior.map(bindingSignature));
521
- if (prior.length > 0 && families.size === 1) {
522
- const selected = prior.at(-1);
523
- return {
524
- bindingId: selected?.id ?? null,
525
- evidence: bindingEvidence("selected", prior, selected)
526
- };
527
- }
528
- if (prior.length > 1) {
529
- return {
530
- bindingId: null,
531
- unresolvedReason: "ambiguous_service_binding_candidates",
532
- evidence: bindingEvidence("ambiguous", prior)
533
- };
534
- }
535
- if (candidates2.length > 0) {
536
- return {
537
- bindingId: null,
538
- unresolvedReason: "service_binding_declared_after_call",
539
- evidence: bindingEvidence("rejected_future_binding", candidates2)
540
- };
541
- }
542
- return {
543
- bindingId: null,
544
- evidence: bindingEvidence("unrecoverable", [])
545
- };
546
- }
547
- function bindingCandidates(db, repoId, call) {
548
- const ownerId = callSymbolId(db, repoId, call);
549
- const rows2 = db.prepare(`
550
- SELECT id,symbol_id symbolId,variable_name variableName,alias,alias_expr aliasExpr,
551
- destination_expr destinationExpr,service_path_expr servicePathExpr,
552
- source_file sourceFile,source_line sourceLine,helper_chain_json helperChainJson
553
- FROM service_bindings
554
- WHERE repo_id=? AND variable_name=? AND source_file=?
555
- AND (? IS NULL OR symbol_id IS NULL OR symbol_id=?)
556
- ORDER BY source_line,id
557
- `).all(
558
- repoId,
559
- call.serviceVariableName,
560
- call.sourceFile,
561
- ownerId,
562
- ownerId
563
- );
564
- return rows2.flatMap((row) => {
565
- if (typeof row.id !== "number" || typeof row.variableName !== "string" || typeof row.sourceFile !== "string" || typeof row.sourceLine !== "number")
566
- return [];
567
- return [{
568
- id: row.id,
569
- symbolId: nullableNumber(row.symbolId),
570
- variableName: row.variableName,
571
- alias: nullableString(row.alias),
572
- aliasExpr: nullableString(row.aliasExpr),
573
- destinationExpr: nullableString(row.destinationExpr),
574
- servicePathExpr: nullableString(row.servicePathExpr),
575
- sourceFile: row.sourceFile,
576
- sourceLine: row.sourceLine,
577
- helperChainJson: nullableString(row.helperChainJson)
578
- }];
579
- });
580
- }
581
- function nullableString(value) {
582
- return value === null || typeof value === "string" ? value : void 0;
583
- }
584
- function nullableNumber(value) {
585
- return value === null || typeof value === "number" ? value : void 0;
586
- }
587
- function callSymbolId(db, repoId, call) {
588
- const row = db.prepare(`
589
- SELECT id FROM symbols
590
- WHERE repo_id=? AND source_file=?
591
- AND ((? IS NOT NULL AND qualified_name=?)
592
- OR (start_line<=? AND end_line>=?))
593
- ORDER BY CASE WHEN qualified_name=? THEN 0 ELSE 1 END,
594
- (end_line-start_line),id
595
- LIMIT 1
596
- `).get(
597
- repoId,
598
- call.sourceFile,
599
- call.sourceSymbolQualifiedName,
600
- call.sourceSymbolQualifiedName,
601
- call.sourceLine,
602
- call.sourceLine,
603
- call.sourceSymbolQualifiedName
604
- );
605
- return typeof row?.id === "number" ? row.id : void 0;
606
- }
607
- function bindingEvidence(status, candidates2, selected) {
608
- const projection = projectBounded(candidates2, (left, right) => Number(right.id === selected?.id) - Number(left.id === selected?.id) || left.sourceFile.localeCompare(right.sourceFile) || left.sourceLine - right.sourceLine || left.id - right.id);
609
- return {
610
- status,
611
- candidateCount: projection.totalCount,
612
- shownCandidateCount: projection.shownCount,
613
- omittedCandidateCount: projection.omittedCount,
614
- selectedBindingId: selected?.id,
615
- sourceOrderRule: "binding_source_line_must_not_follow_call",
616
- candidates: projection.items.map((candidate) => ({
617
- bindingId: candidate.id,
618
- symbolId: candidate.symbolId,
619
- variableName: candidate.variableName,
620
- alias: candidate.alias,
621
- aliasExpr: candidate.aliasExpr,
622
- destinationExpr: candidate.destinationExpr,
623
- servicePathExpr: candidate.servicePathExpr,
624
- sourceFile: candidate.sourceFile,
625
- sourceLine: candidate.sourceLine,
626
- helperChain: parseBindingChain(candidate.helperChainJson)
627
- }))
628
- };
629
- }
630
- function bindingSignature(candidate) {
631
- return JSON.stringify([
632
- candidate.alias,
633
- candidate.aliasExpr,
634
- candidate.destinationExpr,
635
- candidate.servicePathExpr
636
- ]);
637
- }
638
- function parseBindingChain(value) {
639
- if (!value) return void 0;
640
- try {
641
- return JSON.parse(value);
642
- } catch {
643
- return void 0;
644
- }
645
- }
646
866
 
647
867
  // src/discovery/discover-repositories.ts
648
868
  import fs from "fs/promises";
@@ -3059,10 +3279,14 @@ function wrapperCallFact(source, filePath, call, spec, serviceBindings) {
3059
3279
  payloadSummary: call.getText(source),
3060
3280
  sourceFile: normalizePath(filePath),
3061
3281
  sourceLine: lineOf3(source, call),
3282
+ callSiteStartOffset: call.getStart(source),
3283
+ callSiteEndOffset: call.getEnd(),
3062
3284
  confidence: operationPathExpr ? 0.85 : 0.5,
3063
3285
  unresolvedReason,
3064
3286
  evidence: {
3065
3287
  parser: "typescript_ast",
3288
+ startOffset: call.getStart(source),
3289
+ endOffset: call.getEnd(),
3066
3290
  classifier: importedWrapperClassifier(pathAnalysis.status),
3067
3291
  receiver: client.text,
3068
3292
  wrapperFunction: spec.chain[0],
@@ -4012,7 +4236,15 @@ function classifyOutboundCallsInSource(source, filePath) {
4012
4236
  const wrapperSpecs = collectWrapperSpecs(source);
4013
4237
  const wrapperInternalRanges = [...wrapperSpecs.values()].map((spec) => ({ start: spec.internalStart, end: spec.internalEnd }));
4014
4238
  const add = (node, fact, extra) => {
4015
- calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf4(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
4239
+ calls.push({ node, fact: {
4240
+ ...fact,
4241
+ sourceFile,
4242
+ sourceLine: lineOf4(source.text, node.getStart(source)),
4243
+ callSiteStartOffset: node.getStart(source),
4244
+ callSiteEndOffset: node.getEnd(),
4245
+ confidence: fact.confidence ?? 0.8,
4246
+ evidence: parserEvidence(source, node, extra)
4247
+ } });
4016
4248
  };
4017
4249
  const visit = (node) => {
4018
4250
  if (ts10.isCallExpression(node)) {
@@ -4167,6 +4399,8 @@ function parseLocalServiceCalls(text, filePath, source = ts10.createSourceFile(
4167
4399
  aliasChain: parsed.chain,
4168
4400
  sourceFile: normalizePath(filePath),
4169
4401
  sourceLine: lineOf4(text, node.getStart(source)),
4402
+ callSiteStartOffset: node.getStart(source),
4403
+ callSiteEndOffset: node.getEnd(),
4170
4404
  confidence: 0.9,
4171
4405
  unresolvedReason: ["send", "emit", "publish", "on"].includes(parsed.operation) ? "transport_client_method" : void 0,
4172
4406
  evidence: parserEvidence(source, node, {
@@ -5757,51 +5991,516 @@ function linkPackageImportSymbolCalls(db, workspaceId) {
5757
5991
  return summary;
5758
5992
  }
5759
5993
 
5760
- // src/linker/cross-repo-linker.ts
5761
- function linkWorkspace(db, workspaceId, vars = {}) {
5762
- return db.transaction(() => {
5763
- const generation = nextGraphGeneration(db, workspaceId);
5764
- db.prepare("DELETE FROM graph_edges WHERE workspace_id=?").run(workspaceId);
5765
- const deps = linkHelperPackages(db, workspaceId, generation);
5766
- linkPackageImportSymbolCalls(db, workspaceId);
5767
- const impl = linkImplementations(db, workspaceId, generation);
5768
- const callSummary = linkCalls(db, workspaceId, vars, generation);
5769
- db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
5770
- return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount, implementationUnresolvedCount: impl.unresolvedCount };
5771
- });
5994
+ // src/linker/004-event-subscription-handler-linker.ts
5995
+ var symbolCallReasonLimit = 512;
5996
+ function subscriptionRows(db, workspaceId) {
5997
+ return db.prepare(`SELECT c.id,r.workspace_id workspaceId,c.repo_id repoId,r.name repoName,
5998
+ c.source_symbol_id sourceSymbolId,c.event_name_expr eventName,
5999
+ c.source_file sourceFile,c.source_line sourceLine,
6000
+ c.call_site_start_offset startOffset,c.call_site_end_offset endOffset,
6001
+ c.confidence
6002
+ FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
6003
+ WHERE r.workspace_id=? AND c.call_type='async_subscribe'
6004
+ ORDER BY r.name COLLATE BINARY,r.id,c.source_file COLLATE BINARY,
6005
+ c.call_site_start_offset,c.call_site_end_offset,c.id`).all(
6006
+ workspaceId
6007
+ );
5772
6008
  }
5773
- function nextGraphGeneration(db, workspaceId) {
5774
- const row = db.prepare("SELECT COALESCE(MAX(graph_generation),0) generation FROM repositories WHERE workspace_id=?").get(workspaceId);
5775
- return Number(row?.generation ?? 0) + 1;
6009
+ function roleSiteRows(db, subscription) {
6010
+ if (typeof subscription.startOffset !== "number" || typeof subscription.endOffset !== "number") return [];
6011
+ return db.prepare(`SELECT sc.id,sc.caller_symbol_id callerSymbolId,
6012
+ sc.callee_symbol_id calleeSymbolId,sc.status,
6013
+ sc.unresolved_reason unresolvedReason,sc.confidence,sc.source_line sourceLine,
6014
+ json_extract(sc.evidence_json,'$.factOrigin') factOrigin,
6015
+ json_extract(sc.evidence_json,'$.wrapperFunction') wrapperFunction,
6016
+ json_extract(sc.evidence_json,'$.candidateStrategy') strategy,
6017
+ json_extract(sc.evidence_json,'$.candidateCount') candidateCount,
6018
+ target.source_file targetSourceFile,target.start_line targetSourceLine,
6019
+ target_repo.workspace_id targetWorkspaceId
6020
+ FROM symbol_calls sc LEFT JOIN symbols target ON target.id=sc.callee_symbol_id
6021
+ LEFT JOIN repositories target_repo ON target_repo.id=target.repo_id
6022
+ WHERE sc.repo_id=? AND sc.source_file=?
6023
+ AND sc.call_site_start_offset=? AND sc.call_site_end_offset=?
6024
+ AND sc.call_role='event_subscribe_handler'
6025
+ ORDER BY sc.id`).all(
6026
+ subscription.repoId,
6027
+ subscription.sourceFile,
6028
+ subscription.startOffset,
6029
+ subscription.endOffset
6030
+ );
5776
6031
  }
5777
- function linkCalls(db, workspaceId, vars, generation) {
5778
- let edgeCount = 0;
5779
- let unresolvedCount = 0;
5780
- let resolvedCount = 0;
5781
- let remoteResolvedCount = 0;
5782
- let localResolvedCount = 0;
5783
- let ambiguousCount = 0;
5784
- let dynamicCount = 0;
5785
- let terminalCount = 0;
5786
- const calls = db.prepare(`SELECT c.*,r.name repoName,b.id selectedBindingId,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.is_dynamic isDynamic,b.placeholders_json placeholdersJson,b.source_file bindingSourceFile,b.source_line bindingSourceLine,b.helper_chain_json helperChainJson,req.service_path requireServicePath,req.destination requireDestination FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id LEFT JOIN service_bindings b ON b.id=c.service_binding_id LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias WHERE r.workspace_id=?`).all(workspaceId);
5787
- for (const call of calls) {
5788
- const result = insertCallEdge(db, workspaceId, call, vars, generation);
5789
- edgeCount += 1;
5790
- resolvedCount += result.status === "resolved" ? 1 : 0;
5791
- remoteResolvedCount += result.status === "resolved" && result.callType !== "local_service_call" ? 1 : 0;
5792
- localResolvedCount += result.status === "resolved" && result.callType === "local_service_call" ? 1 : 0;
5793
- unresolvedCount += result.status === "unresolved" ? 1 : 0;
5794
- ambiguousCount += result.status === "ambiguous" ? 1 : 0;
5795
- dynamicCount += result.status === "dynamic" ? 1 : 0;
5796
- terminalCount += result.status === "terminal" ? 1 : 0;
5797
- }
5798
- return { edgeCount, unresolvedCount, resolvedCount, remoteResolvedCount, localResolvedCount, ambiguousCount, dynamicCount, terminalCount };
6032
+ function invalidSpan(subscription) {
6033
+ return typeof subscription.startOffset !== "number" || typeof subscription.endOffset !== "number" || subscription.startOffset < 0 || subscription.endOffset <= subscription.startOffset;
5799
6034
  }
5800
- function insertCallEdge(db, workspaceId, call, vars, generation) {
5801
- const callType = String(call.call_type);
5802
- const rawOp = applyVariables(String(call.operation_path_expr ?? ""), vars);
5803
- const intent = classifyODataPathIntent(rawOp, call.method);
5804
- const isEntityQueryIntent = ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
6035
+ function associationFor(subscription, matches2) {
6036
+ if (invalidSpan(subscription)) return missingAssociation(
6037
+ subscription.id,
6038
+ matches2.length,
6039
+ "subscription_call_span_missing"
6040
+ );
6041
+ if (matches2.length === 0) return missingAssociation(
6042
+ subscription.id,
6043
+ 0,
6044
+ "subscription_handler_role_site_missing"
6045
+ );
6046
+ if (matches2.length > 1) return ambiguousRoleSiteAssociation(
6047
+ subscription.id,
6048
+ matches2
6049
+ );
6050
+ const call = matches2[0];
6051
+ return call ? singleCallAssociation(subscription, call) : missingAssociation(
6052
+ subscription.id,
6053
+ 0,
6054
+ "subscription_handler_role_site_missing"
6055
+ );
6056
+ }
6057
+ function ambiguousRoleSiteAssociation(subscriptionId, matches2) {
6058
+ return {
6059
+ status: "ambiguous",
6060
+ toKind: "subscription_handler",
6061
+ toId: String(subscriptionId),
6062
+ reasonCode: "multiple_handler_role_site_matches",
6063
+ matchCount: matches2.length,
6064
+ missing: false,
6065
+ factOrigin: agreedOrMixed(matches2.map((match) => match.factOrigin)),
6066
+ symbolCallResolutionStatus: agreedOrMixed(
6067
+ matches2.map((match) => match.status)
6068
+ )
6069
+ };
6070
+ }
6071
+ function singleCallAssociation(subscription, call) {
6072
+ const mismatch2 = associationMismatch(subscription, call);
6073
+ if (mismatch2) return missingAssociation(subscription.id, 1, mismatch2, call);
6074
+ return handlerReferenceAssociation(call);
6075
+ }
6076
+ function associationMismatch(subscription, call) {
6077
+ if (subscription.sourceSymbolId != null && call.callerSymbolId !== subscription.sourceSymbolId)
6078
+ return "subscription_handler_caller_mismatch";
6079
+ if (call.sourceLine !== subscription.sourceLine)
6080
+ return "subscription_handler_source_line_mismatch";
6081
+ if (call.targetWorkspaceId != null && call.targetWorkspaceId !== subscription.workspaceId)
6082
+ return "subscription_handler_target_workspace_mismatch";
6083
+ return void 0;
6084
+ }
6085
+ function handlerReferenceAssociation(call) {
6086
+ if (call.status === "resolved" && typeof call.calleeSymbolId === "number")
6087
+ return { status: "resolved", toKind: "symbol", toId: String(call.calleeSymbolId), call, matchCount: 1, missing: false };
6088
+ if (call.status === "ambiguous") return {
6089
+ status: "ambiguous",
6090
+ toKind: "symbol_reference",
6091
+ toId: String(call.id),
6092
+ reasonCode: "subscription_handler_reference_ambiguous",
6093
+ call,
6094
+ matchCount: 1,
6095
+ missing: false
6096
+ };
6097
+ return {
6098
+ status: "unresolved",
6099
+ toKind: "symbol_reference",
6100
+ toId: String(call.id),
6101
+ reasonCode: call.status === "resolved" ? "resolved_handler_symbol_missing" : "subscription_handler_reference_unresolved",
6102
+ call,
6103
+ matchCount: 1,
6104
+ missing: false
6105
+ };
6106
+ }
6107
+ function agreedOrMixed(values) {
6108
+ const distinct = new Set(values.map((value2) => value2 ?? "missing"));
6109
+ if (distinct.size > 1) return "mixed";
6110
+ const value = distinct.values().next().value;
6111
+ return value === "missing" ? void 0 : value;
6112
+ }
6113
+ function missingAssociation(subscriptionId, matchCount, reasonCode, call) {
6114
+ return {
6115
+ status: "unresolved",
6116
+ toKind: "subscription_handler",
6117
+ toId: String(subscriptionId),
6118
+ reasonCode,
6119
+ call,
6120
+ matchCount,
6121
+ missing: true
6122
+ };
6123
+ }
6124
+ function evidenceFor(subscription, association) {
6125
+ const call = association.call ?? {};
6126
+ const symbolCallReason = boundedSymbolCallReason(call.unresolvedReason);
6127
+ return {
6128
+ eventName: subscription.eventName,
6129
+ associationBasis: "exact_subscription_call_span",
6130
+ dispatchScope: "workspace_event_name_only",
6131
+ subscribeCallId: subscription.id,
6132
+ symbolCallId: call.id,
6133
+ roleSiteMatchCount: association.matchCount,
6134
+ callRole: association.matchCount > 0 ? "event_subscribe_handler" : void 0,
6135
+ factOrigin: association.factOrigin ?? call.factOrigin,
6136
+ repositoryId: subscription.repoId,
6137
+ repositoryName: subscription.repoName,
6138
+ sourceFile: subscription.sourceFile,
6139
+ sourceLine: subscription.sourceLine,
6140
+ callSiteStartOffset: subscription.startOffset,
6141
+ callSiteEndOffset: subscription.endOffset,
6142
+ handlerSymbolId: call.calleeSymbolId,
6143
+ handlerSourceFile: call.targetSourceFile,
6144
+ handlerSourceLine: call.targetSourceLine,
6145
+ wrapperFunction: call.wrapperFunction,
6146
+ associationStatus: association.status,
6147
+ symbolCallResolutionStatus: association.symbolCallResolutionStatus ?? call.status,
6148
+ resolutionStatus: association.status,
6149
+ resolutionStrategy: call.strategy,
6150
+ candidateCount: call.candidateCount,
6151
+ reasonCode: association.reasonCode,
6152
+ ...symbolCallReason
6153
+ };
6154
+ }
6155
+ function boundedSymbolCallReason(reason) {
6156
+ if (!reason) return {};
6157
+ const value = reason.slice(0, symbolCallReasonLimit);
6158
+ return {
6159
+ symbolCallUnresolvedReason: value,
6160
+ omittedSymbolCallUnresolvedReasonCharacterCount: Math.max(0, reason.length - value.length)
6161
+ };
6162
+ }
6163
+ function insertAssociationEdge(db, workspaceId, generation, subscription, association) {
6164
+ db.prepare(`INSERT INTO graph_edges(
6165
+ workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
6166
+ confidence,evidence_json,is_dynamic,unresolved_reason,generation
6167
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(
6168
+ workspaceId,
6169
+ "EVENT_SUBSCRIPTION_HANDLED_BY",
6170
+ association.status,
6171
+ "event",
6172
+ subscription.eventName,
6173
+ association.toKind,
6174
+ association.toId,
6175
+ association.call?.confidence ?? subscription.confidence,
6176
+ JSON.stringify(evidenceFor(subscription, association)),
6177
+ 0,
6178
+ association.reasonCode ?? association.call?.unresolvedReason ?? null,
6179
+ generation
6180
+ );
6181
+ }
6182
+ function linkEventSubscriptionHandlers(db, workspaceId, generation) {
6183
+ const summary = {
6184
+ edgeCount: 0,
6185
+ resolvedCount: 0,
6186
+ ambiguousCount: 0,
6187
+ unresolvedCount: 0,
6188
+ missingAssociationCount: 0
6189
+ };
6190
+ for (const subscription of subscriptionRows(db, workspaceId)) {
6191
+ const association = associationFor(
6192
+ subscription,
6193
+ roleSiteRows(db, subscription)
6194
+ );
6195
+ insertAssociationEdge(
6196
+ db,
6197
+ workspaceId,
6198
+ generation,
6199
+ subscription,
6200
+ association
6201
+ );
6202
+ summary.edgeCount += 1;
6203
+ summary.resolvedCount += association.status === "resolved" ? 1 : 0;
6204
+ summary.ambiguousCount += association.status === "ambiguous" ? 1 : 0;
6205
+ summary.unresolvedCount += association.status === "unresolved" ? 1 : 0;
6206
+ summary.missingAssociationCount += association.missing ? 1 : 0;
6207
+ }
6208
+ return summary;
6209
+ }
6210
+
6211
+ // src/db/schema.ts
6212
+ var schemaTablesSql = `
6213
+ CREATE TABLE IF NOT EXISTS workspaces (id INTEGER PRIMARY KEY, root_path TEXT UNIQUE NOT NULL, db_path TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
6214
+ CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, fingerprint TEXT, fact_generation INTEGER NOT NULL DEFAULT 0, graph_generation INTEGER NOT NULL DEFAULT 0, graph_stale_reason TEXT, graph_stale_at TEXT, fact_analyzer_version TEXT, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
6215
+ CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, relative_path TEXT NOT NULL, extension TEXT NOT NULL, sha256 TEXT NOT NULL, size_bytes INTEGER NOT NULL, last_indexed_at TEXT NOT NULL, UNIQUE(repo_id, relative_path), FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
6216
+ CREATE TABLE IF NOT EXISTS cds_requires (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, alias TEXT NOT NULL, kind TEXT, model TEXT, destination TEXT, service_path TEXT, request_timeout INTEGER, raw_json TEXT NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
6217
+ CREATE TABLE IF NOT EXISTS cds_services (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, namespace TEXT, service_name TEXT NOT NULL, qualified_name TEXT NOT NULL, service_path TEXT NOT NULL, is_extend INTEGER NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, extension_local_ref TEXT, extension_imported_symbol TEXT, extension_local_alias TEXT, extension_module_specifier TEXT, extension_import_kind TEXT, extension_base_service_id INTEGER, extension_base_status TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(extension_base_service_id) REFERENCES cds_services(id) ON DELETE SET NULL);
6218
+ CREATE TABLE IF NOT EXISTS cds_operations (id INTEGER PRIMARY KEY, service_id INTEGER NOT NULL, operation_type TEXT NOT NULL, operation_name TEXT NOT NULL, operation_path TEXT NOT NULL, params_json TEXT NOT NULL, return_type TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, provenance TEXT NOT NULL DEFAULT 'direct', base_operation_id INTEGER, FOREIGN KEY(service_id) REFERENCES cds_services(id) ON DELETE CASCADE, FOREIGN KEY(base_operation_id) REFERENCES cds_operations(id) ON DELETE SET NULL);
6219
+ CREATE TABLE IF NOT EXISTS symbols (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, file_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL, qualified_name TEXT NOT NULL, exported INTEGER NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, start_offset INTEGER, end_offset INTEGER, source_file TEXT, exported_name TEXT, evidence_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE);
6220
+ CREATE TABLE IF NOT EXISTS handler_classes (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, class_name TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
6221
+ CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_class_id INTEGER NOT NULL, method_name TEXT NOT NULL, decorator_kind TEXT NOT NULL, decorator_value TEXT, decorator_raw_expression TEXT NOT NULL, decorator_resolution_json TEXT NOT NULL DEFAULT '{}', source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE CASCADE);
6222
+ CREATE TABLE IF NOT EXISTS handler_registrations (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, handler_class_id INTEGER, class_name TEXT, import_source TEXT, registration_file TEXT NOT NULL, registration_line INTEGER NOT NULL, registration_kind TEXT NOT NULL, confidence REAL NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE SET NULL);
6223
+ CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, variable_name TEXT NOT NULL, alias TEXT, alias_expr TEXT, destination_expr TEXT, service_path_expr TEXT, is_dynamic INTEGER NOT NULL, placeholders_json TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, helper_chain_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
6224
+ CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, call_site_start_offset INTEGER, call_site_end_offset INTEGER, confidence REAL NOT NULL, unresolved_reason TEXT, local_service_name TEXT, local_service_lookup TEXT, alias_chain_json TEXT, evidence_json TEXT, external_target_kind TEXT, external_target_id TEXT, external_target_label TEXT, external_target_dynamic INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
6225
+ CREATE TABLE IF NOT EXISTS symbol_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, caller_symbol_id INTEGER NOT NULL, callee_symbol_id INTEGER, callee_expression TEXT NOT NULL, import_source TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, call_site_start_offset INTEGER, call_site_end_offset INTEGER, call_role TEXT NOT NULL DEFAULT 'legacy_unknown', status TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, unresolved_reason TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(caller_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE, FOREIGN KEY(callee_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
6226
+ CREATE TABLE IF NOT EXISTS graph_edges (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, edge_type TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'unresolved', from_kind TEXT NOT NULL, from_id TEXT NOT NULL, to_kind TEXT NOT NULL, to_id TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, is_dynamic INTEGER NOT NULL, unresolved_reason TEXT, generation INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
6227
+ CREATE TABLE IF NOT EXISTS index_runs (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, repo_count INTEGER NOT NULL, file_count INTEGER NOT NULL, diagnostic_count INTEGER NOT NULL, error_message TEXT, owner_pid INTEGER, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
6228
+ CREATE TABLE IF NOT EXISTS diagnostics (id INTEGER PRIMARY KEY, repo_id INTEGER, file_id INTEGER, severity TEXT NOT NULL, code TEXT NOT NULL, message TEXT NOT NULL, source_file TEXT, source_line INTEGER, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE SET NULL);
6229
+ `;
6230
+ var schemaIndexesSql = `
6231
+ CREATE INDEX IF NOT EXISTS idx_repo_name ON repositories(name);
6232
+ CREATE INDEX IF NOT EXISTS idx_service_path ON cds_services(service_path);
6233
+ CREATE INDEX IF NOT EXISTS idx_extension_import ON cds_services(extension_module_specifier, extension_imported_symbol, is_extend);
6234
+ CREATE INDEX IF NOT EXISTS idx_operation_name ON cds_operations(operation_name, operation_path);
6235
+ CREATE INDEX IF NOT EXISTS idx_operation_base ON cds_operations(base_operation_id);
6236
+ CREATE INDEX IF NOT EXISTS idx_calls_repo ON outbound_calls(repo_id, call_type);
6237
+ CREATE INDEX IF NOT EXISTS idx_symbol_calls_caller ON symbol_calls(repo_id, caller_symbol_id);
6238
+ CREATE INDEX IF NOT EXISTS idx_outbound_call_site ON outbound_calls(repo_id, source_file, call_site_start_offset, call_site_end_offset, call_type);
6239
+ CREATE INDEX IF NOT EXISTS idx_symbol_call_site_role ON symbol_calls(repo_id, source_file, call_site_start_offset, call_site_end_offset, call_role);
6240
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, repo);
6241
+ `;
6242
+ var schemaSql = `${schemaTablesSql}
6243
+ ${schemaIndexesSql}`;
6244
+
6245
+ // src/db/migrations.ts
6246
+ var CURRENT_SCHEMA_VERSION = 12;
6247
+ var columns = {
6248
+ handler_methods: [
6249
+ { name: "decorator_resolution_json", ddl: "ALTER TABLE handler_methods ADD COLUMN decorator_resolution_json TEXT NOT NULL DEFAULT '{}'" }
6250
+ ],
6251
+ service_bindings: [
6252
+ { name: "helper_chain_json", ddl: "ALTER TABLE service_bindings ADD COLUMN helper_chain_json TEXT" },
6253
+ { name: "alias_expr", ddl: "ALTER TABLE service_bindings ADD COLUMN alias_expr TEXT" }
6254
+ ],
6255
+ repositories: [
6256
+ { name: "fingerprint", ddl: "ALTER TABLE repositories ADD COLUMN fingerprint TEXT" },
6257
+ { name: "fact_generation", ddl: "ALTER TABLE repositories ADD COLUMN fact_generation INTEGER NOT NULL DEFAULT 0" },
6258
+ { name: "graph_generation", ddl: "ALTER TABLE repositories ADD COLUMN graph_generation INTEGER NOT NULL DEFAULT 0" },
6259
+ { name: "graph_stale_reason", ddl: "ALTER TABLE repositories ADD COLUMN graph_stale_reason TEXT" },
6260
+ { name: "graph_stale_at", ddl: "ALTER TABLE repositories ADD COLUMN graph_stale_at TEXT" },
6261
+ { name: "fact_analyzer_version", ddl: "ALTER TABLE repositories ADD COLUMN fact_analyzer_version TEXT DEFAULT 'legacy'" }
6262
+ ],
6263
+ graph_edges: [
6264
+ { name: "status", ddl: "ALTER TABLE graph_edges ADD COLUMN status TEXT NOT NULL DEFAULT 'unresolved'" },
6265
+ { name: "generation", ddl: "ALTER TABLE graph_edges ADD COLUMN generation INTEGER NOT NULL DEFAULT 0" }
6266
+ ],
6267
+ handler_registrations: [
6268
+ { name: "class_name", ddl: "ALTER TABLE handler_registrations ADD COLUMN class_name TEXT" },
6269
+ { name: "import_source", ddl: "ALTER TABLE handler_registrations ADD COLUMN import_source TEXT" }
6270
+ ],
6271
+ symbols: [
6272
+ { name: "start_offset", ddl: "ALTER TABLE symbols ADD COLUMN start_offset INTEGER" },
6273
+ { name: "end_offset", ddl: "ALTER TABLE symbols ADD COLUMN end_offset INTEGER" },
6274
+ { name: "source_file", ddl: "ALTER TABLE symbols ADD COLUMN source_file TEXT" },
6275
+ { name: "exported_name", ddl: "ALTER TABLE symbols ADD COLUMN exported_name TEXT" },
6276
+ { name: "evidence_json", ddl: "ALTER TABLE symbols ADD COLUMN evidence_json TEXT" }
6277
+ ],
6278
+ cds_services: [
6279
+ { name: "extension_local_ref", ddl: "ALTER TABLE cds_services ADD COLUMN extension_local_ref TEXT" },
6280
+ { name: "extension_imported_symbol", ddl: "ALTER TABLE cds_services ADD COLUMN extension_imported_symbol TEXT" },
6281
+ { name: "extension_local_alias", ddl: "ALTER TABLE cds_services ADD COLUMN extension_local_alias TEXT" },
6282
+ { name: "extension_module_specifier", ddl: "ALTER TABLE cds_services ADD COLUMN extension_module_specifier TEXT" },
6283
+ { name: "extension_import_kind", ddl: "ALTER TABLE cds_services ADD COLUMN extension_import_kind TEXT" },
6284
+ { name: "extension_base_service_id", ddl: "ALTER TABLE cds_services ADD COLUMN extension_base_service_id INTEGER" },
6285
+ { name: "extension_base_status", ddl: "ALTER TABLE cds_services ADD COLUMN extension_base_status TEXT" }
6286
+ ],
6287
+ cds_operations: [
6288
+ { name: "provenance", ddl: "ALTER TABLE cds_operations ADD COLUMN provenance TEXT NOT NULL DEFAULT 'direct'" },
6289
+ { name: "base_operation_id", ddl: "ALTER TABLE cds_operations ADD COLUMN base_operation_id INTEGER" }
6290
+ ],
6291
+ outbound_calls: [
6292
+ { name: "local_service_name", ddl: "ALTER TABLE outbound_calls ADD COLUMN local_service_name TEXT" },
6293
+ { name: "local_service_lookup", ddl: "ALTER TABLE outbound_calls ADD COLUMN local_service_lookup TEXT" },
6294
+ { name: "alias_chain_json", ddl: "ALTER TABLE outbound_calls ADD COLUMN alias_chain_json TEXT" },
6295
+ { name: "evidence_json", ddl: "ALTER TABLE outbound_calls ADD COLUMN evidence_json TEXT" },
6296
+ { name: "external_target_kind", ddl: "ALTER TABLE outbound_calls ADD COLUMN external_target_kind TEXT" },
6297
+ { name: "external_target_id", ddl: "ALTER TABLE outbound_calls ADD COLUMN external_target_id TEXT" },
6298
+ { name: "external_target_label", ddl: "ALTER TABLE outbound_calls ADD COLUMN external_target_label TEXT" },
6299
+ { name: "external_target_dynamic", ddl: "ALTER TABLE outbound_calls ADD COLUMN external_target_dynamic INTEGER NOT NULL DEFAULT 0" },
6300
+ { name: "call_site_start_offset", ddl: "ALTER TABLE outbound_calls ADD COLUMN call_site_start_offset INTEGER" },
6301
+ { name: "call_site_end_offset", ddl: "ALTER TABLE outbound_calls ADD COLUMN call_site_end_offset INTEGER" }
6302
+ ],
6303
+ symbol_calls: [
6304
+ { name: "call_site_start_offset", ddl: "ALTER TABLE symbol_calls ADD COLUMN call_site_start_offset INTEGER" },
6305
+ { name: "call_site_end_offset", ddl: "ALTER TABLE symbol_calls ADD COLUMN call_site_end_offset INTEGER" },
6306
+ { name: "call_role", ddl: "ALTER TABLE symbol_calls ADD COLUMN call_role TEXT NOT NULL DEFAULT 'legacy_unknown'" }
6307
+ ],
6308
+ index_runs: [
6309
+ { name: "error_message", ddl: "ALTER TABLE index_runs ADD COLUMN error_message TEXT" },
6310
+ { name: "owner_pid", ddl: "ALTER TABLE index_runs ADD COLUMN owner_pid INTEGER" }
6311
+ ]
6312
+ };
6313
+ function hasColumn(db, table, column) {
6314
+ return db.prepare(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
6315
+ }
6316
+ function userVersion(db) {
6317
+ const row = db.pragma("user_version")[0];
6318
+ return Number(row?.user_version ?? 0);
6319
+ }
6320
+ function addMissingColumns(db) {
6321
+ for (const [table, tableColumns] of Object.entries(columns)) {
6322
+ for (const column of tableColumns) {
6323
+ if (!hasColumn(db, table, column.name)) db.prepare(column.ddl).run();
6324
+ }
6325
+ }
6326
+ }
6327
+ function normalizeLegacyStatus(db) {
6328
+ db.prepare("UPDATE graph_edges SET status=CASE WHEN edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' THEN 'resolved' WHEN edge_type IN ('HANDLER_RUNS_DB_QUERY','HANDLER_CALLS_EXTERNAL_HTTP','HANDLER_EMITS_EVENT','EVENT_CONSUMED_BY_HANDLER') THEN 'terminal' WHEN edge_type='DYNAMIC_EDGE_CANDIDATE' THEN 'dynamic' WHEN status='ambiguous' THEN 'ambiguous' ELSE status END").run();
6329
+ db.prepare("UPDATE repositories SET graph_stale_reason='schema_migration_requires_relink', graph_stale_at=COALESCE(graph_stale_at, datetime('now')) WHERE EXISTS (SELECT 1 FROM graph_edges WHERE graph_edges.workspace_id=repositories.workspace_id) AND graph_generation=0").run();
6330
+ }
6331
+ function markCallSiteMigrationStale(db, priorVersion) {
6332
+ if (priorVersion >= 12) return;
6333
+ db.prepare(`UPDATE repositories
6334
+ SET graph_stale_reason='schema_v12_call_sites_require_reindex',
6335
+ graph_stale_at=COALESCE(graph_stale_at,datetime('now'))
6336
+ WHERE index_status='indexed' OR last_indexed_at IS NOT NULL`).run();
6337
+ }
6338
+ function migrate(db) {
6339
+ db.transaction(() => {
6340
+ const version = userVersion(db);
6341
+ if (version > CURRENT_SCHEMA_VERSION) throw new Error(`Unsupported future service-flow schema version ${version}`);
6342
+ db.exec(schemaTablesSql);
6343
+ addMissingColumns(db);
6344
+ db.exec(schemaIndexesSql);
6345
+ normalizeLegacyStatus(db);
6346
+ markCallSiteMigrationStale(db, version);
6347
+ const violations = db.pragma("foreign_key_check");
6348
+ if (violations.length > 0) throw new Error("SQLite foreign_key_check failed during migration");
6349
+ db.pragma(`user_version = ${CURRENT_SCHEMA_VERSION}`);
6350
+ });
6351
+ }
6352
+ function schemaVersion(db) {
6353
+ return userVersion(db);
6354
+ }
6355
+
6356
+ // src/db/001-fact-lifecycle.ts
6357
+ var remediation = [
6358
+ "service-flow index --workspace /workspace --force",
6359
+ "service-flow link --workspace /workspace --force"
6360
+ ].join("\n");
6361
+ function count(db, sql, ...params) {
6362
+ const row = db.prepare(sql).get(...params);
6363
+ return Number(row?.count ?? 0);
6364
+ }
6365
+ function oldAnalyzerCount(db, workspaceId) {
6366
+ return count(
6367
+ db,
6368
+ `SELECT COUNT(*) count FROM repositories
6369
+ WHERE (? IS NULL OR workspace_id=?)
6370
+ AND (COALESCE(index_status,'pending')<>'indexed'
6371
+ OR COALESCE(fact_analyzer_version,'legacy')<>?)`,
6372
+ workspaceId,
6373
+ workspaceId,
6374
+ ANALYZER_VERSION
6375
+ );
6376
+ }
6377
+ function invalidCurrentFactCount(db, workspaceId) {
6378
+ const outbound = count(
6379
+ db,
6380
+ `SELECT COUNT(*) count
6381
+ FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
6382
+ WHERE (? IS NULL OR r.workspace_id=?) AND r.fact_analyzer_version=?
6383
+ AND (typeof(c.call_site_start_offset)<>'integer'
6384
+ OR typeof(c.call_site_end_offset)<>'integer'
6385
+ OR c.call_site_start_offset<0
6386
+ OR c.call_site_end_offset<=c.call_site_start_offset)`,
6387
+ workspaceId,
6388
+ workspaceId,
6389
+ ANALYZER_VERSION
6390
+ );
6391
+ const symbols = count(
6392
+ db,
6393
+ `SELECT COUNT(*) count
6394
+ FROM symbol_calls c JOIN repositories r ON r.id=c.repo_id
6395
+ WHERE (? IS NULL OR r.workspace_id=?)
6396
+ AND (c.call_role='legacy_unknown'
6397
+ OR (r.fact_analyzer_version=? AND (
6398
+ typeof(c.call_site_start_offset)<>'integer'
6399
+ OR typeof(c.call_site_end_offset)<>'integer'
6400
+ OR c.call_site_start_offset<0
6401
+ OR c.call_site_end_offset<=c.call_site_start_offset
6402
+ OR c.call_role NOT IN ('ordinary_call','event_subscribe_handler'))))`,
6403
+ workspaceId,
6404
+ workspaceId,
6405
+ ANALYZER_VERSION
6406
+ );
6407
+ return outbound + symbols;
6408
+ }
6409
+ function factLifecycleDiagnostic(db, workspaceId) {
6410
+ return schemaLifecycleDiagnostic(db) ?? currentFactLifecycleDiagnostic(db, workspaceId);
6411
+ }
6412
+ function schemaLifecycleDiagnostic(db) {
6413
+ const currentSchema = schemaVersion(db);
6414
+ if (currentSchema > CURRENT_SCHEMA_VERSION) return {
6415
+ severity: "error",
6416
+ code: "unsupported_future_schema",
6417
+ message: `Database schema ${currentSchema} is newer than the supported schema ${CURRENT_SCHEMA_VERSION}; upgrade service-flow before reading this database.`,
6418
+ remediation: "Install a service-flow version that supports this database schema.",
6419
+ currentSchemaVersion: currentSchema,
6420
+ supportedSchemaVersion: CURRENT_SCHEMA_VERSION
6421
+ };
6422
+ if (currentSchema < CURRENT_SCHEMA_VERSION) return {
6423
+ severity: "error",
6424
+ code: "schema_upgrade_required",
6425
+ message: `Database schema ${currentSchema} must be upgraded to ${CURRENT_SCHEMA_VERSION} before this command can read current call-site facts.`,
6426
+ remediation,
6427
+ currentSchemaVersion: currentSchema,
6428
+ requiredSchemaVersion: CURRENT_SCHEMA_VERSION
6429
+ };
6430
+ return void 0;
6431
+ }
6432
+ function currentFactLifecycleDiagnostic(db, workspaceId) {
6433
+ const staleRepositories = oldAnalyzerCount(db, workspaceId);
6434
+ const invalidFacts = invalidCurrentFactCount(db, workspaceId);
6435
+ if (staleRepositories === 0 && invalidFacts === 0) return void 0;
6436
+ return {
6437
+ severity: "error",
6438
+ code: "reindex_required",
6439
+ message: "Call-site facts are stale or lack typed roles and exact spans; force index and link before tracing or rebuilding graph edges.",
6440
+ remediation,
6441
+ staleRepositoryCount: staleRepositories,
6442
+ invalidCallFactCount: invalidFacts,
6443
+ requiredAnalyzerVersion: ANALYZER_VERSION
6444
+ };
6445
+ }
6446
+ function assertWorkspaceLinkable(db, workspaceId) {
6447
+ const diagnostic = factLifecycleDiagnostic(db, workspaceId);
6448
+ if (!diagnostic) return;
6449
+ throw new Error(`${diagnostic.code}: ${diagnostic.message}
6450
+ ${diagnostic.remediation}`);
6451
+ }
6452
+
6453
+ // src/linker/cross-repo-linker.ts
6454
+ function linkWorkspace(db, workspaceId, vars = {}) {
6455
+ return db.transaction(() => {
6456
+ assertWorkspaceLinkable(db, workspaceId);
6457
+ const generation = nextGraphGeneration(db, workspaceId);
6458
+ db.prepare("DELETE FROM graph_edges WHERE workspace_id=?").run(workspaceId);
6459
+ const deps = linkHelperPackages(db, workspaceId, generation);
6460
+ linkPackageImportSymbolCalls(db, workspaceId);
6461
+ const subscriptions = linkEventSubscriptionHandlers(
6462
+ db,
6463
+ workspaceId,
6464
+ generation
6465
+ );
6466
+ const impl = linkImplementations(db, workspaceId, generation);
6467
+ const callSummary = linkCalls(db, workspaceId, vars, generation);
6468
+ db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
6469
+ return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount + subscriptions.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount, implementationUnresolvedCount: impl.unresolvedCount, subscriptionHandlerResolvedCount: subscriptions.resolvedCount, subscriptionHandlerAmbiguousCount: subscriptions.ambiguousCount, subscriptionHandlerUnresolvedCount: subscriptions.unresolvedCount, subscriptionHandlerMissingAssociationCount: subscriptions.missingAssociationCount };
6470
+ });
6471
+ }
6472
+ function nextGraphGeneration(db, workspaceId) {
6473
+ const row = db.prepare("SELECT COALESCE(MAX(graph_generation),0) generation FROM repositories WHERE workspace_id=?").get(workspaceId);
6474
+ return Number(row?.generation ?? 0) + 1;
6475
+ }
6476
+ function linkCalls(db, workspaceId, vars, generation) {
6477
+ let edgeCount = 0;
6478
+ let unresolvedCount = 0;
6479
+ let resolvedCount = 0;
6480
+ let remoteResolvedCount = 0;
6481
+ let localResolvedCount = 0;
6482
+ let ambiguousCount = 0;
6483
+ let dynamicCount = 0;
6484
+ let terminalCount = 0;
6485
+ const calls = db.prepare(`SELECT c.*,r.name repoName,b.id selectedBindingId,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.is_dynamic isDynamic,b.placeholders_json placeholdersJson,b.source_file bindingSourceFile,b.source_line bindingSourceLine,b.helper_chain_json helperChainJson,req.service_path requireServicePath,req.destination requireDestination FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id LEFT JOIN service_bindings b ON b.id=c.service_binding_id LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias WHERE r.workspace_id=?`).all(workspaceId);
6486
+ for (const call of calls) {
6487
+ const result = insertCallEdge(db, workspaceId, call, vars, generation);
6488
+ edgeCount += 1;
6489
+ resolvedCount += result.status === "resolved" ? 1 : 0;
6490
+ remoteResolvedCount += result.status === "resolved" && result.callType !== "local_service_call" ? 1 : 0;
6491
+ localResolvedCount += result.status === "resolved" && result.callType === "local_service_call" ? 1 : 0;
6492
+ unresolvedCount += result.status === "unresolved" ? 1 : 0;
6493
+ ambiguousCount += result.status === "ambiguous" ? 1 : 0;
6494
+ dynamicCount += result.status === "dynamic" ? 1 : 0;
6495
+ terminalCount += result.status === "terminal" ? 1 : 0;
6496
+ }
6497
+ return { edgeCount, unresolvedCount, resolvedCount, remoteResolvedCount, localResolvedCount, ambiguousCount, dynamicCount, terminalCount };
6498
+ }
6499
+ function insertCallEdge(db, workspaceId, call, vars, generation) {
6500
+ const callType = String(call.call_type);
6501
+ const rawOp = applyVariables(String(call.operation_path_expr ?? ""), vars);
6502
+ const intent = classifyODataPathIntent(rawOp, call.method);
6503
+ const isEntityQueryIntent = ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
5805
6504
  const resolutionRawOp = callType === "remote_query" && isEntityQueryIntent ? intent.pathWithoutQuery : rawOp;
5806
6505
  const normalized = normalizeODataOperationInvocationPath(resolutionRawOp);
5807
6506
  const op = normalized?.normalizedOperationPath ?? resolutionRawOp;
@@ -5890,11 +6589,11 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
5890
6589
  const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
5891
6590
  const unresolvedReason = status === "terminal" ? null : String(call.unresolved_reason ?? unresolvedOperationReason(resolution));
5892
6591
  const externalTarget = callType === "external_http" ? externalHttpTarget(call) : void 0;
5893
- const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : callType === "external_http" ? externalTarget?.toKind ?? "external_endpoint" : "operation_candidate";
6592
+ const targetKind2 = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : callType === "external_http" ? externalTarget?.toKind ?? "external_endpoint" : "operation_candidate";
5894
6593
  const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : callType === "remote_action" ? op ? `Remote action: ${op}` : call.unresolved_reason === "dynamic_operation_path_identifier" ? "Remote action: dynamic path" : "Remote action: unknown path" : callType === "external_http" ? String(externalTarget?.toId ?? "unknown") : String(call.event_name_expr ?? op ?? "unknown");
5895
6594
  const graphLevelDynamic = edgeType === "DYNAMIC_EDGE_CANDIDATE" && resolution.status === "dynamic";
5896
6595
  const finalEvidence = externalTarget ? { ...evidence, externalTarget } : evidence;
5897
- 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, edgeType, status, "call", String(call.id), targetKind, targetId, Number(call.confidence ?? 0.2), JSON.stringify(finalEvidence), graphLevelDynamic ? 1 : 0, unresolvedReason, generation);
6596
+ 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, edgeType, status, "call", String(call.id), targetKind2, targetId, Number(call.confidence ?? 0.2), JSON.stringify(finalEvidence), graphLevelDynamic ? 1 : 0, unresolvedReason, generation);
5898
6597
  return { status, callType };
5899
6598
  }
5900
6599
  function operationCandidateCount(db, workspaceId, operationPath, operationName) {
@@ -6733,18 +7432,18 @@ function dynamicRoutingContext(db, workspaceId, evidence) {
6733
7432
  fallbackUsed: true
6734
7433
  };
6735
7434
  }
6736
- function dynamicReferenceProvenance(reference, kind, template, value) {
6737
- const sourceKind = reference.selection === "selected_binding" ? `selected_binding.${kind}` : reference.selection === "selected_binding_require" ? `selected_binding_require.${kind}` : `${reference.sourceKind}.${kind}`;
7435
+ function dynamicReferenceProvenance(reference2, kind, template, value) {
7436
+ const sourceKind = reference2.selection === "selected_binding" ? `selected_binding.${kind}` : reference2.selection === "selected_binding_require" ? `selected_binding_require.${kind}` : `${reference2.sourceKind}.${kind}`;
6738
7437
  return {
6739
7438
  sourceKind,
6740
7439
  value,
6741
7440
  rule: "exact_indexed_reference_template_match",
6742
7441
  template,
6743
- sourceRepo: reference.repoName,
6744
- sourceFile: reference.sourceFile,
6745
- sourceLine: reference.sourceLine,
6746
- selection: reference.selection,
6747
- bindingId: reference.bindingId
7442
+ sourceRepo: reference2.repoName,
7443
+ sourceFile: reference2.sourceFile,
7444
+ sourceLine: reference2.sourceLine,
7445
+ selection: reference2.selection,
7446
+ bindingId: reference2.bindingId
6748
7447
  };
6749
7448
  }
6750
7449
  function contextBase(selected, status, alternatives) {
@@ -6779,12 +7478,12 @@ function selectedBinding(db, workspaceId, evidence) {
6779
7478
  function selectedReferenceFromRow(row) {
6780
7479
  const outboundCallId = numberValue6(row?.outboundCallId);
6781
7480
  const callerRepoId = numberValue6(row?.callerRepoId);
6782
- const reference = referenceFromRow(
7481
+ const reference2 = referenceFromRow(
6783
7482
  row,
6784
7483
  "service_binding",
6785
7484
  "selected_binding"
6786
7485
  )[0];
6787
- return reference && outboundCallId !== void 0 && callerRepoId !== void 0 ? { ...reference, outboundCallId, callerRepoId } : void 0;
7486
+ return reference2 && outboundCallId !== void 0 && callerRepoId !== void 0 ? { ...reference2, outboundCallId, callerRepoId } : void 0;
6788
7487
  }
6789
7488
  function exactRequireReferences(db, workspaceId, selected) {
6790
7489
  if (!(selected.aliasExpr ?? selected.alias)) return [];
@@ -6967,7 +7666,7 @@ function buildCandidates(db, targets, references, inputs) {
6967
7666
  const state = emptyCandidate(target, inputs);
6968
7667
  applyDirectSignal(state, inputs, "operationPath", target.operationPath, 0.25);
6969
7668
  applyDirectSignal(state, inputs, "servicePath", target.servicePath, 0.35);
6970
- const matchingReferences = references.filter((reference) => referenceMatchesCandidate(reference, target.servicePath) && referenceMatchesSelectedAlias(reference, inputs.routing.selectedBinding));
7669
+ const matchingReferences = references.filter((reference2) => referenceMatchesCandidate(reference2, target.servicePath) && referenceMatchesSelectedAlias(reference2, inputs.routing.selectedBinding));
6971
7670
  const referencesForSignals = fallbackReferencesForCandidate(
6972
7671
  state,
6973
7672
  matchingReferences,
@@ -6990,12 +7689,12 @@ function fallbackReferencesForCandidate(state, references, fallbackUsed) {
6990
7689
  }
6991
7690
  function uniqueFallbackReferences(references) {
6992
7691
  const seen = /* @__PURE__ */ new Set();
6993
- return references.filter((reference) => {
7692
+ return references.filter((reference2) => {
6994
7693
  const signature = [
6995
- reference.sourceKind,
6996
- reference.alias,
6997
- reference.destination,
6998
- reference.servicePath
7694
+ reference2.sourceKind,
7695
+ reference2.alias,
7696
+ reference2.destination,
7697
+ reference2.servicePath
6999
7698
  ].join("\0");
7000
7699
  if (seen.has(signature)) return false;
7001
7700
  seen.add(signature);
@@ -7067,15 +7766,15 @@ function applyReferenceSignal(state, inputs, references, kind) {
7067
7766
  const original = inputs.original[kind];
7068
7767
  const effective = inputs.effective[kind];
7069
7768
  if (!original || extractPlaceholders(original).length === 0) return;
7070
- const values = references.flatMap((reference) => {
7071
- const concrete = kind === "alias" ? reference.alias : reference.destination;
7072
- return isConcrete(concrete) ? [{ reference, concrete }] : [];
7769
+ const values = references.flatMap((reference2) => {
7770
+ const concrete = kind === "alias" ? reference2.alias : reference2.destination;
7771
+ return isConcrete(concrete) ? [{ reference: reference2, concrete }] : [];
7073
7772
  });
7074
7773
  if (effective && extractPlaceholders(effective).length === 0 && values.length > 0 && !values.some(({ concrete }) => concrete === effective)) {
7075
7774
  reject(state, `${kind}_contradicts_runtime_substitution`);
7076
7775
  }
7077
7776
  let matchedSignal = false;
7078
- for (const { reference, concrete } of values) {
7777
+ for (const { reference: reference2, concrete } of values) {
7079
7778
  const matched = matchRuntimeTemplate(original, concrete);
7080
7779
  if (!matched) continue;
7081
7780
  matchedSignal = true;
@@ -7084,7 +7783,7 @@ function applyReferenceSignal(state, inputs, references, kind) {
7084
7783
  state,
7085
7784
  key,
7086
7785
  value,
7087
- dynamicReferenceProvenance(reference, kind, original, value)
7786
+ dynamicReferenceProvenance(reference2, kind, original, value)
7088
7787
  );
7089
7788
  }
7090
7789
  }
@@ -7278,13 +7977,13 @@ function variableOrder(templates, required) {
7278
7977
  ].flatMap((value) => extractPlaceholders(value));
7279
7978
  return [.../* @__PURE__ */ new Set([...ordered, ...required])];
7280
7979
  }
7281
- function referenceMatchesCandidate(reference, servicePath) {
7282
- return matchRuntimeTemplate(reference.servicePath, servicePath) !== void 0;
7980
+ function referenceMatchesCandidate(reference2, servicePath) {
7981
+ return matchRuntimeTemplate(reference2.servicePath, servicePath) !== void 0;
7283
7982
  }
7284
- function referenceMatchesSelectedAlias(reference, selected) {
7285
- if (reference.selection !== "selected_binding_require") return true;
7983
+ function referenceMatchesSelectedAlias(reference2, selected) {
7984
+ if (reference2.selection !== "selected_binding_require") return true;
7286
7985
  const template = selected?.aliasExpr ?? selected?.alias;
7287
- return matchRuntimeTemplate(template, reference.alias) !== void 0;
7986
+ return matchRuntimeTemplate(template, reference2.alias) !== void 0;
7288
7987
  }
7289
7988
  function cliFor(variables, order) {
7290
7989
  return order.filter((key) => variables[key] !== void 0).map((key) => `--var ${shellArgument(`${key}=${variables[key]}`)}`).join(" ");
@@ -7582,7 +8281,7 @@ function baseTraceEvidence(row, call, persistedEvidence, contextualEvidence2) {
7582
8281
  };
7583
8282
  }
7584
8283
  function runtimeResolution(db, row, evidence, options, workspaceId, contextualState) {
7585
- const dynamicMode = options.dynamicMode ?? "strict";
8284
+ const dynamicMode2 = options.dynamicMode ?? "strict";
7586
8285
  const candidateCap = positiveCandidateCap(options.maxDynamicCandidates);
7587
8286
  const boundedEvidence = boundCandidateLikeEvidence(evidence, candidateCap);
7588
8287
  if (!isDynamicRemoteOperationEdge(row, evidence))
@@ -7596,7 +8295,7 @@ function runtimeResolution(db, row, evidence, options, workspaceId, contextualSt
7596
8295
  db,
7597
8296
  substituted,
7598
8297
  workspaceId,
7599
- dynamicMode,
8298
+ dynamicMode2,
7600
8299
  candidateCap
7601
8300
  );
7602
8301
  const enriched = boundDynamicEvidence(
@@ -7608,7 +8307,7 @@ function runtimeResolution(db, row, evidence, options, workspaceId, contextualSt
7608
8307
  row,
7609
8308
  enriched,
7610
8309
  analysis,
7611
- dynamicMode,
8310
+ dynamicMode2,
7612
8311
  appliedRuntimeValues,
7613
8312
  contextualState
7614
8313
  );
@@ -7626,10 +8325,10 @@ function runtimeResolution(db, row, evidence, options, workspaceId, contextualSt
7626
8325
  }
7627
8326
  return resolveSuppliedRuntimeOperation(db, row, enriched, workspaceId, contextualState);
7628
8327
  }
7629
- function analyzedRuntimeResolution(row, evidence, analysis, dynamicMode, appliedRuntimeValues, contextualState) {
8328
+ function analyzedRuntimeResolution(row, evidence, analysis, dynamicMode2, appliedRuntimeValues, contextualState) {
7630
8329
  if (analysis && analysis.viableCandidateCount === 0 && Object.keys(analysis.appliedSuppliedVariables).length > 0)
7631
8330
  return noCandidateRuntimeResolution(row, evidence, contextualState);
7632
- const inferred = dynamicMode === "infer" ? inferredTarget(analysis) : void 0;
8331
+ const inferred = dynamicMode2 === "infer" ? inferredTarget(analysis) : void 0;
7633
8332
  if (inferred && !isStructuralContextualBlocker(contextualState))
7634
8333
  return resolvedRuntimeResolution(row, evidence, inferred, inferred.reasons);
7635
8334
  if (analysis && analysis.missingVariables.length > 0 && appliedRuntimeValues) {
@@ -8110,19 +8809,25 @@ function positiveCandidateCap(value) {
8110
8809
  function dynamicCandidateBranches(depth, call, evidence) {
8111
8810
  const exploration = objectRecord2(evidence.dynamicTargetExploration);
8112
8811
  return recordArray4(evidence.dynamicTargetCandidateSuggestions).map((candidate) => ({
8113
- step: depth,
8114
- type: "dynamic_candidate_branch",
8115
- from: `${call.repoName}:${call.source_file}:${call.source_line}`,
8116
- to: `${String(candidate.servicePath ?? "")}${String(candidate.operationPath ?? "")}`,
8117
- evidence: {
8118
- ...candidate,
8119
- exploratory: true,
8120
- dynamicMode: String(exploration.mode ?? "candidates"),
8121
- selected: false,
8122
- omittedCandidateCount: numericValue4(exploration.omittedCandidateCount)
8812
+ edge: {
8813
+ step: depth,
8814
+ type: "dynamic_candidate_branch",
8815
+ from: `${call.repoName}:${call.source_file}:${call.source_line}`,
8816
+ to: `${String(candidate.servicePath ?? "")}${String(candidate.operationPath ?? "")}`,
8817
+ evidence: {
8818
+ ...candidate,
8819
+ exploratory: true,
8820
+ dynamicMode: String(exploration.mode ?? "candidates"),
8821
+ selected: false,
8822
+ omittedCandidateCount: numericValue4(exploration.omittedCandidateCount)
8823
+ },
8824
+ confidence: numericValue4(candidate.score),
8825
+ unresolvedReason: "Exploratory dynamic target candidate; provide runtime variables to select it"
8123
8826
  },
8124
- confidence: numericValue4(candidate.score),
8125
- unresolvedReason: "Exploratory dynamic target candidate; provide runtime variables to select it"
8827
+ operationId: optionalNumber(candidate.candidateOperationId),
8828
+ repositoryId: optionalNumber(candidate.repoId),
8829
+ servicePath: optionalString(candidate.servicePath),
8830
+ operationPath: optionalString(candidate.operationPath)
8126
8831
  }));
8127
8832
  }
8128
8833
  function objectRecord2(value) {
@@ -8134,6 +8839,12 @@ function recordArray4(value) {
8134
8839
  function numericValue4(value) {
8135
8840
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
8136
8841
  }
8842
+ function optionalNumber(value) {
8843
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
8844
+ }
8845
+ function optionalString(value) {
8846
+ return typeof value === "string" && value.length > 0 ? value : void 0;
8847
+ }
8137
8848
 
8138
8849
  // src/trace/002-trace-diagnostics.ts
8139
8850
  function loadTraceDiagnostics(db, repoId, includeWorkspaceDiagnostics, workspaceId) {
@@ -8455,155 +9166,377 @@ function numberValue9(value) {
8455
9166
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
8456
9167
  }
8457
9168
 
8458
- // src/trace/trace-engine.ts
8459
- function normalizeOperation2(value) {
8460
- if (!value) return void 0;
8461
- return value.startsWith("/") ? value.slice(1) : value;
8462
- }
8463
- function positiveDepth(value) {
8464
- return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
9169
+ // src/trace/010-traversal-scope.ts
9170
+ import { createHash as createHash2 } from "crypto";
9171
+ function compareBinary(left, right) {
9172
+ const leftPoints = [...left];
9173
+ const rightPoints = [...right];
9174
+ const length = Math.min(leftPoints.length, rightPoints.length);
9175
+ for (let index = 0; index < length; index += 1) {
9176
+ const leftPoint = leftPoints[index]?.codePointAt(0) ?? 0;
9177
+ const rightPoint = rightPoints[index]?.codePointAt(0) ?? 0;
9178
+ if (leftPoint !== rightPoint) return leftPoint < rightPoint ? -1 : 1;
9179
+ }
9180
+ return leftPoints.length - rightPoints.length;
9181
+ }
9182
+ function resolveTraversalWorkspaceId(db, requestedWorkspaceId, repoId) {
9183
+ if (requestedWorkspaceId !== void 0) return requestedWorkspaceId;
9184
+ if (repoId !== void 0) {
9185
+ const row = db.prepare(
9186
+ "SELECT workspace_id workspaceId FROM repositories WHERE id=?"
9187
+ ).get(repoId);
9188
+ return typeof row?.workspaceId === "number" ? row.workspaceId : void 0;
9189
+ }
9190
+ const rows2 = db.prepare(`SELECT DISTINCT w.id workspaceId FROM workspaces w
9191
+ JOIN repositories r ON r.workspace_id=w.id ORDER BY w.id LIMIT 2`).all();
9192
+ return rows2.length === 1 && typeof rows2[0]?.workspaceId === "number" ? rows2[0].workspaceId : void 0;
9193
+ }
9194
+ function structuralScopeKey(workspaceId, repoId, files, symbolIds) {
9195
+ return JSON.stringify([
9196
+ workspaceId ?? null,
9197
+ repoId ?? null,
9198
+ files ? [...files].sort(compareBinary) : null,
9199
+ symbolIds ? [...symbolIds].sort((left, right) => left - right) : null
9200
+ ]);
8465
9201
  }
8466
- function operationStartScope(db, repoId, start, hintOptions, workspaceId) {
8467
- const requested = normalizeOperation2(start.operationPath ?? start.operation);
8468
- if (!requested) return void 0;
8469
- 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
8470
- FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
8471
- WHERE (? IS NULL OR r.workspace_id=?) AND (? IS NULL OR r.id=?)
8472
- AND (? IS NULL OR s.service_path=?) AND (o.operation_name=? OR o.operation_path=? OR o.operation_path=?)
8473
- 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}`);
8474
- if (rows2.length === 0) return void 0;
8475
- const repoCount = new Set(rows2.map((row) => String(row.repoName))).size;
8476
- const serviceCount = new Set(rows2.map((row) => `${String(row.repoName)}:${String(row.servicePath)}`)).size;
8477
- if (!repoId && repoCount > 1)
8478
- return { diagnostics: [ambiguousStartDiagnostic(requested, rows2, "Operation trace start matched multiple repositories; add --repo to disambiguate")] };
8479
- if (!start.servicePath && serviceCount > 1)
8480
- return { diagnostics: [ambiguousStartDiagnostic(requested, rows2, "Operation trace start matched multiple services; add --service to disambiguate")] };
8481
- if (rows2.length !== 1)
8482
- return { diagnostics: [ambiguousStartDiagnostic(requested, rows2, "Operation trace start matched multiple indexed operations")] };
8483
- const operationId = String(rows2[0]?.operationId);
8484
- const impl = implementationScope(db, operationId);
8485
- if (impl.edge?.status === "resolved" && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? /* @__PURE__ */ new Set([impl.symbolId]) : void 0, repoId: impl.repoId, operationId, diagnostics: [] };
8486
- const hinted = hintedImplementationSelection(
8487
- db,
8488
- impl.edge,
8489
- operationId,
8490
- hintOptions
8491
- );
8492
- if (hinted.methodId) {
8493
- const hintedScope = handlerScope(db, hinted.methodId);
8494
- if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? /* @__PURE__ */ new Set([hintedScope.symbolId]) : void 0, repoId: hintedScope.repoId, operationId, diagnostics: [] };
9202
+ function canonicalContextFingerprint(context) {
9203
+ const entries = [...(context ?? /* @__PURE__ */ new Map()).entries()].map(([name, binding]) => `${JSON.stringify(name)}:${canonicalValue(binding)}`).sort(compareBinary);
9204
+ return createHash2("sha256").update(`[${entries.join(",")}]`).digest("hex");
9205
+ }
9206
+ function evaluationScopeKey(structuralKey, context) {
9207
+ return JSON.stringify([structuralKey, canonicalContextFingerprint(context)]);
9208
+ }
9209
+ var TraversalScopeScheduler = class {
9210
+ #scheduled = /* @__PURE__ */ new Set();
9211
+ #expanded = /* @__PURE__ */ new Set();
9212
+ #structuralByEvaluation = /* @__PURE__ */ new Map();
9213
+ #childrenByEvaluation = /* @__PURE__ */ new Map();
9214
+ schedule(identity, parent) {
9215
+ const state = scopeState(identity, parent);
9216
+ this.#rememberState(state);
9217
+ if (parent) this.#rememberState(parent);
9218
+ const cycle = parent ? parent.ancestry.has(state.structuralKey) || this.#reachesStructural(state.evaluationKey, parent.structuralKey) : false;
9219
+ if (parent) this.#recordEdge(parent.evaluationKey, state.evaluationKey);
9220
+ if (cycle)
9221
+ return { kind: "cycle", state, alreadyExpanded: this.#expanded.has(state.evaluationKey) };
9222
+ if (this.#scheduled.has(state.evaluationKey))
9223
+ return { kind: "converged", state, alreadyExpanded: this.#expanded.has(state.evaluationKey) };
9224
+ this.#scheduled.add(state.evaluationKey);
9225
+ return { kind: "scheduled", state, alreadyExpanded: false };
9226
+ }
9227
+ markExpanded(state) {
9228
+ if (this.#expanded.has(state.evaluationKey)) return false;
9229
+ this.#expanded.add(state.evaluationKey);
9230
+ return true;
8495
9231
  }
8496
- if (impl.edge) {
8497
- const evidence = parseEvidence(impl.edge.evidence_json);
8498
- const hintDiagnostic = implementationHintDiagnostic(hinted, evidence);
8499
- const diagnostics = [implementationStartDiagnostic(impl.edge, evidence)];
8500
- return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
9232
+ #rememberState(state) {
9233
+ this.#structuralByEvaluation.set(state.evaluationKey, state.structuralKey);
9234
+ }
9235
+ #recordEdge(parent, child) {
9236
+ const children = this.#childrenByEvaluation.get(parent) ?? /* @__PURE__ */ new Set();
9237
+ children.add(child);
9238
+ this.#childrenByEvaluation.set(parent, children);
9239
+ }
9240
+ #reachesStructural(start, targetStructuralKey) {
9241
+ const pending = [start];
9242
+ const seen = /* @__PURE__ */ new Set();
9243
+ let cursor = 0;
9244
+ while (cursor < pending.length) {
9245
+ const current = pending[cursor];
9246
+ cursor += 1;
9247
+ if (seen.has(current)) continue;
9248
+ seen.add(current);
9249
+ if (this.#structuralByEvaluation.get(current) === targetStructuralKey)
9250
+ return true;
9251
+ pending.push(...this.#childrenByEvaluation.get(current) ?? []);
9252
+ }
9253
+ return false;
8501
9254
  }
8502
- return { operationId, diagnostics: [{ severity: "warning", code: "trace_start_implementation_unresolved", message: "Indexed operation matched but no implementation candidate exists", resolutionStage: "implementation", resolutionStatus: "operation_without_implementation" }] };
9255
+ };
9256
+ function scopeState(identity, parent) {
9257
+ const structuralKey = structuralScopeKey(
9258
+ identity.workspaceId,
9259
+ identity.repoId,
9260
+ identity.files,
9261
+ identity.symbolIds
9262
+ );
9263
+ const ancestry = new Set(parent?.ancestry ?? []);
9264
+ ancestry.add(structuralKey);
9265
+ return {
9266
+ structuralKey,
9267
+ evaluationKey: evaluationScopeKey(structuralKey, identity.context),
9268
+ ancestry
9269
+ };
9270
+ }
9271
+ function canonicalValue(value) {
9272
+ if (value === null) return "null";
9273
+ if (value === void 0) return "undefined";
9274
+ if (typeof value === "string") return `string:${JSON.stringify(value)}`;
9275
+ if (typeof value === "boolean") return `boolean:${String(value)}`;
9276
+ if (typeof value === "number") return canonicalNumber(value);
9277
+ if (typeof value === "bigint") return `bigint:${String(value)}`;
9278
+ if (Array.isArray(value))
9279
+ return `array:[${value.map(canonicalValue).join(",")}]`;
9280
+ if (!isRecord8(value)) return `unsupported:${typeof value}`;
9281
+ const entries = Object.entries(value).sort(([left], [right]) => compareBinary(left, right));
9282
+ return `object:{${entries.map(([key, child]) => `${JSON.stringify(key)}:${canonicalValue(child)}`).join(",")}}`;
9283
+ }
9284
+ function canonicalNumber(value) {
9285
+ if (Number.isNaN(value)) return "number:NaN";
9286
+ if (value === Number.POSITIVE_INFINITY) return "number:Infinity";
9287
+ if (value === Number.NEGATIVE_INFINITY) return "number:-Infinity";
9288
+ if (Object.is(value, -0)) return "number:-0";
9289
+ return `number:${String(value)}`;
9290
+ }
9291
+ function isRecord8(value) {
9292
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
8503
9293
  }
8504
- function sourceFilesForStart(db, repoId, start, workspaceId) {
8505
- return sourceScopeForSelector(db, repoId, start, workspaceId);
9294
+
9295
+ // src/trace/011-event-subscriber-traversal.ts
9296
+ function planEventSubscriberTransitions(db, query, scheduler, parent, depth, maxDepth) {
9297
+ return loadEventSubscriberTransitions(db, query).map((transition) => {
9298
+ const handler = transition.handler;
9299
+ if (!handler) return plannedTransition(transition, "not_resolved");
9300
+ if (depth >= maxDepth)
9301
+ return plannedTransition(transition, "depth_limited");
9302
+ const state = scheduler.schedule({
9303
+ workspaceId: query.workspaceId,
9304
+ repoId: handler.repoId,
9305
+ files: /* @__PURE__ */ new Set([handler.sourceFile]),
9306
+ symbolIds: /* @__PURE__ */ new Set([handler.symbolId]),
9307
+ context: /* @__PURE__ */ new Map()
9308
+ }, parent);
9309
+ const bodyExpansion = state.kind === "scheduled" ? "scheduled" : state.kind === "cycle" ? "cycle_blocked" : state.alreadyExpanded ? "already_expanded" : "already_scheduled";
9310
+ return plannedTransition(transition, bodyExpansion, state.state);
9311
+ });
8506
9312
  }
8507
- function startScope(db, start, hintOptions, workspaceId) {
8508
- const repos = start.repo ? reposByName(db, start.repo, workspaceId).map((row) => ({
8509
- id: row.id,
8510
- name: row.name,
8511
- packageName: row.package_name ?? void 0
8512
- })) : [];
8513
- if (start.repo && repos.length === 0) return {
8514
- selectorMatched: false,
8515
- startDiagnostics: [selectorRepoNotFoundDiagnostic(start.repo)]
9313
+ function plannedTransition(transition, bodyExpansion, state) {
9314
+ return {
9315
+ transition,
9316
+ node: eventSubscriberNode(transition),
9317
+ evidence: eventTransitionEvidence(transition, bodyExpansion),
9318
+ bodyExpansion,
9319
+ state
8516
9320
  };
8517
- if (start.repo && repos.length > 1) return {
8518
- selectorMatched: false,
8519
- startDiagnostics: [selectorRepoAmbiguousDiagnostic(start.repo, repos)]
9321
+ }
9322
+ function eventSubscriberNode(transition) {
9323
+ const handler = transition.handler;
9324
+ if (!handler) return {
9325
+ id: `event_subscription:${transition.graphEdgeId}`,
9326
+ kind: transition.targetKind,
9327
+ label: `${transition.targetKind}:${transition.targetId}`,
9328
+ graphEdgeId: transition.graphEdgeId
8520
9329
  };
8521
- const repo = repos[0];
8522
- const operationScope = operationStartScope(
8523
- db,
8524
- repo?.id,
8525
- start,
8526
- hintOptions,
8527
- workspaceId
9330
+ const fileName = handler.sourceFile.split("/").at(-1) ?? handler.sourceFile;
9331
+ return {
9332
+ id: `symbol:${handler.symbolId}`,
9333
+ kind: "symbol",
9334
+ label: `${fileName}:${handler.qualifiedName}`,
9335
+ symbolId: handler.symbolId,
9336
+ symbolName: handler.qualifiedName,
9337
+ qualifiedName: handler.qualifiedName,
9338
+ sourceFile: handler.sourceFile,
9339
+ startLine: handler.sourceLine,
9340
+ endLine: handler.endLine,
9341
+ repoName: handler.repoName,
9342
+ repoId: handler.repoId
9343
+ };
9344
+ }
9345
+ function eventTransitionEvidence(transition, bodyExpansion) {
9346
+ return {
9347
+ graphEdgeId: transition.graphEdgeId,
9348
+ graphGeneration: transition.graphGeneration,
9349
+ subscribeCallId: transition.subscribeCallId,
9350
+ symbolCallId: transition.symbolCallId,
9351
+ eventName: transition.eventName,
9352
+ matchStrategy: "workspace_exact_event_name",
9353
+ dispatchCertainty: "static_name_only",
9354
+ associationBasis: transition.associationBasis,
9355
+ dispatchScope: transition.dispatchScope,
9356
+ roleSiteMatchCount: transition.roleSiteMatchCount,
9357
+ callRole: transition.callRole,
9358
+ factOrigin: transition.factOrigin,
9359
+ repositoryId: transition.subscriptionRepoId,
9360
+ repositoryName: transition.subscriptionRepoName,
9361
+ sourceFile: transition.sourceFile,
9362
+ sourceLine: transition.sourceLine,
9363
+ callSiteStartOffset: transition.callSiteStartOffset,
9364
+ callSiteEndOffset: transition.callSiteEndOffset,
9365
+ wrapperFunction: transition.wrapperFunction,
9366
+ handlerSymbolId: transition.handler?.symbolId,
9367
+ handlerSourceFile: transition.handler?.sourceFile,
9368
+ handlerSourceLine: transition.handler?.sourceLine,
9369
+ associationStatus: transition.associationStatus ?? transition.status,
9370
+ symbolCallResolutionStatus: transition.symbolCallResolutionStatus,
9371
+ resolutionStatus: transition.status,
9372
+ resolutionStrategy: transition.resolutionStrategy,
9373
+ candidateCount: transition.candidateCount,
9374
+ symbolCallUnresolvedReason: transition.symbolCallUnresolvedReason,
9375
+ omittedSymbolCallUnresolvedReasonCharacterCount: transition.omittedSymbolCallUnresolvedReasonCharacterCount,
9376
+ reasonCode: transition.reasonCode,
9377
+ bodyExpansion,
9378
+ cycle: bodyExpansion === "cycle_blocked" || void 0,
9379
+ cycleReason: bodyExpansion === "cycle_blocked" ? "structural_ancestry_cycle" : void 0
9380
+ };
9381
+ }
9382
+ function loadEventSubscriberTransitions(db, query) {
9383
+ const rows2 = db.prepare(`SELECT ge.id graphEdgeId,ge.generation graphGeneration,
9384
+ ge.from_id eventName,ge.status,ge.to_kind targetKind,ge.to_id targetId,
9385
+ ge.confidence,ge.unresolved_reason unresolvedReason,ge.evidence_json evidenceJson,
9386
+ subscribe.id subscribeCallId,subscribe.repo_id subscriptionRepoId,
9387
+ subscribe.source_file sourceFile,subscribe.source_line sourceLine,
9388
+ subscribe.call_site_start_offset callSiteStartOffset,
9389
+ subscribe.call_site_end_offset callSiteEndOffset,
9390
+ subscription_repo.name subscriptionRepoName,
9391
+ handler.id handlerSymbolId,handler.kind handlerKind,
9392
+ handler.qualified_name handlerQualifiedName,handler.source_file handlerSourceFile,
9393
+ handler.start_line handlerSourceLine,handler.end_line handlerEndLine,
9394
+ handler.start_offset handlerStartOffset,handler.end_offset handlerEndOffset,
9395
+ handler_repo.id handlerRepoId,handler_repo.name handlerRepoName,
9396
+ handler_repo.package_name handlerPackageName
9397
+ FROM graph_edges ge
9398
+ LEFT JOIN outbound_calls subscribe
9399
+ ON subscribe.id=CAST(json_extract(ge.evidence_json,'$.subscribeCallId') AS INTEGER)
9400
+ LEFT JOIN repositories subscription_repo
9401
+ ON subscription_repo.id=subscribe.repo_id
9402
+ AND subscription_repo.workspace_id=ge.workspace_id
9403
+ LEFT JOIN symbols handler
9404
+ ON ge.to_kind='symbol' AND handler.id=CAST(ge.to_id AS INTEGER)
9405
+ LEFT JOIN repositories handler_repo
9406
+ ON handler_repo.id=handler.repo_id AND handler_repo.workspace_id=ge.workspace_id
9407
+ WHERE ge.workspace_id=? AND ge.generation=?
9408
+ AND ge.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY' AND ge.from_kind='event'
9409
+ AND ge.from_id COLLATE BINARY=? COLLATE BINARY
9410
+ ORDER BY COALESCE(subscription_repo.name,'') COLLATE BINARY,
9411
+ COALESCE(subscription_repo.id,0),COALESCE(subscribe.source_file,'') COLLATE BINARY,
9412
+ subscribe.call_site_start_offset,subscribe.call_site_end_offset,ge.id`).all(
9413
+ query.workspaceId,
9414
+ query.graphGeneration,
9415
+ query.eventName
8528
9416
  );
8529
- const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === "operation" || d.resolutionStage === "implementation");
8530
- const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start, workspaceId);
8531
- const terminalSelectorScope = Boolean(sourceScope?.diagnostics?.length && !sourceScope.files);
8532
- const sourceFiles = sourceScope?.files;
8533
- const hasSelector = Boolean(
8534
- start.handler ?? start.operation ?? start.operationPath ?? start.servicePath
9417
+ return rows2.flatMap((row) => {
9418
+ const transition = transitionFromRow(row);
9419
+ return transition ? [transition] : [];
9420
+ });
9421
+ }
9422
+ function transitionFromRow(row) {
9423
+ const graphEdgeId = numberValue10(row.graphEdgeId);
9424
+ const graphGeneration2 = numberValue10(row.graphGeneration);
9425
+ const eventName = stringValue13(row.eventName);
9426
+ const targetId = stringValue13(row.targetId);
9427
+ if (graphEdgeId === void 0 || graphGeneration2 === void 0 || eventName === void 0 || targetId === void 0) return void 0;
9428
+ const evidence = parseEvidence(row.evidenceJson);
9429
+ const handler = symbolTarget(row);
9430
+ const status = transitionStatus(stringValue13(row.status), handler);
9431
+ const reasonCode = status === "resolved" ? void 0 : stringValue13(evidence.reasonCode) ?? missingTargetReason(row, handler);
9432
+ return {
9433
+ graphEdgeId,
9434
+ graphGeneration: graphGeneration2,
9435
+ eventName,
9436
+ status,
9437
+ targetKind: targetKind(row.targetKind),
9438
+ targetId,
9439
+ confidence: numberValue10(row.confidence) ?? 0,
9440
+ unresolvedReason: status === "resolved" ? void 0 : stringValue13(row.unresolvedReason) ?? reasonCode,
9441
+ reasonCode,
9442
+ ...associationEvidence(row, evidence),
9443
+ handler
9444
+ };
9445
+ }
9446
+ function associationEvidence(row, evidence) {
9447
+ const symbolCallUnresolvedReason = stringValue13(
9448
+ evidence.symbolCallUnresolvedReason
8535
9449
  );
8536
- if (start.servicePath && !start.operation && !start.operationPath && !start.handler)
8537
- return { repo, selectorMatched: false };
8538
9450
  return {
8539
- repo,
8540
- executionRepoId: sourceScope?.repoId ?? repo?.id,
8541
- sourceFiles,
8542
- symbolIds: sourceScope?.symbols,
8543
- selectorMatched: !terminalOperationScope && !terminalSelectorScope && (!hasSelector || sourceFiles !== void 0),
8544
- startOperationId: operationScope?.operationId,
8545
- startDiagnostics: operationScope?.diagnostics?.length ? operationScope.diagnostics : sourceScope?.diagnostics
9451
+ subscribeCallId: numberValue10(row.subscribeCallId),
9452
+ symbolCallId: numberValue10(evidence.symbolCallId),
9453
+ roleSiteMatchCount: nonNegativeCount(evidence.roleSiteMatchCount),
9454
+ callRole: stringValue13(evidence.callRole),
9455
+ factOrigin: stringValue13(evidence.factOrigin),
9456
+ associationBasis: stringValue13(evidence.associationBasis),
9457
+ dispatchScope: stringValue13(evidence.dispatchScope),
9458
+ subscriptionRepoId: numberValue10(row.subscriptionRepoId),
9459
+ subscriptionRepoName: stringValue13(row.subscriptionRepoName),
9460
+ sourceFile: stringValue13(row.sourceFile),
9461
+ sourceLine: numberValue10(row.sourceLine),
9462
+ callSiteStartOffset: numberValue10(row.callSiteStartOffset),
9463
+ callSiteEndOffset: numberValue10(row.callSiteEndOffset),
9464
+ wrapperFunction: stringValue13(evidence.wrapperFunction),
9465
+ resolutionStrategy: stringValue13(evidence.resolutionStrategy),
9466
+ associationStatus: stringValue13(evidence.associationStatus),
9467
+ symbolCallResolutionStatus: stringValue13(evidence.symbolCallResolutionStatus),
9468
+ candidateCount: nonNegativeCount(evidence.candidateCount),
9469
+ symbolCallUnresolvedReason,
9470
+ omittedSymbolCallUnresolvedReasonCharacterCount: symbolCallUnresolvedReason ? nonNegativeCount(evidence.omittedSymbolCallUnresolvedReasonCharacterCount) : void 0
9471
+ };
9472
+ }
9473
+ function symbolTarget(row) {
9474
+ const symbolId = numberValue10(row.handlerSymbolId);
9475
+ const repoId = numberValue10(row.handlerRepoId);
9476
+ const repoName = stringValue13(row.handlerRepoName);
9477
+ const sourceFile = stringValue13(row.handlerSourceFile);
9478
+ const sourceLine2 = numberValue10(row.handlerSourceLine);
9479
+ const endLine = numberValue10(row.handlerEndLine);
9480
+ const kind = stringValue13(row.handlerKind);
9481
+ const qualifiedName = stringValue13(row.handlerQualifiedName);
9482
+ if (symbolId === void 0 || repoId === void 0 || !repoName || !sourceFile || sourceLine2 === void 0 || endLine === void 0 || !kind || !qualifiedName)
9483
+ return void 0;
9484
+ return {
9485
+ symbolId,
9486
+ repoId,
9487
+ repoName,
9488
+ sourceFile,
9489
+ sourceLine: sourceLine2,
9490
+ endLine,
9491
+ kind,
9492
+ qualifiedName,
9493
+ packageName: stringValue13(row.handlerPackageName),
9494
+ startOffset: numberValue10(row.handlerStartOffset),
9495
+ endOffset: numberValue10(row.handlerEndOffset)
8546
9496
  };
8547
9497
  }
8548
- function handlerFilesForOperation(db, operationId) {
8549
- const op = db.prepare(
8550
- `SELECT o.operation_name operationName,o.operation_path operationPath,s.repo_id repoId
8551
- FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?`
8552
- ).get(operationId);
8553
- if (!op) return /* @__PURE__ */ new Set();
8554
- const operation = normalizeOperation2(op.operationPath ?? op.operationName);
8555
- const rows2 = db.prepare(
8556
- `SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId FROM handler_classes hc
8557
- JOIN handler_methods hm ON hm.handler_class_id=hc.id
8558
- LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
8559
- AND sym.source_file=hc.source_file
8560
- AND sym.qualified_name=hc.class_name || '.' || hm.method_name
8561
- AND sym.start_line=hm.source_line
8562
- WHERE hc.repo_id=?
8563
- AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
8564
- CASE WHEN hm.decorator_kind='Event' THEN 'event'
8565
- WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
8566
- ELSE 'unsupported' END)='operation'
8567
- AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
8568
- CASE WHEN hm.decorator_kind IN ('Action','Func','On')
8569
- THEN 1 ELSE 0 END)=1
8570
- AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`
8571
- ).all(op.repoId, operation, operation, op.operationName);
8572
- return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
9498
+ function transitionStatus(value, handler) {
9499
+ if (value === "resolved" && handler) return "resolved";
9500
+ return value === "ambiguous" ? "ambiguous" : "unresolved";
8573
9501
  }
8574
- function implementationEdge(db, operationId) {
8575
- return db.prepare("SELECT * FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND from_kind='operation' AND from_id=? ORDER BY CASE status WHEN 'resolved' THEN 0 WHEN 'ambiguous' THEN 1 ELSE 2 END,id LIMIT 1").get(operationId);
9502
+ function missingTargetReason(row, handler) {
9503
+ return stringValue13(row.status) === "resolved" && !handler ? "subscription_handler_target_missing" : void 0;
8576
9504
  }
8577
- function implementationScope(db, operationId) {
8578
- const edge = implementationEdge(db, operationId);
8579
- if (!edge || edge.status !== "resolved") return { files: /* @__PURE__ */ new Set(), edge };
8580
- 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);
8581
- if (!row || typeof row.symbolId !== "number")
8582
- return { repoId: row?.repoId, files: /* @__PURE__ */ new Set(), edge };
8583
- return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
9505
+ function targetKind(value) {
9506
+ return value === "symbol" || value === "symbol_reference" || value === "subscription_handler" ? value : "subscription_handler";
8584
9507
  }
8585
- function handlerScope(db, methodId) {
8586
- 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);
8587
- if (!row || typeof row.symbolId !== "number") return void 0;
8588
- return { repoId: row.repoId, files: new Set(row.sourceFile ? [row.sourceFile] : []), symbolId: row.symbolId };
9508
+ function parseEvidence(value) {
9509
+ try {
9510
+ const parsed = JSON.parse(String(value ?? "{}"));
9511
+ return isRecord9(parsed) ? parsed : {};
9512
+ } catch {
9513
+ return {};
9514
+ }
8589
9515
  }
8590
- function traceEdgeType(call, row) {
8591
- if (row.to_kind === "operation" && row.edge_type === "REMOTE_CALL_RESOLVES_TO_OPERATION") return "remote_action";
8592
- if (row.to_kind === "operation" && row.edge_type === "LOCAL_CALL_RESOLVES_TO_OPERATION") return "local_service_call";
8593
- return String(call.call_type);
9516
+ function nonNegativeCount(value) {
9517
+ const count2 = numberValue10(value);
9518
+ return count2 === void 0 ? 0 : Math.max(0, Math.floor(count2));
8594
9519
  }
8595
- function includeCall(type, options) {
8596
- if (!options.includeDb && type === "local_db_query") return false;
8597
- if (!options.includeExternal && type === "external_http") return false;
8598
- if (!options.includeAsync && type.startsWith("async_")) return false;
8599
- return true;
9520
+ function numberValue10(value) {
9521
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
9522
+ }
9523
+ function stringValue13(value) {
9524
+ return typeof value === "string" ? value : void 0;
8600
9525
  }
9526
+ function isRecord9(value) {
9527
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
9528
+ }
9529
+
9530
+ // src/trace/012-trace-graph-lookups.ts
8601
9531
  function graphForCalls(db, callIds) {
8602
9532
  const map = /* @__PURE__ */ new Map();
8603
9533
  if (callIds.length === 0) return map;
8604
- const rows2 = db.prepare(
8605
- `SELECT * FROM graph_edges WHERE from_kind='call' AND from_id IN (${callIds.map(() => "?").join(",")}) ORDER BY id`
8606
- ).all(...callIds.map((id) => String(id)));
9534
+ const rows2 = db.prepare(`SELECT * FROM graph_edges
9535
+ WHERE from_kind='call'
9536
+ AND from_id IN (${callIds.map(() => "?").join(",")})
9537
+ ORDER BY id`).all(
9538
+ ...callIds.map(String)
9539
+ );
8607
9540
  for (const row of rows2) {
8608
9541
  const id = Number(row.from_id);
8609
9542
  map.set(id, [...map.get(id) ?? [], row]);
@@ -8611,92 +9544,487 @@ function graphForCalls(db, callIds) {
8611
9544
  return map;
8612
9545
  }
8613
9546
  function symbolNode(db, symbolId) {
8614
- const row = db.prepare(`SELECT s.id symbolId,s.name symbolName,s.qualified_name qualifiedName,s.source_file sourceFile,s.start_line startLine,s.end_line endLine,r.name repoName,r.id repoId FROM symbols s JOIN repositories r ON r.id=s.repo_id WHERE s.id=?`).get(symbolId);
9547
+ const row = db.prepare(`SELECT s.id symbolId,s.name symbolName,
9548
+ s.qualified_name qualifiedName,s.source_file sourceFile,
9549
+ s.start_line startLine,s.end_line endLine,r.name repoName,r.id repoId
9550
+ FROM symbols s JOIN repositories r ON r.id=s.repo_id
9551
+ WHERE s.id=?`).get(symbolId);
8615
9552
  if (!row) return void 0;
8616
- const fileName = String(row.sourceFile ?? "").split("/").at(-1) ?? String(row.sourceFile ?? "");
8617
- return { id: `symbol:${symbolId}`, kind: "symbol", label: `${fileName}:${String(row.qualifiedName ?? row.symbolName)}`, ...row };
9553
+ const sourceFile = String(row.sourceFile ?? "");
9554
+ const fileName = sourceFile.split("/").at(-1) ?? sourceFile;
9555
+ return {
9556
+ id: `symbol:${symbolId}`,
9557
+ kind: "symbol",
9558
+ label: `${fileName}:${String(row.qualifiedName ?? row.symbolName)}`,
9559
+ ...row
9560
+ };
8618
9561
  }
8619
9562
  function operationNode(db, operationId) {
8620
- const row = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_type operationType,o.operation_path operationPath,o.source_file sourceFile,o.source_line sourceLine,s.id serviceId,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,r.id repoId,r.name repoName FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE o.id=?`).get(operationId);
9563
+ const row = db.prepare(`SELECT o.id operationId,o.operation_name operationName,
9564
+ o.operation_type operationType,o.operation_path operationPath,
9565
+ o.source_file sourceFile,o.source_line sourceLine,s.id serviceId,
9566
+ s.service_name serviceName,s.qualified_name qualifiedName,
9567
+ s.service_path servicePath,r.id repoId,r.name repoName
9568
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
9569
+ JOIN repositories r ON r.id=s.repo_id WHERE o.id=?`).get(
9570
+ operationId
9571
+ );
8621
9572
  if (!row) return void 0;
8622
- return { id: `operation:${operationId}`, kind: "operation", label: `${String(row.repoName)}:${String(row.servicePath)}${String(row.operationPath)}`, ...row };
9573
+ return {
9574
+ id: `operation:${operationId}`,
9575
+ kind: "operation",
9576
+ label: `${String(row.repoName)}:${String(row.servicePath)}${String(row.operationPath)}`,
9577
+ ...row
9578
+ };
9579
+ }
9580
+
9581
+ // src/trace/013-trace-root-scopes.ts
9582
+ function createTraceRootPlan(db, scheduler, scope, requestedWorkspaceId, includeAsync) {
9583
+ const workspaceId = resolveTraversalWorkspaceId(
9584
+ db,
9585
+ requestedWorkspaceId,
9586
+ scope.repoId
9587
+ );
9588
+ if (workspaceId !== void 0) {
9589
+ const lifecycle = currentFactLifecycleDiagnostic(db, workspaceId);
9590
+ if (lifecycle) return {
9591
+ workspaceId,
9592
+ queue: [],
9593
+ pendingRoots: [],
9594
+ diagnostic: lifecycle
9595
+ };
9596
+ }
9597
+ if (!scope.selectorMatched)
9598
+ return { workspaceId, queue: [], pendingRoots: [] };
9599
+ if (workspaceId === void 0) return {
9600
+ workspaceId,
9601
+ queue: [],
9602
+ pendingRoots: [],
9603
+ diagnostic: workspaceAmbiguityDiagnostic(db)
9604
+ };
9605
+ const pendingRoots = includeAsync ? rootScopes(db, workspaceId, scope) : void 0;
9606
+ if (pendingRoots)
9607
+ return { workspaceId, queue: [], pendingRoots };
9608
+ return {
9609
+ workspaceId,
9610
+ queue: initialQueue(scheduler, workspaceId, scope),
9611
+ pendingRoots: []
9612
+ };
8623
9613
  }
8624
- function workspaceIdForCall(db, callId) {
8625
- return db.prepare("SELECT r.workspace_id workspaceId FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE c.id=?").get(callId)?.workspaceId;
9614
+ function nextPendingRoot(pendingRoots, scheduler, workspaceId) {
9615
+ while (pendingRoots.length > 0) {
9616
+ const root = pendingRoots.shift();
9617
+ if (!root) return void 0;
9618
+ const context = /* @__PURE__ */ new Map();
9619
+ const scheduled = scheduler.schedule({
9620
+ workspaceId,
9621
+ repoId: root.repoId,
9622
+ files: root.files,
9623
+ symbolIds: root.symbolIds,
9624
+ context
9625
+ });
9626
+ if (scheduled.kind !== "scheduled") continue;
9627
+ return { ...root, depth: 1, context, state: scheduled.state };
9628
+ }
9629
+ return void 0;
8626
9630
  }
8627
- function parseEvidence(value) {
9631
+ function claimPendingRoot(pendingRoots, target) {
9632
+ const index = pendingRoots.findIndex((root) => target.repoId === root.repoId && !root.unownedOnly && setsEqual(root.files, target.files) && setsEqual(root.symbolIds, target.symbolIds));
9633
+ if (index < 0) return false;
9634
+ pendingRoots.splice(index, 1);
9635
+ return true;
9636
+ }
9637
+ function enqueueCausalScope(queue, pendingRoots, scope) {
9638
+ if (scope.context.size === 0 && scope.files && scope.symbolIds)
9639
+ claimPendingRoot(pendingRoots, {
9640
+ repoId: scope.repoId,
9641
+ files: scope.files,
9642
+ symbolIds: scope.symbolIds
9643
+ });
9644
+ queue.push(scope);
9645
+ }
9646
+ function initialQueue(scheduler, workspaceId, scope) {
9647
+ const context = /* @__PURE__ */ new Map();
9648
+ const scheduled = scheduler.schedule({
9649
+ workspaceId,
9650
+ repoId: scope.repoId,
9651
+ files: scope.files,
9652
+ symbolIds: scope.symbolIds,
9653
+ context
9654
+ });
9655
+ return scheduled.kind === "scheduled" ? [{
9656
+ repoId: scope.repoId,
9657
+ files: scope.files,
9658
+ symbolIds: scope.symbolIds,
9659
+ depth: 1,
9660
+ context,
9661
+ state: scheduled.state
9662
+ }] : [];
9663
+ }
9664
+ function rootScopes(db, workspaceId, scope) {
9665
+ if (scope.symbolIds && scope.symbolIds.size === 1) return void 0;
9666
+ const calls = scopedRootCalls(db, workspaceId, scope);
9667
+ if (!hasExactDispatch(db, calls)) return void 0;
9668
+ if (scope.symbolIds && scope.symbolIds.size > 1)
9669
+ return selectedSymbolRoots(db, workspaceId, scope.symbolIds);
9670
+ return callOwnerRoots(calls);
9671
+ }
9672
+ function scopedRootCalls(db, workspaceId, scope) {
9673
+ return rootCalls(db, workspaceId, scope.repoId, scope.files).filter((call) => !scope.symbolIds || typeof call.sourceSymbolId === "number" && scope.symbolIds.has(call.sourceSymbolId));
9674
+ }
9675
+ function callOwnerRoots(calls) {
9676
+ const roots = /* @__PURE__ */ new Map();
9677
+ for (const call of calls) {
9678
+ const [key, root] = callOwnerRoot(call);
9679
+ if (roots.has(key)) continue;
9680
+ roots.set(key, root);
9681
+ }
9682
+ return roots.size > 0 ? [...roots.values()] : void 0;
9683
+ }
9684
+ function callOwnerRoot(call) {
9685
+ const symbolId = call.sourceSymbolId;
9686
+ const owned = typeof symbolId === "number";
9687
+ const key = owned ? `symbol:${symbolId}` : `unowned:${call.repoId}:${call.sourceFile}`;
9688
+ return [key, {
9689
+ repoId: call.repoId,
9690
+ files: /* @__PURE__ */ new Set([call.sourceFile]),
9691
+ symbolIds: owned ? /* @__PURE__ */ new Set([symbolId]) : /* @__PURE__ */ new Set(),
9692
+ unownedOnly: !owned,
9693
+ rootObservationOnly: true
9694
+ }];
9695
+ }
9696
+ function selectedSymbolRoots(db, workspaceId, symbolIds) {
9697
+ const ids = [...symbolIds];
9698
+ const rows2 = db.prepare(`SELECT s.id,s.repo_id repoId,s.source_file sourceFile
9699
+ FROM symbols s JOIN repositories r ON r.id=s.repo_id
9700
+ WHERE r.workspace_id=? AND s.id IN (${ids.map(() => "?").join(",")})
9701
+ ORDER BY r.name COLLATE BINARY,r.id,s.source_file COLLATE BINARY,
9702
+ s.start_offset,s.end_offset,s.id`).all(workspaceId, ...ids);
9703
+ return rows2.flatMap((row) => typeof row.id === "number" && typeof row.repoId === "number" && typeof row.sourceFile === "string" ? [{
9704
+ repoId: row.repoId,
9705
+ files: /* @__PURE__ */ new Set([row.sourceFile]),
9706
+ symbolIds: /* @__PURE__ */ new Set([row.id]),
9707
+ unownedOnly: false,
9708
+ rootObservationOnly: false
9709
+ }] : []);
9710
+ }
9711
+ function setsEqual(left, right) {
9712
+ if (left.size !== right.size) return false;
9713
+ return [...left].every((value) => right.has(value));
9714
+ }
9715
+ function rootCalls(db, workspaceId, repoId, files) {
9716
+ const rows2 = db.prepare(`SELECT c.id,c.repo_id repoId,r.name repoName,
9717
+ r.workspace_id workspaceId,r.graph_generation graphGeneration,
9718
+ c.source_symbol_id sourceSymbolId,c.source_file sourceFile,
9719
+ c.call_type callType,c.event_name_expr eventName
9720
+ FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
9721
+ WHERE r.workspace_id=? AND (? IS NULL OR c.repo_id=?)
9722
+ ORDER BY r.name COLLATE BINARY,r.id,c.source_file COLLATE BINARY,
9723
+ c.call_site_start_offset,c.call_site_end_offset,c.source_line,c.id`).all(
9724
+ workspaceId,
9725
+ repoId,
9726
+ repoId
9727
+ );
9728
+ return files ? rows2.filter((row) => files.has(row.sourceFile)) : rows2;
9729
+ }
9730
+ function hasExactDispatch(db, calls) {
9731
+ const match = db.prepare(`SELECT 1 matched FROM graph_edges emitted
9732
+ WHERE emitted.workspace_id=? AND emitted.generation=?
9733
+ AND emitted.edge_type='HANDLER_EMITS_EVENT'
9734
+ AND emitted.from_kind='call' AND emitted.from_id=?
9735
+ AND EXISTS (SELECT 1 FROM graph_edges subscriber
9736
+ WHERE subscriber.workspace_id=emitted.workspace_id
9737
+ AND subscriber.generation=emitted.generation
9738
+ AND subscriber.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
9739
+ AND subscriber.from_kind='event'
9740
+ AND subscriber.from_id COLLATE BINARY=? COLLATE BINARY)
9741
+ LIMIT 1`);
9742
+ return calls.some((call) => call.callType === "async_emit" && typeof call.eventName === "string" && Boolean(match.get(
9743
+ call.workspaceId,
9744
+ call.graphGeneration,
9745
+ String(call.id),
9746
+ call.eventName
9747
+ )));
9748
+ }
9749
+ function workspaceAmbiguityDiagnostic(db) {
9750
+ const total = Number(db.prepare(`SELECT COUNT(DISTINCT w.id) count
9751
+ FROM workspaces w JOIN repositories r ON r.workspace_id=w.id`).get()?.count ?? 0);
9752
+ const workspaceIds = db.prepare(`SELECT DISTINCT w.id FROM workspaces w
9753
+ JOIN repositories r ON r.workspace_id=w.id ORDER BY w.id LIMIT 5`).all().flatMap((row) => typeof row.id === "number" ? [row.id] : []);
9754
+ return {
9755
+ severity: "error",
9756
+ code: "trace_workspace_ambiguous",
9757
+ message: total > 1 ? "Trace spans multiple indexed workspaces; provide a workspace identity." : "No indexed workspace could be selected for this trace.",
9758
+ workspaceCount: total,
9759
+ workspaceIds,
9760
+ omittedWorkspaceCount: Math.max(0, total - workspaceIds.length),
9761
+ remediation: "Pass TraceOptions.workspaceId or select a repository in one workspace."
9762
+ };
9763
+ }
9764
+
9765
+ // src/trace/017-trace-context.ts
9766
+ function parseTraceEvidence(value) {
8628
9767
  try {
8629
9768
  const parsed = JSON.parse(String(value || "{}"));
8630
- return isEvidenceRecord2(parsed) ? boundCandidateLikeEvidence(parsed) : {};
9769
+ return isRecord10(parsed) ? boundCandidateLikeEvidence(parsed) : {};
8631
9770
  } catch {
8632
9771
  return {};
8633
9772
  }
8634
9773
  }
8635
- function isEvidenceRecord2(value) {
8636
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
8637
- }
8638
- function receiverFromEvidence(value) {
8639
- const evidence = parseEvidence(value);
9774
+ function receiverFromTraceEvidence(value) {
9775
+ const evidence = parseTraceEvidence(value);
8640
9776
  return typeof evidence.receiver === "string" ? evidence.receiver : void 0;
8641
9777
  }
8642
- function hasDynamicPlaceholder(value) {
8643
- return extractPlaceholders(value).length > 0;
8644
- }
8645
- function enrichBinding(row) {
8646
- const effectiveServicePath = row.servicePathExpr && !hasDynamicPlaceholder(row.servicePathExpr) ? row.servicePathExpr : !row.servicePathExpr ? row.requireServicePath : void 0;
8647
- const effectiveDestination = row.destinationExpr && !hasDynamicPlaceholder(row.destinationExpr) ? row.destinationExpr : !row.destinationExpr ? row.requireDestination : void 0;
8648
- return { ...row, effectiveServicePath, effectiveDestination };
8649
- }
8650
9778
  function knownBindingsForCalls(db, calls) {
8651
9779
  const map = /* @__PURE__ */ new Map();
8652
- for (const call of calls) {
8653
- const receiver = receiverFromEvidence(call.evidence_json);
8654
- const bindingId = Number(call.service_binding_id ?? 0);
8655
- if (!receiver || !bindingId) continue;
8656
- const row = db.prepare(`SELECT b.id,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.source_file sourceFile,b.source_line sourceLine,req.service_path requireServicePath,req.destination requireDestination
8657
- FROM service_bindings b LEFT JOIN cds_requires req ON req.repo_id=b.repo_id AND req.alias=b.alias
8658
- WHERE b.id=?`).get(bindingId);
8659
- if (row) map.set(receiver, enrichBinding({ ...row, bindingId, source: "local_service_binding", calleeReceiver: receiver }));
8660
- }
9780
+ for (const call of calls) addCallBinding(db, map, call);
8661
9781
  return map;
8662
9782
  }
8663
9783
  function knownBindingsForScope(db, repoId, symbolIds, files) {
8664
9784
  const map = /* @__PURE__ */ new Map();
8665
9785
  if (repoId === void 0) return map;
8666
- const rows2 = db.prepare(`SELECT b.id,b.symbol_id symbolId,b.variable_name variableName,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.source_file sourceFile,b.source_line sourceLine,req.service_path requireServicePath,req.destination requireDestination
8667
- FROM service_bindings b LEFT JOIN cds_requires req ON req.repo_id=b.repo_id AND req.alias=b.alias
8668
- WHERE b.repo_id=?`).all(repoId);
8669
- for (const row of rows2) {
8670
- if (!row.variableName) continue;
8671
- if (files && !files.has(String(row.sourceFile))) continue;
8672
- if (symbolIds && symbolIds.size > 0 && row.symbolId != null && !symbolIds.has(Number(row.symbolId))) continue;
8673
- const candidate = enrichBinding({
8674
- ...row,
8675
- bindingId: Number(row.id),
8676
- source: "local_service_binding",
8677
- calleeReceiver: row.variableName,
8678
- resolutionStatus: "selected"
8679
- });
8680
- const existing = map.get(row.variableName);
8681
- if (!existing) {
8682
- map.set(row.variableName, candidate);
8683
- continue;
9786
+ const rows2 = db.prepare(`SELECT b.id,b.symbol_id symbolId,
9787
+ b.variable_name variableName,b.alias,b.alias_expr aliasExpr,
9788
+ b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,
9789
+ b.source_file sourceFile,b.source_line sourceLine,
9790
+ req.service_path requireServicePath,req.destination requireDestination
9791
+ FROM service_bindings b LEFT JOIN cds_requires req
9792
+ ON req.repo_id=b.repo_id AND req.alias=b.alias
9793
+ WHERE b.repo_id=?
9794
+ ORDER BY b.source_file COLLATE BINARY,b.source_line,b.id`).all(
9795
+ repoId
9796
+ );
9797
+ for (const row of rows2) addScopeBinding(map, row, symbolIds, files);
9798
+ return map;
9799
+ }
9800
+ function contextForSymbolCall(db, symbolCall, callerBindings) {
9801
+ const next = /* @__PURE__ */ new Map();
9802
+ if (callerBindings.size === 0) return next;
9803
+ const context = symbolCallContext(db, symbolCall);
9804
+ context.args.forEach((argument, index) => addArgumentBindings(
9805
+ next,
9806
+ callerBindings,
9807
+ context,
9808
+ argument,
9809
+ index
9810
+ ));
9811
+ return next;
9812
+ }
9813
+ function addCallBinding(db, map, call) {
9814
+ const receiver = receiverFromTraceEvidence(call.evidence_json);
9815
+ const bindingId = Number(call.service_binding_id ?? 0);
9816
+ if (!receiver || !bindingId) return;
9817
+ const row = db.prepare(`SELECT b.id,b.alias,b.alias_expr aliasExpr,
9818
+ b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,
9819
+ b.source_file sourceFile,b.source_line sourceLine,
9820
+ req.service_path requireServicePath,req.destination requireDestination
9821
+ FROM service_bindings b LEFT JOIN cds_requires req
9822
+ ON req.repo_id=b.repo_id AND req.alias=b.alias WHERE b.id=?`).get(
9823
+ bindingId
9824
+ );
9825
+ if (row) map.set(receiver, enrichBinding({
9826
+ ...row,
9827
+ bindingId,
9828
+ source: "local_service_binding",
9829
+ calleeReceiver: receiver
9830
+ }));
9831
+ }
9832
+ function addScopeBinding(map, row, symbolIds, files) {
9833
+ if (!row.variableName) return;
9834
+ if (files && !files.has(String(row.sourceFile))) return;
9835
+ if (symbolIds?.size && row.symbolId != null && !symbolIds.has(Number(row.symbolId))) return;
9836
+ const candidate = enrichBinding({
9837
+ ...row,
9838
+ bindingId: Number(row.id),
9839
+ source: "local_service_binding",
9840
+ calleeReceiver: row.variableName,
9841
+ resolutionStatus: "selected"
9842
+ });
9843
+ const existing = map.get(row.variableName);
9844
+ if (!existing) {
9845
+ map.set(row.variableName, candidate);
9846
+ return;
9847
+ }
9848
+ map.set(row.variableName, ambiguousBinding(existing, candidate));
9849
+ }
9850
+ function ambiguousBinding(existing, candidate) {
9851
+ const bindingCandidates2 = uniqueBindingCandidates([
9852
+ ...existing.bindingCandidates ?? [bindingEvidence3(existing)],
9853
+ bindingEvidence3(candidate)
9854
+ ]);
9855
+ return {
9856
+ ...candidate,
9857
+ bindingId: void 0,
9858
+ source: "ambiguous_local_service_bindings",
9859
+ resolutionStatus: "ambiguous",
9860
+ bindingCandidates: bindingCandidates2
9861
+ };
9862
+ }
9863
+ function symbolCallContext(db, symbolCall) {
9864
+ const callEvidence = parseTraceEvidence(symbolCall.evidence_json);
9865
+ const callee = db.prepare(`SELECT evidence_json evidenceJson,
9866
+ source_file sourceFile,start_line startLine FROM symbols WHERE id=?`).get(
9867
+ symbolCall.callee_symbol_id
9868
+ );
9869
+ const evidence = parseTraceEvidence(callee?.evidenceJson);
9870
+ return {
9871
+ args: recordArray6(callEvidence.callArguments),
9872
+ params: stringArray6(evidence.parameters),
9873
+ parameterBindings: recordArray6(evidence.parameterBindings),
9874
+ parameterPropertyAliases: recordArray6(evidence.parameterPropertyAliases),
9875
+ provenance: {
9876
+ callerSite: {
9877
+ sourceFile: String(symbolCall.source_file ?? ""),
9878
+ sourceLine: Number(symbolCall.source_line ?? 0)
9879
+ },
9880
+ calleeSite: { sourceFile: callee?.sourceFile, sourceLine: callee?.startLine }
8684
9881
  }
8685
- const candidates2 = uniqueBindingCandidates([
8686
- ...existing.bindingCandidates ?? [bindingCandidateEvidence(existing)],
8687
- bindingCandidateEvidence(candidate)
8688
- ]);
8689
- map.set(row.variableName, {
8690
- ...candidate,
8691
- bindingId: void 0,
8692
- source: "ambiguous_local_service_bindings",
8693
- resolutionStatus: "ambiguous",
8694
- bindingCandidates: candidates2
9882
+ };
9883
+ }
9884
+ function addArgumentBindings(next, caller, context, argument, index) {
9885
+ const parameterBinding = context.parameterBindings.find(
9886
+ (binding) => binding.index === index
9887
+ );
9888
+ const parameter = parameterBinding?.kind === "identifier" && typeof parameterBinding.name === "string" ? parameterBinding.name : context.params[index];
9889
+ addIdentifierBinding(next, caller, context.provenance, argument, parameter);
9890
+ addObjectBindings(next, caller, context, argument, parameterBinding, parameter, index);
9891
+ addArrayBindings(next, caller, context.provenance, argument, parameterBinding, index);
9892
+ }
9893
+ function addIdentifierBinding(next, caller, provenance, argument, parameter) {
9894
+ if (argument.kind !== "identifier" || typeof argument.name !== "string" || !parameter) return;
9895
+ const binding = caller.get(argument.name);
9896
+ if (binding) next.set(parameter, {
9897
+ ...binding,
9898
+ ...provenance,
9899
+ source: "local_symbol_argument",
9900
+ callerArgument: argument.name,
9901
+ calleeParameter: parameter,
9902
+ calleeReceiver: parameter
9903
+ });
9904
+ }
9905
+ function addObjectBindings(next, caller, context, argument, parameterBinding, parameter, index) {
9906
+ if (argument.kind !== "object_literal" || !Array.isArray(argument.properties)) return;
9907
+ for (const property of recordArray6(argument.properties)) addObjectPropertyBinding(
9908
+ next,
9909
+ caller,
9910
+ context,
9911
+ property,
9912
+ parameterBinding,
9913
+ parameter,
9914
+ index
9915
+ );
9916
+ }
9917
+ function addObjectPropertyBinding(next, caller, context, property, parameterBinding, parameter, index) {
9918
+ if (typeof property.property !== "string" || typeof property.argument !== "string") return;
9919
+ const binding = caller.get(property.argument);
9920
+ if (!binding) return;
9921
+ const local = objectPatternLocal(parameterBinding, property.property);
9922
+ if (local) {
9923
+ next.set(local, objectBinding(
9924
+ binding,
9925
+ context.provenance,
9926
+ property.property,
9927
+ property.argument,
9928
+ String(index),
9929
+ local,
9930
+ "local_symbol_destructured_object_argument"
9931
+ ));
9932
+ return;
9933
+ }
9934
+ if (!parameter) return;
9935
+ const receiver = `${parameter}.${property.property}`;
9936
+ next.set(receiver, objectBinding(
9937
+ binding,
9938
+ context.provenance,
9939
+ property.property,
9940
+ property.argument,
9941
+ parameter,
9942
+ receiver,
9943
+ "local_symbol_object_argument"
9944
+ ));
9945
+ addObjectAliases(next, binding, context, property, parameter, receiver);
9946
+ }
9947
+ function addObjectAliases(next, binding, context, property, parameter, receiver) {
9948
+ for (const alias of context.parameterPropertyAliases) {
9949
+ if (alias.parameter !== parameter || alias.property !== property.property || typeof alias.local !== "string") continue;
9950
+ next.set(alias.local, {
9951
+ ...objectBinding(
9952
+ binding,
9953
+ context.provenance,
9954
+ String(property.property),
9955
+ String(property.argument),
9956
+ parameter,
9957
+ alias.local,
9958
+ "local_symbol_object_parameter_destructure"
9959
+ ),
9960
+ calleeObjectProperty: receiver,
9961
+ calleeLocalDestructuredIdentifier: alias.local,
9962
+ parameterPropertyAliasKind: alias.kind,
9963
+ parameterPropertyAliasLine: alias.line
8695
9964
  });
8696
9965
  }
8697
- return map;
8698
9966
  }
8699
- function bindingCandidateEvidence(binding) {
9967
+ function addArrayBindings(next, caller, provenance, argument, parameterBinding, index) {
9968
+ const arrays = arrayPattern(argument, parameterBinding);
9969
+ if (!arrays) return;
9970
+ for (const element of arrays.elements)
9971
+ addArrayElement(next, caller, provenance, element, arrays.targets, index);
9972
+ }
9973
+ function arrayPattern(argument, binding) {
9974
+ if (argument.kind !== "array_literal") return void 0;
9975
+ if (!Array.isArray(argument.elements)) return void 0;
9976
+ if (binding?.kind !== "array_pattern") return void 0;
9977
+ if (!Array.isArray(binding.elements)) return void 0;
9978
+ return {
9979
+ elements: recordArray6(argument.elements),
9980
+ targets: recordArray6(binding.elements)
9981
+ };
9982
+ }
9983
+ function addArrayElement(next, caller, provenance, element, targets, index) {
9984
+ if (element.kind !== "identifier" || typeof element.name !== "string") return;
9985
+ const target = targets.find((item) => item.index === element.index);
9986
+ if (typeof target?.local !== "string") return;
9987
+ const binding = caller.get(element.name);
9988
+ if (binding) next.set(target.local, {
9989
+ ...binding,
9990
+ ...provenance,
9991
+ source: "local_symbol_destructured_array_argument",
9992
+ callerArgument: element.name,
9993
+ calleeParameter: String(index),
9994
+ calleeReceiver: target.local
9995
+ });
9996
+ }
9997
+ function objectPatternLocal(binding, property) {
9998
+ if (binding?.kind !== "object_pattern" || !Array.isArray(binding.properties)) return void 0;
9999
+ const match = recordArray6(binding.properties).find(
10000
+ (item) => item.property === property && typeof item.local === "string"
10001
+ );
10002
+ return typeof match?.local === "string" ? match.local : void 0;
10003
+ }
10004
+ function objectBinding(binding, provenance, callerProperty, callerArgument, parameter, receiver, source) {
10005
+ return {
10006
+ ...binding,
10007
+ ...provenance,
10008
+ source,
10009
+ callerProperty,
10010
+ callerArgument,
10011
+ calleeParameter: parameter,
10012
+ calleeReceiver: receiver
10013
+ };
10014
+ }
10015
+ function enrichBinding(row) {
10016
+ const servicePath = row.servicePathExpr && !hasPlaceholder(row.servicePathExpr) ? row.servicePathExpr : !row.servicePathExpr ? row.requireServicePath : void 0;
10017
+ const destination = row.destinationExpr && !hasPlaceholder(row.destinationExpr) ? row.destinationExpr : !row.destinationExpr ? row.requireDestination : void 0;
10018
+ return {
10019
+ ...row,
10020
+ effectiveServicePath: servicePath,
10021
+ effectiveDestination: destination
10022
+ };
10023
+ }
10024
+ function hasPlaceholder(value) {
10025
+ return extractPlaceholders(value).length > 0;
10026
+ }
10027
+ function bindingEvidence3(binding) {
8700
10028
  return {
8701
10029
  bindingId: binding.bindingId,
8702
10030
  sourceFile: binding.sourceFile,
@@ -8707,74 +10035,672 @@ function bindingCandidateEvidence(binding) {
8707
10035
  servicePathExpr: binding.servicePathExpr
8708
10036
  };
8709
10037
  }
8710
- function uniqueBindingCandidates(candidates2) {
8711
- const seen = /* @__PURE__ */ new Set();
8712
- return candidates2.filter((candidate) => {
8713
- const key = JSON.stringify(candidate);
8714
- if (seen.has(key)) return false;
8715
- seen.add(key);
8716
- return true;
8717
- });
10038
+ function uniqueBindingCandidates(candidates2) {
10039
+ const seen = /* @__PURE__ */ new Set();
10040
+ return candidates2.filter((candidate) => {
10041
+ const key = JSON.stringify(candidate);
10042
+ if (seen.has(key)) return false;
10043
+ seen.add(key);
10044
+ return true;
10045
+ });
10046
+ }
10047
+ function recordArray6(value) {
10048
+ return Array.isArray(value) ? value.filter(isRecord10) : [];
10049
+ }
10050
+ function stringArray6(value) {
10051
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
10052
+ }
10053
+ function isRecord10(value) {
10054
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
10055
+ }
10056
+
10057
+ // src/trace/015-trace-edge-recorder.ts
10058
+ var nonResolvedGraphStatuses = {
10059
+ ambiguous: "ambiguous",
10060
+ dynamic: "dynamic",
10061
+ terminal: "terminal"
10062
+ };
10063
+ var TraceEdgeRecorder = class {
10064
+ constructor(edges, observer) {
10065
+ this.edges = edges;
10066
+ this.observer = observer;
10067
+ }
10068
+ edges;
10069
+ observer;
10070
+ record(edge, semantics) {
10071
+ const ordinal = this.edges.length;
10072
+ this.edges.push(edge);
10073
+ this.observer?.record(observation(ordinal, edge, semantics));
10074
+ return ordinal;
10075
+ }
10076
+ unavailable(side, endpointKind) {
10077
+ return {
10078
+ kind: "unavailable",
10079
+ side,
10080
+ endpointKind,
10081
+ detailedEdgeIndex: this.edges.length
10082
+ };
10083
+ }
10084
+ };
10085
+ function semanticCallSource(call, workspaceId) {
10086
+ const symbolId = positiveNumber(call.source_symbol_id);
10087
+ if (symbolId !== void 0) return { kind: "symbol", symbolId };
10088
+ return {
10089
+ kind: "call_site",
10090
+ workspaceId,
10091
+ repositoryId: call.repo_id,
10092
+ repositoryName: call.repoName,
10093
+ sourceFile: call.source_file,
10094
+ sourceLine: call.source_line,
10095
+ startOffset: finiteNumber(call.call_site_start_offset),
10096
+ endOffset: finiteNumber(call.call_site_end_offset),
10097
+ callId: call.id
10098
+ };
10099
+ }
10100
+ function semanticOperation(value, unavailable) {
10101
+ const operationId = positiveNumber(value);
10102
+ return operationId === void 0 ? unavailable() : { kind: "operation", operationId };
10103
+ }
10104
+ function semanticSymbol(value, unavailable) {
10105
+ const symbolId = positiveNumber(value);
10106
+ return symbolId === void 0 ? unavailable() : { kind: "symbol", symbolId };
10107
+ }
10108
+ function semanticHandler(methodIdValue, symbolIdValue, unavailable) {
10109
+ const symbolId = positiveNumber(symbolIdValue);
10110
+ if (symbolId !== void 0) return { kind: "symbol", symbolId };
10111
+ const handlerMethodId = positiveNumber(methodIdValue);
10112
+ return handlerMethodId === void 0 ? unavailable() : { kind: "handler_method", handlerMethodId };
10113
+ }
10114
+ function semanticGraphTarget(row, call, workspaceId, unavailable) {
10115
+ if (row.to_kind === "event") return {
10116
+ kind: "event",
10117
+ workspaceId,
10118
+ eventName: typeof call.event_name_expr === "string" ? call.event_name_expr : row.to_id
10119
+ };
10120
+ const id = positiveNumber(row.to_id);
10121
+ if (row.to_kind === "operation")
10122
+ return id === void 0 ? unavailable() : { kind: "operation", operationId: id };
10123
+ if (row.to_kind === "symbol")
10124
+ return id === void 0 ? unavailable() : { kind: "symbol", symbolId: id };
10125
+ if (row.to_kind === "handler_method") return id === void 0 ? unavailable() : { kind: "handler_method", handlerMethodId: id };
10126
+ return {
10127
+ kind: "target",
10128
+ workspaceId,
10129
+ repositoryId: positiveNumber(call.repo_id),
10130
+ targetKind: row.to_kind,
10131
+ targetId: row.to_id
10132
+ };
10133
+ }
10134
+ function semanticScopeTarget(workspaceId, repositoryId, sourceFiles, symbolIds, structuralKey) {
10135
+ return {
10136
+ kind: "scope",
10137
+ workspaceId,
10138
+ repositoryId,
10139
+ sourceFiles: [...sourceFiles ?? []],
10140
+ symbolIds: [...symbolIds ?? []],
10141
+ structuralKey
10142
+ };
10143
+ }
10144
+ function compactGraphStatus(row, evidence, unresolvedReason, dynamicMode2) {
10145
+ const effective = recordValue2(evidence.effectiveResolution);
10146
+ const status = stringValue14(effective.status) ?? row.status ?? "unresolved";
10147
+ if (status !== "resolved")
10148
+ return nonResolvedGraphStatuses[status] ?? "unresolved";
10149
+ if (unresolvedReason) return "unresolved";
10150
+ const inference = recordValue2(evidence.dynamicTargetInference);
10151
+ if (isDynamicInference(dynamicMode2, inference)) return "dynamic";
10152
+ return traversableTargetKind(row.to_kind) ? "resolved" : "terminal";
10153
+ }
10154
+ function isDynamicInference(mode, evidence) {
10155
+ return mode === "infer" && evidence.status === "resolved";
10156
+ }
10157
+ function traversableTargetKind(kind) {
10158
+ return ["operation", "symbol", "handler_method"].includes(kind);
10159
+ }
10160
+ function compactResolutionStatus(status, unresolvedReason) {
10161
+ if (status === "ambiguous") return "ambiguous";
10162
+ if (status === "dynamic") return "dynamic";
10163
+ return status === "resolved" && !unresolvedReason ? "resolved" : "unresolved";
10164
+ }
10165
+ function compactEventStatus(status, bodyExpansion) {
10166
+ if (bodyExpansion === "cycle_blocked") return "cycle";
10167
+ if (status === "ambiguous") return "ambiguous";
10168
+ return status === "resolved" ? "inferred" : "unresolved";
10169
+ }
10170
+ function compactDecisionFromEvidence(evidence, overrides = {}) {
10171
+ const effective = recordValue2(evidence.effectiveResolution);
10172
+ const persisted = recordValue2(evidence.persistedResolution);
10173
+ const dynamic = recordValue2(evidence.dynamicTargetExploration);
10174
+ const implementation = recordValue2(evidence.implementationSelection);
10175
+ const missing = stringArray7(dynamic.missingVariables ?? evidence.missingRuntimeVariables);
10176
+ const authoritativeMissingCount = finiteNumber(
10177
+ dynamic.missingVariableCount ?? evidence.missingVariableCount
10178
+ );
10179
+ return {
10180
+ effectiveResolutionStatus: stringValue14(effective.status),
10181
+ effectiveTarget: targetSummary(effective),
10182
+ persistedResolutionStatus: stringValue14(persisted.status),
10183
+ persistedTarget: targetSummary(persisted),
10184
+ missingVariableNames: missing.length > 0 ? missing : void 0,
10185
+ missingVariableCount: authoritativeMissingCount ?? (missing.length > 0 ? missing.length : void 0),
10186
+ dynamicMode: dynamicMode(dynamic.mode),
10187
+ candidateCount: firstNumber(
10188
+ dynamic.candidateCount,
10189
+ implementation.candidateCount,
10190
+ implementation.candidateScoreCount,
10191
+ evidence.candidateCount,
10192
+ evidence.persistedCandidateCount
10193
+ ),
10194
+ viableCandidateCount: finiteNumber(dynamic.viableCandidateCount),
10195
+ rejectedCandidateCount: finiteNumber(dynamic.rejectedCandidateCount),
10196
+ omittedCandidateCount: finiteNumber(dynamic.omittedCandidateCount),
10197
+ implementationStrategy: stringValue14(implementation.strategy),
10198
+ implementationGuided: booleanValue(implementation.guided),
10199
+ implementationContextual: booleanValue(
10200
+ evidence.contextualImplementationSelected
10201
+ ),
10202
+ reasonCode: stringValue14(evidence.reasonCode ?? evidence.cycleReason),
10203
+ eventMatchStrategy: stringValue14(evidence.matchStrategy),
10204
+ dispatchCertainty: stringValue14(evidence.dispatchCertainty),
10205
+ associationStatus: stringValue14(evidence.associationStatus),
10206
+ associationBasis: stringValue14(evidence.associationBasis),
10207
+ eventScope: stringValue14(evidence.dispatchScope),
10208
+ callRole: stringValue14(evidence.callRole),
10209
+ factOrigin: stringValue14(evidence.factOrigin),
10210
+ roleSiteMatchCount: finiteNumber(evidence.roleSiteMatchCount),
10211
+ bodyExpansion: stringValue14(evidence.bodyExpansion),
10212
+ ...overrides
10213
+ };
10214
+ }
10215
+ function compactRefs(values) {
10216
+ return {
10217
+ graphEdgeIds: reference(values.graphEdgeId),
10218
+ outboundCallIds: reference(values.outboundCallId),
10219
+ subscribeCallIds: reference(values.subscribeCallId),
10220
+ symbolCallIds: reference(values.symbolCallId),
10221
+ operationIds: reference(values.operationId),
10222
+ symbolIds: reference(values.symbolId),
10223
+ handlerMethodIds: reference(values.handlerMethodId)
10224
+ };
10225
+ }
10226
+ function compactSite(values) {
10227
+ return {
10228
+ repository: stringValue14(values.repository ?? values.repoName),
10229
+ sourceFile: stringValue14(values.sourceFile ?? values.source_file),
10230
+ sourceLine: finiteNumber(values.sourceLine ?? values.source_line),
10231
+ startOffset: finiteNumber(values.startOffset ?? values.call_site_start_offset),
10232
+ endOffset: finiteNumber(values.endOffset ?? values.call_site_end_offset)
10233
+ };
10234
+ }
10235
+ function observation(ordinal, edge, semantics) {
10236
+ return {
10237
+ ordinal,
10238
+ step: edge.step,
10239
+ type: edge.type,
10240
+ source: semantics.source,
10241
+ target: semantics.target,
10242
+ status: semantics.status,
10243
+ confidence: edge.confidence,
10244
+ decision: semantics.decision,
10245
+ refs: semantics.refs,
10246
+ site: semantics.site
10247
+ };
10248
+ }
10249
+ function targetSummary(value) {
10250
+ const kind = stringValue14(value.targetKind);
10251
+ const id = stringValue14(value.targetId);
10252
+ return kind && id ? { kind, id } : void 0;
10253
+ }
10254
+ function reference(value) {
10255
+ if (typeof value === "string" && value.length > 0) {
10256
+ const numeric2 = Number(value);
10257
+ if (Number.isSafeInteger(numeric2)) return numeric2 > 0 ? [numeric2] : void 0;
10258
+ return [value];
10259
+ }
10260
+ const number = finiteNumber(value);
10261
+ return number === void 0 || number <= 0 ? void 0 : [number];
10262
+ }
10263
+ function dynamicMode(value) {
10264
+ return value === "strict" || value === "candidates" || value === "infer" ? value : void 0;
10265
+ }
10266
+ function recordValue2(value) {
10267
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
10268
+ }
10269
+ function stringArray7(value) {
10270
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
10271
+ }
10272
+ function stringValue14(value) {
10273
+ return typeof value === "string" ? value : void 0;
10274
+ }
10275
+ function finiteNumber(value) {
10276
+ if (typeof value === "number" && Number.isFinite(value)) return value;
10277
+ if (typeof value !== "string" || value.trim() === "") return void 0;
10278
+ const parsed = Number(value);
10279
+ return Number.isFinite(parsed) ? parsed : void 0;
10280
+ }
10281
+ function positiveNumber(value) {
10282
+ const number = finiteNumber(value);
10283
+ return number !== void 0 && number > 0 ? number : void 0;
10284
+ }
10285
+ function booleanValue(value) {
10286
+ return typeof value === "boolean" ? value : void 0;
10287
+ }
10288
+ function firstNumber(...values) {
10289
+ for (const value of values) {
10290
+ const numeric2 = finiteNumber(value);
10291
+ if (numeric2 !== void 0) return numeric2;
10292
+ }
10293
+ return void 0;
10294
+ }
10295
+
10296
+ // src/trace/019-trace-edge-semantics.ts
10297
+ function recordImplementationObservation(recorder, edge, input) {
10298
+ const target = semanticHandler(
10299
+ input.handlerMethodId,
10300
+ input.handlerSymbolId,
10301
+ () => recorder.unavailable("target", "selected_handler")
10302
+ );
10303
+ recorder.record(edge, {
10304
+ source: semanticOperation(
10305
+ input.operationId,
10306
+ () => recorder.unavailable("source", "operation")
10307
+ ),
10308
+ target,
10309
+ status: compactResolutionStatus(input.effectiveStatus, input.unresolvedReason),
10310
+ decision: compactDecisionFromEvidence(input.evidence, {
10311
+ effectiveResolutionStatus: input.unresolvedReason ? "unresolved" : input.effectiveStatus,
10312
+ effectiveTarget: decisionTarget(target),
10313
+ persistedResolutionStatus: input.persistedStatus,
10314
+ persistedTarget: input.persistedTargetKind && input.persistedTargetId ? { kind: input.persistedTargetKind, id: input.persistedTargetId } : void 0,
10315
+ implementationStrategy: input.strategy,
10316
+ implementationGuided: input.guided,
10317
+ implementationContextual: input.contextual,
10318
+ reasonCode: input.unresolvedReason ? "selected_handler_unavailable" : void 0
10319
+ }),
10320
+ refs: compactRefs({
10321
+ graphEdgeId: input.graphEdgeId,
10322
+ operationId: input.operationId,
10323
+ handlerMethodId: input.handlerMethodId,
10324
+ symbolId: input.handlerSymbolId
10325
+ }),
10326
+ site: compactSite(input.site)
10327
+ });
10328
+ }
10329
+ function recordLocalCallObservation(recorder, edge, input) {
10330
+ const source = semanticSymbol(
10331
+ input.symbolCall.caller_symbol_id,
10332
+ () => recorder.unavailable("source", "caller_symbol")
10333
+ );
10334
+ const target = semanticSymbol(
10335
+ input.symbolCall.callee_symbol_id,
10336
+ () => recorder.unavailable("target", "callee_symbol")
10337
+ );
10338
+ recorder.record(edge, {
10339
+ source,
10340
+ target,
10341
+ status: compactResolutionStatus(
10342
+ input.symbolCall.status,
10343
+ input.unresolvedReason
10344
+ ),
10345
+ decision: compactDecisionFromEvidence(input.evidence, {
10346
+ effectiveResolutionStatus: String(input.symbolCall.status),
10347
+ reasonCode: input.unresolvedReason ? "symbol_call_unresolved" : void 0
10348
+ }),
10349
+ refs: {
10350
+ symbolCallIds: idArray(input.symbolCall.id),
10351
+ symbolIds: idArray(
10352
+ input.symbolCall.caller_symbol_id,
10353
+ input.symbolCall.callee_symbol_id
10354
+ )
10355
+ },
10356
+ site: compactSite(input.symbolCall)
10357
+ });
10358
+ return target;
10359
+ }
10360
+ function recordCycleObservation(recorder, edge, source, scope, refs, site) {
10361
+ recorder.record(edge, {
10362
+ source,
10363
+ target: scope.workspaceId === void 0 ? recorder.unavailable("target", "cycle_scope") : semanticScopeTarget(
10364
+ scope.workspaceId,
10365
+ scope.repositoryId,
10366
+ scope.sourceFiles,
10367
+ scope.symbolIds,
10368
+ scope.structuralKey
10369
+ ),
10370
+ status: "cycle",
10371
+ decision: compactDecisionFromEvidence(edge.evidence, {
10372
+ reasonCode: "structural_ancestry_cycle"
10373
+ }),
10374
+ refs: compactRefs(refs),
10375
+ site: compactSite(site)
10376
+ });
10377
+ }
10378
+ function recordOutboundObservation(recorder, edge, input) {
10379
+ const source = semanticCallSource(input.call, input.workspaceId);
10380
+ const target = semanticGraphTarget(
10381
+ input.row,
10382
+ input.call,
10383
+ input.workspaceId,
10384
+ () => recorder.unavailable("target", input.row.to_kind)
10385
+ );
10386
+ recorder.record(edge, {
10387
+ source,
10388
+ target,
10389
+ status: compactGraphStatus(
10390
+ input.row,
10391
+ input.evidence,
10392
+ input.unresolvedReason,
10393
+ input.dynamicMode
10394
+ ),
10395
+ decision: compactDecisionFromEvidence(input.evidence, {
10396
+ reasonCode: input.unresolvedReason ? safeReasonCode(input.evidence.reasonCode, "outbound_target_unresolved") : void 0
10397
+ }),
10398
+ refs: compactRefs({
10399
+ graphEdgeId: positiveId(input.evidence.persistedGraphEdgeId),
10400
+ outboundCallId: input.call.id,
10401
+ operationId: input.row.to_kind === "operation" ? input.row.to_id : void 0,
10402
+ symbolId: input.call.source_symbol_id
10403
+ }),
10404
+ site: compactSite(input.call)
10405
+ });
10406
+ return { source, target };
10407
+ }
10408
+ function recordEventBridgeObservation(recorder, edge, plan, workspaceId, subscriptionCount) {
10409
+ const handler = plan.transition.handler;
10410
+ const target = handler ? { kind: "symbol", symbolId: handler.symbolId } : {
10411
+ kind: "target",
10412
+ workspaceId,
10413
+ repositoryId: plan.transition.subscriptionRepoId,
10414
+ targetKind: plan.transition.targetKind,
10415
+ targetId: plan.transition.targetId
10416
+ };
10417
+ recorder.record(edge, {
10418
+ source: {
10419
+ kind: "event",
10420
+ workspaceId,
10421
+ eventName: plan.transition.eventName
10422
+ },
10423
+ target,
10424
+ status: compactEventStatus(plan.transition.status, plan.bodyExpansion),
10425
+ decision: compactDecisionFromEvidence(plan.evidence, {
10426
+ eventSubscriptionCount: subscriptionCount,
10427
+ reasonCode: plan.transition.reasonCode
10428
+ }),
10429
+ refs: eventReferences(plan),
10430
+ site: eventSite(plan)
10431
+ });
10432
+ return target;
10433
+ }
10434
+ function recordEventCycleObservation(recorder, edge, plan, source, workspaceId) {
10435
+ const handler = plan.transition.handler;
10436
+ if (!plan.state) return;
10437
+ recorder.record(edge, {
10438
+ source,
10439
+ target: semanticScopeTarget(
10440
+ workspaceId,
10441
+ handler?.repoId,
10442
+ handler ? /* @__PURE__ */ new Set([handler.sourceFile]) : void 0,
10443
+ handler ? /* @__PURE__ */ new Set([handler.symbolId]) : void 0,
10444
+ plan.state.structuralKey
10445
+ ),
10446
+ status: "cycle",
10447
+ decision: compactDecisionFromEvidence(edge.evidence, {
10448
+ reasonCode: "structural_ancestry_cycle",
10449
+ dispatchCertainty: "static_name_only"
10450
+ }),
10451
+ refs: eventReferences(plan),
10452
+ site: eventSite(plan)
10453
+ });
10454
+ }
10455
+ function recordDynamicBranchObservation(recorder, branch, call, source, evidence, workspaceId) {
10456
+ const target = dynamicBranchTarget(recorder, branch, workspaceId);
10457
+ recorder.record(branch.edge, {
10458
+ source,
10459
+ target,
10460
+ status: "dynamic",
10461
+ decision: compactDecisionFromEvidence(evidence, {
10462
+ dynamicMode: "candidates",
10463
+ effectiveTarget: branch.operationId === void 0 ? void 0 : { kind: "operation", id: String(branch.operationId) },
10464
+ remediationCode: "provide_runtime_variables"
10465
+ }),
10466
+ refs: compactRefs({
10467
+ graphEdgeId: positiveId(evidence.persistedGraphEdgeId),
10468
+ outboundCallId: call.id,
10469
+ operationId: branch.operationId,
10470
+ symbolId: call.source_symbol_id
10471
+ }),
10472
+ site: compactSite(call)
10473
+ });
10474
+ }
10475
+ function dynamicBranchTarget(recorder, branch, workspaceId) {
10476
+ if (branch.operationId !== void 0)
10477
+ return { kind: "operation", operationId: branch.operationId };
10478
+ if (branch.servicePath && branch.operationPath) return {
10479
+ kind: "target",
10480
+ workspaceId,
10481
+ repositoryId: branch.repositoryId,
10482
+ targetKind: "dynamic_operation_candidate",
10483
+ targetId: JSON.stringify([branch.servicePath, branch.operationPath])
10484
+ };
10485
+ return recorder.unavailable("target", "dynamic_operation_candidate");
10486
+ }
10487
+ function eventReferences(plan) {
10488
+ return compactRefs({
10489
+ graphEdgeId: plan.transition.graphEdgeId,
10490
+ subscribeCallId: plan.transition.subscribeCallId,
10491
+ symbolCallId: plan.transition.symbolCallId,
10492
+ symbolId: plan.transition.handler?.symbolId
10493
+ });
10494
+ }
10495
+ function eventSite(plan) {
10496
+ return compactSite({
10497
+ repository: plan.transition.subscriptionRepoName,
10498
+ sourceFile: plan.transition.sourceFile,
10499
+ sourceLine: plan.transition.sourceLine,
10500
+ startOffset: plan.transition.callSiteStartOffset,
10501
+ endOffset: plan.transition.callSiteEndOffset
10502
+ });
10503
+ }
10504
+ function safeReasonCode(value, fallback) {
10505
+ return typeof value === "string" && /^[a-z][a-z0-9_.-]{0,79}$/.test(value) ? value : fallback;
10506
+ }
10507
+ function positiveId(value) {
10508
+ if (typeof value === "number") return Number.isFinite(value) && value > 0 ? value : void 0;
10509
+ if (typeof value !== "string" || value.length === 0) return void 0;
10510
+ const numeric2 = Number(value);
10511
+ if (Number.isSafeInteger(numeric2)) return numeric2 > 0 ? numeric2 : void 0;
10512
+ return value;
10513
+ }
10514
+ function idArray(...values) {
10515
+ const ids = values.flatMap((value) => {
10516
+ const id = positiveId(value);
10517
+ return id === void 0 ? [] : [id];
10518
+ });
10519
+ return ids.length > 0 ? ids : void 0;
10520
+ }
10521
+ function decisionTarget(endpoint) {
10522
+ if (endpoint.kind === "symbol")
10523
+ return { kind: "symbol", id: String(endpoint.symbolId) };
10524
+ if (endpoint.kind === "handler_method")
10525
+ return { kind: "handler_method", id: String(endpoint.handlerMethodId) };
10526
+ return void 0;
10527
+ }
10528
+
10529
+ // src/trace/trace-engine.ts
10530
+ var compactObserverKey = /* @__PURE__ */ Symbol("service-flow.compact-trace-observer");
10531
+ function compactObserver(options) {
10532
+ const observed = options;
10533
+ return observed[compactObserverKey];
10534
+ }
10535
+ function normalizeOperation2(value) {
10536
+ if (!value) return void 0;
10537
+ return value.startsWith("/") ? value.slice(1) : value;
10538
+ }
10539
+ function positiveDepth(value) {
10540
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
10541
+ }
10542
+ function operationStartScope(db, repoId, start, hintOptions, workspaceId) {
10543
+ const requested = normalizeOperation2(start.operationPath ?? start.operation);
10544
+ if (!requested) return void 0;
10545
+ 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
10546
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
10547
+ WHERE (? IS NULL OR r.workspace_id=?) AND (? IS NULL OR r.id=?)
10548
+ AND (? IS NULL OR s.service_path=?) AND (o.operation_name=? OR o.operation_path=? OR o.operation_path=?)
10549
+ 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}`);
10550
+ if (rows2.length === 0) return void 0;
10551
+ const repoCount = new Set(rows2.map((row) => String(row.repoName))).size;
10552
+ const serviceCount = new Set(rows2.map((row) => `${String(row.repoName)}:${String(row.servicePath)}`)).size;
10553
+ if (!repoId && repoCount > 1)
10554
+ return { diagnostics: [ambiguousStartDiagnostic(requested, rows2, "Operation trace start matched multiple repositories; add --repo to disambiguate")] };
10555
+ if (!start.servicePath && serviceCount > 1)
10556
+ return { diagnostics: [ambiguousStartDiagnostic(requested, rows2, "Operation trace start matched multiple services; add --service to disambiguate")] };
10557
+ if (rows2.length !== 1)
10558
+ return { diagnostics: [ambiguousStartDiagnostic(requested, rows2, "Operation trace start matched multiple indexed operations")] };
10559
+ const operationId = String(rows2[0]?.operationId);
10560
+ const impl = implementationScope(db, operationId);
10561
+ if (impl.edge?.status === "resolved" && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? /* @__PURE__ */ new Set([impl.symbolId]) : void 0, repoId: impl.repoId, operationId, diagnostics: [] };
10562
+ const hinted = hintedImplementationSelection(
10563
+ db,
10564
+ impl.edge,
10565
+ operationId,
10566
+ hintOptions
10567
+ );
10568
+ if (hinted.methodId) {
10569
+ const hintedScope = handlerScope(db, hinted.methodId);
10570
+ if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? /* @__PURE__ */ new Set([hintedScope.symbolId]) : void 0, repoId: hintedScope.repoId, operationId, diagnostics: [] };
10571
+ }
10572
+ if (impl.edge) {
10573
+ const evidence = parseTraceEvidence(impl.edge.evidence_json);
10574
+ const hintDiagnostic = implementationHintDiagnostic(hinted, evidence);
10575
+ const diagnostics = [implementationStartDiagnostic(impl.edge, evidence)];
10576
+ return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
10577
+ }
10578
+ return { operationId, diagnostics: [{ severity: "warning", code: "trace_start_implementation_unresolved", message: "Indexed operation matched but no implementation candidate exists", resolutionStage: "implementation", resolutionStatus: "operation_without_implementation" }] };
10579
+ }
10580
+ function sourceFilesForStart(db, repoId, start, workspaceId) {
10581
+ return sourceScopeForSelector(db, repoId, start, workspaceId);
10582
+ }
10583
+ function startScope(db, start, hintOptions, workspaceId) {
10584
+ const repos = start.repo ? reposByName(db, start.repo, workspaceId).map((row) => ({
10585
+ id: row.id,
10586
+ name: row.name,
10587
+ packageName: row.package_name ?? void 0
10588
+ })) : [];
10589
+ if (start.repo && repos.length === 0) return {
10590
+ selectorMatched: false,
10591
+ startDiagnostics: [selectorRepoNotFoundDiagnostic(start.repo)]
10592
+ };
10593
+ if (start.repo && repos.length > 1) return {
10594
+ selectorMatched: false,
10595
+ startDiagnostics: [selectorRepoAmbiguousDiagnostic(start.repo, repos)]
10596
+ };
10597
+ const repo = repos[0];
10598
+ const operationScope = operationStartScope(
10599
+ db,
10600
+ repo?.id,
10601
+ start,
10602
+ hintOptions,
10603
+ workspaceId
10604
+ );
10605
+ const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === "operation" || d.resolutionStage === "implementation");
10606
+ const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start, workspaceId);
10607
+ const terminalSelectorScope = Boolean(sourceScope?.diagnostics?.length && !sourceScope.files);
10608
+ const sourceFiles = sourceScope?.files;
10609
+ const hasSelector = Boolean(
10610
+ start.handler ?? start.operation ?? start.operationPath ?? start.servicePath
10611
+ );
10612
+ if (start.servicePath && !start.operation && !start.operationPath && !start.handler)
10613
+ return { repo, selectorMatched: false };
10614
+ return {
10615
+ repo,
10616
+ executionRepoId: sourceScope?.repoId ?? repo?.id,
10617
+ sourceFiles,
10618
+ symbolIds: sourceScope?.symbols,
10619
+ selectorMatched: !terminalOperationScope && !terminalSelectorScope && (!hasSelector || sourceFiles !== void 0),
10620
+ startOperationId: operationScope?.operationId,
10621
+ startDiagnostics: operationScope?.diagnostics?.length ? operationScope.diagnostics : sourceScope?.diagnostics
10622
+ };
10623
+ }
10624
+ function handlerFilesForOperation(db, operationId) {
10625
+ const op = db.prepare(
10626
+ `SELECT o.operation_name operationName,o.operation_path operationPath,s.repo_id repoId
10627
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?`
10628
+ ).get(operationId);
10629
+ if (!op) return /* @__PURE__ */ new Set();
10630
+ const operation = normalizeOperation2(op.operationPath ?? op.operationName);
10631
+ const rows2 = db.prepare(
10632
+ `SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId FROM handler_classes hc
10633
+ JOIN handler_methods hm ON hm.handler_class_id=hc.id
10634
+ LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
10635
+ AND sym.source_file=hc.source_file
10636
+ AND sym.qualified_name=hc.class_name || '.' || hm.method_name
10637
+ AND sym.start_line=hm.source_line
10638
+ WHERE hc.repo_id=?
10639
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
10640
+ CASE WHEN hm.decorator_kind='Event' THEN 'event'
10641
+ WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
10642
+ ELSE 'unsupported' END)='operation'
10643
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
10644
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On')
10645
+ THEN 1 ELSE 0 END)=1
10646
+ AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`
10647
+ ).all(op.repoId, operation, operation, op.operationName);
10648
+ return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
10649
+ }
10650
+ function implementationEdge(db, operationId) {
10651
+ return db.prepare("SELECT * FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND from_kind='operation' AND from_id=? ORDER BY CASE status WHEN 'resolved' THEN 0 WHEN 'ambiguous' THEN 1 ELSE 2 END,id LIMIT 1").get(operationId);
10652
+ }
10653
+ function implementationScope(db, operationId) {
10654
+ const edge = implementationEdge(db, operationId);
10655
+ if (!edge || edge.status !== "resolved") return { files: /* @__PURE__ */ new Set(), edge };
10656
+ 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);
10657
+ if (!row || typeof row.symbolId !== "number")
10658
+ return { repoId: row?.repoId, files: /* @__PURE__ */ new Set(), edge };
10659
+ return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
10660
+ }
10661
+ function handlerScope(db, methodId) {
10662
+ 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);
10663
+ if (!row || typeof row.symbolId !== "number") return void 0;
10664
+ return { repoId: row.repoId, files: new Set(row.sourceFile ? [row.sourceFile] : []), symbolId: row.symbolId };
8718
10665
  }
8719
- function contextForSymbolCall(db, symbolCall, callerBindings) {
8720
- const next = /* @__PURE__ */ new Map();
8721
- if (callerBindings.size === 0) return next;
8722
- const callEvidence = parseEvidence(symbolCall.evidence_json);
8723
- const callee = db.prepare("SELECT evidence_json evidenceJson,source_file sourceFile,start_line startLine FROM symbols WHERE id=?").get(symbolCall.callee_symbol_id);
8724
- const calleeEvidence = parseEvidence(callee?.evidenceJson);
8725
- const params = Array.isArray(calleeEvidence.parameters) ? calleeEvidence.parameters.filter((item) => typeof item === "string") : [];
8726
- const parameterBindings = Array.isArray(calleeEvidence.parameterBindings) ? calleeEvidence.parameterBindings.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
8727
- const parameterPropertyAliases = Array.isArray(calleeEvidence.parameterPropertyAliases) ? calleeEvidence.parameterPropertyAliases.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
8728
- const args = Array.isArray(callEvidence.callArguments) ? callEvidence.callArguments : [];
8729
- const provenance = {
8730
- callerSite: { sourceFile: String(symbolCall.source_file ?? ""), sourceLine: Number(symbolCall.source_line ?? 0) },
8731
- calleeSite: { sourceFile: callee?.sourceFile, sourceLine: callee?.startLine }
8732
- };
8733
- args.forEach((arg, index) => {
8734
- const paramBinding = parameterBindings.find((binding) => binding.index === index);
8735
- const param = paramBinding?.kind === "identifier" && typeof paramBinding.name === "string" ? paramBinding.name : params[index];
8736
- if (arg.kind === "identifier" && typeof arg.name === "string") {
8737
- const binding = callerBindings.get(arg.name);
8738
- if (binding && param) next.set(param, { ...binding, ...provenance, source: "local_symbol_argument", callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
8739
- }
8740
- if (arg.kind === "object_literal" && Array.isArray(arg.properties)) {
8741
- for (const prop of arg.properties) {
8742
- if (typeof prop.property !== "string" || typeof prop.argument !== "string") continue;
8743
- const binding = callerBindings.get(prop.argument);
8744
- if (!binding) continue;
8745
- const destructured = paramBinding?.kind === "object_pattern" && Array.isArray(paramBinding.properties) ? paramBinding.properties.find((item) => item.property === prop.property && typeof item.local === "string") : void 0;
8746
- if (destructured && typeof destructured.local === "string") next.set(destructured.local, { ...binding, ...provenance, source: "local_symbol_destructured_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: String(index), calleeReceiver: destructured.local });
8747
- else if (param) {
8748
- next.set(`${param}.${prop.property}`, { ...binding, ...provenance, source: "local_symbol_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
8749
- for (const alias of parameterPropertyAliases) {
8750
- if (alias.parameter === param && alias.property === prop.property && typeof alias.local === "string") next.set(alias.local, { ...binding, ...provenance, source: "local_symbol_object_parameter_destructure", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeObjectProperty: `${param}.${prop.property}`, calleeReceiver: alias.local, calleeLocalDestructuredIdentifier: alias.local, parameterPropertyAliasKind: alias.kind, parameterPropertyAliasLine: alias.line });
8751
- }
8752
- }
8753
- }
8754
- }
8755
- if (arg.kind === "array_literal" && Array.isArray(arg.elements) && paramBinding?.kind === "array_pattern" && Array.isArray(paramBinding.elements)) {
8756
- for (const element of arg.elements) {
8757
- const target = paramBinding.elements.find((item) => item.index === element.index);
8758
- if (element.kind !== "identifier" || typeof element.name !== "string" || typeof target?.local !== "string") continue;
8759
- const binding = callerBindings.get(element.name);
8760
- if (binding) next.set(target.local, { ...binding, ...provenance, source: "local_symbol_destructured_array_argument", callerArgument: element.name, calleeParameter: String(index), calleeReceiver: target.local });
8761
- }
8762
- }
8763
- });
8764
- return next;
10666
+ function traceEdgeType(call, row) {
10667
+ if (row.to_kind === "operation" && row.edge_type === "REMOTE_CALL_RESOLVES_TO_OPERATION") return "remote_action";
10668
+ if (row.to_kind === "operation" && row.edge_type === "LOCAL_CALL_RESOLVES_TO_OPERATION") return "local_service_call";
10669
+ return String(call.call_type);
10670
+ }
10671
+ function includeCall(type, options) {
10672
+ if (!options.includeDb && type === "local_db_query") return false;
10673
+ if (!options.includeExternal && type === "external_http") return false;
10674
+ if (!options.includeAsync && type.startsWith("async_")) return false;
10675
+ return true;
8765
10676
  }
8766
10677
  function trace(db, start, options) {
10678
+ const observer = compactObserver(options);
10679
+ const schemaLifecycle = schemaLifecycleDiagnostic(db);
10680
+ if (schemaLifecycle)
10681
+ return { start, nodes: [], edges: [], diagnostics: [schemaLifecycle] };
8767
10682
  const hintOptions = { implementationRepo: options.implementationRepo, implementationHints: options.implementationHints };
8768
10683
  const scope = startScope(db, start, hintOptions, options.workspaceId);
8769
10684
  const hasSelector = Boolean(start.repo || start.handler || start.operation || start.operationPath || start.servicePath);
8770
10685
  const diagnosticRepoId = scope.executionRepoId ?? scope.repo?.id;
10686
+ const scheduler = new TraversalScopeScheduler();
10687
+ const roots = createTraceRootPlan(db, scheduler, {
10688
+ repoId: diagnosticRepoId,
10689
+ files: scope.sourceFiles,
10690
+ symbolIds: scope.symbolIds,
10691
+ selectorMatched: scope.selectorMatched
10692
+ }, options.workspaceId, Boolean(options.includeAsync));
10693
+ observer?.setWorkspaceId?.(roots.workspaceId);
10694
+ if (roots.diagnostic)
10695
+ return { start, nodes: [], edges: [], diagnostics: [roots.diagnostic] };
10696
+ const { workspaceId, queue, pendingRoots } = roots;
8771
10697
  const diagnostics = loadTraceDiagnostics(
8772
10698
  db,
8773
10699
  diagnosticRepoId,
8774
10700
  !hasSelector,
8775
- options.workspaceId
10701
+ workspaceId
8776
10702
  );
8777
- 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) : [];
10703
+ 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, workspaceId, workspaceId) : [];
8778
10704
  for (const row of stale)
8779
10705
  prependTraceDiagnostic(diagnostics, { severity: "warning", code: "graph_stale", message: `Graph is stale for ${row.name ?? "repository"}: ${row.reason ?? "facts_changed"}. Run service-flow link.` });
8780
10706
  for (const diagnostic of scope.startDiagnostics ?? [])
@@ -8783,9 +10709,8 @@ function trace(db, start, options) {
8783
10709
  prependTraceDiagnostic(diagnostics, selectorNotFoundDiagnostic(start));
8784
10710
  const maxDepth = positiveDepth(options.depth);
8785
10711
  const edges = [];
10712
+ const recorder = new TraceEdgeRecorder(edges, observer);
8786
10713
  const nodes = /* @__PURE__ */ new Map();
8787
- const seenEdges = /* @__PURE__ */ new Set();
8788
- const queue = scope.selectorMatched ? [{ repoId: scope.executionRepoId, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1, context: /* @__PURE__ */ new Map() }] : [];
8789
10714
  if (scope.startOperationId && scope.selectorMatched) {
8790
10715
  const op = operationNode(db, scope.startOperationId);
8791
10716
  const impl = implementationScope(db, scope.startOperationId);
@@ -8799,7 +10724,7 @@ function trace(db, start, options) {
8799
10724
  if (impl.edge && (impl.edge.status === "resolved" || startSelection.methodId)) {
8800
10725
  const selectedMethodId = impl.edge.status === "resolved" ? impl.edge.to_id : startSelection.methodId;
8801
10726
  const implEvidence = {
8802
- ...parseEvidence(impl.edge.evidence_json),
10727
+ ...parseTraceEvidence(impl.edge.evidence_json),
8803
10728
  startResolution: {
8804
10729
  strategy: "indexed_operation_graph",
8805
10730
  matchedOperationId: scope.startOperationId,
@@ -8816,39 +10741,109 @@ function trace(db, start, options) {
8816
10741
  ) : { evidence: implEvidence };
8817
10742
  if (selected.diagnostic) prependTraceDiagnostic(diagnostics, selected.diagnostic);
8818
10743
  if (selected.handler) nodes.set(String(selected.handler.id), selected.handler);
8819
- seenEdges.add(Number(impl.edge.id));
8820
- edges.push({ step: 1, type: "operation_implemented_by_handler", from: op?.label ? String(op.label) : `operation:${scope.startOperationId}`, to: selected.handler?.label ? String(selected.handler.label) : `${impl.edge.to_kind}:${impl.edge.to_id}`, evidence: selected.evidence, confidence: Number(impl.edge.confidence ?? 0), unresolvedReason: selected.unresolvedReason ?? (impl.edge.status === "resolved" || startSelection.methodId ? void 0 : String(impl.edge.unresolved_reason ?? impl.edge.status)) });
10744
+ const unresolvedReason = selected.unresolvedReason ?? (impl.edge.status === "resolved" || startSelection.methodId ? void 0 : String(impl.edge.unresolved_reason ?? impl.edge.status));
10745
+ const selectedScope = selectedMethodId ? handlerScope(db, selectedMethodId) : void 0;
10746
+ const edge = { step: 1, type: "operation_implemented_by_handler", from: op?.label ? String(op.label) : `operation:${scope.startOperationId}`, to: selected.handler?.label ? String(selected.handler.label) : `${impl.edge.to_kind}:${impl.edge.to_id}`, evidence: selected.evidence, confidence: Number(impl.edge.confidence ?? 0), unresolvedReason };
10747
+ recordImplementationObservation(recorder, edge, {
10748
+ operationId: scope.startOperationId,
10749
+ handlerMethodId: selectedMethodId,
10750
+ handlerSymbolId: selectedScope?.symbolId,
10751
+ graphEdgeId: impl.edge.id,
10752
+ persistedStatus: impl.edge.status,
10753
+ persistedTargetKind: impl.edge.to_kind,
10754
+ persistedTargetId: impl.edge.to_id,
10755
+ effectiveStatus: startSelection.methodId ? "resolved" : String(impl.edge.status ?? "unresolved"),
10756
+ strategy: String(startSelection.evidence.strategy ?? "indexed_operation_graph"),
10757
+ guided: startSelection.evidence.guided === true,
10758
+ unresolvedReason,
10759
+ evidence: selected.evidence,
10760
+ site: op ?? {}
10761
+ });
8821
10762
  }
8822
10763
  }
8823
- const seenScopes = /* @__PURE__ */ new Set();
8824
- while (queue.length > 0) {
10764
+ while (queue.length > 0 || pendingRoots.length > 0) {
10765
+ if ((queue[0]?.depth ?? Number.POSITIVE_INFINITY) > 1 && workspaceId !== void 0) {
10766
+ const root = nextPendingRoot(pendingRoots, scheduler, workspaceId);
10767
+ if (root) queue.unshift(root);
10768
+ }
8825
10769
  const current = queue.shift();
8826
10770
  if (!current || current.depth > maxDepth) continue;
8827
- const contextKey = [...(current.context ?? /* @__PURE__ */ new Map()).keys()].sort().join(",");
8828
- const key = `${current.repoId ?? "*"}:${[...current.symbolIds ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${[...current.files ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${contextKey}`;
8829
- if (seenScopes.has(key)) continue;
8830
- seenScopes.add(key);
10771
+ if (!scheduler.markExpanded(current.state)) continue;
8831
10772
  const calls = db.prepare(
8832
- `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`
8833
- ).all(current.repoId, current.repoId, options.workspaceId, options.workspaceId);
10773
+ `SELECT c.*,r.name repoName,r.workspace_id workspaceId,
10774
+ r.graph_generation graphGeneration
10775
+ FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
10776
+ WHERE (? IS NULL OR c.repo_id=?) AND (? IS NULL OR r.workspace_id=?)
10777
+ ORDER BY c.source_file COLLATE BINARY,c.call_site_start_offset,
10778
+ c.call_site_end_offset,c.source_line,c.id`
10779
+ ).all(current.repoId, current.repoId, workspaceId, workspaceId);
8834
10780
  const filtered = calls.filter(
8835
- (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)
10781
+ (c) => (current.unownedOnly ? c.source_symbol_id == null : !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)
8836
10782
  );
8837
- const callerBindings = new Map([...current.context ?? /* @__PURE__ */ new Map(), ...knownBindingsForScope(db, current.repoId, current.symbolIds, current.files), ...knownBindingsForCalls(db, filtered)]);
8838
- if (current.symbolIds && current.symbolIds.size > 0 && current.depth < maxDepth) {
8839
- const symbolRows = db.prepare(`SELECT sc.*,s.repo_id calleeRepoId,s.source_file calleeFile FROM symbol_calls sc LEFT JOIN symbols s ON s.id=sc.callee_symbol_id WHERE sc.caller_symbol_id IN (${[...current.symbolIds].map(() => "?").join(",")}) ORDER BY sc.source_file,sc.source_line`).all(...current.symbolIds);
10783
+ const callerBindings = new Map([...current.context, ...knownBindingsForScope(db, current.repoId, current.symbolIds, current.files), ...knownBindingsForCalls(db, filtered)]);
10784
+ if (!current.rootObservationOnly && current.symbolIds && current.symbolIds.size > 0 && current.depth < maxDepth) {
10785
+ const symbolRows = db.prepare(`SELECT sc.*,s.repo_id calleeRepoId,
10786
+ s.source_file calleeFile FROM symbol_calls sc
10787
+ LEFT JOIN symbols s ON s.id=sc.callee_symbol_id
10788
+ WHERE sc.call_role='ordinary_call'
10789
+ AND sc.caller_symbol_id IN (${[...current.symbolIds].map(() => "?").join(",")})
10790
+ ORDER BY sc.source_file COLLATE BINARY,sc.call_site_start_offset,
10791
+ sc.call_site_end_offset,sc.source_line,sc.id`).all(
10792
+ ...current.symbolIds
10793
+ );
8840
10794
  for (const symbolCall of symbolRows) {
8841
10795
  if (!symbolCall.callee_symbol_id) continue;
8842
10796
  const nextSymbols = /* @__PURE__ */ new Set([Number(symbolCall.callee_symbol_id)]);
8843
10797
  const nextFiles = /* @__PURE__ */ new Set([String(symbolCall.calleeFile)]);
8844
10798
  const nextRepoId = Number(symbolCall.calleeRepoId);
8845
- const nextKey = `${nextRepoId}:${[...nextSymbols].join(",")}:${[...nextFiles].join(",")}`;
10799
+ const nextContext = contextForSymbolCall(db, symbolCall, callerBindings);
10800
+ const scheduling = scheduler.schedule({
10801
+ workspaceId,
10802
+ repoId: nextRepoId,
10803
+ files: nextFiles,
10804
+ symbolIds: nextSymbols,
10805
+ context: nextContext
10806
+ }, current.state);
8846
10807
  const calleeNode = symbolNode(db, Number(symbolCall.callee_symbol_id));
8847
10808
  if (calleeNode) nodes.set(String(calleeNode.id), calleeNode);
8848
- const evidence = { ...parseEvidence(symbolCall.evidence_json), sourceFile: symbolCall.source_file, sourceLine: symbolCall.source_line, calleeSymbolId: symbolCall.callee_symbol_id, calleeSymbolName: calleeNode?.symbolName, calleeSymbolFile: calleeNode?.sourceFile, resolutionStatus: symbolCall.status };
8849
- edges.push({ step: current.depth, type: "local_symbol_call", from: String(symbolCall.callee_expression), to: calleeNode?.label ? String(calleeNode.label) : `symbol:${String(symbolCall.callee_symbol_id)}`, evidence, confidence: Number(symbolCall.confidence ?? 0.8), unresolvedReason: String(symbolCall.status) === "resolved" ? void 0 : symbolCall.unresolved_reason ? String(symbolCall.unresolved_reason) : void 0 });
8850
- if (seenScopes.has(nextKey)) edges.push({ step: current.depth, type: "cycle", from: String(symbolCall.callee_expression), to: nextKey, evidence: { cycle: true, symbolCallId: symbolCall.id }, confidence: 1, unresolvedReason: "Cycle detected; downstream symbol already visited" });
8851
- else queue.push({ repoId: nextRepoId, files: nextFiles, symbolIds: nextSymbols, depth: current.depth + 1, context: contextForSymbolCall(db, symbolCall, callerBindings) });
10809
+ const evidence = { ...parseTraceEvidence(symbolCall.evidence_json), sourceFile: symbolCall.source_file, sourceLine: symbolCall.source_line, calleeSymbolId: symbolCall.callee_symbol_id, calleeSymbolName: calleeNode?.symbolName, calleeSymbolFile: calleeNode?.sourceFile, resolutionStatus: symbolCall.status };
10810
+ const unresolvedReason = String(symbolCall.status) === "resolved" ? void 0 : symbolCall.unresolved_reason ? String(symbolCall.unresolved_reason) : void 0;
10811
+ const edge = { step: current.depth, type: "local_symbol_call", from: String(symbolCall.callee_expression), to: calleeNode?.label ? String(calleeNode.label) : `symbol:${String(symbolCall.callee_symbol_id)}`, evidence, confidence: Number(symbolCall.confidence ?? 0.8), unresolvedReason };
10812
+ const target = recordLocalCallObservation(recorder, edge, {
10813
+ symbolCall,
10814
+ evidence,
10815
+ unresolvedReason
10816
+ });
10817
+ if (scheduling.kind === "cycle") {
10818
+ const cycleEvidence = {
10819
+ cycle: true,
10820
+ cycleReason: "structural_ancestry_cycle",
10821
+ symbolCallId: symbolCall.id
10822
+ };
10823
+ const cycleEdge = { step: current.depth, type: "cycle", from: String(symbolCall.callee_expression), to: scheduling.state.structuralKey, evidence: cycleEvidence, confidence: 1, unresolvedReason: "Cycle detected in structural ancestry; downstream symbol was not expanded" };
10824
+ recordCycleObservation(recorder, cycleEdge, target, {
10825
+ workspaceId,
10826
+ repositoryId: nextRepoId,
10827
+ sourceFiles: nextFiles,
10828
+ symbolIds: nextSymbols,
10829
+ structuralKey: scheduling.state.structuralKey
10830
+ }, {
10831
+ symbolCallId: symbolCall.id,
10832
+ symbolId: symbolCall.callee_symbol_id
10833
+ }, symbolCall);
10834
+ }
10835
+ if (scheduling.kind === "scheduled") enqueueCausalScope(
10836
+ queue,
10837
+ pendingRoots,
10838
+ {
10839
+ repoId: nextRepoId,
10840
+ files: nextFiles,
10841
+ symbolIds: nextSymbols,
10842
+ depth: current.depth + 1,
10843
+ context: nextContext,
10844
+ state: scheduling.state
10845
+ }
10846
+ );
8852
10847
  }
8853
10848
  }
8854
10849
  const graph = graphForCalls(
@@ -8866,29 +10861,27 @@ function trace(db, start, options) {
8866
10861
  callType: call.call_type
8867
10862
  });
8868
10863
  const persistedRowsForCall = graph.get(Number(call.id)) ?? [];
8869
- const contextual = contextualRuntimeResolution(db, call, callerBindings.get(receiverFromEvidence(call.evidence_json) ?? ""), workspaceIdForCall(db, String(call.id)), persistedRowsForCall);
10864
+ const contextual = contextualRuntimeResolution(db, call, callerBindings.get(receiverFromTraceEvidence(call.evidence_json) ?? ""), call.workspaceId, persistedRowsForCall);
8870
10865
  const graphRows = contextual.row ? [contextual.row] : persistedRowsForCall;
8871
10866
  for (const row of graphRows) {
8872
- if (seenEdges.has(Number(row.id))) continue;
8873
- seenEdges.add(Number(row.id));
8874
- const persistedEvidence = parseEvidence(row.evidence_json);
10867
+ const persistedEvidence = parseTraceEvidence(row.evidence_json);
8875
10868
  const rawEvidence = baseTraceEvidence(row, call, persistedEvidence, contextual.evidence);
8876
10869
  const effective = runtimeResolution(db, row, rawEvidence, {
8877
10870
  vars: options.vars,
8878
10871
  dynamicMode: options.dynamicMode ?? "strict",
8879
10872
  maxDynamicCandidates: options.maxDynamicCandidates
8880
- }, workspaceIdForCall(db, String(call.id)), contextual.state);
10873
+ }, call.workspaceId, contextual.state);
8881
10874
  const evidence = effective.evidence;
8882
10875
  const effectiveRow = effective.row;
8883
- const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
10876
+ const targetNode2 = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
8884
10877
  const opNode = effectiveRow.to_kind === "operation" ? operationNode(db, effectiveRow.to_id) : void 0;
8885
- nodes.set(targetNode, opNode ?? {
8886
- id: targetNode,
10878
+ nodes.set(targetNode2, opNode ?? {
10879
+ id: targetNode2,
8887
10880
  kind: effectiveRow.to_kind,
8888
10881
  label: effectiveRow.to_kind === "db_entity" ? `Entity: ${effectiveRow.to_id || "unknown"}` : effectiveRow.to_id
8889
10882
  });
8890
10883
  const to = edgeTarget(effectiveRow, evidence);
8891
- edges.push({
10884
+ const edge = {
8892
10885
  step: current.depth,
8893
10886
  type: traceEdgeType(call, effectiveRow),
8894
10887
  from: `${call.repoName}:${call.source_file}:${call.source_line}`,
@@ -8896,9 +10889,88 @@ function trace(db, start, options) {
8896
10889
  evidence,
8897
10890
  confidence: Number(effectiveRow.confidence ?? call.confidence),
8898
10891
  unresolvedReason: effective.unresolvedReason
10892
+ };
10893
+ const semanticWorkspaceId = workspaceId ?? call.workspaceId;
10894
+ const semantic = recordOutboundObservation(recorder, edge, {
10895
+ call,
10896
+ row: effectiveRow,
10897
+ evidence,
10898
+ workspaceId: semanticWorkspaceId,
10899
+ dynamicMode: options.dynamicMode,
10900
+ unresolvedReason: effective.unresolvedReason
8899
10901
  });
8900
- if ((options.dynamicMode ?? "strict") === "candidates" && effectiveRow.status !== "resolved")
8901
- edges.push(...dynamicCandidateBranches(current.depth, call, evidence));
10902
+ if (options.includeAsync && call.call_type === "async_emit" && effectiveRow.edge_type === "HANDLER_EMITS_EVENT" && typeof call.event_name_expr === "string") {
10903
+ const plans = planEventSubscriberTransitions(db, {
10904
+ workspaceId: workspaceId ?? call.workspaceId,
10905
+ graphGeneration: call.graphGeneration,
10906
+ eventName: call.event_name_expr
10907
+ }, scheduler, current.state, current.depth, maxDepth);
10908
+ for (const plan of plans) {
10909
+ const nodeId = String(plan.node.id);
10910
+ const targetLabel = String(plan.node.label ?? nodeId);
10911
+ nodes.set(nodeId, plan.node);
10912
+ const bridgeEdge = {
10913
+ step: current.depth,
10914
+ type: "event_name_matches_subscription_handler",
10915
+ from: plan.transition.eventName,
10916
+ to: targetLabel,
10917
+ evidence: plan.evidence,
10918
+ confidence: plan.transition.confidence,
10919
+ unresolvedReason: plan.transition.unresolvedReason
10920
+ };
10921
+ const handler = plan.transition.handler;
10922
+ const bridgeTarget = recordEventBridgeObservation(
10923
+ recorder,
10924
+ bridgeEdge,
10925
+ plan,
10926
+ semanticWorkspaceId,
10927
+ plans.length
10928
+ );
10929
+ if (plan.bodyExpansion === "cycle_blocked" && plan.state) {
10930
+ const cycleEvidence = {
10931
+ cycle: true,
10932
+ cycleReason: "structural_ancestry_cycle",
10933
+ graphEdgeId: plan.transition.graphEdgeId
10934
+ };
10935
+ const cycleEdge = { step: current.depth, type: "cycle", from: targetLabel, to: plan.state.structuralKey, evidence: cycleEvidence, confidence: 1, unresolvedReason: "Cycle detected across an event subscriber boundary; downstream symbol was not expanded" };
10936
+ recordEventCycleObservation(
10937
+ recorder,
10938
+ cycleEdge,
10939
+ plan,
10940
+ bridgeTarget,
10941
+ semanticWorkspaceId
10942
+ );
10943
+ }
10944
+ if (plan.bodyExpansion === "scheduled" && plan.state && handler) {
10945
+ const files = /* @__PURE__ */ new Set([handler.sourceFile]);
10946
+ const symbolIds = /* @__PURE__ */ new Set([handler.symbolId]);
10947
+ enqueueCausalScope(queue, pendingRoots, {
10948
+ repoId: handler.repoId,
10949
+ files,
10950
+ symbolIds,
10951
+ depth: current.depth + 1,
10952
+ context: /* @__PURE__ */ new Map(),
10953
+ state: plan.state
10954
+ });
10955
+ }
10956
+ }
10957
+ }
10958
+ if ((options.dynamicMode ?? "strict") === "candidates" && effectiveRow.status !== "resolved") {
10959
+ for (const branch of dynamicCandidateBranches(
10960
+ current.depth,
10961
+ call,
10962
+ evidence
10963
+ )) {
10964
+ recordDynamicBranchObservation(
10965
+ recorder,
10966
+ branch,
10967
+ call,
10968
+ semantic.source,
10969
+ evidence,
10970
+ semanticWorkspaceId
10971
+ );
10972
+ }
10973
+ }
8902
10974
  if (effectiveRow.to_kind === "operation") {
8903
10975
  const implementation = implementationScope(db, effectiveRow.to_id);
8904
10976
  const contextSelection = contextualImplementationSelection(
@@ -8912,7 +10984,7 @@ function trace(db, start, options) {
8912
10984
  const contextMethodId = contextSelection.methodId;
8913
10985
  let selectedHandlerAvailable = true;
8914
10986
  if (implementation.edge) {
8915
- const implEvidence = parseEvidence(implementation.edge.evidence_json);
10987
+ const implEvidence = parseTraceEvidence(implementation.edge.evidence_json);
8916
10988
  const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence);
8917
10989
  if (hintDiagnostic) prependTraceDiagnostic(diagnostics, hintDiagnostic);
8918
10990
  const selectedMethodId = implementation.edge.status === "resolved" ? implementation.edge.to_id : contextMethodId;
@@ -8935,14 +11007,32 @@ function trace(db, start, options) {
8935
11007
  if (selected.diagnostic) prependTraceDiagnostic(diagnostics, selected.diagnostic);
8936
11008
  const implTo = selected.handler?.label ? String(selected.handler.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
8937
11009
  if (selected.handler) nodes.set(String(selected.handler.id), selected.handler);
8938
- edges.push({
11010
+ const unresolvedReason = selected.unresolvedReason ?? (implementation.edge.status === "resolved" || contextMethodId ? void 0 : String(implementation.edge.unresolved_reason ?? implementation.edge.status));
11011
+ const implementationTraceEdge = {
8939
11012
  step: current.depth,
8940
11013
  type: "operation_implemented_by_handler",
8941
11014
  from: to,
8942
11015
  to: implTo,
8943
11016
  evidence: selected.evidence,
8944
11017
  confidence: Number(implementation.edge.confidence ?? 0),
8945
- unresolvedReason: selected.unresolvedReason ?? (implementation.edge.status === "resolved" || contextMethodId ? void 0 : String(implementation.edge.unresolved_reason ?? implementation.edge.status))
11018
+ unresolvedReason
11019
+ };
11020
+ const selectedScope = selectedMethodId ? handlerScope(db, selectedMethodId) : void 0;
11021
+ recordImplementationObservation(recorder, implementationTraceEdge, {
11022
+ operationId: effectiveRow.to_id,
11023
+ handlerMethodId: selectedMethodId,
11024
+ handlerSymbolId: selectedScope?.symbolId,
11025
+ graphEdgeId: implementation.edge.id,
11026
+ persistedStatus: implementation.edge.status,
11027
+ persistedTargetKind: implementation.edge.to_kind,
11028
+ persistedTargetId: implementation.edge.to_id,
11029
+ effectiveStatus: contextMethodId ? "resolved" : String(implementation.edge.status),
11030
+ strategy: String(contextSelection.evidence.strategy ?? (contextMethodId ? "contextual_implementation_selection" : "indexed_operation_graph")),
11031
+ guided: contextSelection.evidence.guided === true,
11032
+ contextual: Boolean(contextMethodId && contextSelection.evidence.strategy !== "implementation_repo_hint"),
11033
+ unresolvedReason,
11034
+ evidence: selected.evidence,
11035
+ site: call
8946
11036
  });
8947
11037
  }
8948
11038
  if (current.depth >= maxDepth) continue;
@@ -8953,24 +11043,53 @@ function trace(db, start, options) {
8953
11043
  const targetRepoId = contextScope?.repoId ?? implementation.repoId ?? db.prepare(
8954
11044
  "SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
8955
11045
  ).get(effectiveRow.to_id)?.repoId;
8956
- const nextKey = `${targetRepoId ?? "*"}:${[...symbolIds ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${[...files].sort().join(",")}`;
8957
- if (seenScopes.has(nextKey))
8958
- edges.push({
11046
+ const nextContext = /* @__PURE__ */ new Map();
11047
+ const scheduling = scheduler.schedule({
11048
+ workspaceId: workspaceId ?? call.workspaceId,
11049
+ repoId: targetRepoId,
11050
+ files,
11051
+ symbolIds,
11052
+ context: nextContext
11053
+ }, current.state);
11054
+ if (scheduling.kind === "cycle") {
11055
+ const cycleEvidence = {
11056
+ ...evidence,
11057
+ cycle: true,
11058
+ cycleReason: "structural_ancestry_cycle"
11059
+ };
11060
+ const cycleEdge = {
8959
11061
  step: current.depth,
8960
11062
  type: "cycle",
8961
11063
  from: to,
8962
- to: nextKey,
8963
- evidence: { ...evidence, cycle: true },
11064
+ to: scheduling.state.structuralKey,
11065
+ evidence: cycleEvidence,
8964
11066
  confidence: 1,
8965
- unresolvedReason: "Cycle detected; downstream scope already visited"
8966
- });
8967
- else
8968
- queue.push({
11067
+ unresolvedReason: "Cycle detected in structural ancestry; downstream scope was not expanded"
11068
+ };
11069
+ recordCycleObservation(recorder, cycleEdge, semantic.target, {
11070
+ workspaceId: semanticWorkspaceId,
11071
+ repositoryId: targetRepoId,
11072
+ sourceFiles: files,
11073
+ symbolIds,
11074
+ structuralKey: scheduling.state.structuralKey
11075
+ }, {
11076
+ graphEdgeId: evidence.persistedGraphEdgeId,
11077
+ outboundCallId: call.id,
11078
+ operationId: effectiveRow.to_id
11079
+ }, call);
11080
+ }
11081
+ if (scheduling.kind === "scheduled") enqueueCausalScope(
11082
+ queue,
11083
+ pendingRoots,
11084
+ {
8969
11085
  repoId: targetRepoId,
8970
11086
  files,
8971
11087
  symbolIds,
8972
- depth: current.depth + 1
8973
- });
11088
+ depth: current.depth + 1,
11089
+ context: nextContext,
11090
+ state: scheduling.state
11091
+ }
11092
+ );
8974
11093
  }
8975
11094
  }
8976
11095
  }
@@ -8982,9 +11101,1010 @@ function trace(db, start, options) {
8982
11101
  prependTraceDiagnostic(diagnostics, diagnostic);
8983
11102
  return { start, nodes: [...nodes.values()], edges, diagnostics };
8984
11103
  }
11104
+ function traceWithObserver(db, start, options, observer) {
11105
+ const observed = {
11106
+ ...options,
11107
+ [compactObserverKey]: observer
11108
+ };
11109
+ return trace(db, start, observed);
11110
+ }
11111
+
11112
+ // src/trace/014-compact-contract.ts
11113
+ var CompactObservationCollector = class {
11114
+ observations = [];
11115
+ workspaceId;
11116
+ record(observation2) {
11117
+ this.observations.push(observation2);
11118
+ }
11119
+ setWorkspaceId(workspaceId) {
11120
+ this.workspaceId = workspaceId;
11121
+ }
11122
+ };
11123
+
11124
+ // src/trace/020-compact-field-projection.ts
11125
+ var compactNameLimit = 8;
11126
+ var compactDiagnosticMessages = {
11127
+ schema_upgrade_required: "The database schema must be upgraded before tracing.",
11128
+ reindex_required: "Current analyzer facts are required before tracing.",
11129
+ trace_workspace_ambiguous: "The trace workspace is ambiguous.",
11130
+ trace_runtime_variables_missing: "Runtime variable names are required to resolve a branch.",
11131
+ implementation_hint_mismatch: "The implementation hint did not select one implementation.",
11132
+ selected_handler_provenance_mismatch: "Selected handler provenance did not match its graph target.",
11133
+ selected_handler_target_not_found: "The selected handler target is not indexed.",
11134
+ trace_start_implementation_unresolved: "The trace start implementation is unresolved."
11135
+ };
11136
+ function projectCompactDecision(input) {
11137
+ if (!input) return {};
11138
+ const out = resolutionDecision(input);
11139
+ addNameDecision(out, input);
11140
+ addDynamicDecision(out, input);
11141
+ addImplementationDecision(out, input);
11142
+ addEventDecision(out, input);
11143
+ const reasonCode = compactSafeCode(input.reasonCode);
11144
+ if (reasonCode) out.reasonCode = reasonCode;
11145
+ addRemediationDecision(out, input);
11146
+ return out;
11147
+ }
11148
+ function resolutionDecision(input) {
11149
+ const out = {};
11150
+ const status = compactSafeCode(input.effectiveResolutionStatus);
11151
+ if (status) out.effectiveResolutionStatus = status;
11152
+ if (!persistedResolutionDiffers(input)) return out;
11153
+ const persistedStatus = compactSafeCode(input.persistedResolutionStatus);
11154
+ if (persistedStatus) out.persistedResolutionStatus = persistedStatus;
11155
+ return out;
11156
+ }
11157
+ function addNameDecision(out, input) {
11158
+ const allNames = safeVariableNames(input.missingVariableNames);
11159
+ const names = allNames.slice(0, compactNameLimit);
11160
+ const total = Math.max(compactCount(input.missingVariableCount), allNames.length);
11161
+ if (names.length > 0) out.missingVariableNames = names;
11162
+ if (total === 0) return;
11163
+ out.missingVariableCount = total;
11164
+ out.shownMissingVariableCount = names.length;
11165
+ out.omittedMissingVariableCount = Math.max(0, total - names.length);
11166
+ }
11167
+ function addDynamicDecision(out, input) {
11168
+ if (input.dynamicMode) out.dynamicMode = input.dynamicMode;
11169
+ if (input.candidateCount !== void 0) out.candidateCount = compactCount(input.candidateCount);
11170
+ if (input.viableCandidateCount !== void 0)
11171
+ out.viableCandidateCount = compactCount(input.viableCandidateCount);
11172
+ if (input.rejectedCandidateCount !== void 0)
11173
+ out.rejectedCandidateCount = compactCount(input.rejectedCandidateCount);
11174
+ if (input.omittedCandidateCount !== void 0)
11175
+ out.omittedCandidateCount = compactCount(input.omittedCandidateCount);
11176
+ }
11177
+ function addImplementationDecision(out, input) {
11178
+ const strategy = compactSafeCode(input.implementationStrategy);
11179
+ if (strategy) out.implementationStrategy = strategy;
11180
+ if (input.implementationGuided !== void 0)
11181
+ out.implementationGuided = input.implementationGuided;
11182
+ if (input.implementationContextual !== void 0)
11183
+ out.implementationContextual = input.implementationContextual;
11184
+ }
11185
+ function addEventDecision(out, input) {
11186
+ addEventCodes(out, input);
11187
+ if (input.eventSubscriptionCount !== void 0)
11188
+ out.eventSubscriptionCount = compactCount(input.eventSubscriptionCount);
11189
+ if (input.roleSiteMatchCount !== void 0)
11190
+ out.roleSiteMatchCount = compactCount(input.roleSiteMatchCount);
11191
+ }
11192
+ function addEventCodes(out, input) {
11193
+ const values = [
11194
+ ["eventMatchStrategy", compactSafeCode(input.eventMatchStrategy)],
11195
+ ["dispatchCertainty", compactSafeCode(input.dispatchCertainty)],
11196
+ ["associationStatus", compactSafeCode(input.associationStatus)],
11197
+ ["associationBasis", compactSafeCode(input.associationBasis)],
11198
+ ["eventScope", compactSafeCode(input.eventScope)],
11199
+ ["callRole", compactSafeCode(input.callRole)],
11200
+ ["factOrigin", compactSafeCode(input.factOrigin)],
11201
+ ["bodyExpansion", compactSafeCode(input.bodyExpansion)]
11202
+ ];
11203
+ for (const [key, value] of values) {
11204
+ if (value) Object.assign(out, { [key]: value });
11205
+ }
11206
+ }
11207
+ function addRemediationDecision(out, input) {
11208
+ const hint = input.remediationCode ? compactRemediationHint(input.remediationCode) : void 0;
11209
+ if (!hint) return;
11210
+ out.remediationHint = hint;
11211
+ const total = Math.max(1, compactCount(input.remediationHintCount));
11212
+ out.omittedRemediationHintCount = Math.max(0, total - 1);
11213
+ }
11214
+ function projectCompactDiagnostics(values) {
11215
+ return values.map((value, index) => compactDiagnostic(value, index)).sort(compareCompactDiagnostic);
11216
+ }
11217
+ function projectCompactStart(start) {
11218
+ return {
11219
+ repo: start.repo ?? null,
11220
+ servicePath: start.servicePath ?? null,
11221
+ operation: start.operation ?? null,
11222
+ operationPath: start.operationPath ?? null,
11223
+ handler: start.handler ?? null
11224
+ };
11225
+ }
11226
+ function projectCompactQuery(options) {
11227
+ const hints = (options.implementationHints ?? []).map(projectCompactHint).sort((left, right) => compareBinary(
11228
+ JSON.stringify(left),
11229
+ JSON.stringify(right)
11230
+ ));
11231
+ return {
11232
+ depth: compactPositiveInteger(options.depth) ?? 25,
11233
+ includeAsync: Boolean(options.includeAsync),
11234
+ includeDb: Boolean(options.includeDb),
11235
+ includeExternal: Boolean(options.includeExternal),
11236
+ dynamicMode: options.dynamicMode ?? "strict",
11237
+ maxDynamicCandidates: compactPositiveInteger(options.maxDynamicCandidates) ?? 5,
11238
+ suppliedVariableNames: compactSortedUnique(Object.keys(options.vars ?? {})),
11239
+ runtimeValuesOmitted: true,
11240
+ implementationRepo: options.implementationRepo ?? null,
11241
+ implementationHints: hints
11242
+ };
11243
+ }
11244
+ function projectCompactHint(hint) {
11245
+ return {
11246
+ servicePath: hint.servicePath ?? null,
11247
+ operationPath: hint.operationPath ?? null,
11248
+ packageName: hint.packageName ?? null,
11249
+ repositoryName: hint.repositoryName ?? null,
11250
+ candidateFamily: hint.candidateFamily ?? null,
11251
+ implementationRepo: hint.implementationRepo ?? null
11252
+ };
11253
+ }
11254
+ function compactStatusCounts(values) {
11255
+ const counts = {
11256
+ resolved: 0,
11257
+ terminal: 0,
11258
+ inferred: 0,
11259
+ dynamic: 0,
11260
+ ambiguous: 0,
11261
+ unresolved: 0,
11262
+ cycle: 0
11263
+ };
11264
+ for (const value of values) counts[value.status] += 1;
11265
+ return counts;
11266
+ }
11267
+ function compactCompleteness(counts, diagnostics) {
11268
+ const total = compactStatusTotal(counts);
11269
+ if (total === 0 && diagnostics.some(compactBlockingDiagnostic)) return "blocked";
11270
+ if (counts.dynamic + counts.ambiguous + counts.unresolved > 0) return "partial";
11271
+ if (diagnostics.some((item) => item[1] === "error" || item[1] === "warning"))
11272
+ return "partial";
11273
+ return "complete";
11274
+ }
11275
+ function compactStatusTotal(counts) {
11276
+ return counts.resolved + counts.terminal + counts.inferred + counts.dynamic + counts.ambiguous + counts.unresolved + counts.cycle;
11277
+ }
11278
+ function removeEquivalentCompactPersistedDecision(decision) {
11279
+ if (decision.persistedResolutionStatus !== decision.effectiveResolutionStatus) return;
11280
+ if (!decision.persistedTarget || decision.persistedTarget !== decision.effectiveTarget) return;
11281
+ delete decision.persistedResolutionStatus;
11282
+ delete decision.persistedTarget;
11283
+ }
11284
+ function compactBlockingDiagnostic(item) {
11285
+ if (item[1] === "error") return true;
11286
+ return item[2] === "schema_upgrade_required" || item[2] === "reindex_required" || item[2] === "trace_workspace_ambiguous" || item[2].startsWith("selector_") || item[2].startsWith("trace_start_");
11287
+ }
11288
+ function compactDiagnostic(value, index) {
11289
+ const code = compactSafeCode(value.code) ?? "unknown_diagnostic";
11290
+ const details = compactDiagnosticDetails(value, code);
11291
+ return {
11292
+ index,
11293
+ severity: compactDiagnosticSeverity(value.severity),
11294
+ code,
11295
+ message: compactDiagnosticMessages[code] ?? `See detailed diagnostic at index ${index}.`,
11296
+ file: compactSafeSourceFile(value.sourceFile) ?? compactSafeSourceFile(value.file),
11297
+ line: compactPositiveInteger(value.sourceLine) ?? compactPositiveInteger(value.line),
11298
+ details: Object.keys(details).length > 0 ? details : void 0
11299
+ };
11300
+ }
11301
+ function compactDiagnosticDetails(value, code) {
11302
+ const out = {};
11303
+ const reasonCode = compactSafeCode(value.reasonCode);
11304
+ if (reasonCode) out.reasonCode = reasonCode;
11305
+ addDiagnosticNames(out, value);
11306
+ addDiagnosticCounts(out, value);
11307
+ const hint = compactDiagnosticRemediation(code);
11308
+ if (hint) {
11309
+ out.remediationHint = hint;
11310
+ out.omittedHintCount = Math.max(
11311
+ 0,
11312
+ compactDiagnosticHintCount(value, code, out) - 1
11313
+ );
11314
+ }
11315
+ return out;
11316
+ }
11317
+ function addDiagnosticNames(out, value) {
11318
+ const allNames = safeVariableNames(compactStringArray(value.missingVariables));
11319
+ const names = allNames.slice(0, compactNameLimit);
11320
+ const total = Math.max(allNames.length, compactCount(value.missingVariableCount));
11321
+ if (names.length > 0) out.missingVariableNames = names;
11322
+ if (total === 0) return;
11323
+ out.missingVariableCount = total;
11324
+ out.shownMissingVariableCount = names.length;
11325
+ out.omittedMissingVariableCount = Math.max(0, total - names.length);
11326
+ }
11327
+ function addDiagnosticCounts(out, value) {
11328
+ if (value.candidateCount !== void 0)
11329
+ out.candidateCount = compactCount(value.candidateCount);
11330
+ if (value.viableCandidateCount !== void 0)
11331
+ out.viableCandidateCount = compactCount(value.viableCandidateCount);
11332
+ if (value.rejectedCandidateCount !== void 0)
11333
+ out.rejectedCandidateCount = compactCount(value.rejectedCandidateCount);
11334
+ }
11335
+ function compareCompactDiagnostic(left, right) {
11336
+ const ranks = { error: 0, warning: 1, info: 2 };
11337
+ return ranks[left.severity] - ranks[right.severity] || compareBinary(left.code, right.code) || compareBinary(left.file ?? "", right.file ?? "") || (left.line ?? Number.MAX_SAFE_INTEGER) - (right.line ?? Number.MAX_SAFE_INTEGER) || compareBinary(left.message, right.message) || left.index - right.index;
11338
+ }
11339
+ function persistedResolutionDiffers(input) {
11340
+ return input.persistedResolutionStatus !== void 0 && (input.persistedResolutionStatus !== input.effectiveResolutionStatus || JSON.stringify(input.persistedTarget) !== JSON.stringify(input.effectiveTarget));
11341
+ }
11342
+ function compactDiagnosticSeverity(value) {
11343
+ return value === "error" || value === "warning" ? value : "info";
11344
+ }
11345
+ function compactDiagnosticRemediation(code) {
11346
+ if (code === "schema_upgrade_required" || code === "reindex_required")
11347
+ return compactRemediationHint("reindex_and_link");
11348
+ if (code === "trace_runtime_variables_missing")
11349
+ return compactRemediationHint("provide_runtime_variables");
11350
+ if (code === "implementation_hint_mismatch")
11351
+ return compactRemediationHint("select_implementation");
11352
+ return void 0;
11353
+ }
11354
+ function compactDiagnosticHintCount(value, code, details) {
11355
+ const missing = code === "trace_runtime_variables_missing" ? details.missingVariableCount ?? 0 : 0;
11356
+ return Math.max(
11357
+ 1,
11358
+ missing,
11359
+ compactArrayLength(value.suggestions),
11360
+ compactArrayLength(value.implementationHintSuggestions),
11361
+ compactArrayLength(value.copyableExamples),
11362
+ compactCount(value.suggestionCount),
11363
+ compactCount(value.implementationHintSuggestionCount),
11364
+ compactCount(value.copyableExampleCount)
11365
+ );
11366
+ }
11367
+ function compactRemediationHint(code) {
11368
+ if (code === "provide_runtime_variables") return "Provide the missing variable names listed in details.";
11369
+ if (code === "select_implementation") return "Select one implementation with a scoped implementation hint.";
11370
+ if (code === "reindex_and_link") return "Force reindex, then force relink the workspace.";
11371
+ if (code === "inspect_detailed_edge") return "Inspect the correlated detailed trace edge.";
11372
+ return void 0;
11373
+ }
11374
+ function compactSafeCode(value) {
11375
+ return typeof value === "string" && /^[a-z][a-z0-9_.-]{0,79}$/.test(value) ? value : void 0;
11376
+ }
11377
+ function projectCompactDecisionTarget(kindValue, id) {
11378
+ const kind = compactSafeCode(kindValue);
11379
+ if (!kind || id.length === 0 || id.length > 240 || /[\r\n]/.test(id)) return void 0;
11380
+ if (/^[a-z]+:\/\//i.test(id) || /\b(?:bearer|token|secret|password|credential|authorization)\b/i.test(id))
11381
+ return void 0;
11382
+ const redacted = redactText(id);
11383
+ return redacted === id ? `${kind}:${redacted}` : void 0;
11384
+ }
11385
+ function compactSafeSourceFile(value) {
11386
+ return typeof value === "string" && value.length > 0 && value.length <= 512 && !/^[a-z]+:\/\//i.test(value) && !/[\r\n]/.test(value) ? value : void 0;
11387
+ }
11388
+ function safeVariableNames(values) {
11389
+ return compactSortedUnique((values ?? []).filter((value) => /^[A-Za-z_$][\w$]*$/.test(value)));
11390
+ }
11391
+ function compactCount(value) {
11392
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
11393
+ }
11394
+ function compactPositiveInteger(value) {
11395
+ const normalized = compactCount(value);
11396
+ return normalized > 0 ? normalized : void 0;
11397
+ }
11398
+ function compactStringArray(value) {
11399
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
11400
+ }
11401
+ function compactArrayLength(value) {
11402
+ return Array.isArray(value) ? value.length : 0;
11403
+ }
11404
+ function compactSortedUnique(values) {
11405
+ return [...new Set(values)].sort(compareBinary);
11406
+ }
11407
+
11408
+ // src/trace/016-compact-projector.ts
11409
+ var REFERENCE_LIMIT = 5;
11410
+ function projectCompactGraph(input) {
11411
+ validateObservationOrdinals(input.observations, input.trace.edges.length);
11412
+ const resolved3 = input.observations.map((item) => resolveObservation(input.db, item));
11413
+ const diagnostics = projectCompactDiagnostics(input.trace.diagnostics);
11414
+ const aggregates = aggregateObservations(resolved3);
11415
+ const nodes = canonicalNodes(resolved3);
11416
+ const repos = sortedUnique(nodes.flatMap((node) => node.repo ? [node.repo] : []));
11417
+ const files = sortedUnique([
11418
+ ...nodes.flatMap((node) => node.file ? [node.file] : []),
11419
+ ...diagnostics.flatMap((item) => item.file ? [item.file] : [])
11420
+ ]);
11421
+ const nodeRows = compactNodeRows(nodes, repos, files);
11422
+ const edgeRows = compactEdgeRows(aggregates, nodes);
11423
+ const diagnosticRows = compactDiagnosticRows(diagnostics, files);
11424
+ const result = compactResult(input, nodes, nodeRows, edgeRows, diagnosticRows, repos, files);
11425
+ validateCompactResult(result);
11426
+ return result;
11427
+ }
11428
+ function resolveObservation(db, input) {
11429
+ const target = resolveEndpoint(db, input.target, input.ordinal, "target", input.site);
11430
+ return {
11431
+ input,
11432
+ source: resolveEndpoint(db, input.source, input.ordinal, "source", input.site),
11433
+ target,
11434
+ decision: observationDecision(db, input, target)
11435
+ };
11436
+ }
11437
+ function observationDecision(db, input, target) {
11438
+ const decision = projectCompactDecision(input.decision);
11439
+ if (decision.effectiveResolutionStatus && target.decisionTarget)
11440
+ decision.effectiveTarget = target.decisionTarget;
11441
+ if (decision.persistedResolutionStatus && input.decision?.persistedTarget) {
11442
+ const persisted = persistedDecisionTarget(db, input.decision.persistedTarget);
11443
+ if (persisted) decision.persistedTarget = persisted;
11444
+ }
11445
+ removeEquivalentCompactPersistedDecision(decision);
11446
+ return decision;
11447
+ }
11448
+ function persistedDecisionTarget(db, target) {
11449
+ const numeric2 = numericId(target.id);
11450
+ if (target.kind === "operation" && numeric2 !== void 0)
11451
+ return operationNode2(db, numeric2)?.decisionTarget;
11452
+ if (target.kind === "symbol" && numeric2 !== void 0)
11453
+ return symbolNode2(db, numeric2)?.decisionTarget;
11454
+ if (target.kind === "handler_method" && numeric2 !== void 0)
11455
+ return handlerNode(db, numeric2)?.decisionTarget;
11456
+ return projectCompactDecisionTarget(target.kind, target.id);
11457
+ }
11458
+ function resolveEndpoint(db, endpoint, ordinal, side, site) {
11459
+ if (endpoint.kind === "operation")
11460
+ return resolvedOrUnavailable(
11461
+ operationNode2(db, endpoint.operationId),
11462
+ side,
11463
+ "operation",
11464
+ ordinal,
11465
+ site
11466
+ );
11467
+ if (endpoint.kind === "symbol")
11468
+ return resolvedOrUnavailable(
11469
+ symbolNode2(db, endpoint.symbolId),
11470
+ side,
11471
+ "symbol",
11472
+ ordinal,
11473
+ site
11474
+ );
11475
+ if (endpoint.kind === "handler_method")
11476
+ return resolvedOrUnavailable(
11477
+ handlerNode(db, endpoint.handlerMethodId),
11478
+ side,
11479
+ "handler_method",
11480
+ ordinal,
11481
+ site
11482
+ );
11483
+ if (endpoint.kind === "event") return eventNode(endpoint.workspaceId, endpoint.eventName);
11484
+ if (endpoint.kind === "target") return targetNode(db, endpoint, ordinal, side, site);
11485
+ if (endpoint.kind === "call_site") return callSiteNode(db, endpoint);
11486
+ if (endpoint.kind === "scope") return scopeNode(db, endpoint);
11487
+ return unavailableNode(
11488
+ endpoint.side,
11489
+ endpoint.endpointKind,
11490
+ endpoint.detailedEdgeIndex,
11491
+ endpoint.site ?? site
11492
+ );
11493
+ }
11494
+ function resolvedOrUnavailable(node, side, kind, ordinal, site) {
11495
+ return node ?? unavailableNode(side, kind, ordinal, site);
11496
+ }
11497
+ function operationNode2(db, operationId) {
11498
+ const row = db.prepare(`SELECT o.id,o.operation_name operationName,
11499
+ o.operation_path operationPath,o.source_file sourceFile,o.source_line sourceLine,
11500
+ s.service_path servicePath,r.workspace_id workspaceId,r.relative_path relativePath,
11501
+ r.name repoName
11502
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
11503
+ JOIN repositories r ON r.id=s.repo_id WHERE o.id=?`).get(operationId);
11504
+ if (!row) return void 0;
11505
+ const operationPath = stringValue15(row.operationPath);
11506
+ const servicePath = stringValue15(row.servicePath);
11507
+ const repo = repositoryLabel(row);
11508
+ const workspaceId = numberValue11(row.workspaceId);
11509
+ if (!operationPath) return void 0;
11510
+ if (!servicePath) return void 0;
11511
+ if (!repo) return void 0;
11512
+ if (workspaceId === void 0) return void 0;
11513
+ const operationName = stringValue15(row.operationName);
11514
+ return {
11515
+ key: canonicalKey("operation", workspaceId, repo, servicePath, operationPath),
11516
+ kind: "operation",
11517
+ label: `${servicePath}:${operationName || operationPath}`,
11518
+ repo,
11519
+ file: stringValue15(row.sourceFile),
11520
+ line: numberValue11(row.sourceLine),
11521
+ synthetic: false,
11522
+ decisionTarget: projectCompactDecisionTarget(
11523
+ "operation",
11524
+ `${repo}:${servicePath}:${operationPath}`
11525
+ )
11526
+ };
11527
+ }
11528
+ function symbolNode2(db, symbolId) {
11529
+ const row = db.prepare(`SELECT s.id,s.kind,s.qualified_name qualifiedName,
11530
+ s.source_file sourceFile,s.start_line startLine,s.start_offset startOffset,
11531
+ s.end_offset endOffset,r.workspace_id workspaceId,r.relative_path relativePath,
11532
+ r.name repoName FROM symbols s JOIN repositories r ON r.id=s.repo_id
11533
+ WHERE s.id=?`).get(symbolId);
11534
+ return symbolNodeFromRow(row);
11535
+ }
11536
+ function symbolNodeFromRow(row) {
11537
+ if (!row) return void 0;
11538
+ const name = stringValue15(row.qualifiedName);
11539
+ const repo = repositoryLabel(row);
11540
+ const file = stringValue15(row.sourceFile);
11541
+ const workspaceId = numberValue11(row.workspaceId);
11542
+ if (!name) return void 0;
11543
+ if (!repo) return void 0;
11544
+ if (!file) return void 0;
11545
+ if (workspaceId === void 0) return void 0;
11546
+ const startOffset = numberValue11(row.startOffset);
11547
+ const endOffset = numberValue11(row.endOffset);
11548
+ return {
11549
+ key: canonicalKey(
11550
+ "symbol",
11551
+ workspaceId,
11552
+ repo,
11553
+ file,
11554
+ startOffset,
11555
+ endOffset,
11556
+ name
11557
+ ),
11558
+ kind: "symbol",
11559
+ label: name,
11560
+ repo,
11561
+ file,
11562
+ line: numberValue11(row.startLine),
11563
+ synthetic: false,
11564
+ decisionTarget: projectCompactDecisionTarget(
11565
+ "symbol",
11566
+ `${repo}:${file}:${startOffset ?? ""}:${endOffset ?? ""}:${name}`
11567
+ )
11568
+ };
11569
+ }
11570
+ function handlerNode(db, handlerMethodId) {
11571
+ const symbol = db.prepare(`SELECT s.kind,s.qualified_name qualifiedName,
11572
+ s.source_file sourceFile,s.start_line startLine,s.start_offset startOffset,
11573
+ s.end_offset endOffset,r.workspace_id workspaceId,r.relative_path relativePath,
11574
+ r.name repoName
11575
+ FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id
11576
+ JOIN repositories r ON r.id=hc.repo_id LEFT JOIN symbols s
11577
+ ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file
11578
+ AND s.qualified_name=hc.class_name || '.' || hm.method_name
11579
+ AND s.start_line=hm.source_line WHERE hm.id=?
11580
+ ORDER BY s.id LIMIT 1`).get(handlerMethodId);
11581
+ const resolved3 = symbolNodeFromRow(symbol);
11582
+ return resolved3 ?? standaloneHandlerNode(db, handlerMethodId);
11583
+ }
11584
+ function standaloneHandlerNode(db, handlerMethodId) {
11585
+ const row = db.prepare(`SELECT hm.method_name methodName,hm.source_file sourceFile,
11586
+ hm.source_line sourceLine,hc.class_name className,r.workspace_id workspaceId,
11587
+ r.relative_path relativePath,r.name repoName FROM handler_methods hm
11588
+ JOIN handler_classes hc ON hc.id=hm.handler_class_id
11589
+ JOIN repositories r ON r.id=hc.repo_id WHERE hm.id=?`).get(handlerMethodId);
11590
+ if (!row) return void 0;
11591
+ const methodName2 = stringValue15(row.methodName);
11592
+ const className = stringValue15(row.className);
11593
+ const repo = repositoryLabel(row);
11594
+ const file = stringValue15(row.sourceFile);
11595
+ const workspaceId = numberValue11(row.workspaceId);
11596
+ if (!methodName2) return void 0;
11597
+ if (!className) return void 0;
11598
+ if (!repo) return void 0;
11599
+ if (!file) return void 0;
11600
+ if (workspaceId === void 0) return void 0;
11601
+ const sourceLine2 = numberValue11(row.sourceLine);
11602
+ return {
11603
+ key: canonicalKey(
11604
+ "handler_method",
11605
+ workspaceId,
11606
+ repo,
11607
+ file,
11608
+ sourceLine2,
11609
+ className,
11610
+ methodName2
11611
+ ),
11612
+ kind: "handler_method",
11613
+ label: `${className}.${methodName2}`,
11614
+ repo,
11615
+ file,
11616
+ line: sourceLine2,
11617
+ synthetic: false,
11618
+ decisionTarget: projectCompactDecisionTarget(
11619
+ "handler_method",
11620
+ `${repo}:${file}:${sourceLine2 ?? ""}:${className}.${methodName2}`
11621
+ )
11622
+ };
11623
+ }
11624
+ function eventNode(workspaceId, eventName) {
11625
+ return {
11626
+ key: canonicalKey("event", workspaceId, eventName),
11627
+ kind: "event",
11628
+ label: eventName,
11629
+ synthetic: false,
11630
+ decisionTarget: projectCompactDecisionTarget("event", eventName)
11631
+ };
11632
+ }
11633
+ function targetNode(db, endpoint, ordinal, side, site) {
11634
+ const linked = linkedTargetNode(db, endpoint.targetKind, endpoint.targetId);
11635
+ if (linked !== void 0)
11636
+ return linked ?? unavailableNode(side, endpoint.targetKind, ordinal, site);
11637
+ const repo = endpoint.repositoryId === void 0 ? void 0 : repositoryById(db, endpoint.repositoryId);
11638
+ return {
11639
+ key: canonicalKey(
11640
+ "target",
11641
+ endpoint.workspaceId,
11642
+ repo,
11643
+ endpoint.targetKind,
11644
+ endpoint.targetId
11645
+ ),
11646
+ kind: compactTargetKind(endpoint.targetKind),
11647
+ label: endpoint.targetId || endpoint.targetKind,
11648
+ repo,
11649
+ synthetic: false,
11650
+ decisionTarget: projectCompactDecisionTarget(endpoint.targetKind, endpoint.targetId)
11651
+ };
11652
+ }
11653
+ function linkedTargetNode(db, kind, id) {
11654
+ const numeric2 = numericId(id);
11655
+ if (kind === "operation") {
11656
+ if (numeric2 === void 0) return null;
11657
+ return operationNode2(db, numeric2) ?? null;
11658
+ }
11659
+ if (kind === "symbol") {
11660
+ if (numeric2 === void 0) return null;
11661
+ return symbolNode2(db, numeric2) ?? null;
11662
+ }
11663
+ if (kind === "handler_method") {
11664
+ if (numeric2 === void 0) return null;
11665
+ return handlerNode(db, numeric2) ?? null;
11666
+ }
11667
+ return void 0;
11668
+ }
11669
+ function callSiteNode(db, endpoint) {
11670
+ const repo = repositoryById(db, endpoint.repositoryId) ?? endpoint.repositoryName;
11671
+ const span = endpoint.startOffset === void 0 || endpoint.endOffset === void 0 ? ["line", endpoint.sourceLine] : [endpoint.startOffset, endpoint.endOffset];
11672
+ return {
11673
+ key: canonicalKey(
11674
+ "call_site",
11675
+ endpoint.workspaceId,
11676
+ repo,
11677
+ endpoint.sourceFile,
11678
+ ...span,
11679
+ endpoint.callId
11680
+ ),
11681
+ kind: "call_site",
11682
+ label: `${repo}:${endpoint.sourceFile}:${endpoint.sourceLine}`,
11683
+ repo,
11684
+ file: endpoint.sourceFile,
11685
+ line: endpoint.sourceLine,
11686
+ synthetic: false,
11687
+ decisionTarget: projectCompactDecisionTarget(
11688
+ "call_site",
11689
+ `${repo}:${endpoint.sourceFile}:${span.join(":")}`
11690
+ )
11691
+ };
11692
+ }
11693
+ function scopeNode(db, endpoint) {
11694
+ const repo = endpoint.repositoryId === void 0 ? void 0 : repositoryById(db, endpoint.repositoryId);
11695
+ const files = sortedUnique(endpoint.sourceFiles);
11696
+ const symbols = sortedUnique(endpoint.symbolIds.flatMap((id) => {
11697
+ const key = symbolNode2(db, id)?.key;
11698
+ return key ? [key] : [];
11699
+ }));
11700
+ const identity = canonicalKey("scope", endpoint.workspaceId, repo, files, symbols);
11701
+ return {
11702
+ key: symbols.length > 0 || files.length > 0 ? identity : canonicalKey("scope", endpoint.workspaceId, repo, endpoint.structuralKey),
11703
+ kind: "scope",
11704
+ label: repo ? `scope:${repo}` : "scope:workspace",
11705
+ repo,
11706
+ file: files.length === 1 ? files[0] : void 0,
11707
+ synthetic: false,
11708
+ decisionTarget: projectCompactDecisionTarget("scope", identity)
11709
+ };
11710
+ }
11711
+ function unavailableNode(side, endpointKind, ordinal, site) {
11712
+ return {
11713
+ key: canonicalKey(
11714
+ "unavailable",
11715
+ side,
11716
+ endpointKind,
11717
+ site?.repository,
11718
+ site?.sourceFile,
11719
+ site?.startOffset,
11720
+ site?.endOffset,
11721
+ site?.sourceLine,
11722
+ ordinal
11723
+ ),
11724
+ kind: "synthetic",
11725
+ label: `${side}:${compactSafeCode(endpointKind) ?? "unavailable"}`,
11726
+ repo: site?.repository,
11727
+ file: site?.sourceFile,
11728
+ line: site?.sourceLine,
11729
+ synthetic: true
11730
+ };
11731
+ }
11732
+ function aggregateObservations(items) {
11733
+ const groups = /* @__PURE__ */ new Map();
11734
+ for (const item of items) {
11735
+ const key = aggregationKey(item);
11736
+ const current = groups.get(key);
11737
+ if (current) appendAggregate(current, item);
11738
+ else groups.set(key, createAggregate(item));
11739
+ }
11740
+ return [...groups.values()].map(finalizeAggregate);
11741
+ }
11742
+ function aggregationKey(item) {
11743
+ return JSON.stringify([
11744
+ item.input.step,
11745
+ item.input.type,
11746
+ item.source.key,
11747
+ item.target.key,
11748
+ item.input.status,
11749
+ normalizedConfidence(item.input.confidence),
11750
+ item.decision
11751
+ ]);
11752
+ }
11753
+ function createAggregate(item) {
11754
+ return {
11755
+ source: item.source,
11756
+ target: item.target,
11757
+ step: item.input.step,
11758
+ type: item.input.type,
11759
+ status: item.input.status,
11760
+ confidence: normalizedConfidence(item.input.confidence),
11761
+ decision: item.decision,
11762
+ ordinals: [item.input.ordinal],
11763
+ refs: item.input.refs ? [item.input.refs] : [],
11764
+ site: item.input.site
11765
+ };
11766
+ }
11767
+ function appendAggregate(group, item) {
11768
+ group.ordinals.push(item.input.ordinal);
11769
+ if (item.input.refs) group.refs.push(item.input.refs);
11770
+ if (compareSite(item.input.site, group.site) < 0) group.site = item.input.site;
11771
+ }
11772
+ function finalizeAggregate(group) {
11773
+ group.ordinals.sort((left, right) => left - right);
11774
+ return group;
11775
+ }
11776
+ function canonicalNodes(items) {
11777
+ const nodes = /* @__PURE__ */ new Map();
11778
+ for (const node of items.flatMap((item) => [item.source, item.target])) {
11779
+ const existing = nodes.get(node.key);
11780
+ if (!existing || compareNodeBody(node, existing) < 0) nodes.set(node.key, node);
11781
+ }
11782
+ return [...nodes.values()].sort((left, right) => compareBinary(left.key, right.key));
11783
+ }
11784
+ function compactNodeRows(nodes, repos, files) {
11785
+ const repoIndexes = indexMap(repos);
11786
+ const fileIndexes = indexMap(files);
11787
+ return nodes.map((node, index) => [
11788
+ `n${index}`,
11789
+ node.kind,
11790
+ redactText(node.label),
11791
+ node.repo === void 0 ? null : repoIndexes.get(node.repo) ?? null,
11792
+ node.file === void 0 ? null : fileIndexes.get(node.file) ?? null,
11793
+ node.line ?? null
11794
+ ]);
11795
+ }
11796
+ function compactEdgeRows(groups, nodes) {
11797
+ const nodeIndexes = new Map(nodes.map((node, index) => [node.key, index]));
11798
+ const sorted = [...groups].sort((left, right) => compareAggregate(left, right, nodeIndexes));
11799
+ return sorted.map((group, index) => edgeRow(group, index, nodeIndexes));
11800
+ }
11801
+ function edgeRow(group, index, nodeIndexes) {
11802
+ const source = nodeIndexes.get(group.source.key);
11803
+ const target = nodeIndexes.get(group.target.key);
11804
+ if (source === void 0 || target === void 0) throw compactError("edge_node_missing");
11805
+ return [
11806
+ `e${index}`,
11807
+ group.ordinals,
11808
+ group.step,
11809
+ group.type,
11810
+ `n${source}`,
11811
+ `n${target}`,
11812
+ group.status,
11813
+ group.confidence,
11814
+ group.ordinals.length,
11815
+ edgeDetails(group)
11816
+ ];
11817
+ }
11818
+ function edgeDetails(group) {
11819
+ const refs = projectReferences(group.refs);
11820
+ if (Object.keys(group.decision).length === 0 && Object.keys(refs).length === 0) return null;
11821
+ return { decision: group.decision, refs };
11822
+ }
11823
+ function projectReferences(values) {
11824
+ const out = {};
11825
+ setReference(out, "graphEdgeIds", values.flatMap((item) => item.graphEdgeIds ?? []));
11826
+ setReference(out, "outboundCallIds", values.flatMap((item) => item.outboundCallIds ?? []));
11827
+ setReference(out, "subscribeCallIds", values.flatMap((item) => item.subscribeCallIds ?? []));
11828
+ setReference(out, "symbolCallIds", values.flatMap((item) => item.symbolCallIds ?? []));
11829
+ setReference(out, "operationIds", values.flatMap((item) => item.operationIds ?? []));
11830
+ setReference(out, "symbolIds", values.flatMap((item) => item.symbolIds ?? []));
11831
+ setReference(out, "handlerMethodIds", values.flatMap((item) => item.handlerMethodIds ?? []));
11832
+ return out;
11833
+ }
11834
+ function setReference(out, key, values) {
11835
+ const unique3 = uniqueReferences(values);
11836
+ if (unique3.length === 0) return;
11837
+ const shown = unique3.slice(0, REFERENCE_LIMIT);
11838
+ out[key] = {
11839
+ values: shown,
11840
+ total: unique3.length,
11841
+ shown: shown.length,
11842
+ omitted: unique3.length - shown.length
11843
+ };
11844
+ }
11845
+ function compactDiagnosticRows(diagnostics, files) {
11846
+ const fileIndexes = indexMap(files);
11847
+ return diagnostics.map((item) => [
11848
+ item.index,
11849
+ item.severity,
11850
+ item.code,
11851
+ item.message,
11852
+ item.file === void 0 ? null : fileIndexes.get(item.file) ?? null,
11853
+ item.line ?? null,
11854
+ item.details ?? null
11855
+ ]);
11856
+ }
11857
+ function compactResult(input, resolvedNodes, nodes, edges, diagnostics, repos, files) {
11858
+ const statusCounts = compactStatusCounts(input.observations);
11859
+ return {
11860
+ schema: "service-flow/compact-graph@1",
11861
+ start: projectCompactStart(input.start),
11862
+ query: projectCompactQuery(input.options),
11863
+ source: input.source,
11864
+ summary: {
11865
+ completeness: compactCompleteness(statusCounts, diagnostics),
11866
+ fullTraceNodes: input.trace.nodes.length,
11867
+ fullTraceEdges: input.trace.edges.length,
11868
+ fullTraceDiagnostics: input.trace.diagnostics.length,
11869
+ nodes: nodes.length,
11870
+ edges: edges.length,
11871
+ collapsedEdges: input.trace.edges.length - edges.length,
11872
+ statusCounts,
11873
+ projection: {
11874
+ evidence: "summary-only",
11875
+ syntheticEndpoints: resolvedNodes.filter((node) => node.synthetic).length,
11876
+ omittedUnreferencedFullNodes: omittedDetailedNodeCount(input)
11877
+ }
11878
+ },
11879
+ repos,
11880
+ files,
11881
+ nodeColumns: ["id", "kind", "label", "repo", "file", "line"],
11882
+ nodes,
11883
+ edgeColumns: [
11884
+ "id",
11885
+ "traceOrdinals",
11886
+ "step",
11887
+ "type",
11888
+ "from",
11889
+ "to",
11890
+ "status",
11891
+ "confidence",
11892
+ "count",
11893
+ "details"
11894
+ ],
11895
+ edges,
11896
+ diagnosticColumns: [
11897
+ "fullDiagnosticIndex",
11898
+ "severity",
11899
+ "code",
11900
+ "message",
11901
+ "file",
11902
+ "line",
11903
+ "details"
11904
+ ],
11905
+ diagnostics
11906
+ };
11907
+ }
11908
+ function omittedDetailedNodeCount(input) {
11909
+ const referenced = new Set(input.observations.flatMap((item) => [
11910
+ ...detailedNodeIds(item.source),
11911
+ ...detailedNodeIds(item.target)
11912
+ ]));
11913
+ return input.trace.nodes.filter((node) => {
11914
+ const id = typeof node.id === "string" ? node.id : void 0;
11915
+ return id === void 0 || !referenced.has(id);
11916
+ }).length;
11917
+ }
11918
+ function detailedNodeIds(endpoint) {
11919
+ if (endpoint.kind === "operation") return [`operation:${endpoint.operationId}`];
11920
+ if (endpoint.kind === "symbol") return [`symbol:${endpoint.symbolId}`];
11921
+ if (endpoint.kind === "handler_method")
11922
+ return [`handler_method:${endpoint.handlerMethodId}`];
11923
+ if (endpoint.kind === "event") return [`event:${endpoint.eventName}`];
11924
+ if (endpoint.kind === "target") return [`${endpoint.targetKind}:${endpoint.targetId}`];
11925
+ if (endpoint.kind === "call_site") return [`call:${endpoint.callId}`];
11926
+ if (endpoint.kind === "scope")
11927
+ return endpoint.symbolIds.map((symbolId) => `symbol:${symbolId}`);
11928
+ return [];
11929
+ }
11930
+ function validateObservationOrdinals(observations, fullEdgeCount) {
11931
+ const ordinals = observations.map((item) => item.ordinal).sort((left, right) => left - right);
11932
+ if (ordinals.length !== fullEdgeCount) throw compactError("observation_count_mismatch");
11933
+ if (ordinals.some((value, index) => value !== index))
11934
+ throw compactError("trace_ordinal_partition_invalid");
11935
+ }
11936
+ function validateCompactResult(result) {
11937
+ if (result.summary.nodes !== result.nodes.length) throw compactError("node_count_mismatch");
11938
+ if (result.summary.edges !== result.edges.length) throw compactError("edge_count_mismatch");
11939
+ const statusTotal = compactStatusTotal(result.summary.statusCounts);
11940
+ if (statusTotal !== result.summary.fullTraceEdges) throw compactError("status_count_mismatch");
11941
+ const edgeTotal = result.edges.reduce((sum, edge) => sum + edge[8], 0);
11942
+ if (edgeTotal !== result.summary.fullTraceEdges) throw compactError("edge_member_count_mismatch");
11943
+ if (result.edges.some((edge) => edge.length !== 10 || edge[8] !== edge[1].length))
11944
+ throw compactError("edge_tuple_invalid");
11945
+ if (result.nodes.some((node) => node.length !== 6)) throw compactError("node_tuple_invalid");
11946
+ if (result.diagnostics.some((item) => item.length !== 7)) throw compactError("diagnostic_tuple_invalid");
11947
+ validateResultOrdinals(result);
11948
+ }
11949
+ function validateResultOrdinals(result) {
11950
+ const ordinals = result.edges.flatMap((edge) => edge[1]).sort((left, right) => left - right);
11951
+ if (ordinals.some((value, index) => value !== index) || ordinals.length !== result.summary.fullTraceEdges)
11952
+ throw compactError("output_trace_ordinal_partition_invalid");
11953
+ if (result.summary.collapsedEdges !== result.summary.fullTraceEdges - result.summary.edges)
11954
+ throw compactError("collapsed_edge_count_mismatch");
11955
+ }
11956
+ function compareAggregate(left, right, nodeIndexes) {
11957
+ return left.step - right.step || indexFor(nodeIndexes, left.source.key) - indexFor(nodeIndexes, right.source.key) || indexFor(nodeIndexes, left.target.key) - indexFor(nodeIndexes, right.target.key) || compareBinary(left.type, right.type) || compareBinary(left.status, right.status) || compareSite(left.site, right.site) || (left.ordinals[0] ?? 0) - (right.ordinals[0] ?? 0);
11958
+ }
11959
+ function compareSite(left, right) {
11960
+ return compareBinary(siteSortKey(left), siteSortKey(right));
11961
+ }
11962
+ function siteSortKey(site) {
11963
+ return JSON.stringify([
11964
+ site?.repository ?? "",
11965
+ site?.sourceFile ?? "",
11966
+ sortableNumber(site?.startOffset),
11967
+ sortableNumber(site?.endOffset),
11968
+ sortableNumber(site?.sourceLine)
11969
+ ]);
11970
+ }
11971
+ function sortableNumber(value) {
11972
+ return value === void 0 ? "z" : `n${String(value).padStart(16, "0")}`;
11973
+ }
11974
+ function compareNodeBody(left, right) {
11975
+ return compareBinary(JSON.stringify([
11976
+ left.kind,
11977
+ left.label,
11978
+ left.repo,
11979
+ left.file,
11980
+ left.line
11981
+ ]), JSON.stringify([
11982
+ right.kind,
11983
+ right.label,
11984
+ right.repo,
11985
+ right.file,
11986
+ right.line
11987
+ ]));
11988
+ }
11989
+ function compactTargetKind(kind) {
11990
+ if (kind === "db_entity") return "database_entity";
11991
+ if (kind === "operation_candidate") return "dynamic_target";
11992
+ if (kind === "symbol_reference" || kind === "subscription_handler")
11993
+ return "unresolved_target";
11994
+ return compactSafeCode(kind) ?? "target";
11995
+ }
11996
+ function repositoryById(db, repositoryId) {
11997
+ const row = db.prepare("SELECT relative_path relativePath,name repoName FROM repositories WHERE id=?").get(repositoryId);
11998
+ return repositoryLabel(row);
11999
+ }
12000
+ function repositoryLabel(row) {
12001
+ return stringValue15(row?.relativePath) ?? stringValue15(row?.repoName);
12002
+ }
12003
+ function normalizedConfidence(value) {
12004
+ return Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
12005
+ }
12006
+ function numericId(value) {
12007
+ return /^\d+$/.test(value) ? Number(value) : void 0;
12008
+ }
12009
+ function numberValue11(value) {
12010
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
12011
+ }
12012
+ function stringValue15(value) {
12013
+ return typeof value === "string" ? value : void 0;
12014
+ }
12015
+ function uniqueReferences(values) {
12016
+ const unique3 = new Map(values.map((value) => [`${typeof value}:${String(value)}`, value]));
12017
+ return [...unique3.values()].sort((left, right) => {
12018
+ if (typeof left === "number" && typeof right === "number") return left - right;
12019
+ return compareBinary(`${typeof left}:${String(left)}`, `${typeof right}:${String(right)}`);
12020
+ });
12021
+ }
12022
+ function sortedUnique(values) {
12023
+ return [...new Set(values)].sort(compareBinary);
12024
+ }
12025
+ function canonicalKey(...parts) {
12026
+ return JSON.stringify(parts);
12027
+ }
12028
+ function indexMap(values) {
12029
+ return new Map(values.map((value, index) => [value, index]));
12030
+ }
12031
+ function indexFor(values, key) {
12032
+ const value = values.get(key);
12033
+ if (value === void 0) throw compactError("canonical_node_index_missing");
12034
+ return value;
12035
+ }
12036
+ function compactError(code) {
12037
+ return new Error(`compact_graph_invariant:${code}`);
12038
+ }
12039
+
12040
+ // src/trace/018-compact-trace.ts
12041
+ function compactTrace(db, start, options) {
12042
+ return traceAndCompact(db, start, options).compact;
12043
+ }
12044
+ function traceAndCompact(db, start, options) {
12045
+ const collector = new CompactObservationCollector();
12046
+ const trace2 = traceWithObserver(db, start, options, collector);
12047
+ const source = compactSourceContext(db, options, collector.workspaceId);
12048
+ const compact = projectCompactGraph({
12049
+ db,
12050
+ start,
12051
+ options,
12052
+ source,
12053
+ trace: trace2,
12054
+ observations: collector.observations
12055
+ });
12056
+ return { trace: trace2, compact };
12057
+ }
12058
+ function compactSourceContext(db, options, traversalWorkspaceId) {
12059
+ return {
12060
+ schemaVersion: schemaVersion2(db),
12061
+ analyzerVersion: sourceAnalyzerVersion(
12062
+ db,
12063
+ traversalWorkspaceId ?? options.workspaceId
12064
+ ),
12065
+ graphGeneration: graphGeneration(
12066
+ db,
12067
+ traversalWorkspaceId ?? options.workspaceId
12068
+ )
12069
+ };
12070
+ }
12071
+ function sourceAnalyzerVersion(db, workspaceId) {
12072
+ const rows2 = db.prepare(`SELECT DISTINCT
12073
+ COALESCE(fact_analyzer_version,'legacy_unknown') analyzerVersion
12074
+ FROM repositories WHERE (? IS NULL OR workspace_id=?)
12075
+ ORDER BY analyzerVersion COLLATE BINARY LIMIT 2`).all(
12076
+ workspaceId,
12077
+ workspaceId
12078
+ );
12079
+ if (rows2.length === 0) return "none";
12080
+ if (rows2.length > 1) return "mixed";
12081
+ return stringValue16(rows2[0]?.analyzerVersion) ?? "legacy_unknown";
12082
+ }
12083
+ function graphGeneration(db, workspaceId) {
12084
+ if (workspaceId === void 0) return 0;
12085
+ const rows2 = db.prepare(`SELECT DISTINCT graph_generation generation
12086
+ FROM repositories WHERE workspace_id=?
12087
+ ORDER BY graph_generation LIMIT 2`).all(workspaceId);
12088
+ return rows2.length === 1 ? numberValue12(rows2[0]?.generation) ?? 0 : 0;
12089
+ }
12090
+ function schemaVersion2(db) {
12091
+ const row = db.pragma("user_version")[0];
12092
+ return numberValue12(row?.user_version) ?? CURRENT_SCHEMA_VERSION;
12093
+ }
12094
+ function numberValue12(value) {
12095
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
12096
+ }
12097
+ function stringValue16(value) {
12098
+ return typeof value === "string" && value.length > 0 ? value : void 0;
12099
+ }
8985
12100
 
8986
12101
  export {
12102
+ migrate,
12103
+ VERSION,
12104
+ ANALYZER_VERSION,
8987
12105
  projectBoundedInOrder,
12106
+ insertSymbolCalls,
12107
+ insertCalls,
8988
12108
  upsertWorkspace,
8989
12109
  getWorkspace,
8990
12110
  upsertRepository,
@@ -8998,8 +12118,6 @@ export {
8998
12118
  insertRegistrations,
8999
12119
  insertBindings,
9000
12120
  insertExecutableSymbols,
9001
- insertSymbolCalls,
9002
- insertCalls,
9003
12121
  normalizePath,
9004
12122
  stripQuotes,
9005
12123
  discoverRepositories,
@@ -9014,6 +12132,7 @@ export {
9014
12132
  normalizeODataOperationInvocationPath,
9015
12133
  classifyODataPathIntent,
9016
12134
  parseServiceBindings,
12135
+ classifyOutboundCallsInSource,
9017
12136
  containsSupportedOutboundCall,
9018
12137
  parseOutboundCalls,
9019
12138
  applyVariables,
@@ -9021,9 +12140,12 @@ export {
9021
12140
  substituteVariables,
9022
12141
  parseImplementationHint,
9023
12142
  implementationHintSuggestions,
12143
+ factLifecycleDiagnostic,
9024
12144
  linkWorkspace,
9025
12145
  parseVars,
9026
12146
  selectorRepoAmbiguousDiagnostic,
9027
- trace
12147
+ trace,
12148
+ compactTrace,
12149
+ traceAndCompact
9028
12150
  };
9029
- //# sourceMappingURL=chunk-TBH33OYC.js.map
12151
+ //# sourceMappingURL=chunk-TOVX4WYH.js.map