@fairfox/polly 0.4.2 → 0.5.1

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.
@@ -1314,16 +1314,353 @@ class IntegrationAnalyzer {
1314
1314
  }
1315
1315
 
1316
1316
  // vendor/analysis/src/extract/handlers.ts
1317
- import { Project as Project4, SyntaxKind as SyntaxKind2, Node as Node4 } from "ts-morph";
1317
+ import { Project as Project4, SyntaxKind as SyntaxKind3, Node as Node5 } from "ts-morph";
1318
1318
 
1319
+ // vendor/analysis/src/extract/relationships.ts
1320
+ import { Node as Node4 } from "ts-morph";
1321
+
1322
+ class RelationshipExtractor {
1323
+ extractFromHandler(handlerNode, sourceFile, handlerName) {
1324
+ const relationships = [];
1325
+ const visited = new Set;
1326
+ this.extractFromNode(handlerNode, sourceFile, handlerName, relationships, visited);
1327
+ return this.deduplicateRelationships(relationships);
1328
+ }
1329
+ extractFromNode(node, sourceFile, handlerName, relationships, visited) {
1330
+ node.forEachDescendant((descendant) => {
1331
+ if (Node4.isCallExpression(descendant)) {
1332
+ const expr = descendant.getExpression();
1333
+ if (Node4.isIdentifier(expr)) {
1334
+ const functionName = expr.getText();
1335
+ let functionDecl = sourceFile.getFunction(functionName);
1336
+ let targetSourceFile = sourceFile;
1337
+ if (!functionDecl) {
1338
+ const resolved = this.resolveImportedFunction(functionName, sourceFile);
1339
+ if (resolved) {
1340
+ functionDecl = resolved.functionDecl;
1341
+ targetSourceFile = resolved.sourceFile;
1342
+ }
1343
+ }
1344
+ if (functionDecl && !visited.has(functionName)) {
1345
+ visited.add(functionName);
1346
+ const body = functionDecl.getBody();
1347
+ if (body) {
1348
+ this.extractFromNode(body, targetSourceFile, handlerName, relationships, visited);
1349
+ }
1350
+ return;
1351
+ }
1352
+ if (!functionDecl) {
1353
+ const componentFromName = this.inferComponentFromFunctionName(functionName);
1354
+ if (componentFromName) {
1355
+ relationships.push({
1356
+ from: this.toComponentId(handlerName),
1357
+ to: componentFromName,
1358
+ description: `Calls ${functionName}()`,
1359
+ technology: "Function Call",
1360
+ confidence: "medium",
1361
+ evidence: [`Function call: ${functionName}`]
1362
+ });
1363
+ return;
1364
+ }
1365
+ }
1366
+ }
1367
+ if (Node4.isPropertyAccessExpression(expr)) {
1368
+ const rel2 = this.extractFromPropertyAccess(expr, handlerName);
1369
+ if (rel2) {
1370
+ relationships.push(rel2);
1371
+ return;
1372
+ }
1373
+ }
1374
+ const rel = this.extractFromFunctionCall(descendant, handlerName, sourceFile);
1375
+ if (rel) {
1376
+ relationships.push(rel);
1377
+ }
1378
+ }
1379
+ if (Node4.isAwaitExpression(descendant)) {
1380
+ const rel = this.extractFromDatabaseCall(descendant, handlerName);
1381
+ if (rel) {
1382
+ relationships.push(rel);
1383
+ }
1384
+ }
1385
+ if (Node4.isCallExpression(descendant) && descendant.getExpression().getText() === "fetch") {
1386
+ const rel = this.extractFromFetchCall(descendant, handlerName);
1387
+ if (rel) {
1388
+ relationships.push(rel);
1389
+ }
1390
+ }
1391
+ });
1392
+ }
1393
+ extractFromFunctionCall(callExpr, handlerName, sourceFile) {
1394
+ const expr = callExpr.getExpression();
1395
+ const exprText = expr.getText();
1396
+ const skipList = [
1397
+ "console.",
1398
+ "JSON.",
1399
+ "Math.",
1400
+ "Object.",
1401
+ "Array.",
1402
+ "String.",
1403
+ "Number.",
1404
+ "Date.",
1405
+ "Promise.",
1406
+ "setTimeout",
1407
+ "setInterval",
1408
+ "clearTimeout",
1409
+ "clearInterval"
1410
+ ];
1411
+ if (skipList.some((skip) => exprText.startsWith(skip))) {
1412
+ return null;
1413
+ }
1414
+ let functionName = exprText;
1415
+ let targetComponent = null;
1416
+ if (Node4.isPropertyAccessExpression(expr)) {
1417
+ const objectExpr = expr.getExpression();
1418
+ const objectName = objectExpr.getText();
1419
+ const methodName = expr.getName();
1420
+ targetComponent = this.inferComponentFromCall(objectName, methodName);
1421
+ if (!targetComponent) {
1422
+ return null;
1423
+ }
1424
+ functionName = methodName;
1425
+ } else {
1426
+ targetComponent = this.resolveComponentFromImport(exprText, sourceFile);
1427
+ if (!targetComponent) {
1428
+ return null;
1429
+ }
1430
+ }
1431
+ return {
1432
+ from: this.toComponentId(handlerName),
1433
+ to: targetComponent,
1434
+ description: `Calls ${functionName}()`,
1435
+ technology: "Function Call",
1436
+ confidence: "high",
1437
+ evidence: [`Function call: ${exprText}`]
1438
+ };
1439
+ }
1440
+ extractFromPropertyAccess(propAccess, handlerName) {
1441
+ if (!Node4.isPropertyAccessExpression(propAccess)) {
1442
+ return null;
1443
+ }
1444
+ const fullChain = propAccess.getText();
1445
+ const methodName = propAccess.getName();
1446
+ let rootObject = propAccess.getExpression();
1447
+ while (Node4.isPropertyAccessExpression(rootObject)) {
1448
+ rootObject = rootObject.getExpression();
1449
+ }
1450
+ const objectName = rootObject.getText();
1451
+ const targetComponent = this.inferComponentFromCall(objectName, methodName);
1452
+ if (!targetComponent) {
1453
+ return null;
1454
+ }
1455
+ return {
1456
+ from: this.toComponentId(handlerName),
1457
+ to: targetComponent,
1458
+ description: `Calls ${methodName}()`,
1459
+ technology: "Function Call",
1460
+ confidence: "high",
1461
+ evidence: [`Property access: ${fullChain}`]
1462
+ };
1463
+ }
1464
+ extractFromDatabaseCall(awaitExpr, handlerName) {
1465
+ if (!Node4.isAwaitExpression(awaitExpr)) {
1466
+ return null;
1467
+ }
1468
+ const innerExpr = awaitExpr.getExpression();
1469
+ if (!Node4.isCallExpression(innerExpr)) {
1470
+ return null;
1471
+ }
1472
+ const callExpr = innerExpr.getExpression().getText();
1473
+ if (callExpr.includes("db.query") || callExpr.includes("db.execute") || callExpr.includes("db.select") || callExpr.includes("db.insert") || callExpr.includes("db.update") || callExpr.includes("db.delete")) {
1474
+ const operation = this.inferDatabaseOperation(callExpr);
1475
+ return {
1476
+ from: this.toComponentId(handlerName),
1477
+ to: "database",
1478
+ description: operation,
1479
+ technology: "SQL",
1480
+ confidence: "high",
1481
+ evidence: [`Database call: ${callExpr}`]
1482
+ };
1483
+ }
1484
+ return null;
1485
+ }
1486
+ extractFromFetchCall(callExpr, handlerName) {
1487
+ const args = callExpr.getArguments();
1488
+ if (args.length === 0) {
1489
+ return null;
1490
+ }
1491
+ const urlArg = args[0].getText();
1492
+ let apiName = "external_api";
1493
+ if (urlArg.includes("openai")) {
1494
+ apiName = "openai_api";
1495
+ } else if (urlArg.includes("anthropic")) {
1496
+ apiName = "anthropic_api";
1497
+ }
1498
+ return {
1499
+ from: this.toComponentId(handlerName),
1500
+ to: apiName,
1501
+ description: "Calls external API",
1502
+ technology: "HTTP/REST",
1503
+ confidence: "high",
1504
+ evidence: [`fetch() call to: ${urlArg}`]
1505
+ };
1506
+ }
1507
+ inferComponentFromCall(objectName, methodName) {
1508
+ const mappings = {
1509
+ db: "db_client",
1510
+ database: "database",
1511
+ repos: "repositories",
1512
+ repository: "repositories",
1513
+ repositories: "repositories",
1514
+ cache: "cache",
1515
+ storage: "storage",
1516
+ ai: "ai_service",
1517
+ auth: "auth_service",
1518
+ authservice: "auth_service",
1519
+ user: "user_service",
1520
+ userservice: "user_service",
1521
+ logger: "logger",
1522
+ queue: "queue_service"
1523
+ };
1524
+ const normalized = objectName.toLowerCase();
1525
+ return mappings[normalized] || null;
1526
+ }
1527
+ inferComponentFromFunctionName(functionName) {
1528
+ const normalized = functionName.toLowerCase();
1529
+ if (normalized.startsWith("get") || normalized.startsWith("create")) {
1530
+ const suffix = functionName.substring(normalized.startsWith("get") ? 3 : 6);
1531
+ const suffixLower = suffix.toLowerCase();
1532
+ if (suffixLower.includes("database") || suffixLower === "db" || suffixLower.includes("dbconnection") || suffixLower.includes("connection")) {
1533
+ return "db_client";
1534
+ }
1535
+ if (suffixLower.includes("repositories") || suffixLower.includes("repos") || suffixLower.includes("repository")) {
1536
+ return "repositories";
1537
+ }
1538
+ if (suffixLower.includes("service")) {
1539
+ const serviceMatch = suffix.match(/^(.+?)Service/i);
1540
+ if (serviceMatch) {
1541
+ return this.toComponentId(`${serviceMatch[1]}_service`);
1542
+ }
1543
+ return "service";
1544
+ }
1545
+ if (suffixLower.includes("cache")) {
1546
+ return "cache";
1547
+ }
1548
+ if (suffixLower.includes("storage")) {
1549
+ return "storage";
1550
+ }
1551
+ if (suffixLower.includes("ai") || suffixLower.includes("llm")) {
1552
+ return "ai_service";
1553
+ }
1554
+ if (suffixLower.includes("logger")) {
1555
+ return "logger";
1556
+ }
1557
+ }
1558
+ if (normalized.startsWith("init") || normalized.startsWith("setup")) {
1559
+ const suffix = functionName.substring(normalized.startsWith("init") ? 4 : 5);
1560
+ const suffixLower = suffix.toLowerCase();
1561
+ if (suffixLower.includes("database") || suffixLower === "db") {
1562
+ return "db_client";
1563
+ }
1564
+ if (suffixLower.includes("cache")) {
1565
+ return "cache";
1566
+ }
1567
+ }
1568
+ return null;
1569
+ }
1570
+ resolveComponentFromImport(functionName, sourceFile) {
1571
+ for (const importDecl of sourceFile.getImportDeclarations()) {
1572
+ const namedImports = importDecl.getNamedImports();
1573
+ for (const namedImport of namedImports) {
1574
+ if (namedImport.getName() === functionName) {
1575
+ const modulePath = importDecl.getModuleSpecifierValue();
1576
+ if (modulePath.includes("/db/") || modulePath.includes("/database/")) {
1577
+ return "db_client";
1578
+ }
1579
+ if (modulePath.includes("/repos") || modulePath.includes("/repositories")) {
1580
+ return "repositories";
1581
+ }
1582
+ if (modulePath.includes("/service") || modulePath.includes("/services")) {
1583
+ const match = modulePath.match(/\/([^/]+)\.ts$/);
1584
+ if (match) {
1585
+ return this.toComponentId(match[1]);
1586
+ }
1587
+ }
1588
+ }
1589
+ }
1590
+ }
1591
+ return null;
1592
+ }
1593
+ resolveImportedFunction(functionName, sourceFile) {
1594
+ try {
1595
+ for (const importDecl of sourceFile.getImportDeclarations()) {
1596
+ const namedImports = importDecl.getNamedImports();
1597
+ for (const namedImport of namedImports) {
1598
+ if (namedImport.getName() === functionName) {
1599
+ const moduleSpecifier = importDecl.getModuleSpecifierSourceFile();
1600
+ if (!moduleSpecifier)
1601
+ continue;
1602
+ const functionDecl = moduleSpecifier.getFunction(functionName);
1603
+ if (functionDecl) {
1604
+ return {
1605
+ functionDecl,
1606
+ sourceFile: moduleSpecifier
1607
+ };
1608
+ }
1609
+ const variableDecl = moduleSpecifier.getVariableDeclaration(functionName);
1610
+ if (variableDecl) {
1611
+ const initializer = variableDecl.getInitializer();
1612
+ if (initializer && (Node4.isArrowFunction(initializer) || Node4.isFunctionExpression(initializer))) {
1613
+ return {
1614
+ functionDecl: initializer,
1615
+ sourceFile: moduleSpecifier
1616
+ };
1617
+ }
1618
+ }
1619
+ }
1620
+ }
1621
+ }
1622
+ } catch (error) {
1623
+ return null;
1624
+ }
1625
+ return null;
1626
+ }
1627
+ inferDatabaseOperation(callExpr) {
1628
+ if (callExpr.includes("query") || callExpr.includes("select")) {
1629
+ return "Reads from database";
1630
+ }
1631
+ if (callExpr.includes("execute") || callExpr.includes("insert") || callExpr.includes("update") || callExpr.includes("delete")) {
1632
+ return "Writes to database";
1633
+ }
1634
+ return "Accesses database";
1635
+ }
1636
+ toComponentId(name) {
1637
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1638
+ }
1639
+ deduplicateRelationships(relationships) {
1640
+ const seen = new Set;
1641
+ const unique = [];
1642
+ for (const rel of relationships) {
1643
+ const key = `${rel.from}->${rel.to}`;
1644
+ if (!seen.has(key)) {
1645
+ seen.add(key);
1646
+ unique.push(rel);
1647
+ }
1648
+ }
1649
+ return unique;
1650
+ }
1651
+ }
1652
+
1653
+ // vendor/analysis/src/extract/handlers.ts
1319
1654
  class HandlerExtractor {
1320
1655
  project;
1321
1656
  typeGuardCache;
1657
+ relationshipExtractor;
1322
1658
  constructor(tsConfigPath) {
1323
1659
  this.project = new Project4({
1324
1660
  tsConfigFilePath: tsConfigPath
1325
1661
  });
1326
1662
  this.typeGuardCache = new WeakMap;
1663
+ this.relationshipExtractor = new RelationshipExtractor;
1327
1664
  }
1328
1665
  extractHandlers() {
1329
1666
  const handlers = [];
@@ -1357,9 +1694,9 @@ class HandlerExtractor {
1357
1694
  const filePath = sourceFile.getFilePath();
1358
1695
  const context = this.inferContext(filePath);
1359
1696
  sourceFile.forEachDescendant((node) => {
1360
- if (Node4.isCallExpression(node)) {
1697
+ if (Node5.isCallExpression(node)) {
1361
1698
  const expression = node.getExpression();
1362
- if (Node4.isPropertyAccessExpression(expression)) {
1699
+ if (Node5.isPropertyAccessExpression(expression)) {
1363
1700
  const methodName = expression.getName();
1364
1701
  if (methodName === "on" || methodName === "addEventListener") {
1365
1702
  const handler = this.extractHandler(node, context, filePath);
@@ -1369,17 +1706,17 @@ class HandlerExtractor {
1369
1706
  }
1370
1707
  }
1371
1708
  }
1372
- if (Node4.isSwitchStatement(node)) {
1709
+ if (Node5.isSwitchStatement(node)) {
1373
1710
  const switchHandlers = this.extractSwitchCaseHandlers(node, context, filePath);
1374
1711
  handlers.push(...switchHandlers);
1375
1712
  }
1376
- if (Node4.isVariableDeclaration(node)) {
1713
+ if (Node5.isVariableDeclaration(node)) {
1377
1714
  const mapHandlers = this.extractHandlerMapPattern(node, context, filePath);
1378
1715
  handlers.push(...mapHandlers);
1379
1716
  }
1380
- if (Node4.isIfStatement(node)) {
1717
+ if (Node5.isIfStatement(node)) {
1381
1718
  const parent = node.getParent();
1382
- const isElseIf = parent && Node4.isIfStatement(parent);
1719
+ const isElseIf = parent && Node5.isIfStatement(parent);
1383
1720
  if (!isElseIf) {
1384
1721
  const typeGuardHandlers = this.extractTypeGuardHandlers(node, context, filePath);
1385
1722
  handlers.push(...typeGuardHandlers);
@@ -1395,9 +1732,9 @@ class HandlerExtractor {
1395
1732
  }
1396
1733
  const messageTypeArg = args[0];
1397
1734
  let messageType = null;
1398
- if (Node4.isStringLiteral(messageTypeArg)) {
1735
+ if (Node5.isStringLiteral(messageTypeArg)) {
1399
1736
  messageType = messageTypeArg.getLiteralValue();
1400
- } else if (Node4.isTemplateExpression(messageTypeArg)) {
1737
+ } else if (Node5.isTemplateExpression(messageTypeArg)) {
1401
1738
  messageType = messageTypeArg.getText().replace(/[`'"]/g, "");
1402
1739
  }
1403
1740
  if (!messageType) {
@@ -1407,11 +1744,20 @@ class HandlerExtractor {
1407
1744
  const assignments = [];
1408
1745
  const preconditions = [];
1409
1746
  const postconditions = [];
1410
- if (Node4.isArrowFunction(handlerArg) || Node4.isFunctionExpression(handlerArg)) {
1747
+ if (Node5.isArrowFunction(handlerArg) || Node5.isFunctionExpression(handlerArg)) {
1411
1748
  this.extractAssignments(handlerArg, assignments);
1412
1749
  this.extractVerificationConditions(handlerArg, preconditions, postconditions);
1413
1750
  }
1414
1751
  const line = callExpr.getStartLineNumber();
1752
+ const sourceFile = callExpr.getSourceFile();
1753
+ const handlerName = `${messageType}_handler`;
1754
+ let relationships = undefined;
1755
+ if (Node5.isArrowFunction(handlerArg) || Node5.isFunctionExpression(handlerArg)) {
1756
+ const detectedRelationships = this.relationshipExtractor.extractFromHandler(handlerArg, sourceFile, handlerName);
1757
+ if (detectedRelationships.length > 0) {
1758
+ relationships = detectedRelationships;
1759
+ }
1760
+ }
1415
1761
  return {
1416
1762
  messageType,
1417
1763
  node: context,
@@ -1421,17 +1767,18 @@ class HandlerExtractor {
1421
1767
  location: {
1422
1768
  file: filePath,
1423
1769
  line
1424
- }
1770
+ },
1771
+ relationships
1425
1772
  };
1426
1773
  }
1427
1774
  extractAssignments(funcNode, assignments) {
1428
1775
  funcNode.forEachDescendant((node) => {
1429
- if (Node4.isBinaryExpression(node)) {
1776
+ if (Node5.isBinaryExpression(node)) {
1430
1777
  const operator = node.getOperatorToken().getText();
1431
1778
  if (operator === "=") {
1432
1779
  const left = node.getLeft();
1433
1780
  const right = node.getRight();
1434
- if (Node4.isPropertyAccessExpression(left)) {
1781
+ if (Node5.isPropertyAccessExpression(left)) {
1435
1782
  const fieldPath = this.getPropertyPath(left);
1436
1783
  if (fieldPath.startsWith("state.")) {
1437
1784
  const field = fieldPath.substring(6);
@@ -1450,13 +1797,13 @@ class HandlerExtractor {
1450
1797
  }
1451
1798
  extractVerificationConditions(funcNode, preconditions, postconditions) {
1452
1799
  const body = funcNode.getBody();
1453
- const statements = Node4.isBlock(body) ? body.getStatements() : [body];
1800
+ const statements = Node5.isBlock(body) ? body.getStatements() : [body];
1454
1801
  statements.forEach((statement, index) => {
1455
- if (Node4.isExpressionStatement(statement)) {
1802
+ if (Node5.isExpressionStatement(statement)) {
1456
1803
  const expr = statement.getExpression();
1457
- if (Node4.isCallExpression(expr)) {
1804
+ if (Node5.isCallExpression(expr)) {
1458
1805
  const callee = expr.getExpression();
1459
- if (Node4.isIdentifier(callee)) {
1806
+ if (Node5.isIdentifier(callee)) {
1460
1807
  const functionName = callee.getText();
1461
1808
  if (functionName === "requires") {
1462
1809
  const condition = this.extractCondition(expr);
@@ -1482,7 +1829,7 @@ class HandlerExtractor {
1482
1829
  const conditionArg = args[0];
1483
1830
  const expression = conditionArg.getText();
1484
1831
  let message;
1485
- if (args.length >= 2 && Node4.isStringLiteral(args[1])) {
1832
+ if (args.length >= 2 && Node5.isStringLiteral(args[1])) {
1486
1833
  message = args[1].getLiteralValue();
1487
1834
  }
1488
1835
  const line = callExpr.getStartLineNumber();
@@ -1499,29 +1846,29 @@ class HandlerExtractor {
1499
1846
  getPropertyPath(node) {
1500
1847
  const parts = [];
1501
1848
  let current = node;
1502
- while (Node4.isPropertyAccessExpression(current)) {
1849
+ while (Node5.isPropertyAccessExpression(current)) {
1503
1850
  parts.unshift(current.getName());
1504
1851
  current = current.getExpression();
1505
1852
  }
1506
- if (Node4.isIdentifier(current)) {
1853
+ if (Node5.isIdentifier(current)) {
1507
1854
  parts.unshift(current.getText());
1508
1855
  }
1509
1856
  return parts.join(".");
1510
1857
  }
1511
1858
  extractValue(node) {
1512
- if (Node4.isStringLiteral(node)) {
1859
+ if (Node5.isStringLiteral(node)) {
1513
1860
  return node.getLiteralValue();
1514
1861
  }
1515
- if (Node4.isNumericLiteral(node)) {
1862
+ if (Node5.isNumericLiteral(node)) {
1516
1863
  return node.getLiteralValue();
1517
1864
  }
1518
- if (node.getKind() === SyntaxKind2.TrueKeyword) {
1865
+ if (node.getKind() === SyntaxKind3.TrueKeyword) {
1519
1866
  return true;
1520
1867
  }
1521
- if (node.getKind() === SyntaxKind2.FalseKeyword) {
1868
+ if (node.getKind() === SyntaxKind3.FalseKeyword) {
1522
1869
  return false;
1523
1870
  }
1524
- if (node.getKind() === SyntaxKind2.NullKeyword) {
1871
+ if (node.getKind() === SyntaxKind3.NullKeyword) {
1525
1872
  return null;
1526
1873
  }
1527
1874
  return;
@@ -1536,10 +1883,10 @@ class HandlerExtractor {
1536
1883
  }
1537
1884
  const caseClauses = switchNode.getClauses();
1538
1885
  for (const clause of caseClauses) {
1539
- if (Node4.isCaseClause(clause)) {
1886
+ if (Node5.isCaseClause(clause)) {
1540
1887
  const caseExpr = clause.getExpression();
1541
1888
  let messageType = null;
1542
- if (Node4.isStringLiteral(caseExpr)) {
1889
+ if (Node5.isStringLiteral(caseExpr)) {
1543
1890
  messageType = caseExpr.getLiteralValue();
1544
1891
  }
1545
1892
  if (messageType) {
@@ -1562,7 +1909,7 @@ class HandlerExtractor {
1562
1909
  const handlers = [];
1563
1910
  try {
1564
1911
  const initializer = varDecl.getInitializer();
1565
- if (!initializer || !Node4.isObjectLiteralExpression(initializer)) {
1912
+ if (!initializer || !Node5.isObjectLiteralExpression(initializer)) {
1566
1913
  return handlers;
1567
1914
  }
1568
1915
  const varName = varDecl.getName().toLowerCase();
@@ -1571,12 +1918,12 @@ class HandlerExtractor {
1571
1918
  }
1572
1919
  const properties = initializer.getProperties();
1573
1920
  for (const prop of properties) {
1574
- if (Node4.isPropertyAssignment(prop)) {
1921
+ if (Node5.isPropertyAssignment(prop)) {
1575
1922
  const nameNode = prop.getNameNode();
1576
1923
  let messageType = null;
1577
- if (Node4.isStringLiteral(nameNode)) {
1924
+ if (Node5.isStringLiteral(nameNode)) {
1578
1925
  messageType = nameNode.getLiteralValue();
1579
- } else if (Node4.isIdentifier(nameNode)) {
1926
+ } else if (Node5.isIdentifier(nameNode)) {
1580
1927
  messageType = nameNode.getText();
1581
1928
  }
1582
1929
  if (messageType) {
@@ -1623,7 +1970,7 @@ class HandlerExtractor {
1623
1970
  }
1624
1971
  }
1625
1972
  const elseStatement = currentIf.getElseStatement();
1626
- if (elseStatement && Node4.isIfStatement(elseStatement)) {
1973
+ if (elseStatement && Node5.isIfStatement(elseStatement)) {
1627
1974
  currentIf = elseStatement;
1628
1975
  } else {
1629
1976
  break;
@@ -1639,12 +1986,12 @@ class HandlerExtractor {
1639
1986
  extractHandlerFromIfClause(ifNode, typeGuards, context, filePath) {
1640
1987
  try {
1641
1988
  const condition = ifNode.getExpression();
1642
- if (!Node4.isCallExpression(condition)) {
1989
+ if (!Node5.isCallExpression(condition)) {
1643
1990
  return null;
1644
1991
  }
1645
1992
  const funcExpr = condition.getExpression();
1646
1993
  let funcName;
1647
- if (Node4.isIdentifier(funcExpr)) {
1994
+ if (Node5.isIdentifier(funcExpr)) {
1648
1995
  funcName = funcExpr.getText();
1649
1996
  }
1650
1997
  if (process.env.POLLY_DEBUG && funcName) {
@@ -1656,7 +2003,7 @@ class HandlerExtractor {
1656
2003
  if (process.env.POLLY_DEBUG) {
1657
2004
  console.log(`[DEBUG] Found in local type guards: ${funcName} → ${messageType}`);
1658
2005
  }
1659
- } else if (Node4.isIdentifier(funcExpr)) {
2006
+ } else if (Node5.isIdentifier(funcExpr)) {
1660
2007
  if (process.env.POLLY_DEBUG) {
1661
2008
  console.log(`[DEBUG] Not found locally, trying import resolution for: ${funcName}`);
1662
2009
  }
@@ -1669,13 +2016,24 @@ class HandlerExtractor {
1669
2016
  return null;
1670
2017
  }
1671
2018
  const line = ifNode.getStartLineNumber();
2019
+ const sourceFile = ifNode.getSourceFile();
2020
+ const handlerName = `${messageType}_handler`;
2021
+ let relationships = undefined;
2022
+ const thenStatement = ifNode.getThenStatement();
2023
+ if (thenStatement) {
2024
+ const detectedRelationships = this.relationshipExtractor.extractFromHandler(thenStatement, sourceFile, handlerName);
2025
+ if (detectedRelationships.length > 0) {
2026
+ relationships = detectedRelationships;
2027
+ }
2028
+ }
1672
2029
  return {
1673
2030
  messageType,
1674
2031
  node: context,
1675
2032
  assignments: [],
1676
2033
  preconditions: [],
1677
2034
  postconditions: [],
1678
- location: { file: filePath, line }
2035
+ location: { file: filePath, line },
2036
+ relationships
1679
2037
  };
1680
2038
  } catch (error) {
1681
2039
  return null;
@@ -1684,20 +2042,20 @@ class HandlerExtractor {
1684
2042
  findTypePredicateFunctions(sourceFile) {
1685
2043
  const typeGuards = new Map;
1686
2044
  sourceFile.forEachDescendant((node) => {
1687
- if (Node4.isFunctionDeclaration(node) || Node4.isFunctionExpression(node) || Node4.isArrowFunction(node)) {
2045
+ if (Node5.isFunctionDeclaration(node) || Node5.isFunctionExpression(node) || Node5.isArrowFunction(node)) {
1688
2046
  const returnTypeNode = node.getReturnTypeNode();
1689
- if (returnTypeNode && Node4.isTypePredicate(returnTypeNode)) {
2047
+ if (returnTypeNode && Node5.isTypePredicate(returnTypeNode)) {
1690
2048
  let functionName;
1691
- if (Node4.isFunctionDeclaration(node)) {
2049
+ if (Node5.isFunctionDeclaration(node)) {
1692
2050
  functionName = node.getName();
1693
- } else if (Node4.isFunctionExpression(node)) {
2051
+ } else if (Node5.isFunctionExpression(node)) {
1694
2052
  const parent = node.getParent();
1695
- if (Node4.isVariableDeclaration(parent)) {
2053
+ if (Node5.isVariableDeclaration(parent)) {
1696
2054
  functionName = parent.getName();
1697
2055
  }
1698
- } else if (Node4.isArrowFunction(node)) {
2056
+ } else if (Node5.isArrowFunction(node)) {
1699
2057
  const parent = node.getParent();
1700
- if (Node4.isVariableDeclaration(parent)) {
2058
+ if (Node5.isVariableDeclaration(parent)) {
1701
2059
  functionName = parent.getName();
1702
2060
  }
1703
2061
  }
@@ -1738,15 +2096,15 @@ class HandlerExtractor {
1738
2096
  return null;
1739
2097
  }
1740
2098
  for (const def of definitions) {
1741
- if (Node4.isFunctionDeclaration(def) || Node4.isFunctionExpression(def) || Node4.isArrowFunction(def)) {
2099
+ if (Node5.isFunctionDeclaration(def) || Node5.isFunctionExpression(def) || Node5.isArrowFunction(def)) {
1742
2100
  const returnTypeNode = def.getReturnTypeNode();
1743
2101
  if (process.env.POLLY_DEBUG) {
1744
2102
  const returnType = def.getReturnType().getText();
1745
2103
  console.log(`[DEBUG] Function ${funcName} return type (resolved): ${returnType}`);
1746
2104
  console.log(`[DEBUG] Has return type node: ${!!returnTypeNode}`);
1747
- console.log(`[DEBUG] Is type predicate node: ${returnTypeNode && Node4.isTypePredicate(returnTypeNode)}`);
2105
+ console.log(`[DEBUG] Is type predicate node: ${returnTypeNode && Node5.isTypePredicate(returnTypeNode)}`);
1748
2106
  }
1749
- if (returnTypeNode && Node4.isTypePredicate(returnTypeNode)) {
2107
+ if (returnTypeNode && Node5.isTypePredicate(returnTypeNode)) {
1750
2108
  const typeNode = returnTypeNode.getTypeNode();
1751
2109
  if (typeNode) {
1752
2110
  const typeName = typeNode.getText();
@@ -2288,10 +2646,33 @@ class StructurizrDSLGenerator {
2288
2646
  if (this.options.groups && this.options.groups.length > 0) {
2289
2647
  parts.push(...this.generateGroupedComponents(componentDefs, this.options.groups));
2290
2648
  } else {
2291
- for (const comp of componentDefs) {
2292
- parts.push(this.generateComponentDefinition(comp, " "));
2649
+ const autoGroups = this.generateAutomaticGroups(componentDefs);
2650
+ if (autoGroups.length > 0) {
2651
+ parts.push(...this.generateGroupedComponents(componentDefs, autoGroups));
2652
+ } else {
2653
+ for (const comp of componentDefs) {
2654
+ parts.push(this.generateComponentDefinition(comp, " "));
2655
+ }
2293
2656
  }
2294
2657
  }
2658
+ const serviceComponents = new Set;
2659
+ for (const handler of contextInfo.handlers) {
2660
+ if (handler.relationships) {
2661
+ for (const rel of handler.relationships) {
2662
+ const targetId = this.toId(rel.to);
2663
+ const isHandler = componentDefs.some((c) => c.id === targetId);
2664
+ if (!isHandler) {
2665
+ serviceComponents.add(rel.to);
2666
+ }
2667
+ }
2668
+ }
2669
+ }
2670
+ for (const serviceId of serviceComponents) {
2671
+ const serviceName = serviceId.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2672
+ parts.push(` ${this.toId(serviceId)} = component "${serviceName}" "Business logic service" {`);
2673
+ parts.push(` tags "Service" "Auto-detected"`);
2674
+ parts.push(` }`);
2675
+ }
2295
2676
  if (contextInfo.components) {
2296
2677
  for (const comp of contextInfo.components) {
2297
2678
  parts.push(` ${this.toId(comp.name)} = component "${comp.name}" "${this.escape(comp.description || "UI component")}" {`);
@@ -2444,6 +2825,21 @@ class StructurizrDSLGenerator {
2444
2825
  parts.push(` }`);
2445
2826
  }
2446
2827
  }
2828
+ for (const handler of contextInfo.handlers) {
2829
+ if (handler.relationships && handler.relationships.length > 0) {
2830
+ for (const rel of handler.relationships) {
2831
+ const fromId = this.toId(rel.from);
2832
+ const toId = this.toId(rel.to);
2833
+ const description = this.escape(rel.description);
2834
+ parts.push(` ${fromId} -> ${toId} "${description}" {`);
2835
+ if (rel.technology) {
2836
+ parts.push(` technology "${this.escape(rel.technology)}"`);
2837
+ }
2838
+ parts.push(` tags "Auto-detected"`);
2839
+ parts.push(` }`);
2840
+ }
2841
+ }
2842
+ }
2447
2843
  const stateHandlers = [];
2448
2844
  const queryHandlers = [];
2449
2845
  for (const [messageType, handlers] of handlersByType) {
@@ -2568,6 +2964,10 @@ class StructurizrDSLGenerator {
2568
2964
  parts.push(this.generateUserDynamicDiagram(diagram));
2569
2965
  }
2570
2966
  }
2967
+ const autoDiagrams = this.generateAutomaticDynamicDiagrams();
2968
+ if (autoDiagrams.length > 0) {
2969
+ parts.push(...autoDiagrams);
2970
+ }
2571
2971
  const flowsByDomain = new Map;
2572
2972
  for (const flow of this.analysis.messageFlows) {
2573
2973
  const messageType = flow.messageType.toLowerCase();
@@ -2596,6 +2996,111 @@ class StructurizrDSLGenerator {
2596
2996
 
2597
2997
  `);
2598
2998
  }
2999
+ generateAutomaticDynamicDiagrams() {
3000
+ const diagrams = [];
3001
+ const processedHandlers = new Set;
3002
+ const handlersWithRelationships = [];
3003
+ for (const [contextType, contextInfo] of Object.entries(this.analysis.contexts)) {
3004
+ for (const handler of contextInfo.handlers) {
3005
+ if (handler.relationships && handler.relationships.length > 0) {
3006
+ handlersWithRelationships.push({
3007
+ handler,
3008
+ contextType,
3009
+ contextName: this.toId(contextType)
3010
+ });
3011
+ }
3012
+ }
3013
+ }
3014
+ const handlerGroups = new Map;
3015
+ for (const hwc of handlersWithRelationships) {
3016
+ const messageType = hwc.handler.messageType.toLowerCase();
3017
+ let category = "general";
3018
+ if (messageType.includes("auth") || messageType.includes("login") || messageType.includes("logout") || messageType.includes("verify") || messageType.includes("register")) {
3019
+ category = "authentication";
3020
+ } else if (messageType.includes("user")) {
3021
+ category = "user";
3022
+ } else if (messageType.includes("todo")) {
3023
+ category = "todo";
3024
+ } else if (messageType.includes("query") || messageType.includes("get") || messageType.includes("fetch") || messageType.includes("list")) {
3025
+ category = "query";
3026
+ } else if (messageType.includes("command") || messageType.includes("create") || messageType.includes("update") || messageType.includes("delete") || messageType.includes("add") || messageType.includes("remove")) {
3027
+ category = "command";
3028
+ }
3029
+ if (!handlerGroups.has(category)) {
3030
+ handlerGroups.set(category, []);
3031
+ }
3032
+ handlerGroups.get(category).push(hwc);
3033
+ }
3034
+ let diagramCount = 0;
3035
+ const maxDiagrams = 5;
3036
+ const totalHandlers = handlersWithRelationships.length;
3037
+ const categoriesWithHandlers = handlerGroups.size;
3038
+ if (totalHandlers <= 5 && categoriesWithHandlers <= 3) {
3039
+ const allHandlers = Array.from(handlerGroups.values()).flat();
3040
+ const diagram = this.generateHandlerFlowDiagram("general", allHandlers);
3041
+ if (diagram) {
3042
+ diagrams.push(diagram);
3043
+ }
3044
+ } else {
3045
+ for (const [category, handlers] of handlerGroups) {
3046
+ if (diagramCount >= maxDiagrams)
3047
+ break;
3048
+ if (handlers.length === 0)
3049
+ continue;
3050
+ const diagram = this.generateHandlerFlowDiagram(category, handlers);
3051
+ if (diagram) {
3052
+ diagrams.push(diagram);
3053
+ diagramCount++;
3054
+ }
3055
+ }
3056
+ }
3057
+ return diagrams;
3058
+ }
3059
+ generateHandlerFlowDiagram(category, handlers) {
3060
+ const parts = [];
3061
+ const title = this.getCategoryTitle(category);
3062
+ const description = this.getCategoryDescription(category);
3063
+ const scope = handlers[0]?.contextName ? `extension.${handlers[0].contextName}` : "extension";
3064
+ parts.push(` dynamic ${scope} "${title}" "${description}" {`);
3065
+ let stepCount = 0;
3066
+ for (const { handler, contextName } of handlers) {
3067
+ const handlerComponentId = this.toId(`${handler.messageType}_handler`);
3068
+ for (const rel of handler.relationships) {
3069
+ const toComponent = this.toId(rel.to);
3070
+ parts.push(` ${handlerComponentId} -> ${toComponent} "${rel.description}"`);
3071
+ stepCount++;
3072
+ }
3073
+ }
3074
+ if (stepCount === 0) {
3075
+ return null;
3076
+ }
3077
+ parts.push(" autoLayout lr");
3078
+ parts.push(" }");
3079
+ return parts.join(`
3080
+ `);
3081
+ }
3082
+ getCategoryTitle(category) {
3083
+ const titles = {
3084
+ authentication: "Authentication Flow",
3085
+ user: "User Management Flow",
3086
+ todo: "Todo Management Flow",
3087
+ query: "Query Processing Flow",
3088
+ command: "Command Processing Flow",
3089
+ general: "Message Processing Flow"
3090
+ };
3091
+ return titles[category] || `${this.capitalize(category)} Flow`;
3092
+ }
3093
+ getCategoryDescription(category) {
3094
+ const descriptions = {
3095
+ authentication: "Shows authentication request processing and service interactions",
3096
+ user: "Shows user management operations and service dependencies",
3097
+ todo: "Shows todo item operations and service dependencies",
3098
+ query: "Shows read operations flow through query handlers and services",
3099
+ command: "Shows write operations flow through command handlers and services",
3100
+ general: "Shows message processing flow through handlers and services"
3101
+ };
3102
+ return descriptions[category] || "Shows message flow through system components";
3103
+ }
2599
3104
  generateUserDynamicDiagram(diagram) {
2600
3105
  const parts = [];
2601
3106
  parts.push(` dynamic ${diagram.scope || "extension"} "${this.escape(diagram.title)}" "${this.escape(diagram.description || "")}" {`);
@@ -2892,6 +3397,105 @@ class StructurizrDSLGenerator {
2892
3397
  return parts.join(`
2893
3398
  `);
2894
3399
  }
3400
+ generateAutomaticGroups(componentDefs) {
3401
+ const groups = [];
3402
+ const assigned = new Set;
3403
+ const authHandlers = componentDefs.filter((comp) => {
3404
+ const type = comp.messageType.toLowerCase();
3405
+ return type.includes("login") || type.includes("logout") || type.includes("auth") || type.includes("verify") || type.includes("register") || type.includes("signup");
3406
+ });
3407
+ if (authHandlers.length > 0) {
3408
+ groups.push({
3409
+ name: "Authentication",
3410
+ components: authHandlers.map((c) => c.id)
3411
+ });
3412
+ authHandlers.forEach((h) => assigned.add(h.id));
3413
+ }
3414
+ const subscriptionHandlers = componentDefs.filter((comp) => {
3415
+ const type = comp.messageType.toLowerCase();
3416
+ return type.includes("subscribe") || type.includes("unsubscribe");
3417
+ });
3418
+ if (subscriptionHandlers.length > 0) {
3419
+ groups.push({
3420
+ name: "Subscriptions",
3421
+ components: subscriptionHandlers.map((c) => c.id)
3422
+ });
3423
+ subscriptionHandlers.forEach((h) => assigned.add(h.id));
3424
+ }
3425
+ const entityGroups = new Map;
3426
+ for (const comp of componentDefs) {
3427
+ if (assigned.has(comp.id))
3428
+ continue;
3429
+ const type = comp.messageType.toLowerCase();
3430
+ if (type === "connection" || type === "message" || type === "close" || type === "error") {
3431
+ continue;
3432
+ }
3433
+ let entity = null;
3434
+ const underscoreMatch = type.match(/^([a-z]+)_(add|create|update|delete|remove|get|fetch|load|list|query)/);
3435
+ if (underscoreMatch) {
3436
+ entity = underscoreMatch[1];
3437
+ }
3438
+ if (!entity) {
3439
+ const camelMatch = type.match(/(add|create|update|delete|remove|get|fetch|load|list|query)([a-z]+)/i);
3440
+ if (camelMatch) {
3441
+ entity = camelMatch[2].toLowerCase();
3442
+ }
3443
+ }
3444
+ if (!entity && type.match(/^[a-z]+$/)) {
3445
+ entity = type;
3446
+ }
3447
+ if (entity) {
3448
+ if (!entityGroups.has(entity)) {
3449
+ entityGroups.set(entity, []);
3450
+ }
3451
+ entityGroups.get(entity).push(comp.id);
3452
+ assigned.add(comp.id);
3453
+ }
3454
+ }
3455
+ for (const [entity, componentIds] of entityGroups) {
3456
+ if (componentIds.length >= 2) {
3457
+ const entityName = entity.charAt(0).toUpperCase() + entity.slice(1);
3458
+ groups.push({
3459
+ name: `${entityName} Management`,
3460
+ components: componentIds
3461
+ });
3462
+ } else {
3463
+ componentIds.forEach((id) => assigned.delete(id));
3464
+ }
3465
+ }
3466
+ const queryHandlers = componentDefs.filter((comp) => {
3467
+ if (assigned.has(comp.id))
3468
+ return false;
3469
+ const type = comp.messageType.toLowerCase();
3470
+ return type.includes("query") || type.includes("get") || type.includes("fetch") || type.includes("load") || type.includes("list") || type.includes("read");
3471
+ });
3472
+ const commandHandlers = componentDefs.filter((comp) => {
3473
+ if (assigned.has(comp.id))
3474
+ return false;
3475
+ const type = comp.messageType.toLowerCase();
3476
+ return type.includes("command") || type.includes("add") || type.includes("create") || type.includes("update") || type.includes("delete") || type.includes("remove") || type.includes("set") || type.includes("clear");
3477
+ });
3478
+ if (queryHandlers.length >= 2) {
3479
+ groups.push({
3480
+ name: "Query Handlers",
3481
+ components: queryHandlers.map((c) => c.id)
3482
+ });
3483
+ queryHandlers.forEach((h) => assigned.add(h.id));
3484
+ }
3485
+ if (commandHandlers.length >= 2) {
3486
+ groups.push({
3487
+ name: "Command Handlers",
3488
+ components: commandHandlers.map((c) => c.id)
3489
+ });
3490
+ commandHandlers.forEach((h) => assigned.add(h.id));
3491
+ }
3492
+ const groupedCount = assigned.size;
3493
+ const totalCount = componentDefs.length;
3494
+ if (groupedCount >= totalCount * 0.5 || groups.length >= 2) {
3495
+ return groups;
3496
+ }
3497
+ return [];
3498
+ }
2895
3499
  generateGroupedComponents(componentDefs, groups) {
2896
3500
  const parts = [];
2897
3501
  const assignedComponents = new Set;
@@ -3399,4 +4003,4 @@ Stack trace:`, COLORS.gray));
3399
4003
  process.exit(1);
3400
4004
  });
3401
4005
 
3402
- //# debugId=85D3947587AAAF0E64756E2164756E21
4006
+ //# debugId=5BFF854178F00CEB64756E2164756E21