diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a899af8..207e8fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,13 @@ jobs: test: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - node: [18, 20, 22] + # The published package supports Node >= 18.17, so we type-check, build + # and pack on Node 18. The test runner (Vitest 4) requires Node >= 20 + # (it imports `styleText` from node:util), so the Test step below is + # skipped on Node 18 and runs on 20/22/24. + node: [18, 20, 22, 24] steps: - name: Checkout uses: actions/checkout@v4 @@ -50,6 +55,8 @@ jobs: run: yarn build - name: Test + # Vitest 4 requires Node >= 20; Node 18 verifies build + pack only. + if: matrix.node != '18' run: yarn test - name: Pack (sanity) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..531af67 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,168 @@ +# Changelog + +All notable changes to this project are documented here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.3.0] + +### Fixed (mathematical correctness) + +Every fix is verified against an independent brute-force enumeration (see +`tests/math-correctness.test.ts`). + +- **`DiceQuery.probabilityOf(label)` over-counted.** It summed the full `bin.p` + of every combined bin that merely *contained* a label, but bins hold multiple + mutually-exclusive outcomes — so `probabilityOf('crit')` returned 0.49 where + the true P(crit)=0.05. It now returns the correct Poisson-binomial marginal + (= `probAtLeastOne`). `missChance()` is fixed by the same change. +- **`DiceQuery.probExactlyK([labels], k)` array-path** delegated to the buggy + `probabilityOf`, disagreeing with the (correct) single-label string path; both + now match the true binomial. +- **`DiceQuery.variance()/stddev()`** used the unstable `E[X²]−E[X]²` form and + lost all precision under a large constant damage offset (`1d6 + 1e8` gave + variance 2 instead of 35/12). Now uses the centered, additive-per-single form. +- **`DiceQuery.mean()/variance()`** now stay consistent with an explicitly + supplied `combined` that diverges from `convolve(singles)`. +- **`PMF.convolve()` produced `NaN`** for a zero-mass operand (divide-by-zero in + the mass rescale), silently poisoning `DiceQuery.combined`. A zero-mass + convolution now correctly yields mass 0. +- **`probAtLeastOne` is now mass-invariant** (per-attack probability divided by + the single's mass) and clamped to `[0,1]` (was returning `1.0000000002`). +- **`PMF.firstSuccessWeights`** throws on `pSpecial > pSuccess` instead of + returning out-of-range probabilities. +- **Parser `hd6`/`hd20` (reroll-one)** used a weighted union giving + `P(1)=1/(2s−1)`; now uses `reroll(1)` for the correct `P(1)=1/s²`. The parser + `hd` distribution now matches the builder's `reroll(1)` exactly (the + previously loosened tests are tightened). +- **`DiceQuery.snapshot()` outcome probabilities** (`atLeastOneProbability`, + `allProbability`) were aggregated as expected counts and could exceed 1 for + multi-attack queries. They now use the correct Poisson-binomial marginals + (P(≥1) and P(all)) and are always in [0,1]. (`damageRange.avg` remains a + size-biased mean for N≥2 — see Known limitations.) +- **Parser save-for-half mislabeled outcomes** on odd/constant damage (e.g. + `(d20 DC 15) * (3) save half`): the brittle "2×half ∈ hit" detection + false-negatived, tagging the success mass as `saveFail` and the failure mass + as `hit`. Detection is now deterministic (the presence of a save distribution), + so `saveHalf`/`saveFail` are always labeled correctly. +- **`PMF.compact()` corrupted PMFs that shared bin objects.** It deleted + sub-epsilon `count`/`attr` entries *in place* and reused that same bin + reference in the compacted map. Because bins are shared by reference across + PMFs (the `branch()` / `addScaled()` / `scaleMass()` fast paths can carry + another PMF's bin objects), this silently mutated the source PMF — and the + receiver's own bins. `compact()` now clones each surviving bin before pruning; + the compacted result is unchanged. + +### Security / hardening + +- **Parser resource-exhaustion guards.** Adversarial expressions are rejected + with a `DiceParseError` instead of exhausting CPU/memory: a die over 1,000,000 + faces, a dice count over 10,000, a keep whose `faces^count` enumeration would + exceed 1,000,000 outcomes, and a binary operation whose `faces₁ × faces₂` work + would exceed 100,000,000 face pairs. The last closes a gap the per-operand + caps missed — two individually-legal large dice (e.g. `d100000 + d100000`, + ~10¹⁰ operations) previously hung for tens of seconds. All legitimate + expressions, including `d100000`, still parse. + +### Known limitations (documented; recommend maintainer review) + +These are real but require API/architecture decisions, so they are documented +and pinned by tests rather than changed blindly: + +- **Parser crit probability with bonus to-hit dice is wrong.** With bonus dice in + the to-hit (e.g. Bless, `d20 + 5 + 1d4`), the string parser collapses crit to + `1/(20·∏bonusSides)` (and the DPR is off by a few %), because the natural-20 + slice can't be separated after the bonus dice are convolved. **The builder API + computes it correctly** — use `d20.plus(..).plus(bonusDie).ac(..).onCrit(..)`. +- **Multi-attack conditional damage `avg` is size-biased.** The `avg` returned by + `damageStatsFrom()` (single label), `outcomeDamageRanges()` and + `snapshot().damageRange` aggregates the combined PMF's `count` (an *expected + count* for N≥2 attacks), so it is the size-biased mean E[dmg·#label]/E[#label] + rather than a clean conditional expectation. It is correct for a single attack. + (The associated *probabilities* are now correct — see Fixed.) +- **`PMF.mixN`/`gate`/`branch` build O(2ⁿ) identifier strings**, which can blow up + (multi-MB, eventual `RangeError`) for very deep (≈20+) gate chains. Prefer + `PMF.exclusive`/`PMF.mix` for large mixtures. + +### Breaking + +- **`PMF.toJSON()` now returns a plain object** (`{ bins, normalized, identifier }`) + instead of a JSON string, following the standard `toJSON` contract. This means + `JSON.stringify(pmf)` no longer double-encodes. If you relied on the old string + return, call the new `PMF.toJSONString()` instead. +- **`DiceQuery.firstSuccessSplit()` is typed as `OutcomeType | OutcomeType[]`** + (previously `string | string[]`). Only affects callers passing arbitrary strings; + valid outcome labels are unchanged. + +### Added + +- **`DiceParseError`** — `parse()` now throws this typed error (a subclass of + `Error`) instead of a plain `Error`. Existing `try/catch` and message checks keep + working; you can now narrow with `instanceof DiceParseError` and read + `error.expression` / `error.cause`. +- **`PMF.toJSONString()`** — returns the JSON string form (the previous + `toJSON()` behavior). +- **`DiceQuery.stdev()`** — alias of `stddev()`, matching `PMF.stdev()`. +- **`PMF.hasAttribution()`** — O(1) check for whether a PMF already carries + damage-attribution metadata. + +### Performance + +- **`DiceQuery.mean()` / `variance()` / `stddev()` use moment additivity** + (`E[ΣX]=ΣE[X]`, `Var[ΣX]=ΣVar[X]`) computed directly from the single PMFs. +- **`DiceQuery.combined` is now built lazily** (on first access) instead of in + the constructor. Combined with the above, a query used only for DPR / mean / + variance never performs the N-way convolution — multi-attack stats-only + queries are ~10000× faster (e.g. ~17 ms → ~0.001 ms for a heavy 4-attack + expression). The materialized `combined` distribution is unchanged; mean and + variance may differ from the previous convolution-based values by at most a + few ULP (well within the library's tolerances). +- **`DiceQuery.combinedWithAttribution()` reuses `combined`** when every single + already carries attribution (as parser-generated PMFs do), avoiding a + redundant convolution pass. Result is bit-for-bit identical. +- **`PMF.convolve()` inner loop accumulates directly into destination bins** + instead of allocating a temporary bin per term and merging — ~1.6× faster + convolution (the cost of building the combined distribution for charts). The + probability channel is bit-identical; per-label `count`/`attr` provenance may + re-associate by at most a few ULP (≤1e-14 even at 16 attacks, ~100× below the + eps pruning threshold). +- **Convolution cache-key fingerprint is memoized** on each (immutable) PMF + instead of re-summing every bin key on every `convolve()` call — ~36% faster + on warm cache hits. Bit-identical (`PMF.fingerprint()` returns the same string). +- **`Dice.calculateHitDistribution()` no longer clones outcome distributions per + face** — it reads the stored maps once instead of `O(faces × outcomes)` clones, + ~10% faster cold parsing of wide-support expressions. Bit-identical. +- **`DiceQuery.toStackedChartData()` drops a dead `O(N×L)` precomputation pass** + whose result was discarded — ~2× faster. Bit-identical. +- Minor bit-identical cleanups on the parse path (`Dice.toPMF` iterates the + internal face map directly; `multiplyDiceByDice` uses a `Map`). +- **`DiceQuery` count queries (`probExactlyK` / `probAtLeastK` / `probAtMostK`, + array-label paths)** compute each attack's success probability and the binomial + DP once instead of rebuilding a query per requested count (~3× on the looped + variants). +- **`PMF.branch()` assembles its Bernoulli mixture in a single pass** rather than + chaining two `addScaled` calls (which copied the failure branch's bins twice). +- **`keepSumPMF` packs its DP state into a single integer key** instead of a + `"used|r"` string parsed on every transition. +- **`computeMaxOfPMF` walks the support once with a running CDF** for large pools, + reducing the max-of computation from O(N²) to O(N). +- **`Dice.reroll()` uses a `Set` for membership** and **`Dice.binaryOp()` hoists + the inner die's face list** out of its loop. + + All of the above were verified bit-for-bit identical (probabilities, counts, + means, variance) across the full expression corpus. + +### Changed + +- Removed the stale `package-lock.json` (the project uses Yarn 4) and dropped the + unused `ts-node` / `tsconfig-paths` dev dependencies. +- Removed `console.error` calls from the parser so the library no longer writes to + a consumer's console. +- Internal refactors with no behavioral change: deduplicated `Bin` clone/scale + logic in `PMF`, removed dead code and impossible iterator branches, and tightened + internal `any` usage. +- `Dice.outcomeData` is typed `Partial>` (dropping an + unsound `as Record<…>` cast); `getFullOutcomeDistribution()`'s return type + matches. Type-only change; runtime output is unchanged. +- Added a `yarn format` script (ESLint autofix). diff --git a/README.md b/README.md index 6eeae2e..7293e44 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,23 @@ const query = new DiceQuery(pmf); console.log("DPR:", query.mean()); ``` +### Error Handling + +`parse()` throws a `DiceParseError` (a subclass of `Error`) for invalid input. +Narrow with `instanceof` and inspect the offending `expression`: + +```ts +import { parse, DiceParseError } from "@yipe/dice"; + +try { + parse("d6@3"); +} catch (err) { + if (err instanceof DiceParseError) { + console.warn(`Bad dice expression: ${err.expression}`); + } +} +``` + ### Sneak Attack (Conditional Damage) Conditional damage ("once-per-turn damage riders") like Sneak Attack can be modeled easily: diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index d795d09..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2792 +0,0 @@ -{ - "name": "dice", - "version": "0.2.23", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "dice", - "version": "0.2.23", - "devDependencies": { - "@types/node": "^24.3.1", - "ts-node": "^10.9.2", - "tsconfig-paths": "^4.2.0", - "tsup": "^8.5.0", - "tsx": "^4.20.5", - "typescript": "^5.9.2", - "vitest": "^3.2.4" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", - "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", - "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", - "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", - "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", - "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", - "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", - "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", - "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", - "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", - "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", - "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", - "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", - "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", - "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", - "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", - "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", - "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", - "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", - "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", - "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", - "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", - "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", - "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.10.0" - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" - } - }, - "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/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "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" - } - }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "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/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "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": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/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", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/rollup": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", - "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.50.1", - "@rollup/rollup-android-arm64": "4.50.1", - "@rollup/rollup-darwin-arm64": "4.50.1", - "@rollup/rollup-darwin-x64": "4.50.1", - "@rollup/rollup-freebsd-arm64": "4.50.1", - "@rollup/rollup-freebsd-x64": "4.50.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", - "@rollup/rollup-linux-arm-musleabihf": "4.50.1", - "@rollup/rollup-linux-arm64-gnu": "4.50.1", - "@rollup/rollup-linux-arm64-musl": "4.50.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", - "@rollup/rollup-linux-ppc64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-musl": "4.50.1", - "@rollup/rollup-linux-s390x-gnu": "4.50.1", - "@rollup/rollup-linux-x64-gnu": "4.50.1", - "@rollup/rollup-linux-x64-musl": "4.50.1", - "@rollup/rollup-openharmony-arm64": "4.50.1", - "@rollup/rollup-win32-arm64-msvc": "4.50.1", - "@rollup/rollup-win32-ia32-msvc": "4.50.1", - "@rollup/rollup-win32-x64-msvc": "4.50.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "deprecated": "The work that was done in this beta branch won't be included in future versions", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-literal": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", - "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/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": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", - "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tsup": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.0.tgz", - "integrity": "sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.25.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "0.8.0-beta.0", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" - }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/tsx": { - "version": "4.20.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", - "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", - "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.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": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.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 - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - } - } -} diff --git a/package.json b/package.json index 2d26ffe..bf3a3f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@yipe/dice", - "version": "0.2.23", + "version": "0.3.0", "description": "A high-performance dice probability engine for D&D 5e DPR calculations. Powers dprcalc.com.", "keywords": [ "dnd", @@ -62,6 +62,7 @@ "./dist/builder/dc.js" ], "scripts": { + "format": "eslint . --fix", "lint": "eslint .", "typecheck": "tsc -p config/tsconfig.build.json --noEmit", "build": "tsup --config config/tsup.config.ts", @@ -79,16 +80,14 @@ "node": ">=18.17" }, "devDependencies": { - "@types/node": "^24.13.2", - "@typescript-eslint/eslint-plugin": "^8.61.0", - "@typescript-eslint/parser": "^8.61.0", - "@vitest/coverage-v8": "^4.1.8", - "eslint": "^9.0.0", - "ts-node": "^10.9.2", - "tsconfig-paths": "^4.2.0", + "@types/node": "^26.0.1", + "@typescript-eslint/eslint-plugin": "^8.62.0", + "@typescript-eslint/parser": "^8.62.0", + "@vitest/coverage-v8": "^4.1.9", + "eslint": "^10.6.0", "tsup": "^8.5.1", "tsx": "^4.22.4", "typescript": "^6.0.3", - "vitest": "^4.1.8" + "vitest": "^4.1.9" } } diff --git a/src/builder/ac.test.ts b/src/builder/ac.test.ts index c035d5e..b050368 100644 --- a/src/builder/ac.test.ts +++ b/src/builder/ac.test.ts @@ -102,8 +102,8 @@ describe("AttackRollBuilder", () => { const copy = original.copy(); expect(copy.toExpression()).toBe(original.toExpression()); - const a = JSON.parse(copy.toPMF().toJSON()); - const b = JSON.parse(original.toPMF().toJSON()); + const a = JSON.parse(copy.toPMF().toJSONString()); + const b = JSON.parse(original.toPMF().toJSONString()); delete a.identifier; delete b.identifier; expect(a).toEqual(b); diff --git a/src/builder/ast.ts b/src/builder/ast.ts index 8de90f0..005f5dd 100644 --- a/src/builder/ast.ts +++ b/src/builder/ast.ts @@ -8,6 +8,7 @@ import type { DieNode, ExpressionNode, KeepNode, + MaxOfNode, SumNode, } from "./nodes"; import type { RollBuilder } from "./roll"; @@ -80,7 +81,11 @@ export function astFromRollConfigs( if (trials === 1) { node = perTrial; } else { - node = { type: "maxOf", count: trials, child: perTrial } as any; + node = { + type: "maxOf", + count: trials, + child: perTrial, + } as MaxOfNode; } } else if (trials === baseCount) { // Classic pool: keep K of N faces from N iid dice @@ -366,16 +371,19 @@ function computeMaxOfPMF( dfs(count, -Infinity, 1); } else { - // For larger cases, use the CDF method + // For larger cases, use the CDF method. Walk the sorted support once while + // accumulating a running CDF, so each P(max = v) costs O(1) instead of a + // full-map cdfAt() scan (previously O(N) per value → O(N²) overall). + // Between two consecutive support points there is no probability mass, so + // the running CDF up to (but not including) v equals cdfAt(v - 1). const sortedSupport = [...support].sort((a, b) => a - b); + let runningCdf = 0; for (const value of sortedSupport) { - // P(max = value) = P(all rolls <= value) - P(all rolls <= value-1) - const cdfAtValue = pmf.cdfAt(value); - const cdfAtValueMinus1 = - value > sortedSupport[0] ? pmf.cdfAt(value - 1) : 0; + const prevCdf = runningCdf; + runningCdf += pmf.pAt(value); - const probMax = - Math.pow(cdfAtValue, count) - Math.pow(cdfAtValueMinus1, count); + // P(max = value) = P(all rolls <= value) - P(all rolls <= value-1) + const probMax = Math.pow(runningCdf, count) - Math.pow(prevCdf, count); if (probMax > eps) { out.set(value, probMax); } @@ -425,8 +433,13 @@ function keepSumPMF( // Select t = min(X, keep - used) into the sum (highest picks first), then continue with r - X. type SumMap = Map; - let state: Map = new Map(); - const keyOf = (used: number, r: number) => `${used}|${r}`; + let state: Map = new Map(); + // Pack the (used, remainingTrials) state into a single integer key instead of + // a "used|r" string. r ∈ [0, total], so a stride of (total + 1) is collision + // free, and decoding is plain integer math — no split()/parseInt() per + // transition in the hot loop. Behavior is identical (same states, same order). + const stride = total + 1; + const keyOf = (used: number, r: number) => used * stride + r; state.set(keyOf(0, total), new Map([[0, 1]])); @@ -470,9 +483,9 @@ function keepSumPMF( return out.size === m.size ? m : out; }; - const pruneState = (st: Map, threshold: number) => { + const pruneState = (st: Map, threshold: number) => { if (threshold <= 0) return st; - const out = new Map(); + const out = new Map(); for (const [k, m] of st) { const mm = pruneMap(m, threshold); if (mm.size > 0) out.set(k, mm); @@ -486,12 +499,11 @@ function keepSumPMF( if (p <= 0) continue; const q = Math.max(eps, 1 - processedMass); const pCond = Math.min(1, p / q); - const next: Map = new Map(); + const next: Map = new Map(); for (const [k, m] of state) { - const [usedStr, rStr] = k.split("|"); - const used = parseInt(usedStr, 10); - const r = parseInt(rStr, 10); + const used = Math.floor(k / stride); + const r = k - used * stride; if (r === 0) { // No trials left; carry state forward unchanged const destKey = keyOf(used, 0); diff --git a/src/builder/attack.ts b/src/builder/attack.ts index 7c00a2c..4ac9567 100644 --- a/src/builder/attack.ts +++ b/src/builder/attack.ts @@ -140,8 +140,8 @@ export class AttackBuilder implements CheckBuilder { let pcrit = 0; let pmiss = 0; - for (const [r, rec] of d20 as any as Iterable<[number, any]>) { - const pr = typeof rec === "number" ? rec : rec.p; + for (const [r, bin] of d20) { + const pr = bin.p; if (pr <= 0) continue; // Natural 1 always misses @@ -165,8 +165,8 @@ export class AttackBuilder implements CheckBuilder { if (check instanceof AlwaysHitBuilder) { // Preserve rollType for crit odds let pCrit = 0; - for (const [r, rec] of d20 as any as Iterable<[number, any]>) { - const pr = typeof rec === "number" ? rec : rec.p; + for (const [r, bin] of d20) { + const pr = bin.p; if (pr <= 0) continue; if (r >= critThreshold) pCrit += pr; } @@ -188,8 +188,8 @@ export class AttackBuilder implements CheckBuilder { let phit = 0; let pmiss = 0; - for (const [r, rec] of d20 as any as Iterable<[number, any]>) { - const pr = typeof rec === "number" ? rec : rec.p; + for (const [r, bin] of d20) { + const pr = bin.p; if (pr <= 0) continue; // Handle auto-miss diff --git a/src/builder/dc.ts b/src/builder/dc.ts index aa762be..ddf22ca 100644 --- a/src/builder/dc.ts +++ b/src/builder/dc.ts @@ -79,8 +79,8 @@ export class DCBuilder extends RollBuilder { : PMF.delta(0, eps); let psuccess = 0; - for (const [r, rec] of d20 as any as Iterable<[number, any]>) { - const pr = typeof rec === "number" ? rec : rec.p; + for (const [r, bin] of d20) { + const pr = bin.p; if (pr <= 0) continue; const need = saveDC - staticMod - r; psuccess += pr * bonusPMF.tailProbGE(need); diff --git a/src/builder/roll.ts b/src/builder/roll.ts index f37ae48..170a495 100644 --- a/src/builder/roll.ts +++ b/src/builder/roll.ts @@ -752,8 +752,10 @@ export class HalfRollBuilder extends RollBuilder { // Which is fine. // The only issue is if we want it to return HalfRollBuilder, but it doesn't seem designed for that. - get lastConfig() { - return (this.innerRoll as any).lastConfig; + override get lastConfig(): RollConfig { + // `lastConfig` is protected on the base class; reach it on the wrapped + // instance via a typed view rather than `any`. + return (this.innerRoll as unknown as { lastConfig: RollConfig }).lastConfig; } getSubRollConfigs(): readonly RollConfig[] { @@ -795,8 +797,10 @@ export class MaxOfRollBuilder extends RollBuilder { return this.innerRoll.hasHiddenState(); } - get lastConfig() { - return (this.innerRoll as any).lastConfig; + override get lastConfig(): RollConfig { + // `lastConfig` is protected on the base class; reach it on the wrapped + // instance via a typed view rather than `any`. + return (this.innerRoll as unknown as { lastConfig: RollConfig }).lastConfig; } getSubRollConfigs(): readonly RollConfig[] { diff --git a/src/builder/save.ts b/src/builder/save.ts index b32c6e7..d15591f 100644 --- a/src/builder/save.ts +++ b/src/builder/save.ts @@ -90,8 +90,8 @@ function resolveProbabilities(check: DCBuilder): { const die = d20RollPMF(d20Type, baseReroll > 0); const faceP = new Map(); - for (const [r, rec] of die as any as Iterable<[number, any]>) { - const pr = typeof rec === "number" ? rec : rec.p; + for (const [r, bin] of die) { + const pr = bin.p; if (pr > 0) faceP.set(r, pr); } diff --git a/src/builder/string-damage.test.ts b/src/builder/string-damage.test.ts index 9c38bcc..a939375 100644 --- a/src/builder/string-damage.test.ts +++ b/src/builder/string-damage.test.ts @@ -78,15 +78,14 @@ describe("String Damage Expressions", () => { expect(stringRes.weights.crit).toBeCloseTo(builderRes.weights.crit, 10); expect(stringRes.weights.miss).toBeCloseTo(builderRes.weights.miss, 10); - // Check damage distributions match - // Note: parser's hd syntax uses different algorithm than builder's reroll(1), - // so we expect slight differences in the distributions - expect(stringRes.hit.mean()).toBeCloseTo(builderRes.hit.mean(), 0); + // The parser's hd syntax now uses the same reroll(1) semantics as the + // builder, so the distributions match exactly. + expect(stringRes.hit.mean()).toBeCloseTo(builderRes.hit.mean(), 10); expect(stringRes.hit.min()).toBe(builderRes.hit.min()); expect(stringRes.hit.max()).toBe(builderRes.hit.max()); // Check overall PMF mean - expect(stringRes.pmf.mean()).toBeCloseTo(builderRes.pmf.mean(), 0); + expect(stringRes.pmf.mean()).toBeCloseTo(builderRes.pmf.mean(), 10); }); it("should parse '1hd12+3' with explicit crit and match roll(1).d(12).reroll(1).plus(3)", () => { @@ -103,9 +102,9 @@ describe("String Damage Expressions", () => { const stringRes = stringAttack.resolve(); const builderRes = builderAttack.resolve(); - // Note: parser's hd syntax uses different algorithm than builder's reroll(1) - expect(stringRes.pmf.mean()).toBeCloseTo(builderRes.pmf.mean(), 0); - expect(stringRes.hit.mean()).toBeCloseTo(builderRes.hit.mean(), 0); + // Parser hd now matches builder reroll(1) exactly. + expect(stringRes.pmf.mean()).toBeCloseTo(builderRes.pmf.mean(), 10); + expect(stringRes.hit.mean()).toBeCloseTo(builderRes.hit.mean(), 10); }); }); diff --git a/src/common/errors.ts b/src/common/errors.ts new file mode 100644 index 0000000..8e1b61b --- /dev/null +++ b/src/common/errors.ts @@ -0,0 +1,34 @@ +/** + * Error thrown when a dice expression cannot be parsed. + * + * Extends the built-in {@link Error}, so existing `catch (e)` / message checks + * continue to work, while callers can now narrow with `instanceof DiceParseError`. + * + * @example + * try { + * parse("d6@3"); + * } catch (e) { + * if (e instanceof DiceParseError) { + * // e.expression === "d6@3" + * } + * } + */ +export class DiceParseError extends Error { + /** The original expression that failed to parse, when available. */ + readonly expression?: string; + + /** The underlying error that triggered this one, when available. */ + readonly cause?: unknown; + + constructor( + message: string, + options?: { expression?: string; cause?: unknown } + ) { + super(message); + this.name = "DiceParseError"; + this.expression = options?.expression; + this.cause = options?.cause; + // Restore the prototype chain for reliable `instanceof` across targets. + Object.setPrototypeOf(this, DiceParseError.prototype); + } +} diff --git a/src/index.ts b/src/index.ts index ad5c73c..f9bfdd1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +export * from "./common/errors"; export * from "./common/lru-cache"; export * from "./common/types"; export * from "./parser/parser"; diff --git a/src/parser/dice.ts b/src/parser/dice.ts index e361c30..ea89618 100644 --- a/src/parser/dice.ts +++ b/src/parser/dice.ts @@ -1,5 +1,6 @@ // dice.ts (internal) +import { DiceParseError } from "../common/errors"; import type { Bin, DamageDistribution, @@ -9,16 +10,36 @@ import type { import { EPS } from "../common/types"; import { PMF } from "../pmf/pmf"; +/** + * Work budget for a single dice×dice operation. binaryOp is O(faces₁ × faces₂), + * so two individually-legal large dice (e.g. `d100000 + d100000`) would otherwise + * enumerate ~10^10 face pairs and hang. The parser's per-die / per-count caps + * bound each operand but not their product, so this bounds the operation itself. + * Generous enough for any realistic expression; only pathological blow-ups trip + * it. (Chosen so expressions well beyond normal use still parse; tune if needed.) + */ +const MAX_BINARY_OUTCOMES = 100_000_000; + +/** Internal bookkeeping attached to a {@link Dice} during parsing. */ +export interface DicePrivateData { + /** Marks a DC (saving-throw) check so outcomes are attributed correctly. */ + isDCCheck?: boolean; + /** The "other" distribution recorded by {@link Dice.combine}. */ + except?: Dice | Record; + /** Keep-highest/lowest selector applied when a die is multiplied out. */ + keep?: (values: number[]) => number; +} + /** * @internal */ export class Dice { private readonly faces: DamageDistribution = {}; - public privateData: Record = {}; - private outcomeData: Record = {} as Record< - OutcomeType, - DamageDistribution - >; + public privateData: DicePrivateData = {}; + // Partial: the object starts empty and gains keys as outcomes are recorded, + // so the type must not claim every OutcomeType is present. (Previously typed + // as a full Record via an `as` cast, which lied about missing keys.) + private outcomeData: Partial> = {}; private hasHitDistributionCalculated = false; public identifier?: string; @@ -40,9 +61,8 @@ export class Dice { return { ...distribution }; } - getFullOutcomeDistribution(): Record< - OutcomeType, - DamageDistribution | undefined + getFullOutcomeDistribution(): Partial< + Record > { return { ...this.outcomeData }; } @@ -91,21 +111,28 @@ export class Dice { calculateHitDistribution(): DamageDistribution { const hitValues: DamageDistribution = {}; + // Hoist the per-outcome distributions out of the face loop: they are + // constant across faces, so fetching (and previously cloning) them once is + // O(faces + outcomes) instead of O(faces × outcomes). The stored maps are + // only read here, never mutated, so reading them directly is safe and + // produces identical counts. + const subtractedOutcomes: (DamageDistribution | undefined)[] = [ + this.outcomeData.crit, + this.outcomeData.missNone, + this.outcomeData.missDamage, + this.outcomeData.saveHalf, + this.outcomeData.saveFail, + this.outcomeData.pc, + ]; + for (const [face, totalCount] of Object.entries(this.faces)) { const numFace = Number(face); let hitCount = totalCount; - for (const outcomeType of [ - "crit", - "missNone", - "missDamage", - "saveHalf", - "saveFail", - "pc", - ] as Partial[]) { - const distribution = this.getOutcomeDistribution(outcomeType); - if (distribution && distribution[numFace]) { - hitCount -= distribution[numFace]; + for (const distribution of subtractedOutcomes) { + const outcomeCount = distribution?.[numFace]; + if (outcomeCount) { + hitCount -= outcomeCount; } } @@ -114,9 +141,9 @@ export class Dice { hitCount = 0; } - //WARNING: This is checking for the bug in old inclusion-exclusion logic. + // Defensive clamp: guards against negative hit counts from older + // inclusion-exclusion logic. Should never trigger in practice. if (hitCount < 0) { - console.error("hitCount is <=0?", face, totalCount, hitCount); hitCount = 0; } hitValues[numFace] = hitCount; @@ -144,6 +171,18 @@ export class Dice { const isScalar = typeof other === "number"; const keys1 = this.keys(); + // Hoist the inner die's faces out of the outer loop: they are constant + // across key1, so computing them once avoids re-allocating the array on + // every outer iteration. + const keys2 = isScalar ? [] : (other as Dice).keys(); + + // Guard the O(faces₁ × faces₂) work before running it: a binary op between + // two large (but individually legal) dice would otherwise blow up. + if (!isScalar && keys1.length * keys2.length > MAX_BINARY_OUTCOMES) { + throw new DiceParseError( + `Dice operation over ${keys1.length}×${keys2.length} face pairs exceeds the maximum of ${MAX_BINARY_OUTCOMES}` + ); + } for (const key1 of keys1) { const value1 = this.faces[key1]!; @@ -152,7 +191,6 @@ export class Dice { const resultKey = op(key1, other as number); result.increment(resultKey, value1); } else { - const keys2 = other.keys(); for (const key2 of keys2) { const value2 = other.faces[key2]!; const resultKey = op(key1, key2); @@ -179,7 +217,7 @@ export class Dice { return result; } - // PUBLIC FUNTIONS + // PUBLIC FUNCTIONS getFaceEntries(): [number, number][] { return Object.entries(this.faces).map(([k, v]) => [Number(k), v]); @@ -356,11 +394,13 @@ export class Dice { const rerollDice = typeof toReroll === "number" ? Dice.scalar(toReroll) : toReroll; - const removed = this.removeFaces(rerollDice.keys()); + const rerollKeys = rerollDice.keys(); + const rerollSet = new Set(rerollKeys); + const removed = this.removeFaces(rerollKeys); let result = new Dice(); for (const face of this.keys()) { - const wasRerolled = rerollDice.keys().includes(face); + const wasRerolled = rerollSet.has(face); result = result.combine(removed); if (wasRerolled) { result = result.combine(this); @@ -451,21 +491,20 @@ export class Dice { const saveDistro = this.getOutcomeDistribution("saveHalf") || {}; const pcDistro = this.getOutcomeDistribution("pc") || {}; - let isSaveHalf = false; - for (const halfDamage of Object.keys(saveDistro).map(Number)) { - const fullDamage = halfDamage * 2; - if (fullDamage > 0 && hitDistro[fullDamage]) { - isSaveHalf = true; - break; - } - } + // A non-empty save distribution means the "save" mechanic was used (e.g. + // "save half"): its success mass is a saveHalf outcome and the remaining + // (full-damage) mass is a saveFail. The previous heuristic — "isSaveHalf iff + // 2×(a half value) appears in the hit distribution" — false-negatived on odd + // or constant damage, mislabeling saveHalf as saveFail and saveFail as hit. + const isSaveHalf = Object.keys(saveDistro).length > 0; const isDCCheck = this.privateData.isDCCheck === true; const clampNonNeg = (x: number) => (x < 0 && x > -1e-15 ? 0 : x); - // Process each face value - for (const [faceStr, faceCountRaw] of Object.entries(this.getFaceMap())) { + // Process each face value (iterate the internal map directly; this loop + // only reads it, so the defensive clone from getFaceMap() is unnecessary) + for (const [faceStr, faceCountRaw] of Object.entries(this.faces)) { const face = Number(faceStr); const faceCount = Number(faceCountRaw); @@ -564,10 +603,9 @@ export class Dice { map.set(face, bin); } + // Fall back to a sentinel identifier if none was assigned. This indicates + // a Dice constructed outside the normal parse/build flow. const identifier = this.identifier || "ERROR"; - if (identifier === "ERROR") { - console.error("Dice identifier is undefined", this); - } // Normalize, drop zeros again if any snuck in, then compaction and pruning return new PMF(map, numEpsilon, true, identifier).compact(numEpsilon, true); diff --git a/src/parser/parser.ts b/src/parser/parser.ts index a3c3e48..8c5f17f 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -1,3 +1,4 @@ +import { DiceParseError } from "../common/errors"; import { LRUCache } from "../common/lru-cache"; import type { OutcomeType } from "../common/types"; import type { PMF } from "../pmf/pmf"; @@ -7,6 +8,18 @@ type DiceOperation = ((this: Dice, other: Dice | number) => Dice) & { unary?: boolean; }; +/** + * Resource-exhaustion guards. Adversarial expressions (a huge die, a huge dice + * count, or a keep over a large enumerated pool) can otherwise blow up memory + * and CPU. These caps are deliberately generous so every legitimate expression + * the suite exercises (e.g. d100000) still parses. + */ +// Must stay >= 100000: the suite asserts d100000 (100k faces) parses. +const MAX_DIE_SIDES = 1_000_000; +const MAX_DICE_COUNT = 10_000; +// multiplyDiceByDice enumerates faces^count outcomes when a keep is applied. +const MAX_KEEP_OUTCOMES = 1_000_000; + /** * Internal parse cache for PMFs produced from string expressions. * Keyed by cleaned expression (spaces stripped, lowercased) and optional `n` value. @@ -48,21 +61,23 @@ export function parse(expression: string, n: number = 0): PMF { const chars = [...cleaned]; - let result: Dice | undefined = undefined; + let result: Dice; try { result = parseExpression(chars, n); } catch (error) { - throw new Error(`Cannot parse dice expression [${expression}]: ${error}`); + throw new DiceParseError( + `Cannot parse dice expression [${expression}]: ${error}`, + { expression, cause: error } + ); } - try { - (result as any).privateData = (result as any).privateData || {}; - result.identifier = cleaned; - } catch {} + result.privateData = result.privateData || {}; + result.identifier = cleaned; if (chars.length > 0) { - throw new Error( - `Unexpected token: '${chars[0]}' from expression: '${expression}'` + throw new DiceParseError( + `Unexpected token: '${chars[0]}' from expression: '${expression}'`, + { expression } ); } @@ -104,6 +119,15 @@ function parseExpression(arr: string[], n: number): Dice { const arg = !op.unary ? parseArgument(arr, n) : finalResult; // Handle crit (e.g. xcrit, crit) + // + // KNOWN LIMITATION: this peels the maximum FACE of the (already convolved) + // to-hit distribution to represent the natural 20. That is correct only when + // the to-hit has no bonus dice. With bonus dice in the to-hit (e.g. Bless, + // "d20 + 5 + 1d4"), a natural 20 is "d20 == 20 with any bonus", whose mass is + // smeared across several total values that also contain non-crit mass, so the + // d20 identity is lost and the crit slice collapses to 1/(20·∏bonusSides). + // Use the builder API (d20.plus(...).plus(bonusDie).ac(...).onCrit(...)) for + // a correct crit probability with bonus to-hit dice. See CHANGELOG. let crit: Dice | undefined; let critNorm = 1; if (arr[0] === "x" || arr[0] === "c") { @@ -268,7 +292,10 @@ function multiplyDiceByDice(d1: Dice | number, d2: Dice | number): Dice { if (typeof d2 === "number") d2 = Dice.scalar(d2); const result = new Dice(); - const faces: Record = {}; + // Keyed by face value. A Map avoids the number→string→parseFloat round-trip of + // an object and preserves insertion order, which matches d1.keys() ascending + // order — so the combine order below is identical to the previous version. + const faces = new Map(); let normalizationFactor = 1; for (const key of d1.keys()) { @@ -279,7 +306,14 @@ function multiplyDiceByDice(d1: Dice | number, d2: Dice | number): Dice { } if (d2.privateData.keep) { - // Repeat dice2 "key" times and apply keep + // Repeat dice2 "key" times and apply keep. opDice enumerates the full + // faces^count outcome space, so guard against a combinatorial blow-up. + const faceCount = d2.keys().length; + if (Math.pow(faceCount, key) > MAX_KEEP_OUTCOMES) { + throw new DiceParseError( + `Keep enumeration of ${faceCount}^${key} outcomes exceeds the maximum of ${MAX_KEEP_OUTCOMES}` + ); + } const repeat: Dice[] = Array(key).fill(d2); face = opDice(repeat, d2.privateData.keep); } else { @@ -287,12 +321,10 @@ function multiplyDiceByDice(d1: Dice | number, d2: Dice | number): Dice { } normalizationFactor *= face.total(); - faces[key] = face; + faces.set(key, face); } - for (const key of Object.keys(faces)) { - const k = parseFloat(key); // keys from object are strings - const face = faces[k]; + for (const [k, face] of faces) { const count = d1.get(k); result.combineInPlace( face.normalize((count * normalizationFactor) / face.total()) @@ -304,6 +336,11 @@ function multiplyDiceByDice(d1: Dice | number, d2: Dice | number): Dice { } function multiplyDice(n: number, d: Dice): Dice { + if (n > MAX_DICE_COUNT) { + throw new DiceParseError( + `Dice count ${n} exceeds the maximum of ${MAX_DICE_COUNT}` + ); + } if (n === 0) return new Dice(0); if (n === 1) return d; @@ -421,10 +458,19 @@ function parseDice(s: string[], n: number): Dice | undefined { } const sides = parseNumber(s, n); + if (sides > MAX_DIE_SIDES) { + throw new DiceParseError( + `Die size ${sides} exceeds the maximum of ${MAX_DIE_SIDES}` + ); + } let result = new Dice(sides); if (rerollOne) { - result = result.deleteFace(1).combine(result); + // Reroll a rolled 1 exactly once, keeping the second roll (e.g. halfling + // luck). This is the same semantics as the `reroll 1` operator; the previous + // deleteFace(1).combine(result) computed a weighted union (P(1)=1/(2s−1)), + // not a reroll (correct P(1)=1/s²). + result = result.reroll(1); } return result; diff --git a/src/pmf/mixture.ts b/src/pmf/mixture.ts index 79b62fd..379abd2 100644 --- a/src/pmf/mixture.ts +++ b/src/pmf/mixture.ts @@ -2,8 +2,6 @@ import type { Bin } from "../common/types"; import { EPS } from "../common/types"; import { PMF } from "./pmf"; -type BinEntries = Iterable<[number, Bin]>; - /** A labeled mixture builder that preserves provenance in Bin.count. */ export class Mixture { private readonly totals = new Map(); // raw mass per outcome (pre-normalization) @@ -39,10 +37,9 @@ export class Mixture { add(label: L, pmf: PMF, weight = 1): this { if (!Number.isFinite(weight) || weight <= 0) return this; - // Stream probabilities. Works whether PMF iterates as [v, p] or [v, Bin]. - for (const [v, binOrNumber] of pmf as any as BinEntries) { - const isNumber = typeof binOrNumber === "number"; - const p = isNumber ? binOrNumber : binOrNumber?.p ?? 0; + // Stream probabilities from each [value, Bin] pair. + for (const [v, bin] of pmf) { + const p = bin.p; if (p <= 0) continue; const add = weight * p; diff --git a/src/pmf/pmf.ts b/src/pmf/pmf.ts index 03989dd..005bb5a 100644 --- a/src/pmf/pmf.ts +++ b/src/pmf/pmf.ts @@ -22,6 +22,7 @@ export class PMF { private _mean?: number; private _variance?: number; private _stdev?: number; + private _fingerprint?: string; constructor( public readonly map: Map = new Map(), @@ -77,9 +78,11 @@ export class PMF { const q = 1 - p; - // Fast paths - if (p === 0) return failurePMF.scaleMass(1); // clone - if (p === 1) return successPMF.scaleMass(1); // clone + // Fast paths. scaleMass(1) returns the same instance, so these hand back the + // branch PMF unchanged. That is safe because PMFs are treated as immutable + // (compact() and the other transforms now clone bins rather than mutate). + if (p === 0) return failurePMF.scaleMass(1); + if (p === 1) return successPMF.scaleMass(1); // Choose epsilon. You can also pick Math.min for a tighter threshold. const eps = successPMF.epsilon ?? failurePMF.epsilon; @@ -87,14 +90,21 @@ export class PMF { successPMF.identifier }*${p.toFixed(6)})`; - // Proper Bernoulli mixture: q·failure ⊕ p·success - const out = PMF.empty(eps, id) - .addScaled(failurePMF, q) - .addScaled(successPMF, p); + // Proper Bernoulli mixture: q·failure ⊕ p·success, assembled in a single + // pass. The previous `empty().addScaled(failure,q).addScaled(success,p)` + // chain copied failurePMF's bins twice (into the intermediate, then again + // when the intermediate was copied by the second addScaled). Merging both + // scaled branches directly into one fresh map keeps the same accumulation + // order — q·failure first, then p·success — so the result is bit-identical. + const resultMap = new Map(); + for (const [damageValue, bin] of failurePMF.map) { + PMF.mergeInto(resultMap, damageValue, PMF.scaleBin(bin, q)); + } + for (const [damageValue, bin] of successPMF.map) { + PMF.mergeInto(resultMap, damageValue, PMF.scaleBin(bin, p)); + } - // Optional hygiene: drop exact zeros - // return out.compact(); - return out; + return new PMF(resultMap, eps, false, id); } /** @@ -144,8 +154,8 @@ export class PMF { * @param fallback PMF to apply when this PMF is *not* selected. * @returns A new PMF representing the weighted mixture of this PMF and the fallback. */ - gate(p: number, zero: PMF) { - return PMF.branch(this, zero, p); + gate(p: number, fallback: PMF) { + return PMF.branch(this, fallback, p); } /** @@ -251,16 +261,26 @@ export class PMF { * * @returns New PMF with attr field populated in each bin */ - withAttribution(): PMF { - // Fast path: if attr already exists, return this PMF unchanged - // Check if any non-zero damage bin has attr + /** + * Returns true if this PMF already carries damage attribution metadata. + * + * Only the first positive-damage bin is inspected (parser-generated PMFs + * populate `attr` uniformly), so this is O(1) in practice. + */ + hasAttribution(): boolean { for (const [damage, bin] of this.map) { if (damage !== 0 && bin.attr && Object.keys(bin.attr).length > 0) { - return this; // Already has attribution + return true; } - // Only check first non-zero bin for performance + // Only check the first non-zero bin for performance if (damage > 0) break; } + return false; + } + + withAttribution(): PMF { + // Fast path: if attr already exists, return this PMF unchanged + if (this.hasAttribution()) return this; const newMap = new Map(); @@ -404,7 +424,7 @@ export class PMF { */ replicate(n: number): PMF[] { if (!Number.isInteger(n) || n <= 0) { - throw new Error("combineN(n): n must be a positive integer"); + throw new Error("replicate(n): n must be a positive integer"); } if (n === 1) return [this]; return Array.from({ length: n }, () => this); @@ -439,11 +459,11 @@ export class PMF { const normalizationFactor = this.mass(); if (normalizationFactor === 0) return this; + // Note: this divides by normalizationFactor rather than multiplying by its + // reciprocal, to keep results bit-identical to direct division. const normalizedMap = new Map(); for (const [damageValue, probabilityBin] of this.map) { - const normalizedProbability = probabilityBin.p / normalizationFactor; const normalizedCount: OutcomeLabelMap = {}; - for (const labelKey in probabilityBin.count) { normalizedCount[labelKey] = (probabilityBin.count[labelKey] as number) / normalizationFactor; @@ -459,7 +479,7 @@ export class PMF { } normalizedMap.set(damageValue, { - p: normalizedProbability, + p: probabilityBin.p / normalizationFactor, count: normalizedCount, attr: normalizedAttributes, }); @@ -488,26 +508,30 @@ export class PMF { if (!shouldKeep) continue; - // Clean up count entries below epsilon (mutates the bin like your original) - for (const labelKey in probabilityBin.count) { - if (Math.abs(probabilityBin.count[labelKey] || 0) < eps) { - delete probabilityBin.count[labelKey]; + // Build a fresh Bin rather than mutating the source. Bins are shared by + // reference across PMFs (e.g. branch()/addScaled()/scaleMass() fast paths + // can carry another PMF's bin objects), so deleting sub-eps entries in + // place would silently corrupt the source PMF's count/attr. + const cleanedBin = PMF.cloneBin(probabilityBin); + + for (const labelKey in cleanedBin.count) { + if (Math.abs(cleanedBin.count[labelKey] || 0) < eps) { + delete cleanedBin.count[labelKey]; } } - // Clean up attr entries below epsilon - if (probabilityBin.attr) { - for (const labelKey in probabilityBin.attr) { - if (Math.abs(probabilityBin.attr[labelKey] || 0) < eps) { - delete probabilityBin.attr[labelKey]; + if (cleanedBin.attr) { + for (const labelKey in cleanedBin.attr) { + if (Math.abs(cleanedBin.attr[labelKey] || 0) < eps) { + delete cleanedBin.attr[labelKey]; } } - if (Object.keys(probabilityBin.attr).length === 0) { - probabilityBin.attr = undefined; + if (Object.keys(cleanedBin.attr).length === 0) { + cleanedBin.attr = undefined; } } - compactedMap.set(damageValue, probabilityBin); + compactedMap.set(damageValue, cleanedBin); } return new PMF(compactedMap, eps, this.normalized, this.identifier); @@ -582,6 +606,33 @@ export class PMF { return this._stdev; } + /** Deep-copies a Bin, cloning its count and (optional) attr maps. */ + private static cloneBin(bin: Bin): Bin { + return { + p: bin.p, + count: { ...bin.count }, + attr: bin.attr ? { ...bin.attr } : undefined, + }; + } + + /** Returns a new Bin with p, count, and attr all multiplied by `factor`. */ + private static scaleBin(bin: Bin, factor: number): Bin { + const count: OutcomeLabelMap = {}; + for (const k in bin.count) { + count[k] = (bin.count[k] as number) * factor; + } + + let attr: OutcomeLabelMap | undefined; + if (bin.attr) { + attr = {}; + for (const k in bin.attr) { + attr[k] = (bin.attr[k] as number) * factor; + } + } + + return { p: bin.p * factor, count, attr }; + } + private static mergeInto( destinationMap: Map, damageValue: number, @@ -589,11 +640,7 @@ export class PMF { ) { const existingBin = destinationMap.get(damageValue); if (!existingBin) { - destinationMap.set(damageValue, { - p: binToAdd.p, - count: { ...binToAdd.count }, - attr: binToAdd.attr ? { ...binToAdd.attr } : undefined, - }); + destinationMap.set(damageValue, PMF.cloneBin(binToAdd)); return; } @@ -634,33 +681,15 @@ export class PMF { const resultMap = new Map(); for (const [dmg, bin] of this.map) { - resultMap.set(dmg, { - p: bin.p, - count: { ...bin.count }, - attr: bin.attr ? { ...bin.attr } : undefined, - }); + resultMap.set(dmg, PMF.cloneBin(bin)); } for (const [damageValue, probabilityBin] of branch.map) { - const scaledCount: OutcomeLabelMap = {}; - for (const k in probabilityBin.count) { - scaledCount[k] = probability * (probabilityBin.count[k] as number); - } - - let scaledAttributes: OutcomeLabelMap | undefined; - if (probabilityBin.attr) { - scaledAttributes = {}; - for (const k in probabilityBin.attr) { - scaledAttributes[k] = - probability * (probabilityBin.attr[k] as number); - } - } - - PMF.mergeInto(resultMap, damageValue, { - p: probability * probabilityBin.p, - count: scaledCount, - attr: scaledAttributes, - }); + PMF.mergeInto( + resultMap, + damageValue, + PMF.scaleBin(probabilityBin, probability) + ); } return new PMF( @@ -676,26 +705,7 @@ export class PMF { const scaledMap = new Map(); for (const [damageValue, probabilityBin] of this.map) { - const scaledCount: OutcomeLabelMap = {}; - for (const labelKey in probabilityBin.count) { - scaledCount[labelKey] = - (probabilityBin.count[labelKey] as number) * factor; - } - - let scaledAttributes: OutcomeLabelMap | undefined; - if (probabilityBin.attr) { - scaledAttributes = {}; - for (const labelKey in probabilityBin.attr) { - scaledAttributes[labelKey] = - (probabilityBin.attr[labelKey] as number) * factor; - } - } - - scaledMap.set(damageValue, { - p: probabilityBin.p * factor, - count: scaledCount, - attr: scaledAttributes, - }); + scaledMap.set(damageValue, PMF.scaleBin(probabilityBin, factor)); } return new PMF( scaledMap, @@ -709,11 +719,11 @@ export class PMF { const transformedMap = new Map(); for (const [originalDamage, probabilityBin] of this.map) { const transformedDamage = damageTransformFunction(originalDamage); - PMF.mergeInto(transformedMap, transformedDamage, { - p: probabilityBin.p, - count: { ...probabilityBin.count }, - attr: probabilityBin.attr ? { ...probabilityBin.attr } : undefined, - }); + PMF.mergeInto( + transformedMap, + transformedDamage, + PMF.cloneBin(probabilityBin) + ); } return new PMF( transformedMap, @@ -744,17 +754,22 @@ export class PMF { ): string { const [id1, id2] = [p1.identifier, p2.identifier].sort(); - // tiny fingerprints so keys change if the underlying numbers changed - const fp = (x: PMF) => { - // robust + cheap: mass + size + face sum - const m = x.mass().toFixed(12); - const n = x.map.size; - let faceSum = 0; - for (const k of x.map.keys()) faceSum += k; - return `${m}|${n}|${faceSum}`; - }; + return `v4:${raw ? "RAW" : "N"}:${id1}+${id2}@${eps}|${p1.fingerprint()}|${p2.fingerprint()}`; + } - return `v4:${raw ? "RAW" : "N"}:${id1}+${id2}@${eps}|${fp(p1)}|${fp(p2)}`; + /** + * A small content fingerprint (mass + bin count + face sum) so convolution + * cache keys change if the underlying numbers do. Memoized because a PMF is + * immutable once constructed — this avoids re-summing every key on each + * convolve() call (including cache hits). + */ + fingerprint(): string { + if (this._fingerprint === undefined) { + let faceSum = 0; + for (const k of this.map.keys()) faceSum += k; + this._fingerprint = `${this.mass().toFixed(12)}|${this.map.size}|${faceSum}`; + } + return this._fingerprint; } convolve(other: PMF, eps?: number, raw = false): PMF { @@ -771,30 +786,45 @@ export class PMF { const cached = pmfCache?.get(cacheKey); if (cached) return cached; + // Accumulate directly into each destination bin instead of building a + // temporary Bin per (a,b) pair and merging it. The probability channel + // (`dest.p += ap*bp`) accumulates in the same order as before, so it is + // bit-identical; only the per-label `count`/`attr` sums re-associate, which + // shifts them by at most a few ULP (far below the eps pruning threshold). const combinedMap = new Map(); for (const [aVal, aBin] of A.map) { + const ap = aBin.p; + const aCount = aBin.count; + const aAttr = aBin.attr; for (const [bVal, bBin] of B.map) { - const p = aBin.p * bBin.p; + const bp = bBin.p; const dmg = aVal + bVal; - const count: OutcomeLabelMap = {}; - for (const k in aBin.count) - count[k] = (count[k] || 0) + (aBin.count[k] as number) * bBin.p; + let dest = combinedMap.get(dmg); + if (dest === undefined) { + dest = { p: 0, count: {} }; + combinedMap.set(dmg, dest); + } + + dest.p += ap * bp; + + const dc = dest.count; + for (const k in aCount) dc[k] = (dc[k] || 0) + (aCount[k] as number) * bp; for (const k in bBin.count) - count[k] = (count[k] || 0) + (bBin.count[k] as number) * aBin.p; - - let attr: OutcomeLabelMap | undefined; - if (aBin.attr || bBin.attr) { - attr = {}; - if (aBin.attr) - for (const k in aBin.attr) - attr[k] = (attr[k] || 0) + (aBin.attr[k] as number) * bBin.p; + dc[k] = (dc[k] || 0) + (bBin.count[k] as number) * ap; + + if (aAttr || bBin.attr) { + let da = dest.attr; + if (da === undefined) { + da = {}; + dest.attr = da; + } + if (aAttr) + for (const k in aAttr) da[k] = (da[k] || 0) + (aAttr[k] as number) * bp; if (bBin.attr) for (const k in bBin.attr) - attr[k] = (attr[k] || 0) + (bBin.attr[k] as number) * aBin.p; + da[k] = (da[k] || 0) + (bBin.attr[k] as number) * ap; } - - PMF.mergeInto(combinedMap, dmg, { p, count, attr }); } } @@ -808,10 +838,13 @@ export class PMF { // Enforce mass invariant: mass(out) = (raw? A.mass():1) * (raw? B.mass():1) const mExp = (raw ? A.mass() : 1) * (raw ? B.mass() : 1); const mGot = result.mass(); - if (mExp !== 0 && Math.abs(mGot - mExp) > epsilon) { + // Guard mGot !== 0: a zero-mass operand convolves to the zero measure + // (mass 0). Without this guard the non-raw path would scaleMass(mExp/0) = + // scaleMass(Infinity), poisoning every bin to 0*Infinity = NaN. + if (mExp !== 0 && mGot !== 0 && Math.abs(mGot - mExp) > epsilon) { result = result.scaleMass(mExp / mGot); } - if (!raw && Math.abs(result.mass() - 1) > epsilon) + if (!raw && mGot !== 0 && Math.abs(result.mass() - 1) > epsilon) result = result.normalize(); pmfCache?.set(cacheKey, result); @@ -823,31 +856,6 @@ export class PMF { return this.convolve(other, eps, true); } - // Collapse repeated identical PMFs using power() and return a sorted list - // Temporarily disabled for now. This was used for a performance optimization, but can lose data provenance. - // private static collapseIdentical(pmfList: PMF[], eps: number): PMF[] { - // const grouped = new Map(); - // for (const pmf of pmfList) { - // const id = pmf.identifier; - // const g = grouped.get(id); - // if (g) g.count++; - // else grouped.set(id, { pmf, count: 1 }); - // } - - // if (grouped.size >= pmfList.length) return pmfList; - - // const collapsed: PMF[] = []; - // for (const { pmf, count } of grouped.values()) { - // collapsed.push(count > 1 ? pmf.power(count, eps) : pmf); - // } - - // // Stable order for better cache locality - // collapsed.sort((a, b) => - // a.identifier < b.identifier ? -1 : a.identifier > b.identifier ? 1 : 0 - // ); - // return collapsed; - // } - // Reduce a list of PMFs by left-folding convolve() with the given eps private static reduceConvolveLeft(pmfList: PMF[], eps: number): PMF { let result = pmfList[0]; @@ -871,21 +879,33 @@ export class PMF { if (pmfList.length === 0) return PMF.empty(eps); if (pmfList.length === 1) return pmfList[0]; - // Optimization: Group identical PMFs and use power() to collapse repeats - // If nothing collapses, this returns the same array reference. - // const collapsed = PMF.collapseIdentical(pmfList, eps); - - // Optimization: Linear combination with automatic intermediate caching - // Whether collapsed or not, reduce via left-folded convolved() + // Linear combination with automatic intermediate caching: each prefix + // (A+B, (A+B)+C, ...) is a stable cache key, maximizing reuse. return PMF.reduceConvolveLeft(pmfList, eps); } - toJSON() { - return JSON.stringify({ + /** + * Returns a plain, JSON-serializable representation of this PMF. + * + * Follows the standard `toJSON` contract, so `JSON.stringify(pmf)` produces + * the expected output (no double-encoding). Use {@link PMF.fromJSON} to + * reconstruct, or {@link PMF.toJSONString} if you need the string directly. + */ + toJSON(): { + bins: Array<[number, Bin]>; + normalized: boolean; + identifier: string; + } { + return { bins: [...this.map.entries()], normalized: this.normalized, identifier: this.identifier, - }); + }; + } + + /** Serializes this PMF to a JSON string (equivalent to `JSON.stringify(pmf)`). */ + toJSONString(): string { + return JSON.stringify(this); } static fromJSON(jsonData: { @@ -977,7 +997,6 @@ export class PMF { return new PMF(prunedMap, epsRel, false, `prune(${this.identifier})`); } - /** NEW - REVIEW IF THESE ARE USEFUL OR DUPLCIATIVE? */ /** Probability mass at exactly x. */ pAt(x: number): number { return this.map.get(x)?.p ?? 0; @@ -1075,9 +1094,8 @@ export class PMF { tailProbGE(t: number): number { let s = 0; - for (const [x, rec] of this) { - const p = typeof rec === "number" ? rec : rec.p; - if (p > 0 && x >= t) s += p; + for (const [x, bin] of this) { + if (bin.p > 0 && x >= t) s += bin.p; } return s; } @@ -1156,6 +1174,22 @@ export class PMF { pSpecial: number, n: number ) { + // Preconditions: the "special" successes are a subset of all successes, so + // 0 <= pSpecial <= pSuccess <= 1. Without this guard, violating inputs + // silently produce probabilities outside [0,1]. + if ( + !Number.isFinite(pSuccess) || + !Number.isFinite(pSpecial) || + pSuccess < 0 || + pSuccess > 1 || + pSpecial < 0 || + pSpecial - pSuccess > EPS + ) { + throw new Error( + `firstSuccessWeights: require 0 <= pSpecial <= pSuccess <= 1 (got pSuccess=${pSuccess}, pSpecial=${pSpecial})` + ); + } + const pFail = 1 - pSuccess; const pFailAll = Math.pow(pFail, n); @@ -1195,15 +1229,14 @@ export class PMF { const probs = new Map(); const counts = new Map>(); - for (const [v, rec] of this) { - if (Math.abs(rec.p) < eps) continue; + for (const [v, bin] of this) { + if (Math.abs(bin.p) < eps) continue; const u = round(f(v)); - probs.set(u, (probs.get(u) ?? 0) + rec.p); + probs.set(u, (probs.get(u) ?? 0) + bin.p); if (preserveCounts) { // Merge counts if present - const rec = this.map.get(v); - const src = typeof rec === "number" ? undefined : rec?.count; + const src = bin.count; if (src) { const dest = counts.get(u) ?? {}; for (const k in src) { diff --git a/src/pmf/query.ts b/src/pmf/query.ts index 71e7878..a5f0a5b 100644 --- a/src/pmf/query.ts +++ b/src/pmf/query.ts @@ -15,7 +15,9 @@ import { PMF } from "./pmf"; export class DiceQuery { public readonly singles: PMF[]; - public readonly combined: PMF; + private readonly _eps: number; + private readonly _combinedProvided: boolean; + private _combined?: PMF; private _combinedWithAttr?: PMF; constructor(singles: PMF | PMF[], combined?: PMF, eps = EPS) { @@ -23,8 +25,33 @@ export class DiceQuery { if (this.singles.some((s) => s === undefined)) { throw new Error("DiceQuery contains undefined singles"); } - const c = combined ?? PMF.convolveMany(this.singles); - this.combined = Math.abs(c.mass() - 1) <= eps ? c : c.normalize(); + this._eps = eps; + // When the caller supplies an explicit combined distribution that may not + // equal convolve(singles), the additive closed-form moments would describe + // a different distribution than cdf/percentiles. Track this so mean()/ + // variance() can fall back to the provided combined and stay consistent. + this._combinedProvided = combined !== undefined; + if (combined !== undefined) { + this._combined = + Math.abs(combined.mass() - 1) <= eps ? combined : combined.normalize(); + } + } + + /** + * The combined damage distribution of all single PMFs (their convolution), + * normalized to total probability 1. + * + * Computed lazily on first access and cached. Queries that only need + * additive statistics — {@link DiceQuery.mean}, {@link DiceQuery.variance}, + * {@link DiceQuery.stddev} — never trigger this convolution. + */ + get combined(): PMF { + if (this._combined === undefined) { + const c = PMF.convolveMany(this.singles); + this._combined = + Math.abs(c.mass() - 1) <= this._eps ? c : c.normalize(); + } + return this._combined; } private static readonly DEFAULT_OUTCOMES: readonly OutcomeType[] = [ @@ -57,8 +84,16 @@ export class DiceQuery { return this._combinedWithAttr; } - // Add attribution to each single PMF, then convolve - // The existing convolution logic will preserve attribution correctly + // Fast path: if every single already carries attribution (as parser- + // generated PMFs do), the attributed convolution is bit-for-bit identical + // to `combined` — reuse it instead of convolving a second time. + if (this.singles.every((pmf) => pmf.hasAttribution())) { + this._combinedWithAttr = this.combined; + return this._combinedWithAttr; + } + + // Otherwise (e.g. builder-generated PMFs that only carry `count`), add + // attribution to each single PMF, then convolve. const singlesWithAttr = this.singles.map((pmf) => pmf.withAttribution()); const combined = PMF.convolveMany(singlesWithAttr, this.combined.epsilon); @@ -79,11 +114,26 @@ export class DiceQuery { * Use case: "What's my average damage per round?" */ mean(): number { - let totalSum = 0; - for (const [damageValue, probabilityBin] of this.combined) { - totalSum += damageValue * probabilityBin.p; + // If the caller supplied a combined distribution that may diverge from + // convolve(singles), report its mean so mean() stays consistent with + // cdf/percentiles/min/max (which all read `combined`). + if (this._combinedProvided) { + let m = 0; + for (const [damageValue, bin] of this.combined) m += damageValue * bin.p; + return m; + } + // Expectation is additive over independent attacks: E[Σ Xᵢ] = Σ E[Xᵢ]. + // Computing it directly from the singles avoids building `combined`. + let totalMean = 0; + for (const single of this.singles) { + const mass = single.mass(); + if (mass <= 0) continue; + // Match `combined`'s semantics: only divide by mass when it is not + // already 1 (within eps), so an already-normalized single is bit-exact. + totalMean += + Math.abs(mass - 1) <= this._eps ? single.mean() : single.mean() / mass; } - return totalSum; + return totalMean; } /** @@ -94,13 +144,40 @@ export class DiceQuery { * High variance means higher risk/reward. Lower variance means more consistent damage. */ variance(): number { - const meanValue = this.mean(); - let varianceSum = 0; - for (const [damageValue, probabilityBin] of this.combined) { - const deviationFromMean = damageValue - meanValue; - varianceSum += deviationFromMean * deviationFromMean * probabilityBin.p; + // Consistent with mean(): if a (possibly divergent) combined was provided, + // compute the variance of that distribution directly. + if (this._combinedProvided) { + const mu = this.mean(); + let v = 0; + for (const [damageValue, bin] of this.combined) { + const dev = damageValue - mu; + v += dev * dev * bin.p; + } + return v; } - return varianceSum; + // Variance is additive over independent attacks: Var[Σ Xᵢ] = Σ Var[Xᵢ]. + // Use the centered form per single (E[(X−μ)²]) rather than E[X²]−μ², which + // suffers catastrophic cancellation when damage has a large constant offset. + let totalVariance = 0; + for (const single of this.singles) { + const mass = single.mass(); + if (mass <= 0) continue; + if (Math.abs(mass - 1) <= this._eps) { + // PMF.variance() is the centered, cached, mass-1 variance. + totalVariance += single.variance(); + } else { + // Normalized centered variance of a non-unit-mass single. + let mu = 0; + for (const [d, b] of single) mu += d * (b.p / mass); + let v = 0; + for (const [d, b] of single) { + const dev = d - mu; + v += dev * dev * (b.p / mass); + } + totalVariance += v; + } + } + return totalVariance; } /** @@ -114,6 +191,11 @@ export class DiceQuery { return Math.sqrt(this.variance()); } + /** Alias of {@link DiceQuery.stddev}, matching {@link PMF.stdev}. */ + stdev(): number { + return this.stddev(); + } + /** * Returns the Cumulative Distribution Function. */ @@ -218,11 +300,46 @@ export class DiceQuery { } private singleProb(diceIndex: number, label: OutcomeType): number { + const single = this.singles[diceIndex]; let probabilitySum = 0; - for (const [, probabilityBin] of this.singles[diceIndex]) { + for (const [, probabilityBin] of single) { probabilitySum += probabilityBin.count[label] || 0; } - return probabilitySum; + // Return the conditional per-event probability. Dividing by the single's + // mass keeps the result in [0,1] for a non-normalized single (e.g. one that + // has been scaled), so probAtLeastOne / the binomial DP stay mass-invariant. + const mass = single.mass(); + return mass > 0 ? probabilitySum / mass : 0; + } + + /** + * Full count distribution [P(0), P(1), …, P(n)] for "an attack succeeds if it + * carries ANY of `labels`", over the n independent singles. + * + * Each single's per-event success probability is the Poisson-binomial + * marginal P(≥1 of labels) from {@link probabilityOf} (i.e. probAtLeastOne), + * computed exactly once. The binomial DP then runs once to produce the whole + * distribution, so the array-label paths of probExactlyK / probAtLeastK / + * probAtMostK can slice or sum from it instead of rebuilding a DiceQuery and + * re-running the DP per requested k. + */ + private countDistribution(labels: OutcomeType[]): number[] { + const n = this.singles.length; + const successProbabilities = this.singles.map((single) => + new DiceQuery([single]).probabilityOf(labels) + ); + + const dist = new Array(n + 1).fill(0); + dist[0] = 1; + for (const successProb of successProbabilities) { + for (let outcomeCount = n; outcomeCount >= 1; outcomeCount--) { + dist[outcomeCount] = + dist[outcomeCount] * (1 - successProb) + + dist[outcomeCount - 1] * successProb; + } + dist[0] *= 1 - successProb; + } + return dist; } probAtLeastK(labels: OutcomeType | OutcomeType[], k: number): number { @@ -232,9 +349,12 @@ export class DiceQuery { if (k <= 0) return 1; if (k > n) return 0; + // Sum the upper tail of the single, shared count distribution rather than + // calling probExactlyK (which rebuilt the distribution) once per i. + const dist = this.countDistribution(L); let tail = 0; for (let i = k; i <= n; i++) { - tail += this.probExactlyK(L, i); + tail += dist[i]; } if (tail < 0) return 0; @@ -266,14 +386,19 @@ export class DiceQuery { let productOfNonOccurrence = 1; for (let diceIndex = 0; diceIndex < this.singles.length; diceIndex++) { - // Calculate total probability of any of the specified labels occurring + // Calculate total probability of any of the specified labels occurring. + // Clamp to [0,1] so floating-point drift in the per-attack sum cannot push + // the complement out of range. let combinedProbability = 0; for (const label of labels) { combinedProbability += this.singleProb(diceIndex, label); } + if (combinedProbability < 0) combinedProbability = 0; + else if (combinedProbability > 1) combinedProbability = 1; productOfNonOccurrence *= 1 - combinedProbability; } - return 1 - productOfNonOccurrence; + const result = 1 - productOfNonOccurrence; + return result < 0 ? 0 : result > 1 ? 1 : result; } /** @@ -323,7 +448,7 @@ export class DiceQuery { * Array examples: * - probExactlyK(['hit', 'crit'], 2) = probability exactly 2 attacks succeed * - probExactlyK(['hit', 'crit'], 1) = probability exactly 1 attack succeeds - * - probExactlyK(['miss', 'missNone'], 0) = probability no attacks miss + * - probExactlyK(['missDamage', 'missNone'], 0) = probability no attacks miss * * Use cases: * - "What's the chance exactly one of my attacks hits?" @@ -340,26 +465,10 @@ export class DiceQuery { return probabilityArray[k]; } - // For multiple labels, we need to compute success probability for each single attack - const successProbabilities = this.singles.map((single) => { - const singleQuery = new DiceQuery([single]); - return singleQuery.probabilityOf(labels); - }); - - // Use binomial distribution with combined success probability - const binomialProbs = new Array(k + 1).fill(0); - binomialProbs[0] = 1; - - for (const successProb of successProbabilities) { - for (let outcomeCount = k; outcomeCount >= 1; outcomeCount--) { - binomialProbs[outcomeCount] = - binomialProbs[outcomeCount] * (1 - successProb) + - binomialProbs[outcomeCount - 1] * successProb; - } - binomialProbs[0] *= 1 - successProb; - } - - return binomialProbs[k]; + // For multiple labels, derive P(exactly k) from the single shared count + // distribution. (k > n is impossible, so index out of range reads as 0.) + const dist = this.countDistribution(labels); + return k >= 0 && k < dist.length ? dist[k] : 0; } /** @@ -368,7 +477,7 @@ export class DiceQuery { * Single label examples: * - probAtMostK('hit', 1) = probability 0 or 1 attacks hit (at most 1) * - probAtMostK('crit', 0) = probability no attacks crit - * - probAtMostK('miss', 2) = probability at most 2 attacks miss + * - probAtMostK('missDamage', 2) = probability at most 2 attacks miss * * Array examples: * - probAtMostK(['hit', 'crit'], 1) = probability at most 1 attack succeeds @@ -391,10 +500,13 @@ export class DiceQuery { return cumulativeSum; } - // For multiple labels, compute probabilities for 0 to k + // For multiple labels, sum the lower tail of the single shared count + // distribution rather than recomputing it per outcomeCount. + const dist = this.countDistribution(labels); + const upper = Math.min(k, dist.length - 1); let cumulativeSum = 0; - for (let outcomeCount = 0; outcomeCount <= k; outcomeCount++) { - cumulativeSum += this.probExactlyK(labels, outcomeCount); + for (let outcomeCount = 0; outcomeCount <= upper; outcomeCount++) { + cumulativeSum += dist[outcomeCount]; } return cumulativeSum; } @@ -444,7 +556,7 @@ export class DiceQuery { * * Array examples: * - damageStatsFrom(['hit', 'crit']) = damage range when at least one attack succeeds - * - damageStatsFrom(['miss', 'missNone']) = damage range when at least one attack misses + * - damageStatsFrom(['missDamage', 'missNone']) = damage range when at least one attack misses * * Tactical Use Cases: * - "Given that I don't completely whiff (99% of turns), what damage should I expect?" @@ -462,6 +574,12 @@ export class DiceQuery { * This includes mixed scenarios (2 hits + 1 crit, 3 hits + 1 miss, etc.) which * occur far more frequently than pure scenarios. For pure scenarios, use combinedDamageStats. * + * KNOWN LIMITATION (multi-attack, single label): the returned `count` is an + * EXPECTED COUNT (E[#label], so > 1 for N≥2 attacks, not a probability), and + * `avg` is the size-biased conditional mean E[dmg·#label]/E[#label] rather than + * E[dmg | the label occurs]. For a single attack both are the plain + * conditional figures. Use {@link probAtLeastOne} for the scenario probability. + * * @example * // High-level tactical planning * const successStats = query.damageStatsFrom('hit') @@ -594,7 +712,8 @@ export class DiceQuery { } /** - * Returns the probability that a result includes ANY of the specified labels. + * Returns the probability that at least one attack carries ANY of the + * specified labels (the marginal P(≥1) across the independent attacks). * * Examples: * - `query.probabilityOf('hit')` → 0.88 (probability at least one hit occurs) @@ -603,32 +722,15 @@ export class DiceQuery { * Use cases: * - "What's the chance my resolution includes a success label?" * - "How likely am I to get any hits or crits across all attacks?" + * + * Note: this must NOT be computed by summing `combined` bin probabilities. A + * single combined damage total is reachable by many outcome combinations and + * a bin can hold several labels at once, so summing `bin.p` over bins that + * contain a label over-counts. The correct marginal is the Poisson-binomial + * complement over the per-attack probabilities, i.e. {@link probAtLeastOne}. */ probabilityOf(labels: OutcomeType | OutcomeType[]): number { - // Handle single label case (backward compatibility) - if (typeof labels === "string") { - labels = [labels]; - } - - let totalProbability = 0; - for (const [, probabilityBin] of this.combined) { - // Calculate probability that this bin contains ANY of the specified labels - // Use inclusion-exclusion principle to avoid double-counting overlaps - let binHasAnyLabel = false; - for (const label of labels) { - if ( - probabilityBin.count[label] && - (probabilityBin.count[label] as number) > 0 - ) { - binHasAnyLabel = true; - break; - } - } - if (binHasAnyLabel) { - totalProbability += probabilityBin.p; - } - } - return totalProbability; + return this.probAtLeastOne(labels); } /** @@ -702,12 +804,6 @@ export class DiceQuery { ): { labels: number[]; datasets: Array<{ label: string; data: number[] }> } { const damageValues = this.combined.support(); - // Precompute totalCount per bin for the labels you intend to stack for efficiency. - damageValues.map((dmg) => { - const bin = this.combined.map.get(dmg)!; - return labels.reduce((sum, lab) => sum + (bin.count[lab] || 0), 0); - }); - const datasets = labels.map((outcomeLabel) => ({ label: outcomeLabel, data: damageValues.map((dmg) => { @@ -1315,6 +1411,16 @@ export class DiceQuery { * Snapshot of the distribution in the exact shape the UI consumes. * - outcome probabilities are "at least one" (and equal to "all" for a single PMF) * - damageRange is conditional on the outcome occurring + * + * The outcome probabilities use the correct Poisson-binomial marginals + * (`atLeastOneProbability` = P(≥1 attack has it), `allProbability` = P(all do)), + * so they are always valid probabilities in [0,1]. + * + * KNOWN LIMITATION (multi-attack): `damageRange.avg` is still aggregated from + * the combined PMF's `count`, which the convolution accumulates as an EXPECTED + * COUNT, so for N≥2 attacks it is the size-biased mean E[dmg·#label]/E[#label] + * rather than a clean conditional expectation. It is correct for a single + * attack. */ snapshot(order?: readonly OutcomeType[]): Snapshot { // 1) Discover which outcomes actually appear in this PMF @@ -1345,13 +1451,11 @@ export class DiceQuery { // 2) Aggregate per-outcome mass and conditional damage ranges via labeled table const rows = this.toLabeledTable(outcomes as OutcomeType[]); - const totals = new Map(); const rangeAcc = new Map< OutcomeType, { min?: number; max?: number; sum: number; mass: number } >(); for (const ot of outcomes) { - totals.set(ot as OutcomeType, 0); rangeAcc.set(ot as OutcomeType, { sum: 0, mass: 0 }); } @@ -1360,7 +1464,6 @@ export class DiceQuery { for (const ot of outcomes) { const p = (row[ot] as number) || 0; if (p <= 0) continue; - totals.set(ot as OutcomeType, (totals.get(ot as OutcomeType) || 0) + p); const r = rangeAcc.get(ot as OutcomeType)!; r.sum += dmg * p; @@ -1370,14 +1473,17 @@ export class DiceQuery { } } + const n = this.singles.length; const outcomeMap = new Map(); for (const ot of outcomes) { - const total = totals.get(ot as OutcomeType) || 0; const r = rangeAcc.get(ot as OutcomeType)!; const avg = r.mass > 0 ? r.sum / r.mass : 0; + // Use the correct per-attack marginals (Poisson-binomial) so these are + // always valid probabilities in [0,1]. For a single attack both reduce to + // P(outcome); the previous `total` was an expected count and could exceed 1. outcomeMap.set(ot as OutcomeType, { - atLeastOneProbability: total, - allProbability: total, // single aggregate PMF: same value + atLeastOneProbability: this.probAtLeastOne(ot as OutcomeType), + allProbability: this.probAtLeastK(ot as OutcomeType, n), damageRange: { min: r.min ?? 0, avg, max: r.max ?? 0 }, }); } @@ -1540,8 +1646,8 @@ export class DiceQuery { * Returns tuple: [pFirstNonSubset, pFirstSubset, pAnySuccess, pNone] */ public firstSuccessSplit( - successOutcome: string | string[], - subsetOutcome: string | string[], + successOutcome: OutcomeType | OutcomeType[], + subsetOutcome: OutcomeType | OutcomeType[], eps = EPS ): readonly [pSuccess: number, pSubset: number, pAny: number, pNone: number] { const pmfs = this.singles; @@ -1549,15 +1655,16 @@ export class DiceQuery { throw new Error("firstSuccessSplitFromPMFs: pmfs must be non-empty"); } - const toArr = (x: string | string[]) => (Array.isArray(x) ? x : [x]); + const toArr = (x: OutcomeType | OutcomeType[]): OutcomeType[] => + Array.isArray(x) ? x : [x]; const clamp01 = (x: number) => Math.max(0, Math.min(1, x)); const tol = Math.max(eps, 8 * Number.EPSILON); // Per-event probabilities from each PMF via DiceQuery([pmf]) const per = pmfs.map((pmf) => { const dq = new DiceQuery([pmf]); - const pS = dq.probAtLeastOne(toArr(successOutcome) as any); - const pB = dq.probAtLeastOne(toArr(subsetOutcome) as any); + const pS = dq.probAtLeastOne(toArr(successOutcome)); + const pB = dq.probAtLeastOne(toArr(subsetOutcome)); if (pB - pS > eps) { throw new Error( "firstSuccessSplitFromPMFs: P(subset) > P(success) for an event. Ensure subset ⊆ success." diff --git a/tests/api-modernization.test.ts b/tests/api-modernization.test.ts new file mode 100644 index 0000000..e7f9499 --- /dev/null +++ b/tests/api-modernization.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import { DiceParseError, parse, PMF } from "../src/index"; + +describe("DiceParseError", () => { + it("is thrown for unexpected tokens and is an Error", () => { + let caught: unknown; + try { + parse("d6@3"); + } catch (e) { + caught = e; + } + expect(caught).toBeInstanceOf(DiceParseError); + expect(caught).toBeInstanceOf(Error); + expect((caught as DiceParseError).message).toContain("Unexpected token"); + expect((caught as DiceParseError).expression).toBe("d6@3"); + }); + + it("wraps parse failures with the original cause", () => { + let caught: unknown; + try { + parse("d6+"); + } catch (e) { + caught = e; + } + expect(caught).toBeInstanceOf(DiceParseError); + expect((caught as DiceParseError).cause).toBeDefined(); + }); +}); + +describe("PMF JSON serialization", () => { + it("toJSON returns a plain object (no double-encoding)", () => { + const pmf = PMF.delta(3); + const json = pmf.toJSON(); + expect(typeof json).toBe("object"); + expect(Array.isArray(json.bins)).toBe(true); + }); + + it("round-trips through JSON.stringify + fromJSON", () => { + const pmf = parse("2d6 + 3"); + const restored = PMF.fromJSON(JSON.parse(JSON.stringify(pmf))); + expect(restored.mean()).toBeCloseTo(pmf.mean(), 12); + expect(restored.support()).toEqual(pmf.support()); + }); + + it("toJSONString preserves the legacy string output", () => { + const pmf = PMF.delta(5); + const str = pmf.toJSONString(); + expect(typeof str).toBe("string"); + expect(JSON.parse(str)).toEqual(pmf.toJSON()); + }); +}); + +describe("DiceQuery.stdev alias", () => { + it("matches stddev()", () => { + const q = parse("2d6 + 3").query(); + expect(q.stdev()).toBe(q.stddev()); + }); +}); diff --git a/tests/fixtures/refactor-characterization.json b/tests/fixtures/refactor-characterization.json new file mode 100644 index 0000000..80badee --- /dev/null +++ b/tests/fixtures/refactor-characterization.json @@ -0,0 +1 @@ +{"kh3of4d6":{"3":{"p":0.0007716049382716042,"count":{}},"4":{"p":0.0030864197530864183,"count":{}},"5":{"p":0.007716049382716047,"count":{}},"6":{"p":0.0162037037037037,"count":{}},"7":{"p":0.029320987654320986,"count":{}},"8":{"p":0.04783950617283951,"count":{}},"9":{"p":0.07021604938271604,"count":{}},"10":{"p":0.0941358024691358,"count":{}},"11":{"p":0.11419753086419754,"count":{}},"12":{"p":0.12885802469135804,"count":{}},"13":{"p":0.13271604938271606,"count":{}},"14":{"p":0.12345679012345678,"count":{}},"15":{"p":0.10108024691358025,"count":{}},"16":{"p":0.07253086419753087,"count":{}},"17":{"p":0.041666666666666664,"count":{}},"18":{"p":0.016203703703703703,"count":{}}},"kl2of5d8":{"2":{"p":0.12072753906249997,"count":{}},"3":{"p":0.16860961914062494,"count":{}},"4":{"p":0.18023681640625003,"count":{}},"5":{"p":0.15869140625,"count":{}},"6":{"p":0.12957763671875003,"count":{}},"7":{"p":0.09292602539062503,"count":{}},"8":{"p":0.06396484375000001,"count":{}},"9":{"p":0.039062500000000014,"count":{}},"10":{"p":0.02383422851562501,"count":{}},"11":{"p":0.012359619140625003,"count":{}},"12":{"p":0.006439208984375003,"count":{}},"13":{"p":0.0024414062500000013,"count":{}},"14":{"p":0.0009460449218750007,"count":{}},"15":{"p":0.0001525878906250001,"count":{}},"16":{"p":0.00003051757812500002,"count":{}}},"kh2of6d6":{"2":{"p":0.000021433470507544556,"count":{}},"3":{"p":0.0001286008230452674,"count":{}},"4":{"p":0.0013503086419753081,"count":{}},"5":{"p":0.004115226337448558,"count":{}},"6":{"p":0.014253257887517145,"count":{}},"7":{"p":0.03125,"count":{}},"8":{"p":0.07203789437585734,"count":{}},"9":{"p":0.12757201646090535,"count":{}},"10":{"p":0.21585648148148148,"count":{}},"11":{"p":0.27019032921810693,"count":{}},"12":{"p":0.2632244513031551,"count":{}}},"kh1of6d10":{"1":{"p":0.000001000000000005723,"count":{}},"2":{"p":0.00006300000000036046,"count":{}},"3":{"p":0.0006650000000038152,"count":{}},"4":{"p":0.0033670000000193905,"count":{}},"5":{"p":0.011529000000063959,"count":{}},"6":{"p":0.03103100000018796,"count":{}},"7":{"p":0.07099300000045661,"count":{}},"8":{"p":0.14449500000095072,"count":{}},"9":{"p":0.2692970000012541,"count":{}},"10":{"p":0.4685589999970631,"count":{}}},"kh1of10d20":{"1":{"p":9.765624999999984e-14,"count":{}},"2":{"p":9.990234374999984e-11,"count":{}},"3":{"p":5.666503906249996e-9,"count":{}},"4":{"p":9.663349609374983e-8,"count":{}},"5":{"p":8.51274316406248e-7,"count":{}},"6":{"p":0.0000049512256835937364,"count":{}},"7":{"p":0.000021680573535156185,"count":{}},"8":{"p":0.0000772721264648435,"count":{}},"9":{"p":0.0002356486891601555,"count":{}},"10":{"p":0.0006360562108398416,"count":{}},"11":{"p":0.001556389121191401,"count":{}},"12":{"p":0.0035136659788085863,"count":{}},"13":{"p":0.007416125744628898,"count":{}},"14":{"p":0.014784781555371081,"count":{}},"15":{"p":0.02806598980947265,"count":{}},"16":{"p":0.05106066769052736,"count":{}},"17":{"p":0.08950022194072271,"count":{}},"18":{"p":0.15180403575927748,"count":{}},"19":{"p":0.2500584991383792,"count":{}},"20":{"p":0.40126306076162066,"count":{}}},"kl1of5d8":{"1":{"p":0.487091064453125,"count":{}},"2":{"p":0.275604248046875,"count":{}},"3":{"p":0.141937255859375,"count":{}},"4":{"p":0.064117431640625,"count":{}},"5":{"p":0.023834228515625,"count":{}},"6":{"p":0.006439208984375,"count":{}},"7":{"p":0.000946044921875,"count":{}},"8":{"p":0.000030517578125,"count":{}}},"maxOf8_d8":{"1":{"p":5.960464477539063e-8,"count":{}},"2":{"p":0.00001519918441772461,"count":{}},"3":{"p":0.0003758072853088379,"count":{}},"4":{"p":0.003515183925628662,"count":{}},"5":{"p":0.019376814365386963,"count":{}},"6":{"p":0.07682985067367554,"count":{}},"7":{"p":0.24349600076675415,"count":{}},"8":{"p":0.6563910841941833,"count":{}}},"maxOf3_d6":{"1":{"p":0.0046296296296296285,"count":{}},"2":{"p":0.0324074074074074,"count":{}},"3":{"p":0.08796296296296292,"count":{}},"4":{"p":0.1712962962962961,"count":{}},"5":{"p":0.2824074074074072,"count":{}},"6":{"p":0.42129629629629667,"count":{}}},"branch_0.3":{"2":{"p":0.0875,"count":{"hit":0.0875},"attr":{"hit":0.175}},"3":{"p":0.0875,"count":{"hit":0.0875},"attr":{"hit":0.26249999999999996}},"4":{"p":0.0875,"count":{"hit":0.0875},"attr":{"hit":0.35}},"5":{"p":0.09583333333333333,"count":{"hit":0.09583333333333333},"attr":{"hit":0.4791666666666667}},"6":{"p":0.10416666666666666,"count":{"hit":0.10416666666666666},"attr":{"hit":0.6249999999999999}},"7":{"p":0.11249999999999999,"count":{"hit":0.11249999999999999},"attr":{"hit":0.7875}},"8":{"p":0.12083333333333332,"count":{"hit":0.12083333333333332},"attr":{"hit":0.9666666666666666}},"9":{"p":0.12916666666666665,"count":{"hit":0.12916666666666665},"attr":{"hit":1.1625}},"10":{"p":0.049999999999999996,"count":{"hit":0.049999999999999996},"attr":{"hit":0.5}},"11":{"p":0.041666666666666664,"count":{"hit":0.041666666666666664},"attr":{"hit":0.45833333333333326}},"12":{"p":0.03333333333333333,"count":{"hit":0.03333333333333333},"attr":{"hit":0.39999999999999997}},"13":{"p":0.024999999999999998,"count":{"hit":0.024999999999999998},"attr":{"hit":0.32499999999999996}},"14":{"p":0.016666666666666666,"count":{"hit":0.016666666666666666},"attr":{"hit":0.23333333333333334}},"15":{"p":0.008333333333333333,"count":{"hit":0.008333333333333333},"attr":{"hit":0.125}}},"branch_withProbability_0.25":{"0":{"p":0.75,"count":{"miss":0.75},"attr":{}},"5":{"p":0.006944444444444444,"count":{"hit":0.006944444444444444},"attr":{"hit":0.034722222222222224}},"6":{"p":0.013888888888888888,"count":{"hit":0.013888888888888888},"attr":{"hit":0.08333333333333333}},"7":{"p":0.020833333333333332,"count":{"hit":0.020833333333333332},"attr":{"hit":0.14583333333333334}},"8":{"p":0.027777777777777776,"count":{"hit":0.027777777777777776},"attr":{"hit":0.2222222222222222}},"9":{"p":0.034722222222222224,"count":{"hit":0.034722222222222224},"attr":{"hit":0.3125}},"10":{"p":0.041666666666666664,"count":{"hit":0.041666666666666664},"attr":{"hit":0.4166666666666667}},"11":{"p":0.034722222222222224,"count":{"hit":0.034722222222222224},"attr":{"hit":0.3819444444444444}},"12":{"p":0.027777777777777776,"count":{"hit":0.027777777777777776},"attr":{"hit":0.3333333333333333}},"13":{"p":0.020833333333333332,"count":{"hit":0.020833333333333332},"attr":{"hit":0.2708333333333333}},"14":{"p":0.013888888888888888,"count":{"hit":0.013888888888888888},"attr":{"hit":0.19444444444444445}},"15":{"p":0.006944444444444444,"count":{"hit":0.006944444444444444},"attr":{"hit":0.10416666666666667}}},"gate_0.5":{"2":{"p":0.0625,"count":{"hit":0.0625},"attr":{"hit":0.125}},"3":{"p":0.0625,"count":{"hit":0.0625},"attr":{"hit":0.1875}},"4":{"p":0.0625,"count":{"hit":0.0625},"attr":{"hit":0.25}},"5":{"p":0.0763888888888889,"count":{"hit":0.0763888888888889},"attr":{"hit":0.3819444444444444}},"6":{"p":0.09027777777777778,"count":{"hit":0.09027777777777778},"attr":{"hit":0.5416666666666666}},"7":{"p":0.10416666666666666,"count":{"hit":0.10416666666666666},"attr":{"hit":0.7291666666666667}},"8":{"p":0.11805555555555555,"count":{"hit":0.11805555555555555},"attr":{"hit":0.9444444444444444}},"9":{"p":0.13194444444444445,"count":{"hit":0.13194444444444445},"attr":{"hit":1.1875}},"10":{"p":0.08333333333333333,"count":{"hit":0.08333333333333333},"attr":{"hit":0.8333333333333334}},"11":{"p":0.06944444444444445,"count":{"hit":0.06944444444444445},"attr":{"hit":0.7638888888888888}},"12":{"p":0.05555555555555555,"count":{"hit":0.05555555555555555},"attr":{"hit":0.6666666666666666}},"13":{"p":0.041666666666666664,"count":{"hit":0.041666666666666664},"attr":{"hit":0.5416666666666666}},"14":{"p":0.027777777777777776,"count":{"hit":0.027777777777777776},"attr":{"hit":0.3888888888888889}},"15":{"p":0.013888888888888888,"count":{"hit":0.013888888888888888},"attr":{"hit":0.20833333333333334}}},"reroll_d6_1":{"1":{"p":0.027777777777777776,"count":{"hit":0.027777777777777776},"attr":{"hit":0.027777777777777776}},"2":{"p":0.19444444444444445,"count":{"hit":0.19444444444444445},"attr":{"hit":0.3888888888888889}},"3":{"p":0.19444444444444445,"count":{"hit":0.19444444444444445},"attr":{"hit":0.5833333333333334}},"4":{"p":0.19444444444444445,"count":{"hit":0.19444444444444445},"attr":{"hit":0.7777777777777778}},"5":{"p":0.19444444444444445,"count":{"hit":0.19444444444444445},"attr":{"hit":0.9722222222222222}},"6":{"p":0.19444444444444445,"count":{"hit":0.19444444444444445},"attr":{"hit":1.1666666666666667}}},"reroll_d8_2":{"1":{"p":0.140625,"count":{"hit":0.140625},"attr":{"hit":0.140625}},"2":{"p":0.015625,"count":{"hit":0.015625},"attr":{"hit":0.03125}},"3":{"p":0.140625,"count":{"hit":0.140625},"attr":{"hit":0.421875}},"4":{"p":0.140625,"count":{"hit":0.140625},"attr":{"hit":0.5625}},"5":{"p":0.140625,"count":{"hit":0.140625},"attr":{"hit":0.703125}},"6":{"p":0.140625,"count":{"hit":0.140625},"attr":{"hit":0.84375}},"7":{"p":0.140625,"count":{"hit":0.140625},"attr":{"hit":0.984375}},"8":{"p":0.140625,"count":{"hit":0.140625},"attr":{"hit":1.125}}},"hd6":{"1":{"p":0.027777777777777776,"count":{"hit":0.027777777777777776},"attr":{"hit":0.027777777777777776}},"2":{"p":0.19444444444444445,"count":{"hit":0.19444444444444445},"attr":{"hit":0.3888888888888889}},"3":{"p":0.19444444444444445,"count":{"hit":0.19444444444444445},"attr":{"hit":0.5833333333333334}},"4":{"p":0.19444444444444445,"count":{"hit":0.19444444444444445},"attr":{"hit":0.7777777777777778}},"5":{"p":0.19444444444444445,"count":{"hit":0.19444444444444445},"attr":{"hit":0.9722222222222222}},"6":{"p":0.19444444444444445,"count":{"hit":0.19444444444444445},"attr":{"hit":1.1666666666666667}}},"d6_plus_d4":{"2":{"p":0.041666666666666664,"count":{"hit":0.041666666666666664},"attr":{"hit":0.08333333333333333}},"3":{"p":0.08333333333333333,"count":{"hit":0.08333333333333333},"attr":{"hit":0.25}},"4":{"p":0.125,"count":{"hit":0.125},"attr":{"hit":0.5}},"5":{"p":0.16666666666666666,"count":{"hit":0.16666666666666666},"attr":{"hit":0.8333333333333334}},"6":{"p":0.16666666666666666,"count":{"hit":0.16666666666666666},"attr":{"hit":1}},"7":{"p":0.16666666666666666,"count":{"hit":0.16666666666666666},"attr":{"hit":1.1666666666666667}},"8":{"p":0.125,"count":{"hit":0.125},"attr":{"hit":1}},"9":{"p":0.08333333333333333,"count":{"hit":0.08333333333333333},"attr":{"hit":0.75}},"10":{"p":0.041666666666666664,"count":{"hit":0.041666666666666664},"attr":{"hit":0.4166666666666667}}},"d8_max_d6":{"1":{"p":0.020833333333333332,"count":{"hit":0.020833333333333332},"attr":{"hit":0.020833333333333332}},"2":{"p":0.0625,"count":{"hit":0.0625},"attr":{"hit":0.125}},"3":{"p":0.10416666666666667,"count":{"hit":0.10416666666666667},"attr":{"hit":0.3125}},"4":{"p":0.14583333333333334,"count":{"hit":0.14583333333333334},"attr":{"hit":0.5833333333333334}},"5":{"p":0.1875,"count":{"hit":0.1875},"attr":{"hit":0.9375}},"6":{"p":0.22916666666666666,"count":{"hit":0.22916666666666666},"attr":{"hit":1.375}},"7":{"p":0.125,"count":{"hit":0.125},"attr":{"hit":0.875}},"8":{"p":0.125,"count":{"hit":0.125},"attr":{"hit":1}}},"compact_result":{"5":{"p":0.5,"count":{"hit":0.5},"attr":{"hit":2.5}},"7":{"p":0.5,"count":{"hit":0.5}}},"countquery_exactly":[0.08662499999999995,0.33762499999999995,0.4148750000000001,0.16087500000000002],"countquery_atLeast":[1,0.913375,0.5757500000000001,0.16087500000000002],"countquery_atMost":[0.08662499999999995,0.4242499999999999,0.839125,1],"countquery_exactly_single":[0.08662499999999995,0.33762499999999995,0.4148750000000001,0.16087500000000002]} diff --git a/tests/math-correctness.test.ts b/tests/math-correctness.test.ts new file mode 100644 index 0000000..f2c2f62 --- /dev/null +++ b/tests/math-correctness.test.ts @@ -0,0 +1,456 @@ +import { describe, expect, it } from "vitest"; +import { DiceQuery, EPS, parse, PMF } from "../src/index"; +import { d20, d4, roll } from "../src/builder/index"; + +/** + * Mathematical-correctness lock-down suite. + * + * Each expectation is checked against an INDEPENDENT brute-force reference + * (full enumeration of the sample space) or a hand-derived closed form — never + * against the engine's own output. This both confirms the bugs fixed in the + * correctness pass and guards the invariants that must always hold. + */ + +// --------------------------------------------------------------------------- +// Independent brute-force reference (no engine code) +// --------------------------------------------------------------------------- +type Dist = Map; // value -> probability + +function diceDist(count: number, sides: number, flat = 0): Dist { + let cur: Dist = new Map([[flat, 1]]); + for (let i = 0; i < count; i++) { + const next: Dist = new Map(); + for (const [v, p] of cur) + for (let f = 1; f <= sides; f++) + next.set(v + f, (next.get(v + f) ?? 0) + p / sides); + cur = next; + } + return cur; +} +function scaleProb(a: Dist, w: number): Dist { + const o: Dist = new Map(); + for (const [k, v] of a) o.set(k, v * w); + return o; +} +function mixDist(parts: Dist[]): Dist { + const o: Dist = new Map(); + for (const d of parts) for (const [k, v] of d) o.set(k, (o.get(k) ?? 0) + v); + return o; +} +function d20faces(mode: "flat" | "adv" | "dis" | "elven"): Dist { + const o: Dist = new Map(); + if (mode === "flat") { + for (let r = 1; r <= 20; r++) o.set(r, 1 / 20); + return o; + } + if (mode === "elven") { + for (let a = 1; a <= 20; a++) + for (let b = 1; b <= 20; b++) + for (let c = 1; c <= 20; c++) { + const r = Math.max(a, b, c); + o.set(r, (o.get(r) ?? 0) + 1 / 8000); + } + return o; + } + for (let a = 1; a <= 20; a++) + for (let b = 1; b <= 20; b++) { + const r = mode === "adv" ? Math.max(a, b) : Math.min(a, b); + o.set(r, (o.get(r) ?? 0) + 1 / 400); + } + return o; +} +function attack( + mode: "flat" | "adv" | "dis" | "elven", + mod: number, + ac: number, + normalDmg: Dist, + critDmg: Dist +) { + const faces = d20faces(mode); + const parts: Dist[] = []; + let pHit = 0, + pCrit = 0, + pMiss = 0; + for (const [r, p] of faces) { + if (r === 1) { + pMiss += p; + parts.push(scaleProb(new Map([[0, 1]]), p)); + } else if (r === 20) { + pCrit += p; + parts.push(scaleProb(critDmg, p)); + } else if (r + mod >= ac) { + pHit += p; + parts.push(scaleProb(normalDmg, p)); + } else { + pMiss += p; + parts.push(scaleProb(new Map([[0, 1]]), p)); + } + } + return { dist: mixDist(parts), pHit, pCrit, pMiss }; +} +const meanOf = (d: Dist) => { + let m = 0; + for (const [k, v] of d) m += k * v; + return m; +}; +const varOf = (d: Dist) => { + const mu = meanOf(d); + let v = 0; + for (const [k, p] of d) v += (k - mu) * (k - mu) * p; + return v; +}; +const massOf = (d: Dist) => { + let s = 0; + for (const v of d.values()) s += v; + return s; +}; + +// --------------------------------------------------------------------------- + +describe("Convolution & mass invariants", () => { + it("sum of independent dice matches full enumeration bin-by-bin", () => { + for (const [c, s] of [ + [2, 6], + [3, 4], + [2, 8], + ] as const) { + const ref = diceDist(c, s); + const pmf = parse(`${c}d${s}`).normalize(); + expect(pmf.mass()).toBeCloseTo(1, 12); + for (const [v, p] of ref) expect(pmf.pAt(v)).toBeCloseTo(p, 12); + } + }); + + it("a zero-mass operand convolves to mass 0, never NaN", () => { + const d6 = parse("1d6"); + const c = d6.convolve(PMF.emptyMass()); + expect(Number.isNaN(c.mass())).toBe(false); + expect(c.mass()).toBe(0); + for (const [, bin] of c) expect(bin.p).toBe(0); + + // raw and non-raw paths now agree + expect(d6.combineRaw(PMF.emptyMass()).mass()).toBe(0); + + // and it does not poison a DiceQuery + const q = new DiceQuery([d6, PMF.emptyMass()]); + expect(Number.isNaN(q.combined.mass())).toBe(false); + expect(Number.isNaN(q.mean())).toBe(false); + }); + + it("convolveMany with a zeroed middle operand and power() stay finite", () => { + const d6 = parse("1d6"); + expect(PMF.convolveMany([d6, d6.scaleMass(0), d6]).mass()).toBe(0); + expect(PMF.emptyMass().power(3).mass()).toBe(0); + }); +}); + +describe("Moments (additivity, stability, consistency)", () => { + it("mean & variance match brute force for a realistic attack", () => { + const ref = attack("flat", 5, 15, diceDist(1, 8, 3), diceDist(2, 8, 3)); + const q = parse("(d20 + 5 AC 15) * (1d8 + 3) crit (2d8 + 3)").query(); + expect(q.mean()).toBeCloseTo(meanOf(ref.dist), 9); + expect(q.variance()).toBeCloseTo(varOf(ref.dist), 6); + expect(q.stddev()).toBeCloseTo(Math.sqrt(varOf(ref.dist)), 6); + }); + + it("variance is shift-invariant (no catastrophic cancellation at scale)", () => { + // Var(1d6 + K) = Var(1d6) = 35/12 for every offset K. + for (const K of [0, 1e3, 1e6, 1e8, 1e10]) { + expect(parse(`1d6 + ${K}`).query().variance()).toBeCloseTo(35 / 12, 9); + } + }); + + it("mean/variance stay consistent with an explicitly-provided combined", () => { + const provided = PMF.fromMap(new Map([[100, 1]])); + const q = new DiceQuery([parse("1d6")], provided); // combined != convolve(singles) + expect(q.mean()).toBe(q.combined.mean()); + expect(q.mean()).toBe(100); + expect(q.variance()).toBe(q.combined.variance()); + }); + + it("mean is additive across independent attacks", () => { + const single = parse("(d20 + 7 AC 16) * (2d6 + 4) crit (4d6 + 4)"); + const one = new DiceQuery([single]).mean(); + for (const n of [1, 2, 3, 4]) { + expect(new DiceQuery(single.replicate(n)).mean()).toBeCloseTo(one * n, 9); + } + }); +}); + +describe("Counting queries (Poisson-binomial)", () => { + const single = parse("(d20 + 5 AC 15) * (1d8 + 3) crit (2d8 + 3)"); + // per-attack success probabilities (from the brute-force attack model) + const ref = attack("flat", 5, 15, diceDist(1, 8, 3), diceDist(2, 8, 3)); + const pCrit = ref.pCrit; // 0.05 + const pSucc = ref.pHit + ref.pCrit; // hit or crit + + const binom = (n: number, k: number, p: number) => { + const c = (a: number, b: number) => { + let r = 1; + for (let i = 0; i < b; i++) r = (r * (a - i)) / (i + 1); + return r; + }; + return c(n, k) * p ** k * (1 - p) ** (n - k); + }; + + it("probExactlyK string-path and array-path agree and match the true binomial", () => { + const q = new DiceQuery(single.replicate(3)); + for (let k = 0; k <= 3; k++) { + const truth = binom(3, k, pCrit); + expect(q.probExactlyK("crit", k)).toBeCloseTo(truth, 9); + expect(q.probExactlyK(["crit"], k)).toBeCloseTo(truth, 9); // array path (was badly wrong) + } + }); + + it("probAtLeastOne / probAtLeastK / probAtMostK match enumeration", () => { + const q = new DiceQuery(single.replicate(3)); + expect(q.probAtLeastOne(["hit", "crit"])).toBeCloseTo( + 1 - (1 - pSucc) ** 3, + 9 + ); + expect(q.probAtLeastK("crit", 1)).toBeCloseTo(1 - (1 - pCrit) ** 3, 9); + expect(q.probAtMostK("crit", 0)).toBeCloseTo((1 - pCrit) ** 3, 9); + }); + + it("probabilityOf equals the marginal P(>=1), not an over-count of bins", () => { + const q = new DiceQuery(single.replicate(2)); + // P(>=1 crit) over 2 attacks = 1 - 0.95^2 = 0.0975 (NOT the bin-mass sum) + expect(q.probabilityOf("crit")).toBeCloseTo(1 - (1 - pCrit) ** 2, 9); + expect(q.probabilityOf("crit")).toBeCloseTo(q.probAtLeastOne("crit"), 12); + }); + + it("single-attack probabilityOf('crit') is exactly P(nat 20) = 0.05", () => { + expect(new DiceQuery([single]).probabilityOf("crit")).toBeCloseTo(0.05, 9); + }); + + it("missChance matches P(miss) by enumeration", () => { + // single attack d20+5 vs AC15: miss on nat1 or total<15 -> 9/20 = 0.45 + const q = parse("(d20 + 5 AC 15) * (1d6)").query(); + expect(q.missChance()).toBeCloseTo(0.45, 9); + }); + + it("counting queries are invariant to single mass scaling", () => { + const q1 = new DiceQuery([single, single]); + const q2 = new DiceQuery([single.scaleMass(0.5), single.scaleMass(0.5)]); + expect(q2.probAtLeastOne("crit")).toBeCloseTo(q1.probAtLeastOne("crit"), 9); + expect(q2.probExactlyK("crit", 1)).toBeCloseTo(q1.probExactlyK("crit", 1), 9); + }); +}); + +describe("Parser D&D mechanics vs brute force", () => { + it("flat attack: mean, hit and crit probabilities", () => { + const ref = attack("flat", 6, 15, diceDist(2, 6, 4), diceDist(4, 6, 4)); + const q = parse("(d20 + 6 AC 15) * (2d6 + 4) crit (4d6 + 4)").query(); + expect(q.mean()).toBeCloseTo(meanOf(ref.dist), 9); + expect(q.probAtLeastOne("crit")).toBeCloseTo(ref.pCrit, 9); // 0.05 + expect(q.probAtLeastOne("hit")).toBeCloseTo(ref.pHit, 9); + }); + + it("advantage and disadvantage hit probabilities", () => { + const adv = attack("adv", 7, 16, diceDist(1, 8, 3), diceDist(2, 8, 3)); + const qa = parse("(d20 > d20 + 7 AC 16) * (1d8 + 3) crit (2d8 + 3)").query(); + expect(qa.mean()).toBeCloseTo(meanOf(adv.dist), 9); + expect(qa.probAtLeastOne("hit")).toBeCloseTo(adv.pHit, 9); + + const dis = attack("dis", 7, 16, diceDist(1, 8, 3), diceDist(2, 8, 3)); + const qd = parse("(d20 < d20 + 7 AC 16) * (1d8 + 3) crit (2d8 + 3)").query(); + expect(qd.mean()).toBeCloseTo(meanOf(dis.dist), 9); + expect(qd.probAtLeastOne("hit")).toBeCloseTo(dis.pHit, 9); + }); + + it("elven accuracy (triple advantage) crit probability", () => { + // P(crit) = 1 - P(no 20 in 3 rolls) = 1 - (19/20)^3 + const q = parse("(d20 > d20 > d20 + 7 AC 16) * (1d8) crit (2d8)").query(); + expect(q.probAtLeastOne("crit")).toBeCloseTo(1 - (19 / 20) ** 3, 9); + }); + + it("hd (reroll-one) shorthand: P(1)=1/s^2 and matches reroll(1)", () => { + expect(parse("hd6").pAt(1)).toBeCloseTo(1 / 36, 12); + expect(parse("hd6").mean()).toBeCloseTo(parse("d6 reroll 1").mean(), 12); + expect(parse("hd20").pAt(1)).toBeCloseTo(1 / 400, 12); + expect(parse("hd20").mean()).toBeCloseTo(10.975, 9); + // string hd matches the builder reroll(1) + expect(parse("2hd6").mean()).toBeCloseTo( + roll(2).d(6).reroll(1).pmf.mean(), + 12 + ); + }); + + it("keep-highest pool 4kh3d6 matches enumeration mean", () => { + // mean of sum of top 3 of 4d6 = 12.2446... + let total = 0; + for (let a = 1; a <= 6; a++) + for (let b = 1; b <= 6; b++) + for (let c = 1; c <= 6; c++) + for (let d = 1; d <= 6; d++) { + const xs = [a, b, c, d].sort((x, y) => y - x); + total += xs[0] + xs[1] + xs[2]; + } + const refMean = total / 6 ** 4; + expect(parse("4kh3d6").mean()).toBeCloseTo(refMean, 9); + }); + + it("save-for-half: mean and (full vs half) split by enumeration", () => { + // (d20 DC 15) * 8d6 save half: fail (roll<15) -> full 8d6; success -> floor(8d6/2) + const full = diceDist(8, 6); + const half: Dist = new Map(); + for (const [k, v] of full) + half.set(Math.floor(k / 2), (half.get(Math.floor(k / 2)) ?? 0) + v); + let pFail = 0, + pSucc = 0; + for (let r = 1; r <= 20; r++) (r >= 15 ? (pSucc += 1 / 20) : (pFail += 1 / 20)); + const ref = mixDist([scaleProb(full, pFail), scaleProb(half, pSucc)]); + const q = parse("(d20 + 0 DC 15) * 8d6 save half").query(); + expect(q.mean()).toBeCloseTo(meanOf(ref), 7); + expect(q.probAtLeastOne("saveFail")).toBeCloseTo(pFail, 6); + expect(q.probAtLeastOne("saveHalf")).toBeCloseTo(pSucc, 6); + }); + + it("save-for-half labels are correct for constant / odd damage", () => { + // (d20 DC 15) * (D) save half: fail (roll<15, 0.7) -> full D; success (0.3) -> + // floor(D/2). The success mass is saveHalf, the failure mass is saveFail, and + // there is no "hit" mass. This previously mislabeled on odd/constant damage. + for (const [expr, D] of [ + ["(d20 + 0 DC 15) * (3) save half", 3], + ["(d20 + 0 DC 15) * (1d6 + 3) save half", null], + ["(d20 + 0 DC 15) * (1d4 + 1) save half", null], + ] as const) { + const q = parse(expr).query(); + expect(q.probAtLeastOne("saveFail")).toBeCloseTo(0.7, 6); + expect(q.probAtLeastOne("saveHalf")).toBeCloseTo(0.3, 6); + expect(q.probAtLeastOne("hit")).toBeCloseTo(0, 6); + if (D !== null) { + // mean = 0.7*D + 0.3*floor(D/2) + expect(q.mean()).toBeCloseTo(0.7 * D + 0.3 * Math.floor(D / 2), 9); + } + } + }); +}); + +describe("Mixtures & guards", () => { + it("PMF.exclusive produces the exact weighted mixture and mass 1", () => { + const a = PMF.delta(2); + const b = PMF.delta(5); + const mix = PMF.exclusive([ + [a, 0.3], + [b, 0.7], + ]); + expect(mix.mass()).toBeCloseTo(1, 12); + expect(mix.pAt(2)).toBeCloseTo(0.3, 12); + expect(mix.pAt(5)).toBeCloseTo(0.7, 12); + }); + + it("PMF.exclusive sends leftover weight to zero", () => { + const mix = PMF.exclusive([[PMF.delta(5), 0.6]]); + expect(mix.mass()).toBeCloseTo(1, 12); + expect(mix.pAt(0)).toBeCloseTo(0.4, 12); + expect(mix.pAt(5)).toBeCloseTo(0.6, 12); + }); + + it("branch is a proper Bernoulli mixture", () => { + const out = PMF.branch(PMF.delta(10), PMF.delta(0), 0.25); + expect(out.pAt(10)).toBeCloseTo(0.25, 12); + expect(out.pAt(0)).toBeCloseTo(0.75, 12); + }); + + it("firstSuccessWeights: valid inputs sum correctly, invalid inputs throw", () => { + const w = PMF.firstSuccessWeights(0.5, 0.05, 3); + expect(w.pSpecificSuccess + w.pGeneralSuccess + w.pNone).toBeCloseTo(1, 12); + expect(w.pAny).toBeCloseTo(1 - 0.5 ** 3, 12); + expect(() => PMF.firstSuccessWeights(0.3, 0.5, 2)).toThrow(); // pSpecial > pSuccess + }); +}); + +describe("Distribution invariants hold across the corpus", () => { + const exprs = [ + "(d20 + 6 AC 15) * (2d6 + 4)", + "(d20 + 6 AC 15) * (2d6 + 4) crit (4d6 + 4)", + "(d20 > d20 + 7 AC 16) * (1d8 + 3) crit (2d8 + 3)", + "(d20 + 0 DC 15) * 8d6 save half", + "4kh3d6", + "hd20", + ]; + for (const e of exprs) { + it(`${e}: mass 1, monotone CDF, probabilities in [0,1]`, () => { + const q = parse(e).query(); + expect(q.combined.mass()).toBeCloseTo(1, 9); + let prev = -1; + for (const x of q.combined.support()) { + const c = q.cdf(x); + expect(c).toBeGreaterThanOrEqual(prev - 1e-9); + expect(c).toBeLessThanOrEqual(1 + 1e-9); + prev = c; + } + expect(q.cdf(q.max())).toBeCloseTo(1, 9); + }); + } +}); + +describe("KNOWN LIMITATION: parser crit probability with bonus to-hit dice", () => { + // The string parser cannot separate the natural-20 slice once bonus dice are + // convolved into the to-hit (the d20 identity is lost), so crit probability + // collapses to 1/(20·∏bonusSides). The BUILDER API computes it correctly. + // This test locks the correct builder behavior and pins the known parser gap + // so any change to either is caught. See CHANGELOG "Known limitations". + it("builder computes crit = 0.05 with a +1d4 (bless) to-hit", () => { + const b = d20 + .plus(5) + .plus(d4) + .ac(15) + .onHit(roll(1, 8).plus(3)) + .onCrit(roll(2, 8).plus(3)); + expect(b.resolve().weights.crit).toBeCloseTo(0.05, 9); + }); + + it("parser without bonus to-hit dice is correct (crit = 0.05)", () => { + const q = parse("(d20 + 5 AC 15) * (1d8 + 3) crit (2d8 + 3)").query(); + expect(q.probAtLeastOne("crit")).toBeCloseTo(0.05, 9); + }); + + it("parser WITH bonus to-hit dice is currently wrong (pinned)", () => { + // KNOWN BUG: should be 0.05 but the parser returns 1/80 = 0.0125. + // Pinned so the discrepancy is tracked; update when the parser is fixed. + const q = parse("(d20 + 5 + 1d4 AC 15) * (1d8 + 3) crit (2d8 + 3)").query(); + expect(q.probAtLeastOne("crit")).toBeCloseTo(0.0125, 6); + }); +}); + +describe("Conditional statistics: single-attack correctness + multi-attack caveat", () => { + const single = parse("(d20 + 5 AC 15) * (1d6) crit (2d6)"); + + it("single-attack damageStatsFrom is the true conditional expectation", () => { + // hit damage is 1d6 -> E=3.5, range 1..6, count = P(hit) = 0.5 + const hit = new DiceQuery([single]).damageStatsFrom("hit"); + expect(hit.avg).toBeCloseTo(3.5, 9); + expect(hit.min).toBe(1); + expect(hit.max).toBe(6); + expect(hit.count).toBeCloseTo(0.5, 9); + }); + + it("single-attack snapshot probabilities are valid (<= 1)", () => { + const snap = new DiceQuery([single]).snapshot(["hit", "crit", "missNone"]); + expect(snap.outcomes.get("hit")!.atLeastOneProbability).toBeCloseTo(0.5, 9); + expect(snap.outcomes.get("crit")!.atLeastOneProbability).toBeCloseTo( + 0.05, + 9 + ); + }); + + it("multi-attack snapshot reports valid Poisson-binomial probabilities", () => { + const q = new DiceQuery([single, single, single, single]); // 4 attacks, P(hit)=0.5 each + const snap = q.snapshot(["hit"]); + const hit = snap.outcomes.get("hit")!; + // P(>=1 hit) = 1 - 0.5^4 = 0.9375 ; P(all 4 hit) = 0.5^4 = 0.0625 + expect(hit.atLeastOneProbability).toBeCloseTo(1 - 0.5 ** 4, 9); + expect(hit.atLeastOneProbability).toBeLessThanOrEqual(1); + expect(hit.allProbability).toBeCloseTo(0.5 ** 4, 9); + }); + + it("KNOWN LIMITATION: multi-attack damageRange.avg is size-biased (pinned)", () => { + // damageRange.avg still weights by expected count (E[dmg·#label]/E[#label]), + // so it is not a clean conditional mean for N>=2. Pinned for tracking. + const q = new DiceQuery([single, single, single, single]); + const avg = q.snapshot(["hit"]).outcomes.get("hit")!.damageRange.avg; + expect(avg).toBeGreaterThan(0); // documented size-biased quantity, not E[dmg|>=1 hit] + }); +}); diff --git a/tests/mixture.test.ts b/tests/mixture.test.ts index 3bf87af..861bf21 100644 --- a/tests/mixture.test.ts +++ b/tests/mixture.test.ts @@ -256,34 +256,17 @@ describe("Mixture Helpers", () => { expect(pmfs).toEqual({}); }); - it("getProb (via add) should correctly extract probability from Bin or number", () => { + it("add should correctly extract probability from each [value, Bin] pair", () => { const mix = new Mixture(); - // Create a mock PMF-like object that iterates with Bin objects - const pmfWithBins = { - [Symbol.iterator]: function* () { - yield [1, { p: 0.25, count: {} }]; - yield [2, { p: 0.75, count: {} }]; - }, - }; - - // @ts-expect-error - using a mock PMF - mix.add("a", pmfWithBins, 1); + mix.add("a", PMF.fromMap(new Map([[1, 0.25], [2, 0.75]])), 1); const pmf1 = mix.buildPMF(); expect(pmf1.pAt(1)).toBeCloseTo(0.25, 12); expect(pmf1.pAt(2)).toBeCloseTo(0.75, 12); mix.clear(); - // Create a mock PMF-like object that iterates with raw numbers (less common) - const pmfWithNumbers = { - [Symbol.iterator]: function* () { - yield [5, 0.4]; - yield [6, 0.6]; - }, - }; - // @ts-expect-error - using a mock PMF - mix.add("b", pmfWithNumbers, 1); + mix.add("b", PMF.fromMap(new Map([[5, 0.4], [6, 0.6]])), 1); const pmf2 = mix.buildPMF(); expect(pmf2.pAt(5)).toBeCloseTo(0.4, 12); expect(pmf2.pAt(6)).toBeCloseTo(0.6, 12); diff --git a/tests/parser-resource-guards.test.ts b/tests/parser-resource-guards.test.ts new file mode 100644 index 0000000..0a46a2b --- /dev/null +++ b/tests/parser-resource-guards.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; +import { DiceParseError, parse } from "../src/index"; + +/** + * Resource-exhaustion guards (item 7). + * + * Adversarial expressions — an enormous die, an enormous dice count, or a keep + * over a combinatorially huge enumerated pool — must be rejected rather than + * exhausting memory/CPU. The caps are generous enough that every legitimate + * expression still parses. + */ +describe("parser resource-exhaustion guards", () => { + describe("die size cap", () => { + it("rejects a die larger than the cap", () => { + expect(() => parse("d2000000")).toThrow(/exceeds the maximum/); + expect(() => parse("d2000000")).toThrow(DiceParseError); + }); + + it("still allows large-but-legal dice (d100000)", () => { + const r = parse("d100000"); + expect(r.support().length).toBe(100000); + expect(r.max()).toBe(100000); + }); + + it("allows a normal die", () => { + expect(parse("d20").max()).toBe(20); + }); + }); + + describe("dice count cap", () => { + it("rejects a dice count larger than the cap", () => { + expect(() => parse("10001d2")).toThrow(/Dice count .* exceeds the maximum/); + expect(() => parse("20000d6")).toThrow(DiceParseError); + }); + + it("still allows a modest dice count", () => { + const r = parse("50d6"); + expect(r.min()).toBe(50); + expect(r.max()).toBe(300); + }); + }); + + describe("keep enumeration cap", () => { + it("rejects a keep over a combinatorially huge pool", () => { + // 10^10 enumerated outcomes — far over the cap. + expect(() => parse("10kh1d10")).toThrow(/Keep enumeration .* exceeds the maximum/); + expect(() => parse("10kh1d10")).toThrow(DiceParseError); + }); + + it("still allows a normal keep pool (4kh3d6 = 6^4 outcomes)", () => { + const r = parse("4kh3d6"); + // keep highest 3 of 4d6 -> support 3..18 + expect(r.min()).toBe(3); + expect(r.max()).toBe(18); + expect(r.mass()).toBeCloseTo(1, 9); + }); + }); + + describe("binary-operation work budget", () => { + it("rejects a binary op between two large dice (product of face counts)", () => { + // Each operand is individually legal (well under the die-size cap), but the + // dice×dice operation is O(faces1 × faces2) and would otherwise hang. + expect(() => parse("d20000 + d20000")).toThrow(/face pairs.*exceeds the maximum/); + expect(() => parse("d20000 + d20000")).toThrow(DiceParseError); + // Also covers other dice×dice operators (multiply / comparison). + expect(() => parse("d20000 ** d20000")).toThrow(DiceParseError); + expect(() => parse("d50000 > d50000")).toThrow(DiceParseError); + }); + + it("still allows a normal binary op between modest dice", () => { + const r = parse("d1000 + d1000"); // 10^6 face pairs, well under the budget + expect(r.min()).toBe(2); + expect(r.max()).toBe(2000); + expect(r.mass()).toBeCloseTo(1, 9); + }); + }); +}); diff --git a/tests/pmf-advanced.test.ts b/tests/pmf-advanced.test.ts index d5eb236..1647f8f 100644 --- a/tests/pmf-advanced.test.ts +++ b/tests/pmf-advanced.test.ts @@ -118,13 +118,13 @@ describe("PMF Advanced Operations", () => { it("should throw for non-positive integers", () => { expect(() => d6.replicate(0)).toThrow( - "combineN(n): n must be a positive integer" + "replicate(n): n must be a positive integer" ); expect(() => d6.replicate(-1)).toThrow( - "combineN(n): n must be a positive integer" + "replicate(n): n must be a positive integer" ); expect(() => d6.replicate(2.5)).toThrow( - "combineN(n): n must be a positive integer" + "replicate(n): n must be a positive integer" ); }); diff --git a/tests/refactor-characterization.test.ts b/tests/refactor-characterization.test.ts new file mode 100644 index 0000000..cc369b7 --- /dev/null +++ b/tests/refactor-characterization.test.ts @@ -0,0 +1,271 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { DiceQuery, EPS, parse, PMF } from "../src/index"; +import { d6, d8, d10, d20 } from "../src/builder/index"; + +/** + * Characterization / lock-down suite for the optimization + correctness pass. + * + * Each scenario's output was captured from the pre-change branch and frozen in + * tests/fixtures/refactor-characterization.json. The behavior-preserving + * refactors (integer DP keys, O(N) max-of CDF, single-pass branch mixture, + * Set-based reroll, hoisted binaryOp keys, re-derived count queries) must leave + * these outputs unchanged. compact()'s captured RESULT must also be unchanged; + * its separate test below additionally asserts the source is no longer mutated. + */ + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const baseline = JSON.parse( + readFileSync(join(__dirname, "fixtures/refactor-characterization.json"), "utf8") +) as Record; + +type BinObj = { p: number; count: Record; attr?: Record }; + +function pmfToObj(p: PMF): Record { + const out: Record = {}; + for (const [k, bin] of [...p.map.entries()].sort((a, b) => a[0] - b[0])) { + out[k] = { + p: bin.p, + count: { ...bin.count } as Record, + attr: bin.attr ? ({ ...bin.attr } as Record) : undefined, + }; + } + return out; +} + +// Deep float comparison with tight tolerance. The refactors are intended to be +// bit-identical; the tolerance only absorbs sub-ULP re-association. +function expectClose(actual: number, expected: number, label: string) { + const tol = 1e-12 + 1e-9 * Math.abs(expected); + expect( + Math.abs(actual - expected) <= tol, + `${label}: got ${actual}, expected ${expected}` + ).toBe(true); +} + +function expectPMFMatches(actual: Record, key: string) { + const expected = baseline[key] as Record; + expect(Object.keys(actual).sort(), `${key}: support`).toEqual( + Object.keys(expected).sort() + ); + for (const dmg of Object.keys(expected)) { + const a = actual[dmg]; + const e = expected[dmg]; + expect(a, `${key}: bin ${dmg} present`).toBeDefined(); + expectClose(a.p, e.p, `${key} bin ${dmg} p`); + const ckeys = new Set([ + ...Object.keys(a.count ?? {}), + ...Object.keys(e.count ?? {}), + ]); + for (const c of ckeys) { + expectClose(a.count?.[c] ?? 0, e.count?.[c] ?? 0, `${key} bin ${dmg} count.${c}`); + } + const akeys = new Set([ + ...Object.keys(a.attr ?? {}), + ...Object.keys(e.attr ?? {}), + ]); + for (const c of akeys) { + expectClose(a.attr?.[c] ?? 0, e.attr?.[c] ?? 0, `${key} bin ${dmg} attr.${c}`); + } + } +} + +describe("refactor characterization: keepSumPMF DP (item 2)", () => { + it("keepHighest(4,3) of d6", () => + expectPMFMatches(pmfToObj(d6.keepHighest(4, 3).toPMF()), "kh3of4d6")); + it("keepLowest(5,2) of d8", () => + expectPMFMatches(pmfToObj(d8.keepLowest(5, 2).toPMF()), "kl2of5d8")); + it("keepHighest(6,2) of d6", () => + expectPMFMatches(pmfToObj(d6.keepHighest(6, 2).toPMF()), "kh2of6d6")); +}); + +describe("refactor characterization: computeMaxOfPMF (item 3)", () => { + it("keepHighest(6,1) of d10 (small enumerate)", () => + expectPMFMatches(pmfToObj(d10.keepHighest(6, 1).toPMF()), "kh1of6d10")); + it("keepHighest(10,1) of d20 (large-count CDF path)", () => + expectPMFMatches(pmfToObj(d20.keepHighest(10, 1).toPMF()), "kh1of10d20")); + it("keepLowest(5,1) of d8 (min via negated max)", () => + expectPMFMatches(pmfToObj(d8.keepLowest(5, 1).toPMF()), "kl1of5d8")); + it("maxOf(8) of d8 (large-count CDF path)", () => + expectPMFMatches(pmfToObj(d8.maxOf(8).toPMF()), "maxOf8_d8")); + it("maxOf(3) of d6 (small enumerate)", () => + expectPMFMatches(pmfToObj(d6.maxOf(3).toPMF()), "maxOf3_d6")); +}); + +describe("refactor characterization: PMF.branch (item 4)", () => { + it("branch(2d6+3, 1d8+1, 0.3)", () => { + const out = PMF.branch(parse("2d6+3"), parse("1d8+1"), 0.3); + expectPMFMatches(pmfToObj(out), "branch_0.3"); + }); + it("withProbability(2d6+3, 0.25)", () => + expectPMFMatches( + pmfToObj(PMF.withProbability(parse("2d6+3"), 0.25)), + "branch_withProbability_0.25" + )); + it("gate(0.5)", () => + expectPMFMatches(pmfToObj(parse("2d6+3").gate(0.5, parse("1d8+1"))), "gate_0.5")); + it("preserves identifier (feeds convolve cache key)", () => { + const a = parse("2d6+3"); + const b = parse("1d8+1"); + const out = PMF.branch(a, b, 0.3); + expect(out.identifier).toBe( + `branch(${b.identifier}*${(0.7).toFixed(6)} + ${a.identifier}*${(0.3).toFixed(6)})` + ); + }); +}); + +describe("refactor characterization: reroll (item 5)", () => { + it("d6 reroll 1", () => expectPMFMatches(pmfToObj(parse("d6 reroll 1")), "reroll_d6_1")); + it("d8 reroll 2", () => expectPMFMatches(pmfToObj(parse("d8 reroll 2")), "reroll_d8_2")); + it("hd6 (halfling luck)", () => expectPMFMatches(pmfToObj(parse("hd6")), "hd6")); +}); + +describe("refactor characterization: binaryOp non-scalar (item 6)", () => { + it("d6 + d4", () => expectPMFMatches(pmfToObj(parse("d6 + d4")), "d6_plus_d4")); + it("d8 > d6 (max)", () => expectPMFMatches(pmfToObj(parse("d8 > d6")), "d8_max_d6")); +}); + +describe("refactor characterization: count queries (item 9)", () => { + const buildQuery = () => + new DiceQuery([ + parse("(d20 + 5 ac 15) * (2d6 + 3)"), + parse("(d20 + 3 ac 15) * (1d8 + 2)"), + parse("(d20 + 7 ac 15) * (1d10 + 4)"), + ]); + const arr = ["hit", "crit"] as any; + + it("probExactlyK array path matches baseline", () => { + const q = buildQuery(); + for (let k = 0; k <= 3; k++) { + expectClose(q.probExactlyK(arr, k), baseline.countquery_exactly[k], `exactly ${k}`); + } + }); + it("probAtLeastK array path matches baseline", () => { + const q = buildQuery(); + for (let k = 0; k <= 3; k++) { + expectClose(q.probAtLeastK(arr, k), baseline.countquery_atLeast[k], `atLeast ${k}`); + } + }); + it("probAtMostK array path matches baseline", () => { + const q = buildQuery(); + for (let k = 0; k <= 3; k++) { + expectClose(q.probAtMostK(arr, k), baseline.countquery_atMost[k], `atMost ${k}`); + } + }); + it("single-label path unchanged", () => { + const q = buildQuery(); + for (let k = 0; k <= 3; k++) { + expectClose( + q.probExactlyK("hit" as any, k), + baseline.countquery_exactly_single[k], + `exactly-single ${k}` + ); + } + }); + + it("single-attack (n=1) array path collapses to the per-event marginal", () => { + // n=1 is the most common real call and collapses the binomial DP. For a + // single attack, P(exactly 1) must equal the per-event marginal P(>=1), and + // the array path must agree with the single-label string path. + const q = new DiceQuery([parse("(d20 + 5 ac 15) * (2d6 + 3)")]); + const arr = ["hit"] as any; + const marginal = q.probAtLeastOne("hit" as any); + expect(marginal).toBeGreaterThan(0); // sanity: the label exists + expect(q.probExactlyK(arr, 1)).toBeCloseTo(marginal, 10); + expect(q.probExactlyK(arr, 0)).toBeCloseTo(1 - marginal, 10); + expect(q.probExactlyK(arr, 2)).toBe(0); // k > n + expect(q.probExactlyK(arr, 1)).toBeCloseTo(q.probExactlyK("hit" as any, 1), 12); + expect(q.probAtLeastK(arr, 1)).toBeCloseTo(marginal, 10); + expect(q.probAtMostK(arr, 0)).toBeCloseTo(1 - marginal, 10); + }); + + it("array count distribution is internally consistent", () => { + const q = buildQuery(); + let sum = 0; + for (let k = 0; k <= 3; k++) sum += q.probExactlyK(arr, k); + expect(sum).toBeCloseTo(1, 10); + // atLeastK and atMostK are complementary slices of the same distribution + for (let k = 0; k <= 3; k++) { + const atMostKMinus1 = k === 0 ? 0 : q.probAtMostK(arr, k - 1); + expect(q.probAtLeastK(arr, k)).toBeCloseTo(1 - atMostKMinus1, 10); + } + }); +}); + +describe("combinedWithAttribution slow path (builder PMFs without attribution)", () => { + it("runs the withAttribution()+convolve fallback and stays consistent with combined", () => { + // Builder-generated PMFs carry only `count` (no `attr`), so this takes the + // slow path rather than the fast hasAttribution() identity reuse. + const singles = [d6.plus(3).toPMF(), d8.toPMF()]; + expect(singles.every((p) => !p.hasAttribution())).toBe(true); + + const q = new DiceQuery(singles); + const attributed = q.combinedWithAttribution(); + + expect(attributed.mass()).toBeCloseTo(1, 9); + expect(attributed.support()).toEqual(q.combined.support()); + // Per-bin probabilities match the plain combined distribution. + for (const v of q.combined.support()) { + expectClose( + attributed.map.get(v)!.p, + q.combined.map.get(v)!.p, + `combinedWithAttribution bin ${v} p` + ); + } + }); +}); + +describe("compact() (item 1)", () => { + it("drops sub-eps count/attr entries in the RESULT (unchanged)", () => { + const m = new Map(); + m.set(5, { p: 0.5, count: { hit: 0.5, tiny: 1e-20 }, attr: { hit: 2.5, tiny: 1e-20 } }); + m.set(7, { p: 0.5, count: { hit: 0.5 } }); + m.set(9, { p: 1e-20, count: { hit: 1e-20 } }); + const pmf = new PMF(m, EPS); + expectPMFMatches(pmfToObj(pmf.compact()), "compact_result"); + }); + + it("does not mutate the receiver's own bins", () => { + const m = new Map(); + m.set(5, { p: 0.5, count: { hit: 0.5, tiny: 1e-20 }, attr: { hit: 2.5, tiny: 1e-20 } }); + m.set(7, { p: 0.5, count: { hit: 0.5 } }); + const pmf = new PMF(m, EPS); + pmf.compact(); + expect(pmf.map.get(5)!.count.tiny).toBe(1e-20); + expect(pmf.map.get(5)!.attr!.tiny).toBe(1e-20); + }); + + it("does not corrupt another PMF that shares the same bin objects by reference", () => { + // Bins are shared by reference across PMFs (branch()/addScaled()/scaleMass() + // fast paths can carry another PMF's bin objects). Compacting one PMF must + // not delete sub-eps entries out of a bin a different PMF still holds. + const sharedBin = { + p: 0.5, + count: { hit: 0.5, tiny: 1e-20 }, + attr: { hit: 2.5, tiny: 1e-20 }, + }; + const a = new PMF( + new Map([ + [5, sharedBin], + [7, { p: 0.5, count: { hit: 0.5 } }], + ]), + EPS + ); + const b = new PMF(new Map([[5, sharedBin]]), EPS); + + b.compact(); + + expect(a.map.get(5)!.count.tiny).toBe(1e-20); + expect(a.map.get(5)!.attr!.tiny).toBe(1e-20); + }); + + it("branch(p=0) fast path returns a PMF whose compact() does not corrupt the failure branch", () => { + const failure = parse("2d6+1"); + const before = JSON.stringify(failure.toJSON()); + const branched = PMF.branch(parse("1d8"), failure, 0); + branched.compact(1e-9); // aggressive eps to force count pruning + expect(JSON.stringify(failure.toJSON())).toBe(before); + }); +}); diff --git a/tests/statistical-perf.test.ts b/tests/statistical-perf.test.ts new file mode 100644 index 0000000..4593a5a --- /dev/null +++ b/tests/statistical-perf.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { DiceQuery, parse, PMF } from "../src/index"; + +/** + * These tests lock in the statistical performance optimizations in DiceQuery: + * - mean()/variance() use moment additivity (no convolution required) + * - `combined` is built lazily, so stats-only queries never convolve + * - combinedWithAttribution() reuses `combined` when singles already carry attr + */ +describe("Statistical performance optimizations", () => { + const exprs = [ + "(d20 + 6 AC 15) * (2d6 + 4)", + "(d20 +11 AC 10) * (4d8 + 5) crit (8d8 + 5)", + "(d20 + 6 DC 15) * 8d6 save half", + ]; + + describe("mean()/variance() are additive and match the convolution", () => { + for (const expr of exprs) { + for (const n of [1, 2, 4]) { + it(`${expr} ×${n}`, () => { + const singles = parse(expr).replicate(n); + const q = new DiceQuery(singles); + + // Reference values derived from the fully-convolved distribution. + const combined = q.combined; + let refMean = 0; + for (const [d, bin] of combined) refMean += d * bin.p; + let refVar = 0; + for (const [d, bin] of combined) + refVar += (d - refMean) * (d - refMean) * bin.p; + + expect(q.mean()).toBeCloseTo(refMean, 9); + expect(q.variance()).toBeCloseTo(refVar, 6); + expect(q.stdev()).toBeCloseTo(Math.sqrt(refVar), 6); + }); + } + } + }); + + it("mean() does not build the combined distribution (stays lazy)", () => { + const q = new DiceQuery(parse("(d20 +60 AC 25) * (672) crit (24d6 + 600)").replicate(4)); + q.mean(); + q.variance(); + q.stdev(); + // `combined` is computed lazily; stats-only access must not materialize it. + expect((q as unknown as { _combined?: PMF })._combined).toBeUndefined(); + + // Accessing combined materializes and caches it. + void q.combined; + expect((q as unknown as { _combined?: PMF })._combined).toBeDefined(); + }); + + it("combinedWithAttribution equals combined when singles carry attribution", () => { + const singles = parse("(d20 +11 AC 10) * (4d8 + 5) crit (8d8 + 5)").replicate(3); + const q = new DiceQuery(singles); + const attributed = q.combinedWithAttribution(); + + // Parser PMFs already carry attr, so this is the fast path: same object. + expect(attributed).toBe(q.combined); + }); +}); diff --git a/yarn.lock b/yarn.lock index 48f5eaf..2e10cfd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -47,15 +47,6 @@ __metadata: languageName: node linkType: hard -"@cspotcode/source-map-support@npm:^0.8.0": - version: 0.8.1 - resolution: "@cspotcode/source-map-support@npm:0.8.1" - dependencies: - "@jridgewell/trace-mapping": "npm:0.3.9" - checksum: 10c0/05c5368c13b662ee4c122c7bfbe5dc0b613416672a829f3e78bc49a357a197e0218d6e74e7c66cfcd04e15a179acab080bd3c69658c9fbefd0e1ccd950a07fc6 - languageName: node - linkType: hard - "@emnapi/core@npm:1.10.0": version: 1.10.0 resolution: "@emnapi/core@npm:1.10.0" @@ -470,80 +461,56 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.12.2": +"@eslint-community/regexpp@npm:^4.12.2": version: 4.12.2 resolution: "@eslint-community/regexpp@npm:4.12.2" checksum: 10c0/fddcbc66851b308478d04e302a4d771d6917a0b3740dc351513c0da9ca2eab8a1adf99f5e0aa7ab8b13fa0df005c81adeee7e63a92f3effd7d367a163b721c2d languageName: node linkType: hard -"@eslint/config-array@npm:^0.21.2": - version: 0.21.2 - resolution: "@eslint/config-array@npm:0.21.2" +"@eslint/config-array@npm:^0.23.5": + version: 0.23.5 + resolution: "@eslint/config-array@npm:0.23.5" dependencies: - "@eslint/object-schema": "npm:^2.1.7" + "@eslint/object-schema": "npm:^3.0.5" debug: "npm:^4.3.1" - minimatch: "npm:^3.1.5" - checksum: 10c0/89dfe815d18456177c0a1f238daf4593107fd20298b3598e0103054360d3b8d09d967defd8318f031185d68df1f95cfa68becf1390a9c5c6887665f1475142e3 + minimatch: "npm:^10.2.4" + checksum: 10c0/b24833c4c76e78ee075d306cd3f095db46b2db0f90cc13a6ee6e4275f9889731c05bf5403ab5fefb79c756e07ac9184ed0e04570341382f9eccbccc80e6d1a0c languageName: node linkType: hard -"@eslint/config-helpers@npm:^0.4.2": - version: 0.4.2 - resolution: "@eslint/config-helpers@npm:0.4.2" +"@eslint/config-helpers@npm:^0.6.0": + version: 0.6.0 + resolution: "@eslint/config-helpers@npm:0.6.0" dependencies: - "@eslint/core": "npm:^0.17.0" - checksum: 10c0/92efd7a527b2d17eb1a148409d71d80f9ac160b565ac73ee092252e8bf08ecd08670699f46b306b94f13d22e88ac88a612120e7847570dd7cdc72f234d50dcb4 + "@eslint/core": "npm:^1.2.1" + checksum: 10c0/f9af20e8b60b0ba27edb74b8eb40c0c5d51a9bf9baf9e053bb57833a87cb0a1c49b4dfaad88fc24d49c907ad1324c8a0b668684fa9c321351dac4bc9155ec10a languageName: node linkType: hard -"@eslint/core@npm:^0.17.0": - version: 0.17.0 - resolution: "@eslint/core@npm:0.17.0" +"@eslint/core@npm:^1.2.1": + version: 1.2.1 + resolution: "@eslint/core@npm:1.2.1" dependencies: "@types/json-schema": "npm:^7.0.15" - checksum: 10c0/9a580f2246633bc752298e7440dd942ec421860d1946d0801f0423830e67887e4aeba10ab9a23d281727a978eb93d053d1922a587d502942a713607f40ed704e - languageName: node - linkType: hard - -"@eslint/eslintrc@npm:^3.3.5": - version: 3.3.5 - resolution: "@eslint/eslintrc@npm:3.3.5" - dependencies: - ajv: "npm:^6.14.0" - debug: "npm:^4.3.2" - espree: "npm:^10.0.1" - globals: "npm:^14.0.0" - ignore: "npm:^5.2.0" - import-fresh: "npm:^3.2.1" - js-yaml: "npm:^4.1.1" - minimatch: "npm:^3.1.5" - strip-json-comments: "npm:^3.1.1" - checksum: 10c0/9fb9f1ca65e46d6173966e3aaa5bd353e3a65d7f1f582bebf77f578fab7d7960a399fac1ecfb1e7d52bd61f5cefd6531087ca52a3a3c388f2e1b4f1ebd3da8b7 - languageName: node - linkType: hard - -"@eslint/js@npm:9.39.4": - version: 9.39.4 - resolution: "@eslint/js@npm:9.39.4" - checksum: 10c0/5aa7dea2cbc5decf7f5e3b0c6f86a084ccee0f792d288ca8e839f8bc1b64e03e227068968e49b26096e6f71fd857ab6e42691d1b993826b9a3883f1bdd7a0e46 + checksum: 10c0/10979b40588ecfef771fcb5013a542a35fb30692cc95a65f3481b0b36fbd89f5679efeb30d57f4eed35203d859aabace2a620177d6c536f71b299a1af2f3398f languageName: node linkType: hard -"@eslint/object-schema@npm:^2.1.7": - version: 2.1.7 - resolution: "@eslint/object-schema@npm:2.1.7" - checksum: 10c0/936b6e499853d1335803f556d526c86f5fe2259ed241bc665000e1d6353828edd913feed43120d150adb75570cae162cf000b5b0dfc9596726761c36b82f4e87 +"@eslint/object-schema@npm:^3.0.5": + version: 3.0.5 + resolution: "@eslint/object-schema@npm:3.0.5" + checksum: 10c0/1db337431f520b99e9edda64ef5fafd7ec6a029843eeb608753025125b6649d861d843cffafafd3c4e37926d7d5f9ec0c6a8e3665c13c3da2144e8132892e92e languageName: node linkType: hard -"@eslint/plugin-kit@npm:^0.4.1": - version: 0.4.1 - resolution: "@eslint/plugin-kit@npm:0.4.1" +"@eslint/plugin-kit@npm:^0.7.2": + version: 0.7.2 + resolution: "@eslint/plugin-kit@npm:0.7.2" dependencies: - "@eslint/core": "npm:^0.17.0" + "@eslint/core": "npm:^1.2.1" levn: "npm:^0.4.1" - checksum: 10c0/51600f78b798f172a9915dffb295e2ffb44840d583427bc732baf12ecb963eb841b253300e657da91d890f4b323d10a1bd12934bf293e3018d8bb66fdce5217b + checksum: 10c0/aafba08077bcd6d7dde6c2e21db18086046a88f914f29971a84cac9ad2d48952ded1b293e665e523805297eff756522dafa16f0062195e2c7143dcd1d47d11ed languageName: node linkType: hard @@ -611,30 +578,20 @@ __metadata: languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": +"@jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" checksum: 10c0/d502e6fb516b35032331406d4e962c21fe77cdf1cbdb49c6142bcbd9e30507094b18972778a6e27cbad756209cfe34b1a27729e6fa08a2eb92b33943f680cf1e languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": +"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": version: 1.5.5 resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:0.3.9": - version: 0.3.9 - resolution: "@jridgewell/trace-mapping@npm:0.3.9" - dependencies: - "@jridgewell/resolve-uri": "npm:^3.0.3" - "@jridgewell/sourcemap-codec": "npm:^1.4.10" - checksum: 10c0/fa425b606d7c7ee5bfa6a31a7b050dd5814b4082f318e0e4190f991902181b4330f43f4805db1dd4f2433fd0ed9cc7a7b9c2683f1deeab1df1b0a98b1e24055b - languageName: node - linkType: hard - "@jridgewell/trace-mapping@npm:^0.3.24": version: 0.3.30 resolution: "@jridgewell/trace-mapping@npm:0.3.30" @@ -973,34 +930,6 @@ __metadata: languageName: node linkType: hard -"@tsconfig/node10@npm:^1.0.7": - version: 1.0.11 - resolution: "@tsconfig/node10@npm:1.0.11" - checksum: 10c0/28a0710e5d039e0de484bdf85fee883bfd3f6a8980601f4d44066b0a6bcd821d31c4e231d1117731c4e24268bd4cf2a788a6787c12fc7f8d11014c07d582783c - languageName: node - linkType: hard - -"@tsconfig/node12@npm:^1.0.7": - version: 1.0.11 - resolution: "@tsconfig/node12@npm:1.0.11" - checksum: 10c0/dddca2b553e2bee1308a056705103fc8304e42bb2d2cbd797b84403a223b25c78f2c683ec3e24a095e82cd435387c877239bffcb15a590ba817cd3f6b9a99fd9 - languageName: node - linkType: hard - -"@tsconfig/node14@npm:^1.0.0": - version: 1.0.3 - resolution: "@tsconfig/node14@npm:1.0.3" - checksum: 10c0/67c1316d065fdaa32525bc9449ff82c197c4c19092b9663b23213c8cbbf8d88b6ed6a17898e0cbc2711950fbfaf40388938c1c748a2ee89f7234fc9e7fe2bf44 - languageName: node - linkType: hard - -"@tsconfig/node16@npm:^1.0.2": - version: 1.0.4 - resolution: "@tsconfig/node16@npm:1.0.4" - checksum: 10c0/05f8f2734e266fb1839eb1d57290df1664fe2aa3b0fdd685a9035806daa635f7519bf6d5d9b33f6e69dd545b8c46bd6e2b5c79acb2b1f146e885f7f11a42a5bb - languageName: node - linkType: hard - "@tybys/wasm-util@npm:^0.10.2": version: 0.10.2 resolution: "@tybys/wasm-util@npm:0.10.2" @@ -1026,6 +955,13 @@ __metadata: languageName: node linkType: hard +"@types/esrecurse@npm:^4.3.1": + version: 4.3.1 + resolution: "@types/esrecurse@npm:4.3.1" + checksum: 10c0/90dad74d5da3ad27606d8e8e757322f33171cfeaa15ad558b615cf71bb2a516492d18f55f4816384685a3eb2412142e732bbae9a4a7cd2cf3deb7572aa4ebe03 + languageName: node + linkType: hard + "@types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" @@ -1033,6 +969,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:^1.0.8": + version: 1.0.9 + resolution: "@types/estree@npm:1.0.9" + checksum: 10c0/3ad3286ca2988cd550dafb8f2ad599c8474868e954fa601a36655bdfefd8039f7c714b8c1c7f2ae219ffbd58bd4660e66fa7479a0120fc02d4777057d4865387 + languageName: node + linkType: hard + "@types/json-schema@npm:^7.0.15": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -1040,114 +983,114 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^24.13.2": - version: 24.13.2 - resolution: "@types/node@npm:24.13.2" +"@types/node@npm:^26.0.1": + version: 26.0.1 + resolution: "@types/node@npm:26.0.1" dependencies: - undici-types: "npm:~7.18.0" - checksum: 10c0/d7d48a88a4feb0a6aac3cbfaf9ef3b12752b4b09447f88dd0b4c77c03b281e3d4330fe6982a99aedcd63fc16c7540a0c248b91eb2abb0b3edd884d7fe684e9ea + undici-types: "npm:~8.3.0" + checksum: 10c0/31d204333c70124da6bcac7d1f27d8980149fe3f95a8419bfcd19c3e5823705c0e370d71ac34399352e1263b2d5fc41c87af964ec81e5a05a32224d65d8d224e languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^8.61.0": - version: 8.61.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.61.0" +"@typescript-eslint/eslint-plugin@npm:^8.62.0": + version: 8.62.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.62.0" dependencies: "@eslint-community/regexpp": "npm:^4.12.2" - "@typescript-eslint/scope-manager": "npm:8.61.0" - "@typescript-eslint/type-utils": "npm:8.61.0" - "@typescript-eslint/utils": "npm:8.61.0" - "@typescript-eslint/visitor-keys": "npm:8.61.0" + "@typescript-eslint/scope-manager": "npm:8.62.0" + "@typescript-eslint/type-utils": "npm:8.62.0" + "@typescript-eslint/utils": "npm:8.62.0" + "@typescript-eslint/visitor-keys": "npm:8.62.0" ignore: "npm:^7.0.5" natural-compare: "npm:^1.4.0" ts-api-utils: "npm:^2.5.0" peerDependencies: - "@typescript-eslint/parser": ^8.61.0 + "@typescript-eslint/parser": ^8.62.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10c0/1141253e18424a9a21d253dcf28e166894b6b914f2138b3e016144e451385f8e23f0f02028c7bd2d21c81953c52e478657aa9e5888cd0bdffdb8d68aab736878 + checksum: 10c0/2f257bb53a3b9b8b6f56f6b85c68ce193c7ba4cddf7d2d1995b0aec0591230859f808b3a1a85f0b7ed581f938a88efeacccc730aec307e80e1c14fc669222f9a languageName: node linkType: hard -"@typescript-eslint/parser@npm:^8.61.0": - version: 8.61.0 - resolution: "@typescript-eslint/parser@npm:8.61.0" +"@typescript-eslint/parser@npm:^8.62.0": + version: 8.62.0 + resolution: "@typescript-eslint/parser@npm:8.62.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.61.0" - "@typescript-eslint/types": "npm:8.61.0" - "@typescript-eslint/typescript-estree": "npm:8.61.0" - "@typescript-eslint/visitor-keys": "npm:8.61.0" + "@typescript-eslint/scope-manager": "npm:8.62.0" + "@typescript-eslint/types": "npm:8.62.0" + "@typescript-eslint/typescript-estree": "npm:8.62.0" + "@typescript-eslint/visitor-keys": "npm:8.62.0" debug: "npm:^4.4.3" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10c0/39b122ab20a3b5fbd4e66874f60917b37c49f32eb987531797561d2f96b443e81546f1c9d03d44d37dede89098276e5d8d0f05c1e5f9e1b998f8cf6c24e8e5e7 + checksum: 10c0/eab526557fb679c862a0462e841581e3ca24a88b8f19764630e7d10e01ba01143906100a051c8bede36e3c6eb6bef21c14ac7dccf9d41e04c84448c058450a94 languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.61.0": - version: 8.61.0 - resolution: "@typescript-eslint/project-service@npm:8.61.0" +"@typescript-eslint/project-service@npm:8.62.0": + version: 8.62.0 + resolution: "@typescript-eslint/project-service@npm:8.62.0" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.61.0" - "@typescript-eslint/types": "npm:^8.61.0" + "@typescript-eslint/tsconfig-utils": "npm:^8.62.0" + "@typescript-eslint/types": "npm:^8.62.0" debug: "npm:^4.4.3" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10c0/8ff86b93bfcf103a42e8e996e11c46ded83da07d3a0bc8bd9ec4d536116d7f6253a404786510ab13847e69d6e185b17d15d7140075c26966e9b4f85c03296f21 + checksum: 10c0/4369e9ec0c8b2ce6e6cf90142ad781ef99b57350beb4ae48751871e8894e95a8f929de2f56d73849ec0166d2cdb345e3e7a42d30ea8463d3f1b65607648ac582 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.61.0": - version: 8.61.0 - resolution: "@typescript-eslint/scope-manager@npm:8.61.0" +"@typescript-eslint/scope-manager@npm:8.62.0": + version: 8.62.0 + resolution: "@typescript-eslint/scope-manager@npm:8.62.0" dependencies: - "@typescript-eslint/types": "npm:8.61.0" - "@typescript-eslint/visitor-keys": "npm:8.61.0" - checksum: 10c0/76cdf1c181ebbc706ddc8b2366e8ebfda529c13d82ff10c0797c96c0b38dd82f6471b24995f58ac267194a753b23d77452d925dd615b1e651922ddbe6e451c6b + "@typescript-eslint/types": "npm:8.62.0" + "@typescript-eslint/visitor-keys": "npm:8.62.0" + checksum: 10c0/1e7192b6bf18955ee76861321a92e08815f1bc3feab23861b6330dde8343dfb346a47c7c8bf3d94b0897a98184adcf283568b5857a0e5d509ce0c21c6cf4cc44 languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.61.0, @typescript-eslint/tsconfig-utils@npm:^8.61.0": - version: 8.61.0 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.61.0" +"@typescript-eslint/tsconfig-utils@npm:8.62.0, @typescript-eslint/tsconfig-utils@npm:^8.62.0": + version: 8.62.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.62.0" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10c0/b498675f14ef90a5730de7c58388eb2522085a56c3fcad42ad9f89320b96221eafb5b4f9650375f29092025153d03533e3f23ea8f45ce3bc95a57593059edef3 + checksum: 10c0/9423908009e95b8bba8ac2ad1e4bf4bc9dd7052fa44be613659f81aad363787bf7fa1ea017eb9dcb059fed41866bc2d8e50f1cf41c7dfad25a4431c333fbf2fa languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.61.0": - version: 8.61.0 - resolution: "@typescript-eslint/type-utils@npm:8.61.0" +"@typescript-eslint/type-utils@npm:8.62.0": + version: 8.62.0 + resolution: "@typescript-eslint/type-utils@npm:8.62.0" dependencies: - "@typescript-eslint/types": "npm:8.61.0" - "@typescript-eslint/typescript-estree": "npm:8.61.0" - "@typescript-eslint/utils": "npm:8.61.0" + "@typescript-eslint/types": "npm:8.62.0" + "@typescript-eslint/typescript-estree": "npm:8.62.0" + "@typescript-eslint/utils": "npm:8.62.0" debug: "npm:^4.4.3" ts-api-utils: "npm:^2.5.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10c0/6347f451301ca7089500fe6eb3b98e5efd769e56ffda07eb735130fd209b9053c02e952b6fda7a15acf7851fb63f11fc50166ba8bd90513480732c599644b36b + checksum: 10c0/caf6a0072ff48e9351564dbc856c095fd5233bf2176daf9e8361a534be9ded6835c984ff8867365ca3cba158189074b14cc199b19c1a2e20d09682170c3784db languageName: node linkType: hard -"@typescript-eslint/types@npm:8.61.0, @typescript-eslint/types@npm:^8.61.0": - version: 8.61.0 - resolution: "@typescript-eslint/types@npm:8.61.0" - checksum: 10c0/c19407d66fb5ad26e2670cd272bee91d150087d917752422257759e17920220af27cd54593205e9726367a440a237bf8d27ed805cae0b282a79172161f007207 +"@typescript-eslint/types@npm:8.62.0, @typescript-eslint/types@npm:^8.62.0": + version: 8.62.0 + resolution: "@typescript-eslint/types@npm:8.62.0" + checksum: 10c0/28d7a6851cb79301ef1ee004fb8d75811e52eb2a7258d7f8ad2f234886ab2faaf1888cbf5a71cb53afd4d1024c51f71ea359f3103ea70d6f7ccd626ffbfd49c1 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.61.0": - version: 8.61.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.61.0" +"@typescript-eslint/typescript-estree@npm:8.62.0": + version: 8.62.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.62.0" dependencies: - "@typescript-eslint/project-service": "npm:8.61.0" - "@typescript-eslint/tsconfig-utils": "npm:8.61.0" - "@typescript-eslint/types": "npm:8.61.0" - "@typescript-eslint/visitor-keys": "npm:8.61.0" + "@typescript-eslint/project-service": "npm:8.62.0" + "@typescript-eslint/tsconfig-utils": "npm:8.62.0" + "@typescript-eslint/types": "npm:8.62.0" + "@typescript-eslint/visitor-keys": "npm:8.62.0" debug: "npm:^4.4.3" minimatch: "npm:^10.2.2" semver: "npm:^7.7.3" @@ -1155,41 +1098,41 @@ __metadata: ts-api-utils: "npm:^2.5.0" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10c0/460819feeca826bfd895f821a5008c3eaa79b9495259641976fdc6ec319a7e9587bc28603437ea3d9a10c3b28037f1dea883cbe8d2858616dd33847e8db2179e + checksum: 10c0/c1b76203f37870e66487379c75b1d1a9af0a9c88e8e58a3d2fc106427ce42dce9bd7358a3d2cb7d344004cbb693dba899abf19391d853bbb9f345aa8683635c4 languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.61.0": - version: 8.61.0 - resolution: "@typescript-eslint/utils@npm:8.61.0" +"@typescript-eslint/utils@npm:8.62.0": + version: 8.62.0 + resolution: "@typescript-eslint/utils@npm:8.62.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.9.1" - "@typescript-eslint/scope-manager": "npm:8.61.0" - "@typescript-eslint/types": "npm:8.61.0" - "@typescript-eslint/typescript-estree": "npm:8.61.0" + "@typescript-eslint/scope-manager": "npm:8.62.0" + "@typescript-eslint/types": "npm:8.62.0" + "@typescript-eslint/typescript-estree": "npm:8.62.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10c0/f7b2241fc4defd40107243642e26697193707be12af1552c60bc414e71df1285c9cdff429f913b30ed08ae87a7e6e13388eaf05c1be5fb8310f6a63a6c4f7f73 + checksum: 10c0/5c5d1dd2c37d43ec913e4d144d071058701bf3548733671a05025fae306f7afdc4fdc3d9867d0eae32537eb68ecba046ab8aa6111a5d97497ec78fee98a25809 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.61.0": - version: 8.61.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.61.0" +"@typescript-eslint/visitor-keys@npm:8.62.0": + version: 8.62.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.62.0" dependencies: - "@typescript-eslint/types": "npm:8.61.0" + "@typescript-eslint/types": "npm:8.62.0" eslint-visitor-keys: "npm:^5.0.0" - checksum: 10c0/5b656aed426a92dfc9a481f0bf535ceb47321303f476f32ba979f73423c739b51d7a5ad76c81d7be9df0a9beb361f4a11ff530dd86d59f41b89bb5f09af7be9f + checksum: 10c0/29102828fab6b060e607effee0d7666bc82aef269241c7c5c44ffb3386b3cb69ea585bf7c5d88d428c489f46d5888cbe90677e15cbad8cb27acfa1fc1661bd5b languageName: node linkType: hard -"@vitest/coverage-v8@npm:^4.1.8": - version: 4.1.8 - resolution: "@vitest/coverage-v8@npm:4.1.8" +"@vitest/coverage-v8@npm:^4.1.9": + version: 4.1.9 + resolution: "@vitest/coverage-v8@npm:4.1.9" dependencies: "@bcoe/v8-coverage": "npm:^1.0.2" - "@vitest/utils": "npm:4.1.8" + "@vitest/utils": "npm:4.1.9" ast-v8-to-istanbul: "npm:^1.0.0" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-report: "npm:^3.0.1" @@ -1199,34 +1142,34 @@ __metadata: std-env: "npm:^4.0.0-rc.1" tinyrainbow: "npm:^3.1.0" peerDependencies: - "@vitest/browser": 4.1.8 - vitest: 4.1.8 + "@vitest/browser": 4.1.9 + vitest: 4.1.9 peerDependenciesMeta: "@vitest/browser": optional: true - checksum: 10c0/e3419115fa413e19bda5edd72c8394b255d418af75278b2cd74341257399f8e5921f78b966a547ba8d67ac8268ccdd5af019230084cc1b9a3fc8fae8283ae76b + checksum: 10c0/02bc408d5d8d5188bb569ae1df1f56903e9e19a6a19ddeff07140e1b344b82662f0669c7ee87b6f4f82c9d1d63a0296fe40c023f923a500a707463c8c05ec133 languageName: node linkType: hard -"@vitest/expect@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/expect@npm:4.1.8" +"@vitest/expect@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/expect@npm:4.1.9" dependencies: "@standard-schema/spec": "npm:^1.1.0" "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.1.8" - "@vitest/utils": "npm:4.1.8" + "@vitest/spy": "npm:4.1.9" + "@vitest/utils": "npm:4.1.9" chai: "npm:^6.2.2" tinyrainbow: "npm:^3.1.0" - checksum: 10c0/f7bf6c720d2427c3bd0b35472ebd84d963be7d09ecf52a0fb05e8c4d5d0c9ee164a8c28eee6360947be1b245b47faefab54560cb98e5cb678c1c1074260b9149 + checksum: 10c0/243bacaed2cba5e0ea4ec7465662fcec465a358a0e06381e337fac49426aa67a73b104fbb9d65d8bccadfba8f70e27f57ffb897aacfa140f579a556367357875 languageName: node linkType: hard -"@vitest/mocker@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/mocker@npm:4.1.8" +"@vitest/mocker@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/mocker@npm:4.1.9" dependencies: - "@vitest/spy": "npm:4.1.8" + "@vitest/spy": "npm:4.1.9" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.21" peerDependencies: @@ -1237,56 +1180,56 @@ __metadata: optional: true vite: optional: true - checksum: 10c0/f8cb2b8b55dc2cba0b2399aeee528b0187042f22cbc2d50a4fd6141f5aa246ebc41700f45dd1d73eca44ddfb57dcde48b2eb317bfbb1198f5ab2cc4fd04b2ea0 + checksum: 10c0/707353b7435bbfd441cc754e4ee7bc5921b70d07b051c6e414b6bbe4ca369154702b0ddeb603389469fe87ca1983e002eb2d55044582661f54a1945dd27e5c82 languageName: node linkType: hard -"@vitest/pretty-format@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/pretty-format@npm:4.1.8" +"@vitest/pretty-format@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/pretty-format@npm:4.1.9" dependencies: tinyrainbow: "npm:^3.1.0" - checksum: 10c0/553c456692a4b9ae13cd116c234c74b4495e0f1a0d5c51ffc3fab8ea085e3550769967e29db79bdac0cf127b1bf88b7f70bfba3dcc72be6bddf834433e30cc91 + checksum: 10c0/5b96295f25ab885616230ad1355fc82f490bebb39cc707688d7c8969c08270d7e076ed8a10af4e762ed57145193c6061a1f549f136f0ded344f8db0c2b3fb3de languageName: node linkType: hard -"@vitest/runner@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/runner@npm:4.1.8" +"@vitest/runner@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/runner@npm:4.1.9" dependencies: - "@vitest/utils": "npm:4.1.8" + "@vitest/utils": "npm:4.1.9" pathe: "npm:^2.0.3" - checksum: 10c0/706808a4b7b95ea9a9268fc152dd39e15a9a754f37c7990aea167486a9094caa913dae454771ae02c18dccfabd667f8cc38eed33a1307a79d32a89878b5bcce1 + checksum: 10c0/d206b4891a64b1f55c346f832b0a7b489108094d8ae34438d3b53e78be7b45b139fa95ffa027c98c357bd532268ee573168de1943235b7eed32a9236ed5978bb languageName: node linkType: hard -"@vitest/snapshot@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/snapshot@npm:4.1.8" +"@vitest/snapshot@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/snapshot@npm:4.1.9" dependencies: - "@vitest/pretty-format": "npm:4.1.8" - "@vitest/utils": "npm:4.1.8" + "@vitest/pretty-format": "npm:4.1.9" + "@vitest/utils": "npm:4.1.9" magic-string: "npm:^0.30.21" pathe: "npm:^2.0.3" - checksum: 10c0/ba4c32112491d42d24986f921c50ede5edbdb4b7eafa16c72cf8d2c9ecc44121fdb3d9365236747a9841f0d6776affc6457470fcbb082df9dbc28c24792a0c6d + checksum: 10c0/c3099df12ad1f9c1e180441856c9eb82f1990f87ff16aafedd6fa19978eaff20bc59220b692a99fcc822daef86eab256ba3dadb49544b7bd625b57c49cd9d995 languageName: node linkType: hard -"@vitest/spy@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/spy@npm:4.1.8" - checksum: 10c0/3c10c0325a09d16bc0e77c0be96c47c15416186e33332880c0d1dd0a51d51a866091067b81f2a2ef6fb422a7760e6cf15c04d91a0eca4d59f62e8c8401fa53fc +"@vitest/spy@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/spy@npm:4.1.9" + checksum: 10c0/e51f328f55b76e8ba66e5e18f183484a8dc0a092685b101112d3e9fb8e989ddca162c98ddf00254476502c25bc05c4ec1e277fd6ad8bfc702464c08f6b5dd115 languageName: node linkType: hard -"@vitest/utils@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/utils@npm:4.1.8" +"@vitest/utils@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/utils@npm:4.1.9" dependencies: - "@vitest/pretty-format": "npm:4.1.8" + "@vitest/pretty-format": "npm:4.1.9" convert-source-map: "npm:^2.0.0" tinyrainbow: "npm:^3.1.0" - checksum: 10c0/acda9d3d640c1ebc81afb358ac30589d7d7d583af81e2d09419f0af9cbe41f3ce0b90527326943bf0da51614be5fc31afcd32259f6beb32b3417999d6ef380f3 + checksum: 10c0/d55506c077fd72c091eb66f02926f0abf72801c87a085f565698289562f47befa114ae2c680ab8736dfe46abab0cfd6b8031f2ac519bafeb37578aa6e5ad03c5 languageName: node linkType: hard @@ -1294,17 +1237,15 @@ __metadata: version: 0.0.0-use.local resolution: "@yipe/dice@workspace:." dependencies: - "@types/node": "npm:^24.13.2" - "@typescript-eslint/eslint-plugin": "npm:^8.61.0" - "@typescript-eslint/parser": "npm:^8.61.0" - "@vitest/coverage-v8": "npm:^4.1.8" - eslint: "npm:^9.0.0" - ts-node: "npm:^10.9.2" - tsconfig-paths: "npm:^4.2.0" + "@types/node": "npm:^26.0.1" + "@typescript-eslint/eslint-plugin": "npm:^8.62.0" + "@typescript-eslint/parser": "npm:^8.62.0" + "@vitest/coverage-v8": "npm:^4.1.9" + eslint: "npm:^10.6.0" tsup: "npm:^8.5.1" tsx: "npm:^4.22.4" typescript: "npm:^6.0.3" - vitest: "npm:^4.1.8" + vitest: "npm:^4.1.9" languageName: unknown linkType: soft @@ -1324,16 +1265,7 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.1.1": - version: 8.3.4 - resolution: "acorn-walk@npm:8.3.4" - dependencies: - acorn: "npm:^8.11.0" - checksum: 10c0/76537ac5fb2c37a64560feaf3342023dadc086c46da57da363e64c6148dc21b57d49ace26f949e225063acb6fb441eabffd89f7a3066de5ad37ab3e328927c62 - languageName: node - linkType: hard - -"acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.4.1": +"acorn@npm:^8.15.0": version: 8.15.0 resolution: "acorn@npm:8.15.0" bin: @@ -1342,6 +1274,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.16.0": + version: 8.17.0 + resolution: "acorn@npm:8.17.0" + bin: + acorn: bin/acorn + checksum: 10c0/5dcefea5f8f023b6cc24cbe71fb5a8112b601d36c4fa07d14e4e6ffc2ee47383332c46b36c766d9437725aa6660156eae50efa0c838719823b50d7c327c4ed42 + languageName: node + linkType: hard + "agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": version: 7.1.4 resolution: "agent-base@npm:7.1.4" @@ -1375,7 +1316,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": +"ansi-styles@npm:^4.0.0": version: 4.3.0 resolution: "ansi-styles@npm:4.3.0" dependencies: @@ -1398,20 +1339,6 @@ __metadata: languageName: node linkType: hard -"arg@npm:^4.1.0": - version: 4.1.3 - resolution: "arg@npm:4.1.3" - checksum: 10c0/070ff801a9d236a6caa647507bdcc7034530604844d64408149a26b9e87c2f97650055c0f049abd1efc024b334635c01f29e0b632b371ac3f26130f4cf65997a - languageName: node - linkType: hard - -"argparse@npm:^2.0.1": - version: 2.0.1 - resolution: "argparse@npm:2.0.1" - checksum: 10c0/c5640c2d89045371c7cedd6a70212a04e360fd34d6edeae32f6952c63949e3525ea77dbec0289d8213a99bbaeab5abfa860b5c12cf88a2e6cf8106e90dd27a7e - languageName: node - linkType: hard - "ast-v8-to-istanbul@npm:^1.0.0": version: 1.0.4 resolution: "ast-v8-to-istanbul@npm:1.0.4" @@ -1437,16 +1364,6 @@ __metadata: languageName: node linkType: hard -"brace-expansion@npm:^1.1.7": - version: 1.1.12 - resolution: "brace-expansion@npm:1.1.12" - dependencies: - balanced-match: "npm:^1.0.0" - concat-map: "npm:0.0.1" - checksum: 10c0/975fecac2bb7758c062c20d0b3b6288c7cc895219ee25f0a64a9de662dbac981ff0b6e89909c3897c1f84fa353113a721923afdec5f8b2350255b097f12b1f73 - languageName: node - linkType: hard - "brace-expansion@npm:^2.0.1": version: 2.0.2 resolution: "brace-expansion@npm:2.0.2" @@ -1503,13 +1420,6 @@ __metadata: languageName: node linkType: hard -"callsites@npm:^3.0.0": - version: 3.1.0 - resolution: "callsites@npm:3.1.0" - checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301 - languageName: node - linkType: hard - "chai@npm:^6.2.2": version: 6.2.2 resolution: "chai@npm:6.2.2" @@ -1517,16 +1427,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0": - version: 4.1.2 - resolution: "chalk@npm:4.1.2" - dependencies: - ansi-styles: "npm:^4.1.0" - supports-color: "npm:^7.1.0" - checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880 - languageName: node - linkType: hard - "chokidar@npm:^4.0.3": version: 4.0.3 resolution: "chokidar@npm:4.0.3" @@ -1566,13 +1466,6 @@ __metadata: languageName: node linkType: hard -"concat-map@npm:0.0.1": - version: 0.0.1 - resolution: "concat-map@npm:0.0.1" - checksum: 10c0/c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f - languageName: node - linkType: hard - "confbox@npm:^0.1.8": version: 0.1.8 resolution: "confbox@npm:0.1.8" @@ -1594,13 +1487,6 @@ __metadata: languageName: node linkType: hard -"create-require@npm:^1.1.0": - version: 1.1.1 - resolution: "create-require@npm:1.1.1" - checksum: 10c0/157cbc59b2430ae9a90034a5f3a1b398b6738bf510f713edc4d4e45e169bc514d3d99dd34d8d01ca7ae7830b5b8b537e46ae8f3c8f932371b0875c0151d7ec91 - languageName: node - linkType: hard - "cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -1650,13 +1536,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:^4.0.1": - version: 4.0.2 - resolution: "diff@npm:4.0.2" - checksum: 10c0/81b91f9d39c4eaca068eb0c1eb0e4afbdc5bb2941d197f513dd596b820b956fef43485876226d65d497bebc15666aa2aa82c679e84f65d5f2bfbf14ee46e32c1 - languageName: node - linkType: hard - "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -1893,13 +1772,15 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^8.4.0": - version: 8.4.0 - resolution: "eslint-scope@npm:8.4.0" +"eslint-scope@npm:^9.1.2": + version: 9.1.2 + resolution: "eslint-scope@npm:9.1.2" dependencies: + "@types/esrecurse": "npm:^4.3.1" + "@types/estree": "npm:^1.0.8" esrecurse: "npm:^4.3.0" estraverse: "npm:^5.2.0" - checksum: 10c0/407f6c600204d0f3705bd557f81bd0189e69cd7996f408f8971ab5779c0af733d1af2f1412066b40ee1588b085874fc37a2333986c6521669cdbdd36ca5058e0 + checksum: 10c0/9fb8bca5a73e5741efb6cec84467027b6cb6f4203ff9b43a938e272c5cd30800bde46a5c20dfd1609f840225f0b62b7673be391b20acadf8658ca9fa4729b3dd languageName: node linkType: hard @@ -1910,45 +1791,35 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^4.2.1": - version: 4.2.1 - resolution: "eslint-visitor-keys@npm:4.2.1" - checksum: 10c0/fcd43999199d6740db26c58dbe0c2594623e31ca307e616ac05153c9272f12f1364f5a0b1917a8e962268fdecc6f3622c1c2908b4fcc2e047a106fe6de69dc43 - languageName: node - linkType: hard - -"eslint-visitor-keys@npm:^5.0.0": +"eslint-visitor-keys@npm:^5.0.0, eslint-visitor-keys@npm:^5.0.1": version: 5.0.1 resolution: "eslint-visitor-keys@npm:5.0.1" checksum: 10c0/16190bdf2cbae40a1109384c94450c526a79b0b9c3cb21e544256ed85ac48a4b84db66b74a6561d20fe6ab77447f150d711c2ad5ad74df4fcc133736bce99678 languageName: node linkType: hard -"eslint@npm:^9.0.0": - version: 9.39.4 - resolution: "eslint@npm:9.39.4" +"eslint@npm:^10.6.0": + version: 10.6.0 + resolution: "eslint@npm:10.6.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.8.0" - "@eslint-community/regexpp": "npm:^4.12.1" - "@eslint/config-array": "npm:^0.21.2" - "@eslint/config-helpers": "npm:^0.4.2" - "@eslint/core": "npm:^0.17.0" - "@eslint/eslintrc": "npm:^3.3.5" - "@eslint/js": "npm:9.39.4" - "@eslint/plugin-kit": "npm:^0.4.1" + "@eslint-community/regexpp": "npm:^4.12.2" + "@eslint/config-array": "npm:^0.23.5" + "@eslint/config-helpers": "npm:^0.6.0" + "@eslint/core": "npm:^1.2.1" + "@eslint/plugin-kit": "npm:^0.7.2" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" "@humanwhocodes/retry": "npm:^0.4.2" "@types/estree": "npm:^1.0.6" ajv: "npm:^6.14.0" - chalk: "npm:^4.0.0" cross-spawn: "npm:^7.0.6" debug: "npm:^4.3.2" escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^8.4.0" - eslint-visitor-keys: "npm:^4.2.1" - espree: "npm:^10.4.0" - esquery: "npm:^1.5.0" + eslint-scope: "npm:^9.1.2" + eslint-visitor-keys: "npm:^5.0.1" + espree: "npm:^11.2.0" + esquery: "npm:^1.7.0" esutils: "npm:^2.0.2" fast-deep-equal: "npm:^3.1.3" file-entry-cache: "npm:^8.0.0" @@ -1958,8 +1829,7 @@ __metadata: imurmurhash: "npm:^0.1.4" is-glob: "npm:^4.0.0" json-stable-stringify-without-jsonify: "npm:^1.0.1" - lodash.merge: "npm:^4.6.2" - minimatch: "npm:^3.1.5" + minimatch: "npm:^10.2.4" natural-compare: "npm:^1.4.0" optionator: "npm:^0.9.3" peerDependencies: @@ -1969,22 +1839,22 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10c0/1955067c2d991f0c84f4c4abfafe31bb47fa3b717a7fd3e43fe1e511c6f859d7700cbca969f85661dc4c130f7aeced5e5444884314198a54428f5e5141db9337 + checksum: 10c0/ebe0261fc750afb7f1a0c5a14f5288e57b971e0bee9754b1620132d22ad0c23690183977b0c9514ffa7ad460768d689d3fb9792ad9267b6c6205e2e0c17d8563 languageName: node linkType: hard -"espree@npm:^10.0.1, espree@npm:^10.4.0": - version: 10.4.0 - resolution: "espree@npm:10.4.0" +"espree@npm:^11.2.0": + version: 11.2.0 + resolution: "espree@npm:11.2.0" dependencies: - acorn: "npm:^8.15.0" + acorn: "npm:^8.16.0" acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^4.2.1" - checksum: 10c0/c63fe06131c26c8157b4083313cb02a9a54720a08e21543300e55288c40e06c3fc284bdecf108d3a1372c5934a0a88644c98714f38b6ae8ed272b40d9ea08d6b + eslint-visitor-keys: "npm:^5.0.1" + checksum: 10c0/cf87e18ffd9dc113eb8d16588e7757701bc10c9934a71cce8b89c2611d51672681a918307bd6b19ac3ccd0e7ba1cbccc2f815b36b52fa7e73097b251014c3d81 languageName: node linkType: hard -"esquery@npm:^1.5.0": +"esquery@npm:^1.7.0": version: 1.7.0 resolution: "esquery@npm:1.7.0" dependencies: @@ -2182,13 +2052,6 @@ __metadata: languageName: node linkType: hard -"globals@npm:^14.0.0": - version: 14.0.0 - resolution: "globals@npm:14.0.0" - checksum: 10c0/b96ff42620c9231ad468d4c58ff42afee7777ee1c963013ff8aabe095a451d0ceeb8dcd8ef4cbd64d2538cef45f787a78ba3a9574f4a634438963e334471302d - languageName: node - linkType: hard - "graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -2260,16 +2123,6 @@ __metadata: languageName: node linkType: hard -"import-fresh@npm:^3.2.1": - version: 3.3.1 - resolution: "import-fresh@npm:3.3.1" - dependencies: - parent-module: "npm:^1.0.0" - resolve-from: "npm:^4.0.0" - checksum: 10c0/bf8cc494872fef783249709385ae883b447e3eb09db0ebd15dcead7d9afe7224dad7bd7591c6b73b0b19b3c0f9640eb8ee884f01cfaf2887ab995b0b36a0cbec - languageName: node - linkType: hard - "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -2376,17 +2229,6 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.1.1": - version: 4.2.0 - resolution: "js-yaml@npm:4.2.0" - dependencies: - argparse: "npm:^2.0.1" - bin: - js-yaml: bin/js-yaml.js - checksum: 10c0/1916456c118746603b067d74bbcbb0445d9a1d5e474ad4ae775e7b20525bed902e01d9d97dd0c81fcd8d4f596162309d0eb057f4aa38f3e9647f14075e9dea45 - languageName: node - linkType: hard - "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -2408,15 +2250,6 @@ __metadata: languageName: node linkType: hard -"json5@npm:^2.2.2": - version: 2.2.3 - resolution: "json5@npm:2.2.3" - bin: - json5: lib/cli.js - checksum: 10c0/5a04eed94810fa55c5ea138b2f7a5c12b97c3750bc63d11e511dcecbfef758003861522a070c2272764ee0f4e3e323862f386945aeb5b85b87ee43f084ba586c - languageName: node - linkType: hard - "keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -2586,13 +2419,6 @@ __metadata: languageName: node linkType: hard -"lodash.merge@npm:^4.6.2": - version: 4.6.2 - resolution: "lodash.merge@npm:4.6.2" - checksum: 10c0/402fa16a1edd7538de5b5903a90228aa48eb5533986ba7fa26606a49db2572bf414ff73a2c9f5d5fd36b31c46a5d5c7e1527749c07cbcf965ccff5fbdf32c506 - languageName: node - linkType: hard - "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -2638,13 +2464,6 @@ __metadata: languageName: node linkType: hard -"make-error@npm:^1.1.1": - version: 1.3.6 - resolution: "make-error@npm:1.3.6" - checksum: 10c0/171e458d86854c6b3fc46610cfacf0b45149ba043782558c6875d9f42f222124384ad0b468c92e996d815a8a2003817a710c0a160e49c1c394626f76fa45396f - languageName: node - linkType: hard - "make-fetch-happen@npm:^14.0.3": version: 14.0.3 resolution: "make-fetch-happen@npm:14.0.3" @@ -2664,7 +2483,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.2.2": +"minimatch@npm:^10.2.2, minimatch@npm:^10.2.4": version: 10.2.5 resolution: "minimatch@npm:10.2.5" dependencies: @@ -2673,15 +2492,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.1.5": - version: 3.1.5 - resolution: "minimatch@npm:3.1.5" - dependencies: - brace-expansion: "npm:^1.1.7" - checksum: 10c0/2ecbdc0d33f07bddb0315a8b5afbcb761307a8778b48f0b312418ccbced99f104a2d17d8aca7573433c70e8ccd1c56823a441897a45e384ea76ef401a26ace70 - languageName: node - linkType: hard - "minimatch@npm:^9.0.4": version: 9.0.5 resolution: "minimatch@npm:9.0.5" @@ -2691,13 +2501,6 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.6": - version: 1.2.8 - resolution: "minimist@npm:1.2.8" - checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 - languageName: node - linkType: hard - "minipass-collect@npm:^2.0.1": version: 2.0.1 resolution: "minipass-collect@npm:2.0.1" @@ -2927,15 +2730,6 @@ __metadata: languageName: node linkType: hard -"parent-module@npm:^1.0.0": - version: 1.0.1 - resolution: "parent-module@npm:1.0.1" - dependencies: - callsites: "npm:^3.0.0" - checksum: 10c0/c63d6e80000d4babd11978e0d3fee386ca7752a02b035fd2435960ffaa7219dc42146f07069fb65e6e8bf1caef89daf9af7535a39bddf354d78bf50d8294f556 - languageName: node - linkType: hard - "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -3078,13 +2872,6 @@ __metadata: languageName: node linkType: hard -"resolve-from@npm:^4.0.0": - version: 4.0.0 - resolution: "resolve-from@npm:4.0.0" - checksum: 10c0/8408eec31a3112ef96e3746c37be7d64020cda07c03a920f5024e77290a218ea758b26ca9529fd7b1ad283947f34b2291c1c0f6aa0ed34acfdda9c6014c8d190 - languageName: node - linkType: hard - "resolve-from@npm:^5.0.0": version: 5.0.0 resolution: "resolve-from@npm:5.0.0" @@ -3395,20 +3182,6 @@ __metadata: languageName: node linkType: hard -"strip-bom@npm:^3.0.0": - version: 3.0.0 - resolution: "strip-bom@npm:3.0.0" - checksum: 10c0/51201f50e021ef16672593d7434ca239441b7b760e905d9f33df6e4f3954ff54ec0e0a06f100d028af0982d6f25c35cd5cda2ce34eaebccd0250b8befb90d8f1 - languageName: node - linkType: hard - -"strip-json-comments@npm:^3.1.1": - version: 3.1.1 - resolution: "strip-json-comments@npm:3.1.1" - checksum: 10c0/9681a6257b925a7fa0f285851c0e613cc934a50661fa7bb41ca9cbbff89686bb4a0ee366e6ecedc4daafd01e83eee0720111ab294366fe7c185e935475ebcecd - languageName: node - linkType: hard - "sucrase@npm:^3.35.0": version: 3.35.0 resolution: "sucrase@npm:3.35.0" @@ -3541,55 +3314,6 @@ __metadata: languageName: node linkType: hard -"ts-node@npm:^10.9.2": - version: 10.9.2 - resolution: "ts-node@npm:10.9.2" - dependencies: - "@cspotcode/source-map-support": "npm:^0.8.0" - "@tsconfig/node10": "npm:^1.0.7" - "@tsconfig/node12": "npm:^1.0.7" - "@tsconfig/node14": "npm:^1.0.0" - "@tsconfig/node16": "npm:^1.0.2" - acorn: "npm:^8.4.1" - acorn-walk: "npm:^8.1.1" - arg: "npm:^4.1.0" - create-require: "npm:^1.1.0" - diff: "npm:^4.0.1" - make-error: "npm:^1.1.1" - v8-compile-cache-lib: "npm:^3.0.1" - yn: "npm:3.1.1" - peerDependencies: - "@swc/core": ">=1.2.50" - "@swc/wasm": ">=1.2.50" - "@types/node": "*" - typescript: ">=2.7" - peerDependenciesMeta: - "@swc/core": - optional: true - "@swc/wasm": - optional: true - bin: - ts-node: dist/bin.js - ts-node-cwd: dist/bin-cwd.js - ts-node-esm: dist/bin-esm.js - ts-node-script: dist/bin-script.js - ts-node-transpile-only: dist/bin-transpile.js - ts-script: dist/bin-script-deprecated.js - checksum: 10c0/5f29938489f96982a25ba650b64218e83a3357d76f7bede80195c65ab44ad279c8357264639b7abdd5d7e75fc269a83daa0e9c62fd8637a3def67254ecc9ddc2 - languageName: node - linkType: hard - -"tsconfig-paths@npm:^4.2.0": - version: 4.2.0 - resolution: "tsconfig-paths@npm:4.2.0" - dependencies: - json5: "npm:^2.2.2" - minimist: "npm:^1.2.6" - strip-bom: "npm:^3.0.0" - checksum: 10c0/09a5877402d082bb1134930c10249edeebc0211f36150c35e1c542e5b91f1047b1ccf7da1e59babca1ef1f014c525510f4f870de7c9bda470c73bb4e2721b3ea - languageName: node - linkType: hard - "tslib@npm:^2.4.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" @@ -3690,10 +3414,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.18.0": - version: 7.18.2 - resolution: "undici-types@npm:7.18.2" - checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d +"undici-types@npm:~8.3.0": + version: 8.3.0 + resolution: "undici-types@npm:8.3.0" + checksum: 10c0/c8aa7e2fbebfce519654dafadc0ece59be888d2ccaf180fb4495da875e7b536d2456345c384069c7e6f3e9c9ab7435f074957da306f142343eee86ff8048855a languageName: node linkType: hard @@ -3724,13 +3448,6 @@ __metadata: languageName: node linkType: hard -"v8-compile-cache-lib@npm:^3.0.1": - version: 3.0.1 - resolution: "v8-compile-cache-lib@npm:3.0.1" - checksum: 10c0/bdc36fb8095d3b41df197f5fb6f11e3a26adf4059df3213e3baa93810d8f0cc76f9a74aaefc18b73e91fe7e19154ed6f134eda6fded2e0f1c8d2272ed2d2d391 - languageName: node - linkType: hard - "vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0": version: 8.0.16 resolution: "vite@npm:8.0.16" @@ -3788,17 +3505,17 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^4.1.8": - version: 4.1.8 - resolution: "vitest@npm:4.1.8" +"vitest@npm:^4.1.9": + version: 4.1.9 + resolution: "vitest@npm:4.1.9" dependencies: - "@vitest/expect": "npm:4.1.8" - "@vitest/mocker": "npm:4.1.8" - "@vitest/pretty-format": "npm:4.1.8" - "@vitest/runner": "npm:4.1.8" - "@vitest/snapshot": "npm:4.1.8" - "@vitest/spy": "npm:4.1.8" - "@vitest/utils": "npm:4.1.8" + "@vitest/expect": "npm:4.1.9" + "@vitest/mocker": "npm:4.1.9" + "@vitest/pretty-format": "npm:4.1.9" + "@vitest/runner": "npm:4.1.9" + "@vitest/snapshot": "npm:4.1.9" + "@vitest/spy": "npm:4.1.9" + "@vitest/utils": "npm:4.1.9" es-module-lexer: "npm:^2.0.0" expect-type: "npm:^1.3.0" magic-string: "npm:^0.30.21" @@ -3816,12 +3533,12 @@ __metadata: "@edge-runtime/vm": "*" "@opentelemetry/api": ^1.9.0 "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.1.8 - "@vitest/browser-preview": 4.1.8 - "@vitest/browser-webdriverio": 4.1.8 - "@vitest/coverage-istanbul": 4.1.8 - "@vitest/coverage-v8": 4.1.8 - "@vitest/ui": 4.1.8 + "@vitest/browser-playwright": 4.1.9 + "@vitest/browser-preview": 4.1.9 + "@vitest/browser-webdriverio": 4.1.9 + "@vitest/coverage-istanbul": 4.1.9 + "@vitest/coverage-v8": 4.1.9 + "@vitest/ui": 4.1.9 happy-dom: "*" jsdom: "*" vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -3851,8 +3568,8 @@ __metadata: vite: optional: false bin: - vitest: vitest.mjs - checksum: 10c0/f459c500f8818c7a2318cd23228b4e5c0b5efb25bf8e5cb7f116c6d26e51482b2f800a8bb19837c0b5f0d05c51519edbf502bc8ceb5bd86868e8facf1d2c498e + vitest: ./vitest.mjs + checksum: 10c0/1ac80ef4991be82822a52aea48415f1bc64ddf8fd88ee24c172ec368f1d480fefacbde622c3c951982f7961a1d07313e18deaafc774d29e42ad6f6ffa63334a7 languageName: node linkType: hard @@ -3933,13 +3650,6 @@ __metadata: languageName: node linkType: hard -"yn@npm:3.1.1": - version: 3.1.1 - resolution: "yn@npm:3.1.1" - checksum: 10c0/0732468dd7622ed8a274f640f191f3eaf1f39d5349a1b72836df484998d7d9807fbea094e2f5486d6b0cd2414aad5775972df0e68f8604db89a239f0f4bf7443 - languageName: node - linkType: hard - "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0"