@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 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) ?? 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) next.set(`${param}.${prop.property}`, { ...binding, source: "local_symbol_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
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-VDJTNOEI.js.map
2567
+ //# sourceMappingURL=chunk-SWPGRM7Z.js.map