@saptools/service-flow 0.1.29 → 0.1.31
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 +14 -0
- package/README.md +9 -1
- package/TECHNICAL-NOTE.md +14 -0
- package/dist/{chunk-GOE6ZK6I.js → chunk-AM2JZFQ6.js} +306 -53
- package/dist/chunk-AM2JZFQ6.js.map +1 -0
- package/dist/cli.js +29 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-GOE6ZK6I.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.31
|
|
4
|
+
|
|
5
|
+
- Resolve operation and path trace selectors from indexed CDS operations and persisted implementation edges before conservative handler fallback, including generated `Action<Name>` and `Func<Name>` decorator constants whose method names differ from public operation names.
|
|
6
|
+
- Emit the selected start operation and initial implementation hop once, with structured start-resolution evidence and ambiguity/not-found diagnostics that point to the operation, implementation edge, or handler-scope stage.
|
|
7
|
+
- Replace numeric external HTTP terminal targets with semantic external destination and endpoint nodes, preserving redacted structured target evidence for destinations, static URLs, dynamic URL expressions, and unknown calls.
|
|
8
|
+
- Add schema version 6 migration columns for queryable external target metadata and a strict doctor aggregate for external HTTP target quality.
|
|
9
|
+
|
|
10
|
+
## 0.1.30
|
|
11
|
+
|
|
12
|
+
- Normalize balanced OData operation invocations when multiline template placeholders appear inside function/action argument lists.
|
|
13
|
+
- Preserve invocation argument placeholders as non-routing evidence instead of treating them as missing operation-target runtime variables.
|
|
14
|
+
- Reuse the shared OData invocation normalizer during contextual trace resolution so persisted links and trace-time helper propagation handle the same path shapes.
|
|
15
|
+
- Keep GET entity key reads, navigation reads, and collection queries terminal unless strong indexed operation evidence resolves them.
|
|
16
|
+
|
|
3
17
|
## 0.1.29
|
|
4
18
|
|
|
5
19
|
- Classify GET OData entity/query paths with query strings, filter functions, key predicates, navigation reads, and query placeholders as terminal remote query/entity edges when no strong indexed CDS operation candidate resolves them.
|
package/README.md
CHANGED
|
@@ -219,7 +219,15 @@ service-flow list calls --workspace /path/to/workspace --repo facade-service --o
|
|
|
219
219
|
| `list operations` | Print indexed actions/functions/events, optionally filtered by repo and service |
|
|
220
220
|
| `list calls` | Print indexed outbound calls, optionally filtered by repo and operation/path |
|
|
221
221
|
|
|
222
|
-
###
|
|
222
|
+
### Operation selector resolution
|
|
223
|
+
|
|
224
|
+
Operation-based trace starts first resolve indexed CDS operations, then follow the persisted `OPERATION_IMPLEMENTED_BY_HANDLER` graph edge to the exact handler method symbol. Generated decorator constants such as `ActionPublishRecord.name` and `FuncLookupRecord.name` are normalized through the same shared helper used by linking and diagnostics, so public operations like `/publishRecord` can trace into methods such as `executePublish`. If the same operation name exists in multiple services or repositories, `service-flow` returns `trace_start_ambiguous`; add `--service /CatalogService` or `--repo catalog-api` to disambiguate. Unique operation selectors emit the initial operation node and implementation hop exactly once before traversing handler-owned calls.
|
|
225
|
+
|
|
226
|
+
### External HTTP targets
|
|
227
|
+
|
|
228
|
+
External HTTP facts use semantic terminal nodes instead of outbound-call row ids. Literal destinations render as `External destination: ANALYTICS_API`; static absolute or relative URLs render as redacted `External endpoint` labels; dynamic URL expressions render as `External endpoint: dynamic URL`; unavailable target evidence renders as `External endpoint: unknown`. URL user information, query-string values, credentials, tokens, cookies, headers, and payload bodies are not stored in labels. Run `service-flow link` after schema migration so legacy numeric targets are rebuilt from the current parser evidence. `service-flow doctor --strict` reports `strict_external_http_target_quality` with semantic, dynamic, unknown, numeric, and malformed-evidence counts.
|
|
229
|
+
|
|
230
|
+
## Troubleshooting resolution accuracy
|
|
223
231
|
|
|
224
232
|
- If a remote edge is unresolved, run `service-flow list calls --operation <name>`
|
|
225
233
|
and `service-flow inspect operation <name>` to compare the captured call path
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Service Flow Resolution Notes
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
## 0.1.31 selector and external target notes
|
|
5
|
+
|
|
6
|
+
Operation trace selectors are resolved from CDS operation facts before handler-source fallback. The start resolver scopes by repository and service path, requires disambiguation when multiple repositories or services match, and only queues traversal from a resolved implementation edge. Generated operation decorator constants are normalized in `operation-decorator-normalizer` so linker, trace fallback, diagnostics, and tests share one conservative implementation.
|
|
7
|
+
|
|
8
|
+
Schema version 6 adds queryable external target metadata columns to `outbound_calls`. The linker writes semantic `external_destination` or `external_endpoint` graph targets and keeps redacted structured target details in edge evidence. Existing databases migrate forward without dropping facts; relinking rebuilds legacy numeric external HTTP targets.
|
|
9
|
+
|
|
10
|
+
## 0.1.30 OData invocation argument placeholder notes
|
|
11
|
+
|
|
12
|
+
- Balanced top-level OData function/action imports such as `/readDetails(ID='${id}',version=0)` now normalize to the operation segment even when placeholder expressions span multiple lines or contain nested JavaScript parentheses.
|
|
13
|
+
- Placeholders inside the invocation argument list are recorded as `invocationArgumentPlaceholderKeys` evidence. They are not route selectors and do not produce `missing_variable:*` operation-target diagnostics after a stable operation segment has been identified.
|
|
14
|
+
- Runtime `--var` substitution still applies to actual route selectors: service path, destination, alias, and dynamic operation path segments. GET entity key reads, navigation reads, and query-string placeholders remain conservative terminal remote query/entity evidence unless strong indexed operation evidence resolves them.
|
|
15
|
+
- Trace-time contextual service-client resolution uses the same shared OData invocation normalizer as persisted linking, keeping helper-propagated calls consistent with full graph linking.
|
|
16
|
+
|
|
3
17
|
## 0.1.29 OData entity-query intent notes
|
|
4
18
|
|
|
5
19
|
- Service-client `GET` paths with OData collection queries, filter/search functions in the query string, entity key predicates, navigation reads, or query-string placeholders are classified conservatively as terminal remote entity/query reads when no strong indexed CDS operation candidate resolves the path. Examples include `/Books?$filter=contains(title,'A')`, `/Books(ID='1000')`, and `/Authors('A1')/books?$select=ID`.
|
|
@@ -526,16 +526,25 @@ function normalizeODataOperationInvocationPath(path9) {
|
|
|
526
526
|
if (path9 === void 0) return void 0;
|
|
527
527
|
const raw = path9.trim();
|
|
528
528
|
if (!raw) return void 0;
|
|
529
|
+
const rejected = (reason) => ({ rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false, invocationArgumentPlaceholderKeys: [], normalizationRejectedReason: reason });
|
|
529
530
|
const open = raw.indexOf("(");
|
|
530
|
-
if (open < 0) return
|
|
531
|
-
const query = raw
|
|
532
|
-
if (query >= 0) return
|
|
533
|
-
if (!raw.startsWith("/")
|
|
531
|
+
if (open < 0) return rejected("no_top_level_parenthesis");
|
|
532
|
+
const query = topLevelQueryIndex(raw);
|
|
533
|
+
if (query >= 0) return rejected("query_string_paths_are_not_operation_invocations");
|
|
534
|
+
if (!raw.startsWith("/")) return rejected("path_is_not_absolute");
|
|
535
|
+
if (raw.slice(1, open).includes("/")) return rejected("operation_segment_contains_navigation_separator");
|
|
534
536
|
const close = matchingClose(raw, open);
|
|
535
|
-
if (close === void 0
|
|
537
|
+
if (close === void 0) return rejected("top_level_invocation_parenthesis_is_unbalanced");
|
|
538
|
+
if (raw.slice(close + 1).trim().length > 0) return rejected("top_level_invocation_does_not_cover_remaining_path");
|
|
536
539
|
const operationSegment = raw.slice(0, open).trim();
|
|
537
|
-
if (operationSegment.length <= 1) return
|
|
538
|
-
return {
|
|
540
|
+
if (operationSegment.length <= 1) return rejected("operation_segment_is_empty");
|
|
541
|
+
return {
|
|
542
|
+
rawOperationPath: raw,
|
|
543
|
+
normalizedOperationPath: operationSegment,
|
|
544
|
+
wasInvocation: true,
|
|
545
|
+
invocationArgumentPlaceholderKeys: [...new Set(extractTemplatePlaceholders(raw.slice(open + 1, close)))],
|
|
546
|
+
normalizationReason: "balanced_top_level_operation_invocation"
|
|
547
|
+
};
|
|
539
548
|
}
|
|
540
549
|
function classifyODataPathIntent(path9, method) {
|
|
541
550
|
const rawPath = (path9 ?? "").trim();
|
|
@@ -576,7 +585,16 @@ function looksLikeLowerCamelInvocation(segment) {
|
|
|
576
585
|
return /^[a-z][A-Za-z0-9_]*$/.test(name);
|
|
577
586
|
}
|
|
578
587
|
function extractTemplatePlaceholders(text) {
|
|
579
|
-
|
|
588
|
+
const keys = [];
|
|
589
|
+
for (let index = 0; index < text.length - 1; index += 1) {
|
|
590
|
+
if (text[index] !== "$" || text[index + 1] !== "{") continue;
|
|
591
|
+
const close = matchingPlaceholderClose(text, index + 1);
|
|
592
|
+
if (close === void 0) continue;
|
|
593
|
+
const key = text.slice(index + 2, close).trim();
|
|
594
|
+
if (key) keys.push(key);
|
|
595
|
+
index = close;
|
|
596
|
+
}
|
|
597
|
+
return keys;
|
|
580
598
|
}
|
|
581
599
|
function matchingClose(text, openIndex) {
|
|
582
600
|
let depth = 0;
|
|
@@ -586,9 +604,21 @@ function matchingClose(text, openIndex) {
|
|
|
586
604
|
const prev = text[index - 1];
|
|
587
605
|
if (quote) {
|
|
588
606
|
if (prev === "\\") continue;
|
|
607
|
+
if (quote === "template" && char === "$" && text[index + 1] === "{") {
|
|
608
|
+
const close = matchingPlaceholderClose(text, index + 1);
|
|
609
|
+
if (close === void 0) return void 0;
|
|
610
|
+
index = close;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
589
613
|
if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
|
|
590
614
|
continue;
|
|
591
615
|
}
|
|
616
|
+
if (char === "$" && text[index + 1] === "{") {
|
|
617
|
+
const close = matchingPlaceholderClose(text, index + 1);
|
|
618
|
+
if (close === void 0) return void 0;
|
|
619
|
+
index = close;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
592
622
|
if (char === "'") {
|
|
593
623
|
quote = "single";
|
|
594
624
|
continue;
|
|
@@ -610,6 +640,81 @@ function matchingClose(text, openIndex) {
|
|
|
610
640
|
}
|
|
611
641
|
return void 0;
|
|
612
642
|
}
|
|
643
|
+
function matchingPlaceholderClose(text, openBraceIndex) {
|
|
644
|
+
let depth = 0;
|
|
645
|
+
let quote;
|
|
646
|
+
for (let index = openBraceIndex; index < text.length; index += 1) {
|
|
647
|
+
const char = text[index];
|
|
648
|
+
const prev = text[index - 1];
|
|
649
|
+
if (quote) {
|
|
650
|
+
if (prev === "\\") continue;
|
|
651
|
+
if (quote === "template" && char === "$" && text[index + 1] === "{") {
|
|
652
|
+
depth += 1;
|
|
653
|
+
index += 1;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (char === "'") {
|
|
660
|
+
quote = "single";
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (char === '"') {
|
|
664
|
+
quote = "double";
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
if (char === "`") {
|
|
668
|
+
quote = "template";
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (char === "{") depth += 1;
|
|
672
|
+
if (char === "}") {
|
|
673
|
+
depth -= 1;
|
|
674
|
+
if (depth === 0) return index;
|
|
675
|
+
if (depth < 0) return void 0;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return void 0;
|
|
679
|
+
}
|
|
680
|
+
function topLevelQueryIndex(text) {
|
|
681
|
+
let quote;
|
|
682
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
683
|
+
const char = text[index];
|
|
684
|
+
const prev = text[index - 1];
|
|
685
|
+
if (quote) {
|
|
686
|
+
if (prev === "\\") continue;
|
|
687
|
+
if (quote === "template" && char === "$" && text[index + 1] === "{") {
|
|
688
|
+
const close = matchingPlaceholderClose(text, index + 1);
|
|
689
|
+
if (close === void 0) return -1;
|
|
690
|
+
index = close;
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (char === "$" && text[index + 1] === "{") {
|
|
697
|
+
const close = matchingPlaceholderClose(text, index + 1);
|
|
698
|
+
if (close === void 0) return -1;
|
|
699
|
+
index = close;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (char === "'") {
|
|
703
|
+
quote = "single";
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
if (char === '"') {
|
|
707
|
+
quote = "double";
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (char === "`") {
|
|
711
|
+
quote = "template";
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (char === "?") return index;
|
|
715
|
+
}
|
|
716
|
+
return -1;
|
|
717
|
+
}
|
|
613
718
|
|
|
614
719
|
// src/parsers/outbound-call-parser.ts
|
|
615
720
|
import fs6 from "fs/promises";
|
|
@@ -693,6 +798,70 @@ function nameOfProperty(name) {
|
|
|
693
798
|
if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name)) return name.text;
|
|
694
799
|
return void 0;
|
|
695
800
|
}
|
|
801
|
+
function staticExpressionText(expr, initializers) {
|
|
802
|
+
if (!expr) return void 0;
|
|
803
|
+
if (isStringLike(expr)) return expr.text;
|
|
804
|
+
if (ts4.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
|
|
805
|
+
return void 0;
|
|
806
|
+
}
|
|
807
|
+
function propertyInitializer(object, key) {
|
|
808
|
+
for (const property of object.properties) {
|
|
809
|
+
if (ts4.isPropertyAssignment(property) && nameOfProperty(property.name) === key) return property.initializer;
|
|
810
|
+
if (ts4.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
|
|
811
|
+
}
|
|
812
|
+
return void 0;
|
|
813
|
+
}
|
|
814
|
+
function httpMethodFromObject(object, initializers) {
|
|
815
|
+
const text = staticExpressionText(propertyInitializer(object, "method"), initializers);
|
|
816
|
+
return text ? stripQuotes(text).toUpperCase() : void 0;
|
|
817
|
+
}
|
|
818
|
+
function urlTargetFromExpression(expr, initializers) {
|
|
819
|
+
const text = staticExpressionText(expr, initializers);
|
|
820
|
+
if (text) return { kind: "static_url", expression: text, dynamic: false };
|
|
821
|
+
if (expr && (ts4.isTemplateExpression(expr) || ts4.isIdentifier(expr) || ts4.isPropertyAccessExpression(expr) || ts4.isCallExpression(expr))) return { kind: "url_expression", expression: expr.getText(expr.getSourceFile()), dynamic: true };
|
|
822
|
+
return { kind: "unknown", dynamic: false };
|
|
823
|
+
}
|
|
824
|
+
function destinationTargetFromExpression(expr, initializers) {
|
|
825
|
+
const text = staticExpressionText(expr, initializers);
|
|
826
|
+
if (text) return { kind: "destination", expression: text, dynamic: false };
|
|
827
|
+
if (expr && ts4.isIdentifier(expr)) return { kind: "destination", expression: expr.text, dynamic: true };
|
|
828
|
+
return void 0;
|
|
829
|
+
}
|
|
830
|
+
function externalHttpEvidence(node, source, initializers) {
|
|
831
|
+
const expr = node.expression;
|
|
832
|
+
const exprText = expr.getText(source);
|
|
833
|
+
if (exprText === "useOrFetchDestination") {
|
|
834
|
+
const objectArg = node.arguments[0];
|
|
835
|
+
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
836
|
+
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"), initializers);
|
|
837
|
+
return { externalTarget: destination ?? { kind: "unknown", dynamic: false }, classifier: "sap_destination_lookup", sourceCallShape: "useOrFetchDestination" };
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (exprText === "executeHttpRequest") {
|
|
841
|
+
const destination = destinationTargetFromExpression(node.arguments[0], initializers);
|
|
842
|
+
const config = node.arguments[1];
|
|
843
|
+
const method = config && ts4.isObjectLiteralExpression(config) ? httpMethodFromObject(config, initializers) : void 0;
|
|
844
|
+
const url = config && ts4.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, "url"), initializers) : { kind: "unknown", dynamic: false };
|
|
845
|
+
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: "sap_execute_http_request", sourceCallShape: "executeHttpRequest" };
|
|
846
|
+
}
|
|
847
|
+
if (exprText === "axios") {
|
|
848
|
+
const config = node.arguments[0];
|
|
849
|
+
if (config && ts4.isObjectLiteralExpression(config)) {
|
|
850
|
+
const method = httpMethodFromObject(config, initializers);
|
|
851
|
+
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, "url"), initializers), classifier: "axios_config_call", sourceCallShape: "axios(config)" };
|
|
852
|
+
}
|
|
853
|
+
return { externalTarget: { kind: "unknown", dynamic: false }, classifier: "axios_unknown_call", sourceCallShape: "axios(...)" };
|
|
854
|
+
}
|
|
855
|
+
if (exprText === "fetch") {
|
|
856
|
+
const init = node.arguments[1];
|
|
857
|
+
const method = init && ts4.isObjectLiteralExpression(init) ? httpMethodFromObject(init, initializers) : void 0;
|
|
858
|
+
return { method, externalTarget: urlTargetFromExpression(node.arguments[0], initializers), classifier: "fetch_call", sourceCallShape: "fetch" };
|
|
859
|
+
}
|
|
860
|
+
if (ts4.isPropertyAccessExpression(expr) && ["get", "post", "put", "patch", "delete", "head"].includes(expr.name.text) && expr.expression.getText(source) === "axios") {
|
|
861
|
+
return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], initializers), classifier: "axios_member_call", sourceCallShape: `axios.${expr.name.text}` };
|
|
862
|
+
}
|
|
863
|
+
return void 0;
|
|
864
|
+
}
|
|
696
865
|
function collectServiceVariables(source) {
|
|
697
866
|
const vars = /* @__PURE__ */ new Set(["cds", "messaging", "messageClient", "eventClient"]);
|
|
698
867
|
const visit = (node) => {
|
|
@@ -825,8 +994,9 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
825
994
|
const eventName = literalText(node.arguments[0]);
|
|
826
995
|
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" });
|
|
827
996
|
}
|
|
828
|
-
} else
|
|
829
|
-
|
|
997
|
+
} else {
|
|
998
|
+
const external = externalHttpEvidence(node, source, initializers);
|
|
999
|
+
if (external) add(node, { callType: "external_http", method: external.method, payloadSummary: summarizeExpression(node.arguments.map((arg) => arg.getText(source)).join(", ")), confidence: 0.7, unresolvedReason: "External HTTP destination is outside indexed CAP services" }, { classifier: external.classifier, externalTarget: { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape }, sourceCallShape: external.sourceCallShape });
|
|
830
1000
|
}
|
|
831
1001
|
}
|
|
832
1002
|
ts4.forEachChild(node, visit);
|
|
@@ -1756,6 +1926,80 @@ function linkHelperPackages(db, workspaceId, generation) {
|
|
|
1756
1926
|
return summary;
|
|
1757
1927
|
}
|
|
1758
1928
|
|
|
1929
|
+
// src/linker/operation-decorator-normalizer.ts
|
|
1930
|
+
function lowerFirst(value) {
|
|
1931
|
+
return value ? `${value[0]?.toLowerCase() ?? ""}${value.slice(1)}` : value;
|
|
1932
|
+
}
|
|
1933
|
+
function normalizedOperationName(value) {
|
|
1934
|
+
return value.replace(/^\//, "");
|
|
1935
|
+
}
|
|
1936
|
+
function clean(value) {
|
|
1937
|
+
return value.replace(/^[`'"]|[`'"]$/g, "");
|
|
1938
|
+
}
|
|
1939
|
+
function generatedFromConstantName(value) {
|
|
1940
|
+
for (const prefix of ["Action", "Func"]) {
|
|
1941
|
+
if (value.startsWith(prefix) && value.length > prefix.length && /^[A-Z]/.test(value.slice(prefix.length))) return lowerFirst(value.slice(prefix.length));
|
|
1942
|
+
}
|
|
1943
|
+
return void 0;
|
|
1944
|
+
}
|
|
1945
|
+
function normalizeDecoratorOperation(value, raw) {
|
|
1946
|
+
if (value) {
|
|
1947
|
+
const literal = clean(value);
|
|
1948
|
+
const generated = generatedFromConstantName(literal);
|
|
1949
|
+
return generated ?? normalizedOperationName(literal);
|
|
1950
|
+
}
|
|
1951
|
+
if (!raw) return void 0;
|
|
1952
|
+
const expression = raw.trim();
|
|
1953
|
+
const nameMatch = /(?:^|\.)(Action[A-Z][\w$]*|Func[A-Z][\w$]*)\.name$/.exec(expression);
|
|
1954
|
+
if (nameMatch?.[1]) return generatedFromConstantName(nameMatch[1]);
|
|
1955
|
+
const tail = expression.split(".").filter(Boolean).at(-1);
|
|
1956
|
+
return tail ? normalizedOperationName(clean(tail)) : void 0;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// src/linker/external-http-target.ts
|
|
1960
|
+
import { createHash } from "crypto";
|
|
1961
|
+
var sensitiveKeys = /* @__PURE__ */ new Set(["token", "access_token", "id_token", "api_key", "apikey", "key", "password", "passwd", "pwd", "secret", "client_secret", "authorization", "cookie", "signature"]);
|
|
1962
|
+
function hash(value) {
|
|
1963
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
1964
|
+
}
|
|
1965
|
+
function methodPrefix(method) {
|
|
1966
|
+
return typeof method === "string" && method.length > 0 ? `${method.toUpperCase()} ` : "";
|
|
1967
|
+
}
|
|
1968
|
+
function redactUrl(value) {
|
|
1969
|
+
try {
|
|
1970
|
+
const url = new URL(value, value.startsWith("/") ? "https://relative.invalid" : void 0);
|
|
1971
|
+
url.username = "";
|
|
1972
|
+
url.password = "";
|
|
1973
|
+
for (const key of [...url.searchParams.keys()]) url.searchParams.set(key, sensitiveKeys.has(key.toLowerCase()) ? "<redacted>" : "<redacted>");
|
|
1974
|
+
const path9 = `${url.pathname}${url.search ? url.search : ""}`;
|
|
1975
|
+
return value.startsWith("/") ? path9 : `${url.origin}${path9}`;
|
|
1976
|
+
} catch {
|
|
1977
|
+
return value.replace(/([?&][^=;&]*(?:token|key|password|secret|cookie|authorization)[^=;&]*=)[^&]*/gi, "$1<redacted>");
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
function externalHttpTarget(call) {
|
|
1981
|
+
const evidence = typeof call.evidence_json === "string" ? safeParse(call.evidence_json) : {};
|
|
1982
|
+
const target = evidence.externalTarget && typeof evidence.externalTarget === "object" && !Array.isArray(evidence.externalTarget) ? evidence.externalTarget : {};
|
|
1983
|
+
const method = typeof call.method === "string" ? call.method : typeof target.method === "string" ? target.method : void 0;
|
|
1984
|
+
const kind = typeof target.kind === "string" ? target.kind : "unknown";
|
|
1985
|
+
const expression = typeof target.expression === "string" ? target.expression : void 0;
|
|
1986
|
+
if (kind === "destination" && expression) return { kind, toKind: "external_destination", toId: `destination:${expression}`, label: `External destination: ${expression}`, method, dynamic: false, expression };
|
|
1987
|
+
if (kind === "static_url" && expression) {
|
|
1988
|
+
const redacted = redactUrl(expression);
|
|
1989
|
+
return { kind, toKind: "external_endpoint", toId: `endpoint:${hash(`${method ?? ""}:${redacted}`)}`, label: `External endpoint: ${methodPrefix(method)}${redacted}`, method, dynamic: false, expression: redacted };
|
|
1990
|
+
}
|
|
1991
|
+
if (kind === "url_expression" && expression) return { kind, toKind: "external_endpoint", toId: `dynamic:${hash(expression)}`, label: `External endpoint: ${methodPrefix(method)}dynamic URL`, method, dynamic: true, expression: `expr:${hash(expression)}` };
|
|
1992
|
+
return { kind: "unknown", toKind: "external_endpoint", toId: "unknown", label: "External endpoint: unknown", method, dynamic: false };
|
|
1993
|
+
}
|
|
1994
|
+
function safeParse(value) {
|
|
1995
|
+
try {
|
|
1996
|
+
const parsed = JSON.parse(value);
|
|
1997
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1998
|
+
} catch {
|
|
1999
|
+
return {};
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1759
2003
|
// src/linker/cross-repo-linker.ts
|
|
1760
2004
|
function linkWorkspace(db, workspaceId, vars = {}) {
|
|
1761
2005
|
return db.transaction(() => {
|
|
@@ -1825,10 +2069,12 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
1825
2069
|
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";
|
|
1826
2070
|
const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
|
|
1827
2071
|
const unresolvedReason = status === "terminal" ? null : String(call.unresolved_reason ?? unresolvedOperationReason(resolution));
|
|
1828
|
-
const
|
|
1829
|
-
const
|
|
2072
|
+
const externalTarget = callType === "external_http" ? externalHttpTarget(call) : void 0;
|
|
2073
|
+
const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : externalTarget?.toKind ?? "external_endpoint";
|
|
2074
|
+
const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : callType === "remote_action" ? op ? `Remote action: ${op}` : call.unresolved_reason === "dynamic_operation_path_identifier" ? "Remote action: dynamic path" : "Remote action: unknown path" : callType === "external_http" ? String(externalTarget?.toId ?? "unknown") : String(call.event_name_expr ?? op ?? "unknown");
|
|
1830
2075
|
const graphLevelDynamic = edgeType === "DYNAMIC_EDGE_CANDIDATE" && resolution.status === "dynamic";
|
|
1831
|
-
|
|
2076
|
+
const finalEvidence = externalTarget ? { ...evidence, externalTarget } : evidence;
|
|
2077
|
+
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(finalEvidence), graphLevelDynamic ? 1 : 0, unresolvedReason, generation);
|
|
1832
2078
|
return { status, callType };
|
|
1833
2079
|
}
|
|
1834
2080
|
function unresolvedOperationReason(resolution) {
|
|
@@ -1853,7 +2099,7 @@ function objectJson(value) {
|
|
|
1853
2099
|
}
|
|
1854
2100
|
function callEvidence(call, resolution, servicePath, op, destination, normalized, odataPathIntent) {
|
|
1855
2101
|
const bindingHasDynamicExpression = Boolean(Number(call.isDynamic ?? 0));
|
|
1856
|
-
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, rawOperationPath: normalized?.wasInvocation ? normalized.rawOperationPath : odataPathIntent?.rawPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, 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, targetServicePath: resolution.target?.servicePath, targetOperationPath: resolution.target?.operationPath, targetOperation: resolution.target?.operationName, helperChain: parseJson(call.helperChainJson), candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, odataPathIntent, queryStringPresent: odataPathIntent?.hasQueryString || void 0, queryPlaceholderKeys: odataPathIntent?.placeholderKeys.length ? odataPathIntent.placeholderKeys : void 0, 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 };
|
|
2102
|
+
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, rawOperationPath: normalized?.wasInvocation ? normalized.rawOperationPath : odataPathIntent?.rawPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : void 0, odataOperationNormalizationReason: normalized?.normalizationReason, odataOperationNormalizationRejectedReason: normalized?.normalizationRejectedReason, 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, targetServicePath: resolution.target?.servicePath, targetOperationPath: resolution.target?.operationPath, targetOperation: resolution.target?.operationName, helperChain: parseJson(call.helperChainJson), candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, odataPathIntent, queryStringPresent: odataPathIntent?.hasQueryString || void 0, queryPlaceholderKeys: odataPathIntent?.placeholderKeys.length ? odataPathIntent.placeholderKeys : void 0, 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 };
|
|
1857
2103
|
}
|
|
1858
2104
|
function linkImplementations(db, workspaceId, generation) {
|
|
1859
2105
|
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);
|
|
@@ -2082,28 +2328,16 @@ function candidateEvidence(candidate, rank) {
|
|
|
2082
2328
|
};
|
|
2083
2329
|
}
|
|
2084
2330
|
function implementationMethodSignal(row, operation) {
|
|
2085
|
-
const op =
|
|
2331
|
+
const op = normalizedOperationName(String(operation.operationPath ?? operation.operationName ?? ""));
|
|
2086
2332
|
const decorator = normalizeDecoratorOperation(typeof row.decoratorValue === "string" ? row.decoratorValue : void 0, typeof row.decoratorRawExpression === "string" ? row.decoratorRawExpression : void 0);
|
|
2087
2333
|
if (decorator && decorator === op) return { matches: true, contradicted: false, acceptedReasons: ["decorator targets operation"], rejectedReasons: [] };
|
|
2088
2334
|
if (decorator && decorator !== op) return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ["method_name_matches_but_decorator_targets_different_operation"] };
|
|
2089
2335
|
if (String(row.methodName ?? "") === op) return { matches: true, contradicted: false, acceptedReasons: ["method name fallback matched operation"], rejectedReasons: [] };
|
|
2090
2336
|
return { matches: false, contradicted: false, acceptedReasons: [], rejectedReasons: ["method name does not match operation"] };
|
|
2091
2337
|
}
|
|
2092
|
-
function normalizeDecoratorOperation(value, raw) {
|
|
2093
|
-
const candidate = value ?? raw?.split(".").filter(Boolean).at(-2);
|
|
2094
|
-
if (!candidate) return void 0;
|
|
2095
|
-
const cleaned = candidate.replace(/^['"`]|['"`]$/g, "");
|
|
2096
|
-
for (const prefix of ["Func", "Action"]) {
|
|
2097
|
-
if (cleaned.startsWith(prefix) && cleaned.length > prefix.length) return lowerFirst(cleaned.slice(prefix.length));
|
|
2098
|
-
}
|
|
2099
|
-
return normalizedOperation(cleaned);
|
|
2100
|
-
}
|
|
2101
2338
|
function upperFirst(value) {
|
|
2102
2339
|
return value ? `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}` : value;
|
|
2103
2340
|
}
|
|
2104
|
-
function lowerFirst(value) {
|
|
2105
|
-
return value ? `${value[0]?.toLowerCase() ?? ""}${value.slice(1)}` : value;
|
|
2106
|
-
}
|
|
2107
2341
|
function flag(value) {
|
|
2108
2342
|
return Boolean(Number(value ?? 0));
|
|
2109
2343
|
}
|
|
@@ -2122,6 +2356,25 @@ function normalizeOperation(value) {
|
|
|
2122
2356
|
function positiveDepth(value) {
|
|
2123
2357
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
|
|
2124
2358
|
}
|
|
2359
|
+
function operationStartScope(db, repoId, start) {
|
|
2360
|
+
const requested = normalizeOperation(start.operationPath ?? start.operation);
|
|
2361
|
+
if (!requested) return void 0;
|
|
2362
|
+
const rows2 = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_path operationPath,s.service_path servicePath,r.id repoId,r.name repoName
|
|
2363
|
+
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
|
|
2364
|
+
WHERE (? IS NULL OR r.id=?) AND (? IS NULL OR s.service_path=?) AND (o.operation_name=? OR o.operation_path=? OR o.operation_path=?)
|
|
2365
|
+
ORDER BY r.name,s.service_path,o.operation_name,o.id`).all(repoId, repoId, start.servicePath, start.servicePath, requested, requested, requested.startsWith("/") ? requested : `/${requested}`);
|
|
2366
|
+
if (rows2.length === 0) return { diagnostics: [{ severity: "warning", code: "trace_start_not_found", message: "No indexed operation matched the requested trace selector", normalizedSelectorValue: requested }] };
|
|
2367
|
+
const repoCount = new Set(rows2.map((row) => String(row.repoName))).size;
|
|
2368
|
+
const serviceCount = new Set(rows2.map((row) => `${String(row.repoName)}:${String(row.servicePath)}`)).size;
|
|
2369
|
+
if (!repoId && repoCount > 1) return { diagnostics: [{ severity: "warning", code: "trace_start_ambiguous", message: "Operation trace start matched multiple repositories; add --repo to disambiguate", normalizedSelectorValue: requested, candidates: rows2 }] };
|
|
2370
|
+
if (!start.servicePath && serviceCount > 1) return { diagnostics: [{ severity: "warning", code: "trace_start_ambiguous", message: "Operation trace start matched multiple services; add --service to disambiguate", normalizedSelectorValue: requested, candidates: rows2 }] };
|
|
2371
|
+
if (rows2.length !== 1) return { diagnostics: [{ severity: "warning", code: "trace_start_ambiguous", message: "Operation trace start matched multiple indexed operations", normalizedSelectorValue: requested, candidates: rows2 }] };
|
|
2372
|
+
const operationId = String(rows2[0]?.operationId);
|
|
2373
|
+
const impl = implementationScope(db, operationId);
|
|
2374
|
+
if (impl.edge?.status === "resolved" && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? /* @__PURE__ */ new Set([impl.symbolId]) : void 0, operationId, diagnostics: [] };
|
|
2375
|
+
if (impl.edge) return { operationId, diagnostics: [{ severity: "warning", code: impl.edge.status === "ambiguous" ? "trace_start_ambiguous" : "trace_start_not_found", message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? "unresolved")}`, implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, candidates: parseEvidence(impl.edge.evidence_json).candidates }] };
|
|
2376
|
+
return { operationId, diagnostics: [{ severity: "warning", code: "trace_start_not_found", message: "Indexed operation matched but no implementation candidate exists" }] };
|
|
2377
|
+
}
|
|
2125
2378
|
function sourceFilesForStart(db, repoId, start) {
|
|
2126
2379
|
const handler = start.handler;
|
|
2127
2380
|
const operation = normalizeOperation(start.operation ?? start.operationPath);
|
|
@@ -2167,7 +2420,8 @@ function startScope(db, start) {
|
|
|
2167
2420
|
"SELECT id,name FROM repositories WHERE name=? OR package_name=?"
|
|
2168
2421
|
).get(start.repo, start.repo) : void 0;
|
|
2169
2422
|
if (start.repo && !repo) return { repo, selectorMatched: false };
|
|
2170
|
-
const
|
|
2423
|
+
const operationScope = operationStartScope(db, repo?.id, start);
|
|
2424
|
+
const sourceScope = operationScope?.files ? operationScope : sourceFilesForStart(db, repo?.id, start);
|
|
2171
2425
|
const sourceFiles = sourceScope?.files;
|
|
2172
2426
|
const hasSelector = Boolean(
|
|
2173
2427
|
start.handler ?? start.operation ?? start.operationPath ?? start.servicePath
|
|
@@ -2178,7 +2432,9 @@ function startScope(db, start) {
|
|
|
2178
2432
|
repo,
|
|
2179
2433
|
sourceFiles,
|
|
2180
2434
|
symbolIds: sourceScope?.symbols,
|
|
2181
|
-
selectorMatched: !hasSelector || sourceFiles !== void 0
|
|
2435
|
+
selectorMatched: !hasSelector || sourceFiles !== void 0,
|
|
2436
|
+
startOperationId: operationScope?.operationId,
|
|
2437
|
+
startDiagnostics: operationScope?.diagnostics
|
|
2182
2438
|
};
|
|
2183
2439
|
}
|
|
2184
2440
|
function handlerFilesForOperation(db, operationId) {
|
|
@@ -2402,18 +2658,16 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
|
2402
2658
|
}
|
|
2403
2659
|
function contextualRuntimeResolution(db, call, binding, workspaceId) {
|
|
2404
2660
|
if (!binding || String(call.call_type) !== "remote_action" || call.operation_path_expr === void 0 || call.operation_path_expr === null) return {};
|
|
2405
|
-
const
|
|
2661
|
+
const normalized = normalizeODataOperationInvocationPath(String(call.operation_path_expr));
|
|
2662
|
+
const op = normalized?.normalizedOperationPath ?? (String(call.operation_path_expr).startsWith("/") ? String(call.operation_path_expr) : `/${String(call.operation_path_expr)}`);
|
|
2406
2663
|
const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
|
|
2407
2664
|
const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
|
|
2408
2665
|
const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
|
|
2409
|
-
const evidence = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine, alias: binding.alias, aliasExpr: binding.aliasExpr, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination }, operationPath: op, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: resolution.candidates.length, candidates: resolution.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
|
|
2666
|
+
const evidence = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine, alias: binding.alias, aliasExpr: binding.aliasExpr, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination }, operationPath: op, rawOperationPath: normalized?.rawOperationPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : void 0, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: resolution.candidates.length, candidates: resolution.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
|
|
2410
2667
|
if (!resolution.target) return { evidence, unresolvedReason: resolution.status === "ambiguous" ? "Ambiguous contextual operation candidates" : resolution.status === "dynamic" ? `Dynamic contextual target is missing runtime variables: ${resolution.reasons.join(", ")}` : "No contextual operation candidate matched" };
|
|
2411
2668
|
const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
|
|
2412
2669
|
return { row: { id: -Number(call.id), edge_type: "REMOTE_CALL_RESOLVES_TO_OPERATION", from_id: String(call.id), to_kind: "operation", to_id: String(resolution.target.operationId), confidence: resolution.target.score, evidence_json: JSON.stringify(resolvedEvidence), status: "resolved" }, evidence: resolvedEvidence, unresolvedReason: void 0 };
|
|
2413
2670
|
}
|
|
2414
|
-
function normalizeODataOperationInvocationPathForTrace(raw) {
|
|
2415
|
-
return raw.startsWith("/") ? raw : `/${raw}`;
|
|
2416
|
-
}
|
|
2417
2671
|
function edgeTarget(row, evidence) {
|
|
2418
2672
|
const runtimeCandidate = evidence.runtimeResolvedCandidate;
|
|
2419
2673
|
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath)
|
|
@@ -2427,6 +2681,10 @@ function edgeTarget(row, evidence) {
|
|
|
2427
2681
|
const targetRepo = typeof evidence.targetRepo === "string" ? evidence.targetRepo : "";
|
|
2428
2682
|
if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return `Entity: ${row.to_id || "unknown"}`;
|
|
2429
2683
|
if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return typeof evidence.remoteQueryTarget === "string" ? evidence.remoteQueryTarget : `Remote query: ${row.to_id || "unknown"}`;
|
|
2684
|
+
if (row.edge_type === "HANDLER_CALLS_EXTERNAL_HTTP") {
|
|
2685
|
+
const target = evidence.externalTarget;
|
|
2686
|
+
return typeof target?.label === "string" ? target.label : `External endpoint: ${row.to_id || "unknown"}`;
|
|
2687
|
+
}
|
|
2430
2688
|
return servicePath && operationPath ? `${servicePath}${operationPath}` : targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
|
|
2431
2689
|
}
|
|
2432
2690
|
function trace(db, start, options) {
|
|
@@ -2437,7 +2695,8 @@ function trace(db, start, options) {
|
|
|
2437
2695
|
const stale = db.prepare("SELECT name,graph_stale_reason reason FROM repositories WHERE graph_stale_reason IS NOT NULL AND (? IS NULL OR id=?)").all(scope.repo?.id, scope.repo?.id);
|
|
2438
2696
|
for (const row of stale)
|
|
2439
2697
|
diagnostics.unshift({ severity: "warning", code: "graph_stale", message: `Graph is stale for ${row.name ?? "repository"}: ${row.reason ?? "facts_changed"}. Run service-flow link.` });
|
|
2440
|
-
|
|
2698
|
+
for (const diagnostic of scope.startDiagnostics ?? []) diagnostics.unshift(diagnostic);
|
|
2699
|
+
if (!scope.selectorMatched && !scope.startDiagnostics?.length)
|
|
2441
2700
|
diagnostics.unshift({
|
|
2442
2701
|
severity: "warning",
|
|
2443
2702
|
code: "trace_start_not_found",
|
|
@@ -2446,27 +2705,21 @@ function trace(db, start, options) {
|
|
|
2446
2705
|
const maxDepth = positiveDepth(options.depth);
|
|
2447
2706
|
const edges = [];
|
|
2448
2707
|
const nodes = /* @__PURE__ */ new Map();
|
|
2708
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
2449
2709
|
const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1, context: /* @__PURE__ */ new Map() }] : [];
|
|
2450
|
-
if (
|
|
2451
|
-
const
|
|
2452
|
-
const
|
|
2453
|
-
if (
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
const
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
if (impl.edge) {
|
|
2461
|
-
const implEvidence = JSON.parse(String(impl.edge.evidence_json || "{}"));
|
|
2462
|
-
const handlerNode = impl.edge.status === "resolved" ? handlerMethodNode(db, impl.edge.to_id) : void 0;
|
|
2463
|
-
if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
|
|
2464
|
-
edges.push({ step: 1, type: "operation_implemented_by_handler", from: op?.label ? String(op.label) : `${start.servicePath}/${startOperation ?? ""}`, to: handlerNode?.label ? String(handlerNode.label) : `${impl.edge.to_kind}:${impl.edge.to_id}`, evidence: implEvidence, confidence: Number(impl.edge.confidence ?? 0), unresolvedReason: impl.edge.status === "resolved" ? void 0 : String(impl.edge.unresolved_reason ?? impl.edge.status) });
|
|
2465
|
-
}
|
|
2710
|
+
if (scope.startOperationId) {
|
|
2711
|
+
const op = operationNode(db, scope.startOperationId);
|
|
2712
|
+
const impl = implementationScope(db, scope.startOperationId);
|
|
2713
|
+
if (op) nodes.set(String(op.id), op);
|
|
2714
|
+
if (impl.edge) {
|
|
2715
|
+
const implEvidence = { ...parseEvidence(impl.edge.evidence_json), startResolution: { strategy: "indexed_operation_graph", matchedOperationId: scope.startOperationId, implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, selectedHandlerMethodId: impl.edge.status === "resolved" ? impl.edge.to_id : void 0 } };
|
|
2716
|
+
const handlerNode = impl.edge.status === "resolved" ? handlerMethodNode(db, impl.edge.to_id) : void 0;
|
|
2717
|
+
if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
|
|
2718
|
+
seenEdges.add(Number(impl.edge.id));
|
|
2719
|
+
edges.push({ step: 1, type: "operation_implemented_by_handler", from: op?.label ? String(op.label) : `operation:${scope.startOperationId}`, to: handlerNode?.label ? String(handlerNode.label) : `${impl.edge.to_kind}:${impl.edge.to_id}`, evidence: implEvidence, confidence: Number(impl.edge.confidence ?? 0), unresolvedReason: impl.edge.status === "resolved" ? void 0 : String(impl.edge.unresolved_reason ?? impl.edge.status) });
|
|
2466
2720
|
}
|
|
2467
2721
|
}
|
|
2468
2722
|
const seenScopes = /* @__PURE__ */ new Set();
|
|
2469
|
-
const seenEdges = /* @__PURE__ */ new Set();
|
|
2470
2723
|
while (queue.length > 0) {
|
|
2471
2724
|
const current = queue.shift();
|
|
2472
2725
|
if (!current || current.depth > maxDepth) continue;
|
|
@@ -2614,4 +2867,4 @@ export {
|
|
|
2614
2867
|
linkWorkspace,
|
|
2615
2868
|
trace
|
|
2616
2869
|
};
|
|
2617
|
-
//# sourceMappingURL=chunk-
|
|
2870
|
+
//# sourceMappingURL=chunk-AM2JZFQ6.js.map
|