@saptools/service-flow 0.1.18 → 0.1.20
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 +3 -11
- package/TECHNICAL-NOTE.md +13 -0
- package/dist/{chunk-DUGM7FRA.js → chunk-EKLZJJW7.js} +209 -24
- package/dist/chunk-EKLZJJW7.js.map +1 -0
- package/dist/cli.js +63 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-DUGM7FRA.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.20
|
|
4
|
+
|
|
5
|
+
- Preserve service-binding evidence when helpers return connected clients inside object properties and callers destructure or create simple transaction aliases.
|
|
6
|
+
- Track full template-expression placeholder keys such as `${domainInfo.serviceName}`, `${domainInfo.shortName?.toLowerCase()}`, and `${items[0].service}` for runtime `--var` substitution without evaluating JavaScript.
|
|
7
|
+
- Render unresolved remote actions with unknown or dynamic paths as semantic targets instead of numeric call ids, with parser evidence for shorthand `path` identifiers.
|
|
8
|
+
- Expand strict doctor aggregates for normalized OData invocation ambiguity and remote-action unresolved target quality.
|
|
9
|
+
|
|
10
|
+
## 0.1.19
|
|
11
|
+
|
|
12
|
+
- Normalize balanced top-level OData action/function invocation paths before remote operation resolution, including namespace-qualified invocation lookup, while preserving raw and normalized evidence.
|
|
13
|
+
- Model remote service-client queries as terminal remote-query edges with stable semantic targets instead of unresolved operation calls.
|
|
14
|
+
- Add strict doctor aggregates for remote-query target quality and OData invocation resolution quality.
|
|
15
|
+
- Document remote query terminal semantics and normalized invocation resolution behavior.
|
|
16
|
+
|
|
3
17
|
## 0.1.18
|
|
4
18
|
|
|
5
19
|
- Applied conservative CAP receiver eligibility to `.emit()` and `.publish()` so generic realtime, socket, DOM, and EventEmitter-style calls no longer become CAP async event facts without explicit CAP messaging/service evidence.
|
package/README.md
CHANGED
|
@@ -18,14 +18,6 @@ Index independent Git repositories, persist CAP/CDS facts in SQLite, resolve cro
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
## 0.1.18 quality update
|
|
22
|
-
|
|
23
|
-
`service-flow` 0.1.18 hardens auditability after the 0.1.17 audit. Event `.emit()` and `.publish()` parsing now requires conservative CAP receiver evidence, local CAP service calls persist structured parser evidence, call-derived graph and JSON trace evidence include nested outbound parser evidence, terminal DB/external/event edges are not marked graph-dynamic, and `doctor --strict` reports outbound evidence, graph evidence propagation, event receiver classification, and dynamic flag consistency.
|
|
24
|
-
|
|
25
|
-
## 0.1.17 quality update
|
|
26
|
-
|
|
27
|
-
`service-flow` 0.1.17 hardens outbound extraction after the 0.1.16 audit. Outbound calls are classified from the TypeScript AST, so comments and strings are ignored; CAP/service lifecycle registrations that remain indexed receive synthetic event-registration owners; generic event emitters and response `.send()` calls are not treated as CAP service-flow edges; and `doctor --strict` reports actionable ownerless categories. The previous 0.1.16 source ownership and proxy-evidence improvements remain in place. It indexes class property arrow/function members, creates conservative synthetic callback symbols only for top-level framework callbacks containing supported outbound calls, prefers explicit outbound source-symbol names when provided, hardens proxy-member resolution away from ambiguous global name-only matches, and splits link output into remote/local/unresolved/ambiguous/dynamic/terminal operation-call buckets.
|
|
28
|
-
|
|
29
21
|
## ✨ Features
|
|
30
22
|
|
|
31
23
|
- 🧭 **Cross-repository CAP tracing** — starts from a repo, service, operation path, operation name, or handler and follows the indexed flow across workspace boundaries
|
|
@@ -62,7 +54,7 @@ npm install @saptools/service-flow
|
|
|
62
54
|
|
|
63
55
|
### Correctness notes
|
|
64
56
|
|
|
65
|
-
- 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.
|
|
66
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]`.
|
|
67
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.
|
|
68
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.
|
|
@@ -318,7 +310,7 @@ service-flow trace --workspace /path/to/workspace --repo facade-service --operat
|
|
|
318
310
|
|
|
319
311
|
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
312
|
|
|
321
|
-
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.
|
|
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 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.
|
|
322
314
|
|
|
323
315
|
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
316
|
|
|
@@ -419,7 +411,7 @@ Run `service-flow index` after source, CDS, package metadata, or helper-package
|
|
|
419
411
|
<summary><b>Why is an expected call unresolved?</b></summary>
|
|
420
412
|
|
|
421
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 custom wrappers may need new parser support.
|
|
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.
|
|
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 and remote-action target quality, including whether unresolved unknown/dynamic paths are semantic instead of numeric call ids.
|
|
423
415
|
|
|
424
416
|
</details>
|
|
425
417
|
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Service Flow Resolution Notes
|
|
2
2
|
|
|
3
|
+
## 0.1.20 helper object clients, expression placeholders, and remote-action target notes
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- 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.
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
|
|
10
|
+
## 0.1.19 remote invocation and query notes
|
|
11
|
+
|
|
12
|
+
- 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.
|
|
13
|
+
- Remote `send({ query })` calls without explicit operation-path evidence now become `HANDLER_RUNS_REMOTE_QUERY` terminal edges. Static entities produce `Remote entity: ...` targets; dynamic or unknown entities produce `Remote query: unknown` with parser-warning evidence.
|
|
14
|
+
- Strict doctor includes remote-query target-quality and OData invocation-resolution aggregates to catch numeric query targets and unresolved normalized invocation paths.
|
|
15
|
+
|
|
3
16
|
## 0.1.18 auditability notes
|
|
4
17
|
|
|
5
18
|
- CAP async event parsing treats `.emit()`, `.publish()`, and `.on()` consistently: a row is indexed only when the direct or chained root receiver has explicit CAP service or messaging evidence such as `cds` or a variable initialized from `cds.connect.to(...)`. Generic realtime/socket/EventEmitter receivers are ignored for CAP graph purposes by default.
|
|
@@ -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,33 @@ 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 (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
|
|
904
|
+
const fact = findConnectInExpression(node.initializer);
|
|
905
|
+
if (fact) bindings.set(node.name.text, fact);
|
|
906
|
+
}
|
|
907
|
+
if (ts5.isReturnStatement(node) && node.expression && ts5.isObjectLiteralExpression(node.expression)) {
|
|
908
|
+
for (const prop of node.expression.properties) {
|
|
909
|
+
if (ts5.isShorthandPropertyAssignment(prop)) returns.set(prop.name.text, prop.name.text);
|
|
910
|
+
if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.initializer)) {
|
|
911
|
+
const propertyName = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
912
|
+
if (propertyName) returns.set(propertyName, prop.initializer.text);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
ts5.forEachChild(node, visit);
|
|
917
|
+
}
|
|
918
|
+
visit(fn);
|
|
919
|
+
const out = /* @__PURE__ */ new Map();
|
|
920
|
+
for (const [propertyName, variableName] of returns) {
|
|
921
|
+
const fact = bindings.get(variableName);
|
|
922
|
+
if (fact) out.set(propertyName, fact);
|
|
923
|
+
}
|
|
924
|
+
return out;
|
|
925
|
+
}
|
|
892
926
|
function exportedLocalNames(sf) {
|
|
893
927
|
const exports = /* @__PURE__ */ new Map();
|
|
894
928
|
for (const stmt of sf.statements) {
|
|
@@ -924,6 +958,8 @@ async function helperBindings(repoPath, filePath) {
|
|
|
924
958
|
if (!fact) ts5.forEachChild(node, visit);
|
|
925
959
|
});
|
|
926
960
|
if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf4(sf, stmt) });
|
|
961
|
+
for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(stmt))
|
|
962
|
+
factsByLocal.set(`${stmt.name.text}#${returnedProperty}`, { ...objectFact, returnedProperty, sourceLine: lineOf4(sf, stmt) });
|
|
927
963
|
}
|
|
928
964
|
if (ts5.isVariableStatement(stmt))
|
|
929
965
|
for (const decl of stmt.declarationList.declarations) {
|
|
@@ -946,6 +982,14 @@ async function helperBindings(repoPath, filePath) {
|
|
|
946
982
|
sourceLine: fact.sourceLine
|
|
947
983
|
});
|
|
948
984
|
}
|
|
985
|
+
for (const [key, fact] of factsByLocal) {
|
|
986
|
+
const [localName, returnedProperty] = key.split("#");
|
|
987
|
+
if (!returnedProperty) continue;
|
|
988
|
+
for (const [exportedName, exportedLocal] of exportedLocals) {
|
|
989
|
+
if (exportedLocal !== localName) continue;
|
|
990
|
+
out.push({ ...fact, exportedName, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: fact.sourceLine });
|
|
991
|
+
}
|
|
992
|
+
}
|
|
949
993
|
return out;
|
|
950
994
|
}
|
|
951
995
|
async function parseServiceBindings(repoPath, filePath) {
|
|
@@ -956,6 +1000,14 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
956
1000
|
const imports = await importsFor(repoPath, filePath, sf);
|
|
957
1001
|
const helperCache = /* @__PURE__ */ new Map();
|
|
958
1002
|
const classHelpers = collectClassHelpers(sourceFileAst);
|
|
1003
|
+
const localObjectHelpers = /* @__PURE__ */ new Map();
|
|
1004
|
+
for (const stmt of sourceFileAst.statements) {
|
|
1005
|
+
if (!ts5.isFunctionDeclaration(stmt) || !stmt.name) continue;
|
|
1006
|
+
const rows2 = [];
|
|
1007
|
+
for (const [returnedProperty, fact] of collectReturnedObjectBindings(stmt))
|
|
1008
|
+
rows2.push({ ...fact, exportedName: stmt.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, stmt) });
|
|
1009
|
+
if (rows2.length > 0) localObjectHelpers.set(stmt.name.text, rows2);
|
|
1010
|
+
}
|
|
959
1011
|
async function importedHelper(localName) {
|
|
960
1012
|
const imp = imports.find((i) => i.localName === localName && i.sourceFile);
|
|
961
1013
|
if (!imp?.sourceFile) return void 0;
|
|
@@ -1005,6 +1057,37 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1005
1057
|
});
|
|
1006
1058
|
}
|
|
1007
1059
|
}
|
|
1060
|
+
async function helperForCall(call) {
|
|
1061
|
+
if (!ts5.isIdentifier(call.expression)) return void 0;
|
|
1062
|
+
const local = localObjectHelpers.get(call.expression.text)?.[0];
|
|
1063
|
+
if (local) return { helper: local };
|
|
1064
|
+
const resolved = await importedHelper(call.expression.text);
|
|
1065
|
+
return resolved ? { helper: resolved.helper, imp: resolved.imp } : void 0;
|
|
1066
|
+
}
|
|
1067
|
+
async function recordDestructuredHelper(decl) {
|
|
1068
|
+
if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
|
|
1069
|
+
const call = unwrapCall(decl.initializer);
|
|
1070
|
+
if (!call) return;
|
|
1071
|
+
const resolved = await helperForCall(call);
|
|
1072
|
+
if (!resolved) return;
|
|
1073
|
+
for (const el of decl.name.elements) {
|
|
1074
|
+
if (!ts5.isIdentifier(el.name)) continue;
|
|
1075
|
+
const propertyName = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
1076
|
+
if (resolved.helper.returnedProperty !== propertyName) continue;
|
|
1077
|
+
out.push({
|
|
1078
|
+
variableName: el.name.text,
|
|
1079
|
+
alias: resolved.helper.alias,
|
|
1080
|
+
aliasExpr: resolved.helper.aliasExpr,
|
|
1081
|
+
destinationExpr: resolved.helper.destinationExpr,
|
|
1082
|
+
servicePathExpr: resolved.helper.servicePathExpr,
|
|
1083
|
+
isDynamic: resolved.helper.isDynamic,
|
|
1084
|
+
placeholders: resolved.helper.placeholders,
|
|
1085
|
+
sourceFile: normalizePath(filePath),
|
|
1086
|
+
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 }]
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1008
1091
|
function recordDestructuredClassHelper(decl) {
|
|
1009
1092
|
if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
|
|
1010
1093
|
const call = unwrapCall(decl.initializer);
|
|
@@ -1044,8 +1127,14 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1044
1127
|
}
|
|
1045
1128
|
collectDeclarations(sourceFileAst);
|
|
1046
1129
|
for (const decl of declarations) {
|
|
1130
|
+
await recordDestructuredHelper(decl);
|
|
1047
1131
|
recordDestructuredClassHelper(decl);
|
|
1048
1132
|
await recordVariable(decl);
|
|
1133
|
+
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
|
+
const sourceName = decl.initializer.expression.expression.text;
|
|
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" }] });
|
|
1137
|
+
}
|
|
1049
1138
|
}
|
|
1050
1139
|
return out;
|
|
1051
1140
|
}
|
|
@@ -1106,22 +1195,22 @@ function collectClassHelpers(sf) {
|
|
|
1106
1195
|
}
|
|
1107
1196
|
|
|
1108
1197
|
// src/linker/dynamic-edge-resolver.ts
|
|
1109
|
-
var PLACEHOLDER = /\$\{
|
|
1198
|
+
var PLACEHOLDER = /\$\{([^}]*)\}/g;
|
|
1110
1199
|
function applyVariables(template, vars) {
|
|
1111
1200
|
return substituteVariables(template, vars).effective;
|
|
1112
1201
|
}
|
|
1113
1202
|
function extractPlaceholders(template) {
|
|
1114
|
-
return [...(template ?? "").matchAll(PLACEHOLDER)].map((m) => m[1] ?? "").filter(Boolean);
|
|
1203
|
+
return [...(template ?? "").matchAll(PLACEHOLDER)].map((m) => (m[1] ?? "").trim()).filter(Boolean);
|
|
1115
1204
|
}
|
|
1116
1205
|
function substituteVariables(template, vars) {
|
|
1117
1206
|
if (!template) return { placeholders: [], missing: [], supplied: [], changed: false };
|
|
1118
1207
|
const placeholders2 = [...new Set(extractPlaceholders(template))];
|
|
1119
1208
|
const supplied = placeholders2.filter((key) => Object.hasOwn(vars, key));
|
|
1120
1209
|
const missing = placeholders2.filter((key) => !Object.hasOwn(vars, key));
|
|
1121
|
-
const effective = template.replace(
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
);
|
|
1210
|
+
const effective = template.replace(PLACEHOLDER, (_m, key) => {
|
|
1211
|
+
const trimmed = key.trim();
|
|
1212
|
+
return Object.hasOwn(vars, trimmed) ? vars[trimmed] ?? "" : `\${${trimmed}}`;
|
|
1213
|
+
});
|
|
1125
1214
|
return {
|
|
1126
1215
|
original: template,
|
|
1127
1216
|
effective,
|
|
@@ -1132,21 +1221,105 @@ function substituteVariables(template, vars) {
|
|
|
1132
1221
|
};
|
|
1133
1222
|
}
|
|
1134
1223
|
|
|
1224
|
+
// src/linker/odata-path-normalizer.ts
|
|
1225
|
+
function normalizeODataOperationInvocationPath(path9) {
|
|
1226
|
+
if (path9 === void 0) return void 0;
|
|
1227
|
+
const raw = path9.trim();
|
|
1228
|
+
if (!raw) return void 0;
|
|
1229
|
+
const open = raw.indexOf("(");
|
|
1230
|
+
if (open < 0) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
|
|
1231
|
+
if (!raw.startsWith("/") || raw.slice(1, open).includes("/")) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
|
|
1232
|
+
const close = matchingClose(raw, open);
|
|
1233
|
+
if (close === void 0 || raw.slice(close + 1).trim().length > 0) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
|
|
1234
|
+
const operationSegment = raw.slice(0, open).trim();
|
|
1235
|
+
if (operationSegment.length <= 1) return { rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false };
|
|
1236
|
+
return { rawOperationPath: raw, normalizedOperationPath: operationSegment, wasInvocation: true };
|
|
1237
|
+
}
|
|
1238
|
+
function matchingClose(text, openIndex) {
|
|
1239
|
+
let depth = 0;
|
|
1240
|
+
let quote;
|
|
1241
|
+
for (let index = openIndex; index < text.length; index += 1) {
|
|
1242
|
+
const char = text[index];
|
|
1243
|
+
const prev = text[index - 1];
|
|
1244
|
+
if (quote) {
|
|
1245
|
+
if (prev === "\\") continue;
|
|
1246
|
+
if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (char === "'") {
|
|
1250
|
+
quote = "single";
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
if (char === '"') {
|
|
1254
|
+
quote = "double";
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
if (char === "`") {
|
|
1258
|
+
quote = "template";
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
if (char === "(") depth += 1;
|
|
1262
|
+
if (char === ")") {
|
|
1263
|
+
depth -= 1;
|
|
1264
|
+
if (depth === 0) return index;
|
|
1265
|
+
if (depth < 0) return void 0;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return void 0;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/linker/remote-query-target.ts
|
|
1272
|
+
function buildRemoteQueryTarget(input) {
|
|
1273
|
+
const entity = typeof input.queryEntity === "string" && input.queryEntity.trim() ? input.queryEntity.trim() : void 0;
|
|
1274
|
+
const servicePath = input.servicePath?.trim();
|
|
1275
|
+
const prefix = servicePath ? `${servicePath}:` : "";
|
|
1276
|
+
const label = entity ? `Remote entity: ${prefix}${entity}` : "Remote query: unknown";
|
|
1277
|
+
return {
|
|
1278
|
+
toKind: entity ? "remote_entity" : "remote_query",
|
|
1279
|
+
toId: entity ? `${prefix}${entity}` : "unknown",
|
|
1280
|
+
label,
|
|
1281
|
+
evidence: {
|
|
1282
|
+
remoteQueryTarget: label,
|
|
1283
|
+
queryEntity: entity,
|
|
1284
|
+
queryTargetKind: entity ? "remote_entity" : "remote_query_unknown",
|
|
1285
|
+
queryEntityDynamic: entity ? void 0 : Boolean(input.isDynamic) || void 0,
|
|
1286
|
+
serviceAlias: input.serviceAlias,
|
|
1287
|
+
serviceAliasExpr: input.serviceAliasExpr,
|
|
1288
|
+
destination: input.destination,
|
|
1289
|
+
servicePath,
|
|
1290
|
+
parserWarning: entity ? input.parserWarning : input.parserWarning ?? { code: "query_entity_unknown", message: "Remote query entity is dynamic or unavailable" }
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1135
1295
|
// src/linker/service-resolver.ts
|
|
1136
1296
|
function rows(db, operationPath, workspaceId) {
|
|
1297
|
+
const names = operationLookupNames(operationPath);
|
|
1137
1298
|
return db.prepare(
|
|
1138
1299
|
`SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score,'' reasons
|
|
1139
1300
|
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
|
|
1140
|
-
WHERE (? IS NULL OR r.workspace_id=?) AND (o.operation_path
|
|
1301
|
+
WHERE (? IS NULL OR r.workspace_id=?) AND (o.operation_path IN (?,?) OR o.operation_name IN (?,?)) ORDER BY r.name,s.service_path,o.operation_name`
|
|
1141
1302
|
).all(
|
|
1142
1303
|
workspaceId,
|
|
1143
1304
|
workspaceId,
|
|
1144
|
-
|
|
1145
|
-
|
|
1305
|
+
names.path,
|
|
1306
|
+
names.simplePath,
|
|
1307
|
+
names.name,
|
|
1308
|
+
names.simpleName
|
|
1146
1309
|
);
|
|
1147
1310
|
}
|
|
1311
|
+
function operationLookupNames(operationPath) {
|
|
1312
|
+
const name = operationPath.replace(/^\//, "");
|
|
1313
|
+
const simpleName = name.split(".").at(-1) ?? name;
|
|
1314
|
+
return { path: operationPath, simplePath: `/${simpleName}`, name, simpleName };
|
|
1315
|
+
}
|
|
1316
|
+
function operationMatches(candidate, operationPath) {
|
|
1317
|
+
if (!operationPath) return false;
|
|
1318
|
+
const names = operationLookupNames(operationPath);
|
|
1319
|
+
return candidate.operationPath === names.path || candidate.operationPath === names.simplePath || candidate.operationName === names.name || candidate.operationName === names.simpleName;
|
|
1320
|
+
}
|
|
1148
1321
|
function resolveOperation(db, signals, workspaceId) {
|
|
1149
|
-
const missing = [signals.servicePath, signals.alias, signals.destination, signals.operationPath].flatMap((value) => [...(value ?? "").matchAll(/\$\{
|
|
1322
|
+
const missing = [signals.servicePath, signals.alias, signals.destination, signals.operationPath].flatMap((value) => [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((match) => (match[1] ?? "").trim())).filter(Boolean);
|
|
1150
1323
|
if (missing.length > 0)
|
|
1151
1324
|
return {
|
|
1152
1325
|
status: "dynamic",
|
|
@@ -1212,7 +1385,7 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
1212
1385
|
c.score += 0.2;
|
|
1213
1386
|
c.reasons.push(signals.repoId !== void 0 ? "explicit_local_service_call" : "explicit_dynamic_override");
|
|
1214
1387
|
}
|
|
1215
|
-
if (signals.repoId !== void 0 && candidates.length === 1 && signals.serviceName && c.reasons.includes("local_service_name_mismatch") && (c
|
|
1388
|
+
if (signals.repoId !== void 0 && candidates.length === 1 && signals.serviceName && c.reasons.includes("local_service_name_mismatch") && operationMatches(c, signals.operationPath)) {
|
|
1216
1389
|
c.score = Math.max(c.score, 0.9);
|
|
1217
1390
|
c.reasons.push("same_repo_unique_operation_path_with_lookup_mismatch");
|
|
1218
1391
|
}
|
|
@@ -1235,7 +1408,7 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
1235
1408
|
candidates,
|
|
1236
1409
|
reasons: ["operation_path_only_has_no_strong_target_signal"]
|
|
1237
1410
|
};
|
|
1238
|
-
if (best && best.score >= 0.9 && (best.servicePath === signals.servicePath || Boolean(signals.serviceName && (!best.reasons.includes("local_service_name_mismatch") || best.reasons.includes("same_repo_unique_operation_path_with_lookup_mismatch")))) && (best
|
|
1411
|
+
if (best && best.score >= 0.9 && (best.servicePath === signals.servicePath || Boolean(signals.serviceName && (!best.reasons.includes("local_service_name_mismatch") || best.reasons.includes("same_repo_unique_operation_path_with_lookup_mismatch")))) && operationMatches(best, signals.operationPath) && (!second || best.score - second.score >= 0.25))
|
|
1239
1412
|
return {
|
|
1240
1413
|
status: "resolved",
|
|
1241
1414
|
target: best,
|
|
@@ -1365,13 +1538,20 @@ function linkCalls(db, workspaceId, vars, generation) {
|
|
|
1365
1538
|
}
|
|
1366
1539
|
function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
1367
1540
|
const callType = String(call.call_type);
|
|
1368
|
-
const
|
|
1541
|
+
const rawOp = applyVariables(String(call.operation_path_expr ?? ""), vars);
|
|
1542
|
+
const normalized = normalizeODataOperationInvocationPath(rawOp);
|
|
1543
|
+
const op = normalized?.normalizedOperationPath ?? rawOp;
|
|
1369
1544
|
const servicePath = applyVariables(call.servicePathExpr ?? call.requireServicePath, vars);
|
|
1370
1545
|
const destination = call.destinationExpr ?? call.requireDestination;
|
|
1371
1546
|
const isDynamic = Boolean(Number(call.isDynamic ?? 0));
|
|
1372
|
-
const isOperationCall = callType
|
|
1547
|
+
const isOperationCall = callType === "remote_action" || callType === "local_service_call" || callType === "remote_query" && Boolean(op);
|
|
1373
1548
|
const resolution = isOperationCall ? resolveOperation(db, { servicePath, operationPath: op, serviceName: call.local_service_name, repoId: callType === "local_service_call" ? Number(call.repo_id) : void 0, alias: applyVariables(call.aliasExpr ?? call.alias, vars), destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 || callType === "local_service_call" }, workspaceId) : { status: "unresolved", candidates: [], reasons: [] };
|
|
1374
|
-
const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0);
|
|
1549
|
+
const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0, normalized);
|
|
1550
|
+
if (callType === "remote_query" && !op) {
|
|
1551
|
+
const target = buildRemoteQueryTarget({ queryEntity: call.query_entity, servicePath, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, parserWarning: evidence.parserWarning });
|
|
1552
|
+
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "HANDLER_RUNS_REMOTE_QUERY", "terminal", "call", String(call.id), target.toKind, target.toId, Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, ...target.evidence }), 0, generation);
|
|
1553
|
+
return { status: "terminal", callType };
|
|
1554
|
+
}
|
|
1375
1555
|
if (callType === "local_service_call" && call.unresolved_reason === "transport_client_method" && !resolution.target && resolution.candidates.length === 0) {
|
|
1376
1556
|
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "HANDLER_CALLS_EXTERNAL_HTTP", "terminal", "call", String(call.id), "external", String(op || "transport_client_method"), Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, classification: "transport_client_method" }), 0, generation);
|
|
1377
1557
|
return { status: "terminal", callType };
|
|
@@ -1384,7 +1564,7 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
1384
1564
|
const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
|
|
1385
1565
|
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"));
|
|
1386
1566
|
const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : "external";
|
|
1387
|
-
const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : String(call.event_name_expr ?? op ?? call.id);
|
|
1567
|
+
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);
|
|
1388
1568
|
const graphLevelDynamic = edgeType === "DYNAMIC_EDGE_CANDIDATE" && resolution.status === "dynamic";
|
|
1389
1569
|
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);
|
|
1390
1570
|
return { status, callType };
|
|
@@ -1401,9 +1581,9 @@ function objectJson(value) {
|
|
|
1401
1581
|
const parsed = parseJson(value);
|
|
1402
1582
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
1403
1583
|
}
|
|
1404
|
-
function callEvidence(call, resolution, servicePath, op, destination) {
|
|
1584
|
+
function callEvidence(call, resolution, servicePath, op, destination, normalized) {
|
|
1405
1585
|
const bindingHasDynamicExpression = Boolean(Number(call.isDynamic ?? 0));
|
|
1406
|
-
return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, callId: call.id, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: parseJson(call.alias_chain_json), transport: call.call_type === "local_service_call" ? "local" : void 0, targetRepo: resolution.target?.repoName, targetOperation: resolution.target?.operationName, helperChain: parseJson(call.helperChainJson), candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, bindingHasDynamicExpression: bindingHasDynamicExpression || void 0, outboundEvidence: objectJson(call.evidence_json), analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
|
|
1586
|
+
return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, callId: call.id, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, rawOperationPath: normalized?.wasInvocation ? normalized.rawOperationPath : void 0, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: parseJson(call.alias_chain_json), transport: call.call_type === "local_service_call" ? "local" : void 0, targetRepo: resolution.target?.repoName, targetServicePath: resolution.target?.servicePath, targetOperationPath: resolution.target?.operationPath, targetOperation: resolution.target?.operationName, helperChain: parseJson(call.helperChainJson), candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, bindingHasDynamicExpression: bindingHasDynamicExpression || void 0, outboundEvidence: objectJson(call.evidence_json), analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
|
|
1407
1587
|
}
|
|
1408
1588
|
function linkImplementations(db, workspaceId, generation) {
|
|
1409
1589
|
const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,r.package_name modelPackage,r.kind modelKind FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=?`).all(workspaceId);
|
|
@@ -1833,7 +2013,7 @@ function runtimeResolution(db, row, evidence, vars) {
|
|
|
1833
2013
|
return { row, evidence, unresolvedReason: row.unresolved_reason };
|
|
1834
2014
|
const nextEvidence = evidenceWithRuntimeVariables(evidence, vars);
|
|
1835
2015
|
const servicePath = typeof nextEvidence.servicePath === "string" ? nextEvidence.servicePath : void 0;
|
|
1836
|
-
const operationPath = typeof nextEvidence.operationPath === "string" ? nextEvidence.operationPath : void 0;
|
|
2016
|
+
const operationPath = typeof nextEvidence.normalizedOperationPath === "string" ? nextEvidence.normalizedOperationPath : typeof nextEvidence.operationPath === "string" ? nextEvidence.operationPath : void 0;
|
|
1837
2017
|
const alias = typeof nextEvidence.serviceAliasExpr === "string" ? nextEvidence.serviceAliasExpr : typeof nextEvidence.serviceAlias === "string" ? nextEvidence.serviceAlias : void 0;
|
|
1838
2018
|
const destination = typeof nextEvidence.destination === "string" ? nextEvidence.destination : void 0;
|
|
1839
2019
|
const resolution = resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: true }, workspaceIdForCall(db, row.from_id));
|
|
@@ -1850,11 +2030,15 @@ function edgeTarget(row, evidence) {
|
|
|
1850
2030
|
const runtimeCandidate = evidence.runtimeResolvedCandidate;
|
|
1851
2031
|
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath)
|
|
1852
2032
|
return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
|
|
2033
|
+
const targetServicePath = typeof evidence.targetServicePath === "string" ? evidence.targetServicePath : void 0;
|
|
2034
|
+
const targetOperationPath = typeof evidence.targetOperationPath === "string" ? evidence.targetOperationPath : void 0;
|
|
2035
|
+
if (targetServicePath && targetOperationPath) return `${targetServicePath}${targetOperationPath}`;
|
|
1853
2036
|
const servicePath = typeof evidence.servicePath === "string" ? evidence.servicePath : void 0;
|
|
1854
2037
|
const operationPath = typeof evidence.operationPath === "string" ? evidence.operationPath : void 0;
|
|
1855
2038
|
const targetOperation = typeof evidence.targetOperation === "string" ? evidence.targetOperation : void 0;
|
|
1856
2039
|
const targetRepo = typeof evidence.targetRepo === "string" ? evidence.targetRepo : "";
|
|
1857
2040
|
if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return `Entity: ${row.to_id || "unknown"}`;
|
|
2041
|
+
if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return typeof evidence.remoteQueryTarget === "string" ? evidence.remoteQueryTarget : `Remote query: ${row.to_id || "unknown"}`;
|
|
1858
2042
|
return servicePath && operationPath ? `${servicePath}${operationPath}` : targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
|
|
1859
2043
|
}
|
|
1860
2044
|
function trace(db, start, options) {
|
|
@@ -2033,7 +2217,8 @@ export {
|
|
|
2033
2217
|
applyVariables,
|
|
2034
2218
|
extractPlaceholders,
|
|
2035
2219
|
substituteVariables,
|
|
2220
|
+
normalizeODataOperationInvocationPath,
|
|
2036
2221
|
linkWorkspace,
|
|
2037
2222
|
trace
|
|
2038
2223
|
};
|
|
2039
|
-
//# sourceMappingURL=chunk-
|
|
2224
|
+
//# sourceMappingURL=chunk-EKLZJJW7.js.map
|