@saptools/service-flow 0.1.15 → 0.1.17

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.17
4
+
5
+ - Replaced remaining raw-text outbound call detection with TypeScript AST classification so comments and strings do not create outbound facts or graph edges.
6
+ - Added AST parser/range evidence for outbound calls and conservative CAP/service event registration ownership.
7
+ - Tightened synthetic callback and async subscription classification to avoid generic response `.send()` and non-CAP event listener noise.
8
+ - Expanded strict doctor ownerless source categories and examples.
9
+
10
+ ## 0.1.16
11
+
12
+ - Index class property arrow/function members as executable symbols so outbound calls inside handler helper properties receive precise source-symbol ownership.
13
+ - Add conservative synthetic callback symbols for top-level CAP lifecycle, route, and event callbacks that contain supported outbound calls.
14
+ - Harden proxy-member symbol-call resolution with richer evidence and avoid ambiguous repository-wide member-name fallback.
15
+ - Split `service-flow link` operation-call output into remote resolved, local resolved, unresolved, ambiguous, dynamic, and terminal buckets.
16
+ - Extend strict doctor ownership diagnostics with ownerless outbound-call details by call type and syntactic gap.
17
+ - Keep `OutboundCallFact.sourceSymbolQualifiedName` aligned with persistence by preferring explicit qualified names before line-range fallback.
18
+
3
19
  ## 0.1.15
4
20
 
5
21
  - 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.
package/README.md CHANGED
@@ -18,9 +18,9 @@ Index independent Git repositories, persist CAP/CDS facts in SQLite, resolve cro
18
18
 
19
19
  ---
20
20
 
21
- ## 0.1.15 quality update
21
+ ## 0.1.17 quality update
22
22
 
23
- `service-flow` 0.1.15 fixes symbol-call evidence persistence so fresh databases store object-shaped parser evidence, extends strict doctor checks for evidence and parser-quality ratios, and conservatively resolves exported static class-member and exported shorthand object-map helper flows through relative imports without restoring broad symbol-call fallback noise.
23
+ `service-flow` 0.1.17 hardens outbound extraction after the 0.1.16 audit. Outbound calls are classified from the TypeScript AST, so comments and strings are ignored; CAP/service lifecycle registrations that remain indexed receive synthetic event-registration owners; generic event emitters and response `.send()` calls are not treated as CAP service-flow edges; and `doctor --strict` reports actionable ownerless categories. The previous 0.1.16 source ownership and proxy-evidence improvements remain in place. 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
24
 
25
25
  ## ✨ Features
26
26
 
@@ -135,7 +135,7 @@ service-flow index --workspace /path/to/workspace --repo identity-service --forc
135
135
 
136
136
  ### 🔗 `service-flow link`
137
137
 
138
- 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.
139
139
 
140
140
  ```bash
141
141
  service-flow link --workspace /path/to/workspace
@@ -155,7 +155,7 @@ target handler up to `--depth` instead of showing only calls in the first file.
155
155
 
156
156
  ### Symbol-scoped helper traversal
157
157
 
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. 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.
159
159
 
160
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.
161
161
 
@@ -473,3 +473,8 @@ Handler registrations persist parsed class names and import sources. Linking res
473
473
  ### Graph variables
474
474
 
475
475
  The `graph` command accepts repeatable `--var key=value` options, matching `trace`, for runtime substitution previews in JSON or Mermaid output.
476
+
477
+
478
+ ### 0.1.17 parser ownership policy
479
+
480
+ Outbound call extraction is AST-based and ignores comments, block comments, and string literals. CAP/service `.on(...)` registrations are indexed only when the receiver has CAP/service evidence, and top-level registrations receive `module:<relative-file>#event:<event-name>:<line>` synthetic owners. Generic event emitters such as desktop or window events are ignored by default rather than guessed as CAP async edges. Unsupported source shapes are surfaced through diagnostics and strict doctor ownerless categories instead of guessed graph edges.
package/TECHNICAL-NOTE.md CHANGED
@@ -6,6 +6,14 @@
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
+
9
17
  ## 0.1.15 audit follow-up notes
10
18
 
11
19
  - Symbol-call rows now require object-shaped parser evidence in strict doctor; this catches numeric JSON regressions that `json_valid()` alone would miss.
@@ -60,3 +68,8 @@
60
68
  - Fingerprints hash normalized package facts and package bytes in addition to source file paths/content and analyzer version.
61
69
  - The CLI version imports package metadata, so package metadata, `service-flow --version`, changelog, and analyzer/fingerprint version share one release source.
62
70
  - Supported runtime is Node.js 24+ with `node:sqlite` validation; older runtimes should fail with a compatibility message instead of a late `DatabaseSync` error.
71
+
72
+
73
+ ### 0.1.17 parser ownership policy
74
+
75
+ Outbound call extraction is AST-based and ignores comments, block comments, and string literals. CAP/service `.on(...)` registrations are indexed only when the receiver has CAP/service evidence, and top-level registrations receive `module:<relative-file>#event:<event-name>:<line>` synthetic owners. Generic event emitters such as desktop or window events are ignored by default rather than guessed as CAP async edges. Unsupported source shapes are surfaced through diagnostics and strict doctor ownerless categories instead of guessed graph edges.
@@ -528,33 +528,6 @@ import ts4 from "typescript";
528
528
  function lineOf3(text, idx) {
529
529
  return text.slice(0, idx).split("\n").length;
530
530
  }
531
- function firstArg2(body, key) {
532
- return new RegExp(`${key}\\s*:\\s*([^,}\\n]+)`).exec(body)?.[1]?.trim();
533
- }
534
- function matchingParen(text, open) {
535
- let depth = 0;
536
- let quote;
537
- let escaped = false;
538
- for (let i = open; i < text.length; i += 1) {
539
- const ch = text[i] ?? "";
540
- if (quote) {
541
- if (escaped) escaped = false;
542
- else if (ch === "\\") escaped = true;
543
- else if (ch === quote) quote = void 0;
544
- continue;
545
- }
546
- if (ch === '"' || ch === "'" || ch === "`") {
547
- quote = ch;
548
- continue;
549
- }
550
- if (ch === "(") depth += 1;
551
- if (ch === ")") {
552
- depth -= 1;
553
- if (depth === 0) return i;
554
- }
555
- }
556
- return -1;
557
- }
558
531
  function entityFromExpression(expr) {
559
532
  if (!expr) return void 0;
560
533
  if (ts4.isIdentifier(expr) || ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
@@ -606,64 +579,98 @@ function queryWarning(expr) {
606
579
  if (/^\s*\w+\s*$/.test(expr)) return "query_variable_without_static_initializer";
607
580
  return "dynamic_entity_expression";
608
581
  }
582
+ function parserEvidence(source, node, extra) {
583
+ return { parser: "typescript_ast", startOffset: node.getStart(source), endOffset: node.getEnd(), ...extra };
584
+ }
585
+ function isStringLike(expr) {
586
+ return Boolean(expr && (ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr)));
587
+ }
588
+ function literalText(expr) {
589
+ if (isStringLike(expr)) return expr.text;
590
+ return void 0;
591
+ }
592
+ function objectPropertyText(object, key) {
593
+ const prop = object.properties.find((property) => ts4.isPropertyAssignment(property) && nameOfProperty(property.name) === key);
594
+ return prop ? prop.initializer.getText() : void 0;
595
+ }
596
+ function nameOfProperty(name) {
597
+ if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name)) return name.text;
598
+ return void 0;
599
+ }
600
+ function collectServiceVariables(source) {
601
+ const vars = /* @__PURE__ */ new Set(["cds", "messaging", "messageClient", "eventClient"]);
602
+ const visit = (node) => {
603
+ if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer) {
604
+ const text = node.initializer.getText(source);
605
+ if (/cds\.connect\.(to|messaging)\s*\(/.test(text) || /create.*(Client|Remote|Service|Messaging)/.test(text)) vars.add(node.name.text);
606
+ }
607
+ ts4.forEachChild(node, visit);
608
+ };
609
+ visit(source);
610
+ return vars;
611
+ }
612
+ function receiverName(expr) {
613
+ if (ts4.isIdentifier(expr)) return expr.text;
614
+ if (ts4.isPropertyAccessExpression(expr)) return expr.getText();
615
+ return void 0;
616
+ }
617
+ function isSupportedEventReceiver(receiver, serviceVariables) {
618
+ if (!receiver) return false;
619
+ if (receiver === "cds") return true;
620
+ if (serviceVariables.has(receiver)) return true;
621
+ if (/^(srv|service|serviceClient|messaging|messageClient|eventClient|.*Client)$/.test(receiver)) return true;
622
+ return false;
623
+ }
624
+ function classifyOutboundCallsInSource(source, filePath) {
625
+ const calls = [];
626
+ const sourceFile = normalizePath(filePath);
627
+ const initializers = variableInitializers(source);
628
+ const serviceVariables = collectServiceVariables(source);
629
+ const add = (node, fact, extra) => {
630
+ calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf3(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
631
+ };
632
+ const visit = (node) => {
633
+ if (ts4.isCallExpression(node)) {
634
+ const expr = node.expression;
635
+ const exprText = expr.getText(source);
636
+ if (exprText === "cds.run") {
637
+ const arg = node.arguments[0];
638
+ const entity = arg ? queryEntityFromAst(arg, initializers) : void 0;
639
+ const payload = arg?.getText(source) ?? "";
640
+ add(node, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) });
641
+ } else if (ts4.isPropertyAccessExpression(expr) && expr.name.text === "send" && ts4.isIdentifier(expr.expression)) {
642
+ const objectArg = node.arguments[0];
643
+ if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
644
+ const receiver = expr.expression.text;
645
+ const query = objectPropertyText(objectArg, "query");
646
+ const op = objectPropertyText(objectArg, "path") ?? objectPropertyText(objectArg, "event");
647
+ add(node, { callType: query ? "remote_query" : "remote_action", serviceVariableName: receiver, method: stripQuotes(objectPropertyText(objectArg, "method") ?? "POST"), operationPathExpr: op ? `/${stripQuotes(op).replace(/^\//, "")}` : void 0, queryEntity: query ? extractQueryEntity(query) : void 0, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4 }, { receiver, classifier: "service_client_send_object" });
648
+ }
649
+ } else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
650
+ const receiver = receiverName(expr.expression);
651
+ if (expr.name.text !== "on" || isSupportedEventReceiver(receiver, serviceVariables)) {
652
+ const eventName = literalText(node.arguments[0]);
653
+ if (eventName) add(node, { callType: expr.name.text === "on" ? "async_subscribe" : "async_emit", serviceVariableName: receiver, eventNameExpr: eventName }, { receiver, classifier: expr.name.text === "on" ? "cap_service_event_subscription" : "cap_service_event_emit" });
654
+ }
655
+ } else if (exprText === "axios" || exprText === "executeHttpRequest" || exprText === "useOrFetchDestination") {
656
+ add(node, { callType: "external_http", payloadSummary: summarizeExpression(node.arguments.map((arg) => arg.getText(source)).join(", ")), confidence: 0.7, unresolvedReason: "External HTTP destination is outside indexed CAP services" });
657
+ }
658
+ }
659
+ ts4.forEachChild(node, visit);
660
+ };
661
+ visit(source);
662
+ return calls;
663
+ }
664
+ function containsSupportedOutboundCall(node) {
665
+ const source = node.getSourceFile();
666
+ const start = node.getFullStart();
667
+ const end = node.getEnd();
668
+ return classifyOutboundCallsInSource(source, source.fileName).some((call) => call.node.getStart(source) >= start && call.node.getEnd() <= end);
669
+ }
609
670
  async function parseOutboundCalls(repoPath, filePath) {
610
671
  const text = await fs6.readFile(path7.join(repoPath, filePath), "utf8");
611
- const out = [];
612
- for (const m of text.matchAll(/(\w+)\.send\s*\(\s*\{([\s\S]*?)\}\s*\)/g)) {
613
- const body = m[2] ?? "";
614
- const query = firstArg2(body, "query");
615
- const op = firstArg2(body, "path") ?? firstArg2(body, "event");
616
- out.push({
617
- callType: query ? "remote_query" : "remote_action",
618
- serviceVariableName: m[1],
619
- method: stripQuotes(firstArg2(body, "method") ?? "POST"),
620
- operationPathExpr: op ? `/${stripQuotes(op).replace(/^\//, "")}` : void 0,
621
- queryEntity: extractQueryEntity(query ?? ""),
622
- payloadSummary: summarizeExpression(body),
623
- sourceFile: normalizePath(filePath),
624
- sourceLine: lineOf3(text, m.index ?? 0),
625
- confidence: op || query ? 0.8 : 0.4
626
- });
627
- }
628
- for (const m of text.matchAll(/cds\.run\s*\(/g)) {
629
- const open = (m.index ?? 0) + m[0].lastIndexOf("(");
630
- const close = matchingParen(text, open);
631
- const expr = close > open ? text.slice(open + 1, close) : "";
632
- const entity = extractQueryEntity(expr);
633
- out.push({
634
- callType: "local_db_query",
635
- queryEntity: entity,
636
- payloadSummary: summarizeExpression(expr),
637
- sourceFile: normalizePath(filePath),
638
- sourceLine: lineOf3(text, m.index ?? 0),
639
- confidence: entity ? 0.9 : 0.55,
640
- unresolvedReason: entity ? void 0 : queryWarning(expr)
641
- });
642
- }
643
- for (const m of text.matchAll(
644
- /(\w+)\.(emit|publish|on)\s*\(\s*(['"`])([^'"`]+)\3/g
645
- ))
646
- out.push({
647
- callType: m[2] === "on" ? "async_subscribe" : "async_emit",
648
- serviceVariableName: m[1],
649
- eventNameExpr: m[4],
650
- sourceFile: normalizePath(filePath),
651
- sourceLine: lineOf3(text, m.index ?? 0),
652
- confidence: 0.8
653
- });
654
- for (const m of text.matchAll(
655
- /(?:axios\s*\(|executeHttpRequest\s*\(|useOrFetchDestination\s*\()([\s\S]*?)\)/g
656
- ))
657
- out.push({
658
- callType: "external_http",
659
- payloadSummary: summarizeExpression(m[1] ?? ""),
660
- sourceFile: normalizePath(filePath),
661
- sourceLine: lineOf3(text, m.index ?? 0),
662
- confidence: 0.7,
663
- unresolvedReason: "External HTTP destination is outside indexed CAP services"
664
- });
665
- out.push(...parseLocalServiceCalls(text, filePath));
666
- return out;
672
+ const source = ts4.createSourceFile(filePath, text, ts4.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts4.ScriptKind.TS : ts4.ScriptKind.JS);
673
+ return [...classifyOutboundCallsInSource(source, filePath).map((call) => call.fact), ...parseLocalServiceCalls(text, filePath)];
667
674
  }
668
675
  function parseLocalServiceCalls(text, filePath) {
669
676
  const source = ts4.createSourceFile(filePath, text, ts4.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts4.ScriptKind.TS : ts4.ScriptKind.JS);
@@ -1318,6 +1325,8 @@ function linkCalls(db, workspaceId, vars, generation) {
1318
1325
  let edgeCount = 0;
1319
1326
  let unresolvedCount = 0;
1320
1327
  let resolvedCount = 0;
1328
+ let remoteResolvedCount = 0;
1329
+ let localResolvedCount = 0;
1321
1330
  let ambiguousCount = 0;
1322
1331
  let dynamicCount = 0;
1323
1332
  let terminalCount = 0;
@@ -1326,12 +1335,14 @@ function linkCalls(db, workspaceId, vars, generation) {
1326
1335
  const result = insertCallEdge(db, workspaceId, call, vars, generation);
1327
1336
  edgeCount += 1;
1328
1337
  resolvedCount += result.status === "resolved" ? 1 : 0;
1338
+ remoteResolvedCount += result.status === "resolved" && result.callType !== "local_service_call" ? 1 : 0;
1339
+ localResolvedCount += result.status === "resolved" && result.callType === "local_service_call" ? 1 : 0;
1329
1340
  unresolvedCount += result.status === "unresolved" ? 1 : 0;
1330
1341
  ambiguousCount += result.status === "ambiguous" ? 1 : 0;
1331
1342
  dynamicCount += result.status === "dynamic" ? 1 : 0;
1332
1343
  terminalCount += result.status === "terminal" ? 1 : 0;
1333
1344
  }
1334
- return { edgeCount, unresolvedCount, resolvedCount, ambiguousCount, dynamicCount, terminalCount };
1345
+ return { edgeCount, unresolvedCount, resolvedCount, remoteResolvedCount, localResolvedCount, ambiguousCount, dynamicCount, terminalCount };
1335
1346
  }
1336
1347
  function insertCallEdge(db, workspaceId, call, vars, generation) {
1337
1348
  const callType = String(call.call_type);
@@ -1344,11 +1355,11 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
1344
1355
  const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0);
1345
1356
  if (callType === "local_service_call" && call.unresolved_reason === "transport_client_method" && !resolution.target && resolution.candidates.length === 0) {
1346
1357
  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);
1347
- return { status: "terminal" };
1358
+ return { status: "terminal", callType };
1348
1359
  }
1349
1360
  if (resolution.target) {
1350
1361
  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);
1351
- return { status: "resolved" };
1362
+ return { status: "resolved", callType };
1352
1363
  }
1353
1364
  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";
1354
1365
  const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
@@ -1356,7 +1367,7 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
1356
1367
  const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : "external";
1357
1368
  const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : String(call.event_name_expr ?? op ?? call.id);
1358
1369
  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);
1359
- return { status };
1370
+ return { status, callType };
1360
1371
  }
1361
1372
  function callEvidence(call, resolution, servicePath, op, destination) {
1362
1373
  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 };
@@ -1983,6 +1994,7 @@ export {
1983
1994
  parseHandlerRegistrations,
1984
1995
  redactText,
1985
1996
  redactValue,
1997
+ containsSupportedOutboundCall,
1986
1998
  parseOutboundCalls,
1987
1999
  parseServiceBindings,
1988
2000
  applyVariables,
@@ -1991,4 +2003,4 @@ export {
1991
2003
  linkWorkspace,
1992
2004
  trace
1993
2005
  };
1994
- //# sourceMappingURL=chunk-ZSUL2X77.js.map
2006
+ //# sourceMappingURL=chunk-HBR27SPD.js.map