@saptools/service-flow 0.1.20 → 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 +14 -0
- package/README.md +12 -4
- package/TECHNICAL-NOTE.md +15 -0
- package/dist/{chunk-EKLZJJW7.js → chunk-JIY37C44.js} +186 -35
- 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-EKLZJJW7.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.1.21
|
|
12
|
+
|
|
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.
|
|
14
|
+
- Preserve conservative object-return analysis by binding only returned properties backed by local `cds.connect.to(...)` client variables, while ignoring unrelated metadata fields.
|
|
15
|
+
- Document contextual implementation selection evidence and strict doctor checks for operation-path-only remote actions.
|
|
16
|
+
|
|
3
17
|
## 0.1.20
|
|
4
18
|
|
|
5
19
|
- Preserve service-binding evidence when helpers return connected clients inside object properties and callers destructure or create simple transaction aliases.
|
package/README.md
CHANGED
|
@@ -227,7 +227,15 @@ service-flow list calls --workspace /path/to/workspace --repo facade-service --o
|
|
|
227
227
|
- Service bindings are matched to outbound calls by repository, source file, and
|
|
228
228
|
variable name to avoid false cross-file matches. If a helper-returned client is
|
|
229
229
|
not linked, export the helper from a relative import target and ensure it returns
|
|
230
|
-
`cds.connect.to(...)` directly or
|
|
230
|
+
`cds.connect.to(...)` directly or returns an object property backed by a local
|
|
231
|
+
connected-client variable. Supported helper shapes include function declarations,
|
|
232
|
+
arrow-function variables, function-expression variables, named export lists, and
|
|
233
|
+
aliased exports such as `export { connectCatalog as createCatalogClient }`.
|
|
234
|
+
Shorthand returns like `return { client }` and explicit returns like
|
|
235
|
+
`return { serviceClient: client }` are followed only when the returned value is
|
|
236
|
+
a concrete client. Trace evidence includes the caller variable, returned
|
|
237
|
+
property, imported helper, source file, exported symbol, placeholders, and
|
|
238
|
+
transaction alias steps.
|
|
231
239
|
- `SELECT.one.from(Entity)`, `SELECT.from(Entity)`, `INSERT.into(Entity)`,
|
|
232
240
|
`UPDATE(Entity)`, and `DELETE.from(Entity)` are indexed as local database
|
|
233
241
|
query entities when statically knowable.
|
|
@@ -310,7 +318,7 @@ service-flow trace --workspace /path/to/workspace --repo facade-service --operat
|
|
|
310
318
|
|
|
311
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.
|
|
312
320
|
|
|
313
|
-
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.
|
|
314
322
|
|
|
315
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.
|
|
316
324
|
|
|
@@ -410,8 +418,8 @@ Run `service-flow index` after source, CDS, package metadata, or helper-package
|
|
|
410
418
|
<details>
|
|
411
419
|
<summary><b>Why is an expected call unresolved?</b></summary>
|
|
412
420
|
|
|
413
|
-
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`, and
|
|
414
|
-
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.
|
|
415
423
|
|
|
416
424
|
</details>
|
|
417
425
|
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.1.21 helper-return propagation and contextual trace notes
|
|
12
|
+
|
|
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.
|
|
14
|
+
- Returned object properties are bound only when their value is a local variable initialized from `cds.connect.to(...)`. Shorthand properties and explicit property assignments are supported; unrelated strings, codes, and metadata fields are ignored. Destructuring renames and `.tx()` aliases append helper-chain evidence instead of replacing the original binding evidence.
|
|
15
|
+
- Trace-time contextual implementation selection may use caller repository, runtime-resolved service path, destination or alias expression, package dependency evidence, handler and registration packages, and local service ownership to continue from an ambiguous operation edge into one handler. If candidates tie or no static signal is strong enough, trace keeps the ambiguous edge and reports the tie or unresolved reason.
|
|
16
|
+
- `doctor --strict` is the intended place for broad regression aggregates, including helper-return coverage, contextual implementation ambiguity, and remote actions that have an operation path but no service binding id.
|
|
17
|
+
|
|
3
18
|
## 0.1.20 helper object clients, expression placeholders, and remote-action target notes
|
|
4
19
|
|
|
5
20
|
- Helper-return binding analysis now follows concrete `cds.connect.to(...)` clients returned through object shorthand or explicit properties. Callers that destructure those properties, rename them, or assign a simple `.tx()` transaction alias keep helper-chain evidence including caller variable, returned property, helper source, destination expression, service-path expression, and placeholders. Arbitrary object returns are ignored.
|
|
@@ -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) {
|
|
@@ -900,6 +950,8 @@ function collectReturnedObjectBindings(fn) {
|
|
|
900
950
|
const bindings = /* @__PURE__ */ new Map();
|
|
901
951
|
const returns = /* @__PURE__ */ new Map();
|
|
902
952
|
function visit(node) {
|
|
953
|
+
if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
|
|
954
|
+
return;
|
|
903
955
|
if (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
|
|
904
956
|
const fact = findConnectInExpression(node.initializer);
|
|
905
957
|
if (fact) bindings.set(node.name.text, fact);
|
|
@@ -923,6 +975,35 @@ function collectReturnedObjectBindings(fn) {
|
|
|
923
975
|
}
|
|
924
976
|
return out;
|
|
925
977
|
}
|
|
978
|
+
function functionLikeInitializer(expr) {
|
|
979
|
+
if (!expr) return void 0;
|
|
980
|
+
if (ts5.isArrowFunction(expr) || ts5.isFunctionExpression(expr)) return expr;
|
|
981
|
+
return void 0;
|
|
982
|
+
}
|
|
983
|
+
function directReturnConnectFact(fn) {
|
|
984
|
+
const localBindings = /* @__PURE__ */ new Map();
|
|
985
|
+
let returned;
|
|
986
|
+
function visit(node) {
|
|
987
|
+
if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
|
|
988
|
+
return;
|
|
989
|
+
if (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
|
|
990
|
+
const fact = findConnectInExpression(node.initializer);
|
|
991
|
+
if (fact) localBindings.set(node.name.text, fact);
|
|
992
|
+
}
|
|
993
|
+
if (!returned && ts5.isReturnStatement(node) && node.expression)
|
|
994
|
+
returned = node.expression;
|
|
995
|
+
if (!returned) ts5.forEachChild(node, visit);
|
|
996
|
+
}
|
|
997
|
+
visit(fn);
|
|
998
|
+
if (!returned) return void 0;
|
|
999
|
+
if (ts5.isIdentifier(returned)) return localBindings.get(returned.text);
|
|
1000
|
+
return findConnectInExpression(returned);
|
|
1001
|
+
}
|
|
1002
|
+
function directConnectFactFromFunctionLike(fn) {
|
|
1003
|
+
if (ts5.isArrowFunction(fn) && fn.body && !ts5.isBlock(fn.body))
|
|
1004
|
+
return findConnectInExpression(fn.body);
|
|
1005
|
+
return directReturnConnectFact(fn);
|
|
1006
|
+
}
|
|
926
1007
|
function exportedLocalNames(sf) {
|
|
927
1008
|
const exports = /* @__PURE__ */ new Map();
|
|
928
1009
|
for (const stmt of sf.statements) {
|
|
@@ -951,12 +1032,7 @@ async function helperBindings(repoPath, filePath) {
|
|
|
951
1032
|
const factsByLocal = /* @__PURE__ */ new Map();
|
|
952
1033
|
for (const stmt of sf.statements) {
|
|
953
1034
|
if (ts5.isFunctionDeclaration(stmt) && stmt.name) {
|
|
954
|
-
|
|
955
|
-
stmt.forEachChild(function visit(node) {
|
|
956
|
-
if (!fact && ts5.isReturnStatement(node) && node.expression)
|
|
957
|
-
fact = findConnectInExpression(node.expression);
|
|
958
|
-
if (!fact) ts5.forEachChild(node, visit);
|
|
959
|
-
});
|
|
1035
|
+
const fact = directConnectFactFromFunctionLike(stmt);
|
|
960
1036
|
if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf4(sf, stmt) });
|
|
961
1037
|
for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(stmt))
|
|
962
1038
|
factsByLocal.set(`${stmt.name.text}#${returnedProperty}`, { ...objectFact, returnedProperty, sourceLine: lineOf4(sf, stmt) });
|
|
@@ -964,6 +1040,22 @@ async function helperBindings(repoPath, filePath) {
|
|
|
964
1040
|
if (ts5.isVariableStatement(stmt))
|
|
965
1041
|
for (const decl of stmt.declarationList.declarations) {
|
|
966
1042
|
if (!ts5.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
1043
|
+
const helper = functionLikeInitializer(decl.initializer);
|
|
1044
|
+
if (helper) {
|
|
1045
|
+
const directReturn = directConnectFactFromFunctionLike(helper);
|
|
1046
|
+
if (directReturn)
|
|
1047
|
+
factsByLocal.set(decl.name.text, {
|
|
1048
|
+
...directReturn,
|
|
1049
|
+
sourceLine: lineOf4(sourceFileAst, decl)
|
|
1050
|
+
});
|
|
1051
|
+
for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(helper))
|
|
1052
|
+
factsByLocal.set(`${decl.name.text}#${returnedProperty}`, {
|
|
1053
|
+
...objectFact,
|
|
1054
|
+
returnedProperty,
|
|
1055
|
+
sourceLine: lineOf4(sourceFileAst, decl)
|
|
1056
|
+
});
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
967
1059
|
const fact = findConnectInExpression(decl.initializer);
|
|
968
1060
|
if (fact)
|
|
969
1061
|
factsByLocal.set(decl.name.text, {
|
|
@@ -1002,22 +1094,65 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1002
1094
|
const classHelpers = collectClassHelpers(sourceFileAst);
|
|
1003
1095
|
const localObjectHelpers = /* @__PURE__ */ new Map();
|
|
1004
1096
|
for (const stmt of sourceFileAst.statements) {
|
|
1005
|
-
if (
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1097
|
+
if (ts5.isFunctionDeclaration(stmt) && stmt.name) {
|
|
1098
|
+
const rows2 = [];
|
|
1099
|
+
for (const [returnedProperty, fact] of collectReturnedObjectBindings(stmt))
|
|
1100
|
+
rows2.push({ ...fact, exportedName: stmt.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, stmt) });
|
|
1101
|
+
if (rows2.length > 0) localObjectHelpers.set(stmt.name.text, rows2);
|
|
1102
|
+
}
|
|
1103
|
+
if (ts5.isVariableStatement(stmt)) {
|
|
1104
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
1105
|
+
if (!ts5.isIdentifier(decl.name)) continue;
|
|
1106
|
+
const helper = functionLikeInitializer(decl.initializer);
|
|
1107
|
+
if (!helper) continue;
|
|
1108
|
+
const rows2 = [];
|
|
1109
|
+
for (const [returnedProperty, fact] of collectReturnedObjectBindings(helper))
|
|
1110
|
+
rows2.push({ ...fact, exportedName: decl.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, decl) });
|
|
1111
|
+
if (rows2.length > 0) localObjectHelpers.set(decl.name.text, rows2);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1010
1114
|
}
|
|
1011
|
-
async function
|
|
1115
|
+
async function importedHelpers(localName) {
|
|
1012
1116
|
const imp = imports.find((i) => i.localName === localName && i.sourceFile);
|
|
1013
|
-
if (!imp?.sourceFile) return
|
|
1117
|
+
if (!imp?.sourceFile) return [];
|
|
1014
1118
|
if (!helperCache.has(imp.sourceFile))
|
|
1015
1119
|
helperCache.set(
|
|
1016
1120
|
imp.sourceFile,
|
|
1017
1121
|
await helperBindings(repoPath, imp.sourceFile)
|
|
1018
1122
|
);
|
|
1019
|
-
|
|
1020
|
-
|
|
1123
|
+
return (helperCache.get(imp.sourceFile) ?? []).filter((h) => h.exportedName === imp.exportedName).map((helper) => ({ imp, helper }));
|
|
1124
|
+
}
|
|
1125
|
+
async function importedHelper(localName) {
|
|
1126
|
+
return (await importedHelpers(localName)).find((row) => !row.helper.returnedProperty);
|
|
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");
|
|
1021
1156
|
}
|
|
1022
1157
|
async function recordVariable(decl) {
|
|
1023
1158
|
if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
|
|
@@ -1057,23 +1192,24 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1057
1192
|
});
|
|
1058
1193
|
}
|
|
1059
1194
|
}
|
|
1060
|
-
async function
|
|
1061
|
-
if (!ts5.isIdentifier(call.expression)) return
|
|
1062
|
-
const local = localObjectHelpers.get(call.expression.text)
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
return resolved ? { helper: resolved.helper, imp: resolved.imp } : void 0;
|
|
1195
|
+
async function helpersForCall(call) {
|
|
1196
|
+
if (!ts5.isIdentifier(call.expression)) return [];
|
|
1197
|
+
const local = localObjectHelpers.get(call.expression.text) ?? [];
|
|
1198
|
+
const imported = await importedHelpers(call.expression.text);
|
|
1199
|
+
return [...local.map((helper) => ({ helper })), ...imported];
|
|
1066
1200
|
}
|
|
1067
1201
|
async function recordDestructuredHelper(decl) {
|
|
1068
1202
|
if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
|
|
1069
1203
|
const call = unwrapCall(decl.initializer);
|
|
1070
1204
|
if (!call) return;
|
|
1071
|
-
const
|
|
1072
|
-
if (
|
|
1205
|
+
const helpers = await helpersForCall(call);
|
|
1206
|
+
if (helpers.length === 0) return;
|
|
1073
1207
|
for (const el of decl.name.elements) {
|
|
1074
1208
|
if (!ts5.isIdentifier(el.name)) continue;
|
|
1075
1209
|
const propertyName = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
1076
|
-
|
|
1210
|
+
const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
|
|
1211
|
+
if (matches.length !== 1) continue;
|
|
1212
|
+
const resolved = matches[0];
|
|
1077
1213
|
out.push({
|
|
1078
1214
|
variableName: el.name.text,
|
|
1079
1215
|
alias: resolved.helper.alias,
|
|
@@ -1084,7 +1220,7 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1084
1220
|
placeholders: resolved.helper.placeholders,
|
|
1085
1221
|
sourceFile: normalizePath(filePath),
|
|
1086
1222
|
sourceLine: lineOf4(sourceFileAst, decl),
|
|
1087
|
-
helperChain: [{ callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName, importSource: resolved.imp?.sourceFile, helperSourceFile: resolved.helper.sourceFile, helperSourceLine: resolved.helper.sourceLine }]
|
|
1223
|
+
helperChain: [{ callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName, importSource: resolved.imp?.sourceFile, exportedSymbol: resolved.imp?.exportedName, helperSourceFile: resolved.helper.sourceFile, helperSourceLine: resolved.helper.sourceLine }]
|
|
1088
1224
|
});
|
|
1089
1225
|
}
|
|
1090
1226
|
}
|
|
@@ -1130,10 +1266,9 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1130
1266
|
await recordDestructuredHelper(decl);
|
|
1131
1267
|
recordDestructuredClassHelper(decl);
|
|
1132
1268
|
await recordVariable(decl);
|
|
1269
|
+
recordIdentityAlias(decl);
|
|
1133
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)) {
|
|
1134
|
-
|
|
1135
|
-
const existing = out.find((row) => row.variableName === sourceName && row.sourceFile === normalizePath(filePath));
|
|
1136
|
-
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");
|
|
1137
1272
|
}
|
|
1138
1273
|
}
|
|
1139
1274
|
return out;
|
|
@@ -1941,10 +2076,25 @@ function implementationScope(db, operationId) {
|
|
|
1941
2076
|
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
|
|
1942
2077
|
}
|
|
1943
2078
|
function contextImplementationMethodId(edge, callerRepoId) {
|
|
1944
|
-
if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return
|
|
2079
|
+
if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return { evidence: { status: "not_applicable" } };
|
|
1945
2080
|
const evidence = JSON.parse(String(edge.evidence_json || "{}"));
|
|
1946
|
-
const
|
|
1947
|
-
|
|
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 } };
|
|
1948
2098
|
}
|
|
1949
2099
|
function handlerScope(db, methodId) {
|
|
1950
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);
|
|
@@ -2150,7 +2300,8 @@ function trace(db, start, options) {
|
|
|
2150
2300
|
});
|
|
2151
2301
|
if (effectiveRow.to_kind === "operation") {
|
|
2152
2302
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
2153
|
-
const
|
|
2303
|
+
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId);
|
|
2304
|
+
const contextMethodId = contextSelection.methodId;
|
|
2154
2305
|
const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : void 0;
|
|
2155
2306
|
if (implementation.edge) {
|
|
2156
2307
|
const implEvidence = JSON.parse(String(implementation.edge.evidence_json || "{}"));
|
|
@@ -2162,9 +2313,9 @@ function trace(db, start, options) {
|
|
|
2162
2313
|
type: "operation_implemented_by_handler",
|
|
2163
2314
|
from: to,
|
|
2164
2315
|
to: implTo,
|
|
2165
|
-
evidence: implEvidence,
|
|
2316
|
+
evidence: contextMethodId ? { ...implEvidence, contextualImplementationSelected: true, contextualImplementation: contextSelection.evidence } : { ...implEvidence, contextualImplementation: contextSelection.evidence },
|
|
2166
2317
|
confidence: Number(implementation.edge.confidence ?? 0),
|
|
2167
|
-
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)
|
|
2168
2319
|
});
|
|
2169
2320
|
}
|
|
2170
2321
|
if (current.depth >= maxDepth) continue;
|
|
@@ -2221,4 +2372,4 @@ export {
|
|
|
2221
2372
|
linkWorkspace,
|
|
2222
2373
|
trace
|
|
2223
2374
|
};
|
|
2224
|
-
//# sourceMappingURL=chunk-
|
|
2375
|
+
//# sourceMappingURL=chunk-JIY37C44.js.map
|