From 7fa2b32d57183efd4b04c471032364e448ad02eb Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 3 Mar 2026 19:39:26 -0800 Subject: [PATCH 01/21] =?UTF-8?q?fix(deps):=20upgrade=20vitest=202.1.9=20?= =?UTF-8?q?=E2=86=92=204.0.18,=20fix=20breaking=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vitest 4 removed the deprecated test(name, fn, { options }) signature. Migrate all per-test timeouts to the new test(name, { timeout }, fn) form, or lift uniform timeouts to the describe level. Also fix vi.fn().mockImplementation(() => ...) constructor mocks to use function expressions per Vitest 4 requirements (SyncController, HealthCheckService mocks). Resolves all 5 remaining moderate-severity npm audit advisories (esbuild, vite, @vitest/mocker, vite-node, vitest). --- package-lock.json | 657 +++++++++--------- package.json | 2 +- test/unit/cli/doctor.test.js | 14 +- .../domain/WarpGraph.cascadeDelete.test.js | 18 +- .../WarpGraph.deleteGuardEnforce.test.js | 28 +- .../domain/WarpGraph.noCoordination.test.js | 24 +- test/unit/domain/WarpGraph.patchMany.test.js | 4 +- .../domain/WarpGraph.syncMaterialize.test.js | 8 +- .../domain/services/BisectService.test.js | 20 +- .../services/IndexRebuildService.deep.test.js | 4 +- .../domain/services/SyncController.test.js | 8 +- 11 files changed, 406 insertions(+), 381 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e0ec691..591fa395 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@git-stunts/git-warp", - "version": "13.0.0", + "version": "13.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@git-stunts/git-warp", - "version": "13.0.0", + "version": "13.0.1", "license": "Apache-2.0", "dependencies": { "@git-stunts/alfred": "^0.4.0", @@ -40,7 +40,7 @@ "prettier": "^3.4.2", "typescript": "^5.9.3", "typescript-eslint": "^8.54.0", - "vitest": "^2.1.8" + "vitest": "^4.0.18" }, "engines": { "node": ">=22.0.0" @@ -135,9 +135,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -148,13 +148,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -165,13 +165,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -182,13 +182,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -199,13 +199,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -216,13 +216,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -233,13 +233,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -250,13 +250,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -267,13 +267,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -284,13 +284,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -301,13 +301,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -318,13 +318,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -335,13 +335,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -352,13 +352,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -369,13 +369,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -386,13 +386,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -403,13 +403,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -420,13 +420,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -437,13 +454,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -454,13 +488,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -471,13 +522,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -488,13 +539,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -505,13 +556,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -522,7 +573,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1220,6 +1271,31 @@ "win32" ] }, + "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/@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", @@ -1501,38 +1577,40 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", + "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0" + "vite": "^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -1544,70 +1622,66 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1862,16 +1936,6 @@ "node": ">=8" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/cacache": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", @@ -1999,18 +2063,11 @@ } }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -2027,16 +2084,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -2207,16 +2254,6 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2352,9 +2389,9 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2362,32 +2399,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/escape-string-regexp": { @@ -3349,13 +3389,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "11.2.4", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", @@ -3789,6 +3822,17 @@ "node": ">= 0.4" } }, + "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/open": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", @@ -3983,22 +4027,12 @@ } }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "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/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4020,9 +4054,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -4451,11 +4485,14 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { "version": "0.2.15", @@ -4474,30 +4511,10 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", "engines": { @@ -4663,21 +4680,24 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -4686,19 +4706,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.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 }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -4719,74 +4745,60 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, @@ -4794,10 +4806,19 @@ "@edge-runtime/vm": { "optional": true }, + "@opentelemetry/api": { + "optional": true + }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { "optional": true }, "@vitest/ui": { diff --git a/package.json b/package.json index ca6d8925..88c9445c 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "prettier": "^3.4.2", "typescript": "^5.9.3", "typescript-eslint": "^8.54.0", - "vitest": "^2.1.8" + "vitest": "^4.0.18" }, "keywords": [ "git", diff --git a/test/unit/cli/doctor.test.js b/test/unit/cli/doctor.test.js index e3e4350f..fdc8fe3d 100644 --- a/test/unit/cli/doctor.test.js +++ b/test/unit/cli/doctor.test.js @@ -12,12 +12,14 @@ vi.mock('../../../bin/cli/shared.js', () => ({ // Mock HealthCheckService vi.mock('../../../src/domain/services/HealthCheckService.js', () => ({ - default: vi.fn().mockImplementation(() => ({ - getHealth: vi.fn().mockResolvedValue({ - status: 'healthy', - components: { repository: { status: 'healthy', latencyMs: 1 } }, - }), - })), + default: vi.fn().mockImplementation(function () { + return { + getHealth: vi.fn().mockResolvedValue({ + status: 'healthy', + components: { repository: { status: 'healthy', latencyMs: 1 } }, + }), + }; + }), })); // Mock ClockAdapter diff --git a/test/unit/domain/WarpGraph.cascadeDelete.test.js b/test/unit/domain/WarpGraph.cascadeDelete.test.js index c4212292..fc52f82e 100644 --- a/test/unit/domain/WarpGraph.cascadeDelete.test.js +++ b/test/unit/domain/WarpGraph.cascadeDelete.test.js @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import WarpGraph from '../../../src/domain/WarpGraph.js'; import { createGitRepo } from '../../helpers/warpGraphTestUtils.js'; -describe('Cascade delete mode (HS/DELGUARD/3)', () => { +describe('Cascade delete mode (HS/DELGUARD/3)', { timeout: 15000 }, () => { it('cascade delete generates EdgeRemove ops for 3 connected edges + NodeRemove', async () => { const repo = await createGitRepo('cascade'); try { @@ -49,7 +49,7 @@ describe('Cascade delete mode (HS/DELGUARD/3)', () => { } finally { await repo.cleanup(); } - }, { timeout: 15000 }); + }); it('materialized state has no dangling edges after cascade delete', async () => { const repo = await createGitRepo('cascade'); @@ -90,7 +90,7 @@ describe('Cascade delete mode (HS/DELGUARD/3)', () => { } finally { await repo.cleanup(); } - }, { timeout: 15000 }); + }); it('cascade delete on node with no edges produces only NodeRemove', async () => { const repo = await createGitRepo('cascade'); @@ -121,7 +121,7 @@ describe('Cascade delete mode (HS/DELGUARD/3)', () => { } finally { await repo.cleanup(); } - }, { timeout: 15000 }); + }); it('cascade delete handles both incoming and outgoing edges', async () => { const repo = await createGitRepo('cascade'); @@ -174,7 +174,7 @@ describe('Cascade delete mode (HS/DELGUARD/3)', () => { } finally { await repo.cleanup(); } - }, { timeout: 15000 }); + }); it('cascade delete handles self-loop edge correctly', async () => { const repo = await createGitRepo('cascade'); @@ -220,7 +220,7 @@ describe('Cascade delete mode (HS/DELGUARD/3)', () => { } finally { await repo.cleanup(); } - }, { timeout: 15000 }); + }); it('generated EdgeRemove ops appear in committed patch (auditable)', async () => { const repo = await createGitRepo('cascade'); @@ -264,7 +264,7 @@ describe('Cascade delete mode (HS/DELGUARD/3)', () => { } finally { await repo.cleanup(); } - }, { timeout: 15000 }); + }); it('cascade mode preserves unrelated edges', async () => { const repo = await createGitRepo('cascade'); @@ -306,7 +306,7 @@ describe('Cascade delete mode (HS/DELGUARD/3)', () => { } finally { await repo.cleanup(); } - }, { timeout: 15000 }); + }); it('without cascade mode, removeNode does not generate EdgeRemove ops', async () => { const repo = await createGitRepo('cascade'); @@ -338,5 +338,5 @@ describe('Cascade delete mode (HS/DELGUARD/3)', () => { } finally { await repo.cleanup(); } - }, { timeout: 15000 }); + }); }); diff --git a/test/unit/domain/WarpGraph.deleteGuardEnforce.test.js b/test/unit/domain/WarpGraph.deleteGuardEnforce.test.js index 434b9e4e..8f3d14c4 100644 --- a/test/unit/domain/WarpGraph.deleteGuardEnforce.test.js +++ b/test/unit/domain/WarpGraph.deleteGuardEnforce.test.js @@ -2,7 +2,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import WarpGraph from '../../../src/domain/WarpGraph.js'; import { createGitRepo } from '../../helpers/warpGraphTestUtils.js'; -describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { +describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', { timeout: 15000 }, () => { /** @type {any} */ let repo; @@ -40,7 +40,7 @@ describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { expect(() => patch.removeNode('n1')).toThrow( /Cannot delete node 'n1': node has attached data.*propert/ ); - }, { timeout: 15000 }); + }); it('throws when deleting a node that has edges', async () => { repo = await createGitRepo('delguard'); @@ -65,7 +65,7 @@ describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { expect(() => patch.removeNode('n1')).toThrow( /Cannot delete node 'n1': node has attached data.*edge/ ); - }, { timeout: 15000 }); + }); it('throws when deleting a node that is an edge target', async () => { repo = await createGitRepo('delguard'); @@ -90,7 +90,7 @@ describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { expect(() => patch.removeNode('n2')).toThrow( /Cannot delete node 'n2': node has attached data.*edge/ ); - }, { timeout: 15000 }); + }); it('succeeds when deleting a node with no attached data', async () => { repo = await createGitRepo('delguard'); @@ -115,7 +115,7 @@ describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { expect(typeof sha).toBe('string'); expect(sha.length).toBe(40); - }, { timeout: 15000 }); + }); it('mentions both edges and properties in error when both exist', async () => { repo = await createGitRepo('delguard'); @@ -140,7 +140,7 @@ describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { expect(() => patch.removeNode('n1')).toThrow( /1 edge\(s\) and 1 propert/ ); - }, { timeout: 15000 }); + }); it('error message suggests cascade mode', async () => { repo = await createGitRepo('delguard'); @@ -162,7 +162,7 @@ describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { expect(() => patch.removeNode('n1')).toThrow( /set onDeleteWithData to 'cascade'/ ); - }, { timeout: 15000 }); + }); }); // --------------------------------------------------------------------------- @@ -206,7 +206,7 @@ describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { expect(warnSpy).toHaveBeenCalledOnce(); expect(warnSpy.mock.calls[0][0]).toMatch(/Deleting node 'n1'/); expect(warnSpy.mock.calls[0][0]).toMatch(/propert/); - }, { timeout: 15000 }); + }); it('logs warning via logger when deleting node with edges', async () => { repo = await createGitRepo('delguard'); @@ -234,7 +234,7 @@ describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { expect(typeof sha).toBe('string'); expect(warnSpy).toHaveBeenCalled(); expect(warnSpy.mock.calls[0][0]).toMatch(/edge/); - }, { timeout: 15000 }); + }); it('does not warn when deleting node with no attached data', async () => { repo = await createGitRepo('delguard'); @@ -260,7 +260,7 @@ describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { expect(typeof sha).toBe('string'); expect(warnSpy).not.toHaveBeenCalled(); - }, { timeout: 15000 }); + }); }); // --------------------------------------------------------------------------- @@ -291,7 +291,7 @@ describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { expect(() => patch.removeNode('n1')).toThrow( /Cannot delete node 'n1'/ ); - }, { timeout: 15000 }); + }); it('warn mode works through writer().commitPatch()', async () => { repo = await createGitRepo('delguard'); @@ -319,7 +319,7 @@ describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { expect(typeof sha).toBe('string'); expect(warnSpy).toHaveBeenCalledOnce(); expect(warnSpy.mock.calls[0][0]).toMatch(/propert/); - }, { timeout: 15000 }); + }); }); // --------------------------------------------------------------------------- @@ -354,7 +354,7 @@ describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { expect(typeof sha).toBe('string'); expect(warnSpy).not.toHaveBeenCalled(); - }, { timeout: 15000 }); + }); }); // --------------------------------------------------------------------------- @@ -375,6 +375,6 @@ describe('WarpGraph deleteGuard enforcement (HS/DELGUARD/2)', () => { // removeNode should not throw because there's no state to check against const patch = await graph.createPatch(); expect(() => patch.removeNode('n1')).not.toThrow(); - }, { timeout: 15000 }); + }); }); }); diff --git a/test/unit/domain/WarpGraph.noCoordination.test.js b/test/unit/domain/WarpGraph.noCoordination.test.js index 50cd64bb..5d7b7057 100644 --- a/test/unit/domain/WarpGraph.noCoordination.test.js +++ b/test/unit/domain/WarpGraph.noCoordination.test.js @@ -16,7 +16,7 @@ async function assertLinearWriterChain(/** @type {any} */ persistence, /** @type } describe('No-coordination regression suite', () => { - it('keeps writer refs linear after sync cycles', async () => { + it('keeps writer refs linear after sync cycles', { timeout: 20000 }, async () => { const repoA = await createGitRepo('nocoord'); const repoB = await createGitRepo('nocoord'); @@ -67,7 +67,7 @@ describe('No-coordination regression suite', () => { await repoA.cleanup(); await repoB.cleanup(); } - }, { timeout: 20000 }); + }); it('does not enumerate other writer heads during commit', async () => { const repo = await createGitRepo('nocoord'); @@ -89,7 +89,7 @@ describe('No-coordination regression suite', () => { } }); - it('survives random sync/commit interleavings without merge commits', async () => { + it('survives random sync/commit interleavings without merge commits', { timeout: 30000 }, async () => { const opArb = fc.array( fc.constantFrom('commitA', 'commitB', 'syncAB', 'syncBA'), { minLength: 1, maxLength: 6 } @@ -138,10 +138,10 @@ describe('No-coordination regression suite', () => { }), { seed: 4242, numRuns: 8 } ); - }, { timeout: 30000 }); + }); describe('Lamport clock global-max monotonicity', () => { - it('first-time writer beats existing writer when it materializes first', async () => { + it('first-time writer beats existing writer when it materializes first', { timeout: 20000 }, async () => { // Regression: when writer B makes its very first commit to a repo where writer A // has already committed at tick N, B must commit at tick > N so its operations // win the LWW CRDT tiebreaker — not lose to A's tick-1 commit. @@ -191,9 +191,9 @@ describe('No-coordination regression suite', () => { } finally { await repo.cleanup(); } - }, { timeout: 20000 }); + }); - it('_maxObservedLamport is updated after each commit on the same instance', async () => { + it('_maxObservedLamport is updated after each commit on the same instance', { timeout: 10000 }, async () => { const repo = await createGitRepo('lamport-mono'); try { const graph = await WarpGraph.open({ @@ -219,9 +219,9 @@ describe('No-coordination regression suite', () => { } finally { await repo.cleanup(); } - }, { timeout: 10000 }); + }); - it('cross-writer PropSet LWW: later writer override is not silently discarded', async () => { + it('cross-writer PropSet LWW: later writer override is not silently discarded', { timeout: 20000 }, async () => { // Regression for: "Lamport clock doesn't advance past cross-writer ticks // during materialize()" — Writer A commits setProperty at tick N, Writer B // materializes (observes A), then commits setProperty for the same key. @@ -284,9 +284,9 @@ describe('No-coordination regression suite', () => { } finally { await repo.cleanup(); } - }, { timeout: 20000 }); + }); - it('materialize updates _maxObservedLamport from observed patches', async () => { + it('materialize updates _maxObservedLamport from observed patches', { timeout: 10000 }, async () => { const repo = await createGitRepo('lamport-mono'); try { // Seed with writer-z at tick 1 @@ -317,7 +317,7 @@ describe('No-coordination regression suite', () => { } finally { await repo.cleanup(); } - }, { timeout: 10000 }); + }); }); }); diff --git a/test/unit/domain/WarpGraph.patchMany.test.js b/test/unit/domain/WarpGraph.patchMany.test.js index 04ba9208..c42b1769 100644 --- a/test/unit/domain/WarpGraph.patchMany.test.js +++ b/test/unit/domain/WarpGraph.patchMany.test.js @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import WarpGraph from '../../../src/domain/WarpGraph.js'; import { createGitRepo } from '../../helpers/warpGraphTestUtils.js'; -describe('WarpGraph.patchMany()', () => { +describe('WarpGraph.patchMany()', { timeout: 30000 }, () => { it('returns empty array when called with no arguments', async () => { const repo = await createGitRepo('patchMany-empty'); try { @@ -147,4 +147,4 @@ describe('WarpGraph.patchMany()', () => { await repo.cleanup(); } }); -}, { timeout: 30000 }); +}); diff --git a/test/unit/domain/WarpGraph.syncMaterialize.test.js b/test/unit/domain/WarpGraph.syncMaterialize.test.js index e3cf1bfa..8ae17d0e 100644 --- a/test/unit/domain/WarpGraph.syncMaterialize.test.js +++ b/test/unit/domain/WarpGraph.syncMaterialize.test.js @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import WarpGraph from '../../../src/domain/WarpGraph.js'; import { createGitRepo } from '../../helpers/warpGraphTestUtils.js'; -describe('syncWith({ materialize }) option', () => { +describe('syncWith({ materialize }) option', { timeout: 20000 }, () => { it('syncWith(peer, { materialize: true }) returns fresh state in result', async () => { const repoA = await createGitRepo('syncmat'); const repoB = await createGitRepo('syncmat'); @@ -38,7 +38,7 @@ describe('syncWith({ materialize }) option', () => { await repoA.cleanup(); await repoB.cleanup(); } - }, { timeout: 20000 }); + }); it('syncWith(peer) (default) does NOT auto-materialize — result has no state field', async () => { const repoA = await createGitRepo('syncmat'); @@ -69,7 +69,7 @@ describe('syncWith({ materialize }) option', () => { await repoA.cleanup(); await repoB.cleanup(); } - }, { timeout: 20000 }); + }); it('sync applies 0 patches + materialize:true — materialize still runs', async () => { const repoA = await createGitRepo('syncmat'); @@ -102,5 +102,5 @@ describe('syncWith({ materialize }) option', () => { await repoA.cleanup(); await repoB.cleanup(); } - }, { timeout: 20000 }); + }); }); diff --git a/test/unit/domain/services/BisectService.test.js b/test/unit/domain/services/BisectService.test.js index f8ffa742..4c490ed1 100644 --- a/test/unit/domain/services/BisectService.test.js +++ b/test/unit/domain/services/BisectService.test.js @@ -4,7 +4,7 @@ import BisectService from '../../../../src/domain/services/BisectService.js'; import { orsetContains } from '../../../../src/domain/crdt/ORSet.js'; import { createGitRepo } from '../../../helpers/warpGraphTestUtils.js'; -describe('BisectService', () => { +describe('BisectService', { timeout: 30000 }, () => { it('vector 1: linear chain — finds first bad patch', async () => { const repo = await createGitRepo('bisect-linear'); try { @@ -42,7 +42,7 @@ describe('BisectService', () => { } finally { await repo.cleanup(); } - }, { timeout: 30000 }); + }); it('vector 2: same good and bad — range-error', async () => { const repo = await createGitRepo('bisect-same'); @@ -69,7 +69,7 @@ describe('BisectService', () => { } finally { await repo.cleanup(); } - }, { timeout: 30000 }); + }); it('vector 3: single step — A→B, good=A bad=B → result=B, 0 steps', async () => { const repo = await createGitRepo('bisect-single'); @@ -102,7 +102,7 @@ describe('BisectService', () => { } finally { await repo.cleanup(); } - }, { timeout: 30000 }); + }); it('vector 4: good is not ancestor of bad — range-error', async () => { const repo = await createGitRepo('bisect-reversed'); @@ -131,7 +131,7 @@ describe('BisectService', () => { } finally { await repo.cleanup(); } - }, { timeout: 30000 }); + }); it('vector 5: SHA not found in chain — range-error', async () => { const repo = await createGitRepo('bisect-notfound'); @@ -159,7 +159,7 @@ describe('BisectService', () => { } finally { await repo.cleanup(); } - }, { timeout: 30000 }); + }); it('vector 6: testFn receives candidate SHA', async () => { const repo = await createGitRepo('bisect-sha-arg'); @@ -199,7 +199,7 @@ describe('BisectService', () => { } finally { await repo.cleanup(); } - }, { timeout: 30000 }); + }); it('vector 7: all-bad — first candidate after good is the first bad patch', async () => { const repo = await createGitRepo('bisect-all-bad'); @@ -230,7 +230,7 @@ describe('BisectService', () => { } finally { await repo.cleanup(); } - }, { timeout: 30000 }); + }); it('vector 8: testFn throws — promise rejects with same error', async () => { const repo = await createGitRepo('bisect-throws'); @@ -259,7 +259,7 @@ describe('BisectService', () => { } finally { await repo.cleanup(); } - }, { timeout: 30000 }); + }); it('vector 9: empty writer chain — range-error', async () => { const repo = await createGitRepo('bisect-empty-writer'); @@ -289,5 +289,5 @@ describe('BisectService', () => { } finally { await repo.cleanup(); } - }, { timeout: 30000 }); + }); }); diff --git a/test/unit/domain/services/IndexRebuildService.deep.test.js b/test/unit/domain/services/IndexRebuildService.deep.test.js index ac20366c..d806d478 100644 --- a/test/unit/domain/services/IndexRebuildService.deep.test.js +++ b/test/unit/domain/services/IndexRebuildService.deep.test.js @@ -3,7 +3,7 @@ import IndexRebuildService from '../../../../src/domain/services/IndexRebuildSer import GraphNode from '../../../../src/domain/entities/GraphNode.js'; describe('IndexRebuildService Deep DAG Test', () => { - it('handles 10,000 node chain without stack overflow', async () => { + it('handles 10,000 node chain without stack overflow', { timeout: 30000 }, async () => { const CHAIN_LENGTH = 10_000; // Generate a linear chain: node0 <- node1 <- node2 <- ... <- node9999 @@ -60,7 +60,7 @@ describe('IndexRebuildService Deep DAG Test', () => { treeEntries.forEach(/** @param {any} entry */ entry => { expect(entry).toMatch(/^100644 blob blob\d+\t(meta|shards)_.+\.json$/); }); - }, 30000); // 30 second timeout for large test + }); it('handles wide DAG (node with 1000 parents) without issues', async () => { const PARENT_COUNT = 1000; diff --git a/test/unit/domain/services/SyncController.test.js b/test/unit/domain/services/SyncController.test.js index efa74c44..914fcbfe 100644 --- a/test/unit/domain/services/SyncController.test.js +++ b/test/unit/domain/services/SyncController.test.js @@ -9,9 +9,11 @@ const { timeoutMock, retryMock, httpSyncServerMock } = vi.hoisted(() => { return await fn(ac.signal); }); const retryMock = vi.fn(async (/** @type {Function} */ fn) => await fn()); - const httpSyncServerMock = vi.fn().mockImplementation(() => ({ - listen: vi.fn().mockResolvedValue({ close: vi.fn(), url: 'http://127.0.0.1:3000/sync' }), - })); + const httpSyncServerMock = vi.fn().mockImplementation(function () { + return { + listen: vi.fn().mockResolvedValue({ close: vi.fn(), url: 'http://127.0.0.1:3000/sync' }), + }; + }); return { timeoutMock, retryMock, httpSyncServerMock }; }); From 63dd929f26e8c427cc8f6250f04408977aa61ceb Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 3 Mar 2026 19:50:27 -0800 Subject: [PATCH 02/21] docs(changelog): add vitest 4 upgrade to [Unreleased] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b764338..683f1dc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- **Vitest 2.1.9 → 4.0.18** — major test framework upgrade. Migrated deprecated `test(name, fn, { timeout })` signatures to `test(name, { timeout }, fn)` across 7 test files (40 call sites). Fixed `vi.fn().mockImplementation()` constructor mocks to use `function` expressions per Vitest 4 requirements. Resolves 5 remaining moderate-severity npm audit advisories (`esbuild` [GHSA-67mh-4wv8-2f99](https://github.com/advisories/GHSA-67mh-4wv8-2f99), `vite`, `@vitest/mocker`, `vite-node`, `vitest`). **`npm audit` now reports 0 vulnerabilities.** + ## [13.0.1] — 2026-03-03 ### Fixed From cbd1b017cc47cefedb4dc7f81d2ea7d2a40c4ee4 Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 3 Mar 2026 22:01:06 -0800 Subject: [PATCH 03/21] fix(test): externalize roaring in vitest config for Bun compatibility Vite 7 (pulled in by vitest 4) attempts to transform all imports through its pipeline, which breaks native C++ addons like roaring. Add server.deps.external to skip transformation of the roaring package, restoring Bun integration test compatibility. --- vitest.config.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vitest.config.js b/vitest.config.js index 4a697ee0..b3f78c25 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -1,6 +1,12 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ + server: { + deps: { + // Native C++ addons must not be transformed by Vite's pipeline. + external: ['roaring'], + }, + }, test: { include: [ '**/*.{test,spec}.?(c|m)[jt]s?(x)', From d43472c8f572ab56ac9c744067b26e02b57ae7b5 Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 3 Mar 2026 22:06:14 -0800 Subject: [PATCH 04/21] fix(test): move roaring externalization to test.server.deps.external The external config was placed at the top-level server key instead of under test.server.deps.external where Vitest 4 reads it. --- vitest.config.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vitest.config.js b/vitest.config.js index b3f78c25..db38a1a8 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -1,17 +1,17 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ - server: { - deps: { - // Native C++ addons must not be transformed by Vite's pipeline. - external: ['roaring'], - }, - }, test: { include: [ '**/*.{test,spec}.?(c|m)[jt]s?(x)', '**/benchmark/*.benchmark.js', ], testTimeout: 60000, // 60s timeout for benchmark tests + server: { + deps: { + // Native C++ addons must not be transformed by Vite's pipeline. + external: ['roaring'], + }, + }, }, }); From aee350df99ac6db015f19163017a547e844d8c4b Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 3 Mar 2026 22:12:04 -0800 Subject: [PATCH 05/21] fix(test): add ssr.external for roaring native module under Bun Vitest 4 (Vite 7) intercepts dynamic import('roaring') through its SSR transform pipeline, preventing the native .node binary from loading under Bun. Add both ssr.external and test.server.deps.external (regex pattern) to ensure the roaring package bypasses Vite's transform and resolution phases entirely. --- vitest.config.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/vitest.config.js b/vitest.config.js index db38a1a8..f9f25752 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -1,6 +1,12 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ + // Externalize native C++ addons from Vite's SSR transform pipeline. + // Without this, Vite 7 (vitest 4) intercepts dynamic import('roaring') + // and fails to load the .node binary, breaking Bun integration tests. + ssr: { + external: ['roaring'], + }, test: { include: [ '**/*.{test,spec}.?(c|m)[jt]s?(x)', @@ -9,8 +15,7 @@ export default defineConfig({ testTimeout: 60000, // 60s timeout for benchmark tests server: { deps: { - // Native C++ addons must not be transformed by Vite's pipeline. - external: ['roaring'], + external: [/roaring/], }, }, }, From 4393a6c1d5a6692da6dac2a152a9066f65313a99 Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 3 Mar 2026 22:27:15 -0800 Subject: [PATCH 06/21] fix(test): use createRequire fallback for roaring native module under Bun/Vite Vite 7's module runner intercepts dynamic import() calls and fails to transform native C++ addons (.node binaries). initRoaring() now catches the import failure and falls back to createRequire() which loads native modules directly, bypassing the module runner. --- CHANGELOG.md | 4 ++++ src/domain/utils/roaring.js | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 683f1dc8..018952db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **Roaring native module loading under Vite 7** — `initRoaring()` now catches dynamic `import('roaring')` failures and falls back to `createRequire()` for direct `.node` binary loading. Fixes `test-bun` CI failures caused by Vite 7's module runner intercepting native C++ addon imports. + ### Changed - **Vitest 2.1.9 → 4.0.18** — major test framework upgrade. Migrated deprecated `test(name, fn, { timeout })` signatures to `test(name, { timeout }, fn)` across 7 test files (40 call sites). Fixed `vi.fn().mockImplementation()` constructor mocks to use `function` expressions per Vitest 4 requirements. Resolves 5 remaining moderate-severity npm audit advisories (`esbuild` [GHSA-67mh-4wv8-2f99](https://github.com/advisories/GHSA-67mh-4wv8-2f99), `vite`, `@vitest/mocker`, `vite-node`, `vitest`). **`npm audit` now reports 0 vulnerabilities.** diff --git a/src/domain/utils/roaring.js b/src/domain/utils/roaring.js index 79ab922d..1416beab 100644 --- a/src/domain/utils/roaring.js +++ b/src/domain/utils/roaring.js @@ -112,7 +112,16 @@ export async function initRoaring(mod) { return; } if (!roaringModule) { - roaringModule = /** @type {RoaringModule} */ (await import('roaring')); + try { + roaringModule = /** @type {RoaringModule} */ (await import('roaring')); + } catch { + // Dynamic import() can fail when a module runner (e.g. Vite 7) + // intercepts the call and cannot transform native C++ addons. + // Fall back to CJS require() which loads .node binaries directly. + const { createRequire } = await import('node:module'); + const req = createRequire(import.meta.url); + roaringModule = /** @type {RoaringModule} */ (req('roaring')); + } // Handle both ESM default export and CJS module.exports if (roaringModule.default && roaringModule.default.RoaringBitmap32) { roaringModule = roaringModule.default; From 6c70e0dac02046387adb844fc23bf3b02cd5e91f Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 3 Mar 2026 22:54:31 -0800 Subject: [PATCH 07/21] fix(docker): build roaring native module in Bun container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bun install blocks lifecycle scripts by default, so roaring's node-pre-gyp install never runs, leaving the .node binary missing. Bun also reports a fictional Node ABI (v137) that has no prebuilt binaries available. Fix: install nodejs, python3, and ca-certificates in the Bun Dockerfile, then run node-pre-gyp under real Node.js after bun install. This downloads the correct prebuilt binary (or compiles from source as fallback). Bun loads the binary via roaring's MODULE_NOT_FOUND fallback path (build/Release/roaring.node). The root cause was Docker layer cache invalidation from the Vitest 4 upgrade — previous CI runs reused a cached layer that happened to have the binary from an earlier build. --- CHANGELOG.md | 2 +- docker/Dockerfile.bun | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 018952db..3ef92041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- **Roaring native module loading under Vite 7** — `initRoaring()` now catches dynamic `import('roaring')` failures and falls back to `createRequire()` for direct `.node` binary loading. Fixes `test-bun` CI failures caused by Vite 7's module runner intercepting native C++ addon imports. +- **Roaring native module loading under Bun** — `initRoaring()` now catches dynamic `import('roaring')` failures and falls back to `createRequire()` for direct `.node` binary loading. Bun Dockerfile updated to install `nodejs`, `python3`, and `ca-certificates` so that `node-pre-gyp` can download or compile the roaring native binary (Bun reports a fictional Node ABI v137 with no prebuilt available). Fixes `test-bun` CI failures caused by missing native binary after Vitest 4 upgrade invalidated Docker layer cache. ### Changed diff --git a/docker/Dockerfile.bun b/docker/Dockerfile.bun index bc0d4f90..958e66b6 100644 --- a/docker/Dockerfile.bun +++ b/docker/Dockerfile.bun @@ -3,18 +3,30 @@ # CLI tests are excluded — the CLI uses node: built-ins. # Build context is the parent monorepo directory (context: ..). FROM oven/bun:1.2-slim -# make/g++: native module compilation (roaring bitmaps). -# No bats/python3 — BATS CLI tests are Node-only. +# nodejs: required for roaring's node-pre-gyp install script, which downloads +# prebuilt native binaries. bun install blocks lifecycle scripts by default, and +# Bun reports a fictional Node ABI (v137) that has no prebuilt binaries. Running +# the install script under real Node.js downloads the correct prebuilt binary. +# make/g++: native module compilation fallback (if prebuilt download fails). RUN apt-get update && apt-get install -y --no-install-recommends \ git \ make \ g++ \ + nodejs \ + python3 \ + ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY git-warp/package*.json ./ COPY git-warp/scripts ./scripts COPY git-warp/patches ./patches RUN bun install +# Install roaring native binary using Node.js (correct ABI for prebuilt download). +# Bun reports a fictional Node ABI (v137) with no prebuilt available, so we use +# real Node.js to download or compile the binary. The build places roaring.node at +# both native// and build/Release/ — Bun finds it via the latter path +# when node-pre-gyp's ABI lookup fails (MODULE_NOT_FOUND → fallback). +RUN cd node_modules/roaring && node node-pre-gyp.js install --fallback-to-build COPY git-warp . # Init a git repo so plumbing operations work inside the container. RUN git init -q \ From 3c525b70cd0fc92b08901fea9b1b71cf2b397f2d Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 3 Mar 2026 23:00:37 -0800 Subject: [PATCH 08/21] fix(docker): use multi-stage build for roaring native module The single-stage approach installed nodejs in the Bun container, which caused Vitest 4 to use node instead of bun for running tests, breaking globalThis.crypto (and all 64 integration tests). Switch to a multi-stage build: - Stage 1 (node:18-slim): npm install + node-pre-gyp to download or compile the roaring native binary with the correct Node ABI - Stage 2 (oven/bun:1.2-slim): bun install + COPY the compiled binary from stage 1, keeping the runtime image Node-free --- docker/Dockerfile.bun | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/docker/Dockerfile.bun b/docker/Dockerfile.bun index 958e66b6..f64fbfd2 100644 --- a/docker/Dockerfile.bun +++ b/docker/Dockerfile.bun @@ -2,31 +2,34 @@ # Runs: API integration tests only (vitest via bunx). # CLI tests are excluded — the CLI uses node: built-ins. # Build context is the parent monorepo directory (context: ..). + +# ---------- Stage 1: build roaring native module with Node.js ---------- +# bun install blocks lifecycle scripts by default, and Bun reports a +# fictional Node ABI (v137) that has no prebuilt binaries. We use a +# Node.js stage to download or compile the correct native binary. +FROM node:18-slim AS roaring-builder +RUN apt-get update && apt-get install -y --no-install-recommends \ + make g++ python3 && rm -rf /var/lib/apt/lists/* +WORKDIR /build +COPY git-warp/package*.json ./ +RUN npm install --ignore-scripts +RUN cd node_modules/roaring && npx --yes node-pre-gyp install --fallback-to-build + +# ---------- Stage 2: Bun runtime ---------- FROM oven/bun:1.2-slim -# nodejs: required for roaring's node-pre-gyp install script, which downloads -# prebuilt native binaries. bun install blocks lifecycle scripts by default, and -# Bun reports a fictional Node ABI (v137) that has no prebuilt binaries. Running -# the install script under real Node.js downloads the correct prebuilt binary. -# make/g++: native module compilation fallback (if prebuilt download fails). RUN apt-get update && apt-get install -y --no-install-recommends \ git \ - make \ - g++ \ - nodejs \ - python3 \ - ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY git-warp/package*.json ./ COPY git-warp/scripts ./scripts COPY git-warp/patches ./patches RUN bun install -# Install roaring native binary using Node.js (correct ABI for prebuilt download). -# Bun reports a fictional Node ABI (v137) with no prebuilt available, so we use -# real Node.js to download or compile the binary. The build places roaring.node at -# both native// and build/Release/ — Bun finds it via the latter path -# when node-pre-gyp's ABI lookup fails (MODULE_NOT_FOUND → fallback). -RUN cd node_modules/roaring && node node-pre-gyp.js install --fallback-to-build +# Copy the compiled roaring native binary from the builder stage. +# The binary lands in build/Release/ which is roaring's MODULE_NOT_FOUND +# fallback path — Bun finds it there when node-pre-gyp's ABI lookup fails. +COPY --from=roaring-builder /build/node_modules/roaring/build/ node_modules/roaring/build/ +COPY --from=roaring-builder /build/node_modules/roaring/native/ node_modules/roaring/native/ COPY git-warp . # Init a git repo so plumbing operations work inside the container. RUN git init -q \ From 15bbcd82f29ddd7b21ea2e663cb36d554e82f7ca Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 3 Mar 2026 23:09:09 -0800 Subject: [PATCH 09/21] fix(docker): exclude bitmap tests from Bun suite (V8 API incompatibility) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The roaring npm package uses V8 C++ API (#include ), not Node-API/NAPI. Bun uses JavaScriptCore, not V8, so the native .node binary can never load — 'napi_register_module_v1' symbol not found. Bitmap index tests (materializedView, checkpointIndex.notStale) are excluded from the Bun test suite. This is not a regression — these tests only passed historically due to Docker layer cache luck. The bitmap index system already handles this gracefully via _buildView()'s catch block, degrading to linear scan when roaring is unavailable. Reverts the multi-stage Docker build (unnecessary since the binary can't work regardless of how it's compiled). Simplifies Dockerfile back to git + bun install. --- CHANGELOG.md | 2 +- docker-compose.test.yml | 10 ++++++++-- docker/Dockerfile.bun | 27 +++++++-------------------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ef92041..cd55a0f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- **Roaring native module loading under Bun** — `initRoaring()` now catches dynamic `import('roaring')` failures and falls back to `createRequire()` for direct `.node` binary loading. Bun Dockerfile updated to install `nodejs`, `python3`, and `ca-certificates` so that `node-pre-gyp` can download or compile the roaring native binary (Bun reports a fictional Node ABI v137 with no prebuilt available). Fixes `test-bun` CI failures caused by missing native binary after Vitest 4 upgrade invalidated Docker layer cache. +- **Roaring native module loading under Bun** — `initRoaring()` now catches dynamic `import('roaring')` failures and falls back to `createRequire()` for direct `.node` binary loading. Bitmap index tests (`materializedView`, `materialize.checkpointIndex.notStale`) excluded from Bun test suite: the `roaring` package uses V8 C++ API (not Node-API/NAPI), making it fundamentally incompatible with Bun's JavaScriptCore runtime. Bitmap indexes gracefully degrade to linear scan when roaring is unavailable. ### Changed diff --git a/docker-compose.test.yml b/docker-compose.test.yml index da25a12d..d5afe065 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -32,14 +32,20 @@ services: command: ["sh", "-c", "npx vitest run test/unit test/integration && bats test/bats/"] profiles: [node22, full] - # Bun: API integration tests only (CLI is Node-only) + # Bun: API integration tests only (CLI is Node-only). + # Bitmap index tests are excluded — the roaring package uses V8 C++ API (not + # Node-API/NAPI), which is incompatible with Bun's JavaScriptCore runtime. + # Bitmap indexes gracefully degrade to linear scan when roaring is unavailable. test-bun: build: context: .. dockerfile: git-warp/docker/Dockerfile.bun environment: - GIT_STUNTS_DOCKER=1 - command: ["bunx", "vitest", "run", "test/integration/api/"] + command: >- + bunx vitest run test/integration/api/ + --exclude test/integration/api/materializedView.test.js + --exclude test/integration/api/materialize.checkpointIndex.notStale.test.js profiles: [bun, full] # Deno: API integration tests via Deno.test() wrappers diff --git a/docker/Dockerfile.bun b/docker/Dockerfile.bun index f64fbfd2..c3a37026 100644 --- a/docker/Dockerfile.bun +++ b/docker/Dockerfile.bun @@ -2,20 +2,6 @@ # Runs: API integration tests only (vitest via bunx). # CLI tests are excluded — the CLI uses node: built-ins. # Build context is the parent monorepo directory (context: ..). - -# ---------- Stage 1: build roaring native module with Node.js ---------- -# bun install blocks lifecycle scripts by default, and Bun reports a -# fictional Node ABI (v137) that has no prebuilt binaries. We use a -# Node.js stage to download or compile the correct native binary. -FROM node:18-slim AS roaring-builder -RUN apt-get update && apt-get install -y --no-install-recommends \ - make g++ python3 && rm -rf /var/lib/apt/lists/* -WORKDIR /build -COPY git-warp/package*.json ./ -RUN npm install --ignore-scripts -RUN cd node_modules/roaring && npx --yes node-pre-gyp install --fallback-to-build - -# ---------- Stage 2: Bun runtime ---------- FROM oven/bun:1.2-slim RUN apt-get update && apt-get install -y --no-install-recommends \ git \ @@ -25,11 +11,6 @@ COPY git-warp/package*.json ./ COPY git-warp/scripts ./scripts COPY git-warp/patches ./patches RUN bun install -# Copy the compiled roaring native binary from the builder stage. -# The binary lands in build/Release/ which is roaring's MODULE_NOT_FOUND -# fallback path — Bun finds it there when node-pre-gyp's ABI lookup fails. -COPY --from=roaring-builder /build/node_modules/roaring/build/ node_modules/roaring/build/ -COPY --from=roaring-builder /build/node_modules/roaring/native/ node_modules/roaring/native/ COPY git-warp . # Init a git repo so plumbing operations work inside the container. RUN git init -q \ @@ -41,4 +22,10 @@ RUN git init -q \ RUN useradd -m warp && chown -R warp:warp /app USER warp ENV GIT_STUNTS_DOCKER=1 -CMD ["bunx", "vitest", "run", "test/integration/api/"] +# Bitmap index tests (materializedView, checkpointIndex.notStale) are excluded: +# the roaring package uses V8 C++ API (not Node-API/NAPI), which is incompatible +# with Bun's JavaScriptCore runtime. Bitmap indexes gracefully degrade to linear +# scan when roaring is unavailable — see _buildView() catch block. +CMD ["bunx", "vitest", "run", "test/integration/api/", \ + "--exclude", "**/materializedView.test.js", \ + "--exclude", "**/materialize.checkpointIndex.notStale.test.js"] From 4b425ba11be2b2f9444010675f636f2237784661 Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 3 Mar 2026 23:15:05 -0800 Subject: [PATCH 10/21] fix: preserve both load failures in roaring fallback (CodeRabbit) When both import('roaring') and createRequire('roaring') fail, throw an AggregateError containing both root causes instead of silently dropping the original import error. Also update vitest.config.js comment to be accurate (roaring uses V8 C++ API, not NAPI). --- src/domain/utils/roaring.js | 15 +++++++++++---- vitest.config.js | 5 ++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/domain/utils/roaring.js b/src/domain/utils/roaring.js index 1416beab..de0df7c9 100644 --- a/src/domain/utils/roaring.js +++ b/src/domain/utils/roaring.js @@ -114,13 +114,20 @@ export async function initRoaring(mod) { if (!roaringModule) { try { roaringModule = /** @type {RoaringModule} */ (await import('roaring')); - } catch { + } catch (importErr) { // Dynamic import() can fail when a module runner (e.g. Vite 7) // intercepts the call and cannot transform native C++ addons. // Fall back to CJS require() which loads .node binaries directly. - const { createRequire } = await import('node:module'); - const req = createRequire(import.meta.url); - roaringModule = /** @type {RoaringModule} */ (req('roaring')); + try { + const { createRequire } = await import('node:module'); + const req = createRequire(import.meta.url); + roaringModule = /** @type {RoaringModule} */ (req('roaring')); + } catch (requireErr) { + throw new AggregateError( + [importErr, requireErr], + 'Failed to load roaring via both import() and require()', + ); + } } // Handle both ESM default export and CJS module.exports if (roaringModule.default && roaringModule.default.RoaringBitmap32) { diff --git a/vitest.config.js b/vitest.config.js index f9f25752..fd714967 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -1,9 +1,8 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ - // Externalize native C++ addons from Vite's SSR transform pipeline. - // Without this, Vite 7 (vitest 4) intercepts dynamic import('roaring') - // and fails to load the .node binary, breaking Bun integration tests. + // Externalize the roaring native module from Vite's transform pipeline. + // roaring contains a .node C++ addon that Vite cannot bundle/transform. ssr: { external: ['roaring'], }, From 0a1139ccc7a8e0b8473895b5984635650b423618 Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 3 Mar 2026 23:39:58 -0800 Subject: [PATCH 11/21] feat: add roaring-wasm WASM fallback for Bun/Deno bitmap indexes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit initRoaring() now has a three-tier fallback chain: Tier 1: import('roaring') — ESM native V8 bindings Tier 2: createRequire('roaring') — CJS native (Vite workaround) Tier 3: import('roaring-wasm') — WASM portable fallback The WASM tier activates automatically when native V8 C++ bindings are unavailable (Bun's JavaScriptCore, Deno). Serialization formats are wire-compatible — portable bitmaps from native and WASM are byte-identical. Bitmap index tests (materializedView, checkpointIndex.notStale) are no longer excluded from the Bun test suite. Refactored initRoaring() into per-tier helper functions (tryNativeImport, tryCjsRequire, tryWasmFallback, unwrapDefault) to satisfy ESLint complexity/depth/line-count limits. --- CHANGELOG.md | 6 +- docker-compose.test.yml | 10 +--- docker/Dockerfile.bun | 11 ++-- package-lock.json | 10 ++++ package.json | 1 + src/domain/utils/roaring.js | 113 +++++++++++++++++++++++++++++------- vitest.config.js | 2 +- 7 files changed, 116 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd55a0f6..d5bd6f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **`roaring-wasm` WASM fallback for Bun/Deno bitmap indexes** — `initRoaring()` now has a three-tier fallback chain: (1) ESM `import('roaring')`, (2) CJS `createRequire('roaring')`, (3) `import('roaring-wasm')` with WASM initialization. The WASM tier activates automatically when native V8 bindings are unavailable (Bun's JSC, Deno). Bitmap index tests (`materializedView`, `materialize.checkpointIndex.notStale`) are no longer excluded from the Bun test suite. Serialization formats are wire-compatible — portable bitmaps produced by native and WASM are byte-identical. + ### Fixed -- **Roaring native module loading under Bun** — `initRoaring()` now catches dynamic `import('roaring')` failures and falls back to `createRequire()` for direct `.node` binary loading. Bitmap index tests (`materializedView`, `materialize.checkpointIndex.notStale`) excluded from Bun test suite: the `roaring` package uses V8 C++ API (not Node-API/NAPI), making it fundamentally incompatible with Bun's JavaScriptCore runtime. Bitmap indexes gracefully degrade to linear scan when roaring is unavailable. +- **Roaring native module loading under Bun** — `initRoaring()` now catches dynamic `import('roaring')` failures and falls back to `createRequire()` for direct `.node` binary loading. ### Changed diff --git a/docker-compose.test.yml b/docker-compose.test.yml index d5afe065..6ec20fb8 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -33,19 +33,15 @@ services: profiles: [node22, full] # Bun: API integration tests only (CLI is Node-only). - # Bitmap index tests are excluded — the roaring package uses V8 C++ API (not - # Node-API/NAPI), which is incompatible with Bun's JavaScriptCore runtime. - # Bitmap indexes gracefully degrade to linear scan when roaring is unavailable. + # Bitmap index tests pass via roaring-wasm WASM fallback (native roaring uses + # V8 C++ API, incompatible with Bun's JSC runtime). test-bun: build: context: .. dockerfile: git-warp/docker/Dockerfile.bun environment: - GIT_STUNTS_DOCKER=1 - command: >- - bunx vitest run test/integration/api/ - --exclude test/integration/api/materializedView.test.js - --exclude test/integration/api/materialize.checkpointIndex.notStale.test.js + command: bunx vitest run test/integration/api/ profiles: [bun, full] # Deno: API integration tests via Deno.test() wrappers diff --git a/docker/Dockerfile.bun b/docker/Dockerfile.bun index c3a37026..ae43a541 100644 --- a/docker/Dockerfile.bun +++ b/docker/Dockerfile.bun @@ -22,10 +22,7 @@ RUN git init -q \ RUN useradd -m warp && chown -R warp:warp /app USER warp ENV GIT_STUNTS_DOCKER=1 -# Bitmap index tests (materializedView, checkpointIndex.notStale) are excluded: -# the roaring package uses V8 C++ API (not Node-API/NAPI), which is incompatible -# with Bun's JavaScriptCore runtime. Bitmap indexes gracefully degrade to linear -# scan when roaring is unavailable — see _buildView() catch block. -CMD ["bunx", "vitest", "run", "test/integration/api/", \ - "--exclude", "**/materializedView.test.js", \ - "--exclude", "**/materialize.checkpointIndex.notStale.test.js"] +# Bitmap index tests now pass under Bun via the roaring-wasm WASM fallback. +# The native roaring package (V8 C++ API) is incompatible with Bun's JSC runtime, +# but initRoaring() falls through to roaring-wasm automatically. +CMD ["bunx", "vitest", "run", "test/integration/api/"] diff --git a/package-lock.json b/package-lock.json index 591fa395..8d83a46e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "elkjs": "^0.11.0", "figures": "^6.0.1", "roaring": "^2.7.0", + "roaring-wasm": "^1.1.0", "string-width": "^7.1.0", "wrap-ansi": "^9.0.0", "zod": "3.24.1" @@ -4203,6 +4204,15 @@ } } }, + "node_modules/roaring-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/roaring-wasm/-/roaring-wasm-1.1.0.tgz", + "integrity": "sha512-mhNqA0BOqIW7k4ZYSYe3kCyvn5T3VWT+2661G7fZH0C6XcVkGoTDLAqne7b47xCNQE6LhuYviMKBnzbOiBXkdw==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", diff --git a/package.json b/package.json index 88c9445c..ae899dcb 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "elkjs": "^0.11.0", "figures": "^6.0.1", "roaring": "^2.7.0", + "roaring-wasm": "^1.1.0", "string-width": "^7.1.0", "wrap-ansi": "^9.0.0", "zod": "3.24.1" diff --git a/src/domain/utils/roaring.js b/src/domain/utils/roaring.js index de0df7c9..aa59326c 100644 --- a/src/domain/utils/roaring.js +++ b/src/domain/utils/roaring.js @@ -97,11 +97,92 @@ function loadRoaring() { return roaringModule; } +/** + * Adapts the `roaring-wasm` module to match the `roaring` native API surface. + * + * The WASM module is already largely compatible (serialize/deserialize accept + * booleans), but it lacks `isNativelyInstalled` which `getNativeRoaringAvailable()` + * probes. This shim adds it so downstream code works without branching. + * + * @param {Record} wasmMod - The loaded `roaring-wasm` module + * @returns {RoaringModule} Adapted module matching native `roaring` shape + * @private + */ +function adaptWasmApi(wasmMod) { + wasmMod.RoaringBitmap32.isNativelyInstalled = () => false; + return /** @type {RoaringModule} */ (wasmMod); +} + +/** + * Tier 1: ESM dynamic import of native roaring. + * @returns {Promise} + * @private + */ +async function tryNativeImport() { + try { + return /** @type {RoaringModule} */ (await import('roaring')); + } catch { + return null; + } +} + +/** + * Tier 2: CJS require() — works when Vite intercepts import() but can't + * transform native C++ addons. + * @returns {Promise} + * @private + */ +async function tryCjsRequire() { + try { + const { createRequire } = await import('node:module'); + const req = createRequire(import.meta.url); + return /** @type {RoaringModule} */ (req('roaring')); + } catch { + return null; + } +} + +/** + * Tier 3: WASM fallback — works on Bun (JSC) and Deno without native bindings. + * @returns {Promise} + * @private + */ +async function tryWasmFallback() { + try { + const wasmMod = await import('roaring-wasm'); + if (typeof wasmMod.roaringLibraryInitialize === 'function') { + await wasmMod.roaringLibraryInitialize(); + } + return adaptWasmApi(wasmMod); + } catch { + return null; + } +} + +/** + * Unwraps ESM default-export wrappers on a loaded roaring module. + * Handles both ESM `{ default: { RoaringBitmap32 } }` and CJS `module.exports`. + * @param {RoaringModule} mod + * @returns {RoaringModule} + * @private + */ +function unwrapDefault(mod) { + if (mod.default && mod.default.RoaringBitmap32) { + return /** @type {RoaringModule} */ (mod.default); + } + return mod; +} + /** * Initializes the roaring module. Must be called before getRoaringBitmap32(). * This is called automatically via top-level await when the module is imported, * but can also be called manually with a pre-loaded module for testing. * + * Fallback chain: + * Tier 1: await import('roaring') — ESM native V8 bindings + * Tier 2: createRequire('roaring') — CJS native (Vite workaround) + * Tier 3: await import('roaring-wasm') — WASM portable fallback + * * @param {RoaringModule} [mod] - Pre-loaded roaring module (for testing/DI) * @returns {Promise} */ @@ -111,29 +192,19 @@ export async function initRoaring(mod) { initError = null; return; } + if (roaringModule) { + return; + } + roaringModule = + (await tryNativeImport()) ?? + (await tryCjsRequire()) ?? + (await tryWasmFallback()); if (!roaringModule) { - try { - roaringModule = /** @type {RoaringModule} */ (await import('roaring')); - } catch (importErr) { - // Dynamic import() can fail when a module runner (e.g. Vite 7) - // intercepts the call and cannot transform native C++ addons. - // Fall back to CJS require() which loads .node binaries directly. - try { - const { createRequire } = await import('node:module'); - const req = createRequire(import.meta.url); - roaringModule = /** @type {RoaringModule} */ (req('roaring')); - } catch (requireErr) { - throw new AggregateError( - [importErr, requireErr], - 'Failed to load roaring via both import() and require()', - ); - } - } - // Handle both ESM default export and CJS module.exports - if (roaringModule.default && roaringModule.default.RoaringBitmap32) { - roaringModule = roaringModule.default; - } + throw new Error( + 'Failed to load roaring via import(), require(), and roaring-wasm', + ); } + roaringModule = unwrapDefault(roaringModule); } // Auto-initialize on module load (top-level await) diff --git a/vitest.config.js b/vitest.config.js index fd714967..9fb58f65 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -4,7 +4,7 @@ export default defineConfig({ // Externalize the roaring native module from Vite's transform pipeline. // roaring contains a .node C++ addon that Vite cannot bundle/transform. ssr: { - external: ['roaring'], + external: ['roaring', 'roaring-wasm'], }, test: { include: [ From 60039ba37e038104ea6aa13c11eef5efbd90983e Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 3 Mar 2026 23:43:15 -0800 Subject: [PATCH 12/21] docs: add roaring-wasm to dependency tables and What's New - README.md: add roaring-wasm to dependency table, add WASM fallback bullet to What's New in v13.0.1 - CLAUDE.md: add roaring-wasm to dependency table --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 54747593..26b75381 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ ## What's New in v13.0.1 +- **`roaring-wasm` WASM fallback for Bun/Deno bitmap indexes** — bitmap indexes now work on Bun (JSC) and Deno via a three-tier fallback: native V8 bindings → CJS require → WASM. Wire-compatible, byte-identical serialization. - **Dev dependency security updates** — resolved 4 high-severity advisories (`tar`, `rollup`, `minimatch`, `@isaacs/brace-expansion`). No runtime dependencies affected. See the [full changelog](CHANGELOG.md) for details. @@ -642,6 +643,7 @@ The codebase follows hexagonal architecture with ports and adapters: | `@git-stunts/trailer-codec` | Git trailer encoding | | `cbor-x` | CBOR binary serialization | | `roaring` | Roaring bitmap indexes (native C++ bindings) | +| `roaring-wasm` | Roaring bitmap WASM fallback (Bun/Deno) | | `zod` | Schema validation | ## Testing From 5bdfe64bba98acbe0222747de05f0a45d5fa9857 Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 3 Mar 2026 23:44:18 -0800 Subject: [PATCH 13/21] fix: use RoaringModule type for adaptWasmApi param (tsc strict) --- src/domain/utils/roaring.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/domain/utils/roaring.js b/src/domain/utils/roaring.js index aa59326c..0fb43e43 100644 --- a/src/domain/utils/roaring.js +++ b/src/domain/utils/roaring.js @@ -104,13 +104,13 @@ function loadRoaring() { * booleans), but it lacks `isNativelyInstalled` which `getNativeRoaringAvailable()` * probes. This shim adds it so downstream code works without branching. * - * @param {Record} wasmMod - The loaded `roaring-wasm` module - * @returns {RoaringModule} Adapted module matching native `roaring` shape + * @param {RoaringModule} wasmMod - The loaded `roaring-wasm` module + * @returns {RoaringModule} Adapted module with `isNativelyInstalled` shim * @private */ function adaptWasmApi(wasmMod) { wasmMod.RoaringBitmap32.isNativelyInstalled = () => false; - return /** @type {RoaringModule} */ (wasmMod); + return wasmMod; } /** @@ -153,7 +153,7 @@ async function tryWasmFallback() { if (typeof wasmMod.roaringLibraryInitialize === 'function') { await wasmMod.roaringLibraryInitialize(); } - return adaptWasmApi(wasmMod); + return adaptWasmApi(/** @type {RoaringModule} */ (wasmMod)); } catch { return null; } From a0fd67f65262c66dde126246ca3f8ac484629f24 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 4 Mar 2026 00:06:42 -0800 Subject: [PATCH 14/21] feat: add levels, transitiveReduction, transitiveClosure, rootAncestors to GraphTraversal Five new graph algorithms in GraphTraversal, all backed by NeighborProviderPort for memory-efficient traversal: - levels(): longest-path level assignment for DAGs (O(V+E)) - transitiveReduction(): minimal edge set preserving reachability (DAGs) - transitiveClosure(): all implied reachability edges with maxEdges safety - rootAncestors(): find all in-degree-0 ancestors via backward BFS - BFS reverse reachability verification (direction: 'in') Includes LogicalTraversal facade methods, index.d.ts types, cross-provider equivalence tests, 4 new fixtures (F15-F18), and dedicated test files (47 new test cases). --- CHANGELOG.md | 3 + README.md | 1 + index.d.ts | 21 ++ src/domain/services/GraphTraversal.js | 341 ++++++++++++++++++ src/domain/services/LogicalTraversal.js | 118 ++++++ test/helpers/fixtureDsl.js | 84 +++++ .../services/GraphTraversal.bfs.test.js | 30 ++ .../GraphTraversal.crossProvider.test.js | 48 +++ .../services/GraphTraversal.levels.test.js | 114 ++++++ .../GraphTraversal.rootAncestors.test.js | 124 +++++++ .../GraphTraversal.transitiveClosure.test.js | 145 ++++++++ ...GraphTraversal.transitiveReduction.test.js | 153 ++++++++ 12 files changed, 1182 insertions(+) create mode 100644 test/unit/domain/services/GraphTraversal.levels.test.js create mode 100644 test/unit/domain/services/GraphTraversal.rootAncestors.test.js create mode 100644 test/unit/domain/services/GraphTraversal.transitiveClosure.test.js create mode 100644 test/unit/domain/services/GraphTraversal.transitiveReduction.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d5bd6f57..bd22dce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **5 new graph algorithms in `GraphTraversal`** — `levels()` (longest-path level assignment for DAGs), `transitiveReduction()` (minimal edge set preserving reachability), `transitiveClosure()` (all implied reachability edges with `maxEdges` safety), `rootAncestors()` (find all in-degree-0 ancestors via backward BFS). All methods respect `NeighborProviderPort` abstraction, support `AbortSignal` cancellation, and produce deterministic output. Corresponding `LogicalTraversal` facade methods added. New error code: `E_MAX_EDGES_EXCEEDED`. +- **4 new test fixtures** — `F15_WIDE_DAG_FOR_LEVELS`, `F16_TRANSITIVE_REDUCTION`, `F17_MULTI_ROOT_DAG`, `F18_TRANSITIVE_CLOSURE_CHAIN` in the canonical fixture DSL. +- **BFS reverse reachability verification tests** — confirms `bfs(node, { direction: 'in' })` correctly discovers all backward-reachable ancestors. - **`roaring-wasm` WASM fallback for Bun/Deno bitmap indexes** — `initRoaring()` now has a three-tier fallback chain: (1) ESM `import('roaring')`, (2) CJS `createRequire('roaring')`, (3) `import('roaring-wasm')` with WASM initialization. The WASM tier activates automatically when native V8 bindings are unavailable (Bun's JSC, Deno). Bitmap index tests (`materializedView`, `materialize.checkpointIndex.notStale`) are no longer excluded from the Bun test suite. Serialization formats are wire-compatible — portable bitmaps produced by native and WASM are byte-identical. ### Fixed diff --git a/README.md b/README.md index 26b75381..3c1cedda 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ ## What's New in v13.0.1 +- **5 new graph algorithms** — `levels()`, `transitiveReduction()`, `transitiveClosure()`, `rootAncestors()` in `GraphTraversal`, plus BFS reverse reachability verification. All use `NeighborProviderPort` and support cancellation. - **`roaring-wasm` WASM fallback for Bun/Deno bitmap indexes** — bitmap indexes now work on Bun (JSC) and Deno via a three-tier fallback: native V8 bindings → CJS require → WASM. Wire-compatible, byte-identical serialization. - **Dev dependency security updates** — resolved 4 high-severity advisories (`tar`, `rollup`, `minimatch`, `@isaacs/brace-expansion`). No runtime dependencies affected. diff --git a/index.d.ts b/index.d.ts index b2f630cd..8ee27844 100644 --- a/index.d.ts +++ b/index.d.ts @@ -295,6 +295,27 @@ export interface LogicalTraversal { labelFilter?: string | string[]; signal?: AbortSignal; }): Promise<{ path: string[]; totalCost: number }>; + levels(start: string | string[], options?: { + dir?: 'out' | 'in' | 'both'; + labelFilter?: string | string[]; + signal?: AbortSignal; + }): Promise<{ levels: Map; maxLevel: number }>; + transitiveReduction(start: string | string[], options?: { + dir?: 'out' | 'in' | 'both'; + labelFilter?: string | string[]; + signal?: AbortSignal; + }): Promise<{ edges: Array<{ from: string; to: string; label: string }>; removed: number }>; + transitiveClosure(start: string | string[], options?: { + dir?: 'out' | 'in' | 'both'; + labelFilter?: string | string[]; + maxEdges?: number; + signal?: AbortSignal; + }): Promise<{ edges: Array<{ from: string; to: string }> }>; + rootAncestors(start: string, options?: { + labelFilter?: string | string[]; + maxDepth?: number; + signal?: AbortSignal; + }): Promise<{ roots: string[] }>; } /** diff --git a/src/domain/services/GraphTraversal.js b/src/domain/services/GraphTraversal.js index 0763e3ff..6b7eb9cf 100644 --- a/src/domain/services/GraphTraversal.js +++ b/src/domain/services/GraphTraversal.js @@ -955,6 +955,347 @@ export default class GraphTraversal { return { path, totalCost: /** @type {number} */ (dist.get(goal)), stats: this._stats(sorted.length, rs) }; } + // ==== Section 5: Graph Analysis (levels, rootAncestors, transitiveReduction, transitiveClosure) ==== + + /** + * Longest-path level assignment (DAGs only). + * + * Each node's level is its longest-path distance from any root. + * Roots (in-degree 0 within the reachable subgraph) get level 0. + * + * @param {{ start: string | string[], direction?: Direction, options?: NeighborOptions, maxNodes?: number, signal?: AbortSignal }} params + * @returns {Promise<{levels: Map, maxLevel: number, stats: TraversalStats}>} + * @throws {TraversalError} code 'ERR_GRAPH_HAS_CYCLES' if graph has cycles + * @throws {TraversalError} code 'INVALID_START' if start node missing + */ + async levels({ + start, direction = 'out', options, + maxNodes = DEFAULT_MAX_NODES, + signal, + }) { + // Topo sort with cycle detection + neighbor edge map reuse + const { sorted, _neighborEdgeMap } = await this.topologicalSort({ + start, + direction, + options, + maxNodes, + throwOnCycle: true, + signal, + _returnAdjList: true, + }); + + const rs = this._newRunStats(); + + // DP forward: level[v] = max(level[v], level[u] + 1) + /** @type {Map} */ + const levelMap = new Map(); + for (const nodeId of sorted) { + if (!levelMap.has(nodeId)) { + levelMap.set(nodeId, 0); + } + } + + let maxLevel = 0; + for (const nodeId of sorted) { + checkAborted(signal, 'levels'); + const currentLevel = /** @type {number} */ (levelMap.get(nodeId)); + const neighbors = _neighborEdgeMap + ? (_neighborEdgeMap.get(nodeId) || []) + : await this._getNeighbors(nodeId, direction, rs, options); + rs.edgesTraversed += neighbors.length; + + for (const { neighborId } of neighbors) { + const neighborLevel = levelMap.get(neighborId) ?? 0; + const candidate = currentLevel + 1; + if (candidate > neighborLevel) { + levelMap.set(neighborId, candidate); + if (candidate > maxLevel) { + maxLevel = candidate; + } + } + } + } + + return { levels: levelMap, maxLevel, stats: this._stats(sorted.length, rs) }; + } + + /** + * Find all root ancestors (in-degree-0 nodes) reachable backward from start. + * + * Works on cyclic graphs — uses BFS reachability. + * + * @param {{ start: string, options?: NeighborOptions, maxNodes?: number, maxDepth?: number, signal?: AbortSignal }} params + * @returns {Promise<{roots: string[], stats: TraversalStats}>} + * @throws {TraversalError} code 'INVALID_START' if start node missing + */ + async rootAncestors({ + start, options, + maxNodes = DEFAULT_MAX_NODES, + maxDepth = DEFAULT_MAX_DEPTH, + signal, + }) { + // BFS backward from start + const { nodes: visited, stats: bfsStats } = await this.bfs({ + start, + direction: 'in', + options, + maxNodes, + maxDepth, + signal, + }); + + const rs = this._newRunStats(); + + // Check each visited node: if it has no incoming neighbors, it's a root + /** @type {string[]} */ + const roots = []; + for (const nodeId of visited) { + checkAborted(signal, 'rootAncestors'); + const inNeighbors = await this._getNeighbors(nodeId, 'in', rs, options); + rs.edgesTraversed += inNeighbors.length; + if (inNeighbors.length === 0) { + roots.push(nodeId); + } + } + + // Sort lexicographically for determinism + roots.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + + return { + roots, + stats: { + nodesVisited: bfsStats.nodesVisited, + edgesTraversed: bfsStats.edgesTraversed + rs.edgesTraversed, + cacheHits: bfsStats.cacheHits + rs.cacheHits, + cacheMisses: bfsStats.cacheMisses + rs.cacheMisses, + }, + }; + } + + /** + * Transitive reduction — minimal edge set preserving reachability (DAGs only). + * + * For each node u with direct successors, BFS from u's grandchildren + * to find which direct successors are also reachable via longer paths. + * Those direct edges are redundant and removed. + * + * @param {{ start: string | string[], direction?: Direction, options?: NeighborOptions, maxNodes?: number, signal?: AbortSignal }} params + * @returns {Promise<{edges: Array<{from: string, to: string, label: string}>, removed: number, stats: TraversalStats}>} + * @throws {TraversalError} code 'ERR_GRAPH_HAS_CYCLES' if graph has cycles + * @throws {TraversalError} code 'INVALID_START' if start node missing + */ + async transitiveReduction({ + start, direction = 'out', options, + maxNodes = DEFAULT_MAX_NODES, + signal, + }) { + // Topo sort with cycle detection + neighbor edge map reuse + const { sorted, _neighborEdgeMap } = await this.topologicalSort({ + start, + direction, + options, + maxNodes, + throwOnCycle: true, + signal, + _returnAdjList: true, + }); + + const rs = this._newRunStats(); + /** @type {Map} */ + const adjList = new Map(); + + // Build adjacency list from topo sort data + for (const nodeId of sorted) { + const neighbors = _neighborEdgeMap + ? (_neighborEdgeMap.get(nodeId) || []) + : await this._getNeighbors(nodeId, direction, rs, options); + adjList.set(nodeId, neighbors.map((n) => n.neighborId)); + } + + // For each node, find redundant edges via DFS/BFS from grandchildren + /** @type {Set} — keys are "from\0to" */ + const redundant = new Set(); + + for (const u of sorted) { + checkAborted(signal, 'transitiveReduction'); + const directSuccessors = adjList.get(u) || []; + if (directSuccessors.length <= 1) { + continue; // Cannot have redundant edges with 0 or 1 successor + } + + const directSet = new Set(directSuccessors); + + // BFS from all grandchildren (successors-of-successors) + // Any direct successor reachable from a grandchild is redundant + /** @type {Set} */ + const visited = new Set(); + /** @type {string[]} */ + let frontier = []; + + for (const s of directSuccessors) { + const sSuccessors = adjList.get(s) || []; + for (const gc of sSuccessors) { + if (!visited.has(gc)) { + visited.add(gc); + frontier.push(gc); + } + } + } + + // BFS forward from grandchildren + while (frontier.length > 0) { + /** @type {string[]} */ + const nextFrontier = []; + for (const nodeId of frontier) { + if (directSet.has(nodeId)) { + redundant.add(`${u}\0${nodeId}`); + } + const successors = adjList.get(nodeId) || []; + for (const s of successors) { + if (!visited.has(s)) { + visited.add(s); + nextFrontier.push(s); + } + } + } + frontier = nextFrontier; + } + } + + // Collect non-redundant edges with labels from the original neighbor data + /** @type {Array<{from: string, to: string, label: string}>} */ + const edges = []; + let removed = 0; + + for (const nodeId of sorted) { + const neighbors = _neighborEdgeMap + ? (_neighborEdgeMap.get(nodeId) || []) + : []; + for (const { neighborId, label } of neighbors) { + if (redundant.has(`${nodeId}\0${neighborId}`)) { + removed++; + } else { + edges.push({ from: nodeId, to: neighborId, label }); + } + } + } + + // Sort edges for determinism + edges.sort((a, b) => { + if (a.from < b.from) { return -1; } + if (a.from > b.from) { return 1; } + if (a.to < b.to) { return -1; } + if (a.to > b.to) { return 1; } + if (a.label < b.label) { return -1; } + if (a.label > b.label) { return 1; } + return 0; + }); + + return { edges, removed, stats: this._stats(sorted.length, rs) }; + } + + /** + * Transitive closure — all implied reachability edges. + * + * For each node, BFS to find all reachable nodes and emit an edge + * for each pair. Works on cyclic graphs. + * + * @param {{ start: string | string[], direction?: Direction, options?: NeighborOptions, maxNodes?: number, maxEdges?: number, signal?: AbortSignal }} params + * @returns {Promise<{edges: Array<{from: string, to: string}>, stats: TraversalStats}>} + * @throws {TraversalError} code 'INVALID_START' if start node missing + * @throws {TraversalError} code 'E_MAX_EDGES_EXCEEDED' if closure exceeds maxEdges + */ + async transitiveClosure({ + start, direction = 'out', options, + maxNodes = DEFAULT_MAX_NODES, + maxEdges = 1000000, + signal, + }) { + const rs = this._newRunStats(); + const starts = [...new Set(Array.isArray(start) ? start : [start])]; + for (const s of starts) { + await this._validateStart(s); + } + + // Phase 1: Discover all reachable nodes via BFS from all starts + const allVisited = new Set(); + /** @type {string[]} */ + const queue = [...starts]; + let qHead = 0; + for (const s of starts) { + allVisited.add(s); + } + + while (qHead < queue.length) { + if (allVisited.size % 1000 === 0) { + checkAborted(signal, 'transitiveClosure'); + } + if (allVisited.size >= maxNodes) { + break; + } + const nodeId = /** @type {string} */ (queue[qHead++]); + const neighbors = await this._getNeighbors(nodeId, direction, rs, options); + rs.edgesTraversed += neighbors.length; + for (const { neighborId } of neighbors) { + if (!allVisited.has(neighborId)) { + allVisited.add(neighborId); + queue.push(neighborId); + } + } + } + + // Phase 2: For each node, BFS to collect all reachable nodes + /** @type {Array<{from: string, to: string}>} */ + const edges = []; + let edgeCount = 0; + + const nodeList = [...allVisited].sort(); + + for (const fromNode of nodeList) { + checkAborted(signal, 'transitiveClosure'); + + // BFS from fromNode + const visited = new Set([fromNode]); + /** @type {string[]} */ + let frontier = [fromNode]; + + while (frontier.length > 0) { + /** @type {string[]} */ + const nextFrontier = []; + for (const nodeId of frontier) { + const neighbors = await this._getNeighbors(nodeId, direction, rs, options); + rs.edgesTraversed += neighbors.length; + for (const { neighborId } of neighbors) { + if (!visited.has(neighborId)) { + visited.add(neighborId); + nextFrontier.push(neighborId); + edgeCount++; + if (edgeCount > maxEdges) { + throw new TraversalError( + `Transitive closure exceeds maxEdges limit (${maxEdges})`, + { code: 'E_MAX_EDGES_EXCEEDED', context: { maxEdges, edgesSoFar: edgeCount } }, + ); + } + edges.push({ from: fromNode, to: neighborId }); + } + } + } + frontier = nextFrontier; + } + } + + // Sort edges for determinism + edges.sort((a, b) => { + if (a.from < b.from) { return -1; } + if (a.from > b.from) { return 1; } + if (a.to < b.to) { return -1; } + if (a.to > b.to) { return 1; } + return 0; + }); + + return { edges, stats: this._stats(allVisited.size, rs) }; + } + // ==== Private Helpers ==== /** diff --git a/src/domain/services/LogicalTraversal.js b/src/domain/services/LogicalTraversal.js index ef1dfba2..6ec57255 100644 --- a/src/domain/services/LogicalTraversal.js +++ b/src/domain/services/LogicalTraversal.js @@ -417,4 +417,122 @@ export default class LogicalTraversal { }); return { path, totalCost }; } + + /** + * Longest-path level assignment (DAGs only). + * + * @param {string|string[]} start - One or more start nodes + * @param {{ dir?: 'out'|'in'|'both', labelFilter?: string|string[], signal?: AbortSignal }} [options] - Traversal options + * @returns {Promise<{levels: Map, maxLevel: number}>} + * @throws {TraversalError} code 'ERR_GRAPH_HAS_CYCLES' if graph has cycles + * @throws {TraversalError} code 'NODE_NOT_FOUND' if a start node does not exist + */ + async levels(start, options = {}) { + const { engine, direction, options: opts } = await this._prepareEngine(options); + + const starts = Array.isArray(start) ? start : [start]; + for (const s of starts) { + if (!(await this._graph.hasNode(s))) { + throw new TraversalError(`Start node not found: ${s}`, { + code: 'NODE_NOT_FOUND', + context: { start: s }, + }); + } + } + + const { levels, maxLevel } = await engine.levels({ + start, + direction, + options: opts, + maxNodes: Infinity, + signal: options.signal, + }); + return { levels, maxLevel }; + } + + /** + * Transitive reduction — minimal edge set preserving reachability (DAGs only). + * + * @param {string|string[]} start - One or more start nodes + * @param {{ dir?: 'out'|'in'|'both', labelFilter?: string|string[], signal?: AbortSignal }} [options] - Traversal options + * @returns {Promise<{edges: Array<{from: string, to: string, label: string}>, removed: number}>} + * @throws {TraversalError} code 'ERR_GRAPH_HAS_CYCLES' if graph has cycles + * @throws {TraversalError} code 'NODE_NOT_FOUND' if a start node does not exist + */ + async transitiveReduction(start, options = {}) { + const { engine, direction, options: opts } = await this._prepareEngine(options); + + const starts = Array.isArray(start) ? start : [start]; + for (const s of starts) { + if (!(await this._graph.hasNode(s))) { + throw new TraversalError(`Start node not found: ${s}`, { + code: 'NODE_NOT_FOUND', + context: { start: s }, + }); + } + } + + const { edges, removed } = await engine.transitiveReduction({ + start, + direction, + options: opts, + maxNodes: Infinity, + signal: options.signal, + }); + return { edges, removed }; + } + + /** + * Transitive closure — all implied reachability edges. + * + * @param {string|string[]} start - One or more start nodes + * @param {{ dir?: 'out'|'in'|'both', labelFilter?: string|string[], maxEdges?: number, signal?: AbortSignal }} [options] - Traversal options + * @returns {Promise<{edges: Array<{from: string, to: string}>}>} + * @throws {TraversalError} code 'E_MAX_EDGES_EXCEEDED' if closure exceeds maxEdges + * @throws {TraversalError} code 'NODE_NOT_FOUND' if a start node does not exist + */ + async transitiveClosure(start, options = {}) { + const { engine, direction, options: opts } = await this._prepareEngine(options); + + const starts = Array.isArray(start) ? start : [start]; + for (const s of starts) { + if (!(await this._graph.hasNode(s))) { + throw new TraversalError(`Start node not found: ${s}`, { + code: 'NODE_NOT_FOUND', + context: { start: s }, + }); + } + } + + const { edges } = await engine.transitiveClosure({ + start, + direction, + options: opts, + maxNodes: Infinity, + maxEdges: options.maxEdges, + signal: options.signal, + }); + return { edges }; + } + + /** + * Find all root ancestors (in-degree-0 nodes) reachable backward from start. + * + * @param {string} start - Starting node ID + * @param {{ labelFilter?: string|string[], maxDepth?: number, signal?: AbortSignal }} [options] - Traversal options + * @returns {Promise<{roots: string[]}>} + * @throws {TraversalError} code 'NODE_NOT_FOUND' if start node does not exist + */ + async rootAncestors(start, options = {}) { + const { engine, options: opts, depthLimit } = await this._prepare(start, options); + + const { roots } = await engine.rootAncestors({ + start, + options: opts, + maxNodes: Infinity, + maxDepth: options.maxDepth ?? depthLimit, + signal: options.signal, + }); + return { roots }; + } } diff --git a/test/helpers/fixtureDsl.js b/test/helpers/fixtureDsl.js index 1bf56440..f172b558 100644 --- a/test/helpers/fixtureDsl.js +++ b/test/helpers/fixtureDsl.js @@ -458,6 +458,90 @@ export const F14_NODE_WEIGHTS = new Map([ ['END', 0], ]); +/** + * F15 — WIDE_DAG_FOR_LEVELS + * + * Tests longest-path level assignment. + * A→B, A→C, B→D, D→E, C→E. + * Longest path to each: A=0, B=1, C=1, D=2, E=3 (via A→B→D→E, not A→C→E). + * + * A + * / \ + * B C + * | \ + * D | + * \ / + * E + */ +export const F15_WIDE_DAG_FOR_LEVELS = makeFixture({ + nodes: ['A', 'B', 'C', 'D', 'E'], + edges: [ + { from: 'A', to: 'B' }, + { from: 'A', to: 'C' }, + { from: 'B', to: 'D' }, + { from: 'D', to: 'E' }, + { from: 'C', to: 'E' }, + ], +}); + +/** + * F16 — TRANSITIVE_REDUCTION + * + * A→B, A→C (redundant), B→C. + * Transitive reduction removes A→C because A→B→C already reaches C. + * + * A ──→ B + * \ ↓ + * └→ C + */ +export const F16_TRANSITIVE_REDUCTION = makeFixture({ + nodes: ['A', 'B', 'C'], + edges: [ + { from: 'A', to: 'B' }, + { from: 'A', to: 'C' }, + { from: 'B', to: 'C' }, + ], +}); + +/** + * F17 — MULTI_ROOT_DAG + * + * Two root nodes (in-degree 0) converge on D. + * R1→A→D, R2→B→D, R2→C→D. + * rootAncestors(D) should return [R1, R2]. + * + * R1 → A ──┐ + * ↓ + * R2 → B → D + * └→ C ──┘ + */ +export const F17_MULTI_ROOT_DAG = makeFixture({ + nodes: ['R1', 'R2', 'A', 'B', 'C', 'D'], + edges: [ + { from: 'R1', to: 'A' }, + { from: 'A', to: 'D' }, + { from: 'R2', to: 'B' }, + { from: 'R2', to: 'C' }, + { from: 'B', to: 'D' }, + { from: 'C', to: 'D' }, + ], +}); + +/** + * F18 — TRANSITIVE_CLOSURE_CHAIN + * + * A→B→C→D. Linear chain. + * Transitive closure adds: A→C, A→D, B→D = 3 new edges + 3 existing = 6 total. + */ +export const F18_TRANSITIVE_CLOSURE_CHAIN = makeFixture({ + nodes: ['A', 'B', 'C', 'D'], + edges: [ + { from: 'A', to: 'B' }, + { from: 'B', to: 'C' }, + { from: 'C', to: 'D' }, + ], +}); + // ── Utility: weight function from a Map ───────────────────────────────────── /** diff --git a/test/unit/domain/services/GraphTraversal.bfs.test.js b/test/unit/domain/services/GraphTraversal.bfs.test.js index c71f8b00..53664e76 100644 --- a/test/unit/domain/services/GraphTraversal.bfs.test.js +++ b/test/unit/domain/services/GraphTraversal.bfs.test.js @@ -10,6 +10,7 @@ import { F3_DIAMOND_EQUAL_PATHS, F9_UNICODE_CODEPOINT_ORDER, F13_BFS_MULTI_PARENT_DEDUP, + F17_MULTI_ROOT_DAG, } from '../../../helpers/fixtureDsl.js'; describe('GraphTraversal.bfs', () => { @@ -135,6 +136,35 @@ describe('GraphTraversal.bfs', () => { }); }); + // Reverse reachability — BFS with direction: 'in' + describe('reverse reachability (direction: "in")', () => { + it('F17 — BFS backward from D reaches all ancestors', async () => { + const provider = makeAdjacencyProvider(F17_MULTI_ROOT_DAG); + const engine = new GraphTraversal({ provider }); + const { nodes } = await engine.bfs({ start: 'D', direction: 'in' }); + + // D has incoming from A, B, C; A has incoming from R1; B,C from R2 + expect(nodes.sort()).toEqual(['A', 'B', 'C', 'D', 'R1', 'R2']); + }); + + it('F3 — BFS backward from D finds complete reverse graph', async () => { + const provider = makeAdjacencyProvider(F3_DIAMOND_EQUAL_PATHS); + const engine = new GraphTraversal({ provider }); + const { nodes } = await engine.bfs({ start: 'D', direction: 'in' }); + + // D←B←A, D←C←A + expect(nodes).toEqual(['D', 'B', 'C', 'A']); + }); + + it('BFS backward from root node returns only itself', async () => { + const provider = makeAdjacencyProvider(F17_MULTI_ROOT_DAG); + const engine = new GraphTraversal({ provider }); + const { nodes } = await engine.bfs({ start: 'R1', direction: 'in' }); + + expect(nodes).toEqual(['R1']); + }); + }); + // M18 — start node validation describe('start node validation (M18)', () => { it('throws INVALID_START for a nonexistent start node', async () => { diff --git a/test/unit/domain/services/GraphTraversal.crossProvider.test.js b/test/unit/domain/services/GraphTraversal.crossProvider.test.js index 35a91aef..cfe190ba 100644 --- a/test/unit/domain/services/GraphTraversal.crossProvider.test.js +++ b/test/unit/domain/services/GraphTraversal.crossProvider.test.js @@ -19,6 +19,10 @@ import { F5_WEIGHTS, F8_TOPO_CYCLE_3, F9_UNICODE_CODEPOINT_ORDER, + F15_WIDE_DAG_FOR_LEVELS, + F16_TRANSITIVE_REDUCTION, + F17_MULTI_ROOT_DAG, + F18_TRANSITIVE_CLOSURE_CHAIN, makeWeightFn, } from '../../../helpers/fixtureDsl.js'; @@ -112,4 +116,48 @@ describe('Cross-provider equivalence', () => { expect(nodes).toEqual(['D', 'B', 'C', 'A']); }); }); + + describe('levels: F15 wide DAG', () => { + forEachProvider(F15_WIDE_DAG_FOR_LEVELS, async (/** @type {*} */ engine) => { + const { levels, maxLevel } = await engine.levels({ start: 'A' }); + expect(levels.get('A')).toBe(0); + expect(levels.get('B')).toBe(1); + expect(levels.get('C')).toBe(1); + expect(levels.get('D')).toBe(2); + expect(levels.get('E')).toBe(3); + expect(maxLevel).toBe(3); + }); + }); + + describe('transitiveReduction: F16', () => { + forEachProvider(F16_TRANSITIVE_REDUCTION, async (/** @type {*} */ engine) => { + const { edges, removed } = await engine.transitiveReduction({ start: 'A' }); + expect(removed).toBe(1); + expect(edges).toEqual([ + { from: 'A', to: 'B', label: '' }, + { from: 'B', to: 'C', label: '' }, + ]); + }); + }); + + describe('transitiveClosure: F18 chain', () => { + forEachProvider(F18_TRANSITIVE_CLOSURE_CHAIN, async (/** @type {*} */ engine) => { + const { edges } = await engine.transitiveClosure({ start: 'A' }); + expect(edges).toEqual([ + { from: 'A', to: 'B' }, + { from: 'A', to: 'C' }, + { from: 'A', to: 'D' }, + { from: 'B', to: 'C' }, + { from: 'B', to: 'D' }, + { from: 'C', to: 'D' }, + ]); + }); + }); + + describe('rootAncestors: F17 multi-root', () => { + forEachProvider(F17_MULTI_ROOT_DAG, async (/** @type {*} */ engine) => { + const { roots } = await engine.rootAncestors({ start: 'D' }); + expect(roots).toEqual(['R1', 'R2']); + }); + }); }); diff --git a/test/unit/domain/services/GraphTraversal.levels.test.js b/test/unit/domain/services/GraphTraversal.levels.test.js new file mode 100644 index 00000000..5f734691 --- /dev/null +++ b/test/unit/domain/services/GraphTraversal.levels.test.js @@ -0,0 +1,114 @@ +/** + * GraphTraversal.levels() — longest-path level assignment. + */ + +import { describe, it, expect } from 'vitest'; +import GraphTraversal from '../../../../src/domain/services/GraphTraversal.js'; +import { + makeAdjacencyProvider, + F3_DIAMOND_EQUAL_PATHS, + F8_TOPO_CYCLE_3, + F15_WIDE_DAG_FOR_LEVELS, +} from '../../../helpers/fixtureDsl.js'; + +describe('GraphTraversal.levels()', () => { + describe('F15 — wide DAG level assignment', () => { + it('assigns longest-path levels', async () => { + const provider = makeAdjacencyProvider(F15_WIDE_DAG_FOR_LEVELS); + const engine = new GraphTraversal({ provider }); + const { levels, maxLevel } = await engine.levels({ start: 'A' }); + + expect(levels.get('A')).toBe(0); + expect(levels.get('B')).toBe(1); + expect(levels.get('C')).toBe(1); + expect(levels.get('D')).toBe(2); + expect(levels.get('E')).toBe(3); + expect(maxLevel).toBe(3); + }); + + it('returns stats', async () => { + const provider = makeAdjacencyProvider(F15_WIDE_DAG_FOR_LEVELS); + const engine = new GraphTraversal({ provider }); + const { stats } = await engine.levels({ start: 'A' }); + + expect(stats.nodesVisited).toBe(5); + expect(stats.edgesTraversed).toBeGreaterThanOrEqual(5); + }); + }); + + describe('F3 — diamond equal paths', () => { + it('assigns correct levels for diamond', async () => { + const provider = makeAdjacencyProvider(F3_DIAMOND_EQUAL_PATHS); + const engine = new GraphTraversal({ provider }); + const { levels, maxLevel } = await engine.levels({ start: 'A' }); + + expect(levels.get('A')).toBe(0); + expect(levels.get('B')).toBe(1); + expect(levels.get('C')).toBe(1); + expect(levels.get('D')).toBe(2); + expect(maxLevel).toBe(2); + }); + }); + + describe('single node', () => { + it('assigns level 0 to a lone start', async () => { + const provider = makeAdjacencyProvider(F15_WIDE_DAG_FOR_LEVELS); + const engine = new GraphTraversal({ provider }); + const { levels, maxLevel } = await engine.levels({ start: 'E' }); + + expect(levels.get('E')).toBe(0); + expect(maxLevel).toBe(0); + expect(levels.size).toBe(1); + }); + }); + + describe('multiple starts', () => { + it('accepts array of start nodes', async () => { + const provider = makeAdjacencyProvider(F15_WIDE_DAG_FOR_LEVELS); + const engine = new GraphTraversal({ provider }); + const { levels } = await engine.levels({ start: ['A', 'B'] }); + + expect(levels.get('A')).toBe(0); + expect(levels.get('B')).toBe(1); + }); + }); + + describe('cycle detection', () => { + it('throws ERR_GRAPH_HAS_CYCLES on cyclic graph', async () => { + const provider = makeAdjacencyProvider(F8_TOPO_CYCLE_3); + const engine = new GraphTraversal({ provider }); + + await expect(engine.levels({ start: 'A' })).rejects.toThrow( + expect.objectContaining({ + code: 'ERR_GRAPH_HAS_CYCLES', + }), + ); + }); + }); + + describe('INVALID_START', () => { + it('throws when start node does not exist', async () => { + const provider = makeAdjacencyProvider(F15_WIDE_DAG_FOR_LEVELS); + const engine = new GraphTraversal({ provider }); + + await expect(engine.levels({ start: 'NOPE' })).rejects.toThrow( + expect.objectContaining({ + code: 'INVALID_START', + }), + ); + }); + }); + + describe('AbortSignal', () => { + it('respects cancellation', async () => { + const provider = makeAdjacencyProvider(F15_WIDE_DAG_FOR_LEVELS); + const engine = new GraphTraversal({ provider }); + const controller = new AbortController(); + controller.abort(); + + await expect( + engine.levels({ start: 'A', signal: controller.signal }), + ).rejects.toThrow(); + }); + }); +}); diff --git a/test/unit/domain/services/GraphTraversal.rootAncestors.test.js b/test/unit/domain/services/GraphTraversal.rootAncestors.test.js new file mode 100644 index 00000000..f02eb59b --- /dev/null +++ b/test/unit/domain/services/GraphTraversal.rootAncestors.test.js @@ -0,0 +1,124 @@ +/** + * GraphTraversal.rootAncestors() — find in-degree-0 ancestors. + */ + +import { describe, it, expect } from 'vitest'; +import GraphTraversal from '../../../../src/domain/services/GraphTraversal.js'; +import { + makeAdjacencyProvider, + makeFixture, + F3_DIAMOND_EQUAL_PATHS, + F8_TOPO_CYCLE_3, + F17_MULTI_ROOT_DAG, +} from '../../../helpers/fixtureDsl.js'; + +describe('GraphTraversal.rootAncestors()', () => { + describe('F17 — multi-root DAG', () => { + it('finds all root ancestors from leaf node', async () => { + const provider = makeAdjacencyProvider(F17_MULTI_ROOT_DAG); + const engine = new GraphTraversal({ provider }); + const { roots } = await engine.rootAncestors({ start: 'D' }); + + expect(roots).toEqual(['R1', 'R2']); + }); + + it('returns the node itself if it is a root', async () => { + const provider = makeAdjacencyProvider(F17_MULTI_ROOT_DAG); + const engine = new GraphTraversal({ provider }); + const { roots } = await engine.rootAncestors({ start: 'R1' }); + + expect(roots).toEqual(['R1']); + }); + + it('finds roots from intermediate node', async () => { + const provider = makeAdjacencyProvider(F17_MULTI_ROOT_DAG); + const engine = new GraphTraversal({ provider }); + const { roots } = await engine.rootAncestors({ start: 'B' }); + + expect(roots).toEqual(['R2']); + }); + }); + + describe('F3 — diamond', () => { + it('finds single root from leaf', async () => { + const provider = makeAdjacencyProvider(F3_DIAMOND_EQUAL_PATHS); + const engine = new GraphTraversal({ provider }); + const { roots } = await engine.rootAncestors({ start: 'D' }); + + expect(roots).toEqual(['A']); + }); + + it('returns root as its own ancestor', async () => { + const provider = makeAdjacencyProvider(F3_DIAMOND_EQUAL_PATHS); + const engine = new GraphTraversal({ provider }); + const { roots } = await engine.rootAncestors({ start: 'A' }); + + expect(roots).toEqual(['A']); + }); + }); + + describe('cyclic graph', () => { + it('works on cyclic graphs (BFS reachability)', async () => { + const provider = makeAdjacencyProvider(F8_TOPO_CYCLE_3); + const engine = new GraphTraversal({ provider }); + // All nodes in a cycle have in-degree > 0, so no roots + const { roots } = await engine.rootAncestors({ start: 'A' }); + + expect(roots).toEqual([]); + }); + }); + + describe('disconnected root', () => { + it('finds only backward-reachable roots', async () => { + const fixture = makeFixture({ + nodes: ['X', 'Y', 'Z'], + edges: [ + { from: 'X', to: 'Y' }, + ], + }); + const provider = makeAdjacencyProvider(fixture); + const engine = new GraphTraversal({ provider }); + // Y has root X backward; Z is disconnected + const { roots } = await engine.rootAncestors({ start: 'Y' }); + + expect(roots).toEqual(['X']); + }); + }); + + describe('INVALID_START', () => { + it('throws when start node does not exist', async () => { + const provider = makeAdjacencyProvider(F17_MULTI_ROOT_DAG); + const engine = new GraphTraversal({ provider }); + + await expect(engine.rootAncestors({ start: 'NOPE' })).rejects.toThrow( + expect.objectContaining({ + code: 'INVALID_START', + }), + ); + }); + }); + + describe('maxDepth', () => { + it('respects maxDepth limit', async () => { + const provider = makeAdjacencyProvider(F17_MULTI_ROOT_DAG); + const engine = new GraphTraversal({ provider }); + // maxDepth=1 from D reaches A, B, C but NOT R1, R2 + const { roots } = await engine.rootAncestors({ start: 'D', maxDepth: 1 }); + + // A, B, C all have incoming edges so none are roots within depth 1 + // D itself has incoming edges, so it's not a root either + expect(roots).toEqual([]); + }); + }); + + describe('stats', () => { + it('returns traversal stats', async () => { + const provider = makeAdjacencyProvider(F17_MULTI_ROOT_DAG); + const engine = new GraphTraversal({ provider }); + const { stats } = await engine.rootAncestors({ start: 'D' }); + + expect(stats.nodesVisited).toBeGreaterThan(0); + expect(stats.edgesTraversed).toBeGreaterThan(0); + }); + }); +}); diff --git a/test/unit/domain/services/GraphTraversal.transitiveClosure.test.js b/test/unit/domain/services/GraphTraversal.transitiveClosure.test.js new file mode 100644 index 00000000..7c926dce --- /dev/null +++ b/test/unit/domain/services/GraphTraversal.transitiveClosure.test.js @@ -0,0 +1,145 @@ +/** + * GraphTraversal.transitiveClosure() — all implied reachability edges. + */ + +import { describe, it, expect } from 'vitest'; +import GraphTraversal from '../../../../src/domain/services/GraphTraversal.js'; +import { + makeAdjacencyProvider, + makeFixture, + F3_DIAMOND_EQUAL_PATHS, + F8_TOPO_CYCLE_3, + F18_TRANSITIVE_CLOSURE_CHAIN, +} from '../../../helpers/fixtureDsl.js'; + +describe('GraphTraversal.transitiveClosure()', () => { + describe('F18 — linear chain A→B→C→D', () => { + it('produces 6 reachability edges', async () => { + const provider = makeAdjacencyProvider(F18_TRANSITIVE_CLOSURE_CHAIN); + const engine = new GraphTraversal({ provider }); + const { edges } = await engine.transitiveClosure({ start: 'A' }); + + // A→B, A→C, A→D, B→C, B→D, C→D + expect(edges).toEqual([ + { from: 'A', to: 'B' }, + { from: 'A', to: 'C' }, + { from: 'A', to: 'D' }, + { from: 'B', to: 'C' }, + { from: 'B', to: 'D' }, + { from: 'C', to: 'D' }, + ]); + }); + }); + + describe('F3 — diamond', () => { + it('includes both paths plus transitive A→D', async () => { + const provider = makeAdjacencyProvider(F3_DIAMOND_EQUAL_PATHS); + const engine = new GraphTraversal({ provider }); + const { edges } = await engine.transitiveClosure({ start: 'A' }); + + // A→B, A→C, A→D, B→D, C→D + expect(edges).toEqual([ + { from: 'A', to: 'B' }, + { from: 'A', to: 'C' }, + { from: 'A', to: 'D' }, + { from: 'B', to: 'D' }, + { from: 'C', to: 'D' }, + ]); + }); + }); + + describe('cyclic graph', () => { + it('works on cyclic graphs', async () => { + const provider = makeAdjacencyProvider(F8_TOPO_CYCLE_3); + const engine = new GraphTraversal({ provider }); + const { edges } = await engine.transitiveClosure({ start: 'A' }); + + // A→B, A→C, B→A, B→C, C→A, C→B — full reachability + expect(edges).toEqual([ + { from: 'A', to: 'B' }, + { from: 'A', to: 'C' }, + { from: 'B', to: 'A' }, + { from: 'B', to: 'C' }, + { from: 'C', to: 'A' }, + { from: 'C', to: 'B' }, + ]); + }); + }); + + describe('single node', () => { + it('returns empty edges for isolated node', async () => { + const fixture = makeFixture({ + nodes: ['X'], + edges: [], + }); + const provider = makeAdjacencyProvider(fixture); + const engine = new GraphTraversal({ provider }); + const { edges } = await engine.transitiveClosure({ start: 'X' }); + + expect(edges).toEqual([]); + }); + }); + + describe('maxEdges safety', () => { + it('throws E_MAX_EDGES_EXCEEDED when limit hit', async () => { + const provider = makeAdjacencyProvider(F18_TRANSITIVE_CLOSURE_CHAIN); + const engine = new GraphTraversal({ provider }); + + await expect( + engine.transitiveClosure({ start: 'A', maxEdges: 3 }), + ).rejects.toThrow( + expect.objectContaining({ + code: 'E_MAX_EDGES_EXCEEDED', + }), + ); + }); + + it('succeeds when edges within limit', async () => { + const provider = makeAdjacencyProvider(F18_TRANSITIVE_CLOSURE_CHAIN); + const engine = new GraphTraversal({ provider }); + const { edges } = await engine.transitiveClosure({ start: 'A', maxEdges: 6 }); + + expect(edges.length).toBe(6); + }); + }); + + describe('INVALID_START', () => { + it('throws when start node does not exist', async () => { + const provider = makeAdjacencyProvider(F18_TRANSITIVE_CLOSURE_CHAIN); + const engine = new GraphTraversal({ provider }); + + await expect(engine.transitiveClosure({ start: 'NOPE' })).rejects.toThrow( + expect.objectContaining({ + code: 'INVALID_START', + }), + ); + }); + }); + + describe('deterministic output', () => { + it('edges are sorted lexicographically', async () => { + const provider = makeAdjacencyProvider(F3_DIAMOND_EQUAL_PATHS); + const engine = new GraphTraversal({ provider }); + const { edges } = await engine.transitiveClosure({ start: 'A' }); + + for (let i = 1; i < edges.length; i++) { + const prev = edges[i - 1]; + const curr = edges[i]; + const cmp = prev.from < curr.from ? -1 : prev.from > curr.from ? 1 : + prev.to < curr.to ? -1 : prev.to > curr.to ? 1 : 0; + expect(cmp).toBeLessThanOrEqual(0); + } + }); + }); + + describe('stats', () => { + it('returns traversal stats', async () => { + const provider = makeAdjacencyProvider(F18_TRANSITIVE_CLOSURE_CHAIN); + const engine = new GraphTraversal({ provider }); + const { stats } = await engine.transitiveClosure({ start: 'A' }); + + expect(stats.nodesVisited).toBe(4); + expect(stats.edgesTraversed).toBeGreaterThan(0); + }); + }); +}); diff --git a/test/unit/domain/services/GraphTraversal.transitiveReduction.test.js b/test/unit/domain/services/GraphTraversal.transitiveReduction.test.js new file mode 100644 index 00000000..9e01737c --- /dev/null +++ b/test/unit/domain/services/GraphTraversal.transitiveReduction.test.js @@ -0,0 +1,153 @@ +/** + * GraphTraversal.transitiveReduction() — minimal edge set preserving reachability. + */ + +import { describe, it, expect } from 'vitest'; +import GraphTraversal from '../../../../src/domain/services/GraphTraversal.js'; +import { + makeAdjacencyProvider, + makeFixture, + F3_DIAMOND_EQUAL_PATHS, + F8_TOPO_CYCLE_3, + F16_TRANSITIVE_REDUCTION, +} from '../../../helpers/fixtureDsl.js'; + +describe('GraphTraversal.transitiveReduction()', () => { + describe('F16 — redundant edge removal', () => { + it('removes redundant A→C edge', async () => { + const provider = makeAdjacencyProvider(F16_TRANSITIVE_REDUCTION); + const engine = new GraphTraversal({ provider }); + const { edges, removed } = await engine.transitiveReduction({ start: 'A' }); + + expect(removed).toBe(1); + // A→B and B→C should remain; A→C removed + expect(edges).toEqual([ + { from: 'A', to: 'B', label: '' }, + { from: 'B', to: 'C', label: '' }, + ]); + }); + }); + + describe('F3 — diamond (no redundant edges)', () => { + it('preserves all edges in diamond', async () => { + const provider = makeAdjacencyProvider(F3_DIAMOND_EQUAL_PATHS); + const engine = new GraphTraversal({ provider }); + const { edges, removed } = await engine.transitiveReduction({ start: 'A' }); + + // A→B, A→C, B→D, C→D — none redundant + expect(removed).toBe(0); + expect(edges).toEqual([ + { from: 'A', to: 'B', label: '' }, + { from: 'A', to: 'C', label: '' }, + { from: 'B', to: 'D', label: '' }, + { from: 'C', to: 'D', label: '' }, + ]); + }); + }); + + describe('chain (no redundant edges)', () => { + it('preserves all edges in linear chain', async () => { + const fixture = makeFixture({ + nodes: ['A', 'B', 'C'], + edges: [ + { from: 'A', to: 'B' }, + { from: 'B', to: 'C' }, + ], + }); + const provider = makeAdjacencyProvider(fixture); + const engine = new GraphTraversal({ provider }); + const { edges, removed } = await engine.transitiveReduction({ start: 'A' }); + + expect(removed).toBe(0); + expect(edges).toEqual([ + { from: 'A', to: 'B', label: '' }, + { from: 'B', to: 'C', label: '' }, + ]); + }); + }); + + describe('multiple redundant edges', () => { + it('removes all transitively implied edges', async () => { + // A→B, A→C, A→D (redundant), B→C, B→D (redundant), C→D + const fixture = makeFixture({ + nodes: ['A', 'B', 'C', 'D'], + edges: [ + { from: 'A', to: 'B' }, + { from: 'A', to: 'C' }, + { from: 'A', to: 'D' }, + { from: 'B', to: 'C' }, + { from: 'B', to: 'D' }, + { from: 'C', to: 'D' }, + ], + }); + const provider = makeAdjacencyProvider(fixture); + const engine = new GraphTraversal({ provider }); + const { edges, removed } = await engine.transitiveReduction({ start: 'A' }); + + expect(removed).toBe(3); // A→C, A→D, B→D + expect(edges).toEqual([ + { from: 'A', to: 'B', label: '' }, + { from: 'B', to: 'C', label: '' }, + { from: 'C', to: 'D', label: '' }, + ]); + }); + }); + + describe('preserves labels', () => { + it('edge labels survive reduction', async () => { + const fixture = makeFixture({ + nodes: ['A', 'B', 'C'], + edges: [ + { from: 'A', to: 'B', label: 'manages' }, + { from: 'B', to: 'C', label: 'owns' }, + { from: 'A', to: 'C', label: 'redundant' }, + ], + }); + const provider = makeAdjacencyProvider(fixture); + const engine = new GraphTraversal({ provider }); + const { edges, removed } = await engine.transitiveReduction({ start: 'A' }); + + expect(removed).toBe(1); + expect(edges).toEqual([ + { from: 'A', to: 'B', label: 'manages' }, + { from: 'B', to: 'C', label: 'owns' }, + ]); + }); + }); + + describe('cycle detection', () => { + it('throws ERR_GRAPH_HAS_CYCLES', async () => { + const provider = makeAdjacencyProvider(F8_TOPO_CYCLE_3); + const engine = new GraphTraversal({ provider }); + + await expect(engine.transitiveReduction({ start: 'A' })).rejects.toThrow( + expect.objectContaining({ + code: 'ERR_GRAPH_HAS_CYCLES', + }), + ); + }); + }); + + describe('INVALID_START', () => { + it('throws when start node does not exist', async () => { + const provider = makeAdjacencyProvider(F16_TRANSITIVE_REDUCTION); + const engine = new GraphTraversal({ provider }); + + await expect(engine.transitiveReduction({ start: 'NOPE' })).rejects.toThrow( + expect.objectContaining({ + code: 'INVALID_START', + }), + ); + }); + }); + + describe('stats', () => { + it('returns traversal stats', async () => { + const provider = makeAdjacencyProvider(F16_TRANSITIVE_REDUCTION); + const engine = new GraphTraversal({ provider }); + const { stats } = await engine.transitiveReduction({ start: 'A' }); + + expect(stats.nodesVisited).toBe(3); + }); + }); +}); From 8d394dd00a88d5550062e2eb0571444507518bb5 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 4 Mar 2026 00:18:05 -0800 Subject: [PATCH 15/21] docs(roadmap): add B149-B151 large-graph streaming backlog items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three items for making new graph algorithms memory-efficient on large graphs: - B149: levels() two-pass streaming (O(V+E) → O(V) memory) - B150: transitiveReduction() on-demand neighbor fetch - B151: transitiveClosure() async iterator output --- ROADMAP.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b8872024..415bc018 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -203,6 +203,9 @@ Items picked up opportunistically without blocking milestones. No milestone assi | B81 | **`attachContent` ORPHAN BLOB GUARD** — `attachContent()` unconditionally writes blob before `setProperty()`. Validate before push to prevent orphan blobs. From B-CODE-2. **File:** `src/domain/services/PatchBuilderV2.js` | | ~~B146~~ | ✅ ~~**UNIFY `CorePersistence` / `FullPersistence` TYPEDEFS**~~ — replaced `FullPersistence` with imported `CorePersistence`. Done in v13.0.0. | | B147 | **RFC FIELD COUNT DRIFT DETECTOR** — script that counts WarpGraph instance fields (grep `this._` in constructor) and warns if design RFC field counts diverge. Prevents stale numbers in `warpgraph-decomposition.md`. From B145 PR review. | +| B149 | **LARGE-GRAPH `levels()` — TWO-PASS STREAMING** — `levels()` currently holds O(V+E) via `topologicalSort({ _returnAdjList: true })`. Refactor to two-pass: (1) topo sort discards edge cache, (2) DP pass re-fetches neighbors from provider. Reduces steady-state memory from O(V+E) to O(V). Trade-off: one extra I/O pass over edges. **File:** `src/domain/services/GraphTraversal.js` | +| B150 | **LARGE-GRAPH `transitiveReduction()` — ON-DEMAND NEIGHBOR FETCH** — `transitiveReduction()` holds full adjacency list from topo sort AND builds a second `Map` for per-node BFS. Refactor BFS phase to call `getNeighbors()` on demand instead of caching. Reduces memory from O(V+E) to O(V) working set per BFS sweep. Trade-off: redundant provider calls (up to V BFS sweeps re-fetching same neighbors). Consider provider-level LRU to amortize. **File:** `src/domain/services/GraphTraversal.js` | +| B151 | **LARGE-GRAPH `transitiveClosure()` — STREAMING OUTPUT** — `transitiveClosure()` collects all O(V²) reachability edges in an array before returning. For large graphs this can exhaust memory even with `maxEdges`. Refactor to async iterator/generator that yields `{from, to}` pairs as they're discovered. Per-node BFS working memory is already O(V); the bottleneck is the output array. **File:** `src/domain/services/GraphTraversal.js` | ### CI & Tooling Pack @@ -321,11 +324,11 @@ Pick opportunistically between milestones. Recommended order within tiers: | **Milestone (M12)** | 18 | B66, B67, B70, B73, B75, B105–B115, B117, B118 | | **Milestone (M13)** | 1 | B116 (internal: DONE; wire-format: DEFERRED) | | **Milestone (M14)** | 16 | B130–B145 | -| **Standalone** | 35 | B12, B19, B22, B28, B34–B37, B43, B48, B49, B53, B54, B57, B76, B79–B81, B83, B85–B88, B95–B99, B102–B104, B119, B123, B127–B129, B147 | +| **Standalone** | 38 | B12, B19, B22, B28, B34–B37, B43, B48, B49, B53, B54, B57, B76, B79–B81, B83, B85–B88, B95–B99, B102–B104, B119, B123, B127–B129, B147, B149–B151 | | **Standalone (done)** | 29 | B26, B44, B46, B47, B50–B52, B55, B71, B72, B77, B78, B82, B84, B89–B94, B100, B120–B122, B124, B125, B126, B146, B148 | | **Deferred** | 7 | B4, B7, B16, B20, B21, B27, B101 | | **Rejected** | 7 | B5, B6, B13, B17, B18, B25, B45 | -| **Total tracked** | **123** total; 29 standalone done | | +| **Total tracked** | **126** total; 29 standalone done | | ### STANK.md Cross-Reference From cf3cba8244100415d4561d91173447a7da59a73e Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 4 Mar 2026 00:22:36 -0800 Subject: [PATCH 16/21] docs(roadmap): add B152-B156 streaming API, layout, and structural diff items - B152: async generator traversal API (generalized streaming) - B153: topologicalSort lightweight mode (O(V) memory) - B154: transitiveReduction redundant adjList copy fix - B155: levels() as lightweight --view layout (skip ELK) - B156: structural diff via transitive reduction comparison --- ROADMAP.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 415bc018..16e136b6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -206,6 +206,11 @@ Items picked up opportunistically without blocking milestones. No milestone assi | B149 | **LARGE-GRAPH `levels()` — TWO-PASS STREAMING** — `levels()` currently holds O(V+E) via `topologicalSort({ _returnAdjList: true })`. Refactor to two-pass: (1) topo sort discards edge cache, (2) DP pass re-fetches neighbors from provider. Reduces steady-state memory from O(V+E) to O(V). Trade-off: one extra I/O pass over edges. **File:** `src/domain/services/GraphTraversal.js` | | B150 | **LARGE-GRAPH `transitiveReduction()` — ON-DEMAND NEIGHBOR FETCH** — `transitiveReduction()` holds full adjacency list from topo sort AND builds a second `Map` for per-node BFS. Refactor BFS phase to call `getNeighbors()` on demand instead of caching. Reduces memory from O(V+E) to O(V) working set per BFS sweep. Trade-off: redundant provider calls (up to V BFS sweeps re-fetching same neighbors). Consider provider-level LRU to amortize. **File:** `src/domain/services/GraphTraversal.js` | | B151 | **LARGE-GRAPH `transitiveClosure()` — STREAMING OUTPUT** — `transitiveClosure()` collects all O(V²) reachability edges in an array before returning. For large graphs this can exhaust memory even with `maxEdges`. Refactor to async iterator/generator that yields `{from, to}` pairs as they're discovered. Per-node BFS working memory is already O(V); the bottleneck is the output array. **File:** `src/domain/services/GraphTraversal.js` | +| B152 | **ASYNC GENERATOR TRAVERSAL API** — streaming variants of all GraphTraversal algorithms (`bfsStream()`, `dfsStream()`, etc.) returning `AsyncGenerator` instead of collected arrays. Enables early break, backpressure, and pipeline composition. Array-returning methods become sugar over `collect()`. Generalizes B151 to the full traversal surface. Stats delivery via hooks or generator return value. B151 is the proof-of-concept; this is the full rollout. **File:** `src/domain/services/GraphTraversal.js` | +| B153 | **`topologicalSort` LIGHTWEIGHT MODE** — discovery phase unconditionally builds `adjList` + `neighborEdgeMap` (O(V+E)) even when `_returnAdjList` is false. Add a `_lightweight` mode that only tracks in-degree counts during discovery and re-fetches neighbors from provider during Kahn processing. Reduces topo sort memory from O(V+E) to O(V). Root cause behind B149/B150 — both inherit full-graph materialization from their topo sort call. **File:** `src/domain/services/GraphTraversal.js` | +| B154 | **`transitiveReduction` REDUNDANT ADJLIST COPY** — after receiving `_neighborEdgeMap` from topo sort, builds a second `adjList: Map` by extracting neighborIds. Two representations of the same edge set in memory simultaneously. Should use `_neighborEdgeMap` directly, accessing `.neighborId` inline during BFS. **File:** `src/domain/services/GraphTraversal.js` | +| B155 | **`levels()` AS LIGHTWEIGHT `--view` LAYOUT** — `levels()` is exactly the Y-axis assignment a layered DAG layout needs. For simple DAGs, `levels()` + left-to-right X sweep could produce clean layouts without the 2.5MB ELK import. Offer `--view --layout=levels` as an instant rendering mode, reserving ELK for complex graphs. **Files:** `src/visualization/layouts/`, `bin/cli/commands/view.js` | +| B156 | **STRUCTURAL DIFF VIA TRANSITIVE REDUCTION** — compute `transitiveReduction(stateA)` vs `transitiveReduction(stateB)` to produce a compact structural diff that strips implied edges and shows only "load-bearing" changes. Natural fit for H1 (Time-Travel Delta Engine) as `warp diff --mode=structural`. | ### CI & Tooling Pack @@ -324,11 +329,11 @@ Pick opportunistically between milestones. Recommended order within tiers: | **Milestone (M12)** | 18 | B66, B67, B70, B73, B75, B105–B115, B117, B118 | | **Milestone (M13)** | 1 | B116 (internal: DONE; wire-format: DEFERRED) | | **Milestone (M14)** | 16 | B130–B145 | -| **Standalone** | 38 | B12, B19, B22, B28, B34–B37, B43, B48, B49, B53, B54, B57, B76, B79–B81, B83, B85–B88, B95–B99, B102–B104, B119, B123, B127–B129, B147, B149–B151 | +| **Standalone** | 43 | B12, B19, B22, B28, B34–B37, B43, B48, B49, B53, B54, B57, B76, B79–B81, B83, B85–B88, B95–B99, B102–B104, B119, B123, B127–B129, B147, B149–B156 | | **Standalone (done)** | 29 | B26, B44, B46, B47, B50–B52, B55, B71, B72, B77, B78, B82, B84, B89–B94, B100, B120–B122, B124, B125, B126, B146, B148 | | **Deferred** | 7 | B4, B7, B16, B20, B21, B27, B101 | | **Rejected** | 7 | B5, B6, B13, B17, B18, B25, B45 | -| **Total tracked** | **126** total; 29 standalone done | | +| **Total tracked** | **131** total; 29 standalone done | | ### STANK.md Cross-Reference From 7f497801043bcd872bd552632ee6c1c047c3e2dd Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 4 Mar 2026 00:48:27 -0800 Subject: [PATCH 17/21] =?UTF-8?q?docs(roadmap):=20priority=20triage=20?= =?UTF-8?q?=E2=80=94=2045=20items=20into=20P0=E2=80=93P6=20tiers=20with=20?= =?UTF-8?q?wave=20execution=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restructure Standalone Lane from flat Near-Term table + separate pack sections into 8 priority-grouped sub-tables (P0 Quick Wins through P6 Documentation, plus Uncategorized/Platform) - Replace Standalone Priority Sequence with 6 execution waves mapping concrete implementation order - Add Dependency Chains section with ASCII graph showing B97→B85→B57, B153→B149/B150, B154→B150, B151→B152, B36→test velocity chains - Add effort estimates (XS/S/M/L) and dependency annotations to all items - Mark all milestones complete (M10–M14); update Final Command section - Fix inventory count: 43→45 standalone, 131→133 total tracked - Archive B44, B124, B125, B146, B148 to COMPLETED.md --- CHANGELOG.md | 1 + ROADMAP.md | 282 ++++++++++++++++++++++---------------- docs/ROADMAP/COMPLETED.md | 10 ++ 3 files changed, 177 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd22dce7..bf668168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **ROADMAP priority triage** — 45 standalone items sorted into 6 priority tiers (P0–P6) with wave-based execution order and dependency chain mapping. Replaced flat Near-Term table with priority-grouped sub-tables. All milestones (M10–M14) marked complete. Inventory corrected to 133 total tracked items. - **Vitest 2.1.9 → 4.0.18** — major test framework upgrade. Migrated deprecated `test(name, fn, { timeout })` signatures to `test(name, { timeout }, fn)` across 7 test files (40 call sites). Fixed `vi.fn().mockImplementation()` constructor mocks to use `function` expressions per Vitest 4 requirements. Resolves 5 remaining moderate-severity npm audit advisories (`esbuild` [GHSA-67mh-4wv8-2f99](https://github.com/advisories/GHSA-67mh-4wv8-2f99), `vite`, `@vitest/mocker`, `vite-node`, `vitest`). **`npm audit` now reports 0 vulnerabilities.** ## [13.0.1] — 2026-03-03 diff --git a/ROADMAP.md b/ROADMAP.md index 16e136b6..cfa2816b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,7 +1,7 @@ # ROADMAP — @git-stunts/git-warp > **Current version:** v13.0.0 -> **Last reconciled:** 2026-03-03 (v13.0.0 release: M11 COMPASS II complete, B100/B140 breaking, B44/B124/B125/B146 done) +> **Last reconciled:** 2026-03-04 (priority triage: 45 standalone items sorted into P0–P6 tiers, wave-based execution order, dependency chains mapped) > **Completed milestones:** [docs/ROADMAP/COMPLETED.md](docs/ROADMAP/COMPLETED.md) --- @@ -173,97 +173,106 @@ Archived to [COMPLETED.md](docs/ROADMAP/COMPLETED.md#milestone-11--compass-ii). ## Standalone Lane (Ongoing) -Items picked up opportunistically without blocking milestones. No milestone assignment. +45 active items sorted into priority tiers. Guiding principles: (1) harden first — correctness, memory safety, test infra, CI gates before features; (2) large-graph support is forward-looking — medium priority; (3) CI & Tooling items batch into one PR. > Completed standalone items archived in [COMPLETED.md](docs/ROADMAP/COMPLETED.md#standalone-lane--completed-items). -### Near-Term - -| ID | Item | -|----|------| -| ~~B124~~ | ✅ ~~**TRUST PAYLOAD PARITY TESTS**~~ — 22 tests verifying CLI vs service shape parity. Done in v13.0.0. | -| ~~B125~~ | ✅ ~~**`CachedValue` NULL-PAYLOAD SEMANTIC TESTS**~~ — 3 tests documenting null = "no value" sentinel. Done in v13.0.0. | -| B127 | **DENO SMOKE TEST** — `npm run test:deno:smoke` for fast local pre-push confidence without full Docker matrix. From BACKLOG 2026-02-25. | -| ~~B44~~ | ✅ ~~**SUBSCRIBER UNSUBSCRIBE-DURING-CALLBACK E2E**~~ — 3 edge-case tests (cross-unsubscribe, subscribe-during-callback, unsubscribe-in-onError). Done in v13.0.0. | -| B34 | **DOCS: SECURITY_SYNC.md** — extract threat model from JSDoc into operator doc | -| B35 | **DOCS: README INSTALL SECTION** — Quick Install with Docker + native paths | -| B36 | **FLUENT STATE BUILDER FOR TESTS** — `StateBuilder` helper replacing manual `WarpStateV5` literals | -| B37 | **SHARED MOCK PERSISTENCE FIXTURE** — dedup `createMockPersistence()` across trust test files | -| B43 | **VITEST EXPLICIT RUNTIME EXCLUDES** — prevent accidental local runs of Docker-only suites | -| B12 | **DOCS-VERSION-SYNC PRE-COMMIT CHECK** — grep version literals in .md files against `package.json` | -| B48 | **ESLINT BAN `= {}` CONSTRUCTOR DEFAULTS WITH REQUIRED PARAMS** — catches the pattern where `= {}` silently makes required options optional at the type level (found in CommitDagTraversalService, DagTraversal, DagPathFinding, DagTopology, BitmapIndexReader) | -| B49 | **TIGHTEN `checkDeclarations` INLINE COMMENT STRIPPING** — strip trailing `//` and `/* */` comments before checking for `any` in `ts-policy-check.js`; low priority but closes theoretical false-positive gap | -| B53 | **FIX JSR PUBLISH DRY-RUN DENO PANIC** — Deno 2.6.7 `deno_ast` panics on overlapping text changes from duplicate `roaring` import rewrites; either pin Deno version, vendor the import, or file upstream issue and add workaround | -| B54 | **`typedCustom()` ZOD HELPER** — `z.custom()` without a generic yields `unknown` in JS; a JSDoc-friendly wrapper (or `@typedef`-based pattern) would eliminate verbose `/** @type {z.ZodType} */ (z.custom(...))` casts across HttpSyncServer and future Zod schemas | -| B57 | **CI: AUTO-VALIDATE `type-surface.m8.json` AGAINST `index.d.ts`** — add a CI gate or pre-push check that parses the manifest and confirms every declared method/property/return type matches the corresponding signature in `index.d.ts`; prevents drift like the missing `setSeekCache` and `syncWith.state` return found in review | -| B28 | **PURE TYPESCRIPT EXAMPLE APP** — CI compile-only stub (`tsc --noEmit` on minimal TS consumer). | -| B76 | **WARPGRAPH INVISIBLE API SURFACE DOCS** — add `// API Surface` block listing all 40+ dynamically wired methods with source module. Consider generating as build step. From B-AUDIT-4 (STANK). **File:** `src/domain/WarpGraph.js:451-478` | -| B79 | **WARPGRAPH CONSTRUCTOR LIFECYCLE DOCS** — document cache invalidation strategy for 25 instance variables: which operations dirty which caches, which flush them. From B-AUDIT-16 (TSK TSK). **File:** `src/domain/WarpGraph.js:69-198` | -| B80 | **CHECKPOINTSERVICE CONTENT BLOB UNBOUNDED MEMORY** — iterates all properties into single `Set` before tree serialization. Stream content OIDs in batches. From B-AUDIT-10 (JANK). **File:** `src/domain/services/CheckpointService.js:224-226` | -| B81 | **`attachContent` ORPHAN BLOB GUARD** — `attachContent()` unconditionally writes blob before `setProperty()`. Validate before push to prevent orphan blobs. From B-CODE-2. **File:** `src/domain/services/PatchBuilderV2.js` | -| ~~B146~~ | ✅ ~~**UNIFY `CorePersistence` / `FullPersistence` TYPEDEFS**~~ — replaced `FullPersistence` with imported `CorePersistence`. Done in v13.0.0. | -| B147 | **RFC FIELD COUNT DRIFT DETECTOR** — script that counts WarpGraph instance fields (grep `this._` in constructor) and warns if design RFC field counts diverge. Prevents stale numbers in `warpgraph-decomposition.md`. From B145 PR review. | -| B149 | **LARGE-GRAPH `levels()` — TWO-PASS STREAMING** — `levels()` currently holds O(V+E) via `topologicalSort({ _returnAdjList: true })`. Refactor to two-pass: (1) topo sort discards edge cache, (2) DP pass re-fetches neighbors from provider. Reduces steady-state memory from O(V+E) to O(V). Trade-off: one extra I/O pass over edges. **File:** `src/domain/services/GraphTraversal.js` | -| B150 | **LARGE-GRAPH `transitiveReduction()` — ON-DEMAND NEIGHBOR FETCH** — `transitiveReduction()` holds full adjacency list from topo sort AND builds a second `Map` for per-node BFS. Refactor BFS phase to call `getNeighbors()` on demand instead of caching. Reduces memory from O(V+E) to O(V) working set per BFS sweep. Trade-off: redundant provider calls (up to V BFS sweeps re-fetching same neighbors). Consider provider-level LRU to amortize. **File:** `src/domain/services/GraphTraversal.js` | -| B151 | **LARGE-GRAPH `transitiveClosure()` — STREAMING OUTPUT** — `transitiveClosure()` collects all O(V²) reachability edges in an array before returning. For large graphs this can exhaust memory even with `maxEdges`. Refactor to async iterator/generator that yields `{from, to}` pairs as they're discovered. Per-node BFS working memory is already O(V); the bottleneck is the output array. **File:** `src/domain/services/GraphTraversal.js` | -| B152 | **ASYNC GENERATOR TRAVERSAL API** — streaming variants of all GraphTraversal algorithms (`bfsStream()`, `dfsStream()`, etc.) returning `AsyncGenerator` instead of collected arrays. Enables early break, backpressure, and pipeline composition. Array-returning methods become sugar over `collect()`. Generalizes B151 to the full traversal surface. Stats delivery via hooks or generator return value. B151 is the proof-of-concept; this is the full rollout. **File:** `src/domain/services/GraphTraversal.js` | -| B153 | **`topologicalSort` LIGHTWEIGHT MODE** — discovery phase unconditionally builds `adjList` + `neighborEdgeMap` (O(V+E)) even when `_returnAdjList` is false. Add a `_lightweight` mode that only tracks in-degree counts during discovery and re-fetches neighbors from provider during Kahn processing. Reduces topo sort memory from O(V+E) to O(V). Root cause behind B149/B150 — both inherit full-graph materialization from their topo sort call. **File:** `src/domain/services/GraphTraversal.js` | -| B154 | **`transitiveReduction` REDUNDANT ADJLIST COPY** — after receiving `_neighborEdgeMap` from topo sort, builds a second `adjList: Map` by extracting neighborIds. Two representations of the same edge set in memory simultaneously. Should use `_neighborEdgeMap` directly, accessing `.neighborId` inline during BFS. **File:** `src/domain/services/GraphTraversal.js` | -| B155 | **`levels()` AS LIGHTWEIGHT `--view` LAYOUT** — `levels()` is exactly the Y-axis assignment a layered DAG layout needs. For simple DAGs, `levels()` + left-to-right X sweep could produce clean layouts without the 2.5MB ELK import. Offer `--view --layout=levels` as an instant rendering mode, reserving ELK for complex graphs. **Files:** `src/visualization/layouts/`, `bin/cli/commands/view.js` | -| B156 | **STRUCTURAL DIFF VIA TRANSITIVE REDUCTION** — compute `transitiveReduction(stateA)` vs `transitiveReduction(stateB)` to produce a compact structural diff that strips implied edges and shows only "load-bearing" changes. Natural fit for H1 (Time-Travel Delta Engine) as `warp diff --mode=structural`. | - -### CI & Tooling Pack - -| ID | Item | -|----|------| -| B83 | **DEDUP CI `type-firewall` AND `lint` JOBS** — merge into one job (add `npm audit` to `type-firewall`, drop `lint`) or chain with `needs:`. From B-CI-1. **File:** GitHub workflow file `.github/workflows/ci.yml` | -| B85 | **TYPE-ONLY EXPORT MANIFEST SECTION** — `typeExports` section in `type-surface.m8.json` to catch accidental type removal from `index.d.ts`. From B-CI-3. **Files:** `contracts/type-surface.m8.json`, `scripts/check-dts-surface.js` | -| B86 | **MARKDOWNLINT CI GATE** — catch MD040 (missing code fence language) etc. From B-DOC-1. **File:** GitHub workflow file `.github/workflows/ci.yml` | -| B87 | **CODE SAMPLE LINTER** — syntax-check JS/TS code blocks in markdown files via `eslint-plugin-markdown` or custom extractor. From B-DOC-2. **Files:** new script, `docs/**/*.md` | -| B88 | **MERMAID RENDERING SMOKE TEST** — parse all ` ```mermaid ` blocks with `@mermaid-js/mermaid-cli` in CI. From B-DIAG-2. **File:** GitHub workflow file `.github/workflows/ci.yml` or `scripts/` | -| B119 | **`scripts/pr-ready` MERGE-READINESS CLI** — single tool aggregating unresolved review threads, pending/failed checks, CodeRabbit status/cooldown, and human-review count into one deterministic verdict. Dedupes ~20 BACKLOG items from 6 PR feedback sessions. From BACKLOG 2026-02-27/28. | -| B123 | **BENCHMARK BUDGETS + CI REGRESSION GATE** — define perf thresholds for eager post-commit and materialize hash cost; fail CI on agreed regression. From BACKLOG 2026-02-27. | -| B128 | **DOCS CONSISTENCY PREFLIGHT** — automated pass in `release:preflight` verifying changelog/readme/guide updates for behavior changes in hot paths (materialize, checkpoint, sync). From BACKLOG 2026-02-28. | - -### Surface Validator Pack - -All items target `scripts/check-dts-surface.js`: - -| ID | Item | -|----|------| -| B95 | **NAMESPACE EXPORT SUPPORT** — handle `export declare namespace Foo`. From B-SURF-5. | - -### Type Surface Pack - -| ID | Item | -|----|------| -| B96 | **CONSUMER TEST TYPE-ONLY IMPORT COVERAGE** — exercise all exported types beyond just declaring variables. Types like `OpOutcome`, `TraversalDirection`, `LogLevelValue` aren't tested at all. From B-TYPE-1. **File:** `test/type-check/consumer.ts` | -| B97 | **AUDIT MANIFEST vs `index.js` DRIFT** — manifest has 70 entries, `index.js` has 66 exports. 4 stale or type-only entries need reconciliation. From B-TYPE-2. **Files:** `contracts/type-surface.m8.json`, `index.js` | -| B98 | **TEST-FILE WILDCARD RATCHET** — `ts-policy-check.js` excludes test files entirely. Add separate ratchet with higher threshold or document exclusion as intentional. From B-TYPE-3. **File:** `scripts/ts-policy-check.js` | - -### Content Attachment - -| ID | Item | -|----|------| -| B99 | **DETERMINISM FUZZER FOR TREE CONSTRUCTION** — property-based test randomizing content blob insertion order in `PatchBuilderV2` and content OID iteration order in `CheckpointService.createV5()`, verifying identical tree OID. From B-FEAT-2. **File:** new test in `test/unit/domain/services/` | - -### Conformance Property Pack (B19 + B22) - -Single lightweight property suite — not a milestone anchor: - -- **B19** (CANONICAL SERIALIZATION PROPERTY TESTS) — fuzz `canonicalStringify`; verify idempotency, determinism, round-trip stability. -- **B22** (CANONICAL PARSE DETERMINISM TEST) — verify `canonicalStringify(TrustRecordSchema.parse(record))` produces identical output across repeated calls. - -**Rationale:** Golden fixtures test known paths; property tests test unknown edge combinations. For a deterministic engine, this is not optional forever. Trimmed to a single file covering canonical serialize idempotence + order-invariance. - -### Process (no code) - -| ID | Item | -|----|------| -| B102 | **API EXAMPLES REVIEW CHECKLIST** — add to `CONTRIBUTING.md`: each `createPatch()`/`commit()` uses own builder, async methods `await`ed, examples copy-pasteable. From B-DOC-3. | -| B103 | **BATCH REVIEW FIX COMMITS** — batch all review fixes into one commit before re-requesting CodeRabbit. Reduces duplicate findings across incremental pushes. From B-DX-2. | -| B104 | **MERMAID DIAGRAM CONTENT CHECKLIST** — for diagram migrations: count annotations in source/target, verify edge labels survive, check complexity annotations preserved. From B-DIAG-1. | -| B129 | **CONTRIBUTOR REVIEW-LOOP HYGIENE GUIDE** — add section to `CONTRIBUTING.md` covering commit sizing, CodeRabbit cooldown strategy, and when to request bot review. From BACKLOG 2026-02-27. | +### P0 — Quick Wins (unblock other work, trivial effort) + +No dependencies. Do these first. + +| ID | Item | Effort | +|----|------|--------| +| B154 | **`transitiveReduction` REDUNDANT ADJLIST COPY** — after receiving `_neighborEdgeMap` from topo sort, builds a second `adjList: Map` by extracting neighborIds. Two representations of the same edge set in memory simultaneously. Should use `_neighborEdgeMap` directly, accessing `.neighborId` inline during BFS. **File:** `src/domain/services/GraphTraversal.js`. **Unblocks:** B150 (P4) | XS | +| B97 | **AUDIT MANIFEST vs `index.js` DRIFT** — manifest has 70 entries, `index.js` has 66 exports. 4 stale or type-only entries need reconciliation. From B-TYPE-2. **Files:** `contracts/type-surface.m8.json`, `index.js`. **Unblocks:** B85 → B57 (P2 chain) | S | +| B81 | **`attachContent` ORPHAN BLOB GUARD** — `attachContent()` unconditionally writes blob before `setProperty()`. Validate before push to prevent orphan blobs. From B-CODE-2. **File:** `src/domain/services/PatchBuilderV2.js` | S | + +### P1 — Correctness & Test Infrastructure + +B36 and B37 improve velocity for all future test work — do them early. B19 + B22 batch as one PR (Conformance Property Pack). + +| ID | Item | Effort | +|----|------|--------| +| B36 | **FLUENT STATE BUILDER FOR TESTS** — `StateBuilder` helper replacing manual `WarpStateV5` literals | M | +| B37 | **SHARED MOCK PERSISTENCE FIXTURE** — dedup `createMockPersistence()` across trust test files | S | +| B48 | **ESLINT BAN `= {}` CONSTRUCTOR DEFAULTS WITH REQUIRED PARAMS** — catches the pattern where `= {}` silently makes required options optional at the type level (found in CommitDagTraversalService, DagTraversal, DagPathFinding, DagTopology, BitmapIndexReader) | S | +| B80 | **CHECKPOINTSERVICE CONTENT BLOB UNBOUNDED MEMORY** — iterates all properties into single `Set` before tree serialization. Stream content OIDs in batches. From B-AUDIT-10 (JANK). **File:** `src/domain/services/CheckpointService.js:224-226` | M | +| B99 | **DETERMINISM FUZZER FOR TREE CONSTRUCTION** — property-based test randomizing content blob insertion order in `PatchBuilderV2` and content OID iteration order in `CheckpointService.createV5()`, verifying identical tree OID. From B-FEAT-2. **File:** new test in `test/unit/domain/services/` | M | +| B19 | **CANONICAL SERIALIZATION PROPERTY TESTS** — fuzz `canonicalStringify`; verify idempotency, determinism, round-trip stability. Golden fixtures test known paths; property tests test unknown edge combinations. | S | +| B22 | **CANONICAL PARSE DETERMINISM TEST** — verify `canonicalStringify(TrustRecordSchema.parse(record))` produces identical output across repeated calls. Batch with B19 as one PR. | S | + +### P2 — CI & Tooling (one batch PR) + +**Internal chain:** B97 (P0) → B85 → B57 — must complete P0 first. B123 is the largest item — may need to split out if the PR gets too big. + +| ID | Item | Depends on | Effort | +|----|------|------------|--------| +| B83 | **DEDUP CI `type-firewall` AND `lint` JOBS** — merge into one job (add `npm audit` to `type-firewall`, drop `lint`) or chain with `needs:`. From B-CI-1. **File:** `.github/workflows/ci.yml` | — | S | +| B85 | **TYPE-ONLY EXPORT MANIFEST SECTION** — `typeExports` section in `type-surface.m8.json` to catch accidental type removal from `index.d.ts`. From B-CI-3. **Files:** `contracts/type-surface.m8.json`, `scripts/check-dts-surface.js` | B97 (P0) | S | +| B57 | **CI: AUTO-VALIDATE `type-surface.m8.json` AGAINST `index.d.ts`** — add a CI gate or pre-push check that parses the manifest and confirms every declared method/property/return type matches the corresponding signature in `index.d.ts`; prevents drift like the missing `setSeekCache` and `syncWith.state` return found in review | B97, B85 | M | +| B86 | **MARKDOWNLINT CI GATE** — catch MD040 (missing code fence language) etc. From B-DOC-1. **File:** `.github/workflows/ci.yml` | — | S | +| B87 | **CODE SAMPLE LINTER** — syntax-check JS/TS code blocks in markdown files via `eslint-plugin-markdown` or custom extractor. From B-DOC-2. **Files:** new script, `docs/**/*.md` | — | M | +| B88 | **MERMAID RENDERING SMOKE TEST** — parse all ` ```mermaid ` blocks with `@mermaid-js/mermaid-cli` in CI. From B-DIAG-2. **File:** `.github/workflows/ci.yml` or `scripts/` | — | S | +| B119 | **`scripts/pr-ready` MERGE-READINESS CLI** — single tool aggregating unresolved review threads, pending/failed checks, CodeRabbit status/cooldown, and human-review count into one deterministic verdict. From BACKLOG 2026-02-27/28. | — | M | +| B123 | **BENCHMARK BUDGETS + CI REGRESSION GATE** — define perf thresholds for eager post-commit and materialize hash cost; fail CI on agreed regression. From BACKLOG 2026-02-27. | — | L | +| B128 | **DOCS CONSISTENCY PREFLIGHT** — automated pass in `release:preflight` verifying changelog/readme/guide updates for behavior changes in hot paths (materialize, checkpoint, sync). From BACKLOG 2026-02-28. | — | S | +| B12 | **DOCS-VERSION-SYNC PRE-COMMIT CHECK** — grep version literals in .md files against `package.json` | — | S | +| B43 | **VITEST EXPLICIT RUNTIME EXCLUDES** — prevent accidental local runs of Docker-only suites | — | S | + +### P3 — Type Safety & Surface + +No hard dependencies. Pick up opportunistically after P2. + +| ID | Item | Effort | +|----|------|--------| +| B95 | **NAMESPACE EXPORT SUPPORT** — handle `export declare namespace Foo` in surface validator. From B-SURF-5. **File:** `scripts/check-dts-surface.js` | S | +| B96 | **CONSUMER TEST TYPE-ONLY IMPORT COVERAGE** — exercise all exported types beyond just declaring variables. Types like `OpOutcome`, `TraversalDirection`, `LogLevelValue` aren't tested at all. From B-TYPE-1. **File:** `test/type-check/consumer.ts` | M | +| B98 | **TEST-FILE WILDCARD RATCHET** — `ts-policy-check.js` excludes test files entirely. Add separate ratchet with higher threshold or document exclusion as intentional. From B-TYPE-3. **File:** `scripts/ts-policy-check.js` | S | +| B54 | **`typedCustom()` ZOD HELPER** — `z.custom()` without a generic yields `unknown` in JS; a JSDoc-friendly wrapper (or `@typedef`-based pattern) would eliminate verbose `/** @type {z.ZodType} */ (z.custom(...))` casts across HttpSyncServer and future Zod schemas | S | +| B49 | **TIGHTEN `checkDeclarations` INLINE COMMENT STRIPPING** — strip trailing `//` and `/* */` comments before checking for `any` in `ts-policy-check.js`; low priority but closes theoretical false-positive gap | XS | +| B28 | **PURE TYPESCRIPT EXAMPLE APP** — CI compile-only stub (`tsc --noEmit` on minimal TS consumer). | M | + +### P4 — Large-Graph Performance (forward-looking) + +**Execution order:** B154 (P0) → B153 → B149 + B150 (parallel) → B151 → B152. B153 is the keystone — fixes topo sort memory, which cascades to B149 and B150. + +| ID | Item | Depends on | Effort | +|----|------|------------|--------| +| B153 | **`topologicalSort` LIGHTWEIGHT MODE** — discovery phase unconditionally builds `adjList` + `neighborEdgeMap` (O(V+E)) even when `_returnAdjList` is false. Add a `_lightweight` mode that only tracks in-degree counts during discovery and re-fetches neighbors from provider during Kahn processing. Reduces topo sort memory from O(V+E) to O(V). Root cause behind B149/B150 — both inherit full-graph materialization from their topo sort call. **File:** `src/domain/services/GraphTraversal.js` | — | M | +| B149 | **LARGE-GRAPH `levels()` — TWO-PASS STREAMING** — `levels()` currently holds O(V+E) via `topologicalSort({ _returnAdjList: true })`. Refactor to two-pass: (1) topo sort discards edge cache, (2) DP pass re-fetches neighbors from provider. Reduces steady-state memory from O(V+E) to O(V). Trade-off: one extra I/O pass over edges. **File:** `src/domain/services/GraphTraversal.js` | B153 | M | +| B150 | **LARGE-GRAPH `transitiveReduction()` — ON-DEMAND NEIGHBOR FETCH** — `transitiveReduction()` holds full adjacency list from topo sort AND builds a second `Map` for per-node BFS. Refactor BFS phase to call `getNeighbors()` on demand instead of caching. Reduces memory from O(V+E) to O(V) working set per BFS sweep. Trade-off: redundant provider calls. Consider provider-level LRU to amortize. **File:** `src/domain/services/GraphTraversal.js` | B153, B154 (P0) | M | +| B151 | **LARGE-GRAPH `transitiveClosure()` — STREAMING OUTPUT** — `transitiveClosure()` collects all O(V²) reachability edges in an array before returning. For large graphs this can exhaust memory even with `maxEdges`. Refactor to async iterator/generator that yields `{from, to}` pairs as they're discovered. Per-node BFS working memory is already O(V); the bottleneck is the output array. **File:** `src/domain/services/GraphTraversal.js` | — | M | +| B152 | **ASYNC GENERATOR TRAVERSAL API** — streaming variants of all GraphTraversal algorithms (`bfsStream()`, `dfsStream()`, etc.) returning `AsyncGenerator` instead of collected arrays. Enables early break, backpressure, and pipeline composition. Array-returning methods become sugar over `collect()`. Generalizes B151 to the full traversal surface. **File:** `src/domain/services/GraphTraversal.js` | B151 | L | + +### P5 — Features & Visualization + +| ID | Item | Effort | +|----|------|--------| +| B155 | **`levels()` AS LIGHTWEIGHT `--view` LAYOUT** — `levels()` is exactly the Y-axis assignment a layered DAG layout needs. For simple DAGs, `levels()` + left-to-right X sweep could produce clean layouts without the 2.5MB ELK import. Offer `--view --layout=levels` as an instant rendering mode, reserving ELK for complex graphs. **Files:** `src/visualization/layouts/`, `bin/cli/commands/view.js` | M | +| B156 | **STRUCTURAL DIFF VIA TRANSITIVE REDUCTION** — compute `transitiveReduction(stateA)` vs `transitiveReduction(stateB)` to produce a compact structural diff that strips implied edges and shows only "load-bearing" changes. Natural fit for H1 (Time-Travel Delta Engine) as `warp diff --mode=structural`. | L | + +### P6 — Documentation & Process + +Low urgency. Fold into PRs that already touch related files. + +| ID | Item | Effort | +|----|------|--------| +| B34 | **DOCS: SECURITY_SYNC.md** — extract threat model from JSDoc into operator doc | M | +| B35 | **DOCS: README INSTALL SECTION** — Quick Install with Docker + native paths | S | +| B76 | **WARPGRAPH INVISIBLE API SURFACE DOCS** — add `// API Surface` block listing all 40+ dynamically wired methods with source module. Consider generating as build step. From B-AUDIT-4 (STANK). **File:** `src/domain/WarpGraph.js:451-478` | M | +| B79 | **WARPGRAPH CONSTRUCTOR LIFECYCLE DOCS** — document cache invalidation strategy for 25 instance variables: which operations dirty which caches, which flush them. From B-AUDIT-16 (TSK TSK). **File:** `src/domain/WarpGraph.js:69-198`. **Depends on:** B143 RFC (exists) | M | +| B102 | **API EXAMPLES REVIEW CHECKLIST** — add to `CONTRIBUTING.md`: each `createPatch()`/`commit()` uses own builder, async methods `await`ed, examples copy-pasteable. From B-DOC-3. | S | +| B103 | **BATCH REVIEW FIX COMMITS** — batch all review fixes into one commit before re-requesting CodeRabbit. Reduces duplicate findings across incremental pushes. From B-DX-2. | XS | +| B104 | **MERMAID DIAGRAM CONTENT CHECKLIST** — for diagram migrations: count annotations in source/target, verify edge labels survive, check complexity annotations preserved. From B-DIAG-1. | XS | +| B129 | **CONTRIBUTOR REVIEW-LOOP HYGIENE GUIDE** — add section to `CONTRIBUTING.md` covering commit sizing, CodeRabbit cooldown strategy, and when to request bot review. From BACKLOG 2026-02-27. | S | +| B147 | **RFC FIELD COUNT DRIFT DETECTOR** — script that counts WarpGraph instance fields (grep `this._` in constructor) and warns if design RFC field counts diverge. Prevents stale numbers in `warpgraph-decomposition.md`. From B145 PR review. **Depends on:** B143 RFC (exists) | S | + +### Uncategorized / Platform + +| ID | Item | Effort | +|----|------|--------| +| B53 | **FIX JSR PUBLISH DRY-RUN DENO PANIC** — Deno 2.6.7 `deno_ast` panics on overlapping text changes from duplicate `roaring` import rewrites; either pin Deno version, vendor the import, or file upstream issue and add workaround. Promote if JSR publish becomes imminent. | M | +| B127 | **DENO SMOKE TEST** — `npm run test:deno:smoke` for fast local pre-push confidence without full Docker matrix. From BACKLOG 2026-02-25. | S | --- @@ -292,29 +301,72 @@ B5, B6, B13, B17, B18, B25, B45 — rejected 2026-02-17 with cause recorded in ` ## Execution Order -### Milestones: M10 → M12 → M13 → M11 → M14 +### Milestones (all complete) 1. **M10 SENTINEL** — Trust + sync safety + correctness — **DONE** -2. **M12 SCALPEL** — STANK audit cleanup (minus edge prop encoding) — **DONE** (all tasks complete, gate verified) -3. **M13 SCALPEL II** — Edge property canonicalization — **DONE** (internal model complete; wire-format cutover deferred by ADR 3) -4. **M11 COMPASS II** — Developer experience (B2 impl, B3, B11) — ✅ **DONE** (v13.0.0), archived -5. **M14 HYGIENE** — Test quality, DRY extraction, SOLID quick-wins — **NEXT** (from HEX_AUDIT) - -### Standalone Priority Sequence - -Pick opportunistically between milestones. Recommended order within tiers: - -1. ~~**Immediate** (B46, B47, B26, B71, B126)~~ — **ALL DONE.** -2. **Near-term correctness** (B76, B80, B81) — prioritize items touching core services -3. **Near-term DX** (B36, B37, B43, B127) — test ergonomics and developer velocity -4. **Near-term docs/types** (B34, B35) — alignment and documentation -5. **Near-term tooling** (B12, B48, B49, B53, B54, B57, B28) — remaining type safety items -6. **CI & Tooling Pack** (B83, B85–B88, B119, B123, B128) — batch as one PR -7. **Surface Validator Pack** (B95) — only namespace export support remains -8. **Type Surface Pack** (B96–B98) — batch as one PR -9. **Content Attachment** (B99) — standalone property test -10. **Conformance Property Pack** (B19, B22) — standalone property suite -11. **Process** (B102–B104, B129) — fold into CONTRIBUTING.md when touching that file +2. **M12 SCALPEL** — STANK audit cleanup (minus edge prop encoding) — **DONE** +3. **M13 SCALPEL II** — Edge property canonicalization — **DONE** (internal; wire-format deferred by ADR 3) +4. **M11 COMPASS II** — Developer experience — ✅ **DONE** (v13.0.0), archived +5. **M14 HYGIENE** — Test quality, DRY extraction, SOLID quick-wins — **DONE** + +### Standalone Execution Waves + +Guiding principles: (1) harden first — correctness, memory safety, test infra, CI gates before features; (2) large-graph support is forward-looking — medium priority; (3) CI & Tooling items batch into one PR. + +#### Wave 1: Foundation (P0 + P1 start) + +1. **B154** — transitiveReduction adjList dedup (XS) +2. **B97** — Manifest audit (S, unblocks P2 chain) +3. **B81** — Orphan blob guard (S, correctness) +4. **B36** — Fluent StateBuilder (M, test DX) +5. **B37** — Shared mock persistence (S, test DRY) + +#### Wave 2: Correctness (P1 finish) + +6. **B48** — ESLint `= {}` defaults rule (S) +7. **B80** — CheckpointService memory streaming (M) +8. **B19 + B22** — Conformance property pack (S, one PR) +9. **B99** — Determinism fuzzer (M) + +#### Wave 3: CI & Tooling (P2, one batch PR) + +10. **B83, B85, B57, B86, B87, B88, B119, B123, B128, B12, B43** + +Internal chain: B97 (P0, Wave 1) → B85 → B57. B123 is the largest — may split out. + +#### Wave 4: Type Surface (P3) + +11. **B95, B96, B98, B54, B49** — batch or cherry-pick +12. **B28** — TypeScript example app + +#### Wave 5: Large-Graph (P4) + +13. **B153** — topologicalSort lightweight mode (keystone) +14. **B149 + B150** — levels + transitiveReduction streaming (parallel after B153) +15. **B151** — transitiveClosure streaming output +16. **B152** — full async generator API + +#### Wave 6: Features + Docs (P5 + P6) + +17. **B155** — levels() as --view layout +18. **B156** — structural diff (if H1 is in play) +19. Docs/process items (B34, B35, B76, B79, B102–B104, B129, B147) folded into related PRs + +### Dependency Chains + +```text +B97 (P0) ──→ B85 (P2) ──→ B57 (P2) + manifest auto-validate + +B153 (P4) ──→ B149 (P4) levels() streaming + └──→ B150 (P4) transitiveReduction() streaming + ↑ +B154 (P0) ─────┘ adjList dedup (quick fix) + +B151 (P4) ──→ B152 (P4) closure streaming → full async generator API + +B36 (P1) ──→ (improves velocity for B99, B19, B22, future tests) +``` --- @@ -329,11 +381,11 @@ Pick opportunistically between milestones. Recommended order within tiers: | **Milestone (M12)** | 18 | B66, B67, B70, B73, B75, B105–B115, B117, B118 | | **Milestone (M13)** | 1 | B116 (internal: DONE; wire-format: DEFERRED) | | **Milestone (M14)** | 16 | B130–B145 | -| **Standalone** | 43 | B12, B19, B22, B28, B34–B37, B43, B48, B49, B53, B54, B57, B76, B79–B81, B83, B85–B88, B95–B99, B102–B104, B119, B123, B127–B129, B147, B149–B156 | +| **Standalone** | 45 | B12, B19, B22, B28, B34–B37, B43, B48, B49, B53, B54, B57, B76, B79–B81, B83, B85–B88, B95–B99, B102–B104, B119, B123, B127–B129, B147, B149–B156 | | **Standalone (done)** | 29 | B26, B44, B46, B47, B50–B52, B55, B71, B72, B77, B78, B82, B84, B89–B94, B100, B120–B122, B124, B125, B126, B146, B148 | | **Deferred** | 7 | B4, B7, B16, B20, B21, B27, B101 | | **Rejected** | 7 | B5, B6, B13, B17, B18, B25, B45 | -| **Total tracked** | **131** total; 29 standalone done | | +| **Total tracked** | **133** total; 29 standalone done | | ### STANK.md Cross-Reference @@ -435,11 +487,9 @@ Pick opportunistically between milestones. Recommended order within tiers: ## Final Command Every milestone has a hard gate. No milestone blurs into the next. -Execution: M10 SENTINEL → **M12 SCALPEL** → **M13 SCALPEL II** → **M11 COMPASS II** → **M14 HYGIENE**. M11 is complete and archived. Standalone items fill the gaps. - -M12 is complete (including T8/T9). M13 internal canonicalization (ADR 1) is complete — canonical `NodePropSet`/`EdgePropSet` semantics, wire gate split, reserved-byte validation, version namespace separation. The persisted wire-format half of B116 is deferred by ADR 2 and governed by ADR 3 readiness gates. +All milestones are complete: M10 → M12 → M13 (internal) → M11 → M14. M13 wire-format cutover remains deferred by ADR 3 readiness gates. -M14 HYGIENE is the current priority — test hardening, DRY extraction, and SOLID quick-wins from the HEX_AUDIT. M11 is complete and archived in COMPLETED.md. +The active backlog is **45 standalone items** sorted into **6 priority tiers** (P0–P6) with **6 execution waves**. Wave 1 (foundation) targets quick wins and test infrastructure. See [Execution Order](#execution-order) for the full sequence. Rejected items live in `GRAVEYARD.md`. Resurrections require an RFC. `BACKLOG.md` retired — all intake goes directly into this file (policy in `CLAUDE.md`). diff --git a/docs/ROADMAP/COMPLETED.md b/docs/ROADMAP/COMPLETED.md index 6078ded2..eeb85509 100644 --- a/docs/ROADMAP/COMPLETED.md +++ b/docs/ROADMAP/COMPLETED.md @@ -368,3 +368,13 @@ Investigation revealed the correct approach is a two-phase split: | B92 | ~~**SURFACE VALIDATOR UNIT TESTS**~~ — **DONE.** 34 tests for `parseExportBlock`, `extractJsExports`, `extractDtsExports`. | | B93 | ~~**DEDUP EXPORT PARSING LOGIC**~~ — **DONE.** `parseExportBlock()` extracted as shared helper; `collectExportBlocks()` internal. | | B94 | ~~**STANDALONE EXPORT DECLARATIONS**~~ — **DONE.** `extractJsExports` now handles `export const/function/class`. | + +### v13.0.0 Standalone Items (completed) + +| ID | Item | +|----|------| +| B44 | ~~**SUBSCRIBER UNSUBSCRIBE-DURING-CALLBACK E2E**~~ — **DONE (v13.0.0).** 3 edge-case tests (cross-unsubscribe, subscribe-during-callback, unsubscribe-in-onError). | +| B124 | ~~**TRUST PAYLOAD PARITY TESTS**~~ — **DONE (v13.0.0).** 22 tests verifying CLI vs service shape parity. | +| B125 | ~~**`CachedValue` NULL-PAYLOAD SEMANTIC TESTS**~~ — **DONE (v13.0.0).** 3 tests documenting null = "no value" sentinel. | +| B146 | ~~**UNIFY `CorePersistence` / `FullPersistence` TYPEDEFS**~~ — **DONE (v13.0.0).** Replaced `FullPersistence` with imported `CorePersistence`. | +| B148 | ~~**REVIEW NITS + INVENTORY RECONCILIATION**~~ — **DONE (v13.0.0).** Fix inventory counts, COMPLETED.md ordering, stale Deno test name, BisectService invariant comment. | From 88e7b0365c83598b1bb8970c47c58a4d164c9a58 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 4 Mar 2026 04:07:25 -0800 Subject: [PATCH 18/21] fix(roaring): reset nativeAvailability cache on reinit, preserve per-tier load errors - Reset `nativeAvailability = NOT_CHECKED` in `initRoaring()` when called with a new module or after fresh load, preventing stale cached values from `getNativeRoaringAvailable()` after module swap - Apply `unwrapDefault()` to injected modules in `initRoaring(mod)` path so default-export wrapping is handled consistently - Collect per-tier errors in `tryNativeImport()`, `tryCjsRequire()`, and `tryWasmFallback()` and throw `AggregateError` on total failure instead of a plain Error, preserving root cause diagnostics - Add 3 unit tests: cache reset on reinit, sequential reinit with different module types, and default-export unwrapping --- src/domain/utils/roaring.js | 34 ++++++++---- test/unit/domain/utils/roaring.test.js | 75 ++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 test/unit/domain/utils/roaring.test.js diff --git a/src/domain/utils/roaring.js b/src/domain/utils/roaring.js index 0fb43e43..d88c9284 100644 --- a/src/domain/utils/roaring.js +++ b/src/domain/utils/roaring.js @@ -115,13 +115,15 @@ function adaptWasmApi(wasmMod) { /** * Tier 1: ESM dynamic import of native roaring. + * @param {Error[]} errors - Collects per-tier failures for diagnostics * @returns {Promise} * @private */ -async function tryNativeImport() { +async function tryNativeImport(errors) { try { return /** @type {RoaringModule} */ (await import('roaring')); - } catch { + } catch (err) { + errors.push(err instanceof Error ? err : new Error(String(err))); return null; } } @@ -129,32 +131,36 @@ async function tryNativeImport() { /** * Tier 2: CJS require() — works when Vite intercepts import() but can't * transform native C++ addons. + * @param {Error[]} errors - Collects per-tier failures for diagnostics * @returns {Promise} * @private */ -async function tryCjsRequire() { +async function tryCjsRequire(errors) { try { const { createRequire } = await import('node:module'); const req = createRequire(import.meta.url); return /** @type {RoaringModule} */ (req('roaring')); - } catch { + } catch (err) { + errors.push(err instanceof Error ? err : new Error(String(err))); return null; } } /** * Tier 3: WASM fallback — works on Bun (JSC) and Deno without native bindings. + * @param {Error[]} errors - Collects per-tier failures for diagnostics * @returns {Promise} * @private */ -async function tryWasmFallback() { +async function tryWasmFallback(errors) { try { const wasmMod = await import('roaring-wasm'); if (typeof wasmMod.roaringLibraryInitialize === 'function') { await wasmMod.roaringLibraryInitialize(); } return adaptWasmApi(/** @type {RoaringModule} */ (wasmMod)); - } catch { + } catch (err) { + errors.push(err instanceof Error ? err : new Error(String(err))); return null; } } @@ -188,23 +194,29 @@ function unwrapDefault(mod) { */ export async function initRoaring(mod) { if (mod) { - roaringModule = mod; + roaringModule = unwrapDefault(mod); + nativeAvailability = NOT_CHECKED; initError = null; return; } if (roaringModule) { return; } + /** @type {Error[]} */ + const loadErrors = []; roaringModule = - (await tryNativeImport()) ?? - (await tryCjsRequire()) ?? - (await tryWasmFallback()); + (await tryNativeImport(loadErrors)) ?? + (await tryCjsRequire(loadErrors)) ?? + (await tryWasmFallback(loadErrors)); if (!roaringModule) { - throw new Error( + throw new AggregateError( + loadErrors, 'Failed to load roaring via import(), require(), and roaring-wasm', ); } roaringModule = unwrapDefault(roaringModule); + nativeAvailability = NOT_CHECKED; + initError = null; } // Auto-initialize on module load (top-level await) diff --git a/test/unit/domain/utils/roaring.test.js b/test/unit/domain/utils/roaring.test.js new file mode 100644 index 00000000..159332a5 --- /dev/null +++ b/test/unit/domain/utils/roaring.test.js @@ -0,0 +1,75 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +let roaringMod; + +beforeEach(async () => { + vi.resetModules(); + roaringMod = await import('../../../../src/domain/utils/roaring.js'); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe('initRoaring', () => { + it('resets nativeAvailability when called with a new module', async () => { + const { initRoaring, getNativeRoaringAvailable } = roaringMod; + + // Probe native availability to cache a value from the real module + const first = getNativeRoaringAvailable(); + expect([true, false, null]).toContain(first); + + // Reinit with a fake module where isNativelyInstalled => false + const fakeMod = { + RoaringBitmap32: Object.assign(function FakeBitmap() {}, { + isNativelyInstalled: () => false, + }), + }; + await initRoaring(fakeMod); + + // After reinit, availability must reflect the NEW module + const second = getNativeRoaringAvailable(); + expect(second).toBe(false); + }); + + it('resets nativeAvailability on fresh load path', async () => { + const { initRoaring, getNativeRoaringAvailable } = roaringMod; + + // First call caches availability + getNativeRoaringAvailable(); + + // Reinit with a native-style module + const nativeMod = { + RoaringBitmap32: Object.assign(function NativeBitmap() {}, { + isNativelyInstalled: () => true, + }), + }; + await initRoaring(nativeMod); + expect(getNativeRoaringAvailable()).toBe(true); + + // Reinit again with WASM-style module + const wasmMod = { + RoaringBitmap32: Object.assign(function WasmBitmap() {}, { + isNativelyInstalled: () => false, + }), + }; + await initRoaring(wasmMod); + expect(getNativeRoaringAvailable()).toBe(false); + }); + + it('unwraps default exports when called with a module', async () => { + const { initRoaring, getRoaringBitmap32 } = roaringMod; + + const innerBitmap = Object.assign(function WrappedBitmap() {}, { + isNativelyInstalled: () => false, + }); + const wrappedMod = { + default: { RoaringBitmap32: innerBitmap }, + RoaringBitmap32: undefined, + }; + await initRoaring(wrappedMod); + + // Should have unwrapped to the inner module + expect(getRoaringBitmap32()).toBe(innerBitmap); + }); +}); From 8d5f910d02cd8e2df7826424b1c7c897799a98a9 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 4 Mar 2026 04:07:35 -0800 Subject: [PATCH 19/21] docs(readme): fix What's New heading to reflect unreleased status The heading said "v13.0.1" but listed features (new graph algorithms, roaring-wasm fallback) that are in [Unreleased] in CHANGELOG.md. v13.0.1 was only a dev dependency security patch. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c1cedda..90330d45 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ git-warp CLI demo

-## What's New in v13.0.1 +## What's New (Unreleased) - **5 new graph algorithms** — `levels()`, `transitiveReduction()`, `transitiveClosure()`, `rootAncestors()` in `GraphTraversal`, plus BFS reverse reachability verification. All use `NeighborProviderPort` and support cancellation. - **`roaring-wasm` WASM fallback for Bun/Deno bitmap indexes** — bitmap indexes now work on Bun (JSC) and Deno via a three-tier fallback: native V8 bindings → CJS require → WASM. Wire-compatible, byte-identical serialization. From 7b77bc1f848fcff34dd9deae115349c318f583b9 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 4 Mar 2026 04:08:38 -0800 Subject: [PATCH 20/21] docs(changelog): add roaring cache reset and AggregateError fix entries --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf668168..150e910f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - **Roaring native module loading under Bun** — `initRoaring()` now catches dynamic `import('roaring')` failures and falls back to `createRequire()` for direct `.node` binary loading. +- **Stale `nativeAvailability` cache on `initRoaring()` reinit** — `getNativeRoaringAvailable()` now returns the correct value after swapping roaring implementations via `initRoaring(mod)`. Previously, the cached availability from the old module was returned. +- **Lost root causes on roaring load failure** — when all three tiers (native ESM, CJS require, WASM) fail, `initRoaring()` now throws `AggregateError` with per-tier errors instead of a plain `Error`, preserving diagnostic detail. ### Changed From 21dae789a46a180892b3e2e77b0a57d2cc0ef1c8 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 4 Mar 2026 04:09:51 -0800 Subject: [PATCH 21/21] fix(types): add JSDoc type annotations to roaring test module variable --- test/unit/domain/utils/roaring.test.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/unit/domain/utils/roaring.test.js b/test/unit/domain/utils/roaring.test.js index 159332a5..2b36fe1a 100644 --- a/test/unit/domain/utils/roaring.test.js +++ b/test/unit/domain/utils/roaring.test.js @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +/** @type {typeof import('../../../../src/domain/utils/roaring.js')} */ let roaringMod; beforeEach(async () => { @@ -63,10 +64,12 @@ describe('initRoaring', () => { const innerBitmap = Object.assign(function WrappedBitmap() {}, { isNativelyInstalled: () => false, }); - const wrappedMod = { - default: { RoaringBitmap32: innerBitmap }, - RoaringBitmap32: undefined, - }; + const wrappedMod = /** @type {import('../../../../src/domain/utils/roaring.js').RoaringModule} */ ( + /** @type {unknown} */ ({ + default: { RoaringBitmap32: innerBitmap }, + RoaringBitmap32: undefined, + }) + ); await initRoaring(wrappedMod); // Should have unwrapped to the inner module