diff --git a/bun.lock b/bun.lock index 7d36ab2..ae68ee8 100644 --- a/bun.lock +++ b/bun.lock @@ -109,6 +109,7 @@ "@bb/errors": "workspace:*", "@bb/graph-core": "workspace:*", "@bb/graph-db": "workspace:*", + "@bb/ladybug": "workspace:*", "@bb/llm": "workspace:*", "@bb/logger": "workspace:*", "@bb/mongo": "workspace:*", @@ -118,6 +119,22 @@ "@bb/types": "workspace:*", }, }, + "packages/ladybug": { + "name": "@bb/ladybug", + "version": "0.0.0", + "dependencies": { + "@bb/config": "workspace:*", + "@bb/errors": "workspace:*", + "@bb/graph-core": "workspace:*", + "@bb/graph-db": "workspace:*", + "@bb/types": "workspace:*", + "@ladybugdb/core": "^0.16.1", + "parquetjs": "^0.11.2", + }, + "devDependencies": { + "@types/parquetjs": "^0.10.6", + }, + }, "packages/llm": { "name": "@bb/llm", "version": "0.0.0", @@ -216,6 +233,7 @@ "@bb/errors": "workspace:*", "@bb/graph-db": "workspace:*", "@bb/ingest-github": "workspace:*", + "@bb/ladybug": "workspace:*", "@bb/mcp": "workspace:*", "@bb/mongo": "workspace:*", "@bb/neo4j": "workspace:*", @@ -270,6 +288,8 @@ "@bb/ingest-github": ["@bb/ingest-github@workspace:packages/ingest-github"], + "@bb/ladybug": ["@bb/ladybug@workspace:packages/ladybug"], + "@bb/llm": ["@bb/llm@workspace:packages/llm"], "@bb/logger": ["@bb/logger@workspace:packages/logger"], @@ -360,6 +380,20 @@ "@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@ladybugdb/core": ["@ladybugdb/core@0.16.1", "", { "dependencies": { "cmake-js": "^8.0.0", "node-addon-api": "^6.0.0" }, "optionalDependencies": { "@ladybugdb/core-darwin-arm64": "0.16.1", "@ladybugdb/core-darwin-x64": "0.16.1", "@ladybugdb/core-linux-arm64": "0.16.1", "@ladybugdb/core-linux-x64": "0.16.1", "@ladybugdb/core-win32-x64": "0.16.1" } }, "sha512-qwuEcR8CVMKb6tNDaHtq7Ux8hT/XbPC0db+vwutX6JxNAejyx7YomHKPSy9XAKURhYK8mezZe3UN8rf+xpHOjQ=="], + + "@ladybugdb/core-darwin-arm64": ["@ladybugdb/core-darwin-arm64@0.16.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nl+Cf70rD+HaC9IBHv+oeUwqX9plghXD7PN9tyMzMohRVPvcGEbqWPB6YcdJa8rR7qRqCCbmaNMDen5wg4rY2w=="], + + "@ladybugdb/core-darwin-x64": ["@ladybugdb/core-darwin-x64@0.16.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-4eAjfimAAQRSmDfUUkGrl9OhefxcW1ziA9tl0eljBlGoUseE7dL02+RSqjGohYMcQ+lzuHAq1QWb0XRlMA8YTQ=="], + + "@ladybugdb/core-linux-arm64": ["@ladybugdb/core-linux-arm64@0.16.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-zkctksev+hsPFrNxHHdq4lYK5OWdLhWfRdQzjzkgDyaHayHU6yCL2fgD6uPGQ8TRQ6/2DxMErb4p3FzGW85Ubw=="], + + "@ladybugdb/core-linux-x64": ["@ladybugdb/core-linux-x64@0.16.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5rAb9T5vif8WKhHwhobosu2/aiOwJkWb/ViybvUc5GFKunKl8VI6RmZQVeufT9zUzRktUwrxBrxblCxsnamXJw=="], + + "@ladybugdb/core-win32-x64": ["@ladybugdb/core-win32-x64@0.16.1", "", { "os": "win32", "cpu": "x64" }, "sha512-ShOUTrIuZKQ63J95tcRJxKf1cvg8yi2FSYx9kMTSercc1FdQZPV+zxUN0myMq3MTWOl7xDxsVMmdp/t80O29UQ=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.9", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-RXSxsokhAF/4nWys8An8npsqOI33Ex1Hlzqjw2pZOO+GKtMAR2noGnUdsFiGwsaO/xXI+56mtjTmDA3JXJsvmA=="], @@ -404,6 +438,10 @@ "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/node-int64": ["@types/node-int64@0.4.32", "", { "dependencies": { "@types/node": "*" } }, "sha512-xf/JsSlnXQ+mzvc0IpXemcrO4BrCfpgNpMco+GLcXkFk01k/gW9lGJu+Vof0ZSvHK6DsHJDPSbjFPs36QkWXqw=="], + + "@types/parquetjs": ["@types/parquetjs@0.10.6", "", { "dependencies": { "@types/node-int64": "*" } }, "sha512-ZCsD6j97YD0mGU8/VnVs3NjORXa7zeHvqlpJpCqy4jU8a1O21dalL+MFn9QNbdEfy8rszR1N7NHeT7/LdtHf+A=="], + "@types/qs": ["@types/qs@6.15.0", "", {}, "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], @@ -468,11 +506,15 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "bindings": ["bindings@1.2.1", "", {}, "sha512-u4cBQNepWxYA55FunZSM7wMi55yQaN0otnhhilNoWHq0MfOfJeQx0v0mRRpolGOExPjZcl6FtB0BB8Xkb88F0g=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], - "bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="], + "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], + + "bson": ["bson@1.1.6", "", {}, "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], @@ -490,6 +532,8 @@ "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + "cli-boxes": ["cli-boxes@4.0.1", "", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="], "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], @@ -500,6 +544,8 @@ "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "cmake-js": ["cmake-js@8.0.0", "", { "dependencies": { "debug": "^4.4.3", "fs-extra": "^11.3.3", "node-api-headers": "^1.8.0", "rc": "1.2.8", "semver": "^7.7.3", "tar": "^7.5.6", "url-join": "^4.0.1", "which": "^6.0.0", "yargs": "^17.7.2" }, "bin": { "cmake-js": "bin/cmake-js" } }, "sha512-YbUP88RDwCvoQkZhRtGURYm9RIpWdtvZuhT87fKNoLjk8kIFIFeARpKfuZQGdwfH99GZpUmqSfcDrK62X7lTgg=="], + "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], "color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="], @@ -546,6 +592,8 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], @@ -650,6 +698,8 @@ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], @@ -670,6 +720,8 @@ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], @@ -696,12 +748,14 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], "ink": ["ink@7.0.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.3", "auto-bind": "^5.0.1", "chalk": "^5.6.2", "cli-boxes": "^4.0.1", "cli-cursor": "^4.0.0", "cli-truncate": "^6.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.45.1", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^9.0.0", "stack-utils": "^2.0.6", "string-width": "^8.2.0", "terminal-size": "^4.0.1", "type-fest": "^5.5.0", "widest-line": "^6.0.0", "wrap-ansi": "^10.0.0", "ws": "^8.20.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.2.0", "react": ">=19.2.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-o6LAC268PLawlGVYrXTyaTfke4VtJftEheuwbgkQf7yvSXyWp1nRwBbAyKEkWXFZZsW/la5wrMuNbuBvZK2C1w=="], "ink-text-input": ["ink-text-input@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "type-fest": "^4.18.2" }, "peerDependencies": { "ink": ">=5", "react": ">=18" } }, "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw=="], + "int53": ["int53@0.2.4", "", {}, "sha512-a5jlKftS7HUOhkUyYD7j2sJ/ZnvWiNlZS1ldR+g1ifQ+/UuZXIE+YTc/lK1qGj/GwAU5F8Z0e1eVq2t1J5Ob2g=="], + "ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="], "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], @@ -746,6 +800,8 @@ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], @@ -782,6 +838,8 @@ "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], + "lzo": ["lzo@0.4.11", "", { "dependencies": { "bindings": "~1.2.1" } }, "sha512-apQHNoW2Alg72FMqaC/7pn03I7umdgSVFt2KRkCXXils4Z9u3QBh1uOtl2O5WmZIDLd9g6Lu4lIdOLmiSTFVCQ=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], @@ -804,6 +862,10 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], "mongodb": ["mongodb@7.2.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^7.2.0", "mongodb-connection-string-url": "^7.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.806.0", "@mongodb-js/zstd": "^7.0.0", "gcp-metadata": "^7.0.1", "kerberos": "^7.0.0", "mongodb-client-encryption": ">=7.0.0 <7.1.0", "snappy": "^7.3.2", "socks": "^2.8.6" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw=="], @@ -828,14 +890,22 @@ "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], + "node-addon-api": ["node-addon-api@6.1.0", "", {}, "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="], + + "node-api-headers": ["node-api-headers@1.8.0", "", {}, "sha512-jfnmiKWjRAGbdD1yQS28bknFM1tbHC1oucyuMPjmkEs+kpiu76aRs40WlTmBmyEgzDM76ge1DQ7XJ3R5deiVjQ=="], + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "object-stream": ["object-stream@0.0.1", "", {}, "sha512-+NPJnRvX9RDMRY9mOWOo/NDppBjbZhXirNNSu2IBnuNboClC9h1ZGHXgHBLDbJMHsxeJDq922aVmG5xs24a/cA=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], @@ -852,6 +922,8 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parquetjs": ["parquetjs@0.11.2", "", { "dependencies": { "brotli": "^1.3.0", "bson": "^1.0.4", "int53": "^0.2.4", "object-stream": "0.0.1", "snappyjs": "^0.6.0", "thrift": "^0.11.0", "varint": "^5.0.0" }, "optionalDependencies": { "lzo": "^0.4.0" } }, "sha512-Y6FOc3Oi2AxY4TzJPz7fhICCR8tQNL3p+2xGQoUAMbmlJBR7+JJmMrwuyMjIpDiM7G8Wj/8oqOH4UDUmu4I5ZA=="], + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], @@ -880,12 +952,16 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "q": ["q@1.5.1", "", {}, "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw=="], + "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], @@ -942,6 +1018,8 @@ "slice-ansi": ["slice-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA=="], + "snappyjs": ["snappyjs@0.6.1", "", {}, "sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg=="], + "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], @@ -960,14 +1038,20 @@ "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + "tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="], + "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + "thrift": ["thrift@0.11.0", "", { "dependencies": { "node-int64": "^0.4.0", "q": "^1.5.0", "ws": ">= 2.2.3" } }, "sha512-UpsBhOC45a45TpeHOXE4wwYwL8uD2apbHTbtBvkwtUU4dNwCjC7DpQTjw2Q6eIdfNtw+dKthdwq94uLXTJPfFw=="], + "tiktoken": ["tiktoken@1.0.22", "", {}, "sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA=="], "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], @@ -996,12 +1080,18 @@ "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "url-join": ["url-join@4.0.1", "", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "varint": ["varint@5.0.2", "", {}, "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], @@ -1028,6 +1118,8 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -1058,6 +1150,10 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "cmake-js/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], + + "global-directory/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "ink-text-input/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -1072,6 +1168,8 @@ "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "mongodb/bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="], + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1088,6 +1186,8 @@ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "cmake-js/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], + "listr2/cli-truncate/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], "listr2/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], diff --git a/infra/docker/docker-compose.yml b/infra/docker/docker-compose.yml index afd6cb8..3a667dd 100644 --- a/infra/docker/docker-compose.yml +++ b/infra/docker/docker-compose.yml @@ -60,6 +60,19 @@ services: retries: 12 start_period: 5s + ladybug-explorer: + image: ghcr.io/ladybugdb/explorer:latest + container_name: bytebell-ladybug-explorer + restart: unless-stopped + ports: + - "127.0.0.1:8000:8000" + volumes: + - /Users/zeta/.bytebell:/database + environment: + - LBUG_FILE=ladybug.lbug + networks: + - bytebell + networks: bytebell: name: bytebell diff --git a/packages/config/src/schema-fields.ts b/packages/config/src/schema-fields.ts index ae6cc54..142b21d 100644 --- a/packages/config/src/schema-fields.ts +++ b/packages/config/src/schema-fields.ts @@ -83,6 +83,8 @@ export function readField(cfg: BytebellConfig, key: K): Config return cfg.graph_provider as ConfigValue; case Config.SqlitePath: return cfg.sqlite_path as ConfigValue; + case Config.LadybugPath: + return cfg.ladybug_path as ConfigValue; default: throw new Error(`Unknown config key: ${key}`); } @@ -168,6 +170,8 @@ export function writeField(cfg: BytebellConfig, key: K, value: return { ...cfg, graph_provider: value as string }; case Config.SqlitePath: return { ...cfg, sqlite_path: value as string }; + case Config.LadybugPath: + return { ...cfg, ladybug_path: value as string }; default: throw new Error(`Unknown config key: ${key}`); } diff --git a/packages/config/src/schema.ts b/packages/config/src/schema.ts index bb94dcd..7df1683 100644 --- a/packages/config/src/schema.ts +++ b/packages/config/src/schema.ts @@ -56,6 +56,7 @@ export const configSchema = z db_provider: z.string().default("mongo"), graph_provider: z.string().default("neo4j"), sqlite_path: z.string().default(""), + ladybug_path: z.string().default(""), }) .strict(); @@ -103,6 +104,7 @@ export type ConfigValueMap = { [Config.DbProvider]: string; [Config.GraphProvider]: string; [Config.SqlitePath]: string; + [Config.LadybugPath]: string; }; export type ConfigValue = ConfigValueMap[K]; @@ -164,6 +166,7 @@ export const HINTS: Readonly> = { [Config.DbProvider]: "bytebell set db-provider ", [Config.GraphProvider]: "bytebell set graph-provider ", [Config.SqlitePath]: "bytebell set sqlite-path ", + [Config.LadybugPath]: "bytebell set ladybug-path ", }; export { readField, writeField } from "./schema-fields.ts"; diff --git a/packages/graph-core/src/index.ts b/packages/graph-core/src/index.ts index d4f3b94..bf2f4bc 100644 --- a/packages/graph-core/src/index.ts +++ b/packages/graph-core/src/index.ts @@ -33,12 +33,13 @@ export interface IGraphFileRepository { upsertFileNode(input: UpsertFileNodeInput): Promise; deleteFileNodes(knowledgeId: string, paths: string[]): Promise; snapshotFilesToVersion(input: SnapshotFilesInput): Promise; - upsertFileNodesBatch(inputs: readonly UpsertFileNodeInput[]): Promise; + upsertFileNodesBatch?(inputs: readonly UpsertFileNodeInput[]): Promise; + bulkUpsertFiles?(knowledgeId: string, fileStream: AsyncIterable): Promise; } export interface IGraphFolderRepository { upsertFolderNode(input: UpsertFolderNodeInput): Promise; - upsertFolderNodesBatch(inputs: readonly UpsertFolderNodeInput[]): Promise; + upsertFolderNodesBatch?(inputs: readonly UpsertFolderNodeInput[]): Promise; } export interface IGraphRepoRepository { diff --git a/packages/graph-db/src/index.ts b/packages/graph-db/src/index.ts index bbbae50..da4f3b9 100644 --- a/packages/graph-db/src/index.ts +++ b/packages/graph-db/src/index.ts @@ -49,12 +49,39 @@ export const filesGraph: IGraphFileRepository = { upsertFileNode: (...args) => getGraph().files.upsertFileNode(...args), deleteFileNodes: (...args) => getGraph().files.deleteFileNodes(...args), snapshotFilesToVersion: (...args) => getGraph().files.snapshotFilesToVersion(...args), - upsertFileNodesBatch: (...args) => getGraph().files.upsertFileNodesBatch(...args), + upsertFileNodesBatch: async (inputs) => { + const f = getGraph().files; + if (f.upsertFileNodesBatch) { + await f.upsertFileNodesBatch(inputs); + } else { + for (const input of inputs) { + await f.upsertFileNode(input); + } + } + }, + bulkUpsertFiles: async (knowledgeId, fileStream) => { + const f = getGraph().files; + if (f.bulkUpsertFiles) { + return f.bulkUpsertFiles(knowledgeId, fileStream); + } + for await (const input of fileStream) { + await f.upsertFileNode(input); + } + }, }; export const foldersGraph: IGraphFolderRepository = { upsertFolderNode: (...args) => getGraph().folders.upsertFolderNode(...args), - upsertFolderNodesBatch: (...args) => getGraph().folders.upsertFolderNodesBatch(...args), + upsertFolderNodesBatch: async (inputs) => { + const f = getGraph().folders; + if (f.upsertFolderNodesBatch) { + await f.upsertFolderNodesBatch(inputs); + } else { + for (const input of inputs) { + await f.upsertFolderNode(input); + } + } + }, }; export const repoGraph: IGraphRepoRepository = { diff --git a/packages/ingest-github/package.json b/packages/ingest-github/package.json index ecae6fb..2d17d40 100644 --- a/packages/ingest-github/package.json +++ b/packages/ingest-github/package.json @@ -21,6 +21,7 @@ "@bb/mongo": "workspace:*", "@bb/sqlite": "workspace:*", "@bb/neo4j": "workspace:*", + "@bb/ladybug": "workspace:*", "@bb/queue": "workspace:*", "@bb/db-core": "workspace:*", "@bb/graph-core": "workspace:*", diff --git a/packages/ingest-github/src/bootstrap.ts b/packages/ingest-github/src/bootstrap.ts index 583d7db..9fef78e 100644 --- a/packages/ingest-github/src/bootstrap.ts +++ b/packages/ingest-github/src/bootstrap.ts @@ -6,6 +6,7 @@ import { connectGraph } from "@bb/graph-db"; import "@bb/mongo"; import "@bb/sqlite"; import "@bb/neo4j"; +import "@bb/ladybug"; export interface BootstrapRuntimeOptions { config: unknown; diff --git a/packages/ingest-github/src/strategies/flat-folder/folder-summary-api.ts b/packages/ingest-github/src/strategies/flat-folder/folder-summary-api.ts new file mode 100644 index 0000000..83f9af5 --- /dev/null +++ b/packages/ingest-github/src/strategies/flat-folder/folder-summary-api.ts @@ -0,0 +1,245 @@ +import { readFile, readdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { askJsonLLM, type AskLlmOptions } from "@bb/llm"; +import { LlmConfigError, LlmError } from "@bb/errors"; +import { logger } from "@bb/logger"; +import { Config } from "@bb/types"; +import { getConfigValue } from "@bb/config"; +import type { CondensedFileAnalysis } from "#src/types/condensed-file-analysis.ts"; +import type { MetaPaths } from "#src/types/meta-paths.ts"; +import { encodeMetaPath } from "#src/pipeline/paths.ts"; +import { + FOLDER_ANALYSIS_SYSTEM_PROMPT, + FOLDER_BATCH_SYSTEM_PROMPT, + folderAnalysisUserPrompt, + folderBatchUserPrompt, + type BatchedFolderInput, +} from "./prompts/folder-summary.ts"; +import type { FolderSummary } from "./types.ts"; + +export interface FolderBucket { + folderPath: string; + files: CondensedFileAnalysis[]; +} + +interface FolderSummaryJson { + purpose?: unknown; + summary?: unknown; + keywords?: unknown; + classes?: unknown; + functions?: unknown; + importsInternal?: unknown; + importsExternal?: unknown; + dependencyGraph?: unknown; +} + +/** + * Splits the folder groups into "individual" (one LLM call per folder, used + * for big folders or when batching is disabled) and "batches" (N small + * folders summarised in one LLM call). Driven by `Config.FolderSummaryBatchSize` + * (set to 1 to disable batching entirely) and `Config.FolderSummaryBatchMaxFiles` + * (folders exceeding this file count always take the individual path). + * + * Folders are sorted by path so that two runs of the same repo produce the + * same batch composition — helpful when A/B-comparing outputs. + */ +export function groupFoldersForBatching(groups: Map): { + individual: FolderBucket[]; + batches: FolderBucket[][]; +} { + const batchSize = getConfigValue(Config.FolderSummaryBatchSize); + const maxFiles = getConfigValue(Config.FolderSummaryBatchMaxFiles); + const sorted: FolderBucket[] = [...groups.entries()] + .map(([folderPath, files]) => ({ folderPath, files })) + .sort((a, b) => a.folderPath.localeCompare(b.folderPath)); + + if (batchSize <= 1) { + return { individual: sorted, batches: [] }; + } + + const individual: FolderBucket[] = []; + const batchable: FolderBucket[] = []; + for (const bucket of sorted) { + if (bucket.files.length > maxFiles) { + individual.push(bucket); + } else { + batchable.push(bucket); + } + } + + const batches: FolderBucket[][] = []; + for (let i = 0; i < batchable.length; i += batchSize) { + batches.push(batchable.slice(i, i + batchSize)); + } + return { individual, batches }; +} + +export async function summariseFolder( + folderPath: string, + files: CondensedFileAnalysis[], + llmCallContext?: AskLlmOptions, +): Promise<{ + summary: FolderSummary | null; + tokenUsage: { inputTokens: number; outputTokens: number; costUsd: number }; +}> { + const userPrompt = folderAnalysisUserPrompt(folderPath, files); + try { + const response = await askJsonLLM( + FOLDER_ANALYSIS_SYSTEM_PROMPT, + userPrompt, + llmCallContext ?? {}, + ); + if (response.result === null) { + logger.warn(`summariseFolder: ${folderPath || ""} returned unparseable JSON`); + return { + summary: null, + tokenUsage: { + inputTokens: response.usage.inputTokens, + outputTokens: response.usage.outputTokens, + costUsd: response.usage.costUsd, + }, + }; + } + return { + summary: shapeFolderSummary(folderPath, response.result), + tokenUsage: { + inputTokens: response.usage.inputTokens, + outputTokens: response.usage.outputTokens, + costUsd: response.usage.costUsd, + }, + }; + } catch (cause: unknown) { + if (cause instanceof LlmConfigError || cause instanceof LlmError) { + throw cause; + } + const msg = cause instanceof Error ? cause.message : String(cause); + logger.warn(`summariseFolder: ${folderPath || ""} askJsonLLM failed: ${msg}`); + return { summary: null, tokenUsage: { inputTokens: 0, outputTokens: 0, costUsd: 0 } }; + } +} + +/** + * Multi-folder summary. Builds a label-indexed prompt, parses the keyed JSON + * response, returns one `FolderSummary | null` per folder. Folders missing + * from the response (or whose entry fails shape validation) are surfaced as + * `null` with a warn log; the caller counts those as failed. + */ +export async function summariseFolderBatch( + batch: FolderBucket[], + llmCallContext?: AskLlmOptions, +): Promise<{ + summaries: Map; + tokenUsage: { inputTokens: number; outputTokens: number; costUsd: number }; +}> { + const labeled: BatchedFolderInput[] = batch.map((b, i) => ({ label: i, folderPath: b.folderPath, files: b.files })); + const userPrompt = folderBatchUserPrompt(labeled); + const summaries = new Map(); + try { + const response = await askJsonLLM>( + FOLDER_BATCH_SYSTEM_PROMPT, + userPrompt, + llmCallContext ?? {}, + ); + if (response.result === null) { + logger.warn(`summariseFolderBatch: batch of ${batch.length} returned unparseable JSON`); + for (const b of batch) { + summaries.set(b.folderPath, null); + } + return { + summaries, + tokenUsage: { + inputTokens: response.usage.inputTokens, + outputTokens: response.usage.outputTokens, + costUsd: response.usage.costUsd, + }, + }; + } + for (const b of labeled) { + const raw = response.result[String(b.label)]; + if (raw === undefined || typeof raw !== "object" || raw === null) { + logger.warn(`summariseFolderBatch: missing/invalid entry for label ${b.label} (${b.folderPath || ""})`); + summaries.set(b.folderPath, null); + continue; + } + summaries.set(b.folderPath, shapeFolderSummary(b.folderPath, raw)); + } + return { + summaries, + tokenUsage: { + inputTokens: response.usage.inputTokens, + outputTokens: response.usage.outputTokens, + costUsd: response.usage.costUsd, + }, + }; + } catch (cause: unknown) { + if (cause instanceof LlmConfigError || cause instanceof LlmError) { + throw cause; + } + const msg = cause instanceof Error ? cause.message : String(cause); + logger.warn(`summariseFolderBatch: batch of ${batch.length} askJsonLLM failed: ${msg}`); + for (const b of batch) { + summaries.set(b.folderPath, null); + } + return { summaries, tokenUsage: { inputTokens: 0, outputTokens: 0, costUsd: 0 } }; + } +} + +export async function persistFolderSummary(metaPaths: MetaPaths, summary: FolderSummary): Promise { + const file = path.join(metaPaths.folderSummariesDir, `${encodeMetaPath(summary.folderPath || "__ROOT__")}.json`); + await writeFile(file, JSON.stringify(summary, null, 2), "utf8"); +} + +export async function* iterateFolderSummaries(metaPaths: MetaPaths): AsyncGenerator { + let entries: string[]; + try { + entries = await readdir(metaPaths.folderSummariesDir); + } catch { + return; + } + for (const name of entries) { + if (!name.endsWith(".json")) { + continue; + } + try { + const raw = await readFile(path.join(metaPaths.folderSummariesDir, name), "utf8"); + const parsed: unknown = JSON.parse(raw); + if (typeof parsed === "object" && parsed !== null) { + yield parsed as FolderSummary; + } + } catch { + continue; + } + } +} + +export function shapeFolderSummary(folderPath: string, raw: FolderSummaryJson): FolderSummary { + return { + folderPath, + purpose: pickString(raw.purpose, ""), + summary: pickString(raw.summary, ""), + keywords: pickStringArray(raw.keywords), + classes: pickStringArray(raw.classes), + functions: pickStringArray(raw.functions), + importsInternal: pickStringArray(raw.importsInternal), + importsExternal: pickStringArray(raw.importsExternal), + dependencyGraph: pickString(raw.dependencyGraph, ""), + generatedAt: new Date().toISOString(), + }; +} + +function pickString(value: unknown, fallback: string): string { + return typeof value === "string" && value.length > 0 ? value : fallback; +} + +function pickStringArray(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + const out: string[] = []; + for (const item of value) { + if (typeof item === "string" && item.length > 0) { + out.push(item); + } + } + return out; +} diff --git a/packages/ingest-github/src/strategies/flat-folder/folder-summary.ts b/packages/ingest-github/src/strategies/flat-folder/folder-summary.ts index cdd9c5d..09031a5 100644 --- a/packages/ingest-github/src/strategies/flat-folder/folder-summary.ts +++ b/packages/ingest-github/src/strategies/flat-folder/folder-summary.ts @@ -1,26 +1,21 @@ -import { readFile, readdir, writeFile } from "node:fs/promises"; -import path from "node:path"; -import { askJsonLLM, type AskLlmOptions } from "@bb/llm"; -import { LlmConfigError, LlmError } from "@bb/errors"; +import type { AskLlmOptions } from "@bb/llm"; import { logger } from "@bb/logger"; -import { Config } from "@bb/types"; -import { getConfigValue } from "@bb/config"; import type { CondensedFileAnalysis } from "#src/types/condensed-file-analysis.ts"; import type { MetaPaths } from "#src/types/meta-paths.ts"; -import { encodeMetaPath } from "#src/pipeline/paths.ts"; import type { ConcurrencyLimiter } from "#src/pipeline/concurrency.ts"; import { throwIfCancelled, CancellationError } from "#src/pipeline/cancellation.ts"; import type { ProgressContext } from "#src/progress/types.ts"; import type { FileAnalysisCache } from "./file-analysis-cache.ts"; import { directFolderOf } from "./folder-path.ts"; import { - FOLDER_ANALYSIS_SYSTEM_PROMPT, - FOLDER_BATCH_SYSTEM_PROMPT, - folderAnalysisUserPrompt, - folderBatchUserPrompt, - type BatchedFolderInput, -} from "./prompts/folder-summary.ts"; -import type { FolderSummary } from "./types.ts"; + type FolderBucket, + groupFoldersForBatching, + summariseFolder, + summariseFolderBatch, + persistFolderSummary, +} from "./folder-summary-api.ts"; + +export { iterateFolderSummaries } from "./folder-summary-api.ts"; export function groupByDirectFolder(cache: FileAnalysisCache): Map { const groups = new Map(); @@ -33,201 +28,6 @@ export function groupByDirectFolder(cache: FileAnalysisCache): Map): { - individual: FolderBucket[]; - batches: FolderBucket[][]; -} { - const batchSize = getConfigValue(Config.FolderSummaryBatchSize); - const maxFiles = getConfigValue(Config.FolderSummaryBatchMaxFiles); - const sorted: FolderBucket[] = [...groups.entries()] - .map(([folderPath, files]) => ({ folderPath, files })) - .sort((a, b) => a.folderPath.localeCompare(b.folderPath)); - - if (batchSize <= 1) { - return { individual: sorted, batches: [] }; - } - - const individual: FolderBucket[] = []; - const batchable: FolderBucket[] = []; - for (const bucket of sorted) { - if (bucket.files.length > maxFiles) { - individual.push(bucket); - } else { - batchable.push(bucket); - } - } - - const batches: FolderBucket[][] = []; - for (let i = 0; i < batchable.length; i += batchSize) { - batches.push(batchable.slice(i, i + batchSize)); - } - return { individual, batches }; -} - -export async function summariseFolder( - folderPath: string, - files: CondensedFileAnalysis[], - llmCallContext?: AskLlmOptions, -): Promise<{ - summary: FolderSummary | null; - tokenUsage: { inputTokens: number; outputTokens: number; costUsd: number }; -}> { - const userPrompt = folderAnalysisUserPrompt(folderPath, files); - try { - const response = await askJsonLLM( - FOLDER_ANALYSIS_SYSTEM_PROMPT, - userPrompt, - llmCallContext ?? {}, - ); - if (response.result === null) { - logger.warn(`summariseFolder: ${folderPath || ""} returned unparseable JSON`); - return { - summary: null, - tokenUsage: { - inputTokens: response.usage.inputTokens, - outputTokens: response.usage.outputTokens, - costUsd: response.usage.costUsd, - }, - }; - } - return { - summary: shapeFolderSummary(folderPath, response.result), - tokenUsage: { - inputTokens: response.usage.inputTokens, - outputTokens: response.usage.outputTokens, - costUsd: response.usage.costUsd, - }, - }; - } catch (cause: unknown) { - if (cause instanceof LlmConfigError || cause instanceof LlmError) { - throw cause; - } - const msg = cause instanceof Error ? cause.message : String(cause); - logger.warn(`summariseFolder: ${folderPath || ""} askJsonLLM failed: ${msg}`); - return { summary: null, tokenUsage: { inputTokens: 0, outputTokens: 0, costUsd: 0 } }; - } -} - -/** - * Multi-folder summary. Builds a label-indexed prompt, parses the keyed JSON - * response, returns one `FolderSummary | null` per folder. Folders missing - * from the response (or whose entry fails shape validation) are surfaced as - * `null` with a warn log; the caller counts those as failed. - */ -export async function summariseFolderBatch( - batch: FolderBucket[], - llmCallContext?: AskLlmOptions, -): Promise<{ - summaries: Map; - tokenUsage: { inputTokens: number; outputTokens: number; costUsd: number }; -}> { - const labeled: BatchedFolderInput[] = batch.map((b, i) => ({ label: i, folderPath: b.folderPath, files: b.files })); - const userPrompt = folderBatchUserPrompt(labeled); - const summaries = new Map(); - try { - const response = await askJsonLLM>( - FOLDER_BATCH_SYSTEM_PROMPT, - userPrompt, - llmCallContext ?? {}, - ); - if (response.result === null) { - logger.warn(`summariseFolderBatch: batch of ${batch.length} returned unparseable JSON`); - for (const b of batch) { - summaries.set(b.folderPath, null); - } - return { - summaries, - tokenUsage: { - inputTokens: response.usage.inputTokens, - outputTokens: response.usage.outputTokens, - costUsd: response.usage.costUsd, - }, - }; - } - for (const b of labeled) { - const raw = response.result[String(b.label)]; - if (raw === undefined || typeof raw !== "object" || raw === null) { - logger.warn(`summariseFolderBatch: missing/invalid entry for label ${b.label} (${b.folderPath || ""})`); - summaries.set(b.folderPath, null); - continue; - } - summaries.set(b.folderPath, shapeFolderSummary(b.folderPath, raw)); - } - return { - summaries, - tokenUsage: { - inputTokens: response.usage.inputTokens, - outputTokens: response.usage.outputTokens, - costUsd: response.usage.costUsd, - }, - }; - } catch (cause: unknown) { - if (cause instanceof LlmConfigError || cause instanceof LlmError) { - throw cause; - } - const msg = cause instanceof Error ? cause.message : String(cause); - logger.warn(`summariseFolderBatch: batch of ${batch.length} askJsonLLM failed: ${msg}`); - for (const b of batch) { - summaries.set(b.folderPath, null); - } - return { summaries, tokenUsage: { inputTokens: 0, outputTokens: 0, costUsd: 0 } }; - } -} - -export async function persistFolderSummary(metaPaths: MetaPaths, summary: FolderSummary): Promise { - const file = path.join(metaPaths.folderSummariesDir, `${encodeMetaPath(summary.folderPath || "__ROOT__")}.json`); - await writeFile(file, JSON.stringify(summary, null, 2), "utf8"); -} - -export async function* iterateFolderSummaries(metaPaths: MetaPaths): AsyncGenerator { - let entries: string[]; - try { - entries = await readdir(metaPaths.folderSummariesDir); - } catch { - return; - } - for (const name of entries) { - if (!name.endsWith(".json")) { - continue; - } - try { - const raw = await readFile(path.join(metaPaths.folderSummariesDir, name), "utf8"); - const parsed: unknown = JSON.parse(raw); - if (typeof parsed === "object" && parsed !== null) { - yield parsed as FolderSummary; - } - } catch { - continue; - } - } -} - interface FolderSummaryTotals { succeeded: number; failed: number; @@ -386,35 +186,3 @@ export async function runFolderSummaryPhase( tokenUsage: { inputTokens: totals.inputTokens, outputTokens: totals.outputTokens, costUsd: totals.costUsd }, }; } - -function shapeFolderSummary(folderPath: string, raw: FolderSummaryJson): FolderSummary { - return { - folderPath, - purpose: pickString(raw.purpose, ""), - summary: pickString(raw.summary, ""), - keywords: pickStringArray(raw.keywords), - classes: pickStringArray(raw.classes), - functions: pickStringArray(raw.functions), - importsInternal: pickStringArray(raw.importsInternal), - importsExternal: pickStringArray(raw.importsExternal), - dependencyGraph: pickString(raw.dependencyGraph, ""), - generatedAt: new Date().toISOString(), - }; -} - -function pickString(value: unknown, fallback: string): string { - return typeof value === "string" && value.length > 0 ? value : fallback; -} - -function pickStringArray(value: unknown): string[] { - if (!Array.isArray(value)) { - return []; - } - const out: string[] = []; - for (const item of value) { - if (typeof item === "string" && item.length > 0) { - out.push(item); - } - } - return out; -} diff --git a/packages/ingest-github/src/strategies/flat-folder/phases/store-flat-analysis.ts b/packages/ingest-github/src/strategies/flat-folder/phases/store-flat-analysis.ts index 19913fb..f05e2af 100644 --- a/packages/ingest-github/src/strategies/flat-folder/phases/store-flat-analysis.ts +++ b/packages/ingest-github/src/strategies/flat-folder/phases/store-flat-analysis.ts @@ -117,7 +117,13 @@ export async function storeFlatAnalysis(input: StoreFlatAnalysisInput): Promise< for (let i = 0; i < folderInputs.length; i += batchSize) { throwIfCancelled(input.scope.knowledgeId); const batch = folderInputs.slice(i, i + batchSize); - await foldersGraph.upsertFolderNodesBatch(batch); + if (foldersGraph.upsertFolderNodesBatch) { + await foldersGraph.upsertFolderNodesBatch(batch); + } else { + for (const item of batch) { + await foldersGraph.upsertFolderNode(item); + } + } foldersWritten += batch.length; nodesWritten += batch.length; for (const item of batch) { @@ -125,35 +131,50 @@ export async function storeFlatAnalysis(input: StoreFlatAnalysisInput): Promise< } } - // 5. Batched file upserts. - const fileInputs: UpsertFileNodeInput[] = []; - for (const file of input.cache.values()) { - fileInputs.push({ - orgId: input.scope.orgId, - knowledgeId: input.scope.knowledgeId, - repoId: input.scope.repoId, - relativePath: file.relativePath, - folderPath: directFolderOf(file.relativePath), - language: file.language.length > 0 ? file.language : languageFromPath(file.relativePath), - sha: file.sha256, - sizeBytes: file.sizeBytes, - analysis: file.analysis, - isBigFile: file.isBigFile, - totalChunks: file.totalChunks, - totalTokenCount: file.totalTokenCount, - }); + // 5. File upsert stream. + async function* yieldFiles() { + for (const file of input.cache.values()) { + throwIfCancelled(input.scope.knowledgeId); + const upsertInput: UpsertFileNodeInput = { + orgId: input.scope.orgId, + knowledgeId: input.scope.knowledgeId, + repoId: input.scope.repoId, + relativePath: file.relativePath, + folderPath: directFolderOf(file.relativePath), + language: file.language.length > 0 ? file.language : languageFromPath(file.relativePath), + sha: file.sha256, + sizeBytes: file.sizeBytes, + analysis: file.analysis, + isBigFile: file.isBigFile, + totalChunks: file.totalChunks, + totalTokenCount: file.totalTokenCount, + }; + filesWritten += 1; + nodesWritten += 1; + yield upsertInput; + fileReporter?.increment(1, { fileName: file.relativePath }); + } } - logger.info( - `phase7: file upsert dispatching ${Math.ceil(fileInputs.length / batchSize)} batches of up to ${batchSize} files (total=${fileInputs.length})`, - ); - for (let i = 0; i < fileInputs.length; i += batchSize) { - throwIfCancelled(input.scope.knowledgeId); - const batch = fileInputs.slice(i, i + batchSize); - await filesGraph.upsertFileNodesBatch(batch); - filesWritten += batch.length; - nodesWritten += batch.length; - for (const item of batch) { - fileReporter?.increment(1, { fileName: item.relativePath }); + + if (typeof filesGraph.bulkUpsertFiles === "function") { + await filesGraph.bulkUpsertFiles(input.scope.knowledgeId, yieldFiles()); + } else if (typeof filesGraph.upsertFileNodesBatch === "function") { + let batch: UpsertFileNodeInput[] = []; + for await (const f of yieldFiles()) { + batch.push(f); + if (batch.length >= batchSize) { + throwIfCancelled(input.scope.knowledgeId); + await filesGraph.upsertFileNodesBatch(batch); + batch = []; + } + } + if (batch.length > 0) { + throwIfCancelled(input.scope.knowledgeId); + await filesGraph.upsertFileNodesBatch(batch); + } + } else { + for await (const f of yieldFiles()) { + await filesGraph.upsertFileNode(f); } } } finally { diff --git a/packages/ingest-github/src/strategies/flat-folder/store-pull.ts b/packages/ingest-github/src/strategies/flat-folder/store-pull.ts index 6b2d23e..a435538 100644 --- a/packages/ingest-github/src/strategies/flat-folder/store-pull.ts +++ b/packages/ingest-github/src/strategies/flat-folder/store-pull.ts @@ -3,9 +3,8 @@ import { logger } from "@bb/logger"; import { filesGraph, foldersGraph, repoGraph, indexesGraph } from "@bb/graph-db"; import { rawDb } from "@bb/db"; import type { GithubIndexPayload } from "@bb/types"; -import type { NodeScope } from "@bb/graph-core"; +import type { NodeScope, UpsertFileNodeInput } from "@bb/graph-core"; import type { MetaPaths } from "#src/types/meta-paths.ts"; -import type { CondensedFileAnalysis } from "#src/types/condensed-file-analysis.ts"; import { throwIfCancelled } from "#src/pipeline/cancellation.ts"; import type { DiffResult } from "#src/pipeline/git-diff.ts"; import { readCondensed } from "#src/strategies/flat-folder/big-file/storage.ts"; @@ -99,33 +98,57 @@ export async function storePullAnalysis(input: StorePullInput): Promise r.newPath), ]; - const seen = new Set(); - for (const relativePath of upsertPaths) { - if (seen.has(relativePath)) { - continue; - } - seen.add(relativePath); - throwIfCancelled(input.scope.knowledgeId); - - const condensed = await readCondensed(input.metaPaths, relativePath); - if (condensed === null) { - logger.warn(`pull-store: condensed analysis missing for ${relativePath}; skipping file upsert`); - continue; - } - - const folderPath = directFolderOf(relativePath); - if (!folderPaths.has(folderPath)) { - await foldersGraph.upsertFolderNode({ - scope: input.scope, + async function* yieldFiles() { + const seen = new Set(); + for (const relativePath of upsertPaths) { + if (seen.has(relativePath)) { + continue; + } + seen.add(relativePath); + throwIfCancelled(input.scope.knowledgeId); + + const condensed = await readCondensed(input.metaPaths, relativePath); + if (condensed === null) { + logger.warn(`pull-store: condensed analysis missing for ${relativePath}; skipping file upsert`); + continue; + } + + const folderPath = directFolderOf(relativePath); + if (!folderPaths.has(folderPath)) { + await foldersGraph.upsertFolderNode({ + scope: input.scope, + folderPath, + summary: emptyFolderPayload(), + }); + folderPaths.add(folderPath); + foldersUpserted += 1; + } + + const upsertInput: UpsertFileNodeInput = { + orgId: input.scope.orgId, + knowledgeId: input.scope.knowledgeId, + repoId: input.scope.repoId, + relativePath: condensed.relativePath, folderPath, - summary: emptyFolderPayload(), - }); - folderPaths.add(folderPath); - foldersUpserted += 1; + language: condensed.language.length > 0 ? condensed.language : languageFromPath(condensed.relativePath), + sha: condensed.sha256, + sizeBytes: condensed.sizeBytes, + analysis: condensed.analysis, + isBigFile: condensed.isBigFile, + totalChunks: condensed.totalChunks, + totalTokenCount: condensed.totalTokenCount, + }; + filesUpserted += 1; + yield upsertInput; } + } - await upsertFileNodeFromCondensed(input.scope, folderPath, condensed); - filesUpserted += 1; + if (typeof filesGraph.bulkUpsertFiles === "function") { + await filesGraph.bulkUpsertFiles(input.scope.knowledgeId, yieldFiles()); + } else { + for await (const f of yieldFiles()) { + await filesGraph.upsertFileNode(f); + } } logger.info( @@ -134,27 +157,6 @@ export async function storePullAnalysis(input: StorePullInput): Promise { - await filesGraph.upsertFileNode({ - orgId: scope.orgId, - knowledgeId: scope.knowledgeId, - repoId: scope.repoId, - relativePath: file.relativePath, - folderPath, - language: file.language.length > 0 ? file.language : languageFromPath(file.relativePath), - sha: file.sha256, - sizeBytes: file.sizeBytes, - analysis: file.analysis, - isBigFile: file.isBigFile, - totalChunks: file.totalChunks, - totalTokenCount: file.totalTokenCount, - }); -} - function shapeFolderPayload(folder: FolderSummary): { purpose: string; summary: string; diff --git a/packages/ladybug/README.md b/packages/ladybug/README.md new file mode 100644 index 0000000..8888df6 --- /dev/null +++ b/packages/ladybug/README.md @@ -0,0 +1,64 @@ +# `@bb/ladybug` — context + +## Tier + +Infrastructure. Depends on Kernel (`@bb/types` for `Config` and `KnowledgeState`, `@bb/errors` for typed error classes) and Sibling (`@bb/config` for `Config.LadybugPath`). +May be imported by Strategy (`@bb/queue` workers via `@bb/ingest-github`), Domain, and Binaries — never by `@bb/cli`. + +This package implements the `@bb/graph-core` `IGraphDatabaseProvider` interface for **LadybugDB**, an embedded property graph OLAP database. + +## Responsibility + +The package owns: + +- A single shared `@ladybugdb/core` `Database` and `Connection` instance (lazy connection; graceful close). +- A health probe (`pingLadybug`). +- An internal `_runCypher(query, params)` helper that compiles and runs a query, caching prepared statements globally. +- Schema bootstrap — creating tables for `Knowledge`, `Repo`, `Folder`, `File`, `FileVersion`, `Keyword`, `Class`, `Function`, and `Module`. +- Knowledge-node CRUD (`upsertKnowledgeNode`, `setKnowledgeStateInGraph`, `deleteKnowledgeGraph`). +- Folder and Repository CRUD (`upsertFolderNode`, `upsertRepoNode`). +- Optimized File-node Bulk Upsert (`bulkUpsertFiles`) — maps files to Parquet rows, writes them to temporary files on disk, and executes single-transaction `DELETE` and SQL `COPY FROM` commands. +- File-node Snapshotting (`snapshotFilesToVersion`) — copies live files to snapshots before updates. + +## Public exports + +```ts +function connectLadybug(): Promise; +function closeLadybug(): Promise; +function pingLadybug(): Promise; + +function upsertKnowledgeNode(doc: KnowledgeDoc): Promise; +function setKnowledgeStateInGraph(knowledgeId: string, state: KnowledgeState): Promise; +function deleteKnowledgeGraph(knowledgeId: string): Promise; +function upsertFileNode(input: UpsertFileNodeInput): Promise; +function bulkUpsertFiles(knowledgeId: string, fileStream: AsyncIterable): Promise; +function deleteFileNodes(knowledgeId: string, paths: string[]): Promise; + +function runCypher(query: string, params?: Record): Promise; +``` + +## Graph schema (v1) + +``` +(:Knowledge {knowledgeId, sourceKind, sourceUrl, branch, repoName, state, createdAt, updatedAt}) + -[:HAS_FILE]-> +(:File {id, orgId, knowledgeId, repoId, relativePath, language, sha, sizeBytes, purpose, summary, businessContext, ...}) + -[:HAS_KEYWORD]-> (:Keyword {name}) // global, lowercase, MERGE-deduped + -[:HAS_CLASS]-> (:Class {signature}) // global, MERGE-deduped + -[:HAS_FUNCTION]-> (:Function {signature}) // global, MERGE-deduped + -[:HAS_IMPORT_INTERNAL]-> (:Module {name}) // relative imports (./ or ../) + -[:HAS_IMPORT_EXTERNAL]-> (:Module {name}) // external packages / stdlib +``` + +Uniqueness and primary keys are enforced through computed surrogate IDs (e.g. `${knowledgeId}::${relativePath}`) due to LadybugDB's single-column primary key constraints. + +Polymorphic relationships (such as `CONTAINS` and `HAS_KEYWORD`) are loaded with explicit query routing: + +- `COPY CONTAINS FROM '...' (FROM='Folder', TO='File')` +- `COPY HAS_KEYWORD FROM '...' (FROM='File', TO='Keyword')` + +## Invariants + +1. **Memory-Safe Streaming Ingestion**: `bulkUpsertFiles` uses an `AsyncIterable` stream, writing inputs to temporary Parquet files on disk immediately to prevent heap allocation failures for large codebases. +2. **Graceful Connection**: Lazy initialization ensures connection runs once and caches the `Database` and `Connection` handles cleanly. +3. **Parameter Type Casting**: Explicit casts to `LbugValue` ensure type compatibility when executing parameter queries. diff --git a/packages/ladybug/package.json b/packages/ladybug/package.json new file mode 100644 index 0000000..f993e80 --- /dev/null +++ b/packages/ladybug/package.json @@ -0,0 +1,26 @@ +{ + "name": "@bb/ladybug", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "imports": { + "#src/*": "./src/*" + }, + "dependencies": { + "@bb/config": "workspace:*", + "@bb/errors": "workspace:*", + "@bb/graph-core": "workspace:*", + "@bb/graph-db": "workspace:*", + "@bb/types": "workspace:*", + "@ladybugdb/core": "^0.16.1", + "parquetjs": "^0.11.2" + }, + "devDependencies": { + "@types/parquetjs": "^0.10.6" + } +} diff --git a/packages/ladybug/src/README.md b/packages/ladybug/src/README.md new file mode 100644 index 0000000..af2e139 --- /dev/null +++ b/packages/ladybug/src/README.md @@ -0,0 +1,24 @@ +# `@bb/ladybug/src` — context + +Implementation of `@bb/ladybug`. See [../README.md](../README.md) for the package-level contract; this file documents the code structure of the source directory. + +## Files + +- **[index.ts](index.ts)** — Public entrypoint. Re-exports driver controls, entity repositories, and helper types. +- **[client.ts](client.ts)** — Connection lifecycle (`connectLadybug`, `closeLadybug`), schema initialization, global Prepared Statement caching, and parameter query execution. +- **[provider.ts](provider.ts)** — Registers the `"ladybug"` provider and packages the repositories to conform to the `IGraphDatabaseProvider` contract. +- **[files.ts](files.ts)** — Handles files. Implements `bulkUpsertFiles` utilizing `parquetjs` writers and SQL `COPY FROM` commands, writing incoming streams directly to disk. +- **[fileVersions.ts](fileVersions.ts)** — Snapshots file records into the `FileVersion` table before updates. +- **[folder.ts](folder.ts)** — Manages folder node upserting. +- **[repo.ts](repo.ts)** — Manages repository node upserting. +- **[knowledge.ts](knowledge.ts)** — Manages knowledge metadata, branch state, and asynchronous sweeping of orphan entity nodes (`vacuumOrphanEntities`). +- **[indexes.ts](indexes.ts)** & **[flatFolderIndexes.ts](flatFolderIndexes.ts)** — No-op files satisfying interface constraints (indexing is natively optimized in LadybugDB). + +## Invariants + +- **PreparedStatement Caching**: All query strings run via `_runCypher` check a global map for a cached `PreparedStatement` instance, avoiding redundant compiling overhead during loops. +- **Surrogate Keys**: Primary keys are computed strictly in TypeScript (e.g. `${knowledgeId}::${relativePath}`) before inserts. +- **Clean Slate**: `bulkUpsertFiles` executes a targeted clean slate delete of `File` nodes matching the `knowledgeId` before loading the new files, keeping transactions atomic. +- **Polymorphic Copy Mapping**: Restricts database-side ambiguity by passing explicit parameter routing: + - `(FROM='Folder', TO='File')` for `CONTAINS` + - `(FROM='File', TO='Keyword')` for `HAS_KEYWORD` diff --git a/packages/ladybug/src/client.ts b/packages/ladybug/src/client.ts new file mode 100644 index 0000000..d36e2d2 --- /dev/null +++ b/packages/ladybug/src/client.ts @@ -0,0 +1,254 @@ +import { Database, Connection, PreparedStatement, type LbugValue } from "@ladybugdb/core"; +import { getConfigValue } from "@bb/config"; +import { Config } from "@bb/types"; + +export interface PingResult { + ok: boolean; + latencyMs: number; +} + +let db: Database | null = null; +let conn: Connection | null = null; +let connecting: Promise | null = null; + +export async function connectLadybug(): Promise { + if (conn !== null) { + return; + } + if (connecting !== null) { + return connecting; + } + connecting = doConnect().finally(() => { + connecting = null; + }); + return connecting; +} + +async function doConnect(): Promise { + let dbPath = getConfigValue(Config.LadybugPath); + if (dbPath === "") { + dbPath = ":memory:"; + } + + try { + db = new Database(dbPath); + conn = new Connection(db); + await ensureSchema(conn); + } catch (cause: unknown) { + if (db) { + db = null; + } + conn = null; + throw new Error( + `Failed to connect to LadybugDB at '${dbPath}': ${cause instanceof Error ? cause.message : String(cause)}`, + { cause }, + ); + } +} + +async function ensureSchema(c: Connection): Promise { + const nodeTables = [ + `CREATE NODE TABLE Knowledge ( + knowledgeId STRING PRIMARY KEY, + createdAt STRING, + sourceKind STRING, + sourceUrl STRING, + branch STRING, + repoName STRING, + state STRING, + updatedAt STRING + )`, + `CREATE NODE TABLE Repo ( + id STRING PRIMARY KEY, + orgId STRING, + knowledgeId STRING, + repoId STRING, + repoUrl STRING, + branch STRING, + purpose STRING, + summary STRING, + architecture STRING, + dataFlow STRING, + majorSubsystems STRING[], + keyPatterns STRING[], + updatedAt STRING + )`, + `CREATE NODE TABLE Folder ( + id STRING PRIMARY KEY, + orgId STRING, + knowledgeId STRING, + repoId STRING, + folderPath STRING, + purpose STRING, + summary STRING, + dependencyGraph STRING, + updatedAt STRING + )`, + `CREATE NODE TABLE File ( + id STRING PRIMARY KEY, + orgId STRING, + knowledgeId STRING, + repoId STRING, + relativePath STRING, + language STRING, + sha STRING, + sizeBytes INT64, + purpose STRING, + summary STRING, + businessContext STRING, + dataFlowDirection STRING, + ontologyConcepts STRING[], + businessEntities STRING[], + systemCapabilities STRING[], + sideEffects STRING[], + configDependencies STRING[], + integrationSurface STRING[], + contractsProvided STRING[], + contractsConsumed STRING[], + sectionNames STRING[], + sectionDescriptions STRING[], + isBigFile BOOLEAN, + totalChunks INT64, + totalTokenCount INT64, + updatedAt STRING + )`, + `CREATE NODE TABLE FileVersion ( + id STRING PRIMARY KEY, + knowledgeId STRING, + relativePath STRING, + commitHash STRING, + language STRING, + sha STRING, + sizeBytes INT64, + purpose STRING, + summary STRING, + businessContext STRING, + dataFlowDirection STRING, + ontologyConcepts STRING[], + businessEntities STRING[], + systemCapabilities STRING[], + sideEffects STRING[], + configDependencies STRING[], + integrationSurface STRING[], + contractsProvided STRING[], + contractsConsumed STRING[], + sectionNames STRING[], + sectionDescriptions STRING[], + snapshotAt STRING + )`, + `CREATE NODE TABLE Keyword ( + name STRING PRIMARY KEY + )`, + `CREATE NODE TABLE Class ( + signature STRING PRIMARY KEY + )`, + `CREATE NODE TABLE Function ( + signature STRING PRIMARY KEY + )`, + `CREATE NODE TABLE Module ( + name STRING PRIMARY KEY + )`, + ]; + + const relTables = [ + `CREATE REL TABLE HAS_REPO (FROM Knowledge TO Repo)`, + `CREATE REL TABLE HAS_FILE (FROM Knowledge TO File)`, + `CREATE REL TABLE CONTAINS (FROM Repo TO Folder, FROM Folder TO Folder, FROM Folder TO File)`, + `CREATE REL TABLE HAS_KEYWORD (FROM File TO Keyword, FROM Folder TO Keyword, FROM Repo TO Keyword)`, + `CREATE REL TABLE HAS_CLASS (FROM File TO Class)`, + `CREATE REL TABLE HAS_FUNCTION (FROM File TO Function)`, + `CREATE REL TABLE HAS_IMPORT_INTERNAL (FROM File TO Module)`, + `CREATE REL TABLE HAS_IMPORT_EXTERNAL (FROM File TO Module)`, + `CREATE REL TABLE HAS_VERSION (FROM File TO FileVersion)`, + ]; + + for (const q of [...nodeTables, ...relTables]) { + try { + await c.query(q); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + if ( + !msg.includes("already exists") && + !msg.includes("table already exists") && + !msg.includes("Binder exception") + ) { + throw e; + } + } + } +} + +export async function closeLadybug(): Promise { + conn = null; + db = null; +} + +export async function pingLadybug(): Promise { + if (conn === null) { + return { ok: false, latencyMs: 0 }; + } + const start = performance.now(); + try { + await conn.query("MATCH (k:Knowledge) RETURN count(k) LIMIT 1"); + return { ok: true, latencyMs: Math.round(performance.now() - start) }; + } catch { + return { ok: false, latencyMs: Math.round(performance.now() - start) }; + } +} + +export function _getConnection(): Connection { + if (conn === null) { + throw new Error("LadybugDB not connected. Call connectLadybug() first."); + } + return conn; +} +//OPTIMIZATION NEEDED +// export async function _runCypher(query: string, params: Record = {}): Promise { +// const c = _getConnection(); +// const prepared = await c.prepare(query); +// if (!prepared.isSuccess()) { +// throw new Error(`Failed to prepare query: ${prepared.getErrorMessage()}`); +// } +// const result = await c.execute(prepared, params); +// const singleResult = Array.isArray(result) ? result[0] : result; +// if (!singleResult) { +// throw new Error("No query result returned from LadybugDB"); +// } +// const rows = await singleResult.getAll(); +// return rows as T[]; +// } + +// Add a global cache map at the top of client.ts +const preparedCache = new Map(); + +export async function _runCypher(query: string, params: Record = {}): Promise { + const c = _getConnection(); + + // 1. Check if the query has already been compiled and compiled plan is cached + let prepared = preparedCache.get(query); + + if (!prepared) { + prepared = await c.prepare(query); + if (!prepared.isSuccess()) { + throw new Error(`Failed to prepare query: ${prepared.getErrorMessage()}`); + } + // 2. Store it for future iterations in the ingest loop + preparedCache.set(query, prepared); + } + + const result = await c.execute(prepared, params); + const singleResult = Array.isArray(result) ? result[0] : result; + if (!singleResult) { + throw new Error("No query result returned from LadybugDB"); + } + const rows = await singleResult.getAll(); + return rows as T[]; +} + +// Clear the cache if tests reset +export function __resetForTests(): void { + db = null; + conn = null; + connecting = null; + preparedCache.clear(); // Clear cache here +} diff --git a/packages/ladybug/src/fileSchemas.ts b/packages/ladybug/src/fileSchemas.ts new file mode 100644 index 0000000..943ad13 --- /dev/null +++ b/packages/ladybug/src/fileSchemas.ts @@ -0,0 +1,51 @@ +import { ParquetSchema } from "parquetjs"; +import type { FileAnalysis } from "@bb/types"; + +export const fileParquetSchema = new ParquetSchema({ + id: { type: "UTF8" }, + orgId: { type: "UTF8" }, + knowledgeId: { type: "UTF8" }, + repoId: { type: "UTF8" }, + relativePath: { type: "UTF8" }, + language: { type: "UTF8" }, + sha: { type: "UTF8" }, + sizeBytes: { type: "INT64" }, + purpose: { type: "UTF8" }, + summary: { type: "UTF8" }, + businessContext: { type: "UTF8" }, + dataFlowDirection: { type: "UTF8" }, + ontologyConcepts: { type: "UTF8", repeated: true }, + businessEntities: { type: "UTF8", repeated: true }, + systemCapabilities: { type: "UTF8", repeated: true }, + sideEffects: { type: "UTF8", repeated: true }, + configDependencies: { type: "UTF8", repeated: true }, + integrationSurface: { type: "UTF8", repeated: true }, + contractsProvided: { type: "UTF8", repeated: true }, + contractsConsumed: { type: "UTF8", repeated: true }, + sectionNames: { type: "UTF8", repeated: true }, + sectionDescriptions: { type: "UTF8", repeated: true }, + isBigFile: { type: "BOOLEAN" }, + totalChunks: { type: "INT64" }, + totalTokenCount: { type: "INT64" }, + updatedAt: { type: "UTF8" }, +}); + +export const relParquetSchema = new ParquetSchema({ + from: { type: "UTF8" }, + to: { type: "UTF8" }, +}); + +export interface UpsertFileNodeInput { + orgId?: string; + knowledgeId: string; + repoId?: string; + relativePath: string; + language: string; + sha: string; + sizeBytes: number; + analysis: FileAnalysis; + folderPath?: string; + isBigFile?: boolean; + totalChunks?: number; + totalTokenCount?: number; +} diff --git a/packages/ladybug/src/fileVersions.ts b/packages/ladybug/src/fileVersions.ts new file mode 100644 index 0000000..5fd5ee2 --- /dev/null +++ b/packages/ladybug/src/fileVersions.ts @@ -0,0 +1,51 @@ +import { _runCypher } from "./client.ts"; + +/** + * Snapshots the current `:File` set for a knowledge into `:FileVersion` nodes + * tagged with `commitHash`. Run **before** the strategy overwrites the `:File` + * nodes during a pull, so the prior commit's state is preserved as a version + * snapshot rather than being lost. + */ +const SNAPSHOT_FILES_TO_VERSION = ` +MATCH (f:File {knowledgeId: $knowledgeId}) +CREATE (fv:FileVersion { + id: $knowledgeId + "::" + f.relativePath + "::" + $commitHash, + knowledgeId: $knowledgeId, + relativePath: f.relativePath, + commitHash: $commitHash, + language: f.language, + sha: f.sha, + sizeBytes: f.sizeBytes, + purpose: f.purpose, + summary: f.summary, + businessContext: f.businessContext, + dataFlowDirection: f.dataFlowDirection, + ontologyConcepts: f.ontologyConcepts, + businessEntities: f.businessEntities, + systemCapabilities: f.systemCapabilities, + sideEffects: f.sideEffects, + configDependencies: f.configDependencies, + integrationSurface: f.integrationSurface, + contractsProvided: f.contractsProvided, + contractsConsumed: f.contractsConsumed, + sectionNames: f.sectionNames, + sectionDescriptions: f.sectionDescriptions, + snapshotAt: $snapshotAt +}) +CREATE (f)-[:HAS_VERSION]->(fv) +`; + +export interface SnapshotFilesInput { + knowledgeId: string; + /** The commit the current `:File` state corresponds to — i.e. the OLD commitId being archived. */ + commitHash: string; +} + +/** Copies every live `:File` into a `:FileVersion(commitHash)` snapshot. */ +export async function snapshotFilesToVersion(input: SnapshotFilesInput): Promise { + await _runCypher(SNAPSHOT_FILES_TO_VERSION, { + knowledgeId: input.knowledgeId, + commitHash: input.commitHash, + snapshotAt: new Date().toISOString(), + }); +} diff --git a/packages/ladybug/src/files.ts b/packages/ladybug/src/files.ts new file mode 100644 index 0000000..215564a --- /dev/null +++ b/packages/ladybug/src/files.ts @@ -0,0 +1,275 @@ +import { _runCypher } from "./client.ts"; +import { ParquetSchema, ParquetWriter } from "parquetjs"; +import { join } from "node:path"; +import { unlinkSync } from "node:fs"; +import { fileParquetSchema, relParquetSchema } from "./fileSchemas.ts"; +import type { UpsertFileNodeInput } from "./fileSchemas.ts"; + +const DELETE_FILES = ` +MATCH (f:File) +WHERE f.id IN $ids +DETACH DELETE f +`; + +export async function deleteFileNodes(knowledgeId: string, relativePaths: string[]): Promise { + if (relativePaths.length === 0) { + return; + } + const ids = relativePaths.map((p) => `${knowledgeId}::${p}`); + await _runCypher(DELETE_FILES, { ids }); +} + +export async function bulkUpsertFiles( + knowledgeId: string, + fileStream: AsyncIterable, +): Promise { + const timestamp = Date.now(); + const rand = Math.random().toString(36).substring(2, 9); + const tempPaths: string[] = []; + + const openWriter = async ( + prefix: string, + schema: ParquetSchema, + ): Promise<{ writer: ParquetWriter; path: string }> => { + const path = join(process.cwd(), `temp_${prefix}_${timestamp}_${rand}.parquet`); + tempPaths.push(path); + const writer = await ParquetWriter.openFile(schema, path); + return { writer, path }; + }; + + // Generate paths and open all writers upfront + const fileWriterInfo = await openWriter("files", fileParquetSchema); + const hasFileRelWriterInfo = await openWriter("has_file_rel", relParquetSchema); + const containsRelWriterInfo = await openWriter("contains_rel", relParquetSchema); + const hasKeywordRelWriterInfo = await openWriter("keyword_rel", relParquetSchema); + const hasClassRelWriterInfo = await openWriter("class_rel", relParquetSchema); + const hasFunctionRelWriterInfo = await openWriter("function_rel", relParquetSchema); + const hasImportInternalRelWriterInfo = await openWriter("import_int_rel", relParquetSchema); + const hasImportExternalRelWriterInfo = await openWriter("import_ext_rel", relParquetSchema); + + // Initialize record counters to selectively run COPY queries + let fileCount = 0; + let hasFileCount = 0; + let containsCount = 0; + let keywordCount = 0; + let classCount = 0; + let functionCount = 0; + let importIntCount = 0; + let importExtCount = 0; + + try { + const allKeywords = new Set(); + const allClasses = new Set(); + const allFunctions = new Set(); + const allImportsInternal = new Set(); + const allImportsExternal = new Set(); + + for await (const input of fileStream) { + const orgId = input.orgId ?? "local"; + const repoId = input.repoId ?? input.knowledgeId; + const id = `${input.knowledgeId}::${input.relativePath}`; + + // Collect entities for UNWIND MERGE + for (const kw of input.analysis.keywords) { + allKeywords.add(kw.toLowerCase()); + } + for (const c of input.analysis.classes) { + allClasses.add(c); + } + for (const f of input.analysis.functions) { + allFunctions.add(f); + } + for (const i of input.analysis.importsInternal) { + allImportsInternal.add(i); + } + for (const e of input.analysis.importsExternal) { + allImportsExternal.add(e); + } + + // Write file node row + const sectionMap = input.analysis.sectionMap ?? []; + const fileRow = { + id, + orgId, + knowledgeId: input.knowledgeId, + repoId, + relativePath: input.relativePath, + language: input.language, + sha: input.sha, + sizeBytes: input.sizeBytes, + purpose: input.analysis.purpose, + summary: input.analysis.summary, + businessContext: input.analysis.businessContext, + dataFlowDirection: input.analysis.dataFlowDirection ?? "", + ontologyConcepts: input.analysis.ontologyConcepts ?? [], + businessEntities: input.analysis.businessEntities ?? [], + systemCapabilities: input.analysis.systemCapabilities ?? [], + sideEffects: input.analysis.sideEffects ?? [], + configDependencies: input.analysis.configDependencies ?? [], + integrationSurface: input.analysis.integrationSurface ?? [], + contractsProvided: input.analysis.contractsProvided ?? [], + contractsConsumed: input.analysis.contractsConsumed ?? [], + sectionNames: sectionMap.map((s) => s.name), + sectionDescriptions: sectionMap.map((s) => s.description), + isBigFile: input.isBigFile ?? false, + totalChunks: input.totalChunks ?? 0, + totalTokenCount: input.totalTokenCount ?? 0, + updatedAt: new Date().toISOString(), + }; + await fileWriterInfo.writer.appendRow(fileRow); + fileCount++; + + // HAS_FILE link row + await hasFileRelWriterInfo.writer.appendRow({ from: input.knowledgeId, to: id }); + hasFileCount++; + + // CONTAINS link row (Folder) + if (input.folderPath !== undefined) { + const folderId = `${orgId}::${input.knowledgeId}::${repoId}::${input.folderPath}`; + await containsRelWriterInfo.writer.appendRow({ from: folderId, to: id }); + containsCount++; + } + + // HAS_KEYWORD rows + if (input.analysis.keywords.length > 0) { + for (const kw of input.analysis.keywords) { + await hasKeywordRelWriterInfo.writer.appendRow({ from: id, to: kw.toLowerCase() }); + keywordCount++; + } + } + + // HAS_CLASS rows + if (input.analysis.classes.length > 0) { + for (const c of input.analysis.classes) { + await hasClassRelWriterInfo.writer.appendRow({ from: id, to: c }); + classCount++; + } + } + + // HAS_FUNCTION rows + if (input.analysis.functions.length > 0) { + for (const f of input.analysis.functions) { + await hasFunctionRelWriterInfo.writer.appendRow({ from: id, to: f }); + functionCount++; + } + } + + // HAS_IMPORT_INTERNAL rows + if (input.analysis.importsInternal.length > 0) { + for (const i of input.analysis.importsInternal) { + await hasImportInternalRelWriterInfo.writer.appendRow({ from: id, to: i }); + importIntCount++; + } + } + + // HAS_IMPORT_EXTERNAL rows + if (input.analysis.importsExternal.length > 0) { + for (const e of input.analysis.importsExternal) { + await hasImportExternalRelWriterInfo.writer.appendRow({ from: id, to: e }); + importExtCount++; + } + } + } + + // Close all open writers + await fileWriterInfo.writer.close(); + await hasFileRelWriterInfo.writer.close(); + await containsRelWriterInfo.writer.close(); + await hasKeywordRelWriterInfo.writer.close(); + await hasClassRelWriterInfo.writer.close(); + await hasFunctionRelWriterInfo.writer.close(); + await hasImportInternalRelWriterInfo.writer.close(); + await hasImportExternalRelWriterInfo.writer.close(); + + // If no files were written, we are done + if (fileCount === 0) { + return; + } + + // A single Cypher query to clear out old data for this knowledgeId + // Clean slate deletion: MATCH (f:File {knowledgeId: $knowledgeId}) DETACH DELETE f + await _runCypher( + `MATCH (f:File {knowledgeId: $knowledgeId}) + DETACH DELETE f`, + { knowledgeId }, + ); + + // UNWIND MERGE queries for referenced nodes + if (allKeywords.size > 0) { + await _runCypher( + `UNWIND $names AS name + MERGE (kw:Keyword {name: name})`, + { names: Array.from(allKeywords) }, + ); + } + if (allClasses.size > 0) { + await _runCypher( + `UNWIND $signatures AS signature + MERGE (c:Class {signature: signature})`, + { signatures: Array.from(allClasses) }, + ); + } + if (allFunctions.size > 0) { + await _runCypher( + `UNWIND $signatures AS signature + MERGE (fn:Function {signature: signature})`, + { signatures: Array.from(allFunctions) }, + ); + } + if (allImportsInternal.size > 0) { + await _runCypher( + `UNWIND $names AS name + MERGE (m:Module {name: name})`, + { names: Array.from(allImportsInternal) }, + ); + } + if (allImportsExternal.size > 0) { + await _runCypher( + `UNWIND $names AS name + MERGE (m:Module {name: name})`, + { names: Array.from(allImportsExternal) }, + ); + } + + // Execute COPY FROM commands exactly once + if (fileCount > 0) { + await _runCypher(`COPY File FROM '${fileWriterInfo.path}'`); + } + if (hasFileCount > 0) { + await _runCypher(`COPY HAS_FILE FROM '${hasFileRelWriterInfo.path}'`); + } + if (containsCount > 0) { + await _runCypher(`COPY CONTAINS FROM '${containsRelWriterInfo.path}' (FROM='Folder', TO='File')`); + } + if (keywordCount > 0) { + await _runCypher(`COPY HAS_KEYWORD FROM '${hasKeywordRelWriterInfo.path}' (FROM='File', TO='Keyword')`); + } + if (classCount > 0) { + await _runCypher(`COPY HAS_CLASS FROM '${hasClassRelWriterInfo.path}'`); + } + if (functionCount > 0) { + await _runCypher(`COPY HAS_FUNCTION FROM '${hasFunctionRelWriterInfo.path}'`); + } + if (importIntCount > 0) { + await _runCypher(`COPY HAS_IMPORT_INTERNAL FROM '${hasImportInternalRelWriterInfo.path}'`); + } + if (importExtCount > 0) { + await _runCypher(`COPY HAS_IMPORT_EXTERNAL FROM '${hasImportExternalRelWriterInfo.path}'`); + } + } finally { + for (const p of tempPaths) { + try { + unlinkSync(p); + } catch { + // ignore + } + } + } +} + +export async function upsertFileNode(input: UpsertFileNodeInput): Promise { + async function* single() { + yield input; + } + await bulkUpsertFiles(input.knowledgeId, single()); +} diff --git a/packages/ladybug/src/flatFolderIndexes.ts b/packages/ladybug/src/flatFolderIndexes.ts new file mode 100644 index 0000000..cc809e1 --- /dev/null +++ b/packages/ladybug/src/flatFolderIndexes.ts @@ -0,0 +1,5 @@ +export async function ensureFlatFolderIndexes(): Promise { + // LadybugDB implements uniqueness natively via PRIMARY KEY constraints defined during schema creation. + // Full-text indexes are not supported via Cypher index syntax in LadybugDB; standard MATCH scans are used instead. + return Promise.resolve(); +} diff --git a/packages/ladybug/src/folder.ts b/packages/ladybug/src/folder.ts new file mode 100644 index 0000000..d29a204 --- /dev/null +++ b/packages/ladybug/src/folder.ts @@ -0,0 +1,78 @@ +import { _runCypher } from "./client.ts"; +import type { NodeScope } from "./repo.ts"; + +export interface FolderSummaryPayload { + purpose: string; + summary: string; + keywords: string[]; + classes: string[]; + functions: string[]; + importsInternal: string[]; + importsExternal: string[]; + dependencyGraph: string; +} + +export interface UpsertFolderNodeInput { + scope: NodeScope; + folderPath: string; + summary: FolderSummaryPayload; +} + +const UPSERT_FOLDER = ` +MERGE (folder:Folder {id: $id}) +SET folder.orgId = $orgId, + folder.knowledgeId = $knowledgeId, + folder.repoId = $repoId, + folder.folderPath = $folderPath, + folder.purpose = $purpose, + folder.summary = $summary, + folder.dependencyGraph = $dependencyGraph, + folder.updatedAt = $updatedAt +WITH folder +MATCH (r:Repo {id: $repoId_surrogate}) +MERGE (r)-[:CONTAINS]->(folder) +`; + +const CLEAR_FOLDER_KEYWORDS = ` +MATCH (folder:Folder {id: $id})-[rel:HAS_KEYWORD]->() +DELETE rel +`; + +const ATTACH_FOLDER_KEYWORDS = ` +MATCH (folder:Folder {id: $id}) +UNWIND $names AS name +MERGE (kw:Keyword {name: name}) +CREATE (folder)-[:HAS_KEYWORD]->(kw) +`; + +export async function upsertFolderNode(input: UpsertFolderNodeInput): Promise { + const scope = input.scope; + const id = `${scope.orgId}::${scope.knowledgeId}::${scope.repoId}::${input.folderPath}`; + const repoId_surrogate = `${scope.orgId}::${scope.knowledgeId}::${scope.repoId}`; + + const params = { + id, + orgId: scope.orgId, + knowledgeId: scope.knowledgeId, + repoId: scope.repoId, + folderPath: input.folderPath, + repoId_surrogate, + }; + + await _runCypher(UPSERT_FOLDER, { + ...params, + purpose: input.summary.purpose, + summary: input.summary.summary, + dependencyGraph: input.summary.dependencyGraph, + updatedAt: new Date().toISOString(), + }); + + await _runCypher(CLEAR_FOLDER_KEYWORDS, { id }); + + if (input.summary.keywords.length > 0) { + await _runCypher(ATTACH_FOLDER_KEYWORDS, { + id, + names: input.summary.keywords.map((k) => k.toLowerCase()), + }); + } +} diff --git a/packages/ladybug/src/index.ts b/packages/ladybug/src/index.ts new file mode 100644 index 0000000..ac157b3 --- /dev/null +++ b/packages/ladybug/src/index.ts @@ -0,0 +1,28 @@ +import "./provider.ts"; + +export { connectLadybug, closeLadybug, pingLadybug } from "./client.ts"; +export { _runCypher as runCypher } from "./client.ts"; +export type { PingResult } from "./client.ts"; + +export { ensureKnowledgeIndexes } from "./indexes.ts"; +export { ensureFlatFolderIndexes } from "./flatFolderIndexes.ts"; + +export { + upsertKnowledgeNode, + setKnowledgeStateInGraph, + setKnowledgeBranchInGraph, + deleteKnowledgeGraph, + vacuumOrphanEntities, +} from "./knowledge.ts"; + +export { upsertFileNode, deleteFileNodes, bulkUpsertFiles } from "./files.ts"; +export type { UpsertFileNodeInput } from "./fileSchemas.ts"; + +export { upsertRepoNode } from "./repo.ts"; +export type { NodeScope, RepoSummaryPayload, UpsertRepoNodeInput } from "./repo.ts"; + +export { upsertFolderNode } from "./folder.ts"; +export type { FolderSummaryPayload, UpsertFolderNodeInput } from "./folder.ts"; + +export { snapshotFilesToVersion } from "./fileVersions.ts"; +export type { SnapshotFilesInput } from "./fileVersions.ts"; diff --git a/packages/ladybug/src/indexes.ts b/packages/ladybug/src/indexes.ts new file mode 100644 index 0000000..3a9930e --- /dev/null +++ b/packages/ladybug/src/indexes.ts @@ -0,0 +1,5 @@ +export async function ensureKnowledgeIndexes(): Promise { + // LadybugDB implements uniqueness natively via PRIMARY KEY constraints defined during schema creation. + // Full-text indexes are not supported via Cypher index syntax in LadybugDB; standard MATCH scans are used instead. + return Promise.resolve(); +} diff --git a/packages/ladybug/src/knowledge.ts b/packages/ladybug/src/knowledge.ts new file mode 100644 index 0000000..bbbcbbc --- /dev/null +++ b/packages/ladybug/src/knowledge.ts @@ -0,0 +1,149 @@ +import path from "node:path"; +import type { KnowledgeDoc, KnowledgeState } from "@bb/types"; +import { _runCypher } from "./client.ts"; + +const UPSERT_KNOWLEDGE = ` +MERGE (k:Knowledge {knowledgeId: $knowledgeId}) +ON CREATE SET k.createdAt = $createdAt +SET k.sourceKind = $sourceKind, + k.sourceUrl = $sourceUrl, + k.branch = $branch, + k.repoName = $repoName, + k.state = $state, + k.updatedAt = $updatedAt +`; + +const SET_STATE = ` +MATCH (k:Knowledge {knowledgeId: $knowledgeId}) +SET k.state = $state, k.updatedAt = $updatedAt +`; + +const SET_BRANCH = ` +MATCH (k:Knowledge {knowledgeId: $knowledgeId}) +SET k.branch = $branch, k.updatedAt = $updatedAt +`; + +const DELETE_FILES_BY_KNOWLEDGE = ` +MATCH (f:File {knowledgeId: $knowledgeId}) +DETACH DELETE f +`; + +const DELETE_REPOS_BY_KNOWLEDGE = ` +MATCH (r:Repo {knowledgeId: $knowledgeId}) +DETACH DELETE r +`; + +const DELETE_FOLDERS_BY_KNOWLEDGE = ` +MATCH (folder:Folder {knowledgeId: $knowledgeId}) +DETACH DELETE folder +`; + +const DELETE_KNOWLEDGE_NODE = ` +MATCH (k:Knowledge {knowledgeId: $knowledgeId}) +DETACH DELETE k +`; + +// Defensive cleanup: wipe File nodes whose knowledgeId has no matching :Knowledge. +const DELETE_ORPHAN_FILES = ` +MATCH (f:File) +WHERE NOT EXISTS { MATCH (k:Knowledge {knowledgeId: f.knowledgeId}) } +DETACH DELETE f +`; + +// Sweep orphan entities individually. +const DELETE_ORPHAN_KEYWORDS = ` +MATCH (n:Keyword) +WHERE NOT EXISTS { MATCH (:File)-[]->(n) } AND NOT EXISTS { MATCH (:Folder)-[]->(n) } AND NOT EXISTS { MATCH (:Repo)-[]->(n) } +DETACH DELETE n +`; + +const DELETE_ORPHAN_CLASSES = ` +MATCH (n:Class) +WHERE NOT EXISTS { MATCH (:File)-[]->(n) } +DETACH DELETE n +`; + +const DELETE_ORPHAN_FUNCTIONS = ` +MATCH (n:Function) +WHERE NOT EXISTS { MATCH (:File)-[]->(n) } +DETACH DELETE n +`; + +const DELETE_ORPHAN_MODULES = ` +MATCH (n:Module) +WHERE NOT EXISTS { MATCH (:File)-[]->(n) } +DETACH DELETE n +`; + +export async function upsertKnowledgeNode(doc: KnowledgeDoc): Promise { + const sourceKind = doc.source.kind; + const sourceUrl = doc.source.kind === "github" ? (doc.info.repoUrl ?? "") : doc.source.sourcePath; + const branch = doc.source.kind === "github" ? (doc.info.branch ?? null) : null; + await _runCypher(UPSERT_KNOWLEDGE, { + knowledgeId: doc.knowledgeId, + sourceKind, + sourceUrl, + branch, + repoName: deriveRepoName(doc), + state: doc.status.state, + createdAt: doc.createdAt.toISOString(), + updatedAt: doc.updatedAt.toISOString(), + }); +} + +export async function setKnowledgeStateInGraph(knowledgeId: string, state: KnowledgeState): Promise { + await _runCypher(SET_STATE, { + knowledgeId, + state, + updatedAt: new Date().toISOString(), + }); +} + +export async function setKnowledgeBranchInGraph(knowledgeId: string, branch: string): Promise { + await _runCypher(SET_BRANCH, { + knowledgeId, + branch, + updatedAt: new Date().toISOString(), + }); +} + +export async function deleteKnowledgeGraph(knowledgeId: string): Promise { + await _runCypher(DELETE_FILES_BY_KNOWLEDGE, { knowledgeId }); + await _runCypher(DELETE_REPOS_BY_KNOWLEDGE, { knowledgeId }); + await _runCypher(DELETE_FOLDERS_BY_KNOWLEDGE, { knowledgeId }); + await _runCypher(DELETE_ORPHAN_FILES); + await _runCypher(DELETE_KNOWLEDGE_NODE, { knowledgeId }); +} + +export async function vacuumOrphanEntities(): Promise { + await _runCypher(DELETE_ORPHAN_KEYWORDS); + await _runCypher(DELETE_ORPHAN_CLASSES); + await _runCypher(DELETE_ORPHAN_FUNCTIONS); + await _runCypher(DELETE_ORPHAN_MODULES); +} + +function deriveRepoName(doc: KnowledgeDoc): string { + if (doc.source.kind === "local") { + return path.basename(doc.source.sourcePath); + } + return repoNameFromGithubUrl(doc.info.repoUrl ?? ""); +} + +function repoNameFromGithubUrl(repoUrl: string): string { + let pathname: string; + try { + pathname = new URL(repoUrl).pathname; + } catch { + pathname = repoUrl; + } + const segments = pathname + .split("/") + .map((segment) => segment.trim()) + .filter((segment) => segment.length > 0); + const repo = segments.at(-1); + const owner = segments.at(-2); + if (owner === undefined || repo === undefined) { + return repoUrl; + } + return `${owner}/${repo.replace(/\.git$/u, "")}`; +} diff --git a/packages/ladybug/src/provider.ts b/packages/ladybug/src/provider.ts new file mode 100644 index 0000000..ef64e7c --- /dev/null +++ b/packages/ladybug/src/provider.ts @@ -0,0 +1,61 @@ +import { connectLadybug, closeLadybug, pingLadybug, _runCypher } from "./client.ts"; +import * as knowledgeRepo from "./knowledge.ts"; +import * as filesRepo from "./files.ts"; +import * as fileVersionsRepo from "./fileVersions.ts"; +import * as folderRepo from "./folder.ts"; +import * as repoRepo from "./repo.ts"; +import * as indexRepo from "./indexes.ts"; +import * as flatFolderIndexRepo from "./flatFolderIndexes.ts"; + +import { registerGraphProvider } from "@bb/graph-db"; +import type { IGraphDatabaseProvider } from "@bb/graph-core"; +import type { LbugValue } from "@ladybugdb/core"; + +class LadybugGraphProvider implements IGraphDatabaseProvider { + knowledge = { + upsertKnowledgeNode: knowledgeRepo.upsertKnowledgeNode, + setKnowledgeStateInGraph: knowledgeRepo.setKnowledgeStateInGraph, + setKnowledgeBranchInGraph: knowledgeRepo.setKnowledgeBranchInGraph, + deleteKnowledgeGraph: knowledgeRepo.deleteKnowledgeGraph, + }; + + files = { + upsertFileNode: filesRepo.upsertFileNode, + deleteFileNodes: filesRepo.deleteFileNodes, + snapshotFilesToVersion: fileVersionsRepo.snapshotFilesToVersion, + bulkUpsertFiles: filesRepo.bulkUpsertFiles, + }; + + folders = { + upsertFolderNode: folderRepo.upsertFolderNode, + }; + + repo = { + upsertRepoNode: repoRepo.upsertRepoNode, + }; + + indexes = { + ensureKnowledgeIndexes: indexRepo.ensureKnowledgeIndexes, + ensureFlatFolderIndexes: flatFolderIndexRepo.ensureFlatFolderIndexes, + }; + + async connect(): Promise { + await connectLadybug(); + } + + async close(): Promise { + await closeLadybug(); + } + + async ping() { + return pingLadybug(); + } + + async runCypher(query: string, params?: Record): Promise { + return _runCypher(query, params as Record); + } +} + +registerGraphProvider("ladybug", () => new LadybugGraphProvider()); + +export { vacuumOrphanEntities } from "./knowledge.ts"; diff --git a/packages/ladybug/src/repo.ts b/packages/ladybug/src/repo.ts new file mode 100644 index 0000000..e12f5da --- /dev/null +++ b/packages/ladybug/src/repo.ts @@ -0,0 +1,85 @@ +import { _runCypher } from "./client.ts"; + +export interface NodeScope { + orgId: string; + knowledgeId: string; + repoId: string; +} + +export interface RepoSummaryPayload { + purpose: string; + summary: string; + keywords: string[]; + architecture: string; + dataFlow: string; + majorSubsystems: string[]; + keyPatterns: string[]; +} + +export interface UpsertRepoNodeInput { + scope: NodeScope; + repoUrl: string; + branch: string; + summary: RepoSummaryPayload; +} + +const UPSERT_REPO = ` +MERGE (r:Repo {id: $id}) +SET r.orgId = $orgId, + r.knowledgeId = $knowledgeId, + r.repoId = $repoId, + r.repoUrl = $repoUrl, + r.branch = $branch, + r.purpose = $purpose, + r.summary = $summary, + r.architecture = $architecture, + r.dataFlow = $dataFlow, + r.majorSubsystems = $majorSubsystems, + r.keyPatterns = $keyPatterns, + r.updatedAt = $updatedAt +WITH r +MATCH (k:Knowledge {knowledgeId: $knowledgeId}) +MERGE (k)-[:HAS_REPO]->(r) +`; + +const CLEAR_REPO_KEYWORDS = ` +MATCH (r:Repo {id: $id})-[rel:HAS_KEYWORD]->() +DELETE rel +`; + +const ATTACH_REPO_KEYWORDS = ` +MATCH (r:Repo {id: $id}) +UNWIND $names AS name +MERGE (kw:Keyword {name: name}) +CREATE (r)-[:HAS_KEYWORD]->(kw) +`; + +export async function upsertRepoNode(input: UpsertRepoNodeInput): Promise { + const scope = input.scope; + const id = `${scope.orgId}::${scope.knowledgeId}::${scope.repoId}`; + + await _runCypher(UPSERT_REPO, { + id, + orgId: scope.orgId, + knowledgeId: scope.knowledgeId, + repoId: scope.repoId, + repoUrl: input.repoUrl, + branch: input.branch, + purpose: input.summary.purpose, + summary: input.summary.summary, + architecture: input.summary.architecture, + dataFlow: input.summary.dataFlow, + majorSubsystems: input.summary.majorSubsystems, + keyPatterns: input.summary.keyPatterns, + updatedAt: new Date().toISOString(), + }); + + await _runCypher(CLEAR_REPO_KEYWORDS, { id }); + + if (input.summary.keywords.length > 0) { + await _runCypher(ATTACH_REPO_KEYWORDS, { + id, + names: input.summary.keywords.map((k) => k.toLowerCase()), + }); + } +} diff --git a/packages/ladybug/tsconfig.json b/packages/ladybug/tsconfig.json new file mode 100644 index 0000000..79adbd3 --- /dev/null +++ b/packages/ladybug/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.json"] +} diff --git a/packages/neo4j/src/files-batch.ts b/packages/neo4j/src/files-batch.ts new file mode 100644 index 0000000..72065fb --- /dev/null +++ b/packages/neo4j/src/files-batch.ts @@ -0,0 +1,217 @@ +import { _runInTransaction, type CypherStep } from "./client.ts"; +import type { UpsertFileNodeInput } from "./files.ts"; + +const BATCH_UPSERT_FILES = ` +UNWIND $files AS f +MERGE (file:File {knowledgeId: f.knowledgeId, relativePath: f.relativePath}) +SET file.orgId = f.orgId, + file.repoId = f.repoId, + file.language = f.language, + file.sha = f.sha, + file.sizeBytes = f.sizeBytes, + file.purpose = f.purpose, + file.summary = f.summary, + file.businessContext = f.businessContext, + file.dataFlowDirection = f.dataFlowDirection, + file.ontologyConcepts = f.ontologyConcepts, + file.businessEntities = f.businessEntities, + file.systemCapabilities = f.systemCapabilities, + file.sideEffects = f.sideEffects, + file.configDependencies = f.configDependencies, + file.integrationSurface = f.integrationSurface, + file.contractsProvided = f.contractsProvided, + file.contractsConsumed = f.contractsConsumed, + file.sectionNames = f.sectionNames, + file.sectionDescriptions = f.sectionDescriptions, + file.isBigFile = f.isBigFile, + file.totalChunks = f.totalChunks, + file.totalTokenCount = f.totalTokenCount, + file.updatedAt = $updatedAt +WITH file, f +MATCH (k:Knowledge {knowledgeId: f.knowledgeId}) +MERGE (k)-[:HAS_FILE]->(file) +`; + +const BATCH_ATTACH_FILES_TO_FOLDERS = ` +UNWIND $pairs AS pair +MATCH (file:File {knowledgeId: pair.knowledgeId, relativePath: pair.relativePath}) +MATCH (folder:Folder {knowledgeId: pair.knowledgeId, folderPath: pair.folderPath}) +MERGE (folder)-[:CONTAINS]->(file) +`; + +type RelType = "HAS_KEYWORD" | "HAS_CLASS" | "HAS_FUNCTION" | "HAS_IMPORT_INTERNAL" | "HAS_IMPORT_EXTERNAL"; + +const BATCH_CLEAR_RELS_BY_TYPE: Readonly> = { + HAS_KEYWORD: ` +UNWIND $files AS f +MATCH (file:File {knowledgeId: f.knowledgeId, relativePath: f.relativePath})-[r:HAS_KEYWORD]->() +DELETE r +`, + HAS_CLASS: ` +UNWIND $files AS f +MATCH (file:File {knowledgeId: f.knowledgeId, relativePath: f.relativePath})-[r:HAS_CLASS]->() +DELETE r +`, + HAS_FUNCTION: ` +UNWIND $files AS f +MATCH (file:File {knowledgeId: f.knowledgeId, relativePath: f.relativePath})-[r:HAS_FUNCTION]->() +DELETE r +`, + HAS_IMPORT_INTERNAL: ` +UNWIND $files AS f +MATCH (file:File {knowledgeId: f.knowledgeId, relativePath: f.relativePath})-[r:HAS_IMPORT_INTERNAL]->() +DELETE r +`, + HAS_IMPORT_EXTERNAL: ` +UNWIND $files AS f +MATCH (file:File {knowledgeId: f.knowledgeId, relativePath: f.relativePath})-[r:HAS_IMPORT_EXTERNAL]->() +DELETE r +`, +}; + +const BATCH_ATTACH_KEYWORDS = ` +UNWIND $pairs AS p +MATCH (file:File {knowledgeId: p.knowledgeId, relativePath: p.relativePath}) +MERGE (kw:Keyword {name: p.name}) +MERGE (file)-[:HAS_KEYWORD]->(kw) +`; + +const BATCH_ATTACH_CLASSES = ` +UNWIND $pairs AS p +MATCH (file:File {knowledgeId: p.knowledgeId, relativePath: p.relativePath}) +MERGE (c:Class {signature: p.signature}) +MERGE (file)-[:HAS_CLASS]->(c) +`; + +const BATCH_ATTACH_FUNCTIONS = ` +UNWIND $pairs AS p +MATCH (file:File {knowledgeId: p.knowledgeId, relativePath: p.relativePath}) +MERGE (fn:Function {signature: p.signature}) +MERGE (file)-[:HAS_FUNCTION]->(fn) +`; + +const BATCH_ATTACH_IMPORTS_INTERNAL = ` +UNWIND $pairs AS p +MATCH (file:File {knowledgeId: p.knowledgeId, relativePath: p.relativePath}) +MERGE (m:Module {name: p.name}) +MERGE (file)-[:HAS_IMPORT_INTERNAL]->(m) +`; + +const BATCH_ATTACH_IMPORTS_EXTERNAL = ` +UNWIND $pairs AS p +MATCH (file:File {knowledgeId: p.knowledgeId, relativePath: p.relativePath}) +MERGE (m:Module {name: p.name}) +MERGE (file)-[:HAS_IMPORT_EXTERNAL]->(m) +`; + +interface FileRow { + knowledgeId: string; + relativePath: string; +} + +export async function upsertFileNodesBatch(inputs: readonly UpsertFileNodeInput[]): Promise { + if (inputs.length === 0) { + return; + } + const updatedAt = new Date().toISOString(); + const files = inputs.map((input) => fileRowFor(input)); + const fileKeys: FileRow[] = inputs.map((input) => ({ + knowledgeId: input.knowledgeId, + relativePath: input.relativePath, + })); + const folderPairs = inputs + .filter((input): input is UpsertFileNodeInput & { folderPath: string } => input.folderPath !== undefined) + .map((input) => ({ + knowledgeId: input.knowledgeId, + relativePath: input.relativePath, + folderPath: input.folderPath, + })); + + const keywordPairs = flattenPairs(inputs, "keywords", "name", (v) => v.toLowerCase()); + const classPairs = flattenPairs(inputs, "classes", "signature"); + const functionPairs = flattenPairs(inputs, "functions", "signature"); + const importsInternalPairs = flattenPairs(inputs, "importsInternal", "name"); + const importsExternalPairs = flattenPairs(inputs, "importsExternal", "name"); + + const steps: CypherStep[] = [{ query: BATCH_UPSERT_FILES, params: { files, updatedAt } }]; + if (folderPairs.length > 0) { + steps.push({ query: BATCH_ATTACH_FILES_TO_FOLDERS, params: { pairs: folderPairs } }); + } + // Clear existing rels of every type for every file in the batch. + for (const relType of [ + "HAS_KEYWORD", + "HAS_CLASS", + "HAS_FUNCTION", + "HAS_IMPORT_INTERNAL", + "HAS_IMPORT_EXTERNAL", + ] as const) { + steps.push({ query: BATCH_CLEAR_RELS_BY_TYPE[relType], params: { files: fileKeys } }); + } + if (keywordPairs.length > 0) { + steps.push({ query: BATCH_ATTACH_KEYWORDS, params: { pairs: keywordPairs } }); + } + if (classPairs.length > 0) { + steps.push({ query: BATCH_ATTACH_CLASSES, params: { pairs: classPairs } }); + } + if (functionPairs.length > 0) { + steps.push({ query: BATCH_ATTACH_FUNCTIONS, params: { pairs: functionPairs } }); + } + if (importsInternalPairs.length > 0) { + steps.push({ query: BATCH_ATTACH_IMPORTS_INTERNAL, params: { pairs: importsInternalPairs } }); + } + if (importsExternalPairs.length > 0) { + steps.push({ query: BATCH_ATTACH_IMPORTS_EXTERNAL, params: { pairs: importsExternalPairs } }); + } + + await _runInTransaction(steps); +} + +function fileRowFor(input: UpsertFileNodeInput): Record { + const sectionMap = input.analysis.sectionMap ?? []; + return { + knowledgeId: input.knowledgeId, + relativePath: input.relativePath, + orgId: input.orgId ?? "local", + repoId: input.repoId ?? input.knowledgeId, + language: input.language, + sha: input.sha, + sizeBytes: input.sizeBytes, + purpose: input.analysis.purpose, + summary: input.analysis.summary, + businessContext: input.analysis.businessContext, + dataFlowDirection: input.analysis.dataFlowDirection ?? "", + ontologyConcepts: input.analysis.ontologyConcepts ?? [], + businessEntities: input.analysis.businessEntities ?? [], + systemCapabilities: input.analysis.systemCapabilities ?? [], + sideEffects: input.analysis.sideEffects ?? [], + configDependencies: input.analysis.configDependencies ?? [], + integrationSurface: input.analysis.integrationSurface ?? [], + contractsProvided: input.analysis.contractsProvided ?? [], + contractsConsumed: input.analysis.contractsConsumed ?? [], + sectionNames: sectionMap.map((s) => s.name), + sectionDescriptions: sectionMap.map((s) => s.description), + isBigFile: input.isBigFile ?? false, + totalChunks: input.totalChunks ?? 0, + totalTokenCount: input.totalTokenCount ?? 0, + }; +} + +function flattenPairs( + inputs: readonly UpsertFileNodeInput[], + field: "keywords" | "classes" | "functions" | "importsInternal" | "importsExternal", + valueKey: "name" | "signature", + normalize?: (v: string) => string, +): Array> { + const out: Array> = []; + for (const input of inputs) { + const values = input.analysis[field]; + if (!Array.isArray(values)) { + continue; + } + for (const raw of values) { + const value = normalize !== undefined ? normalize(raw) : raw; + out.push({ knowledgeId: input.knowledgeId, relativePath: input.relativePath, [valueKey]: value }); + } + } + return out; +} diff --git a/packages/neo4j/src/files.ts b/packages/neo4j/src/files.ts index 7d049e3..b96e868 100644 --- a/packages/neo4j/src/files.ts +++ b/packages/neo4j/src/files.ts @@ -1,5 +1,22 @@ import type { FileAnalysis } from "@bb/mongo"; -import { _runCypher, _runInTransaction, type CypherStep } from "./client.ts"; +import { _runCypher } from "./client.ts"; + +export { upsertFileNodesBatch } from "./files-batch.ts"; + +export interface UpsertFileNodeInput { + orgId?: string; + knowledgeId: string; + repoId?: string; + relativePath: string; + language: string; + sha: string; + sizeBytes: number; + analysis: FileAnalysis; + folderPath?: string; + isBigFile?: boolean; + totalChunks?: number; + totalTokenCount?: number; +} const UPSERT_FILE = ` MERGE (f:File {knowledgeId: $knowledgeId, relativePath: $relativePath}) @@ -97,21 +114,6 @@ MERGE (m:Module {name: name}) MERGE (f)-[:HAS_IMPORT_EXTERNAL]->(m) `; -export interface UpsertFileNodeInput { - orgId?: string; - knowledgeId: string; - repoId?: string; - relativePath: string; - language: string; - sha: string; - sizeBytes: number; - analysis: FileAnalysis; - folderPath?: string; - isBigFile?: boolean; - totalChunks?: number; - totalTokenCount?: number; -} - const DELETE_FILES = ` MATCH (f:File {knowledgeId: $knowledgeId}) WHERE f.relativePath IN $relativePaths @@ -133,232 +135,6 @@ export async function deleteFileNodes(knowledgeId: string, relativePaths: string await _runCypher(DELETE_FILES, { knowledgeId, relativePaths }); } -// ───────────────────────────────────────────────────────────────────────────── -// Batched upsert — used by the flat-folder indexing phase to land 50+ files in -// one transaction instead of 12 round-trips per file. Same Cypher shape as the -// single-shot path above; just wrapped with an outer UNWIND so one query -// services every file in the batch. The five rel types (HAS_KEYWORD / -// HAS_CLASS / HAS_FUNCTION / HAS_IMPORT_INTERNAL / HAS_IMPORT_EXTERNAL) each -// take two Cyphers: a batched DELETE that clears existing rels for every file -// in the batch by relativePath, then a batched UNWIND that attaches the new -// rels from flattened `(knowledgeId, relativePath, name)` triples. -// ───────────────────────────────────────────────────────────────────────────── - -const BATCH_UPSERT_FILES = ` -UNWIND $files AS f -MERGE (file:File {knowledgeId: f.knowledgeId, relativePath: f.relativePath}) -SET file.orgId = f.orgId, - file.repoId = f.repoId, - file.language = f.language, - file.sha = f.sha, - file.sizeBytes = f.sizeBytes, - file.purpose = f.purpose, - file.summary = f.summary, - file.businessContext = f.businessContext, - file.dataFlowDirection = f.dataFlowDirection, - file.ontologyConcepts = f.ontologyConcepts, - file.businessEntities = f.businessEntities, - file.systemCapabilities = f.systemCapabilities, - file.sideEffects = f.sideEffects, - file.configDependencies = f.configDependencies, - file.integrationSurface = f.integrationSurface, - file.contractsProvided = f.contractsProvided, - file.contractsConsumed = f.contractsConsumed, - file.sectionNames = f.sectionNames, - file.sectionDescriptions = f.sectionDescriptions, - file.isBigFile = f.isBigFile, - file.totalChunks = f.totalChunks, - file.totalTokenCount = f.totalTokenCount, - file.updatedAt = $updatedAt -WITH file, f -MATCH (k:Knowledge {knowledgeId: f.knowledgeId}) -MERGE (k)-[:HAS_FILE]->(file) -`; - -const BATCH_ATTACH_FILES_TO_FOLDERS = ` -UNWIND $pairs AS pair -MATCH (file:File {knowledgeId: pair.knowledgeId, relativePath: pair.relativePath}) -MATCH (folder:Folder {knowledgeId: pair.knowledgeId, folderPath: pair.folderPath}) -MERGE (folder)-[:CONTAINS]->(file) -`; - -const BATCH_CLEAR_RELS_BY_TYPE: Readonly> = { - HAS_KEYWORD: ` -UNWIND $files AS f -MATCH (file:File {knowledgeId: f.knowledgeId, relativePath: f.relativePath})-[r:HAS_KEYWORD]->() -DELETE r -`, - HAS_CLASS: ` -UNWIND $files AS f -MATCH (file:File {knowledgeId: f.knowledgeId, relativePath: f.relativePath})-[r:HAS_CLASS]->() -DELETE r -`, - HAS_FUNCTION: ` -UNWIND $files AS f -MATCH (file:File {knowledgeId: f.knowledgeId, relativePath: f.relativePath})-[r:HAS_FUNCTION]->() -DELETE r -`, - HAS_IMPORT_INTERNAL: ` -UNWIND $files AS f -MATCH (file:File {knowledgeId: f.knowledgeId, relativePath: f.relativePath})-[r:HAS_IMPORT_INTERNAL]->() -DELETE r -`, - HAS_IMPORT_EXTERNAL: ` -UNWIND $files AS f -MATCH (file:File {knowledgeId: f.knowledgeId, relativePath: f.relativePath})-[r:HAS_IMPORT_EXTERNAL]->() -DELETE r -`, -}; - -const BATCH_ATTACH_KEYWORDS = ` -UNWIND $pairs AS p -MATCH (file:File {knowledgeId: p.knowledgeId, relativePath: p.relativePath}) -MERGE (kw:Keyword {name: p.name}) -MERGE (file)-[:HAS_KEYWORD]->(kw) -`; - -const BATCH_ATTACH_CLASSES = ` -UNWIND $pairs AS p -MATCH (file:File {knowledgeId: p.knowledgeId, relativePath: p.relativePath}) -MERGE (c:Class {signature: p.signature}) -MERGE (file)-[:HAS_CLASS]->(c) -`; - -const BATCH_ATTACH_FUNCTIONS = ` -UNWIND $pairs AS p -MATCH (file:File {knowledgeId: p.knowledgeId, relativePath: p.relativePath}) -MERGE (fn:Function {signature: p.signature}) -MERGE (file)-[:HAS_FUNCTION]->(fn) -`; - -const BATCH_ATTACH_IMPORTS_INTERNAL = ` -UNWIND $pairs AS p -MATCH (file:File {knowledgeId: p.knowledgeId, relativePath: p.relativePath}) -MERGE (m:Module {name: p.name}) -MERGE (file)-[:HAS_IMPORT_INTERNAL]->(m) -`; - -const BATCH_ATTACH_IMPORTS_EXTERNAL = ` -UNWIND $pairs AS p -MATCH (file:File {knowledgeId: p.knowledgeId, relativePath: p.relativePath}) -MERGE (m:Module {name: p.name}) -MERGE (file)-[:HAS_IMPORT_EXTERNAL]->(m) -`; - -type RelType = "HAS_KEYWORD" | "HAS_CLASS" | "HAS_FUNCTION" | "HAS_IMPORT_INTERNAL" | "HAS_IMPORT_EXTERNAL"; - -interface FileRow { - knowledgeId: string; - relativePath: string; -} - -export async function upsertFileNodesBatch(inputs: readonly UpsertFileNodeInput[]): Promise { - if (inputs.length === 0) { - return; - } - const updatedAt = new Date().toISOString(); - const files = inputs.map((input) => fileRowFor(input)); - const fileKeys: FileRow[] = inputs.map((input) => ({ - knowledgeId: input.knowledgeId, - relativePath: input.relativePath, - })); - const folderPairs = inputs - .filter((input): input is UpsertFileNodeInput & { folderPath: string } => input.folderPath !== undefined) - .map((input) => ({ - knowledgeId: input.knowledgeId, - relativePath: input.relativePath, - folderPath: input.folderPath, - })); - - const keywordPairs = flattenPairs(inputs, "keywords", "name", (v) => v.toLowerCase()); - const classPairs = flattenPairs(inputs, "classes", "signature"); - const functionPairs = flattenPairs(inputs, "functions", "signature"); - const importsInternalPairs = flattenPairs(inputs, "importsInternal", "name"); - const importsExternalPairs = flattenPairs(inputs, "importsExternal", "name"); - - const steps: CypherStep[] = [{ query: BATCH_UPSERT_FILES, params: { files, updatedAt } }]; - if (folderPairs.length > 0) { - steps.push({ query: BATCH_ATTACH_FILES_TO_FOLDERS, params: { pairs: folderPairs } }); - } - // Clear existing rels of every type for every file in the batch. - for (const relType of [ - "HAS_KEYWORD", - "HAS_CLASS", - "HAS_FUNCTION", - "HAS_IMPORT_INTERNAL", - "HAS_IMPORT_EXTERNAL", - ] as const) { - steps.push({ query: BATCH_CLEAR_RELS_BY_TYPE[relType], params: { files: fileKeys } }); - } - if (keywordPairs.length > 0) { - steps.push({ query: BATCH_ATTACH_KEYWORDS, params: { pairs: keywordPairs } }); - } - if (classPairs.length > 0) { - steps.push({ query: BATCH_ATTACH_CLASSES, params: { pairs: classPairs } }); - } - if (functionPairs.length > 0) { - steps.push({ query: BATCH_ATTACH_FUNCTIONS, params: { pairs: functionPairs } }); - } - if (importsInternalPairs.length > 0) { - steps.push({ query: BATCH_ATTACH_IMPORTS_INTERNAL, params: { pairs: importsInternalPairs } }); - } - if (importsExternalPairs.length > 0) { - steps.push({ query: BATCH_ATTACH_IMPORTS_EXTERNAL, params: { pairs: importsExternalPairs } }); - } - - await _runInTransaction(steps); -} - -function fileRowFor(input: UpsertFileNodeInput): Record { - const sectionMap = input.analysis.sectionMap ?? []; - return { - knowledgeId: input.knowledgeId, - relativePath: input.relativePath, - orgId: input.orgId ?? "local", - repoId: input.repoId ?? input.knowledgeId, - language: input.language, - sha: input.sha, - sizeBytes: input.sizeBytes, - purpose: input.analysis.purpose, - summary: input.analysis.summary, - businessContext: input.analysis.businessContext, - dataFlowDirection: input.analysis.dataFlowDirection ?? "", - ontologyConcepts: input.analysis.ontologyConcepts ?? [], - businessEntities: input.analysis.businessEntities ?? [], - systemCapabilities: input.analysis.systemCapabilities ?? [], - sideEffects: input.analysis.sideEffects ?? [], - configDependencies: input.analysis.configDependencies ?? [], - integrationSurface: input.analysis.integrationSurface ?? [], - contractsProvided: input.analysis.contractsProvided ?? [], - contractsConsumed: input.analysis.contractsConsumed ?? [], - sectionNames: sectionMap.map((s) => s.name), - sectionDescriptions: sectionMap.map((s) => s.description), - isBigFile: input.isBigFile ?? false, - totalChunks: input.totalChunks ?? 0, - totalTokenCount: input.totalTokenCount ?? 0, - }; -} - -function flattenPairs( - inputs: readonly UpsertFileNodeInput[], - field: "keywords" | "classes" | "functions" | "importsInternal" | "importsExternal", - valueKey: "name" | "signature", - normalize?: (v: string) => string, -): Array> { - const out: Array> = []; - for (const input of inputs) { - const values = input.analysis[field]; - if (!Array.isArray(values)) { - continue; - } - for (const raw of values) { - const value = normalize !== undefined ? normalize(raw) : raw; - out.push({ knowledgeId: input.knowledgeId, relativePath: input.relativePath, [valueKey]: value }); - } - } - return out; -} - export async function upsertFileNode(input: UpsertFileNodeInput): Promise { const params = { knowledgeId: input.knowledgeId, relativePath: input.relativePath }; const sectionMap = input.analysis.sectionMap ?? []; diff --git a/packages/server/package.json b/packages/server/package.json index 08b77e0..d19618f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -24,6 +24,7 @@ "@bb/mongo": "workspace:*", "@bb/sqlite": "workspace:*", "@bb/neo4j": "workspace:*", + "@bb/ladybug": "workspace:*", "@bb/queue": "workspace:*", "@bb/redis": "workspace:*", "@bb/types": "workspace:*", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 9bdcc54..e956d6f 100755 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -11,6 +11,8 @@ import { connectQueue } from "@bb/queue"; import "@bb/mongo"; import "@bb/sqlite"; import "@bb/neo4j"; +import "@bb/ladybug"; + import { registerGithubWorkers, registerLocalIngestWorker } from "@bb/ingest-github"; import { ServerConfigError } from "@bb/errors"; import { registerRoutes } from "./routes.ts"; diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 0a0e780..c0b2eac 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -38,6 +38,7 @@ export enum Config { DbProvider = "db_provider", GraphProvider = "graph_provider", SqlitePath = "sqlite_path", + LadybugPath = "ladybug_path", } export enum DbProviderType { diff --git a/tsconfig.json b/tsconfig.json index 80c98f2..b571289 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,27 @@ "compilerOptions": { "noEmit": true }, - "include": ["packages/*/src/**/*.ts", "packages/*/src/**/*.tsx", "packages/*/src/**/*.json"], - "exclude": ["**/node_modules", "**/dist", "**/*.d.ts"] + "files": [], + "references": [ + { "path": "packages/types" }, + { "path": "packages/errors" }, + { "path": "packages/config" }, + { "path": "packages/logger" }, + { "path": "packages/mongo" }, + { "path": "packages/sqlite" }, + { "path": "packages/redis" }, + { "path": "packages/queue" }, + { "path": "packages/llm" }, + { "path": "packages/ingest-github" }, + { "path": "packages/ingest-business-context" }, + { "path": "packages/cli" }, + { "path": "packages/server" }, + { "path": "packages/neo4j" }, + { "path": "packages/mcp" }, + { "path": "packages/db-core" }, + { "path": "packages/graph-core" }, + { "path": "packages/db" }, + { "path": "packages/graph-db" }, + { "path": "packages/ladybug" } + ] }