@saptools/service-flow 0.1.17 → 0.1.18

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.18
4
+
5
+ - Applied conservative CAP receiver eligibility to `.emit()` and `.publish()` so generic realtime, socket, DOM, and EventEmitter-style calls no longer become CAP async event facts without explicit CAP messaging/service evidence.
6
+ - Added structured parser evidence for local CAP service calls, including source offsets, service lookup/name, operation, and alias chain.
7
+ - Propagated outbound parser evidence into call-derived graph and JSON trace evidence under `outboundEvidence` for remote, local, DB, external, and async edges.
8
+ - Clarified graph-level dynamic flags so terminal DB, external, and event edges stay static while dynamic binding details remain in edge evidence.
9
+ - Expanded `doctor --strict` with outbound evidence, graph evidence propagation, event receiver classification, and dynamic terminal-edge consistency aggregates.
10
+
3
11
  ## 0.1.17
4
12
 
5
13
  - Replaced remaining raw-text outbound call detection with TypeScript AST classification so comments and strings do not create outbound facts or graph edges.
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.18 quality update
22
+
23
+ `service-flow` 0.1.18 hardens auditability after the 0.1.17 audit. Event `.emit()` and `.publish()` parsing now requires conservative CAP receiver evidence, local CAP service calls persist structured parser evidence, call-derived graph and JSON trace evidence include nested outbound parser evidence, terminal DB/external/event edges are not marked graph-dynamic, and `doctor --strict` reports outbound evidence, graph evidence propagation, event receiver classification, and dynamic flag consistency.
24
+
21
25
  ## 0.1.17 quality update
22
26
 
23
27
  `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.
package/TECHNICAL-NOTE.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Service Flow Resolution Notes
2
2
 
3
+ ## 0.1.18 auditability notes
4
+
5
+ - CAP async event parsing treats `.emit()`, `.publish()`, and `.on()` consistently: a row is indexed only when the direct or chained root receiver has explicit CAP service or messaging evidence such as `cds` or a variable initialized from `cds.connect.to(...)`. Generic realtime/socket/EventEmitter receivers are ignored for CAP graph purposes by default.
6
+ - Local CAP service calls now carry TypeScript AST evidence with classifier, source offsets, service lookup/name, operation, and alias chain.
7
+ - Call-derived graph evidence nests persisted outbound parser evidence as `outboundEvidence`, allowing JSON trace output to explain parser classification without colliding with linker fields.
8
+ - `graph_edges.is_dynamic` means the edge itself requires runtime operation-target resolution. Terminal database, external HTTP, and async event edges keep `is_dynamic=0`; dynamic binding provenance remains in `evidence_json.bindingHasDynamicExpression`.
9
+ - Strict doctor adds aggregate checks for outbound evidence JSON quality, graph evidence propagation, event receiver classification, and dynamic terminal-edge consistency.
10
+
3
11
  - 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
12
  - Candidate ranking: operation-path matches start as weak candidates. A resolved operation edge requires a strong signal such as exact service path, CDS alias/destination context, or explicit dynamic variable overrides. Otherwise candidates are preserved in edge evidence as ambiguous or unresolved.
5
13
  - Edge states: `REMOTE_CALL_RESOLVES_TO_OPERATION` is used only above the resolution threshold; `DYNAMIC_EDGE_CANDIDATE` preserves runtime-dependent service paths/destinations; `UNRESOLVED_EDGE` carries candidate counts and reasons when static evidence is insufficient.
@@ -602,7 +602,7 @@ function collectServiceVariables(source) {
602
602
  const visit = (node) => {
603
603
  if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer) {
604
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);
605
+ if (/cds\.connect\.(to|messaging)\s*\(/.test(text)) vars.add(node.name.text);
606
606
  }
607
607
  ts4.forEachChild(node, visit);
608
608
  };
@@ -611,14 +611,25 @@ function collectServiceVariables(source) {
611
611
  }
612
612
  function receiverName(expr) {
613
613
  if (ts4.isIdentifier(expr)) return expr.text;
614
- if (ts4.isPropertyAccessExpression(expr)) return expr.getText();
614
+ if (ts4.isPropertyAccessExpression(expr)) return expr.getText(sourceOf(expr));
615
615
  return void 0;
616
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;
617
+ function sourceOf(node) {
618
+ return node.getSourceFile();
619
+ }
620
+ function rootReceiverName(expr) {
621
+ if (ts4.isIdentifier(expr)) return expr.text;
622
+ if (ts4.isPropertyAccessExpression(expr)) return rootReceiverName(expr.expression);
623
+ if (ts4.isCallExpression(expr)) return rootReceiverName(expr.expression);
624
+ return void 0;
625
+ }
626
+ function isSupportedEventReceiver(receiver, rootReceiver, serviceVariables) {
627
+ const candidate = rootReceiver ?? receiver;
628
+ if (!candidate) return false;
629
+ if (candidate === "cds") return true;
630
+ if (serviceVariables.has(candidate)) return true;
631
+ if (receiver && serviceVariables.has(receiver)) return true;
632
+ if (/^(srv|service|serviceClient|messaging|messageClient|eventClient)$/.test(candidate)) return true;
622
633
  return false;
623
634
  }
624
635
  function classifyOutboundCallsInSource(source, filePath) {
@@ -648,9 +659,10 @@ function classifyOutboundCallsInSource(source, filePath) {
648
659
  }
649
660
  } else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
650
661
  const receiver = receiverName(expr.expression);
651
- if (expr.name.text !== "on" || isSupportedEventReceiver(receiver, serviceVariables)) {
662
+ const rootReceiver = rootReceiverName(expr.expression);
663
+ if (isSupportedEventReceiver(receiver, rootReceiver, serviceVariables)) {
652
664
  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" });
665
+ if (eventName) add(node, { callType: expr.name.text === "on" ? "async_subscribe" : "async_emit", serviceVariableName: rootReceiver ?? receiver, eventNameExpr: eventName }, { receiver, rootReceiver, classifier: expr.name.text === "on" ? "cap_service_event_subscription" : "cap_service_event_emit", receiverClassification: "cap_evidence" });
654
666
  }
655
667
  } else if (exprText === "axios" || exprText === "executeHttpRequest" || exprText === "useOrFetchDestination") {
656
668
  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" });
@@ -693,7 +705,14 @@ function parseLocalServiceCalls(text, filePath) {
693
705
  sourceFile: normalizePath(filePath),
694
706
  sourceLine: lineOf3(text, node.getStart(source)),
695
707
  confidence: 0.9,
696
- unresolvedReason: ["send", "emit", "publish", "on"].includes(parsed.operation) ? "transport_client_method" : void 0
708
+ unresolvedReason: ["send", "emit", "publish", "on"].includes(parsed.operation) ? "transport_client_method" : void 0,
709
+ evidence: parserEvidence(source, node, {
710
+ classifier: "local_cap_service_call",
711
+ localServiceLookup: parsed.lookup,
712
+ localServiceName: parsed.service,
713
+ operation: parsed.operation,
714
+ aliasChain: parsed.chain
715
+ })
697
716
  });
698
717
  }
699
718
  ts4.forEachChild(node, visit);
@@ -1358,7 +1377,7 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
1358
1377
  return { status: "terminal", callType };
1359
1378
  }
1360
1379
  if (resolution.target) {
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);
1380
+ 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), 0, generation);
1362
1381
  return { status: "resolved", callType };
1363
1382
  }
1364
1383
  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";
@@ -1366,11 +1385,25 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
1366
1385
  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"));
1367
1386
  const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : "external";
1368
1387
  const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : String(call.event_name_expr ?? op ?? call.id);
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);
1388
+ const graphLevelDynamic = edgeType === "DYNAMIC_EDGE_CANDIDATE" && resolution.status === "dynamic";
1389
+ 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), graphLevelDynamic ? 1 : 0, unresolvedReason, generation);
1370
1390
  return { status, callType };
1371
1391
  }
1392
+ function parseJson(value) {
1393
+ if (!value) return void 0;
1394
+ try {
1395
+ return JSON.parse(String(value));
1396
+ } catch {
1397
+ return void 0;
1398
+ }
1399
+ }
1400
+ function objectJson(value) {
1401
+ const parsed = parseJson(value);
1402
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
1403
+ }
1372
1404
  function callEvidence(call, resolution, servicePath, op, destination) {
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 };
1405
+ const bindingHasDynamicExpression = Boolean(Number(call.isDynamic ?? 0));
1406
+ 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: parseJson(call.alias_chain_json), transport: call.call_type === "local_service_call" ? "local" : void 0, targetRepo: resolution.target?.repoName, targetOperation: resolution.target?.operationName, helperChain: parseJson(call.helperChainJson), candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, bindingHasDynamicExpression: bindingHasDynamicExpression || void 0, outboundEvidence: objectJson(call.evidence_json), analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
1374
1407
  }
1375
1408
  function linkImplementations(db, workspaceId, generation) {
1376
1409
  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);
@@ -2003,4 +2036,4 @@ export {
2003
2036
  linkWorkspace,
2004
2037
  trace
2005
2038
  };
2006
- //# sourceMappingURL=chunk-HBR27SPD.js.map
2039
+ //# sourceMappingURL=chunk-DUGM7FRA.js.map