@saptools/service-flow 0.1.27 → 0.1.28
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/dist/{chunk-VDJTNOEI.js → chunk-SWPGRM7Z.js} +24 -7
- package/dist/chunk-SWPGRM7Z.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,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.28
|
|
4
|
+
|
|
5
|
+
- Propagate contextual service-client bindings through one-level object-parameter destructuring aliases, including renamed and assignment destructuring in neutral CAP helpers.
|
|
6
|
+
- Prefer caller-site higher-order wrapper remote-action evidence for literal wrapper paths while keeping dynamic wrapper path diagnostics on the caller edge.
|
|
7
|
+
- Refine contextual implementation selection evidence for duplicate helper candidates and report structured duplicate-candidate ties when selection is unsafe.
|
|
8
|
+
- Fix strict doctor contextual opportunity metrics so aggregate totals, capped examples, and actionable severity remain internally consistent.
|
|
9
|
+
|
|
3
10
|
## 0.1.27
|
|
4
11
|
|
|
5
12
|
- 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.
|
|
@@ -642,7 +642,8 @@ function collectWrapperSpecs(source) {
|
|
|
642
642
|
const specs = /* @__PURE__ */ new Map();
|
|
643
643
|
const scanFunction = (name, fn) => {
|
|
644
644
|
const params = fn.parameters.map((param) => ts4.isIdentifier(param.name) ? param.name.text : void 0);
|
|
645
|
-
const closure = returnedClosure(fn)
|
|
645
|
+
const closure = returnedClosure(fn);
|
|
646
|
+
if (!closure) return;
|
|
646
647
|
const sends = [];
|
|
647
648
|
const visit = (node) => {
|
|
648
649
|
if (ts4.isCallExpression(node) && ts4.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send" && ts4.isIdentifier(node.expression.expression)) {
|
|
@@ -660,7 +661,7 @@ function collectWrapperSpecs(source) {
|
|
|
660
661
|
const clientIndex = params.indexOf(found.client);
|
|
661
662
|
const pathIndex = params.indexOf(found.path);
|
|
662
663
|
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)) });
|
|
664
|
+
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
665
|
};
|
|
665
666
|
for (const stmt of source.statements) {
|
|
666
667
|
if (ts4.isFunctionDeclaration(stmt) && stmt.name) scanFunction(stmt.name.text, stmt);
|
|
@@ -686,11 +687,15 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
686
687
|
const initializers = variableInitializers(source);
|
|
687
688
|
const serviceVariables = collectServiceVariables(source);
|
|
688
689
|
const wrapperSpecs = collectWrapperSpecs(source);
|
|
690
|
+
const wrapperInternalRanges = [...wrapperSpecs.values()].map((spec) => ({ start: spec.internalStart, end: spec.internalEnd }));
|
|
689
691
|
const add = (node, fact, extra) => {
|
|
690
692
|
calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf3(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
|
|
691
693
|
};
|
|
692
694
|
const visit = (node) => {
|
|
693
695
|
if (ts4.isCallExpression(node)) {
|
|
696
|
+
if (wrapperInternalRanges.some((range) => node.getStart(source) >= range.start && node.getEnd() <= range.end)) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
694
699
|
const expr = node.expression;
|
|
695
700
|
const exprText = expr.getText(source);
|
|
696
701
|
if (exprText === "cds.run") {
|
|
@@ -716,6 +721,8 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
716
721
|
const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
|
|
717
722
|
if (spec && clientArg && ts4.isIdentifier(clientArg) && isStringLike(pathArg)) {
|
|
718
723
|
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 });
|
|
724
|
+
} else if (spec && clientArg && ts4.isIdentifier(clientArg)) {
|
|
725
|
+
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
726
|
}
|
|
720
727
|
} else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
|
|
721
728
|
const receiver = receiverName(expr.expression);
|
|
@@ -2153,7 +2160,7 @@ function implementationScope(db, operationId) {
|
|
|
2153
2160
|
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
2161
|
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
|
|
2155
2162
|
}
|
|
2156
|
-
function contextImplementationMethodId(edge, callerRepoId) {
|
|
2163
|
+
function contextImplementationMethodId(edge, callerRepoId, remoteEvidence = {}) {
|
|
2157
2164
|
if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return { evidence: { status: "not_applicable" } };
|
|
2158
2165
|
const evidence = JSON.parse(String(edge.evidence_json || "{}"));
|
|
2159
2166
|
const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
|
|
@@ -2167,12 +2174,16 @@ function contextImplementationMethodId(edge, callerRepoId) {
|
|
|
2167
2174
|
score += 10;
|
|
2168
2175
|
reasons.push("registration_package_matches_caller_repository");
|
|
2169
2176
|
}
|
|
2177
|
+
if (typeof remoteEvidence.effectiveServicePath === "string" || typeof remoteEvidence.effectiveDestination === "string" || typeof remoteEvidence.effectiveAlias === "string") {
|
|
2178
|
+
score += 1;
|
|
2179
|
+
reasons.push("remote_call_context_available");
|
|
2180
|
+
}
|
|
2170
2181
|
return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
|
|
2171
2182
|
}).sort((a, b) => b.score - a.score);
|
|
2172
2183
|
if (scores.length === 0) return { evidence: { status: "not_applicable", candidateScores: [] } };
|
|
2173
2184
|
const [first, second] = scores;
|
|
2174
2185
|
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 } };
|
|
2186
|
+
return { evidence: { status: "tied", tieReason: scores.length > 1 ? "duplicate_helper_implementation_candidates" : "no_unique_materially_stronger_candidate", candidateScores: scores } };
|
|
2176
2187
|
}
|
|
2177
2188
|
function handlerScope(db, methodId) {
|
|
2178
2189
|
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 +2323,7 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
|
2312
2323
|
const calleeEvidence = parseEvidence(callee?.evidenceJson);
|
|
2313
2324
|
const params = Array.isArray(calleeEvidence.parameters) ? calleeEvidence.parameters.filter((item) => typeof item === "string") : [];
|
|
2314
2325
|
const parameterBindings = Array.isArray(calleeEvidence.parameterBindings) ? calleeEvidence.parameterBindings.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
|
|
2326
|
+
const parameterPropertyAliases = Array.isArray(calleeEvidence.parameterPropertyAliases) ? calleeEvidence.parameterPropertyAliases.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
|
|
2315
2327
|
const args = Array.isArray(callEvidence2.callArguments) ? callEvidence2.callArguments : [];
|
|
2316
2328
|
args.forEach((arg, index) => {
|
|
2317
2329
|
const paramBinding = parameterBindings.find((binding) => binding.index === index);
|
|
@@ -2327,7 +2339,12 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
|
2327
2339
|
if (!binding) continue;
|
|
2328
2340
|
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
2341
|
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)
|
|
2342
|
+
else if (param) {
|
|
2343
|
+
next.set(`${param}.${prop.property}`, { ...binding, source: "local_symbol_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
|
|
2344
|
+
for (const alias of parameterPropertyAliases) {
|
|
2345
|
+
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 });
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2331
2348
|
}
|
|
2332
2349
|
}
|
|
2333
2350
|
});
|
|
@@ -2475,7 +2492,7 @@ function trace(db, start, options) {
|
|
|
2475
2492
|
});
|
|
2476
2493
|
if (effectiveRow.to_kind === "operation") {
|
|
2477
2494
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
2478
|
-
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId);
|
|
2495
|
+
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence);
|
|
2479
2496
|
const contextMethodId = contextSelection.methodId;
|
|
2480
2497
|
const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : void 0;
|
|
2481
2498
|
if (implementation.edge) {
|
|
@@ -2547,4 +2564,4 @@ export {
|
|
|
2547
2564
|
linkWorkspace,
|
|
2548
2565
|
trace
|
|
2549
2566
|
};
|
|
2550
|
-
//# sourceMappingURL=chunk-
|
|
2567
|
+
//# sourceMappingURL=chunk-SWPGRM7Z.js.map
|