@saptools/service-flow 0.1.16 → 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,12 @@
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
+
3
10
  ## 0.1.16
4
11
 
5
12
  - Index class property arrow/function members as executable symbols so outbound calls inside handler helper properties receive precise source-symbol ownership.
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.16 quality update
21
+ ## 0.1.17 quality update
22
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.
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
 
@@ -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
@@ -68,3 +68,8 @@
68
68
  - Fingerprints hash normalized package facts and package bytes in addition to source file paths/content and analyzer version.
69
69
  - The CLI version imports package metadata, so package metadata, `service-flow --version`, changelog, and analyzer/fingerprint version share one release source.
70
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);
@@ -1987,6 +1994,7 @@ export {
1987
1994
  parseHandlerRegistrations,
1988
1995
  redactText,
1989
1996
  redactValue,
1997
+ containsSupportedOutboundCall,
1990
1998
  parseOutboundCalls,
1991
1999
  parseServiceBindings,
1992
2000
  applyVariables,
@@ -1995,4 +2003,4 @@ export {
1995
2003
  linkWorkspace,
1996
2004
  trace
1997
2005
  };
1998
- //# sourceMappingURL=chunk-SSSBQIMK.js.map
2006
+ //# sourceMappingURL=chunk-HBR27SPD.js.map