diff --git a/holistics b/holistics new file mode 100755 index 0000000..5ebd87e --- /dev/null +++ b/holistics @@ -0,0 +1,4 @@ +#!/bin/bash +# Wrapper script to invoke holistics CLI +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "${SCRIPT_DIR}/node_modules/.bin/tsx" "${SCRIPT_DIR}/src/index.ts" "$@" diff --git a/package-lock.json b/package-lock.json index be6d6d3..2146660 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ "devDependencies": { "@types/node": "^22.13.4", "tsx": "^4.19.3", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "vitest": "^4.1.2" } }, "node_modules/@commander-js/extra-typings": { @@ -27,6 +28,43 @@ "commander": "~13.1.0" } }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", @@ -363,6 +401,24 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", @@ -438,190 +494,1870 @@ "node": ">=18.0.0" } }, - "node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "undici-types": "~6.21.0" + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "engines": { - "node": ">=18" + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "peer": true, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.2", - "@esbuild/android-arm": "0.25.2", - "@esbuild/android-arm64": "0.25.2", - "@esbuild/android-x64": "0.25.2", - "@esbuild/darwin-arm64": "0.25.2", - "@esbuild/darwin-x64": "0.25.2", - "@esbuild/freebsd-arm64": "0.25.2", - "@esbuild/freebsd-x64": "0.25.2", - "@esbuild/linux-arm": "0.25.2", - "@esbuild/linux-arm64": "0.25.2", - "@esbuild/linux-ia32": "0.25.2", - "@esbuild/linux-loong64": "0.25.2", - "@esbuild/linux-mips64el": "0.25.2", - "@esbuild/linux-ppc64": "0.25.2", - "@esbuild/linux-riscv64": "0.25.2", - "@esbuild/linux-s390x": "0.25.2", - "@esbuild/linux-x64": "0.25.2", - "@esbuild/netbsd-arm64": "0.25.2", - "@esbuild/netbsd-x64": "0.25.2", - "@esbuild/openbsd-arm64": "0.25.2", - "@esbuild/openbsd-x64": "0.25.2", - "@esbuild/sunos-x64": "0.25.2", - "@esbuild/win32-arm64": "0.25.2", - "@esbuild/win32-ia32": "0.25.2", - "@esbuild/win32-x64": "0.25.2" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/tsx": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", - "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.17" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=20.18.1" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/undici-types": { + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "engines": { + "node": ">=18" + } + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tsx": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "dev": true, + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", + "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true }, + "node_modules/vitest": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/package.json b/package.json index b577493..f2c4a9a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "type": "module", "version": "0.1.1", "scripts": { - "cli": "tsx src/index.ts" + "cli": "tsx src/index.ts", + "test": "vitest run", + "test:watch": "vitest" }, "files": [ "dist" @@ -11,7 +13,8 @@ "devDependencies": { "@types/node": "^22.13.4", "tsx": "^4.19.3", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "vitest": "^4.1.2" }, "dependencies": { "@commander-js/extra-typings": "^13.1.0", diff --git a/src/__tests__/fixtures/compiled-sample.json b/src/__tests__/fixtures/compiled-sample.json new file mode 100644 index 0000000..d05aeb4 --- /dev/null +++ b/src/__tests__/fixtures/compiled-sample.json @@ -0,0 +1,169 @@ +{ + "models/orders.aml": { + "__type__": "TableModel", + "__fqn__": "orders", + "name": "orders", + "label": "Orders", + "data_source_name": "bigquery", + "table_name": "`project`.`schema`.`orders`", + "owner": "data@example.com", + "description": "Order transactions", + "dimension": { + "id": { + "label": "ID", + "type": "text", + "primary_key": true, + "definition": { "__type__": "Heredoc", "content": "{{ #SOURCE.id }}" } + }, + "order_date": { + "label": "Order Date", + "type": "date", + "definition": { "__type__": "Heredoc", "content": "{{ #SOURCE.order_date }}" } + }, + "customer_id": { + "label": "Customer ID", + "type": "text", + "definition": { "__type__": "Heredoc", "content": "{{ #SOURCE.customer_id }}" } + } + }, + "measure": { + "total_amount": { + "label": "Total Amount", + "type": "number", + "aggregation_type": "sum", + "definition": { "__type__": "Heredoc", "content": "{{ #SOURCE.amount }}" } + }, + "order_count": { + "label": "Order Count", + "type": "number", + "aggregation_type": "count" + } + } + }, + "models/customers.aml": { + "__type__": "QueryModel", + "__fqn__": "customers", + "name": "customers", + "label": "Customers", + "data_source_name": "bigquery", + "owner": "data@example.com", + "query": { "__type__": "Heredoc", "content": "SELECT * FROM customers_raw" }, + "dimension": { + "id": { + "label": "ID", + "type": "text", + "primary_key": true + }, + "name": { + "label": "Name", + "type": "text" + }, + "segment": { + "label": "Segment", + "type": "text" + } + }, + "measure": {} + }, + "datasets/ecommerce.aml": { + "__type__": "Dataset", + "__fqn__": "ecommerce", + "name": "ecommerce", + "label": "E-commerce Dataset", + "data_source_name": "bigquery", + "owner": "analytics@example.com", + "description": { "__type__": "Heredoc", "content": "Main ecommerce dataset" }, + "models": [ + { "name": "orders", "__fqn__": "orders" }, + { "name": "customers", "__fqn__": "customers" } + ], + "metric": { + "customer_ltv": { + "label": "Customer LTV", + "type": "number", + "definition": { "__type__": "Heredoc", "content": "sum(orders.total_amount) / count_distinct(customers.id)" } + }, + "avg_order_value": { + "label": "Average Order Value", + "type": "number", + "definition": { "__type__": "Heredoc", "content": "avg(orders.total_amount)" } + } + } + }, + "dashboards/sales_overview.aml": { + "__type__": "Dashboard", + "__fqn__": "sales_overview", + "uname": "sales_overview", + "title": "Sales Overview", + "description": "Key sales metrics", + "owner": "product@example.com", + "block": { + "revenue_chart": { + "def": { + "__type__": "VizBlock", + "label": "Revenue Over Time", + "viz": { + "dataset": { "name": "ecommerce", "__fqn__": "ecommerce" }, + "x_axis": { + "ref": { "__type__": "FieldRef", "model": "orders", "field": "order_date" } + }, + "y_axis": [ + { + "series": [ + { + "field": { + "ref": { "__type__": "FieldRef", "model": "orders", "field": "total_amount" } + } + } + ] + } + ] + } + } + }, + "customer_segments": { + "def": { + "__type__": "VizBlock", + "label": "Customer Segments", + "viz": { + "dataset": { "name": "ecommerce", "__fqn__": "ecommerce" }, + "x_axis": { + "ref": { "__type__": "FieldRef", "model": "customers", "field": "segment" } + }, + "y_axis": [ + { + "series": [ + { + "field": { + "ref": { "__type__": "FieldRef", "model": "orders", "field": "order_count" } + } + } + ] + } + ] + } + } + }, + "aql_calculation_chart": { + "def": { + "__type__": "VizBlock", + "label": "Chart with AQL Calculation", + "viz": { + "dataset": { "name": "ecommerce", "__fqn__": "ecommerce" }, + "x_axis": { + "ref": { "__type__": "FieldRef", "model": "orders", "field": "order_date" } + }, + "calculation": { + "revenue_per_customer": { + "formula": { "__type__": "Heredoc", "content": "sum(orders.total_amount) / count_distinct(customers.id)" } + }, + "high_value_orders": { + "formula": { "__type__": "Heredoc", "content": "count(case(when: orders.total_amount > 100, then: 1, else: null))" } + } + } + } + } + } + } + } +} diff --git a/src/__tests__/fixtures/lineage-output-sample.json b/src/__tests__/fixtures/lineage-output-sample.json new file mode 100644 index 0000000..d635538 --- /dev/null +++ b/src/__tests__/fixtures/lineage-output-sample.json @@ -0,0 +1,247 @@ +{ + "version": "1.0", + "generated_at": "2026-04-01T10:00:00.000Z", + "project": { + "name": "project", + "path": "/example/project" + }, + "entities": { + "data_sources": [ + "bigquery" + ], + "models": [ + { + "fqn": "orders", + "name": "orders", + "label": "Orders", + "type": "TableModel", + "description": "Order transactions", + "data_source": "bigquery", + "owner": "data@example.com", + "fields": [ + { + "name": "id", + "label": "ID", + "type": "text", + "is_dimension": true, + "is_measure": false, + "is_primary_key": true, + "definition": "{{ #SOURCE.id }}" + }, + { + "name": "order_date", + "label": "Order Date", + "type": "date", + "is_dimension": true, + "is_measure": false, + "is_primary_key": false, + "definition": "{{ #SOURCE.order_date }}" + }, + { + "name": "customer_id", + "label": "Customer ID", + "type": "text", + "is_dimension": true, + "is_measure": false, + "is_primary_key": false, + "definition": "{{ #SOURCE.customer_id }}" + }, + { + "name": "total_amount", + "label": "Total Amount", + "type": "number", + "is_dimension": false, + "is_measure": true, + "is_primary_key": false, + "definition": "{{ #SOURCE.amount }}", + "aggregation": "sum" + }, + { + "name": "order_count", + "label": "Order Count", + "type": "number", + "is_dimension": false, + "is_measure": true, + "is_primary_key": false, + "aggregation": "count" + } + ], + "file_path": "models/orders.aml", + "source_table": { + "database": "project", + "schema": "schema", + "table": "orders", + "full_name": "`project`.`schema`.`orders`" + } + }, + { + "fqn": "customers", + "name": "customers", + "label": "Customers", + "type": "QueryModel", + "data_source": "bigquery", + "owner": "data@example.com", + "fields": [ + { + "name": "id", + "label": "ID", + "type": "text", + "is_dimension": true, + "is_measure": false, + "is_primary_key": true + }, + { + "name": "name", + "label": "Name", + "type": "text", + "is_dimension": true, + "is_measure": false, + "is_primary_key": false + }, + { + "name": "segment", + "label": "Segment", + "type": "text", + "is_dimension": true, + "is_measure": false, + "is_primary_key": false + } + ], + "file_path": "models/customers.aml", + "query": "SELECT * FROM customers_raw" + } + ], + "datasets": [ + { + "fqn": "ecommerce", + "name": "ecommerce", + "label": "E-commerce Dataset", + "description": "Main ecommerce dataset", + "data_source": "bigquery", + "owner": "analytics@example.com", + "models": [ + "orders", + "customers" + ], + "file_path": "datasets/ecommerce.aml" + } + ], + "dashboards": [ + { + "fqn": "sales_overview", + "name": "sales_overview", + "title": "Sales Overview", + "description": "Key sales metrics", + "owner": "product@example.com", + "charts": [ + "sales_overview.revenue_chart", + "sales_overview.customer_segments" + ], + "file_path": "dashboards/sales_overview.aml" + } + ], + "charts": [ + { + "fqn": "sales_overview.revenue_chart", + "name": "revenue_chart", + "label": "Revenue Over Time", + "type": "VizBlock", + "dashboard": "sales_overview", + "dataset": "ecommerce", + "models_used": [ + "orders" + ], + "fields_used": [ + { + "model": "orders", + "field": "order_date" + }, + { + "model": "orders", + "field": "total_amount" + } + ] + }, + { + "fqn": "sales_overview.customer_segments", + "name": "customer_segments", + "label": "Customer Segments", + "type": "VizBlock", + "dashboard": "sales_overview", + "dataset": "ecommerce", + "models_used": [ + "customers", + "orders" + ], + "fields_used": [ + { + "model": "customers", + "field": "segment" + }, + { + "model": "orders", + "field": "order_count" + } + ] + } + ] + }, + "lineage": { + "model_to_source": [ + { + "model": "orders", + "source": { + "data_source": "bigquery", + "database": "project", + "schema": "schema", + "table": "orders" + } + } + ], + "dataset_to_model": [ + { + "dataset": "ecommerce", + "models": [ + "orders", + "customers" + ] + } + ], + "chart_to_dataset": [ + { + "chart": "sales_overview.revenue_chart", + "dataset": "ecommerce" + }, + { + "chart": "sales_overview.customer_segments", + "dataset": "ecommerce" + } + ], + "chart_to_model": [ + { + "chart": "sales_overview.revenue_chart", + "dataset": "ecommerce", + "models": [ + "orders" + ] + }, + { + "chart": "sales_overview.customer_segments", + "dataset": "ecommerce", + "models": [ + "customers", + "orders" + ] + } + ], + "dashboard_to_chart": [ + { + "dashboard": "sales_overview", + "charts": [ + "sales_overview.revenue_chart", + "sales_overview.customer_segments" + ] + } + ] + } +} diff --git a/src/__tests__/lineage.test.ts b/src/__tests__/lineage.test.ts new file mode 100644 index 0000000..f5423b1 --- /dev/null +++ b/src/__tests__/lineage.test.ts @@ -0,0 +1,560 @@ +import { describe, it, expect, vi } from 'vitest'; +import { transformToLineage, type CliCoreModule } from '../lineage'; +import compiledSample from './fixtures/compiled-sample.json'; + +describe('transformToLineage', () => { + const projectPath = '/test/project'; + const lineage = transformToLineage(compiledSample, projectPath); + + describe('metadata', () => { + it('should include version and project info', () => { + expect(lineage.version).toBe('1.0'); + expect(lineage.project.name).toBe('project'); + expect(lineage.project.path).toBe(projectPath); + expect(lineage.generated_at).toBeDefined(); + }); + }); + + describe('entities.models', () => { + it('should parse TableModel correctly', () => { + const orders = lineage.entities.models.find(m => m.name === 'orders'); + expect(orders).toBeDefined(); + expect(orders!.fqn).toBe('orders'); + expect(orders!.type).toBe('TableModel'); + expect(orders!.label).toBe('Orders'); + expect(orders!.data_source).toBe('bigquery'); + expect(orders!.owner).toBe('data@example.com'); + expect(orders!.description).toBe('Order transactions'); + }); + + it('should parse QueryModel correctly', () => { + const customers = lineage.entities.models.find(m => m.name === 'customers'); + expect(customers).toBeDefined(); + expect(customers!.type).toBe('QueryModel'); + expect(customers!.query).toBe('SELECT * FROM customers_raw'); + }); + + it('should parse source_table for TableModel', () => { + const orders = lineage.entities.models.find(m => m.name === 'orders'); + expect(orders!.source_table).toBeDefined(); + expect(orders!.source_table!.database).toBe('project'); + expect(orders!.source_table!.schema).toBe('schema'); + expect(orders!.source_table!.table).toBe('orders'); + }); + + it('should parse fields correctly', () => { + const orders = lineage.entities.models.find(m => m.name === 'orders'); + expect(orders!.fields.length).toBe(5); // 3 dimensions + 2 measures + + const idField = orders!.fields.find(f => f.name === 'id'); + expect(idField!.is_dimension).toBe(true); + expect(idField!.is_measure).toBe(false); + expect(idField!.is_primary_key).toBe(true); + + const amountField = orders!.fields.find(f => f.name === 'total_amount'); + expect(amountField!.is_dimension).toBe(false); + expect(amountField!.is_measure).toBe(true); + expect(amountField!.aggregation).toBe('sum'); + }); + }); + + describe('entities.datasets', () => { + it('should parse Dataset correctly', () => { + expect(lineage.entities.datasets.length).toBe(1); + const ecommerce = lineage.entities.datasets[0]; + expect(ecommerce.fqn).toBe('ecommerce'); + expect(ecommerce.name).toBe('ecommerce'); + expect(ecommerce.label).toBe('E-commerce Dataset'); + expect(ecommerce.models).toEqual(['orders', 'customers']); + }); + + it('should extract metrics with AQL references', () => { + const ecommerce = lineage.entities.datasets[0]; + expect(ecommerce.metrics.length).toBe(2); + + const ltvMetric = ecommerce.metrics.find(m => m.name === 'customer_ltv'); + expect(ltvMetric).toBeDefined(); + expect(ltvMetric!.label).toBe('Customer LTV'); + expect(ltvMetric!.models_referenced).toContain('orders'); + expect(ltvMetric!.models_referenced).toContain('customers'); + expect(ltvMetric!.fields_referenced).toContainEqual({ model: 'orders', field: 'total_amount' }); + expect(ltvMetric!.fields_referenced).toContainEqual({ model: 'customers', field: 'id' }); + + const avgMetric = ecommerce.metrics.find(m => m.name === 'avg_order_value'); + expect(avgMetric).toBeDefined(); + expect(avgMetric!.models_referenced).toContain('orders'); + expect(avgMetric!.fields_referenced).toContainEqual({ model: 'orders', field: 'total_amount' }); + }); + }); + + describe('entities.dashboards', () => { + it('should parse Dashboard correctly', () => { + expect(lineage.entities.dashboards.length).toBe(1); + const dashboard = lineage.entities.dashboards[0]; + expect(dashboard.fqn).toBe('sales_overview'); + expect(dashboard.title).toBe('Sales Overview'); + expect(dashboard.owner).toBe('product@example.com'); + expect(dashboard.charts.length).toBe(3); + }); + }); + + describe('entities.charts', () => { + it('should parse charts with field references', () => { + expect(lineage.entities.charts.length).toBe(3); + + const revenueChart = lineage.entities.charts.find(c => c.name === 'revenue_chart'); + expect(revenueChart).toBeDefined(); + expect(revenueChart!.label).toBe('Revenue Over Time'); + expect(revenueChart!.dashboard).toBe('sales_overview'); + expect(revenueChart!.dataset).toBe('ecommerce'); + expect(revenueChart!.models_used).toContain('orders'); + expect(revenueChart!.fields_used).toContainEqual({ model: 'orders', field: 'order_date', source: 'field_ref' }); + expect(revenueChart!.fields_used).toContainEqual({ model: 'orders', field: 'total_amount', source: 'field_ref' }); + }); + + it('should track multiple models used in a chart', () => { + const segmentsChart = lineage.entities.charts.find(c => c.name === 'customer_segments'); + expect(segmentsChart!.models_used).toContain('customers'); + expect(segmentsChart!.models_used).toContain('orders'); + }); + + it('should extract model refs from AQL calculations in charts', () => { + const aqlChart = lineage.entities.charts.find(c => c.name === 'aql_calculation_chart'); + expect(aqlChart).toBeDefined(); + + // Should have both field_ref and aql sources + expect(aqlChart!.fields_used.some(f => f.source === 'field_ref')).toBe(true); + expect(aqlChart!.fields_used.some(f => f.source === 'aql')).toBe(true); + + // Should extract refs from AQL calculations + expect(aqlChart!.fields_used).toContainEqual({ model: 'orders', field: 'total_amount', source: 'aql' }); + expect(aqlChart!.fields_used).toContainEqual({ model: 'customers', field: 'id', source: 'aql' }); + + // Should include both orders and customers in models_used + expect(aqlChart!.models_used).toContain('orders'); + expect(aqlChart!.models_used).toContain('customers'); + }); + }); + + describe('entities.data_sources', () => { + it('should collect unique data sources', () => { + expect(lineage.entities.data_sources).toContain('bigquery'); + }); + }); + + describe('lineage.model_to_source', () => { + it('should create edges for TableModels with source tables', () => { + const ordersEdge = lineage.lineage.model_to_source.find(e => e.model === 'orders'); + expect(ordersEdge).toBeDefined(); + expect(ordersEdge!.source.data_source).toBe('bigquery'); + expect(ordersEdge!.source.database).toBe('project'); + expect(ordersEdge!.source.schema).toBe('schema'); + expect(ordersEdge!.source.table).toBe('orders'); + }); + + it('should not create edges for QueryModels', () => { + const customersEdge = lineage.lineage.model_to_source.find(e => e.model === 'customers'); + expect(customersEdge).toBeUndefined(); + }); + }); + + describe('lineage.dataset_to_model', () => { + it('should map datasets to their models', () => { + expect(lineage.lineage.dataset_to_model.length).toBe(1); + const edge = lineage.lineage.dataset_to_model[0]; + expect(edge.dataset).toBe('ecommerce'); + expect(edge.models).toEqual(['orders', 'customers']); + }); + }); + + describe('lineage.chart_to_dataset', () => { + it('should map charts to their datasets', () => { + expect(lineage.lineage.chart_to_dataset.length).toBe(3); + + const revenueEdge = lineage.lineage.chart_to_dataset.find( + e => e.chart === 'sales_overview.revenue_chart' + ); + expect(revenueEdge).toBeDefined(); + expect(revenueEdge!.dataset).toBe('ecommerce'); + + const segmentsEdge = lineage.lineage.chart_to_dataset.find( + e => e.chart === 'sales_overview.customer_segments' + ); + expect(segmentsEdge).toBeDefined(); + expect(segmentsEdge!.dataset).toBe('ecommerce'); + }); + }); + + describe('lineage.chart_to_model', () => { + it('should map charts to models via field refs', () => { + expect(lineage.lineage.chart_to_model.length).toBe(3); + + const revenueEdge = lineage.lineage.chart_to_model.find( + e => e.chart === 'sales_overview.revenue_chart' + ); + expect(revenueEdge!.dataset).toBe('ecommerce'); + expect(revenueEdge!.models).toContain('orders'); + + const segmentsEdge = lineage.lineage.chart_to_model.find( + e => e.chart === 'sales_overview.customer_segments' + ); + expect(segmentsEdge!.models).toContain('customers'); + expect(segmentsEdge!.models).toContain('orders'); + }); + }); + + describe('lineage.dashboard_to_chart', () => { + it('should map dashboards to their charts', () => { + expect(lineage.lineage.dashboard_to_chart.length).toBe(1); + const edge = lineage.lineage.dashboard_to_chart[0]; + expect(edge.dashboard).toBe('sales_overview'); + expect(edge.charts).toContain('sales_overview.revenue_chart'); + expect(edge.charts).toContain('sales_overview.customer_segments'); + expect(edge.charts).toContain('sales_overview.aql_calculation_chart'); + }); + }); +}); + +describe('parseTableName', () => { + it('should parse BigQuery backtick format', () => { + const lineage = transformToLineage( + { + 'test.aml': { + __type__: 'TableModel', + __fqn__: 'test', + name: 'test', + data_source_name: 'bq', + table_name: '`my-project`.`dataset`.`table`', + dimension: {}, + measure: {}, + }, + }, + '/test' + ); + const model = lineage.entities.models[0]; + expect(model.source_table!.database).toBe('my-project'); + expect(model.source_table!.schema).toBe('dataset'); + expect(model.source_table!.table).toBe('table'); + }); + + it('should parse PostgreSQL double-quote format', () => { + const lineage = transformToLineage( + { + 'test.aml': { + __type__: 'TableModel', + __fqn__: 'test', + name: 'test', + data_source_name: 'pg', + table_name: '"public"."users"', + dimension: {}, + measure: {}, + }, + }, + '/test' + ); + const model = lineage.entities.models[0]; + expect(model.source_table!.schema).toBe('public'); + expect(model.source_table!.table).toBe('users'); + }); + + it('should parse simple schema.table format', () => { + const lineage = transformToLineage( + { + 'test.aml': { + __type__: 'TableModel', + __fqn__: 'test', + name: 'test', + data_source_name: 'mysql', + table_name: 'mydb.orders', + dimension: {}, + measure: {}, + }, + }, + '/test' + ); + const model = lineage.entities.models[0]; + expect(model.source_table!.schema).toBe('mydb'); + expect(model.source_table!.table).toBe('orders'); + }); +}); + +describe('AQL type-checked extraction', () => { + // Sample compiled data for testing + const sampleCompiledData = { + 'datasets/test.aml': { + __type__: 'Dataset', + __fqn__: 'test_dataset', + name: 'test_dataset', + label: 'Test Dataset', + data_source_name: 'bigquery', + models: [ + { name: 'orders', __fqn__: 'orders' }, + { name: 'customers', __fqn__: 'customers' }, + ], + metric: { + test_metric: { + label: 'Test Metric', + type: 'number', + definition: { __type__: 'Heredoc', content: 'sum(orders.amount) / count_distinct(customers.id)' }, + }, + }, + }, + 'dashboards/test.aml': { + __type__: 'Dashboard', + __fqn__: 'test_dashboard', + uname: 'test_dashboard', + title: 'Test Dashboard', + block: { + test_chart: { + def: { + __type__: 'VizBlock', + label: 'Test Chart', + viz: { + dataset: { name: 'test_dataset', __fqn__: 'test_dataset' }, + calculation: { + calc1: { + formula: { __type__: 'Heredoc', content: 'avg(orders.amount)' }, + }, + }, + }, + }, + }, + }, + }, + }; + + describe('without cli-core (regex fallback)', () => { + it('should extract AQL refs using regex when no cli-core provided', () => { + const lineage = transformToLineage(sampleCompiledData, '/test'); + + const dataset = lineage.entities.datasets[0]; + expect(dataset.metrics[0].models_referenced).toContain('orders'); + expect(dataset.metrics[0].models_referenced).toContain('customers'); + expect(dataset.metrics[0].fields_referenced).toContainEqual({ model: 'orders', field: 'amount' }); + expect(dataset.metrics[0].fields_referenced).toContainEqual({ model: 'customers', field: 'id' }); + }); + + it('should extract AQL refs from chart calculations using regex', () => { + const lineage = transformToLineage(sampleCompiledData, '/test'); + + const chart = lineage.entities.charts[0]; + expect(chart.fields_used.some(f => f.source === 'aql')).toBe(true); + expect(chart.fields_used).toContainEqual({ model: 'orders', field: 'amount', source: 'aql' }); + }); + }); + + describe('with cli-core (type-checked extraction)', () => { + it('should use cli-core extractAqlReferences when available', () => { + const mockExtractAqlReferences = vi.fn().mockReturnValue({ + models: ['orders', 'customers'], + fields: [ + { model: 'orders', field: 'amount' }, + { model: 'customers', field: 'id' }, + ], + errors: [], + }); + + const mockCreateDatasetFromCompiled = vi.fn().mockReturnValue({ + name: 'test_dataset', + models: [], + dataSource: { name: 'bigquery', dbtype: 'bigquery' }, + }); + + const mockCliCore: CliCoreModule = { + registerCommands: vi.fn(), + extractAqlReferences: mockExtractAqlReferences, + createDatasetFromCompiled: mockCreateDatasetFromCompiled, + }; + + const lineage = transformToLineage(sampleCompiledData, '/test', mockCliCore); + + // Verify cli-core was called + expect(mockCreateDatasetFromCompiled).toHaveBeenCalled(); + expect(mockExtractAqlReferences).toHaveBeenCalled(); + + // Verify results are correct + const dataset = lineage.entities.datasets[0]; + expect(dataset.metrics[0].fields_referenced).toContainEqual({ model: 'orders', field: 'amount' }); + expect(dataset.metrics[0].fields_referenced).toContainEqual({ model: 'customers', field: 'id' }); + }); + + it('should cache datasets for repeated AQL extractions', () => { + const mockCreateDatasetFromCompiled = vi.fn().mockReturnValue({ + name: 'test_dataset', + models: [], + dataSource: { name: 'bigquery', dbtype: 'bigquery' }, + }); + + const mockCliCore: CliCoreModule = { + registerCommands: vi.fn(), + extractAqlReferences: vi.fn().mockReturnValue({ + models: ['orders'], + fields: [{ model: 'orders', field: 'amount' }], + errors: [], + }), + createDatasetFromCompiled: mockCreateDatasetFromCompiled, + }; + + // Add multiple metrics to test caching + const dataWithMultipleMetrics = { + ...sampleCompiledData, + 'datasets/test.aml': { + ...sampleCompiledData['datasets/test.aml'], + metric: { + metric1: { + label: 'Metric 1', + definition: { __type__: 'Heredoc', content: 'sum(orders.amount)' }, + }, + metric2: { + label: 'Metric 2', + definition: { __type__: 'Heredoc', content: 'avg(orders.amount)' }, + }, + metric3: { + label: 'Metric 3', + definition: { __type__: 'Heredoc', content: 'count(orders.id)' }, + }, + }, + }, + }; + + transformToLineage(dataWithMultipleMetrics, '/test', mockCliCore); + + // Dataset should only be created once (cached for subsequent metrics) + expect(mockCreateDatasetFromCompiled).toHaveBeenCalledTimes(1); + }); + + it('should fall back to regex when cli-core returns errors', () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const mockCliCore: CliCoreModule = { + registerCommands: vi.fn(), + extractAqlReferences: vi.fn().mockReturnValue({ + models: [], + fields: [], + errors: ['Unknown model: orders'], + }), + createDatasetFromCompiled: vi.fn().mockReturnValue({ + name: 'test_dataset', + models: [], + dataSource: { name: 'bigquery', dbtype: 'bigquery' }, + }), + }; + + const lineage = transformToLineage(sampleCompiledData, '/test', mockCliCore); + + // Should log warning about errors + expect(consoleErrorSpy).toHaveBeenCalled(); + + // Should still extract refs using regex fallback + const dataset = lineage.entities.datasets[0]; + expect(dataset.metrics[0].fields_referenced.length).toBeGreaterThan(0); + + consoleErrorSpy.mockRestore(); + }); + + it('should fall back to regex when cli-core throws an error', () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const mockCliCore: CliCoreModule = { + registerCommands: vi.fn(), + extractAqlReferences: vi.fn().mockImplementation(() => { + throw new Error('Type checker failed'); + }), + createDatasetFromCompiled: vi.fn().mockReturnValue({ + name: 'test_dataset', + models: [], + dataSource: { name: 'bigquery', dbtype: 'bigquery' }, + }), + }; + + const lineage = transformToLineage(sampleCompiledData, '/test', mockCliCore); + + // Should log error + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('Type-checked AQL extraction failed') + ); + + // Should still extract refs using regex fallback + const dataset = lineage.entities.datasets[0]; + expect(dataset.metrics[0].fields_referenced).toContainEqual({ model: 'orders', field: 'amount' }); + + consoleErrorSpy.mockRestore(); + }); + + it('should fall back to regex when createDatasetFromCompiled throws', () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const mockCliCore: CliCoreModule = { + registerCommands: vi.fn(), + extractAqlReferences: vi.fn(), + createDatasetFromCompiled: vi.fn().mockImplementation(() => { + throw new Error('Invalid dataset format'); + }), + }; + + const lineage = transformToLineage(sampleCompiledData, '/test', mockCliCore); + + // Should still extract refs using regex fallback + const dataset = lineage.entities.datasets[0]; + expect(dataset.metrics[0].fields_referenced).toContainEqual({ model: 'orders', field: 'amount' }); + + consoleErrorSpy.mockRestore(); + }); + }); + + describe('chart AQL extraction with dataset context', () => { + it('should pass compiled dataset to chart AQL extraction', () => { + const mockExtractAqlReferences = vi.fn().mockReturnValue({ + models: ['orders'], + fields: [{ model: 'orders', field: 'amount' }], + errors: [], + }); + + const mockCliCore: CliCoreModule = { + registerCommands: vi.fn(), + extractAqlReferences: mockExtractAqlReferences, + createDatasetFromCompiled: vi.fn().mockReturnValue({ + name: 'test_dataset', + models: [], + dataSource: { name: 'bigquery', dbtype: 'bigquery' }, + }), + }; + + transformToLineage(sampleCompiledData, '/test', mockCliCore); + + // extractAqlReferences should be called for both metrics and chart calculations + expect(mockExtractAqlReferences.mock.calls.length).toBeGreaterThan(1); + }); + + it('should handle charts without dataset reference gracefully', () => { + const dataWithOrphanChart = { + 'dashboards/test.aml': { + __type__: 'Dashboard', + __fqn__: 'test_dashboard', + uname: 'test_dashboard', + block: { + orphan_chart: { + def: { + __type__: 'VizBlock', + label: 'Orphan Chart', + viz: { + // No dataset reference + calculation: { + calc: { + formula: { __type__: 'Heredoc', content: 'sum(orders.amount)' }, + }, + }, + }, + }, + }, + }, + }, + }; + + // Should not throw, should use regex fallback + const lineage = transformToLineage(dataWithOrphanChart, '/test'); + const chart = lineage.entities.charts[0]; + expect(chart.fields_used).toContainEqual({ model: 'orders', field: 'amount', source: 'aql' }); + }); + }); +}); diff --git a/src/index.ts b/src/index.ts index bf65448..ddb5866 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,110 @@ import { loadModule } from './loader'; import { Command } from 'commander'; +import { spawn } from 'child_process'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { transformToLineage, type CliCoreModule } from './lineage'; const program = new Command(); -const clicore = await loadModule('@holistics/cli-core'); +const clicore: CliCoreModule = await loadModule('@holistics/cli-core'); clicore.registerCommands(program); +// Helper to run compile command and capture JSON output +async function runCompile(projectPath: string): Promise> { + return new Promise((resolvePromise, reject) => { + // Get the path to the holistics wrapper script + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const holisticsWrapper = resolve(__dirname, '..', 'holistics'); + + const child = spawn(holisticsWrapper, ['aml', 'compile', '.'], { + cwd: projectPath, + stdio: ['inherit', 'pipe', 'pipe'], + env: { ...process.env, HOLISTICS_LINEAGE_SUBPROCESS: '1' }, // Prevent recursion + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(`Compile failed (exit ${code}): ${stderr}`)); + return; + } + try { + const parsed = JSON.parse(stdout); + resolvePromise(parsed); + } catch (e) { + reject(new Error(`Failed to parse compile output: ${e}\nOutput: ${stdout.slice(0, 500)}`)); + } + }); + + child.on('error', reject); + }); +} + +// Add lineage command under 'aml' +const amlCommand = program.commands.find(cmd => cmd.name() === 'aml'); +if (amlCommand) { + amlCommand + .command('lineage [path]') + .description('Extract lineage metadata from AML project in a normalized format') + .option('-o, --output ', 'Output file path (default: stdout)') + .option('--entities ', 'Filter by entity types (comma-separated: models,datasets,dashboards,charts)') + .option('--compact', 'Output compact JSON (no pretty printing)') + .option('--compiled ', 'Use pre-compiled JSON file instead of running compile') + .action(async (path: string = '.', options: { output?: string; entities?: string; compact?: boolean; compiled?: string }) => { + try { + const projectPath = resolve(path); + + // Load compiled data either from file or by running compile + let compiledData: Record; + if (options.compiled) { + const { readFile } = await import('fs/promises'); + const content = await readFile(resolve(options.compiled), 'utf-8'); + compiledData = JSON.parse(content); + } else { + compiledData = await runCompile(projectPath); + } + + // Transform to lineage format (pass cli-core for AQL extraction) + const lineage = transformToLineage(compiledData, projectPath, clicore); + + // Filter entities if requested + if (options.entities) { + const allowedTypes = options.entities.split(',').map((t: string) => t.trim().toLowerCase()); + if (!allowedTypes.includes('models')) lineage.entities.models = []; + if (!allowedTypes.includes('datasets')) lineage.entities.datasets = []; + if (!allowedTypes.includes('dashboards')) lineage.entities.dashboards = []; + if (!allowedTypes.includes('charts')) lineage.entities.charts = []; + } + + // Output + const jsonOutput = options.compact + ? JSON.stringify(lineage) + : JSON.stringify(lineage, null, 2); + + if (options.output) { + const { writeFile } = await import('fs/promises'); + await writeFile(options.output, jsonOutput); + console.error(`Lineage written to ${options.output}`); + } else { + console.log(jsonOutput); + } + } catch (error) { + console.error('Error generating lineage:', error); + process.exit(1); + } + }); +} + program.parse(process.argv); diff --git a/src/lineage.ts b/src/lineage.ts new file mode 100644 index 0000000..2a52206 --- /dev/null +++ b/src/lineage.ts @@ -0,0 +1,781 @@ +/** + * Holistics AML Lineage Command + * + * Extracts lineage metadata from compiled AML and outputs a normalized + * JSON structure optimized for integration with data catalogs. + */ + +import { Command } from 'commander'; + +// Type definitions for @holistics/cli-core AQL extraction utilities +export interface AqlFieldReference { + model: string; + field: string; +} + +export interface AqlExtractionResult { + models: string[]; + fields: AqlFieldReference[]; + errors?: string[]; +} + +export interface CliCoreModule { + registerCommands: (program: Command) => void; + // AQL extraction utilities (available in cli-core >= 0.6.19) + extractAqlReferences?: ( + aqlExpression: string, + dataset: any, + options?: { adhocFields?: string[]; allowAmbiguousPaths?: boolean } + ) => AqlExtractionResult; + createDatasetFromCompiled?: ( + compiledDataset: Record, + dataSource?: any + ) => any; +} + +// Types for the lineage output +interface SourceTable { + database?: string; + schema?: string; + table: string; + full_name: string; +} + +interface Field { + name: string; + label: string; + type: string; + is_dimension: boolean; + is_measure: boolean; + is_primary_key: boolean; + description?: string; + definition?: string; + aggregation?: string; +} + +interface Model { + fqn: string; + name: string; + label: string; + type: 'TableModel' | 'QueryModel'; + description?: string; + data_source: string; + source_table?: SourceTable; + query?: string; + owner?: string; + fields: Field[]; + file_path: string; +} + +interface DatasetMetric { + name: string; + label?: string; + type?: string; + models_referenced: string[]; + fields_referenced: Array<{ model: string; field: string }>; +} + +interface Dataset { + fqn: string; + name: string; + label: string; + description?: string; + data_source: string; + owner?: string; + models: string[]; + metrics: DatasetMetric[]; + file_path: string; +} + +interface FieldReference { + model: string; + field: string; + source: 'field_ref' | 'aql'; +} + +interface Chart { + fqn: string; + name: string; + label: string; + type: string; + dashboard: string; + dataset?: string; + models_used: string[]; + fields_used: FieldReference[]; +} + +interface Dashboard { + fqn: string; + name: string; + title?: string; + description?: string; + owner?: string; + charts: string[]; + file_path: string; +} + +interface LineageEdge { + model: string; + source: { + data_source: string; + database?: string; + schema?: string; + table: string; + }; +} + +interface DatasetToModel { + dataset: string; + models: string[]; +} + +interface ChartToModel { + chart: string; + dataset?: string; + models: string[]; +} + +interface ChartToDataset { + chart: string; + dataset: string; +} + +interface DashboardToChart { + dashboard: string; + charts: string[]; +} + +interface LineageOutput { + version: string; + generated_at: string; + project: { + name: string; + path: string; + }; + entities: { + data_sources: string[]; + models: Model[]; + datasets: Dataset[]; + dashboards: Dashboard[]; + charts: Chart[]; + }; + lineage: { + model_to_source: LineageEdge[]; + dataset_to_model: DatasetToModel[]; + chart_to_dataset: ChartToDataset[]; + chart_to_model: ChartToModel[]; + dashboard_to_chart: DashboardToChart[]; + }; +} + +// Parse source table name into components +function parseTableName(tableName: string): SourceTable { + // Handle backtick-quoted BigQuery style: `project`.`schema`.`table` + const bqMatch = tableName.match(/`([^`]+)`\.`([^`]+)`\.`([^`]+)`/); + if (bqMatch) { + return { + database: bqMatch[1], + schema: bqMatch[2], + table: bqMatch[3], + full_name: tableName, + }; + } + + // Handle double-quote style: "schema"."table" + const pgMatch = tableName.match(/"([^"]+)"\.?"([^"]+)"?/); + if (pgMatch) { + return { + schema: pgMatch[1], + table: pgMatch[2], + full_name: tableName, + }; + } + + // Handle simple schema.table + const simpleMatch = tableName.match(/(\w+)\.(\w+)/); + if (simpleMatch) { + return { + schema: simpleMatch[1], + table: simpleMatch[2], + full_name: tableName, + }; + } + + // Just table name + return { + table: tableName, + full_name: tableName, + }; +} + +// Extract heredoc content +function getHeredocContent(heredoc: any): string | undefined { + if (!heredoc || typeof heredoc !== 'object') return undefined; + if (heredoc.__type__ === 'Heredoc') { + return heredoc.content; + } + return undefined; +} + +// Parse a field (dimension or measure) +function parseField(name: string, data: any, isMeasure: boolean): Field { + return { + name, + label: data.label || name, + type: data.type || 'text', + is_dimension: !isMeasure, + is_measure: isMeasure, + is_primary_key: data.primary_key || false, + description: data.description, + definition: getHeredocContent(data.definition), + aggregation: isMeasure ? data.aggregation_type : undefined, + }; +} + +// Parse a model from compiled JSON +function parseModel(filePath: string, data: any): Model | null { + const type = data.__type__; + if (type !== 'TableModel' && type !== 'QueryModel') return null; + + const fields: Field[] = []; + + // Parse dimensions + if (data.dimension) { + for (const [name, dimData] of Object.entries(data.dimension)) { + fields.push(parseField(name, dimData, false)); + } + } + + // Parse measures + if (data.measure) { + for (const [name, measureData] of Object.entries(data.measure)) { + fields.push(parseField(name, measureData, true)); + } + } + + const model: Model = { + fqn: data.__fqn__ || data.name, + name: data.name, + label: data.label || data.name, + type: type as 'TableModel' | 'QueryModel', + description: typeof data.description === 'string' ? data.description : getHeredocContent(data.description), + data_source: data.data_source_name || '', + owner: data.owner, + fields, + file_path: filePath, + }; + + if (type === 'TableModel' && data.table_name) { + model.source_table = parseTableName(data.table_name); + } + + if (type === 'QueryModel' && data.query) { + model.query = getHeredocContent(data.query); + } + + return model; +} + +/** + * Context for AQL extraction, passed through parsing functions. + * Contains the cli-core module (if available) and cached datasets. + */ +interface AqlExtractionContext { + clicore?: CliCoreModule; + datasetCache: Map; // Cached converted datasets for type-checked extraction +} + +/** + * Extract model.field references from AQL using type-checked extraction when available. + * Falls back to regex-based extraction if cli-core utilities aren't available or fail. + */ +function extractAqlRefsWithTypeChecker( + aql: string, + compiledDataset: any, + ctx: AqlExtractionContext, + knownModels?: Set +): Array<{ model: string; field: string }> { + // Try type-checked extraction if cli-core utilities are available + if (ctx.clicore?.extractAqlReferences && ctx.clicore?.createDatasetFromCompiled && compiledDataset) { + try { + // Get or create the Dataset object for type checking + let dataset = ctx.datasetCache.get(compiledDataset.__fqn__ || compiledDataset.name); + if (!dataset) { + dataset = ctx.clicore.createDatasetFromCompiled(compiledDataset); + ctx.datasetCache.set(compiledDataset.__fqn__ || compiledDataset.name, dataset); + } + + const result = ctx.clicore.extractAqlReferences(aql, dataset); + + // If no errors, use the type-checked result + if (!result.errors || result.errors.length === 0) { + return result.fields; + } + + // If there were errors, log them and fall back to regex + console.error(`[lineage] AQL type-check warnings for "${aql.slice(0, 50)}...": ${result.errors.join(', ')}`); + } catch (error) { + // Type-checked extraction failed, fall back to regex + console.error(`[lineage] Type-checked AQL extraction failed, using regex fallback: ${error}`); + } + } + + // Fall back to regex-based extraction + return extractAqlModelRefs(aql, knownModels); +} + +// AQL reserved words and functions to filter out from model.field extraction +const AQL_RESERVED_WORDS = new Set([ + // SQL keywords that might appear as prefix + 'and', 'or', 'not', 'is', 'in', 'as', 'by', 'on', 'to', 'of', + 'null', 'true', 'false', 'case', 'when', 'then', 'else', 'end', + // Common AQL functions + 'sum', 'count', 'avg', 'min', 'max', 'count_distinct', + 'date_diff', 'date_add', 'date_trunc', 'datetrunc', 'datediff', + 'concat', 'coalesce', 'if', 'ifnull', 'nullif', + 'abs', 'round', 'floor', 'ceil', 'power', 'sqrt', + 'lower', 'upper', 'trim', 'length', 'substring', 'replace', + 'year', 'month', 'day', 'hour', 'minute', 'second', 'week', + 'now', 'today', 'current_date', 'current_timestamp', + // AQL-specific + 'where', 'group', 'order', 'limit', 'offset', 'having', + 'asc', 'desc', 'distinct', 'all', 'any', 'exists', + 'between', 'like', 'ilike', 'similar', + 'cast', 'convert', 'extract', + 'row_number', 'rank', 'dense_rank', 'over', 'partition', + 'first_value', 'last_value', 'lag', 'lead', 'nth_value', + 'running', 'cumulative', +]); + +/** + * Extract model.field references from an AQL expression string. + * Uses regex to find patterns like `model_name.field_name`. + * + * @param aql - The AQL expression string + * @param knownModels - Optional set of known model names for validation + * @returns Array of {model, field} references + */ +function extractAqlModelRefs( + aql: string, + knownModels?: Set +): Array<{ model: string; field: string }> { + const refs: Array<{ model: string; field: string }> = []; + + // Match patterns like: model_name.field_name + // - Must start with a letter or underscore + // - Can contain letters, numbers, underscores + // - Excludes patterns inside quotes or after :: + const pattern = /\b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g; + + let match; + while ((match = pattern.exec(aql)) !== null) { + const [, model, field] = match; + + // Skip if the "model" is a reserved word or function + if (AQL_RESERVED_WORDS.has(model.toLowerCase())) { + continue; + } + + // Skip common false positives + if (model === 'SOURCE' || model === '#SOURCE') { + continue; + } + + // If we have known models, validate against them + if (knownModels && !knownModels.has(model)) { + continue; + } + + refs.push({ model, field }); + } + + // Deduplicate + const seen = new Set(); + return refs.filter(ref => { + const key = `${ref.model}.${ref.field}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); +} + +// Parse a dataset from compiled JSON +function parseDataset(filePath: string, data: any, ctx: AqlExtractionContext): Dataset | null { + if (data.__type__ !== 'Dataset') return null; + + const modelNames: string[] = []; + if (Array.isArray(data.models)) { + for (const model of data.models) { + if (model && typeof model === 'object' && model.name) { + modelNames.push(model.__fqn__ || model.name); + } + } + } + + // Create a set of known model names for AQL validation (fallback for regex) + const knownModels = new Set(modelNames.map(m => { + // Extract just the model name from FQN (last part after ::) + const parts = m.split('::'); + return parts[parts.length - 1]; + })); + + // Parse metrics and extract their AQL references + const metrics: DatasetMetric[] = []; + if (data.metric && typeof data.metric === 'object') { + for (const [metricName, metricData] of Object.entries(data.metric)) { + if (!metricData || typeof metricData !== 'object') continue; + + const metric = metricData as any; + const definition = metric.definition; + const aqlContent = getHeredocContent(definition); + + let fieldsReferenced: Array<{ model: string; field: string }> = []; + if (aqlContent) { + // Use type-checked extraction when available + fieldsReferenced = extractAqlRefsWithTypeChecker(aqlContent, data, ctx, knownModels); + } + + const modelsReferenced = [...new Set(fieldsReferenced.map(f => f.model))]; + + metrics.push({ + name: metricName, + label: metric.label, + type: metric.type, + models_referenced: modelsReferenced, + fields_referenced: fieldsReferenced, + }); + } + } + + return { + fqn: data.__fqn__ || data.name, + name: data.name, + label: data.label || data.name, + description: typeof data.description === 'string' ? data.description : getHeredocContent(data.description), + data_source: data.data_source_name || '', + owner: data.owner, + models: modelNames, + metrics, + file_path: filePath, + }; +} + +/** + * Extract AQL content from Heredoc objects recursively. + * Returns all AQL strings found in the object tree. + */ +function extractAqlStrings(obj: any): string[] { + const aqlStrings: string[] = []; + + function traverse(o: any) { + if (!o || typeof o !== 'object') return; + + // Check for Heredoc with AQL content + if (o.__type__ === 'Heredoc' && typeof o.content === 'string') { + // Check if this is an AQL heredoc (parent context usually indicates this) + // For safety, we'll extract from all heredocs and filter later + aqlStrings.push(o.content); + } + + // Check for AqlHeredoc type + if (o.__type__ === 'AqlHeredoc' && typeof o.content === 'string') { + aqlStrings.push(o.content); + } + + if (Array.isArray(o)) { + for (const item of o) traverse(item); + } else { + for (const value of Object.values(o)) traverse(value); + } + } + + traverse(obj); + return aqlStrings; +} + +// Extract field references from a viz object recursively +function extractFieldRefs( + obj: any, + ctx: AqlExtractionContext, + compiledDataset?: any, + knownModels?: Set +): FieldReference[] { + const refs: FieldReference[] = []; + + function traverse(o: any) { + if (!o || typeof o !== 'object') return; + + // Extract explicit FieldRef objects + if (o.__type__ === 'FieldRef' && o.model && o.field) { + refs.push({ model: o.model, field: o.field, source: 'field_ref' }); + } + + if (Array.isArray(o)) { + for (const item of o) traverse(item); + } else { + for (const value of Object.values(o)) traverse(value); + } + } + + traverse(obj); + + // Also extract from AQL strings found in the object + const aqlStrings = extractAqlStrings(obj); + for (const aql of aqlStrings) { + // Use type-checked extraction when we have dataset context + const aqlRefs = compiledDataset + ? extractAqlRefsWithTypeChecker(aql, compiledDataset, ctx, knownModels) + : extractAqlModelRefs(aql, knownModels); + for (const ref of aqlRefs) { + refs.push({ ...ref, source: 'aql' }); + } + } + + // Deduplicate, preferring field_ref over aql for same model.field + const seen = new Map(); + for (const ref of refs) { + const key = `${ref.model}.${ref.field}`; + const existing = seen.get(key); + if (!existing || (existing.source === 'aql' && ref.source === 'field_ref')) { + seen.set(key, ref); + } + } + + return [...seen.values()]; +} + +// Parse a chart (viz block) from a dashboard +function parseChart( + dashboardFqn: string, + blockName: string, + blockData: any, + ctx: AqlExtractionContext, + datasetLookup: Map +): Chart { + const def = blockData.def || {}; + const viz = def.viz || {}; + const datasetRef = viz.dataset || {}; + const datasetFqn = datasetRef.__fqn__ || datasetRef.name; + + // Get the compiled dataset for type-checked AQL extraction + const compiledDataset = datasetFqn ? datasetLookup.get(datasetFqn) : undefined; + + const fieldRefs = extractFieldRefs(viz, ctx, compiledDataset); + const modelsUsed = [...new Set(fieldRefs.map(r => r.model))]; + + return { + fqn: `${dashboardFqn}.${blockName}`, + name: blockName, + label: def.label || blockName, + type: def.__type__ || 'VizBlock', + dashboard: dashboardFqn, + dataset: datasetFqn, + models_used: modelsUsed, + fields_used: fieldRefs, + }; +} + +// Parse a dashboard from compiled JSON +function parseDashboard( + filePath: string, + data: any, + ctx: AqlExtractionContext, + datasetLookup: Map +): { dashboard: Dashboard; charts: Chart[] } | null { + if (data.__type__ !== 'Dashboard') return null; + + const dashboardFqn = data.__fqn__ || data.uname; + const charts: Chart[] = []; + const chartFqns: string[] = []; + + if (data.block && typeof data.block === 'object') { + for (const [blockName, blockData] of Object.entries(data.block)) { + const chart = parseChart(dashboardFqn, blockName, blockData, ctx, datasetLookup); + charts.push(chart); + chartFqns.push(chart.fqn); + } + } + + const dashboard: Dashboard = { + fqn: dashboardFqn, + name: data.uname || dashboardFqn, + title: data.title, + description: typeof data.description === 'string' ? data.description : getHeredocContent(data.description), + owner: data.owner, + charts: chartFqns, + file_path: filePath, + }; + + return { dashboard, charts }; +} + +// Main function to transform compiled JSON to lineage format +export function transformToLineage( + compiledData: Record, + projectPath: string, + clicore?: CliCoreModule +): LineageOutput { + const models: Model[] = []; + const datasets: Dataset[] = []; + const dashboards: Dashboard[] = []; + const charts: Chart[] = []; + const dataSources = new Set(); + + // Create AQL extraction context + const ctx: AqlExtractionContext = { + clicore, + datasetCache: new Map(), + }; + + // Build a lookup map of compiled datasets for type-checked AQL extraction + const datasetLookup = new Map(); + for (const [, data] of Object.entries(compiledData)) { + if (data && typeof data === 'object' && data.__type__ === 'Dataset') { + const fqn = data.__fqn__ || data.name; + if (fqn) datasetLookup.set(fqn, data); + } + } + + // Parse all entities + for (const [filePath, data] of Object.entries(compiledData)) { + if (!data || typeof data !== 'object' || !data.__type__) continue; + + // Skip non-AML files that got compiled + if (!filePath.endsWith('.aml') && !filePath.includes('.aml')) { + // Check if it's a valid entity anyway + if (!['TableModel', 'QueryModel', 'Dataset', 'Dashboard'].includes(data.__type__)) { + continue; + } + } + + const entityType = data.__type__; + + if (entityType === 'TableModel' || entityType === 'QueryModel') { + const model = parseModel(filePath, data); + if (model) { + models.push(model); + if (model.data_source) dataSources.add(model.data_source); + } + } else if (entityType === 'Dataset') { + const dataset = parseDataset(filePath, data, ctx); + if (dataset) { + datasets.push(dataset); + if (dataset.data_source) dataSources.add(dataset.data_source); + } + } else if (entityType === 'Dashboard') { + const result = parseDashboard(filePath, data, ctx, datasetLookup); + if (result) { + dashboards.push(result.dashboard); + charts.push(...result.charts); + } + } + } + + // Build lineage edges + const modelToSource: LineageEdge[] = models + .filter(m => m.type === 'TableModel' && m.source_table) + .map(m => ({ + model: m.fqn, + source: { + data_source: m.data_source, + database: m.source_table!.database, + schema: m.source_table!.schema, + table: m.source_table!.table, + }, + })); + + const datasetToModel: DatasetToModel[] = datasets.map(d => ({ + dataset: d.fqn, + models: d.models, + })); + + const chartToDataset: ChartToDataset[] = charts + .filter(c => c.dataset) + .map(c => ({ + chart: c.fqn, + dataset: c.dataset!, + })); + + const chartToModel: ChartToModel[] = charts + .filter(c => c.models_used.length > 0) + .map(c => ({ + chart: c.fqn, + dataset: c.dataset, + models: c.models_used, + })); + + const dashboardToChart: DashboardToChart[] = dashboards.map(d => ({ + dashboard: d.fqn, + charts: d.charts, + })); + + // Extract project name from path + const projectName = projectPath.split('/').filter(Boolean).pop() || 'unknown'; + + return { + version: '1.0', + generated_at: new Date().toISOString(), + project: { + name: projectName, + path: projectPath, + }, + entities: { + data_sources: [...dataSources], + models, + datasets, + dashboards, + charts, + }, + lineage: { + model_to_source: modelToSource, + dataset_to_model: datasetToModel, + chart_to_dataset: chartToDataset, + chart_to_model: chartToModel, + dashboard_to_chart: dashboardToChart, + }, + }; +} + +// Register the lineage command +export function registerLineageCommand(program: Command, compileFunc: (path: string) => Promise>) { + program + .command('lineage [path]') + .description('Extract lineage metadata from AML project') + .option('-o, --output ', 'Output file path (default: stdout)') + .option('--entities ', 'Filter by entity types (comma-separated: models,datasets,dashboards,charts)') + .option('--compact', 'Output compact JSON (no pretty printing)') + .action(async (path: string = '.', options: { output?: string; entities?: string; compact?: boolean }) => { + try { + // Compile the AML project + const compiledData = await compileFunc(path); + + // Transform to lineage format + const lineage = transformToLineage(compiledData, path); + + // Filter entities if requested + if (options.entities) { + const allowedTypes = options.entities.split(',').map(t => t.trim().toLowerCase()); + if (!allowedTypes.includes('models')) lineage.entities.models = []; + if (!allowedTypes.includes('datasets')) lineage.entities.datasets = []; + if (!allowedTypes.includes('dashboards')) lineage.entities.dashboards = []; + if (!allowedTypes.includes('charts')) lineage.entities.charts = []; + } + + // Output + const jsonOutput = options.compact + ? JSON.stringify(lineage) + : JSON.stringify(lineage, null, 2); + + if (options.output) { + const { writeFile } = await import('fs/promises'); + await writeFile(options.output, jsonOutput); + console.error(`Lineage written to ${options.output}`); + } else { + console.log(jsonOutput); + } + } catch (error) { + console.error('Error generating lineage:', error); + process.exit(1); + } + }); +} diff --git a/src/loader.ts b/src/loader.ts index 574e40b..44ec155 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -1,7 +1,20 @@ import { join } from "path"; import { ensureModule } from "./downloader"; +/** + * Load a module from npm or a local path. + * + * For local development, set CLI_CORE_PATH to point to your local cli-core: + * CLI_CORE_PATH=/path/to/holistics-core/packages/cli-core pnpm cli ... + */ export async function loadModule(pkg: string, version?: string) { + // Support local development via environment variable + if (pkg === '@holistics/cli-core' && process.env.CLI_CORE_PATH) { + const localPath = process.env.CLI_CORE_PATH; + console.error(`[dev] Using local cli-core from: ${localPath}`); + return import(join(localPath, "dist/commands.js")); + } + const modulePath = await ensureModule(pkg, version); return import(join(modulePath, "dist/commands.js")); }