@saptools/service-flow 0.1.6 → 0.1.7
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 +19 -2
- package/TECHNICAL-NOTE.md +2 -2
- package/dist/{chunk-6C5HZ6IR.js → chunk-YFT57U54.js} +116 -124
- package/dist/chunk-YFT57U54.js.map +1 -0
- package/dist/cli.js +141 -144
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-6C5HZ6IR.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.7
|
|
4
|
+
|
|
5
|
+
- Added atomic last-good repository publication so failed source reads preserve the previous complete snapshot and fingerprint.
|
|
6
|
+
- Added explicit graph freshness metadata and stale diagnostics after successful fact changes until relink.
|
|
7
|
+
- Persisted handler registration class/import evidence and linked registered cross-package implementation handlers through application dependency evidence.
|
|
8
|
+
- Made service-only trace selectors return a typed narrowing diagnostic instead of workspace-wide traversal.
|
|
9
|
+
- Expanded link summaries with dependency and implementation categories whose totals reconcile with persisted graph edges.
|
|
10
|
+
- Preserved terminal parser warning evidence separately from routing status.
|
|
11
|
+
|
|
12
|
+
|
|
3
13
|
## 0.1.6
|
|
4
14
|
|
|
5
15
|
- 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.
|
package/README.md
CHANGED
|
@@ -47,12 +47,12 @@ npm install @saptools/service-flow
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
> [!NOTE]
|
|
50
|
-
> Requires **Node.js ≥ 24.0.0** for the bundled `node:sqlite` runtime. Version 0.1.
|
|
50
|
+
> Requires **Node.js ≥ 24.0.0** for the bundled `node:sqlite` runtime. Version 0.1.7 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
54
|
|
|
55
|
-
### Correctness notes for 0.1.
|
|
55
|
+
### Correctness notes for 0.1.7
|
|
56
56
|
|
|
57
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
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]`.
|
|
@@ -436,3 +436,20 @@ MIT
|
|
|
436
436
|
---
|
|
437
437
|
|
|
438
438
|
Made with ❤️ to make your work life easier!
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
### Service-only trace policy
|
|
442
|
+
|
|
443
|
+
`service-flow trace --service <path>` is intentionally not a broad workspace traversal. Provide `--operation`, `--path`, or `--handler`; otherwise the command returns a typed `trace_start_not_found` diagnostic and no edges.
|
|
444
|
+
|
|
445
|
+
### Graph freshness and last-good snapshots
|
|
446
|
+
|
|
447
|
+
Repository facts are parsed before publication and committed atomically. Failed indexing attempts record diagnostics and retain the last complete published snapshot and successful fingerprint. Successful fact publication marks the workspace graph stale until `service-flow link` rebuilds dependency, remote-call, and implementation edges for the current fact generation.
|
|
448
|
+
|
|
449
|
+
### Cross-package implementation evidence
|
|
450
|
+
|
|
451
|
+
Handler registrations persist parsed class names and import sources. Linking resolves implementation edges only through registered application evidence plus model and handler package dependency edges; a decorator-name match alone is not enough.
|
|
452
|
+
|
|
453
|
+
### Graph variables
|
|
454
|
+
|
|
455
|
+
The `graph` command accepts repeatable `--var key=value` options, matching `trace`, for runtime substitution previews in JSON or Mermaid output.
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Service Flow 0.1.
|
|
1
|
+
# Service Flow 0.1.7 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.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
- Repository-level fingerprints include source paths/hashes, package dependencies, and analyzer schema version. Unchanged repositories are skipped unless `--force` is used.
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
## 0.1.
|
|
24
|
+
## 0.1.7 correctness additions
|
|
25
25
|
|
|
26
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
27
|
- Operation candidate scores are clamped into `[0, 1]` before graph or trace rendering.
|
|
@@ -336,6 +336,14 @@ function lineOf2(text, idx) {
|
|
|
336
336
|
async function parseHandlerRegistrations(repoPath, filePath) {
|
|
337
337
|
const text = await fs5.readFile(path6.join(repoPath, filePath), "utf8");
|
|
338
338
|
const out = [];
|
|
339
|
+
const imports = /* @__PURE__ */ new Map();
|
|
340
|
+
for (const m of text.matchAll(/import\s+\{?\s*([A-Za-z0-9_,\s]+)\s*\}?\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
341
|
+
const source = m[2];
|
|
342
|
+
for (const name of (m[1] ?? "").split(",")) {
|
|
343
|
+
const symbol = name.trim().split(/\s+as\s+/).pop()?.trim();
|
|
344
|
+
if (symbol) imports.set(symbol, source);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
339
347
|
for (const m of text.matchAll(
|
|
340
348
|
/createCombinedHandler\s*\(|srv\.prepend\s*\(|cds\.serve\s*\(/g
|
|
341
349
|
))
|
|
@@ -351,6 +359,7 @@ async function parseHandlerRegistrations(repoPath, filePath) {
|
|
|
351
359
|
for (const c of (m[1] ?? "").matchAll(/\b(\w+Handler)\b/g))
|
|
352
360
|
out.push({
|
|
353
361
|
className: c[1],
|
|
362
|
+
importSource: imports.get(c[1]),
|
|
354
363
|
registrationFile: normalizePath(filePath),
|
|
355
364
|
registrationLine: lineOf2(text, (m.index ?? 0) + (c.index ?? 0)),
|
|
356
365
|
registrationKind: "handler-array",
|
|
@@ -992,149 +1001,120 @@ function normalizeName(value) {
|
|
|
992
1001
|
}
|
|
993
1002
|
function candidatesForDependency(repos, dep, sourceId) {
|
|
994
1003
|
const exact = repos.filter((repo) => repo.id !== sourceId && repo.package_name === dep);
|
|
995
|
-
if (exact.length > 0) return exact;
|
|
1004
|
+
if (exact.length > 0) return { candidates: exact, strategy: "exact_package_name" };
|
|
996
1005
|
const normalized = normalizeName(dep);
|
|
997
|
-
return repos.filter((repo) => repo.id !== sourceId && normalizeName(repo.name) === normalized);
|
|
1006
|
+
return { candidates: repos.filter((repo) => repo.id !== sourceId && normalizeName(repo.name) === normalized), strategy: "normalized_directory" };
|
|
998
1007
|
}
|
|
999
|
-
function linkHelperPackages(db, workspaceId) {
|
|
1000
|
-
const repos = db.prepare(
|
|
1001
|
-
|
|
1002
|
-
).all(workspaceId);
|
|
1003
|
-
let count = 0;
|
|
1008
|
+
function linkHelperPackages(db, workspaceId, generation) {
|
|
1009
|
+
const repos = db.prepare("SELECT id,name,package_name,dependencies_json FROM repositories WHERE workspace_id=?").all(workspaceId);
|
|
1010
|
+
const summary = { edgeCount: 0, resolvedCount: 0, ambiguousCount: 0 };
|
|
1004
1011
|
for (const repo of repos) {
|
|
1005
1012
|
const deps = JSON.parse(repo.dependencies_json);
|
|
1006
1013
|
for (const dep of Object.keys(deps)) {
|
|
1007
|
-
const
|
|
1008
|
-
if (candidates.length === 0) continue;
|
|
1009
|
-
const status = candidates.length === 1 ? "resolved" : "ambiguous";
|
|
1010
|
-
const helper =
|
|
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
|
+
const result = candidatesForDependency(repos, dep, repo.id);
|
|
1015
|
+
if (result.candidates.length === 0) continue;
|
|
1016
|
+
const status = result.candidates.length === 1 ? "resolved" : "ambiguous";
|
|
1017
|
+
const helper = status === "resolved" ? result.candidates[0] : void 0;
|
|
1018
|
+
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(
|
|
1014
1019
|
workspaceId,
|
|
1015
1020
|
"REPO_IMPORTS_HELPER_PACKAGE",
|
|
1016
1021
|
status,
|
|
1017
1022
|
"repo",
|
|
1018
1023
|
String(repo.id),
|
|
1019
1024
|
helper ? "repo" : "repo_candidates",
|
|
1020
|
-
helper ? String(helper.id) : candidates.map((candidate) => candidate.id).join(","),
|
|
1025
|
+
helper ? String(helper.id) : result.candidates.map((candidate) => candidate.id).join(","),
|
|
1021
1026
|
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
|
+
JSON.stringify({ dependency: dep, candidates: result.candidates.map((candidate) => ({ id: candidate.id, name: candidate.name, packageName: candidate.package_name })), match: result.strategy }),
|
|
1027
1028
|
0,
|
|
1028
|
-
helper ? null : "Ambiguous dependency package candidates"
|
|
1029
|
+
helper ? null : "Ambiguous dependency package candidates",
|
|
1030
|
+
generation
|
|
1029
1031
|
);
|
|
1030
|
-
|
|
1032
|
+
summary.edgeCount += 1;
|
|
1033
|
+
if (helper) summary.resolvedCount += 1;
|
|
1034
|
+
else summary.ambiguousCount += 1;
|
|
1031
1035
|
}
|
|
1032
1036
|
}
|
|
1033
|
-
return
|
|
1037
|
+
return summary;
|
|
1034
1038
|
}
|
|
1035
1039
|
|
|
1036
1040
|
// src/linker/cross-repo-linker.ts
|
|
1037
1041
|
function linkWorkspace(db, workspaceId, vars = {}) {
|
|
1038
1042
|
return db.transaction(() => {
|
|
1043
|
+
const generation = nextGraphGeneration(db, workspaceId);
|
|
1039
1044
|
db.prepare("DELETE FROM graph_edges WHERE workspace_id=?").run(workspaceId);
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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),
|
|
1080
|
-
servicePath,
|
|
1081
|
-
operationPath: op,
|
|
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
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
return { edgeCount: edges, unresolvedCount: unresolved, resolvedCount, ambiguousCount, dynamicCount, terminalCount };
|
|
1045
|
+
const deps = linkHelperPackages(db, workspaceId, generation);
|
|
1046
|
+
const callSummary = linkCalls(db, workspaceId, vars, generation);
|
|
1047
|
+
const impl = linkImplementations(db, workspaceId, generation);
|
|
1048
|
+
db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
|
|
1049
|
+
return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount };
|
|
1136
1050
|
});
|
|
1137
1051
|
}
|
|
1052
|
+
function nextGraphGeneration(db, workspaceId) {
|
|
1053
|
+
const row = db.prepare("SELECT COALESCE(MAX(graph_generation),0) generation FROM repositories WHERE workspace_id=?").get(workspaceId);
|
|
1054
|
+
return Number(row?.generation ?? 0) + 1;
|
|
1055
|
+
}
|
|
1056
|
+
function linkCalls(db, workspaceId, vars, generation) {
|
|
1057
|
+
let edgeCount = 0;
|
|
1058
|
+
let unresolvedCount = 0;
|
|
1059
|
+
let resolvedCount = 0;
|
|
1060
|
+
let ambiguousCount = 0;
|
|
1061
|
+
let dynamicCount = 0;
|
|
1062
|
+
let terminalCount = 0;
|
|
1063
|
+
const calls = db.prepare(`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=?`).all(workspaceId);
|
|
1064
|
+
for (const call of calls) {
|
|
1065
|
+
const result = insertCallEdge(db, workspaceId, call, vars, generation);
|
|
1066
|
+
edgeCount += 1;
|
|
1067
|
+
resolvedCount += result.status === "resolved" ? 1 : 0;
|
|
1068
|
+
unresolvedCount += result.status === "unresolved" ? 1 : 0;
|
|
1069
|
+
ambiguousCount += result.status === "ambiguous" ? 1 : 0;
|
|
1070
|
+
dynamicCount += result.status === "dynamic" ? 1 : 0;
|
|
1071
|
+
terminalCount += result.status === "terminal" ? 1 : 0;
|
|
1072
|
+
}
|
|
1073
|
+
return { edgeCount, unresolvedCount, resolvedCount, ambiguousCount, dynamicCount, terminalCount };
|
|
1074
|
+
}
|
|
1075
|
+
function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
1076
|
+
const callType = String(call.call_type);
|
|
1077
|
+
const op = applyVariables(String(call.operation_path_expr ?? ""), vars);
|
|
1078
|
+
const servicePath = applyVariables(call.servicePathExpr ?? call.requireServicePath, vars);
|
|
1079
|
+
const destination = call.destinationExpr ?? call.requireDestination;
|
|
1080
|
+
const isDynamic = Boolean(Number(call.isDynamic ?? 0));
|
|
1081
|
+
const resolution = callType.startsWith("remote") ? resolveOperation(db, { servicePath, operationPath: op, alias: applyVariables(call.aliasExpr ?? call.alias, vars), destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 }, workspaceId) : { status: "unresolved", candidates: [], reasons: [] };
|
|
1082
|
+
const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0);
|
|
1083
|
+
if (resolution.target) {
|
|
1084
|
+
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, "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify(evidence), isDynamic ? 1 : 0, generation);
|
|
1085
|
+
return { status: "resolved" };
|
|
1086
|
+
}
|
|
1087
|
+
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";
|
|
1088
|
+
const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
|
|
1089
|
+
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"));
|
|
1090
|
+
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), callType.startsWith("async_") ? "event" : "external", String(call.event_name_expr ?? call.query_entity ?? op ?? call.id), Number(call.confidence ?? 0.2), JSON.stringify(evidence), isDynamic || resolution.status === "dynamic" ? 1 : 0, unresolvedReason, generation);
|
|
1091
|
+
return { status };
|
|
1092
|
+
}
|
|
1093
|
+
function callEvidence(call, resolution, servicePath, op, destination) {
|
|
1094
|
+
return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, targetRepo: resolution.target?.repoName, targetOperation: resolution.target?.operationName, helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0, candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
|
|
1095
|
+
}
|
|
1096
|
+
function linkImplementations(db, workspaceId, generation) {
|
|
1097
|
+
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.package_name modelPackage 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);
|
|
1098
|
+
let edgeCount = 0;
|
|
1099
|
+
let resolvedCount = 0;
|
|
1100
|
+
let ambiguousCount = 0;
|
|
1101
|
+
for (const operation of operations) {
|
|
1102
|
+
const rows2 = implementationCandidates(db, workspaceId, operation);
|
|
1103
|
+
if (rows2.length === 0) continue;
|
|
1104
|
+
const unique = rows2.length === 1 ? rows2[0] : void 0;
|
|
1105
|
+
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, "OPERATION_IMPLEMENTED_BY_HANDLER", unique ? "resolved" : "ambiguous", "operation", String(operation.operationId), unique ? "handler_method" : "handler_method_candidates", unique ? String(unique.methodId) : rows2.map((row) => row.methodId).join(","), unique ? 0.95 : 0.5, JSON.stringify({ servicePath: operation.servicePath, operationPath: operation.operationPath, operationName: operation.operationName, candidates: rows2, evidence: "registered_application_dependency" }), 0, unique ? null : "Ambiguous registered handler implementation candidates", generation);
|
|
1106
|
+
edgeCount += 1;
|
|
1107
|
+
if (unique) resolvedCount += 1;
|
|
1108
|
+
else ambiguousCount += 1;
|
|
1109
|
+
}
|
|
1110
|
+
return { edgeCount, resolvedCount, ambiguousCount };
|
|
1111
|
+
}
|
|
1112
|
+
function implementationCandidates(db, workspaceId, operation) {
|
|
1113
|
+
return db.prepare(`SELECT DISTINCT hm.id methodId,hc.id classId,hc.class_name className,hc.source_file sourceFile,hc.source_line sourceLine,hr.repo_id applicationRepoId,handlerRepo.name handlerRepo,appRepo.name applicationRepo FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id JOIN repositories handlerRepo ON handlerRepo.id=hc.repo_id JOIN handler_registrations hr ON hr.class_name=hc.class_name JOIN repositories appRepo ON appRepo.id=hr.repo_id JOIN graph_edges modelDep ON modelDep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND modelDep.status='resolved' AND modelDep.from_kind='repo' AND modelDep.from_id=CAST(appRepo.id AS TEXT) AND modelDep.to_id=CAST(? AS TEXT) JOIN graph_edges handlerDep ON handlerDep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND handlerDep.status='resolved' AND handlerDep.from_kind='repo' AND handlerDep.from_id=CAST(appRepo.id AS TEXT) AND handlerDep.to_id=CAST(handlerRepo.id AS TEXT) WHERE appRepo.workspace_id=? AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=?)`).all(operation.modelRepoId, workspaceId, normalizedOperation(String(operation.operationPath ?? "")), operation.operationName, operation.operationName);
|
|
1114
|
+
}
|
|
1115
|
+
function normalizedOperation(value) {
|
|
1116
|
+
return value.startsWith("/") ? value.slice(1) : value;
|
|
1117
|
+
}
|
|
1138
1118
|
|
|
1139
1119
|
// src/trace/trace-engine.ts
|
|
1140
1120
|
function normalizeOperation(value) {
|
|
@@ -1182,8 +1162,10 @@ function startScope(db, start) {
|
|
|
1182
1162
|
if (start.repo && !repo) return { repo, selectorMatched: false };
|
|
1183
1163
|
const sourceFiles = sourceFilesForStart(db, repo?.id, start);
|
|
1184
1164
|
const hasSelector = Boolean(
|
|
1185
|
-
start.handler ?? start.operation ?? start.operationPath
|
|
1165
|
+
start.handler ?? start.operation ?? start.operationPath ?? start.servicePath
|
|
1186
1166
|
);
|
|
1167
|
+
if (start.servicePath && !start.operation && !start.operationPath && !start.handler)
|
|
1168
|
+
return { repo, selectorMatched: false };
|
|
1187
1169
|
return {
|
|
1188
1170
|
repo,
|
|
1189
1171
|
sourceFiles,
|
|
@@ -1204,6 +1186,12 @@ function handlerFilesForOperation(db, operationId) {
|
|
|
1204
1186
|
).all(op.repoId, operation, operation, op.operationName);
|
|
1205
1187
|
return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
|
|
1206
1188
|
}
|
|
1189
|
+
function implementationScope(db, operationId) {
|
|
1190
|
+
const edge = db.prepare("SELECT * FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status='resolved' AND from_kind='operation' AND from_id=? ORDER BY id LIMIT 1").get(operationId);
|
|
1191
|
+
if (!edge) return { files: /* @__PURE__ */ new Set() };
|
|
1192
|
+
const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id WHERE hm.id=?").get(edge.to_id);
|
|
1193
|
+
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []) };
|
|
1194
|
+
}
|
|
1207
1195
|
function includeCall(type, options) {
|
|
1208
1196
|
if (!options.includeDb && type === "local_db_query") return false;
|
|
1209
1197
|
if (!options.includeExternal && type === "external_http") return false;
|
|
@@ -1288,11 +1276,14 @@ function trace(db, start, options) {
|
|
|
1288
1276
|
const diagnostics = db.prepare(
|
|
1289
1277
|
"SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)"
|
|
1290
1278
|
).all(scope.repo?.id, scope.repo?.id);
|
|
1279
|
+
const stale = db.prepare("SELECT name,graph_stale_reason reason FROM repositories WHERE graph_stale_reason IS NOT NULL AND (? IS NULL OR id=?)").all(scope.repo?.id, scope.repo?.id);
|
|
1280
|
+
for (const row of stale)
|
|
1281
|
+
diagnostics.unshift({ severity: "warning", code: "graph_stale", message: `Graph is stale for ${row.name ?? "repository"}: ${row.reason ?? "facts_changed"}. Run service-flow link.` });
|
|
1291
1282
|
if (!scope.selectorMatched)
|
|
1292
1283
|
diagnostics.unshift({
|
|
1293
1284
|
severity: "warning",
|
|
1294
1285
|
code: "trace_start_not_found",
|
|
1295
|
-
message: "No handler source matched the requested trace start selector"
|
|
1286
|
+
message: start.servicePath && !start.operation && !start.operationPath && !start.handler ? "Service-only trace requires --operation or --path and will not broaden to the whole workspace" : "No handler source matched the requested trace start selector"
|
|
1296
1287
|
});
|
|
1297
1288
|
const maxDepth = positiveDepth(options.depth);
|
|
1298
1289
|
const edges = [];
|
|
@@ -1354,9 +1345,10 @@ function trace(db, start, options) {
|
|
|
1354
1345
|
unresolvedReason: effective.unresolvedReason
|
|
1355
1346
|
});
|
|
1356
1347
|
if (effectiveRow.to_kind === "operation" && current.depth < maxDepth) {
|
|
1357
|
-
const
|
|
1348
|
+
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
1349
|
+
const files = implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id);
|
|
1358
1350
|
if (files.size > 0) {
|
|
1359
|
-
const targetRepoId = db.prepare(
|
|
1351
|
+
const targetRepoId = implementation.repoId ?? db.prepare(
|
|
1360
1352
|
"SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
|
|
1361
1353
|
).get(effectiveRow.to_id)?.repoId;
|
|
1362
1354
|
const nextKey = `${targetRepoId ?? "*"}:${[...files].sort().join(",")}`;
|
|
@@ -1402,4 +1394,4 @@ export {
|
|
|
1402
1394
|
linkWorkspace,
|
|
1403
1395
|
trace
|
|
1404
1396
|
};
|
|
1405
|
-
//# sourceMappingURL=chunk-
|
|
1397
|
+
//# sourceMappingURL=chunk-YFT57U54.js.map
|