From 78c9e519383685f5a5c69a306232701c7cf399df Mon Sep 17 00:00:00 2001 From: John Uy Date: Wed, 29 Jul 2026 07:53:36 -0700 Subject: [PATCH 1/3] Harden Solavin for research-grade I-V analysis --- .github/workflows/.ciyml | 28 - .github/workflows/ci.yml | 42 + CITATION.cff | 12 + README.md | 274 +-- docs/VALIDATION.md | 141 ++ index.html | 2 +- netlify.toml | 10 +- package-lock.json | 2681 +++++++++++-------------- package.json | 23 +- src/App.jsx | 289 ++- src/components/AboutPage.jsx | 17 +- src/components/ProfileGate.jsx | 14 +- src/components/Welcome.jsx | 2 +- src/components/shared.jsx | 53 +- src/lib/assistant.js | 64 +- src/lib/assistant.test.js | 59 + src/lib/ivAnalysis.js | 626 +++++- src/lib/ivAnalysis.test.js | 3 +- src/lib/ivAnalysis.validation.test.js | 434 ++++ tests/e2e.smoke.mjs | 131 +- vite.config.js | 11 + 21 files changed, 2950 insertions(+), 1966 deletions(-) delete mode 100644 .github/workflows/.ciyml create mode 100644 .github/workflows/ci.yml create mode 100644 CITATION.cff create mode 100644 docs/VALIDATION.md create mode 100644 src/lib/assistant.test.js create mode 100644 src/lib/ivAnalysis.validation.test.js diff --git a/.github/workflows/.ciyml b/.github/workflows/.ciyml deleted file mode 100644 index f345d3e..0000000 --- a/.github/workflows/.ciyml +++ /dev/null @@ -1,28 +0,0 @@ -name: CI - -on: - push: - branches: ["**"] - pull_request: - -jobs: - test-and-build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "npm" - - run: npm ci - - name: Unit tests (physics invariants) - run: npm test - - name: Production build - run: npm run build - - name: Standalone single-file build - run: npm run build:standalone - - name: Upload standalone artifact - uses: actions/upload-artifact@v4 - with: - name: solar-iv-lab-standalone - path: dist-standalone/index.html diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2ee243a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +concurrency: + group: solavin-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-and-build: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "npm" + - run: npm ci + - name: Dependency audit + run: npm audit --audit-level=high + - name: Scientific validation and coverage gates + run: npm run test:coverage + - name: Production and standalone builds + run: npm run build:all + - name: Install Chromium + run: npx playwright install --with-deps chromium + - name: Browser integration test + run: node tests/e2e.smoke.mjs + - name: Upload standalone artifact + uses: actions/upload-artifact@v4 + with: + name: solavin-v4-standalone + path: dist-standalone/index.html + if-no-files-found: error + retention-days: 14 diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..5b50c1b --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,12 @@ +cff-version: 1.2.0 +message: "If you use Solavin in research, please cite the software and record the exact analysis version." +title: "Solavin: Photovoltaic Characterization and Analysis Suite" +type: software +version: 4.0.0 +date-released: 2026-07-29 +authors: + - family-names: Uy + given-names: John Myron +repository-code: "https://github.com/dev-juy/project-solavin" +url: "https://github.com/dev-juy/project-solavin" +license: MIT diff --git a/README.md b/README.md index 4c0b198..2ba1d24 100644 --- a/README.md +++ b/README.md @@ -1,144 +1,170 @@ -# project-solavin# Solavin — Photovoltaic Characterization Suite - -A lab-grade web application for analysing photovoltaic **current–voltage (I-V)** -sweeps. Upload measured data, and Solavin extracts the standard figures of -merit — short-circuit current (Isc), open-circuit voltage -(Voc), maximum power (Pmax), fill factor (FF), series and -shunt resistance (Rs, Rsh) and power-conversion efficiency -(η) — with interactive I-V / P-V / radar visualisations and one-click CSV/Excel -export. - -Built with React 18 + Vite + Recharts. The scientific core is plain, -dependency-free JavaScript covered by an automated test suite, so results are -reproducible and verifiable. - -> Developed by **John Myron Uy**, under the guidance of **Prof. Raymund Sarmiento** — -> Research Laboratory, Solar Cell Characterization. - ---- - -## Highlights - -- **Parameter extraction** — Isc, Voc, Pmax, - Vmp, Imp, FF, Rs, Rsh, η. -- **Multi-channel** — load many sweep conditions per dataset and compare them. -- **Visualisations** — I-V, P-V (with MPP markers), normalised radar profile, - and ranked Pmax comparison; brush-to-zoom and a voltage cursor - lookup. -- **Data ingest** — drag-and-drop `.xlsx` / `.xls` / `.csv`; multi-sheet picker. -- **Export** — CSV and XLSX of the full metrics table, including η when area and - irradiance are supplied. -- **Lab Assistant** — an offline, deterministic analyser that answers from the - extracted metrics (summaries, rankings, fill-factor interpretation, suggested - experiments). No external API, no keys. -- **Local operator profiles** — name a session to keep a persistent dataset - library in your browser, or work in demo mode. No passwords, no backend. -- **Two build targets** — a normal web app for hosting, and a single - self-contained `.html` you can email or run offline from `file://`. - -## Accuracy & conventions - -Photovoltaic figures of merit are computed in -[`src/lib/ivAnalysis.js`](src/lib/ivAnalysis.js) on the **signed** current -(generator convention: current is positive in the power-producing quadrant and -negative beyond Voc). - -| Quantity | Definition used | -|----------|-----------------| -| Isc | Current at V = 0 (exact sample, else interpolated; extrapolated if the sweep starts above 0 V). | -| Voc | Voltage at the first I = 0 crossing (linear interpolation). Flagged if it lies beyond the sweep range. | -| Pmax, Vmp, Imp | Maximum of V·I, **restricted to the power quadrant** (V ≥ 0, I ≥ 0). | -| FF | Pmax / (Isc · Voc) — always < 1 for physical data. | -| Rs | \|dV/dI\| near Voc (slope estimate). | -| Rsh | \|dV/dI\| near Isc (slope estimate). | -| η | Pmax / (G · A), with cell area in cm² and irradiance G in W/m². | - -Rs and Rsh are first-order slope estimates from a single -light sweep; a dark I-V curve or a full single-diode fit gives more rigorous -values. - -The test suite ([`src/lib/ivAnalysis.test.js`](src/lib/ivAnalysis.test.js)) -asserts these invariants on every build — including that the fill factor is -always between 0 and 100 % — so the extraction cannot silently regress. - -## Data format - -Column **A** is voltage (volts); each subsequent column is one current sweep -(amperes, signed). Row 1 holds headers, which become channel labels. - -``` -Voltage(V), Focused Laser, -2mm, +6mm Focus -0, 1.539e-6, 1.46e-6, 1.583e-6 -0.05, 1.528e-6, 1.456e-6, 1.565e-6 -... -2.5, -3.875e-6, -3.052e-6, -2.923e-6 +# Solavin + +Solavin is a research-oriented web application for inspecting photovoltaic +current-voltage (I-V) sweeps. It extracts Isc, Voc, +Pmax, Vmp, Imp, fill factor, local slope +estimates of Rs/Rsh, and—when area and irradiance are +provided—power-conversion efficiency. + +The application is designed to make questionable data visible rather than turn +it into precise-looking numbers. Missing cells remain missing, unbracketed +Voc values are reported as lower bounds, dependent metrics are +withheld when they cannot be supported, and every export carries analysis +status and provenance. + +> Solavin validates calculations performed on supplied data. It does not +> calibrate an instrument or certify a photovoltaic measurement to IEC 60904-1. +> Measurement uncertainty, spectral mismatch, temperature control, irradiance +> uniformity, sweep protocol, contact quality, and instrument calibration remain +> the operator's responsibility. + +Developed by **John Myron Uy**, under the guidance of +**Prof. Raymund Sarmiento** — Research Laboratory, Solar Cell +Characterization. + +## What researchers get + +- Piecewise-linear I-V and P-V plots that do not invent smoothed extrema. +- Exact maximum-power optimization on each measured linear segment. +- Voltage-keyed alignment across sparse channels, so one missing cell cannot + shift later measurements onto the wrong voltage. +- Explicit `pass`, `review`, and `invalid` channel states. +- CSV and three-sheet XLSX exports: + - `Metrics` — values, units, per-metric status, quality flags, source, and + analysis version. + - `Raw Data` — signed measurements aligned by voltage with gaps preserved. + - `Metadata` — conventions, handling rules, source, and import warnings. +- Safe `.xlsx` and RFC 4180 CSV ingestion with a 25 MB browser limit. +- A deterministic offline assistant that only summarizes validated extracted + results. +- A normal hosted build and a self-contained offline HTML build. + +## Analysis definitions + +All scientific calculations live in +[`src/lib/ivAnalysis.js`](src/lib/ivAnalysis.js). Current uses the generator +convention: current is positive while the device produces positive V·I power +and negative beyond Voc. + +| Quantity | Definition and validity rule | +|---|---| +| Isc | Signed current at V = 0. Uses an exact sample, interpolation across zero, or a flagged nearest-pair extrapolation. | +| Voc | First non-negative-voltage positive-to-negative current crossing, linearly interpolated. If no crossing is measured, the highest measured voltage is only a lower bound. | +| Pmax | Maximum of V·I in the measured power quadrant. The maximum is solved analytically on every piecewise-linear segment. | +| Vmp, Imp | Coordinates of the same piecewise-linear Pmax solution. | +| FF | Pmax / (Isc·Voc). Withheld if Voc is unbracketed; out-of-range results are reported as errors, never clamped. | +| Rs | \|dV/dI\| from a local multi-point linear fit near Voc. Withheld when Voc is unbracketed or the fit is degenerate. | +| Rsh | \|dV/dI\| from a local multi-point linear fit near V = 0. | +| η | Pmax / (G·A), with P in W, G in W/m², and illuminated area entered in cm². | + +Rs and Rsh are local light-sweep slope estimates, not a +substitute for uncertainty-aware parameter fitting or dark-I-V analysis. + +## Validation + +The automated suite currently contains 53 tests, including: + +- 300 deterministic randomized linear curves checked against closed-form + Isc, Voc, Pmax, Vmp, + Imp, and FF solutions. +- Six continuous single-diode curves spanning nanoampere to ampere scales, + checked against an independent golden-section maximum-power oracle. +- Current-scale invariance checks. +- Censored sweeps, nonphysical curves, duplicate voltages, all-negative and + sparse sweeps. +- Blank cells, duplicate/blank headers, malicious labels, quoted and malformed + CSV, input safety limits, and spreadsheet-formula neutralization. +- Raw-data alignment, status/provenance export, and invalid efficiency inputs. + +Coverage gates apply to the scientific core: at least 90% statements, 80% +branches, 90% functions, and 90% lines. CI additionally performs a high-severity +dependency audit, both production builds, and a real-Chromium import, +visualization, assistant, and CSV/XLSX download smoke test. + +See [`docs/VALIDATION.md`](docs/VALIDATION.md) for the audit register, +tolerances, reproducibility commands, and residual limitations. + +## Input format + +Use `.xlsx` or `.csv`. Column A is voltage in volts. Each subsequent column is +one signed current sweep in amperes, and row 1 contains channel labels. + +```csv +Voltage (V),Focused Laser,-2 mm,+6 mm Focus +0,1.539e-6,1.460e-6,1.583e-6 +0.05,1.528e-6,1.456e-6,1.565e-6 +2.50,-3.875e-6,-3.052e-6,-2.923e-6 ``` -A ready-to-upload example lives in -[`examples/sample_iv_data.csv`](examples/sample_iv_data.csv) (the same dataset -the app loads by default). +Rules: -## Getting started +- Every accepted channel must contain at least three unique voltage setpoints + with valid voltage-current pairs. +- Blank or nonnumeric current cells are skipped and reported; they are never + converted to zero. +- Exact duplicate voltage setpoints are averaged and flagged. +- Blank and duplicate channel headers are assigned unique visible names. +- Legacy `.xls` files are intentionally rejected; convert them to `.xlsx` or + CSV first. -```bash -npm install -npm run dev # start the dev server (http://localhost:5173) -npm test # run the physics unit tests -npm run build # production build → dist/ -npm run build:standalone # single self-contained file → dist-standalone/index.html -npm run build:all # both builds -``` +The bundled example is +[`examples/sample_iv_data.csv`](examples/sample_iv_data.csv). -### Standalone file +## Run and verify -`npm run build:standalone` produces `dist-standalone/index.html` — a single file -with all JavaScript and CSS inlined. Open it directly (`file://…`) with no server; -it works offline (web fonts fall back to system fonts when there's no network). +Node.js 22 is the CI reference runtime. -## Deployment (Netlify) +```bash +npm ci +npm run validate # dependency audit, coverage-gated tests, both builds +npm run test:e2e # standalone build + real-browser smoke test +npm run dev # local development server +``` -This repo includes [`netlify.toml`](netlify.toml). To go live: +Build targets: -1. Push the repository to GitHub. -2. In the Netlify dashboard, **Add new site → Import an existing project** and - pick this repo. -3. Netlify reads `netlify.toml` automatically (build `npm run build`, publish - `dist`). No manual configuration needed. +```bash +npm run build # dist/ +npm run build:standalone # dist-standalone/index.html +npm run build:all # both +``` -Every push then redeploys. The continuous-integration workflow -([`.github/workflows/ci.yml`](.github/workflows/ci.yml)) runs the unit tests and -both builds, and uploads the standalone HTML as a downloadable artifact. +The standalone file contains the application code and can run from `file://` +without a backend. Browser profile and dataset storage is local and is not an +authentication or access-control system. -## Project structure +## Project map -``` +```text src/ lib/ - ivAnalysis.js # parameter extraction + parsing + export helpers (pure) - ivAnalysis.test.js # physics invariants & reference-value tests - assistant.js # deterministic, offline dataset analyser + ivAnalysis.js + ivAnalysis.test.js + ivAnalysis.validation.test.js + assistant.js components/ - ProfileGate.jsx # local operator gate (no passwords) - Tour.jsx # guided walkthrough overlay - Assistant.jsx # lab-assistant side panel - shared.jsx # chart tooltip, channel toggles, lookup, raw-data table - AboutPage.jsx - App.jsx # main application - theme.js icons.jsx persistence.js styles.css main.jsx -examples/sample_iv_data.csv -tests/e2e.smoke.mjs # optional browser smoke test (skips if no Chromium) + App.jsx +docs/ + VALIDATION.md +examples/ + sample_iv_data.csv +tests/ + e2e.smoke.mjs +.github/workflows/ + ci.yml ``` -## Testing +## Standards context + +IEC 60904-1 defines measurement requirements for photovoltaic I-V +characteristics; Solavin is an analysis and visualization layer, not an IEC +conformity claim. NREL's photovoltaic measurement guidance likewise emphasizes +calibration and uncertainty in the upstream measurement chain. -- `npm test` — Vitest unit tests for the extraction math (run in CI). -- `npm run test:e2e` — optional Playwright smoke test that builds the standalone - bundle, drives it in a real browser, and verifies the app mounts cleanly with - physical fill factors. Playwright is **not** a default dependency; install it - first with `npm i -D playwright && npx playwright install chromium`. The test - skips automatically (exit 0) if Playwright or a browser is unavailable, so it - never blocks CI. +- [IEC 60904-1:2020 — Photovoltaic devices, Part 1](https://webstore.iec.ch/en/publication/32004) +- [NREL — Trust But Verify: Creating and Using Highly Accurate Measurements in Solar Research](https://www.nrel.gov/docs/fy05osti/36527.pdf) -## License +## License and citation -[MIT](LICENSE) © 2026 John Myron Uy. +[MIT](LICENSE) © 2026 John Myron Uy. Citation metadata is provided in +[`CITATION.cff`](CITATION.cff). diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md new file mode 100644 index 0000000..001ff0e --- /dev/null +++ b/docs/VALIDATION.md @@ -0,0 +1,141 @@ +# Solavin 4.0 validation and audit record + +**Validation date:** 2026-07-29 +**Reference runtime:** Node.js 22 +**Scientific method identifier:** `piecewise-linear-v4` +**Audited baseline:** `375fa05129a5b1fdb8364f50dda3029db9001738` + +## Scope and claim + +This record covers the software path from an uploaded array of voltage-current +cells through parsing, parameter extraction, visualization data, and exported +results. It supports confidence that Solavin applies its documented +piecewise-linear method consistently. + +It does not establish metrological traceability, quantify measurement +uncertainty, validate an upstream instrument, or certify a procedure against +IEC 60904-1. Those require calibrated equipment, a documented measurement +protocol, environmental controls, and uncertainty analysis outside this +browser application. + +No serious scientific tool can truthfully promise to be “flawless.” The +defensible claim is narrower: the algorithms below are explicit, their failure +states are visible, and a repeatable automated suite guards the tested scope. + +## Audit findings and remediation + +The baseline dependency scan reported nine known vulnerabilities: one critical, +three high, and five moderate. After replacing the workbook dependency and +upgrading the build/test/chart stack, `npm audit` reported zero known +vulnerabilities on the validation date. + +| Severity | Original finding | Scientific or operational risk | Remediation | +|---|---|---|---| +| Critical | Missing current cells were coerced to zero. | Fabricated points changed curve shape and derived metrics. | Missing/nonnumeric cells are skipped and surfaced as import warnings. | +| Critical | Channels were combined by array index. | A gap in one channel shifted later currents onto incorrect voltages. | Visualization and raw export now use a union keyed by numeric voltage. | +| High | Charts used smoothed `monotone` curves while calculations used linear segments. | The visual could imply unmeasured extrema or curvature. | All I-V/P-V lines use straight measured segments. | +| High | Unbracketed Voc was presented as an exact endpoint. | FF and Rs appeared valid despite a censored sweep. | Voc becomes a lower bound; Pmax is provisional; FF and Rs are withheld. | +| High | Blank headers changed channel-column mapping. | Measurements could be assigned to the wrong condition. | Physical column positions are retained; blank and duplicate labels are uniquely named. | +| High | The Excel dependency and old build/test packages had known vulnerabilities. | Untrusted-workbook and build-chain exposure. | Replaced `xlsx`, upgraded Vite/Vitest/Recharts, and reached `npm audit` zero known vulnerabilities at validation time. | +| Medium | The workflow filename was `.ciyml` inside `.github/workflows`. | GitHub Actions did not recognize the intended CI workflow. | Installed a valid `.github/workflows/ci.yml` with audit, coverage, build, Chromium, and artifact gates. | +| Medium | Comparison charts hard-coded nanowatts. | Ampere-scale and other datasets were mislabeled. | Shared automatic SI scaling is used throughout. | +| Medium | CSV string cells could begin spreadsheet formulas. | Opening an export could execute an injected formula in spreadsheet software. | Formula-leading strings are prefixed before CSV serialization. | +| Medium | The interface implied a connected SMU and four-wire mode. | Researchers could mistake a visualization-only tool for instrument control. | UI now states `LOCAL / NO BACKEND` and makes no wiring/instrument claim. | +| Medium | Endpoint two-point resistance estimates were fragile. | Rs/Rsh varied strongly with one noisy sample. | Local multi-point regressions now return fit diagnostics and withhold degenerate estimates. | + +## Accuracy oracles + +### Closed-form linear curves + +For + +`I(V) = Isc × (1 − V/Voc)` + +the suite checks 300 seeded randomized curves spanning 10⁻⁸ A to 1 A and +0.15 V to 5 V. It independently expects: + +- Vmp = Voc/2 +- Imp = Isc/2 +- Pmax = IscVoc/4 +- FF = 0.25 + +Samples are shuffled and irregularly spaced so success does not depend on input +ordering or a fixed grid. + +### Continuous single-diode curves + +Six ideal single-diode cases span 8 nA to 4.2 A, Voc values from +0.42 V to 2.08 V, and ideality factors from 1.0 to 2.0. A golden-section search +evaluates the continuous source equation independently of Solavin. The sampled +piecewise-linear extraction must meet: + +| Output | Acceptance tolerance | +|---|---:| +| Isc relative error | < 1×10⁻¹⁰ | +| Voc relative error | < 2×10⁻⁵ | +| Vmp absolute error | < 2 sample steps | +| Pmax relative error | < 2×10⁻⁴ | +| FF relative error | < 2×10⁻⁴ | + +These tolerances validate the sampled piecewise-linear method. They are not a +claim about an instrument's measurement accuracy. + +## Data-integrity controls + +- Only finite numeric voltage-current pairs enter extraction. +- Signed current is preserved; absolute current is never used for physics. +- Exact duplicate voltage values are averaged deterministically and counted. +- Sparse channels retain blank gaps and are aligned by voltage. +- Pmax is restricted to measured V ≥ 0, I ≥ 0 segments. +- Voc requires a measured nonnegative positive-to-negative crossing. +- Multiple nonnegative positive-to-negative crossings are ambiguous and + withhold Voc, FF, and Rs. +- FF outside the physical 0–100% range is an error, not a clamped result. +- Efficiency rejects nonfinite, negative-power, zero-area, and + zero-irradiance inputs. +- Every metrics row carries method version, sample counts, statuses, quality, + source, import flags, metric flags, and any efficiency inputs. +- XLSX exports preserve signed raw data and metadata in separate sheets. + +## Reproduction + +From a clean checkout: + +```bash +npm ci +npm audit --audit-level=high +npm run test:coverage +npm run build:all +npx playwright install --with-deps chromium +CI=1 node tests/e2e.smoke.mjs +``` + +Expected automated gates: + +- all Vitest tests pass; +- scientific-core coverage ≥90% statements, ≥80% branches, ≥90% functions, + and ≥90% lines; +- dependency audit has no high/critical finding; +- both standard and standalone Vite builds succeed; +- Chromium imports a sparse CSV, renders finite chart paths, exercises the + assistant and efficiency calculator, and downloads CSV and XLSX outputs. + +## Residual limitations + +| Limitation | Consequence | Required research control | +|---|---|---| +| No uncertainty propagation | Values do not include confidence intervals. | Retain instrument uncertainty and propagate it externally. | +| No spectral or temperature correction | Results from differing conditions may not be directly comparable. | Record and normalize irradiance, spectrum, and device temperature under the chosen protocol. | +| No hysteresis/sweep-rate model | Direction- or timing-dependent devices may appear deceptively stable. | Preserve forward/reverse sweeps and acquisition timing as separate channels and metadata. | +| Rs/Rsh are local slopes | They are not equivalent to a full diode-model parameter fit. | Use dark/light model fitting with uncertainty when those parameters drive conclusions. | +| Exact-voltage duplicate averaging | Repeated setpoints are collapsed without time-order analysis. | Analyze time series separately when drift or hysteresis matters. | +| Browser-local persistence | It is not a controlled laboratory information system. | Archive raw files and exported results in a versioned research repository. | +| No instrument connection | Solavin cannot enforce acquisition settings or calibration. | Record instrument model, calibration, wiring, ranges, dwell time, and compliance separately. | + +## Standards context + +- [IEC 60904-1:2020](https://webstore.iec.ch/en/publication/32004) + specifies photovoltaic I-V measurement requirements. +- [NREL, Trust But Verify](https://www.nrel.gov/docs/fy05osti/36527.pdf) + discusses calibration, uncertainty, and common photovoltaic measurement + errors. diff --git a/index.html b/index.html index 94b1ab6..6117a13 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ Solavin — Photovoltaic Characterization Suite =6.9.0" } }, - "node_modules/@babel/compat-data": { + "node_modules/@babel/helper-validator-identifier": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/core": { + "node_modules/@babel/parser": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" + "@babel/types": "^7.29.7" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" + "bin": { + "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=6.9.0" + "node": ">=6.0.0" } }, - "node_modules/@babel/helper-compilation-targets": { + "node_modules/@babel/types": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "node_modules/@emnapi/core": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" + "@emnapi/wasi-threads": "2.0.1", + "tslib": "^2.4.0" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "node_modules/@emnapi/runtime": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "tslib": "^2.4.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "node_modules/@emnapi/wasi-threads": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "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.9.0" + "node": ">=6.0.0" } }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "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", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } + "license": "MIT" }, - "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", - "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz", + "integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" + "@tybys/wasm-util": "^0.10.3" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", - "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" }, - "engines": { - "node": ">=6.9.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "@emnapi/core": "^2.0.0-alpha.3", + "@emnapi/runtime": "^2.0.0-alpha.3" } }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dev": true, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, - "engines": { - "node": ">=6.9.0" + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "aix" + "android" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" + "darwin" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" + "darwin" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -374,50 +269,50 @@ "license": "MIT", "optional": true, "os": [ - "android" + "freebsd" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ "arm64" ], @@ -425,35 +320,35 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ - "x64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ - "arm" + "s390x" ], "dev": true, "license": "MIT", @@ -462,15 +357,15 @@ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", @@ -479,15 +374,15 @@ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", @@ -496,115 +391,100 @@ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "openharmony" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ - "mips64el" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -612,547 +492,51 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "win32" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "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/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "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.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" - ], + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.0.0" + "tslib": "^2.4.0" } }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, "node_modules/@types/d3-array": { @@ -1218,6 +602,13 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "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.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1225,60 +616,104 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", + "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.10", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", + "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -1290,84 +725,73 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/adler-32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1378,19 +802,25 @@ "node": ">=12" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.40", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", - "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" } }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -1404,109 +834,14 @@ "node": ">=8" } }, - "node_modules/browserslist": { - "version": "4.28.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", - "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.38", - "caniuse-lite": "^1.0.30001799", - "electron-to-chromium": "^1.5.376", - "node-releases": "^2.0.48", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "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/caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/cfb": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", - "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", - "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "crc-32": "~1.2.0" - }, - "engines": { - "node": ">=0.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.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 16" + "node": ">=18" } }, "node_modules/clsx": { @@ -1518,15 +853,6 @@ "node": ">=6" } }, - "node_modules/codepage": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", - "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1534,24 +860,6 @@ "dev": true, "license": "MIT" }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -1673,112 +981,39 @@ "node": ">=12" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, - "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==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=6" - } - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" + "node": ">=8" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.378", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz", - "integrity": "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==", - "dev": true, - "license": "ISC" - }, "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==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "dev": true, "license": "MIT" }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", "license": "MIT", - "engines": { - "node": ">=6" - } + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] }, "node_modules/estree-walker": { "version": "3.0.3", @@ -1791,9 +1026,9 @@ } }, "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, "node_modules/expect-type": { @@ -1806,14 +1041,11 @@ "node": ">=12.0.0" } }, - "node_modules/fast-equals": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", - "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" }, "node_modules/fill-range": { "version": "7.1.1", @@ -1828,15 +1060,6 @@ "node": ">=8" } }, - "node_modules/frac": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", - "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1852,14 +1075,37 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/immer": { + "version": "11.1.15", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.15.tgz", + "integrity": "sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" } }, "node_modules/internmap": { @@ -1881,44 +1127,312 @@ "node": ">=0.12.0" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" }, "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1931,31 +1445,42 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "yallist": "^3.0.2" + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/micromatch": { @@ -1972,17 +1497,10 @@ "node": ">=8.6" } }, - "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/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -1998,42 +1516,33 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/node-releases": { - "version": "2.0.50", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", - "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" }, - "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==", + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=12.20.0" } }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2054,10 +1563,57 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", + "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", + "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz", + "integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==", "dev": true, "funding": [ { @@ -2075,7 +1631,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2083,23 +1639,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -2129,125 +1668,139 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "peer": true }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", "license": "MIT", "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", + "node_modules/read-excel-file": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/read-excel-file/-/read-excel-file-9.3.5.tgz", + "integrity": "sha512-A4EbaJxFrqzBNdU4Lw0g6wzme3m7ywW3MsPLLuBiGPUWJbxETyVDT5vyqsOnl/AW48OGM4MGoOgKFsEZNMMVVw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" + "fflate": "^0.8.3", + "saxen": "^11.1.0", + "unzipper-esm": "^0.13.3", + "worker-f": "^0.1.12" }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" + "engines": { + "node": ">=18" } }, "node_modules/recharts": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", - "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", - "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", "license": "MIT", + "workspaces": [ + "www" + ], "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", "license": "MIT", - "dependencies": { - "decimal.js-light": "^2.4.1" + "peerDependencies": { + "redux": "^5.0.0" } }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/saxen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/saxen/-/saxen-11.1.1.tgz", + "integrity": "sha512-J4BkmJFaM7VgE7pgkFGsNEcqqM3h7+Mz80vfLWFhx7uNOCOXIu6LLjQHYWNejdst3pf/3JUaBIG9+pkk1umlow==", + "license": "MIT", + "engines": { + "node": ">= 20.12" } }, "node_modules/scheduler": { @@ -2260,13 +1813,16 @@ } }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/siginfo": { @@ -2286,18 +1842,6 @@ "node": ">=0.10.0" } }, - "node_modules/ssf": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", - "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", - "license": "Apache-2.0", - "dependencies": { - "frac": "~1.1.2" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -2306,12 +1850,25 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -2326,36 +1883,67 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, - "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==", + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "node_modules/tinyglobby/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": ">=14.0.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -2375,41 +1963,40 @@ "node": ">=8.0" } }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "license": "0BSD", + "optional": true + }, + "node_modules/unzipper-esm": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/unzipper-esm/-/unzipper-esm-0.13.3.tgz", + "integrity": "sha512-LUO6VZ6fCzkDbdMev0/fOhoIeVGKaOkTIOoYxVLE0SQjfvmAHK+oywl7lfhloSZIsdGJ25mJ18Mtd9CyTASjrA==", "license": "MIT", "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", "peerDependencies": { - "browserslist": ">= 4.21.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", @@ -2429,21 +2016,23 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -2452,23 +2041,33 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "less": { + "@vitejs/devtools": { "optional": true }, - "lightningcss": { + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -2485,32 +2084,15 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vite-plugin-singlefile": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.3.tgz", @@ -2533,59 +2115,93 @@ } } }, - "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, + "@opentelemetry/api": { + "optional": true + }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -2596,9 +2212,25 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "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", @@ -2616,51 +2248,26 @@ "node": ">=8" } }, - "node_modules/wmf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", - "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/word": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", - "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", - "license": "Apache-2.0", + "node_modules/worker-f": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/worker-f/-/worker-f-0.1.13.tgz", + "integrity": "sha512-wAtKSRQjuaUueG/rZEN19aJEp9nY4fUEM+HoFidN7qpicB/wSf9fbyQ7Q3gAcJqoWvXHMxQ4kATPFxlLiD6rbQ==", + "license": "MIT", "engines": { - "node": ">=0.8" + "node": ">=12" } }, - "node_modules/xlsx": { - "version": "0.18.5", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", - "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", - "license": "Apache-2.0", + "node_modules/write-excel-file": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/write-excel-file/-/write-excel-file-4.1.1.tgz", + "integrity": "sha512-MUnCnNtQrcZek832ZcU24uU0rSphFmKPD1DvIjXOlygVb93CV7Tme6H3jUTkxsMmjB2W7HIzERzjqTi5kui71A==", + "license": "MIT", "dependencies": { - "adler-32": "~1.3.0", - "cfb": "~1.2.1", - "codepage": "~1.15.0", - "crc-32": "~1.2.1", - "ssf": "~0.11.2", - "wmf": "~1.0.1", - "word": "~0.3.0" - }, - "bin": { - "xlsx": "bin/xlsx.njs" + "fflate": "^0.8.2" }, "engines": { - "node": ">=0.8" + "node": ">=18" } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" } } } diff --git a/package.json b/package.json index 7983dde..a34b313 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "solavin", "private": true, - "version": "3.1.0", + "version": "4.0.0", "type": "module", - "description": "Solavin — a lab-grade photovoltaic current-voltage (I-V) characterization suite. Extracts Isc, Voc, Pmax, FF, Rs, Rsh and efficiency from measured I-V sweeps.", + "description": "Solavin — a status-aware photovoltaic I-V analysis and visualization suite with tested extraction, data-quality flags, and reproducible exports.", "author": "John Myron Uy", "license": "MIT", "scripts": { @@ -13,19 +13,24 @@ "build:all": "npm run build && npm run build:standalone", "preview": "vite preview", "test": "vitest run", + "test:coverage": "vitest run --coverage", "test:watch": "vitest", - "test:e2e": "npm run build:standalone && node tests/e2e.smoke.mjs" + "test:e2e": "npm run build:standalone && node tests/e2e.smoke.mjs", + "validate": "npm audit --audit-level=high && npm run test:coverage && npm run build:all" }, "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", - "recharts": "^2.12.7", - "xlsx": "^0.18.5" + "read-excel-file": "^9.3.5", + "recharts": "^3.10.1", + "write-excel-file": "^4.1.1" }, "devDependencies": { - "@vitejs/plugin-react": "^4.3.1", - "vite": "^5.4.8", - "vite-plugin-singlefile": "^2.0.2", - "vitest": "^2.1.2" + "@vitejs/plugin-react": "^6.0.4", + "@vitest/coverage-v8": "^4.1.10", + "playwright": "^1.62.0", + "vite": "^8.1.5", + "vite-plugin-singlefile": "^2.3.3", + "vitest": "^4.1.10" } } diff --git a/src/App.jsx b/src/App.jsx index 6166d64..e407f81 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,6 @@ import { useState, useCallback, useMemo, useRef, useEffect, useLayoutEffect } from "react"; -import * as XLSX from "xlsx"; +import readExcelFile from "read-excel-file/browser"; +import writeExcelFile from "write-excel-file/browser"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RTooltip, ResponsiveContainer, AreaChart, Area, BarChart, Bar, Cell, ReferenceLine, @@ -10,7 +11,8 @@ import { mkT, C8 } from "./theme.js"; import { Logo, Ic } from "./icons.jsx"; import { calcMetrics, buildSampleDataset, extractIV, computeEfficiency, - metricsToRows, rowsToCsv, siPrefix, fmtSI, + metricsToRows, rowsToCsv, siPrefix, fmtSI, parseCsv, buildAlignedRows, + rawDataToRows, metadataToRows, } from "./lib/ivAnalysis.js"; import { getUserData, saveUserData } from "./persistence.js"; import { ChartTip, Toggles, VLookup, RawDataViewer, TweenNumber } from "./components/shared.jsx"; @@ -21,6 +23,7 @@ import { AboutPage } from "./components/AboutPage.jsx"; import { Welcome } from "./components/Welcome.jsx"; const WELCOME_KEY = "siv_welcome_seen"; +const MAX_IMPORT_BYTES = 25 * 1024 * 1024; function download(blob, filename) { const url = URL.createObjectURL(blob); @@ -52,6 +55,7 @@ export default function App() { // then auto-launches the guided tour. const [autoTourPending, setAutoTourPending] = useState(false); const [importError, setImportError] = useState(""); + const [exportError, setExportError] = useState(""); const [navPill, setNavPill] = useState(null); // {top, height} of the active nav item, in sidebar-local coords const fileRef = useRef(null); const navWrapRef = useRef(null); @@ -67,7 +71,13 @@ export default function App() { if (session && !session.isGuest) { const ud = getUserData(session.id); if (ud.datasets && ud.datasets.length > 0) { - const restored = ud.datasets.map((d) => ({ name: d.name, conditions: d.conditions, ivData: d.ivData })); + const restored = ud.datasets.map((d) => ({ + name: d.name, + conditions: d.conditions, + ivData: d.ivData, + diagnostics: d.diagnostics, + source: d.source, + })); if (restored.length > 0) setDatasets((prev) => prev.concat(restored)); } } @@ -86,7 +96,13 @@ export default function App() { useEffect(() => { if (session && !session.isGuest && datasets.length > 1) { const ud = getUserData(session.id); - ud.datasets = datasets.slice(1).map((d) => ({ name: d.name, conditions: d.conditions, ivData: d.ivData })); + ud.datasets = datasets.slice(1).map((d) => ({ + name: d.name, + conditions: d.conditions, + ivData: d.ivData, + diagnostics: d.diagnostics, + source: d.source, + })); saveUserData(session.id, ud); } }, [datasets, session]); @@ -103,6 +119,14 @@ export default function App() { }, [ds]); const activeConds = useMemo(() => (ds ? ds.conditions.filter((c) => selConds[c]) : []), [ds, selConds]); + const radarConds = useMemo(() => activeConds.filter((condition) => { + const metrics = allM[condition]; + return metrics && + metrics.quality !== "invalid" && + ["measured", "interpolated"].includes(metrics.status?.pmax) && + ["measured", "interpolated"].includes(metrics.status?.voc) && + metrics.status?.ff === "computed"; + }), [activeConds, allM]); // Auto-ranged SI display scales, picked from the dataset's largest current // and power magnitudes so µA-scale lab cells and A-scale production cells @@ -121,14 +145,7 @@ export default function App() { // I-V chart: signed current (dataset scale) → curve correctly crosses zero at Voc. const ivChart = useMemo(() => { - if (!ds) return []; - const f = ds.ivData[ds.conditions[0]]; - if (!f) return []; - return f.map((pt, i) => { - const row = { voltage: pt.voltage }; - activeConds.forEach((c) => { const pts = ds.ivData[c]; if (pts && pts[i]) row[c] = pts[i].rawCurrent / scales.i.div; }); - return row; - }); + return buildAlignedRows(ds, activeConds, (point) => point.rawCurrent / scales.i.div); }, [ds, activeConds, scales]); // P-V chart: signed power (dataset scale). Deep reverse-bias power is @@ -137,19 +154,20 @@ export default function App() { // The zero crossing at Voc plus a 20 % negative margin is kept visible. const pvChart = useMemo(() => { if (!ds) return []; - const f = ds.ivData[ds.conditions[0]]; - if (!f) return []; let gmax = 0; - activeConds.forEach((c) => (ds.ivData[c] || []).forEach((pt) => { const p = pt.voltage * pt.rawCurrent; if (p > gmax) gmax = p; })); + activeConds.forEach((c) => (ds.ivData[c] || []).forEach((pt) => { + const p = (pt.voltage * pt.rawCurrent) / scales.p.div; + if (p > gmax) gmax = p; + })); const cutoff = -0.2 * gmax; - return f.map((pt, i) => { - const row = { voltage: pt.voltage }; - activeConds.forEach((c) => { - const pts = ds.ivData[c]; - if (pts && pts[i]) { - const p = pts[i].voltage * pts[i].rawCurrent; - row[c] = gmax > 0 && p < cutoff ? null : p / scales.p.div; - } + return buildAlignedRows( + ds, + activeConds, + (point) => (point.voltage * point.rawCurrent) / scales.p.div + ).map((sourceRow) => { + const row = { ...sourceRow }; + activeConds.forEach((condition) => { + if (Number.isFinite(row[condition]) && gmax > 0 && row[condition] < cutoff) row[condition] = null; }); return row; }); @@ -172,9 +190,11 @@ export default function App() { const qualityFlags = useMemo(() => { const out = []; if (!ds) return out; + (ds.diagnostics?.warnings || []).forEach((w) => out.push({ c: "IMPORT", w, severity: "warning" })); ds.conditions.forEach((c) => { const m = allM[c]; - if (m && m.warnings) m.warnings.forEach((w) => out.push({ c, w })); + if (m && m.errors) m.errors.forEach((w) => out.push({ c, w, severity: "error" })); + if (m && m.warnings) m.warnings.forEach((w) => out.push({ c, w, severity: "warning" })); }); return out; }, [ds, allM]); @@ -188,14 +208,22 @@ export default function App() { const radarD = useMemo(() => { if (!ds || !Object.keys(allM).length) return []; const mx = { isc: 0, voc: 0, pmax: 0, ff: 0 }; - ds.conditions.forEach((c) => { const m = allM[c]; if (m) ["isc", "voc", "pmax", "ff"].forEach((k) => { if (m[k] > mx[k]) mx[k] = m[k]; }); }); + radarConds.forEach((c) => { + const m = allM[c]; + if (m) ["isc", "voc", "pmax", "ff"].forEach((k) => { + if (Number.isFinite(m[k]) && m[k] > mx[k]) mx[k] = m[k]; + }); + }); return ["Isc", "Voc", "Pmax", "FF"].map((label) => { const row = { metric: label }; const k = label.toLowerCase(); - ds.conditions.forEach((c) => { const m = allM[c]; if (m) row[c] = mx[k] > 0 ? (m[k] / mx[k]) * 100 : 0; }); + radarConds.forEach((c) => { + const m = allM[c]; + if (m) row[c] = mx[k] > 0 && Number.isFinite(m[k]) ? (m[k] / mx[k]) * 100 : 0; + }); return row; }); - }, [ds, allM]); + }, [ds, allM, radarConds]); const efficiency = useMemo(() => { if (!ds) return null; @@ -203,7 +231,10 @@ export default function App() { let any = false; ds.conditions.forEach((c) => { const m = allM[c]; - if (m) { const e = computeEfficiency(m.pmax, cellArea, irradiance); if (e != null) { r[c] = e; any = true; } } + if (m && m.quality !== "invalid" && ["measured", "interpolated"].includes(m.status?.pmax)) { + const e = computeEfficiency(m.pmax, cellArea, irradiance); + if (e != null) { r[c] = e; any = true; } + } }); return any ? r : null; }, [cellArea, irradiance, ds, allM]); @@ -234,39 +265,84 @@ export default function App() { setNavPill({ top: btnRect.top - wrapRect.top, height: btnRect.height }); }, [page, sideOpen, nav]); - const handleFile = useCallback((f) => { + const handleFile = useCallback(async (f) => { if (!f) return; setImportError(""); - f.arrayBuffer().then((buf) => { - const wb = XLSX.read(buf, { type: "array" }); + if (f.size > MAX_IMPORT_BYTES) { + setImportError(`"${f.name}" is larger than the 25 MB safety limit. Split very large workbooks into one sweep group per file.`); + return; + } + const extension = f.name.toLowerCase().split(".").pop(); + if (extension !== "xlsx" && extension !== "csv") { + setImportError(`Unsupported file type ".${extension || "unknown"}". Use .xlsx or .csv; legacy .xls is intentionally rejected.`); + return; + } + try { const sh = {}; - wb.SheetNames.forEach((n) => { sh[n] = XLSX.utils.sheet_to_json(wb.Sheets[n], { header: 1, defval: null }); }); + if (extension === "csv") { + sh.CSV = parseCsv(await f.text()); + } else { + const sheets = await readExcelFile(f); + sheets.forEach(({ sheet, data }) => { sh[sheet] = data; }); + } const valid = Object.keys(sh).filter((s) => { const d = extractIV(sh[s]); return d && d.conditions.length > 0; }); if (!valid.length) { setImportError("No valid I-V data found in \"" + f.name + "\". Solavin expects voltage (V) in column A and one current sweep (A) per following column, with a header row."); return; } - if (valid.length > 1) { setSheetPicker({ file: f, sheets: sh, valid }); return; } + if (valid.length > 1) { setSheetPicker({ fileName: f.name, sheets: sh, valid }); return; } doLoad(f.name, sh, valid[0]); - }).catch((e) => setImportError("Could not read \"" + f.name + "\": " + e.message)); + } catch (e) { + setImportError("Could not read \"" + f.name + "\": " + e.message); + } }, [datasets.length]); function doLoad(fn, sh, sn) { const parsed = extractIV(sh[sn]); if (!parsed) return; - setDatasets((prev) => prev.concat([{ name: fn.replace(/\.xlsx?$/i, "") + (sn ? " (" + sn + ")" : ""), conditions: parsed.conditions, ivData: parsed.ivData }])); + setDatasets((prev) => prev.concat([{ + name: fn.replace(/\.(xlsx|csv)$/i, "") + (sn ? " (" + sn + ")" : ""), + conditions: parsed.conditions, + ivData: parsed.ivData, + diagnostics: parsed.diagnostics, + source: { file: fn, sheet: sn }, + }])); setActiveDs(datasets.length); setSheetPicker(null); setPage("home"); } function doExportCSV() { - const csv = rowsToCsv(metricsToRows(ds, allM, efficiency)); - download(new Blob([csv], { type: "text/csv" }), "solavin_iv_metrics.csv"); + setExportError(""); + try { + const efficiencyInputs = { + cellAreaCm2: String(cellArea).trim() === "" ? NaN : Number(cellArea), + irradianceWm2: String(irradiance).trim() === "" ? NaN : Number(irradiance), + }; + const csv = rowsToCsv(metricsToRows(ds, allM, efficiency, efficiencyInputs)); + download(new Blob(["\ufeff", csv], { type: "text/csv;charset=utf-8" }), "solavin_iv_metrics.csv"); + } catch (error) { + setExportError("CSV export failed: " + error.message); + } } - function doExportXLSX() { - const rows = metricsToRows(ds, allM, efficiency); - const ws = XLSX.utils.aoa_to_sheet(rows); - const wb = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(wb, ws, "Metrics"); - XLSX.writeFile(wb, "solavin_iv_metrics.xlsx"); + async function doExportXLSX() { + setExportError(""); + try { + const efficiencyInputs = { + cellAreaCm2: String(cellArea).trim() === "" ? NaN : Number(cellArea), + irradianceWm2: String(irradiance).trim() === "" ? NaN : Number(irradiance), + }; + const toSheetData = (rows) => rows.map((row, rowIndex) => row.map((value) => ({ + value, + type: typeof value === "number" ? Number : String, + fontWeight: rowIndex === 0 ? "bold" : undefined, + backgroundColor: rowIndex === 0 ? "#E8F5F0" : undefined, + }))); + await writeExcelFile([ + { sheet: "Metrics", data: toSheetData(metricsToRows(ds, allM, efficiency, efficiencyInputs)) }, + { sheet: "Raw Data", data: toSheetData(rawDataToRows(ds)) }, + { sheet: "Metadata", data: toSheetData(metadataToRows(ds, efficiencyInputs)) }, + ]).toFile("solavin_iv_analysis.xlsx"); + } catch (error) { + setExportError("XLSX export failed: " + error.message); + } } function dismissTour() { @@ -304,11 +380,11 @@ export default function App() { {sideOpen &&
Solavin
-
v3.1.0
+
v4.0.0
} {sideOpen &&
- ONLINESMU-2400 + LOCALNO BACKEND
} {sideOpen &&
NAVIGATION
} @@ -329,7 +405,7 @@ export default function App() {
{/* Filled gradient treatment so the assistant reads as a primary feature in the rail, not a muted afterthought. */} - +
@@ -374,28 +450,43 @@ export default function App() {
SESSION ▸ {new Date().toISOString().slice(0, 10)}

Measurement Dashboard

-

{ds.name} · {ds.conditions.length} conditions · {(ds.ivData[ds.conditions[0]] || []).length} sweep points per channel

+

{ds.name} · {ds.conditions.length} conditions · {Math.min(...ds.conditions.map((c) => (ds.ivData[c] || []).length))}–{Math.max(...ds.conditions.map((c) => (ds.ivData[c] || []).length))} valid points/channel

-
METHOD
4-WIRE KELVIN
-
f.c + ": " + f.w).join("\n") : "All channels passed automated sanity checks"} style={{ padding: "6px 12px", borderRadius: 6, background: t.card, border: "1px solid " + t.border, cursor: qualityFlags.length ? "help" : "default" }}>
DATA QUALITY
{qualityFlags.length ? "⚠ " + qualityFlags.length + " FLAG" + (qualityFlags.length > 1 ? "S" : "") : <>CLEAN}
+
CONVENTION
GENERATOR · SIGNED I
+
f.c + ": " + f.w).join("\n") : "All automated checks passed"} style={{ padding: "6px 12px", borderRadius: 6, background: t.card, border: "1px solid " + t.border, cursor: qualityFlags.length ? "help" : "default" }}>
AUTOMATED CHECKS
f.severity === "error") ? t.danger : qualityFlags.length ? t.warn : t.success, fontWeight: 600, marginTop: 2, display: "flex", alignItems: "center", gap: 4 }}>{qualityFlags.length ? "⚠ " + qualityFlags.length + " FLAG" + (qualityFlags.length > 1 ? "S" : "") : <>PASSED}
{/* Best-of stat cards */}
{(() => { - const vals = Object.values(allM).filter(Boolean); - const bp = Math.max(0, ...vals.map((m) => m.pmax)); - const bi = Math.max(0, ...vals.map((m) => m.isc)); - const bv = Math.max(0, ...vals.map((m) => m.voc)); - const bf = Math.max(0, ...vals.map((m) => m.ff)); - const bestOf = (key) => { let bc = "", bw = -Infinity; ds.conditions.forEach((c) => { const m = allM[c]; if (m && m[key] > bw) { bw = m[key]; bc = c; } }); return bc; }; + const eligible = (m, key) => { + if (!m || m.quality === "invalid" || !Number.isFinite(m[key])) return false; + if (key === "voc") return m.status?.voc === "measured" || m.status?.voc === "interpolated"; + if (key === "pmax") return m.status?.pmax === "measured" || m.status?.pmax === "interpolated"; + if (key === "ff") return m.status?.ff === "computed"; + return true; + }; + const bestOf = (key) => { + let best = null; + ds.conditions.forEach((condition) => { + const metrics = allM[condition]; + if (eligible(metrics, key) && (!best || metrics[key] > best.value)) { + best = { condition, value: metrics[key] }; + } + }); + return best; + }; + const bp = bestOf("pmax"); + const bi = bestOf("isc"); + const bv = bestOf("voc"); + const bf = bestOf("ff"); return [ - { l: "P_max", sub: "Maximum power", v: bp / scales.p.div, d: 3, unit: pU, c: dark ? "#fbbf24" : "#b45309", IC: Ic.Up, best: bestOf("pmax") }, - { l: "I_sc", sub: "Short-circuit current", v: bi / scales.i.div, d: 3, unit: iU, c: dark ? "#22d3ee" : "#0891b2", IC: Ic.Act, best: bestOf("isc") }, - { l: "V_oc", sub: "Open-circuit voltage", v: bv, d: 3, unit: "V", c: dark ? "#38bdf8" : "#0284c7", IC: Ic.Target, best: bestOf("voc") }, - { l: "FF", sub: "Fill factor", v: bf * 100, d: 1, unit: "%", c: dark ? "#a78bfa" : "#7c3aed", IC: Ic.Rad, best: bestOf("ff") }, + { l: "P_max", sub: "Maximum power", v: bp ? bp.value / scales.p.div : NaN, d: 3, unit: pU, c: dark ? "#fbbf24" : "#b45309", IC: Ic.Up, best: bp?.condition }, + { l: "I_sc", sub: "Short-circuit current", v: bi ? bi.value / scales.i.div : NaN, d: 3, unit: iU, c: dark ? "#22d3ee" : "#0891b2", IC: Ic.Act, best: bi?.condition }, + { l: "V_oc", sub: "Open-circuit voltage", v: bv ? bv.value : NaN, d: 3, unit: "V", c: dark ? "#38bdf8" : "#0284c7", IC: Ic.Target, best: bv?.condition }, + { l: "FF", sub: "Fill factor", v: bf ? bf.value * 100 : NaN, d: 1, unit: "%", c: dark ? "#a78bfa" : "#7c3aed", IC: Ic.Rad, best: bf?.condition }, ].map((s, i) => (
@@ -411,7 +502,7 @@ export default function App() { {s.unit}
- {s.best} + {s.best || "No valid channel"}
)); @@ -434,8 +525,8 @@ export default function App() { } /> - {ds.conditions.slice(0, 4).map((c, ci) => )} - {ds.conditions.slice(0, 4).map((c, ci) => { const m = allM[c]; if (!m) return null; return ; })} + {ds.conditions.slice(0, 4).map((c, ci) => )} + {ds.conditions.slice(0, 4).map((c, ci) => { const m = allM[c]; if (!m || !["measured", "interpolated", "provisional"].includes(m.status?.pmax)) return null; return ; })}
@@ -475,12 +566,12 @@ export default function App() { } /> - {activeConds.map((c) => )} + {activeConds.map((c) => )} {activeConds.map((c) => { const m = allM[c]; if (!m) return null; return ; })} {/* Annotated extraction markers: MPP on the curve, Voc at I=0, Isc at V=0 */} - {activeConds.map((c) => { const m = allM[c]; if (!m) return null; const col = C8[ds.conditions.indexOf(c) % C8.length]; return ; })} - {activeConds.map((c) => { const m = allM[c]; if (!m || m.notes.vocBeyondRange) return null; const col = C8[ds.conditions.indexOf(c) % C8.length]; return ; })} - {activeConds.map((c) => { const m = allM[c]; if (!m) return null; const col = C8[ds.conditions.indexOf(c) % C8.length]; return ; })} + {activeConds.map((c) => { const m = allM[c]; if (!m || !["measured", "interpolated", "provisional"].includes(m.status?.pmax)) return null; const col = C8[ds.conditions.indexOf(c) % C8.length]; return ; })} + {activeConds.map((c) => { const m = allM[c]; if (!m || m.voc == null || m.notes.vocBeyondRange) return null; const col = C8[ds.conditions.indexOf(c) % C8.length]; return ; })} + {activeConds.map((c) => { const m = allM[c]; if (!m || !Number.isFinite(m.isc)) return null; const col = C8[ds.conditions.indexOf(c) % C8.length]; return ; })} {lookupV != null && } @@ -496,8 +587,8 @@ export default function App() { {C8.map((col, i) => )} +Number(v).toFixed(2)} tick={{ fill: t.textM, fontSize: 9 }} label={{ value: "POWER (" + pU + ")", angle: -90, position: "insideLeft", offset: 0, fill: t.textD, fontSize: 8, letterSpacing: "0.18em", style: { textAnchor: "middle" } }} stroke={t.borderS} />} /> - {activeConds.map((c) => { const ci = ds.conditions.indexOf(c); const col = C8[ci % C8.length]; return ; })} - {activeConds.map((c) => { const m = allM[c]; if (!m) return null; return ; })} + {activeConds.map((c) => { const ci = ds.conditions.indexOf(c); const col = C8[ci % C8.length]; return ; })} + {activeConds.map((c) => { const m = allM[c]; if (!m || !["measured", "interpolated", "provisional"].includes(m.status?.pmax)) return null; return ; })} {lookupV != null && } } @@ -515,8 +606,8 @@ export default function App() { } /> - {activeConds.map((c) => )} - {activeConds.map((c) => { const m = allM[c]; if (!m) return null; return ; })} + {activeConds.map((c) => )} + {activeConds.map((c) => { const m = allM[c]; if (!m || !["measured", "interpolated", "provisional"].includes(m.status?.pmax)) return null; return ; })} {C8.map((col, i) => )} @@ -525,8 +616,8 @@ export default function App() { +Number(v).toFixed(2)} tick={{ fill: t.textM, fontSize: 9 }} label={{ value: "P (" + pU + ")", angle: -90, position: "insideLeft", offset: 8, fill: t.textD, fontSize: 8, letterSpacing: "0.14em", style: { textAnchor: "middle" } }} stroke={t.borderS} /> } /> - {activeConds.map((c) => { const ci = ds.conditions.indexOf(c); const col = C8[ci % C8.length]; return ; })} - {activeConds.map((c) => { const m = allM[c]; if (!m) return null; return ; })} + {activeConds.map((c) => { const ci = ds.conditions.indexOf(c); const col = C8[ci % C8.length]; return ; })} + {activeConds.map((c) => { const m = allM[c]; if (!m || !["measured", "interpolated", "provisional"].includes(m.status?.pmax)) return null; return ; })} } @@ -534,19 +625,26 @@ export default function App() {
Performance ProfileNORMALIZED
-
0 → 100% RELATIVE
+
{radarConds.length} COMPLETE CHANNEL{radarConds.length === 1 ? "" : "S"} · 0 → 100% RELATIVE
- {ds.conditions.map((c, ci) => )}
} + {radarConds.length > 0 + ? {radarConds.map((c) => { const ci = ds.conditions.indexOf(c); return ; })} + :
No active channel has complete reportable Isc, Voc, Pmax, and FF values.
Review sweep coverage and data-quality flags.
} + } {vizTab === "compare" && (() => { - const bd = activeConds.map((c) => { const m = allM[c]; if (!m) return null; return { name: c, Pmax: m.pmax * 1e9, color: C8[ds.conditions.indexOf(c) % C8.length] }; }).filter(Boolean).sort((a, b) => b.Pmax - a.Pmax); + const bd = activeConds.map((c) => { + const m = allM[c]; + if (!m || m.quality === "invalid" || !["measured", "interpolated"].includes(m.status?.pmax)) return null; + return { name: c, Pmax: m.pmax / scales.p.div, color: C8[ds.conditions.indexOf(c) % C8.length] }; + }).filter(Boolean).sort((a, b) => b.Pmax - a.Pmax); return
P_max Comparison
-
RANKED · DESC
+
VALID CHANNELS · DESC
- {bd.map((e, i) => )} + {bd.map((e, i) => )}
; })()}
} @@ -565,10 +663,11 @@ export default function App() { {efficiency && Object.values(efficiency).some((e) => e > 100) &&
η exceeds 100 %, which is unphysical — check that cell area is in cm² and irradiance in W/m².
} + {exportError &&
{exportError}
} {qualityFlags.length > 0 &&
⚠ DATA QUALITY · {qualityFlags.length} FLAG{qualityFlags.length > 1 ? "S" : ""}
- {qualityFlags.map((f, i) =>
{f.c}{f.w}
)} + {qualityFlags.map((f, i) =>
{f.severity === "error" ? "ERROR · " : ""}{f.c}{f.w}
)}
}
@@ -581,19 +680,19 @@ export default function App() { {["CONDITION", "I_SC (" + iU + ")", "V_OC (V)", "P_MAX (" + pU + ")", "FF (%)", "R_S", "R_SH"].concat(efficiency ? ["η (%)"] : []).map((h) => {h})} {ds.conditions.map((c, ci) => { const m = allM[c]; if (!m) return null; const col = C8[ci % C8.length]; const colTxt = (t.chan || C8)[ci % (t.chan || C8).length]; return ( - {c}{m.warnings.length > 0 && } - {(m.isc / scales.i.div).toFixed(3)} - {m.voc.toFixed(3)}{m.notes.vocBeyondRange ? "*" : ""} - {(m.pmax / scales.p.div).toFixed(3)} -
0.5 ? "linear-gradient(90deg,#16a34a,#22c55e)" : m.ff > 0.3 ? "linear-gradient(90deg,#d97706,#f59e0b)" : "linear-gradient(90deg,#dc2626,#f43f5e)", transition: "width .6s ease" }} />
{(m.ff * 100).toFixed(1)}
- {fmtSI(m.rs, "Ω", 2)} - {m.rsh === Infinity ? "∞" : fmtSI(m.rsh, "Ω", 2)} + {c}{[...(m.errors || []), ...(m.warnings || [])].length > 0 && } + {Number.isFinite(m.isc) ? (m.isc / scales.i.div).toFixed(3) : "—"} + {m.voc == null ? "—" : (m.notes.vocBeyondRange ? "≥" : "") + m.voc.toFixed(3)} + {["measured", "interpolated", "provisional"].includes(m.status?.pmax) ? (m.pmax / scales.p.div).toFixed(3) + (m.status?.pmax === "provisional" ? "*" : "") : "—"} + {m.ff == null ? :
0.5 ? "linear-gradient(90deg,#16a34a,#22c55e)" : m.ff > 0.3 ? "linear-gradient(90deg,#d97706,#f59e0b)" : "linear-gradient(90deg,#dc2626,#f43f5e)", transition: "width .6s ease" }} />
{(m.ff * 100).toFixed(1)}
} + {m.rs == null ? "—" : fmtSI(m.rs, "Ω", 2)} + {m.rsh == null ? "—" : m.rsh === Infinity ? "∞" : fmtSI(m.rsh, "Ω", 2)} {efficiency && {efficiency[c] != null ? efficiency[c].toPrecision(3) : "-"}} ); })}
- {Object.values(allM).some((m) => m && m.notes.vocBeyondRange) &&
* V_oc lies beyond the measured sweep range — value shown is the last sweep voltage.
} + {Object.values(allM).some((m) => m && (m.notes.vocBeyondRange || m.status?.pmax === "provisional")) &&
* Sweep does not bracket V_oc: V_oc is a lower bound, P_max is provisional, and FF/R_s are withheld.
}
} @@ -602,17 +701,17 @@ export default function App() {
04 / DATA INGEST

Import Measurement Data

-

Drop a workbook with voltage in column A and current sweeps in subsequent columns. Sheet headers become channel labels.

+

Drop an .xlsx workbook or RFC 4180 CSV with voltage in column A and signed current sweeps in subsequent columns. Sheet headers become channel labels.

{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={(e) => { e.preventDefault(); setDragOver(false); if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]); }} onClick={() => { if (fileRef.current) fileRef.current.click(); }} className="corners" style={{ border: "1.5px dashed " + (dragOver ? t.accent : t.border), borderRadius: 14, padding: "50px 28px", textAlign: "center", background: dragOver ? "linear-gradient(135deg," + t.accent + "14," + t.accent2 + "08)" : "linear-gradient(180deg," + t.card + "," + t.cardAlt + ")", maxWidth: 560, margin: "0 auto", transition: "all .25s", position: "relative", overflow: "hidden", cursor: "pointer" }}>
-
{dragOver ? "Release to upload" : "Drop Excel or CSV here"}
-
or click to browse · supports .xlsx, .xls, .csv
+
{dragOver ? "Release to upload" : "Drop XLSX or CSV here"}
+
or click to browse · .xlsx / .csv · 25 MB maximum
Browse Files
- { if (e.target.files && e.target.files[0]) handleFile(e.target.files[0]); }} /> + { if (e.target.files && e.target.files[0]) handleFile(e.target.files[0]); e.target.value = ""; }} />
- FORMAT · COL A = V, COL B+ = I·UNITS · VOLTS / AMPS + FORMAT · COL A = V, COL B+ = SIGNED I·UNITS · VOLTS / AMPS
{importError &&
{importError}
} @@ -636,10 +735,10 @@ export default function App() { {page === "about" && } - {/* Instrument status strip — the quiet Solavin signature. */} + {/* Application status strip. */}
- SOLAVIN · PRECISION PV CHARACTERIZATION - ISC · VOC · PMAX · FF · η — v3.1.0 + SOLAVIN · RESEARCH PV ANALYSIS + ISC · VOC · PMAX · FF · η — v4.0.0
@@ -649,7 +748,7 @@ export default function App() { {tourStep >= 0 && setTourStep(tourStep + 1)} onBack={() => { if (tourStep > 0) setTourStep(tourStep - 1); }} onSkip={dismissTour} onNav={setPage} onVizTab={setVizTab} onChat={() => setChatOpen(true)} onCloseChat={() => setChatOpen(false)} />} - {sheetPicker &&
Select Sheet
{sheetPicker.valid.map((s) => )}
} + {sheetPicker &&
Select Sheet
{sheetPicker.valid.map((s) => )}
}
); } diff --git a/src/components/AboutPage.jsx b/src/components/AboutPage.jsx index 0d37aba..4ec4771 100644 --- a/src/components/AboutPage.jsx +++ b/src/components/AboutPage.jsx @@ -14,28 +14,29 @@ export function AboutPage(p) {

How It Works

Solavin processes current-voltage data from photovoltaic cells to extract standard figures of merit for research characterization.

-

1. Data ingestion — upload .xlsx/.csv with voltage in column A and one current sweep per following column.

+

1. Data ingestion — upload .xlsx/.csv with voltage in column A and one signed-current sweep per following column. Blank cells are skipped and reported, never replaced with zeros.

2. Parameter extraction — Isc (current at V=0), Voc (the I=0 crossing), Pmax/Vmp/Imp in the power quadrant, FF = Pmax/(Isc·Voc), and slope-based Rs & Rsh.

3. Efficiency — cell area and irradiance inputs give η = Pmax/(G·A).

4. Visualization — I-V, P-V, radar, and comparative charts with MPP overlays and zoom.

-

5. Export — one-click CSV/Excel export of all metrics.

+

5. Export — CSV metrics plus a three-sheet Excel workbook containing metrics, signed raw data, and analysis metadata/provenance.

Accuracy & conventions

-

Extraction operates on signed current (generator convention): positive in the power quadrant, negative beyond Voc. The maximum-power search is restricted to V≥0, I≥0, so fill factor is always physical (< 100%).

-

Isc and Voc are linearly interpolated between the two samples that bracket V = 0 and I = 0 — never nearest-neighbour. The maximum-power point is located on the piecewise-linear P-V curve itself, including between measured samples, so Pmax does not snap to the voltage grid. Automated sanity checks flag non-positive Isc, non-monotonic sweeps and sparse sampling.

-

The core math is validated against single-diode reference silicon cells (FF ≈ 0.83 at STC) by an automated test suite that runs on every build, so results stay reproducible.

+

Extraction operates on signed current (generator convention): positive in the power quadrant, negative beyond Voc. Plots and calculations both use piecewise-linear interpolation, so smoothing cannot invent unmeasured extrema.

+

Isc and Voc are linearly interpolated between samples that bracket V = 0 and I = 0. The maximum-power point is solved exactly on each piecewise-linear segment. If the sweep does not bracket Voc, Solavin reports only a lower bound, marks Pmax provisional, and withholds FF and Rs rather than manufacturing precise-looking values.

+

The core is tested against analytic linear curves, independent high-resolution single-diode oracles, scale invariants, sparse workbooks, malformed input, and deterministic randomized sweeps. Every export includes status and quality columns so caveats travel with the numbers.

+

Solavin validates analysis, not the measurement setup. Calibration, spectral mismatch, device temperature, irradiance uniformity, sweep direction/rate, and measurement uncertainty remain the operator's responsibility. Rs and Rsh are explicitly local slope estimates from one light sweep.

Technology

Frontend: React 18 + Vite
-
Charts: Recharts
-
Excel: SheetJS
-
Tests: Vitest
+
Charts: Recharts · linear interpolation
+
Excel: isolated XLSX reader/writer
+
Tests: Vitest + Playwright
diff --git a/src/components/ProfileGate.jsx b/src/components/ProfileGate.jsx index a22c23b..0567e13 100644 --- a/src/components/ProfileGate.jsx +++ b/src/components/ProfileGate.jsx @@ -49,7 +49,7 @@ export function ProfileGate(props) {
- +
{/* Fixed top nav (above the hero layer so its buttons stay clickable) */} @@ -59,7 +59,7 @@ export function ProfileGate(props) { Solavin
- GitHub ↗ + GitHub ↗
@@ -71,7 +71,7 @@ export function ProfileGate(props) {
- v3 · ACCURACY-VERIFIED EXTRACTION + v4 · STATUS-AWARE ANALYSIS

Photovoltaic I-V data,
@@ -107,12 +107,12 @@ export function ProfileGate(props) {
SAMPLE · ROOM LIGHTS

- ● LIVE + ● SAMPLE
- + @@ -127,9 +127,9 @@ export function ProfileGate(props) { - {/* bottom instrument strip */} + {/* bottom application-status strip */}
- SMU-2400 · CALIBRATED + LOCAL ANALYSIS · NO INSTRUMENT LINK Solavin · PV CHARACTERIZATION SUITE
diff --git a/src/components/Welcome.jsx b/src/components/Welcome.jsx index 2fe7345..3c1c121 100644 --- a/src/components/Welcome.jsx +++ b/src/components/Welcome.jsx @@ -118,7 +118,7 @@ export function Welcome({ t, onClose, onStartTour }) {
WHAT YOU PROVIDE
- An .xlsx, .xls or .csv workbook — one voltage column, any number of current sweeps. Header row becomes channel labels. + An .xlsx or .csv workbook — one voltage column, any number of signed-current sweeps. Header row becomes channel labels; legacy .xls is intentionally rejected.
{["COL", "CONTENT", "UNIT"].map((h) => )} diff --git a/src/components/shared.jsx b/src/components/shared.jsx index 451ecc5..b0a7b57 100644 --- a/src/components/shared.jsx +++ b/src/components/shared.jsx @@ -1,11 +1,12 @@ import { useState, useEffect, useRef } from "react"; import { C8 } from "../theme.js"; import { Ic } from "../icons.jsx"; +import { buildAlignedRows } from "../lib/ivAnalysis.js"; /** * Renders a number that tweens to a new value instead of snapping — used on - * the dashboard stat cards so switching datasets reads as a live instrument - * reading rather than a re-render. Skips the animation under + * the dashboard stat cards so switching datasets has a smooth visual + * transition. Skips the animation under * prefers-reduced-motion, matching the rest of the app's motion tokens * (420ms, cubic ease-out — see --dur-slow / --ease-fluid in styles.css). */ @@ -86,8 +87,9 @@ export function Toggles(p) { /** * Voltage cursor lookup → current per channel at the requested voltage. * Linearly interpolates between the two samples that bracket the target - * (the same convention used for Isc/Voc extraction); clamps to the nearest - * endpoint when the target lies outside the measured range, and says so. + * (the same convention used for Isc/Voc extraction). Values outside a + * channel's measured range are unavailable, never extrapolated. Duplicate + * setpoints use the same explicit average as charts and metrics. * `iSc` is the dataset-wide SI scale ({div, prefix}) so units match the charts. */ export function VLookup(p) { @@ -100,15 +102,21 @@ export function VLookup(p) { if (isNaN(v)) { setResult(null); return; } const r = []; p.ds.conditions.forEach((c) => { - const pts = [...p.ds.ivData[c]].sort((a, b) => a.voltage - b.voltage); + const pts = buildAlignedRows(p.ds, [c]).map((row) => ({ + voltage: row.voltage, + rawCurrent: row[c], + })); if (!pts.length) return; let current, source; - if (v <= pts[0].voltage) { + if (v < pts[0].voltage || v > pts[pts.length - 1].voltage) { + current = null; + source = "out of range"; + } else if (v === pts[0].voltage) { current = pts[0].rawCurrent; - source = v === pts[0].voltage ? "measured" : "out of range"; - } else if (v >= pts[pts.length - 1].voltage) { + source = "measured"; + } else if (v === pts[pts.length - 1].voltage) { current = pts[pts.length - 1].rawCurrent; - source = v === pts[pts.length - 1].voltage ? "measured" : "out of range"; + source = "measured"; } else { let j = 0; while (j < pts.length - 1 && pts[j + 1].voltage < v) j++; @@ -140,7 +148,7 @@ export function VLookup(p) { {result.map((r, i) => ( - + ))} @@ -174,7 +182,7 @@ export function RawDataViewer(p) { if (!ds) return null; const allCols = ["voltage"].concat(ds.conditions); - const rows = ds.ivData[ds.conditions[0]] || []; + const rows = buildAlignedRows(ds, ds.conditions); function doSearch() { setWarning(""); setMatchIdx(-1); @@ -192,14 +200,19 @@ export function RawDataViewer(p) { setMatchIdx(bestI); } else { if (isNaN(num)) { setWarning("Enter a numeric current value (in Amps, e.g. 1.5e-6)."); return; } - const pts = ds.ivData[searchCol]; - if (!pts || pts.length === 0) { setWarning('No data for condition "' + searchCol + '".'); return; } - let bestI = 0, bestD = Math.abs(pts[0].rawCurrent - num); - for (let i = 1; i < pts.length; i++) { const d = Math.abs(pts[i].rawCurrent - num); if (d < bestD) { bestD = d; bestI = i; } } - const relErr = pts[bestI].rawCurrent !== 0 ? bestD / Math.abs(pts[bestI].rawCurrent) : bestD; - if (relErr > 0.1) setWarning("No close match found. Nearest is " + pts[bestI].rawCurrent.toExponential(3) + " A at V = " + pts[bestI].voltage.toFixed(3) + "V."); - else if (bestD > 0) setWarning("Nearest match: " + pts[bestI].rawCurrent.toExponential(3) + " A at V = " + pts[bestI].voltage.toFixed(3) + "V."); - setMatchIdx(bestI); + const candidates = rows + .map((row, index) => ({ index, voltage: row.voltage, current: row[searchCol] })) + .filter((entry) => Number.isFinite(entry.current)); + if (!candidates.length) { setWarning('No data for condition "' + searchCol + '".'); return; } + let best = candidates[0], bestD = Math.abs(candidates[0].current - num); + for (let i = 1; i < candidates.length; i++) { + const d = Math.abs(candidates[i].current - num); + if (d < bestD) { bestD = d; best = candidates[i]; } + } + const relErr = best.current !== 0 ? bestD / Math.abs(best.current) : bestD; + if (relErr > 0.1) setWarning("No close match found. Nearest is " + best.current.toExponential(3) + " A at V = " + best.voltage.toFixed(3) + "V."); + else if (bestD > 0) setWarning("Nearest match: " + best.current.toExponential(3) + " A at V = " + best.voltage.toFixed(3) + "V."); + setMatchIdx(best.index); } } const isErr = (w) => w.indexOf("outside") !== -1 || w.indexOf("No close") !== -1 || w.indexOf("not found") !== -1; @@ -241,7 +254,7 @@ export function RawDataViewer(p) { out.push( - {ds.conditions.map((c, ci) => { const pt = ds.ivData[c][ri]; return ; })} + {ds.conditions.map((c, ci) => { const value = row[c]; return ; })} ); } diff --git a/src/lib/assistant.js b/src/lib/assistant.js index 7f87e99..4b17259 100644 --- a/src/lib/assistant.js +++ b/src/lib/assistant.js @@ -14,34 +14,43 @@ import { fmtSI } from "./ivAnalysis.js"; // cell scale (µA lab devices through A-scale production cells). const fmt = { isc: (m) => fmtSI(m.isc, "A"), - voc: (m) => m.voc.toFixed(3) + " V", - pmax: (m) => fmtSI(m.pmax, "W"), - ff: (m) => (m.ff * 100).toFixed(1) + " %", - rs: (m) => fmtSI(m.rs, "Ω", 2), - rsh: (m) => (m.rsh === Infinity ? "∞" : fmtSI(m.rsh, "Ω", 2)), + voc: (m) => m.voc == null ? "unavailable" : `${m.status?.voc === "lower-bound" ? "≥ " : ""}${m.voc.toFixed(3)} V`, + pmax: (m) => `${fmtSI(m.pmax, "W")}${m.status?.pmax === "provisional" ? " (provisional)" : ""}`, + ff: (m) => Number.isFinite(m.ff) ? (m.ff * 100).toFixed(1) + " %" : "withheld", + rs: (m) => m.rs == null ? "withheld" : fmtSI(m.rs, "Ω", 2), + rsh: (m) => m.rsh == null ? "withheld" : (m.rsh === Infinity ? "∞" : fmtSI(m.rsh, "Ω", 2)), }; +function reportable(m, key) { + if (!m || m.quality === "invalid" || !Number.isFinite(m[key])) return false; + if (key === "voc") return m.status?.voc === "measured" || m.status?.voc === "interpolated"; + if (key === "pmax") return m.status?.pmax === "measured" || m.status?.pmax === "interpolated"; + if (key === "ff") return m.status?.ff === "computed"; + if (key === "isc") return m.status?.isc !== "unavailable"; + return true; +} + function rankBy(dataset, allMetrics, key) { return dataset.conditions .map((c) => ({ c, m: allMetrics[c] })) - .filter((x) => x.m) + .filter((x) => reportable(x.m, key)) .sort((a, b) => b.m[key] - a.m[key]); } function summarize(dataset, allMetrics) { + const lines = dataset.conditions.map((c) => { + const m = allMetrics[c]; + if (!m) return `• ${c}: insufficient valid samples`; + if (m.quality === "invalid") + return `• ${c} [INVALID]: metrics are not reportable; inspect the channel's data-quality errors`; + const flag = m.quality === "pass" ? "" : ` [${m.quality.toUpperCase()}]`; + return `• ${c}${flag}: Pmax ${fmt.pmax(m)}, Isc ${fmt.isc(m)}, Voc ${fmt.voc(m)}, FF ${fmt.ff(m)}`; + }); const ranked = rankBy(dataset, allMetrics, "pmax"); - if (!ranked.length) return "No metrics available for the active dataset."; - const best = ranked[0]; - const worst = ranked[ranked.length - 1]; - const lines = ranked.map( - ({ c, m }) => `• ${c}: Pmax ${fmt.pmax(m)}, Isc ${fmt.isc(m)}, Voc ${fmt.voc(m)}, FF ${fmt.ff(m)}` - ); - return ( - `Summary — ${dataset.name}\n\n` + - lines.join("\n") + - `\n\nHighest Pmax: ${best.c} (${fmt.pmax(best.m)}).` + - `\nLowest Pmax: ${worst.c} (${fmt.pmax(worst.m)}).` - ); + const ranking = ranked.length + ? `\n\nHighest reportable Pmax: ${ranked[0].c} (${fmt.pmax(ranked[0].m)}).` + : "\n\nNo channel has a reportable Pmax; review the data-quality flags."; + return `Summary — ${dataset.name}\n\n${lines.join("\n")}${ranking}`; } function compare(dataset, allMetrics) { @@ -57,7 +66,7 @@ function compare(dataset, allMetrics) { function fillFactor(dataset, allMetrics) { const ranked = rankBy(dataset, allMetrics, "ff"); - if (!ranked.length) return "No fill-factor data available."; + if (!ranked.length) return "No reportable fill-factor values are available. Solavin withholds FF when Voc is not bracketed or a channel fails physical checks."; const vals = ranked.map((x) => x.m.ff * 100); const lo = Math.min(...vals).toFixed(1); const hi = Math.max(...vals).toFixed(1); @@ -66,16 +75,19 @@ function fillFactor(dataset, allMetrics) { `Fill factor measures how "square" the I-V curve is:\n` + `FF = Pmax / (Isc · Voc).\n\n` + `This dataset ranges ${lo}–${hi} %. Best: ${best.c} at ${fmt.ff(best.m)}.\n\n` + - `Values in this range are typical for cells measured below standard ` + - `illumination and/or with appreciable series resistance — both push the ` + - `knee of the curve inward. Crystalline silicon at STC typically reaches ` + - `70–82 %.` + `Interpret FF in the context of device technology, illumination, temperature, ` + + `sweep protocol, and uncertainty. High series resistance, low shunt resistance, ` + + `recombination, or measurement artefacts can all reduce FF; one light sweep ` + + `cannot identify the cause by itself.` ); } function resistances(dataset, allMetrics) { const lines = dataset.conditions - .map((c) => allMetrics[c] && `• ${c}: Rs ≈ ${fmt.rs(allMetrics[c])}, Rsh ≈ ${fmt.rsh(allMetrics[c])}`) + .map((c) => { + const m = allMetrics[c]; + return m && m.quality !== "invalid" && `• ${c}: Rs ≈ ${fmt.rs(m)}, Rsh ≈ ${fmt.rsh(m)}`; + }) .filter(Boolean); return ( `Series (Rs) and shunt (Rsh) resistance estimates, from the I-V slope near ` + @@ -90,7 +102,7 @@ function resistances(dataset, allMetrics) { function nextSteps() { return ( "Suggested next experiments:\n" + - "1. Measure under calibrated AM1.5G (1000 W/m²) to report STC efficiency.\n" + + "1. If reporting STC performance, use a calibrated AM1.5G-equivalent setup at 1000 W/m² and control device temperature.\n" + "2. Run a temperature sweep to extract dV_oc/dT and dI_sc/dT coefficients.\n" + "3. Acquire a dark I-V curve to separate Rs, Rsh and the diode ideality factor.\n" + "4. Repeat sweeps to quantify measurement repeatability (error bars).\n" + @@ -128,10 +140,12 @@ export function analyze(message, ctx) { if (/voc|open.?circuit/.test(l)) { const r = rankBy(dataset, allMetrics, "voc")[0]; + if (!r) return "No reportable Voc is available. The sweep must include a positive-to-negative current crossing at non-negative voltage."; return `Voc is the voltage at zero current (the I-V curve's x-intercept). Highest here: ${r.c} at ${fmt.voc(r.m)}.`; } if (/isc|short.?circuit/.test(l)) { const r = rankBy(dataset, allMetrics, "isc")[0]; + if (!r) return "No reportable Isc is available. Check the current sign convention and whether the sweep reaches or brackets V = 0."; return `Isc is the current at zero volts. Highest here: ${r.c} at ${fmt.isc(r.m)}.`; } diff --git a/src/lib/assistant.test.js b/src/lib/assistant.test.js new file mode 100644 index 0000000..7987e23 --- /dev/null +++ b/src/lib/assistant.test.js @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { analyze } from "./assistant.js"; +import { calcMetrics } from "./ivAnalysis.js"; + +const dataset = { + name: "Assistant fixture", + conditions: ["Validated", "Censored", "Invalid"], + ivData: { + Validated: [ + { voltage: 0, rawCurrent: 1 }, + { voltage: 0.5, rawCurrent: 0.5 }, + { voltage: 1, rawCurrent: 0 }, + { voltage: 1.1, rawCurrent: -0.1 }, + ], + Censored: [ + { voltage: 0, rawCurrent: 1 }, + { voltage: 0.5, rawCurrent: 0.8 }, + { voltage: 1, rawCurrent: 0.6 }, + ], + Invalid: [ + { voltage: 0, rawCurrent: -1 }, + { voltage: 0.5, rawCurrent: -0.5 }, + { voltage: 1, rawCurrent: 0 }, + ], + }, +}; + +const allMetrics = Object.fromEntries( + dataset.conditions.map((condition) => [condition, calcMetrics(dataset.ivData[condition])]) +); + +describe("deterministic assistant validity handling", () => { + it("summarizes bounds and withheld values without presenting them as exact", () => { + const answer = analyze("summarize", { dataset, allMetrics, efficiency: null }); + expect(answer).toMatch(/Censored \[REVIEW\].*Voc ≥ 1\.000 V, FF withheld/s); + expect(answer).toMatch(/Invalid \[INVALID\]: metrics are not reportable/); + expect(answer).toMatch(/Highest reportable Pmax: Validated/); + expect(answer).not.toMatch(/Highest reportable Pmax: Censored/); + }); + + it("ranks only reportable Pmax values", () => { + const answer = analyze("compare", { dataset, allMetrics, efficiency: null }); + expect(answer).toContain("Validated"); + expect(answer).not.toContain("Censored"); + expect(answer).not.toContain("Invalid"); + }); + + it("withholds invalid or censored fill factor values", () => { + const answer = analyze("fill factor", { dataset, allMetrics, efficiency: null }); + expect(answer).toContain("Best: Validated"); + expect(answer).toContain("one light sweep cannot identify the cause"); + }); + + it("does not report an unbracketed Voc as the highest reportable value", () => { + const answer = analyze("Voc", { dataset, allMetrics, efficiency: null }); + expect(answer).toContain("Highest here: Validated"); + expect(answer).not.toContain("Censored"); + }); +}); diff --git a/src/lib/ivAnalysis.js b/src/lib/ivAnalysis.js index 7409b38..e128e52 100644 --- a/src/lib/ivAnalysis.js +++ b/src/lib/ivAnalysis.js @@ -22,14 +22,19 @@ * - current : |rawCurrent|, kept only for legacy display helpers */ +export const ANALYSIS_VERSION = "4.0.0"; +export const ANALYSIS_METHOD = "piecewise-linear-v4"; +const MAX_WORKSHEET_ROWS = 250000; +const MAX_WORKSHEET_COLUMNS = 2048; + /** Linear interpolation of y at target x between (x0,y0) and (x1,y1). */ function lerp(x, x0, y0, x1, y1) { if (x1 === x0) return y0; return y0 + ((x - x0) * (y1 - y0)) / (x1 - x0); } -/** Least-squares slope of y(x); null when degenerate (fewer than 2 points or all x equal). */ -function lsqSlope(xs, ys) { +/** Least-squares line fit of y(x); null when degenerate. */ +function linearFit(xs, ys) { const n = xs.length; if (n < 2) return null; let sx = 0, sy = 0, sxx = 0, sxy = 0; @@ -37,8 +42,27 @@ function lsqSlope(xs, ys) { sx += xs[k]; sy += ys[k]; sxx += xs[k] * xs[k]; sxy += xs[k] * ys[k]; } const den = n * sxx - sx * sx; - if (den === 0) return null; - return (n * sxy - sx * sy) / den; + if (!Number.isFinite(den) || den === 0) return null; + const slope = (n * sxy - sx * sy) / den; + const intercept = (sy - slope * sx) / n; + if (!Number.isFinite(slope) || !Number.isFinite(intercept)) return null; + const mean = sy / n; + let ssRes = 0, ssTot = 0; + for (let k = 0; k < n; k++) { + ssRes += (ys[k] - (intercept + slope * xs[k])) ** 2; + ssTot += (ys[k] - mean) ** 2; + } + const r2 = ssTot === 0 ? 1 : 1 - ssRes / ssTot; + return { slope, intercept, r2, points: n, rmse: Math.sqrt(ssRes / n) }; +} + +function finiteNumber(value) { + if (typeof value === "number") return Number.isFinite(value) ? value : NaN; + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : NaN; + } + return NaN; } /** @@ -47,8 +71,75 @@ function lsqSlope(xs, ys) { */ function signedCurrent(pt) { if (pt == null) return NaN; - if (typeof pt.rawCurrent === "number") return pt.rawCurrent; - return Number(pt.current); + if (pt.rawCurrent != null) return finiteNumber(pt.rawCurrent); + return finiteNumber(pt.current); +} + +/** + * Clean a sweep and average repeated samples at the exact same voltage. + * Repeated setpoints are common in exported instrument data; averaging makes + * extraction deterministic while the returned duplicate count ensures the + * operation is never silent. + */ +function normalizeSweep(pts) { + const raw = pts + .map((p) => ({ v: finiteNumber(p && p.voltage), i: signedCurrent(p) })) + .filter((p) => Number.isFinite(p.v) && Number.isFinite(p.i)) + .sort((a, b) => a.v - b.v); + const clean = []; + for (const point of raw) { + const last = clean[clean.length - 1]; + if (last && last.v === point.v) { + last.sum += point.i; + last.count++; + last.i = last.sum / last.count; + } else { + clean.push({ v: point.v, i: point.i, sum: point.i, count: 1 }); + } + } + return { + clean: clean.map(({ v, i }) => ({ v, i })), + rawCount: raw.length, + duplicateCount: raw.length - clean.length, + }; +} + +function medianPositiveStep(values) { + const steps = []; + for (let k = 1; k < values.length; k++) { + const step = values[k] - values[k - 1]; + if (step > 0) steps.push(step); + } + if (!steps.length) return 0; + steps.sort((a, b) => a - b); + const mid = Math.floor(steps.length / 2); + return steps.length % 2 ? steps[mid] : (steps[mid - 1] + steps[mid]) / 2; +} + +/** + * Fit I(V) locally around a target voltage. At most seven nearest samples are + * used so distant reverse-bias or knee data cannot dominate an endpoint slope. + */ +function localCurrentFit(v, i, target, characteristicSpan) { + const step = medianPositiveStep(v); + const radius = Math.max(characteristicSpan, 3 * step); + let indices = v + .map((value, index) => ({ index, distance: Math.abs(value - target) })) + .filter((entry) => entry.distance <= radius + Number.EPSILON) + .sort((a, b) => a.distance - b.distance); + if (indices.length < 3) { + indices = v + .map((value, index) => ({ index, distance: Math.abs(value - target) })) + .sort((a, b) => a.distance - b.distance) + .slice(0, 3); + } else { + indices = indices.slice(0, 7); + } + indices.sort((a, b) => v[a.index] - v[b.index]); + const xs = indices.map((entry) => v[entry.index]); + const ys = indices.map((entry) => i[entry.index]); + const result = linearFit(xs, ys); + return result ? { ...result, voltageSpan: xs[xs.length - 1] - xs[0] } : null; } /** @@ -57,38 +148,51 @@ function signedCurrent(pt) { * @param {Array<{voltage:number, current?:number, rawCurrent?:number}>} pts * Measured points. Order does not matter — points are sorted by voltage. * @returns {null | { - * isc:number, voc:number, pmax:number, vmp:number, imp:number, - * ff:number, rs:number, rsh:number, crossIndex:number, - * notes:{ iscExtrapolated:boolean, vocBeyondRange:boolean }, - * warnings:string[] - * }} All electrical quantities in SI base units (V, A, W, Ω). `ff` is a - * fraction in [0,1]. `warnings` lists human-readable data-quality flags - * (empty for a clean sweep). Returns null when fewer than 3 valid points - * are given. + * isc:number, voc:number|null, pmax:number, vmp:number, imp:number, + * ff:number|null, rs:number|null, rsh:number|null, crossIndex:number, + * notes:{ iscExtrapolated:boolean, vocBeyondRange:boolean, vocAmbiguous:boolean, duplicateVoltages:number }, + * status:Record, warnings:string[], errors:string[], + * quality:"pass"|"review"|"invalid" + * }} Electrical quantities use SI base units (V, A, W, Ω). `ff` is a + * dimensionless fraction when identifiable. Status fields distinguish + * measured, interpolated, extrapolated, provisional, and withheld results. + * Returns null when fewer than 3 finite, unique-voltage samples remain. */ export function calcMetrics(pts) { if (!Array.isArray(pts) || pts.length < 3) return null; - // Build clean, voltage-sorted arrays of finite samples. - const clean = pts - .map((p) => ({ v: Number(p.voltage), i: signedCurrent(p) })) - .filter((p) => Number.isFinite(p.v) && Number.isFinite(p.i)) - .sort((a, b) => a.v - b.v); + // Build clean, voltage-sorted arrays of finite, unique-voltage samples. + const normalized = normalizeSweep(pts); + const clean = normalized.clean; const n = clean.length; if (n < 3) return null; const v = clean.map((p) => p.v); const i = clean.map((p) => p.i); - const notes = { iscExtrapolated: false, vocBeyondRange: false }; + const notes = { + iscExtrapolated: false, + vocBeyondRange: false, + vocAmbiguous: false, + duplicateVoltages: normalized.duplicateCount, + }; + const status = { + isc: "unavailable", + voc: "unavailable", + pmax: "unavailable", + ff: "unavailable", + rs: "unavailable", + rsh: "unavailable", + }; // ── Short-circuit current Isc = I(V = 0) ───────────────────────────────── // Prefer an exact V=0 sample, else interpolate across the bracketing pair, - // else linearly extrapolate from the two lowest-voltage points to V=0. + // else linearly extrapolate from the two samples nearest V=0. let isc; const zeroExact = v.indexOf(0); if (zeroExact !== -1) { isc = i[zeroExact]; + status.isc = "measured"; } else { let bracket = -1; for (let j = 0; j < n - 1; j++) { @@ -99,28 +203,51 @@ export function calcMetrics(pts) { } if (bracket !== -1) { isc = lerp(0, v[bracket], i[bracket], v[bracket + 1], i[bracket + 1]); + status.isc = "interpolated"; } else { - // All samples on one side of V=0: extrapolate from the nearest two. - isc = lerp(0, v[0], i[0], v[1], i[1]); + const nearest = clean + .map((point) => ({ ...point, distance: Math.abs(point.v) })) + .sort((a, b) => a.distance - b.distance) + .slice(0, 2) + .sort((a, b) => a.v - b.v); + isc = lerp(0, nearest[0].v, nearest[0].i, nearest[1].v, nearest[1].i); notes.iscExtrapolated = true; + status.isc = "extrapolated"; } } // ── Open-circuit voltage Voc = V where I first crosses 0 going negative ─── let voc = null; let crossIndex = -1; + const vocCandidates = []; for (let j = 1; j < n; j++) { if (i[j - 1] > 0 && i[j] <= 0) { - voc = lerp(0, i[j - 1], v[j - 1], i[j], v[j]); // interpolate V at I=0 - crossIndex = j; - break; + const candidate = lerp(0, i[j - 1], v[j - 1], i[j], v[j]); + if (candidate >= 0) { + vocCandidates.push({ + voltage: candidate, + index: j, + status: i[j] === 0 ? "measured" : "interpolated", + }); + } } } - if (voc === null) { - // Current never reaches zero within the sweep — report the last voltage - // and flag that Voc lies beyond the measured range. - voc = v[n - 1]; - notes.vocBeyondRange = true; + if (vocCandidates.length === 1) { + voc = vocCandidates[0].voltage; + crossIndex = vocCandidates[0].index; + status.voc = vocCandidates[0].status; + } else if (vocCandidates.length > 1) { + notes.vocAmbiguous = true; + status.voc = "ambiguous"; + } else { + const nonnegative = clean.filter((point) => point.v >= 0); + if (nonnegative.some((point) => point.i > 0)) { + // Keep the boundary value for charting/backward compatibility, but mark + // it explicitly as a lower bound and do not derive FF or Rs from it. + voc = nonnegative[nonnegative.length - 1].v; + notes.vocBeyondRange = true; + status.voc = "lower-bound"; + } } // ── Maximum power point — restricted to the power quadrant (V≥0, I≥0) ───── @@ -134,61 +261,96 @@ export function calcMetrics(pts) { let pmax = 0; let vmp = 0; let imp = 0; - const considerMpp = (vv, ii) => { - if (vv < 0 || ii < 0) return; + let mppSource = "unavailable"; + let mppOverflow = false; + const considerMpp = (vv, ii, source) => { + if (vv < 0 || ii < 0 || (voc != null && status.voc !== "lower-bound" && vv > voc + 1e-12)) return; const p = vv * ii; - if (p > pmax) { pmax = p; vmp = vv; imp = ii; } + if (!Number.isFinite(p)) { mppOverflow = true; return; } + if (p > pmax) { pmax = p; vmp = vv; imp = ii; mppSource = source; } }; - for (let j = 0; j < n; j++) considerMpp(v[j], i[j]); + for (let j = 0; j < n; j++) considerMpp(v[j], i[j], "measured"); for (let j = 0; j < n - 1; j++) { const dv = v[j + 1] - v[j]; - if (dv <= 0) continue; // duplicate abscissa — no interior to search + if (dv <= 0) continue; const b = (i[j + 1] - i[j]) / dv; if (b >= 0) continue; // parabola opens upward or is flat: endpoints suffice const a = i[j] - b * v[j]; const vStar = -a / (2 * b); - if (vStar > v[j] && vStar < v[j + 1]) considerMpp(vStar, a + b * vStar); + if (vStar > v[j] && vStar < v[j + 1]) considerMpp(vStar, a + b * vStar, "interpolated"); } + if (pmax > 0) status.pmax = status.voc === "lower-bound" ? "provisional" : mppSource; // ── Fill factor FF = Pmax / (Isc · Voc) ────────────────────────────────── // Uses the Pmax actually found on the P-V curve above — never the // Voc·Isc·(assumed FF) shortcut. - const ff = isc > 0 && voc > 0 ? pmax / (isc * voc) : 0; - - // ── Series resistance Rs ≈ −dV/dI at the I = 0 crossing (near Voc) ──────── - // Slope of the single segment that brackets the crossing — the most local - // estimate available from one light sweep. - let rs = 0; - if (crossIndex >= 1) { - const dv = v[crossIndex] - v[crossIndex - 1]; - const di = i[crossIndex] - i[crossIndex - 1]; - if (di !== 0) rs = Math.abs(dv / di); + let ff = null; + if (isc > 0 && voc > 0 && (status.voc === "measured" || status.voc === "interpolated")) { + ff = pmax / (isc * voc); + status.ff = status.isc === "extrapolated" ? "provisional" : "computed"; } - // ── Shunt resistance Rsh ≈ (dI/dV)⁻¹ near short circuit ────────────────── - // Least-squares slope of I(V) over the low-voltage plateau (V ≤ 30 % of Voc, - // including any reverse-bias samples) — more noise-robust than a two-point - // difference. Falls back to the three lowest-voltage samples when the - // plateau holds fewer than three points. - let rsh = Infinity; - { - const cut = voc > 0 ? 0.3 * voc : v[0] + 0.3 * (v[n - 1] - v[0]); - let xs = [], ys = []; - for (let j = 0; j < n; j++) if (v[j] <= cut) { xs.push(v[j]); ys.push(i[j]); } - if (xs.length < 3) { xs = v.slice(0, 3); ys = i.slice(0, 3); } - const slope = lsqSlope(xs, ys); - if (slope !== null && slope !== 0) rsh = Math.abs(1 / slope); + // ── Endpoint resistance estimates ───────────────────────────────────────── + // Fit the nearest local samples rather than using one noise-sensitive pair. + // These remain slope estimates from a light sweep, not single-diode fits. + let rs = null; + let rsFit = null; + if (voc != null && (status.voc === "measured" || status.voc === "interpolated")) { + rsFit = localCurrentFit(v, i, voc, 0.08 * Math.max(voc, medianPositiveStep(v))); + if (rsFit && rsFit.slope !== 0) { + const estimate = Math.abs(1 / rsFit.slope); + if (Number.isFinite(estimate)) { + rs = estimate; + status.rs = "estimated"; + } + } + } + + let rsh = null; + const rshFit = localCurrentFit( + v, + i, + 0, + 0.08 * Math.max(voc || (v[n - 1] - v[0]), medianPositiveStep(v)) + ); + if (rshFit) { + rsh = rshFit.slope === 0 ? Infinity : Math.abs(1 / rshFit.slope); + status.rsh = status.isc === "extrapolated" ? "provisional" : "estimated"; } // ── Data-quality warnings ───────────────────────────────────────────────── // Sanity checks a reviewer would run by eye; surfaced verbatim in the UI. const warnings = []; + const errors = []; + if (normalized.duplicateCount > 0) + warnings.push(`${normalized.duplicateCount} repeated voltage sample(s) were averaged before extraction — split forward/reverse sweeps into separate channels if these repeats represent hysteresis.`); if (notes.iscExtrapolated) - warnings.push("No samples bracket V = 0 — Isc is a linear extrapolation from the two lowest-voltage points."); + warnings.push("No samples bracket V = 0 — Isc is a linear extrapolation from the two nearest voltage points."); if (notes.vocBeyondRange) - warnings.push("Current never crosses zero within the sweep — the quoted Voc is the last measured voltage (a lower bound)."); + warnings.push("Current never crosses zero within the sweep — Voc is reported only as a lower bound; FF and Rs are withheld and Pmax is provisional."); + if (notes.vocAmbiguous) + errors.push(`Current crosses from positive to non-positive ${vocCandidates.length} times at non-negative voltage — Voc is ambiguous, so Voc, FF, and Rs are withheld.`); + if (!Number.isFinite(isc)) + errors.push("Isc calculation overflowed or became non-finite — inspect extreme values, units, and corrupt cells."); + if (mppOverflow) { + status.pmax = "overflow"; + errors.push("At least one V·I product overflowed the numeric range — power metrics are not reportable; inspect extreme values and units."); + } + if (ff != null && !Number.isFinite(ff)) { + status.ff = "unavailable"; + errors.push("Fill-factor calculation became non-finite — power metrics are not reportable; inspect extreme values and units."); + ff = null; + } if (isc <= 0) - warnings.push("Isc ≤ 0. For an illuminated cell in generator convention Isc should be positive — check the current sign or column mapping."); + errors.push("Isc ≤ 0. Solavin requires generator convention (positive current in the power quadrant); invert the current sign or correct the column mapping."); + if (!v.some((value) => value >= 0)) + errors.push("The sweep contains no non-negative voltage samples, so photovoltaic power-quadrant metrics are not identifiable."); + if (voc === null && !notes.vocAmbiguous) + errors.push("No non-negative open-circuit crossing or lower bound can be identified from this sweep."); + if (ff != null && (ff < 0 || ff > 1 + 1e-9)) { + status.ff = "unphysical"; + errors.push(`Computed fill factor is ${(ff * 100).toFixed(2)} %, outside the physical 0–100 % interval — inspect sign convention, sweep coverage, and outliers.`); + } // Monotonicity: between V = 0 and Voc an illuminated cell's current should // fall as voltage rises. Tolerate noise up to 2 % of the Isc scale. { @@ -196,7 +358,8 @@ export function calcMetrics(pts) { const tol = 0.02 * scale; let rises = 0; for (let j = 1; j < n; j++) { - if (v[j - 1] < 0 || v[j] > voc + 1e-12) continue; + const limit = voc == null ? v[n - 1] : voc; + if (v[j - 1] < 0 || v[j] > limit + 1e-12) continue; if (i[j] - i[j - 1] > tol) rises++; } if (rises > 0) @@ -210,7 +373,18 @@ export function calcMetrics(pts) { warnings.push(`Only ${quad} sweep point(s) fall in the power quadrant (V ≥ 0, I ≥ 0) — Vmp/Imp resolution is limited; use a finer voltage step.`); } - return { isc, voc, pmax, vmp, imp, ff, rs, rsh, crossIndex, notes, warnings }; + const quality = errors.length ? "invalid" : warnings.length ? "review" : "pass"; + return { + isc, voc, pmax, vmp, imp, ff, rs, rsh, crossIndex, + sampleCount: normalized.rawCount, + uniqueVoltageCount: n, + notes, + status, + fit: { rs: rsFit, rsh: rshFit }, + warnings, + errors, + quality, + }; } /** @@ -247,11 +421,15 @@ export function fmtSI(value, unit, digits = 3) { * @returns {number|null} Efficiency in percent, or null for invalid inputs. */ export function computeEfficiency(pmaxW, areaCm2, irradiance) { + const p = Number(pmaxW); const a = Number(areaCm2); const g = Number(irradiance); - if (!Number.isFinite(a) || !Number.isFinite(g) || a <= 0 || g <= 0) return null; + if (!Number.isFinite(p) || !Number.isFinite(a) || !Number.isFinite(g) || p < 0 || a <= 0 || g <= 0) return null; const areaM2 = a * 1e-4; // cm² → m² - return (pmaxW / (g * areaM2)) * 100; + const incidentPower = g * areaM2; + if (!Number.isFinite(incidentPower) || incidentPower <= 0) return null; + const efficiency = (p / incidentPower) * 100; + return Number.isFinite(efficiency) ? efficiency : null; } /** @@ -259,28 +437,185 @@ export function computeEfficiency(pmaxW, areaCm2, irradiance) { * sweeps, row 0 = headers) into a structured dataset fragment. * * @param {Array>} rows - * @returns {null | { conditions:string[], ivData:Record }} + * Missing/blank cells are skipped, never coerced to zero. Header names are + * made unique so one channel cannot silently overwrite another. + * + * @returns {null | { + * conditions:string[], + * ivData:Record, + * diagnostics:{warnings:string[], rows:number, channels:Record} + * }} */ export function extractIV(rows) { - if (!rows || rows.length < 3) return null; - const header = rows[0] - .slice(1) - .map((x, idx) => (x == null || x === "" ? `Channel ${idx + 1}` : String(x))); - const conditions = header.filter((_, idx) => rows[0][idx + 1] != null && rows[0][idx + 1] !== ""); + if (!Array.isArray(rows) || rows.length < 2 || !Array.isArray(rows[0])) return null; + if (rows.length - 1 > MAX_WORKSHEET_ROWS) + throw new Error(`Worksheet exceeds the ${MAX_WORKSHEET_ROWS.toLocaleString()}-data-row safety limit.`); + const dataRows = rows.slice(1).filter(Array.isArray); + const maxColumns = rows.reduce((max, row) => (Array.isArray(row) ? Math.max(max, row.length) : max), 0); + if (maxColumns < 2) return null; + if (maxColumns > MAX_WORKSHEET_COLUMNS) + throw new Error(`Worksheet exceeds the ${MAX_WORKSHEET_COLUMNS.toLocaleString()}-column safety limit.`); + + const warnings = []; + const channels = Object.create(null); + const ivData = Object.create(null); + const conditions = []; + const nameCounts = new Map(); + let invalidVoltageRows = 0; + for (const row of dataRows) { + if (!Number.isFinite(finiteNumber(row[0]))) invalidVoltageRows++; + } + + const uniqueName = (raw, columnIndex) => { + let base = raw == null || String(raw).trim() === "" + ? `Channel ${columnIndex}` + : String(raw).trim().slice(0, 120); + if (["__proto__", "prototype", "constructor"].includes(base)) base = `Channel: ${base}`; + const count = (nameCounts.get(base) || 0) + 1; + nameCounts.set(base, count); + return count === 1 ? base : `${base} (${count})`; + }; + + for (let columnIndex = 1; columnIndex < maxColumns; columnIndex++) { + const hasHeader = rows[0][columnIndex] != null && String(rows[0][columnIndex]).trim() !== ""; + const hasAnyCell = dataRows.some((row) => row[columnIndex] != null && String(row[columnIndex]).trim() !== ""); + if (!hasHeader && !hasAnyCell) continue; + + const name = uniqueName(rows[0][columnIndex], columnIndex); + const points = []; + let skippedCurrent = 0; + for (const row of dataRows) { + const voltage = finiteNumber(row[0]); + const current = finiteNumber(row[columnIndex]); + if (!Number.isFinite(voltage)) continue; + if (!Number.isFinite(current)) { + skippedCurrent++; + continue; + } + points.push({ voltage, current: Math.abs(current), rawCurrent: current }); + } + + const uniqueVoltages = new Set(points.map((point) => point.voltage)).size; + if (points.length < 3 || uniqueVoltages < 3) { + warnings.push(`Column ${columnIndex + 1} (${name}) was ignored because it contains ${points.length} valid pair(s) across ${uniqueVoltages} unique voltage(s); at least 3 unique voltages are required.`); + continue; + } + if (!hasHeader) warnings.push(`Column ${columnIndex + 1} had no header and was named "${name}".`); + if (nameCounts.get(String(rows[0][columnIndex] ?? "").trim()) > 1) + warnings.push(`Duplicate channel header "${String(rows[0][columnIndex]).trim()}" was renamed "${name}".`); + if (skippedCurrent > 0) + warnings.push(`${name}: skipped ${skippedCurrent} row(s) with a blank or non-numeric current; no zero values were inserted.`); + + conditions.push(name); + ivData[name] = points; + channels[name] = { + column: columnIndex + 1, + validPoints: points.length, + uniqueVoltages, + skippedCurrent, + }; + } if (conditions.length === 0) return null; + if (invalidVoltageRows > 0) + warnings.push(`Skipped ${invalidVoltageRows} row(s) with a blank or non-numeric voltage.`); - const dataRows = rows.slice(1).filter((r) => r[0] != null && r[0] !== ""); - const ivData = {}; - conditions.forEach((name, ci) => { - ivData[name] = dataRows - .map((r) => { - const voltage = Number(r[0]); - const raw = Number(r[ci + 1] ?? 0); - return { voltage, current: Math.abs(raw), rawCurrent: raw }; - }) - .filter((p) => Number.isFinite(p.voltage) && Number.isFinite(p.rawCurrent)); - }); - return { conditions, ivData }; + return { + conditions, + ivData, + diagnostics: { + warnings, + rows: dataRows.length, + invalidVoltageRows, + channels, + }, + }; +} + +/** + * Parse RFC 4180-style CSV text without executing formulas or coercing values. + * Numeric conversion happens later in extractIV(). + */ +export function parseCsv(text, limits = {}) { + if (typeof text !== "string") throw new TypeError("CSV input must be text."); + const maxRows = limits.maxRows || 250000; + const maxColumns = limits.maxColumns || 2048; + const source = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text; + const rows = []; + let row = []; + let field = ""; + let quoted = false; + + const pushField = () => { + row.push(field); + field = ""; + if (row.length > maxColumns) throw new Error(`CSV exceeds the ${maxColumns}-column safety limit.`); + }; + const pushRow = () => { + pushField(); + rows.push(row); + row = []; + if (rows.length > maxRows) throw new Error(`CSV exceeds the ${maxRows.toLocaleString()}-row safety limit.`); + }; + + for (let index = 0; index < source.length; index++) { + const char = source[index]; + if (quoted) { + if (char === '"') { + if (source[index + 1] === '"') { + field += '"'; + index++; + } else { + quoted = false; + } + } else { + field += char; + } + continue; + } + if (char === '"' && field === "") { + quoted = true; + } else if (char === ",") { + pushField(); + } else if (char === "\n" || char === "\r") { + if (char === "\r" && source[index + 1] === "\n") index++; + pushRow(); + } else { + field += char; + } + } + if (quoted) throw new Error("CSV contains an unterminated quoted field."); + if (field !== "" || row.length > 0) pushRow(); + while (rows.length && rows[rows.length - 1].every((cell) => cell === "")) rows.pop(); + return rows; +} + +/** + * Align channels by numeric voltage rather than array index. Sparse channels + * retain null gaps, preventing a missing cell from shifting every later point + * onto the wrong x-coordinate. + */ +export function buildAlignedRows(dataset, selectedConditions, valueForPoint = (point) => point.rawCurrent) { + if (!dataset || !Array.isArray(dataset.conditions) || !dataset.ivData) return []; + const conditions = Array.isArray(selectedConditions) ? selectedConditions : dataset.conditions; + const rowsByVoltage = new Map(); + for (const condition of conditions) { + const samplesByVoltage = new Map(); + for (const point of dataset.ivData[condition] || []) { + const voltage = finiteNumber(point && point.voltage); + const value = valueForPoint(point, condition); + if (!Number.isFinite(voltage) || !Number.isFinite(value)) continue; + const group = samplesByVoltage.get(voltage) || { sum: 0, count: 0 }; + group.sum += value; + group.count++; + samplesByVoltage.set(voltage, group); + } + for (const [voltage, group] of samplesByVoltage) { + const row = rowsByVoltage.get(voltage) || { voltage }; + row[condition] = group.sum / group.count; + rowsByVoltage.set(voltage, row); + } + } + return [...rowsByVoltage.values()].sort((a, b) => a.voltage - b.voltage); } /** @@ -327,7 +662,12 @@ export function buildSampleDataset() { rawCurrent: r[ci + 1], })); }); - return { name: "Sample: Solar Cell (Room Lights)", conditions, ivData }; + return { + name: "Sample: Solar Cell (Room Lights)", + conditions, + ivData, + source: { file: "examples/sample_iv_data.csv", sheet: "CSV" }, + }; } /** @@ -340,12 +680,13 @@ export function buildSampleDataset() { * @param {{conditions:string[]}} dataset * @param {Record>} allMetrics * @param {Record|null} [efficiency] optional η (%) per condition + * @param {{cellAreaCm2?:number, irradianceWm2?:number}} [efficiencyInputs] */ -export function metricsToRows(dataset, allMetrics, efficiency) { +export function metricsToRows(dataset, allMetrics, efficiency, efficiencyInputs = {}) { const ms = dataset.conditions.map((c) => allMetrics[c]).filter(Boolean); const iScale = siPrefix(Math.max(0, ...ms.map((m) => Math.abs(m.isc)), ...ms.map((m) => Math.abs(m.imp)))); const pScale = siPrefix(Math.max(0, ...ms.map((m) => Math.abs(m.pmax)))); - const rsScale = siPrefix(Math.max(0, ...ms.map((m) => Math.abs(m.rs)))); + const rsScale = siPrefix(Math.max(0, ...ms.filter((m) => Number.isFinite(m.rs)).map((m) => Math.abs(m.rs)))); const rshScale = siPrefix(Math.max(0, ...ms.filter((m) => Number.isFinite(m.rsh)).map((m) => Math.abs(m.rsh)))); const ascii = (p) => (p === "µ" ? "u" : p); const header = [ @@ -359,35 +700,122 @@ export function metricsToRows(dataset, allMetrics, efficiency) { `Rs (${ascii(rsScale.prefix)}Ohm)`, `Rsh (${ascii(rshScale.prefix)}Ohm)`, ]; - if (efficiency) header.push("Efficiency (%)"); + if (efficiency) header.push( + "Efficiency (%)", + "Efficiency status", + "Cell area (cm2)", + "Irradiance (W/m2)" + ); + header.push( + "Isc status", + "Voc status", + "Pmax status", + "FF status", + "Quality", + "Valid samples", + "Unique voltages", + "Metric flags", + "Import flags", + "Source file", + "Source sheet", + "Analysis version", + "Analysis method" + ); + const importFlags = (dataset.diagnostics?.warnings || []).join(" | "); const rows = [header]; dataset.conditions.forEach((c) => { const m = allMetrics[c]; if (!m) return; + const efficiencyValue = efficiency?.[c]; + const hasEfficiency = Number.isFinite(efficiencyValue); + const metricFlags = [...(m.errors || []), ...(m.warnings || [])]; + const hasPmax = ["measured", "interpolated", "provisional"].includes(m.status?.pmax); + if (Number.isFinite(efficiencyValue) && efficiencyValue > 100) + metricFlags.push(`Computed efficiency is ${efficiencyValue.toPrecision(4)} %, above the physical 100 % limit — verify area, irradiance, units, and measurement conditions.`); const row = [ c, - +(m.isc / iScale.div).toFixed(4), - +m.voc.toFixed(4), - +(m.pmax / pScale.div).toFixed(4), - +m.vmp.toFixed(4), - +(m.imp / iScale.div).toFixed(4), - +(m.ff * 100).toFixed(2), - +(m.rs / rsScale.div).toFixed(3), - m.rsh === Infinity ? "Inf" : +(m.rsh / rshScale.div).toFixed(3), + Number.isFinite(m.isc) ? +(m.isc / iScale.div).toFixed(4) : "", + Number.isFinite(m.voc) ? +m.voc.toFixed(4) : "", + hasPmax ? +(m.pmax / pScale.div).toFixed(4) : "", + hasPmax ? +m.vmp.toFixed(4) : "", + hasPmax ? +(m.imp / iScale.div).toFixed(4) : "", + m.ff == null ? "" : +(m.ff * 100).toFixed(2), + m.rs == null ? "" : +(m.rs / rsScale.div).toFixed(3), + m.rsh == null ? "" : m.rsh === Infinity ? "Inf" : +(m.rsh / rshScale.div).toFixed(3), ]; - if (efficiency) row.push(efficiency[c] != null ? +efficiency[c].toPrecision(4) : ""); + if (efficiency) row.push( + hasEfficiency ? +efficiencyValue.toPrecision(4) : "", + !hasEfficiency ? "" : efficiencyValue > 100 ? "unphysical" : "computed", + Number.isFinite(efficiencyInputs.cellAreaCm2) ? efficiencyInputs.cellAreaCm2 : "", + Number.isFinite(efficiencyInputs.irradianceWm2) ? efficiencyInputs.irradianceWm2 : "" + ); + row.push( + m.status?.isc || "", + m.status?.voc || "", + m.status?.pmax || "", + m.status?.ff || "", + m.quality || "", + m.sampleCount ?? "", + m.uniqueVoltageCount ?? "", + metricFlags.join(" | "), + importFlags, + dataset.source?.file || "", + dataset.source?.sheet || "", + ANALYSIS_VERSION, + ANALYSIS_METHOD + ); rows.push(row); }); return rows; } +/** + * Export the signed measurements exactly as aligned for visualization. + * Missing samples remain blank; duplicate exact-voltage samples are averaged + * consistently with buildAlignedRows(). + */ +export function rawDataToRows(dataset) { + if (!dataset || !Array.isArray(dataset.conditions)) return [["Voltage (V)"]]; + const header = ["Voltage (V)", ...dataset.conditions.map((condition) => `${condition} (A)`)]; + const body = buildAlignedRows(dataset).map((aligned) => [ + aligned.voltage, + ...dataset.conditions.map((condition) => + Number.isFinite(aligned[condition]) ? aligned[condition] : "" + ), + ]); + return [header, ...body]; +} + +/** Export analysis provenance and interpretation rules as a two-column sheet. */ +export function metadataToRows(dataset, efficiencyInputs = {}) { + return [ + ["Field", "Value"], + ["Solavin analysis version", ANALYSIS_VERSION], + ["Analysis method", ANALYSIS_METHOD], + ["Dataset", dataset?.name || ""], + ["Source file", dataset?.source?.file || ""], + ["Source sheet", dataset?.source?.sheet || ""], + ["Cell area (cm2)", Number.isFinite(efficiencyInputs.cellAreaCm2) ? efficiencyInputs.cellAreaCm2 : ""], + ["Irradiance (W/m2)", Number.isFinite(efficiencyInputs.irradianceWm2) ? efficiencyInputs.irradianceWm2 : ""], + ["Current convention", "Generator convention: positive current produces positive V*I power"], + ["Visualization", "Measured samples joined with straight segments; no smoothing"], + ["Duplicate voltage handling", "Exact duplicate setpoints are averaged and reported"], + ["Missing cell handling", "Missing or non-numeric measurements stay missing; zeros are never inserted"], + ["Censored sweep handling", "When Voc is not bracketed, Voc is a lower bound, Pmax is provisional, and FF/Rs are withheld"], + ["Import flags", (dataset?.diagnostics?.warnings || []).join(" | ")], + ]; +} + /** Serialise export rows to a CSV string. */ export function rowsToCsv(rows) { return rows .map((r) => r .map((cell) => { - const s = String(cell); + // Prevent spreadsheet formula injection through untrusted channel + // labels while leaving numeric negative values numeric. + const safe = typeof cell === "string" && /^[=+\-@\t\r]/.test(cell) ? `'${cell}` : cell; + const s = String(safe); return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; }) .join(",") diff --git a/src/lib/ivAnalysis.test.js b/src/lib/ivAnalysis.test.js index 314fae4..b6ad916 100644 --- a/src/lib/ivAnalysis.test.js +++ b/src/lib/ivAnalysis.test.js @@ -193,7 +193,8 @@ describe("calcMetrics — data-quality warnings", () => { { voltage: 0.5, rawCurrent: -1.2 }, { voltage: 1.0, rawCurrent: -1.4 }, ]); - expect(m.warnings.some((w) => /Isc ≤ 0/.test(w))).toBe(true); + expect(m.errors.some((w) => /Isc ≤ 0/.test(w))).toBe(true); + expect(m.quality).toBe("invalid"); }); it("flags non-monotonic current between 0 V and Voc", () => { diff --git a/src/lib/ivAnalysis.validation.test.js b/src/lib/ivAnalysis.validation.test.js new file mode 100644 index 0000000..26a6059 --- /dev/null +++ b/src/lib/ivAnalysis.validation.test.js @@ -0,0 +1,434 @@ +import { describe, expect, it } from "vitest"; +import readExcelFile from "read-excel-file/node"; +import writeExcelFile from "write-excel-file/node"; +import { + buildAlignedRows, + calcMetrics, + computeEfficiency, + extractIV, + metadataToRows, + metricsToRows, + parseCsv, + rawDataToRows, + rowsToCsv, +} from "./ivAnalysis.js"; + +function mulberry32(seed) { + return () => { + let value = seed += 0x6d2b79f5; + value = Math.imul(value ^ (value >>> 15), value | 1); + value ^= value + Math.imul(value ^ (value >>> 7), value | 61); + return ((value ^ (value >>> 14)) >>> 0) / 4294967296; + }; +} + +function shuffle(values, random) { + const result = [...values]; + for (let index = result.length - 1; index > 0; index--) { + const target = Math.floor(random() * (index + 1)); + [result[index], result[target]] = [result[target], result[index]]; + } + return result; +} + +function maximize(functionToEvaluate, lower, upper) { + const ratio = (Math.sqrt(5) - 1) / 2; + let left = lower; + let right = upper; + let x1 = right - ratio * (right - left); + let x2 = left + ratio * (right - left); + let y1 = functionToEvaluate(x1); + let y2 = functionToEvaluate(x2); + for (let iteration = 0; iteration < 120; iteration++) { + if (y1 > y2) { + right = x2; + x2 = x1; + y2 = y1; + x1 = right - ratio * (right - left); + y1 = functionToEvaluate(x1); + } else { + left = x1; + x1 = x2; + y1 = y2; + x2 = left + ratio * (right - left); + y2 = functionToEvaluate(x2); + } + } + const x = (left + right) / 2; + return { x, y: functionToEvaluate(x) }; +} + +describe("accuracy validation — independent analytic oracles", () => { + it("recovers 300 deterministic random linear I-V curves to floating-point precision", () => { + const random = mulberry32(0x50a71); + for (let trial = 0; trial < 300; trial++) { + const isc = 10 ** (-8 + 8 * random()); + const voc = 0.15 + 4.85 * random(); + const voltages = [0]; + for (let index = 0; index < 6 + Math.floor(random() * 20); index++) { + voltages.push(voc * 1.15 * random()); + } + voltages.push(voc * 1.05); + const points = voltages.map((voltage) => ({ + voltage, + rawCurrent: isc * (1 - voltage / voc), + })); + const metrics = calcMetrics(shuffle(points, random)); + const expectedPower = isc * voc / 4; + + expect(metrics.isc).toBeCloseTo(isc, 12); + expect(metrics.voc).toBeCloseTo(voc, 12); + expect(metrics.vmp).toBeCloseTo(voc / 2, 12); + expect(metrics.imp).toBeCloseTo(isc / 2, 12); + expect(metrics.pmax).toBeCloseTo(expectedPower, 12); + expect(metrics.ff).toBeCloseTo(0.25, 12); + expect(metrics.errors).toEqual([]); + } + }); + + it("matches a continuous single-diode oracle across scale, Voc, and ideality", () => { + const thermalVoltage = 0.025852; + const cases = [ + { isc: 140e-3, voc: 0.60, n: 1.0 }, + { isc: 35e-3, voc: 0.58, n: 2.0 }, + { isc: 1.5e-6, voc: 2.08, n: 1.4 }, + { isc: 4.2, voc: 0.72, n: 1.15 }, + { isc: 8e-9, voc: 0.42, n: 1.8 }, + { isc: 0.8, voc: 1.25, n: 1.3 }, + ]; + + for (const testCase of cases) { + const saturationCurrent = testCase.isc / Math.expm1(testCase.voc / (testCase.n * thermalVoltage)); + const currentAt = (voltage) => + testCase.isc - saturationCurrent * Math.expm1(voltage / (testCase.n * thermalVoltage)); + const oracle = maximize((voltage) => voltage * currentAt(voltage), 0, testCase.voc); + const step = testCase.voc / 500; + const points = []; + for (let voltage = 0; voltage <= testCase.voc * 1.04; voltage += step) { + points.push({ voltage, rawCurrent: currentAt(voltage) }); + } + const metrics = calcMetrics(points); + const oracleFillFactor = oracle.y / (testCase.isc * testCase.voc); + + expect(Math.abs(metrics.isc / testCase.isc - 1)).toBeLessThan(1e-10); + expect(Math.abs(metrics.voc / testCase.voc - 1)).toBeLessThan(2e-5); + expect(Math.abs(metrics.vmp - oracle.x)).toBeLessThan(2 * step); + expect(Math.abs(metrics.pmax / oracle.y - 1)).toBeLessThan(2e-4); + expect(Math.abs(metrics.ff / oracleFillFactor - 1)).toBeLessThan(2e-4); + expect(metrics.quality).toBe("pass"); + } + }); + + it("preserves dimensionless metrics under current scaling", () => { + const base = [ + { voltage: 0, rawCurrent: 2 }, + { voltage: 0.2, rawCurrent: 1.6 }, + { voltage: 0.5, rawCurrent: 1 }, + { voltage: 0.8, rawCurrent: 0.4 }, + { voltage: 1, rawCurrent: 0 }, + { voltage: 1.1, rawCurrent: -0.2 }, + ]; + const scaled = base.map((point) => ({ ...point, rawCurrent: point.rawCurrent * 1e-6 })); + const originalMetrics = calcMetrics(base); + const scaledMetrics = calcMetrics(scaled); + + expect(scaledMetrics.voc).toBeCloseTo(originalMetrics.voc, 12); + expect(scaledMetrics.vmp).toBeCloseTo(originalMetrics.vmp, 12); + expect(scaledMetrics.ff).toBeCloseTo(originalMetrics.ff, 12); + expect(scaledMetrics.isc / originalMetrics.isc).toBeCloseTo(1e-6, 12); + expect(scaledMetrics.pmax / originalMetrics.pmax).toBeCloseTo(1e-6, 12); + expect(scaledMetrics.rs / originalMetrics.rs).toBeCloseTo(1e6, 8); + expect(scaledMetrics.rsh / originalMetrics.rsh).toBeCloseTo(1e6, 8); + }); +}); + +describe("accuracy validation — censoring and adversarial sweeps", () => { + it("withholds derived metrics when Voc is outside the measured range", () => { + const metrics = calcMetrics([ + { voltage: 0, rawCurrent: 1 }, + { voltage: 0.3, rawCurrent: 0.95 }, + { voltage: 0.6, rawCurrent: 0.8 }, + { voltage: 0.9, rawCurrent: 0.5 }, + ]); + expect(metrics.voc).toBe(0.9); + expect(metrics.status.voc).toBe("lower-bound"); + expect(metrics.status.pmax).toBe("provisional"); + expect(metrics.ff).toBeNull(); + expect(metrics.rs).toBeNull(); + expect(metrics.warnings.some((warning) => /lower bound/.test(warning))).toBe(true); + }); + + it("detects rather than clamps an unphysical fill factor", () => { + const metrics = calcMetrics([ + { voltage: 0, rawCurrent: 1 }, + { voltage: 0.25, rawCurrent: 8 }, + { voltage: 0.5, rawCurrent: 10 }, + { voltage: 1, rawCurrent: 0 }, + { voltage: 1.1, rawCurrent: -1 }, + ]); + expect(metrics.ff).toBeGreaterThan(1); + expect(metrics.status.ff).toBe("unphysical"); + expect(metrics.quality).toBe("invalid"); + expect(metrics.errors.some((error) => /physical 0–100/.test(error))).toBe(true); + }); + + it("withholds Voc-dependent metrics when multiple zero crossings are ambiguous", () => { + const metrics = calcMetrics([ + { voltage: 0, rawCurrent: 1 }, + { voltage: 0.4, rawCurrent: 0.5 }, + { voltage: 0.6, rawCurrent: -0.1 }, + { voltage: 0.8, rawCurrent: 0.2 }, + { voltage: 1, rawCurrent: -0.2 }, + ]); + expect(metrics.voc).toBeNull(); + expect(metrics.status.voc).toBe("ambiguous"); + expect(metrics.ff).toBeNull(); + expect(metrics.rs).toBeNull(); + expect(metrics.quality).toBe("invalid"); + expect(metrics.errors.some((error) => /crosses.*2 times.*ambiguous/.test(error))).toBe(true); + }); + + it("fails closed when extreme finite inputs overflow derived power", () => { + const metrics = calcMetrics([ + { voltage: 0, rawCurrent: 1e308 }, + { voltage: 2, rawCurrent: 1e308 }, + { voltage: 4, rawCurrent: -1e308 }, + ]); + expect(metrics.quality).toBe("invalid"); + expect(metrics.status.pmax).toBe("overflow"); + expect(metrics.errors.some((error) => /V·I product overflowed/.test(error))).toBe(true); + const rows = metricsToRows( + { conditions: ["Extreme"], ivData: { Extreme: [] } }, + { Extreme: metrics } + ); + expect(rows[1][rows[0].indexOf("Pmax (W)")]).toBe(""); + expect(computeEfficiency(1e308, 1e-308, 1e-308)).toBeNull(); + }); + + it("averages duplicate voltage samples explicitly and reports the operation", () => { + const metrics = calcMetrics([ + { voltage: 0, rawCurrent: 1 }, + { voltage: 0.5, rawCurrent: 0.4 }, + { voltage: 0.5, rawCurrent: 0.6 }, + { voltage: 1, rawCurrent: 0 }, + { voltage: 1.1, rawCurrent: -0.1 }, + ]); + expect(metrics.sampleCount).toBe(5); + expect(metrics.uniqueVoltageCount).toBe(4); + expect(metrics.notes.duplicateVoltages).toBe(1); + expect(metrics.pmax).toBeCloseTo(0.25, 12); + expect(metrics.warnings.some((warning) => /repeated voltage/.test(warning))).toBe(true); + }); + + it("uses the samples nearest zero when extrapolating Isc from negative voltage", () => { + const metrics = calcMetrics([ + { voltage: -3, rawCurrent: 10 }, + { voltage: -2, rawCurrent: 8 }, + { voltage: -1, rawCurrent: 3 }, + ]); + expect(metrics.isc).toBeCloseTo(-2, 12); + expect(metrics.status.isc).toBe("extrapolated"); + expect(metrics.quality).toBe("invalid"); + }); +}); + +describe("workbook and chart integrity", () => { + it("preserves physical column positions, uniquifies headers, and never inserts zeros", () => { + const parsed = extractIV([ + ["V", "Cell", "", "Reference", "Cell"], + [0, 1, "", 3, 4], + [0.5, "", 0.4, 2, 3], + [1, -0.1, 0.1, 0.5, 2], + [1.5, -0.2, -0.1, -1, -1], + ]); + + expect(parsed.conditions).toEqual(["Cell", "Channel 2", "Reference", "Cell (2)"]); + expect(parsed.ivData.Cell.map((point) => point.rawCurrent)).toEqual([1, -0.1, -0.2]); + expect(parsed.ivData["Channel 2"].map((point) => point.rawCurrent)).toEqual([0.4, 0.1, -0.1]); + expect(parsed.ivData.Reference[0].rawCurrent).toBe(3); + expect(parsed.ivData["Cell (2)"][0].rawCurrent).toBe(4); + expect(parsed.diagnostics.warnings.some((warning) => /no zero values were inserted/.test(warning))).toBe(true); + expect(parsed.diagnostics.warnings.some((warning) => /renamed "Cell \(2\)"/.test(warning))).toBe(true); + }); + + it("rejects channels without three unique voltage setpoints", () => { + const parsed = extractIV([ + ["V", "Repeated", "Valid"], + [0, 1, 1], + [0, 2, 2], + [0, 3, 3], + [0.5, "", 2], + [1, "", 0], + ]); + expect(parsed.conditions).toEqual(["Valid"]); + expect(parsed.diagnostics.warnings).toContain( + "Column 2 (Repeated) was ignored because it contains 3 valid pair(s) across 1 unique voltage(s); at least 3 unique voltages are required." + ); + }); + + it("enforces worksheet row and column safety limits", () => { + const tooManyRows = Array.from({ length: 250002 }, (_, index) => + index === 0 ? ["V", "I"] : [index, 1] + ); + expect(() => extractIV(tooManyRows)).toThrow(/250,000-data-row safety limit/); + const tooWide = [["V"], [0]]; + tooWide[0].length = 2049; + tooWide[1].length = 2049; + expect(() => extractIV(tooWide)).toThrow(/2,048-column safety limit/); + }); + + it("aligns sparse channels by voltage instead of row index", () => { + const dataset = { + conditions: ["A", "B"], + ivData: { + A: [ + { voltage: 0, rawCurrent: 1 }, + { voltage: 1, rawCurrent: 0.5 }, + { voltage: 2, rawCurrent: -0.1 }, + ], + B: [ + { voltage: 0, rawCurrent: 2 }, + { voltage: 0.5, rawCurrent: 1.5 }, + { voltage: 0.5, rawCurrent: 1.7 }, + { voltage: 2, rawCurrent: -0.2 }, + ], + }, + }; + const rows = buildAlignedRows(dataset); + expect(rows.map((row) => row.voltage)).toEqual([0, 0.5, 1, 2]); + expect(rows[1].A).toBeUndefined(); + expect(rows[1].B).toBeCloseTo(1.6, 12); + expect(rows[2].A).toBe(0.5); + expect(rows[2].B).toBeUndefined(); + }); + + it("parses BOM, quoted commas, escaped quotes, and embedded newlines", () => { + const rows = parseCsv("\ufeffV,\"Cell, A\",\"Note\"\r\n0,1,\"line 1\nline 2\"\r\n1,-0.1,\"a \"\"quote\"\"\""); + expect(rows).toEqual([ + ["V", "Cell, A", "Note"], + ["0", "1", "line 1\nline 2"], + ["1", "-0.1", 'a "quote"'], + ]); + }); + + it("rejects malformed or excessive CSV input", () => { + expect(() => parseCsv('V,"open\n1,2')).toThrow(/unterminated/); + expect(() => parseCsv("a,b,c", { maxColumns: 2 })).toThrow(/column safety limit/); + expect(() => parseCsv("a\nb", { maxRows: 1 })).toThrow(/row safety limit/); + }); +}); + +describe("export integrity", () => { + it("exports status, quality, sample counts, and flags with every metric row", () => { + const dataset = { + conditions: ["Censored"], + ivData: { + Censored: [ + { voltage: 0, rawCurrent: 1 }, + { voltage: 0.5, rawCurrent: 0.9 }, + { voltage: 1, rawCurrent: 0.8 }, + ], + }, + }; + const metrics = { Censored: calcMetrics(dataset.ivData.Censored) }; + const rows = metricsToRows(dataset, metrics); + const header = rows[0]; + const data = rows[1]; + expect(data[header.indexOf("FF (%)")]).toBe(""); + expect(data[header.indexOf("Voc status")]).toBe("lower-bound"); + expect(data[header.indexOf("Pmax status")]).toBe("provisional"); + expect(data[header.indexOf("Quality")]).toBe("review"); + expect(data[header.indexOf("Valid samples")]).toBe(3); + expect(data[header.indexOf("Metric flags")]).toMatch(/FF and Rs are withheld/); + expect(data[header.indexOf("Analysis version")]).toBe("4.0.0"); + expect(data[header.indexOf("Analysis method")]).toBe("piecewise-linear-v4"); + }); + + it("exports signed raw measurements, gaps, and provenance without shifting rows", () => { + const dataset = { + name: "Sparse measurement", + source: { file: "run-17.csv", sheet: "CSV" }, + diagnostics: { warnings: ["B: skipped one blank cell."] }, + conditions: ["A", "B"], + ivData: { + A: [ + { voltage: 0, rawCurrent: 1 }, + { voltage: 1, rawCurrent: -0.1 }, + ], + B: [ + { voltage: 0.5, rawCurrent: 2 }, + { voltage: 1, rawCurrent: -0.2 }, + ], + }, + }; + const raw = rawDataToRows(dataset); + expect(raw).toEqual([ + ["Voltage (V)", "A (A)", "B (A)"], + [0, 1, ""], + [0.5, "", 2], + [1, -0.1, -0.2], + ]); + const metadata = Object.fromEntries(metadataToRows(dataset).slice(1)); + expect(metadata["Source file"]).toBe("run-17.csv"); + expect(metadata["Source sheet"]).toBe("CSV"); + expect(metadata["Import flags"]).toMatch(/blank cell/); + }); + + it("round-trips the three-sheet research workbook through the production libraries", async () => { + const toCells = (rows) => rows.map((row) => row.map((value) => ({ + value, + type: typeof value === "number" ? Number : String, + }))); + const buffer = await writeExcelFile([ + { sheet: "Metrics", data: toCells([["Condition", "Pmax (W)"], ["A", 0.25]]) }, + { sheet: "Raw Data", data: toCells([["Voltage (V)", "A (A)"], [0, 1], [1, 0]]) }, + { sheet: "Metadata", data: toCells([["Field", "Value"], ["Analysis version", "4.0.0"]]) }, + ]).toBuffer(); + const sheets = await readExcelFile(buffer); + + expect(sheets.map(({ sheet }) => sheet)).toEqual(["Metrics", "Raw Data", "Metadata"]); + expect(sheets[0].data[1]).toEqual(["A", 0.25]); + expect(sheets[1].data[2]).toEqual([1, 0]); + expect(sheets[2].data[1]).toEqual(["Analysis version", "4.0.0"]); + }); + + it("neutralizes spreadsheet formulas in string cells without changing numbers", () => { + const csv = rowsToCsv([ + ["Condition", "Value"], + ["=HYPERLINK(\"https://example.invalid\")", -2], + ["-2mm", 3], + ]); + expect(csv).toContain(`"'=HYPERLINK(""https://example.invalid"")"`); + expect(csv).toContain("'-2mm,3"); + expect(csv).toContain(',-2'); + }); + + it("rejects invalid power in efficiency calculations", () => { + expect(computeEfficiency(-1, 1, 1000)).toBeNull(); + expect(computeEfficiency(Infinity, 1, 1000)).toBeNull(); + }); + + it("exports efficiency inputs and marks unphysical efficiency explicitly", () => { + const dataset = { + conditions: ["Cell"], + ivData: { + Cell: [ + { voltage: 0, rawCurrent: 2 }, + { voltage: 0.5, rawCurrent: 1 }, + { voltage: 1, rawCurrent: 0 }, + ], + }, + }; + const metrics = { Cell: calcMetrics(dataset.ivData.Cell) }; + const rows = metricsToRows(dataset, metrics, { Cell: 125 }, { + cellAreaCm2: 0.01, + irradianceWm2: 1000, + }); + const header = rows[0]; + const data = rows[1]; + expect(data[header.indexOf("Efficiency status")]).toBe("unphysical"); + expect(data[header.indexOf("Cell area (cm2)")]).toBe(0.01); + expect(data[header.indexOf("Irradiance (W/m2)")]).toBe(1000); + expect(data[header.indexOf("Metric flags")]).toMatch(/above the physical 100 % limit/); + }); +}); diff --git a/tests/e2e.smoke.mjs b/tests/e2e.smoke.mjs index 61593ac..f03f8d8 100644 --- a/tests/e2e.smoke.mjs +++ b/tests/e2e.smoke.mjs @@ -7,18 +7,23 @@ * - the deterministic assistant answers from the dataset. * * Run with: npm run test:e2e - * Requires a Chromium that Playwright can launch. If none is available the test - * SKIPS (exit 0) rather than failing, so it never blocks a CI that hasn't - * provisioned browsers. + * Requires a Chromium that Playwright can launch. A missing browser skips only + * for local development; CI treats it as a hard failure. */ -import { existsSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; +import readExcelFile from "read-excel-file/node"; +import writeExcelFile from "write-excel-file/node"; let chromium; try { ({ chromium } = await import("playwright")); -} catch { - console.log("⚠ playwright not installed — skipping e2e smoke test."); +} catch (error) { + if (process.env.CI === "true") { + console.error("✗ Playwright is required in CI:", error.message); + process.exit(1); + } + console.log("⚠ playwright not installed — skipping local e2e smoke test."); process.exit(0); } @@ -39,7 +44,11 @@ for (const executablePath of [undefined, ...fallbacks]) { break; } catch (e) { if (executablePath === fallbacks[fallbacks.length - 1] || fallbacks.length === 0) { - console.log("⚠ no launchable Chromium (" + e.message.split("\n")[0] + ") — skipping e2e smoke test."); + if (process.env.CI === "true") { + console.error("✗ no launchable Chromium in CI (" + e.message.split("\n")[0] + ")"); + process.exit(1); + } + console.log("⚠ no launchable Chromium (" + e.message.split("\n")[0] + ") — skipping local e2e smoke test."); process.exit(0); } } @@ -47,7 +56,7 @@ for (const executablePath of [undefined, ...fallbacks]) { const errors = []; try { - const page = await browser.newPage(); + const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }); page.on("console", (m) => { if (m.type() === "error" && !m.text().includes("ERR_")) errors.push("console: " + m.text()); }); page.on("pageerror", (e) => errors.push("pageerror: " + e.message)); @@ -70,6 +79,10 @@ try { await page.getByRole("button", { name: tab, exact: true }).click(); await page.waitForTimeout(300); } + const chartIntegrity = await page.evaluate(() => { + const paths = [...document.querySelectorAll(".recharts-line-curve, .recharts-area-curve")]; + return paths.length > 0 && paths.every((path) => !/NaN|Infinity/.test(path.getAttribute("d") || "")); + }); // Metrics + efficiency. await page.getByRole("button", { name: "Metrics & Export", exact: true }).click(); @@ -82,7 +95,18 @@ try { await page.fill('input[placeholder="0.01"]', "0.01"); await page.fill('input[placeholder="1000"]', "1000"); await page.waitForTimeout(300); - const hasEff = await page.evaluate(() => document.body.innerText.includes("η")); + const hasEff = await page.evaluate(() => { + const table = [...document.querySelectorAll("table")].find((candidate) => + candidate.innerText.includes("CONDITION") && candidate.innerText.includes("FF") + ); + if (!table) return false; + const headers = [...table.querySelectorAll("th")].map((cell) => cell.innerText.trim()); + const efficiencyIndex = headers.findIndex((header) => header.includes("η (%)")); + if (efficiencyIndex < 0) return false; + const values = [...table.querySelectorAll("tbody tr")] + .map((row) => Number(row.querySelectorAll("td")[efficiencyIndex]?.innerText)); + return values.length > 0 && values.every((value) => Number.isFinite(value) && value > 0); + }); // Assistant. await page.getByRole("button", { name: /Lab Assistant/ }).click(); @@ -93,18 +117,105 @@ try { const ffOk = Array.isArray(ffValues) && ffValues.length > 0 && ffValues.every((f) => f > 0 && f < 100); + // Import a deliberately sparse CSV. Each channel omits a different voltage, + // which catches the historical row-index alignment bug in the real UI. + await page.getByRole("button", { name: "Import Data", exact: true }).click(); + const sparseCsv = [ + "Voltage(V),Cell A,Cell B", + "0,1,2", + "0.5,,1.5", + "1,0.5,", + "1.5,0.1,0.2", + "2,-0.2,-0.1", + ].join("\n"); + await page.locator('input[type="file"]').setInputFiles({ + name: "sparse_iv.csv", + mimeType: "text/csv", + buffer: Buffer.from(sparseCsv), + }); + await page.getByText("sparse_iv (CSV)", { exact: false }).first().waitFor(); + const sparseAlignmentOk = await page.evaluate(() => { + const table = [...document.querySelectorAll("table")].find((candidate) => + candidate.innerText.includes("Cell A") && candidate.innerText.includes("Cell B") + ); + if (!table) return false; + const rows = [...table.querySelectorAll("tbody tr")].map((row) => + [...row.querySelectorAll("td")].map((cell) => cell.innerText.trim()) + ); + const atHalfVolt = rows.find((row) => row[0] === "0.50"); + const atOneVolt = rows.find((row) => row[0] === "1.00"); + return atHalfVolt?.[1] === "—" && atHalfVolt?.[2] === "1.500e+0" && + atOneVolt?.[1] === "5.000e-1" && atOneVolt?.[2] === "—"; + }); + await page.getByRole("button", { name: "Metrics & Export", exact: true }).click(); + await page.getByText(/skipped 1 row\(s\).*no zero values were inserted/i).first().waitFor(); + + const csvDownloadPromise = page.waitForEvent("download"); + await page.getByRole("button", { name: "EXPORT CSV", exact: true }).click(); + const csvDownload = await csvDownloadPromise; + const csvText = readFileSync(await csvDownload.path(), "utf8"); + const csvExportOk = csvDownload.suggestedFilename() === "solavin_iv_metrics.csv" && + csvText.includes("Analysis version") && + csvText.includes("piecewise-linear-v4") && + csvText.includes("sparse_iv.csv") && + csvText.includes("Import flags"); + + const xlsxDownloadPromise = page.waitForEvent("download"); + await page.getByRole("button", { name: "EXPORT XLSX", exact: true }).click(); + const xlsxDownload = await xlsxDownloadPromise; + const exportedWorkbook = await readExcelFile(await xlsxDownload.path()); + const exportedMetadata = Object.fromEntries( + exportedWorkbook.find(({ sheet }) => sheet === "Metadata").data.slice(1) + ); + const xlsxExportOk = xlsxDownload.suggestedFilename() === "solavin_iv_analysis.xlsx" && + exportedWorkbook.map(({ sheet }) => sheet).join("|") === "Metrics|Raw Data|Metadata" && + exportedMetadata["Solavin analysis version"] === "4.0.0" && + exportedMetadata["Analysis method"] === "piecewise-linear-v4" && + exportedMetadata["Source file"] === "sparse_iv.csv" && + exportedMetadata["Cell area (cm2)"] === 0.01 && + exportedMetadata["Irradiance (W/m2)"] === 1000; + + // Exercise the browser-side XLSX reader and multi-sheet picker with a + // workbook generated independently in Node. + await page.getByRole("button", { name: "Import Data", exact: true }).click(); + const workbookBuffer = await writeExcelFile([ + { + sheet: "Run A", + data: [["Voltage (V)", "Cell X"], [0, 1], [0.5, 0.6], [1, 0], [1.1, -0.1]], + }, + { + sheet: "Run B", + data: [["Voltage (V)", "Cell Y"], [0, 2], [0.5, 1.2], [1, 0], [1.1, -0.2]], + }, + ]).toBuffer(); + await page.locator('input[type="file"]').setInputFiles({ + name: "browser_fixture.xlsx", + mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + buffer: workbookBuffer, + }); + await page.getByText("Select Sheet", { exact: true }).waitFor(); + await page.getByRole("button", { name: "Run B", exact: true }).click(); + const xlsxImport = page.getByText("browser_fixture (Run B)", { exact: false }).first(); + await xlsxImport.waitFor(); + const xlsxImportOk = await xlsxImport.isVisible(); + const fail = []; if (errors.length) fail.push("runtime errors: " + JSON.stringify(errors)); if (!welcomeShown) fail.push("orientation (welcome) panel did not appear on first load"); if (!ffOk) fail.push("fill factors not all in (0,100)%: " + JSON.stringify(ffValues)); if (!hasEff) fail.push("efficiency column missing"); if (!assistantOk) fail.push("assistant did not answer fill-factor query"); + if (!chartIntegrity) fail.push("chart SVG paths were missing or contained NaN/Infinity"); + if (!sparseAlignmentOk) fail.push("sparse channels were not preserved at their correct voltages"); + if (!csvExportOk) fail.push("CSV export did not produce the expected download"); + if (!xlsxExportOk) fail.push("XLSX export did not produce the expected download"); + if (!xlsxImportOk) fail.push("browser XLSX import or multi-sheet selection failed"); if (fail.length) { console.error("✗ e2e smoke test FAILED:\n - " + fail.join("\n - ")); process.exitCode = 1; } else { - console.log("✓ e2e smoke test passed — FF values:", ffValues.map((f) => f.toFixed(1)).join(", "), "%"); + console.log("✓ e2e passed — charts, sparse CSV + multi-sheet XLSX import, assistant, efficiency, CSV/XLSX exports; FF:", ffValues.map((f) => f.toFixed(1)).join(", "), "%"); } } finally { await browser.close(); diff --git a/vite.config.js b/vite.config.js index 828d8d2..f772e2f 100644 --- a/vite.config.js +++ b/vite.config.js @@ -13,5 +13,16 @@ export default defineConfig({ test: { environment: "node", include: ["src/**/*.test.js"], + coverage: { + provider: "v8", + reporter: ["text", "json-summary", "html"], + include: ["src/lib/ivAnalysis.js"], + thresholds: { + statements: 90, + branches: 80, + functions: 90, + lines: 90, + }, + }, }, }); From 8c04aab6f71d9c79d1e41d3dbb0daa8ded2cb372 Mon Sep 17 00:00:00 2001 From: John Uy Date: Thu, 30 Jul 2026 10:11:59 -0700 Subject: [PATCH 2/3] Fix guided-tour browser test --- src/components/Tour.jsx | 2 +- tests/e2e.smoke.mjs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Tour.jsx b/src/components/Tour.jsx index 69745a9..fd1496b 100644 --- a/src/components/Tour.jsx +++ b/src/components/Tour.jsx @@ -100,7 +100,7 @@ export function TourOverlay(p) {
GUIDED WALKTHROUGH
- +

{step.title}

{step.desc}

diff --git a/tests/e2e.smoke.mjs b/tests/e2e.smoke.mjs index f03f8d8..94c4400 100644 --- a/tests/e2e.smoke.mjs +++ b/tests/e2e.smoke.mjs @@ -69,8 +69,9 @@ try { const closeWelcome = page.getByRole("button", { name: "Start analysing" }); if (await closeWelcome.count()) await closeWelcome.first().click(); await page.waitForTimeout(400); - const dismiss = page.getByRole("button", { name: "Dismiss" }); - if (await dismiss.count()) await dismiss.first().click(); + const dismissTour = page.getByRole("button", { name: "Dismiss guided tour", exact: true }); + await dismissTour.click(); + await dismissTour.waitFor({ state: "detached" }); // Visit every visualization tab (including the synced dual view). await page.getByRole("button", { name: "Visualizations", exact: true }).click(); From 3afe2b569308961a3d577d7882f2f431474ed376 Mon Sep 17 00:00:00 2001 From: John Uy Date: Thu, 30 Jul 2026 10:19:37 -0700 Subject: [PATCH 3/3] Target raw-data browser assertions precisely --- src/components/shared.jsx | 2 +- tests/e2e.smoke.mjs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/shared.jsx b/src/components/shared.jsx index b0a7b57..1503a75 100644 --- a/src/components/shared.jsx +++ b/src/components/shared.jsx @@ -239,7 +239,7 @@ export function RawDataViewer(p) { )}
{ const st = e.currentTarget.scrollTop; setScrollTop((prev) => (Math.abs(prev - st) >= ROW_H ? st : prev)); }} style={{ maxHeight: 220, overflow: "auto" }}> -
{h}
{r.cond}{(r.current / iSc.div).toFixed(4)}{Number.isFinite(r.current) ? (r.current / iSc.div).toFixed(4) : "—"} {r.source.toUpperCase()}
{row.voltage.toFixed(2)}{pt ? pt.rawCurrent.toExponential(3) : "-"}{Number.isFinite(value) ? value.toExponential(3) : "—"}
+
{allCols.map((h, i) => { const isSearched = h === (searchCol === "voltage" ? "voltage" : searchCol); return ; })}{(() => { // Virtual window: render only ~15 visible rows plus overscan; diff --git a/tests/e2e.smoke.mjs b/tests/e2e.smoke.mjs index 94c4400..5ec618f 100644 --- a/tests/e2e.smoke.mjs +++ b/tests/e2e.smoke.mjs @@ -135,19 +135,22 @@ try { buffer: Buffer.from(sparseCsv), }); await page.getByText("sparse_iv (CSV)", { exact: false }).first().waitFor(); - const sparseAlignmentOk = await page.evaluate(() => { - const table = [...document.querySelectorAll("table")].find((candidate) => - candidate.innerText.includes("Cell A") && candidate.innerText.includes("Cell B") - ); - if (!table) return false; + const rawDataTable = page.getByRole("table", { name: "Raw I-V measurements", exact: true }); + await rawDataTable.waitFor(); + await rawDataTable.getByRole("columnheader", { name: "Cell A", exact: true }).waitFor(); + const sparseAlignment = await rawDataTable.evaluate((table) => { const rows = [...table.querySelectorAll("tbody tr")].map((row) => [...row.querySelectorAll("td")].map((cell) => cell.innerText.trim()) ); const atHalfVolt = rows.find((row) => row[0] === "0.50"); const atOneVolt = rows.find((row) => row[0] === "1.00"); - return atHalfVolt?.[1] === "—" && atHalfVolt?.[2] === "1.500e+0" && - atOneVolt?.[1] === "5.000e-1" && atOneVolt?.[2] === "—"; + return { + ok: atHalfVolt?.[1] === "—" && atHalfVolt?.[2] === "1.500e+0" && + atOneVolt?.[1] === "5.000e-1" && atOneVolt?.[2] === "—", + rows, + }; }); + const sparseAlignmentOk = sparseAlignment.ok; await page.getByRole("button", { name: "Metrics & Export", exact: true }).click(); await page.getByText(/skipped 1 row\(s\).*no zero values were inserted/i).first().waitFor(); @@ -207,7 +210,7 @@ try { if (!hasEff) fail.push("efficiency column missing"); if (!assistantOk) fail.push("assistant did not answer fill-factor query"); if (!chartIntegrity) fail.push("chart SVG paths were missing or contained NaN/Infinity"); - if (!sparseAlignmentOk) fail.push("sparse channels were not preserved at their correct voltages"); + if (!sparseAlignmentOk) fail.push("sparse channels were not preserved at their correct voltages; observed rows: " + JSON.stringify(sparseAlignment.rows)); if (!csvExportOk) fail.push("CSV export did not produce the expected download"); if (!xlsxExportOk) fail.push("XLSX export did not produce the expected download"); if (!xlsxImportOk) fail.push("browser XLSX import or multi-sheet selection failed");
{i === 0 ? "V (V)" : h}