diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b764338..150e910f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,26 @@ 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]
+
+### 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
+
+- **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
+
+- **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
### Fixed
diff --git a/README.md b/README.md
index 54747593..90330d45 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,10 @@
-## 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.
- **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 +644,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
diff --git a/ROADMAP.md b/ROADMAP.md
index b8872024..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,89 +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. |
-
-### 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 |
---
@@ -284,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)
+```
---
@@ -321,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** | 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** | 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** | **123** total; 29 standalone done | |
+| **Total tracked** | **133** total; 29 standalone done | |
### STANK.md Cross-Reference
@@ -427,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/docker-compose.test.yml b/docker-compose.test.yml
index da25a12d..6ec20fb8 100644
--- a/docker-compose.test.yml
+++ b/docker-compose.test.yml
@@ -32,14 +32,16 @@ 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 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/"]
+ 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 bc0d4f90..ae43a541 100644
--- a/docker/Dockerfile.bun
+++ b/docker/Dockerfile.bun
@@ -3,12 +3,8 @@
# 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.
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
- make \
- g++ \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY git-warp/package*.json ./
@@ -26,4 +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 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/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. |
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/package-lock.json b/package-lock.json
index 1e0ec691..8d83a46e 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",
@@ -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"
@@ -40,7 +41,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 +136,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 +149,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 +166,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 +183,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 +200,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 +217,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 +234,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 +251,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 +268,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 +285,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 +302,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 +319,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 +336,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 +353,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 +370,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 +387,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 +404,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 +421,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 +455,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 +489,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 +523,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 +540,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 +557,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 +574,7 @@
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
@@ -1220,6 +1272,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 +1578,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 +1623,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 +1937,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 +2064,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 +2085,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 +2255,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 +2390,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 +2400,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 +3390,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 +3823,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 +4028,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 +4055,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": [
{
@@ -4169,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",
@@ -4451,11 +4495,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 +4521,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 +4690,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 +4716,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 +4755,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 +4816,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..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"
@@ -122,7 +123,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/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/src/domain/utils/roaring.js b/src/domain/utils/roaring.js
index 79ab922d..d88c9284 100644
--- a/src/domain/utils/roaring.js
+++ b/src/domain/utils/roaring.js
@@ -97,27 +97,126 @@ 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 {RoaringModule} wasmMod - The loaded `roaring-wasm` module
+ * @returns {RoaringModule} Adapted module with `isNativelyInstalled` shim
+ * @private
+ */
+function adaptWasmApi(wasmMod) {
+ wasmMod.RoaringBitmap32.isNativelyInstalled = () => false;
+ return wasmMod;
+}
+
+/**
+ * Tier 1: ESM dynamic import of native roaring.
+ * @param {Error[]} errors - Collects per-tier failures for diagnostics
+ * @returns {Promise}
+ * @private
+ */
+async function tryNativeImport(errors) {
+ try {
+ return /** @type {RoaringModule} */ (await import('roaring'));
+ } catch (err) {
+ errors.push(err instanceof Error ? err : new Error(String(err)));
+ return null;
+ }
+}
+
+/**
+ * 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(errors) {
+ try {
+ const { createRequire } = await import('node:module');
+ const req = createRequire(import.meta.url);
+ return /** @type {RoaringModule} */ (req('roaring'));
+ } 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(errors) {
+ try {
+ const wasmMod = await import('roaring-wasm');
+ if (typeof wasmMod.roaringLibraryInitialize === 'function') {
+ await wasmMod.roaringLibraryInitialize();
+ }
+ return adaptWasmApi(/** @type {RoaringModule} */ (wasmMod));
+ } catch (err) {
+ errors.push(err instanceof Error ? err : new Error(String(err)));
+ 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}
*/
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(loadErrors)) ??
+ (await tryCjsRequire(loadErrors)) ??
+ (await tryWasmFallback(loadErrors));
if (!roaringModule) {
- roaringModule = /** @type {RoaringModule} */ (await import('roaring'));
- // Handle both ESM default export and CJS module.exports
- if (roaringModule.default && roaringModule.default.RoaringBitmap32) {
- roaringModule = roaringModule.default;
- }
+ 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/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/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/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);
+ });
+ });
+});
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 };
});
diff --git a/test/unit/domain/utils/roaring.test.js b/test/unit/domain/utils/roaring.test.js
new file mode 100644
index 00000000..2b36fe1a
--- /dev/null
+++ b/test/unit/domain/utils/roaring.test.js
@@ -0,0 +1,78 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+
+/** @type {typeof import('../../../../src/domain/utils/roaring.js')} */
+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 = /** @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
+ expect(getRoaringBitmap32()).toBe(innerBitmap);
+ });
+});
diff --git a/vitest.config.js b/vitest.config.js
index 4a697ee0..9fb58f65 100644
--- a/vitest.config.js
+++ b/vitest.config.js
@@ -1,11 +1,21 @@
import { defineConfig } from 'vitest/config';
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', 'roaring-wasm'],
+ },
test: {
include: [
'**/*.{test,spec}.?(c|m)[jt]s?(x)',
'**/benchmark/*.benchmark.js',
],
testTimeout: 60000, // 60s timeout for benchmark tests
+ server: {
+ deps: {
+ external: [/roaring/],
+ },
+ },
},
});