@saptools/service-flow 0.1.25 → 0.1.27

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/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  parsePackageJson,
13
13
  parseServiceBindings,
14
14
  trace
15
- } from "./chunk-HS746OHV.js";
15
+ } from "./chunk-VDJTNOEI.js";
16
16
 
17
17
  // src/cli.ts
18
18
  import { Command } from "commander";
@@ -188,7 +188,7 @@ function migrate(db) {
188
188
  // package.json
189
189
  var package_default = {
190
190
  name: "@saptools/service-flow",
191
- version: "0.1.25",
191
+ version: "0.1.27",
192
192
  description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
193
193
  type: "module",
194
194
  publishConfig: {
@@ -929,11 +929,17 @@ async function parseExecutableSymbols(repoPath, filePath) {
929
929
  ts.forEachChild(node, visitProxyVariables);
930
930
  };
931
931
  visitProxyVariables(source);
932
+ const rememberClassInstance = (variableName, className, propertyName) => {
933
+ const importSource = imports.get(className);
934
+ if (!builtInConstructors.has(className) && (importSource && isRelativeImport(importSource) || declaredClasses.has(className))) classInstances.set(variableName, { className, importSource, propertyName });
935
+ };
932
936
  const visitClassInstances = (node) => {
933
937
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
934
- const className = node.initializer.expression.text;
935
- const importSource = imports.get(className);
936
- if (!builtInConstructors.has(className) && (importSource && isRelativeImport(importSource) || declaredClasses.has(className))) classInstances.set(node.name.text, { className, importSource });
938
+ rememberClassInstance(node.name.text, node.initializer.expression.text);
939
+ }
940
+ if (ts.isPropertyDeclaration(node) && node.initializer && ts.isNewExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
941
+ const propertyName = nameOf(node.name);
942
+ if (propertyName) rememberClassInstance(`this.${propertyName}`, node.initializer.expression.text, propertyName);
937
943
  }
938
944
  ts.forEachChild(node, visitClassInstances);
939
945
  };
@@ -946,11 +952,12 @@ async function parseExecutableSymbols(repoPath, filePath) {
946
952
  if (caller) {
947
953
  const callee = callName(node.expression);
948
954
  const proxy = callee.local ? proxyVariables.get(callee.local) : void 0;
949
- const instance = callee.local ? classInstances.get(callee.local) : void 0;
955
+ const instance = (callee.local ? classInstances.get(callee.local) : void 0) ?? (callee.receiver ? classInstances.get(callee.receiver) : void 0);
950
956
  const importSource = instance?.importSource ?? proxy?.importSource ?? (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
951
- const targetName = instance && callee.member ? `${instance.className}.${callee.member}` : proxy && callee.member ? callee.member : callee.expression.startsWith("this.") ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
957
+ const directThisMethod = callee.receiver === "this";
958
+ const targetName = instance && callee.member ? `${instance.className}.${callee.member}` : proxy && callee.member ? callee.member : directThisMethod ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
952
959
  const className = caller.qualifiedName.includes(".") ? caller.qualifiedName.split(".")[0] : void 0;
953
- const thisTarget = className && callee.member ? `${className}.${callee.member}` : void 0;
960
+ const thisTarget = directThisMethod && className && callee.member ? `${className}.${callee.member}` : void 0;
954
961
  const loggerLike = callee.receiver?.endsWith(".logger") || callee.local === "logger" || (callee.expression.startsWith("this.logger.") && callee.member ? loggerMembers.has(callee.member) : false);
955
962
  const terminalMember = callee.member ? commonTerminalMembers.has(callee.member) || loggerMembers.has(callee.member) : false;
956
963
  const provenLocal = Boolean(targetName) && localCallables.has(String(targetName));
@@ -961,7 +968,7 @@ async function parseExecutableSymbols(repoPath, filePath) {
961
968
  const ignored = loggerLike || terminalMember || importedFromPackage || ignoredFrameworkCall(callee);
962
969
  const resolvedTarget = provenThisMethod ? thisTarget : targetName;
963
970
  const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport || provenClassInstance);
964
- if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? callee.local : void 0, importSource, sourceFile, sourceLine: line, evidence: { relation: instance ? "class_instance_method" : proxy ? "relative_import_proxy_member" : importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : "indexed_local_symbol", caller: caller.qualifiedName, targetName: resolvedTarget, instanceVariable: instance ? callee.local : void 0, className: instance?.className, methodName: instance ? callee.member : void 0, classImportSource: instance?.importSource, callArguments: argumentEvidence(node.arguments, source), proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: instance ? instance.importSource ? "relative_import_class_instance_method" : "same_file_class_instance_method" : proxy ? "proxy_member_exact_export_or_unique_member" : void 0 } });
971
+ if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? callee.local ?? callee.receiver : void 0, importSource, sourceFile, sourceLine: line, evidence: { relation: instance ? "class_instance_method" : proxy ? "relative_import_proxy_member" : importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : "indexed_local_symbol", caller: caller.qualifiedName, targetName: resolvedTarget, instanceVariable: instance ? instance.propertyName ?? callee.local : void 0, className: instance?.className, methodName: instance ? callee.member : void 0, classImportSource: instance?.importSource, callArguments: argumentEvidence(node.arguments, source), proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: instance ? instance.importSource ? "relative_import_class_instance_method" : "same_file_class_instance_method" : proxy ? "proxy_member_exact_export_or_unique_member" : void 0 } });
965
972
  }
966
973
  }
967
974
  ts.forEachChild(node, visitCalls);
@@ -1281,6 +1288,7 @@ function parserQualityDiagnostics(db, strict) {
1281
1288
  const classInstanceQuality = classInstanceNoiseQuality(db);
1282
1289
  const bindingPropagationQuality = contextualBindingPropagationQuality(db);
1283
1290
  const wrapperQuality = wrapperPathPropagationQuality(db);
1291
+ const nestedThisQuality = nestedThisReceiverQuality(db);
1284
1292
  return [
1285
1293
  aliasQuality,
1286
1294
  noBindingQuality,
@@ -1288,6 +1296,7 @@ function parserQualityDiagnostics(db, strict) {
1288
1296
  classInstanceQuality,
1289
1297
  bindingPropagationQuality,
1290
1298
  wrapperQuality,
1299
+ nestedThisQuality,
1291
1300
  remoteQuery,
1292
1301
  invocation,
1293
1302
  remoteAction,
@@ -1360,8 +1369,48 @@ function contextualBindingPropagationQuality(db) {
1360
1369
  WHERE json_extract(sc.evidence_json,'$.callArguments[0].kind')='object_literal'
1361
1370
  AND json_extract(s.evidence_json,'$.parameterBindings[0].kind')='object_pattern'
1362
1371
  AND json_array_length(json_extract(sc.evidence_json,'$.callArguments[0].properties')) > json_array_length(json_extract(s.evidence_json,'$.parameterBindings[0].properties'))`).get();
1363
- const contextualResolved = db.prepare(`SELECT COUNT(*) count FROM graph_edges WHERE json_extract(evidence_json,'$.contextualServiceBindingSelected')=1 AND status='resolved'`).get();
1364
- return { severity: Number(missingMetadata.count ?? 0) + Number(destructuredUnmapped.count ?? 0) > 0 ? "warning" : "info", code: "strict_contextual_binding_propagation_quality", message: "Contextual service-client propagation aggregate", localSymbolCallsWithServiceClientArguments: Number(serviceClientCalls.count ?? 0), calleeSymbolsMissingParameterMetadata: Number(missingMetadata.count ?? 0), destructuredObjectParametersPossiblyUnmapped: Number(destructuredUnmapped.count ?? 0), contextualRemoteActionsResolved: Number(contextualResolved.count ?? 0) };
1372
+ const opportunities = db.prepare(`SELECT c.source_file sourceFile,c.source_line sourceLine,json_extract(c.evidence_json,'$.receiver') receiverName,c.operation_path_expr operationPath,b.alias bindingAlias,b.alias_expr bindingAliasExpr,b.service_path_expr servicePathExpr,b.destination_expr destinationExpr,req.service_path requireServicePath,req.destination requireDestination,COALESCE(e.status,'missing_edge') persistedStatus,
1373
+ CASE
1374
+ WHEN (b.alias_expr LIKE '%$%' OR b.service_path_expr LIKE '%$%' OR b.destination_expr LIKE '%$%') THEN 'runtime_variables_required'
1375
+ WHEN b.alias IS NOT NULL AND req.id IS NULL AND b.service_path_expr IS NULL THEN 'alias_without_matching_cds_requires'
1376
+ WHEN req.id IS NOT NULL AND COALESCE(e.status,'missing_edge')!='resolved' THEN 'cds_requires_present_but_persisted_resolution_unresolved'
1377
+ ELSE 'trace_time_contextual_binding_candidate'
1378
+ END contextualStatus
1379
+ FROM outbound_calls c
1380
+ LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1381
+ LEFT JOIN service_bindings b ON b.id=c.service_binding_id
1382
+ LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias
1383
+ WHERE c.call_type='remote_action' AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL
1384
+ AND (c.service_binding_id IS NULL OR e.status IS NULL OR e.status!='resolved')
1385
+ AND EXISTS (SELECT 1 FROM symbol_calls sc WHERE sc.status='resolved' AND sc.source_file=c.source_file)
1386
+ ORDER BY c.source_file,c.source_line LIMIT 8`).all();
1387
+ const statusRows = db.prepare(`SELECT contextualStatus,COUNT(*) count FROM (
1388
+ SELECT CASE
1389
+ WHEN (b.alias_expr LIKE '%$%' OR b.service_path_expr LIKE '%$%' OR b.destination_expr LIKE '%$%') THEN 'runtime_variables_required'
1390
+ WHEN b.alias IS NOT NULL AND req.id IS NULL AND b.service_path_expr IS NULL THEN 'alias_without_matching_cds_requires'
1391
+ WHEN req.id IS NOT NULL AND COALESCE(e.status,'missing_edge')!='resolved' THEN 'cds_requires_present_but_persisted_resolution_unresolved'
1392
+ ELSE 'trace_time_contextual_binding_candidate'
1393
+ END contextualStatus
1394
+ FROM outbound_calls c
1395
+ LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1396
+ LEFT JOIN service_bindings b ON b.id=c.service_binding_id
1397
+ LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias
1398
+ WHERE c.call_type='remote_action' AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL
1399
+ AND (c.service_binding_id IS NULL OR e.status IS NULL OR e.status!='resolved')
1400
+ AND EXISTS (SELECT 1 FROM symbol_calls sc WHERE sc.status='resolved' AND sc.source_file=c.source_file)
1401
+ ) GROUP BY contextualStatus ORDER BY count DESC,contextualStatus`).all();
1402
+ const resolvedContextual = db.prepare(`SELECT COUNT(*) count FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT) WHERE c.call_type='remote_action' AND e.status='resolved' AND c.service_binding_id IS NOT NULL`).get();
1403
+ return { severity: Number(missingMetadata.count ?? 0) + Number(destructuredUnmapped.count ?? 0) + opportunities.length > 0 ? "warning" : "info", code: "strict_contextual_binding_propagation_quality", message: "Contextual service-client propagation opportunities for trace-time helper resolution", localSymbolCallsWithServiceClientArguments: Number(serviceClientCalls.count ?? 0), calleeSymbolsMissingParameterMetadata: Number(missingMetadata.count ?? 0), destructuredObjectParametersPossiblyUnmapped: Number(destructuredUnmapped.count ?? 0), contextualHelperSendsResolvedDuringPersistedLink: Number(resolvedContextual.count ?? 0), traceTimeContextualOpportunities: opportunities.length, traceTimeContextualOpportunityBreakdown: statusRows.length > 0 ? statusRows : [{ contextualStatus: "no_contextual_opportunity", count: 0 }], examples: opportunities };
1404
+ }
1405
+ function nestedThisReceiverQuality(db) {
1406
+ const aggregate = db.prepare(`SELECT COUNT(*) total,
1407
+ SUM(CASE WHEN json_extract(evidence_json,'$.relation')='indexed_this_method' THEN 1 ELSE 0 END) resolvedToCurrentClass,
1408
+ SUM(CASE WHEN json_extract(evidence_json,'$.relation')='class_instance_method' THEN 1 ELSE 0 END) withExplicitHelperInstanceEvidence
1409
+ FROM symbol_calls WHERE callee_expression LIKE 'this.%.%'`).get();
1410
+ const examples = db.prepare(`SELECT source_file sourceFile,source_line sourceLine,callee_expression calleeExpression,json_extract(evidence_json,'$.relation') relation,json_extract(evidence_json,'$.targetName') targetName
1411
+ FROM symbol_calls WHERE callee_expression LIKE 'this.%.%' AND json_extract(evidence_json,'$.relation')='indexed_this_method'
1412
+ ORDER BY source_file,source_line LIMIT 8`).all();
1413
+ return { severity: Number(aggregate.resolvedToCurrentClass ?? 0) > 0 ? "warning" : "info", code: "strict_nested_this_receiver_quality", message: "Nested this receiver symbol-call aggregate", nestedThisReceiverCallsConsidered: Number(aggregate.total ?? 0), nestedThisResolvedToCurrentClass: Number(aggregate.resolvedToCurrentClass ?? 0), nestedThisWithExplicitHelperInstanceEvidence: Number(aggregate.withExplicitHelperInstanceEvidence ?? 0), warningExamples: examples };
1365
1414
  }
1366
1415
  function contextualImplementationQuality(db) {
1367
1416
  const rows = db.prepare(`SELECT status,COALESCE(unresolved_reason,status) reason,COUNT(*) count