@saptools/service-flow 0.1.41 → 0.1.43
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/dist/{chunk-7L4QKKGN.js → chunk-KHQK7CFH.js} +79 -5
- package/dist/chunk-KHQK7CFH.js.map +1 -0
- package/dist/cli.js +61 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/indexer/cds-extension-resolver.ts +69 -7
- package/src/linker/cross-repo-linker.ts +12 -2
- package/src/parsers/cds-parser.ts +18 -1
- package/src/parsers/outbound-call-parser.ts +34 -1
- package/dist/chunk-7L4QKKGN.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.43
|
|
4
|
+
|
|
5
|
+
- Reconciled inherited CDS operation search rows from the exact effective-operation set during extension materialization.
|
|
6
|
+
- Invalidated concrete extension repositories when derived inherited operations or base resolution semantics change, while preserving no-op generations.
|
|
7
|
+
- Tightened lexical binding scope so nested block declarations inside catch and loop bodies do not escape their real blocks.
|
|
8
|
+
- Classified proven CAP `send(operationName, payload)` calls, including immutable aliases, without applying the rule to generic `send()` receivers.
|
|
9
|
+
|
|
10
|
+
## 0.1.42
|
|
11
|
+
|
|
12
|
+
- Continued inherited CDS extension operations into the selected base implementation while retaining concrete routing evidence.
|
|
13
|
+
- Reconciled materialized inherited operations on reindex so renamed or removed base operations do not leave stale effective rows.
|
|
14
|
+
- Kept ambiguous or unresolved extension bases without a selected base id and ignored commented CDS `using` declarations.
|
|
15
|
+
- Added positional remote CAP `Service.send(method, path, ...)` classification for proven CAP clients and tightened catch/loop lexical constant resolution.
|
|
16
|
+
|
|
3
17
|
## 0.1.41
|
|
4
18
|
|
|
5
19
|
- Added imported CDS extension provenance and materialized inherited operations at concrete extension paths without guessing by simple service name.
|
|
@@ -161,6 +161,37 @@ function maskCommentsAndStrings(text) {
|
|
|
161
161
|
}
|
|
162
162
|
return out;
|
|
163
163
|
}
|
|
164
|
+
function maskComments(text) {
|
|
165
|
+
let out = "";
|
|
166
|
+
let mode = "code";
|
|
167
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
168
|
+
const c = text[i] ?? "";
|
|
169
|
+
const n = text[i + 1] ?? "";
|
|
170
|
+
if (mode === "code" && c === "/" && n === "/") {
|
|
171
|
+
mode = "line";
|
|
172
|
+
out += " ";
|
|
173
|
+
i += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (mode === "code" && c === "/" && n === "*") {
|
|
177
|
+
mode = "block";
|
|
178
|
+
out += " ";
|
|
179
|
+
i += 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (mode === "line" && c === "\n") mode = "code";
|
|
183
|
+
if (mode === "block" && c === "*" && n === "/") {
|
|
184
|
+
mode = "code";
|
|
185
|
+
out += " ";
|
|
186
|
+
i += 1;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (mode === "code" && (c === "'" || c === '"' || c === "`")) mode = c === "'" ? "single" : c === '"' ? "double" : "template";
|
|
190
|
+
else if (mode === "single" && c === "'" || mode === "double" && c === '"' || mode === "template" && c === "`") mode = "code";
|
|
191
|
+
out += mode === "line" || mode === "block" ? c === "\n" ? "\n" : " " : c;
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
164
195
|
function readAnnotation(text, index) {
|
|
165
196
|
if (text[index] !== "@") return void 0;
|
|
166
197
|
let i = index + 1;
|
|
@@ -235,7 +266,7 @@ async function parseCdsFile(repoPath, filePath) {
|
|
|
235
266
|
const namespace = /namespace\s+([\w.]+)\s*;/.exec(masked)?.[1];
|
|
236
267
|
const services = [];
|
|
237
268
|
const pendingAnnotations = [];
|
|
238
|
-
const usings = collectUsings(text);
|
|
269
|
+
const usings = collectUsings(maskComments(text));
|
|
239
270
|
for (const a of masked.matchAll(/@\s*\(/g)) pendingAnnotations.push(annotationRawAt(text, masked, a.index ?? 0));
|
|
240
271
|
const serviceRegex = /\b(?:(extend)\s+)?(?:(service)\s+)?([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\b/g;
|
|
241
272
|
let match;
|
|
@@ -919,6 +950,7 @@ function nodeContains(parent, child) {
|
|
|
919
950
|
}
|
|
920
951
|
function declarationScope(node) {
|
|
921
952
|
if (ts4.isParameter(node)) return node.parent;
|
|
953
|
+
if (ts4.isCatchClause(node.parent) && node.parent.variableDeclaration === node) return node.parent;
|
|
922
954
|
const list = node.parent;
|
|
923
955
|
const blockScoped = (list.flags & (ts4.NodeFlags.Const | ts4.NodeFlags.Let)) !== 0;
|
|
924
956
|
let current = list.parent;
|
|
@@ -926,13 +958,24 @@ function declarationScope(node) {
|
|
|
926
958
|
while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
|
|
927
959
|
return current;
|
|
928
960
|
}
|
|
929
|
-
while (current.parent && !ts4.isBlock(current) && !ts4.isSourceFile(current) && !ts4.isModuleBlock(current) && !ts4.isCaseBlock(current) && !isFunctionLikeScope(current)) current = current.parent;
|
|
961
|
+
while (current.parent && !ts4.isBlock(current) && !ts4.isSourceFile(current) && !ts4.isModuleBlock(current) && !ts4.isCaseBlock(current) && !isLoopInitializerScope(node, current) && !isFunctionLikeScope(current)) current = current.parent;
|
|
930
962
|
return current;
|
|
931
963
|
}
|
|
964
|
+
function isLoopInitializerScope(declaration, scope) {
|
|
965
|
+
const list = declaration.parent;
|
|
966
|
+
return ts4.isForStatement(scope) && scope.initializer === list || (ts4.isForInStatement(scope) || ts4.isForOfStatement(scope)) && scope.initializer === list;
|
|
967
|
+
}
|
|
968
|
+
function catchBindingScope(declaration) {
|
|
969
|
+
if (ts4.isParameter(declaration)) return void 0;
|
|
970
|
+
return ts4.isCatchClause(declaration.parent) && declaration.parent.variableDeclaration === declaration ? declaration.parent : void 0;
|
|
971
|
+
}
|
|
932
972
|
function isAccessibleDeclaration(declaration, use) {
|
|
933
973
|
const source = use.getSourceFile();
|
|
934
974
|
if (declaration.name.getStart(source) >= use.getStart(source)) return false;
|
|
975
|
+
const catchScope = catchBindingScope(declaration);
|
|
976
|
+
if (catchScope) return nodeContains(catchScope.block, use);
|
|
935
977
|
const scope = declarationScope(declaration);
|
|
978
|
+
if (ts4.isForStatement(scope) || ts4.isForInStatement(scope) || ts4.isForOfStatement(scope)) return nodeContains(scope.statement, use);
|
|
936
979
|
return ts4.isSourceFile(scope) || nodeContains(scope, use);
|
|
937
980
|
}
|
|
938
981
|
function resolveBinding(identifier, use) {
|
|
@@ -1039,6 +1082,11 @@ function httpMethodFromObject(object, use) {
|
|
|
1039
1082
|
const text = resolveExpression(propertyInitializer(object, "method"), use, "literal").value;
|
|
1040
1083
|
return text ? stripQuotes(text).toUpperCase() : void 0;
|
|
1041
1084
|
}
|
|
1085
|
+
var supportedHttpMethods = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
|
|
1086
|
+
function safeOperationName(value) {
|
|
1087
|
+
if (!value || !/^[A-Za-z_$][\w$]*(?:[./][A-Za-z_$][\w$]*)*$/.test(value)) return void 0;
|
|
1088
|
+
return operationPathFromStatic(value);
|
|
1089
|
+
}
|
|
1042
1090
|
function hasTemplatePlaceholder(value) {
|
|
1043
1091
|
return /\$\{|%7B|%7D/i.test(value);
|
|
1044
1092
|
}
|
|
@@ -1215,6 +1263,22 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
1215
1263
|
const entityCallType = entityCallTypes[intent.kind];
|
|
1216
1264
|
const isODataQueryRead = method.toUpperCase() === "GET" && ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
|
|
1217
1265
|
add(node, { callType: query ? "remote_query" : entityCallType ?? (isODataQueryRead ? "remote_query" : "remote_action"), serviceVariableName: receiver, method, operationPathExpr, queryEntity: query ? extractQueryEntity(query) : isODataQueryRead ? intent.entitySegment : void 0, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : resolvedPath.sourceKind : void 0, odataPathIntent: operationPathExpr ? intent : void 0, staticPathCandidates: candidateEvidence2, parserWarning: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 });
|
|
1266
|
+
} else {
|
|
1267
|
+
const receiver = receiverName(expr.expression);
|
|
1268
|
+
const rootReceiver = rootReceiverName(expr.expression);
|
|
1269
|
+
const firstArg2 = resolveExpression(node.arguments[0], node, "literal");
|
|
1270
|
+
const method = firstArg2.value?.toUpperCase();
|
|
1271
|
+
const pathArg = node.arguments[1];
|
|
1272
|
+
const supported = method && supportedHttpMethods.has(method);
|
|
1273
|
+
if (receiver && supported && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
1274
|
+
const resolvedPath = staticPathExpression(pathArg, node);
|
|
1275
|
+
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
1276
|
+
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
1277
|
+
add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.8 : 0.45, unresolvedReason: operationPathExpr ? void 0 : "dynamic_operation_path_identifier" }, { receiver, rootReceiver, classifier: "service_client_send_method_path", rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : resolvedPath.sourceKind : void 0, odataPathIntent: operationPathExpr ? intent : void 0, parserWarning: operationPathExpr ? void 0 : "dynamic_operation_path_identifier" });
|
|
1278
|
+
} else if (receiver && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
1279
|
+
const operationPathExpr = safeOperationName(firstArg2.value);
|
|
1280
|
+
add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.75 : 0.35, unresolvedReason: operationPathExpr ? void 0 : "unsupported_cap_send_signature" }, { receiver, rootReceiver, classifier: operationPathExpr ? "service_client_send_operation_event" : "service_client_send_unsupported_signature", rawOperationExpression: firstArg2.rawExpression, literalOperationSource: firstArg2.value ? firstArg2.sourceKind : void 0, parserWarning: operationPathExpr ? void 0 : "unsupported_cap_send_signature" });
|
|
1281
|
+
}
|
|
1218
1282
|
}
|
|
1219
1283
|
} else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts4.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
|
|
1220
1284
|
const wrapperName = ts4.isIdentifier(expr) ? expr.text : ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) ? expr.expression.text : "";
|
|
@@ -2448,13 +2512,14 @@ function callEvidence(call, resolution, servicePath, op, destination, normalized
|
|
|
2448
2512
|
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 };
|
|
2449
2513
|
}
|
|
2450
2514
|
function linkImplementations(db, workspaceId, generation) {
|
|
2451
|
-
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);
|
|
2515
|
+
const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,o.provenance provenance,o.base_operation_id baseOperationId,s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,r.package_name modelPackage,r.kind modelKind FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=?`).all(workspaceId);
|
|
2452
2516
|
let edgeCount = 0;
|
|
2453
2517
|
let resolvedCount = 0;
|
|
2454
2518
|
let ambiguousCount = 0;
|
|
2455
2519
|
let unresolvedCount = 0;
|
|
2456
2520
|
for (const operation of operations) {
|
|
2457
|
-
const
|
|
2521
|
+
const implementationContext = implementationContextForOperation(db, operation);
|
|
2522
|
+
const candidates = rankedImplementationCandidates(db, workspaceId, implementationContext);
|
|
2458
2523
|
if (candidates.length === 0) continue;
|
|
2459
2524
|
const accepted = candidates.filter((candidate) => candidate.accepted);
|
|
2460
2525
|
const topScore = accepted[0]?.score ?? 0;
|
|
@@ -2465,6 +2530,9 @@ function linkImplementations(db, workspaceId, generation) {
|
|
|
2465
2530
|
operationPath: operation.operationPath,
|
|
2466
2531
|
operationName: operation.operationName,
|
|
2467
2532
|
modelPackage: { id: operation.modelRepoId, name: operation.modelRepo, packageName: operation.modelPackage },
|
|
2533
|
+
implementationSource: implementationContext.operationId === operation.operationId ? "direct_or_concrete_override" : "inherited_from_base_operation",
|
|
2534
|
+
baseOperationId: operation.baseOperationId,
|
|
2535
|
+
implementationOperationId: implementationContext.operationId,
|
|
2468
2536
|
candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1))
|
|
2469
2537
|
};
|
|
2470
2538
|
if (accepted.length === 0) {
|
|
@@ -2480,6 +2548,12 @@ function linkImplementations(db, workspaceId, generation) {
|
|
|
2480
2548
|
}
|
|
2481
2549
|
return { edgeCount, resolvedCount, ambiguousCount, unresolvedCount };
|
|
2482
2550
|
}
|
|
2551
|
+
function implementationContextForOperation(db, operation) {
|
|
2552
|
+
if (operation.provenance !== "inherited" || !operation.baseOperationId) return operation;
|
|
2553
|
+
const base = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,o.provenance provenance,o.base_operation_id baseOperationId,s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,r.package_name modelPackage,r.kind modelKind FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE o.id=?`).get(operation.baseOperationId);
|
|
2554
|
+
if (!base) return operation;
|
|
2555
|
+
return { ...base, effectiveOperationId: operation.operationId, effectiveServicePath: operation.servicePath, effectiveOperationPath: operation.operationPath };
|
|
2556
|
+
}
|
|
2483
2557
|
function rankedImplementationCandidates(db, workspaceId, operation) {
|
|
2484
2558
|
const rows2 = implementationCandidates(db, workspaceId, operation);
|
|
2485
2559
|
return deduplicateCandidates(rows2.map((row) => scoreImplementationCandidate(row, operation))).sort((a, b) => b.score - a.score || String(a.className).localeCompare(String(b.className)) || a.methodId - b.methodId);
|
|
@@ -3223,4 +3297,4 @@ export {
|
|
|
3223
3297
|
linkWorkspace,
|
|
3224
3298
|
trace
|
|
3225
3299
|
};
|
|
3226
|
-
//# sourceMappingURL=chunk-
|
|
3300
|
+
//# sourceMappingURL=chunk-KHQK7CFH.js.map
|