@saptools/service-flow 0.1.6 → 0.1.8

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.
@@ -328,36 +328,176 @@ async function parseDecorators(repoPath, filePath) {
328
328
  }
329
329
 
330
330
  // src/parsers/handler-registration-parser.ts
331
+ import fsSync from "fs";
331
332
  import fs5 from "fs/promises";
332
333
  import path6 from "path";
333
- function lineOf2(text, idx) {
334
- return text.slice(0, idx).split("\n").length;
334
+ import ts3 from "typescript";
335
+ var MAX_EXPORT_DEPTH = 5;
336
+ function lineOf2(sourceFile, node) {
337
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
338
+ }
339
+ function isRelative(source) {
340
+ return source.startsWith("./") || source.startsWith("../");
341
+ }
342
+ function sourceText(node, sourceFile) {
343
+ if (ts3.isIdentifier(node) || ts3.isStringLiteral(node) || ts3.isNumericLiteral(node)) return node.text;
344
+ return node.getText(sourceFile);
345
+ }
346
+ function importSourceFor(identifier, imports) {
347
+ const evidence = imports.get(identifier);
348
+ return evidence ? `${evidence.source}#${evidence.importedName}` : void 0;
335
349
  }
336
350
  async function parseHandlerRegistrations(repoPath, filePath) {
337
- const text = await fs5.readFile(path6.join(repoPath, filePath), "utf8");
351
+ const absolutePath = path6.join(repoPath, filePath);
352
+ const text = await fs5.readFile(absolutePath, "utf8");
353
+ const sourceFile = ts3.createSourceFile(filePath, text, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
354
+ const imports = collectImports(sourceFile);
355
+ const localArrays = collectLocalArrays(sourceFile, imports, /* @__PURE__ */ new Map(), repoPath, filePath);
338
356
  const out = [];
339
- for (const m of text.matchAll(
340
- /createCombinedHandler\s*\(|srv\.prepend\s*\(|cds\.serve\s*\(/g
341
- ))
342
- out.push({
343
- registrationFile: normalizePath(filePath),
344
- registrationLine: lineOf2(text, m.index ?? 0),
345
- registrationKind: m[0].startsWith("cds") ? "cds.serve" : "combined-handler",
346
- confidence: 0.8
347
- });
348
- for (const m of text.matchAll(
349
- /(?:const|export\s+const)\s+handlers\s*=\s*\[([\s\S]*?)\]/g
350
- ))
351
- for (const c of (m[1] ?? "").matchAll(/\b(\w+Handler)\b/g))
357
+ function emitFromExpression(expression, call) {
358
+ const classes = resolveArrayExpression(expression, localArrays, imports, repoPath, filePath, /* @__PURE__ */ new Set());
359
+ for (const cls of classes) {
352
360
  out.push({
353
- className: c[1],
361
+ className: cls.className,
362
+ importSource: cls.importSource,
354
363
  registrationFile: normalizePath(filePath),
355
- registrationLine: lineOf2(text, (m.index ?? 0) + (c.index ?? 0)),
356
- registrationKind: "handler-array",
357
- confidence: 0.9
364
+ registrationLine: lineOf2(sourceFile, call),
365
+ registrationKind: "combined-handler-class",
366
+ confidence: 0.95
358
367
  });
368
+ }
369
+ if (classes.length === 0) {
370
+ out.push({
371
+ registrationFile: normalizePath(filePath),
372
+ registrationLine: lineOf2(sourceFile, call),
373
+ registrationKind: "combined-handler",
374
+ confidence: 0.75
375
+ });
376
+ }
377
+ }
378
+ function visit(node) {
379
+ if (ts3.isCallExpression(node) && isRegistrationCall(node)) {
380
+ const handlerExpr = handlerExpression(node, sourceFile);
381
+ if (handlerExpr) emitFromExpression(handlerExpr, node);
382
+ else out.push({ registrationFile: normalizePath(filePath), registrationLine: lineOf2(sourceFile, node), registrationKind: "combined-handler", confidence: 0.75 });
383
+ }
384
+ ts3.forEachChild(node, visit);
385
+ }
386
+ visit(sourceFile);
387
+ return out;
388
+ }
389
+ function isRegistrationCall(call) {
390
+ const text = call.expression.getText();
391
+ return text.endsWith("createCombinedHandler") || text.endsWith("srv.prepend") || text.endsWith("cds.serve");
392
+ }
393
+ function handlerExpression(call, sourceFile) {
394
+ for (const arg of call.arguments) {
395
+ if (!ts3.isObjectLiteralExpression(arg)) continue;
396
+ for (const prop of arg.properties) {
397
+ if (!ts3.isPropertyAssignment(prop)) continue;
398
+ if (sourceText(prop.name, sourceFile) === "handler") return prop.initializer;
399
+ }
400
+ }
401
+ return void 0;
402
+ }
403
+ function collectImports(sourceFile) {
404
+ const imports = /* @__PURE__ */ new Map();
405
+ for (const statement of sourceFile.statements) {
406
+ if (!ts3.isImportDeclaration(statement) || !ts3.isStringLiteral(statement.moduleSpecifier)) continue;
407
+ const source = statement.moduleSpecifier.text;
408
+ const clause = statement.importClause;
409
+ if (!clause) continue;
410
+ if (clause.name) imports.set(clause.name.text, { importedName: "default", source });
411
+ const named = clause.namedBindings;
412
+ if (named && ts3.isNamedImports(named)) {
413
+ for (const element of named.elements) imports.set(element.name.text, { importedName: element.propertyName?.text ?? element.name.text, source });
414
+ }
415
+ if (named && ts3.isNamespaceImport(named)) imports.set(named.name.text, { importedName: "*", source });
416
+ }
417
+ return imports;
418
+ }
419
+ function collectLocalArrays(sourceFile, imports, seed, repoPath = "", fromFile = "") {
420
+ const arrays = new Map(seed);
421
+ for (const statement of sourceFile.statements) {
422
+ if (ts3.isVariableStatement(statement)) {
423
+ for (const decl of statement.declarationList.declarations) {
424
+ if (ts3.isIdentifier(decl.name) && decl.initializer && ts3.isArrayLiteralExpression(decl.initializer)) {
425
+ arrays.set(decl.name.text, resolveArrayLiteral(decl.initializer, arrays, imports, repoPath, fromFile, /* @__PURE__ */ new Set()));
426
+ }
427
+ }
428
+ }
429
+ }
430
+ return arrays;
431
+ }
432
+ function resolveArrayExpression(expr, arrays, imports, repoPath, fromFile, seen) {
433
+ if (ts3.isArrayLiteralExpression(expr)) return resolveArrayLiteral(expr, arrays, imports, repoPath, fromFile, seen);
434
+ if (ts3.isIdentifier(expr)) {
435
+ const local = arrays.get(expr.text);
436
+ if (local) return local;
437
+ const evidence = imports.get(expr.text);
438
+ if (evidence && isRelative(evidence.source)) return resolveImportedArray(repoPath, fromFile, evidence, seen);
439
+ if (evidence) return [{ className: evidence.importedName === "default" ? expr.text : evidence.importedName, importSource: `${evidence.source}#${evidence.importedName}` }];
440
+ }
441
+ return [];
442
+ }
443
+ function resolveArrayLiteral(array, arrays, imports, repoPath, fromFile, seen) {
444
+ const out = [];
445
+ for (const element of array.elements) {
446
+ if (ts3.isSpreadElement(element)) out.push(...resolveArrayExpression(element.expression, arrays, imports, repoPath, fromFile, seen));
447
+ else if (ts3.isIdentifier(element)) out.push({ className: element.text, importSource: importSourceFor(element.text, imports) });
448
+ }
359
449
  return out;
360
450
  }
451
+ function resolveImportedArray(repoPath, fromFile, evidence, seen) {
452
+ const moduleFile = resolveRelativeModule(repoPath, fromFile, evidence.source);
453
+ if (!moduleFile) return [];
454
+ const key = `${moduleFile}:${evidence.importedName}`;
455
+ if (seen.has(key) || seen.size > MAX_EXPORT_DEPTH) return [];
456
+ seen.add(key);
457
+ const exports = readExports(repoPath, moduleFile, seen);
458
+ if (evidence.importedName === "default") return exports.defaultArray ?? [];
459
+ return exports.arrays.get(evidence.importedName) ?? exports.arrays.get(exports.aliases.get(evidence.importedName) ?? evidence.importedName) ?? [];
460
+ }
461
+ function resolveRelativeModule(repoPath, fromFile, specifier) {
462
+ const base = path6.resolve(repoPath, path6.dirname(fromFile), specifier);
463
+ for (const candidate of [base, `${base}.ts`, `${base}.js`, path6.join(base, "index.ts"), path6.join(base, "index.js")]) {
464
+ try {
465
+ const stat = fsSync.statSync(candidate);
466
+ if (stat.isFile()) return normalizePath(path6.relative(repoPath, candidate));
467
+ } catch {
468
+ }
469
+ }
470
+ return void 0;
471
+ }
472
+ function readExports(repoPath, filePath, seen) {
473
+ const absolute = path6.join(repoPath, filePath);
474
+ let text;
475
+ try {
476
+ text = fsSync.readFileSync(absolute, "utf8");
477
+ } catch {
478
+ return { arrays: /* @__PURE__ */ new Map(), aliases: /* @__PURE__ */ new Map() };
479
+ }
480
+ const sourceFile = ts3.createSourceFile(filePath, text, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
481
+ const imports = collectImports(sourceFile);
482
+ const arrays = collectLocalArrays(sourceFile, imports, /* @__PURE__ */ new Map(), repoPath, filePath);
483
+ const aliases = /* @__PURE__ */ new Map();
484
+ let defaultArray;
485
+ for (const statement of sourceFile.statements) {
486
+ if (ts3.isExportAssignment(statement) && ts3.isIdentifier(statement.expression)) defaultArray = arrays.get(statement.expression.text);
487
+ if (ts3.isExportDeclaration(statement) && statement.exportClause && ts3.isNamedExports(statement.exportClause)) {
488
+ const module = statement.moduleSpecifier && ts3.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : void 0;
489
+ for (const element of statement.exportClause.elements) {
490
+ const local = element.propertyName?.text ?? element.name.text;
491
+ aliases.set(element.name.text, local);
492
+ if (module && isRelative(module)) {
493
+ const imported = resolveImportedArray(repoPath, filePath, { source: module, importedName: local }, seen);
494
+ if (imported.length > 0) arrays.set(element.name.text, imported);
495
+ }
496
+ }
497
+ }
498
+ }
499
+ return { arrays, defaultArray, aliases };
500
+ }
361
501
 
362
502
  // src/utils/redaction.ts
363
503
  var SENSITIVE = /authorization|cookie|token|secret|password|key|credential/i;
@@ -504,15 +644,15 @@ async function parseOutboundCalls(repoPath, filePath) {
504
644
  // src/parsers/service-binding-parser.ts
505
645
  import fs7 from "fs/promises";
506
646
  import path8 from "path";
507
- import ts3 from "typescript";
647
+ import ts4 from "typescript";
508
648
  function lineOf4(sf, node) {
509
649
  return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
510
650
  }
511
651
  function stringValue(node) {
512
652
  if (!node) return void 0;
513
- if (ts3.isStringLiteralLike(node) || ts3.isNoSubstitutionTemplateLiteral(node))
653
+ if (ts4.isStringLiteralLike(node) || ts4.isNoSubstitutionTemplateLiteral(node))
514
654
  return node.text;
515
- if (ts3.isTemplateExpression(node))
655
+ if (ts4.isTemplateExpression(node))
516
656
  return node.getText().replace(/^`|`$/g, "");
517
657
  return node.getText();
518
658
  }
@@ -521,22 +661,22 @@ function placeholders(value) {
521
661
  }
522
662
  function connectFactFromCall(call) {
523
663
  const expr = call.expression;
524
- if (!ts3.isPropertyAccessExpression(expr) || expr.name.text !== "to")
664
+ if (!ts4.isPropertyAccessExpression(expr) || expr.name.text !== "to")
525
665
  return void 0;
526
666
  const inner = expr.expression;
527
- if (!ts3.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds")
667
+ if (!ts4.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds")
528
668
  return void 0;
529
669
  const first = call.arguments[0];
530
670
  if (!first) return void 0;
531
671
  const second = call.arguments[1];
532
- const objectArg = ts3.isObjectLiteralExpression(first) ? first : second && ts3.isObjectLiteralExpression(second) ? second : void 0;
672
+ const objectArg = ts4.isObjectLiteralExpression(first) ? first : second && ts4.isObjectLiteralExpression(second) ? second : void 0;
533
673
  let alias;
534
674
  let aliasExpr;
535
- if (ts3.isStringLiteralLike(first) || ts3.isNoSubstitutionTemplateLiteral(first))
675
+ if (ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first))
536
676
  alias = first.text;
537
- else if (!ts3.isObjectLiteralExpression(first))
677
+ else if (!ts4.isObjectLiteralExpression(first))
538
678
  aliasExpr = stringValue(first);
539
- if ((ts3.isStringLiteralLike(first) || ts3.isNoSubstitutionTemplateLiteral(first)) && !objectArg)
679
+ if ((ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first)) && !objectArg)
540
680
  return { alias: first.text, isDynamic: false, placeholders: [] };
541
681
  if (!objectArg && aliasExpr)
542
682
  return {
@@ -548,13 +688,13 @@ function connectFactFromCall(call) {
548
688
  let servicePathExpr;
549
689
  function visitObject(obj) {
550
690
  for (const prop of obj.properties) {
551
- if (!ts3.isPropertyAssignment(prop)) continue;
552
- const name = ts3.isIdentifier(prop.name) || ts3.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
691
+ if (!ts4.isPropertyAssignment(prop)) continue;
692
+ const name = ts4.isIdentifier(prop.name) || ts4.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
553
693
  if (name === "destination")
554
694
  destinationExpr = stringValue(prop.initializer);
555
695
  if (name === "path" || name === "servicePath")
556
696
  servicePathExpr = stringValue(prop.initializer);
557
- if (ts3.isObjectLiteralExpression(prop.initializer))
697
+ if (ts4.isObjectLiteralExpression(prop.initializer))
558
698
  visitObject(prop.initializer);
559
699
  }
560
700
  }
@@ -574,8 +714,8 @@ function connectFactFromCall(call) {
574
714
  };
575
715
  }
576
716
  function unwrapCall(expr) {
577
- if (ts3.isAwaitExpression(expr)) return unwrapCall(expr.expression);
578
- if (ts3.isCallExpression(expr)) return expr;
717
+ if (ts4.isAwaitExpression(expr)) return unwrapCall(expr.expression);
718
+ if (ts4.isCallExpression(expr)) return expr;
579
719
  return void 0;
580
720
  }
581
721
  function findConnectInExpression(expr) {
@@ -587,8 +727,8 @@ function findConnectInExpression(expr) {
587
727
  let found;
588
728
  function visit(node) {
589
729
  if (found) return;
590
- if (ts3.isCallExpression(node)) found = connectFactFromCall(node);
591
- if (!found) ts3.forEachChild(node, visit);
730
+ if (ts4.isCallExpression(node)) found = connectFactFromCall(node);
731
+ if (!found) ts4.forEachChild(node, visit);
592
732
  }
593
733
  visit(expr);
594
734
  return found;
@@ -596,12 +736,12 @@ function findConnectInExpression(expr) {
596
736
  async function readSource(abs) {
597
737
  try {
598
738
  const text = await fs7.readFile(abs, "utf8");
599
- return ts3.createSourceFile(
739
+ return ts4.createSourceFile(
600
740
  abs,
601
741
  text,
602
- ts3.ScriptTarget.Latest,
742
+ ts4.ScriptTarget.Latest,
603
743
  true,
604
- ts3.ScriptKind.TS
744
+ ts4.ScriptKind.TS
605
745
  );
606
746
  } catch {
607
747
  return void 0;
@@ -630,7 +770,7 @@ async function resolveImport(repoPath, fromFile, spec) {
630
770
  async function importsFor(repoPath, filePath, sf) {
631
771
  const imports = [];
632
772
  for (const stmt of sf.statements) {
633
- if (!ts3.isImportDeclaration(stmt) || !ts3.isStringLiteralLike(stmt.moduleSpecifier))
773
+ if (!ts4.isImportDeclaration(stmt) || !ts4.isStringLiteralLike(stmt.moduleSpecifier))
634
774
  continue;
635
775
  const sourceFile = await resolveImport(
636
776
  repoPath,
@@ -646,7 +786,7 @@ async function importsFor(repoPath, filePath, sf) {
646
786
  sourceFile
647
787
  });
648
788
  const bindings = clause.namedBindings;
649
- if (bindings && ts3.isNamedImports(bindings))
789
+ if (bindings && ts4.isNamedImports(bindings))
650
790
  for (const el of bindings.elements)
651
791
  imports.push({
652
792
  localName: el.name.text,
@@ -659,17 +799,17 @@ async function importsFor(repoPath, filePath, sf) {
659
799
  function exportedLocalNames(sf) {
660
800
  const exports = /* @__PURE__ */ new Map();
661
801
  for (const stmt of sf.statements) {
662
- const direct = ts3.canHaveModifiers(stmt) ? ts3.getModifiers(stmt)?.some(
663
- (m) => m.kind === ts3.SyntaxKind.ExportKeyword
802
+ const direct = ts4.canHaveModifiers(stmt) ? ts4.getModifiers(stmt)?.some(
803
+ (m) => m.kind === ts4.SyntaxKind.ExportKeyword
664
804
  ) ?? false : false;
665
- if (direct && ts3.isFunctionDeclaration(stmt) && stmt.name)
805
+ if (direct && ts4.isFunctionDeclaration(stmt) && stmt.name)
666
806
  exports.set(stmt.name.text, stmt.name.text);
667
- if (direct && ts3.isVariableStatement(stmt)) {
807
+ if (direct && ts4.isVariableStatement(stmt)) {
668
808
  for (const decl of stmt.declarationList.declarations)
669
- if (ts3.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
809
+ if (ts4.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
670
810
  }
671
- if (!ts3.isExportDeclaration(stmt) || !stmt.exportClause) continue;
672
- if (!ts3.isNamedExports(stmt.exportClause)) continue;
811
+ if (!ts4.isExportDeclaration(stmt) || !stmt.exportClause) continue;
812
+ if (!ts4.isNamedExports(stmt.exportClause)) continue;
673
813
  for (const el of stmt.exportClause.elements)
674
814
  exports.set(el.name.text, el.propertyName?.text ?? el.name.text);
675
815
  }
@@ -683,18 +823,18 @@ async function helperBindings(repoPath, filePath) {
683
823
  const exportedLocals = exportedLocalNames(sf);
684
824
  const factsByLocal = /* @__PURE__ */ new Map();
685
825
  for (const stmt of sf.statements) {
686
- if (ts3.isFunctionDeclaration(stmt) && stmt.name) {
826
+ if (ts4.isFunctionDeclaration(stmt) && stmt.name) {
687
827
  let fact;
688
828
  stmt.forEachChild(function visit(node) {
689
- if (!fact && ts3.isReturnStatement(node) && node.expression)
829
+ if (!fact && ts4.isReturnStatement(node) && node.expression)
690
830
  fact = findConnectInExpression(node.expression);
691
- if (!fact) ts3.forEachChild(node, visit);
831
+ if (!fact) ts4.forEachChild(node, visit);
692
832
  });
693
833
  if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf4(sf, stmt) });
694
834
  }
695
- if (ts3.isVariableStatement(stmt))
835
+ if (ts4.isVariableStatement(stmt))
696
836
  for (const decl of stmt.declarationList.declarations) {
697
- if (!ts3.isIdentifier(decl.name) || !decl.initializer) continue;
837
+ if (!ts4.isIdentifier(decl.name) || !decl.initializer) continue;
698
838
  const fact = findConnectInExpression(decl.initializer);
699
839
  if (fact)
700
840
  factsByLocal.set(decl.name.text, {
@@ -735,7 +875,7 @@ async function parseServiceBindings(repoPath, filePath) {
735
875
  return helper ? { imp, helper } : void 0;
736
876
  }
737
877
  async function recordVariable(decl) {
738
- if (!ts3.isIdentifier(decl.name) || !decl.initializer) return;
878
+ if (!ts4.isIdentifier(decl.name) || !decl.initializer) return;
739
879
  const call = unwrapCall(decl.initializer);
740
880
  if (!call) return;
741
881
  const direct = connectFactFromCall(call);
@@ -746,7 +886,7 @@ async function parseServiceBindings(repoPath, filePath) {
746
886
  sourceFile: normalizePath(filePath),
747
887
  sourceLine: lineOf4(sourceFileAst, decl)
748
888
  });
749
- else if (ts3.isIdentifier(call.expression)) {
889
+ else if (ts4.isIdentifier(call.expression)) {
750
890
  const resolved = await importedHelper(call.expression.text);
751
891
  if (resolved)
752
892
  out.push({
@@ -773,14 +913,14 @@ async function parseServiceBindings(repoPath, filePath) {
773
913
  }
774
914
  }
775
915
  function recordDestructuredClassHelper(decl) {
776
- if (!ts3.isObjectBindingPattern(decl.name) || !decl.initializer) return;
916
+ if (!ts4.isObjectBindingPattern(decl.name) || !decl.initializer) return;
777
917
  const call = unwrapCall(decl.initializer);
778
- if (!call || !ts3.isPropertyAccessExpression(call.expression)) return;
918
+ if (!call || !ts4.isPropertyAccessExpression(call.expression)) return;
779
919
  const target = call.expression;
780
- if (target.expression.kind !== ts3.SyntaxKind.ThisKeyword) return;
920
+ if (target.expression.kind !== ts4.SyntaxKind.ThisKeyword) return;
781
921
  for (const el of decl.name.elements) {
782
- if (!ts3.isIdentifier(el.name)) continue;
783
- const propertyName = el.propertyName && ts3.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
922
+ if (!ts4.isIdentifier(el.name)) continue;
923
+ const propertyName = el.propertyName && ts4.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
784
924
  const helper = classHelpers.find(
785
925
  (h) => h.helperName === target.name.text && h.propertyName === propertyName
786
926
  );
@@ -806,8 +946,8 @@ async function parseServiceBindings(repoPath, filePath) {
806
946
  }
807
947
  const declarations = [];
808
948
  function collectDeclarations(node) {
809
- if (ts3.isVariableDeclaration(node)) declarations.push(node);
810
- ts3.forEachChild(node, collectDeclarations);
949
+ if (ts4.isVariableDeclaration(node)) declarations.push(node);
950
+ ts4.forEachChild(node, collectDeclarations);
811
951
  }
812
952
  collectDeclarations(sourceFileAst);
813
953
  for (const decl of declarations) {
@@ -819,16 +959,16 @@ async function parseServiceBindings(repoPath, filePath) {
819
959
  function collectClassHelpers(sf) {
820
960
  const helpers = [];
821
961
  for (const stmt of sf.statements) {
822
- if (!ts3.isClassDeclaration(stmt) || !stmt.name) continue;
962
+ if (!ts4.isClassDeclaration(stmt) || !stmt.name) continue;
823
963
  for (const member of stmt.members) {
824
964
  let visit2 = function(node) {
825
- if (ts3.isVariableDeclaration(node) && ts3.isIdentifier(node.name) && node.initializer) {
965
+ if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer) {
826
966
  const fact = findConnectInExpression(node.initializer);
827
967
  if (fact) bindings.set(node.name.text, fact);
828
968
  }
829
- if (ts3.isReturnStatement(node) && node.expression && ts3.isObjectLiteralExpression(node.expression)) {
969
+ if (ts4.isReturnStatement(node) && node.expression && ts4.isObjectLiteralExpression(node.expression)) {
830
970
  for (const prop of node.expression.properties) {
831
- if (ts3.isShorthandPropertyAssignment(prop)) {
971
+ if (ts4.isShorthandPropertyAssignment(prop)) {
832
972
  const fact = bindings.get(prop.name.text);
833
973
  if (fact)
834
974
  helpers.push({
@@ -840,8 +980,8 @@ function collectClassHelpers(sf) {
840
980
  sourceLine: lineOf4(sf, prop)
841
981
  });
842
982
  }
843
- if (ts3.isPropertyAssignment(prop) && ts3.isIdentifier(prop.initializer)) {
844
- const propertyName = ts3.isIdentifier(prop.name) || ts3.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
983
+ if (ts4.isPropertyAssignment(prop) && ts4.isIdentifier(prop.initializer)) {
984
+ const propertyName = ts4.isIdentifier(prop.name) || ts4.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
845
985
  const fact = propertyName ? bindings.get(prop.initializer.text) : void 0;
846
986
  if (propertyName && fact)
847
987
  helpers.push({
@@ -855,15 +995,15 @@ function collectClassHelpers(sf) {
855
995
  }
856
996
  }
857
997
  }
858
- ts3.forEachChild(node, visit2);
998
+ ts4.forEachChild(node, visit2);
859
999
  };
860
1000
  var visit = visit2;
861
- if (!ts3.isPropertyDeclaration(member) || !member.initializer) continue;
862
- if (!ts3.isIdentifier(member.name)) continue;
1001
+ if (!ts4.isPropertyDeclaration(member) || !member.initializer) continue;
1002
+ if (!ts4.isIdentifier(member.name)) continue;
863
1003
  const className = stmt.name.text;
864
1004
  const helperName = member.name.text;
865
1005
  const initializer = member.initializer;
866
- if (!ts3.isArrowFunction(initializer) && !ts3.isFunctionExpression(initializer))
1006
+ if (!ts4.isArrowFunction(initializer) && !ts4.isFunctionExpression(initializer))
867
1007
  continue;
868
1008
  const bindings = /* @__PURE__ */ new Map();
869
1009
  visit2(initializer);
@@ -992,149 +1132,143 @@ function normalizeName(value) {
992
1132
  }
993
1133
  function candidatesForDependency(repos, dep, sourceId) {
994
1134
  const exact = repos.filter((repo) => repo.id !== sourceId && repo.package_name === dep);
995
- if (exact.length > 0) return exact;
1135
+ if (exact.length > 0) return { candidates: exact, strategy: "exact_package_name" };
996
1136
  const normalized = normalizeName(dep);
997
- return repos.filter((repo) => repo.id !== sourceId && normalizeName(repo.name) === normalized);
1137
+ return { candidates: repos.filter((repo) => repo.id !== sourceId && normalizeName(repo.name) === normalized), strategy: "normalized_directory" };
998
1138
  }
999
- function linkHelperPackages(db, workspaceId) {
1000
- const repos = db.prepare(
1001
- "SELECT id,name,package_name,dependencies_json FROM repositories WHERE workspace_id=?"
1002
- ).all(workspaceId);
1003
- let count = 0;
1139
+ function linkHelperPackages(db, workspaceId, generation) {
1140
+ const repos = db.prepare("SELECT id,name,package_name,dependencies_json FROM repositories WHERE workspace_id=?").all(workspaceId);
1141
+ const summary = { edgeCount: 0, resolvedCount: 0, ambiguousCount: 0 };
1004
1142
  for (const repo of repos) {
1005
1143
  const deps = JSON.parse(repo.dependencies_json);
1006
1144
  for (const dep of Object.keys(deps)) {
1007
- const candidates = candidatesForDependency(repos, dep, repo.id);
1008
- if (candidates.length === 0) continue;
1009
- const status = candidates.length === 1 ? "resolved" : "ambiguous";
1010
- const helper = candidates.length === 1 ? candidates[0] : void 0;
1011
- db.prepare(
1012
- "INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
1013
- ).run(
1145
+ const result = candidatesForDependency(repos, dep, repo.id);
1146
+ if (result.candidates.length === 0) continue;
1147
+ const status = result.candidates.length === 1 ? "resolved" : "ambiguous";
1148
+ const helper = status === "resolved" ? result.candidates[0] : void 0;
1149
+ 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(
1014
1150
  workspaceId,
1015
1151
  "REPO_IMPORTS_HELPER_PACKAGE",
1016
1152
  status,
1017
1153
  "repo",
1018
1154
  String(repo.id),
1019
1155
  helper ? "repo" : "repo_candidates",
1020
- helper ? String(helper.id) : candidates.map((candidate) => candidate.id).join(","),
1156
+ helper ? String(helper.id) : result.candidates.map((candidate) => candidate.id).join(","),
1021
1157
  helper ? 1 : 0.5,
1022
- JSON.stringify({
1023
- dependency: dep,
1024
- candidates: candidates.map((candidate) => ({ id: candidate.id, name: candidate.name, packageName: candidate.package_name })),
1025
- match: helper?.package_name === dep ? "package_name" : "normalized_directory"
1026
- }),
1158
+ JSON.stringify({ dependency: dep, candidates: result.candidates.map((candidate) => ({ id: candidate.id, name: candidate.name, packageName: candidate.package_name })), match: result.strategy }),
1027
1159
  0,
1028
- helper ? null : "Ambiguous dependency package candidates"
1160
+ helper ? null : "Ambiguous dependency package candidates",
1161
+ generation
1029
1162
  );
1030
- count += 1;
1163
+ summary.edgeCount += 1;
1164
+ if (helper) summary.resolvedCount += 1;
1165
+ else summary.ambiguousCount += 1;
1031
1166
  }
1032
1167
  }
1033
- return count;
1168
+ return summary;
1034
1169
  }
1035
1170
 
1036
1171
  // src/linker/cross-repo-linker.ts
1037
1172
  function linkWorkspace(db, workspaceId, vars = {}) {
1038
1173
  return db.transaction(() => {
1174
+ const generation = nextGraphGeneration(db, workspaceId);
1039
1175
  db.prepare("DELETE FROM graph_edges WHERE workspace_id=?").run(workspaceId);
1040
- let edges = linkHelperPackages(db, workspaceId);
1041
- let unresolved = 0;
1042
- let resolvedCount = 0;
1043
- let ambiguousCount = 0;
1044
- let dynamicCount = 0;
1045
- let terminalCount = 0;
1046
- const calls = db.prepare(
1047
- `SELECT c.*,r.name repoName,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.is_dynamic isDynamic,b.placeholders_json placeholdersJson,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=?`
1048
- ).all(workspaceId);
1049
- for (const call of calls) {
1050
- const callType = String(call.call_type);
1051
- const op = applyVariables(String(call.operation_path_expr ?? ""), vars);
1052
- const servicePath = applyVariables(
1053
- call.servicePathExpr ?? call.requireServicePath,
1054
- vars
1055
- );
1056
- const destination = call.destinationExpr ?? call.requireDestination;
1057
- const isDynamic = Boolean(Number(call.isDynamic ?? 0));
1058
- const resolution = callType.startsWith("remote") ? resolveOperation(
1059
- db,
1060
- {
1061
- servicePath,
1062
- operationPath: op,
1063
- alias: applyVariables(call.aliasExpr ?? call.alias, vars),
1064
- destination: applyVariables(destination, vars),
1065
- isDynamic,
1066
- hasExplicitOverride: Object.keys(vars).length > 0
1067
- },
1068
- workspaceId
1069
- ) : { status: "unresolved", candidates: [], reasons: [] };
1070
- const target = resolution.target;
1071
- const evidence = {
1072
- sourceFile: call.source_file,
1073
- sourceLine: call.source_line,
1074
- file: call.source_file,
1075
- line: call.source_line,
1076
- repo: call.repoName,
1077
- serviceAlias: call.alias,
1078
- serviceAliasExpr: call.aliasExpr,
1079
- destination: applyVariables(destination, vars),
1080
- servicePath,
1081
- operationPath: op,
1082
- targetRepo: target?.repoName,
1083
- targetOperation: target?.operationName,
1084
- helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0,
1085
- candidates: resolution.candidates,
1086
- candidateCount: resolution.candidates.length,
1087
- resolutionStatus: resolution.status,
1088
- resolutionReasons: resolution.reasons
1089
- };
1090
- if (target) {
1091
- db.prepare(
1092
- "INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic) VALUES(?,?,?,?,?,?,?,?,?,?)"
1093
- ).run(
1094
- workspaceId,
1095
- "REMOTE_CALL_RESOLVES_TO_OPERATION",
1096
- "resolved",
1097
- "call",
1098
- String(call.id),
1099
- "operation",
1100
- String(target.operationId),
1101
- target.score,
1102
- JSON.stringify(evidence),
1103
- isDynamic ? 1 : 0
1104
- );
1105
- edges += 1;
1106
- resolvedCount += 1;
1107
- } else {
1108
- const edgeType = callType === "local_db_query" ? "HANDLER_RUNS_DB_QUERY" : callType === "external_http" ? "HANDLER_CALLS_EXTERNAL_HTTP" : callType === "async_emit" ? "HANDLER_EMITS_EVENT" : callType === "async_subscribe" ? "EVENT_CONSUMED_BY_HANDLER" : resolution.status === "dynamic" ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE";
1109
- const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
1110
- const unresolvedReason = status === "terminal" ? null : String(
1111
- call.unresolved_reason ?? (resolution.status === "ambiguous" ? "Ambiguous operation candidates require a strong service signal" : resolution.status === "dynamic" ? `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ["missing runtime variables"]).join(", ")}` : "No indexed target operation matched")
1112
- );
1113
- db.prepare(
1114
- "INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
1115
- ).run(
1116
- workspaceId,
1117
- edgeType,
1118
- status,
1119
- "call",
1120
- String(call.id),
1121
- callType.startsWith("async_") ? "event" : "external",
1122
- String(call.event_name_expr ?? call.query_entity ?? op ?? call.id),
1123
- Number(call.confidence ?? 0.2),
1124
- JSON.stringify(evidence),
1125
- isDynamic || resolution.status === "dynamic" ? 1 : 0,
1126
- unresolvedReason
1127
- );
1128
- edges += 1;
1129
- unresolved += status === "unresolved" ? 1 : 0;
1130
- ambiguousCount += status === "ambiguous" ? 1 : 0;
1131
- dynamicCount += status === "dynamic" ? 1 : 0;
1132
- terminalCount += status === "terminal" ? 1 : 0;
1133
- }
1134
- }
1135
- return { edgeCount: edges, unresolvedCount: unresolved, resolvedCount, ambiguousCount, dynamicCount, terminalCount };
1176
+ const deps = linkHelperPackages(db, workspaceId, generation);
1177
+ const callSummary = linkCalls(db, workspaceId, vars, generation);
1178
+ const impl = linkImplementations(db, workspaceId, generation);
1179
+ db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
1180
+ return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount };
1136
1181
  });
1137
1182
  }
1183
+ function nextGraphGeneration(db, workspaceId) {
1184
+ const row = db.prepare("SELECT COALESCE(MAX(graph_generation),0) generation FROM repositories WHERE workspace_id=?").get(workspaceId);
1185
+ return Number(row?.generation ?? 0) + 1;
1186
+ }
1187
+ function linkCalls(db, workspaceId, vars, generation) {
1188
+ let edgeCount = 0;
1189
+ let unresolvedCount = 0;
1190
+ let resolvedCount = 0;
1191
+ let ambiguousCount = 0;
1192
+ let dynamicCount = 0;
1193
+ let terminalCount = 0;
1194
+ const calls = db.prepare(`SELECT c.*,r.name repoName,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.is_dynamic isDynamic,b.placeholders_json placeholdersJson,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);
1195
+ for (const call of calls) {
1196
+ const result = insertCallEdge(db, workspaceId, call, vars, generation);
1197
+ edgeCount += 1;
1198
+ resolvedCount += result.status === "resolved" ? 1 : 0;
1199
+ unresolvedCount += result.status === "unresolved" ? 1 : 0;
1200
+ ambiguousCount += result.status === "ambiguous" ? 1 : 0;
1201
+ dynamicCount += result.status === "dynamic" ? 1 : 0;
1202
+ terminalCount += result.status === "terminal" ? 1 : 0;
1203
+ }
1204
+ return { edgeCount, unresolvedCount, resolvedCount, ambiguousCount, dynamicCount, terminalCount };
1205
+ }
1206
+ function insertCallEdge(db, workspaceId, call, vars, generation) {
1207
+ const callType = String(call.call_type);
1208
+ const op = applyVariables(String(call.operation_path_expr ?? ""), vars);
1209
+ const servicePath = applyVariables(call.servicePathExpr ?? call.requireServicePath, vars);
1210
+ const destination = call.destinationExpr ?? call.requireDestination;
1211
+ const isDynamic = Boolean(Number(call.isDynamic ?? 0));
1212
+ const resolution = callType.startsWith("remote") ? resolveOperation(db, { servicePath, operationPath: op, alias: applyVariables(call.aliasExpr ?? call.alias, vars), destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 }, workspaceId) : { status: "unresolved", candidates: [], reasons: [] };
1213
+ const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0);
1214
+ if (resolution.target) {
1215
+ db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify(evidence), isDynamic ? 1 : 0, generation);
1216
+ return { status: "resolved" };
1217
+ }
1218
+ const edgeType = callType === "local_db_query" ? "HANDLER_RUNS_DB_QUERY" : callType === "external_http" ? "HANDLER_CALLS_EXTERNAL_HTTP" : callType === "async_emit" ? "HANDLER_EMITS_EVENT" : callType === "async_subscribe" ? "EVENT_CONSUMED_BY_HANDLER" : resolution.status === "dynamic" ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE";
1219
+ const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
1220
+ const unresolvedReason = status === "terminal" ? null : String(call.unresolved_reason ?? (resolution.status === "ambiguous" ? "Ambiguous operation candidates require a strong service signal" : resolution.status === "dynamic" ? `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ["missing runtime variables"]).join(", ")}` : "No indexed target operation matched"));
1221
+ 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), callType.startsWith("async_") ? "event" : "external", String(call.event_name_expr ?? call.query_entity ?? op ?? call.id), Number(call.confidence ?? 0.2), JSON.stringify(evidence), isDynamic || resolution.status === "dynamic" ? 1 : 0, unresolvedReason, generation);
1222
+ return { status };
1223
+ }
1224
+ function callEvidence(call, resolution, servicePath, op, destination) {
1225
+ return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, targetRepo: resolution.target?.repoName, targetOperation: resolution.target?.operationName, helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0, candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
1226
+ }
1227
+ function linkImplementations(db, workspaceId, generation) {
1228
+ const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,s.service_path servicePath,s.repo_id modelRepoId,r.package_name modelPackage FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=?`).all(workspaceId);
1229
+ let edgeCount = 0;
1230
+ let resolvedCount = 0;
1231
+ let ambiguousCount = 0;
1232
+ for (const operation of operations) {
1233
+ const rows2 = implementationCandidates(db, workspaceId, operation);
1234
+ if (rows2.length === 0) continue;
1235
+ const unique = rows2.length === 1 ? rows2[0] : void 0;
1236
+ db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", unique ? "resolved" : "ambiguous", "operation", String(operation.operationId), unique ? "handler_method" : "handler_method_candidates", unique ? String(unique.methodId) : rows2.map((row) => row.methodId).join(","), unique ? 0.95 : 0.5, JSON.stringify({ servicePath: operation.servicePath, operationPath: operation.operationPath, operationName: operation.operationName, candidates: rows2, evidence: "registered_application_dependency" }), 0, unique ? null : "Ambiguous registered handler implementation candidates", generation);
1237
+ edgeCount += 1;
1238
+ if (unique) resolvedCount += 1;
1239
+ else ambiguousCount += 1;
1240
+ }
1241
+ return { edgeCount, resolvedCount, ambiguousCount };
1242
+ }
1243
+ function implementationCandidates(db, workspaceId, operation) {
1244
+ const rows2 = db.prepare(`SELECT DISTINCT
1245
+ hm.id methodId,
1246
+ hc.id classId,
1247
+ hc.class_name className,
1248
+ hc.source_file sourceFile,
1249
+ hc.source_line sourceLine,
1250
+ hr.repo_id applicationRepoId,
1251
+ hr.registration_file registrationFile,
1252
+ hr.registration_line registrationLine,
1253
+ hr.import_source importSource,
1254
+ handlerRepo.id handlerRepoId,
1255
+ handlerRepo.name handlerRepo,
1256
+ appRepo.name applicationRepo,
1257
+ CASE WHEN appRepo.id=handlerRepo.id THEN 1 ELSE 0 END sameRepoRegistration,
1258
+ CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=CAST(? AS TEXT)) THEN 1 ELSE 0 END appDependsOnModel,
1259
+ CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(handlerRepo.id AS TEXT) AND dep.to_id=CAST(? AS TEXT)) THEN 1 ELSE 0 END handlerDependsOnModel
1260
+ FROM handler_methods hm
1261
+ JOIN handler_classes hc ON hc.id=hm.handler_class_id
1262
+ JOIN repositories handlerRepo ON handlerRepo.id=hc.repo_id
1263
+ JOIN handler_registrations hr ON (hr.handler_class_id=hc.id OR (hr.class_name=hc.class_name AND (hr.repo_id=hc.repo_id OR hr.import_source IS NOT NULL)))
1264
+ JOIN repositories appRepo ON appRepo.id=hr.repo_id
1265
+ WHERE appRepo.workspace_id=?
1266
+ AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=?)`).all(operation.modelRepoId, operation.modelRepoId, workspaceId, normalizedOperation(String(operation.operationPath ?? "")), operation.operationName, operation.operationName);
1267
+ return rows2.filter((row) => Boolean(Number(row.sameRepoRegistration)) || Boolean(Number(row.appDependsOnModel)) || Boolean(Number(row.handlerDependsOnModel)));
1268
+ }
1269
+ function normalizedOperation(value) {
1270
+ return value.startsWith("/") ? value.slice(1) : value;
1271
+ }
1138
1272
 
1139
1273
  // src/trace/trace-engine.ts
1140
1274
  function normalizeOperation(value) {
@@ -1182,8 +1316,10 @@ function startScope(db, start) {
1182
1316
  if (start.repo && !repo) return { repo, selectorMatched: false };
1183
1317
  const sourceFiles = sourceFilesForStart(db, repo?.id, start);
1184
1318
  const hasSelector = Boolean(
1185
- start.handler ?? start.operation ?? start.operationPath
1319
+ start.handler ?? start.operation ?? start.operationPath ?? start.servicePath
1186
1320
  );
1321
+ if (start.servicePath && !start.operation && !start.operationPath && !start.handler)
1322
+ return { repo, selectorMatched: false };
1187
1323
  return {
1188
1324
  repo,
1189
1325
  sourceFiles,
@@ -1204,6 +1340,12 @@ function handlerFilesForOperation(db, operationId) {
1204
1340
  ).all(op.repoId, operation, operation, op.operationName);
1205
1341
  return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
1206
1342
  }
1343
+ function implementationScope(db, operationId) {
1344
+ const edge = db.prepare("SELECT * FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status='resolved' AND from_kind='operation' AND from_id=? ORDER BY id LIMIT 1").get(operationId);
1345
+ if (!edge) return { files: /* @__PURE__ */ new Set() };
1346
+ const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id WHERE hm.id=?").get(edge.to_id);
1347
+ return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []) };
1348
+ }
1207
1349
  function includeCall(type, options) {
1208
1350
  if (!options.includeDb && type === "local_db_query") return false;
1209
1351
  if (!options.includeExternal && type === "external_http") return false;
@@ -1288,11 +1430,14 @@ function trace(db, start, options) {
1288
1430
  const diagnostics = db.prepare(
1289
1431
  "SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)"
1290
1432
  ).all(scope.repo?.id, scope.repo?.id);
1433
+ const stale = db.prepare("SELECT name,graph_stale_reason reason FROM repositories WHERE graph_stale_reason IS NOT NULL AND (? IS NULL OR id=?)").all(scope.repo?.id, scope.repo?.id);
1434
+ for (const row of stale)
1435
+ diagnostics.unshift({ severity: "warning", code: "graph_stale", message: `Graph is stale for ${row.name ?? "repository"}: ${row.reason ?? "facts_changed"}. Run service-flow link.` });
1291
1436
  if (!scope.selectorMatched)
1292
1437
  diagnostics.unshift({
1293
1438
  severity: "warning",
1294
1439
  code: "trace_start_not_found",
1295
- message: "No handler source matched the requested trace start selector"
1440
+ message: start.servicePath && !start.operation && !start.operationPath && !start.handler ? "Service-only trace requires --operation or --path and will not broaden to the whole workspace" : "No handler source matched the requested trace start selector"
1296
1441
  });
1297
1442
  const maxDepth = positiveDepth(options.depth);
1298
1443
  const edges = [];
@@ -1354,9 +1499,10 @@ function trace(db, start, options) {
1354
1499
  unresolvedReason: effective.unresolvedReason
1355
1500
  });
1356
1501
  if (effectiveRow.to_kind === "operation" && current.depth < maxDepth) {
1357
- const files = handlerFilesForOperation(db, effectiveRow.to_id);
1502
+ const implementation = implementationScope(db, effectiveRow.to_id);
1503
+ const files = implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id);
1358
1504
  if (files.size > 0) {
1359
- const targetRepoId = db.prepare(
1505
+ const targetRepoId = implementation.repoId ?? db.prepare(
1360
1506
  "SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
1361
1507
  ).get(effectiveRow.to_id)?.repoId;
1362
1508
  const nextKey = `${targetRepoId ?? "*"}:${[...files].sort().join(",")}`;
@@ -1402,4 +1548,4 @@ export {
1402
1548
  linkWorkspace,
1403
1549
  trace
1404
1550
  };
1405
- //# sourceMappingURL=chunk-6C5HZ6IR.js.map
1551
+ //# sourceMappingURL=chunk-NBAO2R2H.js.map