@saptools/service-flow 0.1.19 → 0.1.21
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 +13 -0
- package/README.md +13 -5
- package/TECHNICAL-NOTE.md +14 -0
- package/dist/{chunk-YG66SHAR.js → chunk-TSMDIKML.js} +169 -23
- package/dist/chunk-TSMDIKML.js.map +1 -0
- package/dist/cli.js +28 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-YG66SHAR.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.21
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- Preserve conservative object-return analysis by binding only returned properties backed by local `cds.connect.to(...)` client variables, while ignoring unrelated metadata fields.
|
|
7
|
+
- Document contextual implementation selection evidence and strict doctor checks for operation-path-only remote actions.
|
|
8
|
+
|
|
9
|
+
## 0.1.20
|
|
10
|
+
|
|
11
|
+
- Preserve service-binding evidence when helpers return connected clients inside object properties and callers destructure or create simple transaction aliases.
|
|
12
|
+
- Track full template-expression placeholder keys such as `${domainInfo.serviceName}`, `${domainInfo.shortName?.toLowerCase()}`, and `${items[0].service}` for runtime `--var` substitution without evaluating JavaScript.
|
|
13
|
+
- Render unresolved remote actions with unknown or dynamic paths as semantic targets instead of numeric call ids, with parser evidence for shorthand `path` identifiers.
|
|
14
|
+
- Expand strict doctor aggregates for normalized OData invocation ambiguity and remote-action unresolved target quality.
|
|
15
|
+
|
|
3
16
|
## 0.1.19
|
|
4
17
|
|
|
5
18
|
- Normalize balanced top-level OData action/function invocation paths before remote operation resolution, including namespace-qualified invocation lookup, while preserving raw and normalized evidence.
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ npm install @saptools/service-flow
|
|
|
54
54
|
|
|
55
55
|
### Correctness notes
|
|
56
56
|
|
|
57
|
-
- Runtime `--var` values are considered only for dynamic, ambiguous, or unresolved **remote** graph edges whose alias, destination, service path, or operation path expressions contain supplied placeholders. Local database, external HTTP, event, and already resolved static edges keep their persisted status, target, reason, and confidence. Partial substitutions remain dynamic and report the missing placeholder names.
|
|
57
|
+
- Runtime `--var` values are considered only for dynamic, ambiguous, or unresolved **remote** graph edges whose alias, destination, service path, or operation path expressions contain supplied placeholders. Placeholder keys are the full trimmed expression inside `${...}`, so keys such as `domainInfo.serviceName`, `domainInfo.shortName?.toLowerCase()`, and `items[0].service` can be supplied literally without JavaScript evaluation. Local database, external HTTP, event, and already resolved static edges keep their persisted status, target, reason, and confidence. Partial substitutions remain dynamic and report the missing placeholder names.
|
|
58
58
|
- `trace` and `graph` both accept repeatable `--var key=value` options. Effective substitutions are rendered in trace evidence without mutating the persisted graph. Confidence values are bounded to `[0, 1]`.
|
|
59
59
|
- Repository selectors on list, trace, graph, and inspect commands narrow scope. Unknown selectors return empty machine-readable diagnostics instead of falling back to the whole workspace.
|
|
60
60
|
- Helper-package dependency edges prefer exact indexed package names. Duplicate package-name candidates are persisted as ambiguous evidence rather than silently selecting one repository.
|
|
@@ -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. 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.
|
|
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 simple 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.
|
|
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 and remote-action target quality, 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,19 @@
|
|
|
1
1
|
# Service Flow Resolution Notes
|
|
2
2
|
|
|
3
|
+
## 0.1.21 helper-return propagation and contextual trace notes
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- 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.
|
|
7
|
+
- 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.
|
|
8
|
+
- `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.
|
|
9
|
+
|
|
10
|
+
## 0.1.20 helper object clients, expression placeholders, and remote-action target notes
|
|
11
|
+
|
|
12
|
+
- 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.
|
|
13
|
+
- Template placeholders use the full trimmed expression inside `${...}` as the runtime key. Examples include `domain`, `domainInfo.serviceName`, `domainInfo.shortName?.toLowerCase()`, and `items[0].service`. Runtime `--var` substitution matches these keys literally and does not execute or partially evaluate JavaScript. Missing expression keys keep edges dynamic.
|
|
14
|
+
- Remote action calls that do not expose a static path now use semantic unresolved targets such as `Remote action: unknown path` or `Remote action: dynamic path`; shorthand `path` properties retain `operationPathExpression` and `dynamic_operation_path_identifier` parser evidence.
|
|
15
|
+
- Strict doctor reports normalized OData invocation totals by resolved/dynamic/ambiguous/unresolved status and remote-action target quality, including numeric unresolved targets and semantic unknown/dynamic target counts.
|
|
16
|
+
|
|
3
17
|
## 0.1.19 remote invocation and query notes
|
|
4
18
|
|
|
5
19
|
- Remote action/function paths are scanned for a balanced top-level OData invocation suffix. Single-segment operation imports like `/readConfig(...)` normalize to `/readConfig`; namespace-qualified operation imports keep the qualified request segment for evidence and can resolve against the indexed simple CDS operation when service signals are strong. Navigation/property paths like `/Orders(id='123')/items` are left unchanged. Graph evidence keeps both `rawOperationPath` and `normalizedOperationPath` when normalization occurs.
|
|
@@ -590,8 +590,14 @@ function literalText(expr) {
|
|
|
590
590
|
return void 0;
|
|
591
591
|
}
|
|
592
592
|
function objectPropertyText(object, key) {
|
|
593
|
-
const prop = object.properties.find(
|
|
594
|
-
|
|
593
|
+
const prop = object.properties.find(
|
|
594
|
+
(property) => ts4.isPropertyAssignment(property) && nameOfProperty(property.name) === key || ts4.isShorthandPropertyAssignment(property) && property.name.text === key
|
|
595
|
+
);
|
|
596
|
+
if (!prop) return void 0;
|
|
597
|
+
return ts4.isShorthandPropertyAssignment(prop) ? prop.name.text : prop.initializer.getText();
|
|
598
|
+
}
|
|
599
|
+
function objectPropertyIsShorthand(object, key) {
|
|
600
|
+
return object.properties.some((property) => ts4.isShorthandPropertyAssignment(property) && property.name.text === key);
|
|
595
601
|
}
|
|
596
602
|
function nameOfProperty(name) {
|
|
597
603
|
if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name)) return name.text;
|
|
@@ -655,7 +661,8 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
655
661
|
const receiver = expr.expression.text;
|
|
656
662
|
const query = objectPropertyText(objectArg, "query");
|
|
657
663
|
const op = objectPropertyText(objectArg, "path") ?? objectPropertyText(objectArg, "event");
|
|
658
|
-
|
|
664
|
+
const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
|
|
665
|
+
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 });
|
|
659
666
|
}
|
|
660
667
|
} else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
|
|
661
668
|
const receiver = receiverName(expr.expression);
|
|
@@ -750,7 +757,7 @@ function stringValue(node) {
|
|
|
750
757
|
return node.getText();
|
|
751
758
|
}
|
|
752
759
|
function placeholders(value) {
|
|
753
|
-
return [...(value ?? "").matchAll(/\$\{
|
|
760
|
+
return [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((m) => (m[1] ?? "").trim()).filter(Boolean);
|
|
754
761
|
}
|
|
755
762
|
function connectFactFromCall(call) {
|
|
756
763
|
const expr = call.expression;
|
|
@@ -889,6 +896,64 @@ async function importsFor(repoPath, filePath, sf) {
|
|
|
889
896
|
}
|
|
890
897
|
return imports;
|
|
891
898
|
}
|
|
899
|
+
function collectReturnedObjectBindings(fn) {
|
|
900
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
901
|
+
const returns = /* @__PURE__ */ new Map();
|
|
902
|
+
function visit(node) {
|
|
903
|
+
if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
|
|
904
|
+
return;
|
|
905
|
+
if (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
|
|
906
|
+
const fact = findConnectInExpression(node.initializer);
|
|
907
|
+
if (fact) bindings.set(node.name.text, fact);
|
|
908
|
+
}
|
|
909
|
+
if (ts5.isReturnStatement(node) && node.expression && ts5.isObjectLiteralExpression(node.expression)) {
|
|
910
|
+
for (const prop of node.expression.properties) {
|
|
911
|
+
if (ts5.isShorthandPropertyAssignment(prop)) returns.set(prop.name.text, prop.name.text);
|
|
912
|
+
if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.initializer)) {
|
|
913
|
+
const propertyName = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
914
|
+
if (propertyName) returns.set(propertyName, prop.initializer.text);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
ts5.forEachChild(node, visit);
|
|
919
|
+
}
|
|
920
|
+
visit(fn);
|
|
921
|
+
const out = /* @__PURE__ */ new Map();
|
|
922
|
+
for (const [propertyName, variableName] of returns) {
|
|
923
|
+
const fact = bindings.get(variableName);
|
|
924
|
+
if (fact) out.set(propertyName, fact);
|
|
925
|
+
}
|
|
926
|
+
return out;
|
|
927
|
+
}
|
|
928
|
+
function functionLikeInitializer(expr) {
|
|
929
|
+
if (!expr) return void 0;
|
|
930
|
+
if (ts5.isArrowFunction(expr) || ts5.isFunctionExpression(expr)) return expr;
|
|
931
|
+
return void 0;
|
|
932
|
+
}
|
|
933
|
+
function directReturnConnectFact(fn) {
|
|
934
|
+
const localBindings = /* @__PURE__ */ new Map();
|
|
935
|
+
let returned;
|
|
936
|
+
function visit(node) {
|
|
937
|
+
if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
|
|
938
|
+
return;
|
|
939
|
+
if (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
|
|
940
|
+
const fact = findConnectInExpression(node.initializer);
|
|
941
|
+
if (fact) localBindings.set(node.name.text, fact);
|
|
942
|
+
}
|
|
943
|
+
if (!returned && ts5.isReturnStatement(node) && node.expression)
|
|
944
|
+
returned = node.expression;
|
|
945
|
+
if (!returned) ts5.forEachChild(node, visit);
|
|
946
|
+
}
|
|
947
|
+
visit(fn);
|
|
948
|
+
if (!returned) return void 0;
|
|
949
|
+
if (ts5.isIdentifier(returned)) return localBindings.get(returned.text);
|
|
950
|
+
return findConnectInExpression(returned);
|
|
951
|
+
}
|
|
952
|
+
function directConnectFactFromFunctionLike(fn) {
|
|
953
|
+
if (ts5.isArrowFunction(fn) && fn.body && !ts5.isBlock(fn.body))
|
|
954
|
+
return findConnectInExpression(fn.body);
|
|
955
|
+
return directReturnConnectFact(fn);
|
|
956
|
+
}
|
|
892
957
|
function exportedLocalNames(sf) {
|
|
893
958
|
const exports = /* @__PURE__ */ new Map();
|
|
894
959
|
for (const stmt of sf.statements) {
|
|
@@ -917,17 +982,30 @@ async function helperBindings(repoPath, filePath) {
|
|
|
917
982
|
const factsByLocal = /* @__PURE__ */ new Map();
|
|
918
983
|
for (const stmt of sf.statements) {
|
|
919
984
|
if (ts5.isFunctionDeclaration(stmt) && stmt.name) {
|
|
920
|
-
|
|
921
|
-
stmt.forEachChild(function visit(node) {
|
|
922
|
-
if (!fact && ts5.isReturnStatement(node) && node.expression)
|
|
923
|
-
fact = findConnectInExpression(node.expression);
|
|
924
|
-
if (!fact) ts5.forEachChild(node, visit);
|
|
925
|
-
});
|
|
985
|
+
const fact = directConnectFactFromFunctionLike(stmt);
|
|
926
986
|
if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf4(sf, stmt) });
|
|
987
|
+
for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(stmt))
|
|
988
|
+
factsByLocal.set(`${stmt.name.text}#${returnedProperty}`, { ...objectFact, returnedProperty, sourceLine: lineOf4(sf, stmt) });
|
|
927
989
|
}
|
|
928
990
|
if (ts5.isVariableStatement(stmt))
|
|
929
991
|
for (const decl of stmt.declarationList.declarations) {
|
|
930
992
|
if (!ts5.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
993
|
+
const helper = functionLikeInitializer(decl.initializer);
|
|
994
|
+
if (helper) {
|
|
995
|
+
const directReturn = directConnectFactFromFunctionLike(helper);
|
|
996
|
+
if (directReturn)
|
|
997
|
+
factsByLocal.set(decl.name.text, {
|
|
998
|
+
...directReturn,
|
|
999
|
+
sourceLine: lineOf4(sourceFileAst, decl)
|
|
1000
|
+
});
|
|
1001
|
+
for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(helper))
|
|
1002
|
+
factsByLocal.set(`${decl.name.text}#${returnedProperty}`, {
|
|
1003
|
+
...objectFact,
|
|
1004
|
+
returnedProperty,
|
|
1005
|
+
sourceLine: lineOf4(sourceFileAst, decl)
|
|
1006
|
+
});
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
931
1009
|
const fact = findConnectInExpression(decl.initializer);
|
|
932
1010
|
if (fact)
|
|
933
1011
|
factsByLocal.set(decl.name.text, {
|
|
@@ -946,6 +1024,14 @@ async function helperBindings(repoPath, filePath) {
|
|
|
946
1024
|
sourceLine: fact.sourceLine
|
|
947
1025
|
});
|
|
948
1026
|
}
|
|
1027
|
+
for (const [key, fact] of factsByLocal) {
|
|
1028
|
+
const [localName, returnedProperty] = key.split("#");
|
|
1029
|
+
if (!returnedProperty) continue;
|
|
1030
|
+
for (const [exportedName, exportedLocal] of exportedLocals) {
|
|
1031
|
+
if (exportedLocal !== localName) continue;
|
|
1032
|
+
out.push({ ...fact, exportedName, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: fact.sourceLine });
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
949
1035
|
return out;
|
|
950
1036
|
}
|
|
951
1037
|
async function parseServiceBindings(repoPath, filePath) {
|
|
@@ -956,16 +1042,38 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
956
1042
|
const imports = await importsFor(repoPath, filePath, sf);
|
|
957
1043
|
const helperCache = /* @__PURE__ */ new Map();
|
|
958
1044
|
const classHelpers = collectClassHelpers(sourceFileAst);
|
|
959
|
-
|
|
1045
|
+
const localObjectHelpers = /* @__PURE__ */ new Map();
|
|
1046
|
+
for (const stmt of sourceFileAst.statements) {
|
|
1047
|
+
if (ts5.isFunctionDeclaration(stmt) && stmt.name) {
|
|
1048
|
+
const rows2 = [];
|
|
1049
|
+
for (const [returnedProperty, fact] of collectReturnedObjectBindings(stmt))
|
|
1050
|
+
rows2.push({ ...fact, exportedName: stmt.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, stmt) });
|
|
1051
|
+
if (rows2.length > 0) localObjectHelpers.set(stmt.name.text, rows2);
|
|
1052
|
+
}
|
|
1053
|
+
if (ts5.isVariableStatement(stmt)) {
|
|
1054
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
1055
|
+
if (!ts5.isIdentifier(decl.name)) continue;
|
|
1056
|
+
const helper = functionLikeInitializer(decl.initializer);
|
|
1057
|
+
if (!helper) continue;
|
|
1058
|
+
const rows2 = [];
|
|
1059
|
+
for (const [returnedProperty, fact] of collectReturnedObjectBindings(helper))
|
|
1060
|
+
rows2.push({ ...fact, exportedName: decl.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, decl) });
|
|
1061
|
+
if (rows2.length > 0) localObjectHelpers.set(decl.name.text, rows2);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
async function importedHelpers(localName) {
|
|
960
1066
|
const imp = imports.find((i) => i.localName === localName && i.sourceFile);
|
|
961
|
-
if (!imp?.sourceFile) return
|
|
1067
|
+
if (!imp?.sourceFile) return [];
|
|
962
1068
|
if (!helperCache.has(imp.sourceFile))
|
|
963
1069
|
helperCache.set(
|
|
964
1070
|
imp.sourceFile,
|
|
965
1071
|
await helperBindings(repoPath, imp.sourceFile)
|
|
966
1072
|
);
|
|
967
|
-
|
|
968
|
-
|
|
1073
|
+
return (helperCache.get(imp.sourceFile) ?? []).filter((h) => h.exportedName === imp.exportedName).map((helper) => ({ imp, helper }));
|
|
1074
|
+
}
|
|
1075
|
+
async function importedHelper(localName) {
|
|
1076
|
+
return (await importedHelpers(localName)).find((row) => !row.helper.returnedProperty);
|
|
969
1077
|
}
|
|
970
1078
|
async function recordVariable(decl) {
|
|
971
1079
|
if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
|
|
@@ -1005,6 +1113,38 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1005
1113
|
});
|
|
1006
1114
|
}
|
|
1007
1115
|
}
|
|
1116
|
+
async function helpersForCall(call) {
|
|
1117
|
+
if (!ts5.isIdentifier(call.expression)) return [];
|
|
1118
|
+
const local = localObjectHelpers.get(call.expression.text) ?? [];
|
|
1119
|
+
const imported = await importedHelpers(call.expression.text);
|
|
1120
|
+
return [...local.map((helper) => ({ helper })), ...imported];
|
|
1121
|
+
}
|
|
1122
|
+
async function recordDestructuredHelper(decl) {
|
|
1123
|
+
if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
|
|
1124
|
+
const call = unwrapCall(decl.initializer);
|
|
1125
|
+
if (!call) return;
|
|
1126
|
+
const helpers = await helpersForCall(call);
|
|
1127
|
+
if (helpers.length === 0) return;
|
|
1128
|
+
for (const el of decl.name.elements) {
|
|
1129
|
+
if (!ts5.isIdentifier(el.name)) continue;
|
|
1130
|
+
const propertyName = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
1131
|
+
const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
|
|
1132
|
+
if (matches.length !== 1) continue;
|
|
1133
|
+
const resolved = matches[0];
|
|
1134
|
+
out.push({
|
|
1135
|
+
variableName: el.name.text,
|
|
1136
|
+
alias: resolved.helper.alias,
|
|
1137
|
+
aliasExpr: resolved.helper.aliasExpr,
|
|
1138
|
+
destinationExpr: resolved.helper.destinationExpr,
|
|
1139
|
+
servicePathExpr: resolved.helper.servicePathExpr,
|
|
1140
|
+
isDynamic: resolved.helper.isDynamic,
|
|
1141
|
+
placeholders: resolved.helper.placeholders,
|
|
1142
|
+
sourceFile: normalizePath(filePath),
|
|
1143
|
+
sourceLine: lineOf4(sourceFileAst, decl),
|
|
1144
|
+
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 }]
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1008
1148
|
function recordDestructuredClassHelper(decl) {
|
|
1009
1149
|
if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
|
|
1010
1150
|
const call = unwrapCall(decl.initializer);
|
|
@@ -1044,8 +1184,14 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1044
1184
|
}
|
|
1045
1185
|
collectDeclarations(sourceFileAst);
|
|
1046
1186
|
for (const decl of declarations) {
|
|
1187
|
+
await recordDestructuredHelper(decl);
|
|
1047
1188
|
recordDestructuredClassHelper(decl);
|
|
1048
1189
|
await recordVariable(decl);
|
|
1190
|
+
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
|
+
const sourceName = decl.initializer.expression.expression.text;
|
|
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" }] });
|
|
1194
|
+
}
|
|
1049
1195
|
}
|
|
1050
1196
|
return out;
|
|
1051
1197
|
}
|
|
@@ -1106,22 +1252,22 @@ function collectClassHelpers(sf) {
|
|
|
1106
1252
|
}
|
|
1107
1253
|
|
|
1108
1254
|
// src/linker/dynamic-edge-resolver.ts
|
|
1109
|
-
var PLACEHOLDER = /\$\{
|
|
1255
|
+
var PLACEHOLDER = /\$\{([^}]*)\}/g;
|
|
1110
1256
|
function applyVariables(template, vars) {
|
|
1111
1257
|
return substituteVariables(template, vars).effective;
|
|
1112
1258
|
}
|
|
1113
1259
|
function extractPlaceholders(template) {
|
|
1114
|
-
return [...(template ?? "").matchAll(PLACEHOLDER)].map((m) => m[1] ?? "").filter(Boolean);
|
|
1260
|
+
return [...(template ?? "").matchAll(PLACEHOLDER)].map((m) => (m[1] ?? "").trim()).filter(Boolean);
|
|
1115
1261
|
}
|
|
1116
1262
|
function substituteVariables(template, vars) {
|
|
1117
1263
|
if (!template) return { placeholders: [], missing: [], supplied: [], changed: false };
|
|
1118
1264
|
const placeholders2 = [...new Set(extractPlaceholders(template))];
|
|
1119
1265
|
const supplied = placeholders2.filter((key) => Object.hasOwn(vars, key));
|
|
1120
1266
|
const missing = placeholders2.filter((key) => !Object.hasOwn(vars, key));
|
|
1121
|
-
const effective = template.replace(
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
);
|
|
1267
|
+
const effective = template.replace(PLACEHOLDER, (_m, key) => {
|
|
1268
|
+
const trimmed = key.trim();
|
|
1269
|
+
return Object.hasOwn(vars, trimmed) ? vars[trimmed] ?? "" : `\${${trimmed}}`;
|
|
1270
|
+
});
|
|
1125
1271
|
return {
|
|
1126
1272
|
original: template,
|
|
1127
1273
|
effective,
|
|
@@ -1230,7 +1376,7 @@ function operationMatches(candidate, operationPath) {
|
|
|
1230
1376
|
return candidate.operationPath === names.path || candidate.operationPath === names.simplePath || candidate.operationName === names.name || candidate.operationName === names.simpleName;
|
|
1231
1377
|
}
|
|
1232
1378
|
function resolveOperation(db, signals, workspaceId) {
|
|
1233
|
-
const missing = [signals.servicePath, signals.alias, signals.destination, signals.operationPath].flatMap((value) => [...(value ?? "").matchAll(/\$\{
|
|
1379
|
+
const missing = [signals.servicePath, signals.alias, signals.destination, signals.operationPath].flatMap((value) => [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((match) => (match[1] ?? "").trim())).filter(Boolean);
|
|
1234
1380
|
if (missing.length > 0)
|
|
1235
1381
|
return {
|
|
1236
1382
|
status: "dynamic",
|
|
@@ -1475,7 +1621,7 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
1475
1621
|
const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
|
|
1476
1622
|
const unresolvedReason = status === "terminal" ? null : String(call.unresolved_reason ?? (resolution.status === "ambiguous" ? "Ambiguous operation candidates require a strong service signal" : resolution.status === "dynamic" ? `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ["missing runtime variables"]).join(", ")}` : "No indexed target operation matched"));
|
|
1477
1623
|
const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : "external";
|
|
1478
|
-
const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : String(call.event_name_expr ?? op ?? call.id);
|
|
1624
|
+
const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : callType === "remote_action" ? op ? `Remote action: ${op}` : call.unresolved_reason === "dynamic_operation_path_identifier" ? "Remote action: dynamic path" : "Remote action: unknown path" : String(call.event_name_expr ?? op ?? call.id);
|
|
1479
1625
|
const graphLevelDynamic = edgeType === "DYNAMIC_EDGE_CANDIDATE" && resolution.status === "dynamic";
|
|
1480
1626
|
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, edgeType, status, "call", String(call.id), targetKind, targetId, Number(call.confidence ?? 0.2), JSON.stringify(evidence), graphLevelDynamic ? 1 : 0, unresolvedReason, generation);
|
|
1481
1627
|
return { status, callType };
|
|
@@ -2132,4 +2278,4 @@ export {
|
|
|
2132
2278
|
linkWorkspace,
|
|
2133
2279
|
trace
|
|
2134
2280
|
};
|
|
2135
|
-
//# sourceMappingURL=chunk-
|
|
2281
|
+
//# sourceMappingURL=chunk-TSMDIKML.js.map
|