@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 +14 -0
- package/README.md +1 -1
- package/TECHNICAL-NOTE.md +6 -0
- package/dist/{chunk-VDJTNOEI.js → chunk-GOE6ZK6I.js} +129 -62
- package/dist/chunk-GOE6ZK6I.js.map +1 -0
- package/dist/cli.js +44 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-VDJTNOEI.js.map +0 -1
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)
|
|
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
|
-
|
|
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
|
|
1748
|
-
const
|
|
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 :
|
|
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)
|
|
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-
|
|
2617
|
+
//# sourceMappingURL=chunk-GOE6ZK6I.js.map
|