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 = Isc Voc /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 &&
- ONLINE SMU-2400
+ LOCAL NO 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. */}
- setChatOpen(!chatOpen)} className="press" style={{ display: "flex", alignItems: "center", gap: 9, width: "100%", padding: sideOpen ? "10px 12px" : "10px 0", borderRadius: 8, border: "none", background: "linear-gradient(135deg," + t.accent + "," + t.accent2 + ")", color: "#fff", fontSize: 11.5, fontWeight: 700, justifyContent: sideOpen ? "flex-start" : "center", boxShadow: chatOpen ? "0 0 0 1px " + t.accent + "55 inset" : "0 4px 16px " + t.accentG }}> {sideOpen && Lab Assistant }{sideOpen && AI }
+ setChatOpen(!chatOpen)} className="press" style={{ display: "flex", alignItems: "center", gap: 9, width: "100%", padding: sideOpen ? "10px 12px" : "10px 0", borderRadius: 8, border: "none", background: "linear-gradient(135deg," + t.accent + "," + t.accent2 + ")", color: "#fff", fontSize: 11.5, fontWeight: 700, justifyContent: sideOpen ? "flex-start" : "center", boxShadow: chatOpen ? "0 0 0 1px " + t.accent + "55 inset" : "0 4px 16px " + t.accentG }}> {sideOpen && Lab Assistant }{sideOpen && LOCAL }
setSideOpen(!sideOpen)} className="mono" style={{ borderTop: "1px solid " + t.border, background: "none", border: "none", padding: "9px 10px", color: t.textD, display: "flex", alignItems: "center", justifyContent: sideOpen ? "space-between" : "center", gap: 5, fontSize: 9, width: "100%", letterSpacing: ".1em" }}>{sideOpen && COLLAPSE }
@@ -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
-
-
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 Profile NORMALIZED
-
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) =>
doLoad(sheetPicker.file.name, sheetPicker.sheets, s)} style={{ display: "block", width: "100%", padding: "9px 13px", marginBottom: 4, borderRadius: 8, border: "1px solid " + t.border, background: t.inputBg, color: t.text, fontSize: 11, textAlign: "left" }}>{s} )}
setSheetPicker(null)} style={{ marginTop: 6, background: "none", border: "none", color: t.textM, fontSize: 10 }}>Cancel }
+ {sheetPicker && Select Sheet
{sheetPicker.valid.map((s) =>
doLoad(sheetPicker.fileName, sheetPicker.sheets, s)} style={{ display: "block", width: "100%", padding: "9px 13px", marginBottom: 4, borderRadius: 8, border: "1px solid " + t.border, background: t.inputBg, color: t.text, fontSize: 11, textAlign: "left" }}>{s} )}
setSheetPicker(null)} style={{ marginTop: 6, background: "none", border: "none", color: t.textM, fontSize: 10 }}>Cancel }
);
}
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 ↗
props.onEnter({ id: "guest", name: "Guest", isGuest: true })} className="press" style={{ padding: "11px 22px", borderRadius: 10, border: "1px solid rgba(255,255,255,.22)", background: GRAD, color: "#04140d", fontSize: 13, fontWeight: 800, letterSpacing: ".01em", boxShadow: "0 8px 24px rgba(34,211,238,.4)", display: "inline-flex", alignItems: "center", gap: 8, animation: "glowPulse 2.2s ease-in-out infinite" }}> Try the live demo→
@@ -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/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
- SKIP ✕
+ SKIP ✕
{step.title}
{step.desc}
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) => {h} )}
diff --git a/src/components/shared.jsx b/src/components/shared.jsx
index 451ecc5..1503a75 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) => (
{r.cond}
- {(r.current / iSc.div).toFixed(4)}
+ {Number.isFinite(r.current) ? (r.current / iSc.div).toFixed(4) : "—"}
{r.source.toUpperCase()}
))}
@@ -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;
@@ -226,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" }}>
-
+
{allCols.map((h, i) => { const isSearched = h === (searchCol === "voltage" ? "voltage" : searchCol); return {i === 0 ? "V (V)" : h} ; })}
{(() => {
// Virtual window: render only ~15 visible rows plus overscan;
@@ -241,7 +254,7 @@ export function RawDataViewer(p) {
out.push(
{row.voltage.toFixed(2)}
- {ds.conditions.map((c, ci) => { const pt = ds.ivData[c][ri]; return {pt ? pt.rawCurrent.toExponential(3) : "-"} ; })}
+ {ds.conditions.map((c, ci) => { const value = row[c]; return {Number.isFinite(value) ? value.toExponential(3) : "—"} ; })}
);
}
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..5ec618f 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));
@@ -60,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();
@@ -70,6 +80,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 +96,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 +118,108 @@ 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 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 {
+ 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();
+
+ 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; 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");
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,
+ },
+ },
},
});