@saptools/service-flow 0.1.21 → 0.1.22
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 +8 -0
- package/README.md +3 -3
- package/TECHNICAL-NOTE.md +8 -0
- package/dist/{chunk-TSMDIKML.js → chunk-JIY37C44.js} +104 -10
- package/dist/chunk-JIY37C44.js.map +1 -0
- package/dist/cli.js +42 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-TSMDIKML.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.22
|
|
4
|
+
|
|
5
|
+
- Propagate conservative same-file identity aliases of connected service clients, including typed, `as`, `satisfies`, helper-returned, and transitive aliases, while preserving `.tx()` alias evidence.
|
|
6
|
+
- Link outbound `send(...)` calls through identity aliases to the closest same-file binding row so destination, service-path, placeholder, and helper-chain evidence remains available.
|
|
7
|
+
- Add narrow same-file wrapper literal-path propagation for wrappers that pass a client parameter directly to `send({ path })`; dynamic wrapper paths remain semantic dynamic targets with parser warnings.
|
|
8
|
+
- Improve trace-time contextual implementation selection evidence and suppress stale ambiguous unresolved reasons when a unique contextual handler is selected.
|
|
9
|
+
- Expand `doctor --strict` with alias-binding, no-binding remote-action, contextual implementation, and wrapper path propagation quality aggregates.
|
|
10
|
+
|
|
3
11
|
## 0.1.21
|
|
4
12
|
|
|
5
13
|
- Propagate helper-returned connected clients from function declarations, arrow-function variables, function-expression variables, named export lists, and aliased exports into caller destructuring and transaction aliases.
|
package/README.md
CHANGED
|
@@ -318,7 +318,7 @@ service-flow trace --workspace /path/to/workspace --repo facade-service --operat
|
|
|
318
318
|
|
|
319
319
|
When a concrete target exists after variable substitution, the trace shows both the parameterized evidence and the resolved match. When it does not, `service-flow` keeps the edge as a dynamic candidate or unresolved edge so the missing link remains visible.
|
|
320
320
|
|
|
321
|
-
Service-binding evidence keeps these fields distinct: service alias, alias expression, destination expression, service-path expression, operation-path expression, and runtime placeholders. Helpers that return concrete connected clients inside object properties are followed through destructuring and
|
|
321
|
+
Service-binding evidence keeps these fields distinct: service alias, alias expression, destination expression, service-path expression, operation-path expression, and runtime placeholders. Helpers that return concrete connected clients inside object properties are followed through destructuring, simple identity aliases, transitive same-file aliases, and transaction aliases while preserving helper-chain evidence. This is important for common CAP helpers such as `cds.connect.to(`remote_${code}`, { credentials: { destination: `remote_${code}`, path: `/${entityType}ProcessService` } })`, where the alias is not the service path.
|
|
322
322
|
|
|
323
323
|
By default, production traces should be built from production source files. Keep generated credentials and local state out of git, and use explicit fixture/test workspaces when validating test-only mocked service clients so they do not pollute production graph interpretation.
|
|
324
324
|
|
|
@@ -418,8 +418,8 @@ Run `service-flow index` after source, CDS, package metadata, or helper-package
|
|
|
418
418
|
<details>
|
|
419
419
|
<summary><b>Why is an expected call unresolved?</b></summary>
|
|
420
420
|
|
|
421
|
-
Check `service-flow doctor`, then inspect the facts with `service-flow list services`, `service-flow list operations`, and `service-flow list calls`. Dynamic destinations may need `--var key=value`; the key is the full trimmed expression inside `${...}` and is matched literally without JavaScript evaluation. Operation-path-only ambiguous remote actions usually mean the call had no service binding id; inspect `list calls`, `inspect operation`, and `doctor --strict` to determine whether helper-return propagation or wrapper support is missing. Contextual implementation selection only continues into a handler when static evidence such as caller repository, resolved service path, destination/alias expression, dependency edges, registration package, and local service ownership makes exactly one candidate stronger; ties remain ambiguous with reasons.
|
|
422
|
-
Default doctor output is intended to focus on actionable indexing or trace-impacting issues; use `--strict` when you need exhaustive model-shape diagnostics for entity-only or extension-heavy CDS models. Strict mode also reports normalized OData invocation ambiguity
|
|
421
|
+
Check `service-flow doctor`, then inspect the facts with `service-flow list services`, `service-flow list operations`, and `service-flow list calls`. Dynamic destinations may need `--var key=value`; the key is the full trimmed expression inside `${...}` and is matched literally without JavaScript evaluation. Operation-path-only ambiguous remote actions usually mean the call had no service binding id; same-file identity aliases are propagated, while property/call-expression aliases and ambiguous wrapper flows remain conservative; inspect `list calls`, `inspect operation`, and `doctor --strict` to determine whether helper-return propagation or wrapper support is missing. Contextual implementation selection only continues into a handler when static evidence such as caller repository, resolved service path, destination/alias expression, dependency edges, registration package, and local service ownership makes exactly one candidate stronger; ties remain ambiguous with reasons.
|
|
422
|
+
Default doctor output is intended to focus on actionable indexing or trace-impacting issues; use `--strict` when you need exhaustive model-shape diagnostics for entity-only or extension-heavy CDS models. Strict mode also reports normalized OData invocation ambiguity, remote-action target quality, likely missed identity aliases, no-binding remote actions, contextual implementation stops, and wrapper dynamic-path candidates, including whether unresolved unknown/dynamic paths are semantic instead of numeric call ids.
|
|
423
423
|
|
|
424
424
|
</details>
|
|
425
425
|
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Service Flow Resolution Notes
|
|
2
2
|
|
|
3
|
+
## 0.1.22 alias, wrapper, contextual trace, and strict doctor notes
|
|
4
|
+
|
|
5
|
+
- Same-file service-client identity aliases now create their own service binding rows when the right-hand side is a known connected client variable. The parser accepts direct identifier aliases plus typed, `as`, and `satisfies` forms, supports source-order transitive aliases, and records `aliasKind: identity` helper-chain evidence. It still does not infer aliases from property reads, indexed access, function calls, object metadata, or cross-scope guesses.
|
|
6
|
+
- Outbound calls through identity aliases can attach to the closest alias binding row in the same source file, preserving alias, destination, service path, placeholder, and helper-chain evidence for link and trace.
|
|
7
|
+
- Wrapper path propagation is intentionally narrow: same-file wrappers can resolve literal caller paths only when a wrapper client parameter is passed directly to `.send(...)` and a wrapper path parameter is passed as `send({ path })`. Dynamic caller paths remain dynamic with parser-warning evidence.
|
|
8
|
+
- Contextual implementation selection now records selected/tied score evidence. When trace selects a unique contextual handler, the implementation hop sets `contextualImplementationSelected` and no longer renders the original ambiguous reason as a failure.
|
|
9
|
+
- Strict doctor adds compact aggregates for likely missed identity aliases, remote actions with operation paths but no binding id, contextual implementation stops, and wrapper dynamic-path candidates.
|
|
10
|
+
|
|
3
11
|
## 0.1.21 helper-return propagation and contextual trace notes
|
|
4
12
|
|
|
5
13
|
- Helper-return binding analysis uses the same returned-object scanner for `function` declarations, `async function` declarations, arrow-function variables, async arrow-function variables, and function-expression variables. Named export lists and aliases are resolved, so `export { connectCatalog as createCatalogClient }` can be destructured by callers without losing evidence.
|
|
@@ -638,11 +638,47 @@ function isSupportedEventReceiver(receiver, rootReceiver, serviceVariables) {
|
|
|
638
638
|
if (/^(srv|service|serviceClient|messaging|messageClient|eventClient)$/.test(candidate)) return true;
|
|
639
639
|
return false;
|
|
640
640
|
}
|
|
641
|
+
function collectWrapperSpecs(source) {
|
|
642
|
+
const specs = /* @__PURE__ */ new Map();
|
|
643
|
+
const scanFunction = (name, fn) => {
|
|
644
|
+
const params = fn.parameters.map((param) => ts4.isIdentifier(param.name) ? param.name.text : void 0);
|
|
645
|
+
let found;
|
|
646
|
+
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
|
+
if (ts4.isCallExpression(node) && ts4.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send" && ts4.isIdentifier(node.expression.expression)) {
|
|
653
|
+
const objectArg = node.arguments[0];
|
|
654
|
+
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
655
|
+
const pathProp = objectArg.properties.find((property) => ts4.isShorthandPropertyAssignment(property) && property.name.text === "path");
|
|
656
|
+
if (pathProp) found = { client: node.expression.expression.text, path: pathProp.name.text, method: objectPropertyText(objectArg, "method") };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
ts4.forEachChild(node, visit);
|
|
660
|
+
};
|
|
661
|
+
visit(fn);
|
|
662
|
+
if (!found) return;
|
|
663
|
+
const clientIndex = params.indexOf(found.client);
|
|
664
|
+
const pathIndex = params.indexOf(found.path);
|
|
665
|
+
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 });
|
|
667
|
+
};
|
|
668
|
+
for (const stmt of source.statements) {
|
|
669
|
+
if (ts4.isFunctionDeclaration(stmt) && stmt.name) scanFunction(stmt.name.text, stmt);
|
|
670
|
+
if (ts4.isVariableStatement(stmt)) {
|
|
671
|
+
for (const decl of stmt.declarationList.declarations) if (ts4.isIdentifier(decl.name) && decl.initializer && (ts4.isArrowFunction(decl.initializer) || ts4.isFunctionExpression(decl.initializer))) scanFunction(decl.name.text, decl.initializer);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return specs;
|
|
675
|
+
}
|
|
641
676
|
function classifyOutboundCallsInSource(source, filePath) {
|
|
642
677
|
const calls = [];
|
|
643
678
|
const sourceFile = normalizePath(filePath);
|
|
644
679
|
const initializers = variableInitializers(source);
|
|
645
680
|
const serviceVariables = collectServiceVariables(source);
|
|
681
|
+
const wrapperSpecs = collectWrapperSpecs(source);
|
|
646
682
|
const add = (node, fact, extra) => {
|
|
647
683
|
calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf3(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
|
|
648
684
|
};
|
|
@@ -664,6 +700,14 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
664
700
|
const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
|
|
665
701
|
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 });
|
|
666
702
|
}
|
|
703
|
+
} else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text)) {
|
|
704
|
+
const spec = wrapperSpecs.get(expr.expression.text);
|
|
705
|
+
const clientArg = spec ? expr.arguments[spec.clientIndex] : void 0;
|
|
706
|
+
const pathArg = spec ? expr.arguments[spec.pathIndex] : void 0;
|
|
707
|
+
const methodArg = spec?.methodIndex === void 0 ? void 0 : expr.arguments[spec.methodIndex];
|
|
708
|
+
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: "local_wrapper_literal_path", wrapperFunction: expr.expression.text, literalCallerArgumentDetected: true });
|
|
710
|
+
}
|
|
667
711
|
} else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
|
|
668
712
|
const receiver = receiverName(expr.expression);
|
|
669
713
|
const rootReceiver = rootReceiverName(expr.expression);
|
|
@@ -815,9 +859,15 @@ function connectFactFromCall(call) {
|
|
|
815
859
|
}
|
|
816
860
|
function unwrapCall(expr) {
|
|
817
861
|
if (ts5.isAwaitExpression(expr)) return unwrapCall(expr.expression);
|
|
862
|
+
if (ts5.isAsExpression(expr) || ts5.isSatisfiesExpression(expr)) return unwrapCall(expr.expression);
|
|
818
863
|
if (ts5.isCallExpression(expr)) return expr;
|
|
819
864
|
return void 0;
|
|
820
865
|
}
|
|
866
|
+
function unwrapIdentityExpression(expr) {
|
|
867
|
+
if (ts5.isAwaitExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
868
|
+
if (ts5.isAsExpression(expr) || ts5.isSatisfiesExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
869
|
+
return expr;
|
|
870
|
+
}
|
|
821
871
|
function findConnectInExpression(expr) {
|
|
822
872
|
const direct = unwrapCall(expr);
|
|
823
873
|
if (direct) {
|
|
@@ -1075,6 +1125,35 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1075
1125
|
async function importedHelper(localName) {
|
|
1076
1126
|
return (await importedHelpers(localName)).find((row) => !row.helper.returnedProperty);
|
|
1077
1127
|
}
|
|
1128
|
+
function bindingForVariable(variableName) {
|
|
1129
|
+
const sourceFile = normalizePath(filePath);
|
|
1130
|
+
return [...out].reverse().find((row) => row.variableName === variableName && row.sourceFile === sourceFile);
|
|
1131
|
+
}
|
|
1132
|
+
function cloneAliasBinding(decl, sourceName, aliasKind) {
|
|
1133
|
+
if (!ts5.isIdentifier(decl.name)) return;
|
|
1134
|
+
const existing = bindingForVariable(sourceName);
|
|
1135
|
+
if (!existing) return;
|
|
1136
|
+
out.push({
|
|
1137
|
+
...existing,
|
|
1138
|
+
variableName: decl.name.text,
|
|
1139
|
+
sourceLine: lineOf4(sourceFileAst, decl),
|
|
1140
|
+
helperChain: [
|
|
1141
|
+
...existing.helperChain ?? [],
|
|
1142
|
+
{
|
|
1143
|
+
callerVariable: decl.name.text,
|
|
1144
|
+
aliasOf: sourceName,
|
|
1145
|
+
aliasKind,
|
|
1146
|
+
scopeRule: "same-file-source-order"
|
|
1147
|
+
}
|
|
1148
|
+
]
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
function recordIdentityAlias(decl) {
|
|
1152
|
+
if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
|
|
1153
|
+
const unwrapped = unwrapIdentityExpression(decl.initializer);
|
|
1154
|
+
if (!ts5.isIdentifier(unwrapped)) return;
|
|
1155
|
+
cloneAliasBinding(decl, unwrapped.text, "identity");
|
|
1156
|
+
}
|
|
1078
1157
|
async function recordVariable(decl) {
|
|
1079
1158
|
if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
|
|
1080
1159
|
const call = unwrapCall(decl.initializer);
|
|
@@ -1187,10 +1266,9 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1187
1266
|
await recordDestructuredHelper(decl);
|
|
1188
1267
|
recordDestructuredClassHelper(decl);
|
|
1189
1268
|
await recordVariable(decl);
|
|
1269
|
+
recordIdentityAlias(decl);
|
|
1190
1270
|
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)) {
|
|
1191
|
-
|
|
1192
|
-
const existing = out.find((row) => row.variableName === sourceName && row.sourceFile === normalizePath(filePath));
|
|
1193
|
-
if (existing) out.push({ ...existing, variableName: decl.name.text, sourceLine: lineOf4(sourceFileAst, decl), helperChain: [...existing.helperChain ?? [], { callerVariable: decl.name.text, aliasOf: sourceName, aliasKind: "transaction" }] });
|
|
1271
|
+
cloneAliasBinding(decl, decl.initializer.expression.expression.text, "transaction");
|
|
1194
1272
|
}
|
|
1195
1273
|
}
|
|
1196
1274
|
return out;
|
|
@@ -1998,10 +2076,25 @@ function implementationScope(db, operationId) {
|
|
|
1998
2076
|
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
|
|
1999
2077
|
}
|
|
2000
2078
|
function contextImplementationMethodId(edge, callerRepoId) {
|
|
2001
|
-
if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return
|
|
2079
|
+
if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return { evidence: { status: "not_applicable" } };
|
|
2002
2080
|
const evidence = JSON.parse(String(edge.evidence_json || "{}"));
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2081
|
+
const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
|
|
2082
|
+
const reasons = [];
|
|
2083
|
+
let score = Number(item.score ?? 0);
|
|
2084
|
+
if (Number(item.handlerPackage?.id) === callerRepoId) {
|
|
2085
|
+
score += 10;
|
|
2086
|
+
reasons.push("handler_package_matches_caller_repository");
|
|
2087
|
+
}
|
|
2088
|
+
if (Number(item.applicationPackage?.id) === callerRepoId) {
|
|
2089
|
+
score += 10;
|
|
2090
|
+
reasons.push("registration_package_matches_caller_repository");
|
|
2091
|
+
}
|
|
2092
|
+
return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
|
|
2093
|
+
}).sort((a, b) => b.score - a.score);
|
|
2094
|
+
if (scores.length === 0) return { evidence: { status: "not_applicable", candidateScores: [] } };
|
|
2095
|
+
const [first, second] = scores;
|
|
2096
|
+
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 } };
|
|
2097
|
+
return { evidence: { status: "tied", tieReason: "no_unique_materially_stronger_candidate", candidateScores: scores } };
|
|
2005
2098
|
}
|
|
2006
2099
|
function handlerScope(db, methodId) {
|
|
2007
2100
|
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);
|
|
@@ -2207,7 +2300,8 @@ function trace(db, start, options) {
|
|
|
2207
2300
|
});
|
|
2208
2301
|
if (effectiveRow.to_kind === "operation") {
|
|
2209
2302
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
2210
|
-
const
|
|
2303
|
+
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId);
|
|
2304
|
+
const contextMethodId = contextSelection.methodId;
|
|
2211
2305
|
const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : void 0;
|
|
2212
2306
|
if (implementation.edge) {
|
|
2213
2307
|
const implEvidence = JSON.parse(String(implementation.edge.evidence_json || "{}"));
|
|
@@ -2219,9 +2313,9 @@ function trace(db, start, options) {
|
|
|
2219
2313
|
type: "operation_implemented_by_handler",
|
|
2220
2314
|
from: to,
|
|
2221
2315
|
to: implTo,
|
|
2222
|
-
evidence: implEvidence,
|
|
2316
|
+
evidence: contextMethodId ? { ...implEvidence, contextualImplementationSelected: true, contextualImplementation: contextSelection.evidence } : { ...implEvidence, contextualImplementation: contextSelection.evidence },
|
|
2223
2317
|
confidence: Number(implementation.edge.confidence ?? 0),
|
|
2224
|
-
unresolvedReason: implementation.edge.status === "resolved" ? void 0 : String(implementation.edge.unresolved_reason ?? implementation.edge.status)
|
|
2318
|
+
unresolvedReason: implementation.edge.status === "resolved" || contextMethodId ? void 0 : String(implementation.edge.unresolved_reason ?? implementation.edge.status)
|
|
2225
2319
|
});
|
|
2226
2320
|
}
|
|
2227
2321
|
if (current.depth >= maxDepth) continue;
|
|
@@ -2278,4 +2372,4 @@ export {
|
|
|
2278
2372
|
linkWorkspace,
|
|
2279
2373
|
trace
|
|
2280
2374
|
};
|
|
2281
|
-
//# sourceMappingURL=chunk-
|
|
2375
|
+
//# sourceMappingURL=chunk-JIY37C44.js.map
|