@saptools/service-flow 0.1.27 → 0.1.29

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.29
4
+
5
+ - Classify GET OData entity/query paths with query strings, filter functions, key predicates, navigation reads, and query placeholders as terminal remote query/entity edges when no strong indexed CDS operation candidate resolves them.
6
+ - Preserve raw path, entity segment, query-string presence, query placeholders, method, and classifier reason in link evidence without creating dynamic operation edges from query parameters.
7
+ - Keep balanced top-level OData action/function invocation normalization for real operation imports while avoiding truncation at parentheses inside query strings.
8
+ - Document the conservative entity-query versus operation-invocation distinction for neutral CAP service-client calls.
9
+
10
+ ## 0.1.28
11
+
12
+ - Propagate contextual service-client bindings through one-level object-parameter destructuring aliases, including renamed and assignment destructuring in neutral CAP helpers.
13
+ - Prefer caller-site higher-order wrapper remote-action evidence for literal wrapper paths while keeping dynamic wrapper path diagnostics on the caller edge.
14
+ - Refine contextual implementation selection evidence for duplicate helper candidates and report structured duplicate-candidate ties when selection is unsafe.
15
+ - Fix strict doctor contextual opportunity metrics so aggregate totals, capped examples, and actionable severity remain internally consistent.
16
+
3
17
  ## 0.1.27
4
18
 
5
19
  - Enrich trace-time contextual service-client bindings from package-level CAP `cds.requires` aliases so helper-internal sends resolve through require-derived service paths and destinations.
package/README.md CHANGED
@@ -419,7 +419,7 @@ Run `service-flow index` after source, CDS, package metadata, or helper-package
419
419
  <summary><b>Why is an expected call unresolved?</b></summary>
420
420
 
421
421
  Check `service-flow doctor`, then inspect the facts with `service-flow list services`, `service-flow list operations`, and `service-flow list calls`. Dynamic destinations may need `--var key=value`; the key is the full trimmed expression inside `${...}` and is matched literally without JavaScript evaluation. Operation-path-only ambiguous remote actions usually mean the call had no service binding id; same-file identity aliases are propagated, while property/call-expression aliases and ambiguous wrapper flows remain conservative; inspect `list calls`, `inspect operation`, and `doctor --strict` to determine whether helper-return propagation or wrapper support is missing. Contextual implementation selection only continues into a handler when static evidence such as caller repository, resolved service path, destination/alias expression, dependency edges, registration package, and local service ownership makes exactly one candidate stronger; ties remain ambiguous with reasons.
422
- Default doctor output is intended to focus on actionable indexing or trace-impacting issues; use `--strict` when you need exhaustive model-shape diagnostics for entity-only or extension-heavy CDS models. Strict mode also reports normalized OData invocation ambiguity, remote-action target quality, likely missed identity aliases, no-binding remote actions, contextual implementation stops, and wrapper dynamic-path candidates, including whether unresolved unknown/dynamic paths are semantic instead of numeric call ids.
422
+ Default doctor output is intended to focus on actionable indexing or trace-impacting issues; use `--strict` when you need exhaustive model-shape diagnostics for entity-only or extension-heavy CDS models. Strict mode also reports normalized OData invocation ambiguity, remote-action target quality, likely missed identity aliases, no-binding remote actions, contextual implementation stops, and wrapper dynamic-path candidates, including whether unresolved unknown/dynamic paths are semantic instead of numeric call ids. GET OData entity/query reads such as `/Books?$filter=contains(title,'A')`, `/Books(ID='1000')`, and navigation queries are terminal remote query/entity edges unless strong indexed CDS operation evidence resolves them; placeholders inside query strings are preserved as query evidence rather than dynamic operation selectors.
423
423
 
424
424
  </details>
425
425
 
package/TECHNICAL-NOTE.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Service Flow Resolution Notes
2
2
 
3
+ ## 0.1.29 OData entity-query intent notes
4
+
5
+ - Service-client `GET` paths with OData collection queries, filter/search functions in the query string, entity key predicates, navigation reads, or query-string placeholders are classified conservatively as terminal remote entity/query reads when no strong indexed CDS operation candidate resolves the path. Examples include `/Books?$filter=contains(title,'A')`, `/Books(ID='1000')`, and `/Authors('A1')/books?$select=ID`.
6
+ - Query-string placeholders are recorded as query evidence and do not make the operation target dynamic. Runtime variables in service paths, destinations, aliases, and true operation paths still keep the existing dynamic-edge behavior.
7
+ - OData invocation normalization no longer truncates paths at parentheses that appear inside query strings. Balanced top-level operation imports such as `/calculateScore(input='A')` continue to normalize and resolve against indexed CDS functions/actions when service evidence is strong.
8
+
3
9
  ## 0.1.22 alias, wrapper, contextual trace, and strict doctor notes
4
10
 
5
11
  - Same-file service-client identity aliases now create their own service binding rows when the right-hand side is a known connected client variable. The parser accepts direct identifier aliases plus typed, `as`, and `satisfies` forms, supports source-order transitive aliases, and records `aliasKind: identity` helper-chain evidence. It still does not infer aliases from property reads, indexed access, function calls, object metadata, or cross-scope guesses.
@@ -521,6 +521,96 @@ function summarizeExpression(text) {
521
521
  return redactText(text).slice(0, 240);
522
522
  }
523
523
 
524
+ // src/linker/odata-path-normalizer.ts
525
+ function normalizeODataOperationInvocationPath(path9) {
526
+ if (path9 === void 0) return void 0;
527
+ const raw = path9.trim();
528
+ if (!raw) return void 0;
529
+ const open = raw.indexOf("(");
530
+ if (open < 0) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
531
+ const query = raw.indexOf("?");
532
+ if (query >= 0) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
533
+ if (!raw.startsWith("/") || raw.slice(1, open).includes("/")) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
534
+ const close = matchingClose(raw, open);
535
+ if (close === void 0 || raw.slice(close + 1).trim().length > 0) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
536
+ const operationSegment = raw.slice(0, open).trim();
537
+ if (operationSegment.length <= 1) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
538
+ return { rawOperationPath: raw, normalizedOperationPath: operationSegment, wasInvocation: true };
539
+ }
540
+ function classifyODataPathIntent(path9, method) {
541
+ const rawPath = (path9 ?? "").trim();
542
+ const normalizedMethod = (method ?? "GET").trim().toUpperCase() || "GET";
543
+ const queryIndex = rawPath.indexOf("?");
544
+ const pathWithoutQuery = queryIndex >= 0 ? rawPath.slice(0, queryIndex) : rawPath;
545
+ const queryString = queryIndex >= 0 ? rawPath.slice(queryIndex + 1) : void 0;
546
+ const segments = pathWithoutQuery.replace(/^\//, "").split("/").filter(Boolean);
547
+ const firstSegment = segments[0] ?? "";
548
+ const hasNavigationSegments = segments.length > 1;
549
+ const entitySegment = entitySegmentFromPath(pathWithoutQuery);
550
+ const placeholderKeys = [...new Set(extractTemplatePlaceholders(rawPath))];
551
+ const base = { rawPath, method: normalizedMethod, pathWithoutQuery, queryString, hasQueryString: queryIndex >= 0, entitySegment, placeholderKeys };
552
+ if (!rawPath || !rawPath.startsWith("/")) return { ...base, kind: "unknown", reason: "path_missing_or_not_absolute" };
553
+ if (normalizedMethod !== "GET") return { ...base, kind: "operation_invocation", reason: "non_get_service_send_defaults_to_operation" };
554
+ if (queryIndex >= 0) {
555
+ if (hasNavigationSegments) return { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_and_query_string" };
556
+ if (looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "unknown", reason: "get_invocation_with_query_string_requires_indexed_operation_evidence" };
557
+ return { ...base, kind: "entity_query", reason: "get_collection_path_has_query_string" };
558
+ }
559
+ if (hasNavigationSegments) return { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_segments" };
560
+ if (firstSegment.includes("(")) {
561
+ 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" };
562
+ }
563
+ return { ...base, kind: "unknown", reason: "get_path_has_no_query_key_or_navigation_signal" };
564
+ }
565
+ function entitySegmentFromPath(path9) {
566
+ const first = path9.replace(/^\//, "").split("/")[0]?.trim();
567
+ if (!first) return void 0;
568
+ const open = first.indexOf("(");
569
+ const entity = (open >= 0 ? first.slice(0, open) : first).trim();
570
+ return entity || void 0;
571
+ }
572
+ function looksLikeLowerCamelInvocation(segment) {
573
+ const open = segment.indexOf("(");
574
+ if (open <= 0) return false;
575
+ const name = segment.slice(0, open).split(".").at(-1) ?? segment.slice(0, open);
576
+ return /^[a-z][A-Za-z0-9_]*$/.test(name);
577
+ }
578
+ function extractTemplatePlaceholders(text) {
579
+ return [...text.matchAll(/\$\{([^}]*)\}/g)].map((match) => (match[1] ?? "").trim()).filter(Boolean);
580
+ }
581
+ function matchingClose(text, openIndex) {
582
+ let depth = 0;
583
+ let quote;
584
+ for (let index = openIndex; index < text.length; index += 1) {
585
+ const char = text[index];
586
+ const prev = text[index - 1];
587
+ if (quote) {
588
+ if (prev === "\\") continue;
589
+ if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
590
+ continue;
591
+ }
592
+ if (char === "'") {
593
+ quote = "single";
594
+ continue;
595
+ }
596
+ if (char === '"') {
597
+ quote = "double";
598
+ continue;
599
+ }
600
+ if (char === "`") {
601
+ quote = "template";
602
+ continue;
603
+ }
604
+ if (char === "(") depth += 1;
605
+ if (char === ")") {
606
+ depth -= 1;
607
+ if (depth === 0) return index;
608
+ if (depth < 0) return void 0;
609
+ }
610
+ }
611
+ return void 0;
612
+ }
613
+
524
614
  // src/parsers/outbound-call-parser.ts
525
615
  import fs6 from "fs/promises";
526
616
  import path7 from "path";
@@ -642,7 +732,8 @@ function collectWrapperSpecs(source) {
642
732
  const specs = /* @__PURE__ */ new Map();
643
733
  const scanFunction = (name, fn) => {
644
734
  const params = fn.parameters.map((param) => ts4.isIdentifier(param.name) ? param.name.text : void 0);
645
- const closure = returnedClosure(fn) ?? fn;
735
+ const closure = returnedClosure(fn);
736
+ if (!closure) return;
646
737
  const sends = [];
647
738
  const visit = (node) => {
648
739
  if (ts4.isCallExpression(node) && ts4.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send" && ts4.isIdentifier(node.expression.expression)) {
@@ -660,7 +751,7 @@ function collectWrapperSpecs(source) {
660
751
  const clientIndex = params.indexOf(found.client);
661
752
  const pathIndex = params.indexOf(found.path);
662
753
  const methodIndex = found.method && params.includes(found.method) ? params.indexOf(found.method) : void 0;
663
- if (clientIndex >= 0 && pathIndex >= 0) specs.set(name, { clientIndex, pathIndex, methodIndex, definitionLine: lineOf3(source.text, fn.getStart(source)) });
754
+ if (clientIndex >= 0 && pathIndex >= 0) specs.set(name, { clientIndex, pathIndex, methodIndex, definitionLine: lineOf3(source.text, fn.getStart(source)), internalStart: closure.getStart(source), internalEnd: closure.getEnd() });
664
755
  };
665
756
  for (const stmt of source.statements) {
666
757
  if (ts4.isFunctionDeclaration(stmt) && stmt.name) scanFunction(stmt.name.text, stmt);
@@ -686,11 +777,15 @@ function classifyOutboundCallsInSource(source, filePath) {
686
777
  const initializers = variableInitializers(source);
687
778
  const serviceVariables = collectServiceVariables(source);
688
779
  const wrapperSpecs = collectWrapperSpecs(source);
780
+ const wrapperInternalRanges = [...wrapperSpecs.values()].map((spec) => ({ start: spec.internalStart, end: spec.internalEnd }));
689
781
  const add = (node, fact, extra) => {
690
782
  calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf3(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
691
783
  };
692
784
  const visit = (node) => {
693
785
  if (ts4.isCallExpression(node)) {
786
+ if (wrapperInternalRanges.some((range) => node.getStart(source) >= range.start && node.getEnd() <= range.end)) {
787
+ return;
788
+ }
694
789
  const expr = node.expression;
695
790
  const exprText = expr.getText(source);
696
791
  if (exprText === "cds.run") {
@@ -703,9 +798,13 @@ function classifyOutboundCallsInSource(source, filePath) {
703
798
  if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
704
799
  const receiver = receiverName(expr.expression);
705
800
  const query = objectPropertyText(objectArg, "query");
801
+ const method = stripQuotes(objectPropertyText(objectArg, "method") ?? "POST");
706
802
  const op = objectPropertyText(objectArg, "path") ?? objectPropertyText(objectArg, "event");
707
803
  const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
708
- add(node, { callType: query ? "remote_query" : "remote_action", serviceVariableName: receiver, method: stripQuotes(objectPropertyText(objectArg, "method") ?? "POST"), operationPathExpr: op && !shorthandPath ? `/${stripQuotes(op).replace(/^\//, "")}` : void 0, queryEntity: query ? extractQueryEntity(query) : void 0, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason: !query && shorthandPath ? "dynamic_operation_path_identifier" : void 0 }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, parserWarning: shorthandPath ? "dynamic_operation_path_identifier" : void 0 });
804
+ const operationPathExpr = op && !shorthandPath ? `/${stripQuotes(op).replace(/^\//, "")}` : void 0;
805
+ const intent = classifyODataPathIntent(operationPathExpr, method);
806
+ const isODataQueryRead = method.toUpperCase() === "GET" && ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
807
+ add(node, { callType: query || 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 && shorthandPath ? "dynamic_operation_path_identifier" : void 0 }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, odataPathIntent: operationPathExpr ? intent : void 0, parserWarning: shorthandPath ? "dynamic_operation_path_identifier" : void 0 });
709
808
  }
710
809
  } else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts4.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
711
810
  const wrapperName = ts4.isIdentifier(expr) ? expr.text : ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) ? expr.expression.text : "";
@@ -716,6 +815,8 @@ function classifyOutboundCallsInSource(source, filePath) {
716
815
  const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
717
816
  if (spec && clientArg && ts4.isIdentifier(clientArg) && isStringLike(pathArg)) {
718
817
  add(node, { callType: "remote_action", serviceVariableName: clientArg.text, method: stripQuotes(methodArg?.getText(source) ?? "POST"), operationPathExpr: `/${pathArg.text.replace(/^\//, "")}`, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75 }, { receiver: clientArg.text, classifier: "higher_order_wrapper_literal_path", wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, literalCallerArgumentDetected: true });
818
+ } else if (spec && clientArg && ts4.isIdentifier(clientArg)) {
819
+ add(node, { callType: "remote_action", serviceVariableName: clientArg.text, method: stripQuotes(methodArg?.getText(source) ?? "POST"), payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason: "dynamic_operation_path_identifier" }, { receiver: clientArg.text, classifier: "higher_order_wrapper_dynamic_path", wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, parserWarning: "dynamic_operation_path_identifier" });
719
820
  }
720
821
  } else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
721
822
  const receiver = receiverName(expr.expression);
@@ -1426,53 +1527,6 @@ function substituteVariables(template, vars) {
1426
1527
  };
1427
1528
  }
1428
1529
 
1429
- // src/linker/odata-path-normalizer.ts
1430
- function normalizeODataOperationInvocationPath(path9) {
1431
- if (path9 === void 0) return void 0;
1432
- const raw = path9.trim();
1433
- if (!raw) return void 0;
1434
- const open = raw.indexOf("(");
1435
- if (open < 0) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
1436
- if (!raw.startsWith("/") || raw.slice(1, open).includes("/")) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
1437
- const close = matchingClose(raw, open);
1438
- if (close === void 0 || raw.slice(close + 1).trim().length > 0) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
1439
- const operationSegment = raw.slice(0, open).trim();
1440
- if (operationSegment.length <= 1) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
1441
- return { rawOperationPath: raw, normalizedOperationPath: operationSegment, wasInvocation: true };
1442
- }
1443
- function matchingClose(text, openIndex) {
1444
- let depth = 0;
1445
- let quote;
1446
- for (let index = openIndex; index < text.length; index += 1) {
1447
- const char = text[index];
1448
- const prev = text[index - 1];
1449
- if (quote) {
1450
- if (prev === "\\") continue;
1451
- if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
1452
- continue;
1453
- }
1454
- if (char === "'") {
1455
- quote = "single";
1456
- continue;
1457
- }
1458
- if (char === '"') {
1459
- quote = "double";
1460
- continue;
1461
- }
1462
- if (char === "`") {
1463
- quote = "template";
1464
- continue;
1465
- }
1466
- if (char === "(") depth += 1;
1467
- if (char === ")") {
1468
- depth -= 1;
1469
- if (depth === 0) return index;
1470
- if (depth < 0) return void 0;
1471
- }
1472
- }
1473
- return void 0;
1474
- }
1475
-
1476
1530
  // src/linker/remote-query-target.ts
1477
1531
  function buildRemoteQueryTarget(input) {
1478
1532
  const entity = typeof input.queryEntity === "string" && input.queryEntity.trim() ? input.queryEntity.trim() : void 0;
@@ -1744,15 +1798,18 @@ function linkCalls(db, workspaceId, vars, generation) {
1744
1798
  function insertCallEdge(db, workspaceId, call, vars, generation) {
1745
1799
  const callType = String(call.call_type);
1746
1800
  const rawOp = applyVariables(String(call.operation_path_expr ?? ""), vars);
1747
- const normalized = normalizeODataOperationInvocationPath(rawOp);
1748
- const op = normalized?.normalizedOperationPath ?? rawOp;
1801
+ const intent = classifyODataPathIntent(rawOp, call.method);
1802
+ const isEntityQueryIntent = ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
1803
+ const resolutionRawOp = callType === "remote_query" && isEntityQueryIntent ? intent.pathWithoutQuery : rawOp;
1804
+ const normalized = normalizeODataOperationInvocationPath(resolutionRawOp);
1805
+ const op = normalized?.normalizedOperationPath ?? resolutionRawOp;
1749
1806
  const servicePath = applyVariables(call.servicePathExpr ?? call.requireServicePath, vars);
1750
1807
  const destination = call.destinationExpr ?? call.requireDestination;
1751
1808
  const isDynamic = Boolean(Number(call.isDynamic ?? 0));
1752
1809
  const isOperationCall = callType === "remote_action" || callType === "local_service_call" || callType === "remote_query" && Boolean(op);
1753
1810
  const resolution = isOperationCall ? resolveOperation(db, { servicePath, operationPath: op, serviceName: call.local_service_name, repoId: callType === "local_service_call" ? Number(call.repo_id) : void 0, alias: applyVariables(call.aliasExpr ?? call.alias, vars), destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 || callType === "local_service_call" }, workspaceId) : { status: "unresolved", candidates: [], reasons: [] };
1754
- const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0, normalized);
1755
- if (callType === "remote_query" && !op) {
1811
+ const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0, normalized, intent);
1812
+ if (callType === "remote_query" && (isEntityQueryIntent || !op) && !resolution.target) {
1756
1813
  const target = buildRemoteQueryTarget({ queryEntity: call.query_entity, servicePath, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, parserWarning: evidence.parserWarning });
1757
1814
  db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "HANDLER_RUNS_REMOTE_QUERY", "terminal", "call", String(call.id), target.toKind, target.toId, Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, ...target.evidence }), 0, generation);
1758
1815
  return { status: "terminal", callType };
@@ -1794,9 +1851,9 @@ function objectJson(value) {
1794
1851
  const parsed = parseJson(value);
1795
1852
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
1796
1853
  }
1797
- function callEvidence(call, resolution, servicePath, op, destination, normalized) {
1854
+ function callEvidence(call, resolution, servicePath, op, destination, normalized, odataPathIntent) {
1798
1855
  const bindingHasDynamicExpression = Boolean(Number(call.isDynamic ?? 0));
1799
- 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 : void 0, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, 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, 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 };
1856
+ 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, 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 };
1800
1857
  }
1801
1858
  function linkImplementations(db, workspaceId, generation) {
1802
1859
  const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,s.service_path servicePath,s.repo_id modelRepoId,r.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);
@@ -2153,7 +2210,7 @@ function implementationScope(db, operationId) {
2153
2210
  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);
2154
2211
  return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
2155
2212
  }
2156
- function contextImplementationMethodId(edge, callerRepoId) {
2213
+ function contextImplementationMethodId(edge, callerRepoId, remoteEvidence = {}) {
2157
2214
  if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return { evidence: { status: "not_applicable" } };
2158
2215
  const evidence = JSON.parse(String(edge.evidence_json || "{}"));
2159
2216
  const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
@@ -2167,12 +2224,16 @@ function contextImplementationMethodId(edge, callerRepoId) {
2167
2224
  score += 10;
2168
2225
  reasons.push("registration_package_matches_caller_repository");
2169
2226
  }
2227
+ if (typeof remoteEvidence.effectiveServicePath === "string" || typeof remoteEvidence.effectiveDestination === "string" || typeof remoteEvidence.effectiveAlias === "string") {
2228
+ score += 1;
2229
+ reasons.push("remote_call_context_available");
2230
+ }
2170
2231
  return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
2171
2232
  }).sort((a, b) => b.score - a.score);
2172
2233
  if (scores.length === 0) return { evidence: { status: "not_applicable", candidateScores: [] } };
2173
2234
  const [first, second] = scores;
2174
2235
  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 } };
2175
- return { evidence: { status: "tied", tieReason: "no_unique_materially_stronger_candidate", candidateScores: scores } };
2236
+ return { evidence: { status: "tied", tieReason: scores.length > 1 ? "duplicate_helper_implementation_candidates" : "no_unique_materially_stronger_candidate", candidateScores: scores } };
2176
2237
  }
2177
2238
  function handlerScope(db, methodId) {
2178
2239
  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);
@@ -2312,6 +2373,7 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
2312
2373
  const calleeEvidence = parseEvidence(callee?.evidenceJson);
2313
2374
  const params = Array.isArray(calleeEvidence.parameters) ? calleeEvidence.parameters.filter((item) => typeof item === "string") : [];
2314
2375
  const parameterBindings = Array.isArray(calleeEvidence.parameterBindings) ? calleeEvidence.parameterBindings.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
2376
+ const parameterPropertyAliases = Array.isArray(calleeEvidence.parameterPropertyAliases) ? calleeEvidence.parameterPropertyAliases.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
2315
2377
  const args = Array.isArray(callEvidence2.callArguments) ? callEvidence2.callArguments : [];
2316
2378
  args.forEach((arg, index) => {
2317
2379
  const paramBinding = parameterBindings.find((binding) => binding.index === index);
@@ -2327,7 +2389,12 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
2327
2389
  if (!binding) continue;
2328
2390
  const destructured = paramBinding?.kind === "object_pattern" && Array.isArray(paramBinding.properties) ? paramBinding.properties.find((item) => item.property === prop.property && typeof item.local === "string") : void 0;
2329
2391
  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 });
2330
- else if (param) next.set(`${param}.${prop.property}`, { ...binding, source: "local_symbol_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
2392
+ else if (param) {
2393
+ next.set(`${param}.${prop.property}`, { ...binding, source: "local_symbol_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
2394
+ for (const alias of parameterPropertyAliases) {
2395
+ 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 });
2396
+ }
2397
+ }
2331
2398
  }
2332
2399
  }
2333
2400
  });
@@ -2475,7 +2542,7 @@ function trace(db, start, options) {
2475
2542
  });
2476
2543
  if (effectiveRow.to_kind === "operation") {
2477
2544
  const implementation = implementationScope(db, effectiveRow.to_id);
2478
- const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId);
2545
+ const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence);
2479
2546
  const contextMethodId = contextSelection.methodId;
2480
2547
  const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : void 0;
2481
2548
  if (implementation.edge) {
@@ -2537,14 +2604,14 @@ export {
2537
2604
  parseHandlerRegistrations,
2538
2605
  redactText,
2539
2606
  redactValue,
2607
+ normalizeODataOperationInvocationPath,
2540
2608
  containsSupportedOutboundCall,
2541
2609
  parseOutboundCalls,
2542
2610
  parseServiceBindings,
2543
2611
  applyVariables,
2544
2612
  extractPlaceholders,
2545
2613
  substituteVariables,
2546
- normalizeODataOperationInvocationPath,
2547
2614
  linkWorkspace,
2548
2615
  trace
2549
2616
  };
2550
- //# sourceMappingURL=chunk-VDJTNOEI.js.map
2617
+ //# sourceMappingURL=chunk-GOE6ZK6I.js.map