@saptools/service-flow 0.1.44 → 0.1.46

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.
@@ -577,1518 +577,1716 @@ function summarizeExpression(text) {
577
577
  return redactText(text).slice(0, 240);
578
578
  }
579
579
 
580
- // src/parsers/outbound-call-parser.ts
580
+ // src/parsers/service-binding-parser.ts
581
+ import path8 from "path";
582
+ import ts5 from "typescript";
583
+
584
+ // src/parsers/service-binding-parser-helpers.ts
581
585
  import fs6 from "fs/promises";
582
586
  import path7 from "path";
583
587
  import ts4 from "typescript";
584
-
585
- // src/linker/external-http-target.ts
586
- import { createHash } from "crypto";
587
- var sensitiveKeys = /* @__PURE__ */ new Set(["token", "access_token", "id_token", "api_key", "apikey", "key", "password", "passwd", "pwd", "secret", "client_secret", "authorization", "cookie", "signature"]);
588
- function hash(value) {
589
- return createHash("sha256").update(value).digest("hex").slice(0, 12);
588
+ function lineOf3(sf, node) {
589
+ return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
590
590
  }
591
- function methodPrefix(method) {
592
- return typeof method === "string" && method.length > 0 ? `${method.toUpperCase()} ` : "";
591
+ function stringValue(node) {
592
+ if (!node) return void 0;
593
+ if (ts4.isStringLiteralLike(node) || ts4.isNoSubstitutionTemplateLiteral(node)) return node.text;
594
+ if (ts4.isTemplateExpression(node)) return node.getText().replace(/^`|`$/g, "");
595
+ return node.getText();
593
596
  }
594
- function redactUrl(value) {
595
- try {
596
- const url = new URL(value, value.startsWith("/") ? "https://relative.invalid" : void 0);
597
- url.username = "";
598
- url.password = "";
599
- for (const key of [...url.searchParams.keys()]) url.searchParams.set(key, sensitiveKeys.has(key.toLowerCase()) ? "<redacted>" : "<redacted>");
600
- const path10 = `${url.pathname}${url.search ? url.search : ""}`;
601
- return value.startsWith("/") ? path10 : `${url.origin}${path10}`;
602
- } catch {
603
- return value.replace(/([?&][^=;&]*(?:token|key|password|secret|cookie|authorization)[^=;&]*=)[^&]*/gi, "$1<redacted>");
597
+ function placeholders(value) {
598
+ return [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((m) => (m[1] ?? "").trim()).filter(Boolean);
599
+ }
600
+ function connectFactFromCall(call) {
601
+ const expr = call.expression;
602
+ if (!ts4.isPropertyAccessExpression(expr) || expr.name.text !== "to") return void 0;
603
+ const inner = expr.expression;
604
+ if (!ts4.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds") return void 0;
605
+ const first = call.arguments[0];
606
+ if (!first) return void 0;
607
+ const second = call.arguments[1];
608
+ const objectArg = ts4.isObjectLiteralExpression(first) ? first : second && ts4.isObjectLiteralExpression(second) ? second : void 0;
609
+ let alias;
610
+ let aliasExpr;
611
+ if (ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first)) alias = first.text;
612
+ else if (!ts4.isObjectLiteralExpression(first)) aliasExpr = stringValue(first);
613
+ if ((ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first)) && !objectArg) return { alias: first.text, isDynamic: false, placeholders: [] };
614
+ if (!objectArg && aliasExpr) return { aliasExpr, isDynamic: true, placeholders: placeholders(aliasExpr) };
615
+ const expressions = objectArg ? objectExpressions(objectArg) : {};
616
+ const ph = [...placeholders(aliasExpr ?? alias), ...placeholders(expressions.destinationExpr), ...placeholders(expressions.servicePathExpr)];
617
+ return { alias, aliasExpr, ...expressions, isDynamic: ph.length > 0 || !expressions.destinationExpr && !expressions.servicePathExpr, placeholders: ph };
618
+ }
619
+ function objectExpressions(objectArg) {
620
+ const out = {};
621
+ function visitObject(obj) {
622
+ for (const prop of obj.properties) {
623
+ if (!ts4.isPropertyAssignment(prop)) continue;
624
+ const name = ts4.isIdentifier(prop.name) || ts4.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
625
+ if (name === "destination") out.destinationExpr = stringValue(prop.initializer);
626
+ if (name === "path" || name === "servicePath") out.servicePathExpr = stringValue(prop.initializer);
627
+ if (ts4.isObjectLiteralExpression(prop.initializer)) visitObject(prop.initializer);
628
+ }
604
629
  }
630
+ visitObject(objectArg);
631
+ return out;
605
632
  }
606
- function externalHttpTarget(call) {
607
- const evidence = typeof call.evidence_json === "string" ? safeParse(call.evidence_json) : {};
608
- const target = evidence.externalTarget && typeof evidence.externalTarget === "object" && !Array.isArray(evidence.externalTarget) ? evidence.externalTarget : {};
609
- const method = typeof call.method === "string" ? call.method : typeof target.method === "string" ? target.method : void 0;
610
- const kind = typeof target.kind === "string" ? target.kind : "unknown";
611
- const expression = typeof target.expression === "string" ? target.expression : void 0;
612
- if (kind === "destination" && target.dynamic === true) {
613
- const shape = typeof target.expressionShape === "string" ? target.expressionShape : "expression";
614
- const candidates = Array.isArray(target.candidateLiterals) ? target.candidateLiterals.filter((item) => typeof item === "string") : [];
615
- return { kind, toKind: "external_destination", toId: `destination:dynamic:${hash(`${shape}:${candidates.join("|")}`)}`, label: "External destination: dynamic destination", method, dynamic: true, expression: candidates.length ? `candidates:${candidates.join("|")}` : `shape:${shape}` };
633
+ function unwrapCall(expr) {
634
+ if (ts4.isAwaitExpression(expr)) return unwrapCall(expr.expression);
635
+ if (ts4.isParenthesizedExpression(expr)) return unwrapCall(expr.expression);
636
+ if (ts4.isAsExpression(expr) || ts4.isSatisfiesExpression(expr)) return unwrapCall(expr.expression);
637
+ if (ts4.isTypeAssertionExpression(expr)) return unwrapCall(expr.expression);
638
+ if (ts4.isCallExpression(expr)) return expr;
639
+ return void 0;
640
+ }
641
+ function unwrapIdentityExpression(expr) {
642
+ if (ts4.isAwaitExpression(expr)) return unwrapIdentityExpression(expr.expression);
643
+ if (ts4.isParenthesizedExpression(expr)) return unwrapIdentityExpression(expr.expression);
644
+ if (ts4.isAsExpression(expr) || ts4.isSatisfiesExpression(expr)) return unwrapIdentityExpression(expr.expression);
645
+ if (ts4.isTypeAssertionExpression(expr)) return unwrapIdentityExpression(expr.expression);
646
+ return expr;
647
+ }
648
+ function transactionReceiverName(expr) {
649
+ const call = unwrapCall(expr);
650
+ if (call && ts4.isPropertyAccessExpression(call.expression) && ["tx", "transaction"].includes(call.expression.name.text) && ts4.isIdentifier(call.expression.expression)) return call.expression.expression.text;
651
+ const unwrapped = unwrapIdentityExpression(expr);
652
+ if (!ts4.isConditionalExpression(unwrapped)) return void 0;
653
+ const left = transactionReceiverName(unwrapped.whenTrue);
654
+ const right = transactionReceiverName(unwrapped.whenFalse);
655
+ return left && left === right ? left : void 0;
656
+ }
657
+ function findConnectInExpression(expr) {
658
+ const direct = unwrapCall(expr);
659
+ if (direct) {
660
+ const fact = connectFactFromCall(direct);
661
+ if (fact) return fact;
616
662
  }
617
- if (kind === "destination" && expression) return { kind, toKind: "external_destination", toId: `destination:${expression}`, label: `External destination: ${expression}`, method, dynamic: false, expression };
618
- if (kind === "static_url" && expression) {
619
- const redacted = redactUrl(expression);
620
- return { kind, toKind: "external_endpoint", toId: `endpoint:${hash(`${method ?? ""}:${redacted}`)}`, label: `External endpoint: ${methodPrefix(method)}${redacted}`, method, dynamic: false, expression: redacted };
663
+ let found;
664
+ function visit(node) {
665
+ if (found) return;
666
+ if (ts4.isCallExpression(node)) found = connectFactFromCall(node);
667
+ if (!found) ts4.forEachChild(node, visit);
621
668
  }
622
- if (kind === "url_expression" && expression) return { kind, toKind: "external_endpoint", toId: `dynamic:${hash(expression)}`, label: `External endpoint: ${methodPrefix(method)}dynamic URL`, method, dynamic: true, expression: `expr:${hash(expression)}` };
623
- return { kind: "unknown", toKind: "external_endpoint", toId: "unknown", label: "External endpoint: unknown", method, dynamic: false };
669
+ visit(expr);
670
+ return found;
624
671
  }
625
- function safeParse(value) {
672
+ async function readSource(abs) {
626
673
  try {
627
- const parsed = JSON.parse(value);
628
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
674
+ const text = await fs6.readFile(abs, "utf8");
675
+ return ts4.createSourceFile(abs, text, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
629
676
  } catch {
630
- return {};
677
+ return void 0;
631
678
  }
632
679
  }
633
-
634
- // src/linker/odata-path-normalizer.ts
635
- function normalizeODataOperationInvocationPath(path10) {
636
- if (path10 === void 0) return void 0;
637
- const raw = path10.trim();
638
- if (!raw) return void 0;
639
- const rejected = (reason) => ({ rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false, invocationArgumentPlaceholderKeys: [], normalizationRejectedReason: reason });
640
- const open = raw.indexOf("(");
641
- if (open < 0) return rejected("no_top_level_parenthesis");
642
- const query = topLevelQueryIndex(raw);
643
- if (query >= 0) return rejected("query_string_paths_are_not_operation_invocations");
644
- if (!raw.startsWith("/")) return rejected("path_is_not_absolute");
645
- if (raw.slice(1, open).includes("/")) return rejected("operation_segment_contains_navigation_separator");
646
- const close = matchingClose(raw, open);
647
- if (close === void 0) return rejected("top_level_invocation_parenthesis_is_unbalanced");
648
- if (raw.slice(close + 1).trim().length > 0) return rejected("top_level_invocation_does_not_cover_remaining_path");
649
- const operationSegment = raw.slice(0, open).trim();
650
- if (operationSegment.length <= 1) return rejected("operation_segment_is_empty");
651
- return {
652
- rawOperationPath: raw,
653
- normalizedOperationPath: operationSegment,
654
- wasInvocation: true,
655
- invocationArgumentPlaceholderKeys: [...new Set(extractTemplatePlaceholders(raw.slice(open + 1, close)))],
656
- normalizationReason: "balanced_top_level_operation_invocation"
657
- };
680
+ async function resolveImport(repoPath, fromFile, spec) {
681
+ if (!spec.startsWith(".")) return void 0;
682
+ const rawBase = path7.resolve(repoPath, path7.dirname(fromFile), spec);
683
+ const parsed = path7.parse(rawBase);
684
+ const base = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"].includes(parsed.ext) ? path7.join(parsed.dir, parsed.name) : rawBase;
685
+ for (const candidate of [base, `${base}.ts`, `${base}.js`, path7.join(base, "index.ts"), path7.join(base, "index.js")]) {
686
+ const stat = await fs6.stat(candidate).catch(() => void 0);
687
+ if (stat?.isFile()) return normalizePath(path7.relative(repoPath, candidate));
688
+ }
689
+ return void 0;
658
690
  }
659
- function classifyODataPathIntent(path10, method) {
660
- const rawPath = (path10 ?? "").trim();
661
- const normalizedMethod = (method ?? "GET").trim().toUpperCase() || "GET";
662
- const queryIndex = rawPath.indexOf("?");
663
- const pathWithoutQuery = queryIndex >= 0 ? rawPath.slice(0, queryIndex) : rawPath;
664
- const queryString = queryIndex >= 0 ? rawPath.slice(queryIndex + 1) : void 0;
665
- const segments = pathWithoutQuery.replace(/^\//, "").split("/").filter(Boolean);
666
- const firstSegment = segments[0] ?? "";
667
- const hasNavigationSegments = segments.length > 1;
668
- const entitySegment = entitySegmentFromPath(pathWithoutQuery);
669
- const placeholderKeys = [...new Set(extractTemplatePlaceholders(rawPath))];
670
- const firstOpen = firstSegment.indexOf("(");
671
- const firstClose = firstOpen >= 0 ? matchingClose(firstSegment, firstOpen) : void 0;
672
- const keyPredicateText = firstOpen >= 0 && firstClose !== void 0 ? firstSegment.slice(firstOpen + 1, firstClose) : "";
673
- const rawKeyPredicatePlaceholderKeys = [...new Set(extractTemplatePlaceholders(keyPredicateText))];
674
- const navigationSuffix = hasNavigationSegments ? segments.slice(1).join("/") : void 0;
675
- const lastSegment = segments.at(-1) ?? "";
676
- const mediaOrPropertySuffix = hasNavigationSegments ? lastSegment : void 0;
677
- const invocation = normalizeODataOperationInvocationPath(pathWithoutQuery);
678
- const operationNameFromInvocation = invocation?.wasInvocation ? invocation.normalizedOperationPath.replace(/^\//, "").split(".").at(-1) : void 0;
679
- const topLevelName = firstSegment.split("(")[0]?.replace(/^\//, "");
680
- const topLevelOperationName = operationNameFromInvocation ?? (!hasNavigationSegments && !firstSegment.includes("(") ? topLevelName : void 0);
681
- const topLevelOperationInvocation = Boolean(invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment));
682
- const keyPredicatePlaceholderKeys = topLevelOperationInvocation ? [] : rawKeyPredicatePlaceholderKeys;
683
- const topLevelOperationNameCandidate = Boolean(topLevelOperationName && !hasNavigationSegments && !firstSegment.includes("("));
684
- const base = { rawPath, method: normalizedMethod, pathWithoutQuery, queryString, hasQueryString: queryIndex >= 0, entitySegment, placeholderKeys, keyPredicatePlaceholderKeys, invocationArgumentPlaceholderKeys: invocation?.invocationArgumentPlaceholderKeys ?? [], navigationSuffix, mediaOrPropertySuffix, hasEntityKeyPredicate: firstOpen >= 0 && firstClose !== void 0, hasNavigationSuffix: hasNavigationSegments, hasMediaOrPropertySuffix: Boolean(mediaOrPropertySuffix && isMediaOrPropertySuffix(mediaOrPropertySuffix)), topLevelOperationName, topLevelOperationNameCandidate, topLevelOperationInvocation };
685
- if (!rawPath || !rawPath.startsWith("/")) return { ...base, kind: "unknown", reason: "path_missing_or_not_absolute" };
686
- const upperEntityLike = /^[A-Z][A-Za-z0-9_]*$/.test(entitySegment ?? firstSegment);
687
- const mediaLike = isMediaOrPropertySuffix(segments.at(-1) ?? "");
688
- if (normalizedMethod !== "GET") {
689
- if (invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "operation_invocation", reason: "non_get_balanced_top_level_operation_invocation" };
690
- if (mediaLike) return { ...base, kind: "entity_media", reason: "non_get_entity_media_stream_path" };
691
- if (hasNavigationSegments || firstSegment.includes("(")) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: firstSegment.includes("(") ? "non_get_entity_key_or_navigation_path_shape" : "non_get_entity_navigation_path_shape" };
692
- if (upperEntityLike) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: "non_get_entity_path_shape" };
693
- return { ...base, kind: "operation_invocation", reason: "non_get_lowercase_path_may_be_operation" };
691
+ async function importsFor(repoPath, filePath, sf) {
692
+ const imports = [];
693
+ for (const stmt of sf.statements) {
694
+ if (!ts4.isImportDeclaration(stmt) || !ts4.isStringLiteralLike(stmt.moduleSpecifier)) continue;
695
+ const sourceFile = await resolveImport(repoPath, filePath, stmt.moduleSpecifier.text);
696
+ const clause = stmt.importClause;
697
+ if (!clause) continue;
698
+ if (clause.name) imports.push({ localName: clause.name.text, exportedName: "default", sourceFile });
699
+ const bindings = clause.namedBindings;
700
+ if (bindings && ts4.isNamedImports(bindings))
701
+ for (const el of bindings.elements) imports.push({ localName: el.name.text, exportedName: el.propertyName?.text ?? el.name.text, sourceFile });
694
702
  }
695
- if (queryIndex >= 0) {
696
- if (hasNavigationSegments) return { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_and_query_string" };
697
- if (looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "unknown", reason: "get_invocation_with_query_string_requires_indexed_operation_evidence" };
698
- return { ...base, kind: "entity_query", reason: "get_collection_path_has_query_string" };
703
+ return imports;
704
+ }
705
+
706
+ // src/parsers/service-binding-parser.ts
707
+ function collectLocalBindingFacts(fn) {
708
+ const bindings = /* @__PURE__ */ new Map();
709
+ function visit(node) {
710
+ if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
711
+ return;
712
+ if (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
713
+ const fact = findConnectInExpression(node.initializer);
714
+ if (fact) bindings.set(node.name.text, fact);
715
+ const sourceName = transactionReceiverName(node.initializer);
716
+ if (sourceName) {
717
+ const source = bindings.get(sourceName);
718
+ if (source) bindings.set(node.name.text, { ...source, helperChain: [...source.helperChain ?? [], { aliasOf: sourceName, callerVariable: node.name.text, aliasKind: "transaction", transactionAliasSource: sourceName }] });
719
+ }
720
+ }
721
+ ts5.forEachChild(node, visit);
699
722
  }
700
- if (hasNavigationSegments) return mediaLike ? { ...base, kind: "entity_media", reason: "get_entity_media_stream_path" } : { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_segments" };
701
- if (firstSegment.includes("(")) {
702
- if (invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "operation_invocation", reason: "get_balanced_top_level_operation_invocation" };
703
- return looksLikeLowerCamelInvocation(firstSegment) ? { ...base, kind: "operation_invocation", reason: "get_single_lower_camel_segment_has_top_level_invocation" } : { ...base, kind: "entity_key_read", reason: "get_entity_segment_has_key_predicate" };
723
+ visit(fn);
724
+ return bindings;
725
+ }
726
+ function collectReturnedObjectBindings(fn) {
727
+ const bindings = collectLocalBindingFacts(fn);
728
+ const returns = /* @__PURE__ */ new Map();
729
+ function visit(node) {
730
+ if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
731
+ return;
732
+ if (ts5.isReturnStatement(node) && node.expression && ts5.isObjectLiteralExpression(node.expression)) {
733
+ for (const prop of node.expression.properties) {
734
+ if (ts5.isShorthandPropertyAssignment(prop)) returns.set(prop.name.text, prop.name.text);
735
+ if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.initializer)) {
736
+ const propertyName2 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
737
+ if (propertyName2) returns.set(propertyName2, prop.initializer.text);
738
+ }
739
+ }
740
+ }
741
+ ts5.forEachChild(node, visit);
704
742
  }
705
- if (/^[A-Z][A-Za-z0-9_]*$/.test(firstSegment)) return { ...base, kind: "entity_candidate", reason: "uppercase_collection_segment_without_indexed_entity_evidence" };
706
- return { ...base, kind: "unknown", reason: "get_path_has_no_query_key_or_navigation_signal" };
743
+ visit(fn);
744
+ const out = /* @__PURE__ */ new Map();
745
+ for (const [propertyName2, variableName] of returns) {
746
+ const fact = bindings.get(variableName);
747
+ if (fact) out.set(propertyName2, fact);
748
+ }
749
+ return out;
707
750
  }
708
- function entitySegmentFromPath(path10) {
709
- const first = path10.replace(/^\//, "").split("/")[0]?.trim();
710
- if (!first) return void 0;
711
- const open = first.indexOf("(");
712
- const entity = (open >= 0 ? first.slice(0, open) : first).trim();
713
- return entity || void 0;
751
+ function functionLikeInitializer(expr) {
752
+ if (!expr) return void 0;
753
+ if (ts5.isArrowFunction(expr) || ts5.isFunctionExpression(expr)) return expr;
754
+ return void 0;
714
755
  }
715
- function isMediaOrPropertySuffix(segment) {
716
- return ["file", "content", "$value", "metadata", "items"].includes(segment.toLowerCase());
756
+ function directReturnConnectFact(fn) {
757
+ const localBindings = collectLocalBindingFacts(fn);
758
+ let returned;
759
+ function visit(node) {
760
+ if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
761
+ return;
762
+ if (!returned && ts5.isReturnStatement(node) && node.expression)
763
+ returned = node.expression;
764
+ if (!returned) ts5.forEachChild(node, visit);
765
+ }
766
+ visit(fn);
767
+ if (!returned) return void 0;
768
+ if (ts5.isIdentifier(returned)) return localBindings.get(returned.text);
769
+ return findConnectInExpression(returned);
717
770
  }
718
- function looksLikeLowerCamelInvocation(segment) {
719
- const open = segment.indexOf("(");
720
- if (open <= 0) return false;
721
- const name = segment.slice(0, open).split(".").at(-1) ?? segment.slice(0, open);
722
- return /^[a-z][A-Za-z0-9_]*$/.test(name);
771
+ function directConnectFactFromFunctionLike(fn) {
772
+ if (ts5.isArrowFunction(fn) && fn.body && !ts5.isBlock(fn.body))
773
+ return findConnectInExpression(fn.body);
774
+ return directReturnConnectFact(fn);
723
775
  }
724
- function extractTemplatePlaceholders(text) {
725
- const keys = [];
726
- for (let index = 0; index < text.length - 1; index += 1) {
727
- if (text[index] !== "$" || text[index + 1] !== "{") continue;
728
- const close = matchingPlaceholderClose(text, index + 1);
729
- if (close === void 0) continue;
730
- const key = text.slice(index + 2, close).trim();
731
- if (key) keys.push(key);
732
- index = close;
776
+ function exportedLocalNames(sf) {
777
+ const exports = /* @__PURE__ */ new Map();
778
+ for (const stmt of sf.statements) {
779
+ const direct = ts5.canHaveModifiers(stmt) ? ts5.getModifiers(stmt)?.some(
780
+ (m) => m.kind === ts5.SyntaxKind.ExportKeyword
781
+ ) ?? false : false;
782
+ if (direct && ts5.isFunctionDeclaration(stmt) && stmt.name)
783
+ exports.set(stmt.name.text, stmt.name.text);
784
+ if (direct && ts5.isVariableStatement(stmt)) {
785
+ for (const decl of stmt.declarationList.declarations)
786
+ if (ts5.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
787
+ }
788
+ if (!ts5.isExportDeclaration(stmt) || !stmt.exportClause) continue;
789
+ if (!ts5.isNamedExports(stmt.exportClause)) continue;
790
+ for (const el of stmt.exportClause.elements)
791
+ exports.set(el.name.text, el.propertyName?.text ?? el.name.text);
733
792
  }
734
- return keys;
793
+ return exports;
735
794
  }
736
- function matchingClose(text, openIndex) {
737
- let depth = 0;
738
- let quote;
739
- for (let index = openIndex; index < text.length; index += 1) {
740
- const char = text[index];
741
- const prev = text[index - 1];
742
- if (quote) {
743
- if (prev === "\\") continue;
744
- if (quote === "template" && char === "$" && text[index + 1] === "{") {
745
- const close = matchingPlaceholderClose(text, index + 1);
746
- if (close === void 0) return void 0;
747
- index = close;
748
- continue;
749
- }
750
- if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
751
- continue;
752
- }
753
- if (char === "$" && text[index + 1] === "{") {
754
- const close = matchingPlaceholderClose(text, index + 1);
755
- if (close === void 0) return void 0;
756
- index = close;
757
- continue;
758
- }
759
- if (char === "'") {
760
- quote = "single";
761
- continue;
762
- }
763
- if (char === '"') {
764
- quote = "double";
765
- continue;
766
- }
767
- if (char === "`") {
768
- quote = "template";
769
- continue;
770
- }
771
- if (char === "(") depth += 1;
772
- if (char === ")") {
773
- depth -= 1;
774
- if (depth === 0) return index;
775
- if (depth < 0) return void 0;
795
+ async function helperBindings(repoPath, filePath) {
796
+ const sf = await readSource(path8.join(repoPath, filePath));
797
+ if (!sf) return [];
798
+ const sourceFileAst = sf;
799
+ const out = [];
800
+ const exportedLocals = exportedLocalNames(sf);
801
+ const factsByLocal = /* @__PURE__ */ new Map();
802
+ for (const stmt of sf.statements) {
803
+ if (ts5.isFunctionDeclaration(stmt) && stmt.name) {
804
+ const fact = directConnectFactFromFunctionLike(stmt);
805
+ if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf3(sf, stmt) });
806
+ for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(stmt))
807
+ factsByLocal.set(`${stmt.name.text}#${returnedProperty}`, { ...objectFact, returnedProperty, sourceLine: lineOf3(sf, stmt) });
776
808
  }
777
- }
778
- return void 0;
779
- }
780
- function matchingPlaceholderClose(text, openBraceIndex) {
781
- let depth = 0;
782
- let quote;
783
- for (let index = openBraceIndex; index < text.length; index += 1) {
784
- const char = text[index];
785
- const prev = text[index - 1];
786
- if (quote) {
787
- if (prev === "\\") continue;
788
- if (quote === "template" && char === "$" && text[index + 1] === "{") {
789
- depth += 1;
790
- index += 1;
791
- continue;
809
+ if (ts5.isVariableStatement(stmt))
810
+ for (const decl of stmt.declarationList.declarations) {
811
+ if (!ts5.isIdentifier(decl.name) || !decl.initializer) continue;
812
+ const helper = functionLikeInitializer(decl.initializer);
813
+ if (helper) {
814
+ const directReturn = directConnectFactFromFunctionLike(helper);
815
+ if (directReturn)
816
+ factsByLocal.set(decl.name.text, {
817
+ ...directReturn,
818
+ sourceLine: lineOf3(sourceFileAst, decl)
819
+ });
820
+ for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(helper))
821
+ factsByLocal.set(`${decl.name.text}#${returnedProperty}`, {
822
+ ...objectFact,
823
+ returnedProperty,
824
+ sourceLine: lineOf3(sourceFileAst, decl)
825
+ });
826
+ continue;
827
+ }
828
+ const fact = findConnectInExpression(decl.initializer);
829
+ if (fact)
830
+ factsByLocal.set(decl.name.text, {
831
+ ...fact,
832
+ sourceLine: lineOf3(sourceFileAst, decl)
833
+ });
792
834
  }
793
- if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
794
- continue;
795
- }
796
- if (char === "'") {
797
- quote = "single";
798
- continue;
799
- }
800
- if (char === '"') {
801
- quote = "double";
802
- continue;
803
- }
804
- if (char === "`") {
805
- quote = "template";
806
- continue;
807
- }
808
- if (char === "{") depth += 1;
809
- if (char === "}") {
810
- depth -= 1;
811
- if (depth === 0) return index;
812
- if (depth < 0) return void 0;
835
+ }
836
+ for (const [exportedName, localName] of exportedLocals) {
837
+ const fact = factsByLocal.get(localName);
838
+ if (fact)
839
+ out.push({
840
+ ...fact,
841
+ exportedName,
842
+ sourceFile: normalizePath(filePath),
843
+ sourceLine: fact.sourceLine
844
+ });
845
+ }
846
+ for (const [key, fact] of factsByLocal) {
847
+ const [localName, returnedProperty] = key.split("#");
848
+ if (!returnedProperty) continue;
849
+ for (const [exportedName, exportedLocal] of exportedLocals) {
850
+ if (exportedLocal !== localName) continue;
851
+ out.push({ ...fact, exportedName, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: fact.sourceLine });
813
852
  }
814
853
  }
815
- return void 0;
854
+ return out;
816
855
  }
817
- function topLevelQueryIndex(text) {
818
- let quote;
819
- for (let index = 0; index < text.length; index += 1) {
820
- const char = text[index];
821
- const prev = text[index - 1];
822
- if (quote) {
823
- if (prev === "\\") continue;
824
- if (quote === "template" && char === "$" && text[index + 1] === "{") {
825
- const close = matchingPlaceholderClose(text, index + 1);
826
- if (close === void 0) return -1;
827
- index = close;
828
- continue;
829
- }
830
- if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
831
- continue;
832
- }
833
- if (char === "$" && text[index + 1] === "{") {
834
- const close = matchingPlaceholderClose(text, index + 1);
835
- if (close === void 0) return -1;
836
- index = close;
837
- continue;
838
- }
839
- if (char === "'") {
840
- quote = "single";
841
- continue;
842
- }
843
- if (char === '"') {
844
- quote = "double";
845
- continue;
856
+ async function parseServiceBindings(repoPath, filePath) {
857
+ const sf = await readSource(path8.join(repoPath, filePath));
858
+ if (!sf) return [];
859
+ const sourceFileAst = sf;
860
+ const out = [];
861
+ const imports = await importsFor(repoPath, filePath, sf);
862
+ const helperCache = /* @__PURE__ */ new Map();
863
+ const classHelpers = collectClassHelpers(sourceFileAst);
864
+ const localObjectHelpers = /* @__PURE__ */ new Map();
865
+ const localDirectHelpers = /* @__PURE__ */ new Map();
866
+ const objectHelperVariables = /* @__PURE__ */ new Map();
867
+ for (const stmt of sourceFileAst.statements) {
868
+ if (ts5.isFunctionDeclaration(stmt) && stmt.name) {
869
+ const directFact = directConnectFactFromFunctionLike(stmt);
870
+ if (directFact) localDirectHelpers.set(stmt.name.text, { ...directFact, exportedName: stmt.name.text, sourceFile: normalizePath(filePath), sourceLine: lineOf3(sourceFileAst, stmt) });
871
+ const rows2 = [];
872
+ for (const [returnedProperty, fact] of collectReturnedObjectBindings(stmt))
873
+ rows2.push({ ...fact, exportedName: stmt.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf3(sourceFileAst, stmt) });
874
+ if (rows2.length > 0) localObjectHelpers.set(stmt.name.text, rows2);
846
875
  }
847
- if (char === "`") {
848
- quote = "template";
849
- continue;
876
+ if (ts5.isVariableStatement(stmt)) {
877
+ for (const decl of stmt.declarationList.declarations) {
878
+ if (!ts5.isIdentifier(decl.name)) continue;
879
+ const helper = functionLikeInitializer(decl.initializer);
880
+ if (!helper) continue;
881
+ const directFact = directConnectFactFromFunctionLike(helper);
882
+ if (directFact) localDirectHelpers.set(decl.name.text, { ...directFact, exportedName: decl.name.text, sourceFile: normalizePath(filePath), sourceLine: lineOf3(sourceFileAst, decl) });
883
+ const rows2 = [];
884
+ for (const [returnedProperty, fact] of collectReturnedObjectBindings(helper))
885
+ rows2.push({ ...fact, exportedName: decl.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf3(sourceFileAst, decl) });
886
+ if (rows2.length > 0) localObjectHelpers.set(decl.name.text, rows2);
887
+ }
850
888
  }
851
- if (char === "?") return index;
852
889
  }
853
- return -1;
854
- }
855
-
856
- // src/parsers/outbound-call-parser.ts
857
- function lineOf3(text, idx) {
858
- return text.slice(0, idx).split("\n").length;
859
- }
860
- function entityFromExpression(expr) {
861
- if (!expr) return void 0;
862
- if (ts4.isIdentifier(expr) || ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
863
- if (ts4.isPropertyAccessExpression(expr) && expr.expression.kind === ts4.SyntaxKind.ThisKeyword) return expr.name.text;
864
- if (ts4.isElementAccessExpression(expr) && expr.argumentExpression && (ts4.isStringLiteral(expr.argumentExpression) || ts4.isNoSubstitutionTemplateLiteral(expr.argumentExpression))) return expr.argumentExpression.text;
865
- return void 0;
866
- }
867
- function expressionName(expr) {
868
- if (ts4.isIdentifier(expr)) return expr.text;
869
- if (ts4.isPropertyAccessExpression(expr)) return `${expressionName(expr.expression)}.${expr.name.text}`;
870
- return expr.getText();
871
- }
872
- function variableInitializers(source) {
873
- const initializers = /* @__PURE__ */ new Map();
874
- for (const statement of source.statements) {
875
- if (!ts4.isVariableStatement(statement) || (statement.declarationList.flags & ts4.NodeFlags.Const) === 0) continue;
876
- for (const declaration of statement.declarationList.declarations) {
877
- if (ts4.isIdentifier(declaration.name) && declaration.initializer) initializers.set(declaration.name.text, declaration.initializer);
878
- }
890
+ async function importedHelpers(localName) {
891
+ const imp = imports.find((i) => i.localName === localName && i.sourceFile);
892
+ if (!imp?.sourceFile) return [];
893
+ if (!helperCache.has(imp.sourceFile))
894
+ helperCache.set(
895
+ imp.sourceFile,
896
+ await helperBindings(repoPath, imp.sourceFile)
897
+ );
898
+ return (helperCache.get(imp.sourceFile) ?? []).filter((h) => h.exportedName === imp.exportedName).map((helper) => ({ imp, helper }));
879
899
  }
880
- return initializers;
881
- }
882
- function queryEntityFromAst(expr, initializers = /* @__PURE__ */ new Map()) {
883
- if (ts4.isParenthesizedExpression(expr) || ts4.isAwaitExpression(expr)) return queryEntityFromAst(expr.expression, initializers);
884
- if (ts4.isIdentifier(expr) && initializers.has(expr.text)) return queryEntityFromAst(initializers.get(expr.text), initializers);
885
- if (ts4.isCallExpression(expr)) {
886
- const name = expressionName(expr.expression);
887
- if (name === "cds.run") return queryEntityFromAst(expr.arguments[0], initializers);
888
- if (["SELECT.one.from", "SELECT.from", "SELECT.one", "INSERT.into", "UPSERT.into", "DELETE.from", "UPDATE.entity"].includes(name)) return entityFromExpression(expr.arguments[0]);
889
- if (name === "UPDATE") return entityFromExpression(expr.arguments[0]);
890
- const receiver = ts4.isPropertyAccessExpression(expr.expression) ? expr.expression.expression : void 0;
891
- if (receiver) return queryEntityFromAst(receiver, initializers);
900
+ async function importedHelper(localName) {
901
+ return (await importedHelpers(localName)).find((row) => !row.helper.returnedProperty);
892
902
  }
893
- return void 0;
894
- }
895
- function extractQueryEntity(expr) {
896
- const source = ts4.createSourceFile("query.ts", `const __query = (${expr});`, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
897
- const initializers = variableInitializers(source);
898
- let found;
899
- const visit = (node) => {
900
- if (found) return;
901
- if (ts4.isParenthesizedExpression(node)) found = queryEntityFromAst(node.expression, initializers);
902
- ts4.forEachChild(node, visit);
903
- };
904
- visit(source);
905
- return found;
906
- }
907
- function queryWarning(expr) {
908
- if (/^\s*[`'"]/.test(expr)) return "raw_sql_or_cql_expression";
909
- if (/^\s*\w+\s*$/.test(expr)) return "query_variable_without_static_initializer";
910
- return "dynamic_entity_expression";
911
- }
912
- function parserEvidence(source, node, extra) {
913
- return { parser: "typescript_ast", startOffset: node.getStart(source), endOffset: node.getEnd(), ...extra };
914
- }
915
- function isStringLike(expr) {
916
- return Boolean(expr && (ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr)));
917
- }
918
- function literalText(expr) {
919
- if (isStringLike(expr)) return expr.text;
920
- return void 0;
921
- }
922
- function objectPropertyText(object, key) {
923
- const prop = object.properties.find(
924
- (property) => ts4.isPropertyAssignment(property) && nameOfProperty(property.name) === key || ts4.isShorthandPropertyAssignment(property) && property.name.text === key
925
- );
926
- if (!prop) return void 0;
927
- return ts4.isShorthandPropertyAssignment(prop) ? prop.name.text : prop.initializer.getText();
928
- }
929
- function objectPropertyIsShorthand(object, key) {
930
- return object.properties.some((property) => ts4.isShorthandPropertyAssignment(property) && property.name.text === key);
931
- }
932
- function nameOfProperty(name) {
933
- if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name)) return name.text;
934
- return void 0;
935
- }
936
- var maxAliasDepth = 5;
937
- function safeRaw(expr) {
938
- if (ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr) || ts4.isIdentifier(expr) || ts4.isTemplateExpression(expr)) return expr.getText(expr.getSourceFile());
939
- return void 0;
940
- }
941
- function placeholders(expr) {
942
- return expr.templateSpans.map((span) => span.expression.getText(expr.getSourceFile()));
943
- }
944
- function isFunctionLikeScope(node) {
945
- return ts4.isFunctionLike(node) || ts4.isSourceFile(node);
946
- }
947
- function nodeContains(parent, child) {
948
- const source = child.getSourceFile();
949
- return child.getStart(source) >= parent.getStart(source) && child.getEnd() <= parent.getEnd();
950
- }
951
- function declarationScope(node) {
952
- if (ts4.isParameter(node)) return node.parent;
953
- if (ts4.isCatchClause(node.parent) && node.parent.variableDeclaration === node) return node.parent;
954
- const list = node.parent;
955
- const blockScoped = (list.flags & (ts4.NodeFlags.Const | ts4.NodeFlags.Let)) !== 0;
956
- let current = list.parent;
957
- if (!blockScoped) {
958
- while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
959
- return current;
903
+ function bindingForVariable(variableName) {
904
+ const sourceFile = normalizePath(filePath);
905
+ return [...out].reverse().find((row) => row.variableName === variableName && row.sourceFile === sourceFile);
960
906
  }
961
- while (current.parent && !ts4.isBlock(current) && !ts4.isSourceFile(current) && !ts4.isModuleBlock(current) && !ts4.isCaseBlock(current) && !isLoopInitializerScope(node, current) && !isFunctionLikeScope(current)) current = current.parent;
962
- return current;
963
- }
964
- function isLoopInitializerScope(declaration, scope) {
965
- const list = declaration.parent;
966
- return ts4.isForStatement(scope) && scope.initializer === list || (ts4.isForInStatement(scope) || ts4.isForOfStatement(scope)) && scope.initializer === list;
967
- }
968
- function catchBindingScope(declaration) {
969
- if (ts4.isParameter(declaration)) return void 0;
970
- return ts4.isCatchClause(declaration.parent) && declaration.parent.variableDeclaration === declaration ? declaration.parent : void 0;
971
- }
972
- function isAccessibleDeclaration(declaration, use) {
973
- const source = use.getSourceFile();
974
- if (declaration.name.getStart(source) >= use.getStart(source)) return false;
975
- const catchScope = catchBindingScope(declaration);
976
- if (catchScope) return nodeContains(catchScope.block, use);
977
- const scope = declarationScope(declaration);
978
- if (ts4.isForStatement(scope) || ts4.isForInStatement(scope) || ts4.isForOfStatement(scope)) return nodeContains(scope.statement, use);
979
- return ts4.isSourceFile(scope) || nodeContains(scope, use);
980
- }
981
- function resolveBinding(identifier, use) {
982
- const source = use.getSourceFile();
983
- let best;
984
- const visit = (node) => {
985
- if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
986
- if (ts4.isParameter(node) && ts4.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
987
- ts4.forEachChild(node, visit);
988
- };
989
- visit(source);
990
- if (!best) return { immutable: false, evidence: ["binding_not_found"] };
991
- const immutable = ts4.isVariableDeclaration(best) && (best.parent.flags & ts4.NodeFlags.Const) !== 0;
992
- return { declaration: best, initializer: ts4.isVariableDeclaration(best) ? best.initializer : void 0, immutable, evidence: [immutable ? "lexical_const_binding_before_use" : "lexical_mutable_or_parameter_binding"] };
993
- }
994
- function resolveExpression(expr, use, policy, depth = 0, seen = /* @__PURE__ */ new Set()) {
995
- if (!expr) return { status: "unknown", sourceKind: "dynamic_expression", placeholderKeys: [], evidence: ["expression_missing"] };
996
- if (ts4.isStringLiteral(expr)) return { status: "static", sourceKind: "string_literal", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["string_literal"] };
997
- if (ts4.isNoSubstitutionTemplateLiteral(expr)) return { status: "static", sourceKind: "no_substitution_template", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["no_substitution_template"] };
998
- if (ts4.isTemplateExpression(expr)) {
999
- const keys = placeholders(expr);
1000
- if (policy === "operation_path") return { status: "dynamic", sourceKind: "template_with_substitutions", value: stripQuotes(expr.getText(expr.getSourceFile())), rawExpression: safeRaw(expr), placeholderKeys: keys, evidence: ["operation_path_template_placeholders_retained"] };
1001
- return { status: "dynamic", sourceKind: "template_with_substitutions", placeholderKeys: keys, evidence: ["template_substitutions_not_static_external_target"] };
907
+ function cloneAliasBinding(targetName, sourceName, aliasKind, node) {
908
+ const existing = bindingForVariable(sourceName);
909
+ if (!existing) return;
910
+ out.push({
911
+ ...existing,
912
+ variableName: targetName,
913
+ sourceLine: lineOf3(sourceFileAst, node),
914
+ helperChain: [
915
+ ...existing.helperChain ?? [],
916
+ {
917
+ callerVariable: targetName,
918
+ aliasOf: sourceName,
919
+ aliasKind,
920
+ scopeRule: "same-file-source-order",
921
+ ...aliasKind === "transaction" ? { transactionAliasSource: sourceName } : {}
922
+ }
923
+ ]
924
+ });
1002
925
  }
1003
- if (ts4.isIdentifier(expr)) {
1004
- if (depth >= maxAliasDepth) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_depth_exceeded"], constName: expr.text };
1005
- const binding = resolveBinding(expr, use);
1006
- if (!binding.declaration || !binding.initializer || !binding.immutable) return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: binding.evidence, constName: expr.text };
1007
- if (seen.has(binding.declaration)) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_cycle_detected"], constName: expr.text };
1008
- seen.add(binding.declaration);
1009
- const resolved2 = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
1010
- return { ...resolved2, sourceKind: "const_alias", rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved2.evidence] };
926
+ function recordIdentityAlias(decl) {
927
+ if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
928
+ const unwrapped = unwrapIdentityExpression(decl.initializer);
929
+ if (!ts5.isIdentifier(unwrapped)) return;
930
+ cloneAliasBinding(decl.name.text, unwrapped.text, "identity", decl);
1011
931
  }
1012
- return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${ts4.SyntaxKind[expr.kind] ?? "expression"}`] };
1013
- }
1014
- function staticExpressionText(expr, initializers) {
1015
- if (!expr) return void 0;
1016
- if (isStringLike(expr)) return expr.text;
1017
- if (ts4.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
1018
- return void 0;
1019
- }
1020
- function staticPathExpression(expr, use) {
1021
- const resolution = resolveExpression(expr, use, "operation_path");
1022
- const sourceKind = resolution.sourceKind === "const_alias" ? "const" : resolution.sourceKind === "template_with_substitutions" || resolution.sourceKind === "no_substitution_template" ? "template" : resolution.status === "static" ? "literal" : "dynamic";
1023
- return { text: resolution.value, sourceKind, rawExpression: resolution.rawExpression, constName: resolution.constName, resolution };
1024
- }
1025
- function operationPathFromStatic(text) {
1026
- return text ? `/${stripQuotes(text).replace(/^\//, "")}` : void 0;
1027
- }
1028
- function staticPathCandidates(identifier, call, method) {
1029
- const binding = resolveBinding(identifier, call);
1030
- const declaration = binding.declaration;
1031
- if (!declaration) return void 0;
1032
- const source = call.getSourceFile();
1033
- const paths = [];
1034
- let hasDynamicAssignments = false;
1035
- const addExpr = (expr, origin) => {
1036
- const resolved2 = staticPathExpression(expr, origin);
1037
- if (resolved2.text) paths.push(operationPathFromStatic(resolved2.text) ?? resolved2.text);
1038
- else hasDynamicAssignments = true;
1039
- };
1040
- if (ts4.isVariableDeclaration(declaration)) addExpr(declaration.initializer, declaration);
1041
- const visit = (node) => {
1042
- if (node !== source && isFunctionLikeScope(node) && !nodeContains(node, call)) return;
1043
- if (node.getStart(source) >= call.getStart(source)) return;
1044
- if (ts4.isBinaryExpression(node) && node.operatorToken.kind === ts4.SyntaxKind.EqualsToken && ts4.isIdentifier(node.left)) {
1045
- const target = resolveBinding(node.left, node);
1046
- if (target.declaration === declaration) addExpr(node.right, node);
932
+ async function recordBindingFromExpression(targetName, expr, node, aliasKind) {
933
+ const call = unwrapCall(expr);
934
+ if (!call) return;
935
+ const direct = connectFactFromCall(call);
936
+ if (direct)
937
+ out.push({
938
+ variableName: targetName,
939
+ ...direct,
940
+ sourceFile: normalizePath(filePath),
941
+ sourceLine: lineOf3(sourceFileAst, node),
942
+ helperChain: aliasKind === "assignment" ? [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind, scopeRule: "same-file-source-order" }] : void 0
943
+ });
944
+ else if (ts5.isIdentifier(call.expression)) {
945
+ const localDirect = localDirectHelpers.get(call.expression.text);
946
+ const resolved2 = localDirect ? { helper: localDirect, imp: void 0 } : await importedHelper(call.expression.text);
947
+ if (resolved2)
948
+ out.push({
949
+ variableName: targetName,
950
+ alias: resolved2.helper.alias,
951
+ aliasExpr: resolved2.helper.aliasExpr,
952
+ destinationExpr: resolved2.helper.destinationExpr,
953
+ servicePathExpr: resolved2.helper.servicePathExpr,
954
+ isDynamic: resolved2.helper.isDynamic,
955
+ placeholders: resolved2.helper.placeholders,
956
+ sourceFile: normalizePath(filePath),
957
+ sourceLine: lineOf3(sourceFileAst, node),
958
+ helperChain: [
959
+ ...resolved2.helper.helperChain ?? [],
960
+ {
961
+ callerVariable: targetName,
962
+ ...aliasKind === "assignment" ? { assignedFrom: call.expression.text, aliasKind, scopeRule: "same-file-source-order" } : {},
963
+ importedHelper: call.expression.text,
964
+ importSource: resolved2.imp?.sourceFile,
965
+ exportedSymbol: resolved2.imp?.exportedName ?? resolved2.helper.exportedName,
966
+ helperSourceFile: resolved2.helper.sourceFile,
967
+ helperSourceLine: resolved2.helper.sourceLine
968
+ }
969
+ ]
970
+ });
1047
971
  }
1048
- ts4.forEachChild(node, visit);
1049
- };
1050
- visit(source);
1051
- const candidatePaths = [...new Set(paths)];
1052
- if (candidatePaths.length === 0) return void 0;
1053
- const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, method).topLevelOperationName).filter((value) => Boolean(value)))];
1054
- return { candidatePaths, normalizedCandidateOperations, candidateSourceKind: "static candidates observed before call in lexical scope", candidateIdentifier: identifier.text, hasDynamicAssignments, conservativeReason: hasDynamicAssignments ? "dynamic_assignment_observed" : normalizedCandidateOperations.length > 1 ? "candidate_tie" : void 0 };
1055
- }
1056
- function destinationExpressionShape(expr) {
1057
- if (!expr) return void 0;
1058
- if (ts4.isIdentifier(expr)) return "identifier";
1059
- if (ts4.isPropertyAccessExpression(expr) || ts4.isElementAccessExpression(expr)) return "property_read";
1060
- if (ts4.isCallExpression(expr)) return "function_call";
1061
- if (ts4.isConditionalExpression(expr)) return "conditional";
1062
- if (ts4.isBinaryExpression(expr)) return "binary_expression";
1063
- if (ts4.isTemplateExpression(expr)) return "template_expression";
1064
- return ts4.SyntaxKind[expr.kind] ?? "expression";
1065
- }
1066
- function staticConditionalCandidates(expr, initializers) {
1067
- const resolved2 = expr && ts4.isIdentifier(expr) && initializers.has(expr.text) ? initializers.get(expr.text) : expr;
1068
- if (!resolved2 || !ts4.isConditionalExpression(resolved2)) return void 0;
1069
- const left = staticExpressionText(resolved2.whenTrue, initializers);
1070
- const right = staticExpressionText(resolved2.whenFalse, initializers);
1071
- if (!left || !right) return void 0;
1072
- return [.../* @__PURE__ */ new Set([left, right])];
1073
- }
1074
- function propertyInitializer(object, key) {
1075
- for (const property of object.properties) {
1076
- if (ts4.isPropertyAssignment(property) && nameOfProperty(property.name) === key) return property.initializer;
1077
- if (ts4.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
1078
972
  }
1079
- return void 0;
1080
- }
1081
- function httpMethodFromObject(object, use) {
1082
- const text = resolveExpression(propertyInitializer(object, "method"), use, "literal").value;
1083
- return text ? stripQuotes(text).toUpperCase() : void 0;
1084
- }
1085
- var supportedHttpMethods = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
1086
- function safeOperationName(value) {
1087
- if (!value || !/^[A-Za-z_$][\w$]*(?:[./][A-Za-z_$][\w$]*)*$/.test(value)) return void 0;
1088
- return operationPathFromStatic(value);
1089
- }
1090
- function hasTemplatePlaceholder(value) {
1091
- return /\$\{|%7B|%7D/i.test(value);
1092
- }
1093
- function urlTargetFromExpression(expr, use) {
1094
- const resolved2 = resolveExpression(expr, use, "external");
1095
- if (resolved2.status === "static" && resolved2.value && !hasTemplatePlaceholder(resolved2.value)) return { kind: "static_url", expression: resolved2.value, dynamic: false, sourceKind: resolved2.sourceKind };
1096
- if (expr) return { kind: "url_expression", dynamic: true, expression: `${resolved2.sourceKind}:${resolved2.placeholderKeys.join("|")}`, expressionShape: resolved2.sourceKind, placeholderKeys: resolved2.placeholderKeys };
1097
- return { kind: "unknown", dynamic: false };
1098
- }
1099
- function destinationTargetFromExpression(expr, use) {
1100
- const resolved2 = resolveExpression(expr, use, "external");
1101
- const text = resolved2.value;
1102
- if (resolved2.status === "static" && text && !hasTemplatePlaceholder(text)) return { kind: "destination", expression: text, dynamic: false, sourceKind: resolved2.sourceKind };
1103
- const candidates = staticConditionalCandidates(expr, /* @__PURE__ */ new Map());
1104
- if (candidates) return { kind: "destination", dynamic: true, expressionShape: "conditional", candidateLiterals: candidates };
1105
- const shape = destinationExpressionShape(expr);
1106
- if (shape) return { kind: "destination", dynamic: true, expressionShape: shape };
1107
- return void 0;
1108
- }
1109
- function externalHttpEvidence(node, source) {
1110
- const expr = node.expression;
1111
- const exprText = expr.getText(source);
1112
- if (exprText === "useOrFetchDestination") {
1113
- const objectArg = node.arguments[0];
1114
- if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
1115
- const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"), node);
1116
- return { externalTarget: destination ?? { kind: "unknown", dynamic: false }, classifier: "sap_destination_lookup", sourceCallShape: "useOrFetchDestination" };
1117
- }
973
+ async function recordVariable(decl) {
974
+ if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
975
+ await recordBindingFromExpression(decl.name.text, decl.initializer, decl, "declaration");
1118
976
  }
1119
- if (exprText === "executeHttpRequest") {
1120
- const destination = destinationTargetFromExpression(node.arguments[0], node);
1121
- const config = node.arguments[1];
1122
- const method = config && ts4.isObjectLiteralExpression(config) ? httpMethodFromObject(config, node) : void 0;
1123
- const url = config && ts4.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, "url"), node) : { kind: "unknown", dynamic: false };
1124
- return { method, externalTarget: destination ? { ...url, destination } : url, classifier: "sap_execute_http_request", sourceCallShape: "executeHttpRequest" };
977
+ async function helpersForCall(call) {
978
+ if (!ts5.isIdentifier(call.expression)) return [];
979
+ const local = localObjectHelpers.get(call.expression.text) ?? [];
980
+ const imported = await importedHelpers(call.expression.text);
981
+ return [...local.map((helper) => ({ helper })), ...imported];
1125
982
  }
1126
- if (exprText === "axios") {
1127
- const config = node.arguments[0];
1128
- if (config && ts4.isObjectLiteralExpression(config)) {
1129
- const method = httpMethodFromObject(config, node);
1130
- return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, "url"), node), classifier: "axios_config_call", sourceCallShape: "axios(config)" };
1131
- }
1132
- return { externalTarget: { kind: "unknown", dynamic: false }, classifier: "axios_unknown_call", sourceCallShape: "axios(...)" };
983
+ async function rememberObjectHelperVariable(decl) {
984
+ if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
985
+ const call = unwrapCall(decl.initializer);
986
+ if (!call) return;
987
+ const helpers = (await helpersForCall(call)).filter((row) => row.helper.returnedProperty);
988
+ if (helpers.length > 0) objectHelperVariables.set(decl.name.text, helpers);
1133
989
  }
1134
- if (exprText === "fetch") {
1135
- const init = node.arguments[1];
1136
- const method = init && ts4.isObjectLiteralExpression(init) ? httpMethodFromObject(init, node) : void 0;
1137
- return { method, externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "fetch_call", sourceCallShape: "fetch" };
990
+ function recordObjectPropertyBinding(targetName, expr, node) {
991
+ const unwrapped = unwrapIdentityExpression(expr);
992
+ if (!ts5.isPropertyAccessExpression(unwrapped) || !ts5.isIdentifier(unwrapped.expression)) return false;
993
+ const helpers = objectHelperVariables.get(unwrapped.expression.text) ?? [];
994
+ const matches2 = helpers.filter((row) => row.helper.returnedProperty === unwrapped.name.text);
995
+ if (matches2.length !== 1) return false;
996
+ const resolved2 = matches2[0];
997
+ out.push({
998
+ variableName: targetName,
999
+ alias: resolved2.helper.alias,
1000
+ aliasExpr: resolved2.helper.aliasExpr,
1001
+ destinationExpr: resolved2.helper.destinationExpr,
1002
+ servicePathExpr: resolved2.helper.servicePathExpr,
1003
+ isDynamic: resolved2.helper.isDynamic,
1004
+ placeholders: resolved2.helper.placeholders,
1005
+ sourceFile: normalizePath(filePath),
1006
+ sourceLine: lineOf3(sourceFileAst, node),
1007
+ helperChain: [
1008
+ ...resolved2.helper.helperChain ?? [],
1009
+ {
1010
+ callerVariable: targetName,
1011
+ sourceVariable: unwrapped.expression.text,
1012
+ returnedProperty: unwrapped.name.text,
1013
+ assignedFromProperty: unwrapped.getText(sourceFileAst),
1014
+ importSource: resolved2.imp?.sourceFile,
1015
+ exportedSymbol: resolved2.imp?.exportedName,
1016
+ helperSourceFile: resolved2.helper.sourceFile,
1017
+ helperSourceLine: resolved2.helper.sourceLine
1018
+ }
1019
+ ]
1020
+ });
1021
+ return true;
1138
1022
  }
1139
- if (ts4.isPropertyAccessExpression(expr) && ["get", "post", "put", "patch", "delete", "head"].includes(expr.name.text) && expr.expression.getText(source) === "axios") {
1140
- return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "axios_member_call", sourceCallShape: `axios.${expr.name.text}` };
1023
+ async function recordDestructuredHelper(decl) {
1024
+ if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
1025
+ const call = unwrapCall(decl.initializer);
1026
+ if (!call) return;
1027
+ const helpers = await helpersForCall(call);
1028
+ if (helpers.length === 0) return;
1029
+ for (const el of decl.name.elements) {
1030
+ if (!ts5.isIdentifier(el.name)) continue;
1031
+ const propertyName2 = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1032
+ const matches2 = helpers.filter((row) => row.helper.returnedProperty === propertyName2);
1033
+ if (matches2.length !== 1) continue;
1034
+ const resolved2 = matches2[0];
1035
+ out.push({
1036
+ variableName: el.name.text,
1037
+ alias: resolved2.helper.alias,
1038
+ aliasExpr: resolved2.helper.aliasExpr,
1039
+ destinationExpr: resolved2.helper.destinationExpr,
1040
+ servicePathExpr: resolved2.helper.servicePathExpr,
1041
+ isDynamic: resolved2.helper.isDynamic,
1042
+ placeholders: resolved2.helper.placeholders,
1043
+ sourceFile: normalizePath(filePath),
1044
+ sourceLine: lineOf3(sourceFileAst, decl),
1045
+ helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName2, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
1046
+ });
1047
+ }
1141
1048
  }
1142
- return void 0;
1143
- }
1144
- function collectServiceVariables(source) {
1145
- const vars = /* @__PURE__ */ new Set(["cds", "messaging", "messageClient", "eventClient"]);
1146
- const visit = (node) => {
1147
- if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer) {
1148
- const text = node.initializer.getText(source);
1149
- if (/cds\.connect\.(to|messaging)\s*\(/.test(text)) vars.add(node.name.text);
1150
- }
1151
- ts4.forEachChild(node, visit);
1152
- };
1153
- visit(source);
1154
- return vars;
1155
- }
1156
- function receiverName(expr) {
1157
- if (ts4.isIdentifier(expr)) return expr.text;
1158
- if (ts4.isPropertyAccessExpression(expr)) return expr.getText(sourceOf(expr));
1159
- return void 0;
1160
- }
1161
- function sourceOf(node) {
1162
- return node.getSourceFile();
1163
- }
1164
- function rootReceiverName(expr) {
1165
- if (ts4.isIdentifier(expr)) return expr.text;
1166
- if (ts4.isPropertyAccessExpression(expr)) return rootReceiverName(expr.expression);
1167
- if (ts4.isCallExpression(expr)) return rootReceiverName(expr.expression);
1168
- return void 0;
1169
- }
1170
- function isSupportedEventReceiver(receiver, rootReceiver, serviceVariables) {
1171
- const candidate = rootReceiver ?? receiver;
1172
- if (!candidate) return false;
1173
- if (candidate === "cds") return true;
1174
- if (serviceVariables.has(candidate)) return true;
1175
- if (receiver && serviceVariables.has(receiver)) return true;
1176
- if (/^(srv|service|serviceClient|messaging|messageClient|eventClient)$/.test(candidate)) return true;
1177
- return false;
1178
- }
1179
- function collectWrapperSpecs(source) {
1180
- const specs = /* @__PURE__ */ new Map();
1181
- const serviceVariables = collectServiceVariables(source);
1182
- const calledNames = /* @__PURE__ */ new Set();
1183
- const collectCalls = (node) => {
1184
- if (ts4.isCallExpression(node)) {
1185
- if (ts4.isIdentifier(node.expression)) calledNames.add(node.expression.text);
1186
- if (ts4.isCallExpression(node.expression) && ts4.isIdentifier(node.expression.expression)) calledNames.add(node.expression.expression.text);
1187
- }
1188
- ts4.forEachChild(node, collectCalls);
1189
- };
1190
- collectCalls(source);
1191
- const scanFunction = (name, fn) => {
1192
- if (!calledNames.has(name)) return;
1193
- const params = fn.parameters.map((param) => ts4.isIdentifier(param.name) ? param.name.text : void 0);
1194
- const sends = [];
1195
- const visit = (node) => {
1196
- if (ts4.isCallExpression(node) && ts4.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send" && ts4.isIdentifier(node.expression.expression)) {
1197
- const objectArg = node.arguments[0];
1198
- if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
1199
- const pathProp = propertyInitializer(objectArg, "path");
1200
- const methodProp = propertyInitializer(objectArg, "method");
1201
- const pathName = pathProp && ts4.isIdentifier(pathProp) ? pathProp.text : void 0;
1202
- const methodName = methodProp && ts4.isIdentifier(methodProp) ? methodProp.text : void 0;
1203
- const methodLiteral = resolveExpression(methodProp, node, "literal").value;
1204
- if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
1205
- }
1206
- }
1207
- if (ts4.isCallExpression(node) && ts4.isIdentifier(node.expression) && specs.has(node.expression.text)) {
1208
- const nested = specs.get(node.expression.text);
1209
- const pathArg = nested ? node.arguments[nested.pathIndex] : void 0;
1210
- const clientArg = nested?.clientIndex === void 0 ? void 0 : node.arguments[nested.clientIndex];
1211
- const pathName = pathArg && ts4.isIdentifier(pathArg) ? pathArg.text : void 0;
1212
- const clientName = clientArg && ts4.isIdentifier(clientArg) ? clientArg.text : nested?.clientName;
1213
- if (nested && pathName && clientName) sends.push({ client: clientName, path: pathName, method: nested.methodName, methodLiteral: nested.methodLiteral, nestedWrapperFunction: node.expression.text, start: node.getStart(source), end: node.getEnd() });
1214
- }
1215
- ts4.forEachChild(node, visit);
1216
- };
1217
- visit(fn);
1218
- if (sends.length !== 1) return;
1219
- const found = sends[0];
1220
- const clientIndex = params.indexOf(found.client);
1221
- const pathIndex = params.indexOf(found.path);
1222
- const methodIndex = found.method ? params.indexOf(found.method) : -1;
1223
- const capturesKnownClient = serviceVariables.has(found.client) || /^(srv|service|serviceClient|client|.*Client)$/.test(found.client);
1224
- if (pathIndex >= 0 && (clientIndex >= 0 || capturesKnownClient)) specs.set(name, { clientIndex: clientIndex >= 0 ? clientIndex : void 0, clientName: clientIndex >= 0 ? void 0 : found.client, pathIndex, methodIndex: methodIndex >= 0 ? methodIndex : void 0, methodName: found.method, methodLiteral: found.methodLiteral, nestedWrapperFunction: found.nestedWrapperFunction, definitionLine: lineOf3(source.text, fn.getStart(source)), internalStart: found.start, internalEnd: found.end });
1225
- };
1226
- const visitTop = (node) => {
1227
- if (ts4.isFunctionDeclaration(node) && node.name) scanFunction(node.name.text, node);
1228
- if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer && (ts4.isArrowFunction(node.initializer) || ts4.isFunctionExpression(node.initializer))) scanFunction(node.name.text, node.initializer);
1229
- ts4.forEachChild(node, visitTop);
1230
- };
1231
- visitTop(source);
1232
- return specs;
1233
- }
1234
- function classifyOutboundCallsInSource(source, filePath) {
1235
- const calls = [];
1236
- const sourceFile = normalizePath(filePath);
1237
- const initializers = variableInitializers(source);
1238
- const serviceVariables = collectServiceVariables(source);
1239
- const wrapperSpecs = collectWrapperSpecs(source);
1240
- const wrapperInternalRanges = [...wrapperSpecs.values()].map((spec) => ({ start: spec.internalStart, end: spec.internalEnd }));
1241
- const add = (node, fact, extra) => {
1242
- calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf3(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
1243
- };
1244
- const visit = (node) => {
1245
- if (ts4.isCallExpression(node)) {
1246
- if (wrapperInternalRanges.some((range) => node.getStart(source) >= range.start && node.getEnd() <= range.end)) {
1247
- return;
1049
+ async function recordDestructuredAssignment(pattern, expr, node) {
1050
+ const call = unwrapCall(expr);
1051
+ if (!call) return;
1052
+ const helpers = await helpersForCall(call);
1053
+ if (helpers.length === 0) return;
1054
+ for (const prop of pattern.properties) {
1055
+ let propertyName2;
1056
+ let targetName;
1057
+ if (ts5.isShorthandPropertyAssignment(prop)) {
1058
+ propertyName2 = prop.name.text;
1059
+ targetName = prop.name.text;
1060
+ } else if (ts5.isPropertyAssignment(prop)) {
1061
+ propertyName2 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
1062
+ targetName = ts5.isIdentifier(prop.initializer) ? prop.initializer.text : void 0;
1248
1063
  }
1249
- const expr = node.expression;
1250
- const exprText = expr.getText(source);
1251
- if (exprText === "cds.run") {
1252
- const arg = node.arguments[0];
1253
- const entity = arg ? queryEntityFromAst(arg, initializers) : void 0;
1254
- const payload = arg?.getText(source) ?? "";
1255
- add(node, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) });
1256
- } else if (ts4.isPropertyAccessExpression(expr) && expr.name.text === "send" && (ts4.isIdentifier(expr.expression) || ts4.isPropertyAccessExpression(expr.expression))) {
1257
- const objectArg = node.arguments[0];
1258
- if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
1259
- const receiver = receiverName(expr.expression);
1260
- const query = objectPropertyText(objectArg, "query");
1261
- const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, "method"), node, "literal").value ?? objectPropertyText(objectArg, "method") ?? "POST");
1262
- const pathExpr = propertyInitializer(objectArg, "path") ?? propertyInitializer(objectArg, "event");
1263
- const resolvedPath = staticPathExpression(pathExpr, node);
1264
- const op = pathExpr ? resolvedPath.text ?? pathExpr.getText(source) : void 0;
1265
- const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
1266
- const candidateEvidence2 = pathExpr && ts4.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(pathExpr, node, method) : void 0;
1267
- const candidateOperationPath = candidateEvidence2 && !candidateEvidence2.hasDynamicAssignments && candidateEvidence2.normalizedCandidateOperations.length === 1 && candidateEvidence2.candidatePaths.length > 0 ? candidateEvidence2.candidatePaths[0] : void 0;
1268
- const operationPathExpr = operationPathFromStatic(resolvedPath.text) ?? candidateOperationPath;
1269
- const intent = classifyODataPathIntent(operationPathExpr, method);
1270
- const entityCallTypes = { entity_mutation: "remote_entity_mutation", entity_delete: "remote_entity_delete", entity_media: "remote_entity_media", entity_candidate: "remote_entity_candidate" };
1271
- const entityCallType = entityCallTypes[intent.kind];
1272
- const isODataQueryRead = method.toUpperCase() === "GET" && ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
1273
- add(node, { callType: query ? "remote_query" : entityCallType ?? (isODataQueryRead ? "remote_query" : "remote_action"), serviceVariableName: receiver, method, operationPathExpr, queryEntity: query ? extractQueryEntity(query) : isODataQueryRead ? intent.entitySegment : void 0, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : resolvedPath.sourceKind : void 0, odataPathIntent: operationPathExpr ? intent : void 0, staticPathCandidates: candidateEvidence2, parserWarning: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 });
1274
- } else {
1275
- const receiver = receiverName(expr.expression);
1276
- const rootReceiver = rootReceiverName(expr.expression);
1277
- const firstArg2 = resolveExpression(node.arguments[0], node, "literal");
1278
- const method = firstArg2.value?.toUpperCase();
1279
- const pathArg = node.arguments[1];
1280
- const supported = method && supportedHttpMethods.has(method);
1281
- if (receiver && supported && serviceVariables.has(rootReceiver ?? receiver)) {
1282
- const resolvedPath = staticPathExpression(pathArg, node);
1283
- const operationPathExpr = operationPathFromStatic(resolvedPath.text);
1284
- const intent = classifyODataPathIntent(operationPathExpr, method);
1285
- add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.8 : 0.45, unresolvedReason: operationPathExpr ? void 0 : "dynamic_operation_path_identifier" }, { receiver, rootReceiver, classifier: "service_client_send_method_path", rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : resolvedPath.sourceKind : void 0, odataPathIntent: operationPathExpr ? intent : void 0, parserWarning: operationPathExpr ? void 0 : "dynamic_operation_path_identifier" });
1286
- } else if (receiver && serviceVariables.has(rootReceiver ?? receiver)) {
1287
- const operationPathExpr = safeOperationName(firstArg2.value);
1288
- add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.75 : 0.35, unresolvedReason: operationPathExpr ? void 0 : "unsupported_cap_send_signature" }, { receiver, rootReceiver, classifier: operationPathExpr ? "service_client_send_operation_event" : "service_client_send_unsupported_signature", rawOperationExpression: firstArg2.rawExpression, literalOperationSource: firstArg2.value ? firstArg2.sourceKind : void 0, parserWarning: operationPathExpr ? void 0 : "unsupported_cap_send_signature" });
1064
+ if (!propertyName2 || !targetName) continue;
1065
+ const matches2 = helpers.filter((row) => row.helper.returnedProperty === propertyName2);
1066
+ if (matches2.length !== 1) continue;
1067
+ const resolved2 = matches2[0];
1068
+ out.push({
1069
+ variableName: targetName,
1070
+ alias: resolved2.helper.alias,
1071
+ aliasExpr: resolved2.helper.aliasExpr,
1072
+ destinationExpr: resolved2.helper.destinationExpr,
1073
+ servicePathExpr: resolved2.helper.servicePathExpr,
1074
+ isDynamic: resolved2.helper.isDynamic,
1075
+ placeholders: resolved2.helper.placeholders,
1076
+ sourceFile: normalizePath(filePath),
1077
+ sourceLine: lineOf3(sourceFileAst, node),
1078
+ helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName2, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
1079
+ });
1080
+ }
1081
+ }
1082
+ function recordDestructuredClassHelper(decl) {
1083
+ if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
1084
+ const call = unwrapCall(decl.initializer);
1085
+ if (!call || !ts5.isPropertyAccessExpression(call.expression)) return;
1086
+ const target = call.expression;
1087
+ if (target.expression.kind !== ts5.SyntaxKind.ThisKeyword) return;
1088
+ for (const el of decl.name.elements) {
1089
+ if (!ts5.isIdentifier(el.name)) continue;
1090
+ const propertyName2 = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1091
+ const helper = classHelpers.find(
1092
+ (h) => h.helperName === target.name.text && h.propertyName === propertyName2
1093
+ );
1094
+ if (!helper) continue;
1095
+ out.push({
1096
+ variableName: el.name.text,
1097
+ ...helper.fact,
1098
+ sourceFile: normalizePath(filePath),
1099
+ sourceLine: lineOf3(sourceFileAst, decl),
1100
+ helperChain: [
1101
+ {
1102
+ callerVariable: el.name.text,
1103
+ className: helper.className,
1104
+ classHelper: helper.helperName,
1105
+ returnedProperty: helper.propertyName,
1106
+ helperVariable: helper.variableName,
1107
+ helperSourceFile: normalizePath(filePath),
1108
+ helperSourceLine: helper.sourceLine
1289
1109
  }
1290
- }
1291
- } else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts4.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
1292
- const wrapperName = ts4.isIdentifier(expr) ? expr.text : ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) ? expr.expression.text : "";
1293
- const wrapperArgs = ts4.isIdentifier(expr) ? node.arguments : ts4.isCallExpression(expr) ? expr.arguments : node.arguments;
1294
- const spec = wrapperSpecs.get(wrapperName);
1295
- const clientArg = spec?.clientIndex === void 0 ? void 0 : wrapperArgs[spec.clientIndex];
1296
- const pathArg = spec ? wrapperArgs[spec.pathIndex] : void 0;
1297
- const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
1298
- const receiver = clientArg && ts4.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
1299
- const method = stripQuotes(resolveExpression(methodArg, node, "literal").value ?? spec?.methodLiteral ?? "POST");
1300
- const resolvedPath = staticPathExpression(pathArg, node);
1301
- const operationPathExpr = operationPathFromStatic(resolvedPath.text);
1302
- const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : void 0;
1303
- if (spec && receiver && operationPathExpr) {
1304
- add(node, { callType: "remote_action", serviceVariableName: receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75 }, { receiver, classifier: resolvedPath.sourceKind === "literal" ? "higher_order_wrapper_literal_path" : "higher_order_wrapper_static_path", wrapperFunction: wrapperName, nestedWrapperFunction: spec.nestedWrapperFunction, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf3(source.text, node.getStart(source)), wrapperPathSourceKind: resolvedPath.sourceKind, rawPathExpression: resolvedPath.rawExpression, normalizedOperationPath, literalPathSource: resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : `wrapper_call_${resolvedPath.sourceKind}`, literalCallerArgumentDetected: true });
1305
- } else if (spec && receiver) {
1306
- add(node, { callType: "remote_action", serviceVariableName: receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason: "dynamic_operation_path_identifier" }, { receiver, classifier: "higher_order_wrapper_dynamic_path", wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf3(source.text, node.getStart(source)), wrapperPathSourceKind: resolvedPath.sourceKind, rawPathExpression: resolvedPath.rawExpression, parserWarning: "dynamic_operation_path_identifier" });
1307
- }
1308
- } else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
1309
- const receiver = receiverName(expr.expression);
1310
- const rootReceiver = rootReceiverName(expr.expression);
1311
- if (isSupportedEventReceiver(receiver, rootReceiver, serviceVariables)) {
1312
- const eventName = literalText(node.arguments[0]);
1313
- if (eventName) add(node, { callType: expr.name.text === "on" ? "async_subscribe" : "async_emit", serviceVariableName: rootReceiver ?? receiver, eventNameExpr: eventName }, { receiver, rootReceiver, classifier: expr.name.text === "on" ? "cap_service_event_subscription" : "cap_service_event_emit", receiverClassification: "cap_evidence" });
1314
- }
1315
- } else {
1316
- const external = externalHttpEvidence(node, source);
1317
- if (external) {
1318
- const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
1319
- const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
1320
- add(node, { callType: "external_http", method: external.method, payloadSummary: void 0, confidence: 0.7, unresolvedReason: "External HTTP destination is outside indexed CAP services", externalTarget: { kind: safeTarget.kind, stableId: safeTarget.toId, label: safeTarget.label, dynamic: safeTarget.dynamic } }, { classifier: external.classifier, externalTarget: safeTarget, sourceCallShape: external.sourceCallShape });
1321
- }
1322
- }
1110
+ ]
1111
+ });
1323
1112
  }
1324
- ts4.forEachChild(node, visit);
1325
- };
1326
- visit(source);
1327
- return calls;
1328
- }
1329
- function containsSupportedOutboundCall(node) {
1330
- const source = node.getSourceFile();
1331
- const start = node.getFullStart();
1332
- const end = node.getEnd();
1333
- return classifyOutboundCallsInSource(source, source.fileName).some((call) => call.node.getStart(source) >= start && call.node.getEnd() <= end);
1334
- }
1335
- async function parseOutboundCalls(repoPath, filePath) {
1336
- const text = await fs6.readFile(path7.join(repoPath, filePath), "utf8");
1337
- const source = ts4.createSourceFile(filePath, text, ts4.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts4.ScriptKind.TS : ts4.ScriptKind.JS);
1338
- return [...classifyOutboundCallsInSource(source, filePath).map((call) => call.fact), ...parseLocalServiceCalls(text, filePath)];
1339
- }
1340
- function parseLocalServiceCalls(text, filePath) {
1341
- const source = ts4.createSourceFile(filePath, text, ts4.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts4.ScriptKind.TS : ts4.ScriptKind.JS);
1342
- const aliases = /* @__PURE__ */ new Map();
1343
- const calls = [];
1344
- const visit = (node) => {
1345
- if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer) {
1346
- const origin = serviceLookup(node.initializer, aliases);
1347
- if (origin) aliases.set(node.name.text, { ...origin, chain: [...origin.chain, node.name.text] });
1113
+ }
1114
+ function arrayElementsFromExpression(expr) {
1115
+ const unwrapped = unwrapIdentityExpression(expr);
1116
+ if (ts5.isArrayLiteralExpression(unwrapped)) return { elements: unwrapped.elements, promiseAll: false };
1117
+ const call = unwrapCall(expr);
1118
+ if (!call) return void 0;
1119
+ if (!ts5.isPropertyAccessExpression(call.expression) || call.expression.name.text !== "all" || call.expression.expression.getText(sourceFileAst) !== "Promise") return void 0;
1120
+ const first = call.arguments[0];
1121
+ if (!first) return void 0;
1122
+ const container = unwrapIdentityExpression(first);
1123
+ if (!ts5.isArrayLiteralExpression(container)) return void 0;
1124
+ return { elements: container.elements, promiseAll: true };
1125
+ }
1126
+ async function recordArrayElementBinding(targetName, expr, node, arrayIndex, promiseAll) {
1127
+ const before = out.length;
1128
+ await recordBindingFromExpression(targetName, expr, node, "declaration");
1129
+ if (out.length > before) {
1130
+ const row = out[out.length - 1];
1131
+ row.helperChain = [
1132
+ ...row.helperChain ?? [],
1133
+ { callerVariable: targetName, targetVariable: targetName, arrayIndex, promiseAll, arrayContainer: promiseAll ? "Promise.all" : "array_literal" }
1134
+ ];
1135
+ return;
1348
1136
  }
1349
- if (ts4.isCallExpression(node)) {
1350
- const parsed = serviceOperationCall(node, aliases);
1351
- if (parsed && parsed.operation !== "entities") calls.push({
1352
- callType: "local_service_call",
1353
- operationPathExpr: `/${parsed.operation}`,
1354
- payloadSummary: parsed.service,
1355
- localServiceName: parsed.service,
1356
- localServiceLookup: parsed.lookup,
1357
- aliasChain: parsed.chain,
1358
- sourceFile: normalizePath(filePath),
1359
- sourceLine: lineOf3(text, node.getStart(source)),
1360
- confidence: 0.9,
1361
- unresolvedReason: ["send", "emit", "publish", "on"].includes(parsed.operation) ? "transport_client_method" : void 0,
1362
- evidence: parserEvidence(source, node, {
1363
- classifier: parsed.classifier,
1364
- parserCallType: parsed.operation === "send" ? "transport_client_method" : parsed.classifier,
1365
- localServiceLookup: parsed.lookup,
1366
- localServiceName: parsed.service,
1367
- operation: parsed.operation,
1368
- aliasChain: parsed.chain
1369
- })
1137
+ const unwrapped = unwrapIdentityExpression(expr);
1138
+ if (ts5.isIdentifier(unwrapped)) {
1139
+ const existing = bindingForVariable(unwrapped.text);
1140
+ if (!existing) return;
1141
+ out.push({
1142
+ ...existing,
1143
+ variableName: targetName,
1144
+ sourceLine: lineOf3(sourceFileAst, node),
1145
+ helperChain: [
1146
+ ...existing.helperChain ?? [],
1147
+ { callerVariable: targetName, targetVariable: targetName, sourceVariable: unwrapped.text, aliasKind: "array-destructuring", arrayIndex, promiseAll, arrayContainer: promiseAll ? "Promise.all" : "array_literal" }
1148
+ ]
1370
1149
  });
1371
1150
  }
1372
- ts4.forEachChild(node, visit);
1373
- };
1374
- visit(source);
1375
- return calls;
1376
- }
1377
- function serviceLookup(expr, aliases) {
1378
- if (ts4.isIdentifier(expr)) return aliases.get(expr.text);
1379
- if (ts4.isPropertyAccessExpression(expr) && expr.expression.getText() === "cds.services") return { service: expr.name.text, lookup: expr.getText(), chain: [expr.getText()] };
1380
- if (ts4.isElementAccessExpression(expr) && expr.expression.getText() === "cds.services" && ts4.isStringLiteral(expr.argumentExpression)) return { service: expr.argumentExpression.text, lookup: expr.getText(), chain: [expr.getText()] };
1381
- return void 0;
1151
+ }
1152
+ async function recordArrayDestructuredVariable(decl) {
1153
+ if (!ts5.isArrayBindingPattern(decl.name) || !decl.initializer) return;
1154
+ const container = arrayElementsFromExpression(decl.initializer);
1155
+ if (!container) return;
1156
+ for (let index = 0; index < decl.name.elements.length; index += 1) {
1157
+ const el = decl.name.elements[index];
1158
+ if (!el || ts5.isOmittedExpression(el) || ts5.isBindingElement(el) && el.dotDotDotToken) continue;
1159
+ if (!ts5.isBindingElement(el) || !ts5.isIdentifier(el.name)) continue;
1160
+ const source = container.elements[index];
1161
+ if (!source || ts5.isOmittedExpression(source)) continue;
1162
+ await recordArrayElementBinding(el.name.text, source, decl, index, container.promiseAll);
1163
+ }
1164
+ }
1165
+ async function recordArrayDestructuredAssignment(pattern, expr, node) {
1166
+ const container = arrayElementsFromExpression(expr);
1167
+ if (!container) return;
1168
+ for (let index = 0; index < pattern.elements.length; index += 1) {
1169
+ const el = pattern.elements[index];
1170
+ if (!el || ts5.isOmittedExpression(el) || ts5.isSpreadElement(el) || !ts5.isIdentifier(el)) continue;
1171
+ const source = container.elements[index];
1172
+ if (!source || ts5.isOmittedExpression(source)) continue;
1173
+ await recordArrayElementBinding(el.text, source, node, index, container.promiseAll);
1174
+ }
1175
+ }
1176
+ const events = [];
1177
+ function collectEvents(node) {
1178
+ if (ts5.isVariableDeclaration(node)) events.push({ pos: node.getStart(sourceFileAst), node });
1179
+ if (ts5.isBinaryExpression(node) && node.operatorToken.kind === ts5.SyntaxKind.EqualsToken)
1180
+ events.push({ pos: node.getStart(sourceFileAst), node });
1181
+ ts5.forEachChild(node, collectEvents);
1182
+ }
1183
+ collectEvents(sourceFileAst);
1184
+ events.sort((a, b) => a.pos - b.pos);
1185
+ for (const event of events) {
1186
+ if (ts5.isVariableDeclaration(event.node)) {
1187
+ const decl = event.node;
1188
+ await recordDestructuredHelper(decl);
1189
+ await recordArrayDestructuredVariable(decl);
1190
+ recordDestructuredClassHelper(decl);
1191
+ await rememberObjectHelperVariable(decl);
1192
+ if (ts5.isIdentifier(decl.name) && decl.initializer) recordObjectPropertyBinding(decl.name.text, decl.initializer, decl);
1193
+ await recordVariable(decl);
1194
+ recordIdentityAlias(decl);
1195
+ if (ts5.isIdentifier(decl.name) && decl.initializer) {
1196
+ const sourceName = transactionReceiverName(decl.initializer);
1197
+ if (sourceName) cloneAliasBinding(decl.name.text, sourceName, "transaction", decl);
1198
+ }
1199
+ continue;
1200
+ }
1201
+ const assignment = event.node;
1202
+ if (ts5.isIdentifier(assignment.left)) {
1203
+ const rhs = unwrapIdentityExpression(assignment.right);
1204
+ if (ts5.isIdentifier(rhs)) {
1205
+ cloneAliasBinding(assignment.left.text, rhs.text, "identity-assignment", assignment);
1206
+ continue;
1207
+ }
1208
+ if (recordObjectPropertyBinding(assignment.left.text, assignment.right, assignment)) continue;
1209
+ await recordBindingFromExpression(assignment.left.text, assignment.right, assignment, "assignment");
1210
+ continue;
1211
+ }
1212
+ const left = ts5.isParenthesizedExpression(assignment.left) ? assignment.left.expression : assignment.left;
1213
+ if (ts5.isObjectLiteralExpression(left))
1214
+ await recordDestructuredAssignment(left, assignment.right, assignment);
1215
+ if (ts5.isArrayLiteralExpression(left))
1216
+ await recordArrayDestructuredAssignment(left, assignment.right, assignment);
1217
+ }
1218
+ return out;
1382
1219
  }
1383
- function serviceOperationCall(node, aliases) {
1384
- const expr = node.expression;
1385
- if (!ts4.isPropertyAccessExpression(expr)) return void 0;
1386
- const origin = serviceLookup(expr.expression, aliases);
1387
- if (!origin) return void 0;
1388
- if (expr.name.text === "send") {
1389
- const first = literalText(node.arguments[0]);
1390
- const second = literalText(node.arguments[1]);
1391
- const method = first?.toUpperCase();
1392
- if (method && ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"].includes(method) && second) return { ...origin, operation: second.replace(/^\//, ""), classifier: "cap_service_send_method_path" };
1393
- if (first) return { ...origin, operation: first.replace(/^\//, ""), classifier: "cap_service_send_local_dispatch" };
1220
+ function collectClassHelpers(sf) {
1221
+ const helpers = [];
1222
+ for (const stmt of sf.statements) {
1223
+ if (!ts5.isClassDeclaration(stmt) || !stmt.name) continue;
1224
+ for (const member of stmt.members) {
1225
+ let visit2 = function(node) {
1226
+ if (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
1227
+ const fact = findConnectInExpression(node.initializer);
1228
+ if (fact) bindings.set(node.name.text, fact);
1229
+ }
1230
+ if (ts5.isReturnStatement(node) && node.expression && ts5.isObjectLiteralExpression(node.expression)) {
1231
+ for (const prop of node.expression.properties) {
1232
+ if (ts5.isShorthandPropertyAssignment(prop)) {
1233
+ const fact = bindings.get(prop.name.text);
1234
+ if (fact)
1235
+ helpers.push({
1236
+ className,
1237
+ helperName,
1238
+ propertyName: prop.name.text,
1239
+ variableName: prop.name.text,
1240
+ fact,
1241
+ sourceLine: lineOf3(sf, prop)
1242
+ });
1243
+ }
1244
+ if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.initializer)) {
1245
+ const propertyName2 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
1246
+ const fact = propertyName2 ? bindings.get(prop.initializer.text) : void 0;
1247
+ if (propertyName2 && fact)
1248
+ helpers.push({
1249
+ className,
1250
+ helperName,
1251
+ propertyName: propertyName2,
1252
+ variableName: prop.initializer.text,
1253
+ fact,
1254
+ sourceLine: lineOf3(sf, prop)
1255
+ });
1256
+ }
1257
+ }
1258
+ }
1259
+ ts5.forEachChild(node, visit2);
1260
+ };
1261
+ var visit = visit2;
1262
+ if (!ts5.isPropertyDeclaration(member) || !member.initializer) continue;
1263
+ if (!ts5.isIdentifier(member.name)) continue;
1264
+ const className = stmt.name.text;
1265
+ const helperName = member.name.text;
1266
+ const initializer = member.initializer;
1267
+ if (!ts5.isArrowFunction(initializer) && !ts5.isFunctionExpression(initializer))
1268
+ continue;
1269
+ const bindings = /* @__PURE__ */ new Map();
1270
+ visit2(initializer);
1271
+ }
1394
1272
  }
1395
- return { ...origin, operation: expr.name.text, classifier: "local_cap_service_call" };
1273
+ return helpers;
1396
1274
  }
1397
1275
 
1398
- // src/parsers/service-binding-parser.ts
1399
- import path9 from "path";
1400
- import ts6 from "typescript";
1401
-
1402
- // src/parsers/service-binding-parser-helpers.ts
1276
+ // src/parsers/outbound-call-parser.ts
1403
1277
  import fs7 from "fs/promises";
1404
- import path8 from "path";
1405
- import ts5 from "typescript";
1406
- function lineOf4(sf, node) {
1407
- return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
1408
- }
1409
- function stringValue(node) {
1410
- if (!node) return void 0;
1411
- if (ts5.isStringLiteralLike(node) || ts5.isNoSubstitutionTemplateLiteral(node)) return node.text;
1412
- if (ts5.isTemplateExpression(node)) return node.getText().replace(/^`|`$/g, "");
1413
- return node.getText();
1414
- }
1415
- function placeholders2(value) {
1416
- return [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((m) => (m[1] ?? "").trim()).filter(Boolean);
1278
+ import path10 from "path";
1279
+ import ts7 from "typescript";
1280
+
1281
+ // src/linker/external-http-target.ts
1282
+ import { createHash } from "crypto";
1283
+ var sensitiveKeys = /* @__PURE__ */ new Set(["token", "access_token", "id_token", "api_key", "apikey", "key", "password", "passwd", "pwd", "secret", "client_secret", "authorization", "cookie", "signature"]);
1284
+ function hash(value) {
1285
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
1417
1286
  }
1418
- function connectFactFromCall(call) {
1419
- const expr = call.expression;
1420
- if (!ts5.isPropertyAccessExpression(expr) || expr.name.text !== "to") return void 0;
1421
- const inner = expr.expression;
1422
- if (!ts5.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds") return void 0;
1423
- const first = call.arguments[0];
1424
- if (!first) return void 0;
1425
- const second = call.arguments[1];
1426
- const objectArg = ts5.isObjectLiteralExpression(first) ? first : second && ts5.isObjectLiteralExpression(second) ? second : void 0;
1427
- let alias;
1428
- let aliasExpr;
1429
- if (ts5.isStringLiteralLike(first) || ts5.isNoSubstitutionTemplateLiteral(first)) alias = first.text;
1430
- else if (!ts5.isObjectLiteralExpression(first)) aliasExpr = stringValue(first);
1431
- if ((ts5.isStringLiteralLike(first) || ts5.isNoSubstitutionTemplateLiteral(first)) && !objectArg) return { alias: first.text, isDynamic: false, placeholders: [] };
1432
- if (!objectArg && aliasExpr) return { aliasExpr, isDynamic: true, placeholders: placeholders2(aliasExpr) };
1433
- const expressions = objectArg ? objectExpressions(objectArg) : {};
1434
- const ph = [...placeholders2(aliasExpr ?? alias), ...placeholders2(expressions.destinationExpr), ...placeholders2(expressions.servicePathExpr)];
1435
- return { alias, aliasExpr, ...expressions, isDynamic: ph.length > 0 || !expressions.destinationExpr && !expressions.servicePathExpr, placeholders: ph };
1287
+ function methodPrefix(method) {
1288
+ return typeof method === "string" && method.length > 0 ? `${method.toUpperCase()} ` : "";
1436
1289
  }
1437
- function objectExpressions(objectArg) {
1438
- const out = {};
1439
- function visitObject(obj) {
1440
- for (const prop of obj.properties) {
1441
- if (!ts5.isPropertyAssignment(prop)) continue;
1442
- const name = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
1443
- if (name === "destination") out.destinationExpr = stringValue(prop.initializer);
1444
- if (name === "path" || name === "servicePath") out.servicePathExpr = stringValue(prop.initializer);
1445
- if (ts5.isObjectLiteralExpression(prop.initializer)) visitObject(prop.initializer);
1446
- }
1290
+ function redactUrl(value) {
1291
+ try {
1292
+ const url = new URL(value, value.startsWith("/") ? "https://relative.invalid" : void 0);
1293
+ url.username = "";
1294
+ url.password = "";
1295
+ for (const key of [...url.searchParams.keys()]) url.searchParams.set(key, sensitiveKeys.has(key.toLowerCase()) ? "<redacted>" : "<redacted>");
1296
+ const path11 = `${url.pathname}${url.search ? url.search : ""}`;
1297
+ return value.startsWith("/") ? path11 : `${url.origin}${path11}`;
1298
+ } catch {
1299
+ return value.replace(/([?&][^=;&]*(?:token|key|password|secret|cookie|authorization)[^=;&]*=)[^&]*/gi, "$1<redacted>");
1447
1300
  }
1448
- visitObject(objectArg);
1449
- return out;
1450
- }
1451
- function unwrapCall(expr) {
1452
- if (ts5.isAwaitExpression(expr)) return unwrapCall(expr.expression);
1453
- if (ts5.isParenthesizedExpression(expr)) return unwrapCall(expr.expression);
1454
- if (ts5.isAsExpression(expr) || ts5.isSatisfiesExpression(expr)) return unwrapCall(expr.expression);
1455
- if (ts5.isTypeAssertionExpression(expr)) return unwrapCall(expr.expression);
1456
- if (ts5.isCallExpression(expr)) return expr;
1457
- return void 0;
1458
- }
1459
- function unwrapIdentityExpression(expr) {
1460
- if (ts5.isAwaitExpression(expr)) return unwrapIdentityExpression(expr.expression);
1461
- if (ts5.isParenthesizedExpression(expr)) return unwrapIdentityExpression(expr.expression);
1462
- if (ts5.isAsExpression(expr) || ts5.isSatisfiesExpression(expr)) return unwrapIdentityExpression(expr.expression);
1463
- if (ts5.isTypeAssertionExpression(expr)) return unwrapIdentityExpression(expr.expression);
1464
- return expr;
1465
- }
1466
- function transactionReceiverName(expr) {
1467
- const call = unwrapCall(expr);
1468
- if (call && ts5.isPropertyAccessExpression(call.expression) && ["tx", "transaction"].includes(call.expression.name.text) && ts5.isIdentifier(call.expression.expression)) return call.expression.expression.text;
1469
- const unwrapped = unwrapIdentityExpression(expr);
1470
- if (!ts5.isConditionalExpression(unwrapped)) return void 0;
1471
- const left = transactionReceiverName(unwrapped.whenTrue);
1472
- const right = transactionReceiverName(unwrapped.whenFalse);
1473
- return left && left === right ? left : void 0;
1474
1301
  }
1475
- function findConnectInExpression(expr) {
1476
- const direct = unwrapCall(expr);
1477
- if (direct) {
1478
- const fact = connectFactFromCall(direct);
1479
- if (fact) return fact;
1302
+ function externalHttpTarget(call) {
1303
+ const evidence = typeof call.evidence_json === "string" ? safeParse(call.evidence_json) : {};
1304
+ const target = evidence.externalTarget && typeof evidence.externalTarget === "object" && !Array.isArray(evidence.externalTarget) ? evidence.externalTarget : {};
1305
+ const method = typeof call.method === "string" ? call.method : typeof target.method === "string" ? target.method : void 0;
1306
+ const kind = typeof target.kind === "string" ? target.kind : "unknown";
1307
+ const expression = typeof target.expression === "string" ? target.expression : void 0;
1308
+ if (kind === "destination" && target.dynamic === true) {
1309
+ const shape = typeof target.expressionShape === "string" ? target.expressionShape : "expression";
1310
+ const candidates = Array.isArray(target.candidateLiterals) ? target.candidateLiterals.filter((item) => typeof item === "string") : [];
1311
+ return { kind, toKind: "external_destination", toId: `destination:dynamic:${hash(`${shape}:${candidates.join("|")}`)}`, label: "External destination: dynamic destination", method, dynamic: true, expression: candidates.length ? `candidates:${candidates.join("|")}` : `shape:${shape}` };
1480
1312
  }
1481
- let found;
1482
- function visit(node) {
1483
- if (found) return;
1484
- if (ts5.isCallExpression(node)) found = connectFactFromCall(node);
1485
- if (!found) ts5.forEachChild(node, visit);
1313
+ if (kind === "destination" && expression) return { kind, toKind: "external_destination", toId: `destination:${expression}`, label: `External destination: ${expression}`, method, dynamic: false, expression };
1314
+ if (kind === "static_url" && expression) {
1315
+ const redacted = redactUrl(expression);
1316
+ return { kind, toKind: "external_endpoint", toId: `endpoint:${hash(`${method ?? ""}:${redacted}`)}`, label: `External endpoint: ${methodPrefix(method)}${redacted}`, method, dynamic: false, expression: redacted };
1486
1317
  }
1487
- visit(expr);
1488
- return found;
1318
+ if (kind === "url_expression" && expression) return { kind, toKind: "external_endpoint", toId: `dynamic:${hash(expression)}`, label: `External endpoint: ${methodPrefix(method)}dynamic URL`, method, dynamic: true, expression: `expr:${hash(expression)}` };
1319
+ return { kind: "unknown", toKind: "external_endpoint", toId: "unknown", label: "External endpoint: unknown", method, dynamic: false };
1489
1320
  }
1490
- async function readSource(abs) {
1321
+ function safeParse(value) {
1491
1322
  try {
1492
- const text = await fs7.readFile(abs, "utf8");
1493
- return ts5.createSourceFile(abs, text, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
1323
+ const parsed = JSON.parse(value);
1324
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1494
1325
  } catch {
1495
- return void 0;
1496
- }
1497
- }
1498
- async function resolveImport(repoPath, fromFile, spec) {
1499
- if (!spec.startsWith(".")) return void 0;
1500
- const rawBase = path8.resolve(repoPath, path8.dirname(fromFile), spec);
1501
- const parsed = path8.parse(rawBase);
1502
- const base = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"].includes(parsed.ext) ? path8.join(parsed.dir, parsed.name) : rawBase;
1503
- for (const candidate of [base, `${base}.ts`, `${base}.js`, path8.join(base, "index.ts"), path8.join(base, "index.js")]) {
1504
- const stat = await fs7.stat(candidate).catch(() => void 0);
1505
- if (stat?.isFile()) return normalizePath(path8.relative(repoPath, candidate));
1506
- }
1507
- return void 0;
1508
- }
1509
- async function importsFor(repoPath, filePath, sf) {
1510
- const imports = [];
1511
- for (const stmt of sf.statements) {
1512
- if (!ts5.isImportDeclaration(stmt) || !ts5.isStringLiteralLike(stmt.moduleSpecifier)) continue;
1513
- const sourceFile = await resolveImport(repoPath, filePath, stmt.moduleSpecifier.text);
1514
- const clause = stmt.importClause;
1515
- if (!clause) continue;
1516
- if (clause.name) imports.push({ localName: clause.name.text, exportedName: "default", sourceFile });
1517
- const bindings = clause.namedBindings;
1518
- if (bindings && ts5.isNamedImports(bindings))
1519
- for (const el of bindings.elements) imports.push({ localName: el.name.text, exportedName: el.propertyName?.text ?? el.name.text, sourceFile });
1326
+ return {};
1520
1327
  }
1521
- return imports;
1522
1328
  }
1523
1329
 
1524
- // src/parsers/service-binding-parser.ts
1525
- function collectLocalBindingFacts(fn) {
1526
- const bindings = /* @__PURE__ */ new Map();
1527
- function visit(node) {
1528
- if (node !== fn && (ts6.isFunctionDeclaration(node) || ts6.isArrowFunction(node) || ts6.isFunctionExpression(node)))
1529
- return;
1530
- if (ts6.isVariableDeclaration(node) && ts6.isIdentifier(node.name) && node.initializer) {
1531
- const fact = findConnectInExpression(node.initializer);
1532
- if (fact) bindings.set(node.name.text, fact);
1533
- const sourceName = transactionReceiverName(node.initializer);
1534
- if (sourceName) {
1535
- const source = bindings.get(sourceName);
1536
- if (source) bindings.set(node.name.text, { ...source, helperChain: [...source.helperChain ?? [], { aliasOf: sourceName, callerVariable: node.name.text, aliasKind: "transaction", transactionAliasSource: sourceName }] });
1537
- }
1538
- }
1539
- ts6.forEachChild(node, visit);
1540
- }
1541
- visit(fn);
1542
- return bindings;
1330
+ // src/linker/odata-path-normalizer.ts
1331
+ function normalizeODataOperationInvocationPath(path11) {
1332
+ if (path11 === void 0) return void 0;
1333
+ const raw = path11.trim();
1334
+ if (!raw) return void 0;
1335
+ const rejected = (reason) => ({ rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false, invocationArgumentPlaceholderKeys: [], normalizationRejectedReason: reason });
1336
+ const open = raw.indexOf("(");
1337
+ if (open < 0) return rejected("no_top_level_parenthesis");
1338
+ const query = topLevelQueryIndex(raw);
1339
+ if (query >= 0) return rejected("query_string_paths_are_not_operation_invocations");
1340
+ if (!raw.startsWith("/")) return rejected("path_is_not_absolute");
1341
+ if (raw.slice(1, open).includes("/")) return rejected("operation_segment_contains_navigation_separator");
1342
+ const close = matchingClose(raw, open);
1343
+ if (close === void 0) return rejected("top_level_invocation_parenthesis_is_unbalanced");
1344
+ if (raw.slice(close + 1).trim().length > 0) return rejected("top_level_invocation_does_not_cover_remaining_path");
1345
+ const operationSegment = raw.slice(0, open).trim();
1346
+ if (operationSegment.length <= 1) return rejected("operation_segment_is_empty");
1347
+ return {
1348
+ rawOperationPath: raw,
1349
+ normalizedOperationPath: operationSegment,
1350
+ wasInvocation: true,
1351
+ invocationArguments: raw.slice(open + 1, close),
1352
+ invocationArgumentPlaceholderKeys: [...new Set(extractTemplatePlaceholders(raw.slice(open + 1, close)))],
1353
+ normalizationReason: "balanced_top_level_operation_invocation"
1354
+ };
1543
1355
  }
1544
- function collectReturnedObjectBindings(fn) {
1545
- const bindings = collectLocalBindingFacts(fn);
1546
- const returns = /* @__PURE__ */ new Map();
1547
- function visit(node) {
1548
- if (node !== fn && (ts6.isFunctionDeclaration(node) || ts6.isArrowFunction(node) || ts6.isFunctionExpression(node)))
1549
- return;
1550
- if (ts6.isReturnStatement(node) && node.expression && ts6.isObjectLiteralExpression(node.expression)) {
1551
- for (const prop of node.expression.properties) {
1552
- if (ts6.isShorthandPropertyAssignment(prop)) returns.set(prop.name.text, prop.name.text);
1553
- if (ts6.isPropertyAssignment(prop) && ts6.isIdentifier(prop.initializer)) {
1554
- const propertyName = ts6.isIdentifier(prop.name) || ts6.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
1555
- if (propertyName) returns.set(propertyName, prop.initializer.text);
1556
- }
1557
- }
1558
- }
1559
- ts6.forEachChild(node, visit);
1356
+ function classifyODataPathIntent(path11, method) {
1357
+ const rawPath = (path11 ?? "").trim();
1358
+ const normalizedMethod = (method ?? "GET").trim().toUpperCase() || "GET";
1359
+ const queryIndex = rawPath.indexOf("?");
1360
+ const pathWithoutQuery = queryIndex >= 0 ? rawPath.slice(0, queryIndex) : rawPath;
1361
+ const queryString = queryIndex >= 0 ? rawPath.slice(queryIndex + 1) : void 0;
1362
+ const segments = pathWithoutQuery.replace(/^\//, "").split("/").filter(Boolean);
1363
+ const firstSegment = segments[0] ?? "";
1364
+ const hasNavigationSegments = segments.length > 1;
1365
+ const entitySegment = entitySegmentFromPath(pathWithoutQuery);
1366
+ const placeholderKeys2 = [...new Set(extractTemplatePlaceholders(rawPath))];
1367
+ const firstOpen = firstSegment.indexOf("(");
1368
+ const firstClose = firstOpen >= 0 ? matchingClose(firstSegment, firstOpen) : void 0;
1369
+ const keyPredicateText = firstOpen >= 0 && firstClose !== void 0 ? firstSegment.slice(firstOpen + 1, firstClose) : "";
1370
+ const rawKeyPredicatePlaceholderKeys = [...new Set(extractTemplatePlaceholders(keyPredicateText))];
1371
+ const navigationSuffix = hasNavigationSegments ? segments.slice(1).join("/") : void 0;
1372
+ const lastSegment = segments.at(-1) ?? "";
1373
+ const mediaOrPropertySuffix = hasNavigationSegments ? lastSegment : void 0;
1374
+ const invocation = normalizeODataOperationInvocationPath(pathWithoutQuery);
1375
+ const operationNameFromInvocation = invocation?.wasInvocation ? invocation.normalizedOperationPath.replace(/^\//, "").split(".").at(-1) : void 0;
1376
+ const topLevelName = firstSegment.split("(")[0]?.replace(/^\//, "");
1377
+ const topLevelOperationName = operationNameFromInvocation ?? (!hasNavigationSegments && !firstSegment.includes("(") ? topLevelName : void 0);
1378
+ const topLevelOperationInvocation = Boolean(invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment));
1379
+ const keyPredicatePlaceholderKeys = topLevelOperationInvocation ? [] : rawKeyPredicatePlaceholderKeys;
1380
+ const topLevelOperationNameCandidate = Boolean(topLevelOperationName && !hasNavigationSegments && !firstSegment.includes("("));
1381
+ const base = { rawPath, method: normalizedMethod, pathWithoutQuery, queryString, hasQueryString: queryIndex >= 0, entitySegment, placeholderKeys: placeholderKeys2, keyPredicatePlaceholderKeys, invocationArguments: invocation?.invocationArguments, invocationArgumentPlaceholderKeys: invocation?.invocationArgumentPlaceholderKeys ?? [], navigationSuffix, mediaOrPropertySuffix, hasEntityKeyPredicate: firstOpen >= 0 && firstClose !== void 0, hasNavigationSuffix: hasNavigationSegments, hasMediaOrPropertySuffix: Boolean(mediaOrPropertySuffix && isMediaOrPropertySuffix(mediaOrPropertySuffix)), topLevelOperationName, topLevelOperationNameCandidate, topLevelOperationInvocation };
1382
+ if (!rawPath || !rawPath.startsWith("/")) return { ...base, kind: "unknown", reason: "path_missing_or_not_absolute" };
1383
+ const upperEntityLike = /^[A-Z][A-Za-z0-9_]*$/.test(entitySegment ?? firstSegment);
1384
+ const mediaLike = isMediaOrPropertySuffix(segments.at(-1) ?? "");
1385
+ if (normalizedMethod !== "GET") {
1386
+ if (invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "operation_invocation", reason: "non_get_balanced_top_level_operation_invocation" };
1387
+ if (mediaLike) return { ...base, kind: "entity_media", reason: "non_get_entity_media_stream_path" };
1388
+ if (hasNavigationSegments || firstSegment.includes("(")) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: firstSegment.includes("(") ? "non_get_entity_key_or_navigation_path_shape" : "non_get_entity_navigation_path_shape" };
1389
+ if (upperEntityLike) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: "non_get_entity_path_shape" };
1390
+ return { ...base, kind: "operation_invocation", reason: "non_get_lowercase_path_may_be_operation" };
1560
1391
  }
1561
- visit(fn);
1562
- const out = /* @__PURE__ */ new Map();
1563
- for (const [propertyName, variableName] of returns) {
1564
- const fact = bindings.get(variableName);
1565
- if (fact) out.set(propertyName, fact);
1392
+ if (queryIndex >= 0) {
1393
+ if (hasNavigationSegments) return { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_and_query_string" };
1394
+ if (looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "unknown", reason: "get_invocation_with_query_string_requires_indexed_operation_evidence" };
1395
+ return { ...base, kind: "entity_query", reason: "get_collection_path_has_query_string" };
1396
+ }
1397
+ if (hasNavigationSegments) return mediaLike ? { ...base, kind: "entity_media", reason: "get_entity_media_stream_path" } : { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_segments" };
1398
+ if (firstSegment.includes("(")) {
1399
+ if (invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "operation_invocation", reason: "get_balanced_top_level_operation_invocation" };
1400
+ return looksLikeLowerCamelInvocation(firstSegment) ? { ...base, kind: "operation_invocation", reason: "get_single_lower_camel_segment_has_top_level_invocation" } : { ...base, kind: "entity_key_read", reason: "get_entity_segment_has_key_predicate" };
1566
1401
  }
1567
- return out;
1402
+ if (/^[A-Z][A-Za-z0-9_]*$/.test(firstSegment)) return { ...base, kind: "entity_candidate", reason: "uppercase_collection_segment_without_indexed_entity_evidence" };
1403
+ return { ...base, kind: "unknown", reason: "get_path_has_no_query_key_or_navigation_signal" };
1568
1404
  }
1569
- function functionLikeInitializer(expr) {
1570
- if (!expr) return void 0;
1571
- if (ts6.isArrowFunction(expr) || ts6.isFunctionExpression(expr)) return expr;
1572
- return void 0;
1405
+ function entitySegmentFromPath(path11) {
1406
+ const first = path11.replace(/^\//, "").split("/")[0]?.trim();
1407
+ if (!first) return void 0;
1408
+ const open = first.indexOf("(");
1409
+ const entity = (open >= 0 ? first.slice(0, open) : first).trim();
1410
+ return entity || void 0;
1573
1411
  }
1574
- function directReturnConnectFact(fn) {
1575
- const localBindings = collectLocalBindingFacts(fn);
1576
- let returned;
1577
- function visit(node) {
1578
- if (node !== fn && (ts6.isFunctionDeclaration(node) || ts6.isArrowFunction(node) || ts6.isFunctionExpression(node)))
1579
- return;
1580
- if (!returned && ts6.isReturnStatement(node) && node.expression)
1581
- returned = node.expression;
1582
- if (!returned) ts6.forEachChild(node, visit);
1583
- }
1584
- visit(fn);
1585
- if (!returned) return void 0;
1586
- if (ts6.isIdentifier(returned)) return localBindings.get(returned.text);
1587
- return findConnectInExpression(returned);
1412
+ function isMediaOrPropertySuffix(segment) {
1413
+ return ["file", "content", "$value", "metadata", "items"].includes(segment.toLowerCase());
1588
1414
  }
1589
- function directConnectFactFromFunctionLike(fn) {
1590
- if (ts6.isArrowFunction(fn) && fn.body && !ts6.isBlock(fn.body))
1591
- return findConnectInExpression(fn.body);
1592
- return directReturnConnectFact(fn);
1415
+ function looksLikeLowerCamelInvocation(segment) {
1416
+ const open = segment.indexOf("(");
1417
+ if (open <= 0) return false;
1418
+ const name = segment.slice(0, open).split(".").at(-1) ?? segment.slice(0, open);
1419
+ return /^[a-z][A-Za-z0-9_]*$/.test(name);
1593
1420
  }
1594
- function exportedLocalNames(sf) {
1595
- const exports = /* @__PURE__ */ new Map();
1596
- for (const stmt of sf.statements) {
1597
- const direct = ts6.canHaveModifiers(stmt) ? ts6.getModifiers(stmt)?.some(
1598
- (m) => m.kind === ts6.SyntaxKind.ExportKeyword
1599
- ) ?? false : false;
1600
- if (direct && ts6.isFunctionDeclaration(stmt) && stmt.name)
1601
- exports.set(stmt.name.text, stmt.name.text);
1602
- if (direct && ts6.isVariableStatement(stmt)) {
1603
- for (const decl of stmt.declarationList.declarations)
1604
- if (ts6.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
1605
- }
1606
- if (!ts6.isExportDeclaration(stmt) || !stmt.exportClause) continue;
1607
- if (!ts6.isNamedExports(stmt.exportClause)) continue;
1608
- for (const el of stmt.exportClause.elements)
1609
- exports.set(el.name.text, el.propertyName?.text ?? el.name.text);
1421
+ function extractTemplatePlaceholders(text) {
1422
+ const keys = [];
1423
+ for (let index = 0; index < text.length - 1; index += 1) {
1424
+ if (text[index] !== "$" || text[index + 1] !== "{") continue;
1425
+ const close = matchingPlaceholderClose(text, index + 1);
1426
+ if (close === void 0) continue;
1427
+ const key = text.slice(index + 2, close).trim();
1428
+ if (key) keys.push(key);
1429
+ index = close;
1610
1430
  }
1611
- return exports;
1431
+ return keys;
1612
1432
  }
1613
- async function helperBindings(repoPath, filePath) {
1614
- const sf = await readSource(path9.join(repoPath, filePath));
1615
- if (!sf) return [];
1616
- const sourceFileAst = sf;
1617
- const out = [];
1618
- const exportedLocals = exportedLocalNames(sf);
1619
- const factsByLocal = /* @__PURE__ */ new Map();
1620
- for (const stmt of sf.statements) {
1621
- if (ts6.isFunctionDeclaration(stmt) && stmt.name) {
1622
- const fact = directConnectFactFromFunctionLike(stmt);
1623
- if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf4(sf, stmt) });
1624
- for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(stmt))
1625
- factsByLocal.set(`${stmt.name.text}#${returnedProperty}`, { ...objectFact, returnedProperty, sourceLine: lineOf4(sf, stmt) });
1626
- }
1627
- if (ts6.isVariableStatement(stmt))
1628
- for (const decl of stmt.declarationList.declarations) {
1629
- if (!ts6.isIdentifier(decl.name) || !decl.initializer) continue;
1630
- const helper = functionLikeInitializer(decl.initializer);
1631
- if (helper) {
1632
- const directReturn = directConnectFactFromFunctionLike(helper);
1633
- if (directReturn)
1634
- factsByLocal.set(decl.name.text, {
1635
- ...directReturn,
1636
- sourceLine: lineOf4(sourceFileAst, decl)
1637
- });
1638
- for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(helper))
1639
- factsByLocal.set(`${decl.name.text}#${returnedProperty}`, {
1640
- ...objectFact,
1641
- returnedProperty,
1642
- sourceLine: lineOf4(sourceFileAst, decl)
1643
- });
1644
- continue;
1645
- }
1646
- const fact = findConnectInExpression(decl.initializer);
1647
- if (fact)
1648
- factsByLocal.set(decl.name.text, {
1649
- ...fact,
1650
- sourceLine: lineOf4(sourceFileAst, decl)
1651
- });
1433
+ function matchingClose(text, openIndex) {
1434
+ let depth = 0;
1435
+ let quote;
1436
+ for (let index = openIndex; index < text.length; index += 1) {
1437
+ const char = text[index];
1438
+ const prev = text[index - 1];
1439
+ if (quote) {
1440
+ if (prev === "\\") continue;
1441
+ if (quote === "template" && char === "$" && text[index + 1] === "{") {
1442
+ const close = matchingPlaceholderClose(text, index + 1);
1443
+ if (close === void 0) return void 0;
1444
+ index = close;
1445
+ continue;
1652
1446
  }
1653
- }
1654
- for (const [exportedName, localName] of exportedLocals) {
1655
- const fact = factsByLocal.get(localName);
1656
- if (fact)
1657
- out.push({
1658
- ...fact,
1659
- exportedName,
1660
- sourceFile: normalizePath(filePath),
1661
- sourceLine: fact.sourceLine
1662
- });
1663
- }
1664
- for (const [key, fact] of factsByLocal) {
1665
- const [localName, returnedProperty] = key.split("#");
1666
- if (!returnedProperty) continue;
1667
- for (const [exportedName, exportedLocal] of exportedLocals) {
1668
- if (exportedLocal !== localName) continue;
1669
- out.push({ ...fact, exportedName, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: fact.sourceLine });
1447
+ if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
1448
+ continue;
1449
+ }
1450
+ if (char === "$" && text[index + 1] === "{") {
1451
+ const close = matchingPlaceholderClose(text, index + 1);
1452
+ if (close === void 0) return void 0;
1453
+ index = close;
1454
+ continue;
1455
+ }
1456
+ if (char === "'") {
1457
+ quote = "single";
1458
+ continue;
1459
+ }
1460
+ if (char === '"') {
1461
+ quote = "double";
1462
+ continue;
1463
+ }
1464
+ if (char === "`") {
1465
+ quote = "template";
1466
+ continue;
1467
+ }
1468
+ if (char === "(") depth += 1;
1469
+ if (char === ")") {
1470
+ depth -= 1;
1471
+ if (depth === 0) return index;
1472
+ if (depth < 0) return void 0;
1670
1473
  }
1671
1474
  }
1672
- return out;
1475
+ return void 0;
1673
1476
  }
1674
- async function parseServiceBindings(repoPath, filePath) {
1675
- const sf = await readSource(path9.join(repoPath, filePath));
1676
- if (!sf) return [];
1677
- const sourceFileAst = sf;
1678
- const out = [];
1679
- const imports = await importsFor(repoPath, filePath, sf);
1680
- const helperCache = /* @__PURE__ */ new Map();
1681
- const classHelpers = collectClassHelpers(sourceFileAst);
1682
- const localObjectHelpers = /* @__PURE__ */ new Map();
1683
- const localDirectHelpers = /* @__PURE__ */ new Map();
1684
- const objectHelperVariables = /* @__PURE__ */ new Map();
1685
- for (const stmt of sourceFileAst.statements) {
1686
- if (ts6.isFunctionDeclaration(stmt) && stmt.name) {
1687
- const directFact = directConnectFactFromFunctionLike(stmt);
1688
- if (directFact) localDirectHelpers.set(stmt.name.text, { ...directFact, exportedName: stmt.name.text, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, stmt) });
1689
- const rows2 = [];
1690
- for (const [returnedProperty, fact] of collectReturnedObjectBindings(stmt))
1691
- rows2.push({ ...fact, exportedName: stmt.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, stmt) });
1692
- if (rows2.length > 0) localObjectHelpers.set(stmt.name.text, rows2);
1693
- }
1694
- if (ts6.isVariableStatement(stmt)) {
1695
- for (const decl of stmt.declarationList.declarations) {
1696
- if (!ts6.isIdentifier(decl.name)) continue;
1697
- const helper = functionLikeInitializer(decl.initializer);
1698
- if (!helper) continue;
1699
- const directFact = directConnectFactFromFunctionLike(helper);
1700
- if (directFact) localDirectHelpers.set(decl.name.text, { ...directFact, exportedName: decl.name.text, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, decl) });
1701
- const rows2 = [];
1702
- for (const [returnedProperty, fact] of collectReturnedObjectBindings(helper))
1703
- rows2.push({ ...fact, exportedName: decl.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, decl) });
1704
- if (rows2.length > 0) localObjectHelpers.set(decl.name.text, rows2);
1477
+ function matchingPlaceholderClose(text, openBraceIndex) {
1478
+ let depth = 0;
1479
+ let quote;
1480
+ for (let index = openBraceIndex; index < text.length; index += 1) {
1481
+ const char = text[index];
1482
+ const prev = text[index - 1];
1483
+ if (quote) {
1484
+ if (prev === "\\") continue;
1485
+ if (quote === "template" && char === "$" && text[index + 1] === "{") {
1486
+ depth += 1;
1487
+ index += 1;
1488
+ continue;
1705
1489
  }
1490
+ if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
1491
+ continue;
1492
+ }
1493
+ if (char === "'") {
1494
+ quote = "single";
1495
+ continue;
1496
+ }
1497
+ if (char === '"') {
1498
+ quote = "double";
1499
+ continue;
1500
+ }
1501
+ if (char === "`") {
1502
+ quote = "template";
1503
+ continue;
1504
+ }
1505
+ if (char === "{") depth += 1;
1506
+ if (char === "}") {
1507
+ depth -= 1;
1508
+ if (depth === 0) return index;
1509
+ if (depth < 0) return void 0;
1706
1510
  }
1707
1511
  }
1708
- async function importedHelpers(localName) {
1709
- const imp = imports.find((i) => i.localName === localName && i.sourceFile);
1710
- if (!imp?.sourceFile) return [];
1711
- if (!helperCache.has(imp.sourceFile))
1712
- helperCache.set(
1713
- imp.sourceFile,
1714
- await helperBindings(repoPath, imp.sourceFile)
1715
- );
1716
- return (helperCache.get(imp.sourceFile) ?? []).filter((h) => h.exportedName === imp.exportedName).map((helper) => ({ imp, helper }));
1717
- }
1718
- async function importedHelper(localName) {
1719
- return (await importedHelpers(localName)).find((row) => !row.helper.returnedProperty);
1720
- }
1721
- function bindingForVariable(variableName) {
1722
- const sourceFile = normalizePath(filePath);
1723
- return [...out].reverse().find((row) => row.variableName === variableName && row.sourceFile === sourceFile);
1724
- }
1725
- function cloneAliasBinding(targetName, sourceName, aliasKind, node) {
1726
- const existing = bindingForVariable(sourceName);
1727
- if (!existing) return;
1728
- out.push({
1729
- ...existing,
1730
- variableName: targetName,
1731
- sourceLine: lineOf4(sourceFileAst, node),
1732
- helperChain: [
1733
- ...existing.helperChain ?? [],
1734
- {
1735
- callerVariable: targetName,
1736
- aliasOf: sourceName,
1737
- aliasKind,
1738
- scopeRule: "same-file-source-order",
1739
- ...aliasKind === "transaction" ? { transactionAliasSource: sourceName } : {}
1740
- }
1741
- ]
1742
- });
1512
+ return void 0;
1513
+ }
1514
+ function topLevelQueryIndex(text) {
1515
+ let quote;
1516
+ for (let index = 0; index < text.length; index += 1) {
1517
+ const char = text[index];
1518
+ const prev = text[index - 1];
1519
+ if (quote) {
1520
+ if (prev === "\\") continue;
1521
+ if (quote === "template" && char === "$" && text[index + 1] === "{") {
1522
+ const close = matchingPlaceholderClose(text, index + 1);
1523
+ if (close === void 0) return -1;
1524
+ index = close;
1525
+ continue;
1526
+ }
1527
+ if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
1528
+ continue;
1529
+ }
1530
+ if (char === "$" && text[index + 1] === "{") {
1531
+ const close = matchingPlaceholderClose(text, index + 1);
1532
+ if (close === void 0) return -1;
1533
+ index = close;
1534
+ continue;
1535
+ }
1536
+ if (char === "'") {
1537
+ quote = "single";
1538
+ continue;
1539
+ }
1540
+ if (char === '"') {
1541
+ quote = "double";
1542
+ continue;
1543
+ }
1544
+ if (char === "`") {
1545
+ quote = "template";
1546
+ continue;
1547
+ }
1548
+ if (char === "?") return index;
1743
1549
  }
1744
- function recordIdentityAlias(decl) {
1745
- if (!ts6.isIdentifier(decl.name) || !decl.initializer) return;
1746
- const unwrapped = unwrapIdentityExpression(decl.initializer);
1747
- if (!ts6.isIdentifier(unwrapped)) return;
1748
- cloneAliasBinding(decl.name.text, unwrapped.text, "identity", decl);
1550
+ return -1;
1551
+ }
1552
+
1553
+ // src/parsers/imported-wrapper-parser.ts
1554
+ import path9 from "path";
1555
+ import ts6 from "typescript";
1556
+ async function parseImportedWrapperCalls(repoPath, filePath, source, serviceBindings) {
1557
+ const imports = await importsFor(repoPath, filePath, source);
1558
+ const importedByLocal = new Map(imports.filter((item) => item.sourceFile).map((item) => [item.localName, item]));
1559
+ const calls = collectImportedCalls(source, importedByLocal);
1560
+ const out = [];
1561
+ const cache = /* @__PURE__ */ new Map();
1562
+ for (const call of calls) {
1563
+ if (!ts6.isIdentifier(call.expression)) continue;
1564
+ const imported = importedByLocal.get(call.expression.text);
1565
+ if (!imported?.sourceFile) continue;
1566
+ const spec = await loadWrapperSpec(repoPath, imported, cache, 0);
1567
+ const fact = spec ? wrapperCallFact(source, filePath, call, spec, serviceBindings) : void 0;
1568
+ if (fact) out.push(fact);
1749
1569
  }
1750
- async function recordBindingFromExpression(targetName, expr, node, aliasKind) {
1751
- const call = unwrapCall(expr);
1752
- if (!call) return;
1753
- const direct = connectFactFromCall(call);
1754
- if (direct)
1755
- out.push({
1756
- variableName: targetName,
1757
- ...direct,
1758
- sourceFile: normalizePath(filePath),
1759
- sourceLine: lineOf4(sourceFileAst, node),
1760
- helperChain: aliasKind === "assignment" ? [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind, scopeRule: "same-file-source-order" }] : void 0
1761
- });
1762
- else if (ts6.isIdentifier(call.expression)) {
1763
- const localDirect = localDirectHelpers.get(call.expression.text);
1764
- const resolved2 = localDirect ? { helper: localDirect, imp: void 0 } : await importedHelper(call.expression.text);
1765
- if (resolved2)
1766
- out.push({
1767
- variableName: targetName,
1768
- alias: resolved2.helper.alias,
1769
- aliasExpr: resolved2.helper.aliasExpr,
1770
- destinationExpr: resolved2.helper.destinationExpr,
1771
- servicePathExpr: resolved2.helper.servicePathExpr,
1772
- isDynamic: resolved2.helper.isDynamic,
1773
- placeholders: resolved2.helper.placeholders,
1774
- sourceFile: normalizePath(filePath),
1775
- sourceLine: lineOf4(sourceFileAst, node),
1776
- helperChain: [
1777
- ...resolved2.helper.helperChain ?? [],
1778
- {
1779
- callerVariable: targetName,
1780
- ...aliasKind === "assignment" ? { assignedFrom: call.expression.text, aliasKind, scopeRule: "same-file-source-order" } : {},
1781
- importedHelper: call.expression.text,
1782
- importSource: resolved2.imp?.sourceFile,
1783
- exportedSymbol: resolved2.imp?.exportedName ?? resolved2.helper.exportedName,
1784
- helperSourceFile: resolved2.helper.sourceFile,
1785
- helperSourceLine: resolved2.helper.sourceLine
1786
- }
1787
- ]
1788
- });
1570
+ return out;
1571
+ }
1572
+ function collectImportedCalls(source, imports) {
1573
+ const calls = [];
1574
+ const visit = (node) => {
1575
+ if (ts6.isCallExpression(node) && ts6.isIdentifier(node.expression) && imports.has(node.expression.text)) calls.push(node);
1576
+ ts6.forEachChild(node, visit);
1577
+ };
1578
+ visit(source);
1579
+ return calls;
1580
+ }
1581
+ async function loadWrapperSpec(repoPath, imported, cache, depth) {
1582
+ if (!imported.sourceFile || depth > 5) return void 0;
1583
+ const key = `${imported.sourceFile}#${imported.exportedName}`;
1584
+ const existing = cache.get(key);
1585
+ if (existing) return existing;
1586
+ const pending = inspectWrapper(repoPath, imported.sourceFile, imported.exportedName, cache, depth);
1587
+ cache.set(key, pending);
1588
+ return pending;
1589
+ }
1590
+ async function inspectWrapper(repoPath, sourceFile, exportedName, cache, depth) {
1591
+ const source = await readSource(path9.join(repoPath, sourceFile));
1592
+ if (!source) return void 0;
1593
+ const named = findFunction(source, exportedName);
1594
+ if (!named) return void 0;
1595
+ const direct = directSendSpec(source, sourceFile, named.name, named.fn);
1596
+ if (direct) return direct;
1597
+ return nestedSendSpec(repoPath, sourceFile, source, named.name, named.fn, cache, depth);
1598
+ }
1599
+ function directSendSpec(source, sourceFile, name, fn) {
1600
+ const params = parameterNames(fn);
1601
+ const sends = [];
1602
+ visitFunctionBody(fn, (node) => {
1603
+ if (ts6.isCallExpression(node) && ts6.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send") sends.push(node);
1604
+ });
1605
+ if (sends.length !== 1) return void 0;
1606
+ const send = sends[0];
1607
+ const receiver = send && ts6.isPropertyAccessExpression(send.expression) && ts6.isIdentifier(send.expression.expression) ? send.expression.expression.text : void 0;
1608
+ const object = send?.arguments[0];
1609
+ if (!receiver || !object || !ts6.isObjectLiteralExpression(object)) return void 0;
1610
+ const pathExpr = propertyExpression(object, "path");
1611
+ const methodExpr = propertyExpression(object, "method");
1612
+ const pathName = pathExpr && ts6.isIdentifier(pathExpr) ? pathExpr.text : void 0;
1613
+ const clientIndex = params.indexOf(receiver);
1614
+ const pathIndex = pathName ? params.indexOf(pathName) : -1;
1615
+ if (clientIndex < 0 || pathIndex < 0) return void 0;
1616
+ const methodName = methodExpr && ts6.isIdentifier(methodExpr) ? methodExpr.text : void 0;
1617
+ const methodIndex = methodName ? params.indexOf(methodName) : -1;
1618
+ return { clientIndex, pathIndex, methodIndex: methodIndex >= 0 ? methodIndex : void 0, methodLiteral: literal(methodExpr), sourceFile, sourceLine: lineOf3(source, fn), chain: [name] };
1619
+ }
1620
+ async function nestedSendSpec(repoPath, sourceFile, source, name, fn, cache, depth) {
1621
+ const imports = await importsFor(repoPath, sourceFile, source);
1622
+ const byLocal = new Map(imports.filter((item) => item.sourceFile).map((item) => [item.localName, item]));
1623
+ const calls = [];
1624
+ visitFunctionBody(fn, (node) => {
1625
+ if (ts6.isCallExpression(node) && ts6.isIdentifier(node.expression) && byLocal.has(node.expression.text)) calls.push(node);
1626
+ });
1627
+ if (calls.length !== 1) return void 0;
1628
+ const call = calls[0];
1629
+ const imported = call && ts6.isIdentifier(call.expression) ? byLocal.get(call.expression.text) : void 0;
1630
+ const nested = imported ? await loadWrapperSpec(repoPath, imported, cache, depth + 1) : void 0;
1631
+ if (!call || !nested) return void 0;
1632
+ const params = parameterNames(fn);
1633
+ const clientIndex = mappedParameterIndex(call.arguments[nested.clientIndex], params);
1634
+ const pathIndex = mappedParameterIndex(call.arguments[nested.pathIndex], params);
1635
+ if (clientIndex < 0 || pathIndex < 0) return void 0;
1636
+ const methodIndex = nested.methodIndex === void 0 ? void 0 : mappedParameterIndex(call.arguments[nested.methodIndex], params);
1637
+ return { clientIndex, pathIndex, methodIndex: methodIndex !== void 0 && methodIndex >= 0 ? methodIndex : void 0, methodLiteral: nested.methodLiteral, sourceFile, sourceLine: lineOf3(source, fn), chain: [name, ...nested.chain] };
1638
+ }
1639
+ function wrapperCallFact(source, filePath, call, spec, serviceBindings) {
1640
+ const client = call.arguments[spec.clientIndex];
1641
+ if (!client || !ts6.isIdentifier(client) || !serviceBindings.has(client.text)) return void 0;
1642
+ const pathValue = resolveCallerPath(call.arguments[spec.pathIndex], call);
1643
+ const methodValue = spec.methodIndex === void 0 ? spec.methodLiteral : literal(call.arguments[spec.methodIndex]);
1644
+ const method = (methodValue ?? "POST").toUpperCase();
1645
+ const rawPath = pathValue.rawExpression;
1646
+ const operationPathExpr = pathValue.value ? normalizeOperationPath(pathValue.value) : runtimePathExpression(rawPath);
1647
+ return {
1648
+ callType: "remote_action",
1649
+ serviceVariableName: client.text,
1650
+ method,
1651
+ operationPathExpr,
1652
+ payloadSummary: call.getText(source),
1653
+ sourceFile: normalizePath(filePath),
1654
+ sourceLine: lineOf3(source, call),
1655
+ confidence: operationPathExpr ? 0.85 : 0.5,
1656
+ unresolvedReason: pathValue.value ? void 0 : "dynamic_operation_path_identifier",
1657
+ evidence: {
1658
+ parser: "typescript_ast",
1659
+ classifier: pathValue.value ? "imported_wrapper_literal_path" : "imported_wrapper_dynamic_path",
1660
+ receiver: client.text,
1661
+ wrapperFunction: spec.chain[0],
1662
+ wrapperChain: spec.chain,
1663
+ callerSite: { sourceFile: normalizePath(filePath), sourceLine: lineOf3(source, call) },
1664
+ calleeSite: { sourceFile: spec.sourceFile, sourceLine: spec.sourceLine },
1665
+ rawPathExpression: rawPath,
1666
+ missingPathIdentifier: pathValue.value ? void 0 : rawPath,
1667
+ odataPathIntent: pathValue.value ? classifyODataPathIntent(operationPathExpr, method) : void 0
1668
+ }
1669
+ };
1670
+ }
1671
+ function resolveCallerPath(expr, use) {
1672
+ if (!expr) return { sourceKind: "missing", rawExpression: "" };
1673
+ if (ts6.isStringLiteralLike(expr) || ts6.isNoSubstitutionTemplateLiteral(expr)) return { value: expr.text, sourceKind: "literal", rawExpression: expr.getText() };
1674
+ if (ts6.isTemplateExpression(expr)) return { value: expr.getText().slice(1, -1), sourceKind: "template", rawExpression: expr.getText() };
1675
+ if (ts6.isIdentifier(expr)) {
1676
+ const initializer = constInitializer(expr.text, use);
1677
+ if (initializer) {
1678
+ const resolved2 = resolveCallerPath(initializer, initializer);
1679
+ return { ...resolved2, sourceKind: "const", rawExpression: expr.text };
1789
1680
  }
1790
1681
  }
1791
- async function recordVariable(decl) {
1792
- if (!ts6.isIdentifier(decl.name) || !decl.initializer) return;
1793
- await recordBindingFromExpression(decl.name.text, decl.initializer, decl, "declaration");
1794
- }
1795
- async function helpersForCall(call) {
1796
- if (!ts6.isIdentifier(call.expression)) return [];
1797
- const local = localObjectHelpers.get(call.expression.text) ?? [];
1798
- const imported = await importedHelpers(call.expression.text);
1799
- return [...local.map((helper) => ({ helper })), ...imported];
1682
+ return { sourceKind: "dynamic", rawExpression: expr.getText() };
1683
+ }
1684
+ function constInitializer(name, use) {
1685
+ let found;
1686
+ const source = use.getSourceFile();
1687
+ const visit = (node) => {
1688
+ if (node.getStart(source) >= use.getStart(source)) return;
1689
+ if (ts6.isVariableDeclaration(node) && ts6.isIdentifier(node.name) && node.name.text === name && node.initializer && (node.parent.flags & ts6.NodeFlags.Const) !== 0) found = node.initializer;
1690
+ ts6.forEachChild(node, visit);
1691
+ };
1692
+ visit(source);
1693
+ return found;
1694
+ }
1695
+ function findFunction(source, exportedName) {
1696
+ const localName = exportedLocalName(source, exportedName);
1697
+ for (const statement of source.statements) {
1698
+ if (ts6.isFunctionDeclaration(statement) && statement.name?.text === localName) return { name: exportedName, fn: statement };
1699
+ if (!ts6.isVariableStatement(statement)) continue;
1700
+ for (const declaration of statement.declarationList.declarations) {
1701
+ if (ts6.isIdentifier(declaration.name) && declaration.name.text === localName && declaration.initializer && (ts6.isArrowFunction(declaration.initializer) || ts6.isFunctionExpression(declaration.initializer))) return { name: exportedName, fn: declaration.initializer };
1702
+ }
1800
1703
  }
1801
- async function rememberObjectHelperVariable(decl) {
1802
- if (!ts6.isIdentifier(decl.name) || !decl.initializer) return;
1803
- const call = unwrapCall(decl.initializer);
1804
- if (!call) return;
1805
- const helpers = (await helpersForCall(call)).filter((row) => row.helper.returnedProperty);
1806
- if (helpers.length > 0) objectHelperVariables.set(decl.name.text, helpers);
1704
+ return void 0;
1705
+ }
1706
+ function exportedLocalName(source, exportedName) {
1707
+ for (const statement of source.statements) {
1708
+ if (!ts6.isExportDeclaration(statement) || !statement.exportClause || !ts6.isNamedExports(statement.exportClause)) continue;
1709
+ const match = statement.exportClause.elements.find((item) => item.name.text === exportedName);
1710
+ if (match) return match.propertyName?.text ?? match.name.text;
1807
1711
  }
1808
- function recordObjectPropertyBinding(targetName, expr, node) {
1809
- const unwrapped = unwrapIdentityExpression(expr);
1810
- if (!ts6.isPropertyAccessExpression(unwrapped) || !ts6.isIdentifier(unwrapped.expression)) return false;
1811
- const helpers = objectHelperVariables.get(unwrapped.expression.text) ?? [];
1812
- const matches = helpers.filter((row) => row.helper.returnedProperty === unwrapped.name.text);
1813
- if (matches.length !== 1) return false;
1814
- const resolved2 = matches[0];
1815
- out.push({
1816
- variableName: targetName,
1817
- alias: resolved2.helper.alias,
1818
- aliasExpr: resolved2.helper.aliasExpr,
1819
- destinationExpr: resolved2.helper.destinationExpr,
1820
- servicePathExpr: resolved2.helper.servicePathExpr,
1821
- isDynamic: resolved2.helper.isDynamic,
1822
- placeholders: resolved2.helper.placeholders,
1823
- sourceFile: normalizePath(filePath),
1824
- sourceLine: lineOf4(sourceFileAst, node),
1825
- helperChain: [
1826
- ...resolved2.helper.helperChain ?? [],
1827
- {
1828
- callerVariable: targetName,
1829
- sourceVariable: unwrapped.expression.text,
1830
- returnedProperty: unwrapped.name.text,
1831
- assignedFromProperty: unwrapped.getText(sourceFileAst),
1832
- importSource: resolved2.imp?.sourceFile,
1833
- exportedSymbol: resolved2.imp?.exportedName,
1834
- helperSourceFile: resolved2.helper.sourceFile,
1835
- helperSourceLine: resolved2.helper.sourceLine
1836
- }
1837
- ]
1838
- });
1839
- return true;
1712
+ return exportedName;
1713
+ }
1714
+ function visitFunctionBody(fn, visitor) {
1715
+ const visit = (node) => {
1716
+ if (node !== fn && ts6.isFunctionLike(node)) return;
1717
+ visitor(node);
1718
+ ts6.forEachChild(node, visit);
1719
+ };
1720
+ if (fn.body) visit(fn.body);
1721
+ }
1722
+ function parameterNames(fn) {
1723
+ return fn.parameters.map((parameter) => ts6.isIdentifier(parameter.name) ? parameter.name.text : "");
1724
+ }
1725
+ function mappedParameterIndex(expr, parameters) {
1726
+ return expr && ts6.isIdentifier(expr) ? parameters.indexOf(expr.text) : -1;
1727
+ }
1728
+ function propertyExpression(object, key) {
1729
+ for (const property of object.properties) {
1730
+ if (ts6.isPropertyAssignment(property) && propertyName(property.name) === key) return property.initializer;
1731
+ if (ts6.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
1840
1732
  }
1841
- async function recordDestructuredHelper(decl) {
1842
- if (!ts6.isObjectBindingPattern(decl.name) || !decl.initializer) return;
1843
- const call = unwrapCall(decl.initializer);
1844
- if (!call) return;
1845
- const helpers = await helpersForCall(call);
1846
- if (helpers.length === 0) return;
1847
- for (const el of decl.name.elements) {
1848
- if (!ts6.isIdentifier(el.name)) continue;
1849
- const propertyName = el.propertyName && ts6.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1850
- const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
1851
- if (matches.length !== 1) continue;
1852
- const resolved2 = matches[0];
1853
- out.push({
1854
- variableName: el.name.text,
1855
- alias: resolved2.helper.alias,
1856
- aliasExpr: resolved2.helper.aliasExpr,
1857
- destinationExpr: resolved2.helper.destinationExpr,
1858
- servicePathExpr: resolved2.helper.servicePathExpr,
1859
- isDynamic: resolved2.helper.isDynamic,
1860
- placeholders: resolved2.helper.placeholders,
1861
- sourceFile: normalizePath(filePath),
1862
- sourceLine: lineOf4(sourceFileAst, decl),
1863
- helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
1864
- });
1733
+ return void 0;
1734
+ }
1735
+ function propertyName(name) {
1736
+ return ts6.isIdentifier(name) || ts6.isStringLiteralLike(name) ? name.text : void 0;
1737
+ }
1738
+ function literal(expr) {
1739
+ return expr && (ts6.isStringLiteralLike(expr) || ts6.isNoSubstitutionTemplateLiteral(expr)) ? expr.text : void 0;
1740
+ }
1741
+ function normalizeOperationPath(value) {
1742
+ return value.startsWith("/") ? value : `/${value}`;
1743
+ }
1744
+ function runtimePathExpression(value) {
1745
+ return value ? `\${${value}}` : void 0;
1746
+ }
1747
+
1748
+ // src/parsers/outbound-call-parser.ts
1749
+ function lineOf4(text, idx) {
1750
+ return text.slice(0, idx).split("\n").length;
1751
+ }
1752
+ function entityFromExpression(expr) {
1753
+ if (!expr) return void 0;
1754
+ if (ts7.isIdentifier(expr) || ts7.isStringLiteral(expr) || ts7.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
1755
+ if (ts7.isPropertyAccessExpression(expr) && expr.expression.kind === ts7.SyntaxKind.ThisKeyword) return expr.name.text;
1756
+ if (ts7.isElementAccessExpression(expr) && expr.argumentExpression && (ts7.isStringLiteral(expr.argumentExpression) || ts7.isNoSubstitutionTemplateLiteral(expr.argumentExpression))) return expr.argumentExpression.text;
1757
+ return void 0;
1758
+ }
1759
+ function expressionName(expr) {
1760
+ if (ts7.isIdentifier(expr)) return expr.text;
1761
+ if (ts7.isPropertyAccessExpression(expr)) return `${expressionName(expr.expression)}.${expr.name.text}`;
1762
+ return expr.getText();
1763
+ }
1764
+ function variableInitializers(source) {
1765
+ const initializers = /* @__PURE__ */ new Map();
1766
+ for (const statement of source.statements) {
1767
+ if (!ts7.isVariableStatement(statement) || (statement.declarationList.flags & ts7.NodeFlags.Const) === 0) continue;
1768
+ for (const declaration of statement.declarationList.declarations) {
1769
+ if (ts7.isIdentifier(declaration.name) && declaration.initializer) initializers.set(declaration.name.text, declaration.initializer);
1865
1770
  }
1866
1771
  }
1867
- async function recordDestructuredAssignment(pattern, expr, node) {
1868
- const call = unwrapCall(expr);
1869
- if (!call) return;
1870
- const helpers = await helpersForCall(call);
1871
- if (helpers.length === 0) return;
1872
- for (const prop of pattern.properties) {
1873
- let propertyName;
1874
- let targetName;
1875
- if (ts6.isShorthandPropertyAssignment(prop)) {
1876
- propertyName = prop.name.text;
1877
- targetName = prop.name.text;
1878
- } else if (ts6.isPropertyAssignment(prop)) {
1879
- propertyName = ts6.isIdentifier(prop.name) || ts6.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
1880
- targetName = ts6.isIdentifier(prop.initializer) ? prop.initializer.text : void 0;
1881
- }
1882
- if (!propertyName || !targetName) continue;
1883
- const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
1884
- if (matches.length !== 1) continue;
1885
- const resolved2 = matches[0];
1886
- out.push({
1887
- variableName: targetName,
1888
- alias: resolved2.helper.alias,
1889
- aliasExpr: resolved2.helper.aliasExpr,
1890
- destinationExpr: resolved2.helper.destinationExpr,
1891
- servicePathExpr: resolved2.helper.servicePathExpr,
1892
- isDynamic: resolved2.helper.isDynamic,
1893
- placeholders: resolved2.helper.placeholders,
1894
- sourceFile: normalizePath(filePath),
1895
- sourceLine: lineOf4(sourceFileAst, node),
1896
- helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
1897
- });
1898
- }
1772
+ return initializers;
1773
+ }
1774
+ function queryEntityFromAst(expr, initializers = /* @__PURE__ */ new Map()) {
1775
+ if (ts7.isParenthesizedExpression(expr) || ts7.isAwaitExpression(expr)) return queryEntityFromAst(expr.expression, initializers);
1776
+ if (ts7.isIdentifier(expr) && initializers.has(expr.text)) return queryEntityFromAst(initializers.get(expr.text), initializers);
1777
+ if (ts7.isCallExpression(expr)) {
1778
+ const name = expressionName(expr.expression);
1779
+ if (name === "cds.run") return queryEntityFromAst(expr.arguments[0], initializers);
1780
+ if (["SELECT.one.from", "SELECT.from", "SELECT.one", "INSERT.into", "UPSERT.into", "DELETE.from", "UPDATE.entity"].includes(name)) return entityFromExpression(expr.arguments[0]);
1781
+ if (name === "UPDATE") return entityFromExpression(expr.arguments[0]);
1782
+ const receiver = ts7.isPropertyAccessExpression(expr.expression) ? expr.expression.expression : void 0;
1783
+ if (receiver) return queryEntityFromAst(receiver, initializers);
1784
+ }
1785
+ return void 0;
1786
+ }
1787
+ function extractQueryEntity(expr) {
1788
+ const source = ts7.createSourceFile("query.ts", `const __query = (${expr});`, ts7.ScriptTarget.Latest, true, ts7.ScriptKind.TS);
1789
+ const initializers = variableInitializers(source);
1790
+ let found;
1791
+ const visit = (node) => {
1792
+ if (found) return;
1793
+ if (ts7.isParenthesizedExpression(node)) found = queryEntityFromAst(node.expression, initializers);
1794
+ ts7.forEachChild(node, visit);
1795
+ };
1796
+ visit(source);
1797
+ return found;
1798
+ }
1799
+ function queryWarning(expr) {
1800
+ if (/^\s*[`'"]/.test(expr)) return "raw_sql_or_cql_expression";
1801
+ if (/^\s*\w+\s*$/.test(expr)) return "query_variable_without_static_initializer";
1802
+ return "dynamic_entity_expression";
1803
+ }
1804
+ function parserEvidence(source, node, extra) {
1805
+ return { parser: "typescript_ast", startOffset: node.getStart(source), endOffset: node.getEnd(), ...extra };
1806
+ }
1807
+ function isStringLike(expr) {
1808
+ return Boolean(expr && (ts7.isStringLiteral(expr) || ts7.isNoSubstitutionTemplateLiteral(expr)));
1809
+ }
1810
+ function literalText(expr) {
1811
+ if (isStringLike(expr)) return expr.text;
1812
+ return void 0;
1813
+ }
1814
+ function objectPropertyText(object, key) {
1815
+ const prop = object.properties.find(
1816
+ (property) => ts7.isPropertyAssignment(property) && nameOfProperty(property.name) === key || ts7.isShorthandPropertyAssignment(property) && property.name.text === key
1817
+ );
1818
+ if (!prop) return void 0;
1819
+ return ts7.isShorthandPropertyAssignment(prop) ? prop.name.text : prop.initializer.getText();
1820
+ }
1821
+ function objectPropertyIsShorthand(object, key) {
1822
+ return object.properties.some((property) => ts7.isShorthandPropertyAssignment(property) && property.name.text === key);
1823
+ }
1824
+ function nameOfProperty(name) {
1825
+ if (ts7.isIdentifier(name) || ts7.isStringLiteral(name) || ts7.isNumericLiteral(name)) return name.text;
1826
+ return void 0;
1827
+ }
1828
+ var maxAliasDepth = 5;
1829
+ function safeRaw(expr) {
1830
+ if (ts7.isStringLiteral(expr) || ts7.isNoSubstitutionTemplateLiteral(expr) || ts7.isIdentifier(expr) || ts7.isTemplateExpression(expr)) return expr.getText(expr.getSourceFile());
1831
+ return void 0;
1832
+ }
1833
+ function placeholders2(expr) {
1834
+ return expr.templateSpans.map((span) => span.expression.getText(expr.getSourceFile()));
1835
+ }
1836
+ function isFunctionLikeScope(node) {
1837
+ return ts7.isFunctionLike(node) || ts7.isSourceFile(node);
1838
+ }
1839
+ function nodeContains(parent, child) {
1840
+ const source = child.getSourceFile();
1841
+ return child.getStart(source) >= parent.getStart(source) && child.getEnd() <= parent.getEnd();
1842
+ }
1843
+ function declarationScope(node) {
1844
+ if (ts7.isParameter(node)) return node.parent;
1845
+ if (ts7.isCatchClause(node.parent) && node.parent.variableDeclaration === node) return node.parent;
1846
+ const list = node.parent;
1847
+ const blockScoped = (list.flags & (ts7.NodeFlags.Const | ts7.NodeFlags.Let)) !== 0;
1848
+ let current = list.parent;
1849
+ if (!blockScoped) {
1850
+ while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
1851
+ return current;
1899
1852
  }
1900
- function recordDestructuredClassHelper(decl) {
1901
- if (!ts6.isObjectBindingPattern(decl.name) || !decl.initializer) return;
1902
- const call = unwrapCall(decl.initializer);
1903
- if (!call || !ts6.isPropertyAccessExpression(call.expression)) return;
1904
- const target = call.expression;
1905
- if (target.expression.kind !== ts6.SyntaxKind.ThisKeyword) return;
1906
- for (const el of decl.name.elements) {
1907
- if (!ts6.isIdentifier(el.name)) continue;
1908
- const propertyName = el.propertyName && ts6.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1909
- const helper = classHelpers.find(
1910
- (h) => h.helperName === target.name.text && h.propertyName === propertyName
1911
- );
1912
- if (!helper) continue;
1913
- out.push({
1914
- variableName: el.name.text,
1915
- ...helper.fact,
1916
- sourceFile: normalizePath(filePath),
1917
- sourceLine: lineOf4(sourceFileAst, decl),
1918
- helperChain: [
1919
- {
1920
- callerVariable: el.name.text,
1921
- className: helper.className,
1922
- classHelper: helper.helperName,
1923
- returnedProperty: helper.propertyName,
1924
- helperVariable: helper.variableName,
1925
- helperSourceFile: normalizePath(filePath),
1926
- helperSourceLine: helper.sourceLine
1927
- }
1928
- ]
1929
- });
1930
- }
1853
+ while (current.parent && !ts7.isBlock(current) && !ts7.isSourceFile(current) && !ts7.isModuleBlock(current) && !ts7.isCaseBlock(current) && !isLoopInitializerScope(node, current) && !isFunctionLikeScope(current)) current = current.parent;
1854
+ return current;
1855
+ }
1856
+ function isLoopInitializerScope(declaration, scope) {
1857
+ const list = declaration.parent;
1858
+ return ts7.isForStatement(scope) && scope.initializer === list || (ts7.isForInStatement(scope) || ts7.isForOfStatement(scope)) && scope.initializer === list;
1859
+ }
1860
+ function catchBindingScope(declaration) {
1861
+ if (ts7.isParameter(declaration)) return void 0;
1862
+ return ts7.isCatchClause(declaration.parent) && declaration.parent.variableDeclaration === declaration ? declaration.parent : void 0;
1863
+ }
1864
+ function isAccessibleDeclaration(declaration, use) {
1865
+ const source = use.getSourceFile();
1866
+ if (declaration.name.getStart(source) >= use.getStart(source)) return false;
1867
+ const catchScope = catchBindingScope(declaration);
1868
+ if (catchScope) return nodeContains(catchScope.block, use);
1869
+ const scope = declarationScope(declaration);
1870
+ if (ts7.isForStatement(scope) || ts7.isForInStatement(scope) || ts7.isForOfStatement(scope)) return nodeContains(scope.statement, use);
1871
+ return ts7.isSourceFile(scope) || nodeContains(scope, use);
1872
+ }
1873
+ function resolveBinding(identifier, use) {
1874
+ const source = use.getSourceFile();
1875
+ let best;
1876
+ const visit = (node) => {
1877
+ if (ts7.isVariableDeclaration(node) && ts7.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
1878
+ if (ts7.isParameter(node) && ts7.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
1879
+ ts7.forEachChild(node, visit);
1880
+ };
1881
+ visit(source);
1882
+ if (!best) return { immutable: false, evidence: ["binding_not_found"] };
1883
+ const immutable = ts7.isVariableDeclaration(best) && (best.parent.flags & ts7.NodeFlags.Const) !== 0;
1884
+ return { declaration: best, initializer: ts7.isVariableDeclaration(best) ? best.initializer : void 0, immutable, evidence: [immutable ? "lexical_const_binding_before_use" : "lexical_mutable_or_parameter_binding"] };
1885
+ }
1886
+ function resolveExpression(expr, use, policy, depth = 0, seen = /* @__PURE__ */ new Set()) {
1887
+ if (!expr) return { status: "unknown", sourceKind: "dynamic_expression", placeholderKeys: [], evidence: ["expression_missing"] };
1888
+ if (ts7.isStringLiteral(expr)) return { status: "static", sourceKind: "string_literal", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["string_literal"] };
1889
+ if (ts7.isNoSubstitutionTemplateLiteral(expr)) return { status: "static", sourceKind: "no_substitution_template", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["no_substitution_template"] };
1890
+ if (ts7.isTemplateExpression(expr)) {
1891
+ const keys = placeholders2(expr);
1892
+ if (policy === "operation_path") return { status: "dynamic", sourceKind: "template_with_substitutions", value: stripQuotes(expr.getText(expr.getSourceFile())), rawExpression: safeRaw(expr), placeholderKeys: keys, evidence: ["operation_path_template_placeholders_retained"] };
1893
+ return { status: "dynamic", sourceKind: "template_with_substitutions", placeholderKeys: keys, evidence: ["template_substitutions_not_static_external_target"] };
1931
1894
  }
1932
- function arrayElementsFromExpression(expr) {
1933
- const unwrapped = unwrapIdentityExpression(expr);
1934
- if (ts6.isArrayLiteralExpression(unwrapped)) return { elements: unwrapped.elements, promiseAll: false };
1935
- const call = unwrapCall(expr);
1936
- if (!call) return void 0;
1937
- if (!ts6.isPropertyAccessExpression(call.expression) || call.expression.name.text !== "all" || call.expression.expression.getText(sourceFileAst) !== "Promise") return void 0;
1938
- const first = call.arguments[0];
1939
- if (!first) return void 0;
1940
- const container = unwrapIdentityExpression(first);
1941
- if (!ts6.isArrayLiteralExpression(container)) return void 0;
1942
- return { elements: container.elements, promiseAll: true };
1895
+ if (ts7.isIdentifier(expr)) {
1896
+ if (depth >= maxAliasDepth) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_depth_exceeded"], constName: expr.text };
1897
+ const binding = resolveBinding(expr, use);
1898
+ if (!binding.declaration || !binding.initializer || !binding.immutable) return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: binding.evidence, constName: expr.text };
1899
+ if (seen.has(binding.declaration)) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_cycle_detected"], constName: expr.text };
1900
+ seen.add(binding.declaration);
1901
+ const resolved2 = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
1902
+ return { ...resolved2, sourceKind: "const_alias", rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved2.evidence] };
1943
1903
  }
1944
- async function recordArrayElementBinding(targetName, expr, node, arrayIndex, promiseAll) {
1945
- const before = out.length;
1946
- await recordBindingFromExpression(targetName, expr, node, "declaration");
1947
- if (out.length > before) {
1948
- const row = out[out.length - 1];
1949
- row.helperChain = [
1950
- ...row.helperChain ?? [],
1951
- { callerVariable: targetName, targetVariable: targetName, arrayIndex, promiseAll, arrayContainer: promiseAll ? "Promise.all" : "array_literal" }
1952
- ];
1953
- return;
1954
- }
1955
- const unwrapped = unwrapIdentityExpression(expr);
1956
- if (ts6.isIdentifier(unwrapped)) {
1957
- const existing = bindingForVariable(unwrapped.text);
1958
- if (!existing) return;
1959
- out.push({
1960
- ...existing,
1961
- variableName: targetName,
1962
- sourceLine: lineOf4(sourceFileAst, node),
1963
- helperChain: [
1964
- ...existing.helperChain ?? [],
1965
- { callerVariable: targetName, targetVariable: targetName, sourceVariable: unwrapped.text, aliasKind: "array-destructuring", arrayIndex, promiseAll, arrayContainer: promiseAll ? "Promise.all" : "array_literal" }
1966
- ]
1967
- });
1904
+ return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${ts7.SyntaxKind[expr.kind] ?? "expression"}`] };
1905
+ }
1906
+ function staticExpressionText(expr, initializers) {
1907
+ if (!expr) return void 0;
1908
+ if (isStringLike(expr)) return expr.text;
1909
+ if (ts7.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
1910
+ return void 0;
1911
+ }
1912
+ function staticPathExpression(expr, use) {
1913
+ const resolution = resolveExpression(expr, use, "operation_path");
1914
+ const sourceKind = resolution.sourceKind === "const_alias" ? "const" : resolution.sourceKind === "template_with_substitutions" || resolution.sourceKind === "no_substitution_template" ? "template" : resolution.status === "static" ? "literal" : "dynamic";
1915
+ return { text: resolution.value, sourceKind, rawExpression: resolution.rawExpression, constName: resolution.constName, resolution };
1916
+ }
1917
+ function operationPathFromStatic(text) {
1918
+ return text ? `/${stripQuotes(text).replace(/^\//, "")}` : void 0;
1919
+ }
1920
+ function staticPathCandidates(identifier, call, method) {
1921
+ const binding = resolveBinding(identifier, call);
1922
+ const declaration = binding.declaration;
1923
+ if (!declaration) return void 0;
1924
+ const source = call.getSourceFile();
1925
+ const paths = [];
1926
+ let hasDynamicAssignments = false;
1927
+ const addExpr = (expr, origin) => {
1928
+ const resolved2 = staticPathExpression(expr, origin);
1929
+ if (resolved2.text) paths.push(operationPathFromStatic(resolved2.text) ?? resolved2.text);
1930
+ else hasDynamicAssignments = true;
1931
+ };
1932
+ if (ts7.isVariableDeclaration(declaration)) addExpr(declaration.initializer, declaration);
1933
+ const visit = (node) => {
1934
+ if (node !== source && isFunctionLikeScope(node) && !nodeContains(node, call)) return;
1935
+ if (node.getStart(source) >= call.getStart(source)) return;
1936
+ if (ts7.isBinaryExpression(node) && node.operatorToken.kind === ts7.SyntaxKind.EqualsToken && ts7.isIdentifier(node.left)) {
1937
+ const target = resolveBinding(node.left, node);
1938
+ if (target.declaration === declaration) addExpr(node.right, node);
1968
1939
  }
1940
+ ts7.forEachChild(node, visit);
1941
+ };
1942
+ visit(source);
1943
+ const candidatePaths = [...new Set(paths)];
1944
+ if (candidatePaths.length === 0) return void 0;
1945
+ const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, method).topLevelOperationName).filter((value) => Boolean(value)))];
1946
+ return { candidatePaths, normalizedCandidateOperations, candidateSourceKind: "static candidates observed before call in lexical scope", candidateIdentifier: identifier.text, hasDynamicAssignments, conservativeReason: hasDynamicAssignments ? "dynamic_assignment_observed" : normalizedCandidateOperations.length > 1 ? "candidate_tie" : void 0 };
1947
+ }
1948
+ function destinationExpressionShape(expr) {
1949
+ if (!expr) return void 0;
1950
+ if (ts7.isIdentifier(expr)) return "identifier";
1951
+ if (ts7.isPropertyAccessExpression(expr) || ts7.isElementAccessExpression(expr)) return "property_read";
1952
+ if (ts7.isCallExpression(expr)) return "function_call";
1953
+ if (ts7.isConditionalExpression(expr)) return "conditional";
1954
+ if (ts7.isBinaryExpression(expr)) return "binary_expression";
1955
+ if (ts7.isTemplateExpression(expr)) return "template_expression";
1956
+ return ts7.SyntaxKind[expr.kind] ?? "expression";
1957
+ }
1958
+ function staticConditionalCandidates(expr, initializers) {
1959
+ const resolved2 = expr && ts7.isIdentifier(expr) && initializers.has(expr.text) ? initializers.get(expr.text) : expr;
1960
+ if (!resolved2 || !ts7.isConditionalExpression(resolved2)) return void 0;
1961
+ const left = staticExpressionText(resolved2.whenTrue, initializers);
1962
+ const right = staticExpressionText(resolved2.whenFalse, initializers);
1963
+ if (!left || !right) return void 0;
1964
+ return [.../* @__PURE__ */ new Set([left, right])];
1965
+ }
1966
+ function propertyInitializer(object, key) {
1967
+ for (const property of object.properties) {
1968
+ if (ts7.isPropertyAssignment(property) && nameOfProperty(property.name) === key) return property.initializer;
1969
+ if (ts7.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
1969
1970
  }
1970
- async function recordArrayDestructuredVariable(decl) {
1971
- if (!ts6.isArrayBindingPattern(decl.name) || !decl.initializer) return;
1972
- const container = arrayElementsFromExpression(decl.initializer);
1973
- if (!container) return;
1974
- for (let index = 0; index < decl.name.elements.length; index += 1) {
1975
- const el = decl.name.elements[index];
1976
- if (!el || ts6.isOmittedExpression(el) || ts6.isBindingElement(el) && el.dotDotDotToken) continue;
1977
- if (!ts6.isBindingElement(el) || !ts6.isIdentifier(el.name)) continue;
1978
- const source = container.elements[index];
1979
- if (!source || ts6.isOmittedExpression(source)) continue;
1980
- await recordArrayElementBinding(el.name.text, source, decl, index, container.promiseAll);
1971
+ return void 0;
1972
+ }
1973
+ function httpMethodFromObject(object, use) {
1974
+ const text = resolveExpression(propertyInitializer(object, "method"), use, "literal").value;
1975
+ return text ? stripQuotes(text).toUpperCase() : void 0;
1976
+ }
1977
+ var supportedHttpMethods = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
1978
+ function safeOperationName(value) {
1979
+ if (!value || !/^[A-Za-z_$][\w$]*(?:[./][A-Za-z_$][\w$]*)*$/.test(value)) return void 0;
1980
+ return operationPathFromStatic(value);
1981
+ }
1982
+ function hasTemplatePlaceholder(value) {
1983
+ return /\$\{|%7B|%7D/i.test(value);
1984
+ }
1985
+ function urlTargetFromExpression(expr, use) {
1986
+ const resolved2 = resolveExpression(expr, use, "external");
1987
+ if (resolved2.status === "static" && resolved2.value && !hasTemplatePlaceholder(resolved2.value)) return { kind: "static_url", expression: resolved2.value, dynamic: false, sourceKind: resolved2.sourceKind };
1988
+ if (expr) return { kind: "url_expression", dynamic: true, expression: `${resolved2.sourceKind}:${resolved2.placeholderKeys.join("|")}`, expressionShape: resolved2.sourceKind, placeholderKeys: resolved2.placeholderKeys };
1989
+ return { kind: "unknown", dynamic: false };
1990
+ }
1991
+ function destinationTargetFromExpression(expr, use) {
1992
+ const resolved2 = resolveExpression(expr, use, "external");
1993
+ const text = resolved2.value;
1994
+ if (resolved2.status === "static" && text && !hasTemplatePlaceholder(text)) return { kind: "destination", expression: text, dynamic: false, sourceKind: resolved2.sourceKind };
1995
+ const candidates = staticConditionalCandidates(expr, /* @__PURE__ */ new Map());
1996
+ if (candidates) return { kind: "destination", dynamic: true, expressionShape: "conditional", candidateLiterals: candidates };
1997
+ const shape = destinationExpressionShape(expr);
1998
+ if (shape) return { kind: "destination", dynamic: true, expressionShape: shape };
1999
+ return void 0;
2000
+ }
2001
+ function externalHttpEvidence(node, source) {
2002
+ const expr = node.expression;
2003
+ const exprText = expr.getText(source);
2004
+ if (exprText === "useOrFetchDestination") {
2005
+ const objectArg = node.arguments[0];
2006
+ if (objectArg && ts7.isObjectLiteralExpression(objectArg)) {
2007
+ const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"), node);
2008
+ return { externalTarget: destination ?? { kind: "unknown", dynamic: false }, classifier: "sap_destination_lookup", sourceCallShape: "useOrFetchDestination" };
1981
2009
  }
1982
2010
  }
1983
- async function recordArrayDestructuredAssignment(pattern, expr, node) {
1984
- const container = arrayElementsFromExpression(expr);
1985
- if (!container) return;
1986
- for (let index = 0; index < pattern.elements.length; index += 1) {
1987
- const el = pattern.elements[index];
1988
- if (!el || ts6.isOmittedExpression(el) || ts6.isSpreadElement(el) || !ts6.isIdentifier(el)) continue;
1989
- const source = container.elements[index];
1990
- if (!source || ts6.isOmittedExpression(source)) continue;
1991
- await recordArrayElementBinding(el.text, source, node, index, container.promiseAll);
2011
+ if (exprText === "executeHttpRequest") {
2012
+ const destination = destinationTargetFromExpression(node.arguments[0], node);
2013
+ const config = node.arguments[1];
2014
+ const method = config && ts7.isObjectLiteralExpression(config) ? httpMethodFromObject(config, node) : void 0;
2015
+ const url = config && ts7.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, "url"), node) : { kind: "unknown", dynamic: false };
2016
+ return { method, externalTarget: destination ? { ...url, destination } : url, classifier: "sap_execute_http_request", sourceCallShape: "executeHttpRequest" };
2017
+ }
2018
+ if (exprText === "axios") {
2019
+ const config = node.arguments[0];
2020
+ if (config && ts7.isObjectLiteralExpression(config)) {
2021
+ const method = httpMethodFromObject(config, node);
2022
+ return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, "url"), node), classifier: "axios_config_call", sourceCallShape: "axios(config)" };
1992
2023
  }
2024
+ return { externalTarget: { kind: "unknown", dynamic: false }, classifier: "axios_unknown_call", sourceCallShape: "axios(...)" };
1993
2025
  }
1994
- const events = [];
1995
- function collectEvents(node) {
1996
- if (ts6.isVariableDeclaration(node)) events.push({ pos: node.getStart(sourceFileAst), node });
1997
- if (ts6.isBinaryExpression(node) && node.operatorToken.kind === ts6.SyntaxKind.EqualsToken)
1998
- events.push({ pos: node.getStart(sourceFileAst), node });
1999
- ts6.forEachChild(node, collectEvents);
2026
+ if (exprText === "fetch") {
2027
+ const init = node.arguments[1];
2028
+ const method = init && ts7.isObjectLiteralExpression(init) ? httpMethodFromObject(init, node) : void 0;
2029
+ return { method, externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "fetch_call", sourceCallShape: "fetch" };
2000
2030
  }
2001
- collectEvents(sourceFileAst);
2002
- events.sort((a, b) => a.pos - b.pos);
2003
- for (const event of events) {
2004
- if (ts6.isVariableDeclaration(event.node)) {
2005
- const decl = event.node;
2006
- await recordDestructuredHelper(decl);
2007
- await recordArrayDestructuredVariable(decl);
2008
- recordDestructuredClassHelper(decl);
2009
- await rememberObjectHelperVariable(decl);
2010
- if (ts6.isIdentifier(decl.name) && decl.initializer) recordObjectPropertyBinding(decl.name.text, decl.initializer, decl);
2011
- await recordVariable(decl);
2012
- recordIdentityAlias(decl);
2013
- if (ts6.isIdentifier(decl.name) && decl.initializer) {
2014
- const sourceName = transactionReceiverName(decl.initializer);
2015
- if (sourceName) cloneAliasBinding(decl.name.text, sourceName, "transaction", decl);
2031
+ if (ts7.isPropertyAccessExpression(expr) && ["get", "post", "put", "patch", "delete", "head"].includes(expr.name.text) && expr.expression.getText(source) === "axios") {
2032
+ return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "axios_member_call", sourceCallShape: `axios.${expr.name.text}` };
2033
+ }
2034
+ return void 0;
2035
+ }
2036
+ function collectServiceVariables(source) {
2037
+ const vars = /* @__PURE__ */ new Set(["cds", "messaging", "messageClient", "eventClient"]);
2038
+ const visit = (node) => {
2039
+ if (ts7.isVariableDeclaration(node) && ts7.isIdentifier(node.name) && node.initializer) {
2040
+ const text = node.initializer.getText(source);
2041
+ if (/cds\.connect\.(to|messaging)\s*\(/.test(text)) vars.add(node.name.text);
2042
+ }
2043
+ ts7.forEachChild(node, visit);
2044
+ };
2045
+ visit(source);
2046
+ return vars;
2047
+ }
2048
+ function receiverName(expr) {
2049
+ if (ts7.isIdentifier(expr)) return expr.text;
2050
+ if (ts7.isPropertyAccessExpression(expr)) return expr.getText(sourceOf(expr));
2051
+ return void 0;
2052
+ }
2053
+ function sourceOf(node) {
2054
+ return node.getSourceFile();
2055
+ }
2056
+ function rootReceiverName(expr) {
2057
+ if (ts7.isIdentifier(expr)) return expr.text;
2058
+ if (ts7.isPropertyAccessExpression(expr)) return rootReceiverName(expr.expression);
2059
+ if (ts7.isCallExpression(expr)) return rootReceiverName(expr.expression);
2060
+ return void 0;
2061
+ }
2062
+ function isSupportedEventReceiver(receiver, rootReceiver, serviceVariables) {
2063
+ const candidate = rootReceiver ?? receiver;
2064
+ if (!candidate) return false;
2065
+ if (candidate === "cds") return true;
2066
+ if (serviceVariables.has(candidate)) return true;
2067
+ if (receiver && serviceVariables.has(receiver)) return true;
2068
+ if (/^(srv|service|serviceClient|messaging|messageClient|eventClient)$/.test(candidate)) return true;
2069
+ return false;
2070
+ }
2071
+ function collectWrapperSpecs(source) {
2072
+ const specs = /* @__PURE__ */ new Map();
2073
+ const serviceVariables = collectServiceVariables(source);
2074
+ const calledNames = /* @__PURE__ */ new Set();
2075
+ const collectCalls = (node) => {
2076
+ if (ts7.isCallExpression(node)) {
2077
+ if (ts7.isIdentifier(node.expression)) calledNames.add(node.expression.text);
2078
+ if (ts7.isCallExpression(node.expression) && ts7.isIdentifier(node.expression.expression)) calledNames.add(node.expression.expression.text);
2079
+ }
2080
+ ts7.forEachChild(node, collectCalls);
2081
+ };
2082
+ collectCalls(source);
2083
+ const scanFunction = (name, fn) => {
2084
+ if (!calledNames.has(name)) return;
2085
+ const params = fn.parameters.map((param) => ts7.isIdentifier(param.name) ? param.name.text : void 0);
2086
+ const sends = [];
2087
+ const visit = (node) => {
2088
+ if (ts7.isCallExpression(node) && ts7.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send" && ts7.isIdentifier(node.expression.expression)) {
2089
+ const objectArg = node.arguments[0];
2090
+ if (objectArg && ts7.isObjectLiteralExpression(objectArg)) {
2091
+ const pathProp = propertyInitializer(objectArg, "path");
2092
+ const methodProp = propertyInitializer(objectArg, "method");
2093
+ const pathName = pathProp && ts7.isIdentifier(pathProp) ? pathProp.text : void 0;
2094
+ const methodName = methodProp && ts7.isIdentifier(methodProp) ? methodProp.text : void 0;
2095
+ const methodLiteral = resolveExpression(methodProp, node, "literal").value;
2096
+ if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
2097
+ }
2016
2098
  }
2017
- continue;
2018
- }
2019
- const assignment = event.node;
2020
- if (ts6.isIdentifier(assignment.left)) {
2021
- const rhs = unwrapIdentityExpression(assignment.right);
2022
- if (ts6.isIdentifier(rhs)) {
2023
- cloneAliasBinding(assignment.left.text, rhs.text, "identity-assignment", assignment);
2024
- continue;
2099
+ if (ts7.isCallExpression(node) && ts7.isIdentifier(node.expression) && specs.has(node.expression.text)) {
2100
+ const nested = specs.get(node.expression.text);
2101
+ const pathArg = nested ? node.arguments[nested.pathIndex] : void 0;
2102
+ const clientArg = nested?.clientIndex === void 0 ? void 0 : node.arguments[nested.clientIndex];
2103
+ const pathName = pathArg && ts7.isIdentifier(pathArg) ? pathArg.text : void 0;
2104
+ const clientName = clientArg && ts7.isIdentifier(clientArg) ? clientArg.text : nested?.clientName;
2105
+ if (nested && pathName && clientName) sends.push({ client: clientName, path: pathName, method: nested.methodName, methodLiteral: nested.methodLiteral, nestedWrapperFunction: node.expression.text, start: node.getStart(source), end: node.getEnd() });
2025
2106
  }
2026
- if (recordObjectPropertyBinding(assignment.left.text, assignment.right, assignment)) continue;
2027
- await recordBindingFromExpression(assignment.left.text, assignment.right, assignment, "assignment");
2028
- continue;
2029
- }
2030
- const left = ts6.isParenthesizedExpression(assignment.left) ? assignment.left.expression : assignment.left;
2031
- if (ts6.isObjectLiteralExpression(left))
2032
- await recordDestructuredAssignment(left, assignment.right, assignment);
2033
- if (ts6.isArrayLiteralExpression(left))
2034
- await recordArrayDestructuredAssignment(left, assignment.right, assignment);
2035
- }
2036
- return out;
2107
+ ts7.forEachChild(node, visit);
2108
+ };
2109
+ visit(fn);
2110
+ if (sends.length !== 1) return;
2111
+ const found = sends[0];
2112
+ const clientIndex = params.indexOf(found.client);
2113
+ const pathIndex = params.indexOf(found.path);
2114
+ const methodIndex = found.method ? params.indexOf(found.method) : -1;
2115
+ const capturesKnownClient = serviceVariables.has(found.client) || /^(srv|service|serviceClient|client|.*Client)$/.test(found.client);
2116
+ if (pathIndex >= 0 && (clientIndex >= 0 || capturesKnownClient)) specs.set(name, { clientIndex: clientIndex >= 0 ? clientIndex : void 0, clientName: clientIndex >= 0 ? void 0 : found.client, pathIndex, methodIndex: methodIndex >= 0 ? methodIndex : void 0, methodName: found.method, methodLiteral: found.methodLiteral, nestedWrapperFunction: found.nestedWrapperFunction, definitionLine: lineOf4(source.text, fn.getStart(source)), internalStart: found.start, internalEnd: found.end });
2117
+ };
2118
+ const visitTop = (node) => {
2119
+ if (ts7.isFunctionDeclaration(node) && node.name) scanFunction(node.name.text, node);
2120
+ if (ts7.isVariableDeclaration(node) && ts7.isIdentifier(node.name) && node.initializer && (ts7.isArrowFunction(node.initializer) || ts7.isFunctionExpression(node.initializer))) scanFunction(node.name.text, node.initializer);
2121
+ ts7.forEachChild(node, visitTop);
2122
+ };
2123
+ visitTop(source);
2124
+ return specs;
2037
2125
  }
2038
- function collectClassHelpers(sf) {
2039
- const helpers = [];
2040
- for (const stmt of sf.statements) {
2041
- if (!ts6.isClassDeclaration(stmt) || !stmt.name) continue;
2042
- for (const member of stmt.members) {
2043
- let visit2 = function(node) {
2044
- if (ts6.isVariableDeclaration(node) && ts6.isIdentifier(node.name) && node.initializer) {
2045
- const fact = findConnectInExpression(node.initializer);
2046
- if (fact) bindings.set(node.name.text, fact);
2047
- }
2048
- if (ts6.isReturnStatement(node) && node.expression && ts6.isObjectLiteralExpression(node.expression)) {
2049
- for (const prop of node.expression.properties) {
2050
- if (ts6.isShorthandPropertyAssignment(prop)) {
2051
- const fact = bindings.get(prop.name.text);
2052
- if (fact)
2053
- helpers.push({
2054
- className,
2055
- helperName,
2056
- propertyName: prop.name.text,
2057
- variableName: prop.name.text,
2058
- fact,
2059
- sourceLine: lineOf4(sf, prop)
2060
- });
2061
- }
2062
- if (ts6.isPropertyAssignment(prop) && ts6.isIdentifier(prop.initializer)) {
2063
- const propertyName = ts6.isIdentifier(prop.name) || ts6.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
2064
- const fact = propertyName ? bindings.get(prop.initializer.text) : void 0;
2065
- if (propertyName && fact)
2066
- helpers.push({
2067
- className,
2068
- helperName,
2069
- propertyName,
2070
- variableName: prop.initializer.text,
2071
- fact,
2072
- sourceLine: lineOf4(sf, prop)
2073
- });
2074
- }
2126
+ function classifyOutboundCallsInSource(source, filePath) {
2127
+ const calls = [];
2128
+ const sourceFile = normalizePath(filePath);
2129
+ const initializers = variableInitializers(source);
2130
+ const serviceVariables = collectServiceVariables(source);
2131
+ const wrapperSpecs = collectWrapperSpecs(source);
2132
+ const wrapperInternalRanges = [...wrapperSpecs.values()].map((spec) => ({ start: spec.internalStart, end: spec.internalEnd }));
2133
+ const add = (node, fact, extra) => {
2134
+ calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf4(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
2135
+ };
2136
+ const visit = (node) => {
2137
+ if (ts7.isCallExpression(node)) {
2138
+ if (wrapperInternalRanges.some((range) => node.getStart(source) >= range.start && node.getEnd() <= range.end)) {
2139
+ return;
2140
+ }
2141
+ const expr = node.expression;
2142
+ const exprText = expr.getText(source);
2143
+ if (exprText === "cds.run") {
2144
+ const arg = node.arguments[0];
2145
+ const entity = arg ? queryEntityFromAst(arg, initializers) : void 0;
2146
+ const payload = arg?.getText(source) ?? "";
2147
+ add(node, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) });
2148
+ } else if (ts7.isPropertyAccessExpression(expr) && expr.name.text === "send" && (ts7.isIdentifier(expr.expression) || ts7.isPropertyAccessExpression(expr.expression))) {
2149
+ const objectArg = node.arguments[0];
2150
+ if (objectArg && ts7.isObjectLiteralExpression(objectArg)) {
2151
+ const receiver = receiverName(expr.expression);
2152
+ const query = objectPropertyText(objectArg, "query");
2153
+ const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, "method"), node, "literal").value ?? objectPropertyText(objectArg, "method") ?? "POST");
2154
+ const pathExpr = propertyInitializer(objectArg, "path") ?? propertyInitializer(objectArg, "event");
2155
+ const resolvedPath = staticPathExpression(pathExpr, node);
2156
+ const op = pathExpr ? resolvedPath.text ?? pathExpr.getText(source) : void 0;
2157
+ const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
2158
+ const candidateEvidence2 = pathExpr && ts7.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(pathExpr, node, method) : void 0;
2159
+ const candidateOperationPath = candidateEvidence2 && !candidateEvidence2.hasDynamicAssignments && candidateEvidence2.normalizedCandidateOperations.length === 1 && candidateEvidence2.candidatePaths.length > 0 ? candidateEvidence2.candidatePaths[0] : void 0;
2160
+ const operationPathExpr = operationPathFromStatic(resolvedPath.text) ?? candidateOperationPath;
2161
+ const intent = classifyODataPathIntent(operationPathExpr, method);
2162
+ const entityCallTypes = { entity_mutation: "remote_entity_mutation", entity_delete: "remote_entity_delete", entity_media: "remote_entity_media", entity_candidate: "remote_entity_candidate" };
2163
+ const entityCallType = entityCallTypes[intent.kind];
2164
+ const isODataQueryRead = method.toUpperCase() === "GET" && ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
2165
+ add(node, { callType: query ? "remote_query" : entityCallType ?? (isODataQueryRead ? "remote_query" : "remote_action"), serviceVariableName: receiver, method, operationPathExpr, queryEntity: query ? extractQueryEntity(query) : isODataQueryRead ? intent.entitySegment : void 0, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : resolvedPath.sourceKind : void 0, odataPathIntent: operationPathExpr ? intent : void 0, staticPathCandidates: candidateEvidence2, parserWarning: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 });
2166
+ } else {
2167
+ const receiver = receiverName(expr.expression);
2168
+ const rootReceiver = rootReceiverName(expr.expression);
2169
+ const firstArg2 = resolveExpression(node.arguments[0], node, "literal");
2170
+ const method = firstArg2.value?.toUpperCase();
2171
+ const pathArg = node.arguments[1];
2172
+ const supported = method && supportedHttpMethods.has(method);
2173
+ if (receiver && supported && serviceVariables.has(rootReceiver ?? receiver)) {
2174
+ const resolvedPath = staticPathExpression(pathArg, node);
2175
+ const operationPathExpr = operationPathFromStatic(resolvedPath.text);
2176
+ const intent = classifyODataPathIntent(operationPathExpr, method);
2177
+ add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.8 : 0.45, unresolvedReason: operationPathExpr ? void 0 : "dynamic_operation_path_identifier" }, { receiver, rootReceiver, classifier: "service_client_send_method_path", rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : resolvedPath.sourceKind : void 0, odataPathIntent: operationPathExpr ? intent : void 0, parserWarning: operationPathExpr ? void 0 : "dynamic_operation_path_identifier" });
2178
+ } else if (receiver && serviceVariables.has(rootReceiver ?? receiver)) {
2179
+ const operationPathExpr = safeOperationName(firstArg2.value);
2180
+ add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.75 : 0.35, unresolvedReason: operationPathExpr ? void 0 : "unsupported_cap_send_signature" }, { receiver, rootReceiver, classifier: operationPathExpr ? "service_client_send_operation_event" : "service_client_send_unsupported_signature", rawOperationExpression: firstArg2.rawExpression, literalOperationSource: firstArg2.value ? firstArg2.sourceKind : void 0, parserWarning: operationPathExpr ? void 0 : "unsupported_cap_send_signature" });
2075
2181
  }
2076
2182
  }
2077
- ts6.forEachChild(node, visit2);
2078
- };
2079
- var visit = visit2;
2080
- if (!ts6.isPropertyDeclaration(member) || !member.initializer) continue;
2081
- if (!ts6.isIdentifier(member.name)) continue;
2082
- const className = stmt.name.text;
2083
- const helperName = member.name.text;
2084
- const initializer = member.initializer;
2085
- if (!ts6.isArrowFunction(initializer) && !ts6.isFunctionExpression(initializer))
2086
- continue;
2087
- const bindings = /* @__PURE__ */ new Map();
2088
- visit2(initializer);
2183
+ } else if (ts7.isCallExpression(expr) && ts7.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts7.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
2184
+ const wrapperName = ts7.isIdentifier(expr) ? expr.text : ts7.isCallExpression(expr) && ts7.isIdentifier(expr.expression) ? expr.expression.text : "";
2185
+ const wrapperArgs = ts7.isIdentifier(expr) ? node.arguments : ts7.isCallExpression(expr) ? expr.arguments : node.arguments;
2186
+ const spec = wrapperSpecs.get(wrapperName);
2187
+ const clientArg = spec?.clientIndex === void 0 ? void 0 : wrapperArgs[spec.clientIndex];
2188
+ const pathArg = spec ? wrapperArgs[spec.pathIndex] : void 0;
2189
+ const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
2190
+ const receiver = clientArg && ts7.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
2191
+ const method = stripQuotes(resolveExpression(methodArg, node, "literal").value ?? spec?.methodLiteral ?? "POST");
2192
+ const resolvedPath = staticPathExpression(pathArg, node);
2193
+ const operationPathExpr = operationPathFromStatic(resolvedPath.text);
2194
+ const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : void 0;
2195
+ if (spec && receiver && operationPathExpr) {
2196
+ add(node, { callType: "remote_action", serviceVariableName: receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75 }, { receiver, classifier: resolvedPath.sourceKind === "literal" ? "higher_order_wrapper_literal_path" : "higher_order_wrapper_static_path", wrapperFunction: wrapperName, nestedWrapperFunction: spec.nestedWrapperFunction, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf4(source.text, node.getStart(source)), wrapperPathSourceKind: resolvedPath.sourceKind, rawPathExpression: resolvedPath.rawExpression, normalizedOperationPath, literalPathSource: resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : `wrapper_call_${resolvedPath.sourceKind}`, literalCallerArgumentDetected: true });
2197
+ } else if (spec && receiver) {
2198
+ add(node, { callType: "remote_action", serviceVariableName: receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason: "dynamic_operation_path_identifier" }, { receiver, classifier: "higher_order_wrapper_dynamic_path", wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf4(source.text, node.getStart(source)), wrapperPathSourceKind: resolvedPath.sourceKind, rawPathExpression: resolvedPath.rawExpression, parserWarning: "dynamic_operation_path_identifier" });
2199
+ }
2200
+ } else if (ts7.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
2201
+ const receiver = receiverName(expr.expression);
2202
+ const rootReceiver = rootReceiverName(expr.expression);
2203
+ if (isSupportedEventReceiver(receiver, rootReceiver, serviceVariables)) {
2204
+ const eventName = literalText(node.arguments[0]);
2205
+ if (eventName) add(node, { callType: expr.name.text === "on" ? "async_subscribe" : "async_emit", serviceVariableName: rootReceiver ?? receiver, eventNameExpr: eventName }, { receiver, rootReceiver, classifier: expr.name.text === "on" ? "cap_service_event_subscription" : "cap_service_event_emit", receiverClassification: "cap_evidence" });
2206
+ }
2207
+ } else {
2208
+ const external = externalHttpEvidence(node, source);
2209
+ if (external) {
2210
+ const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
2211
+ const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
2212
+ add(node, { callType: "external_http", method: external.method, payloadSummary: void 0, confidence: 0.7, unresolvedReason: "External HTTP destination is outside indexed CAP services", externalTarget: { kind: safeTarget.kind, stableId: safeTarget.toId, label: safeTarget.label, dynamic: safeTarget.dynamic } }, { classifier: external.classifier, externalTarget: safeTarget, sourceCallShape: external.sourceCallShape });
2213
+ }
2214
+ }
2215
+ }
2216
+ ts7.forEachChild(node, visit);
2217
+ };
2218
+ visit(source);
2219
+ return calls;
2220
+ }
2221
+ function containsSupportedOutboundCall(node) {
2222
+ const source = node.getSourceFile();
2223
+ const start = node.getFullStart();
2224
+ const end = node.getEnd();
2225
+ return classifyOutboundCallsInSource(source, source.fileName).some((call) => call.node.getStart(source) >= start && call.node.getEnd() <= end);
2226
+ }
2227
+ async function parseOutboundCalls(repoPath, filePath) {
2228
+ const text = await fs7.readFile(path10.join(repoPath, filePath), "utf8");
2229
+ const source = ts7.createSourceFile(filePath, text, ts7.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts7.ScriptKind.TS : ts7.ScriptKind.JS);
2230
+ const bindingNames = new Set((await parseServiceBindings(repoPath, filePath)).map((binding) => binding.variableName));
2231
+ const importedWrappers = await parseImportedWrapperCalls(repoPath, filePath, source, bindingNames);
2232
+ return [...classifyOutboundCallsInSource(source, filePath).map((call) => call.fact), ...importedWrappers, ...parseLocalServiceCalls(text, filePath)];
2233
+ }
2234
+ function parseLocalServiceCalls(text, filePath) {
2235
+ const source = ts7.createSourceFile(filePath, text, ts7.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts7.ScriptKind.TS : ts7.ScriptKind.JS);
2236
+ const aliases = /* @__PURE__ */ new Map();
2237
+ const calls = [];
2238
+ const visit = (node) => {
2239
+ if (ts7.isVariableDeclaration(node) && ts7.isIdentifier(node.name) && node.initializer) {
2240
+ const origin = serviceLookup(node.initializer, aliases);
2241
+ if (origin) aliases.set(node.name.text, { ...origin, chain: [...origin.chain, node.name.text] });
2242
+ }
2243
+ if (ts7.isCallExpression(node)) {
2244
+ const parsed = serviceOperationCall(node, aliases);
2245
+ if (parsed && parsed.operation !== "entities") calls.push({
2246
+ callType: "local_service_call",
2247
+ operationPathExpr: `/${parsed.operation}`,
2248
+ payloadSummary: parsed.service,
2249
+ localServiceName: parsed.service,
2250
+ localServiceLookup: parsed.lookup,
2251
+ aliasChain: parsed.chain,
2252
+ sourceFile: normalizePath(filePath),
2253
+ sourceLine: lineOf4(text, node.getStart(source)),
2254
+ confidence: 0.9,
2255
+ unresolvedReason: ["send", "emit", "publish", "on"].includes(parsed.operation) ? "transport_client_method" : void 0,
2256
+ evidence: parserEvidence(source, node, {
2257
+ classifier: parsed.classifier,
2258
+ parserCallType: parsed.operation === "send" ? "transport_client_method" : parsed.classifier,
2259
+ localServiceLookup: parsed.lookup,
2260
+ localServiceName: parsed.service,
2261
+ operation: parsed.operation,
2262
+ aliasChain: parsed.chain
2263
+ })
2264
+ });
2089
2265
  }
2266
+ ts7.forEachChild(node, visit);
2267
+ };
2268
+ visit(source);
2269
+ return calls;
2270
+ }
2271
+ function serviceLookup(expr, aliases) {
2272
+ if (ts7.isIdentifier(expr)) return aliases.get(expr.text);
2273
+ if (ts7.isPropertyAccessExpression(expr) && expr.expression.getText() === "cds.services") return { service: expr.name.text, lookup: expr.getText(), chain: [expr.getText()] };
2274
+ if (ts7.isElementAccessExpression(expr) && expr.expression.getText() === "cds.services" && ts7.isStringLiteral(expr.argumentExpression)) return { service: expr.argumentExpression.text, lookup: expr.getText(), chain: [expr.getText()] };
2275
+ return void 0;
2276
+ }
2277
+ function serviceOperationCall(node, aliases) {
2278
+ const expr = node.expression;
2279
+ if (!ts7.isPropertyAccessExpression(expr)) return void 0;
2280
+ const origin = serviceLookup(expr.expression, aliases);
2281
+ if (!origin) return void 0;
2282
+ if (expr.name.text === "send") {
2283
+ const first = literalText(node.arguments[0]);
2284
+ const second = literalText(node.arguments[1]);
2285
+ const method = first?.toUpperCase();
2286
+ if (method && ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"].includes(method) && second) return { ...origin, operation: second.replace(/^\//, ""), classifier: "cap_service_send_method_path" };
2287
+ if (first) return { ...origin, operation: first.replace(/^\//, ""), classifier: "cap_service_send_local_dispatch" };
2090
2288
  }
2091
- return helpers;
2289
+ return { ...origin, operation: expr.name.text, classifier: "local_cap_service_call" };
2092
2290
  }
2093
2291
 
2094
2292
  // src/linker/dynamic-edge-resolver.ts
@@ -2118,6 +2316,171 @@ function substituteVariables(template, vars) {
2118
2316
  };
2119
2317
  }
2120
2318
 
2319
+ // src/trace/implementation-hints.ts
2320
+ function parseImplementationHint(value) {
2321
+ const hint = {};
2322
+ for (const part of value.split(",")) {
2323
+ const separator = part.indexOf("=");
2324
+ if (separator <= 0 || separator === part.length - 1) throw new Error(`Invalid implementation hint field: ${part}`);
2325
+ assignHintField(hint, part.slice(0, separator).trim(), part.slice(separator + 1).trim());
2326
+ }
2327
+ if (!hint.implementationRepo) throw new Error("Scoped implementation hint requires an implementation repo selection");
2328
+ return { ...hint, implementationRepo: hint.implementationRepo };
2329
+ }
2330
+ function selectImplementation(rawEvidence, hints, legacyRepo) {
2331
+ const evidence = asEvidence(rawEvidence);
2332
+ const scoped = hints ?? [];
2333
+ const matchingHints = scoped.filter((hint2) => hintMatchesEdge(hint2, evidence));
2334
+ if (matchingHints.length === 0) {
2335
+ if (legacyRepo) return selectCandidate(evidence, legacyHint(legacyRepo), "implementation_repo_hint");
2336
+ const reason = scoped.length > 0 ? "no_scoped_hint_matched_edge" : "no_implementation_hint_supplied";
2337
+ return { blocksAutomatic: false, evidence: { status: "not_matched", reason, strategy: "scoped_implementation_hint" } };
2338
+ }
2339
+ if (matchingHints.length > 1) {
2340
+ return {
2341
+ blocksAutomatic: true,
2342
+ evidence: {
2343
+ status: "tied",
2344
+ reason: "multiple_scoped_hints_matched_edge",
2345
+ strategy: "scoped_implementation_hint",
2346
+ matchedHints: matchingHints,
2347
+ candidateCount: matchingHints.length
2348
+ }
2349
+ };
2350
+ }
2351
+ const hint = matchingHints[0];
2352
+ return hint ? selectCandidate(evidence, hint, "scoped_implementation_hint") : { blocksAutomatic: false, evidence: { status: "not_matched" } };
2353
+ }
2354
+ function implementationHintDiagnostic(selection, suggestions) {
2355
+ if (!selection.blocksAutomatic || selection.methodId) return void 0;
2356
+ return {
2357
+ severity: "warning",
2358
+ code: "implementation_hint_mismatch",
2359
+ message: "Implementation hint did not select exactly one viable candidate",
2360
+ hintStatus: selection.evidence.status,
2361
+ candidateCount: selection.evidence.candidateCount,
2362
+ implementationHintSuggestions: Array.isArray(suggestions) && suggestions.length > 0 ? suggestions : void 0,
2363
+ implementationSelection: selection.evidence
2364
+ };
2365
+ }
2366
+ function implementationHintSuggestions(rawEvidence) {
2367
+ const evidence = asEvidence(rawEvidence);
2368
+ const accepted = (evidence.candidates ?? []).filter((candidate) => candidate.accepted);
2369
+ if (accepted.length < 2) return [];
2370
+ const repos = selectableRepositories(accepted);
2371
+ return accepted.flatMap((candidate) => {
2372
+ const repo = candidate.handlerPackage?.name;
2373
+ if (!repo || !repos.includes(repo)) return [];
2374
+ const hint = suggestionHint(evidence, candidate, repo);
2375
+ return [{
2376
+ servicePath: hint.servicePath,
2377
+ operationPath: hint.operationPath,
2378
+ ambiguityReason: evidence.ambiguityReasons?.[0],
2379
+ candidateFamily: hint.candidateFamily,
2380
+ selectableImplementationRepositories: repos,
2381
+ implementationRepo: repo,
2382
+ hint,
2383
+ cli: `--implementation-hint ${hintString(hint)}`
2384
+ }];
2385
+ });
2386
+ }
2387
+ function selectableRepositories(candidates) {
2388
+ const repos = new Set(candidates.flatMap((candidate) => candidate.handlerPackage?.name ? [candidate.handlerPackage.name] : []));
2389
+ return [...repos].filter((repo) => candidates.filter((candidate) => candidateMatchesRepo(candidate, repo)).length === 1).sort();
2390
+ }
2391
+ function assignHintField(hint, key, value) {
2392
+ if (key === "service" || key === "servicePath") hint.servicePath = value;
2393
+ else if (key === "operation" || key === "operationPath") hint.operationPath = value;
2394
+ else if (key === "package" || key === "packageName") hint.packageName = value;
2395
+ else if (key === "repository" || key === "repositoryName") hint.repositoryName = value;
2396
+ else if (key === "family" || key === "candidateFamily") hint.candidateFamily = value;
2397
+ else if (key === "repo" || key === "implementationRepo" || key === "select") hint.implementationRepo = value;
2398
+ else throw new Error(`Unknown implementation hint field: ${key}`);
2399
+ }
2400
+ function selectCandidate(evidence, hint, strategy) {
2401
+ const matches2 = (evidence.candidates ?? []).filter((candidate) => candidate.accepted && candidateMatchesRepo(candidate, hint.implementationRepo));
2402
+ const selected = matches2.length === 1 ? matches2[0] : void 0;
2403
+ if (!selected || selected.methodId === void 0) {
2404
+ return {
2405
+ blocksAutomatic: true,
2406
+ evidence: {
2407
+ status: matches2.length > 1 ? "tied" : "not_matched",
2408
+ reason: matches2.length > 1 ? "hint_matched_multiple_candidates" : "hint_matched_zero_candidates",
2409
+ strategy,
2410
+ matchedHint: hint,
2411
+ selectedRepo: hint.implementationRepo,
2412
+ candidateCount: matches2.length
2413
+ }
2414
+ };
2415
+ }
2416
+ return {
2417
+ methodId: String(selected.methodId),
2418
+ blocksAutomatic: false,
2419
+ evidence: {
2420
+ status: "selected",
2421
+ guided: true,
2422
+ strategy,
2423
+ matchedHint: hint,
2424
+ selectedRepo: hint.implementationRepo,
2425
+ selectedMethodId: selected.methodId,
2426
+ ambiguityReason: evidence.ambiguityReasons?.[0]
2427
+ }
2428
+ };
2429
+ }
2430
+ function suggestionHint(evidence, candidate, repo) {
2431
+ const servicePath = evidence.servicePath ?? candidate.servicePath;
2432
+ const operationPath = evidence.operationPath ?? candidate.operationPath;
2433
+ const family = usefulCandidateFamily(evidence, candidate);
2434
+ return {
2435
+ ...servicePath ? { servicePath } : {},
2436
+ ...operationPath ? { operationPath } : {},
2437
+ ...evidence.modelPackage?.packageName ? { packageName: evidence.modelPackage.packageName } : {},
2438
+ ...evidence.modelPackage?.name ? { repositoryName: evidence.modelPackage.name } : {},
2439
+ ...family ? { candidateFamily: family } : {},
2440
+ implementationRepo: repo
2441
+ };
2442
+ }
2443
+ function usefulCandidateFamily(evidence, candidate) {
2444
+ const family = candidate.handlerPackage?.packageName;
2445
+ if (!family) return void 0;
2446
+ if ((evidence.candidateFamilies ?? []).some((item) => item.packageName === family)) return family;
2447
+ const acceptedFamilies = new Set(
2448
+ (evidence.candidates ?? []).filter((item) => item.accepted).flatMap((item) => item.handlerPackage?.packageName ? [item.handlerPackage.packageName] : [])
2449
+ );
2450
+ return acceptedFamilies.size > 1 ? family : void 0;
2451
+ }
2452
+ function hintString(hint) {
2453
+ const fields = [
2454
+ ["service", hint.servicePath],
2455
+ ["operation", hint.operationPath],
2456
+ ["package", hint.packageName],
2457
+ ["repository", hint.repositoryName],
2458
+ ["family", hint.candidateFamily],
2459
+ ["repo", hint.implementationRepo]
2460
+ ];
2461
+ return fields.flatMap(([key, value]) => value ? [`${key}=${value}`] : []).join(",");
2462
+ }
2463
+ function hintMatchesEdge(hint, evidence) {
2464
+ const model = evidence.modelPackage ?? evidence.candidates?.[0]?.modelPackage;
2465
+ const familyNames = /* @__PURE__ */ new Set([
2466
+ ...(evidence.candidateFamilies ?? []).flatMap((family) => family.packageName ? [family.packageName] : []),
2467
+ ...(evidence.candidates ?? []).flatMap((candidate) => candidate.handlerPackage?.packageName ? [candidate.handlerPackage.packageName] : [])
2468
+ ]);
2469
+ return matches(hint.servicePath, evidence.servicePath ?? evidence.candidates?.[0]?.servicePath) && matches(hint.operationPath, evidence.operationPath ?? evidence.candidates?.[0]?.operationPath) && matches(hint.packageName, model?.packageName) && matches(hint.repositoryName, model?.name) && (!hint.candidateFamily || familyNames.has(hint.candidateFamily));
2470
+ }
2471
+ function candidateMatchesRepo(candidate, value) {
2472
+ return candidate.handlerPackage?.name === value || candidate.handlerPackage?.packageName === value || candidate.sourceFile?.startsWith(value) === true;
2473
+ }
2474
+ function matches(expected, actual) {
2475
+ return expected === void 0 || expected === actual;
2476
+ }
2477
+ function legacyHint(implementationRepo) {
2478
+ return { implementationRepo };
2479
+ }
2480
+ function asEvidence(value) {
2481
+ return value;
2482
+ }
2483
+
2121
2484
  // src/linker/remote-query-target.ts
2122
2485
  function buildRemoteQueryTarget(input) {
2123
2486
  const entity = typeof input.queryEntity === "string" && input.queryEntity.trim() ? input.queryEntity.trim() : void 0;
@@ -2364,9 +2727,9 @@ function generatedFromConstantName(value) {
2364
2727
  return void 0;
2365
2728
  }
2366
2729
  function resolved(value, raw) {
2367
- const literal = clean(value);
2368
- const generated = generatedFromConstantName(literal);
2369
- return { status: "resolved", operationName: generated ?? normalizedOperationName(literal), raw };
2730
+ const literal2 = clean(value);
2731
+ const generated = generatedFromConstantName(literal2);
2732
+ return { status: "resolved", operationName: generated ?? normalizedOperationName(literal2), raw };
2370
2733
  }
2371
2734
  function normalizeDecoratorOperationSignal(value, raw, candidateOperation) {
2372
2735
  if (value) return resolved(value, raw);
@@ -2512,7 +2875,25 @@ function objectJson(value) {
2512
2875
  }
2513
2876
  function callEvidence(call, resolution, servicePath, op, destination, normalized, odataPathIntent) {
2514
2877
  const bindingHasDynamicExpression = Boolean(Number(call.isDynamic ?? 0));
2515
- return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, callId: call.id, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, rawOperationPath: normalized?.wasInvocation ? normalized.rawOperationPath : odataPathIntent?.rawPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : void 0, odataOperationNormalizationReason: normalized?.normalizationReason, odataOperationNormalizationRejectedReason: normalized?.normalizationRejectedReason, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: parseJson(call.alias_chain_json), transport: call.call_type === "local_service_call" ? "local" : void 0, targetRepo: resolution.target?.repoName, targetServicePath: resolution.target?.servicePath, targetOperationPath: resolution.target?.operationPath, targetOperation: resolution.target?.operationName, helperChain: parseJson(call.helperChainJson), candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, odataPathIntent, queryStringPresent: odataPathIntent?.hasQueryString || void 0, queryPlaceholderKeys: odataPathIntent?.placeholderKeys.length ? odataPathIntent.placeholderKeys : void 0, bindingHasDynamicExpression: bindingHasDynamicExpression || void 0, outboundEvidence: objectJson(call.evidence_json), analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
2878
+ const routingPlaceholderKeys = placeholderKeys([servicePath, destination, stringValue2(call.aliasExpr), stringValue2(call.alias)]);
2879
+ return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, callId: call.id, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, rawOperationPath: normalized?.wasInvocation ? normalized.rawOperationPath : odataPathIntent?.rawPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, invocationArguments: normalized?.wasInvocation ? normalized.invocationArguments : void 0, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : void 0, routingPlaceholderKeys: routingPlaceholderKeys.length ? routingPlaceholderKeys : void 0, odataOperationNormalizationReason: normalized?.normalizationReason, odataOperationNormalizationRejectedReason: normalized?.normalizationRejectedReason, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: parseJson(call.alias_chain_json), transport: call.call_type === "local_service_call" ? "local" : void 0, targetRepo: resolution.target?.repoName, targetServicePath: resolution.target?.servicePath, targetOperationPath: resolution.target?.operationPath, targetOperation: resolution.target?.operationName, helperChain: parseJson(call.helperChainJson), candidates: resolution.candidates, candidateScores: compactCandidateScores(resolution.candidates), candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, odataPathIntent, queryStringPresent: odataPathIntent?.hasQueryString || void 0, queryPlaceholderKeys: odataPathIntent?.placeholderKeys.length ? odataPathIntent.placeholderKeys : void 0, bindingHasDynamicExpression: bindingHasDynamicExpression || void 0, outboundEvidence: objectJson(call.evidence_json), analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
2880
+ }
2881
+ function compactCandidateScores(candidates) {
2882
+ return candidates.flatMap((candidate) => {
2883
+ const row = objectValue(candidate);
2884
+ if (!row) return [];
2885
+ return [{ repo: row.repoName, servicePath: row.servicePath, operationPath: row.operationPath, score: row.score, reasons: row.reasons }];
2886
+ });
2887
+ }
2888
+ function placeholderKeys(values) {
2889
+ const keys = values.flatMap((value) => [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((match) => (match[1] ?? "").trim()).filter(Boolean));
2890
+ return [...new Set(keys)].sort();
2891
+ }
2892
+ function objectValue(value) {
2893
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2894
+ }
2895
+ function stringValue2(value) {
2896
+ return typeof value === "string" ? value : void 0;
2516
2897
  }
2517
2898
  function linkImplementations(db, workspaceId, generation) {
2518
2899
  const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,o.provenance provenance,o.base_operation_id baseOperationId,s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,r.package_name modelPackage,r.kind modelKind 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);
@@ -2544,13 +2925,14 @@ function linkImplementations(db, workspaceId, generation) {
2544
2925
  candidateFamilies: duplicateFamilies,
2545
2926
  candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1))
2546
2927
  };
2928
+ const evidenceWithHints = unique ? evidence : { ...evidence, implementationHintSuggestions: implementationHintSuggestions(evidence) };
2547
2929
  if (accepted.length === 0) {
2548
- db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", "unresolved", "operation", graphId(operation.operationId), "handler_method_candidates", candidates.map((row) => graphId(row.methodId)).join(","), 0, JSON.stringify(evidence), 0, "No implementation candidate passed policy", generation);
2930
+ db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", "unresolved", "operation", graphId(operation.operationId), "handler_method_candidates", candidates.map((row) => graphId(row.methodId)).join(","), 0, JSON.stringify(evidenceWithHints), 0, "No implementation candidate passed policy", generation);
2549
2931
  edgeCount += 1;
2550
2932
  unresolvedCount += 1;
2551
2933
  continue;
2552
2934
  }
2553
- 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", graphId(operation.operationId), unique ? "handler_method" : "handler_method_candidates", unique ? graphId(unique.methodId) : selected.map((row) => graphId(row.methodId)).join(","), unique ? 0.95 : 0.5, JSON.stringify(evidence), 0, unique ? null : "Ambiguous registered handler implementation candidates", generation);
2935
+ 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", graphId(operation.operationId), unique ? "handler_method" : "handler_method_candidates", unique ? graphId(unique.methodId) : selected.map((row) => graphId(row.methodId)).join(","), unique ? 0.95 : 0.5, JSON.stringify(evidenceWithHints), 0, unique ? null : "Ambiguous registered handler implementation candidates", generation);
2554
2936
  edgeCount += 1;
2555
2937
  if (unique) resolvedCount += 1;
2556
2938
  else ambiguousCount += 1;
@@ -2841,20 +3223,20 @@ function runtimeVariableDiagnostic(edges) {
2841
3223
  }
2842
3224
  function edgeTarget(row, evidence) {
2843
3225
  const effective = parseObject(evidence.effectiveResolution);
2844
- const targetServicePath = stringValue2(effective.targetServicePath ?? evidence.targetServicePath);
2845
- const targetOperationPath = stringValue2(effective.targetOperationPath ?? evidence.targetOperationPath);
3226
+ const targetServicePath = stringValue3(effective.targetServicePath ?? evidence.targetServicePath);
3227
+ const targetOperationPath = stringValue3(effective.targetOperationPath ?? evidence.targetOperationPath);
2846
3228
  if (targetServicePath && targetOperationPath) return `${targetServicePath}${targetOperationPath}`;
2847
3229
  const runtimeCandidate = evidence.runtimeResolvedCandidate;
2848
3230
  if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath) return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
2849
- const servicePath = stringValue2(evidence.servicePath);
2850
- const operationPath = stringValue2(evidence.operationPath);
2851
- const targetOperation = stringValue2(evidence.targetOperation);
2852
- const targetRepo = stringValue2(evidence.targetRepo) ?? "";
3231
+ const servicePath = stringValue3(evidence.servicePath);
3232
+ const operationPath = stringValue3(evidence.operationPath);
3233
+ const targetOperation = stringValue3(evidence.targetOperation);
3234
+ const targetRepo = stringValue3(evidence.targetRepo) ?? "";
2853
3235
  if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return `Entity: ${row.to_id || "unknown"}`;
2854
- if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return stringValue2(evidence.remoteQueryTarget) ?? `Remote query: ${row.to_id || "unknown"}`;
3236
+ if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return stringValue3(evidence.remoteQueryTarget) ?? `Remote query: ${row.to_id || "unknown"}`;
2855
3237
  if (row.edge_type === "HANDLER_CALLS_EXTERNAL_HTTP") {
2856
3238
  const target = parseObject(evidence.externalTarget);
2857
- return stringValue2(target.label) ?? `External endpoint: ${row.to_id || "unknown"}`;
3239
+ return stringValue3(target.label) ?? `External endpoint: ${row.to_id || "unknown"}`;
2858
3240
  }
2859
3241
  if (servicePath && operationPath) return `${servicePath}${operationPath}`;
2860
3242
  return targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
@@ -2893,10 +3275,12 @@ function withEffectiveResolution(evidence, row, unresolvedReason, resolution) {
2893
3275
  return { ...rest, effectiveResolution: current, linker: { status: current.status, confidence: current.confidence, reason: unresolvedReason, edgeType: current.edgeType } };
2894
3276
  }
2895
3277
  function resolveRuntimeOperation(db, evidence, workspaceId) {
2896
- const servicePath = stringValue2(evidence.servicePath);
2897
- const operationPath = stringValue2(evidence.normalizedOperationPath ?? evidence.operationPath);
2898
- const alias = stringValue2(evidence.serviceAliasExpr ?? evidence.serviceAlias);
2899
- const destination = stringValue2(evidence.destination);
3278
+ const servicePath = stringValue3(evidence.servicePath);
3279
+ const rawOperationPath = stringValue3(evidence.operationPath);
3280
+ const normalized = normalizeODataOperationInvocationPath(rawOperationPath);
3281
+ const operationPath = normalized?.wasInvocation ? normalized.normalizedOperationPath : stringValue3(evidence.normalizedOperationPath) ?? rawOperationPath;
3282
+ const alias = stringValue3(evidence.serviceAliasExpr ?? evidence.serviceAlias);
3283
+ const destination = stringValue3(evidence.destination);
2900
3284
  return resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: true }, workspaceId);
2901
3285
  }
2902
3286
  function evidenceWithRuntimeVariables(evidence, vars) {
@@ -2911,7 +3295,7 @@ function evidenceWithRuntimeVariables(evidence, vars) {
2911
3295
  function runtimeSubstitutions(evidence, vars) {
2912
3296
  const substitutions = {};
2913
3297
  for (const key of ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"]) {
2914
- const substitution = substituteVariables(stringValue2(evidence[key]), vars);
3298
+ const substitution = substituteVariables(stringValue3(evidence[key]), vars);
2915
3299
  if (substitution.placeholders.length > 0) substitutions[key] = substitution;
2916
3300
  }
2917
3301
  return substitutions;
@@ -2933,7 +3317,7 @@ function runtimeUnresolvedReason(resolution) {
2933
3317
  function parseObject(value) {
2934
3318
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2935
3319
  }
2936
- function stringValue2(value) {
3320
+ function stringValue3(value) {
2937
3321
  return typeof value === "string" ? value : void 0;
2938
3322
  }
2939
3323
 
@@ -2945,7 +3329,7 @@ function normalizeOperation(value) {
2945
3329
  function positiveDepth(value) {
2946
3330
  return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
2947
3331
  }
2948
- function operationStartScope(db, repoId, start, implementationRepo) {
3332
+ function operationStartScope(db, repoId, start, hintOptions) {
2949
3333
  const requested = normalizeOperation(start.operationPath ?? start.operation);
2950
3334
  if (!requested) return void 0;
2951
3335
  const rows2 = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_path operationPath,s.service_path servicePath,r.id repoId,r.name repoName
@@ -2961,14 +3345,16 @@ function operationStartScope(db, repoId, start, implementationRepo) {
2961
3345
  const operationId = String(rows2[0]?.operationId);
2962
3346
  const impl = implementationScope(db, operationId);
2963
3347
  if (impl.edge?.status === "resolved" && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? /* @__PURE__ */ new Set([impl.symbolId]) : void 0, operationId, diagnostics: [] };
2964
- const hinted = implementationMethodIdFromHint(impl.edge, implementationRepo);
3348
+ const hinted = implementationMethodIdFromHint(impl.edge, hintOptions);
2965
3349
  if (hinted.methodId) {
2966
3350
  const hintedScope = handlerScope(db, hinted.methodId);
2967
3351
  if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? /* @__PURE__ */ new Set([hintedScope.symbolId]) : void 0, operationId, diagnostics: [] };
2968
3352
  }
2969
3353
  if (impl.edge) {
2970
3354
  const evidence = parseEvidence(impl.edge.evidence_json);
2971
- return { operationId, diagnostics: [{ severity: "warning", code: impl.edge.status === "ambiguous" ? "trace_start_ambiguous" : "trace_start_implementation_unresolved", message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? "unresolved")}`, resolutionStage: "implementation", resolutionStatus: impl.edge.status === "ambiguous" ? "ambiguous_implementation" : "rejected_implementation", implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, implementationAmbiguityReasons: evidence.ambiguityReasons, candidates: evidence.candidates }] };
3355
+ const hintDiagnostic = implementationHintDiagnostic(hinted, evidence.implementationHintSuggestions);
3356
+ const diagnostics = [{ severity: "warning", code: impl.edge.status === "ambiguous" ? "trace_start_ambiguous" : "trace_start_implementation_unresolved", message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? "unresolved")}`, resolutionStage: "implementation", resolutionStatus: impl.edge.status === "ambiguous" ? "ambiguous_implementation" : "rejected_implementation", implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, implementationAmbiguityReasons: evidence.ambiguityReasons, implementationHintSuggestions: evidence.implementationHintSuggestions, candidates: evidence.candidates }];
3357
+ return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
2972
3358
  }
2973
3359
  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" }] };
2974
3360
  }
@@ -3012,12 +3398,12 @@ function sourceFilesForStart(db, repoId, start) {
3012
3398
  }
3013
3399
  return void 0;
3014
3400
  }
3015
- function startScope(db, start, implementationRepo) {
3401
+ function startScope(db, start, hintOptions) {
3016
3402
  const repo = start.repo ? db.prepare(
3017
3403
  "SELECT id,name FROM repositories WHERE name=? OR package_name=?"
3018
3404
  ).get(start.repo, start.repo) : void 0;
3019
3405
  if (start.repo && !repo) return { repo, selectorMatched: false };
3020
- const operationScope = operationStartScope(db, repo?.id, start, implementationRepo);
3406
+ const operationScope = operationStartScope(db, repo?.id, start, hintOptions);
3021
3407
  const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === "operation" || d.resolutionStage === "implementation");
3022
3408
  const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start);
3023
3409
  const sourceFiles = sourceScope?.files;
@@ -3064,27 +3450,15 @@ function implementationScope(db, operationId) {
3064
3450
  const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?").get(edge.to_id);
3065
3451
  return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
3066
3452
  }
3067
- function implementationMethodIdFromHint(edge, implementationRepo) {
3068
- if (!edge || edge.status !== "ambiguous" || !implementationRepo) return { evidence: { status: "not_applicable" } };
3069
- const evidence = parseEvidence(edge.evidence_json);
3070
- const matches = (evidence.candidates ?? []).filter((item) => item.accepted && (item.handlerPackage?.name === implementationRepo || item.handlerPackage?.packageName === implementationRepo || item.sourceFile?.startsWith(implementationRepo)));
3071
- if (matches.length !== 1 || matches[0]?.methodId === void 0) return { evidence: { status: matches.length > 1 ? "tied" : "not_matched", strategy: "implementation_repo_hint", selectedRepo: implementationRepo, candidateCount: matches.length } };
3072
- return {
3073
- methodId: String(matches[0].methodId),
3074
- evidence: {
3075
- status: "selected",
3076
- guided: true,
3077
- strategy: "implementation_repo_hint",
3078
- selectedRepo: implementationRepo,
3079
- selectedMethodId: matches[0].methodId,
3080
- ambiguityReason: evidence.ambiguityReasons?.[0]
3081
- }
3082
- };
3453
+ function implementationMethodIdFromHint(edge, options) {
3454
+ if (!edge || edge.status !== "ambiguous") return { blocksAutomatic: false, evidence: { status: "not_applicable" } };
3455
+ return selectImplementation(parseEvidence(edge.evidence_json), options.implementationHints, options.implementationRepo);
3083
3456
  }
3084
- function contextImplementationMethodId(edge, callerRepoId, remoteEvidence = {}, implementationRepo) {
3085
- const hinted = implementationMethodIdFromHint(edge, implementationRepo);
3457
+ function contextImplementationMethodId(edge, callerRepoId, remoteEvidence, hintOptions) {
3458
+ const hinted = implementationMethodIdFromHint(edge, hintOptions);
3086
3459
  if (hinted.methodId) return hinted;
3087
- if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return { evidence: { status: "not_applicable" } };
3460
+ if (hinted.blocksAutomatic) return hinted;
3461
+ if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return hinted;
3088
3462
  const evidence = JSON.parse(String(edge.evidence_json || "{}"));
3089
3463
  const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
3090
3464
  const reasons = [];
@@ -3103,10 +3477,10 @@ function contextImplementationMethodId(edge, callerRepoId, remoteEvidence = {},
3103
3477
  }
3104
3478
  return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
3105
3479
  }).sort((a, b) => b.score - a.score);
3106
- if (scores.length === 0) return { evidence: { status: "not_applicable", candidateScores: [] } };
3480
+ if (scores.length === 0) return { blocksAutomatic: false, evidence: { status: "not_applicable", candidateScores: [] } };
3107
3481
  const [first, second] = scores;
3108
- if (first && first.methodId !== void 0 && first.score > 0 && (!second || first.score > second.score)) return { methodId: String(first.methodId), evidence: { status: "selected", selectedMethodId: first.methodId, candidateScores: scores } };
3109
- return { evidence: { status: "tied", tieReason: scores.length > 1 ? "duplicate_helper_implementation_candidates" : "no_unique_materially_stronger_candidate", candidateScores: scores } };
3482
+ if (first && first.methodId !== void 0 && first.score > 0 && (!second || first.score > second.score)) return { methodId: String(first.methodId), blocksAutomatic: false, evidence: { status: "selected", selectedMethodId: first.methodId, candidateScores: scores } };
3483
+ return { blocksAutomatic: false, evidence: hinted.evidence.reason === "no_scoped_hint_matched_edge" ? hinted.evidence : { status: "tied", tieReason: scores.length > 1 ? "duplicate_helper_implementation_candidates" : "no_unique_materially_stronger_candidate", candidateScores: scores } };
3110
3484
  }
3111
3485
  function handlerScope(db, methodId) {
3112
3486
  const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?").get(methodId);
@@ -3204,18 +3578,22 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
3204
3578
  const next = /* @__PURE__ */ new Map();
3205
3579
  if (callerBindings.size === 0) return next;
3206
3580
  const callEvidence2 = parseEvidence(symbolCall.evidence_json);
3207
- const callee = db.prepare("SELECT evidence_json evidenceJson FROM symbols WHERE id=?").get(symbolCall.callee_symbol_id);
3581
+ const callee = db.prepare("SELECT evidence_json evidenceJson,source_file sourceFile,start_line startLine FROM symbols WHERE id=?").get(symbolCall.callee_symbol_id);
3208
3582
  const calleeEvidence = parseEvidence(callee?.evidenceJson);
3209
3583
  const params = Array.isArray(calleeEvidence.parameters) ? calleeEvidence.parameters.filter((item) => typeof item === "string") : [];
3210
3584
  const parameterBindings = Array.isArray(calleeEvidence.parameterBindings) ? calleeEvidence.parameterBindings.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
3211
3585
  const parameterPropertyAliases = Array.isArray(calleeEvidence.parameterPropertyAliases) ? calleeEvidence.parameterPropertyAliases.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
3212
3586
  const args = Array.isArray(callEvidence2.callArguments) ? callEvidence2.callArguments : [];
3587
+ const provenance = {
3588
+ callerSite: { sourceFile: String(symbolCall.source_file ?? ""), sourceLine: Number(symbolCall.source_line ?? 0) },
3589
+ calleeSite: { sourceFile: callee?.sourceFile, sourceLine: callee?.startLine }
3590
+ };
3213
3591
  args.forEach((arg, index) => {
3214
3592
  const paramBinding = parameterBindings.find((binding) => binding.index === index);
3215
3593
  const param = paramBinding?.kind === "identifier" && typeof paramBinding.name === "string" ? paramBinding.name : params[index];
3216
3594
  if (arg.kind === "identifier" && typeof arg.name === "string") {
3217
3595
  const binding = callerBindings.get(arg.name);
3218
- if (binding && param) next.set(param, { ...binding, source: "local_symbol_argument", callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
3596
+ if (binding && param) next.set(param, { ...binding, ...provenance, source: "local_symbol_argument", callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
3219
3597
  }
3220
3598
  if (arg.kind === "object_literal" && Array.isArray(arg.properties)) {
3221
3599
  for (const prop of arg.properties) {
@@ -3223,15 +3601,23 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
3223
3601
  const binding = callerBindings.get(prop.argument);
3224
3602
  if (!binding) continue;
3225
3603
  const destructured = paramBinding?.kind === "object_pattern" && Array.isArray(paramBinding.properties) ? paramBinding.properties.find((item) => item.property === prop.property && typeof item.local === "string") : void 0;
3226
- if (destructured && typeof destructured.local === "string") next.set(destructured.local, { ...binding, source: "local_symbol_destructured_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: String(index), calleeReceiver: destructured.local });
3604
+ 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 });
3227
3605
  else if (param) {
3228
- next.set(`${param}.${prop.property}`, { ...binding, source: "local_symbol_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
3606
+ next.set(`${param}.${prop.property}`, { ...binding, ...provenance, source: "local_symbol_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
3229
3607
  for (const alias of parameterPropertyAliases) {
3230
- if (alias.parameter === param && alias.property === prop.property && typeof alias.local === "string") next.set(alias.local, { ...binding, 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 });
3608
+ 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 });
3231
3609
  }
3232
3610
  }
3233
3611
  }
3234
3612
  }
3613
+ if (arg.kind === "array_literal" && Array.isArray(arg.elements) && paramBinding?.kind === "array_pattern" && Array.isArray(paramBinding.elements)) {
3614
+ for (const element of arg.elements) {
3615
+ const target = paramBinding.elements.find((item) => item.index === element.index);
3616
+ if (element.kind !== "identifier" || typeof element.name !== "string" || typeof target?.local !== "string") continue;
3617
+ const binding = callerBindings.get(element.name);
3618
+ if (binding) next.set(target.local, { ...binding, ...provenance, source: "local_symbol_destructured_array_argument", callerArgument: element.name, calleeParameter: String(index), calleeReceiver: target.local });
3619
+ }
3620
+ }
3235
3621
  });
3236
3622
  return next;
3237
3623
  }
@@ -3242,7 +3628,7 @@ function contextualRuntimeResolution(db, call, binding, workspaceId, persistedRo
3242
3628
  const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
3243
3629
  const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
3244
3630
  const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
3245
- const evidence = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine, alias: binding.alias, aliasExpr: binding.aliasExpr, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination }, operationPath: op, rawOperationPath: normalized?.rawOperationPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : void 0, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: resolution.candidates.length, candidates: resolution.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
3631
+ const evidence = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, callerSite: binding.callerSite, calleeSite: binding.calleeSite, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine, alias: binding.alias, aliasExpr: binding.aliasExpr, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination }, operationPath: op, rawOperationPath: normalized?.rawOperationPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : void 0, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: resolution.candidates.length, candidates: resolution.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
3246
3632
  if (!resolution.target) return { evidence, unresolvedReason: resolution.status === "ambiguous" ? "Ambiguous contextual operation candidates" : resolution.status === "dynamic" ? `Dynamic contextual target is missing runtime variables: ${resolution.reasons.join(", ")}` : "No contextual operation candidate matched" };
3247
3633
  const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
3248
3634
  const persistedResolved = persistedRows.find((item) => item.status === "resolved");
@@ -3250,7 +3636,8 @@ function contextualRuntimeResolution(db, call, binding, workspaceId, persistedRo
3250
3636
  return { row: { id: -Number(call.id), edge_type: "REMOTE_CALL_RESOLVES_TO_OPERATION", from_id: String(call.id), to_kind: "operation", to_id: String(resolution.target.operationId), confidence: resolution.target.score, evidence_json: JSON.stringify(resolvedEvidence), status: "resolved" }, evidence: resolvedEvidence, unresolvedReason: void 0 };
3251
3637
  }
3252
3638
  function trace(db, start, options) {
3253
- const scope = startScope(db, start, options.implementationRepo);
3639
+ const hintOptions = { implementationRepo: options.implementationRepo, implementationHints: options.implementationHints };
3640
+ const scope = startScope(db, start, hintOptions);
3254
3641
  const diagnostics = db.prepare(
3255
3642
  "SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)"
3256
3643
  ).all(scope.repo?.id, scope.repo?.id);
@@ -3273,7 +3660,7 @@ function trace(db, start, options) {
3273
3660
  const op = operationNode(db, scope.startOperationId);
3274
3661
  const impl = implementationScope(db, scope.startOperationId);
3275
3662
  if (op) nodes.set(String(op.id), op);
3276
- const startSelection = implementationMethodIdFromHint(impl.edge, options.implementationRepo);
3663
+ const startSelection = implementationMethodIdFromHint(impl.edge, hintOptions);
3277
3664
  if (impl.edge && (impl.edge.status === "resolved" || startSelection.methodId)) {
3278
3665
  const selectedMethodId = impl.edge.status === "resolved" ? impl.edge.to_id : startSelection.methodId;
3279
3666
  const implEvidence = { ...parseEvidence(impl.edge.evidence_json), startResolution: { strategy: "indexed_operation_graph", matchedOperationId: scope.startOperationId, implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, selectedHandlerMethodId: selectedMethodId }, implementationSelection: startSelection.methodId ? startSelection.evidence : void 0 };
@@ -3360,11 +3747,13 @@ function trace(db, start, options) {
3360
3747
  });
3361
3748
  if (effectiveRow.to_kind === "operation") {
3362
3749
  const implementation = implementationScope(db, effectiveRow.to_id);
3363
- const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence, options.implementationRepo);
3750
+ const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence, hintOptions);
3364
3751
  const contextMethodId = contextSelection.methodId;
3365
3752
  const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : void 0;
3366
3753
  if (implementation.edge) {
3367
3754
  const implEvidence = JSON.parse(String(implementation.edge.evidence_json || "{}"));
3755
+ const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence.implementationHintSuggestions);
3756
+ if (hintDiagnostic) diagnostics.unshift(hintDiagnostic);
3368
3757
  const handlerNode = implementation.edge.status === "resolved" ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
3369
3758
  const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
3370
3759
  if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
@@ -3373,7 +3762,7 @@ function trace(db, start, options) {
3373
3762
  type: "operation_implemented_by_handler",
3374
3763
  from: to,
3375
3764
  to: implTo,
3376
- evidence: contextMethodId ? { ...implEvidence, contextualImplementationSelected: contextSelection.evidence.strategy !== "implementation_repo_hint", contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence } : { ...implEvidence, contextualImplementation: contextSelection.evidence },
3765
+ evidence: contextMethodId ? { ...implEvidence, contextualImplementationSelected: contextSelection.evidence.strategy !== "implementation_repo_hint", contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence } : { ...implEvidence, contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence },
3377
3766
  confidence: Number(implementation.edge.confidence ?? 0),
3378
3767
  unresolvedReason: implementation.edge.status === "resolved" || contextMethodId ? void 0 : String(implementation.edge.unresolved_reason ?? implementation.edge.status)
3379
3768
  });
@@ -3426,13 +3815,15 @@ export {
3426
3815
  redactValue,
3427
3816
  normalizeODataOperationInvocationPath,
3428
3817
  classifyODataPathIntent,
3818
+ parseServiceBindings,
3429
3819
  containsSupportedOutboundCall,
3430
3820
  parseOutboundCalls,
3431
- parseServiceBindings,
3432
3821
  applyVariables,
3433
3822
  extractPlaceholders,
3434
3823
  substituteVariables,
3824
+ parseImplementationHint,
3825
+ implementationHintSuggestions,
3435
3826
  linkWorkspace,
3436
3827
  trace
3437
3828
  };
3438
- //# sourceMappingURL=chunk-UKNPHTUS.js.map
3829
+ //# sourceMappingURL=chunk-EGY2A4AT.js.map