@saptools/service-flow 0.1.14 → 0.1.16

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.16
4
+
5
+ - Index class property arrow/function members as executable symbols so outbound calls inside handler helper properties receive precise source-symbol ownership.
6
+ - Add conservative synthetic callback symbols for top-level CAP lifecycle, route, and event callbacks that contain supported outbound calls.
7
+ - Harden proxy-member symbol-call resolution with richer evidence and avoid ambiguous repository-wide member-name fallback.
8
+ - Split `service-flow link` operation-call output into remote resolved, local resolved, unresolved, ambiguous, dynamic, and terminal buckets.
9
+ - Extend strict doctor ownership diagnostics with ownerless outbound-call details by call type and syntactic gap.
10
+ - Keep `OutboundCallFact.sourceSymbolQualifiedName` aligned with persistence by preferring explicit qualified names before line-range fallback.
11
+
12
+ ## 0.1.15
13
+
14
+ - Fixed `symbol_calls.evidence_json` persistence so inserted rows store the parser evidence object instead of a numeric repository id, with explicit initial resolved/unresolved statuses.
15
+ - Added strict doctor diagnostics for non-object symbol-call evidence, documented unresolved/unknown parser-quality thresholds, and outbound-call source-symbol ownership ratios.
16
+ - Made exported public static class methods addressable through relative imports while keeping non-exported, private/protected, and package-imported class member calls conservative.
17
+ - Indexed exported shorthand object maps as alias symbols and added conservative proxy-variable evidence for `const worker = ExportedClass.staticFactory(); worker.method()` flows when the factory comes from a relative import.
18
+ - Preserved 0.1.14 symbol-call noise filtering, semantic unknown DB targets, terminal transport-client classification, and strict parser-quality aggregates.
19
+
3
20
  ## 0.1.14
4
21
 
5
22
  - 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.
package/README.md CHANGED
@@ -18,6 +18,10 @@ Index independent Git repositories, persist CAP/CDS facts in SQLite, resolve cro
18
18
 
19
19
  ---
20
20
 
21
+ ## 0.1.16 quality update
22
+
23
+ `service-flow` 0.1.16 tightens source ownership and proxy evidence after the 0.1.15 audit. It indexes class property arrow/function members, creates conservative synthetic callback symbols only for top-level framework callbacks containing supported outbound calls, prefers explicit outbound source-symbol names when provided, hardens proxy-member resolution away from ambiguous global name-only matches, and splits link output into remote/local/unresolved/ambiguous/dynamic/terminal operation-call buckets.
24
+
21
25
  ## ✨ Features
22
26
 
23
27
  - 🧭 **Cross-repository CAP tracing** — starts from a repo, service, operation path, operation name, or handler and follows the indexed flow across workspace boundaries
@@ -131,7 +135,7 @@ service-flow index --workspace /path/to/workspace --repo identity-service --forc
131
135
 
132
136
  ### 🔗 `service-flow link`
133
137
 
134
- Resolve indexed outbound calls after repositories have been indexed. This rebuilds the `graph_edges` table for the workspace.
138
+ Resolve indexed outbound calls after repositories have been indexed. This rebuilds the `graph_edges` table for the workspace. The summary separates remote operation calls resolved, local operation calls resolved, unresolved operation calls, ambiguous operation calls, dynamic operation calls, and terminal call edges so local CAP service resolutions are not labeled as remote.
135
139
 
136
140
  ```bash
137
141
  service-flow link --workspace /path/to/workspace
@@ -151,7 +155,7 @@ target handler up to `--depth` instead of showing only calls in the first file.
151
155
 
152
156
  ### Symbol-scoped helper traversal
153
157
 
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.
158
+ `service-flow trace` starts from the selected handler method symbol, renders outbound calls owned by that symbol, and follows conservative local helper-call facts. Handler helper properties such as `helper = async () => { ... }` and `helper = function () { ... }` are indexed as `ClassName.helper`; top-level CAP lifecycle, route, and event callbacks receive synthetic `module:<file>#callback:<line>` owners only when their body contains a supported outbound call or event subscription. Supported helper edges include same-file functions, `this.method()` calls, and exactly mapped relative imports/exports that resolve to an indexed executable symbol. Proxy-member calls keep factory/import evidence and avoid resolving by repository-wide member name alone when the target is ambiguous. Calls from unrelated functions in the same source file are not included merely because the file path matches.
155
159
 
156
160
  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.
157
161
 
package/TECHNICAL-NOTE.md CHANGED
@@ -6,6 +6,20 @@
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.16 audit follow-up notes
10
+
11
+ - Executable symbol parsing now treats class property arrow functions and function-expression initializers as method-like symbols with `ClassName.memberName` qualified names. Their body ranges are persisted so outbound-call ownership can use the existing shortest enclosing range lookup.
12
+ - Synthetic callback symbols are intentionally narrow: only top-level CAP lifecycle, route, and event-registration callbacks whose bodies contain supported outbound calls/subscriptions are indexed as `module:<relative-file>#callback:<line>`. Ordinary anonymous callbacks remain out of scope to avoid broad call-graph noise.
13
+ - Proxy-member calls record proxy variable, factory expression, factory import source, and candidate strategy. Resolution prefers explicit object-map evidence and treats ambiguous repository-wide member-name matches as ambiguous instead of picking the first symbol row. Full whole-program TypeScript data-flow remains a non-goal.
14
+ - `link` summaries now distinguish remote and local operation-call resolutions, while retaining the aggregate `resolvedCount` in the programmatic result for compatibility.
15
+ - Strict doctor source-ownership diagnostics include ownerless groupings by call type and syntactic gap so remaining ownerless calls can be audited without weakening the threshold.
16
+
17
+ ## 0.1.15 audit follow-up notes
18
+
19
+ - Symbol-call rows now require object-shaped parser evidence in strict doctor; this catches numeric JSON regressions that `json_valid()` alone would miss.
20
+ - Relative-import symbol resolution remains opt-in, but exported public static class members and exported shorthand object-map aliases are now addressable with explicit evidence (`exported_class_member`, `exported_object_shorthand`, and `relative_import_proxy_member`).
21
+ - Strict parser-quality aggregates document thresholds for symbol-call unresolved ratio (5%), local DB query unknown ratio (25%), and outbound calls without source-symbol ownership (1%).
22
+
9
23
  ## 0.1.14 audit follow-up notes
10
24
 
11
25
  - 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.
@@ -1186,6 +1186,10 @@ function resolveOperation(db, signals, workspaceId) {
1186
1186
  c.score += 0.2;
1187
1187
  c.reasons.push(signals.repoId !== void 0 ? "explicit_local_service_call" : "explicit_dynamic_override");
1188
1188
  }
1189
+ if (signals.repoId !== void 0 && candidates.length === 1 && signals.serviceName && c.reasons.includes("local_service_name_mismatch") && (c.operationPath === signals.operationPath || c.operationName === signals.operationPath.replace(/^\//, ""))) {
1190
+ c.score = Math.max(c.score, 0.9);
1191
+ c.reasons.push("same_repo_unique_operation_path_with_lookup_mismatch");
1192
+ }
1189
1193
  }
1190
1194
  for (const c of candidates) c.score = Math.max(0, Math.min(1, c.score));
1191
1195
  candidates.sort(
@@ -1205,7 +1209,7 @@ function resolveOperation(db, signals, workspaceId) {
1205
1209
  candidates,
1206
1210
  reasons: ["operation_path_only_has_no_strong_target_signal"]
1207
1211
  };
1208
- if (best && best.score >= 0.9 && (best.servicePath === signals.servicePath || Boolean(signals.serviceName && !best.reasons.includes("local_service_name_mismatch"))) && (best.operationPath === signals.operationPath || best.operationName === signals.operationPath.replace(/^\//, "")) && (!second || best.score - second.score >= 0.25))
1212
+ if (best && best.score >= 0.9 && (best.servicePath === signals.servicePath || Boolean(signals.serviceName && (!best.reasons.includes("local_service_name_mismatch") || best.reasons.includes("same_repo_unique_operation_path_with_lookup_mismatch")))) && (best.operationPath === signals.operationPath || best.operationName === signals.operationPath.replace(/^\//, "")) && (!second || best.score - second.score >= 0.25))
1209
1213
  return {
1210
1214
  status: "resolved",
1211
1215
  target: best,
@@ -1314,6 +1318,8 @@ function linkCalls(db, workspaceId, vars, generation) {
1314
1318
  let edgeCount = 0;
1315
1319
  let unresolvedCount = 0;
1316
1320
  let resolvedCount = 0;
1321
+ let remoteResolvedCount = 0;
1322
+ let localResolvedCount = 0;
1317
1323
  let ambiguousCount = 0;
1318
1324
  let dynamicCount = 0;
1319
1325
  let terminalCount = 0;
@@ -1322,12 +1328,14 @@ function linkCalls(db, workspaceId, vars, generation) {
1322
1328
  const result = insertCallEdge(db, workspaceId, call, vars, generation);
1323
1329
  edgeCount += 1;
1324
1330
  resolvedCount += result.status === "resolved" ? 1 : 0;
1331
+ remoteResolvedCount += result.status === "resolved" && result.callType !== "local_service_call" ? 1 : 0;
1332
+ localResolvedCount += result.status === "resolved" && result.callType === "local_service_call" ? 1 : 0;
1325
1333
  unresolvedCount += result.status === "unresolved" ? 1 : 0;
1326
1334
  ambiguousCount += result.status === "ambiguous" ? 1 : 0;
1327
1335
  dynamicCount += result.status === "dynamic" ? 1 : 0;
1328
1336
  terminalCount += result.status === "terminal" ? 1 : 0;
1329
1337
  }
1330
- return { edgeCount, unresolvedCount, resolvedCount, ambiguousCount, dynamicCount, terminalCount };
1338
+ return { edgeCount, unresolvedCount, resolvedCount, remoteResolvedCount, localResolvedCount, ambiguousCount, dynamicCount, terminalCount };
1331
1339
  }
1332
1340
  function insertCallEdge(db, workspaceId, call, vars, generation) {
1333
1341
  const callType = String(call.call_type);
@@ -1340,11 +1348,11 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
1340
1348
  const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0);
1341
1349
  if (callType === "local_service_call" && call.unresolved_reason === "transport_client_method" && !resolution.target && resolution.candidates.length === 0) {
1342
1350
  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" };
1351
+ return { status: "terminal", callType };
1344
1352
  }
1345
1353
  if (resolution.target) {
1346
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,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);
1347
- return { status: "resolved" };
1355
+ return { status: "resolved", callType };
1348
1356
  }
1349
1357
  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";
1350
1358
  const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
@@ -1352,7 +1360,7 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
1352
1360
  const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : "external";
1353
1361
  const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : String(call.event_name_expr ?? op ?? call.id);
1354
1362
  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);
1355
- return { status };
1363
+ return { status, callType };
1356
1364
  }
1357
1365
  function callEvidence(call, resolution, servicePath, op, destination) {
1358
1366
  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 };
@@ -1987,4 +1995,4 @@ export {
1987
1995
  linkWorkspace,
1988
1996
  trace
1989
1997
  };
1990
- //# sourceMappingURL=chunk-UPXFMLUY.js.map
1998
+ //# sourceMappingURL=chunk-SSSBQIMK.js.map