@saptools/service-flow 0.1.26 → 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 +14 -0
- package/dist/{chunk-Q3J2QDNX.js → chunk-SWPGRM7Z.js} +49 -16
- package/dist/chunk-SWPGRM7Z.js.map +1 -0
- package/dist/cli.js +73 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-Q3J2QDNX.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
10
|
+
## 0.1.27
|
|
11
|
+
|
|
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.
|
|
13
|
+
- Preserve contextual binding attempt evidence for unresolved helper sends, including effective service path, destination, candidate counts, and resolution reasons.
|
|
14
|
+
- Refine strict doctor diagnostics for contextual helper sends to distinguish resolved contextual opportunities, missing `cds.requires` rows, require-backed unresolved sends, remaining runtime variables, and workspaces with no contextual opportunity.
|
|
15
|
+
- Expand neutral CAP coverage for positional helper arguments, destructured object helper parameters, renamed properties, late assignments, table output, Mermaid output, and require-derived target operations.
|
|
16
|
+
|
|
3
17
|
## 0.1.26
|
|
4
18
|
|
|
5
19
|
- Resolve helper-internal remote sends contextually in trace output when service clients are passed through positional arguments or one-level destructured object parameters.
|
|
@@ -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);
|
|
@@ -2266,21 +2277,33 @@ function receiverFromEvidence(value) {
|
|
|
2266
2277
|
const evidence = parseEvidence(value);
|
|
2267
2278
|
return typeof evidence.receiver === "string" ? evidence.receiver : void 0;
|
|
2268
2279
|
}
|
|
2280
|
+
function hasDynamicPlaceholder(value) {
|
|
2281
|
+
return extractPlaceholders(value).length > 0;
|
|
2282
|
+
}
|
|
2283
|
+
function enrichBinding(row) {
|
|
2284
|
+
const effectiveServicePath = row.servicePathExpr && !hasDynamicPlaceholder(row.servicePathExpr) ? row.servicePathExpr : !row.servicePathExpr ? row.requireServicePath : void 0;
|
|
2285
|
+
const effectiveDestination = row.destinationExpr && !hasDynamicPlaceholder(row.destinationExpr) ? row.destinationExpr : !row.destinationExpr ? row.requireDestination : void 0;
|
|
2286
|
+
return { ...row, effectiveServicePath, effectiveDestination };
|
|
2287
|
+
}
|
|
2269
2288
|
function knownBindingsForCalls(db, calls) {
|
|
2270
2289
|
const map = /* @__PURE__ */ new Map();
|
|
2271
2290
|
for (const call of calls) {
|
|
2272
2291
|
const receiver = receiverFromEvidence(call.evidence_json);
|
|
2273
2292
|
const bindingId = Number(call.service_binding_id ?? 0);
|
|
2274
2293
|
if (!receiver || !bindingId) continue;
|
|
2275
|
-
const row = db.prepare(
|
|
2276
|
-
|
|
2294
|
+
const row = db.prepare(`SELECT b.id,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.source_file sourceFile,b.source_line sourceLine,req.service_path requireServicePath,req.destination requireDestination
|
|
2295
|
+
FROM service_bindings b LEFT JOIN cds_requires req ON req.repo_id=b.repo_id AND req.alias=b.alias
|
|
2296
|
+
WHERE b.id=?`).get(bindingId);
|
|
2297
|
+
if (row) map.set(receiver, enrichBinding({ ...row, bindingId, source: "local_service_binding", calleeReceiver: receiver }));
|
|
2277
2298
|
}
|
|
2278
2299
|
return map;
|
|
2279
2300
|
}
|
|
2280
2301
|
function knownBindingsForScope(db, repoId, symbolIds, files) {
|
|
2281
2302
|
const map = /* @__PURE__ */ new Map();
|
|
2282
2303
|
if (repoId === void 0) return map;
|
|
2283
|
-
const rows2 = db.prepare(
|
|
2304
|
+
const rows2 = db.prepare(`SELECT b.id,b.variable_name variableName,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.source_file sourceFile,b.source_line sourceLine,req.service_path requireServicePath,req.destination requireDestination
|
|
2305
|
+
FROM service_bindings b LEFT JOIN cds_requires req ON req.repo_id=b.repo_id AND req.alias=b.alias
|
|
2306
|
+
WHERE b.repo_id=?`).all(repoId);
|
|
2284
2307
|
for (const row of rows2) {
|
|
2285
2308
|
if (!row.variableName) continue;
|
|
2286
2309
|
if (files && !files.has(String(row.sourceFile))) continue;
|
|
@@ -2288,7 +2311,7 @@ function knownBindingsForScope(db, repoId, symbolIds, files) {
|
|
|
2288
2311
|
const owner = db.prepare("SELECT id FROM symbols WHERE id IN (" + [...symbolIds].map(() => "?").join(",") + ") AND source_file=? AND start_line<=? AND end_line>=? LIMIT 1").get(...symbolIds, row.sourceFile, row.sourceLine, row.sourceLine);
|
|
2289
2312
|
if (!owner) continue;
|
|
2290
2313
|
}
|
|
2291
|
-
map.set(row.variableName, { ...row, bindingId: Number(row.id), source: "local_service_binding", calleeReceiver: row.variableName });
|
|
2314
|
+
map.set(row.variableName, enrichBinding({ ...row, bindingId: Number(row.id), source: "local_service_binding", calleeReceiver: row.variableName }));
|
|
2292
2315
|
}
|
|
2293
2316
|
return map;
|
|
2294
2317
|
}
|
|
@@ -2300,6 +2323,7 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
|
2300
2323
|
const calleeEvidence = parseEvidence(callee?.evidenceJson);
|
|
2301
2324
|
const params = Array.isArray(calleeEvidence.parameters) ? calleeEvidence.parameters.filter((item) => typeof item === "string") : [];
|
|
2302
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))) : [];
|
|
2303
2327
|
const args = Array.isArray(callEvidence2.callArguments) ? callEvidence2.callArguments : [];
|
|
2304
2328
|
args.forEach((arg, index) => {
|
|
2305
2329
|
const paramBinding = parameterBindings.find((binding) => binding.index === index);
|
|
@@ -2315,7 +2339,12 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
|
2315
2339
|
if (!binding) continue;
|
|
2316
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;
|
|
2317
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 });
|
|
2318
|
-
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
|
+
}
|
|
2319
2348
|
}
|
|
2320
2349
|
}
|
|
2321
2350
|
});
|
|
@@ -2324,10 +2353,13 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
|
2324
2353
|
function contextualRuntimeResolution(db, call, binding, workspaceId) {
|
|
2325
2354
|
if (!binding || String(call.call_type) !== "remote_action" || call.operation_path_expr === void 0 || call.operation_path_expr === null) return {};
|
|
2326
2355
|
const op = normalizeODataOperationInvocationPathForTrace(String(call.operation_path_expr));
|
|
2327
|
-
const
|
|
2328
|
-
const
|
|
2329
|
-
|
|
2330
|
-
|
|
2356
|
+
const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
|
|
2357
|
+
const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
|
|
2358
|
+
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 };
|
|
2360
|
+
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
|
+
const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
|
|
2362
|
+
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 };
|
|
2331
2363
|
}
|
|
2332
2364
|
function normalizeODataOperationInvocationPathForTrace(raw) {
|
|
2333
2365
|
return raw.startsWith("/") ? raw : `/${raw}`;
|
|
@@ -2434,9 +2466,10 @@ function trace(db, start, options) {
|
|
|
2434
2466
|
for (const row of graphRows) {
|
|
2435
2467
|
if (seenEdges.has(Number(row.id))) continue;
|
|
2436
2468
|
seenEdges.add(Number(row.id));
|
|
2437
|
-
const
|
|
2469
|
+
const persistedEvidence = JSON.parse(
|
|
2438
2470
|
String(row.evidence_json || "{}")
|
|
2439
2471
|
);
|
|
2472
|
+
const rawEvidence = contextual.evidence ? { ...persistedEvidence, ...contextual.evidence } : persistedEvidence;
|
|
2440
2473
|
const effective = runtimeResolution(db, row, rawEvidence, options.vars);
|
|
2441
2474
|
const evidence = effective.evidence;
|
|
2442
2475
|
const effectiveRow = effective.row;
|
|
@@ -2459,7 +2492,7 @@ function trace(db, start, options) {
|
|
|
2459
2492
|
});
|
|
2460
2493
|
if (effectiveRow.to_kind === "operation") {
|
|
2461
2494
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
2462
|
-
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId);
|
|
2495
|
+
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence);
|
|
2463
2496
|
const contextMethodId = contextSelection.methodId;
|
|
2464
2497
|
const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : void 0;
|
|
2465
2498
|
if (implementation.edge) {
|
|
@@ -2531,4 +2564,4 @@ export {
|
|
|
2531
2564
|
linkWorkspace,
|
|
2532
2565
|
trace
|
|
2533
2566
|
};
|
|
2534
|
-
//# sourceMappingURL=chunk-
|
|
2567
|
+
//# sourceMappingURL=chunk-SWPGRM7Z.js.map
|