@saptools/service-flow 0.1.16 → 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 +15 -0
- package/README.md +11 -2
- package/TECHNICAL-NOTE.md +13 -0
- package/dist/{chunk-SSSBQIMK.js → chunk-DUGM7FRA.js} +129 -88
- package/dist/chunk-DUGM7FRA.js.map +1 -0
- package/dist/cli.js +53 -23
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-SSSBQIMK.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.1.17
|
|
12
|
+
|
|
13
|
+
- Replaced remaining raw-text outbound call detection with TypeScript AST classification so comments and strings do not create outbound facts or graph edges.
|
|
14
|
+
- Added AST parser/range evidence for outbound calls and conservative CAP/service event registration ownership.
|
|
15
|
+
- Tightened synthetic callback and async subscription classification to avoid generic response `.send()` and non-CAP event listener noise.
|
|
16
|
+
- Expanded strict doctor ownerless source categories and examples.
|
|
17
|
+
|
|
3
18
|
## 0.1.16
|
|
4
19
|
|
|
5
20
|
- 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,13 @@ Index independent Git repositories, persist CAP/CDS facts in SQLite, resolve cro
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
## 0.1.
|
|
21
|
+
## 0.1.18 quality update
|
|
22
22
|
|
|
23
|
-
`service-flow` 0.1.
|
|
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
|
+
|
|
25
|
+
## 0.1.17 quality update
|
|
26
|
+
|
|
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.
|
|
24
28
|
|
|
25
29
|
## ✨ Features
|
|
26
30
|
|
|
@@ -473,3 +477,8 @@ Handler registrations persist parsed class names and import sources. Linking res
|
|
|
473
477
|
### Graph variables
|
|
474
478
|
|
|
475
479
|
The `graph` command accepts repeatable `--var key=value` options, matching `trace`, for runtime substitution previews in JSON or Mermaid output.
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
### 0.1.17 parser ownership policy
|
|
483
|
+
|
|
484
|
+
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
|
@@ -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.
|
|
@@ -68,3 +76,8 @@
|
|
|
68
76
|
- Fingerprints hash normalized package facts and package bytes in addition to source file paths/content and analyzer version.
|
|
69
77
|
- The CLI version imports package metadata, so package metadata, `service-flow --version`, changelog, and analyzer/fingerprint version share one release source.
|
|
70
78
|
- Supported runtime is Node.js 24+ with `node:sqlite` validation; older runtimes should fail with a compatibility message instead of a late `DatabaseSync` error.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
### 0.1.17 parser ownership policy
|
|
82
|
+
|
|
83
|
+
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,110 @@ 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)) 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(sourceOf(expr));
|
|
615
|
+
return void 0;
|
|
616
|
+
}
|
|
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;
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
function classifyOutboundCallsInSource(source, filePath) {
|
|
636
|
+
const calls = [];
|
|
637
|
+
const sourceFile = normalizePath(filePath);
|
|
638
|
+
const initializers = variableInitializers(source);
|
|
639
|
+
const serviceVariables = collectServiceVariables(source);
|
|
640
|
+
const add = (node, fact, extra) => {
|
|
641
|
+
calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf3(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
|
|
642
|
+
};
|
|
643
|
+
const visit = (node) => {
|
|
644
|
+
if (ts4.isCallExpression(node)) {
|
|
645
|
+
const expr = node.expression;
|
|
646
|
+
const exprText = expr.getText(source);
|
|
647
|
+
if (exprText === "cds.run") {
|
|
648
|
+
const arg = node.arguments[0];
|
|
649
|
+
const entity = arg ? queryEntityFromAst(arg, initializers) : void 0;
|
|
650
|
+
const payload = arg?.getText(source) ?? "";
|
|
651
|
+
add(node, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) });
|
|
652
|
+
} else if (ts4.isPropertyAccessExpression(expr) && expr.name.text === "send" && ts4.isIdentifier(expr.expression)) {
|
|
653
|
+
const objectArg = node.arguments[0];
|
|
654
|
+
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
655
|
+
const receiver = expr.expression.text;
|
|
656
|
+
const query = objectPropertyText(objectArg, "query");
|
|
657
|
+
const op = objectPropertyText(objectArg, "path") ?? objectPropertyText(objectArg, "event");
|
|
658
|
+
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" });
|
|
659
|
+
}
|
|
660
|
+
} else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
|
|
661
|
+
const receiver = receiverName(expr.expression);
|
|
662
|
+
const rootReceiver = rootReceiverName(expr.expression);
|
|
663
|
+
if (isSupportedEventReceiver(receiver, rootReceiver, serviceVariables)) {
|
|
664
|
+
const eventName = literalText(node.arguments[0]);
|
|
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" });
|
|
666
|
+
}
|
|
667
|
+
} else if (exprText === "axios" || exprText === "executeHttpRequest" || exprText === "useOrFetchDestination") {
|
|
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" });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
ts4.forEachChild(node, visit);
|
|
672
|
+
};
|
|
673
|
+
visit(source);
|
|
674
|
+
return calls;
|
|
675
|
+
}
|
|
676
|
+
function containsSupportedOutboundCall(node) {
|
|
677
|
+
const source = node.getSourceFile();
|
|
678
|
+
const start = node.getFullStart();
|
|
679
|
+
const end = node.getEnd();
|
|
680
|
+
return classifyOutboundCallsInSource(source, source.fileName).some((call) => call.node.getStart(source) >= start && call.node.getEnd() <= end);
|
|
681
|
+
}
|
|
609
682
|
async function parseOutboundCalls(repoPath, filePath) {
|
|
610
683
|
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;
|
|
684
|
+
const source = ts4.createSourceFile(filePath, text, ts4.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts4.ScriptKind.TS : ts4.ScriptKind.JS);
|
|
685
|
+
return [...classifyOutboundCallsInSource(source, filePath).map((call) => call.fact), ...parseLocalServiceCalls(text, filePath)];
|
|
667
686
|
}
|
|
668
687
|
function parseLocalServiceCalls(text, filePath) {
|
|
669
688
|
const source = ts4.createSourceFile(filePath, text, ts4.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts4.ScriptKind.TS : ts4.ScriptKind.JS);
|
|
@@ -686,7 +705,14 @@ function parseLocalServiceCalls(text, filePath) {
|
|
|
686
705
|
sourceFile: normalizePath(filePath),
|
|
687
706
|
sourceLine: lineOf3(text, node.getStart(source)),
|
|
688
707
|
confidence: 0.9,
|
|
689
|
-
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
|
+
})
|
|
690
716
|
});
|
|
691
717
|
}
|
|
692
718
|
ts4.forEachChild(node, visit);
|
|
@@ -1351,7 +1377,7 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
1351
1377
|
return { status: "terminal", callType };
|
|
1352
1378
|
}
|
|
1353
1379
|
if (resolution.target) {
|
|
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),
|
|
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);
|
|
1355
1381
|
return { status: "resolved", callType };
|
|
1356
1382
|
}
|
|
1357
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";
|
|
@@ -1359,11 +1385,25 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
1359
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"));
|
|
1360
1386
|
const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : "external";
|
|
1361
1387
|
const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : String(call.event_name_expr ?? op ?? call.id);
|
|
1362
|
-
|
|
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);
|
|
1363
1390
|
return { status, callType };
|
|
1364
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
|
+
}
|
|
1365
1404
|
function callEvidence(call, resolution, servicePath, op, destination) {
|
|
1366
|
-
|
|
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 };
|
|
1367
1407
|
}
|
|
1368
1408
|
function linkImplementations(db, workspaceId, generation) {
|
|
1369
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);
|
|
@@ -1987,6 +2027,7 @@ export {
|
|
|
1987
2027
|
parseHandlerRegistrations,
|
|
1988
2028
|
redactText,
|
|
1989
2029
|
redactValue,
|
|
2030
|
+
containsSupportedOutboundCall,
|
|
1990
2031
|
parseOutboundCalls,
|
|
1991
2032
|
parseServiceBindings,
|
|
1992
2033
|
applyVariables,
|
|
@@ -1995,4 +2036,4 @@ export {
|
|
|
1995
2036
|
linkWorkspace,
|
|
1996
2037
|
trace
|
|
1997
2038
|
};
|
|
1998
|
-
//# sourceMappingURL=chunk-
|
|
2039
|
+
//# sourceMappingURL=chunk-DUGM7FRA.js.map
|