@saptools/service-flow 0.1.30 → 0.1.32
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 +20 -1
- package/TECHNICAL-NOTE.md +7 -0
- package/dist/{chunk-UHYHSYCS.js → chunk-VT5FVMOA.js} +238 -71
- package/dist/chunk-VT5FVMOA.js.map +1 -0
- package/dist/cli.js +33 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-UHYHSYCS.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.32
|
|
4
|
+
|
|
5
|
+
- Hardened operation-first trace starts to fail closed on ambiguous, rejected, or non-executable implementation evidence.
|
|
6
|
+
- Made decorator normalization explicit and conservative for unsupported expressions.
|
|
7
|
+
- Populated queryable external HTTP target metadata with sanitized labels and kept CAP candidates distinct from HTTP endpoints.
|
|
8
|
+
- Removed accidental fresh-schema external-target columns from symbols and documented migration/re-index expectations.
|
|
9
|
+
|
|
10
|
+
## 0.1.31
|
|
11
|
+
|
|
12
|
+
- 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.
|
|
13
|
+
- 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.
|
|
14
|
+
- 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.
|
|
15
|
+
- Add schema version 6 migration columns for queryable external target metadata and a strict doctor aggregate for external HTTP target quality.
|
|
16
|
+
|
|
3
17
|
## 0.1.30
|
|
4
18
|
|
|
5
19
|
- Normalize balanced OData operation invocations when multiline template placeholders appear inside function/action argument lists.
|
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
|
|
@@ -482,3 +490,14 @@ The `graph` command accepts repeatable `--var key=value` options, matching `trac
|
|
|
482
490
|
### 0.1.17 parser ownership policy
|
|
483
491
|
|
|
484
492
|
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.
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
## 0.1.32 hardening notes
|
|
496
|
+
|
|
497
|
+
Operation trace starts resolve indexed CDS operations before handler fallback. Ambiguous operation matches, ambiguous/rejected implementation edges, and resolved edges without an executable handler scope now fail closed with structured `resolutionStage` and `resolutionStatus` diagnostics and no speculative traversal. Handler fallback is only considered when no indexed CDS operation matches and the fallback is unique under the existing registration and ownership policy.
|
|
498
|
+
|
|
499
|
+
Decorator signals are interpreted as resolved, absent, unsupported, or malformed. Only resolved string/template values, generated `Action<Name>.name`/`Func<Name>.name` constants, qualified generated constants ending in `.name`, and narrow safe `String(identifier)` wrappers can contradict method-name fallback. Unsupported expressions are preserved as evidence but are not treated as proof that a handler targets another operation.
|
|
500
|
+
|
|
501
|
+
External HTTP facts persist queryable target kind, stable id, safe label, and dynamic flag on `outbound_calls`; graph evidence uses the same sanitized model. URL user info, fragments, query values, headers, cookies, credentials, tokens, API keys, and request/response bodies are not persisted. Legacy workspaces may relink from old evidence when possible, but full semantic enrichment requires re-index plus relink when old parser facts did not contain safe target metadata.
|
|
502
|
+
|
|
503
|
+
Graph taxonomy now keeps CAP operation candidates separate from external HTTP targets. Dynamic, ambiguous, and unresolved CAP calls use operation-candidate semantics, external destinations/endpoints are produced only by `external_http` calls, and terminal local transport-client methods use transport-method semantics rather than external HTTP edges. `doctor --strict` reports external target quality and taxonomy consistency.
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## 0.1.30 OData invocation argument placeholder notes
|
|
4
11
|
|
|
5
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.
|
|
@@ -720,6 +720,52 @@ function topLevelQueryIndex(text) {
|
|
|
720
720
|
import fs6 from "fs/promises";
|
|
721
721
|
import path7 from "path";
|
|
722
722
|
import ts4 from "typescript";
|
|
723
|
+
|
|
724
|
+
// src/linker/external-http-target.ts
|
|
725
|
+
import { createHash } from "crypto";
|
|
726
|
+
var sensitiveKeys = /* @__PURE__ */ new Set(["token", "access_token", "id_token", "api_key", "apikey", "key", "password", "passwd", "pwd", "secret", "client_secret", "authorization", "cookie", "signature"]);
|
|
727
|
+
function hash(value) {
|
|
728
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
729
|
+
}
|
|
730
|
+
function methodPrefix(method) {
|
|
731
|
+
return typeof method === "string" && method.length > 0 ? `${method.toUpperCase()} ` : "";
|
|
732
|
+
}
|
|
733
|
+
function redactUrl(value) {
|
|
734
|
+
try {
|
|
735
|
+
const url = new URL(value, value.startsWith("/") ? "https://relative.invalid" : void 0);
|
|
736
|
+
url.username = "";
|
|
737
|
+
url.password = "";
|
|
738
|
+
for (const key of [...url.searchParams.keys()]) url.searchParams.set(key, sensitiveKeys.has(key.toLowerCase()) ? "<redacted>" : "<redacted>");
|
|
739
|
+
const path9 = `${url.pathname}${url.search ? url.search : ""}`;
|
|
740
|
+
return value.startsWith("/") ? path9 : `${url.origin}${path9}`;
|
|
741
|
+
} catch {
|
|
742
|
+
return value.replace(/([?&][^=;&]*(?:token|key|password|secret|cookie|authorization)[^=;&]*=)[^&]*/gi, "$1<redacted>");
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function externalHttpTarget(call) {
|
|
746
|
+
const evidence = typeof call.evidence_json === "string" ? safeParse(call.evidence_json) : {};
|
|
747
|
+
const target = evidence.externalTarget && typeof evidence.externalTarget === "object" && !Array.isArray(evidence.externalTarget) ? evidence.externalTarget : {};
|
|
748
|
+
const method = typeof call.method === "string" ? call.method : typeof target.method === "string" ? target.method : void 0;
|
|
749
|
+
const kind = typeof target.kind === "string" ? target.kind : "unknown";
|
|
750
|
+
const expression = typeof target.expression === "string" ? target.expression : void 0;
|
|
751
|
+
if (kind === "destination" && expression) return { kind, toKind: "external_destination", toId: `destination:${expression}`, label: `External destination: ${expression}`, method, dynamic: false, expression };
|
|
752
|
+
if (kind === "static_url" && expression) {
|
|
753
|
+
const redacted = redactUrl(expression);
|
|
754
|
+
return { kind, toKind: "external_endpoint", toId: `endpoint:${hash(`${method ?? ""}:${redacted}`)}`, label: `External endpoint: ${methodPrefix(method)}${redacted}`, method, dynamic: false, expression: redacted };
|
|
755
|
+
}
|
|
756
|
+
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)}` };
|
|
757
|
+
return { kind: "unknown", toKind: "external_endpoint", toId: "unknown", label: "External endpoint: unknown", method, dynamic: false };
|
|
758
|
+
}
|
|
759
|
+
function safeParse(value) {
|
|
760
|
+
try {
|
|
761
|
+
const parsed = JSON.parse(value);
|
|
762
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
763
|
+
} catch {
|
|
764
|
+
return {};
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/parsers/outbound-call-parser.ts
|
|
723
769
|
function lineOf3(text, idx) {
|
|
724
770
|
return text.slice(0, idx).split("\n").length;
|
|
725
771
|
}
|
|
@@ -798,6 +844,70 @@ function nameOfProperty(name) {
|
|
|
798
844
|
if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name)) return name.text;
|
|
799
845
|
return void 0;
|
|
800
846
|
}
|
|
847
|
+
function staticExpressionText(expr, initializers) {
|
|
848
|
+
if (!expr) return void 0;
|
|
849
|
+
if (isStringLike(expr)) return expr.text;
|
|
850
|
+
if (ts4.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
|
|
851
|
+
return void 0;
|
|
852
|
+
}
|
|
853
|
+
function propertyInitializer(object, key) {
|
|
854
|
+
for (const property of object.properties) {
|
|
855
|
+
if (ts4.isPropertyAssignment(property) && nameOfProperty(property.name) === key) return property.initializer;
|
|
856
|
+
if (ts4.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
|
|
857
|
+
}
|
|
858
|
+
return void 0;
|
|
859
|
+
}
|
|
860
|
+
function httpMethodFromObject(object, initializers) {
|
|
861
|
+
const text = staticExpressionText(propertyInitializer(object, "method"), initializers);
|
|
862
|
+
return text ? stripQuotes(text).toUpperCase() : void 0;
|
|
863
|
+
}
|
|
864
|
+
function urlTargetFromExpression(expr, initializers) {
|
|
865
|
+
const text = staticExpressionText(expr, initializers);
|
|
866
|
+
if (text) return { kind: "static_url", expression: text, dynamic: false };
|
|
867
|
+
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 };
|
|
868
|
+
return { kind: "unknown", dynamic: false };
|
|
869
|
+
}
|
|
870
|
+
function destinationTargetFromExpression(expr, initializers) {
|
|
871
|
+
const text = staticExpressionText(expr, initializers);
|
|
872
|
+
if (text) return { kind: "destination", expression: text, dynamic: false };
|
|
873
|
+
if (expr && ts4.isIdentifier(expr)) return { kind: "destination", expression: expr.text, dynamic: true };
|
|
874
|
+
return void 0;
|
|
875
|
+
}
|
|
876
|
+
function externalHttpEvidence(node, source, initializers) {
|
|
877
|
+
const expr = node.expression;
|
|
878
|
+
const exprText = expr.getText(source);
|
|
879
|
+
if (exprText === "useOrFetchDestination") {
|
|
880
|
+
const objectArg = node.arguments[0];
|
|
881
|
+
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
882
|
+
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"), initializers);
|
|
883
|
+
return { externalTarget: destination ?? { kind: "unknown", dynamic: false }, classifier: "sap_destination_lookup", sourceCallShape: "useOrFetchDestination" };
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (exprText === "executeHttpRequest") {
|
|
887
|
+
const destination = destinationTargetFromExpression(node.arguments[0], initializers);
|
|
888
|
+
const config = node.arguments[1];
|
|
889
|
+
const method = config && ts4.isObjectLiteralExpression(config) ? httpMethodFromObject(config, initializers) : void 0;
|
|
890
|
+
const url = config && ts4.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, "url"), initializers) : { kind: "unknown", dynamic: false };
|
|
891
|
+
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: "sap_execute_http_request", sourceCallShape: "executeHttpRequest" };
|
|
892
|
+
}
|
|
893
|
+
if (exprText === "axios") {
|
|
894
|
+
const config = node.arguments[0];
|
|
895
|
+
if (config && ts4.isObjectLiteralExpression(config)) {
|
|
896
|
+
const method = httpMethodFromObject(config, initializers);
|
|
897
|
+
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, "url"), initializers), classifier: "axios_config_call", sourceCallShape: "axios(config)" };
|
|
898
|
+
}
|
|
899
|
+
return { externalTarget: { kind: "unknown", dynamic: false }, classifier: "axios_unknown_call", sourceCallShape: "axios(...)" };
|
|
900
|
+
}
|
|
901
|
+
if (exprText === "fetch") {
|
|
902
|
+
const init = node.arguments[1];
|
|
903
|
+
const method = init && ts4.isObjectLiteralExpression(init) ? httpMethodFromObject(init, initializers) : void 0;
|
|
904
|
+
return { method, externalTarget: urlTargetFromExpression(node.arguments[0], initializers), classifier: "fetch_call", sourceCallShape: "fetch" };
|
|
905
|
+
}
|
|
906
|
+
if (ts4.isPropertyAccessExpression(expr) && ["get", "post", "put", "patch", "delete", "head"].includes(expr.name.text) && expr.expression.getText(source) === "axios") {
|
|
907
|
+
return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], initializers), classifier: "axios_member_call", sourceCallShape: `axios.${expr.name.text}` };
|
|
908
|
+
}
|
|
909
|
+
return void 0;
|
|
910
|
+
}
|
|
801
911
|
function collectServiceVariables(source) {
|
|
802
912
|
const vars = /* @__PURE__ */ new Set(["cds", "messaging", "messageClient", "eventClient"]);
|
|
803
913
|
const visit = (node) => {
|
|
@@ -930,8 +1040,13 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
930
1040
|
const eventName = literalText(node.arguments[0]);
|
|
931
1041
|
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" });
|
|
932
1042
|
}
|
|
933
|
-
} else
|
|
934
|
-
|
|
1043
|
+
} else {
|
|
1044
|
+
const external = externalHttpEvidence(node, source, initializers);
|
|
1045
|
+
if (external) {
|
|
1046
|
+
const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
|
|
1047
|
+
const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
|
|
1048
|
+
add(node, { callType: "external_http", method: external.method, payloadSummary: void 0, confidence: 0.7, unresolvedReason: "External HTTP destination is outside indexed CAP services", externalTarget: { kind: safeTarget.kind, stableId: safeTarget.toId, label: safeTarget.label, dynamic: safeTarget.dynamic } }, { classifier: external.classifier, externalTarget: safeTarget, sourceCallShape: external.sourceCallShape });
|
|
1049
|
+
}
|
|
935
1050
|
}
|
|
936
1051
|
}
|
|
937
1052
|
ts4.forEachChild(node, visit);
|
|
@@ -1385,16 +1500,16 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1385
1500
|
helperChain: aliasKind === "assignment" ? [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind, scopeRule: "same-file-source-order" }] : void 0
|
|
1386
1501
|
});
|
|
1387
1502
|
else if (ts5.isIdentifier(call.expression)) {
|
|
1388
|
-
const
|
|
1389
|
-
if (
|
|
1503
|
+
const resolved2 = await importedHelper(call.expression.text);
|
|
1504
|
+
if (resolved2)
|
|
1390
1505
|
out.push({
|
|
1391
1506
|
variableName: targetName,
|
|
1392
|
-
alias:
|
|
1393
|
-
aliasExpr:
|
|
1394
|
-
destinationExpr:
|
|
1395
|
-
servicePathExpr:
|
|
1396
|
-
isDynamic:
|
|
1397
|
-
placeholders:
|
|
1507
|
+
alias: resolved2.helper.alias,
|
|
1508
|
+
aliasExpr: resolved2.helper.aliasExpr,
|
|
1509
|
+
destinationExpr: resolved2.helper.destinationExpr,
|
|
1510
|
+
servicePathExpr: resolved2.helper.servicePathExpr,
|
|
1511
|
+
isDynamic: resolved2.helper.isDynamic,
|
|
1512
|
+
placeholders: resolved2.helper.placeholders,
|
|
1398
1513
|
sourceFile: normalizePath(filePath),
|
|
1399
1514
|
sourceLine: lineOf4(sourceFileAst, node),
|
|
1400
1515
|
helperChain: [
|
|
@@ -1402,10 +1517,10 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1402
1517
|
callerVariable: targetName,
|
|
1403
1518
|
...aliasKind === "assignment" ? { assignedFrom: call.expression.text, aliasKind, scopeRule: "same-file-source-order" } : {},
|
|
1404
1519
|
importedHelper: call.expression.text,
|
|
1405
|
-
importSource:
|
|
1406
|
-
exportedSymbol:
|
|
1407
|
-
helperSourceFile:
|
|
1408
|
-
helperSourceLine:
|
|
1520
|
+
importSource: resolved2.imp.sourceFile,
|
|
1521
|
+
exportedSymbol: resolved2.imp.exportedName,
|
|
1522
|
+
helperSourceFile: resolved2.helper.sourceFile,
|
|
1523
|
+
helperSourceLine: resolved2.helper.sourceLine
|
|
1409
1524
|
}
|
|
1410
1525
|
]
|
|
1411
1526
|
});
|
|
@@ -1432,18 +1547,18 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1432
1547
|
const propertyName = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
1433
1548
|
const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
|
|
1434
1549
|
if (matches.length !== 1) continue;
|
|
1435
|
-
const
|
|
1550
|
+
const resolved2 = matches[0];
|
|
1436
1551
|
out.push({
|
|
1437
1552
|
variableName: el.name.text,
|
|
1438
|
-
alias:
|
|
1439
|
-
aliasExpr:
|
|
1440
|
-
destinationExpr:
|
|
1441
|
-
servicePathExpr:
|
|
1442
|
-
isDynamic:
|
|
1443
|
-
placeholders:
|
|
1553
|
+
alias: resolved2.helper.alias,
|
|
1554
|
+
aliasExpr: resolved2.helper.aliasExpr,
|
|
1555
|
+
destinationExpr: resolved2.helper.destinationExpr,
|
|
1556
|
+
servicePathExpr: resolved2.helper.servicePathExpr,
|
|
1557
|
+
isDynamic: resolved2.helper.isDynamic,
|
|
1558
|
+
placeholders: resolved2.helper.placeholders,
|
|
1444
1559
|
sourceFile: normalizePath(filePath),
|
|
1445
1560
|
sourceLine: lineOf4(sourceFileAst, decl),
|
|
1446
|
-
helperChain: [{ callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName, importSource:
|
|
1561
|
+
helperChain: [{ callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
|
|
1447
1562
|
});
|
|
1448
1563
|
}
|
|
1449
1564
|
}
|
|
@@ -1465,18 +1580,18 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1465
1580
|
if (!propertyName || !targetName) continue;
|
|
1466
1581
|
const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
|
|
1467
1582
|
if (matches.length !== 1) continue;
|
|
1468
|
-
const
|
|
1583
|
+
const resolved2 = matches[0];
|
|
1469
1584
|
out.push({
|
|
1470
1585
|
variableName: targetName,
|
|
1471
|
-
alias:
|
|
1472
|
-
aliasExpr:
|
|
1473
|
-
destinationExpr:
|
|
1474
|
-
servicePathExpr:
|
|
1475
|
-
isDynamic:
|
|
1476
|
-
placeholders:
|
|
1586
|
+
alias: resolved2.helper.alias,
|
|
1587
|
+
aliasExpr: resolved2.helper.aliasExpr,
|
|
1588
|
+
destinationExpr: resolved2.helper.destinationExpr,
|
|
1589
|
+
servicePathExpr: resolved2.helper.servicePathExpr,
|
|
1590
|
+
isDynamic: resolved2.helper.isDynamic,
|
|
1591
|
+
placeholders: resolved2.helper.placeholders,
|
|
1477
1592
|
sourceFile: normalizePath(filePath),
|
|
1478
1593
|
sourceLine: lineOf4(sourceFileAst, node),
|
|
1479
|
-
helperChain: [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName, importSource:
|
|
1594
|
+
helperChain: [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
|
|
1480
1595
|
});
|
|
1481
1596
|
}
|
|
1482
1597
|
}
|
|
@@ -1861,6 +1976,46 @@ function linkHelperPackages(db, workspaceId, generation) {
|
|
|
1861
1976
|
return summary;
|
|
1862
1977
|
}
|
|
1863
1978
|
|
|
1979
|
+
// src/linker/operation-decorator-normalizer.ts
|
|
1980
|
+
function lowerFirst(value) {
|
|
1981
|
+
return value ? `${value[0]?.toLowerCase() ?? ""}${value.slice(1)}` : value;
|
|
1982
|
+
}
|
|
1983
|
+
function normalizedOperationName(value) {
|
|
1984
|
+
return value.replace(/^\//, "");
|
|
1985
|
+
}
|
|
1986
|
+
function clean(value) {
|
|
1987
|
+
return value.replace(/^[`'"]|[`'"]$/g, "");
|
|
1988
|
+
}
|
|
1989
|
+
function generatedFromConstantName(value) {
|
|
1990
|
+
for (const prefix of ["Action", "Func"]) {
|
|
1991
|
+
if (value.startsWith(prefix) && value.length > prefix.length && /^[A-Z]/.test(value.slice(prefix.length))) return lowerFirst(value.slice(prefix.length));
|
|
1992
|
+
}
|
|
1993
|
+
return void 0;
|
|
1994
|
+
}
|
|
1995
|
+
function resolved(value, raw) {
|
|
1996
|
+
const literal = clean(value);
|
|
1997
|
+
const generated = generatedFromConstantName(literal);
|
|
1998
|
+
return { status: "resolved", operationName: generated ?? normalizedOperationName(literal), raw };
|
|
1999
|
+
}
|
|
2000
|
+
function normalizeDecoratorOperationSignal(value, raw, candidateOperation) {
|
|
2001
|
+
if (value) return resolved(value, raw);
|
|
2002
|
+
if (!raw || raw.trim().length === 0) return { status: "none", raw };
|
|
2003
|
+
const expression = raw.trim();
|
|
2004
|
+
const nameMatch = /(?:^|\.)(Action[A-Z][\w$]*|Func[A-Z][\w$]*)\.name$/.exec(expression);
|
|
2005
|
+
if (nameMatch?.[1]) return resolved(nameMatch[1], expression);
|
|
2006
|
+
const stringMatch = /^String\(([A-Za-z_$][\w$]*)\)$/.exec(expression);
|
|
2007
|
+
if (stringMatch?.[1]) {
|
|
2008
|
+
const identifier = stringMatch[1];
|
|
2009
|
+
const generated = generatedFromConstantName(identifier);
|
|
2010
|
+
const normalizedCandidate = candidateOperation ? normalizedOperationName(candidateOperation) : void 0;
|
|
2011
|
+
if (generated) return { status: "resolved", operationName: generated, raw: expression };
|
|
2012
|
+
if (normalizedCandidate && identifier === normalizedCandidate) return { status: "resolved", operationName: identifier, raw: expression };
|
|
2013
|
+
return { status: "unsupported", raw: expression, reason: "string_wrapper_identifier_not_resolved" };
|
|
2014
|
+
}
|
|
2015
|
+
if (/^[`'"]/.test(expression) && !/[`'"]$/.test(expression)) return { status: "malformed", raw: expression, reason: "unterminated_literal" };
|
|
2016
|
+
return { status: "unsupported", raw: expression, reason: "unsupported_decorator_expression" };
|
|
2017
|
+
}
|
|
2018
|
+
|
|
1864
2019
|
// src/linker/cross-repo-linker.ts
|
|
1865
2020
|
function linkWorkspace(db, workspaceId, vars = {}) {
|
|
1866
2021
|
return db.transaction(() => {
|
|
@@ -1920,7 +2075,7 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
1920
2075
|
return { status: "terminal", callType };
|
|
1921
2076
|
}
|
|
1922
2077
|
if (callType === "local_service_call" && call.unresolved_reason === "transport_client_method" && !resolution.target && resolution.candidates.length === 0) {
|
|
1923
|
-
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, "
|
|
2078
|
+
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_TRANSPORT_METHOD", "terminal", "call", String(call.id), "transport_method", String(op || "transport_client_method"), Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, classification: "transport_client_method" }), 0, generation);
|
|
1924
2079
|
return { status: "terminal", callType };
|
|
1925
2080
|
}
|
|
1926
2081
|
if (resolution.target) {
|
|
@@ -1930,10 +2085,12 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
1930
2085
|
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";
|
|
1931
2086
|
const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
|
|
1932
2087
|
const unresolvedReason = status === "terminal" ? null : String(call.unresolved_reason ?? unresolvedOperationReason(resolution));
|
|
1933
|
-
const
|
|
1934
|
-
const
|
|
2088
|
+
const externalTarget = callType === "external_http" ? externalHttpTarget(call) : void 0;
|
|
2089
|
+
const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : callType === "external_http" ? externalTarget?.toKind ?? "external_endpoint" : "operation_candidate";
|
|
2090
|
+
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");
|
|
1935
2091
|
const graphLevelDynamic = edgeType === "DYNAMIC_EDGE_CANDIDATE" && resolution.status === "dynamic";
|
|
1936
|
-
|
|
2092
|
+
const finalEvidence = externalTarget ? { ...evidence, externalTarget } : evidence;
|
|
2093
|
+
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);
|
|
1937
2094
|
return { status, callType };
|
|
1938
2095
|
}
|
|
1939
2096
|
function unresolvedOperationReason(resolution) {
|
|
@@ -2187,28 +2344,16 @@ function candidateEvidence(candidate, rank) {
|
|
|
2187
2344
|
};
|
|
2188
2345
|
}
|
|
2189
2346
|
function implementationMethodSignal(row, operation) {
|
|
2190
|
-
const op =
|
|
2191
|
-
const decorator =
|
|
2192
|
-
if (decorator && decorator === op) return { matches: true, contradicted: false, acceptedReasons: ["decorator targets operation"], rejectedReasons: [] };
|
|
2193
|
-
if (decorator && decorator !== op) return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ["method_name_matches_but_decorator_targets_different_operation"] };
|
|
2347
|
+
const op = normalizedOperationName(String(operation.operationPath ?? operation.operationName ?? ""));
|
|
2348
|
+
const decorator = normalizeDecoratorOperationSignal(typeof row.decoratorValue === "string" ? row.decoratorValue : void 0, typeof row.decoratorRawExpression === "string" ? row.decoratorRawExpression : void 0, op);
|
|
2349
|
+
if (decorator.status === "resolved" && decorator.operationName === op) return { matches: true, contradicted: false, acceptedReasons: ["decorator targets operation"], rejectedReasons: [] };
|
|
2350
|
+
if (decorator.status === "resolved" && decorator.operationName !== op) return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ["method_name_matches_but_decorator_targets_different_operation"] };
|
|
2194
2351
|
if (String(row.methodName ?? "") === op) return { matches: true, contradicted: false, acceptedReasons: ["method name fallback matched operation"], rejectedReasons: [] };
|
|
2195
2352
|
return { matches: false, contradicted: false, acceptedReasons: [], rejectedReasons: ["method name does not match operation"] };
|
|
2196
2353
|
}
|
|
2197
|
-
function normalizeDecoratorOperation(value, raw) {
|
|
2198
|
-
const candidate = value ?? raw?.split(".").filter(Boolean).at(-2);
|
|
2199
|
-
if (!candidate) return void 0;
|
|
2200
|
-
const cleaned = candidate.replace(/^['"`]|['"`]$/g, "");
|
|
2201
|
-
for (const prefix of ["Func", "Action"]) {
|
|
2202
|
-
if (cleaned.startsWith(prefix) && cleaned.length > prefix.length) return lowerFirst(cleaned.slice(prefix.length));
|
|
2203
|
-
}
|
|
2204
|
-
return normalizedOperation(cleaned);
|
|
2205
|
-
}
|
|
2206
2354
|
function upperFirst(value) {
|
|
2207
2355
|
return value ? `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}` : value;
|
|
2208
2356
|
}
|
|
2209
|
-
function lowerFirst(value) {
|
|
2210
|
-
return value ? `${value[0]?.toLowerCase() ?? ""}${value.slice(1)}` : value;
|
|
2211
|
-
}
|
|
2212
2357
|
function flag(value) {
|
|
2213
2358
|
return Boolean(Number(value ?? 0));
|
|
2214
2359
|
}
|
|
@@ -2227,6 +2372,25 @@ function normalizeOperation(value) {
|
|
|
2227
2372
|
function positiveDepth(value) {
|
|
2228
2373
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
|
|
2229
2374
|
}
|
|
2375
|
+
function operationStartScope(db, repoId, start) {
|
|
2376
|
+
const requested = normalizeOperation(start.operationPath ?? start.operation);
|
|
2377
|
+
if (!requested) return void 0;
|
|
2378
|
+
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
|
|
2379
|
+
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
|
|
2380
|
+
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=?)
|
|
2381
|
+
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}`);
|
|
2382
|
+
if (rows2.length === 0) return void 0;
|
|
2383
|
+
const repoCount = new Set(rows2.map((row) => String(row.repoName))).size;
|
|
2384
|
+
const serviceCount = new Set(rows2.map((row) => `${String(row.repoName)}:${String(row.servicePath)}`)).size;
|
|
2385
|
+
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, resolutionStage: "operation", resolutionStatus: "ambiguous_operation", candidates: rows2 }] };
|
|
2386
|
+
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, resolutionStage: "operation", resolutionStatus: "ambiguous_operation", candidates: rows2 }] };
|
|
2387
|
+
if (rows2.length !== 1) return { diagnostics: [{ severity: "warning", code: "trace_start_ambiguous", message: "Operation trace start matched multiple indexed operations", normalizedSelectorValue: requested, resolutionStage: "operation", resolutionStatus: "ambiguous_operation", candidates: rows2 }] };
|
|
2388
|
+
const operationId = String(rows2[0]?.operationId);
|
|
2389
|
+
const impl = implementationScope(db, operationId);
|
|
2390
|
+
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: [] };
|
|
2391
|
+
if (impl.edge) return { operationId, diagnostics: [{ severity: "warning", code: impl.edge.status === "ambiguous" ? "trace_start_ambiguous" : "trace_start_implementation_unresolved", message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? "unresolved")}`, resolutionStage: "implementation", resolutionStatus: impl.edge.status === "ambiguous" ? "ambiguous_implementation" : "rejected_implementation", implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, candidates: parseEvidence(impl.edge.evidence_json).candidates }] };
|
|
2392
|
+
return { operationId, diagnostics: [{ severity: "warning", code: "trace_start_implementation_unresolved", message: "Indexed operation matched but no implementation candidate exists", resolutionStage: "implementation", resolutionStatus: "operation_without_implementation" }] };
|
|
2393
|
+
}
|
|
2230
2394
|
function sourceFilesForStart(db, repoId, start) {
|
|
2231
2395
|
const handler = start.handler;
|
|
2232
2396
|
const operation = normalizeOperation(start.operation ?? start.operationPath);
|
|
@@ -2272,7 +2436,9 @@ function startScope(db, start) {
|
|
|
2272
2436
|
"SELECT id,name FROM repositories WHERE name=? OR package_name=?"
|
|
2273
2437
|
).get(start.repo, start.repo) : void 0;
|
|
2274
2438
|
if (start.repo && !repo) return { repo, selectorMatched: false };
|
|
2275
|
-
const
|
|
2439
|
+
const operationScope = operationStartScope(db, repo?.id, start);
|
|
2440
|
+
const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === "operation" || d.resolutionStage === "implementation");
|
|
2441
|
+
const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start);
|
|
2276
2442
|
const sourceFiles = sourceScope?.files;
|
|
2277
2443
|
const hasSelector = Boolean(
|
|
2278
2444
|
start.handler ?? start.operation ?? start.operationPath ?? start.servicePath
|
|
@@ -2283,7 +2449,9 @@ function startScope(db, start) {
|
|
|
2283
2449
|
repo,
|
|
2284
2450
|
sourceFiles,
|
|
2285
2451
|
symbolIds: sourceScope?.symbols,
|
|
2286
|
-
selectorMatched: !hasSelector || sourceFiles !== void 0
|
|
2452
|
+
selectorMatched: !terminalOperationScope && (!hasSelector || sourceFiles !== void 0),
|
|
2453
|
+
startOperationId: operationScope?.operationId,
|
|
2454
|
+
startDiagnostics: operationScope?.diagnostics
|
|
2287
2455
|
};
|
|
2288
2456
|
}
|
|
2289
2457
|
function handlerFilesForOperation(db, operationId) {
|
|
@@ -2530,6 +2698,10 @@ function edgeTarget(row, evidence) {
|
|
|
2530
2698
|
const targetRepo = typeof evidence.targetRepo === "string" ? evidence.targetRepo : "";
|
|
2531
2699
|
if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return `Entity: ${row.to_id || "unknown"}`;
|
|
2532
2700
|
if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return typeof evidence.remoteQueryTarget === "string" ? evidence.remoteQueryTarget : `Remote query: ${row.to_id || "unknown"}`;
|
|
2701
|
+
if (row.edge_type === "HANDLER_CALLS_EXTERNAL_HTTP") {
|
|
2702
|
+
const target = evidence.externalTarget;
|
|
2703
|
+
return typeof target?.label === "string" ? target.label : `External endpoint: ${row.to_id || "unknown"}`;
|
|
2704
|
+
}
|
|
2533
2705
|
return servicePath && operationPath ? `${servicePath}${operationPath}` : targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
|
|
2534
2706
|
}
|
|
2535
2707
|
function trace(db, start, options) {
|
|
@@ -2540,7 +2712,8 @@ function trace(db, start, options) {
|
|
|
2540
2712
|
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);
|
|
2541
2713
|
for (const row of stale)
|
|
2542
2714
|
diagnostics.unshift({ severity: "warning", code: "graph_stale", message: `Graph is stale for ${row.name ?? "repository"}: ${row.reason ?? "facts_changed"}. Run service-flow link.` });
|
|
2543
|
-
|
|
2715
|
+
for (const diagnostic of scope.startDiagnostics ?? []) diagnostics.unshift(diagnostic);
|
|
2716
|
+
if (!scope.selectorMatched && !scope.startDiagnostics?.length)
|
|
2544
2717
|
diagnostics.unshift({
|
|
2545
2718
|
severity: "warning",
|
|
2546
2719
|
code: "trace_start_not_found",
|
|
@@ -2549,27 +2722,21 @@ function trace(db, start, options) {
|
|
|
2549
2722
|
const maxDepth = positiveDepth(options.depth);
|
|
2550
2723
|
const edges = [];
|
|
2551
2724
|
const nodes = /* @__PURE__ */ new Map();
|
|
2725
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
2552
2726
|
const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1, context: /* @__PURE__ */ new Map() }] : [];
|
|
2553
|
-
if (
|
|
2554
|
-
const
|
|
2555
|
-
const
|
|
2556
|
-
if (
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
const
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
if (impl.edge) {
|
|
2564
|
-
const implEvidence = JSON.parse(String(impl.edge.evidence_json || "{}"));
|
|
2565
|
-
const handlerNode = impl.edge.status === "resolved" ? handlerMethodNode(db, impl.edge.to_id) : void 0;
|
|
2566
|
-
if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
|
|
2567
|
-
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) });
|
|
2568
|
-
}
|
|
2727
|
+
if (scope.startOperationId) {
|
|
2728
|
+
const op = operationNode(db, scope.startOperationId);
|
|
2729
|
+
const impl = implementationScope(db, scope.startOperationId);
|
|
2730
|
+
if (op) nodes.set(String(op.id), op);
|
|
2731
|
+
if (impl.edge && impl.edge.status === "resolved") {
|
|
2732
|
+
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 } };
|
|
2733
|
+
const handlerNode = impl.edge.status === "resolved" ? handlerMethodNode(db, impl.edge.to_id) : void 0;
|
|
2734
|
+
if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
|
|
2735
|
+
seenEdges.add(Number(impl.edge.id));
|
|
2736
|
+
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) });
|
|
2569
2737
|
}
|
|
2570
2738
|
}
|
|
2571
2739
|
const seenScopes = /* @__PURE__ */ new Set();
|
|
2572
|
-
const seenEdges = /* @__PURE__ */ new Set();
|
|
2573
2740
|
while (queue.length > 0) {
|
|
2574
2741
|
const current = queue.shift();
|
|
2575
2742
|
if (!current || current.depth > maxDepth) continue;
|
|
@@ -2717,4 +2884,4 @@ export {
|
|
|
2717
2884
|
linkWorkspace,
|
|
2718
2885
|
trace
|
|
2719
2886
|
};
|
|
2720
|
-
//# sourceMappingURL=chunk-
|
|
2887
|
+
//# sourceMappingURL=chunk-VT5FVMOA.js.map
|