@saptools/service-flow 0.1.12 → 0.1.14
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 +19 -0
- package/README.md +4 -3
- package/TECHNICAL-NOTE.md +11 -1
- package/dist/{chunk-MXYYARUP.js → chunk-UPXFMLUY.js} +122 -28
- package/dist/chunk-UPXFMLUY.js.map +1 -0
- package/dist/cli.js +83 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-MXYYARUP.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.14
|
|
4
|
+
|
|
5
|
+
- Made local symbol-call indexing opt-in: CAP DSL, request helpers, package namespace/CommonJS calls, global runtime APIs, and service-client transport helpers are filtered unless indexed local or relative-import evidence makes the edge actionable.
|
|
6
|
+
- Expanded local DB query entity extraction for `SELECT.one(Entity)`, `UPSERT.into(Entity)`, `UPDATE.entity(Entity)`, static element access such as `this.model['Books']`, and clearer dynamic-query warning reasons.
|
|
7
|
+
- Persisted unknown DB query graph targets as semantic `db_entity:unknown` terminal nodes with source `callId` and parser-warning evidence, so fresh relinks no longer store numeric call ids as DB targets.
|
|
8
|
+
- Classified unresolved local service-client `.send`, `.emit`, `.publish`, and `.on` calls as terminal transport/client calls when the model does not declare a matching operation, while preserving real declared operations.
|
|
9
|
+
- Tightened doctor default local-service warnings and added compact `doctor --strict` symbol-call and DB-query quality aggregates with capped top unresolved examples.
|
|
10
|
+
- Added neutral regression fixtures for symbol-call noise, DB query forms, and local service-client methods.
|
|
11
|
+
|
|
12
|
+
## 0.1.13
|
|
13
|
+
|
|
14
|
+
- Added implementation-context fallback for local `cds.services.*` calls so helper packages can resolve model-package operations only when handler/dependency/registration evidence ties the caller repository to the target operation.
|
|
15
|
+
- Preserved same-repository local service resolution as the strongest path and added explicit evidence/reasons for implementation-context ownership, rejected candidates, and candidate-without-caller-ownership cases.
|
|
16
|
+
- Replaced fragile CAP DB query string extraction with AST traversal for chained `cds.run(SELECT/INSERT/UPDATE/DELETE...)` forms, including multiline `columns(...).where(...)` chains and `this.EntityName` targets.
|
|
17
|
+
- Kept genuinely dynamic DB queries terminal while exposing parser-warning evidence and rendering unknown targets as `Entity: unknown` instead of raw numeric call ids in table/Mermaid output.
|
|
18
|
+
- Further reduced symbol-call noise by filtering built-in collection/string methods, logger calls, global built-ins, third-party package property calls, and unindexed `this.container.method()` calls while retaining indexed local helpers and relative object-literal helper imports.
|
|
19
|
+
- Added doctor aggregate visibility for local service calls resolved by implementation context and calls left unresolved because candidates lack caller ownership.
|
|
20
|
+
- Updated README and technical notes to clarify generated constants remain low-level parser output rather than persisted graph facts.
|
|
21
|
+
|
|
3
22
|
## 0.1.12
|
|
4
23
|
|
|
5
24
|
- Resolved same-repository local CAP service calls by qualified CDS name, simple service name, and service path, with explicit local transport and alias-chain evidence.
|
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ npm install @saptools/service-flow
|
|
|
69
69
|
- Fresh databases include foreign keys for key graph, run, and diagnostic tables. Migrated legacy stores that still lack that metadata are reported by doctor with `legacy_schema_weaker_foreign_keys`; rebuild into a fresh database if strict structural parity is required.
|
|
70
70
|
- 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.
|
|
71
71
|
|
|
72
|
+
|
|
72
73
|
## 🚀 Quick Start
|
|
73
74
|
|
|
74
75
|
```bash
|
|
@@ -152,11 +153,11 @@ target handler up to `--depth` instead of showing only calls in the first file.
|
|
|
152
153
|
|
|
153
154
|
`service-flow trace` starts from the selected handler method symbol, renders outbound calls owned by that symbol, and follows conservative local helper-call facts. Supported helper edges include same-file functions, `this.method()` calls, and exactly mapped relative imports/exports that resolve to an indexed executable symbol. Calls from unrelated functions in the same source file are not included merely because the file path matches.
|
|
154
155
|
|
|
155
|
-
Local CAP calls through `cds.services.<Service>.<operation>()`, bracket service lookups, and simple aliases are indexed as local operation calls. Linking stays within the same repository
|
|
156
|
+
Local CAP calls through `cds.services.<Service>.<operation>()`, bracket service lookups, and simple aliases are indexed as local operation calls. Linking first stays within the same repository and matches the target operation by exact qualified CDS service name, exact simple service name, exact service path, or an unambiguous service-path suffix. If no same-repository service exists, the linker can use implementation-context evidence to resolve model-package operations for helper packages: a resolved/ambiguous implementation candidate, registration package, or dependency/import edge must tie the caller repository to the model operation. Name-only global matches are preserved as unresolved candidate evidence rather than guessed links. Entity accessors such as `cds.services.db.entities(...)` are treated as entity metadata access, not operation calls.
|
|
156
157
|
|
|
157
158
|
Conservative local symbol traversal intentionally excludes decorators, built-ins such as `JSON.parse`, collection methods, third-party APIs, and arbitrary property chains unless the callee can plausibly resolve to an indexed local symbol. Named export lists such as `export { loadTemplate as publicLoadTemplate }` are indexed with the public exported name so relative imports can resolve. One-level object-literal helpers are indexed as symbols named like `cacheHelper.getConfiguration`; nested object literals are not yet expanded beyond the first helper level. `parseGeneratedConstants` remains a public low-level parser export for callers that need it, but generated constants are not persisted as graph facts in this patch; linking uses the deterministic decorator normalizer described above.
|
|
158
159
|
JSON output includes typed nodes for calls, operations, database entities,
|
|
159
|
-
external destinations, and unresolved/dynamic candidates when edges exist.
|
|
160
|
+
external destinations, and unresolved/dynamic candidates when edges exist. Chained CAP DB queries inside `cds.run(...)` are parsed with TypeScript AST evidence for `SELECT`, `INSERT`, `UPDATE`, and `DELETE` forms. When the query target is genuinely dynamic, graph status remains terminal and JSON retains `parserWarning` evidence, while table and Mermaid render the target as `Entity: unknown` rather than a numeric call id.
|
|
160
161
|
|
|
161
162
|
```bash
|
|
162
163
|
service-flow trace --workspace /path/to/workspace --repo facade-service --operation doWork
|
|
@@ -287,7 +288,7 @@ service-flow clean --workspace /path/to/workspace
|
|
|
287
288
|
| Local data access | `cds.run(SELECT...)` and local entity query evidence |
|
|
288
289
|
| Async channels | Event Mesh-style `emit`, `publish`, and `on` facts |
|
|
289
290
|
| External calls | Cloud SDK-style HTTP/destination calls and external edge evidence |
|
|
290
|
-
| Generated constants |
|
|
291
|
+
| Generated constants | low-level `parseGeneratedConstants` parser output for integrations; not persisted as first-class graph facts in this patch |
|
|
291
292
|
|
|
292
293
|
---
|
|
293
294
|
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Service Flow
|
|
1
|
+
# Service Flow 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.
|
|
@@ -6,6 +6,16 @@
|
|
|
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
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
|
+
## 0.1.14 audit follow-up notes
|
|
10
|
+
|
|
11
|
+
- Local CAP service calls keep same-repository service ownership as the strongest resolution path. When the caller repository does not own the CDS service model, the linker searches workspace operations by service identity and operation name/path, then requires caller ownership evidence from implementation edges, ambiguous implementation candidates, registration packages, or resolved dependency/import edges before resolving. Candidate operations without caller evidence are retained with `local_service_candidate_without_caller_ownership` rather than guessed.
|
|
12
|
+
- Trace traversal can use local-call context to choose the caller repository's handler from an otherwise ambiguous global implementation edge. This is scoped to the local call and does not rewrite the global `OPERATION_IMPLEMENTED_BY_HANDLER` ambiguity.
|
|
13
|
+
- CAP DB entity extraction for local `cds.run(...)` calls uses TypeScript AST traversal across chained query expressions, including `SELECT.from(Entity)`, `SELECT.one.from(Entity)`, `SELECT.one(Entity)`, `INSERT.into(Entity)`, `UPSERT.into(Entity)`, `UPDATE(Entity)`, `UPDATE.entity(Entity)`, `DELETE.from(Entity)`, and static element access such as `this.model['Books']`. Dynamic or unknowable query targets remain terminal graph edges with parser-warning evidence such as `dynamic_entity_expression`.
|
|
14
|
+
- Fresh relinks persist unknown DB query targets semantically as `to_kind = db_entity` and `to_id = unknown`; no schema migration is required for this row-level graph change. JSON, table, and Mermaid output use `Entity: unknown`, while evidence keeps the source `callId` and parser warning for machines.
|
|
15
|
+
- Symbol-call indexing is opt-in. Same-file symbols, indexed `this.method()` calls, exact relative import/export matches, and exported object-literal helper methods are kept; CAP DSL, request helpers, package namespace/CommonJS calls, global runtime APIs, and generic transport helpers are filtered unless local indexed evidence makes the edge actionable.
|
|
16
|
+
- `doctor --strict` reports compact parser-quality aggregates for symbol calls and local DB query known/unknown ratios; default doctor stays focused on actionable warnings.
|
|
17
|
+
- Generated constants remain low-level parser output through `parseGeneratedConstants`. They are not persisted as graph facts; implementation linking only uses deterministic decorator normalization for common generated action/function names.
|
|
18
|
+
|
|
9
19
|
## 0.1.4 trace-correctness additions
|
|
10
20
|
|
|
11
21
|
- Helper exports are normalized through a public-to-local export map, so `export { helper }` and `export { helper as publicHelper }` both resolve to the local declaration that contains the `cds.connect.to(...)` call.
|
|
@@ -555,22 +555,56 @@ function matchingParen(text, open) {
|
|
|
555
555
|
}
|
|
556
556
|
return -1;
|
|
557
557
|
}
|
|
558
|
-
function
|
|
559
|
-
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
if (
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
558
|
+
function entityFromExpression(expr) {
|
|
559
|
+
if (!expr) return void 0;
|
|
560
|
+
if (ts4.isIdentifier(expr) || ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
|
|
561
|
+
if (ts4.isPropertyAccessExpression(expr) && expr.expression.kind === ts4.SyntaxKind.ThisKeyword) return expr.name.text;
|
|
562
|
+
if (ts4.isElementAccessExpression(expr) && expr.argumentExpression && (ts4.isStringLiteral(expr.argumentExpression) || ts4.isNoSubstitutionTemplateLiteral(expr.argumentExpression))) return expr.argumentExpression.text;
|
|
563
|
+
return void 0;
|
|
564
|
+
}
|
|
565
|
+
function expressionName(expr) {
|
|
566
|
+
if (ts4.isIdentifier(expr)) return expr.text;
|
|
567
|
+
if (ts4.isPropertyAccessExpression(expr)) return `${expressionName(expr.expression)}.${expr.name.text}`;
|
|
568
|
+
return expr.getText();
|
|
569
|
+
}
|
|
570
|
+
function variableInitializers(source) {
|
|
571
|
+
const initializers = /* @__PURE__ */ new Map();
|
|
572
|
+
const visit = (node) => {
|
|
573
|
+
if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer && (node.parent.flags & ts4.NodeFlags.Const) !== 0) initializers.set(node.name.text, node.initializer);
|
|
574
|
+
ts4.forEachChild(node, visit);
|
|
575
|
+
};
|
|
576
|
+
visit(source);
|
|
577
|
+
return initializers;
|
|
578
|
+
}
|
|
579
|
+
function queryEntityFromAst(expr, initializers = /* @__PURE__ */ new Map()) {
|
|
580
|
+
if (ts4.isParenthesizedExpression(expr) || ts4.isAwaitExpression(expr)) return queryEntityFromAst(expr.expression, initializers);
|
|
581
|
+
if (ts4.isIdentifier(expr) && initializers.has(expr.text)) return queryEntityFromAst(initializers.get(expr.text), initializers);
|
|
582
|
+
if (ts4.isCallExpression(expr)) {
|
|
583
|
+
const name = expressionName(expr.expression);
|
|
584
|
+
if (name === "cds.run") return queryEntityFromAst(expr.arguments[0], initializers);
|
|
585
|
+
if (["SELECT.one.from", "SELECT.from", "SELECT.one", "INSERT.into", "UPSERT.into", "DELETE.from", "UPDATE.entity"].includes(name)) return entityFromExpression(expr.arguments[0]);
|
|
586
|
+
if (name === "UPDATE") return entityFromExpression(expr.arguments[0]);
|
|
587
|
+
const receiver = ts4.isPropertyAccessExpression(expr.expression) ? expr.expression.expression : void 0;
|
|
588
|
+
if (receiver) return queryEntityFromAst(receiver, initializers);
|
|
589
|
+
}
|
|
590
|
+
return void 0;
|
|
571
591
|
}
|
|
572
592
|
function extractQueryEntity(expr) {
|
|
573
|
-
|
|
593
|
+
const source = ts4.createSourceFile("query.ts", `const __query = (${expr});`, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
|
|
594
|
+
const initializers = variableInitializers(source);
|
|
595
|
+
let found;
|
|
596
|
+
const visit = (node) => {
|
|
597
|
+
if (found) return;
|
|
598
|
+
if (ts4.isParenthesizedExpression(node)) found = queryEntityFromAst(node.expression, initializers);
|
|
599
|
+
ts4.forEachChild(node, visit);
|
|
600
|
+
};
|
|
601
|
+
visit(source);
|
|
602
|
+
return found;
|
|
603
|
+
}
|
|
604
|
+
function queryWarning(expr) {
|
|
605
|
+
if (/^\s*[`'"]/.test(expr)) return "raw_sql_or_cql_expression";
|
|
606
|
+
if (/^\s*\w+\s*$/.test(expr)) return "query_variable_without_static_initializer";
|
|
607
|
+
return "dynamic_entity_expression";
|
|
574
608
|
}
|
|
575
609
|
async function parseOutboundCalls(repoPath, filePath) {
|
|
576
610
|
const text = await fs6.readFile(path7.join(repoPath, filePath), "utf8");
|
|
@@ -603,7 +637,7 @@ async function parseOutboundCalls(repoPath, filePath) {
|
|
|
603
637
|
sourceFile: normalizePath(filePath),
|
|
604
638
|
sourceLine: lineOf3(text, m.index ?? 0),
|
|
605
639
|
confidence: entity ? 0.9 : 0.55,
|
|
606
|
-
unresolvedReason: entity ? void 0 :
|
|
640
|
+
unresolvedReason: entity ? void 0 : queryWarning(expr)
|
|
607
641
|
});
|
|
608
642
|
}
|
|
609
643
|
for (const m of text.matchAll(
|
|
@@ -651,7 +685,8 @@ function parseLocalServiceCalls(text, filePath) {
|
|
|
651
685
|
aliasChain: parsed.chain,
|
|
652
686
|
sourceFile: normalizePath(filePath),
|
|
653
687
|
sourceLine: lineOf3(text, node.getStart(source)),
|
|
654
|
-
confidence: 0.9
|
|
688
|
+
confidence: 0.9,
|
|
689
|
+
unresolvedReason: ["send", "emit", "publish", "on"].includes(parsed.operation) ? "transport_client_method" : void 0
|
|
655
690
|
});
|
|
656
691
|
}
|
|
657
692
|
ts4.forEachChild(node, visit);
|
|
@@ -1074,7 +1109,7 @@ function substituteVariables(template, vars) {
|
|
|
1074
1109
|
// src/linker/service-resolver.ts
|
|
1075
1110
|
function rows(db, operationPath, workspaceId) {
|
|
1076
1111
|
return db.prepare(
|
|
1077
|
-
`SELECT o.id operationId,r.name repoName,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
|
|
1112
|
+
`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
|
|
1078
1113
|
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
|
|
1079
1114
|
WHERE (? IS NULL OR r.workspace_id=?) AND (o.operation_path=? OR o.operation_name=?) ORDER BY r.name,s.service_path,o.operation_name`
|
|
1080
1115
|
).all(
|
|
@@ -1098,11 +1133,21 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
1098
1133
|
candidates: [],
|
|
1099
1134
|
reasons: ["missing_operation_path"]
|
|
1100
1135
|
};
|
|
1101
|
-
const
|
|
1136
|
+
const allCandidates = rows(db, signals.operationPath, workspaceId).map((c) => ({
|
|
1102
1137
|
...c,
|
|
1103
1138
|
score: 0.2,
|
|
1104
1139
|
reasons: ["operation_path_match"]
|
|
1105
1140
|
}));
|
|
1141
|
+
let candidates = allCandidates.filter((c) => matchesLocalRepo(db, c.operationId, signals.repoId));
|
|
1142
|
+
if (candidates.length === 0 && signals.repoId !== void 0 && signals.serviceName) {
|
|
1143
|
+
candidates = implementationContextCandidates(db, allCandidates, signals.repoId, signals.serviceName);
|
|
1144
|
+
if (candidates.length === 0)
|
|
1145
|
+
return {
|
|
1146
|
+
status: "unresolved",
|
|
1147
|
+
candidates: allCandidates.filter((c) => serviceMatches(c, signals.serviceName)),
|
|
1148
|
+
reasons: allCandidates.some((c) => serviceMatches(c, signals.serviceName)) ? ["local_service_candidate_without_caller_ownership"] : ["no_operation_candidates"]
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1106
1151
|
if (candidates.length === 0)
|
|
1107
1152
|
return {
|
|
1108
1153
|
status: "unresolved",
|
|
@@ -1173,6 +1218,34 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
1173
1218
|
reasons: ["candidate_score_below_resolution_threshold"]
|
|
1174
1219
|
};
|
|
1175
1220
|
}
|
|
1221
|
+
function serviceMatches(candidate, serviceName) {
|
|
1222
|
+
if (!serviceName) return false;
|
|
1223
|
+
const simple = serviceName.split(".").at(-1) ?? serviceName;
|
|
1224
|
+
return candidate.qualifiedName === serviceName || candidate.serviceName === serviceName || candidate.serviceName === simple || candidate.servicePath === serviceName || candidate.servicePath === `/${serviceName}` || candidate.servicePath === `/${simple}` || candidate.servicePath.endsWith(`/${simple}`);
|
|
1225
|
+
}
|
|
1226
|
+
function implementationContextCandidates(db, candidates, callerRepoId, serviceName) {
|
|
1227
|
+
const matching = candidates.filter((candidate) => serviceMatches(candidate, serviceName));
|
|
1228
|
+
const owned = matching.map((candidate) => ownershipReason(db, candidate, callerRepoId)).filter((item) => Boolean(item));
|
|
1229
|
+
if (owned.length === 0) return [];
|
|
1230
|
+
const direct = owned.filter((item) => item.reason !== "caller_depends_on_model_package");
|
|
1231
|
+
const chosen = direct.length > 0 ? direct : owned.length === 1 ? owned : [];
|
|
1232
|
+
return chosen.map((item) => ({ ...item.candidate, score: 0.95, reasons: [...item.candidate.reasons, "implementation_context_caller_ownership", item.reason] }));
|
|
1233
|
+
}
|
|
1234
|
+
function ownershipReason(db, candidate, callerRepoId) {
|
|
1235
|
+
const edge = db.prepare("SELECT status,evidence_json,to_id FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND from_kind='operation' AND from_id=? ORDER BY CASE status WHEN 'resolved' THEN 0 WHEN 'ambiguous' THEN 1 ELSE 2 END LIMIT 1").get(String(candidate.operationId));
|
|
1236
|
+
if (edge?.status === "resolved") {
|
|
1237
|
+
const row = db.prepare("SELECT hc.repo_id repoId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id WHERE hm.id=?").get(edge.to_id);
|
|
1238
|
+
if (row?.repoId === callerRepoId) return { candidate, reason: "resolved_implementation_handler_repo_matches_caller" };
|
|
1239
|
+
}
|
|
1240
|
+
if (edge?.evidence_json) {
|
|
1241
|
+
const evidence = JSON.parse(edge.evidence_json);
|
|
1242
|
+
const hit = evidence.candidates?.find((item) => item.accepted && (Number(item.handlerPackage?.id) === callerRepoId || Number(item.applicationPackage?.id) === callerRepoId));
|
|
1243
|
+
if (hit) return { candidate, reason: edge.status === "ambiguous" ? "ambiguous_implementation_candidate_repo_matches_caller" : "registration_package_matches_caller" };
|
|
1244
|
+
}
|
|
1245
|
+
const dep = db.prepare("SELECT 1 FROM graph_edges WHERE edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND status='resolved' AND from_kind='repo' AND from_id=? AND to_id=?").get(String(callerRepoId), String(candidate.repoId));
|
|
1246
|
+
if (dep) return { candidate, reason: "caller_depends_on_model_package" };
|
|
1247
|
+
return void 0;
|
|
1248
|
+
}
|
|
1176
1249
|
function matchesLocalRepo(db, operationId, repoId) {
|
|
1177
1250
|
if (repoId === void 0) return true;
|
|
1178
1251
|
const row = db.prepare("SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?").get(operationId);
|
|
@@ -1227,8 +1300,8 @@ function linkWorkspace(db, workspaceId, vars = {}) {
|
|
|
1227
1300
|
const generation = nextGraphGeneration(db, workspaceId);
|
|
1228
1301
|
db.prepare("DELETE FROM graph_edges WHERE workspace_id=?").run(workspaceId);
|
|
1229
1302
|
const deps = linkHelperPackages(db, workspaceId, generation);
|
|
1230
|
-
const callSummary = linkCalls(db, workspaceId, vars, generation);
|
|
1231
1303
|
const impl = linkImplementations(db, workspaceId, generation);
|
|
1304
|
+
const callSummary = linkCalls(db, workspaceId, vars, generation);
|
|
1232
1305
|
db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
|
|
1233
1306
|
return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount, implementationUnresolvedCount: impl.unresolvedCount };
|
|
1234
1307
|
});
|
|
@@ -1265,6 +1338,10 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
1265
1338
|
const isOperationCall = callType.startsWith("remote") || callType === "local_service_call";
|
|
1266
1339
|
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: [] };
|
|
1267
1340
|
const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0);
|
|
1341
|
+
if (callType === "local_service_call" && call.unresolved_reason === "transport_client_method" && !resolution.target && resolution.candidates.length === 0) {
|
|
1342
|
+
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);
|
|
1343
|
+
return { status: "terminal" };
|
|
1344
|
+
}
|
|
1268
1345
|
if (resolution.target) {
|
|
1269
1346
|
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, callType === "local_service_call" ? "LOCAL_CALL_RESOLVES_TO_OPERATION" : "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);
|
|
1270
1347
|
return { status: "resolved" };
|
|
@@ -1272,11 +1349,13 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
1272
1349
|
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";
|
|
1273
1350
|
const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
|
|
1274
1351
|
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"));
|
|
1275
|
-
|
|
1352
|
+
const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : "external";
|
|
1353
|
+
const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : String(call.event_name_expr ?? op ?? call.id);
|
|
1354
|
+
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), isDynamic || resolution.status === "dynamic" ? 1 : 0, unresolvedReason, generation);
|
|
1276
1355
|
return { status };
|
|
1277
1356
|
}
|
|
1278
1357
|
function callEvidence(call, resolution, servicePath, op, destination) {
|
|
1279
|
-
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, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: call.alias_chain_json ? JSON.parse(String(call.alias_chain_json)) : void 0, transport: call.call_type === "local_service_call" ? "local" : void 0, 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 };
|
|
1358
|
+
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: call.alias_chain_json ? JSON.parse(String(call.alias_chain_json)) : void 0, transport: call.call_type === "local_service_call" ? "local" : void 0, 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 };
|
|
1280
1359
|
}
|
|
1281
1360
|
function linkImplementations(db, workspaceId, generation) {
|
|
1282
1361
|
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);
|
|
@@ -1633,6 +1712,17 @@ function implementationScope(db, operationId) {
|
|
|
1633
1712
|
const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?").get(edge.to_id);
|
|
1634
1713
|
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
|
|
1635
1714
|
}
|
|
1715
|
+
function contextImplementationMethodId(edge, callerRepoId) {
|
|
1716
|
+
if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return void 0;
|
|
1717
|
+
const evidence = JSON.parse(String(edge.evidence_json || "{}"));
|
|
1718
|
+
const candidate = evidence.candidates?.find((item) => item.accepted && (Number(item.handlerPackage?.id) === callerRepoId || Number(item.applicationPackage?.id) === callerRepoId));
|
|
1719
|
+
return candidate?.methodId === void 0 ? void 0 : String(candidate.methodId);
|
|
1720
|
+
}
|
|
1721
|
+
function handlerScope(db, methodId) {
|
|
1722
|
+
const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?").get(methodId);
|
|
1723
|
+
if (!row) return void 0;
|
|
1724
|
+
return { repoId: row.repoId, files: new Set(row.sourceFile ? [row.sourceFile] : []), symbolId: row.symbolId };
|
|
1725
|
+
}
|
|
1636
1726
|
function includeCall(type, options) {
|
|
1637
1727
|
if (!options.includeDb && type === "local_db_query") return false;
|
|
1638
1728
|
if (!options.includeExternal && type === "external_http") return false;
|
|
@@ -1716,6 +1806,7 @@ function edgeTarget(row, evidence) {
|
|
|
1716
1806
|
const operationPath = typeof evidence.operationPath === "string" ? evidence.operationPath : void 0;
|
|
1717
1807
|
const targetOperation = typeof evidence.targetOperation === "string" ? evidence.targetOperation : void 0;
|
|
1718
1808
|
const targetRepo = typeof evidence.targetRepo === "string" ? evidence.targetRepo : "";
|
|
1809
|
+
if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return `Entity: ${row.to_id || "unknown"}`;
|
|
1719
1810
|
return servicePath && operationPath ? `${servicePath}${operationPath}` : targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
|
|
1720
1811
|
}
|
|
1721
1812
|
function trace(db, start, options) {
|
|
@@ -1813,7 +1904,7 @@ function trace(db, start, options) {
|
|
|
1813
1904
|
nodes.set(targetNode, opNode ?? {
|
|
1814
1905
|
id: targetNode,
|
|
1815
1906
|
kind: effectiveRow.to_kind,
|
|
1816
|
-
label: effectiveRow.to_id
|
|
1907
|
+
label: effectiveRow.to_kind === "db_entity" ? `Entity: ${effectiveRow.to_id || "unknown"}` : effectiveRow.to_id
|
|
1817
1908
|
});
|
|
1818
1909
|
const to = edgeTarget(effectiveRow, evidence);
|
|
1819
1910
|
edges.push({
|
|
@@ -1827,9 +1918,11 @@ function trace(db, start, options) {
|
|
|
1827
1918
|
});
|
|
1828
1919
|
if (effectiveRow.to_kind === "operation") {
|
|
1829
1920
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
1921
|
+
const contextMethodId = contextImplementationMethodId(implementation.edge, current.repoId);
|
|
1922
|
+
const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : void 0;
|
|
1830
1923
|
if (implementation.edge) {
|
|
1831
1924
|
const implEvidence = JSON.parse(String(implementation.edge.evidence_json || "{}"));
|
|
1832
|
-
const handlerNode = implementation.edge.status === "resolved" ? handlerMethodNode(db, implementation.edge.to_id) :
|
|
1925
|
+
const handlerNode = implementation.edge.status === "resolved" ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
|
|
1833
1926
|
const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
|
|
1834
1927
|
if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
|
|
1835
1928
|
edges.push({
|
|
@@ -1843,10 +1936,11 @@ function trace(db, start, options) {
|
|
|
1843
1936
|
});
|
|
1844
1937
|
}
|
|
1845
1938
|
if (current.depth >= maxDepth) continue;
|
|
1846
|
-
const
|
|
1847
|
-
const
|
|
1848
|
-
|
|
1849
|
-
|
|
1939
|
+
const contextScope = contextMethodId ? handlerScope(db, contextMethodId) : void 0;
|
|
1940
|
+
const files = contextScope?.files ?? (implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id));
|
|
1941
|
+
const symbolIds = contextScope?.symbolId ? /* @__PURE__ */ new Set([contextScope.symbolId]) : implementation.symbolId ? /* @__PURE__ */ new Set([implementation.symbolId]) : void 0;
|
|
1942
|
+
if ((implementation.edge?.status === "resolved" || contextScope) && files.size > 0) {
|
|
1943
|
+
const targetRepoId = contextScope?.repoId ?? implementation.repoId ?? db.prepare(
|
|
1850
1944
|
"SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
|
|
1851
1945
|
).get(effectiveRow.to_id)?.repoId;
|
|
1852
1946
|
const nextKey = `${targetRepoId ?? "*"}:${[...symbolIds ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${[...files].sort().join(",")}`;
|
|
@@ -1893,4 +1987,4 @@ export {
|
|
|
1893
1987
|
linkWorkspace,
|
|
1894
1988
|
trace
|
|
1895
1989
|
};
|
|
1896
|
-
//# sourceMappingURL=chunk-
|
|
1990
|
+
//# sourceMappingURL=chunk-UPXFMLUY.js.map
|