@saptools/service-flow 0.1.22 → 0.1.24
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 +12 -0
- package/dist/{chunk-JIY37C44.js → chunk-RXJNPONU.js} +181 -50
- package/dist/chunk-RXJNPONU.js.map +1 -0
- package/dist/cli.js +69 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-JIY37C44.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.24
|
|
4
|
+
|
|
5
|
+
- Add conservative class instance method symbol-call resolution for same-file and relatively imported helper classes, with auditable class-instance evidence.
|
|
6
|
+
- Propagate service-client binding context through resolved local symbol calls for explicit positional and object-literal helper arguments during trace rendering.
|
|
7
|
+
- Harden one-hop higher-order send wrapper literal path propagation for returned async closures and expose wrapper definition evidence.
|
|
8
|
+
- Refine strict doctor categories for no-binding remote actions and split ambiguous versus unresolved implementation diagnostics with capped examples.
|
|
9
|
+
|
|
10
|
+
## 0.1.23
|
|
11
|
+
|
|
12
|
+
- Retrospective note: preserve same-file late-assignment service binding extraction for connected clients, `cds.connect.to(...)` assignments, identity aliases, and simple transaction aliases before outbound `send(...)` calls.
|
|
13
|
+
- Retrospective note: preserve explicit object destructuring from safe helper-returned client objects so binding rows remain available for downstream remote action linking.
|
|
14
|
+
|
|
3
15
|
## 0.1.22
|
|
4
16
|
|
|
5
17
|
- Propagate conservative same-file identity aliases of connected service clients, including typed, `as`, `satisfies`, helper-returned, and transitive aliases, while preserving `.tx()` alias evidence.
|
|
@@ -642,28 +642,25 @@ 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
|
-
|
|
645
|
+
const closure = returnedClosure(fn) ?? fn;
|
|
646
|
+
const sends = [];
|
|
646
647
|
const visit = (node) => {
|
|
647
|
-
if (found) return;
|
|
648
|
-
if (node !== fn && (ts4.isFunctionDeclaration(node) || ts4.isArrowFunction(node) || ts4.isFunctionExpression(node))) {
|
|
649
|
-
ts4.forEachChild(node, visit);
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
648
|
if (ts4.isCallExpression(node) && ts4.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send" && ts4.isIdentifier(node.expression.expression)) {
|
|
653
649
|
const objectArg = node.arguments[0];
|
|
654
650
|
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
655
651
|
const pathProp = objectArg.properties.find((property) => ts4.isShorthandPropertyAssignment(property) && property.name.text === "path");
|
|
656
|
-
if (pathProp)
|
|
652
|
+
if (pathProp) sends.push({ client: node.expression.expression.text, path: pathProp.name.text, method: objectPropertyText(objectArg, "method") });
|
|
657
653
|
}
|
|
658
654
|
}
|
|
659
655
|
ts4.forEachChild(node, visit);
|
|
660
656
|
};
|
|
661
|
-
visit(
|
|
662
|
-
if (
|
|
657
|
+
visit(closure);
|
|
658
|
+
if (sends.length !== 1) return;
|
|
659
|
+
const found = sends[0];
|
|
663
660
|
const clientIndex = params.indexOf(found.client);
|
|
664
661
|
const pathIndex = params.indexOf(found.path);
|
|
665
662
|
const methodIndex = found.method && params.includes(found.method) ? params.indexOf(found.method) : void 0;
|
|
666
|
-
if (clientIndex >= 0 && pathIndex >= 0) specs.set(name, { clientIndex, pathIndex, methodIndex });
|
|
663
|
+
if (clientIndex >= 0 && pathIndex >= 0) specs.set(name, { clientIndex, pathIndex, methodIndex, definitionLine: lineOf3(source.text, fn.getStart(source)) });
|
|
667
664
|
};
|
|
668
665
|
for (const stmt of source.statements) {
|
|
669
666
|
if (ts4.isFunctionDeclaration(stmt) && stmt.name) scanFunction(stmt.name.text, stmt);
|
|
@@ -673,6 +670,16 @@ function collectWrapperSpecs(source) {
|
|
|
673
670
|
}
|
|
674
671
|
return specs;
|
|
675
672
|
}
|
|
673
|
+
function returnedClosure(fn) {
|
|
674
|
+
const body = fn.body;
|
|
675
|
+
if (!body) return void 0;
|
|
676
|
+
if (ts4.isArrowFunction(fn) && !ts4.isBlock(body)) return ts4.isArrowFunction(body) || ts4.isFunctionExpression(body) ? body : void 0;
|
|
677
|
+
if (!ts4.isBlock(body)) return void 0;
|
|
678
|
+
const returns = body.statements.filter(ts4.isReturnStatement);
|
|
679
|
+
if (returns.length !== 1) return void 0;
|
|
680
|
+
const expr = returns[0]?.expression;
|
|
681
|
+
return expr && (ts4.isArrowFunction(expr) || ts4.isFunctionExpression(expr)) ? expr : void 0;
|
|
682
|
+
}
|
|
676
683
|
function classifyOutboundCallsInSource(source, filePath) {
|
|
677
684
|
const calls = [];
|
|
678
685
|
const sourceFile = normalizePath(filePath);
|
|
@@ -691,22 +698,24 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
691
698
|
const entity = arg ? queryEntityFromAst(arg, initializers) : void 0;
|
|
692
699
|
const payload = arg?.getText(source) ?? "";
|
|
693
700
|
add(node, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) });
|
|
694
|
-
} else if (ts4.isPropertyAccessExpression(expr) && expr.name.text === "send" && ts4.isIdentifier(expr.expression)) {
|
|
701
|
+
} else if (ts4.isPropertyAccessExpression(expr) && expr.name.text === "send" && (ts4.isIdentifier(expr.expression) || ts4.isPropertyAccessExpression(expr.expression))) {
|
|
695
702
|
const objectArg = node.arguments[0];
|
|
696
703
|
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
697
|
-
const receiver = expr.expression
|
|
704
|
+
const receiver = receiverName(expr.expression);
|
|
698
705
|
const query = objectPropertyText(objectArg, "query");
|
|
699
706
|
const op = objectPropertyText(objectArg, "path") ?? objectPropertyText(objectArg, "event");
|
|
700
707
|
const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
|
|
701
708
|
add(node, { callType: query ? "remote_query" : "remote_action", serviceVariableName: receiver, method: stripQuotes(objectPropertyText(objectArg, "method") ?? "POST"), operationPathExpr: op && !shorthandPath ? `/${stripQuotes(op).replace(/^\//, "")}` : void 0, queryEntity: query ? extractQueryEntity(query) : void 0, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason: !query && shorthandPath ? "dynamic_operation_path_identifier" : void 0 }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, parserWarning: shorthandPath ? "dynamic_operation_path_identifier" : void 0 });
|
|
702
709
|
}
|
|
703
|
-
} else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text)) {
|
|
704
|
-
const
|
|
705
|
-
const
|
|
706
|
-
const
|
|
707
|
-
const
|
|
710
|
+
} else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts4.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
|
|
711
|
+
const wrapperName = ts4.isIdentifier(expr) ? expr.text : ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) ? expr.expression.text : "";
|
|
712
|
+
const wrapperArgs = ts4.isIdentifier(expr) ? node.arguments : ts4.isCallExpression(expr) ? expr.arguments : node.arguments;
|
|
713
|
+
const spec = wrapperSpecs.get(wrapperName);
|
|
714
|
+
const clientArg = spec ? wrapperArgs[spec.clientIndex] : void 0;
|
|
715
|
+
const pathArg = spec ? wrapperArgs[spec.pathIndex] : void 0;
|
|
716
|
+
const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
|
|
708
717
|
if (spec && clientArg && ts4.isIdentifier(clientArg) && isStringLike(pathArg)) {
|
|
709
|
-
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: "
|
|
718
|
+
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 });
|
|
710
719
|
}
|
|
711
720
|
} else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
|
|
712
721
|
const receiver = receiverName(expr.expression);
|
|
@@ -859,13 +868,17 @@ function connectFactFromCall(call) {
|
|
|
859
868
|
}
|
|
860
869
|
function unwrapCall(expr) {
|
|
861
870
|
if (ts5.isAwaitExpression(expr)) return unwrapCall(expr.expression);
|
|
871
|
+
if (ts5.isParenthesizedExpression(expr)) return unwrapCall(expr.expression);
|
|
862
872
|
if (ts5.isAsExpression(expr) || ts5.isSatisfiesExpression(expr)) return unwrapCall(expr.expression);
|
|
873
|
+
if (ts5.isTypeAssertionExpression(expr)) return unwrapCall(expr.expression);
|
|
863
874
|
if (ts5.isCallExpression(expr)) return expr;
|
|
864
875
|
return void 0;
|
|
865
876
|
}
|
|
866
877
|
function unwrapIdentityExpression(expr) {
|
|
867
878
|
if (ts5.isAwaitExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
879
|
+
if (ts5.isParenthesizedExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
868
880
|
if (ts5.isAsExpression(expr) || ts5.isSatisfiesExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
881
|
+
if (ts5.isTypeAssertionExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
869
882
|
return expr;
|
|
870
883
|
}
|
|
871
884
|
function findConnectInExpression(expr) {
|
|
@@ -1129,18 +1142,17 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1129
1142
|
const sourceFile = normalizePath(filePath);
|
|
1130
1143
|
return [...out].reverse().find((row) => row.variableName === variableName && row.sourceFile === sourceFile);
|
|
1131
1144
|
}
|
|
1132
|
-
function cloneAliasBinding(
|
|
1133
|
-
if (!ts5.isIdentifier(decl.name)) return;
|
|
1145
|
+
function cloneAliasBinding(targetName, sourceName, aliasKind, node) {
|
|
1134
1146
|
const existing = bindingForVariable(sourceName);
|
|
1135
1147
|
if (!existing) return;
|
|
1136
1148
|
out.push({
|
|
1137
1149
|
...existing,
|
|
1138
|
-
variableName:
|
|
1139
|
-
sourceLine: lineOf4(sourceFileAst,
|
|
1150
|
+
variableName: targetName,
|
|
1151
|
+
sourceLine: lineOf4(sourceFileAst, node),
|
|
1140
1152
|
helperChain: [
|
|
1141
1153
|
...existing.helperChain ?? [],
|
|
1142
1154
|
{
|
|
1143
|
-
callerVariable:
|
|
1155
|
+
callerVariable: targetName,
|
|
1144
1156
|
aliasOf: sourceName,
|
|
1145
1157
|
aliasKind,
|
|
1146
1158
|
scopeRule: "same-file-source-order"
|
|
@@ -1152,25 +1164,25 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1152
1164
|
if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
|
|
1153
1165
|
const unwrapped = unwrapIdentityExpression(decl.initializer);
|
|
1154
1166
|
if (!ts5.isIdentifier(unwrapped)) return;
|
|
1155
|
-
cloneAliasBinding(decl, unwrapped.text, "identity");
|
|
1167
|
+
cloneAliasBinding(decl.name.text, unwrapped.text, "identity", decl);
|
|
1156
1168
|
}
|
|
1157
|
-
async function
|
|
1158
|
-
|
|
1159
|
-
const call = unwrapCall(decl.initializer);
|
|
1169
|
+
async function recordBindingFromExpression(targetName, expr, node, aliasKind) {
|
|
1170
|
+
const call = unwrapCall(expr);
|
|
1160
1171
|
if (!call) return;
|
|
1161
1172
|
const direct = connectFactFromCall(call);
|
|
1162
1173
|
if (direct)
|
|
1163
1174
|
out.push({
|
|
1164
|
-
variableName:
|
|
1175
|
+
variableName: targetName,
|
|
1165
1176
|
...direct,
|
|
1166
1177
|
sourceFile: normalizePath(filePath),
|
|
1167
|
-
sourceLine: lineOf4(sourceFileAst,
|
|
1178
|
+
sourceLine: lineOf4(sourceFileAst, node),
|
|
1179
|
+
helperChain: aliasKind === "assignment" ? [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind, scopeRule: "same-file-source-order" }] : void 0
|
|
1168
1180
|
});
|
|
1169
1181
|
else if (ts5.isIdentifier(call.expression)) {
|
|
1170
1182
|
const resolved = await importedHelper(call.expression.text);
|
|
1171
1183
|
if (resolved)
|
|
1172
1184
|
out.push({
|
|
1173
|
-
variableName:
|
|
1185
|
+
variableName: targetName,
|
|
1174
1186
|
alias: resolved.helper.alias,
|
|
1175
1187
|
aliasExpr: resolved.helper.aliasExpr,
|
|
1176
1188
|
destinationExpr: resolved.helper.destinationExpr,
|
|
@@ -1178,10 +1190,11 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1178
1190
|
isDynamic: resolved.helper.isDynamic,
|
|
1179
1191
|
placeholders: resolved.helper.placeholders,
|
|
1180
1192
|
sourceFile: normalizePath(filePath),
|
|
1181
|
-
sourceLine: lineOf4(sourceFileAst,
|
|
1193
|
+
sourceLine: lineOf4(sourceFileAst, node),
|
|
1182
1194
|
helperChain: [
|
|
1183
1195
|
{
|
|
1184
|
-
callerVariable:
|
|
1196
|
+
callerVariable: targetName,
|
|
1197
|
+
...aliasKind === "assignment" ? { assignedFrom: call.expression.text, aliasKind, scopeRule: "same-file-source-order" } : {},
|
|
1185
1198
|
importedHelper: call.expression.text,
|
|
1186
1199
|
importSource: resolved.imp.sourceFile,
|
|
1187
1200
|
exportedSymbol: resolved.imp.exportedName,
|
|
@@ -1192,6 +1205,10 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1192
1205
|
});
|
|
1193
1206
|
}
|
|
1194
1207
|
}
|
|
1208
|
+
async function recordVariable(decl) {
|
|
1209
|
+
if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
|
|
1210
|
+
await recordBindingFromExpression(decl.name.text, decl.initializer, decl, "declaration");
|
|
1211
|
+
}
|
|
1195
1212
|
async function helpersForCall(call) {
|
|
1196
1213
|
if (!ts5.isIdentifier(call.expression)) return [];
|
|
1197
1214
|
const local = localObjectHelpers.get(call.expression.text) ?? [];
|
|
@@ -1224,6 +1241,39 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1224
1241
|
});
|
|
1225
1242
|
}
|
|
1226
1243
|
}
|
|
1244
|
+
async function recordDestructuredAssignment(pattern, expr, node) {
|
|
1245
|
+
const call = unwrapCall(expr);
|
|
1246
|
+
if (!call) return;
|
|
1247
|
+
const helpers = await helpersForCall(call);
|
|
1248
|
+
if (helpers.length === 0) return;
|
|
1249
|
+
for (const prop of pattern.properties) {
|
|
1250
|
+
let propertyName;
|
|
1251
|
+
let targetName;
|
|
1252
|
+
if (ts5.isShorthandPropertyAssignment(prop)) {
|
|
1253
|
+
propertyName = prop.name.text;
|
|
1254
|
+
targetName = prop.name.text;
|
|
1255
|
+
} else if (ts5.isPropertyAssignment(prop)) {
|
|
1256
|
+
propertyName = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
1257
|
+
targetName = ts5.isIdentifier(prop.initializer) ? prop.initializer.text : void 0;
|
|
1258
|
+
}
|
|
1259
|
+
if (!propertyName || !targetName) continue;
|
|
1260
|
+
const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
|
|
1261
|
+
if (matches.length !== 1) continue;
|
|
1262
|
+
const resolved = matches[0];
|
|
1263
|
+
out.push({
|
|
1264
|
+
variableName: targetName,
|
|
1265
|
+
alias: resolved.helper.alias,
|
|
1266
|
+
aliasExpr: resolved.helper.aliasExpr,
|
|
1267
|
+
destinationExpr: resolved.helper.destinationExpr,
|
|
1268
|
+
servicePathExpr: resolved.helper.servicePathExpr,
|
|
1269
|
+
isDynamic: resolved.helper.isDynamic,
|
|
1270
|
+
placeholders: resolved.helper.placeholders,
|
|
1271
|
+
sourceFile: normalizePath(filePath),
|
|
1272
|
+
sourceLine: lineOf4(sourceFileAst, node),
|
|
1273
|
+
helperChain: [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName, importSource: resolved.imp?.sourceFile, exportedSymbol: resolved.imp?.exportedName, helperSourceFile: resolved.helper.sourceFile, helperSourceLine: resolved.helper.sourceLine }]
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1227
1277
|
function recordDestructuredClassHelper(decl) {
|
|
1228
1278
|
if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
|
|
1229
1279
|
const call = unwrapCall(decl.initializer);
|
|
@@ -1256,20 +1306,40 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1256
1306
|
});
|
|
1257
1307
|
}
|
|
1258
1308
|
}
|
|
1259
|
-
const
|
|
1260
|
-
function
|
|
1261
|
-
if (ts5.isVariableDeclaration(node))
|
|
1262
|
-
ts5.
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1309
|
+
const events = [];
|
|
1310
|
+
function collectEvents(node) {
|
|
1311
|
+
if (ts5.isVariableDeclaration(node)) events.push({ pos: node.getStart(sourceFileAst), node });
|
|
1312
|
+
if (ts5.isBinaryExpression(node) && node.operatorToken.kind === ts5.SyntaxKind.EqualsToken)
|
|
1313
|
+
events.push({ pos: node.getStart(sourceFileAst), node });
|
|
1314
|
+
ts5.forEachChild(node, collectEvents);
|
|
1315
|
+
}
|
|
1316
|
+
collectEvents(sourceFileAst);
|
|
1317
|
+
events.sort((a, b) => a.pos - b.pos);
|
|
1318
|
+
for (const event of events) {
|
|
1319
|
+
if (ts5.isVariableDeclaration(event.node)) {
|
|
1320
|
+
const decl = event.node;
|
|
1321
|
+
await recordDestructuredHelper(decl);
|
|
1322
|
+
recordDestructuredClassHelper(decl);
|
|
1323
|
+
await recordVariable(decl);
|
|
1324
|
+
recordIdentityAlias(decl);
|
|
1325
|
+
if (ts5.isIdentifier(decl.name) && decl.initializer && ts5.isCallExpression(decl.initializer) && ts5.isPropertyAccessExpression(decl.initializer.expression) && decl.initializer.expression.name.text === "tx" && ts5.isIdentifier(decl.initializer.expression.expression)) {
|
|
1326
|
+
cloneAliasBinding(decl.name.text, decl.initializer.expression.expression.text, "transaction", decl);
|
|
1327
|
+
}
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
const assignment = event.node;
|
|
1331
|
+
if (ts5.isIdentifier(assignment.left)) {
|
|
1332
|
+
const rhs = unwrapIdentityExpression(assignment.right);
|
|
1333
|
+
if (ts5.isIdentifier(rhs)) {
|
|
1334
|
+
cloneAliasBinding(assignment.left.text, rhs.text, "identity-assignment", assignment);
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
await recordBindingFromExpression(assignment.left.text, assignment.right, assignment, "assignment");
|
|
1338
|
+
continue;
|
|
1272
1339
|
}
|
|
1340
|
+
const left = ts5.isParenthesizedExpression(assignment.left) ? assignment.left.expression : assignment.left;
|
|
1341
|
+
if (ts5.isObjectLiteralExpression(left))
|
|
1342
|
+
await recordDestructuredAssignment(left, assignment.right, assignment);
|
|
1273
1343
|
}
|
|
1274
1344
|
return out;
|
|
1275
1345
|
}
|
|
@@ -2176,6 +2246,65 @@ function runtimeResolution(db, row, evidence, vars) {
|
|
|
2176
2246
|
const unresolvedReason = resolution.status === "dynamic" ? `Dynamic target is missing runtime variables: ${resolution.reasons.join(", ")}` : resolution.status === "ambiguous" ? "Ambiguous runtime operation candidates" : "No runtime operation candidate matched substituted service and operation path";
|
|
2177
2247
|
return { row, evidence: nextEvidence, unresolvedReason };
|
|
2178
2248
|
}
|
|
2249
|
+
function parseEvidence(value) {
|
|
2250
|
+
try {
|
|
2251
|
+
const parsed = JSON.parse(String(value || "{}"));
|
|
2252
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
2253
|
+
} catch {
|
|
2254
|
+
return {};
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
function receiverFromEvidence(value) {
|
|
2258
|
+
const evidence = parseEvidence(value);
|
|
2259
|
+
return typeof evidence.receiver === "string" ? evidence.receiver : void 0;
|
|
2260
|
+
}
|
|
2261
|
+
function knownBindingsForCalls(db, calls) {
|
|
2262
|
+
const map = /* @__PURE__ */ new Map();
|
|
2263
|
+
for (const call of calls) {
|
|
2264
|
+
const receiver = receiverFromEvidence(call.evidence_json);
|
|
2265
|
+
const bindingId = Number(call.service_binding_id ?? 0);
|
|
2266
|
+
if (!receiver || !bindingId) continue;
|
|
2267
|
+
const row = db.prepare("SELECT id,alias,alias_expr aliasExpr,destination_expr destinationExpr,service_path_expr servicePathExpr,source_file sourceFile,source_line sourceLine FROM service_bindings WHERE id=?").get(bindingId);
|
|
2268
|
+
if (row) map.set(receiver, { ...row, bindingId, source: "local_service_binding", calleeReceiver: receiver });
|
|
2269
|
+
}
|
|
2270
|
+
return map;
|
|
2271
|
+
}
|
|
2272
|
+
function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
2273
|
+
const next = /* @__PURE__ */ new Map();
|
|
2274
|
+
if (callerBindings.size === 0) return next;
|
|
2275
|
+
const callEvidence2 = parseEvidence(symbolCall.evidence_json);
|
|
2276
|
+
const callee = db.prepare("SELECT evidence_json evidenceJson FROM symbols WHERE id=?").get(symbolCall.callee_symbol_id);
|
|
2277
|
+
const calleeEvidence = parseEvidence(callee?.evidenceJson);
|
|
2278
|
+
const params = Array.isArray(calleeEvidence.parameters) ? calleeEvidence.parameters.filter((item) => typeof item === "string") : [];
|
|
2279
|
+
const args = Array.isArray(callEvidence2.callArguments) ? callEvidence2.callArguments : [];
|
|
2280
|
+
args.forEach((arg, index) => {
|
|
2281
|
+
const param = params[index];
|
|
2282
|
+
if (!param) return;
|
|
2283
|
+
if (arg.kind === "identifier" && typeof arg.name === "string") {
|
|
2284
|
+
const binding = callerBindings.get(arg.name);
|
|
2285
|
+
if (binding) next.set(param, { ...binding, source: "local_symbol_argument", callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
|
|
2286
|
+
}
|
|
2287
|
+
if (arg.kind === "object_literal" && Array.isArray(arg.properties)) {
|
|
2288
|
+
for (const prop of arg.properties) {
|
|
2289
|
+
if (typeof prop.property !== "string" || typeof prop.argument !== "string") continue;
|
|
2290
|
+
const binding = callerBindings.get(prop.argument);
|
|
2291
|
+
if (binding) next.set(`${param}.${prop.property}`, { ...binding, source: "local_symbol_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
return next;
|
|
2296
|
+
}
|
|
2297
|
+
function contextualRuntimeResolution(db, call, binding, workspaceId) {
|
|
2298
|
+
if (!binding || String(call.call_type) !== "remote_action" || call.operation_path_expr === void 0 || call.operation_path_expr === null) return {};
|
|
2299
|
+
const op = normalizeODataOperationInvocationPathForTrace(String(call.operation_path_expr));
|
|
2300
|
+
const resolution = resolveOperation(db, { servicePath: binding.servicePathExpr, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination: binding.destinationExpr, hasExplicitOverride: true, isDynamic: false }, workspaceId);
|
|
2301
|
+
const evidence = { contextualServiceBindingSelected: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine }, operationPath: op, servicePath: binding.servicePathExpr, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination: binding.destinationExpr, contextualResolutionStatus: resolution.status, candidates: resolution.candidates, resolutionReasons: resolution.reasons };
|
|
2302
|
+
if (!resolution.target) return { evidence, unresolvedReason: resolution.status === "ambiguous" ? "Ambiguous contextual operation candidates" : "No contextual operation candidate matched" };
|
|
2303
|
+
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(evidence), status: "resolved" }, evidence: { ...evidence, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName }, unresolvedReason: void 0 };
|
|
2304
|
+
}
|
|
2305
|
+
function normalizeODataOperationInvocationPathForTrace(raw) {
|
|
2306
|
+
return raw.startsWith("/") ? raw : `/${raw}`;
|
|
2307
|
+
}
|
|
2179
2308
|
function edgeTarget(row, evidence) {
|
|
2180
2309
|
const runtimeCandidate = evidence.runtimeResolvedCandidate;
|
|
2181
2310
|
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath)
|
|
@@ -2208,7 +2337,7 @@ function trace(db, start, options) {
|
|
|
2208
2337
|
const maxDepth = positiveDepth(options.depth);
|
|
2209
2338
|
const edges = [];
|
|
2210
2339
|
const nodes = /* @__PURE__ */ new Map();
|
|
2211
|
-
const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1 }] : [];
|
|
2340
|
+
const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1, context: /* @__PURE__ */ new Map() }] : [];
|
|
2212
2341
|
if (start.servicePath && (start.operation ?? start.operationPath)) {
|
|
2213
2342
|
const startOperation = normalizeOperation(start.operation ?? start.operationPath);
|
|
2214
2343
|
const startRows = db.prepare(`SELECT o.id operationId,r.name repoName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE (? IS NULL OR s.repo_id=?) AND s.service_path=? AND (o.operation_path=? OR o.operation_name=?) ORDER BY r.name,o.id LIMIT 2`).all(scope.repo?.id, scope.repo?.id, start.servicePath, startOperation, startOperation);
|
|
@@ -2241,6 +2370,7 @@ function trace(db, start, options) {
|
|
|
2241
2370
|
const filtered = calls.filter(
|
|
2242
2371
|
(c) => (!current.symbolIds || current.symbolIds.has(Number(c.source_symbol_id))) && (!current.files || current.files.has(String(c.source_file))) && includeCall(String(c.call_type), options)
|
|
2243
2372
|
);
|
|
2373
|
+
const callerBindings = new Map([...current.context ?? /* @__PURE__ */ new Map(), ...knownBindingsForCalls(db, filtered)]);
|
|
2244
2374
|
if (current.symbolIds && current.symbolIds.size > 0 && current.depth < maxDepth) {
|
|
2245
2375
|
const symbolRows = db.prepare(`SELECT sc.*,s.repo_id calleeRepoId,s.source_file calleeFile FROM symbol_calls sc LEFT JOIN symbols s ON s.id=sc.callee_symbol_id WHERE sc.caller_symbol_id IN (${[...current.symbolIds].map(() => "?").join(",")}) ORDER BY sc.source_file,sc.source_line`).all(...current.symbolIds);
|
|
2246
2376
|
for (const symbolCall of symbolRows) {
|
|
@@ -2254,7 +2384,7 @@ function trace(db, start, options) {
|
|
|
2254
2384
|
const evidence = { ...JSON.parse(String(symbolCall.evidence_json || "{}")), sourceFile: symbolCall.source_file, sourceLine: symbolCall.source_line, calleeSymbolId: symbolCall.callee_symbol_id, calleeSymbolName: calleeNode?.symbolName, calleeSymbolFile: calleeNode?.sourceFile, resolutionStatus: symbolCall.status };
|
|
2255
2385
|
edges.push({ step: current.depth, type: "local_symbol_call", from: String(symbolCall.callee_expression), to: calleeNode?.label ? String(calleeNode.label) : `symbol:${String(symbolCall.callee_symbol_id)}`, evidence, confidence: Number(symbolCall.confidence ?? 0.8), unresolvedReason: String(symbolCall.status) === "resolved" ? void 0 : symbolCall.unresolved_reason ? String(symbolCall.unresolved_reason) : void 0 });
|
|
2256
2386
|
if (seenScopes.has(nextKey)) edges.push({ step: current.depth, type: "cycle", from: String(symbolCall.callee_expression), to: nextKey, evidence: { cycle: true, symbolCallId: symbolCall.id }, confidence: 1, unresolvedReason: "Cycle detected; downstream symbol already visited" });
|
|
2257
|
-
else queue.push({ repoId: nextRepoId, files: nextFiles, symbolIds: nextSymbols, depth: current.depth + 1 });
|
|
2387
|
+
else queue.push({ repoId: nextRepoId, files: nextFiles, symbolIds: nextSymbols, depth: current.depth + 1, context: contextForSymbolCall(db, symbolCall, callerBindings) });
|
|
2258
2388
|
}
|
|
2259
2389
|
}
|
|
2260
2390
|
const graph = graphForCalls(
|
|
@@ -2271,11 +2401,12 @@ function trace(db, start, options) {
|
|
|
2271
2401
|
line: call.source_line,
|
|
2272
2402
|
callType: call.call_type
|
|
2273
2403
|
});
|
|
2274
|
-
const
|
|
2404
|
+
const contextual = contextualRuntimeResolution(db, call, (current.context ?? /* @__PURE__ */ new Map()).get(receiverFromEvidence(call.evidence_json) ?? ""), workspaceIdForCall(db, String(call.id)));
|
|
2405
|
+
const graphRows = contextual.row ? [contextual.row] : graph.get(Number(call.id)) ?? [];
|
|
2275
2406
|
for (const row of graphRows) {
|
|
2276
2407
|
if (seenEdges.has(Number(row.id))) continue;
|
|
2277
2408
|
seenEdges.add(Number(row.id));
|
|
2278
|
-
const rawEvidence = JSON.parse(
|
|
2409
|
+
const rawEvidence = contextual.evidence && row.id < 0 ? contextual.evidence : JSON.parse(
|
|
2279
2410
|
String(row.evidence_json || "{}")
|
|
2280
2411
|
);
|
|
2281
2412
|
const effective = runtimeResolution(db, row, rawEvidence, options.vars);
|
|
@@ -2372,4 +2503,4 @@ export {
|
|
|
2372
2503
|
linkWorkspace,
|
|
2373
2504
|
trace
|
|
2374
2505
|
};
|
|
2375
|
-
//# sourceMappingURL=chunk-
|
|
2506
|
+
//# sourceMappingURL=chunk-RXJNPONU.js.map
|