@saptools/service-flow 0.1.2 → 0.1.3

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.
@@ -369,34 +369,81 @@ function lineOf3(text, idx) {
369
369
  function firstArg2(body, key) {
370
370
  return new RegExp(`${key}\\s*:\\s*([^,}\\n]+)`).exec(body)?.[1]?.trim();
371
371
  }
372
+ function matchingParen(text, open) {
373
+ let depth = 0;
374
+ let quote;
375
+ let escaped = false;
376
+ for (let i = open; i < text.length; i += 1) {
377
+ const ch = text[i] ?? "";
378
+ if (quote) {
379
+ if (escaped) escaped = false;
380
+ else if (ch === "\\") escaped = true;
381
+ else if (ch === quote) quote = void 0;
382
+ continue;
383
+ }
384
+ if (ch === '"' || ch === "'" || ch === "`") {
385
+ quote = ch;
386
+ continue;
387
+ }
388
+ if (ch === "(") depth += 1;
389
+ if (ch === ")") {
390
+ depth -= 1;
391
+ if (depth === 0) return i;
392
+ }
393
+ }
394
+ return -1;
395
+ }
396
+ function argumentForCall(expr, marker) {
397
+ const idx = expr.indexOf(marker);
398
+ if (idx < 0) return void 0;
399
+ const open = expr.indexOf("(", idx + marker.length);
400
+ if (open < 0) return void 0;
401
+ const close = matchingParen(expr, open);
402
+ return close > open ? expr.slice(open + 1, close).trim() : void 0;
403
+ }
404
+ function entityFromArg(arg) {
405
+ if (!arg) return void 0;
406
+ const first = arg.split(",")[0]?.trim();
407
+ if (!first) return void 0;
408
+ return stripQuotes(first).replace(/^this\./, "");
409
+ }
410
+ function extractQueryEntity(expr) {
411
+ return entityFromArg(argumentForCall(expr, "SELECT.one.from")) ?? entityFromArg(argumentForCall(expr, "SELECT.from")) ?? entityFromArg(argumentForCall(expr, "INSERT.into")) ?? entityFromArg(argumentForCall(expr, "UPDATE")) ?? entityFromArg(argumentForCall(expr, "DELETE.from"));
412
+ }
372
413
  async function parseOutboundCalls(repoPath, filePath) {
373
414
  const text = await fs6.readFile(path7.join(repoPath, filePath), "utf8");
374
415
  const out = [];
375
416
  for (const m of text.matchAll(/(\w+)\.send\s*\(\s*\{([\s\S]*?)\}\s*\)/g)) {
376
417
  const body = m[2] ?? "";
377
418
  const query = firstArg2(body, "query");
378
- const op = firstArg2(body, "path");
419
+ const op = firstArg2(body, "path") ?? firstArg2(body, "event");
379
420
  out.push({
380
421
  callType: query ? "remote_query" : "remote_action",
381
422
  serviceVariableName: m[1],
382
423
  method: stripQuotes(firstArg2(body, "method") ?? "POST"),
383
- operationPathExpr: op ? stripQuotes(op) : void 0,
384
- queryEntity: /SELECT(?:\.one)?\.from\(([\w.]+)\)/.exec(query ?? "")?.[1],
424
+ operationPathExpr: op ? `/${stripQuotes(op).replace(/^\//, "")}` : void 0,
425
+ queryEntity: extractQueryEntity(query ?? ""),
385
426
  payloadSummary: summarizeExpression(body),
386
427
  sourceFile: normalizePath(filePath),
387
428
  sourceLine: lineOf3(text, m.index ?? 0),
388
429
  confidence: op || query ? 0.8 : 0.4
389
430
  });
390
431
  }
391
- for (const m of text.matchAll(/cds\.run\s*\(([\s\S]*?)\)/g))
432
+ for (const m of text.matchAll(/cds\.run\s*\(/g)) {
433
+ const open = (m.index ?? 0) + m[0].lastIndexOf("(");
434
+ const close = matchingParen(text, open);
435
+ const expr = close > open ? text.slice(open + 1, close) : "";
436
+ const entity = extractQueryEntity(expr);
392
437
  out.push({
393
438
  callType: "local_db_query",
394
- queryEntity: /(?:SELECT(?:\.one)?\.from|INSERT\.into|UPDATE|DELETE\.from)\(([\w.]+)\)/.exec(m[1] ?? "")?.[1],
395
- payloadSummary: summarizeExpression(m[1] ?? ""),
439
+ queryEntity: entity,
440
+ payloadSummary: summarizeExpression(expr),
396
441
  sourceFile: normalizePath(filePath),
397
442
  sourceLine: lineOf3(text, m.index ?? 0),
398
- confidence: 0.85
443
+ confidence: entity ? 0.9 : 0.55,
444
+ unresolvedReason: entity ? void 0 : "Could not resolve CAP query target entity from nested expression"
399
445
  });
446
+ }
400
447
  for (const m of text.matchAll(
401
448
  /(\w+)\.(emit|publish|on)\s*\(\s*(['"`])([^'"`]+)\3/g
402
449
  ))
@@ -436,67 +483,246 @@ async function parseOutboundCalls(repoPath, filePath) {
436
483
  // src/parsers/service-binding-parser.ts
437
484
  import fs7 from "fs/promises";
438
485
  import path8 from "path";
439
- function lineOf4(text, idx) {
440
- return text.slice(0, idx).split("\n").length;
486
+ import ts3 from "typescript";
487
+ function lineOf4(sf, node) {
488
+ return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
489
+ }
490
+ function stringValue(node) {
491
+ if (!node) return void 0;
492
+ if (ts3.isStringLiteralLike(node) || ts3.isNoSubstitutionTemplateLiteral(node))
493
+ return node.text;
494
+ if (ts3.isTemplateExpression(node))
495
+ return node.getText().replace(/^`|`$/g, "");
496
+ return node.getText();
441
497
  }
442
498
  function placeholders(value) {
443
499
  return [...(value ?? "").matchAll(/\$\{\s*(\w+)\s*\}/g)].map((m) => m[1] ?? "").filter(Boolean);
444
500
  }
501
+ function connectFactFromCall(call) {
502
+ const expr = call.expression;
503
+ if (!ts3.isPropertyAccessExpression(expr) || expr.name.text !== "to")
504
+ return void 0;
505
+ const inner = expr.expression;
506
+ if (!ts3.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds")
507
+ return void 0;
508
+ const first = call.arguments[0];
509
+ if (!first) return void 0;
510
+ if (ts3.isStringLiteralLike(first) || ts3.isNoSubstitutionTemplateLiteral(first))
511
+ return { alias: first.text, isDynamic: false, placeholders: [] };
512
+ if (!ts3.isObjectLiteralExpression(first))
513
+ return {
514
+ servicePathExpr: first.getText(),
515
+ isDynamic: true,
516
+ placeholders: []
517
+ };
518
+ let destinationExpr;
519
+ let servicePathExpr;
520
+ function visitObject(obj) {
521
+ for (const prop of obj.properties) {
522
+ if (!ts3.isPropertyAssignment(prop)) continue;
523
+ const name = ts3.isIdentifier(prop.name) || ts3.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
524
+ if (name === "destination")
525
+ destinationExpr = stringValue(prop.initializer);
526
+ if (name === "path" || name === "servicePath")
527
+ servicePathExpr = stringValue(prop.initializer);
528
+ if (ts3.isObjectLiteralExpression(prop.initializer))
529
+ visitObject(prop.initializer);
530
+ }
531
+ }
532
+ visitObject(first);
533
+ const ph = [
534
+ ...placeholders(destinationExpr),
535
+ ...placeholders(servicePathExpr)
536
+ ];
537
+ return {
538
+ destinationExpr,
539
+ servicePathExpr,
540
+ isDynamic: ph.length > 0 || !destinationExpr && !servicePathExpr,
541
+ placeholders: ph
542
+ };
543
+ }
544
+ function unwrapCall(expr) {
545
+ if (ts3.isAwaitExpression(expr)) return unwrapCall(expr.expression);
546
+ if (ts3.isCallExpression(expr)) return expr;
547
+ return void 0;
548
+ }
549
+ function findConnectInExpression(expr) {
550
+ const direct = unwrapCall(expr);
551
+ if (direct) {
552
+ const fact = connectFactFromCall(direct);
553
+ if (fact) return fact;
554
+ }
555
+ let found;
556
+ function visit(node) {
557
+ if (found) return;
558
+ if (ts3.isCallExpression(node)) found = connectFactFromCall(node);
559
+ if (!found) ts3.forEachChild(node, visit);
560
+ }
561
+ visit(expr);
562
+ return found;
563
+ }
564
+ async function readSource(abs) {
565
+ try {
566
+ const text = await fs7.readFile(abs, "utf8");
567
+ return ts3.createSourceFile(
568
+ abs,
569
+ text,
570
+ ts3.ScriptTarget.Latest,
571
+ true,
572
+ ts3.ScriptKind.TS
573
+ );
574
+ } catch {
575
+ return void 0;
576
+ }
577
+ }
578
+ async function resolveImport(repoPath, fromFile, spec) {
579
+ if (!spec.startsWith(".")) return void 0;
580
+ const base = path8.resolve(repoPath, path8.dirname(fromFile), spec);
581
+ for (const candidate of [
582
+ base,
583
+ `${base}.ts`,
584
+ `${base}.js`,
585
+ path8.join(base, "index.ts"),
586
+ path8.join(base, "index.js")
587
+ ]) {
588
+ try {
589
+ const st = await fs7.stat(candidate);
590
+ if (st.isFile()) return normalizePath(path8.relative(repoPath, candidate));
591
+ } catch {
592
+ }
593
+ }
594
+ return void 0;
595
+ }
596
+ async function importsFor(repoPath, filePath, sf) {
597
+ const imports = [];
598
+ for (const stmt of sf.statements) {
599
+ if (!ts3.isImportDeclaration(stmt) || !ts3.isStringLiteralLike(stmt.moduleSpecifier))
600
+ continue;
601
+ const sourceFile = await resolveImport(
602
+ repoPath,
603
+ filePath,
604
+ stmt.moduleSpecifier.text
605
+ );
606
+ const clause = stmt.importClause;
607
+ if (!clause) continue;
608
+ if (clause.name)
609
+ imports.push({
610
+ localName: clause.name.text,
611
+ exportedName: "default",
612
+ sourceFile
613
+ });
614
+ const bindings = clause.namedBindings;
615
+ if (bindings && ts3.isNamedImports(bindings))
616
+ for (const el of bindings.elements)
617
+ imports.push({
618
+ localName: el.name.text,
619
+ exportedName: el.propertyName?.text ?? el.name.text,
620
+ sourceFile
621
+ });
622
+ }
623
+ return imports;
624
+ }
625
+ async function helperBindings(repoPath, filePath) {
626
+ const sf = await readSource(path8.join(repoPath, filePath));
627
+ if (!sf) return [];
628
+ const sourceFileAst = sf;
629
+ const out = [];
630
+ for (const stmt of sf.statements) {
631
+ const exported = ts3.canHaveModifiers(stmt) ? ts3.getModifiers(stmt)?.some(
632
+ (m) => m.kind === ts3.SyntaxKind.ExportKeyword
633
+ ) ?? false : false;
634
+ if (ts3.isFunctionDeclaration(stmt) && stmt.name) {
635
+ let fact;
636
+ stmt.forEachChild(function visit(node) {
637
+ if (!fact && ts3.isReturnStatement(node) && node.expression)
638
+ fact = findConnectInExpression(node.expression);
639
+ if (!fact) ts3.forEachChild(node, visit);
640
+ });
641
+ if (fact && exported)
642
+ out.push({
643
+ ...fact,
644
+ exportedName: stmt.name.text,
645
+ sourceFile: normalizePath(filePath),
646
+ sourceLine: lineOf4(sf, stmt)
647
+ });
648
+ }
649
+ if (ts3.isVariableStatement(stmt))
650
+ for (const decl of stmt.declarationList.declarations) {
651
+ if (!ts3.isIdentifier(decl.name) || !decl.initializer) continue;
652
+ const fact = findConnectInExpression(decl.initializer);
653
+ if (fact && exported)
654
+ out.push({
655
+ ...fact,
656
+ exportedName: decl.name.text,
657
+ sourceFile: normalizePath(filePath),
658
+ sourceLine: lineOf4(sourceFileAst, decl)
659
+ });
660
+ }
661
+ }
662
+ return out;
663
+ }
445
664
  async function parseServiceBindings(repoPath, filePath) {
446
- const text = await fs7.readFile(path8.join(repoPath, filePath), "utf8");
665
+ const sf = await readSource(path8.join(repoPath, filePath));
666
+ if (!sf) return [];
667
+ const sourceFileAst = sf;
447
668
  const out = [];
448
- for (const m of text.matchAll(
449
- /(?:const|let|this\.)\s*(\w+)\s*=\s*(?:await\s*)?cds\.connect\.to\((['"`])([^'"`]+)\2\)/g
450
- ))
451
- out.push({
452
- variableName: m[1] ?? "service",
453
- alias: m[3],
454
- isDynamic: false,
455
- placeholders: [],
456
- sourceFile: normalizePath(filePath),
457
- sourceLine: lineOf4(text, m.index ?? 0)
458
- });
459
- for (const m of text.matchAll(
460
- /(?:const|let|this\.)\s*(\w+)\s*=\s*(?:await\s*)?cds\.connect\.to\(\{([\s\S]*?)\}\s*\)/g
461
- )) {
462
- const body = m[2] ?? "";
463
- const destination = /destination\s*:\s*([^,\n]+)/.exec(body)?.[1];
464
- const servicePath = /path\s*:\s*([^,\n]+)/.exec(body)?.[1];
465
- const dest = destination ? stripQuotes(destination.trim()) : void 0;
466
- const svc = servicePath ? stripQuotes(servicePath.trim()) : void 0;
467
- const ph = [...placeholders(dest), ...placeholders(svc)];
468
- out.push({
469
- variableName: m[1] ?? "service",
470
- destinationExpr: dest,
471
- servicePathExpr: svc,
472
- isDynamic: ph.length > 0,
473
- placeholders: ph,
474
- sourceFile: normalizePath(filePath),
475
- sourceLine: lineOf4(text, m.index ?? 0)
476
- });
669
+ const imports = await importsFor(repoPath, filePath, sf);
670
+ const helperCache = /* @__PURE__ */ new Map();
671
+ async function importedHelper(localName) {
672
+ const imp = imports.find((i) => i.localName === localName && i.sourceFile);
673
+ if (!imp?.sourceFile) return void 0;
674
+ if (!helperCache.has(imp.sourceFile))
675
+ helperCache.set(
676
+ imp.sourceFile,
677
+ await helperBindings(repoPath, imp.sourceFile)
678
+ );
679
+ const helper = helperCache.get(imp.sourceFile)?.find((h) => h.exportedName === imp.exportedName);
680
+ return helper ? { imp, helper } : void 0;
477
681
  }
478
- for (const m of text.matchAll(
479
- /(?:function\s+(\w+)\s*\([^)]*\)\s*\{[\s\S]*?return|const\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>)\s+(?:await\s*)?cds\.connect\.to\((['"`])([^'"`]+)\3\)/g
480
- ))
481
- out.push({
482
- variableName: m[1] ?? m[2] ?? "connect",
483
- alias: m[4],
484
- isDynamic: false,
485
- placeholders: [],
486
- sourceFile: normalizePath(filePath),
487
- sourceLine: lineOf4(text, m.index ?? 0)
488
- });
489
- const helperAliases = new Map(out.filter((b) => b.alias).map((b) => [b.variableName, b]));
490
- for (const m of text.matchAll(/(?:const|let)\s+(\w+)\s*=\s*(?:await\s*)?(\w+)\s*\(/g)) {
491
- const helper = helperAliases.get(m[2] ?? "");
492
- if (!helper) continue;
493
- out.push({
494
- ...helper,
495
- variableName: m[1] ?? "service",
496
- sourceFile: normalizePath(filePath),
497
- sourceLine: lineOf4(text, m.index ?? 0)
498
- });
682
+ async function recordVariable(decl) {
683
+ if (!ts3.isIdentifier(decl.name) || !decl.initializer) return;
684
+ const call = unwrapCall(decl.initializer);
685
+ if (!call) return;
686
+ const direct = connectFactFromCall(call);
687
+ if (direct)
688
+ out.push({
689
+ variableName: decl.name.text,
690
+ ...direct,
691
+ sourceFile: normalizePath(filePath),
692
+ sourceLine: lineOf4(sourceFileAst, decl)
693
+ });
694
+ else if (ts3.isIdentifier(call.expression)) {
695
+ const resolved = await importedHelper(call.expression.text);
696
+ if (resolved)
697
+ out.push({
698
+ variableName: decl.name.text,
699
+ alias: resolved.helper.alias,
700
+ destinationExpr: resolved.helper.destinationExpr,
701
+ servicePathExpr: resolved.helper.servicePathExpr,
702
+ isDynamic: resolved.helper.isDynamic,
703
+ placeholders: resolved.helper.placeholders,
704
+ sourceFile: normalizePath(filePath),
705
+ sourceLine: lineOf4(sourceFileAst, decl),
706
+ helperChain: [
707
+ {
708
+ callerVariable: decl.name.text,
709
+ importedHelper: call.expression.text,
710
+ importSource: resolved.imp.sourceFile,
711
+ exportedSymbol: resolved.imp.exportedName,
712
+ helperSourceFile: resolved.helper.sourceFile,
713
+ helperSourceLine: resolved.helper.sourceLine
714
+ }
715
+ ]
716
+ });
717
+ }
718
+ }
719
+ const declarations = [];
720
+ function collectDeclarations(node) {
721
+ if (ts3.isVariableDeclaration(node)) declarations.push(node);
722
+ ts3.forEachChild(node, collectDeclarations);
499
723
  }
724
+ collectDeclarations(sourceFileAst);
725
+ for (const decl of declarations) await recordVariable(decl);
500
726
  return out;
501
727
  }
502
728
 
@@ -510,28 +736,82 @@ function applyVariables(template, vars) {
510
736
  }
511
737
 
512
738
  // src/linker/service-resolver.ts
513
- function findOperation(db, servicePath, operationPath, workspaceId) {
514
- if (!operationPath) return void 0;
515
- const row = db.prepare(
516
- `SELECT o.id operationId,r.name repoName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine
517
- FROM cds_operations o
518
- JOIN cds_services s ON s.id=o.service_id
519
- JOIN repositories r ON r.id=s.repo_id
520
- WHERE (? IS NULL OR r.workspace_id=?)
521
- AND (? IS NULL OR s.service_path=?)
522
- AND (o.operation_path=? OR o.operation_name=?)
523
- ORDER BY CASE WHEN s.service_path=? THEN 0 ELSE 1 END
524
- LIMIT 1`
525
- ).get(
739
+ function rows(db, operationPath, workspaceId) {
740
+ return db.prepare(
741
+ `SELECT o.id operationId,r.name repoName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score,'' reasons
742
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
743
+ WHERE (? IS NULL OR r.workspace_id=?) AND (o.operation_path=? OR o.operation_name=?) ORDER BY r.name,s.service_path,o.operation_name`
744
+ ).all(
526
745
  workspaceId,
527
746
  workspaceId,
528
- servicePath,
529
- servicePath,
530
747
  operationPath,
531
- operationPath.replace(/^\//, ""),
532
- servicePath
748
+ operationPath.replace(/^\//, "")
749
+ );
750
+ }
751
+ function resolveOperation(db, signals, workspaceId) {
752
+ if (!signals.operationPath)
753
+ return {
754
+ status: "unresolved",
755
+ candidates: [],
756
+ reasons: ["missing_operation_path"]
757
+ };
758
+ const candidates = rows(db, signals.operationPath, workspaceId).map((c) => ({
759
+ ...c,
760
+ score: 0.2,
761
+ reasons: ["operation_path_match"]
762
+ }));
763
+ if (candidates.length === 0)
764
+ return {
765
+ status: "unresolved",
766
+ candidates: [],
767
+ reasons: ["no_operation_candidates"]
768
+ };
769
+ const hasStrongSignal = Boolean(
770
+ signals.servicePath || signals.alias || signals.destination || signals.hasExplicitOverride
771
+ );
772
+ for (const c of candidates) {
773
+ if (signals.servicePath && c.servicePath === signals.servicePath) {
774
+ c.score += 0.75;
775
+ c.reasons.push("exact_service_path");
776
+ }
777
+ if (signals.servicePath && c.servicePath !== signals.servicePath) {
778
+ c.score -= 0.1;
779
+ c.reasons.push("service_path_mismatch");
780
+ }
781
+ if (signals.hasExplicitOverride) {
782
+ c.score += 0.2;
783
+ c.reasons.push("explicit_dynamic_override");
784
+ }
785
+ }
786
+ candidates.sort(
787
+ (a, b) => b.score - a.score || a.repoName.localeCompare(b.repoName)
533
788
  );
534
- return row;
789
+ const best = candidates[0];
790
+ const second = candidates[1];
791
+ if (signals.isDynamic && !signals.hasExplicitOverride && !signals.servicePath)
792
+ return {
793
+ status: "dynamic",
794
+ candidates,
795
+ reasons: ["dynamic_target_without_override"]
796
+ };
797
+ if (!hasStrongSignal)
798
+ return {
799
+ status: candidates.length > 1 ? "ambiguous" : "unresolved",
800
+ candidates,
801
+ reasons: ["operation_path_only_has_no_strong_target_signal"]
802
+ };
803
+ if (best && best.score >= 0.9 && (!second || best.score - second.score >= 0.25))
804
+ return {
805
+ status: "resolved",
806
+ target: best,
807
+ candidates,
808
+ reasons: best.reasons
809
+ };
810
+ return {
811
+ status: candidates.length > 1 ? "ambiguous" : "unresolved",
812
+ candidates,
813
+ reasons: ["candidate_score_below_resolution_threshold"]
814
+ };
535
815
  }
536
816
 
537
817
  // src/linker/helper-package-linker.ts
@@ -571,7 +851,7 @@ function linkWorkspace(db, workspaceId, vars = {}) {
571
851
  let edges = linkHelperPackages(db, workspaceId);
572
852
  let unresolved = 0;
573
853
  const calls = db.prepare(
574
- `SELECT c.*,r.name repoName,b.alias,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.is_dynamic isDynamic,b.placeholders_json placeholdersJson,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=?`
854
+ `SELECT c.*,r.name repoName,b.alias,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=?`
575
855
  ).all(workspaceId);
576
856
  for (const call of calls) {
577
857
  const callType = String(call.call_type);
@@ -580,16 +860,38 @@ function linkWorkspace(db, workspaceId, vars = {}) {
580
860
  call.servicePathExpr ?? call.requireServicePath,
581
861
  vars
582
862
  );
583
- const target = callType.startsWith("remote") ? findOperation(db, servicePath, op, workspaceId) : void 0;
863
+ const destination = call.destinationExpr ?? call.requireDestination;
864
+ const isDynamic = Boolean(Number(call.isDynamic ?? 0));
865
+ const resolution = callType.startsWith("remote") ? resolveOperation(
866
+ db,
867
+ {
868
+ servicePath,
869
+ operationPath: op,
870
+ alias: call.alias,
871
+ destination,
872
+ isDynamic,
873
+ hasExplicitOverride: Object.keys(vars).length > 0
874
+ },
875
+ workspaceId
876
+ ) : { status: "unresolved", candidates: [], reasons: [] };
877
+ const target = resolution.target;
584
878
  const evidence = {
585
879
  sourceFile: call.source_file,
586
880
  sourceLine: call.source_line,
881
+ file: call.source_file,
882
+ line: call.source_line,
883
+ repo: call.repoName,
587
884
  serviceAlias: call.alias,
588
- destination: call.destinationExpr ?? call.requireDestination,
885
+ destination,
589
886
  servicePath,
590
887
  operationPath: op,
591
888
  targetRepo: target?.repoName,
592
- targetOperation: target?.operationName
889
+ targetOperation: target?.operationName,
890
+ helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0,
891
+ candidates: resolution.candidates,
892
+ candidateCount: resolution.candidates.length,
893
+ resolutionStatus: resolution.status,
894
+ resolutionReasons: resolution.reasons
593
895
  };
594
896
  if (target) {
595
897
  db.prepare(
@@ -601,13 +903,13 @@ function linkWorkspace(db, workspaceId, vars = {}) {
601
903
  String(call.id),
602
904
  "operation",
603
905
  String(target.operationId),
604
- call.isDynamic ? 0.6 : 0.9,
906
+ target.score,
605
907
  JSON.stringify(evidence),
606
- call.isDynamic ? 1 : 0
908
+ isDynamic ? 1 : 0
607
909
  );
608
910
  edges += 1;
609
911
  } else {
610
- 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" : call.isDynamic ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE";
912
+ 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";
611
913
  db.prepare(
612
914
  "INSERT INTO graph_edges(workspace_id,edge_type,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?)"
613
915
  ).run(
@@ -619,8 +921,10 @@ function linkWorkspace(db, workspaceId, vars = {}) {
619
921
  String(call.event_name_expr ?? call.query_entity ?? op ?? call.id),
620
922
  Number(call.confidence ?? 0.2),
621
923
  JSON.stringify(evidence),
622
- call.isDynamic ? 1 : 0,
623
- String(call.unresolved_reason ?? "No indexed target operation matched")
924
+ isDynamic || resolution.status === "dynamic" ? 1 : 0,
925
+ String(
926
+ call.unresolved_reason ?? (resolution.status === "ambiguous" ? "Ambiguous operation candidates require a strong service signal" : resolution.status === "dynamic" ? "Dynamic target requires runtime variable overrides" : "No indexed target operation matched")
927
+ )
624
928
  );
625
929
  edges += 1;
626
930
  unresolved += edgeType === "UNRESOLVED_EDGE" ? 1 : 0;
@@ -641,29 +945,52 @@ function sourceFilesForStart(db, repoId, start) {
641
945
  const handler = start.handler;
642
946
  const operation = normalizeOperation(start.operation ?? start.operationPath);
643
947
  if (!handler && !operation) return void 0;
644
- const rows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile
948
+ const rows2 = db.prepare(
949
+ `SELECT DISTINCT hc.source_file sourceFile
645
950
  FROM handler_classes hc LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id
646
951
  WHERE (? IS NULL OR hc.repo_id=?) AND (? IS NULL OR hc.class_name=? OR hm.method_name=?)
647
- AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)`).all(repoId, repoId, handler, handler, handler, operation, operation, operation);
648
- if (rows.length === 0) return void 0;
649
- return new Set(rows.map((row) => row.sourceFile).filter(Boolean));
952
+ AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)`
953
+ ).all(
954
+ repoId,
955
+ repoId,
956
+ handler,
957
+ handler,
958
+ handler,
959
+ operation,
960
+ operation,
961
+ operation
962
+ );
963
+ if (rows2.length === 0) return void 0;
964
+ return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
650
965
  }
651
966
  function startScope(db, start) {
652
- const repo = start.repo ? db.prepare("SELECT id,name FROM repositories WHERE name=? OR package_name=?").get(start.repo, start.repo) : void 0;
967
+ const repo = start.repo ? db.prepare(
968
+ "SELECT id,name FROM repositories WHERE name=? OR package_name=?"
969
+ ).get(start.repo, start.repo) : void 0;
653
970
  if (start.repo && !repo) return { repo, selectorMatched: false };
654
971
  const sourceFiles = sourceFilesForStart(db, repo?.id, start);
655
- const hasSelector = Boolean(start.handler ?? start.operation ?? start.operationPath);
656
- return { repo, sourceFiles, selectorMatched: !hasSelector || sourceFiles !== void 0 };
972
+ const hasSelector = Boolean(
973
+ start.handler ?? start.operation ?? start.operationPath
974
+ );
975
+ return {
976
+ repo,
977
+ sourceFiles,
978
+ selectorMatched: !hasSelector || sourceFiles !== void 0
979
+ };
657
980
  }
658
981
  function handlerFilesForOperation(db, operationId) {
659
- const op = db.prepare(`SELECT o.operation_name operationName,o.operation_path operationPath,s.repo_id repoId
660
- FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?`).get(operationId);
982
+ const op = db.prepare(
983
+ `SELECT o.operation_name operationName,o.operation_path operationPath,s.repo_id repoId
984
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?`
985
+ ).get(operationId);
661
986
  if (!op) return /* @__PURE__ */ new Set();
662
987
  const operation = normalizeOperation(op.operationPath ?? op.operationName);
663
- const rows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile FROM handler_classes hc
988
+ const rows2 = db.prepare(
989
+ `SELECT DISTINCT hc.source_file sourceFile FROM handler_classes hc
664
990
  JOIN handler_methods hm ON hm.handler_class_id=hc.id
665
- WHERE hc.repo_id=? AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`).all(op.repoId, operation, operation, op.operationName);
666
- return new Set(rows.map((row) => row.sourceFile).filter(Boolean));
991
+ WHERE hc.repo_id=? AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`
992
+ ).all(op.repoId, operation, operation, op.operationName);
993
+ return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
667
994
  }
668
995
  function includeCall(type, options) {
669
996
  if (!options.includeDb && type === "local_db_query") return false;
@@ -674,48 +1001,145 @@ function includeCall(type, options) {
674
1001
  function graphForCalls(db, callIds) {
675
1002
  const map = /* @__PURE__ */ new Map();
676
1003
  if (callIds.length === 0) return map;
677
- const rows = db.prepare(`SELECT * FROM graph_edges WHERE from_kind='call' AND from_id IN (${callIds.map(() => "?").join(",")}) ORDER BY id`).all(...callIds);
678
- for (const row of rows) {
1004
+ const rows2 = db.prepare(
1005
+ `SELECT * FROM graph_edges WHERE from_kind='call' AND from_id IN (${callIds.map(() => "?").join(",")}) ORDER BY id`
1006
+ ).all(...callIds);
1007
+ for (const row of rows2) {
679
1008
  const id = Number(row.from_id);
680
1009
  map.set(id, [...map.get(id) ?? [], row]);
681
1010
  }
682
1011
  return map;
683
1012
  }
1013
+ function candidatesFromEvidence(evidence) {
1014
+ return Array.isArray(evidence.candidates) ? evidence.candidates.filter(
1015
+ (candidate) => typeof candidate === "object" && candidate !== null
1016
+ ) : [];
1017
+ }
1018
+ function evidenceWithRuntimeVariables(evidence, vars) {
1019
+ if (!vars || Object.keys(vars).length === 0) return evidence;
1020
+ const servicePath = typeof evidence.servicePath === "string" ? applyVariables(evidence.servicePath, vars) : void 0;
1021
+ const operationPath = typeof evidence.operationPath === "string" ? applyVariables(evidence.operationPath, vars) : void 0;
1022
+ const candidates = candidatesFromEvidence(evidence);
1023
+ const matched = servicePath ? candidates.find((candidate) => candidate.servicePath === servicePath) : void 0;
1024
+ return {
1025
+ ...evidence,
1026
+ servicePath: servicePath ?? evidence.servicePath,
1027
+ operationPath: operationPath ?? evidence.operationPath,
1028
+ runtimeVariablesApplied: true,
1029
+ runtimeResolvedCandidate: matched ? {
1030
+ repoName: matched.repoName,
1031
+ servicePath: matched.servicePath,
1032
+ operationPath: matched.operationPath,
1033
+ operationName: matched.operationName,
1034
+ score: matched.score
1035
+ } : void 0
1036
+ };
1037
+ }
1038
+ function edgeTarget(row, evidence) {
1039
+ const runtimeCandidate = evidence.runtimeResolvedCandidate;
1040
+ if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath)
1041
+ return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
1042
+ const servicePath = typeof evidence.servicePath === "string" ? evidence.servicePath : void 0;
1043
+ const operationPath = typeof evidence.operationPath === "string" ? evidence.operationPath : void 0;
1044
+ const targetOperation = typeof evidence.targetOperation === "string" ? evidence.targetOperation : void 0;
1045
+ const targetRepo = typeof evidence.targetRepo === "string" ? evidence.targetRepo : "";
1046
+ return servicePath && operationPath ? `${servicePath}${operationPath}` : targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
1047
+ }
684
1048
  function trace(db, start, options) {
685
1049
  const scope = startScope(db, start);
686
- const diagnostics = db.prepare("SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)").all(scope.repo?.id, scope.repo?.id);
687
- if (!scope.selectorMatched) diagnostics.unshift({ severity: "warning", code: "trace_start_not_found", message: "No handler source matched the requested trace start selector" });
1050
+ const diagnostics = db.prepare(
1051
+ "SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)"
1052
+ ).all(scope.repo?.id, scope.repo?.id);
1053
+ if (!scope.selectorMatched)
1054
+ diagnostics.unshift({
1055
+ severity: "warning",
1056
+ code: "trace_start_not_found",
1057
+ message: "No handler source matched the requested trace start selector"
1058
+ });
688
1059
  const maxDepth = positiveDepth(options.depth);
689
1060
  const edges = [];
690
1061
  const nodes = /* @__PURE__ */ new Map();
691
1062
  const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, depth: 1 }] : [];
692
1063
  const seenScopes = /* @__PURE__ */ new Set();
1064
+ const seenEdges = /* @__PURE__ */ new Set();
693
1065
  while (queue.length > 0) {
694
1066
  const current = queue.shift();
695
1067
  if (!current || current.depth > maxDepth) continue;
696
- const key = `${current.repoId ?? "*"}:${[...current.files ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${current.depth}`;
1068
+ const key = `${current.repoId ?? "*"}:${[...current.files ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}`;
697
1069
  if (seenScopes.has(key)) continue;
698
1070
  seenScopes.add(key);
699
- const calls = db.prepare(`SELECT c.*,r.name repoName FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?) ORDER BY c.source_file,c.source_line`).all(current.repoId, current.repoId);
700
- const filtered = calls.filter((c) => (!current.files || current.files.has(String(c.source_file))) && includeCall(String(c.call_type), options));
701
- const graph = graphForCalls(db, filtered.map((c) => Number(c.id)));
1071
+ const calls = db.prepare(
1072
+ `SELECT c.*,r.name repoName FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?) ORDER BY c.source_file,c.source_line`
1073
+ ).all(current.repoId, current.repoId);
1074
+ const filtered = calls.filter(
1075
+ (c) => (!current.files || current.files.has(String(c.source_file))) && includeCall(String(c.call_type), options)
1076
+ );
1077
+ const graph = graphForCalls(
1078
+ db,
1079
+ filtered.map((c) => Number(c.id))
1080
+ );
702
1081
  for (const call of filtered) {
703
1082
  const callNode = `call:${call.id}`;
704
- nodes.set(callNode, { id: callNode, kind: "outbound_call", repo: call.repoName, file: call.source_file, line: call.source_line, callType: call.call_type });
1083
+ nodes.set(callNode, {
1084
+ id: callNode,
1085
+ kind: "outbound_call",
1086
+ repo: call.repoName,
1087
+ file: call.source_file,
1088
+ line: call.source_line,
1089
+ callType: call.call_type
1090
+ });
705
1091
  const graphRows = graph.get(Number(call.id)) ?? [];
706
1092
  for (const row of graphRows) {
707
- const evidence = JSON.parse(String(row.evidence_json || "{}"));
1093
+ if (seenEdges.has(Number(row.id))) continue;
1094
+ seenEdges.add(Number(row.id));
1095
+ const rawEvidence = JSON.parse(
1096
+ String(row.evidence_json || "{}")
1097
+ );
1098
+ const evidence = evidenceWithRuntimeVariables(
1099
+ rawEvidence,
1100
+ options.vars
1101
+ );
708
1102
  const targetNode = `${row.to_kind}:${row.to_id}`;
709
- nodes.set(targetNode, { id: targetNode, kind: row.to_kind, label: row.to_id, ...evidence });
710
- const servicePath = typeof evidence.servicePath === "string" ? evidence.servicePath : void 0;
711
- const operationPath = typeof evidence.operationPath === "string" ? evidence.operationPath : void 0;
712
- const targetOperation = typeof evidence.targetOperation === "string" ? evidence.targetOperation : void 0;
713
- const targetRepo = typeof evidence.targetRepo === "string" ? evidence.targetRepo : "";
714
- const to = servicePath && operationPath ? `${servicePath}${operationPath}` : targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
715
- edges.push({ step: current.depth, type: String(call.call_type), from: `${call.repoName}:${call.source_file}`, to, evidence, confidence: Number(row.confidence ?? call.confidence), unresolvedReason: row.unresolved_reason });
1103
+ nodes.set(targetNode, {
1104
+ id: targetNode,
1105
+ kind: row.to_kind,
1106
+ label: row.to_id,
1107
+ ...evidence
1108
+ });
1109
+ const to = edgeTarget(row, evidence);
1110
+ edges.push({
1111
+ step: current.depth,
1112
+ type: String(call.call_type),
1113
+ from: `${call.repoName}:${call.source_file}`,
1114
+ to,
1115
+ evidence,
1116
+ confidence: Number(row.confidence ?? call.confidence),
1117
+ unresolvedReason: row.unresolved_reason
1118
+ });
716
1119
  if (row.to_kind === "operation" && current.depth < maxDepth) {
717
1120
  const files = handlerFilesForOperation(db, row.to_id);
718
- if (files.size > 0) queue.push({ files, depth: current.depth + 1 });
1121
+ if (files.size > 0) {
1122
+ const targetRepoId = db.prepare(
1123
+ "SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
1124
+ ).get(row.to_id)?.repoId;
1125
+ const nextKey = `${targetRepoId ?? "*"}:${[...files].sort().join(",")}`;
1126
+ if (seenScopes.has(nextKey))
1127
+ edges.push({
1128
+ step: current.depth + 1,
1129
+ type: "cycle",
1130
+ from: to,
1131
+ to: nextKey,
1132
+ evidence: { ...evidence, cycle: true },
1133
+ confidence: 1,
1134
+ unresolvedReason: "Cycle detected; downstream scope already visited"
1135
+ });
1136
+ else
1137
+ queue.push({
1138
+ repoId: targetRepoId,
1139
+ files,
1140
+ depth: current.depth + 1
1141
+ });
1142
+ }
719
1143
  }
720
1144
  }
721
1145
  }
@@ -739,4 +1163,4 @@ export {
739
1163
  linkWorkspace,
740
1164
  trace
741
1165
  };
742
- //# sourceMappingURL=chunk-6U4QKQEL.js.map
1166
+ //# sourceMappingURL=chunk-SIKIGT42.js.map