From 6881489bf6652cd511ffd879b6eecdf15ca3f2ab Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Tue, 14 Jul 2026 09:21:25 -0600 Subject: [PATCH] The Cuckoo Escapement: field report, kernel patch, dashboard What the Raspberry Pi time-server guides get wrong on a Pi 4, with the measurements. The headline artifact is a four-line pps-gpio patch: PREEMPT_RT force-threads IRQ handlers, and pps-gpio takes its timestamp inside its handler, so the realtime kernel puts a scheduler between the electrical edge and the clock. IRQF_NO_THREAD takes RMS offset from 2468 ns to 199 ns. - kernel/ the patch - dashboard/ live status page (position hidden by default) - docs-site/ the write-up (Astro/Starlight, brass, no tutorial section) --- .gitignore | 6 + README.md | 48 + dashboard/.env.example | 13 + dashboard/Makefile | 52 + dashboard/deploy/Caddyfile.tmpl | 11 + dashboard/deploy/caddy-affinity.conf | 10 + dashboard/deploy/deploy.sh | 37 + dashboard/deploy/gpsntp-dashboard.service | 35 + dashboard/deploy/gpsntp-dashboard.sudoers | 4 + dashboard/pyproject.toml | 29 + dashboard/src/gpsntp_dashboard/__init__.py | 3 + dashboard/src/gpsntp_dashboard/chrony.py | 144 + dashboard/src/gpsntp_dashboard/gpsd_reader.py | 165 + dashboard/src/gpsntp_dashboard/main.py | 158 + dashboard/src/gpsntp_dashboard/metrics.py | 88 + dashboard/src/gpsntp_dashboard/static/app.js | 295 + .../src/gpsntp_dashboard/static/index.html | 180 + .../src/gpsntp_dashboard/static/style.css | 225 + .../static/supported-systems-logo.svg | 66 + docs-site/.dockerignore | 19 + docs-site/.env.example | 9 + docs-site/.gitignore | 22 + docs-site/Dockerfile | 47 + docs-site/Makefile | 59 + docs-site/astro.config.mjs | 105 + docs-site/docker-compose.yml | 73 + docs-site/package-lock.json | 6461 +++++++++++++++++ docs-site/package.json | 20 + docs-site/public/favicon.svg | 13 + docs-site/public/supported-systems-logo.svg | 66 + docs-site/src/assets/logo.svg | 43 + docs-site/src/components/Footer.astro | 13 + .../components/SupportedSystemsBadge.astro | 47 + docs-site/src/content.config.ts | 8 + .../docs/explanation/cpu0-is-sacred.md | 51 + .../docs/explanation/no-ptp-on-a-pi-4.md | 52 + .../explanation/preempt-rt-made-it-worse.md | 113 + .../the-interrupt-you-cannot-move.md | 75 + .../docs/explanation/the-observer-effect.md | 82 + .../docs/explanation/where-precision-lives.md | 62 + docs-site/src/content/docs/findings.md | 50 + .../docs/how-to/benchmark-pps-jitter.md | 82 + .../docs/how-to/cross-compile-rt-kernel.md | 111 + .../src/content/docs/how-to/patch-pps-gpio.md | 76 + .../docs/how-to/survive-a-power-cut.md | 84 + docs-site/src/content/docs/index.mdx | 94 + .../content/docs/reference/configuration.md | 88 + .../src/content/docs/reference/downloads.md | 72 + .../src/content/docs/reference/hardware.md | 78 + .../content/docs/reference/measurements.md | 75 + .../src/content/docs/reference/the-patch.md | 65 + docs-site/src/styles/brass.css | 192 + docs-site/src/styles/terminal.css | 119 + docs-site/tsconfig.json | 5 + ...mestamp-in-hard-irq-under-PREEMPT_RT.patch | 18 + kernel/README.md | 79 + 56 files changed, 10297 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 dashboard/.env.example create mode 100644 dashboard/Makefile create mode 100644 dashboard/deploy/Caddyfile.tmpl create mode 100644 dashboard/deploy/caddy-affinity.conf create mode 100644 dashboard/deploy/deploy.sh create mode 100644 dashboard/deploy/gpsntp-dashboard.service create mode 100644 dashboard/deploy/gpsntp-dashboard.sudoers create mode 100644 dashboard/pyproject.toml create mode 100644 dashboard/src/gpsntp_dashboard/__init__.py create mode 100644 dashboard/src/gpsntp_dashboard/chrony.py create mode 100644 dashboard/src/gpsntp_dashboard/gpsd_reader.py create mode 100644 dashboard/src/gpsntp_dashboard/main.py create mode 100644 dashboard/src/gpsntp_dashboard/metrics.py create mode 100644 dashboard/src/gpsntp_dashboard/static/app.js create mode 100644 dashboard/src/gpsntp_dashboard/static/index.html create mode 100644 dashboard/src/gpsntp_dashboard/static/style.css create mode 100644 dashboard/src/gpsntp_dashboard/static/supported-systems-logo.svg create mode 100644 docs-site/.dockerignore create mode 100644 docs-site/.env.example create mode 100644 docs-site/.gitignore create mode 100644 docs-site/Dockerfile create mode 100644 docs-site/Makefile create mode 100644 docs-site/astro.config.mjs create mode 100644 docs-site/docker-compose.yml create mode 100644 docs-site/package-lock.json create mode 100644 docs-site/package.json create mode 100644 docs-site/public/favicon.svg create mode 100644 docs-site/public/supported-systems-logo.svg create mode 100644 docs-site/src/assets/logo.svg create mode 100644 docs-site/src/components/Footer.astro create mode 100644 docs-site/src/components/SupportedSystemsBadge.astro create mode 100644 docs-site/src/content.config.ts create mode 100644 docs-site/src/content/docs/explanation/cpu0-is-sacred.md create mode 100644 docs-site/src/content/docs/explanation/no-ptp-on-a-pi-4.md create mode 100644 docs-site/src/content/docs/explanation/preempt-rt-made-it-worse.md create mode 100644 docs-site/src/content/docs/explanation/the-interrupt-you-cannot-move.md create mode 100644 docs-site/src/content/docs/explanation/the-observer-effect.md create mode 100644 docs-site/src/content/docs/explanation/where-precision-lives.md create mode 100644 docs-site/src/content/docs/findings.md create mode 100644 docs-site/src/content/docs/how-to/benchmark-pps-jitter.md create mode 100644 docs-site/src/content/docs/how-to/cross-compile-rt-kernel.md create mode 100644 docs-site/src/content/docs/how-to/patch-pps-gpio.md create mode 100644 docs-site/src/content/docs/how-to/survive-a-power-cut.md create mode 100644 docs-site/src/content/docs/index.mdx create mode 100644 docs-site/src/content/docs/reference/configuration.md create mode 100644 docs-site/src/content/docs/reference/downloads.md create mode 100644 docs-site/src/content/docs/reference/hardware.md create mode 100644 docs-site/src/content/docs/reference/measurements.md create mode 100644 docs-site/src/content/docs/reference/the-patch.md create mode 100644 docs-site/src/styles/brass.css create mode 100644 docs-site/src/styles/terminal.css create mode 100644 docs-site/tsconfig.json create mode 100644 kernel/0001-pps-gpio-keep-timestamp-in-hard-irq-under-PREEMPT_RT.patch create mode 100644 kernel/README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e844850 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env +.venv/ +__pycache__/ +dist/ +*.egg-info/ +node_modules/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..82f28fe --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# The Cuckoo Escapement + +**What the Raspberry Pi time-server guides get wrong, and the numbers to prove it.** + +Docs: **[cuckoo.warehack.ing](https://cuckoo.warehack.ing)** + +We built a GPS-disciplined Stratum 1 NTP server on a Raspberry Pi 4, followed the +published advice, and measured everything. Most of that advice is wrong on this +board. One piece of it is wrong on *every* board. + +| Change | RMS offset | +|---|---| +| Baseline | 823 ns | +| chrony median `filter` + `prefer` on the PPS refclock | 440 ns | +| PREEMPT_RT, as the guides recommend | **2468 ns** ← *worse* | +| PREEMPT_RT + our `IRQF_NO_THREAD` patch | **199 ns** | + +## The one thing worth stealing + +[`kernel/0001-pps-gpio-keep-timestamp-in-hard-irq-under-PREEMPT_RT.patch`](kernel/) + +PREEMPT_RT force-threads interrupt handlers. `pps-gpio` takes its timestamp +*inside* its handler. So the realtime kernel — the marquee upgrade in every guide +— puts a scheduler between the electrical edge and the clock, and triples your +jitter. Four lines fix it. + +As far as we can tell this isn't applied anywhere, which means anyone running +GPIO-based PPS on a realtime kernel today is silently eating microseconds of +jitter with no reason to suspect it. chrony still says Stratum 1. Everything +still *looks* fine. + +## What's here + +| | | +|---|---| +| `kernel/` | The patch, and how to build it | +| `dashboard/` | The live status page (FastAPI + WebSocket, no build step) | +| `docs-site/` | The write-up — Astro / Starlight | + +## n = 1 + +Every number here comes from one Pi 4 and one GPS module. This is a field report, +not a study. We're publishing the method alongside the results precisely so you +can check it against your own board rather than take our word for it. + +--- + +A [Supported Systems](https://supported.systems) joint. diff --git a/dashboard/.env.example b/dashboard/.env.example new file mode 100644 index 0000000..aa83099 --- /dev/null +++ b/dashboard/.env.example @@ -0,0 +1,13 @@ +# Copy to .env. Nothing site-specific belongs in the Makefile. +PI=deploy@gps-ntp.local +KEY=~/.ssh/id_ed25519 +APPDIR=/opt/gpsntp-dashboard + +# Only needed if you front the dashboard with TLS via `make caddy`. +DOMAIN=clock.example.internal +CERT_SRC=$(HOME)/.certs/clock.example.internal + +# hidden (default) | coarse (~11 km) | exact +# The receiver knows exactly where it is. `hidden` keeps that out of the page — +# and out of any screenshot you post. Think before you change this. +GPSNTP_POSITION=hidden diff --git a/dashboard/Makefile b/dashboard/Makefile new file mode 100644 index 0000000..c6ac81f --- /dev/null +++ b/dashboard/Makefile @@ -0,0 +1,52 @@ +# gpsntp-dashboard — deploy to the Pi. +# +# Everything site-specific lives in .env (copy .env.example). Nothing in this +# file names a host, a domain, or a certificate path. +-include .env +export + +PI ?= deploy@gps-ntp.local +KEY ?= ~/.ssh/id_ed25519 +APPDIR ?= /opt/gpsntp-dashboard +DOMAIN ?= clock.example.internal + +SSH = ssh -i $(KEY) $(PI) +RSYNC = rsync -az -e "ssh -i $(KEY)" + +# Where your Let's Encrypt live/ directory is on THIS machine, and where the +# cert should land on the Pi. Only needed if you front the dashboard with TLS. +CERT_SRC ?= $(HOME)/.certs/$(DOMAIN) +CERT_DST ?= /etc/caddy/certs/$(DOMAIN) + +.PHONY: deploy logs restart status lint run caddy cert-sync + +deploy: + $(RSYNC) --delete --exclude '.venv' --exclude '.git' --exclude '__pycache__' --exclude 'dist' ./ $(PI):/tmp/gpsntp-src/ + $(SSH) 'sudo mkdir -p $(APPDIR) && sudo rsync -a --delete --exclude .venv /tmp/gpsntp-src/ $(APPDIR)/ && sudo APPDIR=$(APPDIR) bash $(APPDIR)/deploy/deploy.sh' + +logs: + $(SSH) 'journalctl -u gpsntp-dashboard -n 60 -f' + +restart: + $(SSH) 'sudo systemctl restart gpsntp-dashboard' + +status: + $(SSH) 'systemctl status gpsntp-dashboard --no-pager; curl -s localhost:8080/healthz' + +# Renders deploy/Caddyfile.tmpl with $(DOMAIN) and installs it. +caddy: + sed 's|{{DOMAIN}}|$(DOMAIN)|g' deploy/Caddyfile.tmpl > /tmp/Caddyfile.rendered + $(RSYNC) /tmp/Caddyfile.rendered $(PI):/tmp/Caddyfile + $(SSH) 'sudo cp /tmp/Caddyfile /etc/caddy/Caddyfile && sudo caddy validate --adapter caddyfile --config /etc/caddy/Caddyfile && sudo systemctl reload caddy && echo reloaded' + +# Push a renewed cert to the Pi. The Pi issues nothing itself — see +# deploy/Caddyfile.tmpl for why. +cert-sync: + $(RSYNC) -L $(CERT_SRC)/fullchain.pem $(CERT_SRC)/privkey.pem $(PI):/tmp/ + $(SSH) 'sudo mv /tmp/fullchain.pem /tmp/privkey.pem $(CERT_DST)/ && sudo chown -R caddy:caddy /etc/caddy/certs && sudo chmod 600 $(CERT_DST)/privkey.pem && sudo systemctl reload caddy && echo "cert synced + caddy reloaded"' + +lint: + uvx ruff check src/ + +run: + GPSD_HOST=$${GPSD_HOST:-gps-ntp.local} uv run gpsntp-dashboard diff --git a/dashboard/deploy/Caddyfile.tmpl b/dashboard/deploy/Caddyfile.tmpl new file mode 100644 index 0000000..06348cc --- /dev/null +++ b/dashboard/deploy/Caddyfile.tmpl @@ -0,0 +1,11 @@ +# gps-ntp dashboard front door. `make caddy` renders {{DOMAIN}} from .env. +# +# The cert is issued OFF-BOX and synced to the Pi (see `make cert-sync`) rather +# than obtained by Caddy itself. A time server usually lives on a LAN with no +# inbound reachability, so HTTP-01 can't work; use DNS-01 wherever you already +# run ACME and copy the result here. +{{DOMAIN}} { + tls /etc/caddy/certs/{{DOMAIN}}/fullchain.pem /etc/caddy/certs/{{DOMAIN}}/privkey.pem + encode zstd gzip + reverse_proxy 127.0.0.1:8080 +} diff --git a/dashboard/deploy/caddy-affinity.conf b/dashboard/deploy/caddy-affinity.conf new file mode 100644 index 0000000..dbfa7d2 --- /dev/null +++ b/dashboard/deploy/caddy-affinity.conf @@ -0,0 +1,10 @@ +# systemd drop-in: /etc/systemd/system/caddy.service.d/affinity.conf +# +# Keep Caddy off cpu0. The PPS interrupt is handled there and cannot be moved +# (Pi 4 GPIO IRQs are demuxed via pinctrl-bcm2835 and refuse an smp_affinity), +# so any work scheduled on cpu0 adds jitter to the PPS timestamp directly. +# Caddy is mostly idle, but on this box cpu0 belongs to the clock. See +# TIMING-NOTES.md. +[Service] +CPUAffinity=1 +Nice=10 diff --git a/dashboard/deploy/deploy.sh b/dashboard/deploy/deploy.sh new file mode 100644 index 0000000..92431fb --- /dev/null +++ b/dashboard/deploy/deploy.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Idempotent install/update of the gps-ntp dashboard. Run on the Pi as root +# from the synced source tree (APPDIR). Safe to re-run. +set -euo pipefail + +APPDIR="${APPDIR:-/opt/gpsntp-dashboard}" +SVC=gpsntp-dashboard + +echo "==> dedicated system user" +id -u gpsntp &>/dev/null || useradd --system --no-create-home --shell /usr/sbin/nologin gpsntp + +echo "==> python venv + editable install" +if [ ! -x "$APPDIR/.venv/bin/python" ]; then + if ! python3 -m venv "$APPDIR/.venv" 2>/dev/null; then + apt-get update -qq && apt-get install -y python3-venv python3-pip + python3 -m venv "$APPDIR/.venv" + fi +fi +"$APPDIR/.venv/bin/pip" install --quiet --upgrade pip +"$APPDIR/.venv/bin/pip" install --quiet -e "$APPDIR" + +echo "==> narrow sudoers rule (clients command only)" +install -m 0440 "$APPDIR/deploy/gpsntp-dashboard.sudoers" /etc/sudoers.d/gpsntp-dashboard +visudo -cf /etc/sudoers.d/gpsntp-dashboard + +echo "==> systemd unit" +install -m 0644 "$APPDIR/deploy/gpsntp-dashboard.service" "/etc/systemd/system/${SVC}.service" + +echo "==> ownership" +chown -R gpsntp:gpsntp "$APPDIR" + +echo "==> enable + (re)start" +systemctl daemon-reload +systemctl enable "${SVC}.service" >/dev/null 2>&1 || true +systemctl restart "${SVC}.service" +sleep 2 +systemctl is-active "${SVC}.service" && echo "==> dashboard active on :8080" diff --git a/dashboard/deploy/gpsntp-dashboard.service b/dashboard/deploy/gpsntp-dashboard.service new file mode 100644 index 0000000..355ae0f --- /dev/null +++ b/dashboard/deploy/gpsntp-dashboard.service @@ -0,0 +1,35 @@ +[Unit] +Description=gps-ntp status dashboard +Documentation=https://git.supported.systems/time-pi +After=network-online.target gpsd.service chrony.service +Wants=network-online.target + +[Service] +Type=exec +User=gpsntp +Group=gpsntp +ExecStart=/opt/gpsntp-dashboard/.venv/bin/gpsntp-dashboard +Environment=GPSNTP_PORT=8080 +Environment=GPSNTP_HOST=0.0.0.0 +Restart=on-failure +RestartSec=3 + +# Keep the dashboard OFF cpu0. The PPS interrupt is handled on cpu0 and cannot +# be moved (Pi 4 GPIO IRQs are demuxed through pinctrl-bcm2835 and refuse an +# smp_affinity), so any load there directly adds jitter to the timestamp. +# Measured: the dashboard cost ~36% more PPS jitter before this. It is a +# monitoring tool; it must never perturb the clock it is watching. +CPUAffinity=1 +Nice=10 + +# Hardening. NoNewPrivileges is intentionally NOT set: the served-clients +# panel shells `sudo -n chronyc -c clients`, allowed by a narrow sudoers rule. +PrivateTmp=true +ProtectHome=true +ProtectControlGroups=true +ProtectKernelTunables=true +RestrictSUIDSGID=false +LockPersonality=true + +[Install] +WantedBy=multi-user.target diff --git a/dashboard/deploy/gpsntp-dashboard.sudoers b/dashboard/deploy/gpsntp-dashboard.sudoers new file mode 100644 index 0000000..0f59500 --- /dev/null +++ b/dashboard/deploy/gpsntp-dashboard.sudoers @@ -0,0 +1,4 @@ +# Least-privilege: the dashboard user may run ONLY this one read-only command. +# `chronyc -c clients` is privileged (returns "501 Not authorised" otherwise), +# and this is the served-client list the dashboard displays. Nothing else. +gpsntp ALL=(root) NOPASSWD: /usr/bin/chronyc -c clients diff --git a/dashboard/pyproject.toml b/dashboard/pyproject.toml new file mode 100644 index 0000000..dbc46c9 --- /dev/null +++ b/dashboard/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "gpsntp-dashboard" +version = "2026.07.12" +description = "Live status dashboard for a GPS/PPS-disciplined chrony Stratum 1 time server" +readme = "README.md" +requires-python = ">=3.11" +authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}] +license = {text = "MIT"} +dependencies = [ + "fastapi>=0.115", + "uvicorn[standard]>=0.34", +] + +[project.scripts] +gpsntp-dashboard = "gpsntp_dashboard.main:run" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/gpsntp_dashboard"] + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B"] diff --git a/dashboard/src/gpsntp_dashboard/__init__.py b/dashboard/src/gpsntp_dashboard/__init__.py new file mode 100644 index 0000000..99efc2a --- /dev/null +++ b/dashboard/src/gpsntp_dashboard/__init__.py @@ -0,0 +1,3 @@ +"""Live status dashboard for a GPS/PPS-disciplined chrony Stratum 1 time server.""" + +__version__ = "2026.07.12" diff --git a/dashboard/src/gpsntp_dashboard/chrony.py b/dashboard/src/gpsntp_dashboard/chrony.py new file mode 100644 index 0000000..546aa93 --- /dev/null +++ b/dashboard/src/gpsntp_dashboard/chrony.py @@ -0,0 +1,144 @@ +"""Read chronyd state via `chronyc -c` (CSV) and shape it for the dashboard.""" + +from __future__ import annotations + +import asyncio + +# `chronyc -c sources` state/mode codes -> human meaning. +_MODE = {"^": "server", "=": "peer", "#": "refclock"} +_STATE = { + "*": "selected", + "+": "combined", + "-": "not_combined", + "?": "unreachable", + "x": "falseticker", + "~": "unstable", +} + + +async def _run(*args: str, stdin: bytes | None = None) -> str: + proc = await asyncio.create_subprocess_exec( + *args, + stdin=asyncio.subprocess.PIPE if stdin is not None else None, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + out, err = await proc.communicate(stdin) + if proc.returncode != 0: + raise RuntimeError(err.decode(errors="replace").strip() or "command failed") + return out.decode(errors="replace") + + +async def collect() -> dict: + """Fetch tracking + sources + sourcestats in a SINGLE chronyc process. + + chronyc reads commands from stdin, so one fork serves all three. This + matters: forking a process per metric per second measurably degraded PPS + jitter (~36%), because the churn lands on the same CPU that handles the + PPS interrupt -- and that IRQ can't be moved off it (GPIO mux). The three + outputs are told apart by field count: 14=tracking, 10=sources, 8=stats. + """ + text = await _run("chronyc", "-c", stdin=b"tracking\nsources\nsourcestats\n") + out: dict = {"tracking": None, "sources": [], "sourcestats": {}} + for line in text.splitlines(): + c = line.split(",") + if len(c) >= 14 and out["tracking"] is None: + out["tracking"] = _parse_tracking(c) + elif len(c) == 10: + out["sources"].append(_parse_source(c)) + elif len(c) == 8: + out["sourcestats"][c[0]] = _parse_sourcestat(c) + return out + + +def _f(value: str) -> float | None: + try: + return float(value) + except (ValueError, TypeError): + return None + + +def _parse_tracking(c: list[str]) -> dict: + """Reference, stratum, offsets, root delay/dispersion.""" + return { + "ref_id": c[0], + "ref_name": c[1], + "stratum": int(c[2]) if c[2].isdigit() else None, + "ref_time": _f(c[3]), + "system_time": _f(c[4]), + "last_offset": _f(c[5]), + "rms_offset": _f(c[6]), + "frequency_ppm": _f(c[7]), + "residual_freq_ppm": _f(c[8]), + "skew_ppm": _f(c[9]), + "root_delay": _f(c[10]), + "root_dispersion": _f(c[11]), + "update_interval": _f(c[12]), + "leap_status": c[13], + } + + +def _parse_source(c: list[str]) -> dict: + state = _STATE.get(c[1], c[1]) + try: + reach = int(c[5], 8) # chrony reports the reachability register in octal + except ValueError: + reach = None + + # chrony reports '?' both for a genuinely unreachable source AND for one it + # simply isn't considering -- notably our GPS refclock, which is `noselect` + # (it exists only to tell PPS which second it is, never to be chosen). If + # samples are still arriving (reach > 0) the source is plainly NOT + # unreachable, so don't cry wolf: call it what it is, a reference. + # Flagging a healthy source red trains you to ignore red. + if state == "unreachable" and reach: + state = "reference_only" + + return { + "mode": _MODE.get(c[0], c[0]), + "state": state, + "name": c[2], + "stratum": int(c[3]) if c[3].isdigit() else None, + "poll": int(c[4]) if c[4].lstrip("-").isdigit() else None, + "reach": reach, + "last_rx": int(c[6]) if c[6].lstrip("-").isdigit() else None, + "offset": _f(c[7]), + "offset_measured": _f(c[8]), + "error": _f(c[9]), + } + + +def _parse_sourcestat(c: list[str]) -> dict: + return { + "samples": int(c[1]) if c[1].isdigit() else None, + "runs": int(c[2]) if c[2].isdigit() else None, + "span": int(c[3]) if c[3].isdigit() else None, + "frequency_ppm": _f(c[4]), + "freq_skew_ppm": _f(c[5]), + "offset": _f(c[6]), + "std_dev": _f(c[7]), + } + + +async def clients() -> list[dict]: + """Served NTP clients. Privileged, so try sudo -n; degrade quietly if denied.""" + for cmd in (("chronyc", "-c", "clients"), ("sudo", "-n", "chronyc", "-c", "clients")): + try: + text = await _run(*cmd) + except RuntimeError: + continue + rows = [] + for line in text.splitlines(): + c = line.split(",") + if len(c) < 2 or not c[0]: + continue + rows.append( + { + "address": c[0], + "ntp_requests": int(c[1]) if c[1].isdigit() else None, + "ntp_dropped": int(c[2]) if len(c) > 2 and c[2].isdigit() else None, + "last_seen": int(c[4]) if len(c) > 4 and c[4].lstrip("-").isdigit() else None, + } + ) + return rows + return [] diff --git a/dashboard/src/gpsntp_dashboard/gpsd_reader.py b/dashboard/src/gpsntp_dashboard/gpsd_reader.py new file mode 100644 index 0000000..7b4b93b --- /dev/null +++ b/dashboard/src/gpsntp_dashboard/gpsd_reader.py @@ -0,0 +1,165 @@ +"""Async gpsd client: streams JSON, keeps the latest fix (TPV) and sky (SKY).""" + +from __future__ import annotations + +import asyncio +import json +import os + +# How much of the receiver's position to expose. DEFAULT IS "hidden", on purpose. +# +# A time server is stationary and its position contributes nothing to time +# accuracy -- so the coordinates are operationally useless here. But people +# screenshot dashboards and paste them into forums and issues, and a lat/lon to +# 7 decimal places is their home address. Leaking something sensitive to display +# something nobody needs is a bad trade. Opt in if you actually want it. +# +# hidden (default) : no coordinates; just "position locked" +# coarse : rounded to ~11 km -- enough to sanity-check the region +# exact : full precision +POSITION_MODE = os.environ.get("GPSNTP_POSITION", "hidden").strip().lower() + +# gpsd gnssid -> constellation name (used for colouring the sky plot). +CONSTELLATION = { + 0: "GPS", + 1: "SBAS", + 2: "Galileo", + 3: "BeiDou", + 4: "IMES", + 5: "QZSS", + 6: "GLONASS", + 7: "NavIC", +} + +_FIX_MODE = {0: "unknown", 1: "none", 2: "2D", 3: "3D"} +# TPV status: 2 = DGPS, 3 = RTK fixed, 4 = RTK float (best-effort labels). +_FIX_STATUS = {0: "unknown", 1: "GPS", 2: "DGPS", 3: "RTK-fixed", 4: "RTK-float"} + + +class GpsdReader: + """Connect to gpsd, WATCH, and cache the newest TPV + SKY reports.""" + + def __init__(self, host: str = "127.0.0.1", port: int = 2947): + self.host = host + self.port = port + self.tpv: dict = {} + self.satellites: list[dict] = [] + self.dop: dict = {} + self.connected = False + self._task: asyncio.Task | None = None + + def start(self) -> None: + self._task = asyncio.create_task(self._run()) + + async def stop(self) -> None: + if self._task: + self._task.cancel() + try: + await self._task + except asyncio.CancelledError: + pass + + async def _run(self) -> None: + while True: + try: + await self._read_loop() + except asyncio.CancelledError: + raise + except Exception: + self.connected = False + await asyncio.sleep(2) # gpsd down / restarting; retry + + async def _read_loop(self) -> None: + reader, writer = await asyncio.open_connection(self.host, self.port) + self.connected = True + writer.write(b'?WATCH={"enable":true,"json":true}\n') + await writer.drain() + try: + while True: + line = await reader.readline() + if not line: + break + self._ingest(line) + finally: + self.connected = False + writer.close() + + def _ingest(self, line: bytes) -> None: + try: + msg = json.loads(line) + except json.JSONDecodeError: + return + cls = msg.get("class") + if cls == "TPV": + # gpsd sends PARTIAL TPV reports — a frame may omit "mode", "lat", + # etc. Overwriting the cache would drop the fix state and render it + # as "unknown". Merge instead, so known fields survive until gpsd + # actually supersedes them. (Same trap as the DOP-only SKY frames.) + self.tpv.update(msg) + elif cls == "SKY": + # DOP-only SKY frames omit satellites[] — keep the last real list. + sats = msg.get("satellites") + if sats is not None: + self.satellites = sats + for k in ("hdop", "vdop", "pdop", "gdop", "tdop", "uSat", "nSat"): + if k in msg: + self.dop[k] = msg[k] + + def _fix_status(self, mode: int) -> str: + """TPV omits `status` when there's no augmentation (e.g. SBAS disabled). + An absent status on a valid fix means plain GPS, NOT 'unknown' -- the + absence of *extra* information is not the absence of information. + """ + status = self.tpv.get("status") + if not status: # None or 0 + return "GPS" if mode >= 2 else "none" + return _FIX_STATUS.get(status, "GPS") + + def _position(self) -> dict: + """Expose position per POSITION_MODE. See the note at the top of this file: + hidden by default, because a screenshotted dashboard should not publish + the operator's home address to display a number nobody needs. + """ + lat, lon = self.tpv.get("lat"), self.tpv.get("lon") + has_fix = lat is not None and lon is not None + if not has_fix or POSITION_MODE == "hidden": + return {"lat": None, "lon": None, "has_position": has_fix, + "position_mode": POSITION_MODE} + if POSITION_MODE == "coarse": + lat, lon = round(lat, 1), round(lon, 1) # ~11 km — region, not street + return {"lat": lat, "lon": lon, "has_position": True, + "position_mode": POSITION_MODE} + + def snapshot(self) -> dict: + t = self.tpv + mode = t.get("mode", 0) + sats = [ + { + "prn": s.get("PRN"), + "az": s.get("az"), + "el": s.get("el"), + "ss": s.get("ss"), + "used": bool(s.get("used")), + "constellation": CONSTELLATION.get(s.get("gnssid"), "other"), + } + for s in self.satellites + if s.get("az") is not None and s.get("el") is not None + ] + return { + "connected": self.connected, + "device": t.get("device"), + "fix_mode": _FIX_MODE.get(mode, "unknown"), + "fix_status": self._fix_status(mode), + **self._position(), # lat/lon gated by GPSNTP_POSITION (default: hidden) + "alt_m": t.get("altMSL", t.get("alt")), + "time": t.get("time"), + "leapseconds": t.get("leapseconds"), + "ept": t.get("ept"), + "epx": t.get("epx"), + "epy": t.get("epy"), + "hdop": self.dop.get("hdop"), + "pdop": self.dop.get("pdop"), + "sats_used": sum(1 for s in sats if s["used"]), + "sats_seen": len(sats), + "satellites": sats, + } diff --git a/dashboard/src/gpsntp_dashboard/main.py b/dashboard/src/gpsntp_dashboard/main.py new file mode 100644 index 0000000..f2fb26f --- /dev/null +++ b/dashboard/src/gpsntp_dashboard/main.py @@ -0,0 +1,158 @@ +"""FastAPI app: one background collector feeds the dashboard, /api, /metrics, /ws.""" + +from __future__ import annotations + +import asyncio +import contextlib +import os +import time +from collections import deque +from importlib.metadata import version +from pathlib import Path + +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse +from fastapi.staticfiles import StaticFiles + +from . import chrony, metrics +from .gpsd_reader import GpsdReader + +STATIC = Path(__file__).parent / "static" +POLL_SECONDS = float(os.environ.get("GPSNTP_POLL_SECONDS", "1")) +HISTORY_LEN = int(os.environ.get("GPSNTP_HISTORY_POINTS", "180")) # ~3 min at 1s +CLIENTS_EVERY = 5 # sample the privileged clients list every Nth cycle + +gpsd = GpsdReader( + host=os.environ.get("GPSD_HOST", "127.0.0.1"), + port=int(os.environ.get("GPSD_PORT", "2947")), +) + +# Shared state produced by the single collector task. +_history: deque[dict] = deque(maxlen=HISTORY_LEN) +_state: dict = {"snapshot": {"version": "?", "history": []}} + + +async def _build(include_clients: bool, prev_clients) -> dict: + """One pass over chrony + gpsd. Each source degrades independently.""" + snap: dict = {"gps": gpsd.snapshot(), "version": _state["snapshot"]["version"], + "server_time": time.time()} + # ONE chronyc process for tracking+sources+sourcestats. Forking a process + # per metric per second measurably worsened PPS jitter (~36%) -- the churn + # lands on the CPU that handles the PPS interrupt. See chrony.collect(). + try: + snap.update(await chrony.collect()) + except Exception as exc: + snap["tracking"] = snap["sources"] = snap["sourcestats"] = None + snap.setdefault("errors", {})["chrony"] = str(exc) + if include_clients: + try: + snap["clients"] = await chrony.clients() + except Exception as exc: + snap["clients"] = None + snap.setdefault("errors", {})["clients"] = str(exc) + else: + snap["clients"] = prev_clients + return snap + + +async def _collect_loop() -> None: + _state["snapshot"]["version"] = version("gpsntp-dashboard") + i = 0 + prev_clients = None + while True: + try: + snap = await _build(include_clients=(i % CLIENTS_EVERY == 0), prev_clients=prev_clients) + prev_clients = snap.get("clients") + t = snap.get("tracking") or {} + srcs = snap.get("sources") or [] + pps = next((s.get("offset") for s in srcs if s["name"] == "PPS"), None) + _history.append({"t": snap["server_time"], "sys": t.get("system_time"), "pps": pps}) + snap["history"] = list(_history) + _state["snapshot"] = snap + except Exception: + pass + i += 1 + await asyncio.sleep(POLL_SECONDS) + + +@contextlib.asynccontextmanager +async def lifespan(app: FastAPI): + gpsd.start() + task = asyncio.create_task(_collect_loop()) + yield + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task + await gpsd.stop() + + +app = FastAPI(title="gps-ntp dashboard", version=version("gpsntp-dashboard"), lifespan=lifespan) + + +@app.get("/api/status") +async def api_status() -> JSONResponse: + return JSONResponse(_state["snapshot"]) + + +@app.get("/metrics") +async def prometheus() -> PlainTextResponse: + return PlainTextResponse(metrics.render(_state["snapshot"]), media_type=metrics.CONTENT_TYPE) + + +@app.get("/healthz") +async def healthz() -> dict: + return {"ok": True, "gpsd_connected": gpsd.connected} + + +@app.websocket("/ws") +async def ws(socket: WebSocket) -> None: + await socket.accept() + try: + while True: + await socket.send_json(_state["snapshot"]) + await asyncio.sleep(POLL_SECONDS) + except WebSocketDisconnect: + pass + + +@app.get("/") +async def index() -> FileResponse: + return FileResponse(STATIC / "index.html") + + +class RevalidatingStatic(StaticFiles): + """StaticFiles, but the browser must ask before reusing a cached copy. + + Starlette sends an ETag and Last-Modified but no Cache-Control, so browsers + fall back to *heuristic* freshness (RFC 9111 4.2.2) and happily serve a + stale style.css for hours. Every deploy replaces these files at the same + URLs, so that means a dashboard someone left open shows old markup against + old CSS — which is exactly how we shipped an unstyled footer once. + + `no-cache` doesn't mean "don't cache", it means "revalidate before use". + The ETag still turns that into a 304 with an empty body, so the cost is one + conditional request for ~30 KB of assets on a LAN. Cheap; correct. + """ + + def is_not_modified(self, response_headers, request_headers) -> bool: # noqa: ANN001 + response_headers["cache-control"] = "no-cache" + return super().is_not_modified(response_headers, request_headers) + + async def get_response(self, path: str, scope): # noqa: ANN001, ANN201 + response = await super().get_response(path, scope) + response.headers["cache-control"] = "no-cache" + return response + + +app.mount("/static", RevalidatingStatic(directory=STATIC), name="static") + + +def run() -> None: + import uvicorn + + uvicorn.run( + "gpsntp_dashboard.main:app", + host=os.environ.get("GPSNTP_HOST", "0.0.0.0"), + port=int(os.environ.get("GPSNTP_PORT", "8080")), + log_level="info", + ) diff --git a/dashboard/src/gpsntp_dashboard/metrics.py b/dashboard/src/gpsntp_dashboard/metrics.py new file mode 100644 index 0000000..1d6f0f8 --- /dev/null +++ b/dashboard/src/gpsntp_dashboard/metrics.py @@ -0,0 +1,88 @@ +"""Render a status snapshot as Prometheus text exposition format (0.0.4).""" + +from __future__ import annotations + +CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8" + +_FIX_MODE_NUM = {"none": 0, "2D": 2, "3D": 3, "unknown": 0} + + +def _esc(v: str) -> str: + return str(v).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + + +def _labels(pairs: dict) -> str: + inner = ",".join(f'{k}="{_esc(v)}"' for k, v in pairs.items() if v is not None) + return f"{{{inner}}}" if inner else "" + + +def render(snap: dict) -> str: + out: list[str] = [] + + def metric(name: str, mtype: str, help_: str, samples: list[tuple[dict, float]]): + vals = [(lbls, v) for lbls, v in samples if v is not None] + if not vals: + return + out.append(f"# HELP {name} {help_}") + out.append(f"# TYPE {name} {mtype}") + for lbls, v in vals: + out.append(f"{name}{_labels(lbls)} {v}") + + metric("gpsntp_up", "gauge", "Dashboard collector produced a snapshot", [({}, 1)]) + + g = snap.get("gps") or {} + metric("gpsntp_gpsd_connected", "gauge", "gpsd socket connected", + [({}, 1 if g.get("connected") else 0)]) + metric("gpsntp_gps_fix_mode", "gauge", "GPS fix mode (0 none, 2 2D, 3 3D)", + [({}, _FIX_MODE_NUM.get(g.get("fix_mode"), 0))]) + metric("gpsntp_gps_satellites_used", "gauge", "Satellites used in the fix", + [({}, g.get("sats_used"))]) + metric("gpsntp_gps_satellites_visible", "gauge", "Satellites visible", + [({}, g.get("sats_seen"))]) + metric("gpsntp_gps_hdop", "gauge", "Horizontal dilution of precision", + [({}, g.get("hdop"))]) + + t = snap.get("tracking") or {} + metric("gpsntp_stratum", "gauge", "Stratum of this server", [({}, t.get("stratum"))]) + metric("gpsntp_system_offset_seconds", "gauge", "System clock offset from true time", + [({}, t.get("system_time"))]) + metric("gpsntp_last_offset_seconds", "gauge", "Last measured offset", + [({}, t.get("last_offset"))]) + metric("gpsntp_rms_offset_seconds", "gauge", "RMS offset", [({}, t.get("rms_offset"))]) + metric("gpsntp_frequency_ppm", "gauge", "Clock frequency correction", + [({}, t.get("frequency_ppm"))]) + metric("gpsntp_skew_ppm", "gauge", "Estimated frequency skew", [({}, t.get("skew_ppm"))]) + metric("gpsntp_root_delay_seconds", "gauge", "Total root delay to the reference", + [({}, t.get("root_delay"))]) + metric("gpsntp_root_dispersion_seconds", "gauge", "Total root dispersion", + [({}, t.get("root_dispersion"))]) + if t.get("ref_name"): + metric("gpsntp_reference_info", "gauge", "Current reference (value always 1)", + [({"ref_id": t.get("ref_id"), "ref_name": t.get("ref_name")}, 1)]) + + sources = snap.get("sources") or [] + stats = snap.get("sourcestats") or {} + metric("gpsntp_source_offset_seconds", "gauge", "Per-source last offset", + [({"name": s["name"], "mode": s["mode"], "state": s["state"]}, s.get("offset")) + for s in sources]) + metric("gpsntp_source_stratum", "gauge", "Per-source stratum", + [({"name": s["name"]}, s.get("stratum")) for s in sources]) + metric("gpsntp_source_reach", "gauge", "Per-source reachability register (0-255)", + [({"name": s["name"]}, s.get("reach")) for s in sources]) + metric("gpsntp_source_last_rx_seconds", "gauge", "Seconds since last sample from source", + [({"name": s["name"]}, s.get("last_rx")) for s in sources]) + metric("gpsntp_source_std_dev_seconds", "gauge", "Per-source estimated std deviation", + [({"name": n}, st.get("std_dev")) for n, st in stats.items()]) + + metric("gpsntp_satellite_snr_db", "gauge", "Per-satellite carrier-to-noise density", + [({"prn": s.get("prn"), "constellation": s.get("constellation"), + "used": "true" if s.get("used") else "false"}, s.get("ss")) + for s in (g.get("satellites") or []) if s.get("ss")]) + + clients = snap.get("clients") + if clients is not None: + metric("gpsntp_clients_total", "gauge", "Number of NTP clients seen", [({}, len(clients))]) + metric("gpsntp_client_ntp_requests_total", "counter", "NTP requests per client", + [({"address": c["address"]}, c.get("ntp_requests")) for c in clients]) + + return "\n".join(out) + "\n" diff --git a/dashboard/src/gpsntp_dashboard/static/app.js b/dashboard/src/gpsntp_dashboard/static/app.js new file mode 100644 index 0000000..021943f --- /dev/null +++ b/dashboard/src/gpsntp_dashboard/static/app.js @@ -0,0 +1,295 @@ +"use strict"; + +const CONSTS = { + GPS: getComputedStyle(document.documentElement).getPropertyValue("--c-gps").trim(), + GLONASS: cssVar("--c-glonass"), + Galileo: cssVar("--c-galileo"), + BeiDou: cssVar("--c-beidou"), + SBAS: cssVar("--c-sbas"), + QZSS: cssVar("--c-qzss"), + other: cssVar("--c-other"), +}; +function cssVar(n) { return getComputedStyle(document.documentElement).getPropertyValue(n).trim(); } +function color(constellation) { return CONSTS[constellation] || CONSTS.other; } +const $ = (id) => document.getElementById(id); + +// ---- clock (interpolated from the server's disciplined time) ---- +let clockBase = null; // { serverMs, perfMs } +function tickClock() { + let d; + if (clockBase) { + d = new Date(clockBase.serverMs + (performance.now() - clockBase.perfMs)); + } else { + d = new Date(); + } + const p = (n, w = 2) => String(n).padStart(w, "0"); + $("utc-clock").firstChild.nodeValue = + `${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}`; + $("utc-frac").textContent = "." + p(d.getUTCMilliseconds(), 3); + $("utc-date").textContent = d.toISOString().slice(0, 10); + requestAnimationFrame(tickClock); +} +requestAnimationFrame(tickClock); + +// ---- formatting ---- +function fmtOffset(sec) { + if (sec === null || sec === undefined) return "—"; + const a = Math.abs(sec); + const sign = sec < 0 ? "−" : "+"; + if (a < 1e-6) return `${sign}${(a * 1e9).toFixed(0)} ns`; + if (a < 1e-3) return `${sign}${(a * 1e6).toFixed(2)} µs`; + if (a < 1) return `${sign}${(a * 1e3).toFixed(3)} ms`; + return `${sign}${a.toFixed(3)} s`; +} +function fmtAbs(sec) { + if (sec === null || sec === undefined) return "—"; + const a = Math.abs(sec); + if (a < 1e-6) return `${(a * 1e9).toFixed(1)} ns`; + if (a < 1e-3) return `${(a * 1e6).toFixed(2)} µs`; + if (a < 1) return `${(a * 1e3).toFixed(2)} ms`; + return `${a.toFixed(3)} s`; +} +function offsetClass(sec) { + const a = Math.abs(sec ?? 1); + if (a < 1e-5) return "good"; + if (a < 1e-2) return "warn"; + return "bad"; +} + +// ---- render ---- +function render(d) { + if (typeof d.server_time === "number") { + clockBase = { serverMs: d.server_time * 1000, perfMs: performance.now() }; + } + renderTracking(d.tracking); + renderGps(d.gps); + // Real PPS jitter comes from chrony's per-source stats (nanoseconds), not + // gpsd's coarse NMEA time-error estimate. + const ppsStats = d.sourcestats && d.sourcestats["PPS"]; + if (ppsStats && ppsStats.std_dev != null) { + $("pps-jitter").textContent = "±" + fmtAbs(ppsStats.std_dev); + } + renderSources(d.sources, d.tracking); + renderClients(d.clients); + renderSky(d.gps ? d.gps.satellites : []); + renderSnr(d.gps ? d.gps.satellites : []); + renderSpark(d.history); + $("foot-version").textContent = "gpsntp-dashboard v" + (d.version || "?"); + $("foot-updated").textContent = "updated " + new Date().toLocaleTimeString(); +} + +function renderTracking(t) { + const badge = $("stratum-badge"); + if (!t) { $("stratum-num").textContent = "—"; badge.dataset.ok = "false"; return; } + $("stratum-num").textContent = t.stratum ?? "—"; + $("ref-name").textContent = t.ref_name || "—"; + const synced = t.stratum === 1 && t.leap_status === "Normal"; + badge.dataset.ok = synced ? "true" : "false"; + + const sys = $("sys-offset"); + sys.textContent = fmtOffset(t.system_time); + sys.className = "card-value " + offsetClass(t.system_time); + $("last-offset").textContent = fmtAbs(t.last_offset); + $("root-delay").textContent = fmtAbs(t.root_delay); + $("root-disp").textContent = fmtAbs(t.root_dispersion); + $("freq").textContent = t.frequency_ppm != null ? t.frequency_ppm.toFixed(3) + " ppm" : "—"; + $("skew").textContent = t.skew_ppm != null ? t.skew_ppm.toFixed(3) + " ppm" : "—"; +} + +function renderGps(g) { + if (!g) return; + const fix = $("fix-mode"); + fix.textContent = g.fix_mode === "3D" ? (g.fix_status || "3D") : (g.fix_mode || "—"); + fix.className = "card-value " + (g.fix_mode === "3D" ? "good" : g.fix_mode === "2D" ? "warn" : "bad"); + $("sats-line").textContent = `${g.sats_used}/${g.sats_seen} sats`; + $("hdop").textContent = g.hdop != null ? g.hdop.toFixed(2) : "—"; + // Position is HIDDEN by default (GPSNTP_POSITION). A stationary time server's + // coordinates do nothing for time accuracy, but a screenshotted dashboard with + // 7 decimal places is somebody's home address. Confirm the fix, don't dox them. + const pin = ` `; + const loc = $("location"); + if (g.lat != null && g.lon != null) { + const dp = g.position_mode === "coarse" ? 1 : 4; + loc.innerHTML = pin + `${g.lat.toFixed(dp)}, ${g.lon.toFixed(dp)}` + + (g.position_mode === "coarse" ? " approx" : ""); + } else if (g.has_position) { + loc.innerHTML = pin + "position locked"; + } else { + loc.innerHTML = pin + "no position"; + } + + // PPS card: pull the PPS source stats via the sources render; here show fix time error. + const pps = $("pps-state"); + if (g.fix_mode === "3D") { pps.textContent = "locked"; pps.className = "card-value good"; } + else { pps.textContent = "waiting"; pps.className = "card-value warn"; } + $("pps-jitter").textContent = g.ept != null ? "±" + fmtAbs(g.ept) : "—"; +} + +function renderSources(sources, tracking) { + const tb = $("sources").querySelector("tbody"); + tb.innerHTML = ""; + if (!sources) { tb.innerHTML = `chronyd unavailable`; return; } + for (const s of sources) { + const tr = document.createElement("tr"); + if (s.state === "selected") tr.className = "sel"; + if (s.mode === "refclock") tr.classList.add("refclk"); + const reach = s.reach === 255 ? "377" : (s.reach ?? "—"); + tr.innerHTML = + `${esc(s.name)}` + + `${s.mode}` + + `${s.stratum ?? "—"}` + + `${reach}` + + `${s.last_rx != null ? s.last_rx + "s" : "—"}` + + `${fmtOffset(s.offset)}` + + `${s.state.replace("_", " ")}`; + tb.appendChild(tr); + } + // PPS jitter into the card if PPS source present +} + +function renderClients(clients) { + const el = $("clients"); + $("clients-count").textContent = clients ? clients.length : "—"; + if (clients === null) { el.innerHTML = `Client list needs elevated access (not granted).`; return; } + if (clients.length === 0) { el.innerHTML = `No clients have queried yet.`; return; } + el.innerHTML = ""; + for (const c of clients) { + const div = document.createElement("div"); + div.className = "client"; + div.innerHTML = `${esc(c.address)}` + + `${c.ntp_requests ?? 0} requests` + + `${c.last_seen != null ? " · " + c.last_seen + "s ago" : ""}`; + el.appendChild(div); + } +} + +// ---- sky plot ---- +const SKY = { cx: 200, cy: 200, r: 178 }; +function elAzToXY(el, az) { + const rr = SKY.r * (90 - Math.max(0, Math.min(90, el))) / 90; + const a = (az * Math.PI) / 180; + return [SKY.cx + rr * Math.sin(a), SKY.cy - rr * Math.cos(a)]; +} +function svgEl(tag, attrs) { + const e = document.createElementNS("http://www.w3.org/2000/svg", tag); + for (const k in attrs) e.setAttribute(k, attrs[k]); + return e; +} +function drawSkyFrame(svg) { + for (const [el, cls] of [[0, "sky-ring"], [30, "sky-ring faint"], [60, "sky-ring faint"]]) { + const rr = SKY.r * (90 - el) / 90; + svg.appendChild(svgEl("circle", { cx: SKY.cx, cy: SKY.cy, r: rr, class: cls })); + } + svg.appendChild(svgEl("line", { x1: SKY.cx, y1: SKY.cy - SKY.r, x2: SKY.cx, y2: SKY.cy + SKY.r, class: "sky-cross" })); + svg.appendChild(svgEl("line", { x1: SKY.cx - SKY.r, y1: SKY.cy, x2: SKY.cx + SKY.r, y2: SKY.cy, class: "sky-cross" })); + for (const [lbl, x, y] of [["N", SKY.cx, SKY.cy - SKY.r - 5], ["S", SKY.cx, SKY.cy + SKY.r + 13], + ["E", SKY.cx + SKY.r + 8, SKY.cy + 4], ["W", SKY.cx - SKY.r - 8, SKY.cy + 4]]) { + const t = svgEl("text", { x, y, class: "sky-card-label", "text-anchor": "middle" }); + t.textContent = lbl; svg.appendChild(t); + } + const t30 = svgEl("text", { x: SKY.cx + 4, y: SKY.cy - SKY.r * (60 / 90) - 3, class: "sky-ring-label" }); + t30.textContent = "30°"; svg.appendChild(t30); +} +function renderSky(sats) { + const svg = $("sky"); + svg.innerHTML = ""; + drawSkyFrame(svg); + for (const s of sats || []) { + if (s.el == null || s.az == null) continue; + const [x, y] = elAzToXY(s.el, s.az); + const rad = 4 + (s.ss ? Math.min(9, s.ss / 6) : 1.5); + const col = color(s.constellation); + svg.appendChild(svgEl("circle", { cx: x, cy: y, r: rad + 4, fill: col, class: "sat-halo" })); + const dot = svgEl("circle", { + cx: x, cy: y, r: rad, class: "sat", + fill: s.used ? col : "transparent", stroke: col, "stroke-width": s.used ? 0 : 1.6, + }); + dot.addEventListener("pointerenter", (e) => showTip(e, s)); + dot.addEventListener("pointerleave", hideTip); + svg.appendChild(dot); + if (s.used && rad >= 6 && s.prn != null) { + const lbl = svgEl("text", { x, y: y + 3, class: "sat-label", "text-anchor": "middle" }); + lbl.textContent = s.prn; svg.appendChild(lbl); + } + } + renderLegend(sats || []); +} +function renderLegend(sats) { + const present = [...new Set(sats.map((s) => s.constellation))]; + const order = ["GPS", "GLONASS", "Galileo", "BeiDou", "SBAS", "QZSS", "other"]; + present.sort((a, b) => order.indexOf(a) - order.indexOf(b)); + $("legend").innerHTML = present.map((c) => + `${c}`).join(""); +} +function showTip(e, s) { + const tip = $("sky-tip"); + const hold = tip.parentElement.getBoundingClientRect(); + tip.hidden = false; + tip.innerHTML = `${s.constellation} #${s.prn ?? "?"}
el ${Math.round(s.el)}° · az ${Math.round(s.az)}°` + + `
${s.ss != null ? s.ss + " dB-Hz" : "no signal"}${s.used ? " · in fix" : ""}`; + tip.style.left = (e.clientX - hold.left) + "px"; + tip.style.top = (e.clientY - hold.top) + "px"; +} +function hideTip() { $("sky-tip").hidden = true; } + +// ---- snr bars ---- +function renderSnr(sats) { + const el = $("snr"); + const withSig = (sats || []).filter((s) => s.ss != null && s.ss > 0) + .sort((a, b) => b.ss - a.ss); + if (withSig.length === 0) { el.innerHTML = `
No satellite signals reported.
`; return; } + el.innerHTML = ""; + for (const s of withSig) { + const bar = document.createElement("div"); + bar.className = "snr-bar" + (s.used ? " used" : ""); + const h = Math.max(3, Math.min(100, (s.ss / 55) * 100)); + bar.innerHTML = `
` + + `
${s.prn ?? ""}
`; + bar.title = `${s.constellation} #${s.prn}: ${s.ss} dB-Hz${s.used ? " (used)" : ""}`; + el.appendChild(bar); + } +} + +// ---- offset history sparkline ---- +function renderSpark(history) { + const svg = $("spark"); + const W = 1000, H = 120, pad = 10; + const pts = (history || []).filter((p) => p.sys != null); + if (pts.length < 2) { svg.innerHTML = ""; $("spark-meta").textContent = "collecting…"; return; } + const maxAbs = Math.max(2e-7, ...pts.map((p) => Math.abs(p.sys))); + const span = maxAbs * 1.2; + const zeroY = H / 2; + const xOf = (i) => pad + (i / (pts.length - 1)) * (W - 2 * pad); + const yOf = (v) => zeroY - (v / span) * (H / 2 - pad); + let line = ""; + pts.forEach((p, i) => { line += (i ? "L" : "M") + xOf(i).toFixed(1) + " " + yOf(p.sys).toFixed(1) + " "; }); + const area = `M ${xOf(0).toFixed(1)} ${zeroY} ` + + pts.map((p, i) => `L ${xOf(i).toFixed(1)} ${yOf(p.sys).toFixed(1)}`).join(" ") + + ` L ${xOf(pts.length - 1).toFixed(1)} ${zeroY} Z`; + const a = cssVar("--accent"); + svg.innerHTML = + `` + + `` + + `` + + `` + + `` + + ``; + $("spark-meta").textContent = + `now ${fmtOffset(pts[pts.length - 1].sys)} · peak ±${fmtAbs(maxAbs)} · ${pts.length}s`; +} + +function esc(s) { const d = document.createElement("div"); d.textContent = s ?? ""; return d.innerHTML; } + +// ---- websocket with reconnect ---- +function setConn(state, label) { + const c = $("conn"); c.dataset.state = state; $("conn-label").textContent = label; +} +function connect() { + const proto = location.protocol === "https:" ? "wss" : "ws"; + const ws = new WebSocket(`${proto}://${location.host}/ws`); + ws.onopen = () => setConn("live", "live"); + ws.onmessage = (ev) => { try { render(JSON.parse(ev.data)); } catch (e) { console.error(e); } }; + ws.onclose = () => { setConn("down", "reconnecting…"); setTimeout(connect, 2000); }; + ws.onerror = () => ws.close(); +} +connect(); diff --git a/dashboard/src/gpsntp_dashboard/static/index.html b/dashboard/src/gpsntp_dashboard/static/index.html new file mode 100644 index 0000000..60508f5 --- /dev/null +++ b/dashboard/src/gpsntp_dashboard/static/index.html @@ -0,0 +1,180 @@ + + + + + + + gps-ntp · time server + + + + + + + +
+
+ +
+

gps-ntp

+

GPS · PPS disciplined time server

+
+
+
+ + connecting… +
+
+ +
+ +
+
+
--:--:--.000
+
· UTC
+
+
+
+ +
+ + stratum +
+
+
synced to
+
+
+ + +
+
+
System offset
+
+
of true time · last
+
+
+
PPS lock
+
+
jitter
+
+
+
GPS fix
+
+
· HDOP
+
+
+
Root delay
+
+
dispersion
+
+
+
Clock freq
+
+
skew
+
+
+
Clients served
+
+
+
+
+ + +
+
+

System offset · last 3 min

+
+
+ +
+ +
+ +
+
+

Sky view

+
+
+
+ + +
+
+ + +
+

Signal (C/N₀ dB-Hz)

+
+
+
+ + +
+

Time sources

+
+ + + + + +
SourceTypeStrReachLastOffsetState
+
+
+ + +
+

Network clients

+
+
+
+ + + + + + diff --git a/dashboard/src/gpsntp_dashboard/static/style.css b/dashboard/src/gpsntp_dashboard/static/style.css new file mode 100644 index 0000000..db2eef2 --- /dev/null +++ b/dashboard/src/gpsntp_dashboard/static/style.css @@ -0,0 +1,225 @@ +:root { + --bg: #0a0e13; + --bg-2: #0e141c; + --panel: #121a23; + --panel-2: #16212d; + --border: #21303f; + --border-soft: #1a2632; + --text: #e7eef5; + --muted: #8a99a8; + --faint: #57687a; + --accent: #22d3ee; + --good: #34d399; + --warn: #f5a524; + --bad: #f4436b; + /* constellations (no purple) */ + --c-gps: #34d399; + --c-glonass: #38bdf8; + --c-galileo: #f5a524; + --c-beidou: #fb7185; + --c-sbas: #a3e635; + --c-qzss: #2dd4bf; + --c-other: #94a3b8; + --shadow: 0 1px 0 rgba(255, 255, 255, 0.03), 0 12px 30px -18px rgba(0, 0, 0, 0.9); + --mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, monospace; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + background: + radial-gradient(1200px 600px at 80% -10%, rgba(34, 211, 238, 0.06), transparent 60%), + radial-gradient(900px 500px at 0% 0%, rgba(52, 211, 153, 0.05), transparent 55%), + var(--bg); + color: var(--text); + font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + -webkit-font-smoothing: antialiased; + min-height: 100vh; +} + +svg { width: 1.15em; height: 1.15em; fill: none; stroke: currentColor; stroke-width: 2; + stroke-linecap: round; stroke-linejoin: round; } + +/* ---------- top bar ---------- */ +.topbar { + display: flex; align-items: center; justify-content: space-between; + gap: 16px; padding: 16px 20px; + border-bottom: 1px solid var(--border-soft); + position: sticky; top: 0; z-index: 5; + background: linear-gradient(var(--bg), rgba(10, 14, 19, 0.86)); + backdrop-filter: blur(8px); +} +.brand { display: flex; align-items: center; gap: 13px; } +.brand-icon { width: 30px; height: 30px; color: var(--accent); } +.brand h1 { font-size: 19px; margin: 0; letter-spacing: 0.3px; font-family: var(--mono); } +.brand-sub { margin: 0; color: var(--muted); font-size: 12.5px; } + +.conn { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); + padding: 6px 12px; border: 1px solid var(--border); border-radius: 999px; background: var(--panel); } +.conn-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--faint); } +.conn[data-state="live"] .conn-dot { background: var(--good); box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.5); + animation: pulse 2s infinite; } +.conn[data-state="live"] { color: var(--good); } +.conn[data-state="down"] .conn-dot { background: var(--bad); } +.conn[data-state="down"] { color: var(--bad); } +@keyframes pulse { 70% { box-shadow: 0 0 0 7px rgba(52, 211, 153, 0); } 100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); } } + +/* ---------- layout ---------- */ +main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18px; } +.panel { + background: linear-gradient(var(--panel), var(--bg-2)); + border: 1px solid var(--border-soft); border-radius: 16px; padding: 18px; + box-shadow: var(--shadow); +} +.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; } +.panel-head h2 { font-size: 14px; margin: 0; color: var(--muted); font-weight: 600; + display: flex; align-items: center; gap: 8px; text-transform: uppercase; letter-spacing: 0.6px; } +.panel-head h2 svg { color: var(--accent); } + +/* ---------- hero ---------- */ +.hero { display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; } +.clock { font-family: var(--mono); font-size: clamp(40px, 11vw, 76px); font-weight: 650; + letter-spacing: 1px; line-height: 1; font-variant-numeric: tabular-nums; + text-shadow: 0 0 34px rgba(34, 211, 238, 0.18); } +.clock-frac { color: var(--accent); font-size: 0.42em; } +.clock-meta { color: var(--muted); margin-top: 8px; font-size: 13.5px; } +.mono { font-family: var(--mono); } + +.verdict { text-align: right; } +.stratum-badge { display: inline-flex; align-items: center; gap: 12px; padding: 12px 18px; + border-radius: 14px; border: 1px solid var(--border); background: var(--panel-2); } +.stratum-badge svg { width: 26px; height: 26px; color: var(--faint); } +.stratum-badge[data-ok="true"] { border-color: rgba(52, 211, 153, 0.5); + background: linear-gradient(rgba(52, 211, 153, 0.14), rgba(52, 211, 153, 0.04)); } +.stratum-badge[data-ok="true"] svg { color: var(--good); } +.stratum-num { font-size: 30px; font-weight: 750; font-family: var(--mono); display: block; line-height: 1; } +.stratum-badge[data-ok="true"] .stratum-num { color: var(--good); } +.stratum-word { font-size: 11px; text-transform: uppercase; letter-spacing: 2px; color: var(--muted); } +.ref-line { color: var(--muted); font-size: 13px; margin-top: 8px; } +.ref-line strong { color: var(--text); font-family: var(--mono); } + +/* ---------- cards ---------- */ +.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); gap: 12px; } +.card { background: var(--panel); border: 1px solid var(--border-soft); border-radius: 14px; padding: 14px 15px; } +.card-head { display: flex; align-items: center; gap: 7px; color: var(--muted); font-size: 12px; + text-transform: uppercase; letter-spacing: 0.4px; } +.card-head svg { width: 15px; height: 15px; color: var(--accent); } +.card-value { font-family: var(--mono); font-size: 25px; font-weight: 650; margin: 7px 0 3px; letter-spacing: 0.3px; } +.card-value.good { color: var(--good); } +.card-value.warn { color: var(--warn); } +.card-value.bad { color: var(--bad); } +.card-sub { color: var(--faint); font-size: 12px; } +.card-sub .ic-inline { width: 12px; height: 12px; vertical-align: -2px; } + +/* ---------- sparkline ---------- */ +.spark-meta { font: 12px var(--mono); color: var(--muted); } +.spark-big { width: 100%; height: 120px; display: block; } +.spark-zero { stroke: var(--border); stroke-width: 1; stroke-dasharray: 4 4; } +.spark-area { fill: url(#spark-grad); opacity: 0.9; } +.spark-line { fill: none; stroke: var(--accent); stroke-width: 1.6; vector-effect: non-scaling-stroke; } + +/* ---------- grid-2 ---------- */ +.grid-2 { display: grid; grid-template-columns: 1fr; gap: 18px; } +@media (min-width: 820px) { .grid-2 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } } + +/* ---------- sky ---------- */ +.sky-hold { position: relative; } +#sky { width: 100%; height: auto; display: block; } +.sky-ring { fill: none; stroke: var(--border); stroke-width: 1; } +.sky-ring.faint { stroke: var(--border-soft); } +.sky-cross { stroke: var(--border-soft); stroke-width: 1; } +.sky-card-label { fill: var(--faint); font: 600 11px var(--mono); } +.sky-ring-label { fill: var(--faint); font: 10px var(--mono); } +.sat { cursor: pointer; transition: r 0.4s ease; } +.sat-halo { opacity: 0.18; } +.sat-label { fill: #05080c; font: 700 8px var(--mono); pointer-events: none; } +.legend { display: flex; flex-wrap: wrap; gap: 10px; } +.legend span { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--muted); } +.legend i { width: 9px; height: 9px; border-radius: 50%; display: inline-block; } +.sky-tip { position: absolute; pointer-events: none; background: #05090e; border: 1px solid var(--border); + border-radius: 8px; padding: 6px 9px; font: 12px/1.35 var(--mono); color: var(--text); + transform: translate(-50%, -120%); white-space: nowrap; z-index: 3; box-shadow: var(--shadow); } + +/* ---------- snr bars ---------- */ +.snr-panel { display: flex; flex-direction: column; } +.snr { display: flex; align-items: flex-end; gap: 5px; flex: 1; min-height: 190px; overflow-x: auto; padding-top: 6px; } +.snr-bar { flex: 0 0 auto; width: 18px; display: flex; flex-direction: column; align-items: center; + justify-content: flex-end; height: 100%; gap: 4px; } +.snr-fill { width: 100%; border-radius: 4px 4px 2px 2px; min-height: 3px; transition: height 0.5s ease; opacity: 0.55; } +.snr-bar.used .snr-fill { opacity: 1; } +.snr-prn { font: 9px var(--mono); color: var(--faint); } +.snr-empty { color: var(--faint); align-self: center; margin: auto; font-size: 13px; } + +/* ---------- table ---------- */ +.table-hold { overflow-x: auto; } +.tbl { width: 100%; border-collapse: collapse; font-size: 13.5px; } +.tbl th { text-align: left; color: var(--faint); font-weight: 600; font-size: 11px; + text-transform: uppercase; letter-spacing: 0.5px; padding: 6px 10px; border-bottom: 1px solid var(--border); } +.tbl td { padding: 9px 10px; border-bottom: 1px solid var(--border-soft); font-family: var(--mono); } +.tbl .num { text-align: right; } +.tbl tr.sel td { background: rgba(52, 211, 153, 0.06); } +.tbl tr.refclk td:first-child { color: var(--accent); } +.pill { display: inline-block; padding: 2px 9px; border-radius: 999px; font-size: 11px; + border: 1px solid var(--border); color: var(--muted); font-family: var(--mono); } +.pill.selected { color: var(--good); border-color: rgba(52, 211, 153, 0.45); background: rgba(52, 211, 153, 0.08); } +.pill.combined { color: var(--accent); border-color: rgba(34, 211, 238, 0.35); } +/* A noselect refclock (our GPS: labels the second, never chosen) is healthy, + not broken. Render it as quiet information so that red keeps meaning red. */ +.pill.reference_only { color: var(--faint); border-color: var(--border-soft); } +.pill.unreachable, .pill.falseticker { color: var(--bad); border-color: rgba(244, 67, 107, 0.35); } + +/* ---------- clients ---------- */ +.clients { display: flex; flex-wrap: wrap; gap: 10px; } +.client { display: flex; flex-direction: column; gap: 2px; padding: 10px 13px; border: 1px solid var(--border-soft); + border-radius: 11px; background: var(--panel); min-width: 150px; } +.client .addr { font-family: var(--mono); font-size: 13px; } +.client .meta { color: var(--faint); font-size: 11.5px; } +.empty-note { color: var(--faint); font-size: 13px; } + +/* ---------- footer ---------- */ +.foot { max-width: 1180px; margin: 0 auto; padding: 14px 20px 30px; } +.foot-meta { display: flex; justify-content: space-between; margin-top: 14px; + color: var(--faint); font-size: 12px; font-family: var(--mono); } + +/* ---------- the maker's plate ---------- + * Clockmakers signed the backplate — the brass face only a repairer sees once + * the case is open. This footer is that plate: a double hairline for the plate + * edge, engraved small-caps for the name, and nothing that moves. A signature + * should be quiet. Same markup as the docs site at cuckoo.warehack.ing, + * recolored from brass to this dashboard's cyan. + */ +.ss-plate__link { + display: flex; gap: 16px; align-items: center; + padding: 18px 20px; text-decoration: none; color: var(--muted); + border: 1px solid var(--border); border-radius: 10px; + box-shadow: 0 0 0 3px var(--bg), 0 0 0 4px var(--border-soft), var(--shadow); + background: + radial-gradient(120% 140% at 0% 0%, rgba(34, 211, 238, 0.07), transparent 60%), + var(--panel); + transition: color .2s, border-color .2s; +} +.ss-plate__link:hover { color: var(--text); border-color: var(--accent); } + +.ss-plate__logo { flex: 0 0 auto; width: 46px; height: auto; opacity: .85; + transition: opacity .2s; } +.ss-plate__link:hover .ss-plate__logo { opacity: 1; } + +.ss-plate__heading { display: block; margin-bottom: 5px; color: var(--text); + font-family: var(--mono); font-size: 12px; font-weight: 600; + text-transform: uppercase; letter-spacing: .14em; } +.ss-plate__body { display: block; font-size: 13px; line-height: 1.55; max-width: 62ch; } +.ss-plate__name { color: var(--accent); } +.ss-plate__cta { display: inline-flex; align-items: center; gap: 5px; margin-top: 8px; + font-size: 12px; color: var(--accent); } +.ss-plate__cta svg { width: 14px; height: 14px; transition: transform .2s; } +.ss-plate__link:hover .ss-plate__cta svg { transform: translateX(2px); } + +@media (max-width: 560px) { + .ss-plate__link { flex-direction: column; align-items: flex-start; } +} + +@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } } + +/* "approx" marker when GPSNTP_POSITION=coarse */ +.pos-note { color: var(--faint); font-size: 10px; text-transform: uppercase; letter-spacing: .5px; } diff --git a/dashboard/src/gpsntp_dashboard/static/supported-systems-logo.svg b/dashboard/src/gpsntp_dashboard/static/supported-systems-logo.svg new file mode 100644 index 0000000..d4b85ba --- /dev/null +++ b/dashboard/src/gpsntp_dashboard/static/supported-systems-logo.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10 + 01 + 11 + 00 + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs-site/.dockerignore b/docs-site/.dockerignore new file mode 100644 index 0000000..f64e7ca --- /dev/null +++ b/docs-site/.dockerignore @@ -0,0 +1,19 @@ +node_modules/ +dist/ +.astro/ + +.env +.env.local +.env.production + +.git/ +.gitignore + +*.log +.DS_Store +.vscode/ +.idea/ + +# don't pull in repo-root artifacts +../artifacts/ +README.md diff --git a/docs-site/.env.example b/docs-site/.env.example new file mode 100644 index 0000000..e77a21f --- /dev/null +++ b/docs-site/.env.example @@ -0,0 +1,9 @@ +# The Cuckoo Escapement — docs-site environment. +# Note: no MODE var. The compose *profile* is the mode switch +# (the warehacking reference doc is stale on this point). + +COMPOSE_PROJECT_NAME=cuckoo-escapement-docs + +# Production: cuckoo.warehack.ing +# Local dev: cuckoo.l.warehack.ing (internal only — never reference publicly) +DOMAIN=cuckoo.warehack.ing diff --git a/docs-site/.gitignore b/docs-site/.gitignore new file mode 100644 index 0000000..3f7bed7 --- /dev/null +++ b/docs-site/.gitignore @@ -0,0 +1,22 @@ +# build output +dist/ +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment +.env +.env.local +.env.production + +# editor +.vscode/ +.idea/ +.DS_Store diff --git a/docs-site/Dockerfile b/docs-site/Dockerfile new file mode 100644 index 0000000..5e489ec --- /dev/null +++ b/docs-site/Dockerfile @@ -0,0 +1,47 @@ +# Multi-stage build for the cuckoo-escapement docs site. +# +# Stages: +# - base : Node, pnpm/npm tooling, deps installed +# - dev : runs `astro dev` with HMR for local development +# - builder : produces the static `dist/` +# - prod : caddy:alpine that serves `dist/` (no Node at runtime) +# +# `docker compose --profile dev up` → dev target +# `docker compose up` (no profile) → prod target + +# Pinned through the mirror.gcr.io pass-through to dodge intermittent +# Docker Hub TLS hiccups during builds. Same content, more reliable +# fetch path. The `docker pull` resolves identically against either. +FROM mirror.gcr.io/library/node:22-alpine AS base +WORKDIR /app +COPY package.json ./ +RUN --mount=type=cache,target=/root/.npm \ + npm install --no-audit --no-fund + + +# ----- dev: astro dev server with HMR ----- +FROM base AS dev +# Astro's binary is in node_modules/.bin — package.json's `dev` script +# already binds to 0.0.0.0 for HMR-behind-Caddy. +COPY . . +ENV ASTRO_TELEMETRY_DISABLED=1 +EXPOSE 4321 +CMD ["npm", "run", "dev"] + + +# ----- builder: produce dist/ ----- +FROM base AS builder +COPY . . +ENV ASTRO_TELEMETRY_DISABLED=1 +RUN npm run build + + +# ----- prod: caddy serves the static build ----- +FROM mirror.gcr.io/library/caddy:2-alpine AS prod +# Caddyfile is intentionally minimal — caddy-docker-proxy on the host +# handles TLS, routing, and the public-facing reverse proxy. This +# container just serves files locally; the proxy points at it. +RUN mkdir -p /srv/docs +COPY --from=builder /app/dist /srv/docs +RUN printf ':80 {\n\troot * /srv/docs\n\tfile_server\n\ttry_files {path} {path}/ /404.html\n\tencode zstd gzip\n}\n' > /etc/caddy/Caddyfile +EXPOSE 80 diff --git a/docs-site/Makefile b/docs-site/Makefile new file mode 100644 index 0000000..1d77058 --- /dev/null +++ b/docs-site/Makefile @@ -0,0 +1,59 @@ +# cuckoo-escapement docs — make targets follow the warehacking cookie-cutter. +# +# `make prod` builds the static site + brings up Caddy serving it. +# `make dev` starts the Astro dev server with HMR behind Caddy. +# `make down` stops both. + +SHELL := /usr/bin/env bash +.SHELLFLAGS := -eu -o pipefail -c +.DEFAULT_GOAL := help + +.PHONY: help +help: ## Show this help + @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_-]+:.*##/ {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +.PHONY: prod +prod: ## Build + run the production docs container (Caddy serves dist/) + docker compose up -d --build docs + +.PHONY: dev +dev: ## Run the Astro dev server with HMR (--profile dev) + docker compose --profile dev up --build docs-dev + +.PHONY: down +down: ## Stop and remove the docs containers + docker compose --profile dev down + docker compose down + +.PHONY: logs +logs: ## Tail logs (works for whichever profile is up) + docker compose logs -f --tail=100 + +.PHONY: build +build: ## Build the static site WITHOUT bringing up Caddy (CI gate) + docker compose build docs + +.PHONY: shell +shell: ## Open a shell in the running dev container (debugging) + docker compose exec docs-dev sh + +# ---- Production deploy -------------------------------------------------- +# +# `make deploy` pulls origin/main on the warehack.ing prod host and rebuilds +# the docs container. Agent-forwarding (`-A`) lets the remote `git pull` use +# the operator's local SSH key for Gitea — nothing persistent is provisioned +# on the deploy host. +# +# Override DEPLOY_HOST / DEPLOY_PATH for a different deployment without +# editing this file. The defaults are the warehack.ing cookie-cutter shape +# (see ~/.claude/references/warehacking.md). + +DEPLOY_HOST ?= warehack-ing@warehack.ing +DEPLOY_PATH ?= ~/cuckoo-escapement + +.PHONY: deploy +deploy: ## Pull main + rebuild the docs container on the prod host + @echo "==> deploying $(DEPLOY_HOST):$(DEPLOY_PATH)" + ssh -A $(DEPLOY_HOST) "cd $(DEPLOY_PATH) && git fetch origin main && git reset --hard origin/main && cd docs-site && make prod" + @echo "==> sanity check" + @curl -s -o /dev/null -w " HTTP %{http_code} %{url_effective}\n" "https://cuckoo.warehack.ing/explanation/bug-detection/" diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs new file mode 100644 index 0000000..435948b --- /dev/null +++ b/docs-site/astro.config.mjs @@ -0,0 +1,105 @@ +// The Cuckoo Escapement — Starlight, diátaxis-shaped. +// +// This site is a FIELD REPORT, not a tutorial. The build guides already exist +// (geerlingguy/time-pi, josh-blake/pixie) and they're good. What doesn't exist +// is a record of everything they get wrong on a Pi 4, with numbers. So the IA is +// deliberately inverted from the usual docs site: heavy on Explanation and +// Reference, and there is no Tutorial section at all. +// +// Telemetry + devToolbar off per project convention. The HMR block is required +// when the dev server runs behind Caddy (TLS-terminating proxy) — without an +// explicit host/protocol/clientPort, Vite's WebSocket drops every ~10s. +// +// Site URL comes from DOMAIN so one image serves both cuckoo.warehack.ing (prod) +// and cuckoo.l.warehack.ing (local dev). + +import mdx from "@astrojs/mdx"; +import sitemap from "@astrojs/sitemap"; +import starlight from "@astrojs/starlight"; +import { defineConfig } from "astro/config"; +import starlightLinksValidator from "starlight-links-validator"; + +const domain = process.env.DOMAIN ?? "cuckoo.warehack.ing"; + +export default defineConfig({ + site: `https://${domain}`, + telemetry: false, + devToolbar: { enabled: false }, + + vite: { + server: { + host: "0.0.0.0", + hmr: { host: domain, protocol: "wss", clientPort: 443 }, + }, + }, + + integrations: [ + starlight({ + title: "The Cuckoo Escapement", + description: + "What the Raspberry Pi time-server guides get wrong, and the numbers to prove it. " + + "GPS Stratum 1 on a Pi 4: PREEMPT_RT makes PPS jitter worse, the PPS interrupt " + + "cannot be pinned, PTP is impossible, and your dashboard is taxing your clock.", + + // The mark IS the word "cuckoo" — a rebus. replacesTitle stops Starlight + // rendering the title text beside it (which would read "…escapement The + // Cuckoo Escapement"). The SVG's aria-label carries the full name. + logo: { src: "./src/assets/logo.svg", replacesTitle: true }, + favicon: "/favicon.svg", + customCss: ["./src/styles/brass.css"], + + social: [ + { + icon: "seti:git", + label: "Source", + href: "https://git.supported.systems/warehack.ing/cuckoo-escapement", + }, + ], + + // Diátaxis, but weighted for a field report. Explanation leads, because the + // whole point is WHY the received wisdom is wrong. There is no Tutorial — + // that would be the one thing the world does not need another of. + // NB: Starlight >=0.39 removed the inline {label, autogenerate} shorthand; + // it must be nested as {label, items: [{autogenerate}]}. + sidebar: [ + { + label: "Start here", + items: [ + { label: "What this is (and isn't)", slug: "index" }, + { label: "The findings, in brief", slug: "findings" }, + ], + }, + { + label: "Explanation", + items: [{ autogenerate: { directory: "explanation" } }], + }, + { + label: "Reference", + items: [{ autogenerate: { directory: "reference" } }], + }, + { + label: "How-to", + items: [{ autogenerate: { directory: "how-to" } }], + }, + ], + + components: { + // Appends the "A Supported Systems Joint" badge under Starlight's default + // footer, without rewriting the component. + Footer: "./src/components/Footer.astro", + }, + + plugins: [ + starlightLinksValidator({ + // Broken internal links fail the build rather than shipping silently. + errorOnRelativeLinks: false, + }), + ], + + pagination: true, + lastUpdated: true, + }), + mdx(), + sitemap(), + ], +}); diff --git a/docs-site/docker-compose.yml b/docs-site/docker-compose.yml new file mode 100644 index 0000000..cb28879 --- /dev/null +++ b/docs-site/docker-compose.yml @@ -0,0 +1,73 @@ +# cuckoo-escapement docs site — two profiles. +# +# Default (no --profile flag): +# prod-style — Caddy serves the built dist/. Use for production-like +# deploys (the public site at cuckoo.warehack.ing runs this). +# +# --profile dev: +# Astro dev server with HMR. Volume mounts on src/ so edits hot-reload. +# The Vite HMR WebSocket is configured in astro.config.mjs to work +# behind the caddy-docker-proxy TLS-terminating front-end — see the +# `caddy.reverse_proxy.*` labels below for the WebSocket-friendly +# timeout configuration. +# +# Both services attach to the external `caddy` network and expose +# themselves to caddy-docker-proxy via labels. Edit DOMAIN in .env to +# switch between cuckoo.warehack.ing (prod) and cuckoo.l.warehack.ing +# (local-dev tier). + +services: + docs: + profiles: ["prod", ""] + build: + context: . + target: prod + image: cuckoo-escapement-docs:prod + container_name: cuckoo-escapement-docs + restart: unless-stopped + networks: + - caddy + labels: + caddy: ${DOMAIN:-cuckoo.warehack.ing} + caddy.reverse_proxy: "{{upstreams 80}}" + # encode + gzip already in the container; let caddy pass through. + + docs-dev: + profiles: ["dev"] + build: + context: . + target: dev + image: cuckoo-escapement-docs:dev + container_name: cuckoo-escapement-docs-dev + restart: unless-stopped + environment: + - DOMAIN=${DOMAIN:-cuckoo.l.warehack.ing} + - ASTRO_TELEMETRY_DISABLED=1 + volumes: + # Hot-reload bind mounts. node_modules stays inside the container + # so host platform mismatches don't break native deps. + - ./astro.config.mjs:/app/astro.config.mjs:ro + - ./tsconfig.json:/app/tsconfig.json:ro + - ./src:/app/src + - ./public:/app/public + networks: + - caddy + labels: + caddy: ${DOMAIN:-cuckoo.l.warehack.ing} + caddy.reverse_proxy: "{{upstreams 4321}}" + # Vite HMR over WebSocket. Caddy's defaults close "idle" WS + # connections after ~10-15s; HMR doesn't send app-level pings, so + # we need explicit long-lived timeouts. Required for Caddy 2.10+ + # (HTTP/2 WS fix). See ~/.claude/references/web-frontend.md. + caddy.reverse_proxy.flush_interval: "-1" + caddy.reverse_proxy.transport: "http" + caddy.reverse_proxy.transport.read_timeout: "0" + caddy.reverse_proxy.transport.write_timeout: "0" + caddy.reverse_proxy.transport.keepalive: "5m" + caddy.reverse_proxy.transport.keepalive_idle_conns: "10" + caddy.reverse_proxy.stream_timeout: "24h" + caddy.reverse_proxy.stream_close_delay: "5s" + +networks: + caddy: + external: true diff --git a/docs-site/package-lock.json b/docs-site/package-lock.json new file mode 100644 index 0000000..3966f5a --- /dev/null +++ b/docs-site/package-lock.json @@ -0,0 +1,6461 @@ +{ + "name": "cuckoo-escapement-docs", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cuckoo-escapement-docs", + "version": "0.1.0", + "dependencies": { + "@astrojs/mdx": "^5.0.4", + "@astrojs/sitemap": "^3.7.2", + "@astrojs/starlight": "^0.39.2", + "astro": "^6.3.1", + "sharp": "^0.34.0", + "starlight-links-validator": "^0.24.0" + } + }, + "node_modules/@astrojs/compiler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-4.0.0.tgz", + "integrity": "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==", + "license": "MIT" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.9.1.tgz", + "integrity": "sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ==", + "license": "MIT", + "dependencies": { + "picomatch": "^4.0.4" + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.1.2.tgz", + "integrity": "sha512-caXZ4Dc2St2dW8luEg22GlP0gupLdztCTQE4EzZOxW1pqWXz9mbeJEuHUkgDYcKWW8tjIHkydYDhWLVoxJ327Q==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.9.1", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "js-yaml": "^4.1.1", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.0", + "smol-toml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/mdx": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-5.0.6.tgz", + "integrity": "sha512-4dKe0ZMmqujofPNDHahzClkwinn9f8jHPcaXcgdGvPAlboD2mjzkUCofli2cBnxYAkdfhC6d50gBJ8i/cH8gHw==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-remark": "7.1.2", + "@mdx-js/mdx": "^3.1.1", + "acorn": "^8.16.0", + "es-module-lexer": "^2.0.0", + "estree-util-visit": "^2.0.0", + "hast-util-to-html": "^9.0.5", + "piccolore": "^0.1.3", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "remark-smartypants": "^3.0.2", + "source-map": "^0.7.6", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "astro": "^6.0.0" + } + }, + "node_modules/@astrojs/prism": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz", + "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.3.tgz", + "integrity": "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA==", + "license": "MIT", + "dependencies": { + "sitemap": "^9.0.0", + "stream-replace-string": "^2.0.0", + "zod": "^4.3.6" + } + }, + "node_modules/@astrojs/starlight": { + "version": "0.39.3", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.39.3.tgz", + "integrity": "sha512-uvAweA2DwhmLgFVfBT9NqG38Ey14k1ck3+y78XNJbceT1pMdzxCCX69RoBajb1QzTJviufsXzSc1xswgRxJfig==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-remark": "^7.1.1", + "@astrojs/mdx": "^5.0.4", + "@astrojs/sitemap": "^3.7.2", + "@pagefind/default-ui": "^1.3.0", + "@types/hast": "^3.0.4", + "@types/js-yaml": "^4.0.9", + "@types/mdast": "^4.0.4", + "astro-expressive-code": "^0.42.0", + "bcp-47": "^2.1.0", + "hast-util-from-html": "^2.0.3", + "hast-util-select": "^6.0.4", + "hast-util-to-string": "^3.0.1", + "hastscript": "^9.0.1", + "i18next": "^26.0.7", + "js-yaml": "^4.1.1", + "klona": "^2.0.6", + "magic-string": "^0.30.21", + "mdast-util-directive": "^3.1.0", + "mdast-util-to-markdown": "^2.1.2", + "mdast-util-to-string": "^4.0.0", + "pagefind": "^1.5.2", + "rehype": "^13.0.2", + "rehype-format": "^5.0.1", + "remark-directive": "^4.0.0", + "ultrahtml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "peerDependencies": { + "astro": "^6.0.0" + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", + "integrity": "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.4.0", + "dset": "^3.1.4", + "is-docker": "^4.0.0", + "is-wsl": "^3.1.1", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz", + "integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@clack/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", + "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz", + "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.3", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@expressive-code/core": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.42.0.tgz", + "integrity": "sha512-MN11+9nfmaC7sYu2BZJXAXqwkBRt8t1xTSqP+Ti1NfTEskgl6xUnzDxoaiQkg0BMzpglA0pys4dpDKquP/cyIw==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.0.4", + "hast-util-select": "^6.0.2", + "hast-util-to-html": "^9.0.1", + "hast-util-to-text": "^4.0.1", + "hastscript": "^9.0.0", + "postcss": "^8.4.38", + "postcss-nested": "^6.0.1", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" + } + }, + "node_modules/@expressive-code/plugin-frames": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.42.0.tgz", + "integrity": "sha512-XtkPm+941Uta7Y+81Acv+OA/20F1NJmJhCX6UYGKpqEIGqplNh3PTOhcURp6tcruhlzJcWcvpWy6Oigz3SrjqA==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.42.0" + } + }, + "node_modules/@expressive-code/plugin-shiki": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.42.0.tgz", + "integrity": "sha512-PMKey/kLmewttAHQezL+Y5Fx3vVssfDi3+FJOYQQS2mXP3tQspFELtKKAfsXfmSXdToZYgwoO69HJndqfE+09g==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.42.0", + "shiki": "^4.0.2" + } + }, + "node_modules/@expressive-code/plugin-text-markers": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.42.0.tgz", + "integrity": "sha512-l59lUx8fq1v5g6SpmbDjiU0+7IdfbiWnAyRmtTVSpfhyq+nZMN4UcmYyu2b9Mynhzt7Gr+O+cXyEPDNb2AVWVQ==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.42.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@pagefind/darwin-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.5.2.tgz", + "integrity": "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/darwin-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.5.2.tgz", + "integrity": "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/default-ui": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.5.2.tgz", + "integrity": "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==", + "license": "MIT" + }, + "node_modules/@pagefind/freebsd-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.5.2.tgz", + "integrity": "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@pagefind/linux-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.5.2.tgz", + "integrity": "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/linux-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.5.2.tgz", + "integrity": "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/windows-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-arm64/-/windows-arm64-1.5.2.tgz", + "integrity": "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@pagefind/windows-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.5.2.tgz", + "integrity": "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.3.1.tgz", + "integrity": "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.3.1", + "@shikijs/types": "4.3.1", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.3.1.tgz", + "integrity": "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.3.1", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.3.1.tgz", + "integrity": "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.3.1", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.3.1.tgz", + "integrity": "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.3.1.tgz", + "integrity": "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.3.1", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.3.1.tgz", + "integrity": "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.3.1.tgz", + "integrity": "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", + "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==", + "license": "MIT" + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/astro": { + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/astro/-/astro-6.4.8.tgz", + "integrity": "sha512-KK5lX90uU9EeVaTjINyj3sy9/NFXVa59aowaqbWBDDKLXZh4rr7GwIaCFYVetE22MJtsCNFerQXn0vlCLmpP/Q==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^4.0.0", + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/markdown-remark": "7.2.0", + "@astrojs/telemetry": "3.3.2", + "@capsizecss/unpack": "^4.0.0", + "@clack/prompts": "^1.1.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.3.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "ci-info": "^4.4.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^2.0.0", + "cookie": "^1.1.1", + "devalue": "^5.8.1", + "diff": "^8.0.3", + "dset": "^3.1.4", + "es-module-lexer": "^2.0.0", + "esbuild": "^0.27.3", + "flattie": "^1.1.1", + "fontace": "~0.4.1", + "get-tsconfig": "5.0.0-beta.4", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "js-yaml": "^4.1.1", + "jsonc-parser": "^3.3.1", + "magic-string": "^0.30.21", + "magicast": "^0.5.2", + "mrmime": "^2.0.1", + "neotraverse": "^0.6.18", + "obug": "^2.1.1", + "p-limit": "^7.3.0", + "p-queue": "^9.1.0", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.4", + "rehype": "^13.0.2", + "semver": "^7.7.4", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "svgo": "^4.0.1", + "tinyclip": "^0.1.12", + "tinyexec": "^1.0.4", + "tinyglobby": "^0.2.15", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.4", + "unist-util-visit": "^5.1.0", + "unstorage": "^1.17.5", + "vfile": "^6.0.3", + "vite": "^7.3.2", + "vitefu": "^1.1.2", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^22.0.0", + "zod": "^4.3.6" + }, + "bin": { + "astro": "bin/astro.mjs" + }, + "engines": { + "node": ">=22.12.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.34.0" + } + }, + "node_modules/astro-expressive-code": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.42.0.tgz", + "integrity": "sha512-aiTePi2Cn0mJPYWZSzP1GcxCinX9mNtJyCCshVVPSg1yRwM7ADvFJOx0FnS440M9t65hp8JH//dc2qr22Bm4ag==", + "license": "MIT", + "dependencies": { + "rehype-expressive-code": "^0.42.0" + }, + "peerDependencies": { + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" + } + }, + "node_modules/astro/node_modules/@astrojs/internal-helpers": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.0.tgz", + "integrity": "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "js-yaml": "^4.1.1", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "unified": "^11.0.5" + } + }, + "node_modules/astro/node_modules/@astrojs/markdown-remark": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.0.tgz", + "integrity": "sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.1.tgz", + "integrity": "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-selector-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", + "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expressive-code": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.42.0.tgz", + "integrity": "sha512-V5DtJLEKuj4wf9O6IRtPtRObkMVy2ggR+S0MdjrTw6m58krZnDioyhW1si3Y04c5YPeooP4nd85Yq9NwEVHS4g==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.42.0", + "@expressive-code/plugin-frames": "^0.42.0", + "@expressive-code/plugin-shiki": "^0.42.0", + "@expressive-code/plugin-text-markers": "^0.42.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fontace": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", + "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.2" + } + }, + "node_modules/fontkitten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz", + "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.4", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", + "integrity": "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/has-flag": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz", + "integrity": "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-format": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-format/-/hast-util-format-1.1.0.tgz", + "integrity": "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-body-ok-link": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", + "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-minify-whitespace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz", + "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "nth-check": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-whitespace-sensitive-tag-names": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz", + "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/i18next": { + "version": "26.3.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz", + "integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-absolute-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-5.0.0.tgz", + "integrity": "sha512-sdJyNpBnQHuVnBunfzjAecOhZr2+A30ywfFvu3EnxtKLUWfwGgyWUmqHbGZiU6vTfHpCPm5GvLe4BAvlU9n8VQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz", + "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "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/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/p-limit": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", + "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.1.tgz", + "integrity": "sha512-POWdiIPmsUPGwb4FeQ4OBg46aqmcInSWe45CKDsGHiOBiVQM9chqfQTuqhuTzcg2Vz9faTI65at0KkVyVEiCHw==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.7.0.tgz", + "integrity": "sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==", + "license": "MIT" + }, + "node_modules/pagefind": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.5.2.tgz", + "integrity": "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==", + "license": "MIT", + "bin": { + "pagefind": "lib/runner/bin.cjs" + }, + "optionalDependencies": { + "@pagefind/darwin-arm64": "1.5.2", + "@pagefind/darwin-x64": "1.5.2", + "@pagefind/freebsd-x64": "1.5.2", + "@pagefind/linux-arm64": "1.5.2", + "@pagefind/linux-x64": "1.5.2", + "@pagefind/windows-arm64": "1.5.2", + "@pagefind/windows-x64": "1.5.2" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-expressive-code": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.42.0.tgz", + "integrity": "sha512-8rp/1YMEVVSYbtz+bFBx+uSx3vA4i4T8RwRm5Q/IWbucQnnQqQ0hDqtmKOr8tv+59Cik6cu5aH3WPo0I7csuTA==", + "license": "MIT", + "dependencies": { + "expressive-code": "^0.42.0" + } + }, + "node_modules/rehype-format": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.1.tgz", + "integrity": "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-format": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-4.0.0.tgz", + "integrity": "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-smartypants": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "license": "MIT", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shiki": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.3.1.tgz", + "integrity": "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.3.1", + "@shikijs/engine-javascript": "4.3.1", + "@shikijs/engine-oniguruma": "4.3.1", + "@shikijs/langs": "4.3.1", + "@shikijs/themes": "4.3.1", + "@shikijs/types": "4.3.1", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz", + "integrity": "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^24.9.2", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" + }, + "bin": { + "sitemap": "dist/esm/cli.js" + }, + "engines": { + "node": ">=20.19.5", + "npm": ">=10.8.2" + } + }, + "node_modules/smol-toml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", + "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/starlight-links-validator": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/starlight-links-validator/-/starlight-links-validator-0.24.1.tgz", + "integrity": "sha512-nATZ0xMLYnmO1YDBGEy9PdjF/WxhbKR7/gQo0dFs5UELcorKLPXcNLM62xbqII7g0AF7/D9wSzD4qSwIGTu8sw==", + "license": "MIT", + "dependencies": { + "@types/picomatch": "^4.0.2", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "is-absolute-url": "^5.0.0", + "mdast-util-mdx-jsx": "^3.2.0", + "mdast-util-to-hast": "^13.2.1", + "picomatch": "^4.0.3", + "terminal-link": "^5.0.0", + "unist-util-visit": "^5.1.0", + "yaml": "^2.8.3" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/starlight": ">=0.38.0", + "astro": ">=6.0.0" + } + }, + "node_modules/stream-replace-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", + "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", + "license": "MIT" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-hyperlinks": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz", + "integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==", + "license": "MIT", + "dependencies": { + "has-flag": "^5.0.1", + "supports-color": "^10.2.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/svgo": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.2.tgz", + "integrity": "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/terminal-link": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-5.0.0.tgz", + "integrity": "sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^4.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinyclip": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.15.tgz", + "integrity": "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "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/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz", + "integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unifont": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz", + "integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ofetch": "^1.5.1", + "ohash": "^2.0.11" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-modify-children": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "license": "MIT", + "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/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs-site/package.json b/docs-site/package.json new file mode 100644 index 0000000..c74adc2 --- /dev/null +++ b/docs-site/package.json @@ -0,0 +1,20 @@ +{ + "name": "cuckoo-escapement-docs", + "type": "module", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "astro dev --host 0.0.0.0", + "build": "astro build", + "preview": "astro preview --host 0.0.0.0", + "astro": "astro" + }, + "dependencies": { + "@astrojs/mdx": "^5.0.4", + "@astrojs/sitemap": "^3.7.2", + "@astrojs/starlight": "^0.39.2", + "astro": "^6.3.1", + "sharp": "^0.34.0", + "starlight-links-validator": "^0.24.0" + } +} diff --git a/docs-site/public/favicon.svg b/docs-site/public/favicon.svg new file mode 100644 index 0000000..7f1ca66 --- /dev/null +++ b/docs-site/public/favicon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/docs-site/public/supported-systems-logo.svg b/docs-site/public/supported-systems-logo.svg new file mode 100644 index 0000000..d4b85ba --- /dev/null +++ b/docs-site/public/supported-systems-logo.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10 + 01 + 11 + 00 + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs-site/src/assets/logo.svg b/docs-site/src/assets/logo.svg new file mode 100644 index 0000000..c09ad86 --- /dev/null +++ b/docs-site/src/assets/logo.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + escapement + diff --git a/docs-site/src/components/Footer.astro b/docs-site/src/components/Footer.astro new file mode 100644 index 0000000..ebb76fa --- /dev/null +++ b/docs-site/src/components/Footer.astro @@ -0,0 +1,13 @@ +--- +// Starlight Footer override. We wrap so all upstream behavior — +// last-updated stamp, prev/next pagination — is preserved, then append the +// maker's plate below it. +// +// Same pattern (Default + extension) used across the other warehack.ing sites, +// which keeps Starlight upgrades cheap. +import Default from "@astrojs/starlight/components/Footer.astro"; +import SupportedSystemsBadge from "./SupportedSystemsBadge.astro"; +--- + + + diff --git a/docs-site/src/components/SupportedSystemsBadge.astro b/docs-site/src/components/SupportedSystemsBadge.astro new file mode 100644 index 0000000..2c3eb40 --- /dev/null +++ b/docs-site/src/components/SupportedSystemsBadge.astro @@ -0,0 +1,47 @@ +--- +// "A Supported Systems Joint" — the maker's plate. +// +// Clockmakers signed the BACKPLATE: the flat brass face of the movement that +// only shows when you open the case. It's the part a repairer sees a century +// later, not the part the owner looks at. A site footer is the same thing — +// the back of the movement — so this is an engraved plate rather than a +// marketing strip, and nothing on it moves. A signature should be quiet. +// +// Shared verbatim with the dashboard's footer (static HTML/CSS port, same +// markup and class names). Styles live in src/styles/brass.css. +--- + + diff --git a/docs-site/src/content.config.ts b/docs-site/src/content.config.ts new file mode 100644 index 0000000..f0cf84d --- /dev/null +++ b/docs-site/src/content.config.ts @@ -0,0 +1,8 @@ +// Starlight content collection — required for content.config.ts in Astro 6.x. +import { defineCollection } from "astro:content"; +import { docsLoader } from "@astrojs/starlight/loaders"; +import { docsSchema } from "@astrojs/starlight/schema"; + +export const collections = { + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), +}; diff --git a/docs-site/src/content/docs/explanation/cpu0-is-sacred.md b/docs-site/src/content/docs/explanation/cpu0-is-sacred.md new file mode 100644 index 0000000..d387ec9 --- /dev/null +++ b/docs-site/src/content/docs/explanation/cpu0-is-sacred.md @@ -0,0 +1,51 @@ +--- +title: cpu0 is sacred +description: The CPU map this box lives by — discovered by measurement, not designed. +sidebar: + order: 6 +--- + +This is the layout the machine ended up with: + +``` +cpu0 ──── PPS interrupt. Nothing else. Ever. +cpu1 ──── web tier (dashboard, Caddy) +cpu2 ──── chronyd (isolated) +cpu3 ──── gpsd + UART IRQ thread (isolated) +``` + +Not one of those four lines came from a guide. Each came from a measurement that +contradicted an assumption. + +- **cpu0 holds the PPS interrupt** because the Pi 4's GPIO mux + [physically refuses to move it](/explanation/the-interrupt-you-cannot-move/). + That isn't a preference; it's a constraint we cannot configure away. +- **cpu2 and cpu3 are isolated** (`isolcpus=2,3`) for the timing daemons, which + want determinism more than throughput. +- **cpu1 got the web tier** only after a benchmark caught it + [taxing the clock 36% from cpu0](/explanation/the-observer-effect/). + +## The rule this implies + +Because the PPS interrupt cannot be relocated, **cpu0 is load-bearing for +precision in a way no other core is**. Anything you schedule there is competing +directly with the timestamp. + +So: every service on this box — an exporter, a log shipper, a backup job, a cron +entry, anything you add six months from now — belongs on cpu1–3. + +```ini +[Service] +CPUAffinity=1 +Nice=10 +``` + +It's a one-line tax, and the alternative is a slow, invisible erosion of the one +number the machine exists to produce. + +:::note[Isolation alone did nothing] +Worth saying plainly: `isolcpus` on its own did **not** improve PPS jitter, and +plausibly made it worse — by evacuating cpu2/3, we concentrated *everything else* +onto cpu0/1, which is where the PPS interrupt lives. Isolation only pays once you +also keep the evacuated work away from the PPS core. +::: diff --git a/docs-site/src/content/docs/explanation/no-ptp-on-a-pi-4.md b/docs-site/src/content/docs/explanation/no-ptp-on-a-pi-4.md new file mode 100644 index 0000000..14fe93c --- /dev/null +++ b/docs-site/src/content/docs/explanation/no-ptp-on-a-pi-4.md @@ -0,0 +1,52 @@ +--- +title: Why PTP is off the table on a Pi 4 +description: PTP's entire value is hardware timestamping. The Pi 4's NIC has no PTP hardware clock. Software PTP is a worse NTP. +sidebar: + order: 3 +--- + +The reference builds all reach for **PTP** (IEEE 1588), and they're right to: on +the right hardware it's dramatically better than NTP. + +The Pi 4 is not the right hardware. One command settles it: + +```console +$ ethtool -T eth0 +Capabilities: + software-transmit + software-receive + software-system-clock +PTP Hardware Clock: none +Hardware Transmit Timestamp Modes: none +Hardware Receive Filter Modes: none +``` + +**`PTP Hardware Clock: none`.** There isn't one. There's no `/dev/ptp0` to open. + +## Why that's fatal rather than inconvenient + +PTP's whole advantage is **hardware timestamping**: the network card itself +stamps the packet as it crosses the wire, in silicon, outside the operating +system. That's what removes kernel scheduling, driver latency, and queueing from +the measurement, and it's why PTP reaches nanoseconds where NTP reaches +microseconds. + +Take the hardware clock away and PTP is just... a protocol. Software-timestamped +PTP has the packets stamped by the *kernel*, on the *CPU*, subject to exactly the +scheduling jitter you were trying to escape. It is a more complicated NTP with +worse tooling. + +## But the guides say the Pi 4's PHY supports PTP + +They do, and the *chip* does — the BCM54213PE PHY has PTP capability on paper. +It doesn't matter. The Pi 4's `bcmgenet` MAC driver doesn't expose a PHC, so +Linux has nothing to give you. And the reference builds that make PTP work feed +the PPS into the NIC through a **SYNC pin that only the CM4/CM5 break out** — a +regular Pi 4 board doesn't route it anywhere you can reach. + +## So don't chase it + +We spent real time on this before running `ethtool -T`, which we should have run +first. If your board reports `PTP Hardware Clock: none`, close the tab. Put the +effort into the PPS path instead — that's where the nanoseconds actually are, and +[it needs the help](/explanation/preempt-rt-made-it-worse/). diff --git a/docs-site/src/content/docs/explanation/preempt-rt-made-it-worse.md b/docs-site/src/content/docs/explanation/preempt-rt-made-it-worse.md new file mode 100644 index 0000000..0272a40 --- /dev/null +++ b/docs-site/src/content/docs/explanation/preempt-rt-made-it-worse.md @@ -0,0 +1,113 @@ +--- +title: Why PREEMPT_RT made it worse +description: The realtime kernel force-threads interrupt handlers. The PPS driver takes its timestamp inside its handler. Those two facts multiply badly. +sidebar: + order: 1 +--- + +Installing a realtime kernel is the prestige move in every Pi time-server guide. +It is also, on its own, the single most damaging thing we did. + +| | raw PPS jitter (σ) | peak-to-peak | +|---|---|---| +| Stock kernel | 2134 ns | 11 µs | +| **PREEMPT_RT** | **6947 ns** | **38 µs** | + +Three times worse. Not marginally, not within noise — **three times.** + +## Why + +PREEMPT_RT achieves its determinism by **force-threading interrupt handlers**. +Instead of running in hard-IRQ context (immediately, uninterruptibly, nanoseconds +after the electrical edge), a handler becomes a schedulable kernel thread that +the scheduler runs *when it gets around to it*. + +For most drivers this is a good trade: you lose a little latency, you gain the +ability to preempt long-running handlers, and the *worst case* improves. That's +the entire pitch of realtime Linux, and it's a good pitch. + +But look at what `pps-gpio` actually does in its handler: + +```c +static irqreturn_t pps_gpio_irq_handler(int irq, void *data) +{ + ... + pps_get_ts(&ts); /* ← THE TIMESTAMP IS TAKEN HERE */ + pps_event(info->pps, &ts, ...); + ... +} +``` + +**The handler is the measurement.** `pps_get_ts()` is the whole point of the +driver — it captures *when the pulse arrived*. Everything downstream, every +nanosecond of accuracy chrony reports, descends from that one call. + +So when PREEMPT_RT threads this handler, it doesn't defer some *work*. It defers +**the act of looking at the clock**. The timestamp is no longer taken at the +electrical edge; it's taken after thread-wakeup latency — microseconds later, and +*variably* later, which is worse. + +We didn't make the system more deterministic. We inserted a scheduler between the +pulse and the clock. + +## You can see it happen + +On a stock kernel, the PPS interrupt has no thread at all: + +```console +$ ps -eo pid,class,rtprio,psr,comm | grep irq/41 +(nothing — it runs in hard-irq context) +``` + +Boot PREEMPT_RT and it materialises: + +```console +$ ps -eo pid,class,rtprio,psr,comm | grep irq/41 + 239 RR 50 3 irq/41-pps@12.-1 +``` + +That thread is the problem. It is also — and this is the cruel part — *exactly +what the guides tell you to go and pin to an isolated core.* You can only +`taskset` a thread. The advice to isolate the PPS IRQ **requires** the very +threading that destroys the timestamp. + +
+ +:::danger[The trap, stated plainly] +**You can pin the PPS interrupt, or you can timestamp it fast. You cannot do +both.** Threading is the price of pinning, and on our board that price was 12× +the accuracy. A hard-IRQ handler on a *busy* CPU 0 beat a threaded-and-pinned one +on a *quiet, isolated* CPU 3 — by a mile. +::: + +## The fix + +Tell the kernel this particular handler must not be threaded: + +```c +flags |= IRQF_NO_THREAD; +``` + +That's it. The timestamp goes back to hard-IRQ context, at the electrical edge, +while the rest of the system keeps every benefit of PREEMPT_RT. + +| | RMS offset | raw PPS jitter | +|---|---|---| +| Stock kernel | 440 ns | 2134 ns | +| PREEMPT_RT (unpatched) | 2468 ns | 6947 ns | +| **PREEMPT_RT + `IRQF_NO_THREAD`** | **199 ns** | 2568 ns | + +The patch is four lines and it's [here](/reference/the-patch/). It is, as far as +we can tell, not applied anywhere — which means **anyone running GPIO-based PPS +on a realtime kernel today is silently eating microseconds of jitter** and has no +reason to suspect it, because everything *looks* fine. chrony still says Stratum 1. +The dashboard still says locked. The number is just quietly worse. + +## The lesson underneath + +The realtime kernel is not "the fast kernel." It is the *predictable* kernel, and +it buys predictability by making things schedulable. If the thing you care about +is **a measurement taken inside an interrupt handler**, making it schedulable is +precisely the wrong move. + +Nothing about that is obvious from the outside. It is only obvious from a number. diff --git a/docs-site/src/content/docs/explanation/the-interrupt-you-cannot-move.md b/docs-site/src/content/docs/explanation/the-interrupt-you-cannot-move.md new file mode 100644 index 0000000..1ef968e --- /dev/null +++ b/docs-site/src/content/docs/explanation/the-interrupt-you-cannot-move.md @@ -0,0 +1,75 @@ +--- +title: The interrupt you cannot move +description: On a Pi 4, GPIO interrupts are demuxed through pinctrl-bcm2835 and refuse an smp_affinity. The IRQ-isolation advice is unachievable here. +sidebar: + order: 2 +--- + +Every guide says the same thing: park the PPS interrupt on its own isolated CPU, +give it realtime priority, and keep the noisy world away from it. + +On a Raspberry Pi 4, **you cannot.** + +```console +$ echo 3 > /proc/irq/41/smp_affinity_list +tee: /proc/irq/41/smp_affinity_list: Operation not permitted +``` + +## Why + +Your PPS arrives on a **GPIO pin**, and GPIO interrupts on the BCM2711 are not +first-class interrupts. They are **demultiplexed** through the GPIO controller: + +```console +$ grep -E 'pps|uart' /proc/interrupts + 40: 3532866 0 0 0 GICv2 153 Level uart-pl011 + 41: 104835 0 0 0 pinctrl-bcm2835 18 Edge pps@12.-1 +``` + +Look at the difference. The UART is a **GICv2** interrupt — a real line into the +interrupt controller, and it takes an affinity happily. The PPS is a +**`pinctrl-bcm2835`** interrupt — one of dozens of GPIO lines multiplexed behind +a single parent IRQ. There is no per-line steering to give. Every GPIO interrupt +lands wherever the GPIO controller's parent lands, together. + +So the PPS interrupt goes where it goes, and no amount of configuration moves it. + +## The cruel bit + +There *is* one way to gain control of it: **PREEMPT_RT force-threads interrupt +handlers**, and a thread can be `taskset` anywhere. Boot a realtime kernel and the +thing you couldn't pin becomes pinnable: + +```console +$ ps -eo pid,class,rtprio,psr,comm | grep irq/41 + 239 RR 50 3 irq/41-pps@12.-1 ← RT priority, isolated CPU 3. It worked! +``` + +The guides are vindicated. Except it's a trap, because +[threading the handler is what destroys the +timestamp](/explanation/preempt-rt-made-it-worse/) — `pps-gpio` takes its +measurement *inside* that handler, so putting it behind the scheduler costs more +than the isolation ever gives back. + +:::danger[Pin it, or timestamp it fast. Not both.] +- **Threaded + pinned to a quiet isolated core:** RMS offset **2468 ns** +- **Hard-IRQ + unpinned on a busy CPU 0:** RMS offset **199 ns** + +The fast handler on the *noisy* core beat the scheduled handler on the *quiet* +core by more than 12×. Interrupt latency dominates CPU contention, and it isn't +close. +::: + +## What to do instead + +Accept that the PPS interrupt lives on CPU 0, and then **treat CPU 0 as sacred**. +You can't move the interrupt, but you can move *everything else*: + +```ini +# every other service gets an affinity that isn't 0 +[Service] +CPUAffinity=1 +``` + +That's the whole strategy. It's not the one in the guides, but it's the one the +hardware permits. [→ cpu0 is sacred](/explanation/cpu0-is-sacred/) diff --git a/docs-site/src/content/docs/explanation/the-observer-effect.md b/docs-site/src/content/docs/explanation/the-observer-effect.md new file mode 100644 index 0000000..28dc5b7 --- /dev/null +++ b/docs-site/src/content/docs/explanation/the-observer-effect.md @@ -0,0 +1,82 @@ +--- +title: The observer effect +description: Our monitoring dashboard cost 36% more PPS jitter. The instrument was bending the measurement. +sidebar: + order: 5 +--- + +We built a status dashboard for the time server. Then somebody asked the obvious +question nobody asks: **is the dashboard hurting the clock?** + +It was. By 36%. + +## The measurement + +A/B/A, sixty-two seconds of raw `ppstest` per round, with the middle round as the +control and the third to prove it wasn't drift: + +| Round | Dashboard | PPS jitter (σ) | peak-to-peak | +|---|---|---|---| +| 1 | **on** | 1912 ns | 10252 ns | +| 2 | **off** | **1304 ns** | **6914 ns** | +| 3 | **on** | 2179 ns | 11683 ns | + +Round 3 reproduces round 1. It's real. + +## Why + +Two facts, individually harmless, catastrophic together: + +1. The collector was **forking `chronyc` four times a second** — once each for + `tracking`, `sources`, `sourcestats`, `clients`. Process creation is one of the + most expensive things you can ask a scheduler to do. + +2. That churn landed on **CPU 0** — the one core the PPS interrupt is welded to + and [cannot be moved off](/explanation/the-interrupt-you-cannot-move/). + +The monitoring tool was standing on the neck of the thing it monitors. And it was +invisible: every metric looked fine, chrony still said Stratum 1, the dashboard +still said "locked". The number was just quietly worse. + +## The fix (no timing code was touched) + +**1. Batch chronyc into one process.** It reads commands from stdin, so one fork +serves all three: + +```bash +printf 'tracking\nsources\nsourcestats\n' | chronyc -c +``` + +Tell the outputs apart by field count: 14 = tracking, 10 = sources, 8 = sourcestats. + +:::caution +Passing multiple commands as **arguments** silently runs only the first. +`chronyc -c tracking sources sourcestats` returns tracking and nothing else, with +no error. Use stdin. +::: + +**2. Get off CPU 0.** + +```ini +[Service] +CPUAffinity=1 +``` + +## The result + +| | Dashboard off | Dashboard on | Penalty | +|---|---|---|---| +| Before | 1304 ns | 1912 / 2179 ns | **+36%** | +| After | 1437 ns | **1169 / 1450 ns** | **none — within noise** | + +The box running its *entire* production stack is now quieter than it was sitting +**idle** before the fix. + +## The general rule + +On a machine where one core is load-bearing for precision, **every other service +you run is a tenant on the other cores**, whether it knows it or not. And process +creation is the loudest neighbour there is. + +A monitoring tool must not perturb what it measures. If you have never checked +whether yours does, you do not know that it doesn't. diff --git a/docs-site/src/content/docs/explanation/where-precision-lives.md b/docs-site/src/content/docs/explanation/where-precision-lives.md new file mode 100644 index 0000000..e12f68e --- /dev/null +++ b/docs-site/src/content/docs/explanation/where-precision-lives.md @@ -0,0 +1,62 @@ +--- +title: Where the precision actually lives +description: NMEA labels the second. PPS carries all the accuracy. Once you internalise that, half the tuning advice evaporates. +sidebar: + order: 4 +--- + +A GPS receiver hands you time twice, in two completely different currencies, and +almost every tuning mistake comes from confusing them. + +## NMEA tells you *which* second it is + +The receiver computes the time precisely, and then it has to **shift a text +sentence out of a serial port**. At 9600 baud that takes hundreds of milliseconds, +and the delay wobbles from second to second depending on how many sentences are +enabled and what the CPU was doing. + +Our NMEA-derived time sat **+160 ms** off, with hundreds of microseconds of noise. +That's not the receiver being bad. That's a UART being a UART. + +## PPS tells you *exactly when* that second began + +The same receiver also raises a **single electrical edge** at the top of every +second, accurate to nanoseconds. No protocol, no encoding, no serial port — just +a voltage going high at the instant the second starts. + +That edge is where every nanosecond of your accuracy comes from. All of it. + +## What that means in practice + +chrony uses them together, and the division of labour is total: + +``` +refclock SHM 0 refid GPS ... noselect # NMEA: labels the second. Never the time source. +refclock PPS /dev/pps0 ... lock GPS # PPS: IS the time source. +``` + +The NMEA source is marked `noselect` — chrony is explicitly told *never to use it +to set the clock*. Its only job is to answer "which second is this pulse?", and +for that it merely has to be within half a second. It has an entire half-second +of slack. + +:::tip[The consequence that saves you a day] +**Anything that improves NMEA and nothing else improves nothing.** + +We raised the module's baud rate from 9600 → 115200, a 12× improvement to the +serial path, and measured the result: + +| | PPS offset | root dispersion | +|---|---|---| +| 9600 baud | −1 ns | 7.6 µs | +| 115200 baud | −1 ns | 6.3 µs | + +**Identical.** We'd improved the thing that doesn't carry the precision. + +Worse: pinning gpsd to that baud later caused a +[total GPS outage after a power cut](/how-to/survive-a-power-cut/). We nearly +took the server down defending an optimisation worth nothing. +::: + +Baud rate, sentence count, SBAS, update rate — all of it lives on the NMEA side of +the wall. Tune it if you enjoy tuning. Just don't expect the clock to notice. diff --git a/docs-site/src/content/docs/findings.md b/docs-site/src/content/docs/findings.md new file mode 100644 index 0000000..ee7cb67 --- /dev/null +++ b/docs-site/src/content/docs/findings.md @@ -0,0 +1,50 @@ +--- +title: The findings, in brief +description: Everything we discovered, with the numbers, on one page. +--- + +For people who want the whole thing in ninety seconds. + +## What we built + +A GPS-disciplined Stratum 1 NTP server: **Raspberry Pi 4** + **BerryGPS-IMU v4** +(u-blox CAM-M8C), PPS on GPIO18, `gpsd` + `chrony`. Final state: **RMS offset +199 ns**, root delay ~1 ns, survives a cold power cut unattended. About $130 of +parts, replacing an appliance that costs $1,500–$10,000. + +## What the guides get wrong on a Pi 4 + +| Claim | Reality | +|---|---| +| "Use PTP for real precision" | **Impossible.** `ethtool -T eth0` → `PTP Hardware Clock: none`. No hardware timestamping exists on this NIC. | +| "Isolate the PPS IRQ on a dedicated core" | **Not permitted.** GPIO IRQs demux through `pinctrl-bcm2835` and reject `smp_affinity`. | +| "Install PREEMPT_RT" | **Made jitter 3× worse** until patched — it threads the handler that takes the timestamp. | +| "Raise the GPS baud rate" | **Irrelevant.** PPS offset measured −1 ns at 9600 vs 115200. Identical. NMEA only *labels* the second. | + +## What actually helped + +| Change | RMS offset | +|---|---| +| Baseline | 823 ns | +| chrony `filter 10` + `prefer` on the PPS refclock | 440 ns | +| PREEMPT_RT + [`IRQF_NO_THREAD` patch](/reference/the-patch/) | **199 ns** | + +## What we broke, and how we found it + +Two failures that a reboot will never reveal. Only a **cold power cut** exposes +them, which is why you must actually pull the plug: + +1. **gpsd was being started by the dashboard.** It's socket-activated; chrony + reads its *shared memory*, never its socket, so nothing else triggered it. The + monitoring page was load-bearing for the time server. +2. **Pinning gpsd's baud turned a module quirk into an outage.** The CAM-M8 keeps + config in supercap-backed RAM and reverts to 9600 on power loss. With gpsd + pinned to 115200 it came back talking to a module that wasn't listening: **no + GPS at all.** Use `GPSD_OPTIONS="-n"` and let it auto-probe. + +## And the instrument was bending the measurement + +Our own dashboard cost **36% more PPS jitter** by forking `chronyc` four times a +second onto CPU 0 — the one core the PPS interrupt is welded to and cannot be +moved from. Batching it to one process and pinning it off CPU 0 erased the +penalty entirely. [→ The observer effect](/explanation/the-observer-effect/) diff --git a/docs-site/src/content/docs/how-to/benchmark-pps-jitter.md b/docs-site/src/content/docs/how-to/benchmark-pps-jitter.md new file mode 100644 index 0000000..ad1d182 --- /dev/null +++ b/docs-site/src/content/docs/how-to/benchmark-pps-jitter.md @@ -0,0 +1,82 @@ +--- +title: Benchmark PPS jitter honestly +description: Measure the kernel's own pulse timestamps, run A/B/A, and don't let chrony's smoothing lie to you. +sidebar: + order: 3 +--- + +Every claim on this site was produced this way. If you want to disagree with us, +disagree with these numbers. + +## Don't use chrony's stats for this + +`chronyc sourcestats` gives you a windowed, median-filtered, slowly-converging +estimate. That is *exactly what you want* for disciplining a clock and *exactly +what you don't want* for measuring a change you just made. It lags, it smooths, +and it will happily hide a regression for several minutes. + +Measure the kernel's PPS timestamps directly instead. + +## The measurement + +Each PPS assert should land exactly 1.000000000 s after the last one. The +deviation from that is the jitter — nothing else. + +```bash +sudo apt install pps-tools + +sudo timeout 62 ppstest /dev/pps0 | awk ' +/assert/ { + split($0, a, "assert "); split(a[2], b, ","); t = b[1] + 0; + if (prev > 0) { + d = (t - prev - 1.0) * 1e9; + n++; sum += d; sumsq += d*d; + if (n == 1 || d > max) max = d; + if (n == 1 || d < min) min = d; + } + prev = t +} +END { + mean = sum/n; sd = sqrt(sumsq/n - mean*mean); + printf "n=%d jitter_sd=%.0f ns p2p=%.0f ns\n", n, sd, max-min +}' +``` + +62 seconds gives you ~60 intervals. That's enough to see a 3× effect and not +enough to see a 5% one — size your window to the effect you're hunting. + +## Always run A/B/A + +This is the part people skip, and it's the part that makes the number mean +something. + +A time server's behaviour drifts on the scale of minutes: the board warms up, +satellites rise and set, the DOP changes. If you measure **A then B** and B is +worse, you cannot distinguish "B is worse" from "the last five minutes were +worse." + +So measure **A → B → A**: + +``` +round 1: feature OFF → 1304 ns +round 2: feature ON → 1912 ns +round 3: feature OFF → 1169 ns ← agrees with round 1. Now believe round 2. +``` + +If the two A rounds disagree with each other by more than the A-to-B difference, +**you have measured nothing** and you should go again with a longer window. + +We caught our [dashboard's 36% tax](/explanation/the-observer-effect/) exactly +this way, and we *disbelieved* two other apparent wins when the bracketing rounds +refused to agree. + +## Sanity check: is it even connected? + +```console +$ sudo ppstest /dev/pps0 +source 0 - assert 1783875773.176667184, sequence: 28 +source 0 - assert 1783875774.176667944, sequence: 29 +``` + +Sequence incrementing once a second = you have a pulse. No output = you have a +blinking LED and a wire that goes nowhere. See [Hardware](/reference/hardware/). diff --git a/docs-site/src/content/docs/how-to/cross-compile-rt-kernel.md b/docs-site/src/content/docs/how-to/cross-compile-rt-kernel.md new file mode 100644 index 0000000..9fae558 --- /dev/null +++ b/docs-site/src/content/docs/how-to/cross-compile-rt-kernel.md @@ -0,0 +1,111 @@ +--- +title: Cross-compile an RT kernel and deploy it to a headless Pi +description: Build an RPi-native aarch64 PREEMPT_RT kernel on an x86 workstation in ~40 minutes, and install it so that a failed boot doesn't cost you a trip to the SD card slot. +sidebar: + order: 2 +--- + +Building natively on a Pi 4 takes hours. Cross-compiling on a normal workstation +takes about forty minutes. And if you've never done it, the scary part isn't the +build — it's that a bad kernel on a headless box means physically extracting the +SD card. This page addresses both. + +:::tip[Or skip it] +We publish the built artifacts. See [Downloads](/reference/downloads/). +::: + +## 1. Toolchain + +```bash +sudo apt install crossbuild-essential-arm64 bc bison flex libssl-dev make \ + libc6-dev libncurses5-dev +``` + +## 2. Source, matched to your running kernel + +```bash +git clone --depth=1 --branch rpi-6.12.y \ + https://github.com/raspberrypi/linux.git +cd linux +``` + +Use **Raspberry Pi's** tree, not vanilla. RPi's PREEMPT_RT is already merged in +6.12 and the board's DT/overlays live there. + +## 3. Configure + +```bash +export ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- +make bcm2711_defconfig # Pi 4 +scripts/config --enable PREEMPT_RT +scripts/config --set-str LOCALVERSION "-rt-cuckoo" +make olddefconfig +``` + +Now apply [the patch](/how-to/patch-pps-gpio/) — this is the whole reason you're +building a kernel rather than installing one. + +:::note[Check that MMC and EXT4 are `=y`] +`bcm2711_defconfig` builds the SD card driver and filesystem *into* the image, not +as modules. That means **you don't need an initramfs** — which removes the single +most common way a hand-built Pi kernel fails to boot. + +```bash +grep -E 'CONFIG_(MMC_BCM2835|EXT4_FS)=' .config # want =y, not =m +``` +::: + +## 4. Build + +```bash +make -j$(nproc) Image modules dtbs +``` + +## 5. Stage the artifacts + +```bash +# Note the ABSOLUTE path. `~` does not expand inside a make variable — +# INSTALL_MOD_PATH=~/out silently installs into a literal "~" directory +# and you end up shipping an 80 KB tarball that contains nothing. +make INSTALL_MOD_PATH=/home/you/out modules_install +tar -C /home/you/out -czf rt-modules.tar.gz lib/modules/ +gzip -c arch/arm64/boot/Image > kernel-rt.img.gz +``` + +## 6. Deploy without a rescue trip + +The rule: **never overwrite the kernel that currently boots.** + +```bash +scp kernel-rt.img.gz rt-modules.tar.gz pi@host:/tmp/ +ssh pi@host +sudo tar -C / -xzf /tmp/rt-modules.tar.gz +zcat /tmp/kernel-rt.img.gz | sudo tee /boot/firmware/kernel-rt.img > /dev/null +``` + +`kernel8.img` — the stock kernel — is untouched. Then add **one** line to +`/boot/firmware/config.txt`: + +```ini +kernel=kernel-rt.img +``` + +```bash +sudo reboot +``` + +:::tip[The recovery path] +If it doesn't come back: pull the SD card, mount the FAT boot partition on any +machine, **delete that one line**, put it back. The stock kernel boots. That's the +whole rollback — no initramfs to regenerate, no bootloader to repair, and it works +from a Windows laptop if that's all you have. + +Test the rollback *before* you need it. +::: + +## 7. Confirm + +```console +$ uname -a +Linux gps-ntp 6.12.x-rt-cuckoo #1 SMP PREEMPT_RT ... aarch64 +``` diff --git a/docs-site/src/content/docs/how-to/patch-pps-gpio.md b/docs-site/src/content/docs/how-to/patch-pps-gpio.md new file mode 100644 index 0000000..a6334d7 --- /dev/null +++ b/docs-site/src/content/docs/how-to/patch-pps-gpio.md @@ -0,0 +1,76 @@ +--- +title: Patch pps-gpio for PREEMPT_RT +description: Rebuild one kernel module in about a minute and get your PPS timestamp back into hard-IRQ context. +sidebar: + order: 1 +--- + +**Do this if:** you run PPS from a GPIO pin on a PREEMPT_RT kernel. Which is to +say — do this if you followed any realtime-kernel time-server guide. +[Why](/explanation/preempt-rt-made-it-worse/). + +You do **not** need to rebuild the whole kernel. `pps-gpio` is a module. + +## 1. Confirm you have the problem + +```console +$ uname -a | grep -o PREEMPT_RT +PREEMPT_RT + +$ ps -eo pid,class,rtprio,psr,comm | grep irq/.*pps + 239 RR 50 3 irq/41-pps@12.-1 ← the handler is a thread. That's the bug. +``` + +If that `ps` prints nothing, your handler is already in hard-IRQ context and you +have nothing to fix. + +## 2. Patch + +In your kernel source tree, `drivers/pps/clients/pps-gpio.c`, in +`get_irqf_trigger_flags()`, just before the `return`: + +```c + /* The handler timestamps the pulse, so it has to run in hard-irq + * context. Under PREEMPT_RT it would otherwise be force-threaded and + * the timestamp taken after thread wakeup latency, adding microseconds + * of jitter to an edge that should be good to nanoseconds. + */ + flags |= IRQF_NO_THREAD; + + return flags; +``` + +## 3. Build just the module + +```bash +make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \ + M=drivers/pps/clients modules +``` + +About a minute, versus ~40 for a full kernel. + +## 4. Install and reboot + +```bash +scp drivers/pps/clients/pps-gpio.ko pi@host:/tmp/ +ssh pi@host 'sudo install -m644 /tmp/pps-gpio.ko \ + /lib/modules/$(uname -r)/kernel/drivers/pps/clients/pps-gpio.ko && \ + sudo depmod -a && sudo reboot' +``` + +:::caution[The module must match the running kernel exactly] +`vermagic` is checked at load. If you build against a different source tree than +the running kernel, `modprobe` fails and — because `pps-gpio` is what creates +`/dev/pps0` — chrony loses its refclock entirely. Build from the *same* tree that +produced the kernel you're running, or rebuild both. +::: + +## 5. Verify + +```console +$ ps -eo pid,class,rtprio,psr,comm | grep irq/.*pps +(nothing — back in hard-irq context) +``` + +Then [measure it](/how-to/benchmark-pps-jitter/). You should see roughly a 3× +improvement in raw PPS jitter, and about a 10× improvement in chrony's RMS offset. diff --git a/docs-site/src/content/docs/how-to/survive-a-power-cut.md b/docs-site/src/content/docs/how-to/survive-a-power-cut.md new file mode 100644 index 0000000..cf3e8a4 --- /dev/null +++ b/docs-site/src/content/docs/how-to/survive-a-power-cut.md @@ -0,0 +1,84 @@ +--- +title: Survive a power cut +description: Two failures that only a cold power-cycle finds. Both of ours were silent, and one of them was our own monitoring dashboard. +sidebar: + order: 4 +--- + +A time server's whole job is to be there. Ours had been up for days, Stratum 1, +199 ns — and it would **not have survived a power outage**. Two independent bugs, +neither visible from any amount of `systemctl status`. + +Pull the plug. It's the only test that finds these. + +## Failure 1: never pin the gpsd baud rate + +Several guides tell you to reconfigure the GPS module to a higher baud rate and +then pin gpsd to match: + +```ini +GPSD_OPTIONS="-n -s 115200" # ← don't +``` + +Here's what happens. Most u-blox modules hold their config in **volatile RAM**. +Cut the power and the module comes back at its factory **9600**. gpsd, pinned to +115200, opens the port, talks to a device that isn't listening, and reports +nothing. Not a degraded fix. **No GPS at all.** Your Stratum 1 server silently +becomes a Stratum 3 client of the internet — and stays that way until a human +notices. + +Let gpsd auto-probe: + +```ini +GPSD_OPTIONS="-n" +``` + +It sweeps the standard baud rates, finds the module wherever it landed, and comes +back on its own. + +:::note[And the baud change bought nothing anyway] +We measured it: **−1 ns** difference in PPS offset between 9600 and 115200. Which +makes sense — [the precision lives in the pulse, not the +sentences](/explanation/where-precision-lives/). NMEA at 9600 has plenty of time +to tell you *which* second it is. Faster serial changes nothing and costs you your +power-cut resilience. +::: + +## Failure 2: your monitoring is load-bearing + +`gpsd` ships with **socket activation**. It starts when something connects to port +2947. And chrony never connects to port 2947 — it reads gpsd's *shared memory*. + +So on a fresh boot, nothing starts gpsd. No gpsd, no NMEA, no `refclock SHM 0`, no +second-numbering for the PPS pulses. + +Ours *appeared* to work. It worked because **the dashboard** — our web status page +— polls gpsd on 2947, and the dashboard starts at boot. The monitoring was +socket-activating the thing it was monitoring. Stop the dashboard "to reduce load +on the clock" and you'd have stopped the clock. + +```console +$ systemctl is-enabled gpsd.service +disabled ← this is the bug +``` + +```bash +sudo systemctl enable gpsd.service +``` + +Check `gpsd.service`, not `gpsd.socket`. + +## What a healthy recovery looks like + +With both fixed, and a few internet NTP servers left in `chrony.conf` as a +backstop, we cut the mains and watched: + +| t+ | state | +|---|---| +| 0 s | power restored, boot | +| ~35 s | chrony up, **Stratum 3** — leaning on internet NTP. Serving time. | +| ~90 s | GPS fix acquired, NMEA flowing | +| **165 s** | PPS trusted, internet sources demoted, **Stratum 1** | + +No human involved. That middle window — serving slightly-worse time instead of no +time — is why you keep the upstream servers configured even on a GPS clock. diff --git a/docs-site/src/content/docs/index.mdx b/docs-site/src/content/docs/index.mdx new file mode 100644 index 0000000..3a74dbb --- /dev/null +++ b/docs-site/src/content/docs/index.mdx @@ -0,0 +1,94 @@ +--- +title: What this is (and isn't) +description: A field report on GPS Stratum 1 timekeeping on a Raspberry Pi 4 — what the guides get wrong, and the measurements that prove it. +--- + +import { Aside, CardGrid, Card } from '@astrojs/starlight/components'; + +**This is not a build guide.** + +Guides for building a GPS-disciplined Stratum 1 NTP server on a Raspberry Pi +already exist. [geerlingguy/time-pi](https://github.com/geerlingguy/time-pi) and +[josh-blake/pixie](https://github.com/josh-blake/pixie) are both good. Go read +them. Come back when your jitter is bad. + +This site is what happened when we followed that advice on a **Raspberry Pi 4** +and measured everything: **most of it is wrong on this board**, one piece of it +is wrong on *every* board, and the single change that helped most was a one-line +kernel patch nobody has written down. + + + +## The short version + + + + The realtime kernel — the marquee upgrade — **tripled our PPS jitter** + (2134 ns → 6947 ns). It force-threads interrupt handlers, and the PPS driver + takes its timestamp *inside* the handler. We put a scheduler between the + electrical edge and the clock. + [→ Why](/explanation/preempt-rt-made-it-worse/) + + + On a Pi 4, GPIO interrupts are demuxed through `pinctrl-bcm2835` and refuse + an `smp_affinity`. The "isolate the PPS IRQ on its own core" advice is + **unachievable here** — and the only way to enable it is the very thing that + costs you the accuracy. + [→ Why](/explanation/the-interrupt-you-cannot-move/) + + + `ethtool -T eth0` → `PTP Hardware Clock: none`. There is no hardware + timestamping. Software PTP is just a worse NTP. Don't chase it. + [→ Why](/explanation/no-ptp-on-a-pi-4/) + + + Ours cost **36% more PPS jitter** — by forking `chronyc` four times a second + onto the one core the PPS interrupt is welded to. The instrument was bending + the measurement. + [→ Why](/explanation/the-observer-effect/) + + + +## What actually moved the needle + +Almost none of the things we expected. + +| Change | RMS offset | +|---|---| +| Baseline | 823 ns | +| chrony: median `filter` + `prefer` on the PPS refclock | 440 ns | +| PREEMPT_RT (unpatched) | **2468 ns** ← *worse* | +| PREEMPT_RT + our `IRQF_NO_THREAD` patch | **199 ns** | + +Baud rate, SBAS, CPU isolation, IRQ pinning: **noise, or actively harmful.** +The full numbers and methodology are in [the measurements](/reference/measurements/). + +## The one artifact worth stealing + +If you take nothing else from this site, take +[the kernel patch](/reference/the-patch/). Every person running GPIO-based PPS +on a PREEMPT_RT kernel is, right now, silently eating microseconds of jitter and +has no idea. It's four lines. It's upstreamable. It's the reason our RMS offset +is 199 ns instead of 2468 ns. + +## Why "The Cuckoo Escapement" + +The **escapement** is the part of a mechanical clock that takes continuous energy +and chops it into discrete, regular ticks. It's the single component that decides +whether a clock is precise or worthless. That is *exactly* what a PPS interrupt +handler does: it takes an electrical edge and turns it into one discrete +timestamp. Our entire finding is that all the precision in the system lives in +that one handler — and that the realtime kernel was putting a scheduler in front +of it. + +We didn't fix a time server. We fixed the escapement. + +The **cuckoo** is the other half. The bird's whole job is to pop out and announce +the hour; the PPS pulse's whole job is to pop out and announce the second. The +bird *is* the pulse. And, well — everything you were told about this turned out +to be a bit cuckoo. diff --git a/docs-site/src/content/docs/reference/configuration.md b/docs-site/src/content/docs/reference/configuration.md new file mode 100644 index 0000000..97e0321 --- /dev/null +++ b/docs-site/src/content/docs/reference/configuration.md @@ -0,0 +1,88 @@ +--- +title: Final configuration +description: The chrony, gpsd, kernel and systemd config this box actually runs. +sidebar: + order: 4 +--- + +## chrony + +```ini +# /etc/chrony/conf.d/gps.conf + +# Coarse NMEA time from gpsd. Labels WHICH second it is. Never the time source. +refclock SHM 0 refid GPS precision 1e-1 offset 0.0 delay 0.2 poll 3 noselect + +# Kernel PPS: the precise source. Median-filter 10 pulses, lock to GPS for +# second-numbering, prefer it as the reference. +refclock PPS /dev/pps0 refid PPS precision 1e-9 poll 2 lock GPS filter 10 prefer + +allow 10.0.0.0/8 +``` + +Plus, in `chrony.conf`: + +```ini +user root # needed to read gpsd's SHM segments (mode 0600, root-owned) +``` + +:::note[Keep the internet servers] +Leave a few upstream NTP servers configured. While the GPS cold-acquires after a +power cut, chrony leans on them and serves *slightly less precise* time rather than +*no* time — then demotes them the instant PPS becomes trustworthy. We watched it +bridge a 165-second gap and promote itself back to Stratum 1 unattended. That +graceful degradation is worth the four lines. +::: + +## gpsd + +```ini +# /etc/default/gpsd +START_DAEMON="true" +USBAUTO="false" +DEVICES="/dev/ttyAMA0" +GPSD_OPTIONS="-n" # -n = poll immediately. NEVER pin the baud with -s. +``` + +```bash +systemctl enable gpsd.service # NOT just gpsd.socket — see below +``` + +:::danger[Two ways gpsd will betray you] +1. **Socket activation isn't enough.** chrony reads gpsd's *shared memory*, never + its socket — so nothing triggers the daemon to start. If it seems to work + anyway, something *else* is connecting to port 2947 and starting it for you. + For us that was the dashboard: our monitoring page was load-bearing for the + time server. Check with `systemctl is-enabled gpsd.service`. + +2. **Never pin the baud.** [The module reverts to 9600 on power + loss](/how-to/survive-a-power-cut/), and a pinned gpsd then talks to a device + that isn't listening. Let it auto-probe. +::: + +## Kernel cmdline + +``` +isolcpus=2,3 irqaffinity=0,1 nohz=off cpuidle.off=1 skew_tick=1 +``` + +## systemd affinities + +[cpu0 is sacred](/explanation/cpu0-is-sacred/) — it holds the PPS interrupt. + +```ini +# chrony.service.d/affinity.conf +[Service] +CPUSchedulingPolicy=rr +CPUSchedulingPriority=20 +CPUAffinity=2 + +# gpsd.service.d/affinity.conf +[Service] +CPUAffinity=3 + +# everything else (dashboard, Caddy, exporters, cron…) +[Service] +CPUAffinity=1 +Nice=10 +``` diff --git a/docs-site/src/content/docs/reference/downloads.md b/docs-site/src/content/docs/reference/downloads.md new file mode 100644 index 0000000..fb4a24a --- /dev/null +++ b/docs-site/src/content/docs/reference/downloads.md @@ -0,0 +1,72 @@ +--- +title: Downloads — prebuilt RT kernel +description: A patched PREEMPT_RT kernel for the Raspberry Pi 4, so you don't need a cross-compile toolchain. +sidebar: + order: 5 +--- + +The only expensive part of [the patch](/reference/the-patch/) is the toolchain. +Building natively on a Pi 4 takes hours; cross-compiling needs an x86 box and a +setup session. So here's the artifact. + +:::danger[Read this before you download] +- **Raspberry Pi 4 / arm64 only.** `bcm2711_defconfig`. It will not boot a Pi 5 or + a Pi 3. +- **Unsigned, community-built.** We built this on a workstation. There is no chain + of trust here beyond "we published the exact recipe and the checksums." If that + isn't good enough for your environment — and for some environments it correctly + isn't — [build it yourself](/how-to/cross-compile-rt-kernel/). It's forty + minutes. +- **Verify the checksums.** They're in `SHA256SUMS`. +::: + +## Artifacts + +Published on the [releases page](https://git.supported.systems/warehack.ing/cuckoo-escapement/releases): + +| File | What | +|---|---| +| `kernel-rt-.img.gz` | The kernel image, gzipped (Pi OS's own format) | +| `rt-modules-.tar.gz` | Matching modules — **must** be installed with the image | +| `install-rt-kernel.sh` | Installer. Adds a *new* image, never replaces `kernel8.img` | +| `SHA256SUMS` | Checksums | + +## Install + +```bash +sha256sum -c SHA256SUMS +sudo ./install-rt-kernel.sh +sudo reboot +``` + +The installer: + +1. Untars the modules into `/lib/modules/` +2. Writes the image as `/boot/firmware/kernel-rt.img` — **`kernel8.img` is left + alone** +3. Appends one line, `kernel=kernel-rt.img`, to `config.txt` + +**Rollback is deleting that one line.** Mount the SD card's FAT partition on any +machine, remove it, and the stock kernel boots. That's deliberate: you should never +have to make a physical trip to a headless box because of a kernel you got from a +website. + +## What's in it + +Raspberry Pi's `rpi-6.12.y` tree, `bcm2711_defconfig`, plus exactly two changes: + +```bash +scripts/config --enable PREEMPT_RT +# + the IRQF_NO_THREAD patch in drivers/pps/clients/pps-gpio.c +``` + +Nothing else. The full recipe is in +[Cross-compile an RT kernel](/how-to/cross-compile-rt-kernel/), and you should be +able to reproduce this byte-for-byte modulo build timestamps. + +:::note[Pinned to a tested version] +The published download always points at a kernel we have **actually booted and +benchmarked** on a Pi 4 — not simply the newest upstream. Shipping a stranger an +unvalidated kernel for a machine they may not be able to physically reach is not +something we're willing to do. +::: diff --git a/docs-site/src/content/docs/reference/hardware.md b/docs-site/src/content/docs/reference/hardware.md new file mode 100644 index 0000000..a29ff16 --- /dev/null +++ b/docs-site/src/content/docs/reference/hardware.md @@ -0,0 +1,78 @@ +--- +title: Hardware +description: BerryGPS-IMU v4 / u-blox CAM-M8C, and the PPS pad that isn't on the header. +sidebar: + order: 3 +--- + +## The board + +**BerryGPS-IMU v4** (Ozzmaker) on a **Raspberry Pi 4**. The GPS is a **u-blox +CAM-M8C** — 72-channel M8 engine, concurrent GPS/GLONASS/Galileo/BeiDou, with an +onboard antenna and a uFL connector for an external one. + +Reported by the module itself: + +```console +$ ubxtool -p MON-VER + swVersion ROM CORE 3.01 (107888) + hwVersion 00080000 # M8 generation + extension FWVER=SPG 3.01 + extension PROTVER=18.00 + extension GPS;GLO;GAL;BDS +``` + +## The PPS pin is not on the header + +This costs people hours, so: **the BerryGPS-IMU's PPS is not wired to any GPIO.** + +The board's normal header connection carries power, the GPS UART (GPIO14/15), and +the IMU's I²C — but the timepulse comes out of a **separate `T_PULSE` pad**, and +you have to run a wire from it yourself. + +The schematic confirms it: the CAM-M8C's TIMEPULSE pin goes through a 2N2222 +buffer that drives both the on-board **PPS LED** and the `T_PULSE` pad. Nothing +routes it to the Pi. + +:::caution[The blinking LED lies to you] +The PPS LED blinks once a second as soon as the module has a fix — **whether or +not the pulse is connected to anything**. It tells you the module is generating +PPS. It tells you nothing about whether your Pi can see it. + +We swept every plausible GPIO with interrupt-driven edge detection and found +nothing, while the LED blinked away merrily. The signal existed; it just had +nowhere to go. +::: + +We soldered a jumper from **`T_PULSE` → GPIO18** (physical pin 12), then: + +```ini +# /boot/firmware/config.txt +dtoverlay=pps-gpio,gpiopin=18 +``` + +Verify with a hardware-timestamped check, not a polling loop — a 100 ms pulse is +easy to miss by polling: + +```console +$ sudo ppstest /dev/pps0 +source 0 - assert 1783875773.176667184, sequence: 28 +source 0 - assert 1783875774.176667944, sequence: 29 # 1.000000760 s later +``` + +## The UART needs freeing first + +The Pi 4's *good* UART (PL011) is wired to **Bluetooth** by default; GPIO14/15 get +the flaky mini-UART whose baud drifts with the CPU clock. And a serial console may +be sitting on the port. Both must go: + +```ini +# /boot/firmware/config.txt +enable_uart=1 +dtoverlay=disable-bt +``` + +```ini +# /boot/firmware/cmdline.txt — remove this: +console=serial0,115200 +``` diff --git a/docs-site/src/content/docs/reference/measurements.md b/docs-site/src/content/docs/reference/measurements.md new file mode 100644 index 0000000..ae84723 --- /dev/null +++ b/docs-site/src/content/docs/reference/measurements.md @@ -0,0 +1,75 @@ +--- +title: The measurements +description: Every number on this site, with the methodology that produced it. Check our work. +sidebar: + order: 2 +--- + +All numbers from **one** Raspberry Pi 4 + BerryGPS-IMU v4 (u-blox CAM-M8C), PPS on +GPIO18. n = 1. Check them against your own board. + +## Headline progression + +| Change | RMS offset | Root dispersion | +|---|---|---| +| Baseline (stock kernel, stock chrony) | 823 ns | 16.8 µs | +| chrony `filter 10` + `prefer` on PPS refclock | 440 ns | 5 µs | +| PREEMPT_RT, unpatched | **2468 ns** ← *worse* | 11.6 µs | +| **PREEMPT_RT + [`IRQF_NO_THREAD`](/reference/the-patch/)** | **199 ns** | 6.3 µs | + +## Raw PPS jitter (kernel-timestamped) + +| Kernel | jitter (σ) | peak-to-peak | +|---|---|---| +| Stock | 2134 ns | 11 µs | +| PREEMPT_RT (threaded handler) | 6947 ns | 38 µs | +| PREEMPT_RT + patch (hard-irq handler) | 2568 ns | 18 µs | + +## The dashboard's tax + +A/B/A, 62 s per round. [Why this matters](/explanation/the-observer-effect/). + +| | Dashboard off | Dashboard on | +|---|---|---| +| Before fix | 1304 ns | 1912 / 2179 ns (**+36%**) | +| After fix | 1437 ns | 1169 / 1450 ns (**no penalty**) | + +## Things that did nothing + +| Change | Result | +|---|---| +| Baud 9600 → 115200 | PPS offset **−1 ns** either way. Identical. | +| SBAS disabled | No measurable change to PPS. | +| `isolcpus` alone | Inconclusive-to-harmful (concentrates load onto the PPS core). | + +## Methodology + +**Don't trust chrony's own stats for this.** `chronyc sourcestats` reports a +windowed, median-filtered figure that lags reality and hides what you're trying to +see. Measure the kernel's PPS timestamps directly: + +```bash +sudo timeout 62 ppstest /dev/pps0 | awk ' +/assert/ { + split($0, a, "assert "); split(a[2], b, ","); t = b[1] + 0; + if (prev > 0) { + d = (t - prev - 1.0) * 1e9; # deviation from exactly 1.000000000 s, in ns + n++; sum += d; sumsq += d*d; + if (d > max || n == 1) max = d; + if (d < min || n == 1) min = d; + } + prev = t +} +END { + mean = sum/n; sd = sqrt(sumsq/n - mean*mean); + printf "n=%d jitter_sd=%.0f ns p2p=%.0f ns\n", n, sd, max-min +}' +``` + +Each pulse should be exactly 1.000000000 s after the last. The deviation *is* the +jitter. + +**Always run A/B/A**, never A/B. Clock behaviour drifts on the scale of minutes; +if you measure on-then-off you cannot tell a real effect from thermal drift or a +satellite geometry change. Go on → off → on, and require the two "on" rounds to +agree before you believe the middle one. diff --git a/docs-site/src/content/docs/reference/the-patch.md b/docs-site/src/content/docs/reference/the-patch.md new file mode 100644 index 0000000..f32690d --- /dev/null +++ b/docs-site/src/content/docs/reference/the-patch.md @@ -0,0 +1,65 @@ +--- +title: The kernel patch +description: Four lines that keep the PPS timestamp in hard-IRQ context under PREEMPT_RT. The single most valuable artifact here. +sidebar: + order: 1 +--- + +If you take one thing from this site, take this. + +```c +--- a/drivers/pps/clients/pps-gpio.c ++++ b/drivers/pps/clients/pps-gpio.c +@@ -156,6 +156,13 @@ get_irqf_trigger_flags(const struct pps_gpio_device_data *data) + IRQF_TRIGGER_FALLING : IRQF_TRIGGER_RISING); + } + ++ /* The handler timestamps the pulse, so it has to run in hard-irq ++ * context. Under PREEMPT_RT it would otherwise be force-threaded and ++ * the timestamp taken after thread wakeup latency, adding microseconds ++ * of jitter to an edge that should be good to nanoseconds. ++ */ ++ flags |= IRQF_NO_THREAD; ++ + return flags; + } +``` + +## What it does + +`pps-gpio` requests its interrupt with only the trigger flags — no +`IRQF_NO_THREAD`. On a stock kernel that's fine, because handlers run in hard-IRQ +context anyway. Under **PREEMPT_RT**, the kernel force-threads it, and since +[the handler is where the timestamp is taken](/explanation/preempt-rt-made-it-worse/), +the measurement moves behind the scheduler. + +`IRQF_NO_THREAD` tells the kernel: *not this one*. The handler stays in hard-IRQ +context; everything else keeps RT's preemptibility. + +## What it's worth + +| | RMS offset | raw PPS jitter | +|---|---|---| +| PREEMPT_RT, unpatched | 2468 ns | 6947 ns | +| **PREEMPT_RT, patched** | **199 ns** | 2568 ns | + +## Why you probably need it + +As far as we can tell this is not applied anywhere. Which means **every person +running GPIO-based PPS on a PREEMPT_RT kernel is, right now, silently eating +microseconds of jitter** — and has no reason to suspect it, because nothing looks +broken. chrony still reports Stratum 1. The dashboard still says locked. The +number is just quietly, invisibly worse. + +If that's you, this patch is free accuracy. + +:::note[Upstreamable] +This belongs upstream, not in a blog post. It's a correctness fix for any +timestamping IRQ handler under RT, not a local hack. If you're a PPS maintainer +reading this: please take it. +::: + +## How to apply it + +See [Patch pps-gpio](/how-to/patch-pps-gpio/) — it's a module, so you can rebuild +just the one `.ko` in about a minute rather than the whole kernel. diff --git a/docs-site/src/styles/brass.css b/docs-site/src/styles/brass.css new file mode 100644 index 0000000..18b3b60 --- /dev/null +++ b/docs-site/src/styles/brass.css @@ -0,0 +1,192 @@ +/* The Cuckoo Escapement — brass-and-cream palette for Starlight. + * + * Why this file exists: the subject is horology. Escapements, gear trains, the + * mechanical business of chopping continuous energy into trustworthy ticks. So + * the site should read like a watchmaker's bench, not a terminal: warm brass on + * near-black, cream text, the feel of an old patent drawing. + * + * We override Starlight's CSS custom properties rather than rewriting its + * components — that keeps us forward-compatible with Starlight updates. + * + * Palette anchors: + * - background: near-black, faint warm tint (not the usual cold slate) + * - accent: brass — links, focus rings, the gear in the logo + * - text: cream, never pure white (easier on the eyes, warmer) + * - red: reserved for the "this is wrong" callouts, of which there are many + */ + +:root { + --sl-font-system-mono: "JetBrains Mono", "Fira Code", "SF Mono", + Menlo, Consolas, "DejaVu Sans Mono", monospace; + + /* Starlight derives every semantic color (asides, card icons) from a HUE + * variable — `--sl-color-purple-low/-/-high` are all hsl(var(--sl-hue-purple)…). + * Retune the hues once and the whole derived scale follows, in BOTH themes, + * instead of overriding nine colors by hand and watching them drift apart. + * + * Stock purple (281) is the loudest thing on the page and it does not belong + * on a brass bench: `:::tip` asides and half the card icons render violet. + * Swap it for verdigris — the blue-green a brass movement actually goes as it + * ages. Same job, right family. + */ + --sl-hue-purple: 172; /* was 281 (violet) → verdigris */ + --sl-hue-blue: 199; /* was 234 (indigo) → steel */ + --sl-hue-green: 145; /* was 101 (lime) → patina */ + --sl-hue-orange: 38; /* was 41 → brass. Already close. */ + --sl-hue-red: 8; /* the "this is wrong" callouts, of which there are many */ +} + +/* Dark is the default — this is a bench at 2am. */ +:root[data-theme="dark"] { + --sl-color-bg: #0b0d10; + --sl-color-bg-nav: #0e1116; + --sl-color-bg-sidebar: #0d1014; + --sl-color-bg-inline-code: #1d1a14; + + --sl-color-text: #e8e4dc; + --sl-color-text-accent: #e8bd6b; + + --sl-color-accent-low: #3d2f14; + --sl-color-accent: #d9a441; + --sl-color-accent-high: #f2d79b; + + --sl-color-white: #f4f1ea; + --sl-color-gray-1: #dbd6cc; + --sl-color-gray-2: #b6b0a4; + --sl-color-gray-3: #837d72; + --sl-color-gray-4: #4f4a43; + --sl-color-gray-5: #2d2a26; + --sl-color-gray-6: #1a1815; + + --sl-color-hairline: #2b2721; + --sl-color-hairline-light: #3a352c; + --sl-color-hairline-shade: #201d18; + +} + +:root[data-theme="light"] { + --sl-color-bg: #faf7f1; + --sl-color-bg-nav: #f2ede3; + --sl-color-bg-sidebar: #f5f1e8; + --sl-color-bg-inline-code: #ece5d6; + + --sl-color-text: #2b2721; + --sl-color-text-accent: #8a6417; + + --sl-color-accent-low: #e8d7ac; + --sl-color-accent: #a97c1f; + --sl-color-accent-high: #5c430f; + + --sl-color-white: #1a1815; + --sl-color-gray-1: #2d2a26; + --sl-color-gray-2: #4f4a43; + --sl-color-gray-3: #837d72; + --sl-color-gray-4: #b6b0a4; + --sl-color-gray-5: #dbd6cc; + --sl-color-gray-6: #ece7dd; + + --sl-color-hairline: #ddd6c8; +} + +/* Monospace headings — this is an engineering document, not an essay. */ +h1, h2, h3, .site-title { + font-family: var(--sl-font-system-mono); + letter-spacing: -0.01em; +} + +/* Inline code gets a warm plate, but not inside links or headings (where it + fights the surrounding type). */ +:not(a):not(h1):not(h2):not(h3):not(h4) > code { + background: var(--sl-color-bg-inline-code); + border: 1px solid var(--sl-color-hairline); + border-radius: 4px; + padding: 0.1em 0.35em; +} + +/* Measurement tables are the whole argument of this site, so let the numbers + line up and let them breathe. */ +table { + font-variant-numeric: tabular-nums; +} +table td:not(:first-child), +table th:not(:first-child) { + font-family: var(--sl-font-system-mono); + font-size: 0.92em; +} + +/* --- "A Supported Systems Joint" — the maker's plate --------------------- + * + * Styled as the engraved backplate of a clock movement: a double hairline + * (the classic brass-plate border), warm plate fill, engraved-looking small + * caps. Shared verbatim with the dashboard footer, recolored to its palette. + * See src/components/SupportedSystemsBadge.astro for the markup + rationale. + */ +.ss-plate { + margin-top: 3.5rem; +} +.ss-plate__link { + display: flex; + gap: 1.1rem; + align-items: center; + padding: 1.25rem 1.4rem; + text-decoration: none; + color: var(--sl-color-gray-2); + + /* Double hairline = brass plate edge. The outer ring is the box-shadow. */ + border: 1px solid var(--sl-color-hairline-light); + border-radius: 6px; + box-shadow: 0 0 0 3px var(--sl-color-bg), 0 0 0 4px var(--sl-color-hairline); + + /* Faint brushed-brass sheen, top-left, the way a plate catches bench light. */ + background: + radial-gradient(120% 140% at 0% 0%, rgba(217, 164, 65, 0.06), transparent 60%), + var(--sl-color-bg-nav); + transition: color 0.2s, border-color 0.2s; +} +.ss-plate__link:hover { + color: var(--sl-color-text); + border-color: var(--sl-color-accent-low); +} + +.ss-plate__logo { + flex: 0 0 auto; + width: 46px; + height: auto; + opacity: 0.85; + transition: opacity 0.2s; +} +.ss-plate__link:hover .ss-plate__logo { opacity: 1; } + +.ss-plate__copy { display: block; } +.ss-plate__heading { + display: block; + margin-bottom: 0.3rem; + font-family: var(--sl-font-system-mono); + font-size: 0.8rem; + font-weight: 600; + /* Engraved: small, wide-tracked caps, the way a name is cut into brass. */ + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--sl-color-white); +} +.ss-plate__body { + display: block; + font-size: 0.85rem; + line-height: 1.55; + max-width: 62ch; +} +.ss-plate__name { color: var(--sl-color-accent); } +.ss-plate__cta { + display: inline-flex; + align-items: center; + gap: 0.3rem; + margin-top: 0.5rem; + font-size: 0.8rem; + color: var(--sl-color-accent); +} +.ss-plate__link:hover .ss-plate__cta svg { transform: translateX(2px); } +.ss-plate__cta svg { transition: transform 0.2s; } + +@media (max-width: 32rem) { + .ss-plate__link { flex-direction: column; align-items: flex-start; } +} diff --git a/docs-site/src/styles/terminal.css b/docs-site/src/styles/terminal.css new file mode 100644 index 0000000..12f2b10 --- /dev/null +++ b/docs-site/src/styles/terminal.css @@ -0,0 +1,119 @@ +/* l2trace docs — terminal-palette overrides for Starlight. + * + * Why this file exists: l2trace's TUI uses a green-on-black terminal feel + * by default; the docs should feel like an extension of the same product. + * We override Starlight's CSS custom properties rather than rewriting + * components — that keeps us forward-compatible with Starlight updates. + * + * Palette anchors: + * - background: near-black, slight green tint (matches terminal phosphor) + * - accent: soft green for links / focus rings + * - amber: reserved for warnings and "audit time" callouts + * - text: warm off-white, not pure white (easier on eyes) + */ + +:root { + /* Monospace stack used throughout — code, headings, and accent UI. */ + --sl-font-system-mono: "JetBrains Mono", "Fira Code", "SF Mono", + Menlo, Consolas, "DejaVu Sans Mono", monospace; +} + +/* Dark theme is the default for the docs (matches the TUI). */ +:root[data-theme="dark"] { + --sl-color-bg: #0a0e0a; + --sl-color-bg-nav: #0d120d; + --sl-color-bg-sidebar: #0c100c; + --sl-color-bg-inline-code: #14201a; + + --sl-color-text: #e3e8e0; + --sl-color-text-accent: #7fdb7f; + + --sl-color-accent-low: #1b3a1b; + --sl-color-accent: #5fcf5f; + --sl-color-accent-high: #b8f0b8; + + --sl-color-white: #f1f5ee; + --sl-color-gray-1: #d6dccf; + --sl-color-gray-2: #b6bdaf; + --sl-color-gray-3: #828a7e; + --sl-color-gray-4: #4d544a; + --sl-color-gray-5: #2c3129; + --sl-color-gray-6: #1a1f18; + + --sl-color-hairline: #233022; + --sl-color-hairline-light: #2c3a2b; + --sl-color-hairline-shade: #182218; + + /* Asides/admonitions — re-color to match palette. */ + --sl-color-orange-high: #ffd98e; + --sl-color-orange: #f0b25c; + --sl-color-orange-low: #3a2a14; + --sl-color-red-high: #ff9a8a; + --sl-color-red: #e96252; + --sl-color-red-low: #3a1714; +} + +/* Light theme — softer, but still recognizably "l2trace". */ +:root[data-theme="light"] { + --sl-color-text-accent: #2f7a2f; + --sl-color-accent-low: #d6f0d6; + --sl-color-accent: #2f7a2f; + --sl-color-accent-high: #173d17; + --sl-color-bg-inline-code: #ebf2e9; +} + +/* Headings get a touch of monospace + slight tracking — newspaper-headline + * energy in a terminal aesthetic. Body text stays in the default sans for + * readability over long passages. */ +.sl-markdown-content h1, +.sl-markdown-content h2, +.sl-markdown-content h3 { + font-family: var(--sl-font-system-mono); + letter-spacing: -0.01em; +} + +/* Site title in the header — monospace to set the tone immediately. */ +.site-title { + font-family: var(--sl-font-system-mono); +} + +/* Inline code: green-tinted background, NO background color when inside + * link text (Starlight default looks muddy there). */ +.sl-markdown-content :not(a, h1, h2, h3, h4, h5, h6) > code:not(pre code) { + background: var(--sl-color-bg-inline-code); + border-radius: 0.2rem; + padding: 0.05rem 0.3rem; + border: 1px solid var(--sl-color-hairline); +} + +/* SVG embeds (the TUI screenshots) need a subtle border so they don't + * float disconnected on the dark page background. */ +.sl-markdown-content img[src$=".svg"] { + border: 1px solid var(--sl-color-hairline); + border-radius: 0.4rem; + background: #000; +} + +/* Side-by-side theme-comparison tables: don't let cells stretch tall, and + * keep the SVGs constrained so the row stays readable on smaller screens. + * Targeting tables that contain images directly is a bit blunt but right + * for our usage — the only such tables in the docs are theme-comparison + * grids on the TUI tour page. */ +.sl-markdown-content table:has(img) { + table-layout: fixed; + width: 100%; +} + +.sl-markdown-content table:has(img) td { + vertical-align: middle; + padding: 0.4rem; +} + +.sl-markdown-content table:has(img) img { + width: 100%; + height: auto; + display: block; + /* Pull off the .sl-markdown-content img[src$=".svg"] outer border since + * the table cell already provides one — double borders look noisy. */ + border-width: 0; +} diff --git a/docs-site/tsconfig.json b/docs-site/tsconfig.json new file mode 100644 index 0000000..8bf91d3 --- /dev/null +++ b/docs-site/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/kernel/0001-pps-gpio-keep-timestamp-in-hard-irq-under-PREEMPT_RT.patch b/kernel/0001-pps-gpio-keep-timestamp-in-hard-irq-under-PREEMPT_RT.patch new file mode 100644 index 0000000..428893b --- /dev/null +++ b/kernel/0001-pps-gpio-keep-timestamp-in-hard-irq-under-PREEMPT_RT.patch @@ -0,0 +1,18 @@ +diff --git a/drivers/pps/clients/pps-gpio.c b/drivers/pps/clients/pps-gpio.c +index 65d17781d..5ba2aaebe 100644 +--- a/drivers/pps/clients/pps-gpio.c ++++ b/drivers/pps/clients/pps-gpio.c +@@ -156,6 +156,13 @@ get_irqf_trigger_flags(const struct pps_gpio_device_data *data) + IRQF_TRIGGER_FALLING : IRQF_TRIGGER_RISING); + } + ++ /* The handler timestamps the pulse, so it has to run in hard-irq ++ * context. Under PREEMPT_RT it would otherwise be force-threaded and ++ * the timestamp taken after thread wakeup latency, adding microseconds ++ * of jitter to an edge that should be good to nanoseconds. ++ */ ++ flags |= IRQF_NO_THREAD; ++ + return flags; + } + diff --git a/kernel/README.md b/kernel/README.md new file mode 100644 index 0000000..b0b501f --- /dev/null +++ b/kernel/README.md @@ -0,0 +1,79 @@ +# PREEMPT_RT kernel for gps-ntp (Raspberry Pi 4) + +A cross-compiled RPi-native realtime kernel, plus a one-line `pps-gpio` fix +that turned out to be the whole point. + +## Why + +Chasing PPS jitter. The theory (from [pixie](https://github.com/josh-blake/pixie)) +is that PREEMPT_RT plus CPU isolation lets you park the PPS interrupt on a +dedicated core at realtime priority. + +On a **Pi 4 that doesn't work out of the box**, for two reasons we found the +hard way: + +1. **The PPS interrupt can't be moved on a stock kernel.** It's a GPIO + interrupt demuxed through `pinctrl-bcm2835`, so writing `smp_affinity` + returns `Operation not permitted`. Individual GPIO lines follow the GPIO + controller's parent IRQ. Pixie's approach assumes a Pi 5. + +2. **PREEMPT_RT alone made jitter 3× worse.** RT force-threads interrupt + handlers — but `pps-gpio` *timestamps the pulse inside its handler* + (`pps_get_ts()` → `pps_event()`). Threaded, that timestamp is taken after + thread-wakeup latency instead of at the electrical edge. We measured raw + PPS jitter go from 2134 ns → 6947 ns. + +## The fix + +`0001-pps-gpio-keep-timestamp-in-hard-irq-under-PREEMPT_RT.patch` adds +`IRQF_NO_THREAD` to the `pps-gpio` IRQ request, so the timestamp stays in +hard-irq context while the rest of the system keeps RT's preemptibility. + +Ironically, RT *also* solved problem (1): a force-threaded IRQ is a +schedulable thread, and a thread **can** be `taskset` to an isolated core — +which the stock kernel refused. So RT unlocked the pinning the hardware +denied us, and the patch undoes the damage RT did on the way. + +## Results (measured, chrony `sourcestats` / raw `ppstest`) + +| Config | RMS offset | Raw PPS jitter | +|---|---|---| +| Baseline (stock kernel, stock chrony) | 823 ns | — | +| + chrony median-filter/prefer | 440 ns | 2134 ns | +| + PREEMPT_RT (threaded PPS) | 2468 ns | 6947 ns | +| **+ `IRQF_NO_THREAD` patch** | **199 ns** | **2568 ns** | + +## Build (cross-compile from x86) + +```bash +sudo pacman -S --needed aarch64-linux-gnu-gcc # Arch/EndeavourOS +git clone --depth=1 --branch rpi-6.18.y https://github.com/raspberrypi/linux +cd linux +export ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- +make bcm2711_defconfig +./scripts/config --enable EXPERT --enable PREEMPT_RT \ + --set-str LOCALVERSION "-rt-timepi" --disable LOCALVERSION_AUTO +make olddefconfig +patch -p1 < ../0001-pps-gpio-keep-timestamp-in-hard-irq-under-PREEMPT_RT.patch +make -j$(nproc) Image modules dtbs +``` + +## Deploy (safe, revertible, headless-friendly) + +The Pi 4's SD and ext4 drivers are built-in (`CONFIG_MMC_BCM2835=y`, +`CONFIG_EXT4_FS=y`), so **no initramfs is needed**. Install the kernel under a +*new* name and leave `kernel8.img` alone — the whole change becomes one +revertible line. + +```bash +gzip -9 -c arch/arm64/boot/Image > Image.gz # match Pi OS's gzip format +scp Image.gz pi:/tmp/ && scp rt-modules.tar.gz pi:/tmp/ +# on the Pi: +sudo cp /tmp/Image.gz /boot/firmware/kernel-rt.img # NEW name; kernel8.img untouched +sudo tar xzf /tmp/rt-modules.tar.gz -C / # new /lib/modules// +sudo depmod 6.18.38-rt-timepi+ +echo 'kernel=kernel-rt.img' | sudo tee -a /boot/firmware/config.txt +``` + +**Recovery:** if it doesn't boot, pull the card and delete the +`kernel=kernel-rt.img` line. The stock kernel returns.