@saptools/service-flow 0.1.4 → 0.1.6
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 +18 -0
- package/README.md +14 -3
- package/TECHNICAL-NOTE.md +19 -2
- package/dist/{chunk-JY6GBGZT.js → chunk-6C5HZ6IR.js} +236 -144
- package/dist/chunk-6C5HZ6IR.js.map +1 -0
- package/dist/cli.js +234 -85
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +18 -2
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-JY6GBGZT.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
- Scoped runtime variable resolution to eligible dynamic, ambiguous, or unresolved remote edges with matching placeholders, preserving terminal and static resolved edge status, target, reason, and confidence.
|
|
6
|
+
- Clamped operation-resolution confidence to the `[0, 1]` range and retained original runtime expressions alongside effective substitutions and missing-variable evidence.
|
|
7
|
+
- Resolved helper/package dependency graph edges primarily by indexed package name, persisted ambiguous dependency candidates with evidence, and marked inserted helper edges with explicit statuses.
|
|
8
|
+
- Expanded repository fingerprints to include normalized package metadata, full `cds.requires`, scripts, package file content, and the analyzer version so metadata-only changes invalidate stale facts.
|
|
9
|
+
- Replaced the hard-coded CLI version with package metadata as the release source of truth and documented selector, graph variable, SQLite, fingerprint, freshness, and parser-warning semantics.
|
|
10
|
+
|
|
11
|
+
## 0.1.5
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
- Operation nodes now use target CDS provenance while call-site evidence remains on edges.
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
- Added schema user-version migration support for edge status and repository fingerprints.
|
|
18
|
+
- Repository indexing now skips unchanged repositories unless `--force` is supplied and reports indexed/skipped counts.
|
|
19
|
+
- `service-flow --version` now matches package metadata for the 0.1.5 release.
|
|
20
|
+
|
|
3
21
|
## 0.1.4
|
|
4
22
|
|
|
5
23
|
- 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,10 +47,21 @@ npm install @saptools/service-flow
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
> [!NOTE]
|
|
50
|
-
> Requires **Node.js ≥
|
|
50
|
+
> Requires **Node.js ≥ 24.0.0** for the bundled `node:sqlite` runtime. Version 0.1.6 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
|
|
|
54
|
+
|
|
55
|
+
### Correctness notes for 0.1.6
|
|
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.
|
|
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
|
+
- 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
|
+
- Helper-package dependency edges prefer exact indexed package names. Duplicate package-name candidates are persisted as ambiguous evidence rather than silently selecting one repository.
|
|
61
|
+
- Repository fingerprints include source content, package name/version, dependencies and devDependencies, scripts, normalized `cds.requires` (including nested credentials), package file content, and the analyzer version. Metadata-only changes therefore trigger reindexing.
|
|
62
|
+
- Index publication is designed around the last-good snapshot: failed parse or persistence attempts are recorded as diagnostics and must not be mixed with older graph facts. After indexing changes, relink before relying on graph/trace output; doctor reports stale or inconsistent stores where detectable.
|
|
63
|
+
- Parser warnings describe analysis completeness, while routing status describes graph behavior. A terminal DB edge can remain terminal while still exposing parser warning evidence about an unknown entity.
|
|
64
|
+
|
|
54
65
|
## 🚀 Quick Start
|
|
55
66
|
|
|
56
67
|
```bash
|
|
@@ -73,7 +84,7 @@ service-flow graph --workspace /path/to/workspace --service /FacadeService --pat
|
|
|
73
84
|
service-flow doctor --workspace /path/to/workspace
|
|
74
85
|
```
|
|
75
86
|
|
|
76
|
-
After `init`, the workspace configuration and SQLite database live below the selected workspace by default. Run `index` whenever source changes
|
|
87
|
+
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
88
|
|
|
78
89
|
---
|
|
79
90
|
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Service Flow 0.1.
|
|
1
|
+
# Service Flow 0.1.6 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,20 @@
|
|
|
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.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## 0.1.6 correctness additions
|
|
25
|
+
|
|
26
|
+
- Runtime resolution now has an explicit eligibility gate: only remote dynamic/ambiguous/unresolved graph edges with affected placeholders are re-resolved in memory. Terminal and resolved static edges are copied through unchanged, and substitutions keep original expressions, effective values, supplied variables, and missing variables separate.
|
|
27
|
+
- Operation candidate scores are clamped into `[0, 1]` before graph or trace rendering.
|
|
28
|
+
- Helper package linking uses exact `repositories.package_name` matches before normalized directory-name fallback. Ambiguous package names are represented as ambiguous graph edges with all candidates in evidence.
|
|
29
|
+
- Fingerprints hash normalized package facts and package bytes in addition to source file paths/content and analyzer version.
|
|
30
|
+
- The CLI version imports package metadata, so package metadata, `service-flow --version`, changelog, and analyzer/fingerprint version share one release source.
|
|
31
|
+
- Supported runtime is Node.js 24+ with `node:sqlite` validation; older runtimes should fail with a compatibility message instead of a late `DatabaseSync` error.
|
|
@@ -873,12 +873,30 @@ function collectClassHelpers(sf) {
|
|
|
873
873
|
}
|
|
874
874
|
|
|
875
875
|
// src/linker/dynamic-edge-resolver.ts
|
|
876
|
+
var PLACEHOLDER = /\$\{\s*(\w+)\s*\}/g;
|
|
876
877
|
function applyVariables(template, vars) {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
878
|
+
return substituteVariables(template, vars).effective;
|
|
879
|
+
}
|
|
880
|
+
function extractPlaceholders(template) {
|
|
881
|
+
return [...(template ?? "").matchAll(PLACEHOLDER)].map((m) => m[1] ?? "").filter(Boolean);
|
|
882
|
+
}
|
|
883
|
+
function substituteVariables(template, vars) {
|
|
884
|
+
if (!template) return { placeholders: [], missing: [], supplied: [], changed: false };
|
|
885
|
+
const placeholders2 = [...new Set(extractPlaceholders(template))];
|
|
886
|
+
const supplied = placeholders2.filter((key) => Object.hasOwn(vars, key));
|
|
887
|
+
const missing = placeholders2.filter((key) => !Object.hasOwn(vars, key));
|
|
888
|
+
const effective = template.replace(
|
|
889
|
+
PLACEHOLDER,
|
|
890
|
+
(_m, key) => Object.hasOwn(vars, key) ? vars[key] ?? "" : `\${${key}}`
|
|
881
891
|
);
|
|
892
|
+
return {
|
|
893
|
+
original: template,
|
|
894
|
+
effective,
|
|
895
|
+
placeholders: placeholders2,
|
|
896
|
+
missing,
|
|
897
|
+
supplied,
|
|
898
|
+
changed: effective !== template
|
|
899
|
+
};
|
|
882
900
|
}
|
|
883
901
|
|
|
884
902
|
// src/linker/service-resolver.ts
|
|
@@ -895,6 +913,13 @@ function rows(db, operationPath, workspaceId) {
|
|
|
895
913
|
);
|
|
896
914
|
}
|
|
897
915
|
function resolveOperation(db, signals, workspaceId) {
|
|
916
|
+
const missing = [signals.servicePath, signals.alias, signals.destination, signals.operationPath].flatMap((value) => [...(value ?? "").matchAll(/\$\{\s*(\w+)\s*\}/g)].map((match) => match[1] ?? "")).filter(Boolean);
|
|
917
|
+
if (missing.length > 0)
|
|
918
|
+
return {
|
|
919
|
+
status: "dynamic",
|
|
920
|
+
candidates: signals.operationPath ? rows(db, signals.operationPath, workspaceId) : [],
|
|
921
|
+
reasons: [...new Set(missing)].map((name) => `missing_variable:${name}`)
|
|
922
|
+
};
|
|
898
923
|
if (!signals.operationPath)
|
|
899
924
|
return {
|
|
900
925
|
status: "unresolved",
|
|
@@ -929,6 +954,7 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
929
954
|
c.reasons.push("explicit_dynamic_override");
|
|
930
955
|
}
|
|
931
956
|
}
|
|
957
|
+
for (const c of candidates) c.score = Math.max(0, Math.min(1, c.score));
|
|
932
958
|
candidates.sort(
|
|
933
959
|
(a, b) => b.score - a.score || a.repoName.localeCompare(b.repoName)
|
|
934
960
|
);
|
|
@@ -946,7 +972,7 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
946
972
|
candidates,
|
|
947
973
|
reasons: ["operation_path_only_has_no_strong_target_signal"]
|
|
948
974
|
};
|
|
949
|
-
if (best && best.score >= 0.9 && (!second || best.score - second.score >= 0.25))
|
|
975
|
+
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
976
|
return {
|
|
951
977
|
status: "resolved",
|
|
952
978
|
target: best,
|
|
@@ -961,31 +987,47 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
961
987
|
}
|
|
962
988
|
|
|
963
989
|
// src/linker/helper-package-linker.ts
|
|
990
|
+
function normalizeName(value) {
|
|
991
|
+
return value.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9]+/g, "");
|
|
992
|
+
}
|
|
993
|
+
function candidatesForDependency(repos, dep, sourceId) {
|
|
994
|
+
const exact = repos.filter((repo) => repo.id !== sourceId && repo.package_name === dep);
|
|
995
|
+
if (exact.length > 0) return exact;
|
|
996
|
+
const normalized = normalizeName(dep);
|
|
997
|
+
return repos.filter((repo) => repo.id !== sourceId && normalizeName(repo.name) === normalized);
|
|
998
|
+
}
|
|
964
999
|
function linkHelperPackages(db, workspaceId) {
|
|
965
1000
|
const repos = db.prepare(
|
|
966
|
-
"SELECT id,name,dependencies_json FROM repositories WHERE workspace_id=?"
|
|
1001
|
+
"SELECT id,name,package_name,dependencies_json FROM repositories WHERE workspace_id=?"
|
|
967
1002
|
).all(workspaceId);
|
|
968
1003
|
let count = 0;
|
|
969
1004
|
for (const repo of repos) {
|
|
970
1005
|
const deps = JSON.parse(repo.dependencies_json);
|
|
971
1006
|
for (const dep of Object.keys(deps)) {
|
|
972
|
-
const
|
|
973
|
-
if (
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1007
|
+
const candidates = candidatesForDependency(repos, dep, repo.id);
|
|
1008
|
+
if (candidates.length === 0) continue;
|
|
1009
|
+
const status = candidates.length === 1 ? "resolved" : "ambiguous";
|
|
1010
|
+
const helper = candidates.length === 1 ? candidates[0] : void 0;
|
|
1011
|
+
db.prepare(
|
|
1012
|
+
"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(?,?,?,?,?,?,?,?,?,?,?)"
|
|
1013
|
+
).run(
|
|
1014
|
+
workspaceId,
|
|
1015
|
+
"REPO_IMPORTS_HELPER_PACKAGE",
|
|
1016
|
+
status,
|
|
1017
|
+
"repo",
|
|
1018
|
+
String(repo.id),
|
|
1019
|
+
helper ? "repo" : "repo_candidates",
|
|
1020
|
+
helper ? String(helper.id) : candidates.map((candidate) => candidate.id).join(","),
|
|
1021
|
+
helper ? 1 : 0.5,
|
|
1022
|
+
JSON.stringify({
|
|
1023
|
+
dependency: dep,
|
|
1024
|
+
candidates: candidates.map((candidate) => ({ id: candidate.id, name: candidate.name, packageName: candidate.package_name })),
|
|
1025
|
+
match: helper?.package_name === dep ? "package_name" : "normalized_directory"
|
|
1026
|
+
}),
|
|
1027
|
+
0,
|
|
1028
|
+
helper ? null : "Ambiguous dependency package candidates"
|
|
1029
|
+
);
|
|
1030
|
+
count += 1;
|
|
989
1031
|
}
|
|
990
1032
|
}
|
|
991
1033
|
return count;
|
|
@@ -993,91 +1035,105 @@ function linkHelperPackages(db, workspaceId) {
|
|
|
993
1035
|
|
|
994
1036
|
// src/linker/cross-repo-linker.ts
|
|
995
1037
|
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
|
-
|
|
1038
|
+
return db.transaction(() => {
|
|
1039
|
+
db.prepare("DELETE FROM graph_edges WHERE workspace_id=?").run(workspaceId);
|
|
1040
|
+
let edges = linkHelperPackages(db, workspaceId);
|
|
1041
|
+
let unresolved = 0;
|
|
1042
|
+
let resolvedCount = 0;
|
|
1043
|
+
let ambiguousCount = 0;
|
|
1044
|
+
let dynamicCount = 0;
|
|
1045
|
+
let terminalCount = 0;
|
|
1046
|
+
const calls = db.prepare(
|
|
1047
|
+
`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=?`
|
|
1048
|
+
).all(workspaceId);
|
|
1049
|
+
for (const call of calls) {
|
|
1050
|
+
const callType = String(call.call_type);
|
|
1051
|
+
const op = applyVariables(String(call.operation_path_expr ?? ""), vars);
|
|
1052
|
+
const servicePath = applyVariables(
|
|
1053
|
+
call.servicePathExpr ?? call.requireServicePath,
|
|
1054
|
+
vars
|
|
1055
|
+
);
|
|
1056
|
+
const destination = call.destinationExpr ?? call.requireDestination;
|
|
1057
|
+
const isDynamic = Boolean(Number(call.isDynamic ?? 0));
|
|
1058
|
+
const resolution = callType.startsWith("remote") ? resolveOperation(
|
|
1059
|
+
db,
|
|
1060
|
+
{
|
|
1061
|
+
servicePath,
|
|
1062
|
+
operationPath: op,
|
|
1063
|
+
alias: applyVariables(call.aliasExpr ?? call.alias, vars),
|
|
1064
|
+
destination: applyVariables(destination, vars),
|
|
1065
|
+
isDynamic,
|
|
1066
|
+
hasExplicitOverride: Object.keys(vars).length > 0
|
|
1067
|
+
},
|
|
1068
|
+
workspaceId
|
|
1069
|
+
) : { status: "unresolved", candidates: [], reasons: [] };
|
|
1070
|
+
const target = resolution.target;
|
|
1071
|
+
const evidence = {
|
|
1072
|
+
sourceFile: call.source_file,
|
|
1073
|
+
sourceLine: call.source_line,
|
|
1074
|
+
file: call.source_file,
|
|
1075
|
+
line: call.source_line,
|
|
1076
|
+
repo: call.repoName,
|
|
1077
|
+
serviceAlias: call.alias,
|
|
1078
|
+
serviceAliasExpr: call.aliasExpr,
|
|
1079
|
+
destination: applyVariables(destination, vars),
|
|
1014
1080
|
servicePath,
|
|
1015
1081
|
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;
|
|
1082
|
+
targetRepo: target?.repoName,
|
|
1083
|
+
targetOperation: target?.operationName,
|
|
1084
|
+
helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0,
|
|
1085
|
+
candidates: resolution.candidates,
|
|
1086
|
+
candidateCount: resolution.candidates.length,
|
|
1087
|
+
resolutionStatus: resolution.status,
|
|
1088
|
+
resolutionReasons: resolution.reasons
|
|
1089
|
+
};
|
|
1090
|
+
if (target) {
|
|
1091
|
+
db.prepare(
|
|
1092
|
+
"INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic) VALUES(?,?,?,?,?,?,?,?,?,?)"
|
|
1093
|
+
).run(
|
|
1094
|
+
workspaceId,
|
|
1095
|
+
"REMOTE_CALL_RESOLVES_TO_OPERATION",
|
|
1096
|
+
"resolved",
|
|
1097
|
+
"call",
|
|
1098
|
+
String(call.id),
|
|
1099
|
+
"operation",
|
|
1100
|
+
String(target.operationId),
|
|
1101
|
+
target.score,
|
|
1102
|
+
JSON.stringify(evidence),
|
|
1103
|
+
isDynamic ? 1 : 0
|
|
1104
|
+
);
|
|
1105
|
+
edges += 1;
|
|
1106
|
+
resolvedCount += 1;
|
|
1107
|
+
} else {
|
|
1108
|
+
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";
|
|
1109
|
+
const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
|
|
1110
|
+
const unresolvedReason = status === "terminal" ? null : String(
|
|
1111
|
+
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")
|
|
1112
|
+
);
|
|
1113
|
+
db.prepare(
|
|
1114
|
+
"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(?,?,?,?,?,?,?,?,?,?,?)"
|
|
1115
|
+
).run(
|
|
1116
|
+
workspaceId,
|
|
1117
|
+
edgeType,
|
|
1118
|
+
status,
|
|
1119
|
+
"call",
|
|
1120
|
+
String(call.id),
|
|
1121
|
+
callType.startsWith("async_") ? "event" : "external",
|
|
1122
|
+
String(call.event_name_expr ?? call.query_entity ?? op ?? call.id),
|
|
1123
|
+
Number(call.confidence ?? 0.2),
|
|
1124
|
+
JSON.stringify(evidence),
|
|
1125
|
+
isDynamic || resolution.status === "dynamic" ? 1 : 0,
|
|
1126
|
+
unresolvedReason
|
|
1127
|
+
);
|
|
1128
|
+
edges += 1;
|
|
1129
|
+
unresolved += status === "unresolved" ? 1 : 0;
|
|
1130
|
+
ambiguousCount += status === "ambiguous" ? 1 : 0;
|
|
1131
|
+
dynamicCount += status === "dynamic" ? 1 : 0;
|
|
1132
|
+
terminalCount += status === "terminal" ? 1 : 0;
|
|
1133
|
+
}
|
|
1078
1134
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1135
|
+
return { edgeCount: edges, unresolvedCount: unresolved, resolvedCount, ambiguousCount, dynamicCount, terminalCount };
|
|
1136
|
+
});
|
|
1081
1137
|
}
|
|
1082
1138
|
|
|
1083
1139
|
// src/trace/trace-engine.ts
|
|
@@ -1096,7 +1152,8 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
1096
1152
|
`SELECT DISTINCT hc.source_file sourceFile
|
|
1097
1153
|
FROM handler_classes hc LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id
|
|
1098
1154
|
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=?)
|
|
1155
|
+
AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)
|
|
1156
|
+
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
1157
|
).all(
|
|
1101
1158
|
repoId,
|
|
1102
1159
|
repoId,
|
|
@@ -1105,8 +1162,16 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
1105
1162
|
handler,
|
|
1106
1163
|
operation,
|
|
1107
1164
|
operation,
|
|
1165
|
+
operation,
|
|
1166
|
+
start.servicePath,
|
|
1167
|
+
start.servicePath,
|
|
1168
|
+
operation,
|
|
1169
|
+
operation,
|
|
1170
|
+
operation,
|
|
1171
|
+
operation,
|
|
1108
1172
|
operation
|
|
1109
1173
|
);
|
|
1174
|
+
if (start.servicePath && rows2.length === 0) return void 0;
|
|
1110
1175
|
if (rows2.length === 0) return void 0;
|
|
1111
1176
|
return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
|
|
1112
1177
|
}
|
|
@@ -1150,37 +1215,63 @@ function graphForCalls(db, callIds) {
|
|
|
1150
1215
|
if (callIds.length === 0) return map;
|
|
1151
1216
|
const rows2 = db.prepare(
|
|
1152
1217
|
`SELECT * FROM graph_edges WHERE from_kind='call' AND from_id IN (${callIds.map(() => "?").join(",")}) ORDER BY id`
|
|
1153
|
-
).all(...callIds);
|
|
1218
|
+
).all(...callIds.map((id) => String(id)));
|
|
1154
1219
|
for (const row of rows2) {
|
|
1155
1220
|
const id = Number(row.from_id);
|
|
1156
1221
|
map.set(id, [...map.get(id) ?? [], row]);
|
|
1157
1222
|
}
|
|
1158
1223
|
return map;
|
|
1159
1224
|
}
|
|
1160
|
-
function
|
|
1161
|
-
return
|
|
1162
|
-
|
|
1163
|
-
|
|
1225
|
+
function hasRuntimeVariable(value, vars) {
|
|
1226
|
+
return typeof value === "string" && extractPlaceholders(value).some((key) => Object.hasOwn(vars, key));
|
|
1227
|
+
}
|
|
1228
|
+
function isRemoteRuntimeCandidate(row, evidence, vars) {
|
|
1229
|
+
if (!vars || Object.keys(vars).length === 0) return false;
|
|
1230
|
+
if (!["dynamic", "ambiguous", "unresolved"].includes(String(row.status ?? ""))) return false;
|
|
1231
|
+
if (!["DYNAMIC_EDGE_CANDIDATE", "UNRESOLVED_EDGE", "REMOTE_CALL_RESOLVES_TO_OPERATION"].includes(row.edge_type)) return false;
|
|
1232
|
+
if (row.status === "resolved") return false;
|
|
1233
|
+
return ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"].some((key) => hasRuntimeVariable(evidence[key], vars));
|
|
1164
1234
|
}
|
|
1165
1235
|
function evidenceWithRuntimeVariables(evidence, vars) {
|
|
1166
1236
|
if (!vars || Object.keys(vars).length === 0) return evidence;
|
|
1167
|
-
const
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
};
|
|
1237
|
+
const substitutions = {};
|
|
1238
|
+
for (const key of ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"]) {
|
|
1239
|
+
const substitution = substituteVariables(typeof evidence[key] === "string" ? String(evidence[key]) : void 0, vars);
|
|
1240
|
+
if (substitution.placeholders.length > 0) substitutions[key] = substitution;
|
|
1241
|
+
}
|
|
1242
|
+
const next = { ...evidence, runtimeVariablesApplied: true, runtimeSubstitutions: substitutions };
|
|
1243
|
+
for (const [key, value] of Object.entries(substitutions)) {
|
|
1244
|
+
if (value.effective) next[key] = value.effective;
|
|
1245
|
+
}
|
|
1246
|
+
const missing = Object.values(substitutions).flatMap((value) => value.missing);
|
|
1247
|
+
if (missing.length > 0) next.missingRuntimeVariables = [...new Set(missing)];
|
|
1248
|
+
return next;
|
|
1249
|
+
}
|
|
1250
|
+
function operationNode(db, operationId) {
|
|
1251
|
+
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);
|
|
1252
|
+
if (!row) return void 0;
|
|
1253
|
+
return { id: `operation:${operationId}`, kind: "operation", label: `${String(row.repoName)}:${String(row.servicePath)}${String(row.operationPath)}`, ...row };
|
|
1254
|
+
}
|
|
1255
|
+
function workspaceIdForCall(db, callId) {
|
|
1256
|
+
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;
|
|
1257
|
+
}
|
|
1258
|
+
function runtimeResolution(db, row, evidence, vars) {
|
|
1259
|
+
if (!isRemoteRuntimeCandidate(row, evidence, vars))
|
|
1260
|
+
return { row, evidence, unresolvedReason: row.unresolved_reason };
|
|
1261
|
+
const nextEvidence = evidenceWithRuntimeVariables(evidence, vars);
|
|
1262
|
+
const servicePath = typeof nextEvidence.servicePath === "string" ? nextEvidence.servicePath : void 0;
|
|
1263
|
+
const operationPath = typeof nextEvidence.operationPath === "string" ? nextEvidence.operationPath : void 0;
|
|
1264
|
+
const alias = typeof nextEvidence.serviceAliasExpr === "string" ? nextEvidence.serviceAliasExpr : typeof nextEvidence.serviceAlias === "string" ? nextEvidence.serviceAlias : void 0;
|
|
1265
|
+
const destination = typeof nextEvidence.destination === "string" ? nextEvidence.destination : void 0;
|
|
1266
|
+
const resolution = resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: true }, workspaceIdForCall(db, row.from_id));
|
|
1267
|
+
nextEvidence.runtimeResolutionStatus = resolution.status;
|
|
1268
|
+
nextEvidence.runtimeResolutionReasons = resolution.reasons;
|
|
1269
|
+
if (resolution.target) {
|
|
1270
|
+
nextEvidence.runtimeResolvedCandidate = resolution.target;
|
|
1271
|
+
return { row: { ...row, to_kind: "operation", to_id: String(resolution.target.operationId), unresolved_reason: void 0, confidence: Math.max(0, Math.min(1, resolution.target.score)) }, evidence: nextEvidence, target: resolution.target };
|
|
1272
|
+
}
|
|
1273
|
+
const 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";
|
|
1274
|
+
return { row, evidence: nextEvidence, unresolvedReason };
|
|
1184
1275
|
}
|
|
1185
1276
|
function edgeTarget(row, evidence) {
|
|
1186
1277
|
const runtimeCandidate = evidence.runtimeResolvedCandidate;
|
|
@@ -1242,33 +1333,32 @@ function trace(db, start, options) {
|
|
|
1242
1333
|
const rawEvidence = JSON.parse(
|
|
1243
1334
|
String(row.evidence_json || "{}")
|
|
1244
1335
|
);
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
const
|
|
1250
|
-
nodes.set(targetNode, {
|
|
1336
|
+
const effective = runtimeResolution(db, row, rawEvidence, options.vars);
|
|
1337
|
+
const evidence = effective.evidence;
|
|
1338
|
+
const effectiveRow = effective.row;
|
|
1339
|
+
const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
|
|
1340
|
+
const opNode = effectiveRow.to_kind === "operation" ? operationNode(db, effectiveRow.to_id) : void 0;
|
|
1341
|
+
nodes.set(targetNode, opNode ?? {
|
|
1251
1342
|
id: targetNode,
|
|
1252
|
-
kind:
|
|
1253
|
-
label:
|
|
1254
|
-
...evidence
|
|
1343
|
+
kind: effectiveRow.to_kind,
|
|
1344
|
+
label: effectiveRow.to_id
|
|
1255
1345
|
});
|
|
1256
|
-
const to = edgeTarget(
|
|
1346
|
+
const to = edgeTarget(effectiveRow, evidence);
|
|
1257
1347
|
edges.push({
|
|
1258
1348
|
step: current.depth,
|
|
1259
1349
|
type: String(call.call_type),
|
|
1260
1350
|
from: `${call.repoName}:${call.source_file}`,
|
|
1261
1351
|
to,
|
|
1262
1352
|
evidence,
|
|
1263
|
-
confidence: Number(
|
|
1264
|
-
unresolvedReason:
|
|
1353
|
+
confidence: Number(effectiveRow.confidence ?? call.confidence),
|
|
1354
|
+
unresolvedReason: effective.unresolvedReason
|
|
1265
1355
|
});
|
|
1266
|
-
if (
|
|
1267
|
-
const files = handlerFilesForOperation(db,
|
|
1356
|
+
if (effectiveRow.to_kind === "operation" && current.depth < maxDepth) {
|
|
1357
|
+
const files = handlerFilesForOperation(db, effectiveRow.to_id);
|
|
1268
1358
|
if (files.size > 0) {
|
|
1269
1359
|
const targetRepoId = db.prepare(
|
|
1270
1360
|
"SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
|
|
1271
|
-
).get(
|
|
1361
|
+
).get(effectiveRow.to_id)?.repoId;
|
|
1272
1362
|
const nextKey = `${targetRepoId ?? "*"}:${[...files].sort().join(",")}`;
|
|
1273
1363
|
if (seenScopes.has(nextKey))
|
|
1274
1364
|
edges.push({
|
|
@@ -1307,7 +1397,9 @@ export {
|
|
|
1307
1397
|
parseOutboundCalls,
|
|
1308
1398
|
parseServiceBindings,
|
|
1309
1399
|
applyVariables,
|
|
1400
|
+
extractPlaceholders,
|
|
1401
|
+
substituteVariables,
|
|
1310
1402
|
linkWorkspace,
|
|
1311
1403
|
trace
|
|
1312
1404
|
};
|
|
1313
|
-
//# sourceMappingURL=chunk-
|
|
1405
|
+
//# sourceMappingURL=chunk-6C5HZ6IR.js.map
|