@saptools/service-flow 0.1.29 → 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 +7 -0
- package/TECHNICAL-NOTE.md +7 -0
- package/dist/{chunk-GOE6ZK6I.js → chunk-UHYHSYCS.js} +118 -15
- package/dist/chunk-UHYHSYCS.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-GOE6ZK6I.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## 0.1.29
|
|
4
11
|
|
|
5
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.
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## 0.1.29 OData entity-query intent notes
|
|
4
11
|
|
|
5
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`.
|
|
@@ -526,16 +526,25 @@ function normalizeODataOperationInvocationPath(path9) {
|
|
|
526
526
|
if (path9 === void 0) return void 0;
|
|
527
527
|
const raw = path9.trim();
|
|
528
528
|
if (!raw) return void 0;
|
|
529
|
+
const rejected = (reason) => ({ rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false, invocationArgumentPlaceholderKeys: [], normalizationRejectedReason: reason });
|
|
529
530
|
const open = raw.indexOf("(");
|
|
530
|
-
if (open < 0) return
|
|
531
|
-
const query = raw
|
|
532
|
-
if (query >= 0) return
|
|
533
|
-
if (!raw.startsWith("/")
|
|
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");
|
|
534
536
|
const close = matchingClose(raw, open);
|
|
535
|
-
if (close === void 0
|
|
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");
|
|
536
539
|
const operationSegment = raw.slice(0, open).trim();
|
|
537
|
-
if (operationSegment.length <= 1) return
|
|
538
|
-
return {
|
|
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
|
+
};
|
|
539
548
|
}
|
|
540
549
|
function classifyODataPathIntent(path9, method) {
|
|
541
550
|
const rawPath = (path9 ?? "").trim();
|
|
@@ -576,7 +585,16 @@ function looksLikeLowerCamelInvocation(segment) {
|
|
|
576
585
|
return /^[a-z][A-Za-z0-9_]*$/.test(name);
|
|
577
586
|
}
|
|
578
587
|
function extractTemplatePlaceholders(text) {
|
|
579
|
-
|
|
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;
|
|
580
598
|
}
|
|
581
599
|
function matchingClose(text, openIndex) {
|
|
582
600
|
let depth = 0;
|
|
@@ -586,9 +604,21 @@ function matchingClose(text, openIndex) {
|
|
|
586
604
|
const prev = text[index - 1];
|
|
587
605
|
if (quote) {
|
|
588
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
|
+
}
|
|
589
613
|
if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
|
|
590
614
|
continue;
|
|
591
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
|
+
}
|
|
592
622
|
if (char === "'") {
|
|
593
623
|
quote = "single";
|
|
594
624
|
continue;
|
|
@@ -610,6 +640,81 @@ function matchingClose(text, openIndex) {
|
|
|
610
640
|
}
|
|
611
641
|
return void 0;
|
|
612
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
|
+
}
|
|
613
718
|
|
|
614
719
|
// src/parsers/outbound-call-parser.ts
|
|
615
720
|
import fs6 from "fs/promises";
|
|
@@ -1853,7 +1958,7 @@ function objectJson(value) {
|
|
|
1853
1958
|
}
|
|
1854
1959
|
function callEvidence(call, resolution, servicePath, op, destination, normalized, odataPathIntent) {
|
|
1855
1960
|
const bindingHasDynamicExpression = Boolean(Number(call.isDynamic ?? 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 };
|
|
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 };
|
|
1857
1962
|
}
|
|
1858
1963
|
function linkImplementations(db, workspaceId, generation) {
|
|
1859
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);
|
|
@@ -2402,18 +2507,16 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
|
2402
2507
|
}
|
|
2403
2508
|
function contextualRuntimeResolution(db, call, binding, workspaceId) {
|
|
2404
2509
|
if (!binding || String(call.call_type) !== "remote_action" || call.operation_path_expr === void 0 || call.operation_path_expr === null) return {};
|
|
2405
|
-
const
|
|
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)}`);
|
|
2406
2512
|
const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
|
|
2407
2513
|
const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
|
|
2408
2514
|
const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
|
|
2409
|
-
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 };
|
|
2410
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" };
|
|
2411
2517
|
const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
|
|
2412
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 };
|
|
2413
2519
|
}
|
|
2414
|
-
function normalizeODataOperationInvocationPathForTrace(raw) {
|
|
2415
|
-
return raw.startsWith("/") ? raw : `/${raw}`;
|
|
2416
|
-
}
|
|
2417
2520
|
function edgeTarget(row, evidence) {
|
|
2418
2521
|
const runtimeCandidate = evidence.runtimeResolvedCandidate;
|
|
2419
2522
|
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath)
|
|
@@ -2614,4 +2717,4 @@ export {
|
|
|
2614
2717
|
linkWorkspace,
|
|
2615
2718
|
trace
|
|
2616
2719
|
};
|
|
2617
|
-
//# sourceMappingURL=chunk-
|
|
2720
|
+
//# sourceMappingURL=chunk-UHYHSYCS.js.map
|