@saptools/service-flow 0.1.34 → 0.1.36
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 +7 -0
- package/TECHNICAL-NOTE.md +5 -0
- package/dist/{chunk-5SR4SFSU.js → chunk-2UOIY2NI.js} +136 -21
- package/dist/chunk-2UOIY2NI.js.map +1 -0
- package/dist/cli.js +31 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-5SR4SFSU.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.35
|
|
4
|
+
|
|
5
|
+
- Hardened OData path precedence so entity key, navigation, and media/property paths with placeholders remain terminal entity evidence instead of dynamic operation candidates.
|
|
6
|
+
- Preserved separate evidence for service-routing placeholders, operation invocation argument placeholders, and entity key placeholders.
|
|
7
|
+
- Render operation-resolved parser entity calls as operation calls in traces while retaining the original parser call type for auditability.
|
|
8
|
+
- Added strict doctor coverage for dynamic remote-entity false positives without indexed operation evidence.
|
|
9
|
+
|
|
3
10
|
## 0.1.34
|
|
4
11
|
|
|
5
12
|
- Prefer indexed CDS operation evidence over heuristic remote-entity classification for service-client operation invocations, while keeping true collection, entity, delete, navigation, and media paths terminal.
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -137,3 +137,8 @@ Schema version 6 adds queryable external target metadata columns to `outbound_ca
|
|
|
137
137
|
### 0.1.17 parser ownership policy
|
|
138
138
|
|
|
139
139
|
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.
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
## 0.1.35 OData placeholder semantics
|
|
143
|
+
|
|
144
|
+
Service-flow now records OData placeholders by semantic layer. Service-routing placeholders belong to service bindings and can make an operation edge dynamic until runtime variables are supplied. Operation invocation argument placeholders, such as action/function call arguments, remain operation evidence but are not service-routing variables. Entity key placeholders belong to entity addressing, so key, navigation, and media/property paths remain terminal remote entity/query edges unless indexed CDS operation evidence provides a credible operation match.
|
|
@@ -557,15 +557,28 @@ function classifyODataPathIntent(path9, method) {
|
|
|
557
557
|
const hasNavigationSegments = segments.length > 1;
|
|
558
558
|
const entitySegment = entitySegmentFromPath(pathWithoutQuery);
|
|
559
559
|
const placeholderKeys = [...new Set(extractTemplatePlaceholders(rawPath))];
|
|
560
|
-
const
|
|
560
|
+
const firstOpen = firstSegment.indexOf("(");
|
|
561
|
+
const firstClose = firstOpen >= 0 ? matchingClose(firstSegment, firstOpen) : void 0;
|
|
562
|
+
const keyPredicateText = firstOpen >= 0 && firstClose !== void 0 ? firstSegment.slice(firstOpen + 1, firstClose) : "";
|
|
563
|
+
const rawKeyPredicatePlaceholderKeys = [...new Set(extractTemplatePlaceholders(keyPredicateText))];
|
|
564
|
+
const navigationSuffix = hasNavigationSegments ? segments.slice(1).join("/") : void 0;
|
|
565
|
+
const lastSegment = segments.at(-1) ?? "";
|
|
566
|
+
const mediaOrPropertySuffix = hasNavigationSegments ? lastSegment : void 0;
|
|
567
|
+
const invocation = normalizeODataOperationInvocationPath(pathWithoutQuery);
|
|
568
|
+
const operationNameFromInvocation = invocation?.wasInvocation ? invocation.normalizedOperationPath.replace(/^\//, "").split(".").at(-1) : void 0;
|
|
569
|
+
const topLevelName = firstSegment.split("(")[0]?.replace(/^\//, "");
|
|
570
|
+
const topLevelOperationName = operationNameFromInvocation ?? (!hasNavigationSegments && !firstSegment.includes("(") ? topLevelName : void 0);
|
|
571
|
+
const topLevelOperationInvocation = Boolean(invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment));
|
|
572
|
+
const keyPredicatePlaceholderKeys = topLevelOperationInvocation ? [] : rawKeyPredicatePlaceholderKeys;
|
|
573
|
+
const topLevelOperationNameCandidate = Boolean(topLevelOperationName && !hasNavigationSegments && !firstSegment.includes("("));
|
|
574
|
+
const base = { rawPath, method: normalizedMethod, pathWithoutQuery, queryString, hasQueryString: queryIndex >= 0, entitySegment, placeholderKeys, keyPredicatePlaceholderKeys, invocationArgumentPlaceholderKeys: invocation?.invocationArgumentPlaceholderKeys ?? [], navigationSuffix, mediaOrPropertySuffix, hasEntityKeyPredicate: firstOpen >= 0 && firstClose !== void 0, hasNavigationSuffix: hasNavigationSegments, hasMediaOrPropertySuffix: Boolean(mediaOrPropertySuffix && isMediaOrPropertySuffix(mediaOrPropertySuffix)), topLevelOperationName, topLevelOperationNameCandidate, topLevelOperationInvocation };
|
|
561
575
|
if (!rawPath || !rawPath.startsWith("/")) return { ...base, kind: "unknown", reason: "path_missing_or_not_absolute" };
|
|
562
576
|
const upperEntityLike = /^[A-Z][A-Za-z0-9_]*$/.test(entitySegment ?? firstSegment);
|
|
563
|
-
const mediaLike =
|
|
564
|
-
const invocation = normalizeODataOperationInvocationPath(pathWithoutQuery);
|
|
577
|
+
const mediaLike = isMediaOrPropertySuffix(segments.at(-1) ?? "");
|
|
565
578
|
if (normalizedMethod !== "GET") {
|
|
566
579
|
if (invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "operation_invocation", reason: "non_get_balanced_top_level_operation_invocation" };
|
|
567
580
|
if (mediaLike) return { ...base, kind: "entity_media", reason: "non_get_entity_media_stream_path" };
|
|
568
|
-
if (hasNavigationSegments || firstSegment.includes("(")) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: "
|
|
581
|
+
if (hasNavigationSegments || firstSegment.includes("(")) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: firstSegment.includes("(") ? "non_get_entity_key_or_navigation_path_shape" : "non_get_entity_navigation_path_shape" };
|
|
569
582
|
if (upperEntityLike) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: "non_get_entity_path_shape" };
|
|
570
583
|
return { ...base, kind: "operation_invocation", reason: "non_get_lowercase_path_may_be_operation" };
|
|
571
584
|
}
|
|
@@ -589,6 +602,9 @@ function entitySegmentFromPath(path9) {
|
|
|
589
602
|
const entity = (open >= 0 ? first.slice(0, open) : first).trim();
|
|
590
603
|
return entity || void 0;
|
|
591
604
|
}
|
|
605
|
+
function isMediaOrPropertySuffix(segment) {
|
|
606
|
+
return ["file", "content", "$value", "metadata", "items"].includes(segment.toLowerCase());
|
|
607
|
+
}
|
|
592
608
|
function looksLikeLowerCamelInvocation(segment) {
|
|
593
609
|
const open = segment.indexOf("(");
|
|
594
610
|
if (open <= 0) return false;
|
|
@@ -1319,16 +1335,32 @@ async function importsFor(repoPath, filePath, sf) {
|
|
|
1319
1335
|
}
|
|
1320
1336
|
return imports;
|
|
1321
1337
|
}
|
|
1322
|
-
function
|
|
1338
|
+
function collectLocalBindingFacts(fn) {
|
|
1323
1339
|
const bindings = /* @__PURE__ */ new Map();
|
|
1324
|
-
const returns = /* @__PURE__ */ new Map();
|
|
1325
1340
|
function visit(node) {
|
|
1326
1341
|
if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
|
|
1327
1342
|
return;
|
|
1328
1343
|
if (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
|
|
1329
1344
|
const fact = findConnectInExpression(node.initializer);
|
|
1330
1345
|
if (fact) bindings.set(node.name.text, fact);
|
|
1346
|
+
const call = unwrapCall(node.initializer);
|
|
1347
|
+
if (call && ts5.isPropertyAccessExpression(call.expression) && call.expression.name.text === "tx" && ts5.isIdentifier(call.expression.expression)) {
|
|
1348
|
+
const sourceName = call.expression.expression.text;
|
|
1349
|
+
const source = bindings.get(sourceName);
|
|
1350
|
+
if (source) bindings.set(node.name.text, { ...source, helperChain: [...source.helperChain ?? [], { aliasOf: sourceName, callerVariable: node.name.text, aliasKind: "transaction", transactionAliasSource: sourceName }] });
|
|
1351
|
+
}
|
|
1331
1352
|
}
|
|
1353
|
+
ts5.forEachChild(node, visit);
|
|
1354
|
+
}
|
|
1355
|
+
visit(fn);
|
|
1356
|
+
return bindings;
|
|
1357
|
+
}
|
|
1358
|
+
function collectReturnedObjectBindings(fn) {
|
|
1359
|
+
const bindings = collectLocalBindingFacts(fn);
|
|
1360
|
+
const returns = /* @__PURE__ */ new Map();
|
|
1361
|
+
function visit(node) {
|
|
1362
|
+
if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
|
|
1363
|
+
return;
|
|
1332
1364
|
if (ts5.isReturnStatement(node) && node.expression && ts5.isObjectLiteralExpression(node.expression)) {
|
|
1333
1365
|
for (const prop of node.expression.properties) {
|
|
1334
1366
|
if (ts5.isShorthandPropertyAssignment(prop)) returns.set(prop.name.text, prop.name.text);
|
|
@@ -1354,15 +1386,11 @@ function functionLikeInitializer(expr) {
|
|
|
1354
1386
|
return void 0;
|
|
1355
1387
|
}
|
|
1356
1388
|
function directReturnConnectFact(fn) {
|
|
1357
|
-
const localBindings =
|
|
1389
|
+
const localBindings = collectLocalBindingFacts(fn);
|
|
1358
1390
|
let returned;
|
|
1359
1391
|
function visit(node) {
|
|
1360
1392
|
if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
|
|
1361
1393
|
return;
|
|
1362
|
-
if (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
|
|
1363
|
-
const fact = findConnectInExpression(node.initializer);
|
|
1364
|
-
if (fact) localBindings.set(node.name.text, fact);
|
|
1365
|
-
}
|
|
1366
1394
|
if (!returned && ts5.isReturnStatement(node) && node.expression)
|
|
1367
1395
|
returned = node.expression;
|
|
1368
1396
|
if (!returned) ts5.forEachChild(node, visit);
|
|
@@ -1466,8 +1494,11 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1466
1494
|
const helperCache = /* @__PURE__ */ new Map();
|
|
1467
1495
|
const classHelpers = collectClassHelpers(sourceFileAst);
|
|
1468
1496
|
const localObjectHelpers = /* @__PURE__ */ new Map();
|
|
1497
|
+
const localDirectHelpers = /* @__PURE__ */ new Map();
|
|
1469
1498
|
for (const stmt of sourceFileAst.statements) {
|
|
1470
1499
|
if (ts5.isFunctionDeclaration(stmt) && stmt.name) {
|
|
1500
|
+
const directFact = directConnectFactFromFunctionLike(stmt);
|
|
1501
|
+
if (directFact) localDirectHelpers.set(stmt.name.text, { ...directFact, exportedName: stmt.name.text, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, stmt) });
|
|
1471
1502
|
const rows2 = [];
|
|
1472
1503
|
for (const [returnedProperty, fact] of collectReturnedObjectBindings(stmt))
|
|
1473
1504
|
rows2.push({ ...fact, exportedName: stmt.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, stmt) });
|
|
@@ -1478,6 +1509,8 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1478
1509
|
if (!ts5.isIdentifier(decl.name)) continue;
|
|
1479
1510
|
const helper = functionLikeInitializer(decl.initializer);
|
|
1480
1511
|
if (!helper) continue;
|
|
1512
|
+
const directFact = directConnectFactFromFunctionLike(helper);
|
|
1513
|
+
if (directFact) localDirectHelpers.set(decl.name.text, { ...directFact, exportedName: decl.name.text, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, decl) });
|
|
1481
1514
|
const rows2 = [];
|
|
1482
1515
|
for (const [returnedProperty, fact] of collectReturnedObjectBindings(helper))
|
|
1483
1516
|
rows2.push({ ...fact, exportedName: decl.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, decl) });
|
|
@@ -1539,7 +1572,8 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1539
1572
|
helperChain: aliasKind === "assignment" ? [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind, scopeRule: "same-file-source-order" }] : void 0
|
|
1540
1573
|
});
|
|
1541
1574
|
else if (ts5.isIdentifier(call.expression)) {
|
|
1542
|
-
const
|
|
1575
|
+
const localDirect = localDirectHelpers.get(call.expression.text);
|
|
1576
|
+
const resolved2 = localDirect ? { helper: localDirect, imp: void 0 } : await importedHelper(call.expression.text);
|
|
1543
1577
|
if (resolved2)
|
|
1544
1578
|
out.push({
|
|
1545
1579
|
variableName: targetName,
|
|
@@ -1552,12 +1586,13 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1552
1586
|
sourceFile: normalizePath(filePath),
|
|
1553
1587
|
sourceLine: lineOf4(sourceFileAst, node),
|
|
1554
1588
|
helperChain: [
|
|
1589
|
+
...resolved2.helper.helperChain ?? [],
|
|
1555
1590
|
{
|
|
1556
1591
|
callerVariable: targetName,
|
|
1557
1592
|
...aliasKind === "assignment" ? { assignedFrom: call.expression.text, aliasKind, scopeRule: "same-file-source-order" } : {},
|
|
1558
1593
|
importedHelper: call.expression.text,
|
|
1559
|
-
importSource: resolved2.imp
|
|
1560
|
-
exportedSymbol: resolved2.imp.exportedName,
|
|
1594
|
+
importSource: resolved2.imp?.sourceFile,
|
|
1595
|
+
exportedSymbol: resolved2.imp?.exportedName ?? resolved2.helper.exportedName,
|
|
1561
1596
|
helperSourceFile: resolved2.helper.sourceFile,
|
|
1562
1597
|
helperSourceLine: resolved2.helper.sourceLine
|
|
1563
1598
|
}
|
|
@@ -1597,7 +1632,7 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1597
1632
|
placeholders: resolved2.helper.placeholders,
|
|
1598
1633
|
sourceFile: normalizePath(filePath),
|
|
1599
1634
|
sourceLine: lineOf4(sourceFileAst, decl),
|
|
1600
|
-
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 }]
|
|
1635
|
+
helperChain: [...resolved2.helper.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 }]
|
|
1601
1636
|
});
|
|
1602
1637
|
}
|
|
1603
1638
|
}
|
|
@@ -1630,7 +1665,7 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1630
1665
|
placeholders: resolved2.helper.placeholders,
|
|
1631
1666
|
sourceFile: normalizePath(filePath),
|
|
1632
1667
|
sourceLine: lineOf4(sourceFileAst, node),
|
|
1633
|
-
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 }]
|
|
1668
|
+
helperChain: [...resolved2.helper.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 }]
|
|
1634
1669
|
});
|
|
1635
1670
|
}
|
|
1636
1671
|
}
|
|
@@ -1666,6 +1701,68 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1666
1701
|
});
|
|
1667
1702
|
}
|
|
1668
1703
|
}
|
|
1704
|
+
function arrayElementsFromExpression(expr) {
|
|
1705
|
+
const unwrapped = unwrapIdentityExpression(expr);
|
|
1706
|
+
if (ts5.isArrayLiteralExpression(unwrapped)) return { elements: unwrapped.elements, promiseAll: false };
|
|
1707
|
+
const call = unwrapCall(expr);
|
|
1708
|
+
if (!call) return void 0;
|
|
1709
|
+
if (!ts5.isPropertyAccessExpression(call.expression) || call.expression.name.text !== "all" || call.expression.expression.getText(sourceFileAst) !== "Promise") return void 0;
|
|
1710
|
+
const first = call.arguments[0];
|
|
1711
|
+
if (!first) return void 0;
|
|
1712
|
+
const container = unwrapIdentityExpression(first);
|
|
1713
|
+
if (!ts5.isArrayLiteralExpression(container)) return void 0;
|
|
1714
|
+
return { elements: container.elements, promiseAll: true };
|
|
1715
|
+
}
|
|
1716
|
+
async function recordArrayElementBinding(targetName, expr, node, arrayIndex, promiseAll) {
|
|
1717
|
+
const before = out.length;
|
|
1718
|
+
await recordBindingFromExpression(targetName, expr, node, "declaration");
|
|
1719
|
+
if (out.length > before) {
|
|
1720
|
+
const row = out[out.length - 1];
|
|
1721
|
+
row.helperChain = [
|
|
1722
|
+
...row.helperChain ?? [],
|
|
1723
|
+
{ callerVariable: targetName, targetVariable: targetName, arrayIndex, promiseAll, arrayContainer: promiseAll ? "Promise.all" : "array_literal" }
|
|
1724
|
+
];
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
const unwrapped = unwrapIdentityExpression(expr);
|
|
1728
|
+
if (ts5.isIdentifier(unwrapped)) {
|
|
1729
|
+
const existing = bindingForVariable(unwrapped.text);
|
|
1730
|
+
if (!existing) return;
|
|
1731
|
+
out.push({
|
|
1732
|
+
...existing,
|
|
1733
|
+
variableName: targetName,
|
|
1734
|
+
sourceLine: lineOf4(sourceFileAst, node),
|
|
1735
|
+
helperChain: [
|
|
1736
|
+
...existing.helperChain ?? [],
|
|
1737
|
+
{ callerVariable: targetName, targetVariable: targetName, sourceVariable: unwrapped.text, aliasKind: "array-destructuring", arrayIndex, promiseAll, arrayContainer: promiseAll ? "Promise.all" : "array_literal" }
|
|
1738
|
+
]
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
async function recordArrayDestructuredVariable(decl) {
|
|
1743
|
+
if (!ts5.isArrayBindingPattern(decl.name) || !decl.initializer) return;
|
|
1744
|
+
const container = arrayElementsFromExpression(decl.initializer);
|
|
1745
|
+
if (!container) return;
|
|
1746
|
+
for (let index = 0; index < decl.name.elements.length; index += 1) {
|
|
1747
|
+
const el = decl.name.elements[index];
|
|
1748
|
+
if (!el || ts5.isOmittedExpression(el) || ts5.isBindingElement(el) && el.dotDotDotToken) continue;
|
|
1749
|
+
if (!ts5.isBindingElement(el) || !ts5.isIdentifier(el.name)) continue;
|
|
1750
|
+
const source = container.elements[index];
|
|
1751
|
+
if (!source || ts5.isOmittedExpression(source)) continue;
|
|
1752
|
+
await recordArrayElementBinding(el.name.text, source, decl, index, container.promiseAll);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
async function recordArrayDestructuredAssignment(pattern, expr, node) {
|
|
1756
|
+
const container = arrayElementsFromExpression(expr);
|
|
1757
|
+
if (!container) return;
|
|
1758
|
+
for (let index = 0; index < pattern.elements.length; index += 1) {
|
|
1759
|
+
const el = pattern.elements[index];
|
|
1760
|
+
if (!el || ts5.isOmittedExpression(el) || ts5.isSpreadElement(el) || !ts5.isIdentifier(el)) continue;
|
|
1761
|
+
const source = container.elements[index];
|
|
1762
|
+
if (!source || ts5.isOmittedExpression(source)) continue;
|
|
1763
|
+
await recordArrayElementBinding(el.text, source, node, index, container.promiseAll);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1669
1766
|
const events = [];
|
|
1670
1767
|
function collectEvents(node) {
|
|
1671
1768
|
if (ts5.isVariableDeclaration(node)) events.push({ pos: node.getStart(sourceFileAst), node });
|
|
@@ -1679,6 +1776,7 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1679
1776
|
if (ts5.isVariableDeclaration(event.node)) {
|
|
1680
1777
|
const decl = event.node;
|
|
1681
1778
|
await recordDestructuredHelper(decl);
|
|
1779
|
+
await recordArrayDestructuredVariable(decl);
|
|
1682
1780
|
recordDestructuredClassHelper(decl);
|
|
1683
1781
|
await recordVariable(decl);
|
|
1684
1782
|
recordIdentityAlias(decl);
|
|
@@ -1700,6 +1798,8 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1700
1798
|
const left = ts5.isParenthesizedExpression(assignment.left) ? assignment.left.expression : assignment.left;
|
|
1701
1799
|
if (ts5.isObjectLiteralExpression(left))
|
|
1702
1800
|
await recordDestructuredAssignment(left, assignment.right, assignment);
|
|
1801
|
+
if (ts5.isArrayLiteralExpression(left))
|
|
1802
|
+
await recordArrayDestructuredAssignment(left, assignment.right, assignment);
|
|
1703
1803
|
}
|
|
1704
1804
|
return out;
|
|
1705
1805
|
}
|
|
@@ -2106,17 +2206,20 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
2106
2206
|
const destination = call.destinationExpr ?? call.requireDestination;
|
|
2107
2207
|
const isDynamic = Boolean(Number(call.isDynamic ?? 0));
|
|
2108
2208
|
const isRemoteEntityCall = callType.startsWith("remote_entity_");
|
|
2109
|
-
const
|
|
2209
|
+
const indexedOperationCandidateCount = operationCandidateCount(db, workspaceId, op, intent.topLevelOperationName);
|
|
2210
|
+
const credibleOperationSignal = Boolean(normalized?.wasInvocation) || Boolean(intent.topLevelOperationNameCandidate) && indexedOperationCandidateCount > 0;
|
|
2211
|
+
const strongEntitySignal = ["entity_media", "entity_delete", "entity_key_read", "entity_navigation_query"].includes(intent.kind) || intent.kind === "entity_mutation" && (intent.hasEntityKeyPredicate || intent.hasNavigationSuffix);
|
|
2212
|
+
const operationLikeRemoteEntity = isRemoteEntityCall && Boolean(op) && credibleOperationSignal && (!strongEntitySignal || indexedOperationCandidateCount > 0);
|
|
2110
2213
|
const isOperationCall = operationLikeRemoteEntity || (callType === "remote_action" || callType === "local_service_call" || callType === "remote_query" && Boolean(op));
|
|
2111
2214
|
const resolution = isOperationCall ? resolveOperation(db, { servicePath, operationPath: op, serviceName: call.local_service_name, repoId: callType === "local_service_call" ? Number(call.repo_id) : void 0, alias: applyVariables(call.aliasExpr ?? call.alias, vars), destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 || callType === "local_service_call" }, workspaceId) : { status: "unresolved", candidates: [], reasons: [] };
|
|
2112
|
-
const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0, normalized, intent);
|
|
2215
|
+
const evidence = { ...callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0, normalized, intent), indexedOperationCandidateCount, parserCallType: callType };
|
|
2113
2216
|
if (isRemoteEntityCall && (resolution.target || resolution.candidates.length > 0 || resolution.status === "dynamic")) {
|
|
2114
2217
|
if (resolution.target) {
|
|
2115
2218
|
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, "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify({ ...evidence, operationEntityPrecedence: "indexed_operation_over_parser_entity" }), 0, generation);
|
|
2116
2219
|
return { status: "resolved", callType };
|
|
2117
2220
|
}
|
|
2118
2221
|
const status2 = resolution.status === "dynamic" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : "unresolved";
|
|
2119
|
-
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, status2 === "dynamic" ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE", status2, "call", String(call.id), "operation_candidate", op ? `Remote action: ${op}` : "Remote action: unknown path", Number(call.confidence ?? 0.2), JSON.stringify({ ...evidence, operationEntityPrecedence: "parser_entity_with_indexed_operation_candidates" }), status2 === "dynamic" ? 1 : 0, unresolvedOperationReason(resolution), generation);
|
|
2222
|
+
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, status2 === "dynamic" ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE", status2, "call", String(call.id), "operation_candidate", op ? `Remote action: ${op}` : "Remote action: unknown path", Number(call.confidence ?? 0.2), JSON.stringify({ ...evidence, operationEntityPrecedence: resolution.candidates.length > 0 ? "parser_entity_with_indexed_operation_candidates" : "parser_entity_operation_candidate_without_indexed_match" }), status2 === "dynamic" ? 1 : 0, unresolvedOperationReason(resolution), generation);
|
|
2120
2223
|
return { status: status2, callType };
|
|
2121
2224
|
}
|
|
2122
2225
|
if (isRemoteEntityCall) {
|
|
@@ -2149,6 +2252,12 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
2149
2252
|
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);
|
|
2150
2253
|
return { status, callType };
|
|
2151
2254
|
}
|
|
2255
|
+
function operationCandidateCount(db, workspaceId, operationPath, operationName) {
|
|
2256
|
+
if (!operationPath && !operationName) return 0;
|
|
2257
|
+
const normalizedName = operationName ?? operationPath?.replace(/^\//, "").split(".").at(-1);
|
|
2258
|
+
const row = db.prepare(`SELECT COUNT(*) count 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=? AND (o.operation_path=? OR o.operation_path=? OR o.operation_name=?)`).get(workspaceId, operationPath, normalizedName ? `/${normalizedName}` : operationPath, normalizedName);
|
|
2259
|
+
return Number(row?.count ?? 0);
|
|
2260
|
+
}
|
|
2152
2261
|
function unresolvedOperationReason(resolution) {
|
|
2153
2262
|
if (resolution.status === "dynamic") return `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ["missing runtime variables"]).join(", ")}`;
|
|
2154
2263
|
if (resolution.candidates.length === 0) return "No indexed target operation matched";
|
|
@@ -2569,6 +2678,11 @@ function handlerScope(db, methodId) {
|
|
|
2569
2678
|
if (!row) return void 0;
|
|
2570
2679
|
return { repoId: row.repoId, files: new Set(row.sourceFile ? [row.sourceFile] : []), symbolId: row.symbolId };
|
|
2571
2680
|
}
|
|
2681
|
+
function traceEdgeType(call, row) {
|
|
2682
|
+
if (row.to_kind === "operation" && row.edge_type === "REMOTE_CALL_RESOLVES_TO_OPERATION") return "remote_action";
|
|
2683
|
+
if (row.to_kind === "operation" && row.edge_type === "LOCAL_CALL_RESOLVES_TO_OPERATION") return "local_service_call";
|
|
2684
|
+
return String(call.call_type);
|
|
2685
|
+
}
|
|
2572
2686
|
function includeCall(type, options) {
|
|
2573
2687
|
if (!options.includeDb && type === "local_db_query") return false;
|
|
2574
2688
|
if (!options.includeExternal && type === "external_http") return false;
|
|
@@ -2862,7 +2976,7 @@ function trace(db, start, options) {
|
|
|
2862
2976
|
const to = edgeTarget(effectiveRow, evidence);
|
|
2863
2977
|
edges.push({
|
|
2864
2978
|
step: current.depth,
|
|
2865
|
-
type:
|
|
2979
|
+
type: traceEdgeType(call, effectiveRow),
|
|
2866
2980
|
from: `${call.repoName}:${call.source_file}:${call.source_line}`,
|
|
2867
2981
|
to,
|
|
2868
2982
|
evidence,
|
|
@@ -2934,6 +3048,7 @@ export {
|
|
|
2934
3048
|
redactText,
|
|
2935
3049
|
redactValue,
|
|
2936
3050
|
normalizeODataOperationInvocationPath,
|
|
3051
|
+
classifyODataPathIntent,
|
|
2937
3052
|
containsSupportedOutboundCall,
|
|
2938
3053
|
parseOutboundCalls,
|
|
2939
3054
|
parseServiceBindings,
|
|
@@ -2943,4 +3058,4 @@ export {
|
|
|
2943
3058
|
linkWorkspace,
|
|
2944
3059
|
trace
|
|
2945
3060
|
};
|
|
2946
|
-
//# sourceMappingURL=chunk-
|
|
3061
|
+
//# sourceMappingURL=chunk-2UOIY2NI.js.map
|