@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 +16 -0
- package/README.md +9 -4
- package/TECHNICAL-NOTE.md +13 -0
- package/dist/{chunk-ZSUL2X77.js → chunk-HBR27SPD.js} +100 -88
- package/dist/chunk-HBR27SPD.js.map +1 -0
- package/dist/cli.js +91 -27
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZSUL2X77.js.map +0 -1
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.
|
|
21
|
+
## 0.1.17 quality update
|
|
22
22
|
|
|
23
|
-
`service-flow` 0.1.
|
|
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
|
|
612
|
-
|
|
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-
|
|
2006
|
+
//# sourceMappingURL=chunk-HBR27SPD.js.map
|