diff --git a/.gitattributes b/.gitattributes index 7ebe0b9..1d558c2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ -models/*.json text eol=lf -evidence/*.json text eol=lf +# Vite's committed production bundle contains third-party template-literal whitespace (not authored source). +web/assets/*.js -whitespace diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f58f246..d541735 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,11 @@ on: push: branches: [main] pull_request: + schedule: + - cron: "17 3 * * 1" permissions: - contents: write + contents: read jobs: simulator: @@ -16,71 +18,81 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" - - name: Unit tests + - name: Python unit and contract tests run: python -m unittest discover -s tests -v - - name: Deterministic experiment smoke test + - name: Deterministic experiment smoke run: python scripts/run_experiment.py --scenario evening --seconds 180 --seeds 2 --output /tmp/elevator-smoke.json - - name: Destination-control experiment smoke test + - name: Destination-control smoke run: python scripts/run_experiment.py --scenario evening --seconds 120 --seeds 3 --control-mode destination --output /tmp/elevator-destination-smoke.json - - name: M3 30-seed six-scenario evidence matrix - if: github.event_name == 'pull_request' - run: | - mkdir -p evidence - python scripts/run_experiment.py --matrix --seconds 180 --seeds 30 --output evidence/m3-evidence.json - - name: M3 statistical regression baseline - if: github.event_name == 'pull_request' - run: python scripts/check_regression_baseline.py evidence/m3-evidence.json - - name: Upload M3 evidence artifacts - if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v4 + - name: Versioned artifact and trace validation + run: python scripts/validate_artifacts.py + - name: Release audit + run: python scripts/audit_release.py + + frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - name: m3-statistical-evidence - path: evidence/ - retention-days: 14 + node-version: "22" + cache: npm + cache-dependency-path: frontend/package-lock.json + - name: Install frontend dependencies + run: npm ci --prefix frontend + - name: TypeScript + run: npm run typecheck --prefix frontend + - name: Frontend unit tests + run: npm test --prefix frontend + - name: Production build into web + run: npm run build --prefix frontend + + browser: + runs-on: ubuntu-latest + needs: [simulator, frontend] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" - uses: actions/setup-node@v4 - if: github.event_name == 'pull_request' && github.head_ref == 'feat/m4-digital-twin-replay' with: node-version: "22" - - name: Install browser QA dependencies - if: github.event_name == 'pull_request' && github.head_ref == 'feat/m4-digital-twin-replay' + cache: npm + - name: Install browser and frontend dependencies run: | - npm install + npm ci + npm ci --prefix frontend + npm run build --prefix frontend npx playwright install --with-deps chromium - - name: Start simulator web server - if: github.event_name == 'pull_request' && github.head_ref == 'feat/m4-digital-twin-replay' - run: | - python -m app.server --port 4173 > /tmp/elevator-server.log 2>&1 & - echo $! > /tmp/elevator-server.pid - for attempt in {1..30}; do - if curl --fail --silent http://127.0.0.1:4173/api/snapshot > /dev/null; then - exit 0 - fi - sleep 1 - done - cat /tmp/elevator-server.log - exit 1 - - name: M4 browser API/replay consistency - if: github.event_name == 'pull_request' && github.head_ref == 'feat/m4-digital-twin-replay' + - name: Playwright E2E, responsive and visual regression run: npm run test:e2e - - name: Preserve M4 README screenshot on PR branch - if: github.event_name == 'pull_request' && github.head_ref == 'feat/m4-digital-twin-replay' && hashFiles('docs/assets/m4-dashboard.png') == '' - env: - HEAD_BRANCH: ${{ github.head_ref }} - run: | - git fetch origin "$HEAD_BRANCH" - git switch -C "$HEAD_BRANCH" "origin/$HEAD_BRANCH" - mkdir -p docs/assets - cp artifacts/m4-dashboard-desktop.png docs/assets/m4-dashboard.png - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add docs/assets/m4-dashboard.png - git commit -m "docs(m4): capture validated digital twin" - git push origin "HEAD:$HEAD_BRANCH" - - name: Upload M4 visual evidence - if: always() && github.event_name == 'pull_request' && github.head_ref == 'feat/m4-digital-twin-replay' + - name: Upload browser QA evidence + if: always() uses: actions/upload-artifact@v4 with: - name: m4-visual-evidence - path: artifacts/ + name: m8-browser-qa + path: | + artifacts/ + test-results/ if-no-files-found: warn retention-days: 14 + + statistical-regression: + if: github.event_name == 'push' || github.event_name == 'schedule' || contains(github.event.pull_request.labels.*.name, 'full-statistical-regression') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: M3 30-seed six-scenario evidence matrix + run: python scripts/run_experiment.py --matrix --seconds 180 --seeds 30 --output /tmp/m3-evidence.json + - name: M3 statistical regression baseline + run: python scripts/check_regression_baseline.py /tmp/m3-evidence.json + - name: Upload statistical evidence + uses: actions/upload-artifact@v4 + with: + name: m3-statistical-regression + path: /tmp/m3-evidence.json + retention-days: 14 diff --git a/.gitignore b/.gitignore index bed71b1..ba6402d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ coverage/ .DS_Store Thumbs.db *.log +*.tsbuildinfo experiment-*.json trace-*.json +frontend/vite.config.js +frontend/vite.config.d.ts diff --git a/README.md b/README.md index 481f238..20d6844 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,30 @@ # Elevator Queue Lab -**A reproducible elevator group-control research lab for an 18-floor office building.** +**A reproducible decision-intelligence workbench for elevator group-control research.** **Live demo:** [https://elevator.oosu.dev/](https://elevator.oosu.dev/) -Elevator Queue Lab starts from a practical failure mode: a hall call is assigned to a car, that -car later becomes full or follows a poor route, and the passenger waits while the controller keeps -treating the stale assignment as valid. The project turns that observation into a controlled -simulation and optimization problem. +Elevator Queue Lab lets a reviewer **simulate, observe, inspect, explain, replay, compare and +falsify** dispatch decisions from passenger-level evidence. It starts from a practical failure +mode: a hall call is assigned to a car, that car later becomes full or follows a poor route, and +the passenger waits while the controller keeps treating the stale assignment as valid. + +## 30-second orientation + +| Question | Answer | +| --- | --- | +| **What problem is being studied?** | Stale elevator assignments under stochastic load: a car that looked good at assignment time may become a poor or impossible pickup before it arrives. | +| **What is CAPR?** | Capacity-Aware Predictive Reassignment: route-insertion ETA + predicted residual capacity + route/load/age scoring + continuous reassignment with hysteresis. | +| **What did the experiments show?** | CAPR is **traffic-regime dependent**, not globally superior. Lunch is a clean M3 candidate improvement; several other regimes expose wait/energy trade-offs or no mean-wait win. | +| **What is Counterflow Criticality?** | M7 evidence supports a fuzzy congestion × counterflow transition: continuous reassignment becomes more valuable when traffic intensity and opposing directional flow rise together. The fitted threshold is **not** a universal critical constant. | +| **Did RL beat the heuristics?** | No general superiority was established. The fixed M5 Dueling Double DQN improves only the held-out mixed-day mixture cleanly; five base regimes regress versus collective. | +| **Can I reproduce it?** | Yes. The repository keeps deterministic traces, versioned provenance, committed statistical evidence, a production Python server path, unit/contract tests, Playwright E2E and visual regression. | + +The product flow is: + +```text +SIMULATE → OBSERVE → INSPECT → EXPLAIN → REPLAY → COMPARE → FALSIFY +``` The target building has **18 floors and six passenger elevators**: three low-zone cars and three high-zone cars. The default synthetic workplace mix is deliberately lobby-centric: **85% of trips @@ -17,11 +34,12 @@ passenger is represented from arrival at a hall call through boarding and destination arrival, so dispatch decisions can be evaluated on passenger outcomes instead of visual car movement alone. -![Elevator Queue Lab live digital twin](docs/assets/m6-dashboard.png) +![Elevator Queue Lab M8 decision intelligence workbench](docs/assets/showcase/m8-live-operations.png) -The screenshot above is captured from the deployed public simulator at `elevator.oosu.dev` after -Chromium verifies the live UI/API contract: all six cars, all 18 floors, the evidence cards, the -learned-policy control path and simulator audit. It is not a mockup or static chart fixture. +The screenshot above is captured by Playwright from the real Python server and production React +build after CAPR has produced a reassignment. It is not a mockup or static chart fixture. The public +demo uses the same single-process Python/static-serving contract; this exact M8 commit is only +considered publicly verified after it has been deployed and remote QA has passed. > **Research takeaway — congestion alone is not the trigger.** Continuous predictive reassignment > becomes valuable when **heavy traffic and enough counterflow rise together**, because opposing @@ -44,7 +62,12 @@ superiority**. ## Current executable surface +- React + TypeScript authored frontend in `frontend/`, with Vite producing the committed `web/` + production artifact so `python -m app.server --port 4173` remains the one-command runtime; +- Decision Intelligence Product Shell with Live Operations, Runs, Dispatch Analysis, Experiments, + Counterflow Criticality, Models and Object Explorer workbenches; - deterministic origin/destination passenger traces with canonical JSON + SHA-256 identity; +- versioned trace/run/dispatch/evidence artifact catalog with seed/scenario/policy/config provenance; - sub-second six-car simulator with acceleration/deceleration, doors and passenger transfer time; - low/high zoned banks and both conventional hall-call and destination-control grouping; - sticky, nearest-car, collective, queue-aware and CAPR dispatch policies; @@ -55,6 +78,13 @@ superiority**. - floor queue heatmap plus live/replay wait and queue time series sourced only from simulator state; - dispatch event stream and candidate-level decision inspector for assignment/reassignment reasoning; - deterministic saved-run replay with a timeline scrubber and live/replay state switching; +- browseable Elevator/Passenger/HallCall/DispatchDecision/Run/Experiment/Model/Evidence objects; +- read-only Decision Trace graph projected from simulator state, decision history and the event + ledger, with pan/zoom/fit-view and inspector linkage; +- deterministic **Ask This Run** explanations backed by recorded evidence and committed M3 + comparisons; no LLM is required or treated as a source of truth; +- typed semantic `ChartSpec` validation and registered React renderers instead of arbitrary + generated HTML/JavaScript; - 30-seed common-random-number experiment engine with morning/lunch/normal/evening/shock/mixed-day; - P50/P95/P99 wait, journey time, throughput, unfinished queue, reassignment latency, floor fairness, capacity misses and a transparent comparative energy proxy; @@ -70,7 +100,9 @@ superiority**. - `rl` runtime policy selectable in the live digital twin using the checked-in M5 model; - JSON + run-level CSV + summary CSV evidence artifacts, paired effect sizes and guardrail flags; - Playwright browser verification that visible metrics, all six cars and all 18 floor queues match API/replay state; -- responsive desktop/mobile visual QA with screenshots generated from that same verified browser run. +- deterministic visual regression for Live Operations desktop/mobile, Replay, Experiments, Theory, + Models and Explorer/Decision Trace; +- `npm run capture:showcase` portfolio capture from the real backend and production frontend. ## 30-seed evidence: CAPR is regime-dependent @@ -173,21 +205,33 @@ limitations, and `docs/M6_EVIDENCE_SUMMARY.md` for tables generated directly fro ## Run locally -Python 3.11+ is enough for the simulator and research server. +Python 3.11+ is enough to run the committed production workbench. Node is only required when +changing or rebuilding the React frontend. ```bash python -m app.server --port 4173 ``` Open `http://127.0.0.1:4173`. The UI can switch traffic regime, policy, simulation speed and -conventional versus destination-control call input, save the current run, scrub deterministic -replay frames and inspect the M3 comparison evidence. +conventional versus destination-control call input, save and scrub replay, inspect dispatch +decisions, browse run objects and Decision Trace, query deterministic explanations and inspect +committed M3/M5/M7 evidence. + +Frontend contributor gates: + +```bash +npm ci --prefix frontend +npm run typecheck --prefix frontend +npm test --prefix frontend +npm run build --prefix frontend +``` Run validation and evidence generation: ```bash python -m unittest discover -s tests -v python scripts/generate_trace.py --scenario evening --seconds 600 --seed 7 --output /tmp/evening-trace.json +python scripts/generate_trace.py --scenario evening --seconds 600 --seed 7 --output /tmp/evening-trace.json --package-dir /tmp/evening-trace-package python scripts/run_experiment.py --scenario evening --seconds 600 --seeds 3 python scripts/run_experiment.py --scenario evening --seconds 600 --seeds 3 --control-mode destination python scripts/run_experiment.py --matrix --seconds 180 --seeds 30 --output evidence/m3-evidence.json @@ -201,24 +245,32 @@ python scripts/audit_release.py For browser/API visual verification: ```bash -npm install +npm ci +npm ci --prefix frontend +npm run frontend:build npx playwright install chromium -python -m app.server --port 4173 npm run test:e2e +npm run capture:showcase ``` +Playwright starts the production Python server automatically unless `BASE_URL` points at an +already-running instance. Intentional visual-baseline updates use `npm run test:e2e:update`. + The matrix command emits a self-describing JSON artifact plus `*.runs.csv` and `*.summary.csv`. Artifacts explicitly record a zero-second warm-up and the measurement window used by the run. ## Project status -**M0 reproducibility through M6 portfolio release are implemented.** The fixed M5 checkpoint has a -30-seed held-out release artifact, the final report/plots are generated from committed evidence, -and the public Mac mini deployment is live at `https://elevator.oosu.dev/`. External Chromium QA -verifies HTTP 200, six cars, all 18 floors, five evidence cards, the RL `mixed_day` control path, -simulator audit success, zero failed browser requests and zero console errors. -`docs/ROADMAP.md` is the canonical work queue and `AGENTS.md` defines the continuation contract for -future coding sessions. +**M0 through M8 are implemented in the repository.** The research simulator, 30-seed evidence, +negative/mixed learned-controller result and M7 falsification evidence are preserved; M8 adds the +typed decision-intelligence product layer without rewriting the simulator core. See +`docs/M8_PRODUCT_WORKBENCH.md` for the architecture and migration contract. + +The existing public Mac mini demo remains at `https://elevator.oosu.dev/`. A local or PR M8 result +is not treated as proof that the public instance has been upgraded: deployment and remote +Chromium/API QA must be run against the exact M8 commit before that claim is made. +`docs/ROADMAP.md` remains the canonical milestone record and `AGENTS.md` defines the continuation +contract. ## Methodology references @@ -239,4 +291,4 @@ MIT, unless a later dependency or imported dataset requires a narrower notice. [`control-systems`](https://github.com/topics/control-systems) · [`dispatch-algorithm`](https://github.com/topics/dispatch-algorithm) · [`elevator`](https://github.com/topics/elevator) · [`elevator-control`](https://github.com/topics/elevator-control) · [`gymnasium`](https://github.com/topics/gymnasium) · [`machine-learning`](https://github.com/topics/machine-learning) · [`operations-research`](https://github.com/topics/operations-research) · [`optimization`](https://github.com/topics/optimization) · [`queueing-theory`](https://github.com/topics/queueing-theory) · [`reinforcement-learning`](https://github.com/topics/reinforcement-learning) · [`research-tool`](https://github.com/topics/research-tool) · [`simulation`](https://github.com/topics/simulation) · [`traffic-simulation`](https://github.com/topics/traffic-simulation) **Implementation stack / 구현 스택**
-[`python`](https://github.com/topics/python) +[`python`](https://github.com/topics/python) · [`react`](https://github.com/topics/react) · [`typescript`](https://github.com/topics/typescript) · [`playwright`](https://github.com/topics/playwright) diff --git a/app/artifacts.py b/app/artifacts.py new file mode 100644 index 0000000..00ce6b0 --- /dev/null +++ b/app/artifacts.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import hashlib +import json +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from .domain import SimulationConfig +from .simulator import ElevatorSimulation +from .trace import PassengerTrace + + +ROOT = Path(__file__).resolve().parents[1] +SIMULATOR_VERSION = "0.1.0" +ARTIFACT_VERSION = "1.0.0" + + +def _created_at() -> str: + """Return an ISO-8601 timestamp for transient artifact envelopes.""" + + return datetime.now(UTC).isoformat() + + +def _sha256_json(payload: object) -> str: + """Hash a JSON-compatible value with deterministic serialization.""" + + raw = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") + return hashlib.sha256(raw).hexdigest() + + +def build_passenger_trace_manifest( + trace: PassengerTrace, + config: SimulationConfig, +) -> dict[str, Any]: + """Build the canonical versioned manifest for a materialized passenger trace.""" + + identity_payload = { + "scenario": trace.scenario, + "seed": trace.seed, + "building": config.as_dict(), + "trace_sha256": trace.digest, + } + return { + "schema_version": "elevator-queue-lab.trace-manifest.v1", + "artifact_version": ARTIFACT_VERSION, + "generator_version": "elevator-queue-lab.demand.v2", + "seed": trace.seed, + "scenario": trace.scenario, + "building_configuration": config.as_dict(), + "traffic_contract": "office-demand-85-10-5", + "duration_seconds": trace.duration_seconds, + "sha256": trace.digest, + "identity_sha256": _sha256_json(identity_payload), + "created_at": _created_at(), + "source": "materialized_trace", + } + + +def build_trace_manifest(simulation: ElevatorSimulation) -> dict[str, Any]: + """Describe the deterministic passenger source without mutating it.""" + + if simulation.trace is not None: + return build_passenger_trace_manifest(simulation.trace, simulation.config) + identity_payload = { + "scenario": simulation.scenario, + "seed": simulation.seed, + "building": simulation.config.as_dict(), + "trace_sha256": None, + } + return { + "schema_version": "elevator-queue-lab.trace-manifest.v1", + "artifact_version": ARTIFACT_VERSION, + "generator_version": "elevator-queue-lab.demand.v2", + "seed": simulation.seed, + "scenario": simulation.scenario, + "building_configuration": simulation.config.as_dict(), + "traffic_contract": "office-demand-85-10-5", + "duration_seconds": int(simulation.sim_time), + "sha256": None, + "identity_sha256": _sha256_json(identity_payload), + "created_at": _created_at(), + "source": "seeded_demand_generator", + } + + +def build_run_artifact(simulation: ElevatorSimulation, run_id: str) -> dict[str, Any]: + """Create a versioned envelope around the current simulator run.""" + + snapshot = simulation.snapshot() + trace_manifest = build_trace_manifest(simulation) + created_at = _created_at() + provenance = { + "schema_version": "elevator-queue-lab.provenance.v1", + "artifact_version": ARTIFACT_VERSION, + "simulator_version": SIMULATOR_VERSION, + "source": "live_simulator", + "seed": simulation.seed, + "scenario": simulation.scenario, + "policy": simulation.policy_name, + "config": simulation.config.as_dict(), + "created_at": created_at, + "trace_sha256": trace_manifest["sha256"], + "trace_identity_sha256": trace_manifest["identity_sha256"], + "evidence_source": None, + "evidence_sources": [ + "evidence/m3-regression-baseline.json", + "evidence/m5-heldout-evaluation.json", + "evidence/m6-heldout-30seed.json", + "evidence/m7-bidirectional-load-sweep.json", + "evidence/m7-threshold-validation.json", + ], + } + return { + "schema_version": "elevator-queue-lab.run.v1", + "artifact_version": ARTIFACT_VERSION, + "simulator_version": SIMULATOR_VERSION, + "run_id": run_id, + "scenario": simulation.scenario, + "policy": simulation.policy_name, + "seed": simulation.seed, + "sim_time": snapshot["sim_time"], + "trace_sha256": trace_manifest["sha256"], + "metrics": snapshot["metrics"], + "provenance": provenance, + "trace_manifest": trace_manifest, + "created_at": created_at, + } + + +def build_artifact_catalog(simulation: ElevatorSimulation, run_id: str) -> dict[str, Any]: + """Expose committed evidence and the live run as typed artifact references.""" + + created_at = _created_at() + trace_manifest = build_trace_manifest(simulation) + live_provenance = { + "schema_version": "elevator-queue-lab.provenance.v1", + "artifact_version": ARTIFACT_VERSION, + "simulator_version": SIMULATOR_VERSION, + "source": "live_simulator", + "seed": simulation.seed, + "scenario": simulation.scenario, + "policy": simulation.policy_name, + "config": simulation.config.as_dict(), + "created_at": created_at, + "trace_sha256": trace_manifest["sha256"], + "evidence_source": None, + } + committed = [ + ("ExperimentArtifact", "evidence/m3-regression-baseline.json"), + ("PolicyEvaluationArtifact", "evidence/m5-heldout-evaluation.json"), + ("PolicyEvaluationArtifact", "evidence/m6-heldout-30seed.json"), + ("TheoryEvidenceArtifact", "evidence/m7-bidirectional-load-sweep.json"), + ("TheoryEvidenceArtifact", "evidence/m7-threshold-validation.json"), + ("ModelArtifact", "models/m5-ddqn-baseline.json"), + ] + artifacts: list[dict[str, Any]] = [ + { + "artifact_type": "SimulationRunArtifact", + "schema_version": "elevator-queue-lab.run.v1", + "artifact_version": ARTIFACT_VERSION, + "artifact_id": run_id, + "source": "live_simulator", + "sha256": None, + "provenance": dict(live_provenance), + }, + { + "artifact_type": "PassengerTraceArtifact", + "schema_version": trace_manifest["schema_version"], + "artifact_version": ARTIFACT_VERSION, + "artifact_id": f"{run_id}:trace", + "source": trace_manifest["source"], + "sha256": trace_manifest["sha256"], + "provenance": { + **live_provenance, + "source": trace_manifest["source"], + }, + }, + { + "artifact_type": "DispatchDecisionArtifact", + "schema_version": "elevator-queue-lab.dispatch-decisions.v1", + "artifact_version": ARTIFACT_VERSION, + "artifact_id": f"{run_id}:dispatch-decisions", + "source": "decision_history", + "sha256": _sha256_json(simulation.decision_history), + "provenance": { + **live_provenance, + "source": "decision_history", + }, + } + ] + for artifact_type, relative_path in committed: + path = ROOT / relative_path + if not path.is_file(): + continue + raw = path.read_bytes() + payload = json.loads(raw) + artifacts.append( + { + "artifact_type": artifact_type, + "schema_version": payload.get("schema", "unknown"), + "artifact_version": ARTIFACT_VERSION, + "artifact_id": relative_path, + "source": relative_path, + "sha256": hashlib.sha256(raw).hexdigest(), + "provenance": { + "schema_version": "elevator-queue-lab.provenance.v1", + "artifact_version": ARTIFACT_VERSION, + "simulator_version": SIMULATOR_VERSION, + "source": relative_path, + "seed": None, + "scenario": None, + "policy": None, + "config": None, + "created_at": created_at, + "trace_sha256": None, + "evidence_source": relative_path, + }, + } + ) + return { + "schema": "elevator-queue-lab.artifact-catalog.v1", + "run_id": run_id, + "artifacts": artifacts, + } diff --git a/app/server.py b/app/server.py index 81bf5ad..a716330 100644 --- a/app/server.py +++ b/app/server.py @@ -6,14 +6,23 @@ import mimetypes import threading import time +import uuid from http import HTTPStatus from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from typing import Any -from urllib.parse import urlparse +from urllib.parse import parse_qs, urlparse +from .artifacts import build_run_artifact from .domain import SimulationConfig from .simulator import ElevatorSimulation +from .workbench import ( + answer_run_question, + build_artifacts_payload, + build_decision_graph, + build_models_payload, + build_objects, +) ROOT = Path(__file__).resolve().parents[1] @@ -33,6 +42,7 @@ def __init__(self) -> None: self.scenario = "morning" self.policy = "capr" self.control_mode = "conventional" + self.run_id = self._new_run_id() self.simulation = self._new_simulation() self.replay_frames: list[dict[str, Any]] = [] self.saved_replay: dict[str, Any] | None = None @@ -42,6 +52,9 @@ def __init__(self) -> None: self.thread = threading.Thread(target=self._loop, daemon=True) self.thread.start() + def _new_run_id(self) -> str: + return f"run-{uuid.uuid4().hex[:12]}" + def _new_simulation(self) -> ElevatorSimulation: return ElevatorSimulation( self.scenario, @@ -104,7 +117,7 @@ def control(self, payload: dict[str, Any]) -> dict[str, Any]: if "speed" in payload: self.speed = max(1, min(120, int(payload["speed"]))) requires_reset = ( - action == "reset" + action in {"reset", "reset_paused"} or next_scenario != self.scenario or next_policy != self.policy or next_control_mode != self.control_mode @@ -113,11 +126,12 @@ def control(self, payload: dict[str, Any]) -> dict[str, Any]: self.scenario = next_scenario self.policy = next_policy self.control_mode = next_control_mode + self.run_id = self._new_run_id() self.simulation = self._new_simulation() self.replay_frames = [] self._last_replay_second = -1 self._record_replay_frame(force=True) - if action == "pause": + if action in {"pause", "reset_paused"}: self.running = False elif action in {"start", "reset", "update"}: self.running = True @@ -191,6 +205,26 @@ def theory(self) -> dict[str, Any]: "validation": json.loads(M7_VALIDATION.read_text(encoding="utf-8")), } + def run_artifact(self) -> dict[str, Any]: + with self.lock: + return build_run_artifact(self.simulation, self.run_id) + + def objects(self, object_type: str | None = None) -> dict[str, Any]: + with self.lock: + return build_objects(self.simulation, self.run_id, object_type) + + def graph(self) -> dict[str, Any]: + with self.lock: + return build_decision_graph(self.simulation, self.run_id) + + def ask(self, question: str) -> dict[str, Any]: + with self.lock: + return answer_run_question(self.simulation, self.run_id, question) + + def artifacts(self) -> dict[str, Any]: + with self.lock: + return build_artifacts_payload(self.simulation, self.run_id) + class Handler(BaseHTTPRequestHandler): runner: SimulationRunner @@ -224,6 +258,35 @@ def do_GET(self) -> None: # noqa: N802 {"error": str(exc)}, status=HTTPStatus.NOT_FOUND, ) + if parsed.path == "/api/runs": + return self._send_json( + { + "schema": "elevator-queue-lab.runs.v1", + "runs": [self.runner.run_artifact()], + } + ) + if parsed.path == f"/api/runs/{self.runner.run_id}": + return self._send_json(self.runner.run_artifact()) + if parsed.path == f"/api/runs/{self.runner.run_id}/objects": + object_type = parse_qs(parsed.query).get("type", [None])[0] + return self._send_json(self.runner.objects(object_type)) + if parsed.path == f"/api/runs/{self.runner.run_id}/decisions": + return self._send_json(self.runner.objects("DispatchDecision")) + if parsed.path == f"/api/runs/{self.runner.run_id}/graph": + return self._send_json(self.runner.graph()) + if parsed.path == f"/api/runs/{self.runner.run_id}/ask": + question = parse_qs(parsed.query).get("q", [""])[0] + return self._send_json(self.runner.ask(question)) + if parsed.path == "/api/artifacts": + return self._send_json(self.runner.artifacts()) + if parsed.path == "/api/models": + try: + return self._send_json(build_models_payload()) + except FileNotFoundError as exc: + return self._send_json( + {"error": str(exc)}, + status=HTTPStatus.NOT_FOUND, + ) path = WEB_ROOT / ("index.html" if parsed.path == "/" else parsed.path.lstrip("/")) resolved = path.resolve() if not str(resolved).startswith(str(WEB_ROOT.resolve())) or not resolved.is_file(): diff --git a/app/workbench.py b/app/workbench.py new file mode 100644 index 0000000..d78b521 --- /dev/null +++ b/app/workbench.py @@ -0,0 +1,571 @@ +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + +from .artifacts import build_artifact_catalog, build_run_artifact +from .simulator import ElevatorSimulation + + +ROOT = Path(__file__).resolve().parents[1] +M3_BASELINE = ROOT / "evidence" / "m3-regression-baseline.json" +M5_EVALUATION = ROOT / "evidence" / "m5-heldout-evaluation.json" +M6_EVALUATION = ROOT / "evidence" / "m6-heldout-30seed.json" +M7_DISCOVERY = ROOT / "evidence" / "m7-bidirectional-load-sweep.json" +M7_VALIDATION = ROOT / "evidence" / "m7-threshold-validation.json" +M5_MODEL = ROOT / "models" / "m5-ddqn-baseline.json" + +OBJECT_TYPES = ( + "Elevator", + "Passenger", + "HallCall", + "DispatchDecision", + "SimulationRun", + "Scenario", + "Policy", + "Experiment", + "Model", + "Evidence", + "TheoryEvidence", +) + +SCENARIOS = ("morning", "lunch", "normal", "evening", "shock", "mixed_day") +POLICIES = ("legacy_sticky", "nearest_car", "collective", "queue_aware", "capr", "rl") + + +def _load(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def _passenger_objects(simulation: ElevatorSimulation) -> list[dict[str, Any]]: + by_id: dict[int, dict[str, Any]] = {} + for event in simulation.ledger.events: + if event.passenger_id is None: + continue + passenger = by_id.setdefault( + event.passenger_id, + { + "id": f"P-{event.passenger_id}", + "object_type": "Passenger", + "passenger_id": event.passenger_id, + "origin": None, + "destination": None, + "arrival": None, + "boarding": None, + "arrival_at_destination": None, + "elevator_id": None, + "status": "waiting", + "event_sequences": [], + }, + ) + passenger["event_sequences"].append(event.sequence) + if event.kind == "arrival": + passenger["origin"] = event.floor + passenger["destination"] = event.details.get("destination") + passenger["arrival"] = event.sim_time + elif event.kind == "board": + passenger["boarding"] = event.sim_time + passenger["elevator_id"] = event.elevator_id + passenger["status"] = "onboard" + elif event.kind == "alight": + passenger["arrival_at_destination"] = event.sim_time + passenger["status"] = "served" + elif event.kind == "abandon": + passenger["status"] = "abandoned" + + for passenger in by_id.values(): + arrival = passenger["arrival"] + boarding = passenger["boarding"] + finished = passenger["arrival_at_destination"] + passenger["wait"] = ( + round(float(boarding) - float(arrival), 3) + if arrival is not None and boarding is not None + else None + ) + passenger["journey_time"] = ( + round(float(finished) - float(arrival), 3) + if arrival is not None and finished is not None + else None + ) + return sorted(by_id.values(), key=lambda item: int(item["passenger_id"]), reverse=True) + + +def _call_id(call: dict[str, Any]) -> str: + direction = "up" if int(call["direction"]) > 0 else "down" + destination = call.get("destination") + suffix = str(destination) if destination is not None else "any" + return f"C-{call['bank']}-{call['floor']}-{direction}-{suffix}" + + +def _hall_call_objects(simulation: ElevatorSimulation) -> list[dict[str, Any]]: + calls_by_id: dict[str, dict[str, Any]] = {} + for call in simulation.snapshot()["calls"]: + item = dict(call) + item["id"] = _call_id(item) + item["object_type"] = "HallCall" + item["status"] = "active" + item["assignment_history"] = [] + item["reassignment_history"] = [] + item["pickup_history"] = [] + item["final_pickup"] = None + item["queue_size"] = next( + ( + simulation.call_queue_size(key) + for key, active in simulation.hall_calls.items() + if active.floor == item["floor"] + and active.direction == item["direction"] + and active.bank == item["bank"] + and active.destination == item["destination"] + ), + 0, + ) + calls_by_id[item["id"]] = item + + for decision in simulation.decision_history: + callspec = { + "bank": decision.get("bank"), + "floor": decision.get("floor"), + "direction": decision.get("direction"), + "destination": decision.get("destination"), + } + call_id = _call_id(callspec) + item = calls_by_id.setdefault( + call_id, + { + "id": call_id, + "object_type": "HallCall", + "floor": decision.get("floor"), + "direction": decision.get("direction"), + "bank": decision.get("bank"), + "destination": decision.get("destination"), + "assigned": None, + "wait": None, + "missed": 0, + "assigned_score": None, + "queue_size": 0, + "status": "historical", + "assignment_history": [], + "reassignment_history": [], + "pickup_history": [], + "final_pickup": None, + }, + ) + previous = decision.get("current_assignment") + chosen = decision.get("chosen_elevator_id") + assignment = { + "sim_time": decision.get("sim_time"), + "previous_elevator_id": previous, + "selected_elevator_id": chosen, + "queue_size": decision.get("queue_size"), + "reason": decision.get("reason"), + } + item["assignment_history"].append(assignment) + if previous and chosen and previous != chosen: + item["reassignment_history"].append(assignment) + if chosen: + item["assigned"] = chosen + if decision.get("queue_size") is not None: + item["queue_size"] = decision.get("queue_size") + + passengers = {item["passenger_id"]: item for item in _passenger_objects(simulation)} + destination_mode = simulation.config.control_mode == "destination" + for event in simulation.ledger.events: + if event.kind != "board" or event.passenger_id is None: + continue + passenger = passengers.get(event.passenger_id) + if passenger is None: + continue + origin = passenger.get("origin") + destination = passenger.get("destination") + if not isinstance(origin, int) or not isinstance(destination, int): + continue + direction = 1 if destination > origin else -1 + non_lobby_floor = destination if origin == 1 else origin + bank = "low" if non_lobby_floor <= simulation.config.low_zone_max else "high" + call_id = _call_id( + { + "bank": bank, + "floor": origin, + "direction": direction, + "destination": destination if destination_mode else None, + } + ) + item = calls_by_id.get(call_id) + if item is None: + continue + pickup = { + "sim_time": event.sim_time, + "passenger_id": event.passenger_id, + "elevator_id": event.elevator_id, + } + item["pickup_history"].append(pickup) + item["final_pickup"] = pickup + if item["status"] != "active": + item["status"] = "served" + return sorted( + calls_by_id.values(), + key=lambda item: float(item["assignment_history"][-1]["sim_time"] if item["assignment_history"] else 0), + reverse=True, + ) + + +def _decision_objects(simulation: ElevatorSimulation) -> list[dict[str, Any]]: + decisions: list[dict[str, Any]] = [] + for index, decision in enumerate(simulation.decision_history, start=1): + item = dict(decision) + item["id"] = f"D-{index}" + item["object_type"] = "DispatchDecision" + item["policy"] = simulation.policy_name + item["timestamp"] = item.get("sim_time") + callspec = { + "bank": item.get("bank"), + "floor": item.get("floor"), + "direction": item.get("direction"), + "destination": item.get("destination"), + } + item["hall_call_id"] = _call_id(callspec) + decisions.append(item) + return list(reversed(decisions)) + + +def build_objects( + simulation: ElevatorSimulation, + run_id: str, + object_type: str | None = None, +) -> dict[str, Any]: + """Project simulator/evidence state into browseable workbench objects.""" + + snapshot = simulation.snapshot() + m3 = _load(M3_BASELINE) if M3_BASELINE.is_file() else {"scenarios": {}} + m5 = _load(M5_EVALUATION) if M5_EVALUATION.is_file() else {} + model = _load(M5_MODEL) if M5_MODEL.is_file() else {} + collections: dict[str, list[dict[str, Any]]] = { + "Elevator": [ + {"id": item["id"], "object_type": "Elevator", **item} + for item in snapshot["elevators"] + ], + "Passenger": _passenger_objects(simulation), + "HallCall": _hall_call_objects(simulation), + "DispatchDecision": _decision_objects(simulation), + "SimulationRun": [ + { + "id": run_id, + "object_type": "SimulationRun", + **build_run_artifact(simulation, run_id), + } + ], + "Scenario": [ + {"id": f"scenario:{name}", "object_type": "Scenario", "name": name} + for name in SCENARIOS + ], + "Policy": [ + {"id": f"policy:{name}", "object_type": "Policy", "name": name} + for name in POLICIES + ], + "Experiment": [ + { + "id": f"experiment:m3:{name}", + "object_type": "Experiment", + "scenario": name, + "source": "evidence/m3-regression-baseline.json", + "policies": list(payload.get("policies", {})), + } + for name, payload in m3.get("scenarios", {}).items() + ], + "Model": [ + { + "id": "model:m5-ddqn-baseline", + "object_type": "Model", + "schema": model.get("schema"), + "architecture": model.get("architecture"), + "observation_size": model.get("observation_size"), + "actions": model.get("actions", []), + "metadata": model.get("metadata", {}), + "held_out_verdict": m5.get("verdict", {}), + } + ] if model else [], + "Evidence": [ + { + "id": f"evidence:{path.name}", + "object_type": "Evidence", + "source": str(path.relative_to(ROOT)), + "schema": _load(path).get("schema"), + } + for path in (M3_BASELINE, M5_EVALUATION, M6_EVALUATION) + if path.is_file() + ], + "TheoryEvidence": [ + { + "id": f"theory:{path.stem}", + "object_type": "TheoryEvidence", + "source": str(path.relative_to(ROOT)), + "schema": _load(path).get("schema"), + } + for path in (M7_DISCOVERY, M7_VALIDATION) + if path.is_file() + ], + } + selected = collections.get(object_type, []) if object_type else [ + item for name in OBJECT_TYPES for item in collections.get(name, []) + ] + return { + "schema": "elevator-queue-lab.objects.v1", + "run_id": run_id, + "object_types": list(OBJECT_TYPES), + "selected_type": object_type, + "objects": selected, + "counts": {name: len(collections.get(name, [])) for name in OBJECT_TYPES}, + } + + +def build_decision_graph(simulation: ElevatorSimulation, run_id: str) -> dict[str, Any]: + """Build a graph projection without introducing a graph database.""" + + all_passengers = _passenger_objects(simulation) + boarded = [item for item in all_passengers if item.get("boarding") is not None][:24] + unboarded = [item for item in all_passengers if item.get("boarding") is None][:16] + passengers = boarded + unboarded + decisions = _decision_objects(simulation)[:40] + decision_call_ids = {str(item["hall_call_id"]) for item in decisions} + calls = [item for item in _hall_call_objects(simulation) if item["id"] in decision_call_ids] + snapshot = simulation.snapshot() + nodes: list[dict[str, Any]] = [] + edges: list[dict[str, Any]] = [] + seen_nodes: set[str] = set() + + def add_node(node_id: str, kind: str, label: str, data: dict[str, Any]) -> None: + if node_id in seen_nodes: + return + seen_nodes.add(node_id) + nodes.append({"id": node_id, "type": kind, "label": label, "data": data}) + + def add_edge(source: str, target: str, relation: str, evidence: str) -> None: + if source in seen_nodes and target in seen_nodes: + edges.append( + { + "id": f"{source}->{target}:{relation}", + "source": source, + "target": target, + "relation": relation, + "evidence": evidence, + } + ) + + for elevator in snapshot["elevators"]: + add_node(elevator["id"], "Elevator", elevator["id"], elevator) + for call in calls: + add_node(call["id"], "HallCall", f"{call['floor']}F {'↑' if call['direction'] > 0 else '↓'}", call) + for passenger in passengers: + add_node(passenger["id"], "Passenger", passenger["id"], passenger) + for decision in decisions: + add_node(decision["id"], "DispatchDecision", decision["id"], decision) + add_node( + "metric:wait", + "WaitMetric", + "Run wait metrics", + { + "avg_wait": snapshot["metrics"]["avg_wait"], + "p95_wait": snapshot["metrics"]["p95_wait"], + "max_wait": snapshot["metrics"]["max_wait"], + "source": "snapshot.metrics", + }, + ) + + for passenger in passengers: + origin = passenger.get("origin") + destination = passenger.get("destination") + if isinstance(origin, int) and isinstance(destination, int): + expected_direction = 1 if destination > origin else -1 + non_lobby_floor = destination if origin == 1 else origin + bank = "low" if non_lobby_floor <= simulation.config.low_zone_max else "high" + passenger_call_id = _call_id( + { + "bank": bank, + "floor": origin, + "direction": expected_direction, + "destination": destination if simulation.config.control_mode == "destination" else None, + } + ) + add_edge(passenger["id"], passenger_call_id, "generated", "passenger arrival ledger") + elevator_id = passenger.get("elevator_id") + if isinstance(elevator_id, str): + add_edge(passenger["id"], elevator_id, "boarded", "event ledger board") + pickup_id = f"pickup:{passenger['id']}" + add_node( + pickup_id, + "Pickup", + f"Pickup {passenger['id']}", + { + "passenger_id": passenger.get("passenger_id"), + "elevator_id": elevator_id, + "boarding": passenger.get("boarding"), + "wait": passenger.get("wait"), + "origin": passenger.get("origin"), + }, + ) + add_edge(elevator_id, pickup_id, "produced", "event ledger board") + add_edge(pickup_id, "metric:wait", "contributed_to", "passenger wait sample") + for decision in decisions: + hall_call_id = str(decision["hall_call_id"]) + add_edge(hall_call_id, decision["id"], "evaluated_by", "dispatch decision ledger") + chosen = decision.get("chosen_elevator_id") + if isinstance(chosen, str): + add_edge(decision["id"], chosen, "selected", str(decision.get("reason", ""))) + previous = decision.get("current_assignment") + if isinstance(previous, str) and previous != chosen: + add_edge(decision["id"], previous, "previous_assignment", "dispatch decision ledger") + + return { + "schema": "elevator-queue-lab.decision-graph.v1", + "run_id": run_id, + "nodes": nodes, + "edges": edges, + "provenance": { + "source": "simulator snapshot + decision ledger + event ledger", + "database": None, + "projection": "read_only", + }, + } + + +def _compare_with_collective(simulation: ElevatorSimulation) -> tuple[str, list[dict[str, Any]]]: + if not M3_BASELINE.is_file(): + return "Committed M3 evidence is unavailable.", [] + baseline = _load(M3_BASELINE) + scenario = baseline.get("scenarios", {}).get(simulation.scenario) + if not scenario: + return f"No committed M3 scenario named {simulation.scenario} is available.", [] + policies = scenario.get("policies", {}) + current = policies.get(simulation.policy_name) + collective = policies.get("collective") + if current is None or collective is None: + return "This live policy is not present in the committed M3 comparison artifact.", [] + delta = float(current["avg_wait"]) - float(collective["avg_wait"]) + direction = "lower" if delta < 0 else "higher" + answer = ( + f"In committed 30-seed M3 evidence for {simulation.scenario}, {simulation.policy_name} " + f"has mean AWT {current['avg_wait']:.2f}s versus collective {collective['avg_wait']:.2f}s " + f"({abs(delta):.2f}s {direction}). Guardrail classification: " + f"{current.get('guardrail_classification', 'unknown')}." + ) + return answer, [ + {"source": "evidence/m3-regression-baseline.json", "policy": simulation.policy_name, "data": current}, + {"source": "evidence/m3-regression-baseline.json", "policy": "collective", "data": collective}, + ] + + +def answer_run_question(simulation: ElevatorSimulation, run_id: str, question: str) -> dict[str, Any]: + """Answer a constrained question from deterministic run evidence only.""" + + normalized = question.strip() + lowered = normalized.lower() + evidence: list[dict[str, Any]] = [] + limitations = [ + "Explanations report observed simulator evidence and controller reasons; they do not infer unrecorded causal mechanisms.", + "Committed statistical comparisons describe synthetic scenarios, not a universal elevator-control theorem.", + ] + intent = "run_summary" + answer = ( + f"Run {run_id} is at simulation time {int(simulation.sim_time)}s with policy " + f"{simulation.policy_name} in the {simulation.scenario} scenario." + ) + + passenger_match = re.search(r"p[- ]?(\d+)", lowered) + decision_match = re.search(r"d[- ]?(\d+)", lowered) + if passenger_match: + intent = "passenger_explanation" + passenger_id = int(passenger_match.group(1)) + passenger = next( + (item for item in _passenger_objects(simulation) if item["passenger_id"] == passenger_id), + None, + ) + passenger_events = [ + event.to_dict() + for event in simulation.ledger.events + if event.passenger_id == passenger_id + ] + if passenger is None: + answer = f"Passenger P-{passenger_id} is not present in the current run ledger." + else: + wait = passenger.get("wait") + wait_text = f" waited {wait:.2f}s" if isinstance(wait, (int, float)) else " has not boarded yet" + answer = ( + f"Passenger P-{passenger_id}{wait_text}. The recorded lifecycle is " + f"{passenger['status']} from {passenger['origin']}F to {passenger['destination']}F" + + (f" on {passenger['elevator_id']}." if passenger.get("elevator_id") else ".") + ) + evidence.append({"source": "event_ledger", "passenger": passenger, "events": passenger_events}) + elif decision_match: + intent = "decision_explanation" + decision_id = f"D-{int(decision_match.group(1))}" + decision = next((item for item in _decision_objects(simulation) if item["id"] == decision_id), None) + if decision is None: + answer = f"Decision {decision_id} is not present in the retained decision ledger." + else: + answer = str(decision.get("reason", "No controller reason was recorded.")) + evidence.append({"source": "decision_ledger", "decision": decision}) + elif "compare" in lowered and "collective" in lowered: + intent = "policy_comparison" + answer, evidence = _compare_with_collective(simulation) + elif "reassign" in lowered: + intent = "reassignment_explanation" + event = next( + (event for event in reversed(simulation.ledger.events) if event.kind in {"assignment_invalidated", "reassign"}), + None, + ) + if event is None: + answer = "No reassignment event has been recorded in the current run yet." + else: + answer = ( + f"The latest reassignment-related event occurred at {event.sim_time:.2f}s on " + f"{event.floor}F. Recorded details: {json.dumps(event.details, sort_keys=True)}" + ) + evidence.append({"source": "event_ledger", "event": event.to_dict()}) + elif "why" in lowered or "choose" in lowered or "selected" in lowered: + intent = "latest_dispatch_explanation" + decision = _decision_objects(simulation)[0] if simulation.decision_history else None + if decision is None: + answer = "No dispatch decision has been recorded in the current run yet." + else: + answer = str(decision.get("reason", "No controller reason was recorded.")) + evidence.append({"source": "decision_ledger", "decision": decision}) + elif "p95" in lowered or "spike" in lowered: + intent = "tail_metric_explanation" + metrics = simulation.snapshot()["metrics"] + answer = ( + f"Current P95 wait is {metrics['p95_wait']:.2f}s with {metrics['current_queue']} passengers waiting. " + "The run ledger can show coincident capacity misses/reassignments, but this interface does not assign a causal driver without recorded evidence." + ) + evidence.append({"source": "snapshot.metrics", "metrics": metrics}) + + return { + "schema": "elevator-queue-lab.ask-run.v1", + "run_id": run_id, + "question": normalized, + "intent": intent, + "answer": answer, + "evidence": evidence, + "limitations": limitations, + "expression_layer": "deterministic", + "llm_required": False, + } + + +def build_models_payload() -> dict[str, Any]: + model = _load(M5_MODEL) + evaluation = _load(M5_EVALUATION) + return { + "schema": "elevator-queue-lab.models-ui.v1", + "model": model, + "evaluation": evaluation, + "source": { + "model": "models/m5-ddqn-baseline.json", + "evaluation": "evidence/m5-heldout-evaluation.json", + }, + } + + +def build_artifacts_payload(simulation: ElevatorSimulation, run_id: str) -> dict[str, Any]: + return build_artifact_catalog(simulation, run_id) diff --git a/contracts/schemas/passenger-trace.schema.json b/contracts/schemas/passenger-trace.schema.json new file mode 100644 index 0000000..f3b23b3 --- /dev/null +++ b/contracts/schemas/passenger-trace.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://elevator.oosu.dev/contracts/passenger-trace.schema.json", + "title": "PassengerTraceArtifact", + "type": "object", + "additionalProperties": false, + "required": ["schema", "scenario", "seed", "duration_seconds", "events"], + "properties": { + "schema": { "const": "elevator-queue-lab.passenger-trace.v1" }, + "scenario": { "type": "string", "minLength": 1 }, + "seed": { "type": "integer" }, + "duration_seconds": { "type": "integer", "minimum": 1 }, + "events": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["at", "passenger_id", "origin", "destination"], + "properties": { + "at": { "type": "integer", "minimum": 1 }, + "passenger_id": { "type": "integer", "minimum": 1 }, + "origin": { "type": "integer", "minimum": 1, "maximum": 18 }, + "destination": { "type": "integer", "minimum": 1, "maximum": 18 } + } + } + } + } +} diff --git a/contracts/schemas/run-artifact.schema.json b/contracts/schemas/run-artifact.schema.json new file mode 100644 index 0000000..92b979d --- /dev/null +++ b/contracts/schemas/run-artifact.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://elevator.oosu.dev/contracts/run-artifact.schema.json", + "title": "SimulationRunArtifact", + "type": "object", + "required": [ + "schema_version", + "artifact_version", + "simulator_version", + "run_id", + "scenario", + "policy", + "seed", + "sim_time", + "trace_sha256", + "metrics", + "provenance", + "trace_manifest", + "created_at" + ], + "properties": { + "schema_version": { "const": "elevator-queue-lab.run.v1" }, + "artifact_version": { "type": "string" }, + "simulator_version": { "type": "string" }, + "run_id": { "type": "string", "minLength": 1 }, + "scenario": { "type": "string" }, + "policy": { "type": "string" }, + "seed": { "type": "integer" }, + "sim_time": { "type": "number", "minimum": 0 }, + "trace_sha256": { "type": ["string", "null"] }, + "metrics": { "type": "object" }, + "provenance": { "type": "object" }, + "trace_manifest": { "$ref": "trace-manifest.schema.json" }, + "created_at": { "type": "string" } + } +} diff --git a/contracts/schemas/trace-manifest.schema.json b/contracts/schemas/trace-manifest.schema.json new file mode 100644 index 0000000..f5ff057 --- /dev/null +++ b/contracts/schemas/trace-manifest.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://elevator-queue-lab.local/contracts/trace-manifest.schema.json", + "title": "Elevator Queue Lab Passenger Trace Manifest", + "type": "object", + "additionalProperties": true, + "required": [ + "schema_version", + "artifact_version", + "generator_version", + "seed", + "scenario", + "building_configuration", + "traffic_contract", + "duration_seconds", + "sha256", + "identity_sha256", + "created_at", + "source" + ], + "properties": { + "schema_version": { + "const": "elevator-queue-lab.trace-manifest.v1" + }, + "artifact_version": { + "type": "string", + "minLength": 1 + }, + "generator_version": { + "type": "string", + "minLength": 1 + }, + "seed": { + "type": "integer" + }, + "scenario": { + "type": "string", + "minLength": 1 + }, + "building_configuration": { + "type": "object" + }, + "traffic_contract": { + "type": "string", + "minLength": 1 + }, + "duration_seconds": { + "type": "number", + "minimum": 0 + }, + "sha256": { + "type": ["string", "null"], + "pattern": "^[0-9a-f]{64}$" + }, + "identity_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "created_at": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1 + } + } +} diff --git a/docs/M8_PRODUCT_WORKBENCH.md b/docs/M8_PRODUCT_WORKBENCH.md new file mode 100644 index 0000000..296bc48 --- /dev/null +++ b/docs/M8_PRODUCT_WORKBENCH.md @@ -0,0 +1,383 @@ +# M8 — Decision Intelligence Product Workbench + +M8 evolves Elevator Queue Lab from a research simulator with a dashboard into a reproducible +decision-intelligence workbench. The simulator remains authoritative. The product layer adds typed +projections over existing simulator state, ledgers and committed evidence so a reviewer can move +through the full research loop: + +`SIMULATE → OBSERVE → INSPECT → EXPLAIN → REPLAY → COMPARE → FALSIFY` + +> Elevator Queue Lab is a reproducible decision-intelligence workbench for elevator group-control +> research: simulate, compare, inspect, replay, explain, and falsify dispatch decisions from +> passenger-level evidence. + +## 1. Previous architecture + +Before M8 the runtime was already research-grade but the browser surface was concentrated in a +large vanilla-JavaScript implementation: + +```text +app/ simulator + server + ↓ JSON API +web/index.html +web/app.js +web/queue-trend.js +web/styles.css +``` + +That architecture delivered deterministic replay, API-consistent live state, committed experiment +evidence and the M7 theory panel. Its main limitation was product composition: navigation, object +exploration, analytics renderers and inspector behavior were difficult to evolve independently. + +## 2. M8 architecture + +M8 keeps the Python simulator and HTTP server intact and inserts read-only evidence projections +between the core and the product workbenches. + +```text +Passenger demand / canonical trace + ↓ + ElevatorSimulation + ├─ snapshot state + ├─ decision history + └─ event ledger + ↓ + read-only projections + ├─ run artifacts + ├─ object catalog + ├─ decision graph + └─ deterministic explanations + ↓ + Python HTTP API + ↓ +frontend/ React + TypeScript source + ├─ Product Shell + ├─ Live Operations + ├─ Runs / Dispatch + ├─ Experiments / Theory / Models + └─ Object Explorer / Decision Trace + ↓ Vite build +web/ committed production artifact + ↓ +python -m app.server --port 4173 +``` + +The important boundary is one-way: the workbench can inspect and request simulator controls, but +it does not replace simulator truth with browser-side state or generated analytics. + +## 3. Frontend boundary + +`frontend/` is the authored UI source. It contains React components, TypeScript API contracts, +semantic chart specifications, tests and design tokens. `frontend/vite.config.ts` builds directly +to `../web` with `emptyOutDir: true`. + +`web/` is therefore a deployment artifact, not a second authored frontend. A clean checkout can +still run the product with only Python because the production build is committed: + +```bash +python3 -m app.server --port 4173 +``` + +Frontend contributors use: + +```bash +npm ci --prefix frontend +npm run typecheck --prefix frontend +npm test --prefix frontend +npm run build --prefix frontend +``` + +The migration intentionally preserves the existing Python static-serving path and Mac mini +service/proxy deployment contract. + +## 4. Simulation-core boundary + +M8 does not rewrite the elevator engine, CAPR controller, traffic generator, experiment runner or +learning environment. `app/artifacts.py` and `app/workbench.py` project existing data into +product-oriented contracts. + +The projections are read-only with respect to the research core: + +- live state comes from `ElevatorSimulation.snapshot()`; +- passenger lifecycle evidence comes from the event ledger; +- candidate and assignment evidence comes from `decision_history`; +- statistical comparisons come from committed evidence JSON; +- learned-controller claims come from the committed M5 model/evaluation artifacts; +- counterflow claims come from committed M7 discovery and held-out validation artifacts. + +No Neo4j, PostgreSQL or secondary analytical source of truth is introduced. + +## 5. Versioned evidence artifacts + +M8 exposes a catalog containing these artifact types: + +| Artifact type | Primary source | Purpose | +| --- | --- | --- | +| `PassengerTraceArtifact` | materialized trace or seeded demand contract | Reproduce passenger arrivals and trace identity | +| `SimulationRunArtifact` | current simulator | Reproduce run configuration, policy and metrics | +| `DispatchDecisionArtifact` | decision history | Preserve controller candidate/selection evidence | +| `ExperimentArtifact` | M3 evidence | Compare repeated seeded policy experiments | +| `PolicyEvaluationArtifact` | M5/M6 evidence | Preserve held-out policy/model verdicts | +| `ModelArtifact` | M5 model JSON | Identify the fixed learned checkpoint | +| `TheoryEvidenceArtifact` | M7 evidence | Preserve discovery and falsification evidence | + +Every catalog entry carries provenance keys for schema/artifact/simulator version, source, seed, +scenario, policy, configuration, creation time, trace SHA-256 and evidence source. Fields that do +not semantically apply to a committed aggregate artifact are explicitly `null` rather than +invented. + +The canonical trace manifest adds metadata around the pre-existing trace digest without changing +trace identity. Its contract includes: + +```text +schema_version +generator_version +seed +scenario +building_configuration +traffic_contract +duration_seconds +sha256 +created_at +``` + +`scripts/generate_trace.py --package-dir ...` can emit: + +```text +trace.json +manifest.json +schema.json +validation.json +``` + +The legacy trace JSON format and `PassengerTrace.digest` remain valid and unchanged. + +## 6. Workbench API + +Existing APIs remain available: + +```text +/api/health +/api/snapshot +/api/control +/api/replay +/api/experiment +/api/theory +``` + +M8 adds run- and evidence-oriented endpoints: + +```text +/api/runs +/api/runs/{run_id} +/api/runs/{run_id}/objects +/api/runs/{run_id}/decisions +/api/runs/{run_id}/graph +/api/runs/{run_id}/ask?question=... +/api/artifacts +/api/models +``` + +Run scoping makes it explicit which simulator identity an object, graph or explanation belongs to. +The additive `reset_paused` control action exists for deterministic browser/visual verification: it +atomically creates a new zero-second run and leaves it paused so tests can advance exact one-second +steps without racing the background simulation loop. + +## 7. Object Explorer and Decision Trace + +The object projection exposes: + +```text +Elevator +Passenger +HallCall +DispatchDecision +SimulationRun +Scenario +Policy +Experiment +Model +Evidence +TheoryEvidence +``` + +The Decision Trace is a read-only graph projection over JSON state and ledgers. It uses React Flow +for pan/zoom/fit-view interaction and links evidence through relationships such as: + +```text +Passenger ─generated→ HallCall +HallCall ─evaluated_by→ DispatchDecision +DispatchDecision ─selected→ Elevator +Elevator ─produced→ Pickup +Pickup ─contributed_to→ WaitMetric +``` + +Selecting graph/object evidence exposes structured data rather than hiding the source in a +visual-only graph. + +## 8. Semantic ChartSpec + +Analytics rendering follows: + +```text +Evidence → typed ChartSpec → runtime validation → registered React renderer +``` + +Supported semantic specs include `TimeSeries`, `PolicyComparison`, `Distribution`, +`TradeoffScatter`, `FloorHeatmap`, `MetricCard`, `ConfidenceInterval` and `ExperimentMatrix`. + +The renderer does not execute arbitrary JavaScript and does not accept arbitrary generated HTML. +Metrics must already exist in simulator/API/committed evidence contracts before a chart can render +them. + +## 9. Evidence flow by workbench + +### Live Operations + +Uses `/api/snapshot` and the existing control/replay APIs. It preserves 18 floors, six cars, queue +state, active calls, assignment links, load, phase, route, wait/queue metrics, heatmap, decision +candidates, event stream, pause/resume/reset, scenario/policy/control-mode/speed controls and saved +replay. + +### Experiments + +Uses only `evidence/m3-regression-baseline.json` through `/api/experiment`, including the 30 actual +per-seed AWT observations, confidence intervals, tail metrics, fairness, energy and guardrail +classification. + +### Theory + +Uses `evidence/m7-bidirectional-load-sweep.json` and +`evidence/m7-threshold-validation.json`. The UI keeps both parts of the conclusion visible: + +> Congestion alone is not the trigger. Continuous predictive reassignment becomes valuable when +> traffic intensity and counterflow rise together. + +and: + +> The threshold is fuzzy and is not a universal critical constant. + +### Models + +Uses `models/m5-ddqn-baseline.json` and `evidence/m5-heldout-evaluation.json`. The fixed M5 result +remains negative/mixed: `accepted_as_general_improvement` is false, with only the held-out mixed-day +mixture classified as a candidate improvement. + +## 10. Ask This Run + +Ask This Run is deterministic in M8: + +```text +question + ↓ +bounded intent parser + ↓ +run/evidence query + ↓ +evidence package + ↓ +deterministic explanation +``` + +It can explain a passenger lifecycle, a recorded decision, the latest reassignment evidence, a tail +metric snapshot, or compare the current policy with collective using committed M3 evidence. The +response always returns its evidence payload and limitations. No LLM is required and an LLM is not +the source of truth. + +## 11. CI and regression strategy + +Every pull request runs: + +```text +Python unit/contract tests +deterministic experiment smoke +destination-control smoke +artifact/trace validation +release audit +frontend npm ci +TypeScript gate +frontend unit tests +production build into web/ +Playwright Chromium install +browser E2E + responsive + visual regression +``` + +The previous branch-name condition that limited Playwright to the M4 feature branch is removed. + +The full 30-seed × six-scenario statistical regression is intentionally separated because it is +much more expensive. It runs on `main`, on the weekly schedule, or when a PR carries the +`full-statistical-regression` label. This keeps every PR fast enough to use while preserving the +release-quality evidence gate. + +Visual regression targets information loss and layout regressions, not pixel-perfect design +competition. Baselines cover Live Operations desktop/mobile, Replay, Experiments, Theory, Models +and Explorer/Decision Trace. Update them intentionally with: + +```bash +npm run test:e2e:update +``` + +## 12. Showcase capture + +`npm run capture:showcase` starts/uses the same production Python server path as E2E and captures +real workbench state after browser interaction. Assets cover Live Operations after a CAPR +reassignment, Replay, Experiments, Theory, Models and Explorer/Decision Trace. + +The capture is evidence of the actual product path; it is not a mock design export. + +## 13. Deployment compatibility + +The production contract remains: + +```bash +python3 -m app.server --port 4173 +``` + +The server continues to serve `web/` and the API from one process. M8 therefore does not require a +Node runtime on the deployed Mac mini once `web/` has been built and committed. The existing +`deploy/` service/proxy structure can continue to target the same Python command and port. + +Public deployment is considered verified only after the M8 commit is actually deployed and remote +browser/API checks pass; local workbench completion alone is not treated as proof of production +deployment. + +## 14. Migration strategy + +The migration is deliberately incremental and backward-compatible: + +1. preserve the simulator, evidence files and HTTP behavior; +2. add read-only projection contracts and tests; +3. build React/TypeScript source in parallel with the old browser implementation; +4. make Vite emit the production artifact to `web/`; +5. migrate Playwright selectors to semantic/stable selectors without reducing coverage; +6. preserve replay and API consistency before removing the old authored vanilla files; +7. generalize CI only after local Python/frontend/browser gates pass. + +This makes the React migration a product-layer replacement rather than a research-core rewrite. + +## 15. Reference and license policy + +M8 used reference projects only within their verified license boundaries: + +- **BIST reference project:** architecture/documentation/demo-automation patterns only; +- **Palantir-style local clone:** interaction architecture ideas such as shell, rail, inspector, + split workbench and compact data presentation only; no source code copied because a root license + was not verified; +- **team-repos/gen_data and ontology_dashboard:** artifact/provenance/CI architecture patterns only; + no source code copied because a root license was not verified; +- **Data Formulator (MIT):** semantic chart-spec and renderer-separation ideas; +- **OpenGenerativeUI (MIT):** typed planner/spec/registered-renderer idea only; Elevator Queue Lab + explicitly does not adopt arbitrary HTML/JavaScript execution. + +## 16. Research-integrity constraints + +M8 does not change the conclusions established by prior evidence: + +- CAPR is not globally superior; +- the fixed M5 RL controller is not generally superior; +- the counterflow threshold is fuzzy rather than a universal critical constant; +- the demand model is synthetic and is not real-building telemetry; +- charts and explanations may only use simulator state or committed evidence; +- no browser or LLM layer may fabricate a metric or override recorded provenance. + +Those constraints are product requirements, not caveats to hide from the interface. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c7cbb33..073a7f5 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -131,3 +131,24 @@ demo without private infrastructure. - [x] expose discovery + held-out theory evidence in the live dashboard through `/api/theory`; - [x] deploy the M7 theory dashboard and re-verify the public demo; - [x] elevate the counterflow-criticality operating rule into a first-read key takeaway in the public dashboard and README. + +## M8 — Decision Intelligence Product Workbench + +- [x] migrate the authored frontend from the M4 vanilla-JS monolith to React + TypeScript while keeping `web/` as the committed production artifact; +- [x] add an operations/research Product Shell with Live Operations, Runs, Dispatch Analysis, Experiments, Theory, Models and Explorer workbenches; +- [x] preserve the 18-floor / six-car live digital twin, control surface, decision inspector and deterministic replay flow; +- [x] add versioned run, passenger-trace, dispatch-decision, experiment, policy-evaluation, model and theory-evidence artifact projections with provenance; +- [x] add browseable research objects and a read-only Decision Trace graph projected from simulator state, decision history and the event ledger; +- [x] introduce a typed semantic `ChartSpec` contract with runtime validation and registered React renderers rather than arbitrary generated HTML/JS; +- [x] expose committed M3/M5/M7 evidence through dedicated Experiment, Model and Counterflow Criticality workbenches without fabricating metrics; +- [x] add deterministic Ask This Run explanations backed only by recorded run evidence and committed comparisons; +- [x] generalize CI so Python contracts, frontend type/unit/build gates and Playwright E2E/visual QA run on every pull request; +- [x] keep the expensive 30-seed statistical regression on `main`, schedule, or an explicit PR label instead of every PR; +- [x] add deterministic visual-regression baselines for Live Operations desktop/mobile, Replay, Experiments, Theory, Models and Explorer/Decision Trace; +- [x] add `npm run capture:showcase` to capture portfolio assets from the real Python server and production frontend; +- [x] document the frontend/simulator boundary, artifact contracts, graph projection, evidence flow, CI strategy, deployment compatibility and reference-license policy. + +**Gate:** A reviewer can inspect a live dispatch decision from passenger → call → candidate evaluation +→ assignment/reassignment → pickup → resulting metric, reproduce the run from its artifact +provenance, compare policies statistically, and verify the same state through automated browser/API +tests. diff --git a/docs/assets/showcase/m8-experiments.png b/docs/assets/showcase/m8-experiments.png new file mode 100644 index 0000000..081c6ee Binary files /dev/null and b/docs/assets/showcase/m8-experiments.png differ diff --git a/docs/assets/showcase/m8-explorer.png b/docs/assets/showcase/m8-explorer.png new file mode 100644 index 0000000..8b56151 Binary files /dev/null and b/docs/assets/showcase/m8-explorer.png differ diff --git a/docs/assets/showcase/m8-live-operations.png b/docs/assets/showcase/m8-live-operations.png new file mode 100644 index 0000000..6697f37 Binary files /dev/null and b/docs/assets/showcase/m8-live-operations.png differ diff --git a/docs/assets/showcase/m8-models.png b/docs/assets/showcase/m8-models.png new file mode 100644 index 0000000..b71ab71 Binary files /dev/null and b/docs/assets/showcase/m8-models.png differ diff --git a/docs/assets/showcase/m8-replay.png b/docs/assets/showcase/m8-replay.png new file mode 100644 index 0000000..2e4398a Binary files /dev/null and b/docs/assets/showcase/m8-replay.png differ diff --git a/docs/assets/showcase/m8-theory.png b/docs/assets/showcase/m8-theory.png new file mode 100644 index 0000000..610ddd8 Binary files /dev/null and b/docs/assets/showcase/m8-theory.png differ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..2c00386 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + Elevator Queue Lab · Decision Intelligence Workbench + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..45ae456 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3297 @@ +{ + "name": "elevator-queue-lab-workbench", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "elevator-queue-lab-workbench", + "version": "0.1.0", + "dependencies": { + "@xyflow/react": "^12.8.5", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "zod": "^4.1.5" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.2", + "jsdom": "^26.1.0", + "typescript": "^5.9.2", + "vite": "^7.1.3", + "vitest": "^3.2.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "peer": true, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "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==", + "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" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.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==", + "dev": true, + "license": "MIT", + "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" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "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" + }, + "engines": { + "node": ">=6.9.0" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "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" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.5.tgz", + "integrity": "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.5.tgz", + "integrity": "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.5.tgz", + "integrity": "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.5.tgz", + "integrity": "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.5.tgz", + "integrity": "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.5.tgz", + "integrity": "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.5.tgz", + "integrity": "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.5.tgz", + "integrity": "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.5.tgz", + "integrity": "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.5.tgz", + "integrity": "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.5.tgz", + "integrity": "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.5.tgz", + "integrity": "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.5.tgz", + "integrity": "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.5.tgz", + "integrity": "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.5.tgz", + "integrity": "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.5.tgz", + "integrity": "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.5.tgz", + "integrity": "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.5.tgz", + "integrity": "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.5.tgz", + "integrity": "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.5.tgz", + "integrity": "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.5.tgz", + "integrity": "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.5.tgz", + "integrity": "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.5.tgz", + "integrity": "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.5.tgz", + "integrity": "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.5.tgz", + "integrity": "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "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" + } + }, + "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==", + "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" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "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", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xyflow/react": { + "version": "12.11.3", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.3.tgz", + "integrity": "sha512-G3jogHz2GWUtIOkhavUGno2YzY9u6fILIJBttfsBendb0/HWB90JG+sOTAvlIMEwyvq9zgy9V9ZQSwyQjR5QzQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.80", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.80", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.80.tgz", + "integrity": "sha512-ywc3ZqG91brzWrH1WlwMdIX4goOfrpBy6AbLdVSaof/Xx9l138ijIKRExM6EkMro2F+OImGmSiA/WKcXvKVcfA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.18.tgz", + "integrity": "sha512-1iEmLEYSiE1SeBoAfPo/Mnx3PzfzHUkDK61ASkCpuk3YXugYLH5DYK1SzqV55F8FMI6s0F+/tCP7Polz1QRjxw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "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", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "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.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "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/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.412", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.412.tgz", + "integrity": "sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "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", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.5.tgz", + "integrity": "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.5", + "@rollup/rollup-android-arm64": "4.62.5", + "@rollup/rollup-darwin-arm64": "4.62.5", + "@rollup/rollup-darwin-x64": "4.62.5", + "@rollup/rollup-freebsd-arm64": "4.62.5", + "@rollup/rollup-freebsd-x64": "4.62.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", + "@rollup/rollup-linux-arm-musleabihf": "4.62.5", + "@rollup/rollup-linux-arm64-gnu": "4.62.5", + "@rollup/rollup-linux-arm64-musl": "4.62.5", + "@rollup/rollup-linux-loong64-gnu": "4.62.5", + "@rollup/rollup-linux-loong64-musl": "4.62.5", + "@rollup/rollup-linux-ppc64-gnu": "4.62.5", + "@rollup/rollup-linux-ppc64-musl": "4.62.5", + "@rollup/rollup-linux-riscv64-gnu": "4.62.5", + "@rollup/rollup-linux-riscv64-musl": "4.62.5", + "@rollup/rollup-linux-s390x-gnu": "4.62.5", + "@rollup/rollup-linux-x64-gnu": "4.62.5", + "@rollup/rollup-linux-x64-musl": "4.62.5", + "@rollup/rollup-openbsd-x64": "4.62.5", + "@rollup/rollup-openharmony-arm64": "4.62.5", + "@rollup/rollup-win32-arm64-msvc": "4.62.5", + "@rollup/rollup-win32-ia32-msvc": "4.62.5", + "@rollup/rollup-win32-x64-gnu": "4.62.5", + "@rollup/rollup-win32-x64-msvc": "4.62.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.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": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "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": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.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": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "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" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..a6dff49 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "elevator-queue-lab-workbench", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 5173", + "build": "tsc -b && vite build", + "typecheck": "tsc -b --pretty false", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@xyflow/react": "^12.8.5", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "zod": "^4.1.5" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.2", + "jsdom": "^26.1.0", + "typescript": "^5.9.2", + "vite": "^7.1.3", + "vitest": "^3.2.4" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..176a06d --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,206 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { api } from "./api/client"; +import { WorkbenchState } from "./components/common/WorkbenchState"; +import { ProductShell, type WorkbenchKey } from "./components/shell/ProductShell"; +import type { + ArtifactCatalogPayload, + DecisionGraphPayload, + ExperimentPayload, + ModelsPayload, + ObjectsPayload, + RunArtifact, + Snapshot, + TheoryPayload, +} from "./contracts/api"; +import { DispatchWorkbench } from "./features/dispatch/DispatchWorkbench"; +import { ExperimentsWorkbench } from "./features/experiments/ExperimentsWorkbench"; +import { ExplorerWorkbench } from "./features/explorer/ExplorerWorkbench"; +import { DecisionInspector } from "./features/live-operations/DecisionInspector"; +import { LiveControls } from "./features/live-operations/LiveControls"; +import { LiveOperations } from "./features/live-operations/LiveOperations"; +import { ModelsWorkbench } from "./features/models/ModelsWorkbench"; +import { RunsWorkbench } from "./features/runs/RunsWorkbench"; +import { TheoryWorkbench } from "./features/theory/TheoryWorkbench"; + + +interface AppData { + snapshot: Snapshot; + experiment: ExperimentPayload; + theory: TheoryPayload; + models: ModelsPayload; + run: RunArtifact; + artifacts: ArtifactCatalogPayload; + objects: ObjectsPayload; + graph: DecisionGraphPayload; +} + +const TITLES: Record = { + live: { + title: "Live Operations", + subtitle: "Simulate and observe six-car group control with passenger-level evidence.", + }, + runs: { + title: "Runs", + subtitle: "Inspect reproducible run provenance, trace identity, and versioned evidence artifacts.", + }, + dispatch: { + title: "Dispatch Analysis", + subtitle: "Compare candidate ETA, residual capacity, feasibility, and controller score at decision time.", + }, + experiments: { + title: "Experiments", + subtitle: "Compare policies across committed 30-seed statistical evidence without fabricated metrics.", + }, + theory: { + title: "Counterflow Criticality", + subtitle: "Inspect the M7 discovery rule, frozen held-out validation, and its limitations.", + }, + models: { + title: "Models", + subtitle: "Review M5 learned-controller metadata and the mixed/negative held-out result as recorded.", + }, + explorer: { + title: "Object Explorer", + subtitle: "Trace passengers, hall calls, decisions, elevators, evidence, and theory through a run projection.", + }, +}; + +function workbenchFromHash(): WorkbenchKey { + const candidate = window.location.hash.replace(/^#\/?/, "") as WorkbenchKey; + return candidate && candidate in TITLES ? candidate : "live"; +} + +async function loadRunContext(run: RunArtifact): Promise> { + const [artifacts, objects, graph] = await Promise.all([ + api.artifacts(), + api.objects(run.run_id), + api.graph(run.run_id), + ]); + return { artifacts, objects, graph }; +} + +export default function App() { + const [active, setActive] = useState(workbenchFromHash); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [inspectorOpen, setInspectorOpen] = useState(true); + + useEffect(() => { + let cancelled = false; + async function load() { + try { + const [snapshot, experiment, theory, models, runs] = await Promise.all([ + api.snapshot(), + api.experiment(), + api.theory(), + api.models(), + api.runs(), + ]); + const run = runs.runs[0]; + if (!run) throw new Error("No live SimulationRunArtifact was returned."); + const context = await loadRunContext(run); + if (!cancelled) setData({ snapshot, experiment, theory, models, run, ...context }); + } catch (cause) { + if (!cancelled) setError(cause instanceof Error ? cause.message : String(cause)); + } + } + void load(); + return () => { cancelled = true; }; + }, []); + + useEffect(() => { + if (!data) return; + const timer = window.setInterval(() => { + void api.snapshot().then((snapshot) => setData((current) => current ? { ...current, snapshot } : current)).catch(() => undefined); + }, 600); + return () => window.clearInterval(timer); + }, [Boolean(data)]); + + useEffect(() => { + if (!data || active !== "explorer") return; + const refresh = () => { + void Promise.all([api.objects(data.run.run_id), api.graph(data.run.run_id)]).then(([objects, graph]) => { + setData((current) => current ? { ...current, objects, graph } : current); + }).catch(() => undefined); + }; + refresh(); + const timer = window.setInterval(refresh, 2500); + return () => window.clearInterval(timer); + }, [active, data?.run.run_id]); + + const refreshRunIdentity = useCallback(async () => { + const runs = await api.runs(); + const run = runs.runs[0]; + if (!run) throw new Error("Run identity disappeared after control update."); + const context = await loadRunContext(run); + setData((current) => current ? { ...current, run, ...context } : current); + }, []); + + const handleControl = useCallback(async (payload: Record) => { + if (!data) return; + setError(null); + try { + const snapshot = await api.control(payload); + setData((current) => current ? { ...current, snapshot } : current); + await refreshRunIdentity(); + } catch (cause) { + setError(cause instanceof Error ? cause.message : String(cause)); + } + }, [data, refreshRunIdentity]); + + function navigate(next: WorkbenchKey) { + setActive(next); + window.location.hash = next; + } + + if (error && !data) { + return
{error}
; + } + if (!data) { + return
; + } + + const copy = TITLES[active]; + const latestDecision = data.snapshot.decision_tail.at(-1); + const toolbar = active === "live" ? : undefined; + const inspector = active === "live" ? : undefined; + let content: React.ReactNode; + if (active === "live") { + content = ; + } else if (active === "runs") { + content = ; + } else if (active === "dispatch") { + content = ; + } else if (active === "experiments") { + content = ; + } else if (active === "theory") { + content = ; + } else if (active === "models") { + content = ; + } else { + content = ; + } + + return ( + setInspectorOpen((value) => !value)} + status={{ + mode: data.snapshot.running ? "LIVE" : "PAUSED", + scenario: data.snapshot.scenario, + policy: data.snapshot.policy, + runId: data.run.run_id, + provenance: data.run.trace_sha256 ? "materialized passenger trace" : "seeded demand generator", + }} + > + {error ? {error} : null} + {content} + + ); +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..8393b6d --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,52 @@ +import type { + ArtifactCatalogPayload, + AskRunPayload, + DecisionGraphPayload, + ExperimentPayload, + ModelsPayload, + ObjectsPayload, + ReplayPayload, + RunsPayload, + Snapshot, + TheoryPayload, +} from "../contracts/api"; + + +async function requestJson(url: string, init?: RequestInit): Promise { + const response = await fetch(url, { cache: "no-store", ...init }); + if (!response.ok) { + const message = await response.text(); + throw new Error(`${response.status} ${response.statusText}: ${message}`); + } + return response.json() as Promise; +} + +export const api = { + snapshot: (): Promise => requestJson("/api/snapshot"), + experiment: (): Promise => requestJson("/api/experiment"), + theory: (): Promise => requestJson("/api/theory"), + models: (): Promise => requestJson("/api/models"), + artifacts: (): Promise => requestJson("/api/artifacts"), + runs: (): Promise => requestJson("/api/runs"), + objects: (runId: string, objectType?: string): Promise => { + const query = objectType ? `?type=${encodeURIComponent(objectType)}` : ""; + return requestJson(`/api/runs/${encodeURIComponent(runId)}/objects${query}`); + }, + graph: (runId: string): Promise => + requestJson(`/api/runs/${encodeURIComponent(runId)}/graph`), + ask: (runId: string, question: string): Promise => + requestJson(`/api/runs/${encodeURIComponent(runId)}/ask?q=${encodeURIComponent(question)}`), + control: (payload: Record): Promise => + requestJson("/api/control", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }), + replay: (): Promise => requestJson("/api/replay"), + saveReplay: (): Promise => + requestJson("/api/replay", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "save" }), + }), +}; diff --git a/frontend/src/components/charts/ChartRenderer.tsx b/frontend/src/components/charts/ChartRenderer.tsx new file mode 100644 index 0000000..9d6dea0 --- /dev/null +++ b/frontend/src/components/charts/ChartRenderer.tsx @@ -0,0 +1,286 @@ +import type { ExperimentPayload, HistoryPoint, PolicyEvidence, Snapshot } from "../../contracts/api"; +import { parseChartSpec, type ChartSpec } from "../../contracts/chartSpec"; +import { WorkbenchState } from "../common/WorkbenchState"; + + +interface ChartRendererProps { + spec: ChartSpec | unknown; + data: unknown; +} + +interface Point { + x: number; + y: number; + label?: string; + detail?: string; +} + +const POLICY_ORDER = ["legacy_sticky", "nearest_car", "collective", "queue_aware", "capr"]; + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} + +function normalize(value: number, min: number, max: number): number { + if (max === min) return 0.5; + return (value - min) / (max - min); +} + +function polyline(points: Point[], width = 620, height = 220): string { + if (!points.length) return ""; + const xs = points.map((point) => point.x); + const ys = points.map((point) => point.y); + const xMin = Math.min(...xs); + const xMax = Math.max(...xs); + const yMin = Math.min(...ys); + const yMax = Math.max(...ys); + return points + .map((point) => { + const x = 28 + normalize(point.x, xMin, xMax) * (width - 56); + const y = 18 + (1 - normalize(point.y, yMin, yMax)) * (height - 44); + return `${x.toFixed(1)},${y.toFixed(1)}`; + }) + .join(" "); +} + +function ChartFrame({ title, source, children }: { title: string; source: string; children: React.ReactNode }) { + return ( +
+
+ {title} + {source} +
+
{children}
+
+ ); +} + +function TimeSeriesRenderer({ spec, data }: { spec: Extract; data: unknown }) { + const history = Array.isArray(data) ? (data as HistoryPoint[]) : []; + const width = 620; + const height = 220; + if (history.length < 2) { + return ; + } + const series = spec.y.map((field) => ({ + field, + points: history.map((item) => ({ x: Number(item[spec.x as keyof HistoryPoint]), y: Number(item[field as keyof HistoryPoint]) })), + })); + return ( + + + {[0, 1, 2, 3, 4].map((row) => ( + + ))} + {series.map((entry, index) => ( + + ))} + +
+ {series.map((entry, index) => {entry.field})} +
+
+ ); +} + +function evidenceScenario(data: unknown, scenario: string) { + const payload = data as ExperimentPayload | undefined; + return payload?.baseline?.scenarios?.[scenario]?.policies ?? {}; +} + +function evidenceValue(evidence: PolicyEvidence | undefined, metric: string): number { + const record = evidence as unknown as Record | undefined; + return Number(record?.[metric] ?? 0); +} + +function PolicyComparisonRenderer({ spec, data }: { spec: Extract; data: unknown }) { + const policies = evidenceScenario(data, spec.scenario); + const values = POLICY_ORDER.filter((policy) => policies[policy]).map((policy) => ({ + policy, + value: evidenceValue(policies[policy], spec.metric), + })); + const max = Math.max(1, ...values.map((item) => item.value)); + return ( + +
+ {values.map((item) => ( +
+ {item.policy} +
+ {item.value.toFixed(2)} +
+ ))} +
+
+ ); +} + +function DistributionRenderer({ spec, data }: { spec: Extract; data: unknown }) { + const policies = evidenceScenario(data, spec.scenario); + const allValues = POLICY_ORDER.flatMap((policy) => policies[policy]?.avg_wait_seed_values ?? []); + const min = Math.min(...allValues, 0); + const max = Math.max(...allValues, 1); + return ( + + + {POLICY_ORDER.map((policy, row) => { + const values = policies[policy]?.avg_wait_seed_values ?? []; + return ( + + {policy} + + {values.map((value, index) => ( + + ))} + + ); + })} + + + ); +} + +function TradeoffRenderer({ spec, data }: { spec: Extract; data: unknown }) { + const policies = evidenceScenario(data, spec.scenario); + const points = POLICY_ORDER.filter((policy) => policies[policy]).map((policy) => ({ + policy, + x: evidenceValue(policies[policy], spec.x), + y: evidenceValue(policies[policy], spec.y), + })); + const xMin = Math.min(...points.map((point) => point.x)); + const xMax = Math.max(...points.map((point) => point.x)); + const yMin = Math.min(...points.map((point) => point.y)); + const yMax = Math.max(...points.map((point) => point.y)); + return ( + + + + + {points.map((point, index) => ( + + + {point.policy} + + ))} + {spec.x} + {spec.y} + + + ); +} + +function FloorHeatmapRenderer({ spec, data }: { spec: Extract; data: unknown }) { + const snapshot = data as Snapshot | undefined; + const counts = Array.from({ length: 18 }, (_, index) => { + const floor = index + 1; + const queue = snapshot?.queues?.[String(floor)] ?? { up: 0, down: 0 }; + return { floor, count: Number(queue.up) + Number(queue.down) }; + }); + const max = Math.max(1, ...counts.map((item) => item.count)); + return ( + +
+ {counts.map((item) => ( +
+ {item.floor}F + {item.count} +
+ ))} +
+
+ ); +} + +function ConfidenceIntervalRenderer({ spec, data }: { spec: Extract; data: unknown }) { + const policies = evidenceScenario(data, spec.scenario); + return ( + +
+ {POLICY_ORDER.filter((policy) => policies[policy]).map((policy) => { + const item = policies[policy]; + const mean = evidenceValue(item, spec.metric); + const halfwidth = Number(item.avg_wait_ci95_halfwidth ?? 0); + return ( +
+ {policy} + {mean.toFixed(2)} ± {halfwidth.toFixed(2)}s +
+ ); + })} +
+
+ ); +} + +function ExperimentMatrixRenderer({ spec, data }: { spec: Extract; data: unknown }) { + const payload = data as ExperimentPayload | undefined; + const scenarios = Object.entries(payload?.baseline?.scenarios ?? {}); + const values = scenarios.flatMap(([, scenario]) => POLICY_ORDER.map((policy) => evidenceValue(scenario.policies[policy], spec.metric))); + const min = Math.min(...values, 0); + const max = Math.max(...values, 1); + return ( + +
+
+ scenario + {POLICY_ORDER.map((policy) => {policy})} +
+ {scenarios.map(([scenarioName, scenario]) => ( +
+ {scenarioName} + {POLICY_ORDER.map((policy) => { + const value = evidenceValue(scenario.policies[policy], spec.metric); + return {value.toFixed(1)}; + })} +
+ ))} +
+
+ ); +} + +function MetricCardRenderer({ spec, data }: { spec: Extract; data: unknown }) { + const record = data as Record; + return ( + +
+ {String(record?.[spec.metric] ?? "—")}{spec.unit ?? ""} + {spec.metric} +
+
+ ); +} + +const REGISTERED_RENDERERS = { + timeSeries: TimeSeriesRenderer, + policyComparison: PolicyComparisonRenderer, + distribution: DistributionRenderer, + tradeoff: TradeoffRenderer, + floorHeatmap: FloorHeatmapRenderer, + metricCard: MetricCardRenderer, + confidenceInterval: ConfidenceIntervalRenderer, + experimentMatrix: ExperimentMatrixRenderer, +} as const; + +export function ChartRenderer({ spec: rawSpec, data }: ChartRendererProps) { + const parsed = parseChartSpec(rawSpec); + const Renderer = REGISTERED_RENDERERS[parsed.type] as React.ComponentType<{ spec: any; data: unknown }>; + return ; +} diff --git a/frontend/src/components/common/DataPill.tsx b/frontend/src/components/common/DataPill.tsx new file mode 100644 index 0000000..790453c --- /dev/null +++ b/frontend/src/components/common/DataPill.tsx @@ -0,0 +1,17 @@ +import type { ReactNode } from "react"; + + +interface DataPillProps { + label: string; + value?: ReactNode; + tone?: "neutral" | "live" | "warning" | "evidence"; +} + +export function DataPill({ label, value, tone = "neutral" }: DataPillProps) { + return ( + + {label} + {value !== undefined ? {value} : null} + + ); +} diff --git a/frontend/src/components/common/MetricCard.tsx b/frontend/src/components/common/MetricCard.tsx new file mode 100644 index 0000000..bcd5128 --- /dev/null +++ b/frontend/src/components/common/MetricCard.tsx @@ -0,0 +1,17 @@ +interface MetricCardProps { + label: string; + value: string; + note?: string; + id?: string; + tone?: "default" | "positive" | "warning" | "negative"; +} + +export function MetricCard({ label, value, note, id, tone = "default" }: MetricCardProps) { + return ( +
+ {label} + {value} + {note ? {note} : null} +
+ ); +} diff --git a/frontend/src/components/common/WorkbenchState.tsx b/frontend/src/components/common/WorkbenchState.tsx new file mode 100644 index 0000000..9ee3326 --- /dev/null +++ b/frontend/src/components/common/WorkbenchState.tsx @@ -0,0 +1,17 @@ +import type { ReactNode } from "react"; + + +interface WorkbenchStateProps { + kind: "loading" | "empty" | "error" | "warning"; + title: string; + children?: ReactNode; +} + +export function WorkbenchState({ kind, title, children }: WorkbenchStateProps) { + return ( +
+ {title} + {children ?
{children}
: null} +
+ ); +} diff --git a/frontend/src/components/shell/Inspector.tsx b/frontend/src/components/shell/Inspector.tsx new file mode 100644 index 0000000..e74b166 --- /dev/null +++ b/frontend/src/components/shell/Inspector.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from "react"; + + +interface InspectorProps { + title: string; + subtitle?: string; + children: ReactNode; +} + +export function Inspector({ title, subtitle, children }: InspectorProps) { + return ( +
+
+ Inspector + {title} + {subtitle ? {subtitle} : null} +
+
{children}
+
+ ); +} diff --git a/frontend/src/components/shell/ProductShell.test.tsx b/frontend/src/components/shell/ProductShell.test.tsx new file mode 100644 index 0000000..a504283 --- /dev/null +++ b/frontend/src/components/shell/ProductShell.test.tsx @@ -0,0 +1,28 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { ProductShell } from "./ProductShell"; + + +describe("ProductShell", () => { + it("exposes every M8 workbench as accessible navigation", () => { + const onNavigate = vi.fn(); + render( + +
Workbench body
+
, + ); + + for (const label of ["Live Operations", "Runs", "Dispatch Analysis", "Experiments", "Theory", "Models", "Explorer"]) { + expect(screen.getByRole("button", { name: new RegExp(`^${label}$`) })).toBeInTheDocument(); + } + fireEvent.click(screen.getByRole("button", { name: "Explorer" })); + expect(onNavigate).toHaveBeenCalledWith("explorer"); + expect(screen.getByRole("heading", { name: "Live Operations" })).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/shell/ProductShell.tsx b/frontend/src/components/shell/ProductShell.tsx new file mode 100644 index 0000000..20c5a6b --- /dev/null +++ b/frontend/src/components/shell/ProductShell.tsx @@ -0,0 +1,109 @@ +import type { ReactNode } from "react"; +import { DataPill } from "../common/DataPill"; + + +export type WorkbenchKey = "live" | "runs" | "dispatch" | "experiments" | "theory" | "models" | "explorer"; + +interface ProductShellProps { + active: WorkbenchKey; + onNavigate: (next: WorkbenchKey) => void; + title: string; + subtitle: string; + toolbar?: ReactNode; + inspector?: ReactNode; + inspectorOpen?: boolean; + onToggleInspector?: () => void; + status: { + mode: string; + scenario?: string; + policy?: string; + runId?: string; + provenance?: string; + }; + children: ReactNode; +} + +const NAV_ITEMS: Array<{ key: WorkbenchKey; label: string; short: string }> = [ + { key: "live", label: "Live Operations", short: "LO" }, + { key: "runs", label: "Runs", short: "RU" }, + { key: "dispatch", label: "Dispatch Analysis", short: "DA" }, + { key: "experiments", label: "Experiments", short: "EX" }, + { key: "theory", label: "Theory", short: "TH" }, + { key: "models", label: "Models", short: "MO" }, + { key: "explorer", label: "Explorer", short: "OB" }, +]; + +export function ProductShell({ + active, + onNavigate, + title, + subtitle, + toolbar, + inspector, + inspectorOpen = true, + onToggleInspector, + status, + children, +}: ProductShellProps) { + return ( +
+
+ +
+ Elevator Queue Lab + Decision Intelligence Workbench +
+
+ + {status.scenario ? : null} + {status.policy ? : null} +
+
+ + + +
+
+
+ {active.replace("-", " ")} +

{title}

+

{subtitle}

+
+ {inspector ? ( + + ) : null} +
+ {toolbar ?
{toolbar}
: null} +
+
{children}
+ {inspector && inspectorOpen ? : null} +
+
+ +
+ provenance: {status.provenance ?? "live simulator"} + {status.runId ?? "run pending"} + seeded reproducibility · no fabricated metrics +
+
+ ); +} diff --git a/frontend/src/contracts/api.ts b/frontend/src/contracts/api.ts new file mode 100644 index 0000000..5d7ba46 --- /dev/null +++ b/frontend/src/contracts/api.ts @@ -0,0 +1,270 @@ +export type ScenarioName = "morning" | "lunch" | "normal" | "evening" | "shock" | "mixed_day"; + +export type PolicyName = "legacy_sticky" | "nearest_car" | "collective" | "queue_aware" | "capr" | "rl"; + +export interface MetricsSnapshot { + avg_wait: number; + p95_wait: number; + max_wait: number; + avg_queue: number; + little_law_lq: number; + arrival_rate_per_min: number; + served: number; + arrivals: number; + missed_capacity: number; + abandoned: number; + assignments: number; + reassignments: number; + invalidations: number; + current_queue: number; +} + +export interface ElevatorSnapshot { + id: string; + bank: "low" | "high"; + floor: number; + direction: number; + load: number; + capacity: number; + stops: number[]; + door_open: boolean; + phase: string; + target_floor: number | null; +} + +export interface HallCallSnapshot { + floor: number; + direction: number; + bank: "low" | "high"; + destination: number | null; + assigned: string | null; + wait: number; + missed: number; + assigned_score: number | null; +} + +export interface CandidateEvaluation { + elevator_id: string; + pickup_eta: number; + route_cost: number; + projected_load: number; + residual_capacity: number; + insertion_index: number; + score: number; + feasible: boolean; + reason: string; +} + +export interface DispatchDecision { + sim_time: number; + floor: number; + direction: number; + bank: string; + destination: number | null; + queue_size: number; + current_assignment: string | null; + chosen_elevator_id: string | null; + reason: string; + candidates: CandidateEvaluation[]; +} + +export interface SimulationEvent { + sequence: number; + sim_time: number; + kind: string; + passenger_id: number | null; + elevator_id: string | null; + floor: number | null; + bank: string | null; + details: Record; +} + +export interface HistoryPoint { + sim_time: number; + avg_wait: number; + p95_wait: number; + avg_queue: number; +} + +export interface SimulationConfig { + floors: number; + low_zone_max: number; + high_zone_min: number; + elevators_per_bank: number; + elevator_capacity: number; + floor_height_m: number; + max_speed_mps: number; + acceleration_mps2: number; + levelling_seconds: number; + door_open_seconds: number; + door_dwell_seconds: number; + door_close_seconds: number; + passenger_transfer_seconds: number; + time_step_seconds: number; + passenger_patience_seconds: number | null; + control_mode: "conventional" | "destination"; + reassignment_interval_seconds: number; + reassignment_cooldown_seconds: number; + reassignment_min_gain: number; + reassignment_min_eta_gain_seconds: number; + max_noncapacity_reassignments_per_call: number; + capacity_reserve: number; +} + +export interface Snapshot { + scenario: ScenarioName; + policy: PolicyName; + sim_time: number; + clock: string; + metrics: MetricsSnapshot; + weights: Record; + elevators: ElevatorSnapshot[]; + queues: Record; + calls: HallCallSnapshot[]; + history: HistoryPoint[]; + audit: Record; + event_tail: SimulationEvent[]; + decision_tail: DispatchDecision[]; + trace_digest: string | null; + simulation_config: SimulationConfig; + running: boolean; + speed: number; + replay_frames: number; + saved_replay_available: boolean; +} + +export interface ReplayPayload { + schema: string; + source: "saved_run" | "live_buffer"; + scenario: ScenarioName; + policy: PolicyName; + control_mode: string; + start_sim_time: number; + end_sim_time: number; + frame_count: number; + frames: Snapshot[]; +} + +export interface PolicyEvidence { + avg_wait: number; + avg_wait_ci95_halfwidth: number; + avg_wait_delta_ci95_halfwidth: number; + avg_wait_delta_vs_collective: number; + avg_wait_max: number; + avg_wait_min: number; + avg_wait_seed_values: number[]; + energy_proxy: number; + guardrail_classification: string; + p95_wait: number; + p99_wait: number; + worst_floor_mean_wait: number; +} + +export interface ExperimentPayload { + schema: string; + source: string; + baseline: { + schema: string; + source: Record; + scenarios: Record; trace_digest_manifest_sha256: string }>; + }; +} + +export interface TheoryPayload { + schema: string; + discovery: Record; + validation: Record; +} + +export interface RunArtifact { + schema_version: string; + artifact_version: string; + simulator_version: string; + run_id: string; + scenario: ScenarioName; + policy: PolicyName; + seed: number; + sim_time: number; + trace_sha256: string | null; + metrics: MetricsSnapshot; + provenance: Record; + trace_manifest: Record; + created_at: string; +} + +export interface RunsPayload { + schema: string; + runs: RunArtifact[]; +} + +export interface WorkbenchObject { + id: string; + object_type: string; + [key: string]: unknown; +} + +export interface ObjectsPayload { + schema: string; + run_id: string; + object_types: string[]; + selected_type: string | null; + objects: WorkbenchObject[]; + counts: Record; +} + +export interface GraphNodePayload { + id: string; + type: string; + label: string; + data: Record; +} + +export interface GraphEdgePayload { + id: string; + source: string; + target: string; + relation: string; + evidence: string; +} + +export interface DecisionGraphPayload { + schema: string; + run_id: string; + nodes: GraphNodePayload[]; + edges: GraphEdgePayload[]; + provenance: Record; +} + +export interface AskRunPayload { + schema: string; + run_id: string; + question: string; + intent: string; + answer: string; + evidence: Array>; + limitations: string[]; + expression_layer: string; + llm_required: boolean; +} + +export interface ArtifactReference { + artifact_type: string; + schema_version: string; + artifact_version: string; + artifact_id: string; + source: string; + sha256: string | null; +} + +export interface ArtifactCatalogPayload { + schema: string; + run_id: string; + artifacts: ArtifactReference[]; +} + +export interface ModelsPayload { + schema: string; + model: Record; + evaluation: Record; + source: Record; +} diff --git a/frontend/src/contracts/chartSpec.test.ts b/frontend/src/contracts/chartSpec.test.ts new file mode 100644 index 0000000..01dd878 --- /dev/null +++ b/frontend/src/contracts/chartSpec.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { parseChartSpec } from "./chartSpec"; + + +describe("ChartSpec runtime validation", () => { + it("accepts a registered distribution chart", () => { + const spec = parseChartSpec({ + type: "distribution", + title: "30-seed AWT distribution", + source: "evidence/m3-regression-baseline.json", + metric: "avg_wait", + groupBy: "policy", + scenario: "lunch", + confidenceInterval: true, + }); + + expect(spec.type).toBe("distribution"); + if (spec.type !== "distribution") throw new Error("Expected a distribution ChartSpec"); + expect(spec.scenario).toBe("lunch"); + }); + + it("rejects arbitrary component types", () => { + expect(() => parseChartSpec({ type: "html", title: "Unsafe", source: "planner", html: " + -
-
-
-
OPERATIONS RESEARCH · GROUP CONTROL
-

Elevator Queue Lab

-

18-floor digital twin · predictive dispatch · reproducible statistical evidence

-
-
-
LIVE SIMULATION
- 08:00:00 - T+00:00 -
-
- -
- - - - -
- - -
-
- -
- OFFICE TRIP MIX - 85% lobby-linked - 10% roof-access - 5% inter-floor - Time of day changes direction, not the purpose mix · synthetic prior, not measured telemetry -
- -
-
-
-
- LIVE DIGITAL TWIN -

Passenger flow & car state

-
-
- Low bank 1–9F - High bank 1, 10–18F - Assigned call -
-
- -
-
-
-
- -
- - -
- - -
- -
-
-
-
- M3 · 30-SEED EVIDENCE -

Policy comparison

-
- -
-

Values below come directly from the checked-in M3 statistical regression baseline. Decision rank prefers guardrail-clean candidates, then the collective reference, then faster-but-riskier tradeoffs. Energy is a comparative proxy, not kWh.

-
-
-
- -
-
-
-
- 30 ACTUAL SEEDS -

Seed AWT distribution

-
- KDE · avg wait -
- -
-

Gaussian-kernel density over the 30 observed per-seed average-wait values. This is an empirical run-to-run distribution, not an assumption that passenger waits are normally distributed.

-
- -
-
-
- DECISION BOARD -

Dispatch policy ranking

-
- guardrail-aware -
-
- - - - - - - - - - - - - - - - -
RankPolicyEvidenceAWT · 95% CIP95P99Worst floorEnergyΔ vs collectiveSpeed rank
-
-
-
-
- -
-
-
- DETERMINISTIC REPLAY -

Inspect a saved run

-
- live buffer -
-

Save the current buffered run, then scrub the exact simulator frames. Replay uses saved API state rather than reconstructed chart values.

-
- - - -
-
- -
Frame 0 / 0—
-
-
-
Scenario—
-
Policy—
-
Frames0
-
-
-
- -
-
-
- M7 · THEORY EXTRACTION · 4,680 CONTROLLED RUNS -

Counterflow criticality

-
- discovery + held-out falsification -
- -
-
- Candidate state variable - B = λ · 4p↑(1 − p↑) - λ = arrivals/min · p↑ = lobby-linked up probability · B = bidirectional load rate -
-

- M7 holds CAPR scoring and parking fixed, then turns only continuous predictive reassignment - on/off. The question is whether counterflow creates enough route competition for reassignment to - pay for its churn. This is a falsifiable empirical phase hypothesis for this simulator — not a - universal queueing theorem or a novelty claim. -

-
-
-
-
-
-
- PHASE DIAGRAM -

Continuous reassignment value vs bidirectional load

-
- ΔAWT · CAPR − static -
- -
- Discovery grid - Held-out grid - Below zero = reassignment helps -
-
-
-
-
- EMPIRICAL REGIMES -

From churn to predictive value

-
-
-
-
-
-
-
-

-
- -
-
QUEUEING DIAGNOSTICLq ≈ λ · Wq
-
Observed Lq0.00
-
λ · Wq0.00
-
Arrival rate0/min
-

This is a simulator sanity diagnostic, not an M/M/c assumption. Elevator service is route- and state-dependent.

-
- -
- Every displayed metric and replay frame is sourced from simulator or checked-in evidence APIs. - M0–M7 research release · controlled phase sweep · held-out falsification -
-
- - +
diff --git a/web/queue-trend.js b/web/queue-trend.js deleted file mode 100644 index 09e0ecc..0000000 --- a/web/queue-trend.js +++ /dev/null @@ -1,100 +0,0 @@ -const heatmapArea = document.querySelector(".heatmap-area"); -const trend = document.createElement("div"); -trend.className = "queue-trend"; -trend.innerHTML = ` -
QUEUE TIME SERIES · Lq0.00
- -`; -heatmapArea.appendChild(trend); - -const style = document.createElement("style"); -style.textContent = ` - .queue-trend { margin-top: 9px; padding-top: 8px; border-top: 1px solid rgba(157,186,207,.1); } - .queue-trend-title { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:4px; color:#6e8899; font-size:7px; font-weight:800; letter-spacing:.08em; } - .queue-trend-title strong { color:#8ecfd2; font-size:9px; font-variant-numeric:tabular-nums; } - #queue-trend-chart { display:block; width:100%; height:50px; border-radius:5px; background:rgba(5,14,21,.5); } -`; -document.head.appendChild(style); - -let replayCache = null; - -function drawQueueTrend(points) { - const canvas = document.querySelector("#queue-trend-chart"); - const ratio = window.devicePixelRatio || 1; - const rect = canvas.getBoundingClientRect(); - const width = Math.max(160, Math.round(rect.width * ratio)); - const height = Math.max(50, Math.round(rect.height * ratio)); - if (canvas.width !== width || canvas.height !== height) { - canvas.width = width; - canvas.height = height; - } - const ctx = canvas.getContext("2d"); - ctx.clearRect(0, 0, width, height); - const values = points.map((point) => Number(point.avg_queue || 0)); - const current = values.at(-1) || 0; - document.querySelector("#queue-trend-value").textContent = current.toFixed(2); - if (values.length < 2) return; - - const pad = 7 * ratio; - const plotWidth = width - pad * 2; - const plotHeight = height - pad * 2; - const maximum = Math.max(1, ...values) * 1.12; - ctx.strokeStyle = "rgba(150,180,200,.10)"; - ctx.lineWidth = ratio; - for (let index = 0; index <= 2; index += 1) { - const y = pad + (plotHeight * index) / 2; - ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(width - pad, y); ctx.stroke(); - } - ctx.strokeStyle = "#6ca9ff"; - ctx.lineWidth = 1.6 * ratio; - ctx.beginPath(); - values.forEach((value, index) => { - const x = pad + (index / (values.length - 1)) * plotWidth; - const y = pad + plotHeight - (value / maximum) * plotHeight; - if (index === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); - }); - ctx.stroke(); -} - -async function refreshLiveTrend() { - if (document.querySelector("#live-state span")?.textContent === "REPLAY MODE") return; - try { - const response = await fetch("/api/snapshot", { cache: "no-store" }); - if (!response.ok) return; - const snapshot = await response.json(); - drawQueueTrend(snapshot.history || []); - } catch (error) { - console.error(error); - } -} - -async function drawReplayTrend() { - try { - if (!replayCache) { - const response = await fetch("/api/replay", { cache: "no-store" }); - if (!response.ok) return; - replayCache = await response.json(); - } - const slider = document.querySelector("#replay-slider"); - const index = Number(slider.value || 0); - const points = (replayCache.frames || []) - .slice(0, index + 1) - .filter((frame) => Number(frame.sim_time) % 15 === 0) - .map((frame) => ({ avg_queue: frame.metrics.avg_queue })); - drawQueueTrend(points); - } catch (error) { - console.error(error); - } -} - -document.querySelector("#save-replay")?.addEventListener("click", () => { replayCache = null; }); -document.querySelector("#enter-replay")?.addEventListener("click", () => setTimeout(drawReplayTrend, 0)); -document.querySelector("#replay-slider")?.addEventListener("input", drawReplayTrend); -document.querySelector("#return-live")?.addEventListener("click", () => setTimeout(refreshLiveTrend, 0)); -window.addEventListener("resize", () => { - if (document.querySelector("#live-state span")?.textContent === "REPLAY MODE") drawReplayTrend(); - else refreshLiveTrend(); -}); - -refreshLiveTrend(); -setInterval(refreshLiveTrend, 1000); diff --git a/web/styles.css b/web/styles.css deleted file mode 100644 index dd8c37b..0000000 --- a/web/styles.css +++ /dev/null @@ -1,466 +0,0 @@ -:root { - color-scheme: dark; - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - background: #071019; - color: #eaf1f7; - --bg: #071019; - --panel: rgba(12, 26, 38, 0.94); - --panel-soft: rgba(16, 33, 47, 0.72); - --line: rgba(157, 186, 207, 0.16); - --muted: #88a0b1; - --text: #edf5fa; - --cyan: #4dd7d1; - --amber: #f1b86a; - --blue: #6ca9ff; - --danger: #ff7b7b; - --green: #79dfb0; -} - -* { box-sizing: border-box; } -html { background: var(--bg); } -body { - margin: 0; - min-height: 100vh; - background: - radial-gradient(circle at 20% 4%, rgba(25, 78, 105, .38) 0, transparent 28%), - radial-gradient(circle at 88% 28%, rgba(34, 92, 88, .12) 0, transparent 24%), - linear-gradient(150deg, #071019 0%, #09151f 56%, #061019 100%); -} -button, select, input { font: inherit; } -button:disabled, select:disabled { opacity: .45; cursor: not-allowed; } - -.shell { width: min(1580px, calc(100vw - 40px)); margin: 0 auto; padding: 24px 0 28px; } -.topbar { display: flex; justify-content: space-between; align-items: flex-end; padding: 4px 4px 22px; gap: 22px; } -.eyebrow, .kicker { color: #76c8cf; font-weight: 800; letter-spacing: .14em; font-size: 10px; } -h1 { margin: 5px 0 2px; font-size: clamp(28px, 3vw, 36px); letter-spacing: -.04em; } -h2, h3 { margin: 4px 0 0; letter-spacing: -.025em; } -h2 { font-size: 20px; } h3 { font-size: 16px; } -.topbar p { margin: 4px 0 0; color: var(--muted); font-size: 13px; } -.clock-block { text-align: right; display: grid; gap: 3px; flex: 0 0 auto; } -.clock-block strong { font-size: 25px; font-variant-numeric: tabular-nums; } -.clock-block > span:last-child { color: var(--muted); font-size: 11px; } -.live-state { display: flex; justify-content: flex-end; align-items: center; gap: 7px; font-size: 10px; font-weight: 800; letter-spacing: .12em; color: #86eee1; } -.live-state i { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #58e0c7; box-shadow: 0 0 10px #58e0c7; } -.live-state.paused { color: var(--amber); } -.live-state.paused i { background: var(--amber); box-shadow: none; } -.live-state.replay { color: var(--blue); } -.live-state.replay i { background: var(--blue); box-shadow: 0 0 10px rgba(108,169,255,.6); } - -.controls { - display: grid; - grid-template-columns: 1.05fr 1.05fr 1.1fr .55fr auto; - gap: 10px; - padding: 12px; - border: 1px solid var(--line); - border-radius: 14px; - background: rgba(7, 17, 26, .84); - margin-bottom: 14px; - position: sticky; - top: 8px; - z-index: 20; - backdrop-filter: blur(14px); -} -.controls label, .compact-select { display: grid; gap: 5px; color: var(--muted); font-size: 9px; text-transform: uppercase; letter-spacing: .11em; font-weight: 700; } -select { color: var(--text); background: #0d1d29; border: 1px solid #243a49; border-radius: 8px; padding: 9px 10px; outline: none; min-width: 0; } -select:focus { border-color: rgba(77,215,209,.55); box-shadow: 0 0 0 2px rgba(77,215,209,.08); } -.control-actions { display: flex; gap: 8px; align-items: end; white-space: nowrap; } -.button { color: var(--text); border: 1px solid #2a4556; border-radius: 8px; padding: 9px 13px; cursor: pointer; background: #102332; } -.button.primary { border-color: #28777a; background: linear-gradient(135deg, #164a52, #163846); } -.button.ghost { background: rgba(12, 27, 38, .72); } -.button:hover:not(:disabled) { filter: brightness(1.13); } - -.demand-contract { - display: flex; - align-items: center; - gap: 13px; - margin: -5px 2px 14px; - color: #7791a3; - font-size: 9px; - flex-wrap: wrap; -} -.demand-contract span { color: #76c8cf; font-weight: 800; letter-spacing: .12em; } -.demand-contract strong { color: #bacbd6; font-size: 10px; } -.demand-contract small { color: #5f788a; margin-left: auto; } - -.panel, .metric { border: 1px solid var(--line); background: var(--panel); border-radius: 14px; box-shadow: 0 18px 55px rgba(0,0,0,.18); } -.workspace { display: grid; grid-template-columns: minmax(620px, 1.45fr) minmax(390px, .8fr); gap: 14px; align-items: start; } -.building-panel { padding: 17px; } -.panel-title { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin-bottom: 14px; } -.panel-title.compact { margin-bottom: 11px; } -.legend { display: flex; gap: 12px; color: var(--muted); font-size: 10px; flex-wrap: wrap; justify-content: flex-end; } -.legend span { display: flex; align-items: center; gap: 5px; } -.legend i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; } -.legend .low { background: var(--cyan); } -.legend .high { background: var(--amber); } -.legend .assignment { width: 15px; height: 2px; background: var(--blue); border-radius: 2px; } - -.building { position: relative; height: 650px; display: grid; grid-template-columns: 44px 1fr; overflow: hidden; border: 1px solid #1a3243; border-radius: 11px; background: linear-gradient(90deg, rgba(27,59,77,.13), transparent 32%), #08141e; } -.floor-labels { position: relative; border-right: 1px solid #203544; z-index: 2; } -.floor-label { position: absolute; right: 10px; transform: translateY(50%); color: #6f8798; font-size: 9px; font-variant-numeric: tabular-nums; } -.floors { position: absolute; left: 44px; right: 0; top: 0; bottom: 0; } -.floor-line { position: absolute; left: 0; right: 0; border-bottom: 1px solid rgba(111,145,165,.105); } -.floor-line.lobby { background: rgba(77, 215, 209, .035); border-bottom-color: rgba(77,215,209,.22); } -.floor-line.roof-access { background: rgba(241,184,106,.035); border-bottom-color: rgba(241,184,106,.26); } -.floor-label.roof-access { color: #d9b477; font-size: 8px; font-weight: 800; } -.queue-badge { position: absolute; left: 12px; transform: translateY(50%); min-width: 82px; display: flex; gap: 5px; color: #b7c8d4; font-size: 9px; z-index: 4; } -.queue-badge b { display: inline-flex; min-width: 27px; justify-content: center; border-radius: 10px; padding: 2px 5px; background: #152837; color: #a7dfe0; } -.queue-badge b.down { color: #f0c691; } - -.shafts { position: absolute; left: 145px; right: 18px; top: 9px; bottom: 9px; display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; pointer-events: none; } -.shaft { position: relative; border-left: 1px solid rgba(88,118,138,.18); border-right: 1px solid rgba(88,118,138,.18); background: rgba(3,9,14,.22); } -.shaft::before { content: attr(data-id); position: absolute; top: 6px; left: 50%; transform: translateX(-50%); color: #536b7d; font-size: 9px; font-weight: 800; z-index: 2; } -.car { position: absolute; left: 7%; width: 86%; height: 28px; border-radius: 5px; display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 3px; padding: 3px 5px; color: #061218; font-size: 9px; font-weight: 900; transition: bottom .19s linear; box-shadow: 0 5px 16px rgba(0,0,0,.32); z-index: 8; } -.car.low { background: linear-gradient(135deg, #57ddd4, #2d9fa7); } -.car.high { background: linear-gradient(135deg, #f2bd70, #cf8c43); } -.car.full { outline: 2px solid var(--danger); box-shadow: 0 0 16px rgba(255,123,123,.45); } -.car.door-open { border-radius: 5px 5px 2px 2px; box-shadow: inset 0 -4px rgba(255,255,255,.28), 0 5px 16px rgba(0,0,0,.32); } -.car span:last-child { font-size: 8px; opacity: .78; } -.assignment-overlay { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 6; } -.assignment-overlay line { stroke-width: 1.15; stroke-dasharray: 4 4; opacity: .56; } -.assignment-overlay circle { stroke: none; opacity: .9; } -.assignment-overlay .link-low { stroke: var(--cyan); fill: var(--cyan); } -.assignment-overlay .link-high { stroke: var(--amber); fill: var(--amber); } - -.twin-footer-grid { display: grid; grid-template-columns: 1.35fr .65fr; gap: 15px; margin-top: 14px; } -.calls-area, .heatmap-area { min-height: 88px; } -.calls { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 8px; } -.call { display: inline-flex; align-items: center; gap: 7px; border: 1px solid #243b4b; background: #0b1a25; border-radius: 20px; padding: 6px 9px; font-size: 9px; } -.call b { color: #dfeaf1; } -.call .wait { color: var(--amber); font-variant-numeric: tabular-nums; } -.call .miss { color: var(--danger); } -.inline-title { display: flex; justify-content: space-between; gap: 8px; align-items: center; } -.inline-title small { color: var(--muted); font-size: 9px; } -.floor-heatmap { display: grid; grid-template-columns: repeat(9, 1fr); gap: 4px; margin-top: 8px; } -.heat-cell { --heat: 0; position: relative; min-width: 0; height: 35px; display: grid; place-items: center; border: 1px solid rgba(82,115,134,.16); border-radius: 5px; background: rgba(77,215,209, calc(.03 + var(--heat) * .42)); overflow: hidden; } -.heat-cell span { position: absolute; top: 3px; left: 4px; color: #7590a2; font-size: 7px; } -.heat-cell strong { font-size: 11px; color: #d9f0f0; } - -.right-column { display: grid; gap: 14px; align-content: start; } -.metric-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } -.metric { padding: 13px 14px; min-height: 98px; display: grid; align-content: center; gap: 3px; } -.metric.hero { background: linear-gradient(145deg, rgba(33,109,112,.31), rgba(12,26,38,.95)); border-color: rgba(77,215,209,.3); } -.metric span { color: #8ea6b7; font-size: 10px; } -.metric strong { font-size: 27px; line-height: 1.1; font-variant-numeric: tabular-nums; } -.metric.hero strong { color: #8af1e8; } -.metric small { color: #627b8e; font-size: 9px; } -.chart-panel, .decision-panel, .event-panel { padding: 14px 15px; } -.tag { border: 1px solid #294252; border-radius: 20px; padding: 4px 8px; color: #8ea7b8; font-size: 9px; font-weight: 800; white-space: nowrap; } -.tag.accent { border-color: #246466; color: #71d9d4; } -#wait-chart { display: block; width: 100%; height: 165px; } -.chart-key { display: flex; gap: 14px; justify-content: flex-end; color: #7f96a7; font-size: 9px; } -.chart-key span { display: flex; align-items: center; gap: 5px; } -.chart-key i { width: 14px; height: 2px; display: inline-block; } -.chart-key .avg { background: var(--cyan); } -.chart-key .p95 { background: var(--amber); } - -.decision-reason { margin: 0 0 8px; padding: 9px 10px; border-left: 2px solid var(--cyan); background: rgba(8,20,29,.65); color: #b9ceda; line-height: 1.45; font-size: 9px; min-height: 40px; } -.decision-meta { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 8px; color: #718b9c; font-size: 9px; } -.candidate-table-wrap { overflow-x: auto; } -.candidate-table { width: 100%; border-collapse: collapse; font-size: 8px; font-variant-numeric: tabular-nums; } -.candidate-table th { color: #657f91; text-align: right; font-weight: 700; padding: 4px 5px; border-bottom: 1px solid var(--line); } -.candidate-table th:first-child, .candidate-table td:first-child { text-align: left; } -.candidate-table td { text-align: right; padding: 5px; border-bottom: 1px solid rgba(157,186,207,.07); color: #a5bac8; } -.candidate-table tr.chosen td { color: #9ef0e6; background: rgba(77,215,209,.05); } -.event-stream { display: grid; gap: 5px; max-height: 174px; overflow: auto; } -.event { display: grid; grid-template-columns: 58px 65px 1fr; gap: 6px; align-items: center; padding: 6px 8px; border: 1px solid rgba(100,135,155,.12); border-radius: 6px; background: rgba(7,18,27,.65); font-size: 8px; } -.event time { color: #617b8d; font-variant-numeric: tabular-nums; } -.event b { font-size: 7px; color: var(--cyan); letter-spacing: .06em; } -.event span { color: #9db0bd; } -.event small { grid-column: 2 / -1; color: #687f90; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.event-full_pass b { color: var(--danger); } -.event-reassign b, .event-assignment_invalidated b { color: var(--amber); } - -.evidence-layout { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(360px, .75fr); gap: 14px; margin-top: 14px; } -.comparison-panel, .replay-panel { padding: 17px; } -.compact-select { min-width: 160px; } -.section-note { margin: -2px 0 13px; color: #70899b; font-size: 9px; line-height: 1.55; } -.policy-leaders { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 7px; margin-bottom: 9px; } -.policy-leader { min-width: 0; padding: 10px 11px; border: 1px solid rgba(111,145,165,.16); border-radius: 9px; background: rgba(7,18,27,.72); display: grid; gap: 3px; } -.policy-leader.primary { border-color: rgba(121,223,176,.34); background: linear-gradient(145deg, rgba(35,88,71,.16), rgba(7,18,27,.78)); } -.policy-leader span { color: #657f91; font-size: 7px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; } -.policy-leader strong { color: #dce9ef; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.policy-leader.primary strong { color: var(--green); } -.policy-leader small { color: #728b9c; font-size: 7px; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.comparison-cards { display: grid; grid-template-columns: repeat(5, 1fr); gap: 7px; } -.comparison-card { padding: 10px; border: 1px solid #223a4a; border-radius: 9px; background: #0a1924; min-width: 0; } -.comparison-card > div:first-child { min-height: 35px; } -.comparison-card-head { display: flex; justify-content: space-between; gap: 6px; align-items: flex-start; } -.comparison-card-head > div { min-width: 0; } -.comparison-card-head > i { flex: 0 0 auto; min-width: 24px; text-align: center; padding: 3px 5px; border-radius: 12px; background: rgba(108,169,255,.08); border: 1px solid rgba(108,169,255,.17); color: #88aee6; font-size: 8px; font-style: normal; font-weight: 900; } -.comparison-card span { display: block; color: #8ca3b3; text-transform: capitalize; font-size: 9px; } -.comparison-card b { display: block; margin-top: 3px; color: #667e8e; font-size: 7px; font-weight: 700; text-transform: uppercase; line-height: 1.25; } -.comparison-card > strong { display: block; margin-top: 10px; font-size: 21px; font-variant-numeric: tabular-nums; } -.comparison-card > small { color: #637b8b; font-size: 8px; } -.comparison-card dl { margin: 10px 0 0; display: grid; gap: 4px; } -.comparison-card dl div { display: flex; justify-content: space-between; gap: 5px; font-size: 7px; } -.comparison-card dt { color: #5f798b; } -.comparison-card dd { margin: 0; color: #98acb9; font-variant-numeric: tabular-nums; } -.comparison-card.good { border-color: rgba(121,223,176,.38); background: linear-gradient(160deg, rgba(35,88,71,.2), #0a1924 68%); } -.comparison-card.good > strong { color: var(--green); } -.comparison-card.reference { border-color: rgba(108,169,255,.35); } -.comparison-card.tradeoff { border-color: rgba(241,184,106,.34); } -.comparison-card.tradeoff > strong { color: #f3c784; } -.comparison-note { margin-top: 10px; padding: 8px 10px; border-radius: 7px; background: #0a1721; color: #91a9b8; font-size: 9px; } -.comparison-note b { color: #c3d6e1; } -.comparison-note span { color: var(--muted); } - -.policy-insight-grid { display: grid; gap: 10px; margin-top: 10px; } -.policy-density-card, .policy-ranking-card { border: 1px solid rgba(94,128,148,.16); border-radius: 10px; background: rgba(7,18,27,.62); padding: 11px 12px; min-width: 0; } -.insight-title { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; margin-bottom: 8px; } -.insight-title h3 { font-size: 14px; } -#policy-density-chart { width: 100%; height: 250px; display: block; overflow: visible; } -.density-grid line { stroke: rgba(127,155,173,.12); stroke-width: 1; } -.density-grid text { fill: #60798b; font-size: 9px; font-variant-numeric: tabular-nums; } -.density-axis { stroke: rgba(147,176,194,.28); stroke-width: 1; } -.density-axis-label, .density-empty { fill: #667f90; font-size: 9px; } -.density-series { color: #9db4c2; } -.density-line { fill: none; stroke: currentColor; stroke-width: 2.2; vector-effect: non-scaling-stroke; } -.density-area { fill: currentColor; opacity: .055; } -.density-mean { fill: currentColor; stroke: #071019; stroke-width: 1.5; vector-effect: non-scaling-stroke; } -.density-legacy_sticky { color: #bd8cff; } -.density-nearest_car { color: #6ca9ff; } -.density-collective { color: #c2d0da; } -.density-queue_aware { color: #4dd7d1; } -.density-capr { color: #f1b86a; } -.policy-density-legend { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; color: #7f96a7; font-size: 8px; margin-top: 2px; } -.policy-density-legend span { display: inline-flex; gap: 5px; align-items: center; } -.policy-density-legend i { width: 16px; height: 2px; display: inline-block; border-radius: 2px; background: currentColor; } -.policy-density-legend b { color: #a9bdc9; font-size: 7px; } -.distribution-note { margin: 8px 0 0; color: #617a8c; font-size: 8px; line-height: 1.45; } - -.policy-ranking-wrap { overflow-x: auto; border: 1px solid rgba(95,128,148,.13); border-radius: 8px; } -.policy-ranking-table { width: 100%; min-width: 880px; border-collapse: collapse; font-size: 8px; font-variant-numeric: tabular-nums; } -.policy-ranking-table th { padding: 7px 8px; color: #637e90; font-size: 7px; text-transform: uppercase; letter-spacing: .055em; text-align: right; border-bottom: 1px solid rgba(112,145,165,.16); background: rgba(11,27,39,.72); white-space: nowrap; } -.policy-ranking-table th:nth-child(2), .policy-ranking-table th:nth-child(3), .policy-ranking-table td:nth-child(2), .policy-ranking-table td:nth-child(3) { text-align: left; } -.policy-ranking-table td { padding: 8px; text-align: right; border-bottom: 1px solid rgba(112,145,165,.09); color: #9cb0bd; white-space: nowrap; } -.policy-ranking-table tbody tr:last-child td { border-bottom: 0; } -.policy-ranking-table tbody tr:hover td { background: rgba(77,215,209,.025); } -.policy-ranking-table td > strong { color: #d7e4eb; } -.policy-ranking-table td b { display: block; color: #d4e3eb; font-size: 9px; } -.policy-ranking-table td small { display: block; color: #657f91; font-size: 7px; margin-top: 2px; } -.decision-rank { display: inline-flex; min-width: 25px; justify-content: center; border-radius: 12px; padding: 3px 5px; color: #a9c4d3 !important; background: rgba(108,169,255,.08); border: 1px solid rgba(108,169,255,.16); } -.evidence-pill { display: inline-flex; padding: 3px 6px; border-radius: 10px; font-size: 7px; font-weight: 800; text-transform: uppercase; border: 1px solid rgba(112,145,165,.18); color: #758e9e; } -.evidence-pill.good { color: var(--green); border-color: rgba(121,223,176,.28); background: rgba(121,223,176,.06); } -.evidence-pill.reference { color: #82aff0; border-color: rgba(108,169,255,.28); background: rgba(108,169,255,.06); } -.evidence-pill.tradeoff { color: #efbf7d; border-color: rgba(241,184,106,.28); background: rgba(241,184,106,.06); } -.policy-ranking-table .delta.better { color: var(--green); } -.policy-ranking-table .delta.worse { color: #e2a6a6; } -.rank-good .decision-rank { color: var(--green) !important; border-color: rgba(121,223,176,.25); } - -.replay-actions { display: flex; gap: 7px; flex-wrap: wrap; } -.replay-timeline { margin-top: 14px; padding: 12px; border: 1px solid #203746; border-radius: 9px; background: rgba(7,18,27,.75); } -#replay-slider { width: 100%; accent-color: var(--cyan); } -.replay-meta { display: flex; justify-content: space-between; gap: 8px; margin-top: 7px; font-size: 9px; color: #718a9a; } -.replay-meta strong { color: #b9d0dc; font-variant-numeric: tabular-nums; } -.replay-summary { display: grid; grid-template-columns: repeat(3,1fr); gap: 7px; margin-top: 10px; } -.replay-summary div { display: grid; gap: 2px; padding: 8px; border: 1px solid rgba(80,112,132,.13); border-radius: 7px; } -.replay-summary span { color: #617b8c; font-size: 7px; text-transform: uppercase; } -.replay-summary strong { font-size: 10px; text-transform: capitalize; overflow: hidden; text-overflow: ellipsis; } - -.theory-panel { margin-top: 14px; padding: 18px; overflow: hidden; } -.theory-heading { margin-bottom: 10px; } -.theory-takeaway { - display: grid; - grid-template-columns: auto minmax(0, 1.35fr) minmax(360px, .9fr); - gap: 14px; - align-items: center; - margin: 0 0 12px; - padding: 15px 16px; - border: 1px solid rgba(77,215,209,.36); - border-radius: 12px; - background: - radial-gradient(circle at 8% 0%, rgba(77,215,209,.15), transparent 42%), - linear-gradient(135deg, rgba(17,63,70,.34), rgba(8,22,31,.88)); - box-shadow: inset 0 1px rgba(255,255,255,.025), 0 15px 35px rgba(0,0,0,.12); -} -.theory-takeaway-label { - align-self: stretch; - display: grid; - place-items: center; - padding: 4px 11px; - border-right: 1px solid rgba(77,215,209,.2); - color: #79dfd7; - font-size: 8px; - font-weight: 900; - letter-spacing: .14em; - writing-mode: vertical-rl; - transform: rotate(180deg); -} -.theory-takeaway-copy { min-width: 0; } -.theory-takeaway-copy > strong { - display: block; - color: #e6fbf8; - font-size: clamp(18px, 2vw, 27px); - line-height: 1.08; - letter-spacing: -.035em; -} -.theory-takeaway-copy p { margin: 7px 0 0; color: #a5bdc9; font-size: 13px; line-height: 1.65; } -.theory-takeaway-copy p b { color: #9af0e6; } -.theory-takeaway-example { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - gap: 9px; - min-width: 0; -} -.theory-takeaway-example > span { - display: grid; - gap: 2px; - min-width: 0; - padding: 9px 10px; - border: 1px solid rgba(115,151,171,.15); - border-radius: 8px; - background: rgba(5,17,25,.48); -} -.theory-takeaway-example b { color: #7e98a9; font-size: 7px; text-transform: uppercase; letter-spacing: .08em; } -.theory-takeaway-example strong { color: #d9eaf0; font-size: 15px; font-variant-numeric: tabular-nums; } -.theory-takeaway-example small { color: #6d8797; font-size: 7px; line-height: 1.35; } -.theory-takeaway-example > i { color: #70cfc9; font-style: normal; font-size: 16px; } -.theory-intro { display: grid; grid-template-columns: minmax(300px,.8fr) minmax(0,1.2fr); gap: 12px; align-items: stretch; } -.theory-intro > p { margin: 0; padding: 12px 13px; border: 1px solid rgba(94,128,148,.13); border-radius: 10px; background: rgba(7,18,27,.52); color: #829aaa; font-size: 12px; line-height: 1.7; } -.theory-intro > p b { color: #c6d8e2; } -.theory-formula-card { display: grid; gap: 4px; padding: 12px 14px; border: 1px solid rgba(77,215,209,.25); border-radius: 10px; background: linear-gradient(135deg, rgba(22,74,82,.28), rgba(8,22,31,.78)); } -.theory-formula-card span { color: #72bfc5; font-size: 8px; font-weight: 800; text-transform: uppercase; letter-spacing: .1em; } -.theory-formula-card strong { color: #a6eee8; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: clamp(19px, 2vw, 28px); letter-spacing: -.04em; } -.theory-formula-card small { color: #617f90; font-size: 8px; line-height: 1.45; } -.theory-leaders { display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; margin: 11px 0; } -.theory-leader { display: grid; gap: 3px; min-width: 0; padding: 10px 11px; border: 1px solid rgba(95,128,148,.15); border-radius: 9px; background: rgba(7,18,27,.66); } -.theory-leader.primary { border-color: rgba(77,215,209,.27); background: linear-gradient(145deg, rgba(29,83,87,.18), rgba(7,18,27,.72)); } -.theory-leader span { color: #667f91; font-size: 7px; text-transform: uppercase; letter-spacing: .07em; } -.theory-leader strong { color: #d7e7ee; font-size: 17px; font-variant-numeric: tabular-nums; } -.theory-leader.primary strong { color: #8ee9e1; } -.theory-leader small { color: #6e8798; font-size: 8px; line-height: 1.35; } -.theory-grid { display: grid; grid-template-columns: minmax(0,1.65fr) minmax(310px,.7fr); gap: 10px; } -.theory-chart-card, .theory-phase-card { min-width: 0; border: 1px solid rgba(94,128,148,.16); border-radius: 10px; background: rgba(7,18,27,.62); padding: 11px 12px; } -#theory-scatter { width: 100%; height: 310px; display: block; overflow: visible; } -.theory-gridline line { stroke: rgba(127,155,173,.105); stroke-width: 1; } -.theory-gridline text, .theory-axis-label { fill: #60798b; font-size: 9px; font-variant-numeric: tabular-nums; } -.theory-zero { stroke: rgba(237,245,250,.42); stroke-width: 1.2; stroke-dasharray: 5 4; } -.theory-fit { stroke: #4dd7d1; stroke-width: 2; stroke-dasharray: 7 4; opacity: .85; vector-effect: non-scaling-stroke; } -.theory-threshold line { stroke: rgba(241,184,106,.7); stroke-width: 1.2; stroke-dasharray: 3 4; } -.theory-threshold text { fill: #d5a969; font-size: 8px; font-weight: 700; } -.theory-point.discovery { fill: #6ca9ff; stroke: #071019; stroke-width: 1.2; opacity: .75; } -.theory-point.discovery.gain { fill: #79dfb0; } -.theory-point.discovery.loss { fill: #ff8a8a; } -.theory-point.validation { fill: rgba(108,169,255,.15); stroke: #b7cee0; stroke-width: 1.4; opacity: .9; } -.theory-point.validation.gain { fill: rgba(121,223,176,.32); stroke: #8ce6ba; } -.theory-chart-legend { display: flex; flex-wrap: wrap; gap: 8px 15px; color: #718999; font-size: 8px; } -.theory-chart-legend span { display: inline-flex; align-items: center; gap: 5px; } -.theory-chart-legend i { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #6ca9ff; } -.theory-chart-legend i.validation { border-radius: 1px; transform: rotate(45deg); background: rgba(108,169,255,.2); border: 1px solid #b7cee0; } -.theory-chart-legend i.gain { background: var(--green); } -.theory-phase-bins { display: grid; gap: 6px; } -.phase-bin { padding: 9px 10px; border-radius: 8px; border: 1px solid rgba(95,128,148,.13); background: rgba(10,25,36,.72); } -.phase-bin > div { display: flex; justify-content: space-between; gap: 8px; align-items: center; } -.phase-bin span { color: #7891a2; font-size: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } -.phase-bin b { color: #637c8d; font-size: 7px; text-transform: uppercase; letter-spacing: .06em; } -.phase-bin strong { display: block; margin-top: 4px; font-size: 16px; font-variant-numeric: tabular-nums; } -.phase-bin small { display: block; color: #60798a; font-size: 7px; margin-top: 2px; } -.phase-bin.static strong { color: #e5aa9d; } -.phase-bin.transition strong { color: #dfbf84; } -.phase-bin.predictive { border-color: rgba(121,223,176,.2); } -.phase-bin.predictive strong { color: var(--green); } -.theory-rule { display: grid; gap: 4px; margin-top: 8px; padding: 10px; border-radius: 8px; background: rgba(20,56,64,.21); border: 1px solid rgba(77,215,209,.16); } -.theory-rule span { color: #6899a1; font-size: 7px; text-transform: uppercase; } -.theory-rule strong { color: #a6dfdd; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; } -.theory-rule small { color: #607f90; font-size: 7px; } -.canonical-theory-row { display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; margin-top: 10px; } -.canonical-theory-row article { padding: 9px 10px; border: 1px solid rgba(95,128,148,.13); border-radius: 8px; background: rgba(7,18,27,.56); display: grid; gap: 2px; } -.canonical-theory-row span { color: #6f8798; font-size: 7px; text-transform: uppercase; } -.canonical-theory-row strong { color: #c9dbe4; font-size: 14px; font-variant-numeric: tabular-nums; } -.canonical-theory-row small { color: #607989; font-size: 7px; } -.theory-caveat { margin: 10px 0 0; padding: 11px 13px; border-left: 2px solid var(--amber); background: rgba(241,184,106,.035); color: #718a9a; font-size: 12px; line-height: 1.65; } -.theory-caveat b { color: #d2bea0; } - -.methodology-strip { margin-top: 14px; padding: 14px 16px; display: grid; grid-template-columns: 1.3fr repeat(3,.7fr) 2fr; gap: 15px; align-items: center; } -.methodology-strip > div { display: grid; gap: 3px; } -.methodology-strip > div > span:not(.kicker) { color: #718a9b; font-size: 8px; } -.methodology-strip strong { font-size: 14px; font-variant-numeric: tabular-nums; } -.methodology-strip > div:first-child strong { color: #b7dfe2; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } -.methodology-strip p { margin: 0; color: #637d8f; font-size: 8px; line-height: 1.45; } -.muted { color: var(--muted); } -footer { display: flex; justify-content: space-between; gap: 15px; padding: 14px 3px 0; color: #526b7d; font-size: 9px; } - -@media (max-width: 1240px) { - .shell { width: calc(100vw - 24px); } - .controls { grid-template-columns: repeat(4, 1fr); position: static; } - .control-actions { grid-column: 1 / -1; justify-content: flex-end; } - .workspace { grid-template-columns: 1fr; } - .right-column { grid-template-columns: 1fr 1fr; } - .metric-grid { grid-column: 1 / -1; grid-template-columns: repeat(4, 1fr); } - .evidence-layout { grid-template-columns: 1fr; } - .theory-grid { grid-template-columns: 1fr; } - .theory-takeaway { grid-template-columns: auto 1fr; } - .theory-takeaway-example { grid-column: 2; } -} - -@media (max-width: 820px) { - .shell { width: calc(100vw - 16px); padding-top: 14px; } - .topbar { align-items: flex-start; padding-bottom: 14px; } - .topbar p { max-width: 430px; } - .controls { grid-template-columns: 1fr 1fr; } - .demand-contract small { width: 100%; margin-left: 0; } - .control-actions { justify-content: stretch; } - .control-actions .button { flex: 1; } - .building { height: 570px; } - .shafts { left: 118px; right: 10px; gap: 4px; } - .queue-badge { left: 7px; min-width: 66px; } - .legend { display: none; } - .twin-footer-grid { grid-template-columns: 1fr; } - .right-column { grid-template-columns: 1fr; } - .metric-grid { grid-template-columns: 1fr 1fr; } - .policy-leaders { grid-template-columns: 1fr 1fr; } - .policy-leader:last-child { grid-column: 1 / -1; } - .comparison-cards { grid-template-columns: 1fr 1fr; } - .comparison-card:last-child { grid-column: span 2; } - .theory-leaders { grid-template-columns: 1fr 1fr; } - .theory-intro { grid-template-columns: 1fr; } - .theory-takeaway { grid-template-columns: 1fr; } - .theory-takeaway-label { - display: block; - padding: 0 0 7px; - border-right: 0; - border-bottom: 1px solid rgba(77,215,209,.18); - writing-mode: horizontal-tb; - transform: none; - } - .theory-takeaway-example { grid-column: auto; } - .canonical-theory-row { grid-template-columns: 1fr 1fr; } - .methodology-strip { grid-template-columns: repeat(3, 1fr); } - .methodology-strip > div:first-child, .methodology-strip p { grid-column: 1 / -1; } -} - -@media (max-width: 560px) { - .shell { width: calc(100vw - 12px); } - .topbar { display: grid; } - .clock-block { text-align: left; grid-template-columns: auto auto; align-items: center; } - .live-state { justify-content: flex-start; grid-column: 1 / -1; } - .controls { grid-template-columns: 1fr; } - .building-panel, .comparison-panel, .replay-panel, .theory-panel { padding: 12px; } - .building { height: 510px; grid-template-columns: 34px 1fr; } - .floors { left: 34px; } - .floor-label { right: 6px; font-size: 7px; } - .shafts { left: 88px; gap: 2px; } - .car { left: 4%; width: 92%; height: 25px; padding: 2px 3px; font-size: 7px; } - .queue-badge { display: none; } - .floor-heatmap { grid-template-columns: repeat(6, 1fr); } - .metric-grid { grid-template-columns: 1fr 1fr; } - .metric strong { font-size: 22px; } - .policy-leaders { grid-template-columns: 1fr; } - .policy-leader:last-child { grid-column: auto; } - .comparison-cards { grid-template-columns: 1fr; } - .comparison-card:last-child { grid-column: auto; } - #policy-density-chart { height: 210px; } - #theory-scatter { height: 235px; } - .theory-leaders { grid-template-columns: 1fr; } - .theory-takeaway { padding: 12px; gap: 10px; } - .theory-takeaway-copy > strong { font-size: 20px; } - .theory-takeaway-example { grid-template-columns: 1fr; } - .theory-takeaway-example > i { transform: rotate(90deg); justify-self: center; } - .canonical-theory-row { grid-template-columns: 1fr 1fr; } - .panel-title { align-items: flex-start; } - .compact-select { min-width: 130px; } - .methodology-strip { grid-template-columns: 1fr 1fr; } - .methodology-strip > div:first-child, .methodology-strip p { grid-column: 1 / -1; } - footer { display: grid; } -}