@saptools/service-flow 0.1.28 → 0.1.30

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.30
4
+
5
+ - Normalize balanced OData operation invocations when multiline template placeholders appear inside function/action argument lists.
6
+ - Preserve invocation argument placeholders as non-routing evidence instead of treating them as missing operation-target runtime variables.
7
+ - Reuse the shared OData invocation normalizer during contextual trace resolution so persisted links and trace-time helper propagation handle the same path shapes.
8
+ - Keep GET entity key reads, navigation reads, and collection queries terminal unless strong indexed operation evidence resolves them.
9
+
10
+ ## 0.1.29
11
+
12
+ - 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.
13
+ - 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.
14
+ - Keep balanced top-level OData action/function invocation normalization for real operation imports while avoiding truncation at parentheses inside query strings.
15
+ - Document the conservative entity-query versus operation-invocation distinction for neutral CAP service-client calls.
16
+
3
17
  ## 0.1.28
4
18
 
5
19
  - Propagate contextual service-client bindings through one-level object-parameter destructuring aliases, including renamed and assignment destructuring in neutral CAP helpers.
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,18 @@
1
1
  # Service Flow Resolution Notes
2
2
 
3
+ ## 0.1.30 OData invocation argument placeholder notes
4
+
5
+ - Balanced top-level OData function/action imports such as `/readDetails(ID='${id}',version=0)` now normalize to the operation segment even when placeholder expressions span multiple lines or contain nested JavaScript parentheses.
6
+ - Placeholders inside the invocation argument list are recorded as `invocationArgumentPlaceholderKeys` evidence. They are not route selectors and do not produce `missing_variable:*` operation-target diagnostics after a stable operation segment has been identified.
7
+ - Runtime `--var` substitution still applies to actual route selectors: service path, destination, alias, and dynamic operation path segments. GET entity key reads, navigation reads, and query-string placeholders remain conservative terminal remote query/entity evidence unless strong indexed operation evidence resolves them.
8
+ - Trace-time contextual service-client resolution uses the same shared OData invocation normalizer as persisted linking, keeping helper-propagated calls consistent with full graph linking.
9
+
10
+ ## 0.1.29 OData entity-query intent notes
11
+
12
+ - 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`.
13
+ - 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.
14
+ - 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.
15
+
3
16
  ## 0.1.22 alias, wrapper, contextual trace, and strict doctor notes
4
17
 
5
18
  - 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,201 @@ 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 rejected = (reason) => ({ rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false, invocationArgumentPlaceholderKeys: [], normalizationRejectedReason: reason });
530
+ const open = raw.indexOf("(");
531
+ if (open < 0) return rejected("no_top_level_parenthesis");
532
+ const query = topLevelQueryIndex(raw);
533
+ if (query >= 0) return rejected("query_string_paths_are_not_operation_invocations");
534
+ if (!raw.startsWith("/")) return rejected("path_is_not_absolute");
535
+ if (raw.slice(1, open).includes("/")) return rejected("operation_segment_contains_navigation_separator");
536
+ const close = matchingClose(raw, open);
537
+ if (close === void 0) return rejected("top_level_invocation_parenthesis_is_unbalanced");
538
+ if (raw.slice(close + 1).trim().length > 0) return rejected("top_level_invocation_does_not_cover_remaining_path");
539
+ const operationSegment = raw.slice(0, open).trim();
540
+ if (operationSegment.length <= 1) return rejected("operation_segment_is_empty");
541
+ return {
542
+ rawOperationPath: raw,
543
+ normalizedOperationPath: operationSegment,
544
+ wasInvocation: true,
545
+ invocationArgumentPlaceholderKeys: [...new Set(extractTemplatePlaceholders(raw.slice(open + 1, close)))],
546
+ normalizationReason: "balanced_top_level_operation_invocation"
547
+ };
548
+ }
549
+ function classifyODataPathIntent(path9, method) {
550
+ const rawPath = (path9 ?? "").trim();
551
+ const normalizedMethod = (method ?? "GET").trim().toUpperCase() || "GET";
552
+ const queryIndex = rawPath.indexOf("?");
553
+ const pathWithoutQuery = queryIndex >= 0 ? rawPath.slice(0, queryIndex) : rawPath;
554
+ const queryString = queryIndex >= 0 ? rawPath.slice(queryIndex + 1) : void 0;
555
+ const segments = pathWithoutQuery.replace(/^\//, "").split("/").filter(Boolean);
556
+ const firstSegment = segments[0] ?? "";
557
+ const hasNavigationSegments = segments.length > 1;
558
+ const entitySegment = entitySegmentFromPath(pathWithoutQuery);
559
+ const placeholderKeys = [...new Set(extractTemplatePlaceholders(rawPath))];
560
+ const base = { rawPath, method: normalizedMethod, pathWithoutQuery, queryString, hasQueryString: queryIndex >= 0, entitySegment, placeholderKeys };
561
+ if (!rawPath || !rawPath.startsWith("/")) return { ...base, kind: "unknown", reason: "path_missing_or_not_absolute" };
562
+ if (normalizedMethod !== "GET") return { ...base, kind: "operation_invocation", reason: "non_get_service_send_defaults_to_operation" };
563
+ if (queryIndex >= 0) {
564
+ if (hasNavigationSegments) return { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_and_query_string" };
565
+ if (looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "unknown", reason: "get_invocation_with_query_string_requires_indexed_operation_evidence" };
566
+ return { ...base, kind: "entity_query", reason: "get_collection_path_has_query_string" };
567
+ }
568
+ if (hasNavigationSegments) return { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_segments" };
569
+ if (firstSegment.includes("(")) {
570
+ 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" };
571
+ }
572
+ return { ...base, kind: "unknown", reason: "get_path_has_no_query_key_or_navigation_signal" };
573
+ }
574
+ function entitySegmentFromPath(path9) {
575
+ const first = path9.replace(/^\//, "").split("/")[0]?.trim();
576
+ if (!first) return void 0;
577
+ const open = first.indexOf("(");
578
+ const entity = (open >= 0 ? first.slice(0, open) : first).trim();
579
+ return entity || void 0;
580
+ }
581
+ function looksLikeLowerCamelInvocation(segment) {
582
+ const open = segment.indexOf("(");
583
+ if (open <= 0) return false;
584
+ const name = segment.slice(0, open).split(".").at(-1) ?? segment.slice(0, open);
585
+ return /^[a-z][A-Za-z0-9_]*$/.test(name);
586
+ }
587
+ function extractTemplatePlaceholders(text) {
588
+ const keys = [];
589
+ for (let index = 0; index < text.length - 1; index += 1) {
590
+ if (text[index] !== "$" || text[index + 1] !== "{") continue;
591
+ const close = matchingPlaceholderClose(text, index + 1);
592
+ if (close === void 0) continue;
593
+ const key = text.slice(index + 2, close).trim();
594
+ if (key) keys.push(key);
595
+ index = close;
596
+ }
597
+ return keys;
598
+ }
599
+ function matchingClose(text, openIndex) {
600
+ let depth = 0;
601
+ let quote;
602
+ for (let index = openIndex; index < text.length; index += 1) {
603
+ const char = text[index];
604
+ const prev = text[index - 1];
605
+ if (quote) {
606
+ if (prev === "\\") continue;
607
+ if (quote === "template" && char === "$" && text[index + 1] === "{") {
608
+ const close = matchingPlaceholderClose(text, index + 1);
609
+ if (close === void 0) return void 0;
610
+ index = close;
611
+ continue;
612
+ }
613
+ if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
614
+ continue;
615
+ }
616
+ if (char === "$" && text[index + 1] === "{") {
617
+ const close = matchingPlaceholderClose(text, index + 1);
618
+ if (close === void 0) return void 0;
619
+ index = close;
620
+ continue;
621
+ }
622
+ if (char === "'") {
623
+ quote = "single";
624
+ continue;
625
+ }
626
+ if (char === '"') {
627
+ quote = "double";
628
+ continue;
629
+ }
630
+ if (char === "`") {
631
+ quote = "template";
632
+ continue;
633
+ }
634
+ if (char === "(") depth += 1;
635
+ if (char === ")") {
636
+ depth -= 1;
637
+ if (depth === 0) return index;
638
+ if (depth < 0) return void 0;
639
+ }
640
+ }
641
+ return void 0;
642
+ }
643
+ function matchingPlaceholderClose(text, openBraceIndex) {
644
+ let depth = 0;
645
+ let quote;
646
+ for (let index = openBraceIndex; index < text.length; index += 1) {
647
+ const char = text[index];
648
+ const prev = text[index - 1];
649
+ if (quote) {
650
+ if (prev === "\\") continue;
651
+ if (quote === "template" && char === "$" && text[index + 1] === "{") {
652
+ depth += 1;
653
+ index += 1;
654
+ continue;
655
+ }
656
+ if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
657
+ continue;
658
+ }
659
+ if (char === "'") {
660
+ quote = "single";
661
+ continue;
662
+ }
663
+ if (char === '"') {
664
+ quote = "double";
665
+ continue;
666
+ }
667
+ if (char === "`") {
668
+ quote = "template";
669
+ continue;
670
+ }
671
+ if (char === "{") depth += 1;
672
+ if (char === "}") {
673
+ depth -= 1;
674
+ if (depth === 0) return index;
675
+ if (depth < 0) return void 0;
676
+ }
677
+ }
678
+ return void 0;
679
+ }
680
+ function topLevelQueryIndex(text) {
681
+ let quote;
682
+ for (let index = 0; index < text.length; index += 1) {
683
+ const char = text[index];
684
+ const prev = text[index - 1];
685
+ if (quote) {
686
+ if (prev === "\\") continue;
687
+ if (quote === "template" && char === "$" && text[index + 1] === "{") {
688
+ const close = matchingPlaceholderClose(text, index + 1);
689
+ if (close === void 0) return -1;
690
+ index = close;
691
+ continue;
692
+ }
693
+ if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
694
+ continue;
695
+ }
696
+ if (char === "$" && text[index + 1] === "{") {
697
+ const close = matchingPlaceholderClose(text, index + 1);
698
+ if (close === void 0) return -1;
699
+ index = close;
700
+ continue;
701
+ }
702
+ if (char === "'") {
703
+ quote = "single";
704
+ continue;
705
+ }
706
+ if (char === '"') {
707
+ quote = "double";
708
+ continue;
709
+ }
710
+ if (char === "`") {
711
+ quote = "template";
712
+ continue;
713
+ }
714
+ if (char === "?") return index;
715
+ }
716
+ return -1;
717
+ }
718
+
524
719
  // src/parsers/outbound-call-parser.ts
525
720
  import fs6 from "fs/promises";
526
721
  import path7 from "path";
@@ -708,9 +903,13 @@ function classifyOutboundCallsInSource(source, filePath) {
708
903
  if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
709
904
  const receiver = receiverName(expr.expression);
710
905
  const query = objectPropertyText(objectArg, "query");
906
+ const method = stripQuotes(objectPropertyText(objectArg, "method") ?? "POST");
711
907
  const op = objectPropertyText(objectArg, "path") ?? objectPropertyText(objectArg, "event");
712
908
  const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
713
- 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 });
909
+ const operationPathExpr = op && !shorthandPath ? `/${stripQuotes(op).replace(/^\//, "")}` : void 0;
910
+ const intent = classifyODataPathIntent(operationPathExpr, method);
911
+ const isODataQueryRead = method.toUpperCase() === "GET" && ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
912
+ 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 });
714
913
  }
715
914
  } else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts4.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
716
915
  const wrapperName = ts4.isIdentifier(expr) ? expr.text : ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) ? expr.expression.text : "";
@@ -1433,53 +1632,6 @@ function substituteVariables(template, vars) {
1433
1632
  };
1434
1633
  }
1435
1634
 
1436
- // src/linker/odata-path-normalizer.ts
1437
- function normalizeODataOperationInvocationPath(path9) {
1438
- if (path9 === void 0) return void 0;
1439
- const raw = path9.trim();
1440
- if (!raw) return void 0;
1441
- const open = raw.indexOf("(");
1442
- if (open < 0) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
1443
- if (!raw.startsWith("/") || raw.slice(1, open).includes("/")) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
1444
- const close = matchingClose(raw, open);
1445
- if (close === void 0 || raw.slice(close + 1).trim().length > 0) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
1446
- const operationSegment = raw.slice(0, open).trim();
1447
- if (operationSegment.length <= 1) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
1448
- return { rawOperationPath: raw, normalizedOperationPath: operationSegment, wasInvocation: true };
1449
- }
1450
- function matchingClose(text, openIndex) {
1451
- let depth = 0;
1452
- let quote;
1453
- for (let index = openIndex; index < text.length; index += 1) {
1454
- const char = text[index];
1455
- const prev = text[index - 1];
1456
- if (quote) {
1457
- if (prev === "\\") continue;
1458
- if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
1459
- continue;
1460
- }
1461
- if (char === "'") {
1462
- quote = "single";
1463
- continue;
1464
- }
1465
- if (char === '"') {
1466
- quote = "double";
1467
- continue;
1468
- }
1469
- if (char === "`") {
1470
- quote = "template";
1471
- continue;
1472
- }
1473
- if (char === "(") depth += 1;
1474
- if (char === ")") {
1475
- depth -= 1;
1476
- if (depth === 0) return index;
1477
- if (depth < 0) return void 0;
1478
- }
1479
- }
1480
- return void 0;
1481
- }
1482
-
1483
1635
  // src/linker/remote-query-target.ts
1484
1636
  function buildRemoteQueryTarget(input) {
1485
1637
  const entity = typeof input.queryEntity === "string" && input.queryEntity.trim() ? input.queryEntity.trim() : void 0;
@@ -1751,15 +1903,18 @@ function linkCalls(db, workspaceId, vars, generation) {
1751
1903
  function insertCallEdge(db, workspaceId, call, vars, generation) {
1752
1904
  const callType = String(call.call_type);
1753
1905
  const rawOp = applyVariables(String(call.operation_path_expr ?? ""), vars);
1754
- const normalized = normalizeODataOperationInvocationPath(rawOp);
1755
- const op = normalized?.normalizedOperationPath ?? rawOp;
1906
+ const intent = classifyODataPathIntent(rawOp, call.method);
1907
+ const isEntityQueryIntent = ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
1908
+ const resolutionRawOp = callType === "remote_query" && isEntityQueryIntent ? intent.pathWithoutQuery : rawOp;
1909
+ const normalized = normalizeODataOperationInvocationPath(resolutionRawOp);
1910
+ const op = normalized?.normalizedOperationPath ?? resolutionRawOp;
1756
1911
  const servicePath = applyVariables(call.servicePathExpr ?? call.requireServicePath, vars);
1757
1912
  const destination = call.destinationExpr ?? call.requireDestination;
1758
1913
  const isDynamic = Boolean(Number(call.isDynamic ?? 0));
1759
1914
  const isOperationCall = callType === "remote_action" || callType === "local_service_call" || callType === "remote_query" && Boolean(op);
1760
1915
  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: [] };
1761
- const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0, normalized);
1762
- if (callType === "remote_query" && !op) {
1916
+ const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0, normalized, intent);
1917
+ if (callType === "remote_query" && (isEntityQueryIntent || !op) && !resolution.target) {
1763
1918
  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 });
1764
1919
  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);
1765
1920
  return { status: "terminal", callType };
@@ -1801,9 +1956,9 @@ function objectJson(value) {
1801
1956
  const parsed = parseJson(value);
1802
1957
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
1803
1958
  }
1804
- function callEvidence(call, resolution, servicePath, op, destination, normalized) {
1959
+ function callEvidence(call, resolution, servicePath, op, destination, normalized, odataPathIntent) {
1805
1960
  const bindingHasDynamicExpression = Boolean(Number(call.isDynamic ?? 0));
1806
- 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 };
1961
+ 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 };
1807
1962
  }
1808
1963
  function linkImplementations(db, workspaceId, generation) {
1809
1964
  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);
@@ -2352,18 +2507,16 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
2352
2507
  }
2353
2508
  function contextualRuntimeResolution(db, call, binding, workspaceId) {
2354
2509
  if (!binding || String(call.call_type) !== "remote_action" || call.operation_path_expr === void 0 || call.operation_path_expr === null) return {};
2355
- const op = normalizeODataOperationInvocationPathForTrace(String(call.operation_path_expr));
2510
+ const normalized = normalizeODataOperationInvocationPath(String(call.operation_path_expr));
2511
+ const op = normalized?.normalizedOperationPath ?? (String(call.operation_path_expr).startsWith("/") ? String(call.operation_path_expr) : `/${String(call.operation_path_expr)}`);
2356
2512
  const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
2357
2513
  const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
2358
2514
  const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
2359
- 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, 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 };
2515
+ 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 };
2360
2516
  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" };
2361
2517
  const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
2362
2518
  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 };
2363
2519
  }
2364
- function normalizeODataOperationInvocationPathForTrace(raw) {
2365
- return raw.startsWith("/") ? raw : `/${raw}`;
2366
- }
2367
2520
  function edgeTarget(row, evidence) {
2368
2521
  const runtimeCandidate = evidence.runtimeResolvedCandidate;
2369
2522
  if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath)
@@ -2554,14 +2707,14 @@ export {
2554
2707
  parseHandlerRegistrations,
2555
2708
  redactText,
2556
2709
  redactValue,
2710
+ normalizeODataOperationInvocationPath,
2557
2711
  containsSupportedOutboundCall,
2558
2712
  parseOutboundCalls,
2559
2713
  parseServiceBindings,
2560
2714
  applyVariables,
2561
2715
  extractPlaceholders,
2562
2716
  substituteVariables,
2563
- normalizeODataOperationInvocationPath,
2564
2717
  linkWorkspace,
2565
2718
  trace
2566
2719
  };
2567
- //# sourceMappingURL=chunk-SWPGRM7Z.js.map
2720
+ //# sourceMappingURL=chunk-UHYHSYCS.js.map