@saptools/service-flow 0.1.4 → 0.1.5
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 +10 -0
- package/README.md +3 -3
- package/TECHNICAL-NOTE.md +9 -2
- package/dist/{chunk-JY6GBGZT.js → chunk-I33ASGX3.js} +154 -101
- package/dist/chunk-I33ASGX3.js.map +1 -0
- package/dist/cli.js +128 -85
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-JY6GBGZT.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5
|
|
4
|
+
|
|
5
|
+
- Runtime `--var` substitution now re-runs operation resolution in memory for trace and graph output, clears stale unresolved reasons on exact matches, and traverses into the matched downstream handler without mutating the persisted graph.
|
|
6
|
+
- Operation nodes now use target CDS provenance while call-site evidence remains on edges.
|
|
7
|
+
- Graph edges now carry explicit status values for resolved, terminal, dynamic, ambiguous, and unresolved cases; normal DB, external HTTP, and event terminals no longer receive remote-resolution failure text.
|
|
8
|
+
- Replaced per-statement SQLite shelling with a persistent `node:sqlite` connection, bound parameters, transactions, read-only query openings, WAL, busy timeouts, and connection-local foreign-key enforcement.
|
|
9
|
+
- Added schema user-version migration support for edge status and repository fingerprints.
|
|
10
|
+
- Repository indexing now skips unchanged repositories unless `--force` is supplied and reports indexed/skipped counts.
|
|
11
|
+
- `service-flow --version` now matches package metadata for the 0.1.5 release.
|
|
12
|
+
|
|
3
13
|
## 0.1.4
|
|
4
14
|
|
|
5
15
|
- Resolved helper-returned service bindings exported through named export lists, including aliased exports, while preserving helper-chain evidence.
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Index independent Git repositories, persist CAP/CDS facts in SQLite, resolve cro
|
|
|
24
24
|
- 🧩 **Static CAP/CDS indexing** — extracts services, actions, functions, events, handler classes, decorator metadata, handler registrations, generated constants, and package-level `cds.requires`
|
|
25
25
|
- 🔗 **Service-to-service linking** — resolves `cds.connect.to(...)`, `remote.send(...)`, `cds.services.*` style calls, helper package imports, dynamic candidates, and unresolved evidence into graph edges
|
|
26
26
|
- 🗄️ **SQLite-backed workspace cache** — stores deterministic facts under `.service-flow/service-flow.db` so large workspaces can be queried repeatedly without reparsing everything
|
|
27
|
-
- 🧠 **Dynamic edge support** — preserves parameterized destinations and service paths such as `svc_${objectCode}_process`, then lets
|
|
27
|
+
- 🧠 **Dynamic edge support** — preserves parameterized destinations and service paths such as `svc_${objectCode}_process`, then lets trace and graph commands apply runtime `--var key=value` values that can turn dynamic candidates into effective traversable operation edges
|
|
28
28
|
- 📊 **Multiple output modes** — renders human-readable tables, JSON for automation, and Mermaid diagrams for architecture docs
|
|
29
29
|
- 🩺 **Diagnostics-first workflow** — records parse/index issues and exposes them through `service-flow doctor` instead of hiding partial analysis
|
|
30
30
|
- 🧩 **CAP helper-aware binding evidence** — follows imported helpers exported directly or through named export lists and separates alias, destination, and service-path expressions for dynamic `cds.connect.to(alias, options)` calls
|
|
@@ -47,7 +47,7 @@ npm install @saptools/service-flow
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
> [!NOTE]
|
|
50
|
-
> Requires **Node.js ≥ 20**. The analyzer is static: it reads files and package metadata, but it does not start CAP services, connect to SAP BTP, or execute application code.
|
|
50
|
+
> Requires **Node.js ≥ 20**. Version 0.1.5 uses a persistent SQLite driver (`node:sqlite` in supported Node builds) for bound parameters, transactions, WAL, busy timeouts, and read-only query commands. The analyzer is static: it reads files and package metadata, but it does not start CAP services, connect to SAP BTP, or execute application code.
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
@@ -73,7 +73,7 @@ service-flow graph --workspace /path/to/workspace --service /FacadeService --pat
|
|
|
73
73
|
service-flow doctor --workspace /path/to/workspace
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
After `init`, the workspace configuration and SQLite database live below the selected workspace by default. Run `index` whenever source changes
|
|
76
|
+
After `init`, the workspace configuration and SQLite database live below the selected workspace by default. Run `index` whenever source changes; unchanged repositories are skipped unless `--force` is supplied. Then run `link` to rebuild the graph edges used by `trace` and `graph`.
|
|
77
77
|
|
|
78
78
|
---
|
|
79
79
|
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Service Flow 0.1.
|
|
1
|
+
# Service Flow 0.1.5 Resolution Notes
|
|
2
2
|
|
|
3
3
|
- Imported helper bindings: TypeScript imports are resolved for relative modules. When a caller assigns `const client = await connectToService()`, the analyzer follows the imported symbol to an exported helper that returns `cds.connect.to(...)` and persists caller-variable evidence plus the helper source/export chain.
|
|
4
4
|
- Candidate ranking: operation-path matches start as weak candidates. A resolved operation edge requires a strong signal such as exact service path, CDS alias/destination context, or explicit dynamic variable overrides. Otherwise candidates are preserved in edge evidence as ambiguous or unresolved.
|
|
5
5
|
- Edge states: `REMOTE_CALL_RESOLVES_TO_OPERATION` is used only above the resolution threshold; `DYNAMIC_EDGE_CANDIDATE` preserves runtime-dependent service paths/destinations; `UNRESOLVED_EDGE` carries candidate counts and reasons when static evidence is insufficient.
|
|
6
6
|
- Trace cycle safety: trace queues carry repository IDs, visited scope keys are independent of depth, graph edge IDs are emitted once, and revisiting an already-seen downstream operation scope creates a cycle marker instead of recursive expansion.
|
|
7
|
-
- SQLite reliability: the package uses
|
|
7
|
+
- SQLite reliability: the package uses a persistent SQLite connection per opened database, bound parameters, transactions, WAL, busy timeouts, read-only openings for query commands, and connection-local foreign-key enforcement. Native driver loading failures produce an actionable startup error before output rendering.
|
|
8
8
|
|
|
9
9
|
## 0.1.4 trace-correctness additions
|
|
10
10
|
|
|
@@ -12,3 +12,10 @@
|
|
|
12
12
|
- Two-argument CAP connections keep alias expressions distinct from `credentials.destination` and `credentials.path` / `credentials.servicePath`; dynamic placeholders from all three fields are retained for later `--var` substitution.
|
|
13
13
|
- Repository discovery validates `.git` markers using `HEAD`, `config`, or gitfile `gitdir:` content and keeps scanning children so outer workspaces can contain many nested repositories.
|
|
14
14
|
- Fresh SQLite stores now declare core parent/child foreign keys with cascading cleanup for repository-owned facts.
|
|
15
|
+
|
|
16
|
+
## 0.1.5 hardening additions
|
|
17
|
+
|
|
18
|
+
- Runtime variables are applied to alias, destination, service path, and operation path evidence. Trace/graph then perform an effective in-memory resolution requiring both operation-path compatibility and an exact service-path signal.
|
|
19
|
+
- Edge evidence and target operation provenance are separate: call file/line and helper chains stay on the edge, while operation nodes are loaded from CDS operation/service/repository rows.
|
|
20
|
+
- The linker stores explicit edge status categories and summary counts. Valid DB, event, and external terminals keep `unresolved_reason` null.
|
|
21
|
+
- Repository-level fingerprints include source paths/hashes, package dependencies, and analyzer schema version. Unchanged repositories are skipped unless `--force` is used.
|
|
@@ -895,6 +895,13 @@ function rows(db, operationPath, workspaceId) {
|
|
|
895
895
|
);
|
|
896
896
|
}
|
|
897
897
|
function resolveOperation(db, signals, workspaceId) {
|
|
898
|
+
const missing = [signals.servicePath, signals.alias, signals.destination, signals.operationPath].flatMap((value) => [...(value ?? "").matchAll(/\$\{\s*(\w+)\s*\}/g)].map((match) => match[1] ?? "")).filter(Boolean);
|
|
899
|
+
if (missing.length > 0)
|
|
900
|
+
return {
|
|
901
|
+
status: "dynamic",
|
|
902
|
+
candidates: signals.operationPath ? rows(db, signals.operationPath, workspaceId) : [],
|
|
903
|
+
reasons: [...new Set(missing)].map((name) => `missing_variable:${name}`)
|
|
904
|
+
};
|
|
898
905
|
if (!signals.operationPath)
|
|
899
906
|
return {
|
|
900
907
|
status: "unresolved",
|
|
@@ -946,7 +953,7 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
946
953
|
candidates,
|
|
947
954
|
reasons: ["operation_path_only_has_no_strong_target_signal"]
|
|
948
955
|
};
|
|
949
|
-
if (best && best.score >= 0.9 && (!second || best.score - second.score >= 0.25))
|
|
956
|
+
if (best && best.score >= 0.9 && best.servicePath === signals.servicePath && (best.operationPath === signals.operationPath || best.operationName === signals.operationPath.replace(/^\//, "")) && (!second || best.score - second.score >= 0.25))
|
|
950
957
|
return {
|
|
951
958
|
status: "resolved",
|
|
952
959
|
target: best,
|
|
@@ -993,91 +1000,105 @@ function linkHelperPackages(db, workspaceId) {
|
|
|
993
1000
|
|
|
994
1001
|
// src/linker/cross-repo-linker.ts
|
|
995
1002
|
function linkWorkspace(db, workspaceId, vars = {}) {
|
|
996
|
-
db.
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1003
|
+
return db.transaction(() => {
|
|
1004
|
+
db.prepare("DELETE FROM graph_edges WHERE workspace_id=?").run(workspaceId);
|
|
1005
|
+
let edges = linkHelperPackages(db, workspaceId);
|
|
1006
|
+
let unresolved = 0;
|
|
1007
|
+
let resolvedCount = 0;
|
|
1008
|
+
let ambiguousCount = 0;
|
|
1009
|
+
let dynamicCount = 0;
|
|
1010
|
+
let terminalCount = 0;
|
|
1011
|
+
const calls = db.prepare(
|
|
1012
|
+
`SELECT c.*,r.name repoName,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.is_dynamic isDynamic,b.placeholders_json placeholdersJson,b.helper_chain_json helperChainJson,req.service_path requireServicePath,req.destination requireDestination FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id LEFT JOIN service_bindings b ON b.id=c.service_binding_id LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias WHERE r.workspace_id=?`
|
|
1013
|
+
).all(workspaceId);
|
|
1014
|
+
for (const call of calls) {
|
|
1015
|
+
const callType = String(call.call_type);
|
|
1016
|
+
const op = applyVariables(String(call.operation_path_expr ?? ""), vars);
|
|
1017
|
+
const servicePath = applyVariables(
|
|
1018
|
+
call.servicePathExpr ?? call.requireServicePath,
|
|
1019
|
+
vars
|
|
1020
|
+
);
|
|
1021
|
+
const destination = call.destinationExpr ?? call.requireDestination;
|
|
1022
|
+
const isDynamic = Boolean(Number(call.isDynamic ?? 0));
|
|
1023
|
+
const resolution = callType.startsWith("remote") ? resolveOperation(
|
|
1024
|
+
db,
|
|
1025
|
+
{
|
|
1026
|
+
servicePath,
|
|
1027
|
+
operationPath: op,
|
|
1028
|
+
alias: applyVariables(call.aliasExpr ?? call.alias, vars),
|
|
1029
|
+
destination: applyVariables(destination, vars),
|
|
1030
|
+
isDynamic,
|
|
1031
|
+
hasExplicitOverride: Object.keys(vars).length > 0
|
|
1032
|
+
},
|
|
1033
|
+
workspaceId
|
|
1034
|
+
) : { status: "unresolved", candidates: [], reasons: [] };
|
|
1035
|
+
const target = resolution.target;
|
|
1036
|
+
const evidence = {
|
|
1037
|
+
sourceFile: call.source_file,
|
|
1038
|
+
sourceLine: call.source_line,
|
|
1039
|
+
file: call.source_file,
|
|
1040
|
+
line: call.source_line,
|
|
1041
|
+
repo: call.repoName,
|
|
1042
|
+
serviceAlias: call.alias,
|
|
1043
|
+
serviceAliasExpr: call.aliasExpr,
|
|
1044
|
+
destination: applyVariables(destination, vars),
|
|
1014
1045
|
servicePath,
|
|
1015
1046
|
operationPath: op,
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
"
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
String(call.event_name_expr ?? call.query_entity ?? op ?? call.id),
|
|
1069
|
-
Number(call.confidence ?? 0.2),
|
|
1070
|
-
JSON.stringify(evidence),
|
|
1071
|
-
isDynamic || resolution.status === "dynamic" ? 1 : 0,
|
|
1072
|
-
String(
|
|
1073
|
-
call.unresolved_reason ?? (resolution.status === "ambiguous" ? "Ambiguous operation candidates require a strong service signal" : resolution.status === "dynamic" ? "Dynamic target requires runtime variable overrides" : "No indexed target operation matched")
|
|
1074
|
-
)
|
|
1075
|
-
);
|
|
1076
|
-
edges += 1;
|
|
1077
|
-
unresolved += edgeType === "UNRESOLVED_EDGE" ? 1 : 0;
|
|
1047
|
+
targetRepo: target?.repoName,
|
|
1048
|
+
targetOperation: target?.operationName,
|
|
1049
|
+
helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0,
|
|
1050
|
+
candidates: resolution.candidates,
|
|
1051
|
+
candidateCount: resolution.candidates.length,
|
|
1052
|
+
resolutionStatus: resolution.status,
|
|
1053
|
+
resolutionReasons: resolution.reasons
|
|
1054
|
+
};
|
|
1055
|
+
if (target) {
|
|
1056
|
+
db.prepare(
|
|
1057
|
+
"INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic) VALUES(?,?,?,?,?,?,?,?,?,?)"
|
|
1058
|
+
).run(
|
|
1059
|
+
workspaceId,
|
|
1060
|
+
"REMOTE_CALL_RESOLVES_TO_OPERATION",
|
|
1061
|
+
"resolved",
|
|
1062
|
+
"call",
|
|
1063
|
+
String(call.id),
|
|
1064
|
+
"operation",
|
|
1065
|
+
String(target.operationId),
|
|
1066
|
+
target.score,
|
|
1067
|
+
JSON.stringify(evidence),
|
|
1068
|
+
isDynamic ? 1 : 0
|
|
1069
|
+
);
|
|
1070
|
+
edges += 1;
|
|
1071
|
+
resolvedCount += 1;
|
|
1072
|
+
} else {
|
|
1073
|
+
const edgeType = callType === "local_db_query" ? "HANDLER_RUNS_DB_QUERY" : callType === "external_http" ? "HANDLER_CALLS_EXTERNAL_HTTP" : callType === "async_emit" ? "HANDLER_EMITS_EVENT" : callType === "async_subscribe" ? "EVENT_CONSUMED_BY_HANDLER" : resolution.status === "dynamic" ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE";
|
|
1074
|
+
const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
|
|
1075
|
+
const unresolvedReason = status === "terminal" ? null : String(
|
|
1076
|
+
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")
|
|
1077
|
+
);
|
|
1078
|
+
db.prepare(
|
|
1079
|
+
"INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
|
|
1080
|
+
).run(
|
|
1081
|
+
workspaceId,
|
|
1082
|
+
edgeType,
|
|
1083
|
+
status,
|
|
1084
|
+
"call",
|
|
1085
|
+
String(call.id),
|
|
1086
|
+
callType.startsWith("async_") ? "event" : "external",
|
|
1087
|
+
String(call.event_name_expr ?? call.query_entity ?? op ?? call.id),
|
|
1088
|
+
Number(call.confidence ?? 0.2),
|
|
1089
|
+
JSON.stringify(evidence),
|
|
1090
|
+
isDynamic || resolution.status === "dynamic" ? 1 : 0,
|
|
1091
|
+
unresolvedReason
|
|
1092
|
+
);
|
|
1093
|
+
edges += 1;
|
|
1094
|
+
unresolved += status === "unresolved" ? 1 : 0;
|
|
1095
|
+
ambiguousCount += status === "ambiguous" ? 1 : 0;
|
|
1096
|
+
dynamicCount += status === "dynamic" ? 1 : 0;
|
|
1097
|
+
terminalCount += status === "terminal" ? 1 : 0;
|
|
1098
|
+
}
|
|
1078
1099
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1100
|
+
return { edgeCount: edges, unresolvedCount: unresolved, resolvedCount, ambiguousCount, dynamicCount, terminalCount };
|
|
1101
|
+
});
|
|
1081
1102
|
}
|
|
1082
1103
|
|
|
1083
1104
|
// src/trace/trace-engine.ts
|
|
@@ -1096,7 +1117,8 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
1096
1117
|
`SELECT DISTINCT hc.source_file sourceFile
|
|
1097
1118
|
FROM handler_classes hc LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id
|
|
1098
1119
|
WHERE (? IS NULL OR hc.repo_id=?) AND (? IS NULL OR hc.class_name=? OR hm.method_name=?)
|
|
1099
|
-
AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)
|
|
1120
|
+
AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)
|
|
1121
|
+
AND (? IS NULL OR EXISTS (SELECT 1 FROM cds_services s JOIN cds_operations o ON o.service_id=s.id WHERE s.repo_id=hc.repo_id AND s.service_path=? AND (? IS NULL OR o.operation_path=? OR o.operation_name=? OR hm.decorator_value=? OR hm.method_name=?)))`
|
|
1100
1122
|
).all(
|
|
1101
1123
|
repoId,
|
|
1102
1124
|
repoId,
|
|
@@ -1105,8 +1127,16 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
1105
1127
|
handler,
|
|
1106
1128
|
operation,
|
|
1107
1129
|
operation,
|
|
1130
|
+
operation,
|
|
1131
|
+
start.servicePath,
|
|
1132
|
+
start.servicePath,
|
|
1133
|
+
operation,
|
|
1134
|
+
operation,
|
|
1135
|
+
operation,
|
|
1136
|
+
operation,
|
|
1108
1137
|
operation
|
|
1109
1138
|
);
|
|
1139
|
+
if (start.servicePath && rows2.length === 0) return void 0;
|
|
1110
1140
|
if (rows2.length === 0) return void 0;
|
|
1111
1141
|
return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
|
|
1112
1142
|
}
|
|
@@ -1150,7 +1180,7 @@ function graphForCalls(db, callIds) {
|
|
|
1150
1180
|
if (callIds.length === 0) return map;
|
|
1151
1181
|
const rows2 = db.prepare(
|
|
1152
1182
|
`SELECT * FROM graph_edges WHERE from_kind='call' AND from_id IN (${callIds.map(() => "?").join(",")}) ORDER BY id`
|
|
1153
|
-
).all(...callIds);
|
|
1183
|
+
).all(...callIds.map((id) => String(id)));
|
|
1154
1184
|
for (const row of rows2) {
|
|
1155
1185
|
const id = Number(row.from_id);
|
|
1156
1186
|
map.set(id, [...map.get(id) ?? [], row]);
|
|
@@ -1182,6 +1212,30 @@ function evidenceWithRuntimeVariables(evidence, vars) {
|
|
|
1182
1212
|
} : void 0
|
|
1183
1213
|
};
|
|
1184
1214
|
}
|
|
1215
|
+
function operationNode(db, operationId) {
|
|
1216
|
+
const row = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_type operationType,o.operation_path operationPath,o.source_file sourceFile,o.source_line sourceLine,s.id serviceId,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,r.id repoId,r.name repoName FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE o.id=?`).get(operationId);
|
|
1217
|
+
if (!row) return void 0;
|
|
1218
|
+
return { id: `operation:${operationId}`, kind: "operation", label: `${String(row.repoName)}:${String(row.servicePath)}${String(row.operationPath)}`, ...row };
|
|
1219
|
+
}
|
|
1220
|
+
function workspaceIdForCall(db, callId) {
|
|
1221
|
+
return db.prepare("SELECT r.workspace_id workspaceId FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE c.id=?").get(callId)?.workspaceId;
|
|
1222
|
+
}
|
|
1223
|
+
function runtimeResolution(db, row, evidence, vars) {
|
|
1224
|
+
if (!vars || Object.keys(vars).length === 0) return { row, evidence, unresolvedReason: row.unresolved_reason };
|
|
1225
|
+
const nextEvidence = evidenceWithRuntimeVariables(evidence, vars);
|
|
1226
|
+
const servicePath = typeof nextEvidence.servicePath === "string" ? nextEvidence.servicePath : void 0;
|
|
1227
|
+
const operationPath = typeof nextEvidence.operationPath === "string" ? nextEvidence.operationPath : void 0;
|
|
1228
|
+
const alias = typeof nextEvidence.serviceAliasExpr === "string" ? applyVariables(nextEvidence.serviceAliasExpr, vars) : typeof nextEvidence.serviceAlias === "string" ? applyVariables(nextEvidence.serviceAlias, vars) : void 0;
|
|
1229
|
+
const destination = typeof nextEvidence.destination === "string" ? applyVariables(nextEvidence.destination, vars) : void 0;
|
|
1230
|
+
const resolution = resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: Boolean(row.status === "dynamic" || nextEvidence.resolutionStatus === "dynamic") }, workspaceIdForCall(db, row.from_id));
|
|
1231
|
+
nextEvidence.runtimeResolutionStatus = resolution.status;
|
|
1232
|
+
nextEvidence.runtimeResolutionReasons = resolution.reasons;
|
|
1233
|
+
if (resolution.target) {
|
|
1234
|
+
nextEvidence.runtimeResolvedCandidate = resolution.target;
|
|
1235
|
+
return { row: { ...row, to_kind: "operation", to_id: String(resolution.target.operationId), unresolved_reason: void 0, confidence: resolution.target.score }, evidence: nextEvidence, target: resolution.target };
|
|
1236
|
+
}
|
|
1237
|
+
return { row, evidence: nextEvidence, unresolvedReason: resolution.status === "dynamic" ? `Dynamic target is missing runtime variables: ${resolution.reasons.join(", ")}` : resolution.status === "ambiguous" ? "Ambiguous runtime operation candidates" : "No runtime operation candidate matched substituted service and operation path" };
|
|
1238
|
+
}
|
|
1185
1239
|
function edgeTarget(row, evidence) {
|
|
1186
1240
|
const runtimeCandidate = evidence.runtimeResolvedCandidate;
|
|
1187
1241
|
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath)
|
|
@@ -1242,33 +1296,32 @@ function trace(db, start, options) {
|
|
|
1242
1296
|
const rawEvidence = JSON.parse(
|
|
1243
1297
|
String(row.evidence_json || "{}")
|
|
1244
1298
|
);
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
const
|
|
1250
|
-
nodes.set(targetNode, {
|
|
1299
|
+
const effective = runtimeResolution(db, row, rawEvidence, options.vars);
|
|
1300
|
+
const evidence = effective.evidence;
|
|
1301
|
+
const effectiveRow = effective.row;
|
|
1302
|
+
const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
|
|
1303
|
+
const opNode = effectiveRow.to_kind === "operation" ? operationNode(db, effectiveRow.to_id) : void 0;
|
|
1304
|
+
nodes.set(targetNode, opNode ?? {
|
|
1251
1305
|
id: targetNode,
|
|
1252
|
-
kind:
|
|
1253
|
-
label:
|
|
1254
|
-
...evidence
|
|
1306
|
+
kind: effectiveRow.to_kind,
|
|
1307
|
+
label: effectiveRow.to_id
|
|
1255
1308
|
});
|
|
1256
|
-
const to = edgeTarget(
|
|
1309
|
+
const to = edgeTarget(effectiveRow, evidence);
|
|
1257
1310
|
edges.push({
|
|
1258
1311
|
step: current.depth,
|
|
1259
1312
|
type: String(call.call_type),
|
|
1260
1313
|
from: `${call.repoName}:${call.source_file}`,
|
|
1261
1314
|
to,
|
|
1262
1315
|
evidence,
|
|
1263
|
-
confidence: Number(
|
|
1264
|
-
unresolvedReason:
|
|
1316
|
+
confidence: Number(effectiveRow.confidence ?? call.confidence),
|
|
1317
|
+
unresolvedReason: effective.unresolvedReason
|
|
1265
1318
|
});
|
|
1266
|
-
if (
|
|
1267
|
-
const files = handlerFilesForOperation(db,
|
|
1319
|
+
if (effectiveRow.to_kind === "operation" && current.depth < maxDepth) {
|
|
1320
|
+
const files = handlerFilesForOperation(db, effectiveRow.to_id);
|
|
1268
1321
|
if (files.size > 0) {
|
|
1269
1322
|
const targetRepoId = db.prepare(
|
|
1270
1323
|
"SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
|
|
1271
|
-
).get(
|
|
1324
|
+
).get(effectiveRow.to_id)?.repoId;
|
|
1272
1325
|
const nextKey = `${targetRepoId ?? "*"}:${[...files].sort().join(",")}`;
|
|
1273
1326
|
if (seenScopes.has(nextKey))
|
|
1274
1327
|
edges.push({
|
|
@@ -1310,4 +1363,4 @@ export {
|
|
|
1310
1363
|
linkWorkspace,
|
|
1311
1364
|
trace
|
|
1312
1365
|
};
|
|
1313
|
-
//# sourceMappingURL=chunk-
|
|
1366
|
+
//# sourceMappingURL=chunk-I33ASGX3.js.map
|