diff --git a/.claude/skills/mutation-hitlist/SKILL.md b/.claude/skills/mutation-hitlist/SKILL.md new file mode 100644 index 00000000..52a620e1 --- /dev/null +++ b/.claude/skills/mutation-hitlist/SKILL.md @@ -0,0 +1,100 @@ +--- +name: mutation-hitlist +description: Work the weekly mutation-testing hit list (issue #591) — look up the top surviving file, pull the full survivor list from CI artifacts, write killing tests, verify with a scoped Stryker run, and report back. Use when asked to "address this week's mutation testing", "work the hit list", or kill surviving mutants. +--- + +# Weekly mutation-testing workflow + +Issue **#591** is the auto-updated tracking issue (workflow: +`.github/workflows/mutation.yml`, weekly schedule). Its body holds the score, +per-shard table, and a truncated top-50 hit list. Work happens in comments; +never edit the body. + +## 1. Look up this week's target + +```bash +gh issue view 591 --repo sleepypod/core --json body -q .body | head -80 +``` + +- Note the **run id** from the "HTML reports" link and the trigger commit. +- Pick the top surviving file that has no recent comment claiming it + (`gh issue view 591 --comments` — prior weeks' work is recorded there). + +## 2. Pull the FULL survivor list (the issue shows only top 50) + +Each shard uploads `mutation-report-` containing `mutation.json`: + +```bash +cd $(mktemp -d) +gh run download --repo sleepypod/core -n mutation-report- +python3 /.claude/skills/mutation-hitlist/survivors.py mutation.json +``` + +Shard names: `hardware-lib`, `homekit`, `hooks`, `server`, +`services-scheduler`, `streaming` (see the matrix in mutation.yml). + +## 3. Branch and verify staleness + +- Branch off **fresh `origin/dev`** (`test/-mutants-591`). +- `git diff origin/dev --stat -- ` — + if the source changed since the run, line numbers are stale; re-run scoped + Stryker first instead of trusting the artifact. + +## 4. Write killing tests + +A killing test must FAIL when the mutation is applied. Expected test file is +`/tests/.test.ts`. Prefer **no source changes**; if internals are +unreachable, extending an existing `__test__` export with pure functions is +the accepted pattern (see `piezoStream.ts`). + +Playbook by mutator: +- `StringLiteral` in thrown errors / WS error messages → assert exact message + content, not just `/some regex/` that an empty string can't fail. +- `StringLiteral` in `console.log/warn` → `vi.spyOn(console, ...)` and assert + `expect.stringContaining(...)` plus exact numeric args where deterministic. +- `EqualityOperator` (`<` vs `<=`) → test the exact boundary value. +- `ConditionalExpression true/false` → one test per branch, asserting the + branch-specific observable (message text, count, absence of a frame). +- `BlockStatement {}` on guards → assert the guarded side effect happened + (port refuses connections after shutdown, no duplicate frames, etc.). +- `ArithmeticOperator` on buffer offsets → multi-tick scenarios (partial + append, then seek) with exactly-once + content assertions. + +## 5. Known equivalent / unkillable categories (don't chase these) + +- **Module-level initializer mutants** (const config at top of file, + `new Decoder({...})` options): activate at import; the vitest runner's + module cache makes them false survivors. Unkillable without per-mutant + process restart. +- Timing/perf-only values (`setTimeout` delays, yield cadence like + `recordsSinceYield >= 500`, read-chunk sizes that only change speed). +- Double-guarded checks (a second guard downstream makes the first + unobservable), `Math.random()` entropy, `>= 0` on sizes iterated with + `for..of` over an empty set. + +## 6. Verify — tests locally, mutation in CI + +Do **NOT** run Stryker locally — mutation runs happen in CI only. + +```bash +pnpm exec vitest run src//tests/.test.ts +``` + +Then let CI produce the mutation numbers: + +- **Per-PR** (preferred): apply the `mutation-test` label to the PR — + `mutation.yml` runs on it and comments a summary. +- **Manual**: Actions → "Mutation Testing" → Run workflow, with the `scope` + input set to the target file glob (blank = all shard defaults). +- **Weekly**: the Saturday 06:00 UTC schedule refreshes issue #591's baseline. + +Pull the run's `mutation-report-` artifact and re-run `survivors.py` +on its `mutation.json` for the post-change survivor list. + +## 7. Report and ship + +- PR to `dev`, title `test(): kill surviving mutants (#591 hit list)`, + with a test plan section. +- Comment on #591 following the established format: before → after counts, + file score, and a line-by-line justification for each remaining survivor + (see the mqttBridge comment for the template). diff --git a/.claude/skills/mutation-hitlist/survivors.py b/.claude/skills/mutation-hitlist/survivors.py new file mode 100644 index 00000000..5c2c83a3 --- /dev/null +++ b/.claude/skills/mutation-hitlist/survivors.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Extract surviving mutants for one file from a Stryker mutation.json. + +Usage: survivors.py +Prints one line per surviving mutant: location, mutator, replacement. +""" +import json +import sys + +if len(sys.argv) != 3: + sys.exit(__doc__) + +report = json.load(open(sys.argv[1])) +needle = sys.argv[2] +matches = [k for k in report['files'] if needle in k] +if not matches: + sys.exit(f'no file matching {needle!r}; files: {len(report["files"])}') +for key in matches: + mutants = [m for m in report['files'][key]['mutants'] if m['status'] == 'Survived'] + print(f'{key}: {len(mutants)} surviving') + for m in sorted(mutants, key=lambda m: (m['location']['start']['line'], + m['location']['start']['column'])): + loc = m['location'] + repl = m['replacement'][:100].replace('\n', ' ') + print(f" L{loc['start']['line']}:{loc['start']['column']}" + f"-{loc['end']['line']}:{loc['end']['column']}" + f" {m['mutatorName']}: {repl}") diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index b3099025..f54ec7a6 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -166,7 +166,7 @@ jobs: - name: Comment on PR (labeled PR runs only) if: github.event_name == 'pull_request' - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const fs = require('fs') @@ -212,7 +212,7 @@ jobs: - name: Update pinned tracking issue (scheduled runs only) id: update-issue if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' - uses: actions/github-script@v7 + uses: actions/github-script@v9 env: ISSUE_TITLE: '📊 Mutation testing — weekly baseline' with: @@ -397,7 +397,7 @@ jobs: - name: Compute aggregated row id: compute - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const fs = require('fs') diff --git a/.github/workflows/python-modules.yml b/.github/workflows/python-modules.yml index 9eebd51e..ea185e7c 100644 --- a/.github/workflows/python-modules.yml +++ b/.github/workflows/python-modules.yml @@ -57,6 +57,11 @@ jobs: target: . cov_source: . deps: "" + - name: calibrator + workdir: modules/calibrator + target: . + cov_source: . + deps: "" - name: piezo-processor workdir: modules/piezo-processor target: . diff --git a/.gitignore b/.gitignore index 57c43eed..e66ee089 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,10 @@ reports/mutation sleepypod.dev.db sleepypod.dev.db-shm sleepypod.dev.db-wal +# biometrics.dev.db itself stays tracked (intentional dev fixture); the WAL/SHM +# sidecars are transient per-machine state and must not be committed. +biometrics.dev.db-shm +biometrics.dev.db-wal # SQLite test/local databases sleepypod.test.db @@ -55,3 +59,4 @@ sleepypod.local.db-wal # SQLite databases (local) *.db .reviews/ +__pycache__/ diff --git a/.node-version b/.node-version index 2bd5a0a9..a45fd52c 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -22 +24 diff --git a/CLAUDE.md b/CLAUDE.md index df4389b2..83c5de12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,21 +4,69 @@ This file provides instructions and context for AI coding agents working on this ## Build & Test -_Add your build and test commands here_ - ```bash -# Example: -# npm install -# npm test +pnpm install # deps (pnpm workspace) +pnpm dev # Next.js dev server +pnpm build # production build (standalone output) +pnpm tsc # type-check (quality gate) +pnpm lint # eslint (quality gate) +pnpm test # vitest unit suite (quality gate) +pnpm test:mutation # Stryker mutation testing (slow; CI tracks score) + +# Python modules (each modules// is a uv project): +cd modules/ && uv run --with pytest pytest test_main.py +# Use the module's locked deps (uv run), NOT an ad-hoc venv — unpinned +# numpy/scipy versions change signal-processing results. ``` +Quality gates (`pnpm tsc && pnpm lint && pnpm test`) also run in the pre-push +hook. CI runs the Python module matrix in +`.github/workflows/python-modules.yml`. + ## Architecture Overview -_Add a brief overview of your project architecture_ +Next.js (App Router) server that runs ON the pod and controls it: + +- `src/hardware/` — DAC transport over Unix socket to frankenfirmware + (`dacTransport.ts` prod server-mode, `socketClient.ts` dev client-mode), + `dacMonitor` status polling, gesture detection, pump-stall guard, snooze. +- `src/server/` — tRPC routers (also exposed REST-style via trpc-to-openapi + at `/api/*`). **No auth: LAN-only trust model** — see `openapi.ts`. +- `src/scheduler/` — node-schedule JobManager (temperature/power/alarm + schedules from sleepypod.db plus prime/reboot/LED/run-once system jobs). +- `src/automation/` — Autopilot engine: signal-driven rules with tick / + timeOfDay / signalChange triggers, evaluated in the device timezone. +- `src/streaming/` — WebSocket sensor stream on port 3001 (`piezoStream`), + frame normalization, optional MQTT bridge (Home Assistant discovery). +- `src/homekit/` — HAP bridge (thermostat, power/snooze switches, sensors); + hardware writes serialize through `sideController` + `sideLock`. +- `src/db/` — two SQLite DBs via drizzle: `sleepypod.db` (config/state, + `migrations/`) and `biometrics.db` (time-series, `biometrics-migrations/`); + `retention.ts` prunes time-series tables. +- `modules/` — Python daemons reading CBOR `*.RAW` sensor files and writing + biometrics.db: piezo-processor (vitals), sleep-detector (presence/sessions), + environment-monitor (temps), calibrator, cover-buttons; `modules/common/` + is their shared library. +- `app/` — routes/UI; `src/components/` + `src/hooks/` — frontend. Device + status prefers the WS stream with tRPC HTTP fallback (`useDeviceStatus`). + +Cross-cutting invariants: +- Per-side hardware writes MUST go through `withSideLock` (globalThis-backed). +- Singletons live on `globalThis` (Turbopack can duplicate module instances). +- DAC protocol has no correlation ids — commands are strictly sequential; + never read a response you didn't just send a command for. ## Conventions & Patterns -_Add your project-specific conventions here_ +- One commit per fix/feature; conventional-commit subjects (semantic-release). +- Vitest tests live in `tests/` dirs beside the code (`src/**/tests/*.test.ts`); + Python tests are `modules//test_main.py` with pod-only imports stubbed + via `sys.modules` so they run on dev machines. +- Stryker mutation testing is active — behavioral fixes need a pinning test + or they surface as surviving mutants. +- DB schema changes: edit `src/db/*schema.ts`, then `pnpm db:generate` / + `pnpm db:biometrics:generate`. Never hand-edit migration journals — entry + `when` values must stay strictly increasing (enforced by a test). ## Debugging a pod diff --git a/app/[lang]/autopilot/page.tsx b/app/[lang]/autopilot/page.tsx new file mode 100644 index 00000000..ba343c09 --- /dev/null +++ b/app/[lang]/autopilot/page.tsx @@ -0,0 +1,5 @@ +import { AutopilotConsole } from '@/src/components/Autopilot/AutopilotConsole' + +export default function AutopilotPage() { + return +} diff --git a/app/api/export/archive/route.handler.test.ts b/app/api/export/archive/route.handler.test.ts index edceb28a..87846b2b 100644 --- a/app/api/export/archive/route.handler.test.ts +++ b/app/api/export/archive/route.handler.test.ts @@ -27,6 +27,7 @@ vi.mock('node:fs/promises', () => ({ ...fsMock, default: fsMock })) interface FakeChild extends EventEmitter { stdout: Readable stderr: EventEmitter + kill: ReturnType } const spawnMock = vi.hoisted(() => ({ @@ -60,6 +61,7 @@ function makeFakeChild(): FakeChild { const child = new EventEmitter() as FakeChild child.stdout = stdout child.stderr = stderr + child.kill = vi.fn() return child } @@ -262,6 +264,65 @@ describe('GET /api/export/archive', () => { expect(res2.status).not.toBe(429) }) + it('rejects non-numeric startTs/endTs with 400 without latching inflight', async () => { + const { GET } = await loadRoute() + const res = await GET(new Request('http://localhost/api/export/archive?startTs=garbage')) + expect(res.status).toBe(400) + const body = await res.json() + expect(body).toEqual({ error: 'startTs and endTs must be numeric epoch seconds' }) + expect(spawnMock.spawn).not.toHaveBeenCalled() + + // inflight must not have been taken by the rejected request. + const res2 = await GET(new Request('http://localhost/api/export/archive?include=raw')) + expect(res2.status).toBe(200) + }) + + it('kills tar, clears inflight, and removes staging when the client disconnects', async () => { + const { GET } = await loadRoute() + const controller = new AbortController() + const req = new Request('http://localhost/api/export/archive?include=raw', { + signal: controller.signal, + }) + const res = await GET(req) + expect(res.status).toBe(200) + + const child = spawnMock.lastChild + if (!child) throw new Error('expected spawn to have been called') + + controller.abort() + + expect(child.kill).toHaveBeenCalledWith('SIGKILL') + expect(fsMock.rm).toHaveBeenCalledWith( + expect.stringContaining('sp-export-'), + { recursive: true, force: true }, + ) + // inflight cleared — a follow-up export is allowed. + const res2 = await GET(new Request('http://localhost/api/export/archive?include=raw')) + expect(res2.status).not.toBe(429) + }) + + it('kills tar and clears inflight when the watchdog expires', async () => { + process.env.EXPORT_WATCHDOG_MS = '30' + try { + const { GET } = await loadRoute() + const res = await GET(new Request('http://localhost/api/export/archive?include=raw')) + expect(res.status).toBe(200) + + const child = spawnMock.lastChild + if (!child) throw new Error('expected spawn to have been called') + + // tar never emits close/error — the watchdog must fire. + await new Promise(r => setTimeout(r, 80)) + expect(child.kill).toHaveBeenCalledWith('SIGKILL') + + const res2 = await GET(new Request('http://localhost/api/export/archive?include=raw')) + expect(res2.status).not.toBe(429) + } + finally { + delete process.env.EXPORT_WATCHDOG_MS + } + }) + it('uses the provided startTs and endTs in the filename', async () => { const { GET } = await loadRoute() const req = new Request('http://localhost/api/export/archive?startTs=100&endTs=200&include=raw') diff --git a/app/api/export/archive/route.ts b/app/api/export/archive/route.ts index 16946041..6ccb4d90 100644 --- a/app/api/export/archive/route.ts +++ b/app/api/export/archive/route.ts @@ -24,6 +24,14 @@ const SAFE_RAW_NAME = /^[\w.-]+\.RAW(\.gz)?$/i let inflight = false +// A hung tar (no close/error event) or a client that disconnects mid-stream +// must not leave `inflight` latched (429s until restart) or leak the staging +// dir. Env override exists for tests. +const EXPORT_WATCHDOG_MS = (() => { + const raw = Number(process.env.EXPORT_WATCHDOG_MS ?? '') + return Number.isFinite(raw) && raw > 0 ? raw : 30 * 60 * 1000 +})() + interface SourceFile { abs: string basename: string @@ -97,10 +105,20 @@ export async function GET(request: Request) { { status: 429, headers: { 'Retry-After': '60' } }, ) } - inflight = true const startTs = Number(url.searchParams.get('startTs') ?? '0') const endTs = Number(url.searchParams.get('endTs') ?? String(Math.floor(Date.now() / 1000))) + if (Number.isNaN(startTs) || Number.isNaN(endTs)) { + // A malformed range would silently export everything in [NaN, NaN] → []. + // Worse, `?startTs=garbage` used to fall through as an all-time export. + return Response.json( + { error: 'startTs and endTs must be numeric epoch seconds' }, + { status: 400 }, + ) + } + + inflight = true + const include = (url.searchParams.get('include') ?? 'raw,db') .split(',') .map(s => s.trim()) @@ -142,11 +160,29 @@ export async function GET(request: Request) { if (dir) rm(dir, { recursive: true, force: true }).catch(() => {}) inflight = false } + + // Both paths kill tar and clean up: a disconnected client stops reading + // the stream, and a wedged tar never emits close on its own. + const abortExport = () => { + tarChild.kill('SIGKILL') + cleanup() + } + const watchdog = setTimeout(abortExport, EXPORT_WATCHDOG_MS) + request.signal.addEventListener('abort', abortExport, { once: true }) + const settle = () => { + clearTimeout(watchdog) + request.signal.removeEventListener('abort', abortExport) + } + tarChild.on('close', (code) => { + settle() if (code !== 0 && tarStderr) console.error('tar:', tarStderr) cleanup() }) - tarChild.on('error', cleanup) + tarChild.on('error', () => { + settle() + cleanup() + }) const stream = Readable.toWeb(tarChild.stdout) as ReadableStream const filename = `sleepypod-export-${startTs}-${endTs}.tar.gz` diff --git a/app/panel/route.test.ts b/app/panel/route.test.ts new file mode 100644 index 00000000..d2ee60c6 --- /dev/null +++ b/app/panel/route.test.ts @@ -0,0 +1,31 @@ +/** + * Tests for the tRPC panel route. The panel exposes every procedure + * unauthenticated, so it must never be served from production pods. + */ +import { afterEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@ajayche/trpc-panel', () => ({ + renderTrpcPanel: vi.fn(() => 'panel'), +})) +vi.mock('@/src/server/routers/app', () => ({ appRouter: {} })) + +import { GET } from './route' + +afterEach(() => { + vi.unstubAllEnvs() +}) + +describe('GET /panel', () => { + it('returns 404 in production', () => { + vi.stubEnv('NODE_ENV', 'production') + const res = GET(new Request('http://pod.local/panel')) + expect(res.status).toBe(404) + }) + + it('serves the panel outside production', () => { + vi.stubEnv('NODE_ENV', 'development') + const res = GET(new Request('http://pod.local/panel')) + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toBe('text/html') + }) +}) diff --git a/app/panel/route.ts b/app/panel/route.ts index fbc88a86..bdb80509 100644 --- a/app/panel/route.ts +++ b/app/panel/route.ts @@ -2,6 +2,14 @@ import { appRouter } from '@/src/server/routers/app' import { renderTrpcPanel } from '@ajayche/trpc-panel' export function GET(req: Request) { + // Dev tool only: the panel exposes every procedure (device.execute, + // system.setInternetAccess, raw.deleteFile, ...) with zero gating, and the + // API itself is unauthenticated (LAN-only trust model — see + // src/server/openapi.ts). Don't serve it from production pods. + if (process.env.NODE_ENV === 'production') { + return new Response('Not found', { status: 404 }) + } + const host = req.headers.get('host') || new URL(req.url).host const proto = req.headers.get('x-forwarded-proto') || 'http' diff --git a/biometrics.dev.db-shm b/biometrics.dev.db-shm deleted file mode 100644 index fe9ac284..00000000 Binary files a/biometrics.dev.db-shm and /dev/null differ diff --git a/biometrics.dev.db-wal b/biometrics.dev.db-wal deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/adr/0022-pump-stall-safety.md b/docs/adr/0022-pump-stall-safety.md index a57d170c..208d110c 100644 --- a/docs/adr/0022-pump-stall-safety.md +++ b/docs/adr/0022-pump-stall-safety.md @@ -341,6 +341,30 @@ the wire protocol's actual reliability. placeholder. Needs data from a pod with a known clog episode to calibrate, or a longer in-field bake before enabling by default. +## Addendum (2026-07-11): false trips on firmware-initiated pump stops + +Field report (Pod 4): the guard tripped both sides daily, always with +`rpm = 0`, at exactly session-duration expiry and at the end of the +scheduled daily prime. Root cause: `expectedActive` was derived solely +from `device_state`, which mirrors the firmware through the +`durationExpired` heuristic and can report a side as powered for +minutes after the firmware ends a session (`currentLevel` stays +non-zero while the water equalizes). The frzHealth frame cadence +(~10s, not the 60s assumed in "Alternatives considered" §7) made the +2-sample dwell a ~20s window — far shorter than the mirror lag. + +Fix: `DeviceStateSync` now suppresses stall counting when the stop is +firmware-commanded, using lag-free firmware-side signals. `pump.duty` +is authoritative when the frame carries it: 0 means a commanded stop, +while a driven pump (duty > 0) reading 0 RPM is the stall signature +and is never suppressed. When duty is absent, fall back to priming +active or ended within 120s, the firmware `targetLevel` reading +neutral, or the session countdown (`heatTimeL/R` on the wire, +`heatingDuration` in `DeviceStatus`) within 90s of its natural end — +bounded to 600s past the projected end so a stale snapshot cannot +suppress a later session's genuine stall. A zero-RPM frame mid-session +with the pump still driven trips exactly as before. + ## References - Incident report: Discord, 2026-05-24, free-sleep user thread. diff --git a/docs/adr/0023-autopilot-reactive-automations.md b/docs/adr/0023-autopilot-reactive-automations.md new file mode 100644 index 00000000..1b372477 --- /dev/null +++ b/docs/adr/0023-autopilot-reactive-automations.md @@ -0,0 +1,275 @@ +# ADR 0023: Reactive automations engine (Autopilot) + +**Status:** Accepted +**Date:** 2026-06-07 + +## Context + +Everything the Pod does on a timer today is **time-triggered**: a cron +expression feeds `node-schedule`, which feeds `JobManager`, which writes to +the hardware through `getSharedHardwareClient()`. Temperature curves, power +schedules, and alarms are all variations on "at time T, do X." There is no way +for the device to react to a *live signal* — to lower a setpoint because the +sleeper started moving, or to hold a temperature relative to room ambient as +the room warms through the night. + +Eight Sleep ships exactly this feature ("autopilot") as an opaque black box: +the bed changes temperature and the user has no way to see why. For a DIY +system whose whole reason to exist is that the owner controls and understands +their hardware, that opacity is the thing to beat. The product wedge is +**transparency** — show *why* a rule fired (an audit log) and *what it would +have done* on a past night (a backtest), so the user can trust the engine +before handing it the thermostat. + +The infrastructure a reactive engine needs already exists and is load-bearing +elsewhere: a shared FIFO hardware socket, a per-side mutex (`withSideLock`), +a liveness heartbeat with reboot recovery (the `JobManager` pattern), the +mutation-broadcast event bus (ADR 0015), and a typed live-signal surface from +the DAC monitor and biometrics modules. What is missing is an *evaluation +layer* that reads those signals and decides when to write — not new plumbing. + +Two motivating examples bound the design; the engine must handle both: + +1. **Continuous policy** — "During 23:00–06:00, hold my temp at `ambient + 3°F`." + The setpoint is a function of a live signal, re-evaluated as ambient moves. + Level-triggered, idempotent re-assertion while in-window. +2. **Edge-triggered rule** — "If movement averages > 200 over the last 10 min, + lower temp by 2°F." A windowed aggregate crossing a threshold fires a + one-shot action with a cooldown. + +The user never picks "continuous" vs "edge" — the distinction is implied by +whether the action references a live signal. The engine handles both through a +per-rule `idle → active → cooldown` state machine. + +## Decision + +Build an `AutomationEngine` that sits **beside** `JobManager`, reads the same +signal buses, and writes through the same hardware path. No new hardware code. + +### The rule model — WHEN / IF / THEN + +A small, three-primitive model rather than a full visual programming language: + +```text +Automation { + id, name, enabled, side?, priority, cooldownMin? + WHEN trigger // signal-change | tick | time-of-day + IF condition[] // AND/OR/NOT guard tree + THEN action[] // params may be expressions, e.g. ambient + 3 +} +``` + +- Example 1 → WHEN `ambient.temperature changes` · IF `time 23:00–06:00` + · THEN `set left.target = clamp(ambient + 3, min, max)` +- Example 2 → WHEN `tick (1m)` · IF `avg(left.movement, 10m) > 200` + · THEN `set left.target -= 2` (cooldown 30m) + +Conditions: numeric comparators, windowed aggregates +(`avg|max|min|sum|count(signal, last N min)`), time-of-day / day-of-week, +state/enum, `AND/OR/NOT`, plus stability primitives (`sustained for N min` +debounce and **hysteresis** with separate on/off thresholds so rules don't +chatter at a boundary). Actions are the existing device verbs +(`setTemperature`, `setPower`, `setLedBrightness`, alarm verbs, `setAwayMode`, +`startPriming`) plus a non-hardware `notify` action that is the safe default +for testing. Action params may be expressions with a visible clamp. + +### Data model + +Two tables, following `src/db/schema.ts` conventions, with the rule "AST" held +in JSON columns validated by zod at the tRPC boundary: + +```text +automations( + id, name, enabled, side? (left|right|null=both), + priority, cooldownMin?, trigger(json), conditions(json), actions(json), + createdAt, updatedAt ) + +automation_runs( + id, automationId, firedAt, + outcome(fired|skipped|clamped|dry_run|error), detail(json) ) +``` + +`automation_runs` is the transparency surface — it is what answers "why did my +bed warm up at 3am." It is not optional telemetry; it is the product. + +A tRPC `automations.*` router (CRUD + enable/dry-run + backtest), modelled on +`schedules.*`, hot-reloads the running engine on every write so changes take +effect without a restart (the `schedules.ts → JobManager` pattern). + +### Safety stack (non-negotiable — this drives a device someone sleeps on) + +- **Two-layer clamp** — every temperature expression is clamped first to the + action's own `[min,max]` band, then unconditionally to the hardware bound + 55–110°F. +- **Anti-thrash** — a setpoint is only re-asserted when it moves ≥0.5°F. Bed + temp slews 1–2°F/min, so spamming the pump with sub-threshold moves is + pointless and harmful. +- **Runaway guard** — a per-rule actions/hour cap; tripping it auto-disables + the rule and surfaces it in `/debug`. +- **Manual override wins** — touching the dial suspends autopilot on that side + for a hold window; the engine logs `skipped` rather than fighting the user. +- **Kill switch & away mode** — a global, persisted off-switch + (`device_settings.autopilotEnabled`) that short-circuits the tick and is + restored at boot; away mode already disables a side. +- **Dry-run** — a rule can run notify-only for several nights, logging + *would-fire* events without touching hardware, so the user builds trust + before the engine controls the bed. + +### Precedence stack (highest wins) + +1. Manual override (timed hold) +2. Active run-once session *(existing)* +3. **Autopilot automations** (by `priority`, then most recent) +4. Recurring temperature/power schedules *(existing)* +5. Neutral default + +Autopilot sits **above recurring schedules, below run-once and manual.** It is +realized without an explicit coordinator: the engine refuses to write a side +while `hasActiveRunOnceSession(side)` is true or a manual-override hold is +active (both log `skipped`); against recurring schedules it is last-writer-wins +on the shared `withSideLock(side)`, and because a continuous-policy rule +re-asserts its setpoint every tick, it wins the steady state. Constant-policy +rules are therefore the supported way to override a schedule. + +### Resolved tunables (P0, 2026-06-07) + +- **Manual-override hold** — `AUTOMATION_MANUAL_OVERRIDE_MS = 30 min`, in-memory + per side. Cleared on reboot (safe default: autopilot resumes). +- **Evaluator cadence** — a single global 60s tick (`AUTOMATION_TICK_MS`) that + also samples signals into the windowed-aggregate ring buffers. 60s matches + the ambient/movement cadence and sits well under the thermal slew, so a + coarser tick loses nothing while avoiding pump spam. Per-signal cadence is a + later refinement. +- **Clamp band** — the per-action `clamp {min,max}` (default + `AUTOMATION_DEFAULT_USER_MIN/MAX = 60/100°F`) is layer 1; the hardware bound + 55–110°F is layer 2, always applied. No new global per-user setting in P0 — + the per-action band is strictly inside the hardware range and is the smallest + surface that satisfies the two-layer requirement. + +### UI — structured forms, not a node graph + +`@xyflow/react` is already a dependency but read-only (`DataPipeline.tsx`); +`dnd-kit` is not installed. v1 is **structured WHEN/IF/THEN forms** with a live +plain-English sentence preview, not a node-graph canvas. The builder uses the +typed signal catalog to offer only valid operators per signal. The backtest +panel — pick a past night, overlay the signal trace with fire markers and the +resulting setpoint line — is the centerpiece, because it is the trust-builder +that a black-box competitor cannot offer. A `/debug` status panel shows live +per-rule state, the run log, the kill switch, and per-rule dry-run. + +## Alternatives Considered + +### 1. Extend the existing scheduler instead of a parallel engine + +Bolt reactive triggers onto `JobManager`. + +**Rejected.** `JobManager` is built around cron → fire-once semantics with a +liveness model tuned to that. Reactive rules need a continuous evaluator, +windowed buffers, and a per-rule state machine — a different lifecycle. Forcing +both into one component couples two concerns that fail differently. A parallel +engine that *shares the hardware path and locks* gets the reuse without the +coupling. + +### 2. A full visual programming language + +Let users compose arbitrary logic blocks. + +**Rejected for v1.** WHEN/IF/THEN with expression params and AND/OR/NOT covers +both motivating examples and the foreseeable ones. A general language is more +surface to validate, secure, and explain — against a transparency-first product +goal it is a net negative until a concrete need exists. + +### 3. Node-graph builder (drag-and-drop canvas) + +`@xyflow/react` is already in the tree. + +**Deferred, not rejected.** A node canvas is a plausible future for advanced +multi-condition logic, but it needs `dnd-kit` (a new dependency) and is a much +larger UI surface. Structured forms + a sentence preview ship the value now; +the node graph can come later without changing the engine or data model. + +### 4. New dedicated hardware write path for autopilot + +Give the engine its own client to avoid contending on the shared socket. + +**Rejected.** The shared FIFO socket, per-side mutex, and mutation broadcast +exist precisely to serialize all writers. A second path would reintroduce the +race conditions those primitives were built to remove and would bypass the +mutation broadcast the UI depends on. The engine is just another writer on the +existing lock. + +### 5. Explicit coordination with recurring schedules + +Have the engine read the schedule table and negotiate who owns a side. + +**Rejected for P0.** Last-writer-wins on the shared side lock already produces +the desired "autopilot above schedules" behavior, because a continuous-policy +rule re-asserts every tick. Explicit negotiation adds a stateful coupling +between two subsystems for a case the lock already resolves. Revisit only if a +concrete conflict appears that last-writer-wins gets wrong. + +### 6. A global per-user temperature band setting in P0 + +Add a `device_settings` min/max consulted by every clamp. + +**Deferred.** The per-action clamp band is strictly inside the hardware range +and satisfies the two-layer safety requirement today. A global band is a +strictly-additive change later — it can replace the default without touching +the engine — so shipping it in P0 is premature surface. + +## Consequences + +### Positive + +- Adds reactive automation as an evaluation layer over existing, battle-tested + plumbing — shared client, side mutex, mutation broadcast, liveness recovery — + rather than new hardware code. +- The `automation_runs` audit log plus the backtest panel deliver the + transparency wedge the black-box competitor structurally cannot match. +- The safety stack (two-layer clamp, anti-thrash, runaway guard, manual + override, kill switch, dry-run) is defense-in-depth on a device someone + sleeps on, with the conservative default at every layer. +- z-score conditions ("HR > baseline + 2σ") are nearly free — + `getVitalsBaseline` already returns mean and SD — giving an anomaly-detection + capability for little extra cost once vitals signals are wired. + +### Negative / costs + +- A second long-lived background component beside `JobManager` to operate, + observe, and reason about during incidents. Mitigated by sharing the locks + and the `/debug` status panel. +- Both motivating examples depend on expression evaluation and windowed + aggregates (the "P2" capability set). The engine supports them; what gates + them firing on live data is the **signal-source wiring**, not the engine. +- The engine handles **numeric** signals only. The builder's catalog is + therefore the numeric/backtestable subset; enum/bool signals (sleep stage, + occupancy) in the full catalog read `undefined` → "skip" until their sources + are wired. This is the safe degradation, but it means the shipped builder is + intentionally a subset of the documented catalog. + +### Open questions + +- **Live signal coverage.** P0/P1 wire only the reliably-available device + signals (per-side temperature/level, `water.low`). Biometric, ambient, and + enum/bool signals read `undefined`→skip until their readers land. Active + rules can only fire on what is wired; backtests already replay the full + recorded series. Tracked as the next phase. +- **Per-signal cadence.** A single 60s tick is correct for the current signal + set; faster-moving signals may later justify per-signal sampling. +- **Per-side target history is not persisted**, so edge-mode backtests compute + setpoints relative to a nominal baseline rather than the true historical + target. Called out at the backtest boundary; revisit if it proves misleading. + +## References + +- Beside `JobManager`: `src/scheduler/jobManager.ts`, + `src/scheduler/instance.ts`. +- Engine: `src/automation/` (`engine.ts`, `evaluator.ts`, `expressions.ts`, + `windows.ts`, `signals.ts`, `backtest.ts`, `types.ts`, `instance.ts`). +- Router + validation: `src/server/routers/automations.ts`, + `src/server/validation-schemas.ts`. +- Kill-switch column: `device_settings.autopilotEnabled` (migration 0014). +- Mutation-broadcast event bus: ADR 0015. +- Vitals baseline (z-score source): `getVitalsBaseline`. +- UI: `app/[lang]/autopilot/`, `src/components/Autopilot/`, + `src/components/diagnostics/DiagnosticsConsole.tsx`. diff --git a/docs/code-review-2026-07-03-fixes.md b/docs/code-review-2026-07-03-fixes.md new file mode 100644 index 00000000..c9dff639 --- /dev/null +++ b/docs/code-review-2026-07-03-fixes.md @@ -0,0 +1,151 @@ +# Code Review Fix Plan — 2026-07-03 + +Execution plan for the remaining findings from the 2026-07-03 adversarial Codex +review of `dev` against `origin/main`. The source reports live in `.reviews/dev/` +(gitignored, local-only, overwritten by the next review run), so every finding is +copied here in full — this document is self-contained. + +Line references were re-verified against `dev` HEAD after commit `0c87deb`. + +**Status of the review's consensus findings:** + +Already fixed in `0c87deb` (`fix(automation): honor manual overrides and missing +setpoints`), with regression tests: + +- Major: `setPower` with an explicit unresolved temperature expression fell + through to the hardware 75°F fallback (`src/automation/engine.ts`). +- Major: `registerManualOverride()` was never called from production manual + controls (device router, gestures, HomeKit). +- Major: edited rules reused stale trigger runtime after `reload()`. + +Remaining: **1 Major + 4 Minor**, planned below. Previous repo-wide review +(2026-07-01, 44 findings) is fully remediated; its plan was retired from +`docs/` and remains in git history. + +--- + +## Batch 1 — Major: shared side-lock invariant + +- [x] **1.1 Route manual hardware writes through `withSideLock()`** + + The branch introduced `withSideLock()` (`src/hardware/sideLock.ts`) as the + global per-side hardware mutex, and CLAUDE.md declares it an invariant: + *per-side hardware writes MUST go through `withSideLock`*. Scheduler + (`src/scheduler/jobManager.ts:324,361,396,425,1064,1104`) and Autopilot + (`src/automation/instance.ts:119` → `engine.ts:410`) honor it — but the + manual control surfaces do not: + + - Device router: `client.setTemperature()` at + `src/server/routers/device.ts:374`, `client.setPower()` at `:445` + (MQTT user commands also flow through this router). + - HomeKit: `src/homekit/accessories/sideController.ts` still serializes + through its own local `sideQueues` promise chain (`:34-45`, reset at + `:205-206`) — a *separate* mutex that does not exclude scheduler/autopilot + writes. + - Gestures: `client.setTemperature()` at + `src/hardware/gestureActionHandler.ts:107`, `client.setPower()` at `:171`. + + **Impact:** a manual/HomeKit command can interleave semantically with a + scheduler/autopilot sequence on the same side. The DAC transport queue + serializes bytes, not the higher-level invariant (e.g. "mark side off, then + skip the now-stale temp/alarm writes"). + + **Change:** wrap the hardware mutation (not surrounding UI/cache logic) in + `withSideLock(side, ...)` at each entrypoint: + - `device.ts`: wrap the final debounced write — the dial-drag collapse + (`:50`) must stay *outside* the lock so rapid calls still coalesce. Mind + the snooze-window comment at `:468` (lock hold time across + connect/setPower). + - `sideController.ts`: replace the local `sideQueues` chain with + `withSideLock`, preserving the optimistic HomeKit intent/cache behavior. + - `gestureActionHandler.ts`: wrap the two writes. + + **Tests:** per-entrypoint serialization test — start a slow scheduler-style + `withSideLock` holder, issue the manual write, assert it does not reach the + hardware client until the lock releases (and that opposite-side writes are + not blocked). Suites: `src/server/routers/tests/device.test.ts`, + `src/homekit/tests/sideController.test.ts`, + `src/hardware/tests/gestureActionHandler.test.ts`. + + One commit: `fix(hardware): route manual writes through shared side lock`. + +--- + +## Batch 2 — Minor: cap-frame data pipeline (`src/streaming/`) + +These three touch the same new persistence path (`piezoStream` → +`capFramePersistence` → `cap_sense_frames`). One commit each, or a single +grouped `fix(streaming)` commit if the diffs stay small. + +- [x] **2.1 Reject partial Pod 3 capSense payloads instead of zero-filling** + + `capSideChannels()` (`src/streaming/normalizeFrame.ts:147-149`) expands any + object with *at least one* of `out`/`cen`/`in` to six channels, zero-filling + the rest — `{ out: 12 }` becomes `[12, 12, 0, 0, 0, 0]`. Missing channels + become real zero-pressure readings in persisted `cap_sense_frames` rows and + any `cap.*` backtest derived from them. + + **Change:** return `null` (skip the frame) unless all three of + `out`/`cen`/`in` are numeric. Safe to be strict: the only production caller + is the persistence path (`src/streaming/piezoStream.ts:796-797`); the live + WS stream does not use this helper. + +- [x] **2.2 Sanity-guard firmware timestamps before cap-frame persistence** + + `piezoStream` passes raw `frame.ts` to `recordCapFrame()` whenever it is a + number (`src/streaming/piezoStream.ts:806-807`), and `recordCapFrame()` + trusts it (`src/streaming/capFramePersistence.ts:118`, `tsSeconds * 1000`). + RAW frames can carry tiny relative timestamps or far-future values — the + Python writers already guard for exactly this + (`modules/environment-monitor/main.py:44-75`, + `modules/sleep-detector/main.py:89-118`). + + **Impact:** a tiny timestamp persists 1970-era rows until prune; a far-future + *first* frame wedges the side's window — later normal timestamps are all + `< startTsMs`, so the window never rolls over and cap history stops flushing + for that side. + + **Change:** apply the same wall-clock sanity window as the Python modules + before accepting a timestamp; skip invalid/future frames (or fall back to + `Date.now() / 1000`). + +- [x] **2.3 Flush in-flight cap windows on RAW file switch and shutdown** + + `recordCapFrame()` flushes a window only on a ≥5s rollover + (`src/streaming/capFramePersistence.ts:118-134`); `resetCapFrameWindows()` + just nulls both accumulators (`:152`). `piezoStream` calls the reset + immediately on RAW file switch (`src/streaming/piezoStream.ts:711`) and + shutdown never flushes — so the tail window of every RAW file is dropped, + leaving a deterministic blind spot at each file boundary. + + **Change:** add `flushCapFrameWindows()` that persists non-empty in-flight + windows, and call it before the reset on file switch and during shutdown. + Do not persist empty windows. + +--- + +## Batch 3 — Minor: DB retention index + +- [x] **3.1 Timestamp-leading index for `cap_sense_frames` pruning** + + The only index on `cap_sense_frames` is the unique `(side, timestamp)` pair + (`src/db/biometrics-schema.ts:134`), but retention deletes filter on + `timestamp` alone (`src/streaming/capFramePersistence.ts:103`) — SQLite + cannot seek the second column, so each prune scans the bulkiest time-series + table. The branch already added timestamp-leading retention indexes to the + other time-series tables for the same reason. + + **Change:** add `index('idx_cap_sense_frames_timestamp').on(t.timestamp)` to + the schema, then `pnpm db:biometrics:generate`. Never hand-edit the + migration journal (entry `when` values must stay strictly increasing). + +--- + +## Definition of done + +- Every behavioral fix lands with a pinning test (Stryker mutation testing is + active — untested fixes surface as surviving mutants). +- Quality gates green: `pnpm tsc && pnpm lint && pnpm test`. +- One conventional commit per fix (or the noted Batch 2 grouping), checkboxes + above ticked in the same commit as the fix. +- Pushed: `git status` clean vs `origin/dev`. diff --git a/docs/temperature-units-action-plan.md b/docs/temperature-units-action-plan.md new file mode 100644 index 00000000..6a78dd27 --- /dev/null +++ b/docs/temperature-units-action-plan.md @@ -0,0 +1,272 @@ +# Temperature Units Action Plan + +## Execution Status + +Implemented 2026-07-03: + +- Priority 1: shared temperature helpers live in `src/lib/tempUtils.ts`; `useTemperatureUnit` delegates to them. +- Priority 2: primary dial, step controls, and side selector render/edit in the preferred unit while sending Fahrenheit mutations. +- Priority 3: schedule cards, charts, set point editor, curve editor controls, and alarm editor/card render/edit in the preferred unit while writing Fahrenheit. +- Priority 4: automation temperature signals are documented as Fahrenheit; diagnostics telemetry remains Fahrenheit and uses shared formatters. +- Priority 5: existing TypeScript/Python normalization fixtures cover live Celsius vs persisted centidegrees, sentinels, and missing zones; Pod 3 partial capSense persistence is now rejected. +- Priority 6: shared schedule/time helpers live in `src/lib/scheduleTime.ts`; `Header`, `useScheduleActive`, `TimeInput`, `DaySelector`, and schedule grouping use/re-export them. + +## Goal + +Respect the user's temperature preference everywhere temperatures are displayed or edited, while keeping hardware, automation, and storage boundaries explicit. + +Canonical units today: + +- Hardware setpoints and device state: Fahrenheit. +- Schedule storage: Fahrenheit. +- Live sensor frames: Celsius. +- Biometrics environment storage: centidegrees Celsius. +- HomeKit and MQTT boundaries: Celsius where required by those protocols. +- Automation signals and thermal diagnostics: Fahrenheit by current contract. + +## Priority 1: Centralize Temperature Unit Boundaries + +### Action + +Replace ad hoc display conversion with a single temperature domain module. + +Suggested target: + +- Extend `src/lib/tempUtils.ts`, or split to `src/lib/temperature.ts` if the file becomes too broad. + +Add explicit helpers: + +- `setpointFToDisplay(tempF, unit)` +- `displayToSetpointF(value, unit)` +- `sensorCToDisplay(celsius, unit)` +- `centidegreesToDisplay(centidegrees, unit)` +- `formatDisplayTemp(value, unit, options?)` +- `formatSetpointF(tempF, unit, options?)` +- `formatSensorC(celsius, unit, options?)` + +Keep low-level conversions: + +- `toF` +- `toC` +- `centiDegreesToC` +- `centiDegreesToF` +- `centiPercentToPercent` + +### Acceptance Criteria + +- Components do not need to know whether a displayed value came from Celsius, centidegrees, or Fahrenheit. +- Existing helpers in `useTemperatureUnit` delegate to the shared module instead of duplicating formulas. +- Names make source units obvious. + +### Tests + +- Add unit tests for each new helper. +- Include null/undefined formatting behavior. +- Include round trips for Celsius preference editing setpoints back to Fahrenheit. + +## Priority 2: Make Primary Temperature Control Preference-Aware + +### Files + +- `src/components/TempScreen/TempScreen.tsx` +- `src/components/TemperatureDial/TemperatureDial.tsx` +- `src/components/SideSelector/SideSelector.tsx` + +### Current Problem + +`TempScreen` reads `settings.device.temperatureUnit`, but the primary dial and side selector still display Fahrenheit. Mutations are correctly sent to hardware in Fahrenheit, but user-facing display ignores the preference. + +### Action + +- Keep `TempScreen` internal state and mutations in Fahrenheit. +- Pass `unit` to `TemperatureDial`. +- Render dial target/current temperatures with `formatSetpointF`. +- Convert drag/key/display values cleanly: + - Dial geometry can remain Fahrenheit for now. + - If Celsius editing is desired, map UI increments to the displayed unit and convert committed values back to Fahrenheit. +- Update `SideSelector` to use the same setpoint formatting helper. + +### Acceptance Criteria + +- With `temperatureUnit = C`, the main dial, current temperature, and side selector show Celsius. +- Hardware mutations still call `device.setTemperature` with Fahrenheit. +- No double conversion occurs when WebSocket status frames update. + +### Tests + +- Add or update component tests for Fahrenheit and Celsius preference. +- Verify that a Celsius-displayed edit sends the expected Fahrenheit payload. + +## Priority 3: Make Schedule Views and Editors Preference-Aware + +### Files + +- `src/hooks/useSchedule.ts` +- `src/hooks/useSchedules.ts` +- `src/hooks/useScheduleActive.ts` +- `src/components/Schedule/CurveCard.tsx` +- `src/components/Schedule/CurveChart.tsx` +- `src/components/Schedule/CurveEditor.tsx` +- `src/components/Schedule/SetPointEditor.tsx` +- `src/components/Schedule/AlarmCard.tsx` +- `src/components/Schedule/AlarmEditor.tsx` + +### Current Problem + +The schedules router supports `unit`, but UI callers use the default Fahrenheit response and hardcode `°F`. Editors also write Fahrenheit directly. + +### Action + +Choose one of these patterns and apply consistently: + +1. Preferred: keep schedule tRPC reads/writes canonical in Fahrenheit and convert only in UI. +2. Alternative: request schedules in the user's unit and convert mutation payloads back to Fahrenheit before writes. + +The preferred pattern is simpler because storage and scheduler execution are already Fahrenheit. + +Implementation steps: + +- Read temperature preference with `useTemperatureUnit`. +- Render schedule setpoints using `formatSetpointF`. +- In editors, display values in the preferred unit but convert submitted values to Fahrenheit. +- Rename local variables where useful: + - `temperatureF` for canonical values. + - `displayTemperature` for UI values. + +### Acceptance Criteria + +- Schedule cards, next-event labels, chart labels, setpoint editor, and alarm editor honor Celsius preference. +- Database writes remain Fahrenheit. +- Scheduler execution receives Fahrenheit. +- Existing Fahrenheit behavior is unchanged. + +### Tests + +- Add hook tests for schedule display conversion. +- Add mutation tests proving Celsius display input writes Fahrenheit. +- Keep existing router tests for `unit=C`, or remove router display conversion later if it becomes unused. + +## Priority 4: Codify Automation and Diagnostics Units + +### Files + +- `src/automation/signals.biometrics.ts` +- `src/automation/backtest.ts` +- `src/components/Autopilot/*` +- `src/server/routers/health.ts` +- `src/components/diagnostics/diagnosticsLogic.ts` +- `src/components/diagnostics/ThermalTrendChart.tsx` + +### Current State + +Automation and thermal diagnostics use Fahrenheit. This appears intentional because automation actions target hardware setpoints. + +### Action + +- Add a short code comment or type alias documenting that automation temperature signals are Fahrenheit. +- Use shared Fahrenheit formatting helpers instead of inline strings like `${value}°F`. +- Decide whether Autopilot builder labels should honor user preference. If not, label them as automation/setpoint units rather than general display units. + +### Acceptance Criteria + +- Automation unit contract is explicit. +- Diagnostics hardcoded Fahrenheit is either documented as engineering telemetry or made preference-aware. +- Future UI code cannot accidentally treat automation values as user-preferred values. + +### Tests + +- Update existing Autopilot and diagnostics tests only where output labels change. + +## Priority 5: Strengthen Sensor Normalization Contract Tests + +### Files + +- `src/streaming/normalizeFrame.ts` +- `modules/common/dialect.py` +- `modules/environment-monitor/main.py` +- `src/streaming/tests/normalizeFrame.test.ts` +- `modules/common/test_dialect.py` + +### Current State + +TypeScript live-frame normalization and Python DB ingestion both normalize firmware dialects. They are conceptually aligned but can drift. + +### Action + +- Add shared fixture payloads for: + - `bedTemp` + - `bedTemp2` + - `frzTemp` + - sentinel values + - partial/missing zone arrays +- Verify TypeScript live normalization returns Celsius. +- Verify Python module normalization returns centidegrees Celsius for DB storage. +- Document that difference in the fixture test names. + +### Acceptance Criteria + +- A future firmware dialect change fails tests in both paths. +- Sentinel behavior is consistent between live UI and persisted DB data. + +## Priority 6: Consolidate Time and Schedule Helpers + +### Files + +- `src/scheduler/timeUtils.ts` +- `src/components/Header/Header.tsx` +- `src/hooks/useScheduleActive.ts` +- `src/components/Schedule/DaySelector.tsx` +- `src/components/Schedule/TimeInput.tsx` +- `src/lib/scheduleGrouping.ts` + +### Current Problem + +Time parsing, minutes conversion, day ordering, and night-window checks are spread across components and hooks. + +### Action + +- Move shared helpers into a non-React module: + - `hhmmToMinutes` + - `formatTime12h` + - `getCurrentDay` + - `isInWindow` + - `isInWindowForTimezone` + - `DAYS_OF_WEEK` +- Keep UI components importing helpers rather than defining their own. + +### Acceptance Criteria + +- `Header` uses the shared timezone-aware night-window helper. +- `useScheduleActive` uses shared day ordering and time parsing. +- `TimeInput` can remain a component, but its pure helpers live outside the component folder. + +### Tests + +- Add DST/wrapped-window tests for the shared helpers. +- Preserve existing `TimeInput` helper test coverage after moving functions. + +## Suggested Implementation Order + +1. Add the new temperature helper API and tests. +2. Refactor `useTemperatureUnit` to delegate to it. +3. Update primary temperature UI. +4. Update schedule UI and editor flows. +5. Codify automation/diagnostics unit contracts. +6. Add cross-path sensor normalization fixture tests. +7. Consolidate schedule/time helpers. + +## Quality Gates + +Run after each priority or before PR: + +```bash +pnpm lint +pnpm tsc +pnpm test +``` + +For Python module changes: + +```bash +python -m pytest modules +``` diff --git a/instrumentation.ts b/instrumentation.ts index 6cdc7e27..87adc627 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -16,6 +16,7 @@ */ import { getJobManager, shutdownJobManager } from '@/src/scheduler' +import { getAutomationEngine, shutdownAutomationEngine } from '@/src/automation' import { closeDatabase, closeBiometricsDatabase } from '@/src/db' import { startBiometricsRetention, stopBiometricsRetention } from '@/src/db/retention' import { getDacMonitor, shutdownDacMonitor } from '@/src/hardware/dacMonitor.instance' @@ -63,6 +64,14 @@ async function gracefulShutdown(signal: string): Promise { console.error('Error shutting down scheduler:', error) } + // Step 1a: Stop the Autopilot rules engine tick + try { + await shutdownAutomationEngine() + } + catch (error) { + console.error('Error shutting down automation engine:', error) + } + // Step 2: Shutdown piezo stream server try { await shutdownPiezoStreamServer() @@ -338,6 +347,15 @@ export async function initializeScheduler(): Promise { // Start biometrics time-series retention loop (non-blocking) startBiometricsRetention() + + // Boot the Autopilot rules engine beside the scheduler (non-blocking). + // Shares the same hardware path; no-op until automations are created. + getAutomationEngine().catch((error) => { + console.warn( + '[automation] engine failed to start:', + error instanceof Error ? error.message : error, + ) + }) } catch (error) { console.error('Failed to initialize job scheduler:', error) diff --git a/modules/biometrics-archiver/sleepypod-biometrics-pruner b/modules/biometrics-archiver/sleepypod-biometrics-pruner index 6677d4c5..3d577448 100755 --- a/modules/biometrics-archiver/sleepypod-biometrics-pruner +++ b/modules/biometrics-archiver/sleepypod-biometrics-pruner @@ -9,16 +9,32 @@ TARGET_USED_PCT="${TARGET_USED_PCT:-80}" mkdir -p "$ARCHIVE_DIR" +# Portable disk-usage read. `df --output=pcent` (and `df -h`) are GNU-only; +# pods can ship busybox df (see the sha256sum/busybox fallback in +# scripts/install), where `--output` is rejected → empty stdout → used_pct +# defaults to 0 → the loop never prunes and the archive grows unbounded. `df -P` +# is POSIX (busybox + GNU) and always prints one data row whose 5th field is +# the capacity percent. +disk_used_pct() { + local pct + pct=$(df -P /persistent | awk 'NR==2 {gsub(/%/,"",$5); print $5}') + echo "${pct:-0}" +} + pruned=0 while :; do - used_pct=$(df --output=pcent /persistent | tail -1 | tr -dc '0-9') - used_pct=${used_pct:-0} + used_pct=$(disk_used_pct) if [ "$used_pct" -le "$TARGET_USED_PCT" ]; then break fi - oldest=$(find "$ARCHIVE_DIR" -maxdepth 1 -type f -name '*.RAW.gz' -printf '%T@ %p\n' 2>/dev/null \ - | sort -n | head -1 | awk '{print $2}') + # Oldest archive by mtime. `ls -tr` (mtime order, oldest first) is + # busybox-safe; the old `find -printf` is GNU-only and silently no-ops on + # busybox find. `awk NR==1` rather than `head -1` consumes the whole stream — + # `head` closes the pipe early and, under `set -o pipefail`, aborts the + # script on SIGPIPE so it never prunes (archive grows unbounded, /persistent + # fills past target). + oldest=$(ls -1tr "$ARCHIVE_DIR"/*.RAW.gz 2>/dev/null | awk 'NR==1') if [ -z "$oldest" ]; then echo "pruner: $used_pct% used but archive is empty — nothing to prune" >&2 break @@ -28,5 +44,4 @@ while :; do pruned=$((pruned + 1)) done -used_pct=$(df --output=pcent /persistent | tail -1 | tr -dc '0-9') -echo "pruner: pruned=$pruned used_pct=$used_pct%" +echo "pruner: pruned=$pruned used_pct=$(disk_used_pct)%" diff --git a/modules/calibrator/main.py b/modules/calibrator/main.py index 25610c36..9bb78663 100644 --- a/modules/calibrator/main.py +++ b/modules/calibrator/main.py @@ -190,17 +190,29 @@ def run_calibration(store: CalibrationStore, side: str, sensor_type: str, return False -def should_run_daily(now: float, last_run: float) -> bool: +def should_run_daily(store: CalibrationStore, now: float, last_run: float) -> bool: """Fallback daily calibration if scheduler trigger didn't fire. The primary trigger is the jobManager's pre-prime-calibration job (30min before pod priming). This fallback only fires if no calibration has run in the last 25 hours — covers the case where priming is disabled. + + Gated on PERSISTED profile age, not just the in-memory last_run: + last_run starts at 0 on every process start, so without the persisted + check every restart re-ran a full recalibration. Also restricted to the + DAILY_HOUR UTC window so the fallback fires at the configured quiet hour + instead of whenever the process happens to (re)start. """ if now - last_run < 25 * 3600: return False - # Check if any profile is older than 25h - return True + if time.gmtime(now).tm_hour != DAILY_HOUR: + return False + for side in ("left", "right"): + for sensor_type in ("capacitance", "piezo", "temperature"): + age = store.get_profile_age_hours(side, sensor_type) + if age is None or age >= 25: + return True + return False # --------------------------------------------------------------------------- @@ -246,7 +258,7 @@ def main() -> None: # Check daily schedule now = time.time() - if should_run_daily(now, daily_last_run): + if should_run_daily(store, now, daily_last_run): log.info("Running daily calibration") for side in ("left", "right"): for st in ("capacitance", "piezo", "temperature"): diff --git a/modules/calibrator/test_main.py b/modules/calibrator/test_main.py new file mode 100644 index 00000000..02154cfe --- /dev/null +++ b/modules/calibrator/test_main.py @@ -0,0 +1,87 @@ +"""Tests for the calibrator's daily-run gate. + +Regression: should_run_daily used only the in-memory last_run (0.0 at process +start) and ignored DAILY_HOUR, so every process restart triggered a full +recalibration regardless of how fresh the persisted profiles were. +""" + +import calendar +import sys +import time + +# Stub pod-only imports so this test runs on a developer machine. +_cbor2_stub = type(sys)("cbor2") +sys.modules.setdefault("cbor2", _cbor2_stub) + +import main # noqa: E402 +from main import DAILY_HOUR, should_run_daily # noqa: E402 + + +def _ts_at_hour(hour: int) -> float: + """Epoch seconds for today at the given UTC hour.""" + t = time.gmtime() + return calendar.timegm((t.tm_year, t.tm_mon, t.tm_mday, hour, 30, 0, 0, 0, 0)) + + +class FakeStore: + def __init__(self, ages): + # ages: dict of (side, sensor_type) -> Optional[float] + self.ages = ages + + def get_profile_age_hours(self, side, sensor_type): + return self.ages.get((side, sensor_type), None) + + +def _fresh_ages(hours=1.0): + return { + (side, st): hours + for side in ("left", "right") + for st in ("capacitance", "piezo", "temperature") + } + + +class TestShouldRunDaily: + def test_fresh_profiles_do_not_rerun_on_restart(self): + """The core regression: last_run=0 (fresh process) + fresh persisted + profiles must NOT trigger a recalibration.""" + now = _ts_at_hour(DAILY_HOUR) + store = FakeStore(_fresh_ages(hours=2.0)) + assert should_run_daily(store, now, last_run=0.0) is False + + def test_stale_profiles_run_within_daily_hour(self): + now = _ts_at_hour(DAILY_HOUR) + store = FakeStore(_fresh_ages(hours=30.0)) + assert should_run_daily(store, now, last_run=0.0) is True + + def test_missing_profile_counts_as_stale(self): + now = _ts_at_hour(DAILY_HOUR) + ages = _fresh_ages(hours=1.0) + del ages[("right", "piezo")] + store = FakeStore(ages) + assert should_run_daily(store, now, last_run=0.0) is True + + def test_stale_profiles_wait_for_daily_hour(self): + """Outside the DAILY_HOUR window the fallback must not fire, even + with stale profiles — restarts at arbitrary times stay quiet.""" + other_hour = (DAILY_HOUR + 3) % 24 + now = _ts_at_hour(other_hour) + store = FakeStore(_fresh_ages(hours=30.0)) + assert should_run_daily(store, now, last_run=0.0) is False + + def test_recent_in_process_run_short_circuits(self): + """A run in the last 25h suppresses the fallback without consulting + the store.""" + now = _ts_at_hour(DAILY_HOUR) + + class ExplodingStore: + def get_profile_age_hours(self, side, sensor_type): + raise AssertionError("store must not be queried") + + assert should_run_daily(ExplodingStore(), now, last_run=now - 3600) is False + + def test_single_stale_sensor_triggers_run(self): + now = _ts_at_hour(DAILY_HOUR) + ages = _fresh_ages(hours=1.0) + ages[("left", "temperature")] = 26.0 + store = FakeStore(ages) + assert should_run_daily(store, now, last_run=0.0) is True diff --git a/modules/common/calibration.py b/modules/common/calibration.py index 2cf8a963..c08ac551 100644 --- a/modules/common/calibration.py +++ b/modules/common/calibration.py @@ -20,6 +20,7 @@ - Presence via noise floor: PMC6522616 (BCG adaptive thresholds) """ +import itertools import json import math import os @@ -235,7 +236,9 @@ def calibrate(self, records: list, side: str) -> CalibrationResult: best_variance = float("inf") window_samples = self.MIN_WINDOW_S # ~1 sample/sec for capSense - for i in range(len(timestamps) - window_samples): + # +1 so the final window is scanned (and a dataset exactly one window + # long yields one candidate instead of none) — matches CapSense2Calibrator. + for i in range(len(timestamps) - window_samples + 1): total_var = 0 for ch in self.CHANNELS: segment = channels[ch][i:i + window_samples] @@ -307,14 +310,21 @@ def calibrate(self, records: list, side: str) -> CalibrationResult: for rec in records: data = rec.get(side, {}) vals = data.get("values") if data else None - if not vals or len(vals) < 8: + # Newer Pod 4 / Pod 5 firmware emits 6-value capSense2 frames + # (3 sensing pairs, no REF pair). The REF pair (indices 6-7) is + # optional — mirror sleep-detector._extract_channel_values, which + # accepts >=6 and uses the REF pair only when present. + if not vals or len(vals) < 6: continue ts = float(rec.get("ts", 0)) timestamps.append(ts) for name, ia, ib in self.SENSE_PAIRS: channels[name].append((vals[ia] + vals[ib]) / 2.0) - rn, ria, rib = self.REF_PAIR - channels["REF"].append((vals[ria] + vals[rib]) / 2.0) + if len(vals) >= 8: + rn, ria, rib = self.REF_PAIR + channels["REF"].append((vals[ria] + vals[rib]) / 2.0) + else: + channels["REF"].append(None) # keep aligned with timestamps if len(timestamps) < 60: raise ValueError( @@ -349,11 +359,14 @@ def calibrate(self, records: list, side: str) -> CalibrationResult: std = max(std, self.MIN_STD) baseline["channels"][name] = {"mean": round(m, 4), "std": round(std, 4)} - # Store REF baseline for drift compensation - ref_seg = channels["REF"][best_start:window_end] - ref_mean = sum(ref_seg) / len(ref_seg) - ref_std = math.sqrt(sum((x - ref_mean) ** 2 for x in ref_seg) / len(ref_seg)) - baseline["ref"] = {"mean": round(ref_mean, 4), "std": round(max(ref_std, 0.001), 4)} + # Store REF baseline for drift compensation — only when the firmware + # actually emits the reference pair (8-value frames). Consumers treat + # `ref` as optional, so omitting it on 6-value firmware is safe. + ref_seg = [v for v in channels["REF"][best_start:window_end] if v is not None] + if ref_seg: + ref_mean = sum(ref_seg) / len(ref_seg) + ref_std = math.sqrt(sum((x - ref_mean) ** 2 for x in ref_seg) / len(ref_seg)) + baseline["ref"] = {"mean": round(ref_mean, 4), "std": round(max(ref_std, 0.001), 4)} # Quality: lower variance = better baseline # capSense2 floats are ~10-30 range, so normalize differently than capSense ints @@ -729,7 +742,9 @@ def is_present_capsense2_calibrated( """ data = record.get(side, {}) vals = data.get("values") if data else None - if not vals or len(vals) < 8: + # Accept 6-value frames (newer firmware drops the optional REF pair). + # Only indices 0-5 (A/B/C sensing pairs) are used below. + if not vals or len(vals) < 6: return False if baselines is None or baselines.get("format") != "capSense2": @@ -782,6 +797,11 @@ def check_trigger(self) -> Optional[dict]: if not triggers: return None data = json.loads(triggers[0].read_text()) + if not isinstance(data, dict): + # Valid JSON but not an object — consumers call .get() on it + # and would crash-loop on the same file after every restart. + triggers[0].unlink(missing_ok=True) + return None return data except (json.JSONDecodeError, OSError): # Corrupt trigger file — remove it @@ -803,14 +823,31 @@ def clear_trigger(self) -> None: pass +_trigger_seq = itertools.count() + + def write_trigger_atomic(payload: dict) -> None: """Write a calibration trigger file atomically. - Uses write-to-tmp-then-rename to prevent partial reads. - Each trigger gets a unique filename to support queuing. + Uses write-to-tmp-then-rename to prevent partial reads. The + pid + counter uniquifier keeps concurrent writers from colliding on a + same-millisecond timestamp — and from sharing a tmp path (the old + with_suffix(".tmp") collapsed every trigger's tmp file to one name). + fsync of the file and its directory make the rename durable, so a power + cut can't leave a truncated trigger that check_trigger then deletes, + silently dropping the request. """ ts = int(time.time() * 1000) - target = TRIGGER_PATH.parent / f".calibrate-trigger.{ts}" - tmp = target.with_suffix(".tmp") - tmp.write_text(json.dumps(payload)) + unique = f"{ts}.{os.getpid()}.{next(_trigger_seq)}" + target = TRIGGER_PATH.parent / f".calibrate-trigger.{unique}" + tmp = TRIGGER_PATH.parent / f".calibrate-trigger.{unique}.tmp" + with open(tmp, "w", encoding="utf-8") as f: + f.write(json.dumps(payload)) + f.flush() + os.fsync(f.fileno()) tmp.rename(target) + dir_fd = os.open(str(TRIGGER_PATH.parent), os.O_RDONLY) + try: + os.fsync(dir_fd) + finally: + os.close(dir_fd) diff --git a/modules/common/raw_follower.py b/modules/common/raw_follower.py index 505746d2..dff410c1 100644 --- a/modules/common/raw_follower.py +++ b/modules/common/raw_follower.py @@ -88,10 +88,20 @@ def read_records(self): continue if latest != self._path: + # Open before closing the old handle: the file can rotate + # away between _find_latest() and open(), and an unhandled + # FileNotFoundError here used to kill the whole module. + try: + new_file = open(latest, "rb") + except OSError as e: + log.warning("RAW file %s vanished before open (%s); retrying", + latest.name, e) + time.sleep(self._poll_interval) + continue log.info("Switched to RAW file: %s", latest.name) if self._file: self._file.close() - self._file = open(latest, "rb") + self._file = new_file self._path = latest self._last_pos = 0 self._consecutive_failures = 0 diff --git a/modules/common/test_raw_follower.py b/modules/common/test_raw_follower.py index c798794c..eebb535c 100644 --- a/modules/common/test_raw_follower.py +++ b/modules/common/test_raw_follower.py @@ -200,3 +200,48 @@ def fake_read(f): assert skip_to >= len(corrupt), \ "corruption recovery must clear a 1 KiB corrupt region in one step" follower._file.close() + + +class TestRotationRace: + """The RAW file can rotate away between _find_latest() and open(); the + resulting FileNotFoundError must not propagate (it killed the whole + module via sys.exit(1) in every consumer).""" + + def test_vanished_file_is_retried_not_fatal(self, monkeypatch, tmp_path): + import common.raw_follower as rf_mod + + real_path = tmp_path / "DATA.RAW" + real_path.write_bytes(b"\x00") + ghost_path = tmp_path / "GHOST.RAW" # never created on disk + + # First poll returns a path that no longer exists (rotated away); + # second returns the real file. + finds = iter([ghost_path, real_path]) + monkeypatch.setattr( + rf_mod.RawFileFollower, "_find_latest", + lambda self: next(finds, real_path), + ) + monkeypatch.setattr(rf_mod.time, "sleep", lambda *_a, **_k: None) + + records = iter([b"payload"]) + + def fake_read(f): + try: + return next(records) + except StopIteration: + event.set() # stop the loop after the first record + raise EOFError + + monkeypatch.setattr(rf_mod, "read_raw_record", fake_read) + monkeypatch.setattr(rf_mod.cbor2, "loads", lambda b: {"decoded": b}, raising=False) + + event = threading.Event() + follower = rf_mod.RawFileFollower(tmp_path, event) + + # Must not raise FileNotFoundError; must switch to the real file and + # yield its record. + record = next(follower.read_records()) + assert record == {"decoded": b"payload"} + assert follower._path == real_path + if follower._file: + follower._file.close() diff --git a/modules/environment-monitor/main.py b/modules/environment-monitor/main.py index 9e81c18d..e408aeb6 100644 --- a/modules/environment-monitor/main.py +++ b/modules/environment-monitor/main.py @@ -16,6 +16,7 @@ import os import sys +import math import time import signal import logging @@ -48,6 +49,36 @@ # Write at most once per 60s per record type DOWNSAMPLE_INTERVAL_S = 60 +# Timestamp sanity window (mirrors sleep-detector's sanitize_ts) +MIN_VALID_WALL_CLOCK_TS = 1577836800.0 # 2020-01-01 00:00:00 UTC +MAX_FUTURE_SKEW_S = 60.0 + + +def sanitize_ts(raw_ts) -> float: + """Coerce a RAW frame's `ts` field into a sane wall-clock timestamp. + + The downsample cursors seed from MAX(timestamp) at startup, so a single + far-future timestamp written to the DB would permanently block all + subsequent writes — surviving restarts. Falls back to time.time() when: + - the field is missing or not a number + - the value is NaN or +/-inf (CBOR-encoded IEEE 754 specials) + - the value is < 2020-01-01 epoch (firmware emitted a relative + timestamp before establishing wall-clock) + - the value is more than MAX_FUTURE_SKEW_S in the future + """ + now = time.time() + try: + ts = float(raw_ts) if raw_ts is not None else now + except (TypeError, ValueError): + return now + if not math.isfinite(ts): + return now + if ts < MIN_VALID_WALL_CLOCK_TS: + return now + if ts > now + MAX_FUTURE_SKEW_S: + return now + return ts + # Hardware sentinel for "no sensor connected" NO_SENSOR = -327.68 # u16-domain sentinels emitted by freezer firmware on disconnected sensors. @@ -219,13 +250,16 @@ def main() -> None: db_conn = open_biometrics_db() follower = RawFileFollower(RAW_DATA_DIR, _shutdown, poll_interval=0.5) - # Seed cursors from DB so restarts don't replay already-ingested samples - last_bed_write = float( + # Seed cursors from DB so restarts don't replay already-ingested samples. + # Clamp to now so a DB already poisoned by a far-future timestamp (written + # before sanitize_ts existed) self-heals instead of blocking writes forever. + now = time.time() + last_bed_write = min(now, float( db_conn.execute("SELECT COALESCE(MAX(timestamp), 0) FROM bed_temp").fetchone()[0] or 0 - ) - last_frz_write = float( + )) + last_frz_write = min(now, float( db_conn.execute("SELECT COALESCE(MAX(timestamp), 0) FROM freezer_temp").fetchone()[0] or 0 - ) + )) report_health("healthy", "environment-monitor started") @@ -236,11 +270,7 @@ def main() -> None: rtype = record.get("type") if rtype not in ("bedTemp", "bedTemp2", "frzTemp"): continue - try: - ts = float(record.get("ts", time.time())) - except (TypeError, ValueError): - log.warning("Skipping record with invalid ts: %r", record.get("ts")) - continue + ts = sanitize_ts(record.get("ts")) if rtype in ("bedTemp", "bedTemp2"): if ts - last_bed_write >= DOWNSAMPLE_INTERVAL_S: diff --git a/modules/environment-monitor/test_main.py b/modules/environment-monitor/test_main.py index ea90bc08..b769da66 100644 --- a/modules/environment-monitor/test_main.py +++ b/modules/environment-monitor/test_main.py @@ -22,8 +22,11 @@ from main import ( # noqa: E402 _safe_freezer_centidegrees, + sanitize_ts, write_freezer_temp, write_bed_temp, + MAX_FUTURE_SKEW_S, + MIN_VALID_WALL_CLOCK_TS, NO_SENSOR, ) @@ -130,6 +133,52 @@ def test_out_of_range_inserts_null(self): assert rows[0][2] is None, "out-of-range heatsink must be NULL" +class TestSanitizeTs: + """Timestamp sanity gate: one far-future ts must not be able to poison + the MAX(timestamp)-seeded downsample cursors and block all writes.""" + + def test_valid_recent_ts_passes_through(self): + import time + ts = time.time() - 5 + assert sanitize_ts(ts) == ts + + def test_slight_future_within_skew_passes_through(self): + import time + ts = time.time() + MAX_FUTURE_SKEW_S / 2 + assert sanitize_ts(ts) == ts + + def test_far_future_falls_back_to_now(self): + import time + before = time.time() + result = sanitize_ts(before + 10 * 365 * 86400) + assert before <= result <= time.time() + 1 + + def test_pre_2020_falls_back_to_now(self): + import time + before = time.time() + result = sanitize_ts(MIN_VALID_WALL_CLOCK_TS - 1) + assert before <= result <= time.time() + 1 + + def test_missing_falls_back_to_now(self): + import time + before = time.time() + result = sanitize_ts(None) + assert before <= result <= time.time() + 1 + + def test_nan_and_inf_fall_back_to_now(self): + import time + for bad in (float("nan"), float("inf"), float("-inf")): + before = time.time() + result = sanitize_ts(bad) + assert before <= result <= time.time() + 1 + + def test_non_numeric_falls_back_to_now(self): + import time + before = time.time() + result = sanitize_ts("not-a-timestamp") + assert before <= result <= time.time() + 1 + + # NOTE: bed_temp sentinel filtering moved into common.dialect.normalize_bed_temp # (PR #486) — write_bed_temp now expects canonical centidegrees, not raw # firmware records. The pre-normalization sentinel test that lived here is no diff --git a/modules/piezo-processor/main.py b/modules/piezo-processor/main.py index 806ca099..b82cb663 100644 --- a/modules/piezo-processor/main.py +++ b/modules/piezo-processor/main.py @@ -118,49 +118,63 @@ def open_biometrics_db() -> sqlite3.Connection: # After this many consecutive write failures, discard the connection and # reopen on the next write attempt (#325). _DB_RECONNECT_THRESHOLD = 5 -_db_write_failures = 0 -_db_conn_ref: Optional[sqlite3.Connection] = None -def _replace_db_connection() -> Optional[sqlite3.Connection]: - """Close the current biometrics connection and open a fresh one. +class DBHolder: + """Shared mutable reference to the biometrics connection. - Returns the new connection, or None if reconnection failed. On failure the - caller should keep using the old handle so subsequent writes can retry. + Both SideProcessor instances point at the same DBHolder so that when one + side triggers a reconnect, the other automatically observes the new + connection on its next write. Holding raw sqlite3.Connection refs on each + processor orphaned the sibling after a reconnect — it kept writing to the + closed handle (the exact bug sleep-detector's DBHolder docstring warns + about). + + The consecutive-failure counter lives on the holder, making it + per-connection: failures observed against a stale handle can no longer + discard a healthy replacement. """ - global _db_conn_ref - old = _db_conn_ref + __slots__ = ("conn", "write_failures") + + def __init__(self, conn: sqlite3.Connection): + self.conn = conn + self.write_failures = 0 + + +def _reconnect_db(holder: "DBHolder") -> None: + """Open a fresh connection, swap into *holder*, then close the old one. + Open-first-then-swap means an open failure leaves the live handle in + place; the close happens after the swap so writers always see a valid + handle. Never raises.""" + old = holder.conn try: - if old is not None: - try: - old.close() - except sqlite3.Error: - pass - _db_conn_ref = open_biometrics_db() - log.info("Reopened biometrics DB connection after write failures") - return _db_conn_ref + new = open_biometrics_db() except sqlite3.Error as e: log.error("Failed to reopen biometrics DB: %s", e) - return None + return + holder.conn = new + log.info("Reopened biometrics DB connection after write failures") + try: + old.close() + except sqlite3.Error: + pass -def write_vitals(conn: sqlite3.Connection, side: str, ts: datetime, +def write_vitals(holder: "DBHolder", side: str, ts: datetime, heart_rate: Optional[float], hrv: Optional[float], breathing_rate: Optional[float], quality_score: float, flags: Optional[list] = None, - hr_raw: Optional[float] = None) -> tuple[sqlite3.Connection, bool]: + hr_raw: Optional[float] = None) -> bool: """Insert one vitals row + paired vitals_quality row. Logs and swallows sqlite3 errors so the main loop survives transient WAL/disk issues (#325). - After _DB_RECONNECT_THRESHOLD consecutive failures, the connection is - replaced and the new handle is returned. + After _DB_RECONNECT_THRESHOLD consecutive failures, the holder's + connection is replaced in place. - Returns (conn, wrote): caller must use the returned connection for - subsequent writes; `wrote` is True only when both inserts committed - so callers don't advance their downsample cursor on a failed write. + Returns True only when both inserts committed so callers don't advance + their downsample cursor on a failed write. """ - global _db_write_failures, _db_conn_ref - _db_conn_ref = conn + conn = holder.conn ts_unix = int(ts.timestamp()) now_unix = int(time.time()) flags_json = json.dumps(flags) if flags else None @@ -175,18 +189,16 @@ def write_vitals(conn: sqlite3.Connection, side: str, ts: datetime, "VALUES (?, ?, ?, ?, ?, ?, ?)", (cur.lastrowid, side, ts_unix, quality_score, flags_json, hr_raw, now_unix), ) - _db_write_failures = 0 - return conn, True + holder.write_failures = 0 + return True except sqlite3.Error as e: - _db_write_failures += 1 + holder.write_failures += 1 log.warning("write_vitals failed (%d consecutive): %s", - _db_write_failures, e) - if _db_write_failures >= _DB_RECONNECT_THRESHOLD: - new_conn = _replace_db_connection() - _db_write_failures = 0 - if new_conn is not None: - return new_conn, False - return conn, False + holder.write_failures, e) + if holder.write_failures >= _DB_RECONNECT_THRESHOLD: + _reconnect_db(holder) + holder.write_failures = 0 + return False def report_health(status: str, message: str) -> None: @@ -234,8 +246,10 @@ class FrzHealthPumpState: def __init__(self): self._pump_rpm = {"left": 0.0, "right": 0.0} - # monotonic seconds when each side's pump last turned off - self._pump_off_at = {"left": 0.0, "right": 0.0} + # monotonic seconds when each side's pump last turned off; -inf = + # never (0.0 would falsely assert the guard for the first + # PUMP_OFF_GUARD_S when time.monotonic() has a near-zero origin) + self._pump_off_at = {"left": float("-inf"), "right": float("-inf")} self._was_active = {"left": False, "right": False} def update(self, record: dict) -> None: @@ -511,12 +525,19 @@ class HRTracker: Bruser et al. 2011. """ + # After this many consecutive uncorrectable readings, re-seed history at + # the new level. A lone outlier must not poison the median, but a + # sustained shift (e.g. different occupant) previously froze the tracker: + # the stale median rejected every new reading forever. + FALLBACK_RESET_STREAK = 3 + def __init__(self, max_delta: float = 15.0, history_len: int = 5): # Bounded deque: only the last history_len entries are ever consulted, # so retaining the full list would leak ~1440 floats/day (#325). self.history: deque = deque(maxlen=history_len) self.max_delta = max_delta self.history_len = history_len + self._fallback_streak = 0 def update(self, hr_candidate: Optional[float], score: float) -> Optional[float]: @@ -533,6 +554,7 @@ def update(self, hr_candidate: Optional[float], # Gaussian consistency weight consistency = np.exp(-0.5 * (delta / self.max_delta) ** 2) if consistency > 0.3: + self._fallback_streak = 0 self.history.append(hr_candidate) return hr_candidate @@ -542,6 +564,7 @@ def update(self, hr_candidate: Optional[float], half_delta = abs(half_hr - recent) half_consistency = np.exp(-0.5 * (half_delta / self.max_delta) ** 2) if half_consistency > 0.3: + self._fallback_streak = 0 self.history.append(half_hr) return half_hr @@ -552,10 +575,19 @@ def update(self, hr_candidate: Optional[float], double_consistency = np.exp( -0.5 * (double_delta / self.max_delta) ** 2) if double_consistency > 0.3: + self._fallback_streak = 0 self.history.append(double_hr) return double_hr - # Accept anyway but don't poison history + # Accept anyway but keep a lone outlier out of history. A sustained + # streak of uncorrectable readings means the tracked level itself is + # stale (e.g. different occupant) — previously the frozen median then + # rejected every reading forever, so re-seed at the new level. + self._fallback_streak += 1 + if self._fallback_streak >= self.FALLBACK_RESET_STREAK: + self.history.clear() + self.history.append(hr_candidate) + self._fallback_streak = 0 return hr_candidate # --------------------------------------------------------------------------- @@ -765,10 +797,10 @@ def compute_hrv(samples: np.ndarray, # --------------------------------------------------------------------------- class SideProcessor: - def __init__(self, side: str, db_conn: sqlite3.Connection, + def __init__(self, side: str, db_holder: DBHolder, pump_state: Optional[FrzHealthPumpState] = None): self.side = side - self.db = db_conn + self.db_holder = db_holder self._hr_buf: deque = deque(maxlen=HR_WINDOW_S * SAMPLE_RATE) self._hrv_buf: deque = deque(maxlen=HRV_WINDOW_S * SAMPLE_RATE) self._br_buf: deque = deque(maxlen=BREATHING_WINDOW_S * SAMPLE_RATE) @@ -883,9 +915,9 @@ def _maybe_write(self) -> None: flags.append("no_br") if med_std < self._presence.enter_threshold: flags.append("low_signal") - self.db, wrote = write_vitals(self.db, self.side, ts, hr, hrv, br, - quality_score=quality, flags=flags or None, - hr_raw=hr_raw) + wrote = write_vitals(self.db_holder, self.side, ts, hr, hrv, br, + quality_score=quality, flags=flags or None, + hr_raw=hr_raw) log.info("vitals %s — HR=%.1f HRV=%.1f BR=%.1f q=%.2f", self.side, hr or 0, hrv or 0, br or 0, quality) # Only advance the downsample cursor when the write actually @@ -901,6 +933,21 @@ def _maybe_write(self) -> None: # Main loop # --------------------------------------------------------------------------- +def _int32_samples(buf) -> np.ndarray: + """Decode an int32 sample buffer, tolerating truncated tails. + + A RAW record cut mid-write can carry a payload whose length is not a + multiple of 4; np.frombuffer would raise ValueError and kill the module. + Truncate to whole samples instead. + """ + if not isinstance(buf, (bytes, bytearray, memoryview)): + return np.empty(0, dtype=np.int32) + usable = len(buf) - (len(buf) % 4) + if usable == 0: + return np.empty(0, dtype=np.int32) + return np.frombuffer(buf[:usable], dtype=np.int32) + + def main() -> None: log.info("Starting piezo-processor v2 (biometrics.db=%s)", BIOMETRICS_DB) @@ -908,11 +955,11 @@ def main() -> None: log.error("Biometrics DB directory does not exist: %s", BIOMETRICS_DB.parent) sys.exit(1) - db_conn = open_biometrics_db() + db_holder = DBHolder(open_biometrics_db()) pump_gate = PumpGate() pump_state = FrzHealthPumpState() - left = SideProcessor("left", db_conn, pump_state=pump_state) - right = SideProcessor("right", db_conn, pump_state=pump_state) + left = SideProcessor("left", db_holder, pump_state=pump_state) + right = SideProcessor("right", db_holder, pump_state=pump_state) left._other = right right._other = left follower = RawFileFollower(RAW_DATA_DIR, _shutdown, poll_interval=0.01) @@ -932,8 +979,8 @@ def main() -> None: continue # Each record contains ~500 int32 samples per channel - l_samples = np.frombuffer(record.get("left1", b""), dtype=np.int32) - r_samples = np.frombuffer(record.get("right1", b""), dtype=np.int32) + l_samples = _int32_samples(record.get("left1", b"")) + r_samples = _int32_samples(record.get("right1", b"")) if l_samples.size == 0 or r_samples.size == 0: continue @@ -950,7 +997,7 @@ def main() -> None: report_health("down", str(e)) sys.exit(1) finally: - db_conn.close() + db_holder.conn.close() log.info("Shutdown complete") # Only reached on clean shutdown (not via sys.exit) diff --git a/modules/piezo-processor/test_main.py b/modules/piezo-processor/test_main.py index 8357ff79..5dcbbc6c 100644 --- a/modules/piezo-processor/test_main.py +++ b/modules/piezo-processor/test_main.py @@ -639,6 +639,64 @@ def test_large_jump_still_returned(self): # The value is returned even though it's an outlier assert result == 200.0 + def test_sustained_shift_reseeds_instead_of_freezing(self): + """A streak of uncorrectable readings re-seeds the tracker (#4.9). + Previously the stale median rejected every new reading forever.""" + tracker = HRTracker() + for hr in [70.0, 71.0, 69.0, 70.5, 70.0]: + tracker.update(hr, 0.5) + + # Sustained genuine shift the harmonic correction can't explain + # (190: direct/half/double all inconsistent with the 70 median) + for _ in range(HRTracker.FALLBACK_RESET_STREAK): + tracker.update(190.0, 0.5) + + # Tracker has re-seeded at the new level… + assert 190.0 in tracker.history + # …and consistent readings near it are accepted into history again. + result = tracker.update(185.0, 0.5) + assert result == 185.0 + assert 185.0 in tracker.history + + def test_single_outlier_does_not_trigger_reseed(self): + """One wild reading between good ones must not reset the tracker.""" + tracker = HRTracker() + for hr in [70.0, 71.0, 69.0, 70.5, 70.0]: + tracker.update(hr, 0.5) + + tracker.update(200.0, 0.5) # outlier (streak 1) + tracker.update(70.0, 0.5) # consistent — streak resets + tracker.update(200.0, 0.5) # outlier (streak 1 again) + tracker.update(200.0, 0.5) # outlier (streak 2) + + assert 200.0 not in tracker.history + + +class TestInt32Samples: + """_int32_samples must tolerate truncated buffers (#4.7) — a RAW record + cut mid-write has len % 4 != 0 and np.frombuffer raised ValueError.""" + + def test_whole_samples_decoded(self): + import main + buf = np.array([1, -2, 3], dtype=np.int32).tobytes() + out = main._int32_samples(buf) + assert out.tolist() == [1, -2, 3] + + def test_truncated_tail_dropped(self): + import main + buf = np.array([1, -2, 3], dtype=np.int32).tobytes() + b"\x07\x07" + out = main._int32_samples(buf) + assert out.tolist() == [1, -2, 3] + + def test_sub_sample_buffer_is_empty(self): + import main + assert main._int32_samples(b"\x01\x02").size == 0 + + def test_non_bytes_is_empty(self): + import main + assert main._int32_samples(None).size == 0 + assert main._int32_samples(12345).size == 0 + def test_history_bounded_under_sustained_input(self): """HRTracker history must not grow unbounded — only the last history_len entries are ever consulted, so retaining more leaks @@ -865,12 +923,12 @@ def test_happy_path_inserts(self): from datetime import datetime, timezone import main conn = self._make_db() - main._db_write_failures = 0 - result_conn, wrote = main.write_vitals(conn, "left", - datetime.now(timezone.utc), - 70.0, 40.0, 15.0, - quality_score=0.9) - assert result_conn is conn + holder = main.DBHolder(conn) + wrote = main.write_vitals(holder, "left", + datetime.now(timezone.utc), + 70.0, 40.0, 15.0, + quality_score=0.9) + assert holder.conn is conn assert wrote is True rows = conn.execute("SELECT * FROM vitals").fetchall() assert len(rows) == 1 @@ -887,19 +945,20 @@ def __exit__(self, *a): return False def execute(self, *a, **k): raise sqlite3.OperationalError("disk I/O error") - main._db_write_failures = 0 + holder = main.DBHolder(BadConn()) # Should not raise - result_conn, wrote = main.write_vitals(BadConn(), "left", - datetime.now(timezone.utc), - 70.0, 40.0, 15.0, - quality_score=0.9) - # Returns the bad conn unchanged on the first failure, with wrote=False. - assert result_conn is not None + wrote = main.write_vitals(holder, "left", + datetime.now(timezone.utc), + 70.0, 40.0, 15.0, + quality_score=0.9) + # Keeps the bad conn on the first failure, with wrote=False. + assert holder.conn is not None assert wrote is False + assert holder.write_failures == 1 def test_reconnect_after_threshold(self, monkeypatch): - """After _DB_RECONNECT_THRESHOLD consecutive failures, the connection - is replaced via open_biometrics_db().""" + """After _DB_RECONNECT_THRESHOLD consecutive failures, the holder's + connection is replaced via open_biometrics_db().""" from datetime import datetime, timezone import sqlite3 import main @@ -919,22 +978,60 @@ def fake_open(): replaced.append(1) return self._make_db() - main._db_write_failures = 0 monkeypatch.setattr(main, "open_biometrics_db", fake_open) bad = BadConn() + holder = main.DBHolder(bad) ts = datetime.now(timezone.utc) - conn = bad for i in range(main._DB_RECONNECT_THRESHOLD): - conn, wrote = main.write_vitals(conn, "left", ts, 70.0, 40.0, 15.0, - quality_score=0.9) + wrote = main.write_vitals(holder, "left", ts, 70.0, 40.0, 15.0, + quality_score=0.9) assert wrote is False # After crossing the threshold the function must have reopened the DB assert len(replaced) == 1, \ "expected one reconnect after _DB_RECONNECT_THRESHOLD failures" + # The stale handle is closed and swapped out of the holder + assert bad.closed is True + assert holder.conn is not bad # Failure counter resets after reconnect - assert main._db_write_failures == 0 + assert holder.write_failures == 0 + + def test_sibling_processor_sees_reconnected_handle(self, monkeypatch): + """Regression: both SideProcessors share one DBHolder, so a reconnect + triggered by one side must be visible to the other. Previously each + side held a raw connection and the sibling kept writing to the closed + handle forever.""" + from datetime import datetime, timezone + import sqlite3 + import main + + class BadConn: + closed = False + def __enter__(self): return self + def __exit__(self, *a): return False + def execute(self, *a, **k): + raise sqlite3.OperationalError("disk full") + def close(self): + self.closed = True + + good = self._make_db() + monkeypatch.setattr(main, "open_biometrics_db", lambda: good) + + holder = main.DBHolder(BadConn()) + ts = datetime.now(timezone.utc) + # "Left side" fails through the threshold and triggers the reconnect + for _ in range(main._DB_RECONNECT_THRESHOLD): + main.write_vitals(holder, "left", ts, 70.0, 40.0, 15.0, + quality_score=0.9) + + # "Right side" writes through the same holder and must hit the fresh + # connection, not the closed one. + wrote = main.write_vitals(holder, "right", ts, 65.0, 35.0, 14.0, + quality_score=0.8) + assert wrote is True + rows = good.execute("SELECT side FROM vitals").fetchall() + assert rows == [("right",)] class TestSideProcessorAbsenceThrottle: @@ -947,7 +1044,7 @@ def test_last_write_advances_on_absence(self): the next ingest call does not immediately re-enter the heavy signal-processing path.""" np.random.seed(42) - proc = SideProcessor("left", db_conn=None) + proc = SideProcessor("left", db_holder=None) # Simulate extended absence — _last_write far in the past. proc._last_write = time.time() - 3600 # 1 hour ago assert time.time() - proc._last_write > VITALS_INTERVAL_S diff --git a/modules/sleep-detector/main.py b/modules/sleep-detector/main.py index bb2c31be..245f569c 100644 --- a/modules/sleep-detector/main.py +++ b/modules/sleep-detector/main.py @@ -84,6 +84,20 @@ ABSENCE_TIMEOUT_S = 120 # Minimum session length to record (filters out accidental detections) MIN_SESSION_S = 300 +# Presence debounce / hysteresis: a present<->absent flip is only committed +# after the new raw state has persisted for this many seconds. Without it, +# brief capSense dropouts (sub-second flaps at 2 Hz while the occupant is +# genuinely in bed) each split the session and bump times_exited_bed, yielding +# dozens of <15min fragments with 66-109 bogus exits overnight (pod 88 field +# debug 2026-06-10). Must be < ABSENCE_TIMEOUT_S so debounced absence still +# closes the session on a real bed-exit. +PRESENCE_DEBOUNCE_S = 30.0 +# Hard upper bound on a single session. Belt-and-suspenders against a session +# that never closes (e.g. presence flapping that historically kept resetting +# the absence timer, producing 2736/2850min runaway durations). A continuous +# presence span beyond this is force-closed at the cap rather than persisted as +# a multi-day sleep_record. +MAX_SESSION_S = 16 * 3600 # How often to write a movement row (seconds) MOVEMENT_INTERVAL_S = 60 # Fallback presence threshold when uncalibrated @@ -464,10 +478,16 @@ class PumpGateCapSense: def __init__(self): # Per-side pump RPM state from frzHealth records self._pump_rpm: Dict[str, float] = {"left": 0.0, "right": 0.0} - # Timestamp (monotonic) when pump last turned off — for guard period - self._pump_off_at: float = 0.0 - # Whether pump was active on previous check (for detecting pump-off transition) - self._was_pump_active: bool = False + # Per-side timestamp (monotonic) when that pump last turned off — + # for the guard period. Per-side because gating both beds on either + # pump under-counted the movement table during long pump runtimes; + # cross-side mechanical coupling is what Signal 2 (correlated + # ref-anomaly) exists to catch. -inf = never turned off (0.0 would + # falsely gate the first PUMP_GUARD_S after process start, since + # time.monotonic() has an arbitrary, possibly near-zero origin). + self._pump_off_at: Dict[str, float] = {"left": float("-inf"), "right": float("-inf")} + # Whether each pump was active on previous check (for detecting pump-off transition) + self._was_pump_active: Dict[str, bool] = {"left": False, "right": False} # Reference channel anomaly state self._ref_anomaly_active: bool = False @@ -522,12 +542,13 @@ def update_pump_state(self, record: dict) -> None: self._pump_rpm[side] = rpm - # Track pump-off transitions for guard period - pump_active = self._pump_rpm["left"] > 0 or self._pump_rpm["right"] > 0 - if self._was_pump_active and not pump_active: - # Pump just turned off — start guard period - self._pump_off_at = time.monotonic() - self._was_pump_active = pump_active + # Track per-side pump-off transitions for the guard period + for side in ("left", "right"): + pump_active = self._pump_rpm[side] > 0 + if self._was_pump_active[side] and not pump_active: + # This side's pump just turned off — start its guard period + self._pump_off_at[side] = time.monotonic() + self._was_pump_active[side] = pump_active def is_gated(self, record: dict, side: str, channel_deltas: Optional[List[float]] = None, @@ -544,12 +565,15 @@ def is_gated(self, record: dict, side: str, Returns True if the delta should be suppressed. """ - # Signal 1: frzHealth pump RPM - if self._pump_rpm["left"] > 0 or self._pump_rpm["right"] > 0: + # Signal 1: frzHealth pump RPM — this side's pump only. Gating both + # sides on either pump zeroed real movement on the idle side for the + # whole pump runtime; the other pump's mechanical coupling (if any) + # is caught by the correlated ref-anomaly check below. + if self._pump_rpm.get(side, 0.0) > 0: return True # Signal 3: Guard period (checked before ref anomaly since it's cheap) - if time.monotonic() - self._pump_off_at < PUMP_GUARD_S: + if time.monotonic() - self._pump_off_at.get(side, float("-inf")) < PUMP_GUARD_S: return True # Signal 2: Reference channel anomaly (capSense2 only) @@ -587,6 +611,13 @@ class SessionTracker: _interval_start: Optional[float] = None _was_present: bool = False _exit_count: int = 0 + # Debounced (committed) presence state and the ts at which it began. + # Distinct from the raw per-sample `present`: only flips after a candidate + # state survives PRESENCE_DEBOUNCE_S (see _apply_debounce). + _debounced_present: bool = False + _state_since: Optional[float] = None + _pending_state: Optional[bool] = None + _pending_since: Optional[float] = None _movement_buf: list = field(default_factory=list) _last_movement_write: float = field(default_factory=time.time) _prev_values: Optional[list] = None # previous sample's channel values @@ -639,31 +670,70 @@ def process(self, ts: float, record: dict) -> None: self._update(ts, present, delta) + def _apply_debounce(self, ts: float, raw_present: bool) -> bool: + """Fold the raw per-sample presence into the committed (debounced) + state. A flip is only committed once the differing raw state has + persisted for PRESENCE_DEBOUNCE_S; transient flaps are cancelled. + + On commit, _state_since is set to the ts at which the new state + actually began (the first differing sample), so interval edges and + the session entry time reflect the true transition, not commit time. + + Returns True iff the committed state flipped on this call. + """ + if raw_present == self._debounced_present: + # Raw agrees with the committed state — cancel any pending flip. + self._pending_state = None + self._pending_since = None + return False + + if self._pending_state != raw_present: + # New candidate differing from committed state — start its dwell. + self._pending_state = raw_present + self._pending_since = ts + return False + + if (self._pending_since is not None + and ts - self._pending_since >= PRESENCE_DEBOUNCE_S): + self._debounced_present = raw_present + self._state_since = self._pending_since + self._pending_state = None + self._pending_since = None + return True + + return False + def _update(self, ts: float, present: bool, movement: float) -> None: self._movement_buf.append(movement) self._flush_movement(ts) - if present: + # Debounce raw presence so brief capSense dropouts don't fragment the + # session or inflate times_exited_bed (pod 88 field debug 2026-06-10). + changed = self._apply_debounce(ts, present) + # Timestamp of the true transition when one just committed, else `ts`. + edge_ts = self._state_since if changed and self._state_since is not None else ts + + if self._debounced_present: if self._session_start is None: - # New session starts - self._session_start = datetime.fromtimestamp(ts, tz=timezone.utc) - self._interval_start = ts + # New session starts at the true entry time. + self._session_start = datetime.fromtimestamp(edge_ts, tz=timezone.utc) + self._interval_start = edge_ts self._was_present = True log.info("%s: session started at %s", self.side, self._session_start.isoformat()) - elif not self._was_present and self._interval_start is not None: + elif changed and self._interval_start is not None: # Returning from absence — close absent interval, open present interval - self._absent_intervals.append([self._interval_start, ts]) - self._interval_start = ts + self._absent_intervals.append([self._interval_start, edge_ts]) + self._interval_start = edge_ts self._last_present_ts = ts self._was_present = True else: - if self._was_present and self._interval_start is not None: - # Just went absent — close present interval, open absent interval - self._present_intervals.append([self._interval_start, ts]) - self._interval_start = ts + if changed and self._was_present and self._interval_start is not None: + # Confirmed bed-exit — close present interval, open absent interval + self._present_intervals.append([self._interval_start, edge_ts]) + self._interval_start = edge_ts self._exit_count += 1 self._was_present = False @@ -677,6 +747,16 @@ def _update(self, ts: float, present: bool, movement: float) -> None: close_ts = self._interval_start if self._interval_start is not None else self._last_present_ts self._close_session(close_ts) + # Safety net: force-close a runaway session that never reaches the + # absence timeout, capping sleep_duration_seconds at MAX_SESSION_S. + # Only when still debounced-present — once absent, the absence-timeout + # path above closes at the true exit edge, so the cap must not move + # left_bed_at forward to the cap timestamp. + if (self._session_start is not None + and self._debounced_present + and ts - self._session_start.timestamp() >= MAX_SESSION_S): + self._close_session(self._session_start.timestamp() + MAX_SESSION_S) + def _close_session(self, left_ts: float) -> None: if self._session_start is None: return @@ -721,6 +801,10 @@ def _close_session(self, left_ts: float) -> None: self._interval_start = None self._was_present = False self._exit_count = 0 + self._debounced_present = False + self._state_since = None + self._pending_state = None + self._pending_since = None self._prev_values = None # avoid stale delta on next session start self._movement_buf = [] # discard leftover deltas from session end self._epoch_scores.clear() diff --git a/modules/sleep-detector/test_main.py b/modules/sleep-detector/test_main.py index 6d166b13..2ad25d10 100644 --- a/modules/sleep-detector/test_main.py +++ b/modules/sleep-detector/test_main.py @@ -230,3 +230,134 @@ def test_reconnect_swaps_holder_observed_by_both_trackers(self, monkeypatch): assert holder.conn is replacement # The original closed-handle is no longer referenced by the holder, so # any tracker reading from holder.conn observes the live connection. + + +class TestPumpGatePerSide: + """Gating both beds whenever EITHER pump ran zeroed real movement on the + idle side for the whole pump runtime, under-counting the movement table. + Signal 1 (RPM) and Signal 3 (guard period) are now per-side; cross-side + mechanical coupling remains covered by Signal 2 (correlated ref-anomaly).""" + + def _frz(self, left_rpm, right_rpm): + return { + "type": "frzHealth", + "left": {"pumpRpm": left_rpm}, + "right": {"pumpRpm": right_rpm}, + } + + def test_only_running_side_is_gated(self): + gate = main.PumpGateCapSense() + gate.update_pump_state(self._frz(left_rpm=3000, right_rpm=0)) + + assert gate.is_gated({}, "left") is True + assert gate.is_gated({}, "right") is False + + def test_both_sides_gated_when_both_pumps_run(self): + gate = main.PumpGateCapSense() + gate.update_pump_state(self._frz(left_rpm=3000, right_rpm=2800)) + + assert gate.is_gated({}, "left") is True + assert gate.is_gated({}, "right") is True + + def test_guard_period_applies_per_side(self): + gate = main.PumpGateCapSense() + gate.update_pump_state(self._frz(left_rpm=3000, right_rpm=0)) + # Left pump turns off → left enters its guard period; right never ran. + gate.update_pump_state(self._frz(left_rpm=0, right_rpm=0)) + + assert gate.is_gated({}, "left") is True, "guard period must gate the side that ran" + assert gate.is_gated({}, "right") is False, "idle side must not inherit the guard" + + def test_no_pumps_no_gate(self): + gate = main.PumpGateCapSense() + gate.update_pump_state(self._frz(left_rpm=0, right_rpm=0)) + + assert gate.is_gated({}, "left") is False + assert gate.is_gated({}, "right") is False + + +def _tracker(): + """A SessionTracker wired to an in-memory DB. calibration/pump_gate are + unused by _update, so None is sufficient for presence/session tests.""" + holder = main.DBHolder(_make_db()) + main._db_write_failures = 0 + return main.SessionTracker(side="left", db=holder, + calibration=None, pump_gate=None) + + +def _feed(t, samples): + """Feed (ts, present) pairs through _update with zero movement.""" + for ts, present in samples: + t._update(ts, present, 0.0) + + +def _rows(t): + return t.db.conn.execute( + "SELECT sleep_duration_seconds, times_exited_bed FROM sleep_records" + ).fetchall() + + +class TestPresenceDebounce: + """Pod 88 field debug 2026-06-10: brief capSense dropouts fragmented one + overnight presence span into dozens of <15min sleep_records with 66-109 + bogus bed-exits and runaway durations. Presence is now debounced.""" + + def test_brief_dropout_does_not_increment_exit_or_split_session(self): + t = _tracker() + base = 1_777_000_000.0 + samples = [] + # Establish committed presence (sustain past PRESENCE_DEBOUNCE_S). + samples += [(base, True), (base + 31, True)] + # 8h in bed at 2 Hz would be huge; sample sparsely but inject many + # sub-debounce dropouts — each a single absent sample immediately + # followed by present. None should commit a flip. + ts = base + 31 + for _ in range(50): + ts += 60 + samples.append((ts, False)) # brief dropout + samples.append((ts + 1, True)) # back within 1s — under debounce + # Real morning exit: sustained absence past debounce + absence timeout. + exit_ts = ts + 3600 + samples.append((exit_ts, False)) + samples.append((exit_ts + 31, False)) # commits absent flip → 1 exit + samples.append((exit_ts + 200, False)) # > ABSENCE_TIMEOUT_S → close + _feed(t, samples) + + rows = _rows(t) + assert len(rows) == 1 + duration_s, exits = rows[0] + assert exits == 1 + # One continuous span: duration ~ (exit_ts - base), well over an hour. + assert duration_s >= 3600 + + def test_sustained_absence_counts_single_exit(self): + t = _tracker() + base = 1_777_000_000.0 + samples = [(base, True), (base + 31, True)] + # Genuine bed-exit: absence sustained past debounce, then past the + # absence timeout so the session closes on that one exit. + leave = base + 4000 + samples += [(leave, False), (leave + 31, False), (leave + 200, False)] + _feed(t, samples) + + rows = _rows(t) + assert len(rows) == 1 + _duration, exits = rows[0] + assert exits == 1 + + def test_runaway_session_is_capped(self): + t = _tracker() + base = 1_777_000_000.0 + samples = [(base, True), (base + 31, True)] + # Presence that never goes absent for > MAX_SESSION_S of wall-clock. + ts = base + 31 + while ts < base + main.MAX_SESSION_S + 7200: + ts += 600 + samples.append((ts, True)) + _feed(t, samples) + + rows = _rows(t) + assert len(rows) >= 1 + # No row exceeds the hard cap. + for duration_s, _exits in rows: + assert duration_s <= main.MAX_SESSION_S diff --git a/package.json b/package.json index 07362e1d..ab7c46a7 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "@types/react-dom": "^19.2.3", "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^5.1.4", - "@vitest/coverage-v8": "4.1.7", + "@vitest/coverage-v8": "4.1.9", "conventional-changelog-conventionalcommits": "^9.1.0", "drizzle-kit": "^0.31.9", "eslint": "^9.39.3", @@ -117,14 +117,14 @@ "jsdom": "^29.0.0", "lint-staged": "^16.4.0", "semantic-release": "^25.0.3", - "tailwindcss": "4.3.0", + "tailwindcss": "4.3.2", "tsx": "^4.21.0", - "typescript": "^5.9.3", + "typescript": "^6.0.0", "typescript-eslint": "^8.56.1", "vite-tsconfig-paths": "^6.1.1", "vitest": "^4.0.18" }, - "packageManager": "pnpm@10.34.1", + "packageManager": "pnpm@10.34.4", "pnpm": { "onlyBuiltDependencies": [ "cbor-extract", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08d02052..b7940609 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,49 +10,49 @@ importers: dependencies: '@ajayche/trpc-panel': specifier: ^2.0.4 - version: 2.1.0(@trpc/server@11.17.0(typescript@5.9.3))(@types/react@19.2.15)(next@16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3) + version: 2.1.0(@trpc/server@11.18.0(typescript@6.0.3))(@types/react@19.2.17)(next@16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) '@base-ui/react': specifier: ^1.2.0 - version: 1.5.0(@date-fns/tz@1.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 1.6.0(@date-fns/tz@1.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@formatjs/intl-localematcher': specifier: ^0.8.0 - version: 0.8.9 + version: 0.8.10 '@lingui/core': specifier: ^5.9.2 - version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0) + version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0) '@lingui/macro': specifier: ^5.9.2 - version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0)(react@19.2.6) + version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0)(react@19.2.7) '@lingui/react': specifier: ^5.9.2 - version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0)(react@19.2.6) + version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0)(react@19.2.7) '@tanstack/react-query': specifier: ^5.90.21 - version: 5.100.14(react@19.2.6) + version: 5.101.2(react@19.2.7) '@trpc/client': specifier: ^11.10.0 - version: 11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3) + version: 11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3) '@trpc/next': specifier: ^11.10.0 - version: 11.17.0(@tanstack/react-query@5.100.14(react@19.2.6))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.17.0(@tanstack/react-query@5.100.14(react@19.2.6))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.17.0(typescript@5.9.3))(react@19.2.6)(typescript@5.9.3))(@trpc/server@11.17.0(typescript@5.9.3))(next@16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3) + version: 11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/react-query@11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(next@16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) '@trpc/react-query': specifier: ^11.10.0 - version: 11.17.0(@tanstack/react-query@5.100.14(react@19.2.6))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.17.0(typescript@5.9.3))(react@19.2.6)(typescript@5.9.3) + version: 11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3) '@trpc/server': specifier: ^11.10.0 - version: 11.17.0(typescript@5.9.3) + version: 11.18.0(typescript@6.0.3) '@xyflow/react': specifier: ^12.10.1 - version: 12.10.2(@types/react@19.2.15)(immer@11.1.4)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 12.11.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(immer@11.1.11)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) better-sqlite3: specifier: ^12.6.2 - version: 12.10.0 + version: 12.11.1 binary-split: specifier: ^1.0.5 version: 1.0.5 bonjour-service: specifier: ^1.3.0 - version: 1.4.0 + version: 1.4.2 cbor-x: specifier: ^1.6.0 version: 1.6.4 @@ -67,13 +67,13 @@ importers: version: 17.4.2 drizzle-orm: specifier: ^0.45.1 - version: 0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@12.10.0) + version: 0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1) hap-nodejs: specifier: 1.2.0 version: 1.2.0 lucide-react: specifier: ^1.0.0 - version: 1.17.0(react@19.2.6) + version: 1.23.0(react@19.2.7) mqtt: specifier: ^5.15.1 version: 5.15.1 @@ -82,7 +82,7 @@ importers: version: 1.0.0 next: specifier: ^16.1.6 - version: 16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) node-schedule: specifier: ^2.1.1 version: 2.1.1 @@ -91,16 +91,16 @@ importers: version: 1.5.4 react: specifier: ^19.2.4 - version: 19.2.6 + version: 19.2.7 react-dom: specifier: ^19.2.4 - version: 19.2.6(react@19.2.6) + version: 19.2.7(react@19.2.7) recharts: specifier: ^3.8.0 - version: 3.8.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.4)(react@19.2.6)(redux@5.0.1) + version: 3.9.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.4)(react@19.2.7)(redux@5.0.1) shadcn: specifier: ^4.0.0 - version: 4.8.2(@types/node@25.9.1)(babel-plugin-macros@3.1.0)(typescript@5.9.3) + version: 4.13.0(babel-plugin-macros@3.1.0)(typescript@6.0.3) superjson: specifier: ^2.2.6 version: 2.2.6 @@ -109,7 +109,7 @@ importers: version: 3.6.0 trpc-to-openapi: specifier: ^3.1.0 - version: 3.3.0(@trpc/server@11.17.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.4.3))(zod@4.4.3) + version: 3.3.0(@trpc/server@11.18.0(typescript@6.0.3))(zod-openapi@5.4.6(zod@4.4.3))(zod@4.4.3) tw-animate-css: specifier: ^1.4.0 version: 1.4.0 @@ -128,10 +128,10 @@ importers: version: 7.29.7(@babel/core@7.29.7) '@codecov/nextjs-webpack-plugin': specifier: ^2.0.1 - version: 2.0.1(next@16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(webpack@5.104.1(lightningcss@1.32.0)) + version: 2.0.1(next@16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(webpack@5.104.1(lightningcss@1.32.0)) '@eslint/css': specifier: ^1.0.0 - version: 1.3.0 + version: 1.4.0 '@eslint/js': specifier: ^9.39.3 version: 9.39.4 @@ -140,46 +140,46 @@ importers: version: 1.2.0 '@lingui/babel-plugin-lingui-macro': specifier: ^5.9.2 - version: 5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3) + version: 5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3) '@lingui/cli': specifier: ^5.9.2 - version: 5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3) + version: 5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3) '@lingui/loader': specifier: ^5.9.2 - version: 5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3)(webpack@5.104.1(lightningcss@1.32.0)) + version: 5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3)(webpack@5.104.1(lightningcss@1.32.0)) '@lingui/swc-plugin': specifier: ^5.11.0 - version: 5.11.0(@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0))(next@16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)) + version: 5.11.0(@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0))(next@16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) '@lingui/vite-plugin': specifier: ^5.9.2 - version: 5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3)(vite@7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) + version: 5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3)(vite@7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0)) '@semantic-release/commit-analyzer': specifier: ^13.0.1 - version: 13.0.1(semantic-release@25.0.3(typescript@5.9.3)) + version: 13.0.1(semantic-release@25.0.5(typescript@6.0.3)) '@semantic-release/git': specifier: ^10.0.1 - version: 10.0.1(semantic-release@25.0.3(typescript@5.9.3)) + version: 10.0.1(semantic-release@25.0.5(typescript@6.0.3)) '@stryker-mutator/core': specifier: ^9.6.1 - version: 9.6.1(@types/node@25.9.1) + version: 9.6.1(@types/node@25.9.4) '@stryker-mutator/typescript-checker': specifier: ^9.6.1 - version: 9.6.1(@stryker-mutator/core@9.6.1(@types/node@25.9.1))(typescript@5.9.3) + version: 9.6.1(@stryker-mutator/core@9.6.1(@types/node@25.9.4))(typescript@6.0.3) '@stryker-mutator/vitest-runner': specifier: ^9.6.1 - version: 9.6.1(@stryker-mutator/core@9.6.1(@types/node@25.9.1))(vitest@4.1.7) + version: 9.6.1(@stryker-mutator/core@9.6.1(@types/node@25.9.4))(vitest@4.1.9) '@stylistic/eslint-plugin': specifier: ^5.9.0 version: 5.10.0(eslint@9.39.4(jiti@2.7.0)) '@tailwindcss/postcss': specifier: ^4.2.1 - version: 4.3.0 + version: 4.3.2 '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 '@testing-library/react': specifier: ^16.3.2 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tsconfig/next': specifier: ^2.0.5 version: 2.0.6 @@ -197,7 +197,7 @@ importers: version: 0.6.4 '@types/node': specifier: ^25.3.0 - version: 25.9.1 + version: 25.9.4 '@types/node-schedule': specifier: ^2.1.8 version: 2.1.8 @@ -206,19 +206,19 @@ importers: version: 1.5.6 '@types/react': specifier: ^19.2.14 - version: 19.2.15 + version: 19.2.17 '@types/react-dom': specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.15) + version: 19.2.3(@types/react@19.2.17) '@types/ws': specifier: ^8.18.1 version: 8.18.1 '@vitejs/plugin-react': specifier: ^5.1.4 - version: 5.2.0(vite@7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) + version: 5.2.0(vite@7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0)) '@vitest/coverage-v8': - specifier: 4.1.7 - version: 4.1.7(vitest@4.1.7) + specifier: 4.1.9 + version: 4.1.9(vitest@4.1.9) conventional-changelog-conventionalcommits: specifier: ^9.1.0 version: 9.3.1 @@ -230,13 +230,13 @@ importers: version: 9.39.4(jiti@2.7.0) eslint-config-next: specifier: ^16.1.6 - version: 16.2.6(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + version: 16.2.10(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) eslint-plugin-react: specifier: ^7.37.5 version: 7.37.5(eslint@9.39.4(jiti@2.7.0)) globals: specifier: ^17.0.0 - version: 17.6.0 + version: 17.7.0 jiti: specifier: ^2.6.1 version: 2.7.0 @@ -248,55 +248,43 @@ importers: version: 16.4.0 semantic-release: specifier: ^25.0.3 - version: 25.0.3(typescript@5.9.3) + version: 25.0.5(typescript@6.0.3) tailwindcss: - specifier: 4.3.0 - version: 4.3.0 + specifier: 4.3.2 + version: 4.3.2 tsx: specifier: ^4.21.0 - version: 4.22.3 + version: 4.23.0 typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^6.0.0 + version: 6.0.3 typescript-eslint: specifier: ^8.56.1 - version: 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + version: 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) vite-tsconfig-paths: specifier: ^6.1.1 - version: 6.1.1(typescript@5.9.3)(vite@7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) + version: 6.1.1(typescript@6.0.3)(vite@7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0)) vitest: specifier: ^4.0.18 - version: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@25.9.1)(typescript@5.9.3))(vite@7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) + version: 4.1.9(@types/node@25.9.4)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@25.9.4)(typescript@6.0.3))(vite@7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0)) packages: - '@actions/core@2.0.1': - resolution: {integrity: sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg==} - '@actions/core@3.0.1': resolution: {integrity: sha512-a6d/Nwahm9fliVGRhdhofo40HjHQasUPusmc7vBfyky+7Z+P2A1J68zyFVaNcEclc/Se+eO595oAr5nwEIoIUA==} - '@actions/exec@2.0.0': - resolution: {integrity: sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==} - '@actions/exec@3.0.0': resolution: {integrity: sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==} '@actions/github@9.1.1': resolution: {integrity: sha512-tL5JbYOBZHc0ngEnCsaDcryUizIUIlQyIMwy1Wkx93H5HzbBJ7TbiPx2PnFjBwZW0Vh05JmfFZhecE6gglYegA==} - '@actions/http-client@3.0.0': - resolution: {integrity: sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ==} - '@actions/http-client@3.0.2': resolution: {integrity: sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==} '@actions/http-client@4.0.1': resolution: {integrity: sha512-+Nvd1ImaOZBSoPbsUtEhv+1z99H12xzncCkz0a3RuehINE81FZSe2QTj3uvAPTcJX/SCzUQHQ0D1GrPMbrPitg==} - '@actions/io@2.0.0': - resolution: {integrity: sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==} - '@actions/io@3.0.2': resolution: {integrity: sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==} @@ -575,8 +563,8 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@base-ui/react@1.5.0': - resolution: {integrity: sha512-z1gSAlced1yY+iM+mHDEtIkD8UI3Ebs52MuBPxvV6f5hRutk+xvCH/wuB7hDqDzK9JG5FoMz5nhrqtSs1wjt1A==} + '@base-ui/react@1.6.0': + resolution: {integrity: sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw==} engines: {node: '>=14.0.0'} peerDependencies: '@date-fns/tz': ^1.2.0 @@ -592,8 +580,8 @@ packages: date-fns: optional: true - '@base-ui/utils@0.2.9': - resolution: {integrity: sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw==} + '@base-ui/utils@0.3.1': + resolution: {integrity: sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg==} peerDependencies: '@types/react': ^17 || ^18 || ^19 react: ^17 || ^18 || ^19 @@ -700,25 +688,25 @@ packages: '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} - '@dotenvx/dotenvx@1.70.0': - resolution: {integrity: sha512-vC/rom87ym8HEyVdzZZS6/PYGg1Z5fmozUZ8l6cw1sYAxdL1lEyvE/JbK8cMFQoq3GsR/P1PiQRY+VXMtDN9bw==} + '@dotenvx/dotenvx@1.75.1': + resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==} hasBin: true + '@dotenvx/primitives@0.8.0': + resolution: {integrity: sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg==} + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} - '@ecies/ciphers@0.2.6': - resolution: {integrity: sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==} - engines: {bun: '>=1', deno: '>=2.7.10', node: '>=16'} - peerDependencies: - '@noble/ciphers': ^1.0.0 - '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} @@ -742,8 +730,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.28.0': - resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -766,8 +754,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.28.0': - resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -790,8 +778,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.28.0': - resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -814,8 +802,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.28.0': - resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -838,8 +826,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.28.0': - resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -862,8 +850,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.28.0': - resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -886,8 +874,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.28.0': - resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -910,8 +898,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.28.0': - resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -934,8 +922,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.28.0': - resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -958,8 +946,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.28.0': - resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -982,8 +970,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.28.0': - resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -1006,8 +994,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.28.0': - resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -1030,8 +1018,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.28.0': - resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -1054,8 +1042,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.28.0': - resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -1078,8 +1066,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.28.0': - resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -1102,8 +1090,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.28.0': - resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -1126,8 +1114,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.28.0': - resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -1144,8 +1132,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.28.0': - resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -1168,8 +1156,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.0': - resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -1186,8 +1174,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.28.0': - resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -1210,8 +1198,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.28.0': - resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -1228,8 +1216,8 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.28.0': - resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -1252,8 +1240,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.28.0': - resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -1276,8 +1264,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.28.0': - resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -1300,8 +1288,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.28.0': - resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -1324,8 +1312,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.28.0': - resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1364,8 +1352,8 @@ packages: resolution: {integrity: sha512-nxMparyhqVWQvadx9x8dIfubfIPOE+X2b2waua8fzdnM9vdp9rgVtwEZlG0TmCwEUz/d/f40fzvO/eqBwdxz0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/css@1.3.0': - resolution: {integrity: sha512-MwY657chvFQWtXmO86syZgD+JpWlzDq7VkKZyi65PwHDbhELQPMzPXh5s8rhrjptG6FCuls0puCmlXk66+14uA==} + '@eslint/css@1.4.0': + resolution: {integrity: sha512-OnRNKgnbX+tufPuZc2kOJS+v1iIARvfJHRNEAVwN77DBpojl+rGjEP8NKTL6pEZ7BR+HtwIuSSdrymEFlANGxg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/eslintrc@3.3.5': @@ -1405,10 +1393,6 @@ packages: '@noble/hashes': optional: true - '@fastify/busboy@2.1.1': - resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} - engines: {node: '>=14'} - '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -1424,11 +1408,11 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - '@formatjs/fast-memoize@3.1.5': - resolution: {integrity: sha512-KLi3fan6WnCHmigd9pmEEN8Hid0v4wiFBW576M/d07KMWYecf1CvyMI3n34vCmHT4AoVqG2n702kiHbXjzZX2A==} + '@formatjs/fast-memoize@3.1.6': + resolution: {integrity: sha512-H5aexk1Le7T9TPmscacZ+1pR6CTa2n1wq+HDVGXhH8TzUlQQpeXzZs91dRtmFHrbeNbjPFPfQujUqm7MHgVoXQ==} - '@formatjs/intl-localematcher@0.8.9': - resolution: {integrity: sha512-GmB0F/gYh4Hdl4rLWjgDsgT+x4pB54fkJeRh8kAZ4XFzKeCK8dGs+SBJWXO42QZtOUni+IDWKNuCw6wiL4lTvw==} + '@formatjs/intl-localematcher@0.8.10': + resolution: {integrity: sha512-P/IC3qws3jH+1fEs+o0RIFgXKRaQlFehjS5W0FPAqdo6hgzawLl+eD0q0JjheQ3XtoOe5n8WSYfX06KQZI/QJA==} '@hapi/bourne@3.0.0': resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} @@ -1938,75 +1922,70 @@ packages: resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} engines: {node: '>=18'} - '@napi-rs/wasm-runtime@0.2.12': - resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 - '@next/env@16.2.6': - resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} + '@next/env@16.2.10': + resolution: {integrity: sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==} - '@next/eslint-plugin-next@16.2.6': - resolution: {integrity: sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==} + '@next/eslint-plugin-next@16.2.10': + resolution: {integrity: sha512-Gs8D2m21VnJeFo9qvYIIqJH94frWerWYu41BprU1pLtRVF7PCQNLiFZZ3fG+iPuj3K83Cwv/rt+msLOy8Qgu3Q==} - '@next/swc-darwin-arm64@16.2.6': - resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==} + '@next/swc-darwin-arm64@16.2.10': + resolution: {integrity: sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.6': - resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==} + '@next/swc-darwin-x64@16.2.10': + resolution: {integrity: sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.6': - resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==} + '@next/swc-linux-arm64-gnu@16.2.10': + resolution: {integrity: sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.6': - resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} + '@next/swc-linux-arm64-musl@16.2.10': + resolution: {integrity: sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.6': - resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} + '@next/swc-linux-x64-gnu@16.2.10': + resolution: {integrity: sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.6': - resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} + '@next/swc-linux-x64-musl@16.2.10': + resolution: {integrity: sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.6': - resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} + '@next/swc-win32-arm64-msvc@16.2.10': + resolution: {integrity: sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.6': - resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==} + '@next/swc-win32-x64-msvc@16.2.10': + resolution: {integrity: sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@noble/ciphers@1.3.0': - resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} - engines: {node: ^14.21.3 || >=16} - - '@noble/curves@1.9.7': - resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} - engines: {node: ^14.21.3 || >=16} - '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -2039,6 +2018,10 @@ packages: resolution: {integrity: sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==} engines: {node: '>= 20'} + '@octokit/endpoint@11.0.3': + resolution: {integrity: sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==} + engines: {node: '>= 20'} + '@octokit/graphql@9.0.3': resolution: {integrity: sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==} engines: {node: '>= 20'} @@ -2058,8 +2041,8 @@ packages: peerDependencies: '@octokit/core': '>=6' - '@octokit/plugin-retry@8.0.3': - resolution: {integrity: sha512-vKGx1i3MC0za53IzYBSBXcrhmd+daQDzuZfYDd52X5S0M2otf3kVZTVP8bLA3EkU0lTvd1WEC2OlNNa4G+dohA==} + '@octokit/plugin-retry@8.1.0': + resolution: {integrity: sha512-O1FZgXeiGb2sowEr/hYTr6YunGdSAFWnr2fyW39Ah85H8O33ELASQxcvOFF5LE6Tjekcyu2ms4qAzJVhSaJxTw==} engines: {node: '>= 20'} peerDependencies: '@octokit/core': '>=7' @@ -2074,6 +2057,10 @@ packages: resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} engines: {node: '>= 20'} + '@octokit/request@10.0.10': + resolution: {integrity: sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w==} + engines: {node: '>= 20'} + '@octokit/request@10.0.7': resolution: {integrity: sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==} engines: {node: '>= 20'} @@ -2101,12 +2088,12 @@ packages: resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} engines: {node: '>=12.22.0'} - '@pnpm/npm-conf@2.3.1': - resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} + '@pnpm/npm-conf@3.0.3': + resolution: {integrity: sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==} engines: {node: '>=12'} - '@reduxjs/toolkit@2.11.2': - resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + '@reduxjs/toolkit@2.12.0': + resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 || ^19 react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 @@ -2119,98 +2106,98 @@ packages: '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} - '@rollup/rollup-android-arm-eabi@4.60.4': - resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.4': - resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.4': - resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.4': - resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.4': - resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.60.4': - resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': - resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.60.4': - resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.4': - resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.4': - resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.4': - resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.60.4': - resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.60.4': - resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.60.4': - resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.60.4': - resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.4': - resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.60.4': - resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} cpu: [s390x] os: [linux] libc: [glibc] @@ -2221,45 +2208,45 @@ packages: os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.4': - resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.4': - resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.4': - resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.60.4': - resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.4': - resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.4': - resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.4': - resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.4': - resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} cpu: [x64] os: [win32] @@ -2289,24 +2276,28 @@ packages: peerDependencies: semantic-release: '>=18.0.0' - '@semantic-release/github@12.0.2': - resolution: {integrity: sha512-qyqLS+aSGH1SfXIooBKjs7mvrv0deg8v+jemegfJg1kq6ji+GJV8CO08VJDEsvjp3O8XJmTTIAjjZbMzagzsdw==} + '@semantic-release/github@12.0.8': + resolution: {integrity: sha512-tej5AAgK5X9wHRoDmYhecMXEHEkFeGOY1XsEblKxu8pIQwahzf1STYyr7iPU6Lpbg6C5I3N2w/ocXrBo+L7jhw==} engines: {node: ^22.14.0 || >= 24.10.0} peerDependencies: semantic-release: '>=24.1.0' - '@semantic-release/npm@13.1.3': - resolution: {integrity: sha512-q7zreY8n9V0FIP1Cbu63D+lXtRAVAIWb30MH5U3TdrfXt6r2MIrWCY0whAImN53qNvSGp0Zt07U95K+Qp9GpEg==} + '@semantic-release/npm@13.1.5': + resolution: {integrity: sha512-Hq5UxzoatN3LHiq2rTsWS54nCdqJHlsssGERCo8WlvdfFA9LoN0vO+OuKVSjtNapIc/S8C2LBj206wKLHg62mg==} engines: {node: ^22.14.0 || >= 24.10.0} peerDependencies: semantic-release: '>=20.1.0' - '@semantic-release/release-notes-generator@14.1.0': - resolution: {integrity: sha512-CcyDRk7xq+ON/20YNR+1I/jP7BYKICr1uKd1HHpROSnnTdGqOTburi4jcRiTYz0cpfhxSloQO3cGhnoot7IEkA==} + '@semantic-release/release-notes-generator@14.1.1': + resolution: {integrity: sha512-Pbd2e2XRMUD0OxehHpgd5/YghsE76cddkRHSoDvKLK+OCy4Ewxn49rWR631MEUU01lgwF/uyVXvbnVuu6+Z6VA==} engines: {node: '>=20.8.1'} peerDependencies: semantic-release: '>=20.1.0' + '@simple-libs/stream-utils@1.2.0': + resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} + engines: {node: '>=18'} + '@sinclair/typebox@0.27.10': resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} @@ -2366,69 +2357,69 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@tailwindcss/node@4.3.0': - resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + '@tailwindcss/node@4.3.2': + resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} - '@tailwindcss/oxide-android-arm64@4.3.0': - resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + '@tailwindcss/oxide-android-arm64@4.3.2': + resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==} engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.3.0': - resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + '@tailwindcss/oxide-darwin-arm64@4.3.2': + resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.3.0': - resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + '@tailwindcss/oxide-darwin-x64@4.3.2': + resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.3.0': - resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + '@tailwindcss/oxide-freebsd-x64@4.3.2': + resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': - resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': + resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==} engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': - resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': + resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.3.0': - resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + '@tailwindcss/oxide-linux-arm64-musl@4.3.2': + resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.3.0': - resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + '@tailwindcss/oxide-linux-x64-gnu@4.3.2': + resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.3.0': - resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + '@tailwindcss/oxide-linux-x64-musl@4.3.2': + resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.3.0': - resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + '@tailwindcss/oxide-wasm32-wasi@4.3.2': + resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -2439,30 +2430,30 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': - resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': + resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.3.0': - resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + '@tailwindcss/oxide-win32-x64-msvc@4.3.2': + resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==} engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.3.0': - resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + '@tailwindcss/oxide@4.3.2': + resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==} engines: {node: '>= 20'} - '@tailwindcss/postcss@4.3.0': - resolution: {integrity: sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==} + '@tailwindcss/postcss@4.3.2': + resolution: {integrity: sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g==} - '@tanstack/query-core@5.100.14': - resolution: {integrity: sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==} + '@tanstack/query-core@5.101.2': + resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} - '@tanstack/react-query@5.100.14': - resolution: {integrity: sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==} + '@tanstack/react-query@5.101.2': + resolution: {integrity: sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==} peerDependencies: react: ^18 || ^19 @@ -2485,21 +2476,21 @@ packages: '@types/react-dom': optional: true - '@trpc/client@11.17.0': - resolution: {integrity: sha512-KpJBFrbKTDeVCFv/3ckL1XBBH5Yssn8hethI/rUy7GIpTj+VzjtPjykDqJpzobuVOz+d26cXCSu1t4I6MYI5Zg==} + '@trpc/client@11.18.0': + resolution: {integrity: sha512-wOqeg3Fvl25V1ZisQhUD3K8G60ZJDlSGJNSyeXrLH24xAo5w6GSR2Kzb1cSNY9Y+IQ2YZvYGZstBU+V/ulo/ow==} hasBin: true peerDependencies: - '@trpc/server': 11.17.0 + '@trpc/server': 11.18.0 typescript: '>=5.7.2' - '@trpc/next@11.17.0': - resolution: {integrity: sha512-EjwMkyamGjf5aslgfYB70hMKs4cgmsjSJbMinmuXktkGvbmdW54VbV04Sk4o4QTY5b//Rns9dz+ku8DN6lVhvw==} + '@trpc/next@11.18.0': + resolution: {integrity: sha512-ocwbruAWMGX9hY3HFg86X4jAcoF2v+xx+A2jDn72SbttRRG2hXR+XKPjrLc1dDJC0oi+/2DJEbL14+k1pyY5og==} hasBin: true peerDependencies: '@tanstack/react-query': ^5.59.15 - '@trpc/client': 11.17.0 - '@trpc/react-query': 11.17.0 - '@trpc/server': 11.17.0 + '@trpc/client': 11.18.0 + '@trpc/react-query': 11.18.0 + '@trpc/server': 11.18.0 next: '*' react: '>=16.8.0' react-dom: '>=16.8.0' @@ -2510,17 +2501,17 @@ packages: '@trpc/react-query': optional: true - '@trpc/react-query@11.17.0': - resolution: {integrity: sha512-AGcl5YAF8NnhBmyJ6PqJqKb1M5VTGSoNRNqJ3orct4o4epdcg0GWhW+qT9q6gPzs/2ImIwYCdfFpgNGdZ9yLHA==} + '@trpc/react-query@11.18.0': + resolution: {integrity: sha512-C1+Wwm2pCeUJucI+bnFpxGYjNuvV+ko1BC1T9tUxBVdrhHRCdn9ubxdevdLSAa49XRJRJiZnSuzl3Ys/yvs1vg==} peerDependencies: '@tanstack/react-query': ^5.80.3 - '@trpc/client': 11.17.0 - '@trpc/server': 11.17.0 + '@trpc/client': 11.18.0 + '@trpc/server': 11.18.0 react: '>=18.2.0' typescript: '>=5.7.2' - '@trpc/server@11.17.0': - resolution: {integrity: sha512-jbAOUe0PpUTCYqziyu+8vYXZdDXPudZgnEhWCQ2NjKnVEjfE93RqHTt1oycZJv/HNf51YlRXfEEwSIAbb161rw==} + '@trpc/server@11.18.0': + resolution: {integrity: sha512-JAvXOuNTxgXjIDfQaOvDq1j66LMNfDJUH1IU7Slfn8EvRv2EkH6ehu3A7zpYhjO0syHHiYg77v2lG2JFJgvw7Q==} hasBin: true peerDependencies: typescript: '>=5.7.2' @@ -2531,8 +2522,8 @@ packages: '@tsconfig/next@2.0.6': resolution: {integrity: sha512-C6eZoNyAMKv2UvzDZqcp8zXPviu6W4Ev1KUbqfmaMSZuq+tmbtTWaqxTif/yaui8WCEcRlQrHC8q9tNzBclHOw==} - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -2654,8 +2645,8 @@ packages: '@types/node-schedule@2.1.8': resolution: {integrity: sha512-k00g6Yj/oUg/CDC+MeLHUzu0+OFxWbIqrFfDiLi6OPKxTujvpv29mHGM8GtKr7B+9Vv92FcK/8mRqi1DK5f3hA==} - '@types/node@25.9.1': - resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@types/node@25.9.4': + resolution: {integrity: sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2671,8 +2662,8 @@ packages: peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.15': - resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} '@types/readable-stream@4.0.23': resolution: {integrity: sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==} @@ -2707,39 +2698,39 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.60.0': - resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==} + '@typescript-eslint/eslint-plugin@8.62.1': + resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.60.0 + '@typescript-eslint/parser': ^8.62.1 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.60.0': - resolution: {integrity: sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==} + '@typescript-eslint/parser@8.62.1': + resolution: {integrity: sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.60.0': - resolution: {integrity: sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==} + '@typescript-eslint/project-service@8.62.1': + resolution: {integrity: sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.60.0': - resolution: {integrity: sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==} + '@typescript-eslint/scope-manager@8.62.1': + resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.60.0': - resolution: {integrity: sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==} + '@typescript-eslint/tsconfig-utils@8.62.1': + resolution: {integrity: sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.60.0': - resolution: {integrity: sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==} + '@typescript-eslint/type-utils@8.62.1': + resolution: {integrity: sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2749,130 +2740,147 @@ packages: resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.60.0': - resolution: {integrity: sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==} + '@typescript-eslint/types@8.62.1': + resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.60.0': - resolution: {integrity: sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==} + '@typescript-eslint/typescript-estree@8.62.1': + resolution: {integrity: sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.60.0': - resolution: {integrity: sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==} + '@typescript-eslint/utils@8.62.1': + resolution: {integrity: sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.60.0': - resolution: {integrity: sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==} + '@typescript-eslint/visitor-keys@8.62.1': + resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} cpu: [arm] os: [android] - '@unrs/resolver-binding-android-arm64@1.11.1': - resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} cpu: [arm64] os: [android] - '@unrs/resolver-binding-darwin-arm64@1.11.1': - resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} cpu: [arm64] os: [darwin] - '@unrs/resolver-binding-darwin-x64@1.11.1': - resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} cpu: [x64] os: [darwin] - '@unrs/resolver-binding-freebsd-x64@1.11.1': - resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} cpu: [x64] os: [freebsd] - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} cpu: [arm64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} cpu: [arm64] os: [linux] libc: [musl] - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} cpu: [ppc64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} cpu: [riscv64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} cpu: [riscv64] os: [linux] libc: [musl] - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} cpu: [s390x] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} cpu: [x64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} cpu: [x64] os: [linux] libc: [musl] - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} cpu: [arm64] os: [win32] - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} cpu: [ia32] os: [win32] - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} cpu: [x64] os: [win32] @@ -2882,20 +2890,20 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@vitest/coverage-v8@4.1.7': - resolution: {integrity: sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==} + '@vitest/coverage-v8@4.1.9': + resolution: {integrity: sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==} peerDependencies: - '@vitest/browser': 4.1.7 - vitest: 4.1.7 + '@vitest/browser': 4.1.9 + vitest: 4.1.9 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.7': - resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} - '@vitest/mocker@4.1.7': - resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2905,20 +2913,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.7': - resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} - '@vitest/runner@4.1.7': - resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} - '@vitest/snapshot@4.1.7': - resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} - '@vitest/spy@4.1.7': - resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} - '@vitest/utils@4.1.7': - resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -2971,14 +2979,21 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - '@xyflow/react@12.10.2': - resolution: {integrity: sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==} + '@xyflow/react@12.11.1': + resolution: {integrity: sha512-L+zBoLGSXham0MnlY8QqjfR7/C5JNw0zxkaey5aZ5XmCgJBAdH4+WRIu8CR40d3l/BdU635V6YbhBK1jMo8/6Q==} peerDependencies: + '@types/react': '>=17' + '@types/react-dom': '>=17' react: '>=17' react-dom: '>=17' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@xyflow/system@0.0.76': - resolution: {integrity: sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==} + '@xyflow/system@0.0.78': + resolution: {integrity: sha512-lY0z2qP33fUhTva9Vaxrk0lqZta2pkbxB1trHAx1omnJqRtPvDlAQYV2r5fhS6AdpkulYmbNW0svy+A4/t4B/g==} abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} @@ -3004,9 +3019,14 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@9.0.0: + resolution: {integrity: sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==} + engines: {node: '>= 20'} aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} @@ -3054,8 +3074,8 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} - ansi-escapes@7.2.0: - resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} ansi-regex@5.0.1: @@ -3155,12 +3175,16 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + atomically@1.7.0: + resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} + engines: {node: '>=10.12.0'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axe-core@4.11.4: - resolution: {integrity: sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==} + axe-core@4.12.1: + resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} engines: {node: '>=4'} axobject-query@4.1.0: @@ -3184,21 +3208,16 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.29: - resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - baseline-browser-mapping@2.10.33: - resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} + baseline-browser-mapping@2.10.42: + resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} engines: {node: '>=6.0.0'} hasBin: true before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} - better-sqlite3@12.10.0: - resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==} + better-sqlite3@12.11.1: + resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} bidi-js@1.0.3: @@ -3220,15 +3239,15 @@ packages: bl@6.1.6: resolution: {integrity: sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==} - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} bonjour-hap@3.10.2: resolution: {integrity: sha512-37ZEbMSKZ/K4O8sK9YDVFKJ62ZqlK4OPXcBUYaBnVlqr6vQjYwIoFuOQL0ZhztkCNgyyiCExfN4FcZB5cS9t1A==} - bonjour-service@1.4.0: - resolution: {integrity: sha512-fGQtj1qdR9vIKjFiWPQd52qIqwjaYqhcI40JEiDuvlZ86E7ZBPBwY9fPgHy9r2rYGIjiRfctNPYz6OQU73ww2w==} + bonjour-service@1.4.2: + resolution: {integrity: sha512-lMskhnsW70yWHr4PhPeh2rvaIkLSaDpp+nmtbXBZaNKTXwxL73QOkW6HhbzqTImXjevn9TreGT4GACGBCGP9nQ==} bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} @@ -3247,8 +3266,8 @@ packages: broker-factory@3.1.14: resolution: {integrity: sha512-L45k5HMbPIrMid0nTOZ/UPXG/c0aRuQKVrSDFIb1zOkvfiyHgYmIjc3cSiN1KwQIvRDOtKE0tfb3I9EZ3CmpQQ==} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + browserslist@4.28.5: + resolution: {integrity: sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -3297,11 +3316,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001792: - resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} - - caniuse-lite@1.0.30001793: - resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + caniuse-lite@1.0.30001802: + resolution: {integrity: sha512-vmv8ub2xwTNmljSKf82mtCk5JH7hC+YgzLj3P5zotvA0tPQ9016tdNNOG8WRca1IxOnhSsivB+J0z5FeE5LOUw==} cbor-extract@2.2.2: resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} @@ -3496,6 +3512,10 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} + conf@10.2.0: + resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==} + engines: {node: '>=12'} + config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -3515,6 +3535,10 @@ packages: resolution: {integrity: sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w==} engines: {node: '>=18'} + conventional-changelog-angular@8.3.1: + resolution: {integrity: sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==} + engines: {node: '>=18'} + conventional-changelog-conventionalcommits@9.3.1: resolution: {integrity: sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==} engines: {node: '>=18'} @@ -3524,6 +3548,11 @@ packages: engines: {node: '>=18'} hasBin: true + conventional-changelog-writer@8.4.0: + resolution: {integrity: sha512-HHBFkk1EECxxmCi4CTu091iuDpQv5/OavuCUAuZmrkWpmYfyD816nom1CvtfXJ/uYfAAjavgHvXHX291tSLK8g==} + engines: {node: '>=18'} + hasBin: true + conventional-commits-filter@5.0.0: resolution: {integrity: sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==} engines: {node: '>=18'} @@ -3533,6 +3562,11 @@ packages: engines: {node: '>=18'} hasBin: true + conventional-commits-parser@6.4.0: + resolution: {integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==} + engines: {node: '>=18'} + hasBin: true + convert-hrtime@5.0.0: resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} engines: {node: '>=12'} @@ -3579,17 +3613,8 @@ packages: typescript: optional: true - cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - - cosmiconfig@9.0.1: - resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + cosmiconfig@9.0.2: + resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==} engines: {node: '>=14'} peerDependencies: typescript: '>=4.9.5' @@ -3693,10 +3718,6 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - data-urls@7.0.0: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -3716,6 +3737,10 @@ packages: date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + debounce-fn@4.0.0: + resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==} + engines: {node: '>=10'} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -3784,6 +3809,10 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} @@ -3845,6 +3874,10 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} + dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} @@ -3955,15 +3988,11 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - eciesjs@0.4.18: - resolution: {integrity: sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==} - engines: {bun: '>=1', deno: '>=2', node: '>=16'} - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.364: - resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} + electron-to-chromium@1.5.387: + resolution: {integrity: sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -3984,12 +4013,12 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.21.3: - resolution: {integrity: sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==} + enhanced-resolve@5.21.6: + resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} - enhanced-resolve@5.22.1: - resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + enhanced-resolve@5.24.2: + resolution: {integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -4038,10 +4067,17 @@ packages: es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-module-lexer@2.3.0: + resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} @@ -4054,8 +4090,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - es-toolkit@1.45.1: - resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + es-toolkit@1.49.0: + resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} @@ -4072,8 +4108,8 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.28.0: - resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -4096,8 +4132,8 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-config-next@16.2.6: - resolution: {integrity: sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==} + eslint-config-next@16.2.10: + resolution: {integrity: sha512-HSybLOY0QKf39i4FWUqPN0xWiNDi6A6UqJmZtgDkS3zMqjXTqULvj/sueXx3cdCG0mVG+qH6k5/qdegklH1d1w==} peerDependencies: eslint: '>=9.0.0' typescript: '>=3.3.1' @@ -4121,8 +4157,8 @@ packages: eslint-plugin-import-x: optional: true - eslint-module-utils@2.12.1: - resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + eslint-module-utils@2.14.0: + resolution: {integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -4335,8 +4371,8 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} fast-wrap-ansi@0.2.0: resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} @@ -4356,10 +4392,6 @@ packages: picomatch: optional: true - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - figures@2.0.0: resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} engines: {node: '>=4'} @@ -4391,6 +4423,10 @@ packages: resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} engines: {node: '>=4'} + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -4418,10 +4454,6 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -4430,17 +4462,14 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} - from2@2.3.0: - resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} - from@0.1.7: resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-extra@11.3.5: - resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} + fs-extra@11.3.6: + resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==} engines: {node: '>=14.14'} fsevents@2.3.3: @@ -4459,6 +4488,10 @@ packages: resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} engines: {node: '>= 0.4'} + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} @@ -4504,10 +4537,6 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-stream@7.0.1: - resolution: {integrity: sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ==} - engines: {node: '>=16'} - get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -4557,8 +4586,8 @@ packages: resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} engines: {node: '>=18'} - globals@17.6.0: - resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + globals@17.7.0: + resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} engines: {node: '>=18'} globalthis@1.0.4: @@ -4578,8 +4607,8 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphql@16.14.0: - resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==} + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} h3@1.15.11: @@ -4590,6 +4619,11 @@ packages: engines: {node: '>=0.4.7'} hasBin: true + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + hap-nodejs@1.2.0: resolution: {integrity: sha512-MusfG+9BJsLbjv7xtF5PDcYmgeYpbtchUhrvTvVBnXPoGMcPePhYt2bo7RphrOskzOpCuSJc8cOMTjJIlHwMNw==} engines: {node: ^18 || ^20 || ^22} @@ -4625,8 +4659,8 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hasown@2.0.3: - resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} hast-util-to-jsx-runtime@2.3.6: @@ -4655,8 +4689,8 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - hono@4.12.23: - resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} + hono@4.12.28: + resolution: {integrity: sha512-YwUvVpSF7m1yOblFPrU3Hbo8XhPheBoiyfGuII6z19LnOr6JpDnyyp7LFNrfV56wS8tpvtBFGRISHN02pDdLOA==} engines: {node: '>=16.9.0'} hook-std@4.0.0: @@ -4667,8 +4701,8 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} - hosted-git-info@9.0.2: - resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} + hosted-git-info@9.0.3: + resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} engines: {node: ^20.17.0 || >=22.9.0} html-encoding-sniffer@6.0.0: @@ -4685,13 +4719,13 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} + http-proxy-agent@9.1.0: + resolution: {integrity: sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==} + engines: {node: '>= 20'} - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} + https-proxy-agent@9.1.0: + resolution: {integrity: sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==} + engines: {node: '>= 20'} human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} @@ -4709,8 +4743,8 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} ieee754@1.2.1: @@ -4724,11 +4758,8 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - immer@10.2.0: - resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} - - immer@11.1.4: - resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + immer@11.1.11: + resolution: {integrity: sha512-qzXuyXAkPySAGYkfsAwodDPWT8Zm7/Uo5BNt4BjhMhG5WlWyZZ4wQqnWwdS8kjlQ1Cwu6gjw3A6+0gTQwlyYtw==} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} @@ -4781,10 +4812,6 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - into-stream@7.0.0: - resolution: {integrity: sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw==} - engines: {node: '>=12'} - ip-address@10.2.0: resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} @@ -4854,11 +4881,20 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -5012,6 +5048,10 @@ packages: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + is-wsl@3.1.1: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} @@ -5029,8 +5069,8 @@ packages: resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} engines: {node: '>=18'} - issue-parser@7.0.1: - resolution: {integrity: sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==} + issue-parser@7.0.2: + resolution: {integrity: sha512-7atWPjhGEIX3JEtMrOYd8TKzboYlq+5sNbdl9POiLYOI14G5HZiQbZP0Xj5EZdrufQVXfJlpTV0hys0CuxwxZw==} engines: {node: ^18.17 || >=20.6.1} istanbul-lib-coverage@3.2.2: @@ -5099,6 +5139,10 @@ packages: resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + jsdom@29.1.1: resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} @@ -5131,12 +5175,18 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@7.0.3: + resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==} + json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-with-bigint@3.5.8: + resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -5277,6 +5327,10 @@ packages: resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} engines: {node: '>=4'} + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -5347,11 +5401,15 @@ packages: resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} engines: {node: 20 || >=22} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@1.17.0: - resolution: {integrity: sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==} + lucide-react@1.23.0: + resolution: {integrity: sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -5369,8 +5427,8 @@ packages: magicast@0.5.2: resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} - make-asynchronous@1.0.1: - resolution: {integrity: sha512-T9BPOmEOhp6SmV25SwLVcHK4E6JyG/coH3C6F1NjNXSziv/fd4GmsqMk8YR6qpPOswfaOCApSNkZv6fxoaYFcQ==} + make-asynchronous@1.1.0: + resolution: {integrity: sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==} engines: {node: '>=18'} make-dir@4.0.0: @@ -5540,6 +5598,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-fn@3.1.0: + resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} + engines: {node: '>=8'} + mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} @@ -5630,8 +5692,8 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -5656,8 +5718,8 @@ packages: nerf-dart@1.0.0: resolution: {integrity: sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==} - next@16.2.6: - resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} + next@16.2.10: + resolution: {integrity: sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -5681,23 +5743,14 @@ packages: resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} engines: {node: '>=10'} - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - node-emoji@2.2.0: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} - node-exports-info@1.6.0: - resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + node-exports-info@1.6.2: + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} engines: {node: '>= 0.4'} - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-gyp-build-optional-packages@5.1.1: resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} hasBin: true @@ -5708,8 +5761,8 @@ packages: node-persist@0.0.12: resolution: {integrity: sha512-Fbia3FYnURzaql53wLu0t19dmAwQg/tXT6O7YPmdwNwysNKEyFmgoT2BQlPD3XXQnYeiQVNvR5lfvufGwKuxhg==} - node-releases@2.0.46: - resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + node-releases@2.0.50: + resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} engines: {node: '>=18'} node-schedule@2.1.1: @@ -5728,9 +5781,9 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-url@8.1.0: - resolution: {integrity: sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==} - engines: {node: '>=14.16'} + normalize-url@9.0.1: + resolution: {integrity: sha512-ARftfC5HdUNu9jJeL8pHj8debUIHA2b91FizCoMzY4lG6dDX13jdvTK0TBe24IBDRf2HvJSzzwEPvmbkQWHRSg==} + engines: {node: '>=20'} npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} @@ -5744,8 +5797,8 @@ packages: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} - npm@11.7.0: - resolution: {integrity: sha512-wiCZpv/41bIobCoJ31NStIWKfAxxYyD1iYnWCtiyns8s5v3+l8y0HCP/sScuH6B5+GhIfda4HQKiqeGZwJWhFw==} + npm@11.17.0: + resolution: {integrity: sha512-PurxiZexEHDTE4SSaLI3ZrnbAGiZfeyUcQcxcP5D+hfytNAze/D1IzDuInTn9XVLIbAQUnQuSPXJx02LHjLvQw==} engines: {node: ^20.17.0 || >=22.9.0} hasBin: true bundledDependencies: @@ -5765,7 +5818,6 @@ packages: - cacache - chalk - ci-info - - cli-columns - fastest-levenshtein - fs-minipass - glob @@ -5879,8 +5931,13 @@ packages: observable-fns@0.6.1: resolution: {integrity: sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==} - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + obug@2.1.2: + resolution: {integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==} + engines: {node: '>=12.20.0'} + + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -5905,6 +5962,10 @@ packages: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + openapi3-ts@4.4.0: resolution: {integrity: sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==} @@ -5939,10 +6000,6 @@ packages: resolution: {integrity: sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==} engines: {node: '>=18'} - p-is-promise@3.0.0: - resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==} - engines: {node: '>=8'} - p-limit@1.3.0: resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} engines: {node: '>=4'} @@ -5959,6 +6016,10 @@ packages: resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} engines: {node: '>=4'} + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -6093,6 +6154,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pify@3.0.0: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} @@ -6105,6 +6170,10 @@ packages: resolution: {integrity: sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==} engines: {node: '>=4'} + pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -6116,20 +6185,16 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-selector-parser@7.1.1: - resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + postcss-selector-parser@7.1.4: + resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} engines: {node: '>=4'} postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.14: - resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} - engines: {node: ^10 || ^12 || >=14} - - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} powershell-utils@0.1.0: @@ -6194,6 +6259,15 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-agent-negotiate@1.1.0: + resolution: {integrity: sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==} + engines: {node: '>= 20'} + peerDependencies: + kerberos: ^2.0.0 + peerDependenciesMeta: + kerberos: + optional: true + pseudolocale@2.2.0: resolution: {integrity: sha512-O+D2eU7fO9wVLqrohvt9V/9fwMadnJQ4jxwiK+LeNEqhMx8JYx4xQHkArDCJFAdPPOp/pQq6z5L37eBvAoc8jw==} engines: {node: '>=16.0.0'} @@ -6230,14 +6304,18 @@ packages: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} engines: {node: '>= 0.6'} raw-body@2.5.3: @@ -6255,10 +6333,10 @@ packages: react-base16-styling@0.10.0: resolution: {integrity: sha512-H1k2eFB6M45OaiRru3PBXkuCcn2qNmx+gzLb4a9IPMR7tMH8oBRXU5jGbPDYG1Hz+82d88ED0vjR8BmqU3pQdg==} - react-dom@19.2.6: - resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: - react: ^19.2.6 + react: ^19.2.7 react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -6281,8 +6359,8 @@ packages: '@types/react': '>=18' react: '>=18' - react-redux@9.2.0: - resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + react-redux@9.3.0: + resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} peerDependencies: '@types/react': ^18.2.25 || ^19 react: ^18.0 || ^19 @@ -6303,8 +6381,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react@19.2.6: - resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} read-package-up@11.0.0: @@ -6315,8 +6393,8 @@ packages: resolution: {integrity: sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==} engines: {node: '>=20'} - read-pkg@10.0.0: - resolution: {integrity: sha512-A70UlgfNdKI5NSvTTfHzLQj7NJRpJ4mT5tGafkllJ4wh71oYuGm/pzphHcmW4s35iox56KSK721AihodoXSc/A==} + read-pkg@10.1.0: + resolution: {integrity: sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==} engines: {node: '>=20'} read-pkg@9.0.1: @@ -6338,12 +6416,12 @@ packages: resolution: {integrity: sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==} engines: {node: '>=8.10.0'} - recast@0.23.11: - resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + recast@0.23.12: + resolution: {integrity: sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==} engines: {node: '>= 4'} - recharts@3.8.1: - resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} + recharts@3.9.1: + resolution: {integrity: sha512-WMcwlXcB7l+BbxiEdyClkG+1sxrMHNZpzT577LEvU4+rXPd8oTAy1wXk72hnk2KOOmxuLvw3z5DtXT7HEAydtg==} engines: {node: '>=18'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -6366,8 +6444,8 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} - registry-auth-token@5.1.0: - resolution: {integrity: sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==} + registry-auth-token@5.1.1: + resolution: {integrity: sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==} engines: {node: '>=14'} remark-parse@11.0.0: @@ -6387,9 +6465,6 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - reselect@5.1.1: - resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} - reselect@5.2.0: resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} @@ -6413,8 +6488,8 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true - resolve@2.0.0-next.6: - resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} engines: {node: '>= 0.4'} hasBin: true @@ -6436,8 +6511,8 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rollup@4.60.4: - resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -6495,8 +6570,8 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} - semantic-release@25.0.3: - resolution: {integrity: sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==} + semantic-release@25.0.5: + resolution: {integrity: sha512-mn61SUJwtM8ThrWn2WmgLVpwVJeG/hPSupua1psdMoufmwRIPyvRLkRkL0JDXkP67OntlLWUYnBnfVc8EDO3/g==} engines: {node: ^22.14.0 || >= 24.10.0} hasBin: true @@ -6508,18 +6583,18 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} engines: {node: '>=10'} hasBin: true - semver@7.8.1: - resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true @@ -6534,8 +6609,8 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - set-cookie-parser@3.1.0: - resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + set-cookie-parser@3.1.1: + resolution: {integrity: sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA==} set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -6552,8 +6627,9 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shadcn@4.8.2: - resolution: {integrity: sha512-pt3KneOg6LGYKNAdoTVf/lpVcf7t2MlV+Ll2Xc3lIIYN3ph4ajrjU+CcG6OVSgO5ubbLZj+9j5oMA9Lqg7o8KA==} + shadcn@4.13.0: + resolution: {integrity: sha512-5fuJ4jI/GcPeA/iTL4cJivCZuYQGXz/N3bIzyd+Gd/FM6xUCy2MxGG+LaDQuw2cjNy9zGPSFPTEmI048UwPTZA==} + engines: {node: '>=20.18.1'} hasBin: true sharp@0.34.5: @@ -6572,6 +6648,10 @@ packages: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + side-channel-map@1.0.1: resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} engines: {node: '>= 0.4'} @@ -6584,6 +6664,10 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -6663,8 +6747,8 @@ packages: spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - spdx-license-ids@3.0.22: - resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} split2@1.0.0: resolution: {integrity: sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg==} @@ -6741,8 +6825,12 @@ packages: resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} engines: {node: '>= 0.4'} - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} engines: {node: '>= 0.4'} string.prototype.trimstart@1.0.8: @@ -6844,6 +6932,12 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + systeminformation@5.31.13: + resolution: {integrity: sha512-iUJXJoKzm4vtLSeT3nwe2s9QjoJAxHg7wYJ0KaQ54Xy2u9jsTq0ULWQQ0+T72FXjX2XnGqubazNx9lUfng7ELw==} + engines: {node: '>=8.0.0'} + os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] + hasBin: true + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} @@ -6854,8 +6948,8 @@ packages: tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} - tailwindcss@4.3.0: - resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + tailwindcss@4.3.2: + resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==} tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} @@ -6872,8 +6966,8 @@ packages: resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} engines: {node: '>=14.16'} - tempy@3.1.0: - resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==} + tempy@3.2.0: + resolution: {integrity: sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ==} engines: {node: '>=14.16'} terser-webpack-plugin@5.6.1: @@ -6960,14 +7054,10 @@ packages: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} - tinyexec@1.2.2: - resolution: {integrity: sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -7032,6 +7122,7 @@ packages: tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -7049,8 +7140,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.22.3: - resolution: {integrity: sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==} + tsx@4.23.0: + resolution: {integrity: sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==} engines: {node: '>=18.0.0'} hasBin: true @@ -7083,14 +7174,14 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - type-fest@5.3.1: - resolution: {integrity: sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==} - engines: {node: '>=20'} - type-fest@5.7.0: resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==} engines: {node: '>=20'} + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + engines: {node: '>=20'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -7115,6 +7206,10 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + typed-inject@5.0.0: resolution: {integrity: sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==} engines: {node: '>=18'} @@ -7126,15 +7221,15 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.60.0: - resolution: {integrity: sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==} + typescript-eslint@8.62.1: + resolution: {integrity: sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true @@ -7162,18 +7257,18 @@ packages: undici-types@7.25.0: resolution: {integrity: sha512-AXNgS1Byr27fTI+2bsPEkV9CxkT8H6xNyRI68b3TatlZo3RkzlqQBLL+w7SmGPVpokjHbcuNVQUWE7FRTg+LRA==} - undici@5.29.0: - resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} - engines: {node: '>=14.0'} - - undici@6.25.0: - resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==} + undici@6.27.0: + resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==} engines: {node: '>=18.17'} undici@7.25.0: resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} engines: {node: '>=20.18.1'} + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + unicode-emoji-modifier-base@1.0.0: resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} engines: {node: '>=4'} @@ -7186,6 +7281,10 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -7223,8 +7322,8 @@ packages: resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} engines: {node: '>=14.0.0'} - unrs-resolver@1.11.1: - resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} @@ -7349,20 +7448,20 @@ packages: yaml: optional: true - vitest@4.1.7: - resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.7 - '@vitest/browser-preview': 4.1.7 - '@vitest/browser-webdriverio': 4.1.7 - '@vitest/coverage-istanbul': 4.1.7 - '@vitest/coverage-v8': 4.1.7 - '@vitest/ui': 4.1.7 + '@vitest/browser-playwright': 4.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -7394,8 +7493,8 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} - watchpack@2.5.1: - resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} engines: {node: '>=10.13.0'} wcwidth@1.0.1: @@ -7404,19 +7503,15 @@ packages: weapon-regex@1.3.6: resolution: {integrity: sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA==} - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - - web-worker@1.2.0: - resolution: {integrity: sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==} + web-worker@1.5.0: + resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} webidl-conversions@8.0.1: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} - webpack-sources@3.5.0: - resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} + webpack-sources@3.5.1: + resolution: {integrity: sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==} engines: {node: '>=10.13.0'} webpack-virtual-modules@0.6.2: @@ -7459,8 +7554,8 @@ packages: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.20: - resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} engines: {node: '>= 0.4'} which@2.0.2: @@ -7595,8 +7690,8 @@ packages: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} engines: {node: '>=12'} yargs@18.0.0: @@ -7607,8 +7702,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yocto-spinner@1.2.0: - resolution: {integrity: sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw==} + yocto-spinner@1.2.1: + resolution: {integrity: sha512-9cbFWLhbiZp+820O4pkHGNncI7+MrUGzBOjw8NMG+ewsY+aG0DdEXnr19Smxao32YOjLZRMdn1UtaxcrXOYOIg==} engines: {node: '>=18.19'} yoctocolors@2.1.2: @@ -7658,20 +7753,11 @@ packages: snapshots: - '@actions/core@2.0.1': - dependencies: - '@actions/exec': 2.0.0 - '@actions/http-client': 3.0.0 - '@actions/core@3.0.1': dependencies: '@actions/exec': 3.0.0 '@actions/http-client': 4.0.1 - '@actions/exec@2.0.0': - dependencies: - '@actions/io': 2.0.0 - '@actions/exec@3.0.0': dependencies: '@actions/io': 3.0.2 @@ -7684,38 +7770,31 @@ snapshots: '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.6) '@octokit/request': 10.0.7 '@octokit/request-error': 7.1.0 - undici: 6.25.0 - - '@actions/http-client@3.0.0': - dependencies: - tunnel: 0.0.6 - undici: 5.29.0 + undici: 6.27.0 '@actions/http-client@3.0.2': dependencies: tunnel: 0.0.6 - undici: 6.25.0 + undici: 6.27.0 '@actions/http-client@4.0.1': dependencies: tunnel: 0.0.6 - undici: 6.25.0 - - '@actions/io@2.0.0': {} + undici: 6.27.0 '@actions/io@3.0.2': {} - '@ajayche/trpc-panel@2.1.0(@trpc/server@11.17.0(typescript@5.9.3))(@types/react@19.2.15)(next@16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3)': + '@ajayche/trpc-panel@2.1.0(@trpc/server@11.18.0(typescript@6.0.3))(@types/react@19.2.17)(next@16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3)': dependencies: - '@microlink/react-json-view': 1.31.20(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@trpc/server': 11.17.0(typescript@5.9.3) + '@microlink/react-json-view': 1.31.20(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@trpc/server': 11.18.0(typescript@6.0.3) clsx: 2.1.1 fuzzysort: 2.0.4 - nuqs: 2.8.9(next@16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6) + nuqs: 2.8.9(next@16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) path: 0.12.7 pretty-bytes: 6.1.1 pretty-ms: 8.0.0 - react-markdown: 9.1.0(@types/react@19.2.15)(react@19.2.6) + react-markdown: 9.1.0(@types/react@19.2.17)(react@19.2.7) string-byte-length: 1.6.0 tailwind-merge: 2.6.1 url: 0.11.4 @@ -7840,7 +7919,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.3 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.2 + browserslist: 4.28.5 lru-cache: 5.1.1 semver: 6.3.1 @@ -7848,7 +7927,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.2 + browserslist: 4.28.5 lru-cache: 5.1.1 semver: 6.3.1 @@ -8053,8 +8132,7 @@ snapshots: '@babel/runtime@7.29.2': {} - '@babel/runtime@7.29.7': - optional: true + '@babel/runtime@7.29.7': {} '@babel/template@7.28.6': dependencies: @@ -8102,29 +8180,29 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@base-ui/react@1.5.0(@date-fns/tz@1.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@base-ui/react@1.6.0(@date-fns/tz@1.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@babel/runtime': 7.29.2 - '@base-ui/utils': 0.2.9(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@babel/runtime': 7.29.7 + '@base-ui/utils': 0.3.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@floating-ui/utils': 0.2.11 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - use-sync-external-store: 1.6.0(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + use-sync-external-store: 1.6.0(react@19.2.7) optionalDependencies: '@date-fns/tz': 1.4.1 - '@types/react': 19.2.15 + '@types/react': 19.2.17 - '@base-ui/utils@0.2.9(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@base-ui/utils@0.3.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@floating-ui/utils': 0.2.11 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) reselect: 5.2.0 - use-sync-external-store: 1.6.0(react@19.2.6) + use-sync-external-store: 1.6.0(react@19.2.7) optionalDependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 '@bcoe/v8-coverage@1.0.2': {} @@ -8155,15 +8233,15 @@ snapshots: '@actions/core': 3.0.1 '@actions/github': 9.1.1 chalk: 4.1.2 - semver: 7.7.4 + semver: 7.8.4 unplugin: 1.16.1 zod: 3.25.76 - '@codecov/nextjs-webpack-plugin@2.0.1(next@16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(webpack@5.104.1(lightningcss@1.32.0))': + '@codecov/nextjs-webpack-plugin@2.0.1(next@16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(webpack@5.104.1(lightningcss@1.32.0))': dependencies: '@codecov/bundler-plugin-core': 2.0.1 '@codecov/webpack-plugin': 2.0.1(webpack@5.104.1(lightningcss@1.32.0)) - next: 16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + next: 16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) unplugin: 1.16.1 webpack: 5.104.1(lightningcss@1.32.0) @@ -8203,25 +8281,28 @@ snapshots: '@date-fns/tz@1.4.1': optional: true - '@dotenvx/dotenvx@1.70.0': + '@dotenvx/dotenvx@1.75.1': dependencies: + '@dotenvx/primitives': 0.8.0 commander: 11.1.0 + conf: 10.2.0 dotenv: 17.4.2 - eciesjs: 0.4.18 enquirer: 2.4.1 + env-paths: 2.2.1 execa: 5.1.1 - fdir: 6.5.0(picomatch@4.0.4) + fdir: 6.5.0(picomatch@4.0.5) ignore: 5.3.2 object-treeify: 1.1.33 - picomatch: 4.0.4 + open: 8.4.2 + picomatch: 4.0.5 + systeminformation: 5.31.13 + undici: 7.28.0 which: 4.0.0 - yocto-spinner: 1.2.0 + yocto-spinner: 1.2.1 - '@drizzle-team/brocli@0.10.2': {} + '@dotenvx/primitives@0.8.0': {} - '@ecies/ciphers@0.2.6(@noble/ciphers@1.3.0)': - dependencies: - '@noble/ciphers': 1.3.0 + '@drizzle-team/brocli@0.10.2': {} '@emnapi/core@1.10.0': dependencies: @@ -8234,6 +8315,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 @@ -8255,7 +8341,7 @@ snapshots: '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/aix-ppc64@0.28.0': + '@esbuild/aix-ppc64@0.28.1': optional: true '@esbuild/android-arm64@0.18.20': @@ -8267,7 +8353,7 @@ snapshots: '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/android-arm64@0.28.0': + '@esbuild/android-arm64@0.28.1': optional: true '@esbuild/android-arm@0.18.20': @@ -8279,7 +8365,7 @@ snapshots: '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/android-arm@0.28.0': + '@esbuild/android-arm@0.28.1': optional: true '@esbuild/android-x64@0.18.20': @@ -8291,7 +8377,7 @@ snapshots: '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/android-x64@0.28.0': + '@esbuild/android-x64@0.28.1': optional: true '@esbuild/darwin-arm64@0.18.20': @@ -8303,7 +8389,7 @@ snapshots: '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/darwin-arm64@0.28.0': + '@esbuild/darwin-arm64@0.28.1': optional: true '@esbuild/darwin-x64@0.18.20': @@ -8315,7 +8401,7 @@ snapshots: '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/darwin-x64@0.28.0': + '@esbuild/darwin-x64@0.28.1': optional: true '@esbuild/freebsd-arm64@0.18.20': @@ -8327,7 +8413,7 @@ snapshots: '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/freebsd-arm64@0.28.0': + '@esbuild/freebsd-arm64@0.28.1': optional: true '@esbuild/freebsd-x64@0.18.20': @@ -8339,7 +8425,7 @@ snapshots: '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/freebsd-x64@0.28.0': + '@esbuild/freebsd-x64@0.28.1': optional: true '@esbuild/linux-arm64@0.18.20': @@ -8351,7 +8437,7 @@ snapshots: '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/linux-arm64@0.28.0': + '@esbuild/linux-arm64@0.28.1': optional: true '@esbuild/linux-arm@0.18.20': @@ -8363,7 +8449,7 @@ snapshots: '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/linux-arm@0.28.0': + '@esbuild/linux-arm@0.28.1': optional: true '@esbuild/linux-ia32@0.18.20': @@ -8375,7 +8461,7 @@ snapshots: '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/linux-ia32@0.28.0': + '@esbuild/linux-ia32@0.28.1': optional: true '@esbuild/linux-loong64@0.18.20': @@ -8387,7 +8473,7 @@ snapshots: '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/linux-loong64@0.28.0': + '@esbuild/linux-loong64@0.28.1': optional: true '@esbuild/linux-mips64el@0.18.20': @@ -8399,7 +8485,7 @@ snapshots: '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/linux-mips64el@0.28.0': + '@esbuild/linux-mips64el@0.28.1': optional: true '@esbuild/linux-ppc64@0.18.20': @@ -8411,7 +8497,7 @@ snapshots: '@esbuild/linux-ppc64@0.27.7': optional: true - '@esbuild/linux-ppc64@0.28.0': + '@esbuild/linux-ppc64@0.28.1': optional: true '@esbuild/linux-riscv64@0.18.20': @@ -8423,7 +8509,7 @@ snapshots: '@esbuild/linux-riscv64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.28.0': + '@esbuild/linux-riscv64@0.28.1': optional: true '@esbuild/linux-s390x@0.18.20': @@ -8435,7 +8521,7 @@ snapshots: '@esbuild/linux-s390x@0.27.7': optional: true - '@esbuild/linux-s390x@0.28.0': + '@esbuild/linux-s390x@0.28.1': optional: true '@esbuild/linux-x64@0.18.20': @@ -8447,7 +8533,7 @@ snapshots: '@esbuild/linux-x64@0.27.7': optional: true - '@esbuild/linux-x64@0.28.0': + '@esbuild/linux-x64@0.28.1': optional: true '@esbuild/netbsd-arm64@0.25.12': @@ -8456,7 +8542,7 @@ snapshots: '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.28.0': + '@esbuild/netbsd-arm64@0.28.1': optional: true '@esbuild/netbsd-x64@0.18.20': @@ -8468,7 +8554,7 @@ snapshots: '@esbuild/netbsd-x64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.28.0': + '@esbuild/netbsd-x64@0.28.1': optional: true '@esbuild/openbsd-arm64@0.25.12': @@ -8477,7 +8563,7 @@ snapshots: '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.28.0': + '@esbuild/openbsd-arm64@0.28.1': optional: true '@esbuild/openbsd-x64@0.18.20': @@ -8489,7 +8575,7 @@ snapshots: '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.28.0': + '@esbuild/openbsd-x64@0.28.1': optional: true '@esbuild/openharmony-arm64@0.25.12': @@ -8498,7 +8584,7 @@ snapshots: '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/openharmony-arm64@0.28.0': + '@esbuild/openharmony-arm64@0.28.1': optional: true '@esbuild/sunos-x64@0.18.20': @@ -8510,7 +8596,7 @@ snapshots: '@esbuild/sunos-x64@0.27.7': optional: true - '@esbuild/sunos-x64@0.28.0': + '@esbuild/sunos-x64@0.28.1': optional: true '@esbuild/win32-arm64@0.18.20': @@ -8522,7 +8608,7 @@ snapshots: '@esbuild/win32-arm64@0.27.7': optional: true - '@esbuild/win32-arm64@0.28.0': + '@esbuild/win32-arm64@0.28.1': optional: true '@esbuild/win32-ia32@0.18.20': @@ -8534,7 +8620,7 @@ snapshots: '@esbuild/win32-ia32@0.27.7': optional: true - '@esbuild/win32-ia32@0.28.0': + '@esbuild/win32-ia32@0.28.1': optional: true '@esbuild/win32-x64@0.18.20': @@ -8546,7 +8632,7 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true - '@esbuild/win32-x64@0.28.0': + '@esbuild/win32-x64@0.28.1': optional: true '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': @@ -8585,7 +8671,7 @@ snapshots: mdn-data: 2.28.1 source-map-js: 1.2.1 - '@eslint/css@1.3.0': + '@eslint/css@1.4.0': dependencies: '@eslint/core': 1.2.1 '@eslint/css-tree': 4.0.4 @@ -8635,8 +8721,6 @@ snapshots: optionalDependencies: '@noble/hashes': 1.8.0 - '@fastify/busboy@2.1.1': {} - '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -8646,19 +8730,19 @@ snapshots: '@floating-ui/core': 1.7.5 '@floating-ui/utils': 0.2.11 - '@floating-ui/react-dom@2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@floating-ui/dom': 1.7.6 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) '@floating-ui/utils@0.2.11': {} - '@formatjs/fast-memoize@3.1.5': {} + '@formatjs/fast-memoize@3.1.6': {} - '@formatjs/intl-localematcher@0.8.9': + '@formatjs/intl-localematcher@0.8.10': dependencies: - '@formatjs/fast-memoize': 3.1.5 + '@formatjs/fast-memoize': 3.1.6 '@hapi/bourne@3.0.0': {} @@ -8680,9 +8764,9 @@ snapshots: safe-buffer: 5.2.1 xml2js: 0.6.2 - '@hono/node-server@1.19.14(hono@4.12.23)': + '@hono/node-server@1.19.14(hono@4.12.28)': dependencies: - hono: 4.12.23 + hono: 4.12.28 '@humanfs/core@0.19.1': {} @@ -8782,7 +8866,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.11.2 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -8796,149 +8880,154 @@ snapshots: '@inquirer/ansi@2.0.5': {} - '@inquirer/ansi@2.0.7': {} + '@inquirer/ansi@2.0.7': + optional: true - '@inquirer/checkbox@5.1.4(@types/node@25.9.1)': + '@inquirer/checkbox@5.1.4(@types/node@25.9.4)': dependencies: '@inquirer/ansi': 2.0.5 - '@inquirer/core': 11.1.9(@types/node@25.9.1) + '@inquirer/core': 11.1.9(@types/node@25.9.4) '@inquirer/figures': 2.0.5 - '@inquirer/type': 4.0.5(@types/node@25.9.1) + '@inquirer/type': 4.0.5(@types/node@25.9.4) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@inquirer/confirm@6.0.12(@types/node@25.9.1)': + '@inquirer/confirm@6.0.12(@types/node@25.9.4)': dependencies: - '@inquirer/core': 11.1.9(@types/node@25.9.1) - '@inquirer/type': 4.0.5(@types/node@25.9.1) + '@inquirer/core': 11.1.9(@types/node@25.9.4) + '@inquirer/type': 4.0.5(@types/node@25.9.4) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@inquirer/confirm@6.1.1(@types/node@25.9.1)': + '@inquirer/confirm@6.1.1(@types/node@25.9.4)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.1) - '@inquirer/type': 4.0.7(@types/node@25.9.1) + '@inquirer/core': 11.2.1(@types/node@25.9.4) + '@inquirer/type': 4.0.7(@types/node@25.9.4) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 + optional: true - '@inquirer/core@11.1.9(@types/node@25.9.1)': + '@inquirer/core@11.1.9(@types/node@25.9.4)': dependencies: '@inquirer/ansi': 2.0.5 '@inquirer/figures': 2.0.5 - '@inquirer/type': 4.0.5(@types/node@25.9.1) + '@inquirer/type': 4.0.5(@types/node@25.9.4) cli-width: 4.1.0 fast-wrap-ansi: 0.2.0 mute-stream: 3.0.0 signal-exit: 4.1.0 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@inquirer/core@11.2.1(@types/node@25.9.1)': + '@inquirer/core@11.2.1(@types/node@25.9.4)': dependencies: '@inquirer/ansi': 2.0.7 '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@25.9.1) + '@inquirer/type': 4.0.7(@types/node@25.9.4) cli-width: 4.1.0 fast-wrap-ansi: 0.2.2 mute-stream: 3.0.0 signal-exit: 4.1.0 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 + optional: true - '@inquirer/editor@5.1.1(@types/node@25.9.1)': + '@inquirer/editor@5.1.1(@types/node@25.9.4)': dependencies: - '@inquirer/core': 11.1.9(@types/node@25.9.1) - '@inquirer/external-editor': 3.0.0(@types/node@25.9.1) - '@inquirer/type': 4.0.5(@types/node@25.9.1) + '@inquirer/core': 11.1.9(@types/node@25.9.4) + '@inquirer/external-editor': 3.0.0(@types/node@25.9.4) + '@inquirer/type': 4.0.5(@types/node@25.9.4) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@inquirer/expand@5.0.13(@types/node@25.9.1)': + '@inquirer/expand@5.0.13(@types/node@25.9.4)': dependencies: - '@inquirer/core': 11.1.9(@types/node@25.9.1) - '@inquirer/type': 4.0.5(@types/node@25.9.1) + '@inquirer/core': 11.1.9(@types/node@25.9.4) + '@inquirer/type': 4.0.5(@types/node@25.9.4) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@inquirer/external-editor@3.0.0(@types/node@25.9.1)': + '@inquirer/external-editor@3.0.0(@types/node@25.9.4)': dependencies: chardet: 2.1.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 '@inquirer/figures@2.0.5': {} - '@inquirer/figures@2.0.7': {} + '@inquirer/figures@2.0.7': + optional: true - '@inquirer/input@5.0.12(@types/node@25.9.1)': + '@inquirer/input@5.0.12(@types/node@25.9.4)': dependencies: - '@inquirer/core': 11.1.9(@types/node@25.9.1) - '@inquirer/type': 4.0.5(@types/node@25.9.1) + '@inquirer/core': 11.1.9(@types/node@25.9.4) + '@inquirer/type': 4.0.5(@types/node@25.9.4) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@inquirer/number@4.0.12(@types/node@25.9.1)': + '@inquirer/number@4.0.12(@types/node@25.9.4)': dependencies: - '@inquirer/core': 11.1.9(@types/node@25.9.1) - '@inquirer/type': 4.0.5(@types/node@25.9.1) + '@inquirer/core': 11.1.9(@types/node@25.9.4) + '@inquirer/type': 4.0.5(@types/node@25.9.4) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@inquirer/password@5.0.12(@types/node@25.9.1)': + '@inquirer/password@5.0.12(@types/node@25.9.4)': dependencies: '@inquirer/ansi': 2.0.5 - '@inquirer/core': 11.1.9(@types/node@25.9.1) - '@inquirer/type': 4.0.5(@types/node@25.9.1) + '@inquirer/core': 11.1.9(@types/node@25.9.4) + '@inquirer/type': 4.0.5(@types/node@25.9.4) optionalDependencies: - '@types/node': 25.9.1 - - '@inquirer/prompts@8.4.2(@types/node@25.9.1)': - dependencies: - '@inquirer/checkbox': 5.1.4(@types/node@25.9.1) - '@inquirer/confirm': 6.0.12(@types/node@25.9.1) - '@inquirer/editor': 5.1.1(@types/node@25.9.1) - '@inquirer/expand': 5.0.13(@types/node@25.9.1) - '@inquirer/input': 5.0.12(@types/node@25.9.1) - '@inquirer/number': 4.0.12(@types/node@25.9.1) - '@inquirer/password': 5.0.12(@types/node@25.9.1) - '@inquirer/rawlist': 5.2.8(@types/node@25.9.1) - '@inquirer/search': 4.1.8(@types/node@25.9.1) - '@inquirer/select': 5.1.4(@types/node@25.9.1) + '@types/node': 25.9.4 + + '@inquirer/prompts@8.4.2(@types/node@25.9.4)': + dependencies: + '@inquirer/checkbox': 5.1.4(@types/node@25.9.4) + '@inquirer/confirm': 6.0.12(@types/node@25.9.4) + '@inquirer/editor': 5.1.1(@types/node@25.9.4) + '@inquirer/expand': 5.0.13(@types/node@25.9.4) + '@inquirer/input': 5.0.12(@types/node@25.9.4) + '@inquirer/number': 4.0.12(@types/node@25.9.4) + '@inquirer/password': 5.0.12(@types/node@25.9.4) + '@inquirer/rawlist': 5.2.8(@types/node@25.9.4) + '@inquirer/search': 4.1.8(@types/node@25.9.4) + '@inquirer/select': 5.1.4(@types/node@25.9.4) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@inquirer/rawlist@5.2.8(@types/node@25.9.1)': + '@inquirer/rawlist@5.2.8(@types/node@25.9.4)': dependencies: - '@inquirer/core': 11.1.9(@types/node@25.9.1) - '@inquirer/type': 4.0.5(@types/node@25.9.1) + '@inquirer/core': 11.1.9(@types/node@25.9.4) + '@inquirer/type': 4.0.5(@types/node@25.9.4) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@inquirer/search@4.1.8(@types/node@25.9.1)': + '@inquirer/search@4.1.8(@types/node@25.9.4)': dependencies: - '@inquirer/core': 11.1.9(@types/node@25.9.1) + '@inquirer/core': 11.1.9(@types/node@25.9.4) '@inquirer/figures': 2.0.5 - '@inquirer/type': 4.0.5(@types/node@25.9.1) + '@inquirer/type': 4.0.5(@types/node@25.9.4) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@inquirer/select@5.1.4(@types/node@25.9.1)': + '@inquirer/select@5.1.4(@types/node@25.9.4)': dependencies: '@inquirer/ansi': 2.0.5 - '@inquirer/core': 11.1.9(@types/node@25.9.1) + '@inquirer/core': 11.1.9(@types/node@25.9.4) '@inquirer/figures': 2.0.5 - '@inquirer/type': 4.0.5(@types/node@25.9.1) + '@inquirer/type': 4.0.5(@types/node@25.9.4) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@inquirer/type@4.0.5(@types/node@25.9.1)': + '@inquirer/type@4.0.5(@types/node@25.9.4)': optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@inquirer/type@4.0.7(@types/node@25.9.1)': + '@inquirer/type@4.0.7(@types/node@25.9.4)': optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 + optional: true '@isaacs/cliui@9.0.0': {} @@ -8951,7 +9040,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 25.9.1 + '@types/node': 25.9.4 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -8983,13 +9072,13 @@ snapshots: '@lingui/babel-plugin-extract-messages@5.9.5': {} - '@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3)': + '@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3)': dependencies: '@babel/core': 7.29.0 '@babel/runtime': 7.29.2 '@babel/types': 7.29.0 - '@lingui/conf': 5.9.5(typescript@5.9.3) - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0) + '@lingui/conf': 5.9.5(typescript@6.0.3) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0) '@lingui/message-utils': 5.9.5 optionalDependencies: babel-plugin-macros: 3.1.0 @@ -8997,7 +9086,7 @@ snapshots: - supports-color - typescript - '@lingui/cli@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3)': + '@lingui/cli@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3)': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -9005,10 +9094,10 @@ snapshots: '@babel/runtime': 7.29.2 '@babel/types': 7.29.0 '@lingui/babel-plugin-extract-messages': 5.9.5 - '@lingui/babel-plugin-lingui-macro': 5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3) - '@lingui/conf': 5.9.5(typescript@5.9.3) - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0) - '@lingui/format-po': 5.9.5(typescript@5.9.3) + '@lingui/babel-plugin-lingui-macro': 5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3) + '@lingui/conf': 5.9.5(typescript@6.0.3) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0) + '@lingui/format-po': 5.9.5(typescript@6.0.3) '@lingui/message-utils': 5.9.5 chokidar: 3.5.1 cli-table: 0.3.11 @@ -9031,50 +9120,50 @@ snapshots: - supports-color - typescript - '@lingui/conf@5.9.5(typescript@5.9.3)': + '@lingui/conf@5.9.5(typescript@6.0.3)': dependencies: '@babel/runtime': 7.29.2 - cosmiconfig: 8.3.6(typescript@5.9.3) + cosmiconfig: 8.3.6(typescript@6.0.3) jest-validate: 29.7.0 jiti: 2.7.0 picocolors: 1.1.1 transitivePeerDependencies: - typescript - '@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0)': + '@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0)': dependencies: '@babel/runtime': 7.29.2 '@lingui/message-utils': 5.9.5 optionalDependencies: - '@lingui/babel-plugin-lingui-macro': 5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3) + '@lingui/babel-plugin-lingui-macro': 5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3) babel-plugin-macros: 3.1.0 - '@lingui/format-po@5.9.5(typescript@5.9.3)': + '@lingui/format-po@5.9.5(typescript@6.0.3)': dependencies: - '@lingui/conf': 5.9.5(typescript@5.9.3) + '@lingui/conf': 5.9.5(typescript@6.0.3) '@lingui/message-utils': 5.9.5 date-fns: 3.6.0 pofile: 1.1.4 transitivePeerDependencies: - typescript - '@lingui/loader@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3)(webpack@5.104.1(lightningcss@1.32.0))': + '@lingui/loader@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3)(webpack@5.104.1(lightningcss@1.32.0))': dependencies: '@babel/runtime': 7.29.2 - '@lingui/cli': 5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3) - '@lingui/conf': 5.9.5(typescript@5.9.3) + '@lingui/cli': 5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3) + '@lingui/conf': 5.9.5(typescript@6.0.3) webpack: 5.104.1(lightningcss@1.32.0) transitivePeerDependencies: - babel-plugin-macros - supports-color - typescript - '@lingui/macro@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0)(react@19.2.6)': + '@lingui/macro@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0)(react@19.2.7)': dependencies: - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0) - '@lingui/react': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0)(react@19.2.6) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0) + '@lingui/react': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0)(react@19.2.7) optionalDependencies: - '@lingui/babel-plugin-lingui-macro': 5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3) + '@lingui/babel-plugin-lingui-macro': 5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3) babel-plugin-macros: 3.1.0 transitivePeerDependencies: - react @@ -9084,26 +9173,26 @@ snapshots: '@messageformat/parser': 5.1.1 js-sha256: 0.10.1 - '@lingui/react@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0)(react@19.2.6)': + '@lingui/react@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0)(react@19.2.7)': dependencies: '@babel/runtime': 7.29.2 - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0) - react: 19.2.6 + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0) + react: 19.2.7 optionalDependencies: - '@lingui/babel-plugin-lingui-macro': 5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3) + '@lingui/babel-plugin-lingui-macro': 5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3) babel-plugin-macros: 3.1.0 - '@lingui/swc-plugin@5.11.0(@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0))(next@16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))': + '@lingui/swc-plugin@5.11.0(@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0))(next@16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': dependencies: - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3))(babel-plugin-macros@3.1.0) optionalDependencies: - next: 16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + next: 16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@lingui/vite-plugin@5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3)(vite@7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0))': + '@lingui/vite-plugin@5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3)(vite@7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0))': dependencies: - '@lingui/cli': 5.9.5(babel-plugin-macros@3.1.0)(typescript@5.9.3) - '@lingui/conf': 5.9.5(typescript@5.9.3) - vite: 7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) + '@lingui/cli': 5.9.5(babel-plugin-macros@3.1.0)(typescript@6.0.3) + '@lingui/conf': 5.9.5(typescript@6.0.3) + vite: 7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -9113,19 +9202,19 @@ snapshots: dependencies: moo: 0.5.3 - '@microlink/react-json-view@1.31.20(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@microlink/react-json-view@1.31.20(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - react: 19.2.6 + react: 19.2.7 react-base16-styling: 0.10.0 - react-dom: 19.2.6(react@19.2.6) + react-dom: 19.2.7(react@19.2.7) react-lifecycles-compat: 3.0.4 - react-textarea-autosize: 8.5.9(@types/react@19.2.15)(react@19.2.6) + react-textarea-autosize: 8.5.9(@types/react@19.2.17)(react@19.2.7) transitivePeerDependencies: - '@types/react' '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': dependencies: - '@hono/node-server': 1.19.14(hono@4.12.23) + '@hono/node-server': 1.19.14(hono@4.12.28) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 @@ -9135,7 +9224,7 @@ snapshots: eventsource-parser: 3.1.0 express: 5.2.1 express-rate-limit: 8.5.2(express@5.2.1) - hono: 4.12.23 + hono: 4.12.28 jose: 6.2.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -9153,51 +9242,47 @@ snapshots: is-node-process: 1.2.0 outvariant: 1.4.3 strict-event-emitter: 0.5.1 + optional: true - '@napi-rs/wasm-runtime@0.2.12': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 + '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@16.2.6': {} + '@next/env@16.2.10': {} - '@next/eslint-plugin-next@16.2.6': + '@next/eslint-plugin-next@16.2.10': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.2.6': + '@next/swc-darwin-arm64@16.2.10': optional: true - '@next/swc-darwin-x64@16.2.6': + '@next/swc-darwin-x64@16.2.10': optional: true - '@next/swc-linux-arm64-gnu@16.2.6': + '@next/swc-linux-arm64-gnu@16.2.10': optional: true - '@next/swc-linux-arm64-musl@16.2.6': + '@next/swc-linux-arm64-musl@16.2.10': optional: true - '@next/swc-linux-x64-gnu@16.2.6': + '@next/swc-linux-x64-gnu@16.2.10': optional: true - '@next/swc-linux-x64-musl@16.2.6': + '@next/swc-linux-x64-musl@16.2.10': optional: true - '@next/swc-win32-arm64-msvc@16.2.6': + '@next/swc-win32-arm64-msvc@16.2.10': optional: true - '@next/swc-win32-x64-msvc@16.2.6': + '@next/swc-win32-x64-msvc@16.2.10': optional: true - '@noble/ciphers@1.3.0': {} - - '@noble/curves@1.9.7': - dependencies: - '@noble/hashes': 1.8.0 - - '@noble/hashes@1.8.0': {} + '@noble/hashes@1.8.0': + optional: true '@nodelib/fs.scandir@2.1.5': dependencies: @@ -9219,7 +9304,7 @@ snapshots: dependencies: '@octokit/auth-token': 6.0.0 '@octokit/graphql': 9.0.3 - '@octokit/request': 10.0.7 + '@octokit/request': 10.0.10 '@octokit/request-error': 7.1.0 '@octokit/types': 16.0.0 before-after-hook: 4.0.0 @@ -9230,9 +9315,14 @@ snapshots: '@octokit/types': 16.0.0 universal-user-agent: 7.0.3 + '@octokit/endpoint@11.0.3': + dependencies: + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 + '@octokit/graphql@9.0.3': dependencies: - '@octokit/request': 10.0.7 + '@octokit/request': 10.0.10 '@octokit/types': 16.0.0 universal-user-agent: 7.0.3 @@ -9248,7 +9338,7 @@ snapshots: '@octokit/core': 7.0.6 '@octokit/types': 16.0.0 - '@octokit/plugin-retry@8.0.3(@octokit/core@7.0.6)': + '@octokit/plugin-retry@8.1.0(@octokit/core@7.0.6)': dependencies: '@octokit/core': 7.0.6 '@octokit/request-error': 7.1.0 @@ -9265,6 +9355,15 @@ snapshots: dependencies: '@octokit/types': 16.0.0 + '@octokit/request@10.0.10': + dependencies: + '@octokit/endpoint': 11.0.3 + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + content-type: 2.0.0 + json-with-bigint: 3.5.8 + universal-user-agent: 7.0.3 + '@octokit/request@10.0.7': dependencies: '@octokit/endpoint': 11.0.2 @@ -9277,16 +9376,20 @@ snapshots: dependencies: '@octokit/openapi-types': 27.0.0 - '@open-draft/deferred-promise@2.2.0': {} + '@open-draft/deferred-promise@2.2.0': + optional: true - '@open-draft/deferred-promise@3.0.0': {} + '@open-draft/deferred-promise@3.0.0': + optional: true '@open-draft/logger@0.3.0': dependencies: is-node-process: 1.2.0 outvariant: 1.4.3 + optional: true - '@open-draft/until@2.1.0': {} + '@open-draft/until@2.1.0': + optional: true '@pnpm/config.env-replace@1.1.0': {} @@ -9294,109 +9397,109 @@ snapshots: dependencies: graceful-fs: 4.2.10 - '@pnpm/npm-conf@2.3.1': + '@pnpm/npm-conf@3.0.3': dependencies: '@pnpm/config.env-replace': 1.1.0 '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 - '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1))(react@19.2.6)': + '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)': dependencies: '@standard-schema/spec': 1.1.0 '@standard-schema/utils': 0.3.0 - immer: 11.1.4 + immer: 11.1.11 redux: 5.0.1 redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.1.1 + reselect: 5.2.0 optionalDependencies: - react: 19.2.6 - react-redux: 9.2.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1) + react: 19.2.7 + react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1) '@rolldown/pluginutils@1.0.0-rc.3': {} - '@rollup/rollup-android-arm-eabi@4.60.4': + '@rollup/rollup-android-arm-eabi@4.62.2': optional: true - '@rollup/rollup-android-arm64@4.60.4': + '@rollup/rollup-android-arm64@4.62.2': optional: true - '@rollup/rollup-darwin-arm64@4.60.4': + '@rollup/rollup-darwin-arm64@4.62.2': optional: true - '@rollup/rollup-darwin-x64@4.60.4': + '@rollup/rollup-darwin-x64@4.62.2': optional: true - '@rollup/rollup-freebsd-arm64@4.60.4': + '@rollup/rollup-freebsd-arm64@4.62.2': optional: true - '@rollup/rollup-freebsd-x64@4.60.4': + '@rollup/rollup-freebsd-x64@4.62.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.4': + '@rollup/rollup-linux-arm-musleabihf@4.62.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.4': + '@rollup/rollup-linux-arm64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.60.4': + '@rollup/rollup-linux-arm64-musl@4.62.2': optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.4': + '@rollup/rollup-linux-loong64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-loong64-musl@4.60.4': + '@rollup/rollup-linux-loong64-musl@4.62.2': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.4': + '@rollup/rollup-linux-ppc64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.4': + '@rollup/rollup-linux-ppc64-musl@4.62.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.4': + '@rollup/rollup-linux-riscv64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.4': + '@rollup/rollup-linux-riscv64-musl@4.62.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.4': + '@rollup/rollup-linux-s390x-gnu@4.62.2': optional: true '@rollup/rollup-linux-x64-gnu@4.6.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.60.4': + '@rollup/rollup-linux-x64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-x64-musl@4.60.4': + '@rollup/rollup-linux-x64-musl@4.62.2': optional: true - '@rollup/rollup-openbsd-x64@4.60.4': + '@rollup/rollup-openbsd-x64@4.62.2': optional: true - '@rollup/rollup-openharmony-arm64@4.60.4': + '@rollup/rollup-openharmony-arm64@4.62.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.4': + '@rollup/rollup-win32-arm64-msvc@4.62.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.4': + '@rollup/rollup-win32-ia32-msvc@4.62.2': optional: true - '@rollup/rollup-win32-x64-gnu@4.60.4': + '@rollup/rollup-win32-x64-gnu@4.62.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.4': + '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true '@rtsao/scc@1.1.0': {} '@sec-ant/readable-stream@0.4.1': {} - '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.3(typescript@5.9.3))': + '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.5(typescript@6.0.3))': dependencies: conventional-changelog-angular: 8.1.0 conventional-changelog-writer: 8.2.0 @@ -9406,7 +9509,7 @@ snapshots: import-from-esm: 2.0.0 lodash-es: 4.17.22 micromatch: 4.0.8 - semantic-release: 25.0.3(typescript@5.9.3) + semantic-release: 25.0.5(typescript@6.0.3) transitivePeerDependencies: - supports-color @@ -9414,7 +9517,7 @@ snapshots: '@semantic-release/error@4.0.0': {} - '@semantic-release/git@10.0.1(semantic-release@25.0.3(typescript@5.9.3))': + '@semantic-release/git@10.0.1(semantic-release@25.0.5(typescript@6.0.3))': dependencies: '@semantic-release/error': 3.0.0 aggregate-error: 3.1.0 @@ -9424,68 +9527,69 @@ snapshots: lodash: 4.17.21 micromatch: 4.0.8 p-reduce: 2.1.0 - semantic-release: 25.0.3(typescript@5.9.3) + semantic-release: 25.0.5(typescript@6.0.3) transitivePeerDependencies: - supports-color - '@semantic-release/github@12.0.2(semantic-release@25.0.3(typescript@5.9.3))': + '@semantic-release/github@12.0.8(semantic-release@25.0.5(typescript@6.0.3))': dependencies: '@octokit/core': 7.0.6 '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) - '@octokit/plugin-retry': 8.0.3(@octokit/core@7.0.6) + '@octokit/plugin-retry': 8.1.0(@octokit/core@7.0.6) '@octokit/plugin-throttling': 11.0.3(@octokit/core@7.0.6) '@semantic-release/error': 4.0.0 aggregate-error: 5.0.0 debug: 4.4.3 dir-glob: 3.0.1 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - issue-parser: 7.0.1 - lodash-es: 4.17.22 + http-proxy-agent: 9.1.0 + https-proxy-agent: 9.1.0 + issue-parser: 7.0.2 + lodash-es: 4.18.1 mime: 4.1.0 p-filter: 4.1.0 - semantic-release: 25.0.3(typescript@5.9.3) - tinyglobby: 0.2.16 - undici: 7.25.0 + semantic-release: 25.0.5(typescript@6.0.3) + tinyglobby: 0.2.17 + undici: 7.28.0 url-join: 5.0.0 transitivePeerDependencies: + - kerberos - supports-color - '@semantic-release/npm@13.1.3(semantic-release@25.0.3(typescript@5.9.3))': + '@semantic-release/npm@13.1.5(semantic-release@25.0.5(typescript@6.0.3))': dependencies: - '@actions/core': 2.0.1 + '@actions/core': 3.0.1 '@semantic-release/error': 4.0.0 aggregate-error: 5.0.0 env-ci: 11.2.0 execa: 9.6.1 - fs-extra: 11.3.5 - lodash-es: 4.17.22 + fs-extra: 11.3.6 + lodash-es: 4.18.1 nerf-dart: 1.0.0 - normalize-url: 8.1.0 - npm: 11.7.0 + normalize-url: 9.0.1 + npm: 11.17.0 rc: 1.2.8 - read-pkg: 10.0.0 - registry-auth-token: 5.1.0 - semantic-release: 25.0.3(typescript@5.9.3) - semver: 7.7.3 - tempy: 3.1.0 + read-pkg: 10.1.0 + registry-auth-token: 5.1.1 + semantic-release: 25.0.5(typescript@6.0.3) + semver: 7.8.4 + tempy: 3.2.0 - '@semantic-release/release-notes-generator@14.1.0(semantic-release@25.0.3(typescript@5.9.3))': + '@semantic-release/release-notes-generator@14.1.1(semantic-release@25.0.5(typescript@6.0.3))': dependencies: - conventional-changelog-angular: 8.1.0 - conventional-changelog-writer: 8.2.0 + conventional-changelog-angular: 8.3.1 + conventional-changelog-writer: 8.4.0 conventional-commits-filter: 5.0.0 - conventional-commits-parser: 6.2.1 + conventional-commits-parser: 6.4.0 debug: 4.4.3 - get-stream: 7.0.1 import-from-esm: 2.0.0 - into-stream: 7.0.0 - lodash-es: 4.17.22 + lodash-es: 4.18.1 read-package-up: 11.0.0 - semantic-release: 25.0.3(typescript@5.9.3) + semantic-release: 25.0.5(typescript@6.0.3) transitivePeerDependencies: - supports-color + '@simple-libs/stream-utils@1.2.0': {} + '@sinclair/typebox@0.27.10': {} '@sindresorhus/is@4.6.0': {} @@ -9505,9 +9609,9 @@ snapshots: tslib: 2.8.1 typed-inject: 5.0.0 - '@stryker-mutator/core@9.6.1(@types/node@25.9.1)': + '@stryker-mutator/core@9.6.1(@types/node@25.9.4)': dependencies: - '@inquirer/prompts': 8.4.2(@types/node@25.9.1) + '@inquirer/prompts': 8.4.2(@types/node@25.9.4) '@stryker-mutator/api': 9.6.1 '@stryker-mutator/instrumenter': 9.6.1 '@stryker-mutator/util': 9.6.1 @@ -9554,24 +9658,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@stryker-mutator/typescript-checker@9.6.1(@stryker-mutator/core@9.6.1(@types/node@25.9.1))(typescript@5.9.3)': + '@stryker-mutator/typescript-checker@9.6.1(@stryker-mutator/core@9.6.1(@types/node@25.9.4))(typescript@6.0.3)': dependencies: '@stryker-mutator/api': 9.6.1 - '@stryker-mutator/core': 9.6.1(@types/node@25.9.1) + '@stryker-mutator/core': 9.6.1(@types/node@25.9.4) '@stryker-mutator/util': 9.6.1 semver: 7.7.4 - typescript: 5.9.3 + typescript: 6.0.3 '@stryker-mutator/util@9.6.1': {} - '@stryker-mutator/vitest-runner@9.6.1(@stryker-mutator/core@9.6.1(@types/node@25.9.1))(vitest@4.1.7)': + '@stryker-mutator/vitest-runner@9.6.1(@stryker-mutator/core@9.6.1(@types/node@25.9.4))(vitest@4.1.9)': dependencies: '@stryker-mutator/api': 9.6.1 - '@stryker-mutator/core': 9.6.1(@types/node@25.9.1) + '@stryker-mutator/core': 9.6.1(@types/node@25.9.4) '@stryker-mutator/util': 9.6.1 semver: 7.7.4 tslib: 2.8.1 - vitest: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@25.9.1)(typescript@5.9.3))(vite@7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) + vitest: 4.1.9(@types/node@25.9.4)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@25.9.4)(typescript@6.0.3))(vite@7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0)) '@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(jiti@2.7.0))': dependencies: @@ -9587,81 +9691,81 @@ snapshots: dependencies: tslib: 2.8.1 - '@tailwindcss/node@4.3.0': + '@tailwindcss/node@4.3.2': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.21.3 + enhanced-resolve: 5.21.6 jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.3.0 + tailwindcss: 4.3.2 - '@tailwindcss/oxide-android-arm64@4.3.0': + '@tailwindcss/oxide-android-arm64@4.3.2': optional: true - '@tailwindcss/oxide-darwin-arm64@4.3.0': + '@tailwindcss/oxide-darwin-arm64@4.3.2': optional: true - '@tailwindcss/oxide-darwin-x64@4.3.0': + '@tailwindcss/oxide-darwin-x64@4.3.2': optional: true - '@tailwindcss/oxide-freebsd-x64@4.3.0': + '@tailwindcss/oxide-freebsd-x64@4.3.2': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + '@tailwindcss/oxide-linux-arm64-musl@4.3.2': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + '@tailwindcss/oxide-linux-x64-gnu@4.3.2': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.3.0': + '@tailwindcss/oxide-linux-x64-musl@4.3.2': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.3.0': + '@tailwindcss/oxide-wasm32-wasi@4.3.2': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + '@tailwindcss/oxide-win32-x64-msvc@4.3.2': optional: true - '@tailwindcss/oxide@4.3.0': + '@tailwindcss/oxide@4.3.2': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.3.0 - '@tailwindcss/oxide-darwin-arm64': 4.3.0 - '@tailwindcss/oxide-darwin-x64': 4.3.0 - '@tailwindcss/oxide-freebsd-x64': 4.3.0 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 - '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 - '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 - '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 - '@tailwindcss/oxide-linux-x64-musl': 4.3.0 - '@tailwindcss/oxide-wasm32-wasi': 4.3.0 - '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 - '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 - - '@tailwindcss/postcss@4.3.0': + '@tailwindcss/oxide-android-arm64': 4.3.2 + '@tailwindcss/oxide-darwin-arm64': 4.3.2 + '@tailwindcss/oxide-darwin-x64': 4.3.2 + '@tailwindcss/oxide-freebsd-x64': 4.3.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.2 + '@tailwindcss/oxide-linux-x64-musl': 4.3.2 + '@tailwindcss/oxide-wasm32-wasi': 4.3.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 + + '@tailwindcss/postcss@4.3.2': dependencies: '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.3.0 - '@tailwindcss/oxide': 4.3.0 - postcss: 8.5.14 - tailwindcss: 4.3.0 + '@tailwindcss/node': 4.3.2 + '@tailwindcss/oxide': 4.3.2 + postcss: 8.5.16 + tailwindcss: 4.3.2 - '@tanstack/query-core@5.100.14': {} + '@tanstack/query-core@5.101.2': {} - '@tanstack/react-query@5.100.14(react@19.2.6)': + '@tanstack/react-query@5.101.2(react@19.2.7)': dependencies: - '@tanstack/query-core': 5.100.14 - react: 19.2.6 + '@tanstack/query-core': 5.101.2 + react: 19.2.7 '@testing-library/dom@10.4.1': dependencies: @@ -9674,44 +9778,44 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@babel/runtime': 7.28.4 '@testing-library/dom': 10.4.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.15 - '@types/react-dom': 19.2.3(@types/react@19.2.15) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3)': + '@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3)': dependencies: - '@trpc/server': 11.17.0(typescript@5.9.3) - typescript: 5.9.3 + '@trpc/server': 11.18.0(typescript@6.0.3) + typescript: 6.0.3 - '@trpc/next@11.17.0(@tanstack/react-query@5.100.14(react@19.2.6))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.17.0(@tanstack/react-query@5.100.14(react@19.2.6))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.17.0(typescript@5.9.3))(react@19.2.6)(typescript@5.9.3))(@trpc/server@11.17.0(typescript@5.9.3))(next@16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3)': + '@trpc/next@11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/react-query@11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(next@16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@trpc/client': 11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3) - '@trpc/server': 11.17.0(typescript@5.9.3) - next: 16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - typescript: 5.9.3 + '@trpc/client': 11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3) + '@trpc/server': 11.18.0(typescript@6.0.3) + next: 16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + typescript: 6.0.3 optionalDependencies: - '@tanstack/react-query': 5.100.14(react@19.2.6) - '@trpc/react-query': 11.17.0(@tanstack/react-query@5.100.14(react@19.2.6))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.17.0(typescript@5.9.3))(react@19.2.6)(typescript@5.9.3) + '@tanstack/react-query': 5.101.2(react@19.2.7) + '@trpc/react-query': 11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3) - '@trpc/react-query@11.17.0(@tanstack/react-query@5.100.14(react@19.2.6))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.17.0(typescript@5.9.3))(react@19.2.6)(typescript@5.9.3)': + '@trpc/react-query@11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@tanstack/react-query': 5.100.14(react@19.2.6) - '@trpc/client': 11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3) - '@trpc/server': 11.17.0(typescript@5.9.3) - react: 19.2.6 - typescript: 5.9.3 + '@tanstack/react-query': 5.101.2(react@19.2.7) + '@trpc/client': 11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3) + '@trpc/server': 11.18.0(typescript@6.0.3) + react: 19.2.7 + typescript: 6.0.3 - '@trpc/server@11.17.0(typescript@5.9.3)': + '@trpc/server@11.18.0(typescript@6.0.3)': dependencies: - typescript: 5.9.3 + typescript: 6.0.3 '@ts-morph/common@0.27.0': dependencies: @@ -9721,7 +9825,7 @@ snapshots: '@tsconfig/next@2.0.6': {} - '@tybys/wasm-util@0.10.2': + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true @@ -9751,11 +9855,11 @@ snapshots: '@types/better-sqlite3@7.6.13': dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 '@types/binary-split@1.0.3': dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 '@types/chai@5.2.3': dependencies: @@ -9841,7 +9945,7 @@ snapshots: '@types/jsdom@28.0.3': dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 '@types/tough-cookie': 4.0.5 parse5: 8.0.1 undici-types: 7.25.0 @@ -9862,9 +9966,9 @@ snapshots: '@types/node-schedule@2.1.8': dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@types/node@25.9.1': + '@types/node@25.9.4': dependencies: undici-types: 7.24.6 @@ -9875,25 +9979,27 @@ snapshots: '@types/qrcode@1.5.6': dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 - '@types/react-dom@19.2.3(@types/react@19.2.15)': + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 - '@types/react@19.2.15': + '@types/react@19.2.17': dependencies: csstype: 3.2.3 '@types/readable-stream@4.0.23': dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 '@types/set-cookie-parser@2.4.10': dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 + optional: true - '@types/statuses@2.0.6': {} + '@types/statuses@2.0.6': + optional: true '@types/tough-cookie@4.0.5': {} @@ -9907,7 +10013,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 '@types/yargs-parser@21.0.3': {} @@ -9915,161 +10021,172 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/type-utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.60.0 + '@typescript-eslint/parser': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/type-utils': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.62.1 eslint: 9.39.4(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.60.0 + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.62.1 debug: 4.4.3 eslint: 9.39.4(jiti@2.7.0) - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.60.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.62.1(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) - '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) + '@typescript-eslint/types': 8.62.1 debug: 4.4.3 - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.60.0': + '@typescript-eslint/scope-manager@8.62.1': dependencies: - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/visitor-keys': 8.60.0 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 - '@typescript-eslint/tsconfig-utils@8.60.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.62.1(typescript@6.0.3)': dependencies: - typescript: 5.9.3 + typescript: 6.0.3 - '@typescript-eslint/type-utils@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 eslint: 9.39.4(jiti@2.7.0) - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.57.2': {} - '@typescript-eslint/types@8.60.0': {} + '@typescript-eslint/types@8.62.1': {} - '@typescript-eslint/typescript-estree@8.60.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.62.1(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.60.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/visitor-keys': 8.60.0 + '@typescript-eslint/project-service': 8.62.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.1 + semver: 7.8.5 tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/utils@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) eslint: 9.39.4(jiti@2.7.0) - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.60.0': + '@typescript-eslint/visitor-keys@8.62.1': dependencies: - '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/types': 8.62.1 eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.1': {} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': optional: true - '@unrs/resolver-binding-android-arm64@1.11.1': + '@unrs/resolver-binding-freebsd-x64@1.12.2': optional: true - '@unrs/resolver-binding-darwin-arm64@1.11.1': + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': optional: true - '@unrs/resolver-binding-darwin-x64@1.11.1': + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': optional: true - '@unrs/resolver-binding-freebsd-x64@1.11.1': + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': optional: true - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': optional: true - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': optional: true - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + '@unrs/resolver-binding-linux-x64-musl@1.12.2': optional: true - '@unrs/resolver-binding-linux-x64-musl@1.11.1': + '@unrs/resolver-binding-openharmony-arm64@1.12.2': optional: true - '@unrs/resolver-binding-wasm32-wasi@1.11.1': + '@unrs/resolver-binding-wasm32-wasi@1.12.2': dependencies: - '@napi-rs/wasm-runtime': 0.2.12 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': optional: true - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': optional: true - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true - '@vitejs/plugin-react@5.2.0(vite@7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0))': + '@vitejs/plugin-react@5.2.0(vite@7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -10077,63 +10194,63 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) + vite: 7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.1.7(vitest@4.1.7)': + '@vitest/coverage-v8@4.1.9(vitest@4.1.9)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.7 + '@vitest/utils': 4.1.9 ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-reports: 3.2.0 magicast: 0.5.2 - obug: 2.1.1 + obug: 2.1.2 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@25.9.1)(typescript@5.9.3))(vite@7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) + vitest: 4.1.9(@types/node@25.9.4)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@25.9.4)(typescript@6.0.3))(vite@7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0)) - '@vitest/expect@4.1.7': + '@vitest/expect@4.1.9': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.7(msw@2.14.6(@types/node@25.9.1)(typescript@5.9.3))(vite@7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0))': + '@vitest/mocker@4.1.9(msw@2.14.6(@types/node@25.9.4)(typescript@6.0.3))(vite@7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.7 + '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.14.6(@types/node@25.9.1)(typescript@5.9.3) - vite: 7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) + msw: 2.14.6(@types/node@25.9.4)(typescript@6.0.3) + vite: 7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0) - '@vitest/pretty-format@4.1.7': + '@vitest/pretty-format@4.1.9': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.7': + '@vitest/runner@4.1.9': dependencies: - '@vitest/utils': 4.1.7 + '@vitest/utils': 4.1.9 pathe: 2.0.3 - '@vitest/snapshot@4.1.7': + '@vitest/snapshot@4.1.9': dependencies: - '@vitest/pretty-format': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.7': {} + '@vitest/spy@4.1.9': {} - '@vitest/utils@4.1.7': + '@vitest/utils@4.1.9': dependencies: - '@vitest/pretty-format': 4.1.7 + '@vitest/pretty-format': 4.1.9 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -10217,18 +10334,20 @@ snapshots: '@xtuc/long@4.2.2': {} - '@xyflow/react@12.10.2(@types/react@19.2.15)(immer@11.1.4)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@xyflow/react@12.11.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(immer@11.1.11)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@xyflow/system': 0.0.76 + '@xyflow/system': 0.0.78 classcat: 5.0.5 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - zustand: 4.5.7(@types/react@19.2.15)(immer@11.1.4)(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + zustand: 4.5.7(@types/react@19.2.17)(immer@11.1.11)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) transitivePeerDependencies: - - '@types/react' - immer - '@xyflow/system@0.0.76': + '@xyflow/system@0.0.78': dependencies: '@types/d3-drag': 3.0.7 '@types/d3-interpolate': 3.0.4 @@ -10249,9 +10368,9 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-import-phases@1.0.4(acorn@8.16.0): + acorn-import-phases@1.0.4(acorn@8.17.0): dependencies: - acorn: 8.16.0 + acorn: 8.17.0 acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -10259,7 +10378,9 @@ snapshots: acorn@8.16.0: {} - agent-base@7.1.4: {} + acorn@8.17.0: {} + + agent-base@9.0.0: {} aggregate-error@3.1.0: dependencies: @@ -10301,7 +10422,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 3.1.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -10309,7 +10430,7 @@ snapshots: ansi-colors@4.1.3: {} - ansi-escapes@7.2.0: + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -10380,7 +10501,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 es-shim-unscopables: 1.1.0 array.prototype.flat@1.3.3: @@ -10431,11 +10552,13 @@ snapshots: async-function@1.0.0: {} + atomically@1.7.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 - axe-core@4.11.4: {} + axe-core@4.12.1: {} axobject-query@4.1.0: {} @@ -10454,13 +10577,11 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.29: {} - - baseline-browser-mapping@2.10.33: {} + baseline-browser-mapping@2.10.42: {} before-after-hook@4.0.0: {} - better-sqlite3@12.10.0: + better-sqlite3@12.11.1: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 @@ -10492,15 +10613,15 @@ snapshots: inherits: 2.0.4 readable-stream: 4.7.0 - body-parser@2.2.2: + body-parser@2.3.0: dependencies: bytes: 3.1.2 - content-type: 1.0.5 + content-type: 2.0.0 debug: 4.4.3 http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 on-finished: 2.4.1 - qs: 6.15.2 + qs: 6.15.3 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -10512,7 +10633,7 @@ snapshots: multicast-dns: 7.2.5 multicast-dns-service-types: 1.1.0 - bonjour-service@1.4.0: + bonjour-service@1.4.2: dependencies: fast-deep-equal: 3.1.3 multicast-dns: 7.2.5 @@ -10534,18 +10655,18 @@ snapshots: broker-factory@3.1.14: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 fast-unique-numbers: 9.0.27 tslib: 2.8.1 worker-factory: 7.0.49 - browserslist@4.28.2: + browserslist@4.28.5: dependencies: - baseline-browser-mapping: 2.10.33 - caniuse-lite: 1.0.30001793 - electron-to-chromium: 1.5.364 - node-releases: 2.0.46 - update-browserslist-db: 1.2.3(browserslist@4.28.2) + baseline-browser-mapping: 2.10.42 + caniuse-lite: 1.0.30001802 + electron-to-chromium: 1.5.387 + node-releases: 2.0.50 + update-browserslist-db: 1.2.3(browserslist@4.28.5) buffer-from@1.1.2: {} @@ -10595,9 +10716,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001792: {} - - caniuse-lite@1.0.30001793: {} + caniuse-lite@1.0.30001802: {} cbor-extract@2.2.2: dependencies: @@ -10727,6 +10846,7 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + optional: true cliui@9.0.1: dependencies: @@ -10800,6 +10920,19 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 + conf@10.2.0: + dependencies: + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + atomically: 1.7.0 + debounce-fn: 4.0.0 + dot-prop: 6.0.1 + env-paths: 2.2.1 + json-schema-typed: 7.0.3 + onetime: 5.1.2 + pkg-up: 3.1.0 + semver: 7.8.5 + config-chain@1.1.13: dependencies: ini: 1.3.8 @@ -10815,6 +10948,10 @@ snapshots: dependencies: compare-func: 2.0.0 + conventional-changelog-angular@8.3.1: + dependencies: + compare-func: 2.0.0 + conventional-changelog-conventionalcommits@9.3.1: dependencies: compare-func: 2.0.0 @@ -10824,7 +10961,15 @@ snapshots: conventional-commits-filter: 5.0.0 handlebars: 4.7.8 meow: 13.2.0 - semver: 7.7.3 + semver: 7.8.4 + + conventional-changelog-writer@8.4.0: + dependencies: + '@simple-libs/stream-utils': 1.2.0 + conventional-commits-filter: 5.0.0 + handlebars: 4.7.9 + meow: 13.2.0 + semver: 7.8.4 conventional-commits-filter@5.0.0: {} @@ -10832,6 +10977,11 @@ snapshots: dependencies: meow: 13.2.0 + conventional-commits-parser@6.4.0: + dependencies: + '@simple-libs/stream-utils': 1.2.0 + meow: 13.2.0 + convert-hrtime@5.0.0: {} convert-source-map@2.0.0: {} @@ -10842,7 +10992,8 @@ snapshots: cookie@0.7.2: {} - cookie@1.1.1: {} + cookie@1.1.1: + optional: true copy-anything@4.0.5: dependencies: @@ -10864,32 +11015,23 @@ snapshots: yaml: 1.10.3 optional: true - cosmiconfig@8.3.6(typescript@5.9.3): + cosmiconfig@8.3.6(typescript@6.0.3): dependencies: import-fresh: 3.3.1 js-yaml: 4.2.0 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.9.3 - - cosmiconfig@9.0.0(typescript@5.9.3): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - parse-json: 5.2.0 - optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 - cosmiconfig@9.0.1(typescript@5.9.3): + cosmiconfig@9.0.2(typescript@6.0.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.2.0 + js-yaml: 4.3.0 parse-json: 5.2.0 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 cron-parser@4.9.0: dependencies: @@ -10984,8 +11126,6 @@ snapshots: damerau-levenshtein@1.0.8: {} - data-uri-to-buffer@4.0.1: {} - data-urls@7.0.0(@noble/hashes@1.8.0): dependencies: whatwg-mimetype: 5.0.0 @@ -11013,6 +11153,10 @@ snapshots: date-fns@3.6.0: {} + debounce-fn@4.0.0: + dependencies: + mimic-fn: 3.1.0 + debug@3.2.7: dependencies: ms: 2.1.3 @@ -11062,6 +11206,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + define-lazy-prop@2.0.0: {} + define-lazy-prop@3.0.0: {} define-properties@1.2.1: @@ -11113,6 +11259,10 @@ snapshots: dependencies: is-obj: 2.0.0 + dot-prop@6.0.1: + dependencies: + is-obj: 2.0.0 + dotenv@17.4.2: {} drizzle-kit@0.31.10: @@ -11120,12 +11270,12 @@ snapshots: '@drizzle-team/brocli': 0.10.2 '@esbuild-kit/esm-loader': 2.6.5 esbuild: 0.25.12 - tsx: 4.22.3 + tsx: 4.23.0 - drizzle-orm@0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@12.10.0): + drizzle-orm@0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1): optionalDependencies: '@types/better-sqlite3': 7.6.13 - better-sqlite3: 12.10.0 + better-sqlite3: 12.11.1 dunder-proto@1.0.1: dependencies: @@ -11139,16 +11289,9 @@ snapshots: duplexer@0.1.2: {} - eciesjs@0.4.18: - dependencies: - '@ecies/ciphers': 0.2.6(@noble/ciphers@1.3.0) - '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.7 - '@noble/hashes': 1.8.0 - ee-first@1.1.1: {} - electron-to-chromium@1.5.364: {} + electron-to-chromium@1.5.387: {} emoji-regex@10.6.0: {} @@ -11164,12 +11307,12 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.21.3: + enhanced-resolve@5.21.6: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 - enhanced-resolve@5.22.1: + enhanced-resolve@5.24.2: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -11242,7 +11385,7 @@ snapshots: set-proto: 1.0.0 stop-iteration-iterator: 1.1.0 string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 + string.prototype.trimend: 1.0.10 string.prototype.trimstart: 1.0.8 typed-array-buffer: 1.0.3 typed-array-byte-length: 1.0.3 @@ -11263,10 +11406,10 @@ snapshots: data-view-byte-offset: 1.0.1 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 es-set-tostringtag: 2.1.0 es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 + function.prototype.name: 1.2.0 get-intrinsic: 1.3.0 get-proto: 1.0.1 get-symbol-description: 1.1.0 @@ -11275,7 +11418,7 @@ snapshots: has-property-descriptors: 1.0.2 has-proto: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.3 + hasown: 2.0.2 internal-slot: 1.1.0 is-array-buffer: 3.0.5 is-callable: 1.2.7 @@ -11298,15 +11441,15 @@ snapshots: safe-regex-test: 1.1.0 set-proto: 1.0.0 stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 string.prototype.trimstart: 1.0.8 typed-array-buffer: 1.0.3 typed-array-byte-length: 1.0.3 typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 + typed-array-length: 1.0.8 unbox-primitive: 1.1.0 - which-typed-array: 1.1.20 + which-typed-array: 1.1.22 es-define-property@1.0.1: {} @@ -11333,10 +11476,16 @@ snapshots: es-module-lexer@2.1.0: {} + es-module-lexer@2.3.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + es-set-tostringtag@2.1.0: dependencies: es-errors: 1.3.0 @@ -11354,7 +11503,7 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - es-toolkit@1.45.1: {} + es-toolkit@1.49.0: {} esbuild@0.18.20: optionalDependencies: @@ -11439,34 +11588,34 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 - esbuild@0.28.0: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.28.0 - '@esbuild/android-arm': 0.28.0 - '@esbuild/android-arm64': 0.28.0 - '@esbuild/android-x64': 0.28.0 - '@esbuild/darwin-arm64': 0.28.0 - '@esbuild/darwin-x64': 0.28.0 - '@esbuild/freebsd-arm64': 0.28.0 - '@esbuild/freebsd-x64': 0.28.0 - '@esbuild/linux-arm': 0.28.0 - '@esbuild/linux-arm64': 0.28.0 - '@esbuild/linux-ia32': 0.28.0 - '@esbuild/linux-loong64': 0.28.0 - '@esbuild/linux-mips64el': 0.28.0 - '@esbuild/linux-ppc64': 0.28.0 - '@esbuild/linux-riscv64': 0.28.0 - '@esbuild/linux-s390x': 0.28.0 - '@esbuild/linux-x64': 0.28.0 - '@esbuild/netbsd-arm64': 0.28.0 - '@esbuild/netbsd-x64': 0.28.0 - '@esbuild/openbsd-arm64': 0.28.0 - '@esbuild/openbsd-x64': 0.28.0 - '@esbuild/openharmony-arm64': 0.28.0 - '@esbuild/sunos-x64': 0.28.0 - '@esbuild/win32-arm64': 0.28.0 - '@esbuild/win32-ia32': 0.28.0 - '@esbuild/win32-x64': 0.28.0 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escalade@3.2.0: {} @@ -11478,20 +11627,20 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-next@16.2.6(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): + eslint-config-next@16.2.10(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@next/eslint-plugin-next': 16.2.6 + '@next/eslint-plugin-next': 16.2.10 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) globals: 16.4.0 - typescript-eslint: 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + typescript-eslint: 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-webpack @@ -11502,11 +11651,11 @@ snapshots: dependencies: debug: 3.2.7 is-core-module: 2.16.2 - resolve: 2.0.0-next.6 + resolve: 2.0.0-next.7 transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -11514,25 +11663,25 @@ snapshots: get-tsconfig: 4.14.0 is-bun-module: 2.0.0 stable-hash: 0.0.5 - tinyglobby: 0.2.16 - unrs-resolver: 1.11.1 + tinyglobby: 0.2.17 + unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -11543,8 +11692,8 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) - hasown: 2.0.3 + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 minimatch: 3.1.5 @@ -11552,10 +11701,10 @@ snapshots: object.groupby: 1.0.3 object.values: 1.2.1 semver: 6.3.1 - string.prototype.trimend: 1.0.9 + string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -11567,12 +11716,12 @@ snapshots: array-includes: 3.1.9 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.11.4 + axe-core: 4.12.1 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 eslint: 9.39.4(jiti@2.7.0) - hasown: 2.0.3 + hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 minimatch: 3.1.5 @@ -11776,7 +11925,7 @@ snapshots: express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.2 + body-parser: 2.3.0 content-disposition: 1.1.0 content-type: 1.0.5 cookie: 0.7.2 @@ -11795,8 +11944,8 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.15.2 - range-parser: 1.2.1 + qs: 6.15.3 + range-parser: 1.3.0 router: 2.2.0 send: 1.2.1 serve-static: 2.2.1 @@ -11842,12 +11991,12 @@ snapshots: fast-unique-numbers@9.0.27: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 tslib: 2.8.1 fast-uri@3.1.0: {} - fast-uri@3.1.2: {} + fast-uri@3.1.3: {} fast-wrap-ansi@0.2.0: dependencies: @@ -11856,6 +12005,7 @@ snapshots: fast-wrap-ansi@0.2.2: dependencies: fast-string-width: 3.0.2 + optional: true fastq@1.20.1: dependencies: @@ -11865,10 +12015,9 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 figures@2.0.0: dependencies: @@ -11905,6 +12054,10 @@ snapshots: dependencies: locate-path: 2.0.0 + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -11936,24 +12089,15 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - forwarded@0.2.0: {} fresh@2.0.0: {} - from2@2.3.0: - dependencies: - inherits: 2.0.4 - readable-stream: 2.3.8 - from@0.1.7: {} fs-constants@1.0.0: {} - fs-extra@11.3.5: + fs-extra@11.3.6: dependencies: graceful-fs: 4.2.11 jsonfile: 6.2.1 @@ -11975,6 +12119,18 @@ snapshots: hasown: 2.0.2 is-callable: 1.2.7 + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 + functions-have-names@1.2.3: {} futoin-hkdf@1.5.3: {} @@ -12013,8 +12169,6 @@ snapshots: get-stream@6.0.1: {} - get-stream@7.0.1: {} - get-stream@8.0.1: {} get-stream@9.0.1: @@ -12070,7 +12224,7 @@ snapshots: globals@16.4.0: {} - globals@17.6.0: {} + globals@17.7.0: {} globalthis@1.0.4: dependencies: @@ -12085,7 +12239,8 @@ snapshots: graceful-fs@4.2.11: {} - graphql@16.14.0: {} + graphql@16.14.2: + optional: true h3@1.15.11: dependencies: @@ -12108,6 +12263,15 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + hap-nodejs@1.2.0: dependencies: '@homebridge/ciao': 1.3.8 @@ -12147,7 +12311,7 @@ snapshots: dependencies: function-bind: 1.1.2 - hasown@2.0.3: + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -12178,7 +12342,8 @@ snapshots: headers-polyfill@5.0.1: dependencies: '@types/set-cookie-parser': 2.4.10 - set-cookie-parser: 3.1.0 + set-cookie-parser: 3.1.1 + optional: true help-me@5.0.0: {} @@ -12192,7 +12357,7 @@ snapshots: highlight.js@10.7.3: {} - hono@4.12.23: {} + hono@4.12.28: {} hook-std@4.0.0: {} @@ -12200,9 +12365,9 @@ snapshots: dependencies: lru-cache: 10.4.3 - hosted-git-info@9.0.2: + hosted-git-info@9.0.3: dependencies: - lru-cache: 11.3.6 + lru-cache: 11.5.1 html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0): dependencies: @@ -12222,18 +12387,22 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - http-proxy-agent@7.0.2: + http-proxy-agent@9.1.0: dependencies: - agent-base: 7.1.4 + agent-base: 9.0.0 debug: 4.4.3 + proxy-agent-negotiate: 1.1.0 transitivePeerDependencies: + - kerberos - supports-color - https-proxy-agent@7.0.6: + https-proxy-agent@9.1.0: dependencies: - agent-base: 7.1.4 + agent-base: 9.0.0 debug: 4.4.3 + proxy-agent-negotiate: 1.1.0 transitivePeerDependencies: + - kerberos - supports-color human-signals@2.1.0: {} @@ -12246,7 +12415,7 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.7.2: + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 @@ -12256,9 +12425,7 @@ snapshots: ignore@7.0.5: {} - immer@10.2.0: {} - - immer@11.1.4: {} + immer@11.1.11: {} import-fresh@3.3.1: dependencies: @@ -12300,11 +12467,6 @@ snapshots: internmap@2.0.3: {} - into-stream@7.0.0: - dependencies: - from2: 2.3.0 - p-is-promise: 3.0.0 - ip-address@10.2.0: {} ipaddr.js@1.9.1: {} @@ -12351,7 +12513,7 @@ snapshots: is-bun-module@2.0.0: dependencies: - semver: 7.8.1 + semver: 7.8.5 is-callable@1.2.7: {} @@ -12361,7 +12523,7 @@ snapshots: is-core-module@2.16.2: dependencies: - hasown: 2.0.3 + hasown: 2.0.4 is-data-view@1.0.2: dependencies: @@ -12376,8 +12538,14 @@ snapshots: is-decimal@2.0.1: {} + is-docker@2.2.1: {} + is-docker@3.0.0: {} + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -12418,7 +12586,8 @@ snapshots: is-negative-zero@2.0.3: {} - is-node-process@1.2.0: {} + is-node-process@1.2.0: + optional: true is-number-object@1.1.1: dependencies: @@ -12444,7 +12613,7 @@ snapshots: call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 - hasown: 2.0.3 + hasown: 2.0.2 is-regexp@3.1.0: {} @@ -12494,6 +12663,10 @@ snapshots: is-what@5.5.0: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 @@ -12506,7 +12679,7 @@ snapshots: isexe@3.1.5: {} - issue-parser@7.0.1: + issue-parser@7.0.2: dependencies: lodash.capitalize: 4.2.1 lodash.escaperegexp: 4.1.2 @@ -12555,7 +12728,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -12581,6 +12754,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + jsdom@29.1.1(@noble/hashes@1.8.0): dependencies: '@asamuzakjp/css-color': 5.1.11 @@ -12621,10 +12798,14 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@7.0.3: {} + json-schema-typed@8.0.2: {} json-stable-stringify-without-jsonify@1.0.1: {} + json-with-bigint@3.5.8: {} + json5@1.0.2: dependencies: minimist: 1.2.8 @@ -12748,6 +12929,11 @@ snapshots: p-locate: 2.0.0 path-exists: 3.0.0 + locate-path@3.0.0: + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -12788,7 +12974,7 @@ snapshots: log-update@6.1.0: dependencies: - ansi-escapes: 7.2.0 + ansi-escapes: 7.3.0 cli-cursor: 5.0.0 slice-ansi: 7.1.2 strip-ansi: 7.2.0 @@ -12808,13 +12994,15 @@ snapshots: lru-cache@11.3.6: {} + lru-cache@11.5.1: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 - lucide-react@1.17.0(react@19.2.6): + lucide-react@1.23.0(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 luxon@3.7.2: {} @@ -12827,24 +13015,24 @@ snapshots: magicast@0.5.2: dependencies: '@babel/parser': 7.29.7 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 source-map-js: 1.2.1 - make-asynchronous@1.0.1: + make-asynchronous@1.1.0: dependencies: p-event: 6.0.1 type-fest: 4.41.0 - web-worker: 1.2.0 + web-worker: 1.5.0 make-dir@4.0.0: dependencies: - semver: 7.8.1 + semver: 7.8.5 map-stream@0.0.7: {} marked-terminal@7.3.0(marked@15.0.12): dependencies: - ansi-escapes: 7.2.0 + ansi-escapes: 7.3.0 ansi-regex: 6.2.2 chalk: 5.6.2 cli-highlight: 2.1.11 @@ -13116,6 +13304,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-fn@3.1.0: {} + mimic-fn@4.0.0: {} mimic-function@5.0.1: {} @@ -13181,14 +13371,14 @@ snapshots: ms@2.1.3: {} - msw@2.14.6(@types/node@25.9.1)(typescript@5.9.3): + msw@2.14.6(@types/node@25.9.4)(typescript@6.0.3): dependencies: - '@inquirer/confirm': 6.1.1(@types/node@25.9.1) + '@inquirer/confirm': 6.1.1(@types/node@25.9.4) '@mswjs/interceptors': 0.41.9 '@open-draft/deferred-promise': 3.0.0 '@types/statuses': 2.0.6 cookie: 1.1.1 - graphql: 16.14.0 + graphql: 16.14.2 headers-polyfill: 5.0.1 is-node-process: 1.2.0 outvariant: 1.4.3 @@ -13198,13 +13388,14 @@ snapshots: statuses: 2.0.2 strict-event-emitter: 0.5.1 tough-cookie: 6.0.1 - type-fest: 5.7.0 + type-fest: 5.8.0 until-async: 3.0.2 - yargs: 17.7.2 + yargs: 17.7.3 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - '@types/node' + optional: true multicast-dns-service-types@1.1.0: {} @@ -13233,7 +13424,7 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.12: {} + nanoid@3.3.15: {} napi-build-utils@2.0.0: {} @@ -13247,25 +13438,25 @@ snapshots: nerf-dart@1.0.0: {} - next@16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + next@16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@next/env': 16.2.6 + '@next/env': 16.2.10 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.29 - caniuse-lite: 1.0.30001792 + baseline-browser-mapping: 2.10.42 + caniuse-lite: 1.0.30001802 postcss: 8.4.31 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - styled-jsx: 5.1.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + styled-jsx: 5.1.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react@19.2.7) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.6 - '@next/swc-darwin-x64': 16.2.6 - '@next/swc-linux-arm64-gnu': 16.2.6 - '@next/swc-linux-arm64-musl': 16.2.6 - '@next/swc-linux-x64-gnu': 16.2.6 - '@next/swc-linux-x64-musl': 16.2.6 - '@next/swc-win32-arm64-msvc': 16.2.6 - '@next/swc-win32-x64-msvc': 16.2.6 + '@next/swc-darwin-arm64': 16.2.10 + '@next/swc-darwin-x64': 16.2.10 + '@next/swc-linux-arm64-gnu': 16.2.10 + '@next/swc-linux-arm64-musl': 16.2.10 + '@next/swc-linux-x64-gnu': 16.2.10 + '@next/swc-linux-x64-musl': 16.2.10 + '@next/swc-win32-arm64-msvc': 16.2.10 + '@next/swc-win32-x64-msvc': 16.2.10 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -13273,9 +13464,7 @@ snapshots: node-abi@3.92.0: dependencies: - semver: 7.8.1 - - node-domexception@1.0.0: {} + semver: 7.8.5 node-emoji@2.2.0: dependencies: @@ -13284,19 +13473,13 @@ snapshots: emojilib: 2.4.0 skin-tone: 2.0.0 - node-exports-info@1.6.0: + node-exports-info@1.6.2: dependencies: array.prototype.flatmap: 1.3.3 es-errors: 1.3.0 object.entries: 1.1.9 semver: 6.3.1 - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - node-gyp-build-optional-packages@5.1.1: dependencies: detect-libc: 2.1.2 @@ -13309,7 +13492,7 @@ snapshots: mkdirp: 0.5.6 q: 1.1.2 - node-releases@2.0.46: {} + node-releases@2.0.50: {} node-schedule@2.1.1: dependencies: @@ -13320,18 +13503,18 @@ snapshots: normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 - semver: 7.7.3 + semver: 7.8.4 validate-npm-package-license: 3.0.4 normalize-package-data@8.0.0: dependencies: - hosted-git-info: 9.0.2 - semver: 7.7.3 + hosted-git-info: 9.0.3 + semver: 7.8.4 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} - normalize-url@8.1.0: {} + normalize-url@9.0.1: {} npm-run-path@4.0.1: dependencies: @@ -13346,7 +13529,7 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 - npm@11.7.0: {} + npm@11.17.0: {} number-allocator@1.0.14: dependencies: @@ -13355,12 +13538,12 @@ snapshots: transitivePeerDependencies: - supports-color - nuqs@2.8.9(next@16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6): + nuqs@2.8.9(next@16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7): dependencies: '@standard-schema/spec': 1.0.0 - react: 19.2.6 + react: 19.2.7 optionalDependencies: - next: 16.2.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + next: 16.2.10(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) object-assign@4.1.1: {} @@ -13408,7 +13591,9 @@ snapshots: observable-fns@0.6.1: {} - obug@2.1.1: {} + obug@2.1.2: {} + + obug@2.1.3: {} on-finished@2.4.1: dependencies: @@ -13439,6 +13624,12 @@ snapshots: powershell-utils: 0.1.0 wsl-utils: 0.3.1 + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + openapi3-ts@4.4.0: dependencies: yaml: 2.9.0 @@ -13476,7 +13667,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.2.0 - outvariant@1.4.3: {} + outvariant@1.4.3: + optional: true own-keys@1.0.1: dependencies: @@ -13494,8 +13686,6 @@ snapshots: dependencies: p-map: 7.0.4 - p-is-promise@3.0.0: {} - p-limit@1.3.0: dependencies: p-try: 1.0.0 @@ -13512,6 +13702,10 @@ snapshots: dependencies: p-limit: 1.3.0 + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -13598,10 +13792,11 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.3.6 + lru-cache: 11.5.1 minipass: 7.1.3 - path-to-regexp@6.3.0: {} + path-to-regexp@6.3.0: + optional: true path-to-regexp@8.4.2: {} @@ -13624,6 +13819,8 @@ snapshots: picomatch@4.0.4: {} + picomatch@4.0.5: {} + pify@3.0.0: {} pkce-challenge@5.0.1: {} @@ -13633,32 +13830,30 @@ snapshots: find-up: 2.1.0 load-json-file: 4.0.0 + pkg-up@3.1.0: + dependencies: + find-up: 3.0.0 + pngjs@5.0.0: {} pofile@1.1.4: {} possible-typed-array-names@1.1.0: {} - postcss-selector-parser@7.1.1: + postcss-selector-parser@7.1.4: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 postcss@8.4.31: dependencies: - nanoid: 3.3.12 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.14: + postcss@8.5.16: dependencies: - nanoid: 3.3.12 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postcss@8.5.15: - dependencies: - nanoid: 3.3.12 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -13729,6 +13924,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-agent-negotiate@1.1.0: {} + pseudolocale@2.2.0: dependencies: commander: 10.0.1 @@ -13758,11 +13955,16 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + queue-microtask@1.2.3: {} radix3@1.1.2: {} - range-parser@1.2.1: {} + range-parser@1.3.0: {} raw-body@2.5.3: dependencies: @@ -13775,7 +13977,7 @@ snapshots: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 unpipe: 1.0.0 rc@1.2.8: @@ -13792,9 +13994,9 @@ snapshots: csstype: 3.2.3 lodash-es: 4.18.1 - react-dom@19.2.6(react@19.2.6): + react-dom@19.2.7(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 scheduler: 0.27.0 react-is@16.13.1: {} @@ -13807,16 +14009,16 @@ snapshots: react-lifecycles-compat@3.0.4: {} - react-markdown@9.1.0(@types/react@19.2.15)(react@19.2.6): + react-markdown@9.1.0(@types/react@19.2.17)(react@19.2.7): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.2.15 + '@types/react': 19.2.17 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.1 - react: 19.2.6 + react: 19.2.7 remark-parse: 11.0.0 remark-rehype: 11.1.2 unified: 11.0.5 @@ -13825,27 +14027,27 @@ snapshots: transitivePeerDependencies: - supports-color - react-redux@9.2.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1): + react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 - react: 19.2.6 - use-sync-external-store: 1.6.0(react@19.2.6) + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) optionalDependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 redux: 5.0.1 react-refresh@0.18.0: {} - react-textarea-autosize@8.5.9(@types/react@19.2.15)(react@19.2.6): + react-textarea-autosize@8.5.9(@types/react@19.2.17)(react@19.2.7): dependencies: - '@babel/runtime': 7.29.2 - react: 19.2.6 - use-composed-ref: 1.4.0(@types/react@19.2.15)(react@19.2.6) - use-latest: 1.3.0(@types/react@19.2.15)(react@19.2.6) + '@babel/runtime': 7.29.7 + react: 19.2.7 + use-composed-ref: 1.4.0(@types/react@19.2.17)(react@19.2.7) + use-latest: 1.3.0(@types/react@19.2.17)(react@19.2.7) transitivePeerDependencies: - '@types/react' - react@19.2.6: {} + react@19.2.7: {} read-package-up@11.0.0: dependencies: @@ -13856,16 +14058,16 @@ snapshots: read-package-up@12.0.0: dependencies: find-up-simple: 1.0.1 - read-pkg: 10.0.0 - type-fest: 5.3.1 + read-pkg: 10.1.0 + type-fest: 5.7.0 - read-pkg@10.0.0: + read-pkg@10.1.0: dependencies: '@types/normalize-package-data': 2.4.4 normalize-package-data: 8.0.0 parse-json: 8.3.0 - type-fest: 5.3.1 - unicorn-magic: 0.3.0 + type-fest: 5.7.0 + unicorn-magic: 0.4.0 read-pkg@9.0.1: dependencies: @@ -13903,7 +14105,7 @@ snapshots: dependencies: picomatch: 2.3.2 - recast@0.23.11: + recast@0.23.12: dependencies: ast-types: 0.16.1 esprima: 4.0.1 @@ -13911,21 +14113,21 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 - recharts@3.8.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.4)(react@19.2.6)(redux@5.0.1): + recharts@3.9.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.4)(react@19.2.7)(redux@5.0.1): dependencies: - '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1))(react@19.2.6) + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7) clsx: 2.1.1 decimal.js-light: 2.5.1 - es-toolkit: 1.45.1 + es-toolkit: 1.49.0 eventemitter3: 5.0.4 - immer: 10.2.0 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + immer: 11.1.11 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) react-is: 19.2.4 - react-redux: 9.2.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1) - reselect: 5.1.1 + react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1) + reselect: 5.2.0 tiny-invariant: 1.3.3 - use-sync-external-store: 1.6.0(react@19.2.6) + use-sync-external-store: 1.6.0(react@19.2.7) victory-vendor: 37.3.6 transitivePeerDependencies: - '@types/react' @@ -13957,9 +14159,9 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 - registry-auth-token@5.1.0: + registry-auth-token@5.1.1: dependencies: - '@pnpm/npm-conf': 2.3.1 + '@pnpm/npm-conf': 3.0.3 remark-parse@11.0.0: dependencies: @@ -13984,8 +14186,6 @@ snapshots: require-main-filename@2.0.0: {} - reselect@5.1.1: {} - reselect@5.2.0: {} resolve-from@4.0.0: {} @@ -14008,11 +14208,11 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.6: + resolve@2.0.0-next.7: dependencies: es-errors: 1.3.0 is-core-module: 2.16.2 - node-exports-info: 1.6.0 + node-exports-info: 1.6.2 object-keys: 1.1.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -14027,41 +14227,42 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 - rettime@0.11.11: {} + rettime@0.11.11: + optional: true reusify@1.1.0: {} rfdc@1.4.1: {} - rollup@4.60.4: + rollup@4.62.2: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.4 - '@rollup/rollup-android-arm64': 4.60.4 - '@rollup/rollup-darwin-arm64': 4.60.4 - '@rollup/rollup-darwin-x64': 4.60.4 - '@rollup/rollup-freebsd-arm64': 4.60.4 - '@rollup/rollup-freebsd-x64': 4.60.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 - '@rollup/rollup-linux-arm-musleabihf': 4.60.4 - '@rollup/rollup-linux-arm64-gnu': 4.60.4 - '@rollup/rollup-linux-arm64-musl': 4.60.4 - '@rollup/rollup-linux-loong64-gnu': 4.60.4 - '@rollup/rollup-linux-loong64-musl': 4.60.4 - '@rollup/rollup-linux-ppc64-gnu': 4.60.4 - '@rollup/rollup-linux-ppc64-musl': 4.60.4 - '@rollup/rollup-linux-riscv64-gnu': 4.60.4 - '@rollup/rollup-linux-riscv64-musl': 4.60.4 - '@rollup/rollup-linux-s390x-gnu': 4.60.4 - '@rollup/rollup-linux-x64-gnu': 4.60.4 - '@rollup/rollup-linux-x64-musl': 4.60.4 - '@rollup/rollup-openbsd-x64': 4.60.4 - '@rollup/rollup-openharmony-arm64': 4.60.4 - '@rollup/rollup-win32-arm64-msvc': 4.60.4 - '@rollup/rollup-win32-ia32-msvc': 4.60.4 - '@rollup/rollup-win32-x64-gnu': 4.60.4 - '@rollup/rollup-win32-x64-msvc': 4.60.4 + '@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.3 router@2.2.0: @@ -14132,15 +14333,15 @@ snapshots: ajv-formats: 2.1.1(ajv@8.20.0) ajv-keywords: 5.1.0(ajv@8.20.0) - semantic-release@25.0.3(typescript@5.9.3): + semantic-release@25.0.5(typescript@6.0.3): dependencies: - '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.3(typescript@5.9.3)) + '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.5(typescript@6.0.3)) '@semantic-release/error': 4.0.0 - '@semantic-release/github': 12.0.2(semantic-release@25.0.3(typescript@5.9.3)) - '@semantic-release/npm': 13.1.3(semantic-release@25.0.3(typescript@5.9.3)) - '@semantic-release/release-notes-generator': 14.1.0(semantic-release@25.0.3(typescript@5.9.3)) + '@semantic-release/github': 12.0.8(semantic-release@25.0.5(typescript@6.0.3)) + '@semantic-release/npm': 13.1.5(semantic-release@25.0.5(typescript@6.0.3)) + '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.5(typescript@6.0.3)) aggregate-error: 5.0.0 - cosmiconfig: 9.0.0(typescript@5.9.3) + cosmiconfig: 9.0.2(typescript@6.0.3) debug: 4.4.3 env-ci: 11.2.0 execa: 9.6.1 @@ -14149,9 +14350,9 @@ snapshots: get-stream: 6.0.1 git-log-parser: 1.2.1 hook-std: 4.0.0 - hosted-git-info: 9.0.2 + hosted-git-info: 9.0.3 import-from-esm: 2.0.0 - lodash-es: 4.17.22 + lodash-es: 4.18.1 marked: 15.0.12 marked-terminal: 7.3.0(marked@15.0.12) micromatch: 4.0.8 @@ -14159,10 +14360,11 @@ snapshots: p-reduce: 3.0.0 read-package-up: 12.0.0 resolve-from: 5.0.0 - semver: 7.7.3 + semver: 7.8.4 signale: 1.4.0 yargs: 18.0.0 transitivePeerDependencies: + - kerberos - supports-color - typescript @@ -14170,11 +14372,11 @@ snapshots: semver@6.3.1: {} - semver@7.7.3: {} - semver@7.7.4: {} - semver@7.8.1: {} + semver@7.8.4: {} + + semver@7.8.5: {} send@1.2.1: dependencies: @@ -14187,7 +14389,7 @@ snapshots: mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 - range-parser: 1.2.1 + range-parser: 1.3.0 statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -14203,7 +14405,8 @@ snapshots: set-blocking@2.0.0: {} - set-cookie-parser@3.1.0: {} + set-cookie-parser@3.1.1: + optional: true set-function-length@1.2.2: dependencies: @@ -14229,45 +14432,42 @@ snapshots: setprototypeof@1.2.0: {} - shadcn@4.8.2(@types/node@25.9.1)(babel-plugin-macros@3.1.0)(typescript@5.9.3): + shadcn@4.13.0(babel-plugin-macros@3.1.0)(typescript@6.0.3): dependencies: '@babel/core': 7.29.7 '@babel/parser': 7.29.7 '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) - '@dotenvx/dotenvx': 1.70.0 + '@dotenvx/dotenvx': 1.75.1 '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) '@types/validate-npm-package-name': 4.0.2 - browserslist: 4.28.2 + browserslist: 4.28.5 commander: 14.0.3 - cosmiconfig: 9.0.1(typescript@5.9.3) + cosmiconfig: 9.0.2(typescript@6.0.3) dedent: 1.7.2(babel-plugin-macros@3.1.0) deepmerge: 4.3.1 diff: 8.0.4 execa: 9.6.1 fast-glob: 3.3.3 - fs-extra: 11.3.5 + fs-extra: 11.3.6 fuzzysort: 3.1.0 - https-proxy-agent: 7.0.6 kleur: 4.1.5 - msw: 2.14.6(@types/node@25.9.1)(typescript@5.9.3) - node-fetch: 3.3.2 open: 11.0.0 ora: 8.2.0 - postcss: 8.5.15 - postcss-selector-parser: 7.1.1 + postcss: 8.5.16 + postcss-selector-parser: 7.1.4 prompts: 2.4.2 - recast: 0.23.11 + recast: 0.23.12 stringify-object: 5.0.0 tailwind-merge: 3.6.0 ts-morph: 26.0.0 tsconfig-paths: 4.2.0 + undici: 7.28.0 validate-npm-package-name: 7.0.2 zod: 3.25.76 zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: - '@cfworker/json-schema' - - '@types/node' - babel-plugin-macros - supports-color - typescript @@ -14276,7 +14476,7 @@ snapshots: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.8.1 + semver: 7.8.5 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -14315,6 +14515,11 @@ snapshots: es-errors: 1.3.0 object-inspect: 1.13.4 + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-map@1.0.1: dependencies: call-bound: 1.0.4 @@ -14338,6 +14543,14 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -14405,16 +14618,16 @@ snapshots: spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.22 + spdx-license-ids: 3.0.23 spdx-exceptions@2.5.0: {} spdx-expression-parse@3.0.1: dependencies: spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.22 + spdx-license-ids: 3.0.23 - spdx-license-ids@3.0.22: {} + spdx-license-ids@3.0.23: {} split2@1.0.0: dependencies: @@ -14451,7 +14664,8 @@ snapshots: duplexer: 0.1.2 through: 2.3.8 - strict-event-emitter@0.5.1: {} + strict-event-emitter@0.5.1: + optional: true string-argv@0.3.2: {} @@ -14511,12 +14725,23 @@ snapshots: es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 - string.prototype.trimend@1.0.9: + string.prototype.trim@1.2.11: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 + define-data-property: 1.1.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 string.prototype.trimstart@1.0.8: dependencies: @@ -14571,10 +14796,10 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-jsx@5.1.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react@19.2.6): + styled-jsx@5.1.6(@babel/core@7.29.7)(babel-plugin-macros@3.1.0)(react@19.2.7): dependencies: client-only: 0.0.1 - react: 19.2.6 + react: 19.2.7 optionalDependencies: '@babel/core': 7.29.7 babel-plugin-macros: 3.1.0 @@ -14582,7 +14807,7 @@ snapshots: super-regex@1.1.0: dependencies: function-timeout: 1.0.2 - make-asynchronous: 1.0.1 + make-asynchronous: 1.1.0 time-span: 5.1.0 superjson@2.2.6: @@ -14610,13 +14835,15 @@ snapshots: symbol-tree@3.2.4: {} + systeminformation@5.31.13: {} + tagged-tag@1.0.0: {} tailwind-merge@2.6.1: {} tailwind-merge@3.6.0: {} - tailwindcss@4.3.0: {} + tailwindcss@4.3.2: {} tapable@2.3.3: {} @@ -14637,7 +14864,7 @@ snapshots: temp-dir@3.0.0: {} - tempy@3.1.0: + tempy@3.2.0: dependencies: is-stream: 3.0.0 temp-dir: 3.0.0 @@ -14657,7 +14884,7 @@ snapshots: terser@5.48.0: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.16.0 + acorn: 8.17.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -14704,12 +14931,7 @@ snapshots: tinyexec@1.0.4: {} - tinyexec@1.2.2: {} - - tinyglobby@0.2.16: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + tinyexec@1.2.4: {} tinyglobby@0.2.17: dependencies: @@ -14746,9 +14968,9 @@ snapshots: trough@2.2.0: {} - trpc-to-openapi@3.3.0(@trpc/server@11.17.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.4.3))(zod@4.4.3): + trpc-to-openapi@3.3.0(@trpc/server@11.18.0(typescript@6.0.3))(zod-openapi@5.4.6(zod@4.4.3))(zod@4.4.3): dependencies: - '@trpc/server': 11.17.0(typescript@5.9.3) + '@trpc/server': 11.18.0(typescript@6.0.3) co-body: 6.2.0 h3: 1.15.11 openapi3-ts: 4.4.0 @@ -14757,18 +14979,18 @@ snapshots: optionalDependencies: '@rollup/rollup-linux-x64-gnu': 4.6.1 - ts-api-utils@2.5.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: - typescript: 5.9.3 + typescript: 6.0.3 ts-morph@26.0.0: dependencies: '@ts-morph/common': 0.27.0 code-block-writer: 13.0.3 - tsconfck@3.1.6(typescript@5.9.3): + tsconfck@3.1.6(typescript@6.0.3): optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 tsconfig-paths@3.15.0: dependencies: @@ -14785,9 +15007,9 @@ snapshots: tslib@2.8.1: {} - tsx@4.22.3: + tsx@4.23.0: dependencies: - esbuild: 0.28.0 + esbuild: 0.28.1 optionalDependencies: fsevents: 2.3.3 @@ -14811,13 +15033,14 @@ snapshots: type-fest@4.41.0: {} - type-fest@5.3.1: + type-fest@5.7.0: dependencies: tagged-tag: 1.0.0 - type-fest@5.7.0: + type-fest@5.8.0: dependencies: tagged-tag: 1.0.0 + optional: true type-is@1.6.18: dependencies: @@ -14863,6 +15086,15 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + typed-inject@5.0.0: {} typed-rest-client@2.3.0: @@ -14875,18 +15107,18 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): + typescript-eslint@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) eslint: 9.39.4(jiti@2.7.0) - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - typescript@5.9.3: {} + typescript@6.0.3: {} ufo@1.6.4: {} @@ -14908,20 +15140,20 @@ snapshots: undici-types@7.25.0: {} - undici@5.29.0: - dependencies: - '@fastify/busboy': 2.1.1 - - undici@6.25.0: {} + undici@6.27.0: {} undici@7.25.0: {} + undici@7.28.0: {} + unicode-emoji-modifier-base@1.0.0: {} unicorn-magic@0.1.0: {} unicorn-magic@0.3.0: {} + unicorn-magic@0.4.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -14970,35 +15202,39 @@ snapshots: acorn: 8.16.0 webpack-virtual-modules: 0.6.2 - unrs-resolver@1.11.1: + unrs-resolver@1.12.2: dependencies: napi-postinstall: 0.3.4 optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.11.1 - '@unrs/resolver-binding-android-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-x64': 1.11.1 - '@unrs/resolver-binding-freebsd-x64': 1.11.1 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 - '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-musl': 1.11.1 - '@unrs/resolver-binding-wasm32-wasi': 1.11.1 - '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 - '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 - '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - - until-async@3.0.2: {} - - update-browserslist-db@1.2.3(browserslist@4.28.2): - dependencies: - browserslist: 4.28.2 + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + until-async@3.0.2: + optional: true + + update-browserslist-db@1.2.3(browserslist@4.28.5): + dependencies: + browserslist: 4.28.5 escalade: 3.2.0 picocolors: 1.1.1 @@ -15013,28 +15249,28 @@ snapshots: punycode: 1.4.1 qs: 6.15.2 - use-composed-ref@1.4.0(@types/react@19.2.15)(react@19.2.6): + use-composed-ref@1.4.0(@types/react@19.2.17)(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 - use-isomorphic-layout-effect@1.2.1(@types/react@19.2.15)(react@19.2.6): + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.17)(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 - use-latest@1.3.0(@types/react@19.2.15)(react@19.2.6): + use-latest@1.3.0(@types/react@19.2.17)(react@19.2.7): dependencies: - react: 19.2.6 - use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.7 + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.17)(react@19.2.7) optionalDependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 - use-sync-external-store@1.6.0(react@19.2.6): + use-sync-external-store@1.6.0(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 util-deprecate@1.0.2: {} @@ -15078,58 +15314,58 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)): + vite-tsconfig-paths@6.1.1(typescript@6.0.3)(vite@7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0)): dependencies: debug: 4.4.3 globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.9.3) - vite: 7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) + tsconfck: 3.1.6(typescript@6.0.3) + vite: 7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - typescript - vite@7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0): + vite@7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0): dependencies: esbuild: 0.27.7 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.15 - rollup: 4.60.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.16 + rollup: 4.62.2 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 fsevents: 2.3.3 jiti: 2.7.0 lightningcss: 1.32.0 terser: 5.48.0 - tsx: 4.22.3 + tsx: 4.23.0 yaml: 2.9.0 - vitest@4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@25.9.1)(typescript@5.9.3))(vite@7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)): + vitest@4.1.9(@types/node@25.9.4)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@25.9.4)(typescript@6.0.3))(vite@7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0)): dependencies: - '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(msw@2.14.6(@types/node@25.9.1)(typescript@5.9.3))(vite@7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.7 - '@vitest/runner': 4.1.7 - '@vitest/snapshot': 4.1.7 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@25.9.4)(typescript@6.0.3))(vite@7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 - obug: 2.1.1 + obug: 2.1.3 pathe: 2.0.3 picomatch: 4.0.4 std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.2.2 - tinyglobby: 0.2.16 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 7.3.0(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) + vite: 7.3.0(@types/node@25.9.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.9.1 - '@vitest/coverage-v8': 4.1.7(vitest@4.1.7) + '@types/node': 25.9.4 + '@vitest/coverage-v8': 4.1.9(vitest@4.1.9) jsdom: 29.1.1(@noble/hashes@1.8.0) transitivePeerDependencies: - msw @@ -15138,9 +15374,8 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - watchpack@2.5.1: + watchpack@2.5.2: dependencies: - glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 wcwidth@1.0.1: @@ -15149,13 +15384,11 @@ snapshots: weapon-regex@1.3.6: {} - web-streams-polyfill@3.3.3: {} - - web-worker@1.2.0: {} + web-worker@1.5.0: {} webidl-conversions@8.0.1: {} - webpack-sources@3.5.0: {} + webpack-sources@3.5.1: {} webpack-virtual-modules@0.6.2: {} @@ -15167,12 +15400,12 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.16.0 - acorn-import-phases: 1.0.4(acorn@8.16.0) - browserslist: 4.28.2 + acorn: 8.17.0 + acorn-import-phases: 1.0.4(acorn@8.17.0) + browserslist: 4.28.5 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.22.1 - es-module-lexer: 2.1.0 + enhanced-resolve: 5.24.2 + es-module-lexer: 2.3.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -15184,8 +15417,8 @@ snapshots: schema-utils: 4.3.3 tapable: 2.3.3 terser-webpack-plugin: 5.6.1(lightningcss@1.32.0)(webpack@5.104.1(lightningcss@1.32.0)) - watchpack: 2.5.1 - webpack-sources: 3.5.0 + watchpack: 2.5.2 + webpack-sources: 3.5.1 transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -15253,7 +15486,7 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 - which-typed-array@1.1.20: + which-typed-array@1.1.22: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.9 @@ -15282,13 +15515,13 @@ snapshots: worker-factory@7.0.49: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 fast-unique-numbers: 9.0.27 tslib: 2.8.1 worker-timers-broker@8.0.16: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 broker-factory: 3.1.14 fast-unique-numbers: 9.0.27 tslib: 2.8.1 @@ -15296,13 +15529,13 @@ snapshots: worker-timers-worker@9.0.14: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 tslib: 2.8.1 worker-factory: 7.0.49 worker-timers@8.0.31: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 tslib: 2.8.1 worker-timers-broker: 8.0.16 worker-timers-worker: 9.0.14 @@ -15367,7 +15600,8 @@ snapshots: yargs-parser@20.2.9: {} - yargs-parser@21.1.1: {} + yargs-parser@21.1.1: + optional: true yargs-parser@22.0.0: {} @@ -15395,7 +15629,7 @@ snapshots: y18n: 5.0.8 yargs-parser: 20.2.9 - yargs@17.7.2: + yargs@17.7.3: dependencies: cliui: 8.0.1 escalade: 3.2.0 @@ -15404,6 +15638,7 @@ snapshots: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 + optional: true yargs@18.0.0: dependencies: @@ -15416,7 +15651,7 @@ snapshots: yocto-queue@0.1.0: {} - yocto-spinner@1.2.0: + yocto-spinner@1.2.1: dependencies: yoctocolors: 2.1.2 @@ -15442,12 +15677,12 @@ snapshots: zod@4.4.3: {} - zustand@4.5.7(@types/react@19.2.15)(immer@11.1.4)(react@19.2.6): + zustand@4.5.7(@types/react@19.2.17)(immer@11.1.11)(react@19.2.7): dependencies: - use-sync-external-store: 1.6.0(react@19.2.6) + use-sync-external-store: 1.6.0(react@19.2.7) optionalDependencies: - '@types/react': 19.2.15 - immer: 11.1.4 - react: 19.2.6 + '@types/react': 19.2.17 + immer: 11.1.11 + react: 19.2.7 zwitch@2.0.4: {} diff --git a/scripts/bin/sp-bundle-logs b/scripts/bin/sp-bundle-logs index 6854c618..bfd9572a 100755 --- a/scripts/bin/sp-bundle-logs +++ b/scripts/bin/sp-bundle-logs @@ -165,6 +165,19 @@ for svc in sleepypod sleepypod-piezo-processor sleepypod-sleep-detector \ 'systemctl show -p Environment "$1.service" | sed "s/^Environment=//" | tr " " "\n" | grep -E "^RAW_DATA_DIR=" || true' _ "$svc" done +# Which RAW file the WebSocket streamer actually attached to. It only tails +# while a Sensors/debug client is connected. "Switched to RAW file: SEQNO.RAW" +# followed by "Resync:" spam is the classic wrong-file symptom (attaching to +# the 16-byte counter instead of a real capture) — only DEV shows in the tab. +capture "raw/ws-3001.txt" bash -c 'ss -ltn 2>/dev/null | grep -q ":3001 " && echo "port 3001 listening" || echo "port 3001 NOT listening"' +capture "raw/sensorstream-log.txt" bash -c 'journalctl -u sleepypod.service -b --no-pager 2>/dev/null | grep -iE "\[sensorStream\]" | tail -40' + +# Retention: gzip cold-archive size vs disk. A large archive next to a +# near-full /persistent means the pruner has stalled — the usual cause of +# "No space left on device" on update. Pruner/archiver journals are captured +# above under journals/. +capture "raw/archive-size.txt" bash -c 'du -sh /persistent/biometrics-archive 2>/dev/null; echo "gz files: $(find /persistent/biometrics-archive -maxdepth 1 -type f -name "*.gz" 2>/dev/null | wc -l)"; df -h /persistent 2>/dev/null' + # --- install log --------------------------------------------------------- if [ -f "$INSTALL_LOG" ]; then cp "$INSTALL_LOG" "$STAGE/sleepypod-install.log" diff --git a/scripts/install b/scripts/install index d268a2f5..dd5dc010 100755 --- a/scripts/install +++ b/scripts/install @@ -156,6 +156,10 @@ if [ "$RESTORE_MODE" = true ]; then echo "" echo "Restoring $fname -> $(basename "$target")" cp "$selected" "$target" + # Remove live WAL/SHM sidecars: a sidecar newer than the restored .db + # would let SQLite replay post-backup transactions on top of it, silently + # undoing the restore (mirrors the sp-update rollback handling). + rm -f "${target}-wal" "${target}-shm" echo "Done. Restart the service to pick up the restored database:" echo " systemctl restart sleepypod" exit 0 @@ -853,8 +857,8 @@ fi # ============================================================================ # Install Node.js via binary download (works on any Linux, no apt required) # ============================================================================ -NODE_WANTED=22 -NODE_FULL="22.17.0" +NODE_WANTED=24 +NODE_FULL="24.16.0" NODE_DIR="/usr/local/lib/nodejs" # Detect architecture @@ -1266,12 +1270,25 @@ echo "Creating systemd service..." # the runtime's checkAndRepairIptables() writes /etc/iptables/rules.v4). mkdir -p /etc/iptables -# Only emit SupplementaryGroups=dac when the dac group exists — otherwise -# systemd refuses to start the unit with "Failed to determine supplementary -# group: No such file or directory". -SUPP_GROUPS_LINE="" +# Only emit groups that exist — otherwise systemd refuses to start the +# unit with "Failed to determine supplementary group: No such file or +# directory". systemd-journal (adm on images without it) lets the +# next-server's /system/logs endpoint run journalctl as User=sleepypod; +# without it the log viewer fails with "No journal files were opened due +# to insufficient permissions" on pods where the journal isn't +# world-readable. +SUPP_GROUPS=() if getent group dac &>/dev/null; then - SUPP_GROUPS_LINE="SupplementaryGroups=dac" + SUPP_GROUPS+=(dac) +fi +if getent group systemd-journal &>/dev/null; then + SUPP_GROUPS+=(systemd-journal) +elif getent group adm &>/dev/null; then + SUPP_GROUPS+=(adm) +fi +SUPP_GROUPS_LINE="" +if [ ${#SUPP_GROUPS[@]} -gt 0 ]; then + SUPP_GROUPS_LINE="SupplementaryGroups=${SUPP_GROUPS[*]}" fi cat > /etc/systemd/system/sleepypod.service << EOF diff --git a/src/automation/backtest.ts b/src/automation/backtest.ts new file mode 100644 index 00000000..935be2ad --- /dev/null +++ b/src/automation/backtest.ts @@ -0,0 +1,451 @@ +/** + * Backtest — replays an automation rule against REAL recorded signal history for + * a chosen past night and reports what it would have done. This is the + * transparency wedge: users see why a rule fires and what setpoint it produces + * before it ever touches the bed. + * + * The replay reuses the engine's pure evaluation core (WindowStore for windowed + * aggregates, evaluateCondition for the IF tree, evaluateExpr for action params) + * over snapshots reconstructed at a fixed step. It never touches hardware or the + * live engine — it is a deterministic function of (rule, history). + * + * Two render modes, matching how the two motivating examples differ: + * - 'policy' — the setpoint is a continuous function of a live signal + * (e.g. ambient + 3), re-asserted each step within a time window. Ambient is + * persisted, so the setpoint line is computed from real data and clamped. + * - 'edge' — a threshold/aggregate crossing fires a one-shot delta with a + * revert window and cooldown. The compared signal (e.g. movement) is real; + * the setpoint is shown relative to a NOMINAL baseline because per-side + * target-temperature history is not persisted (device_state is live-only). + */ + +import { MAX_TEMP, MIN_TEMP } from '@/src/hardware/types' +import { clockInTimezone } from './signals' +import { evaluateCondition } from './evaluator' +import { evaluateExpr, type EvalContext } from './expressions' +import { WindowStore } from './windows' +import { + AUTOMATION_DEFAULT_USER_MAX, + AUTOMATION_DEFAULT_USER_MIN, + type Action, + type Condition, + type Expr, + type Trigger, +} from './types' + +/** A timestamped numeric sample (epoch ms). */ +export interface Sample { t: number, v: number } + +/** Rule shape the backtest replays (the engine AST, side already resolved). */ +export interface BacktestRule { + side: 'left' | 'right' | null + cooldownMin: number | null + trigger: Trigger + conditions: Condition + actions: Action[] +} + +export interface BacktestInput { + rule: BacktestRule + timezone: string + /** Inclusive window the replay walks, epoch ms. */ + startMs: number + endMs: number + /** Step size in minutes (defaults to 1, matching the engine's resolution). */ + stepMin?: number + /** Historical series keyed by concrete signal key (e.g. `left.movement`). */ + series: Record +} + +export interface BacktestSeries { + key: string + label: string + /** One value per replay step; null where no fresh sample was available. */ + values: (number | null)[] +} + +export interface BacktestResult { + mode: 'policy' | 'edge' + stepMin: number + /** Minutes-since-local-midnight for each step (for axis ticks). */ + clockMin: number[] + /** Raw trace of the primary compared signal. */ + primary: BacktestSeries | null + /** Windowed-aggregate trace (the value actually compared), edge mode. */ + avg: BacktestSeries | null + threshold: number | null + /** Resulting setpoint after the two-layer clamp, per step. */ + setpoint: (number | null)[] + /** Pre-clamp setpoint (policy mode ghost line). */ + setpointRaw: (number | null)[] | null + clamp: { min: number, max: number } | null + /** Time-of-day window for the shaded region, minutes since midnight. */ + timeWindow: { startMin: number, endMin: number } | null + fires: number[] + suppressed: number[] + primaryAxis: { min: number, max: number, unit: string } | null + tempAxis: { min: number, max: number } | null + summary: { + wouldFire: number + suppressed: number + clampHits: number + label: string + netEffect: string | null + setpointRange: [number, number] | null + } +} + +/** Last sample at or before `atMs`, within a staleness bound. undefined if none. */ +function sampleAt(buf: Sample[] | undefined, atMs: number, maxAgeMs: number): number | undefined { + if (!buf || buf.length === 0) return undefined + let best: Sample | undefined + for (const s of buf) { + if (s.t <= atMs && (!best || s.t > best.t)) best = s + } + if (!best) return undefined + return atMs - best.t <= maxAgeMs ? best.v : undefined +} + +/** First signal key referenced anywhere in an expression. */ +function firstSignalInExpr(e: Expr): { key: string, windowed: boolean, fn?: string, lastMin?: number } | null { + switch (e.kind) { + case 'signal': + return { key: e.signal, windowed: false } + case 'window': + return { key: e.signal, windowed: true, fn: e.fn, lastMin: e.lastMin } + case 'binary': + return firstSignalInExpr(e.left) ?? firstSignalInExpr(e.right) + case 'clamp': + return firstSignalInExpr(e.value) ?? firstSignalInExpr(e.min) ?? firstSignalInExpr(e.max) + default: + return null + } +} + +/** Find the first comparison that drives the rule (for the chart's primary trace). */ +function findPrimaryCompare(cond: Condition): + { left: Expr, op: string, threshold: number | null } | null { + switch (cond.kind) { + case 'and': + case 'or': { + for (const c of cond.conditions) { + const found = findPrimaryCompare(c) + if (found) return found + } + return null + } + case 'not': + return findPrimaryCompare(cond.condition) + case 'compare': { + // Prefer a comparison whose left side references a signal. + const sig = firstSignalInExpr(cond.left) + if (!sig) return null + const threshold = cond.right.kind === 'literal' ? cond.right.value : null + return { left: cond.left, op: cond.op, threshold } + } + default: + return null + } +} + +/** Extract the time-of-day window from the trigger/condition, if any. */ +function findTimeWindow(trigger: Trigger, cond: Condition): { startMin: number, endMin: number } | null { + const toMin = (hhmm: string): number => { + const [h, m] = hhmm.split(':').map(Number) + return h * 60 + m + } + const walk = (c: Condition): { startMin: number, endMin: number } | null => { + switch (c.kind) { + case 'and': + case 'or': + for (const x of c.conditions) { + const r = walk(x) + if (r) return r + } + return null + case 'not': + return walk(c.condition) + case 'timeBetween': + return { startMin: toMin(c.start), endMin: toMin(c.end) } + default: + return null + } + } + return walk(cond) +} + +const SIGNAL_LABELS: Record = { + 'ambient.temperature': 'Ambient temp', + 'ambient.humidity': 'Ambient humidity', + 'left.movement': 'Movement (L)', + 'right.movement': 'Movement (R)', + 'left.heartRate': 'Heart rate (L)', + 'right.heartRate': 'Heart rate (R)', + 'left.hrv': 'HRV (L)', + 'right.hrv': 'HRV (R)', + 'left.breathingRate': 'Breathing (L)', + 'right.breathingRate': 'Breathing (R)', +} +function signalLabel(key: string): string { + return SIGNAL_LABELS[key] ?? key +} + +/** + * Replay a rule over recorded history. Pure: deterministic in its inputs, no + * hardware, DB, or clock access beyond the injected timezone. + */ +export function runBacktest(input: BacktestInput): BacktestResult { + const stepMin = input.stepMin ?? 1 + const stepMs = stepMin * 60_000 + const { rule, series, timezone } = input + + // ---- introspect the rule for charting -------------------------------- + const setTemp = rule.actions.find(a => a.kind === 'setTemperature') as + Extract | undefined + const primaryCompare = findPrimaryCompare(rule.conditions) + const timeWindow = findTimeWindow(rule.trigger, rule.conditions) + const clampBand = setTemp?.clamp ?? { min: AUTOMATION_DEFAULT_USER_MIN, max: AUTOMATION_DEFAULT_USER_MAX } + + // Policy mode: a setTemperature whose expression tracks a non-target signal + // continuously, with no driving threshold comparison (time-window only). + const tempSignal = setTemp ? firstSignalInExpr(setTemp.temp) : null + const tempTracksLiveSignal = !!tempSignal + && !tempSignal.key.endsWith('.currentTemperature') + && !tempSignal.key.endsWith('.targetTemperature') + const mode: 'policy' | 'edge' = tempTracksLiveSignal && !primaryCompare ? 'policy' : 'edge' + + // ---- build the step grid -------------------------------------------- + const steps: number[] = [] + for (let t = input.startMs; t <= input.endMs; t += stepMs) steps.push(t) + const clockMin = steps.map(t => clockInTimezone(timezone, new Date(t)).nowMinutes) + + // Which signals are windowed (must be fed to the WindowStore each step). + const windows = new WindowStore() + const maxAgeMs = 15 * 60_000 // a sample older than 15 min is "stale" + + const fires: number[] = [] + const suppressed: number[] = [] + const setpoint: (number | null)[] = [] + const setpointRaw: (number | null)[] = [] + const primaryValues: (number | null)[] = [] + const avgValues: (number | null)[] = [] + + let lastFireMs: number | null = null + let lastTrigVal: number | undefined + let lastTrigSeen = false + let lastTimeKey: string | undefined + + // Edge-mode nominal baseline (per-side target history is not persisted). + const nominalBase = Math.round((clampBand.min + clampBand.max) / 2) + const revertMin = setTemp?.durationSec ? Math.round(setTemp.durationSec / 60) : 0 + // active revert windows: list of [startStep, endStep) + const revertUntil: number[] = [] + + for (let i = 0; i < steps.length; i++) { + const nowMs = steps[i] + const { nowMinutes, dayOfWeek } = clockInTimezone(timezone, new Date(nowMs)) + + const snap = (key: string): number | undefined => sampleAt(series[key], nowMs, maxAgeMs) + + // Feed window buffers for any windowed signal referenced by the rule. + if (primaryCompare) { + const sig = firstSignalInExpr(primaryCompare.left) + if (sig?.windowed) { + const v = snap(sig.key) + if (typeof v === 'number') windows.record(sig.key, v, nowMs) + } + } + + const ctx: EvalContext = { + signal: snap, + windows, + nowMs, + nowMinutes, + dayOfWeek, + } + + // primary + avg traces + if (primaryCompare) { + const sig = firstSignalInExpr(primaryCompare.left) + primaryValues.push(sig ? snap(sig.key) ?? null : null) + const v = evaluateExpr(primaryCompare.left, ctx) + avgValues.push(v ?? null) + } + else if (mode === 'policy' && tempSignal) { + primaryValues.push(snap(tempSignal.key) ?? null) + avgValues.push(null) + } + else { + primaryValues.push(null) + avgValues.push(null) + } + + // ---- replay the trigger + condition + gates ---------------------- + // Policy mode re-asserts a setpoint continuously; it has no discrete + // fires, so only edge mode records fire/suppress markers. + let fired = false + if (mode === 'edge') { + const trig = rule.trigger + let triggerActive = false + if (trig.kind === 'tick') { + triggerActive = true // every step is a tick at this resolution + } + else if (trig.kind === 'signalChange') { + const v = ctx.signal(trig.signal) + if (v !== undefined) { + triggerActive = lastTrigSeen && v !== lastTrigVal + lastTrigVal = v + lastTrigSeen = true + } + } + else { + const [h, m] = trig.at.split(':').map(Number) + const atMin = h * 60 + m + if (nowMinutes === atMin && (!trig.days || trig.days.includes(dayOfWeek))) { + const key = `${dayOfWeek}:${atMin}` + if (lastTimeKey !== key) { + triggerActive = true + lastTimeKey = key + } + } + } + + if (triggerActive) { + const cond = evaluateCondition(rule.conditions, ctx) + if (cond === true) { + const cool = rule.cooldownMin != null && lastFireMs != null + && nowMs - lastFireMs < rule.cooldownMin * 60_000 + if (cool) { + suppressed.push(i) + } + else { + fires.push(i) + fired = true + lastFireMs = nowMs + if (revertMin > 0) revertUntil.push(i + Math.max(1, Math.round(revertMin / stepMin))) + } + } + } + } + + // ---- resulting setpoint ------------------------------------------ + if (!setTemp) { + setpoint.push(null) + setpointRaw.push(null) + continue + } + + if (mode === 'policy') { + // Continuous: setpoint = clamp(expr(signals)) whenever in the time window. + const inWindow = timeWindow ? isInWindow(nowMinutes, timeWindow) : true + const raw = inWindow ? evaluateExpr(setTemp.temp, ctx) : undefined + if (raw === undefined) { + setpoint.push(null) + setpointRaw.push(null) + } + else { + setpointRaw.push(round1(raw)) + setpoint.push(round1(twoLayerClamp(raw, clampBand))) + } + } + else { + // Edge: nominal baseline, shifted by the action delta during revert windows. + const active = revertMin > 0 + ? revertUntil.some(end => i < end && i >= end - Math.max(1, Math.round(revertMin / stepMin))) + : fired + const delta = edgeDelta(setTemp.temp, nominalBase) + setpoint.push(active ? round1(twoLayerClamp(nominalBase + delta, clampBand)) : nominalBase) + setpointRaw.push(null) + } + } + + // ---- axes + summary -------------------------------------------------- + const compareSig = primaryCompare ? firstSignalInExpr(primaryCompare.left) : null + const primarySig = primaryCompare + ? compareSig + : (mode === 'policy' && tempSignal ? tempSignal : null) + const primaryUnit = primarySig?.key.includes('movement') ? '' : primarySig?.key.includes('temperature') ? '°F' : '' + const primaryNums = primaryValues.filter((v): v is number => v != null) + const avgNums = avgValues.filter((v): v is number => v != null) + const allPrimary = [...primaryNums, ...avgNums, ...(primaryCompare?.threshold != null ? [primaryCompare.threshold] : [])] + const primaryAxis = primarySig && allPrimary.length + ? { min: Math.min(0, ...allPrimary), max: niceMax(Math.max(...allPrimary)), unit: primaryUnit } + : null + + const setNums = [...setpoint, ...(setpointRaw ?? [])].filter((v): v is number => v != null) + const tempAxis = setNums.length + ? { min: Math.floor(Math.min(...setNums) - 1), max: Math.ceil(Math.max(...setNums) + 1) } + : null + + let clampHits = 0 + for (let i = 0; i < setpointRaw.length; i++) { + const raw = setpointRaw[i] + const c = setpoint[i] + if (raw != null && c != null && Math.abs(raw - c) > 0.05) clampHits++ + } + + const setRange = setNums.length ? [Math.min(...setNums), Math.max(...setNums)] as [number, number] : null + + let netEffect: string | null = null + if (setTemp && mode === 'edge') { + const delta = edgeDelta(setTemp.temp, nominalBase) + netEffect = `${delta >= 0 ? '+' : ''}${delta}°F · ${revertMin || 0}m` + } + + return { + mode, + stepMin, + clockMin, + primary: primarySig ? { key: primarySig.key, label: signalLabel(primarySig.key), values: primaryValues } : null, + avg: compareSig?.windowed + ? { key: compareSig.key, label: `avg ${signalLabel(compareSig.key)}`, values: avgValues } + : null, + threshold: primaryCompare?.threshold ?? null, + setpoint, + setpointRaw: mode === 'policy' ? setpointRaw : null, + clamp: clampBand, + timeWindow, + fires, + suppressed, + primaryAxis, + tempAxis, + summary: { + wouldFire: fires.length, + suppressed: suppressed.length, + clampHits, + label: mode === 'policy' ? 'Continuous' : 'Edge-triggered', + netEffect, + setpointRange: setRange, + }, + } +} + +/** Approximate the constant delta of an edge action's temp expr (e.g. current − 2 → −2). */ +function edgeDelta(temp: Expr, nominalBase: number): number { + if (temp.kind === 'binary' && (temp.op === '+' || temp.op === '-')) { + const lit = temp.right.kind === 'literal' + ? temp.right.value + : temp.left.kind === 'literal' ? temp.left.value : null + if (lit != null) return temp.op === '-' ? -lit : lit + } + if (temp.kind === 'literal') return temp.value - nominalBase + return 0 +} + +function twoLayerClamp(v: number, band: { min: number, max: number }): number { + const l1 = Math.min(Math.max(v, band.min), band.max) + return Math.min(Math.max(l1, MIN_TEMP), MAX_TEMP) +} + +function isInWindow(nowMin: number, w: { startMin: number, endMin: number }): boolean { + if (w.startMin === w.endMin) return false + if (w.startMin < w.endMin) return nowMin >= w.startMin && nowMin < w.endMin + return nowMin >= w.startMin || nowMin < w.endMin +} + +function round1(v: number): number { + return Math.round(v * 10) / 10 +} +function niceMax(v: number): number { + return Math.ceil(v / 100) * 100 || Math.ceil(v) +} diff --git a/src/automation/capReduce.ts b/src/automation/capReduce.ts new file mode 100644 index 00000000..8089659b --- /dev/null +++ b/src/automation/capReduce.ts @@ -0,0 +1,52 @@ +/** + * Pure reducers over a capacitive presence channel array. + * + * capSense2 carries `[A1,A2,B1,B2,C1,C2,ref1,ref2]` per side — body-contact load + * across head/torso/legs (NOT temperature) plus two reference channels. capSense + * (Pod 3) carries a single scalar. These helpers collapse either shape to scalar + * stats (for the automation engine) or a three-zone triple (for the UI/replay). + * + * Kept dependency-free so both the engine (`signals.biometrics`) and the + * streaming persistence writer (`streaming/capFramePersistence`) can import it + * without pulling in a module cycle. + */ + +export function mean(xs: number[]): number { + return xs.reduce((a, b) => a + b, 0) / xs.length +} + +/** + * Reduce a per-side capacitive channel array to scalar matrix signals. Returns + * null when there are no usable channels. `peakZone` is the index (0–2) of the + * highest paired zone (A/B/C), available only on the full 6-channel frame. + */ +export function reduceCap(values: number[]): { max: number, mean: number, spread: number, peakZone: number | null } | null { + // capSense2 carries 6 sensor channels + 2 reference channels; drop the refs. + const zones = values.length >= 8 ? values.slice(0, 6) : values + if (zones.length === 0) return null + const max = Math.max(...zones) + const min = Math.min(...zones) + // Three paired zones (A/B/C) only when the full 6-channel frame is present. + const peakZone = zones.length === 6 + ? [mean([zones[0], zones[1]]), mean([zones[2], zones[3]]), mean([zones[4], zones[5]])] + .reduce((best, v, i, arr) => (v > arr[best] ? i : best), 0) + : null + return { max, mean: mean(zones), spread: max - min, peakZone } +} + +/** + * Collapse a channel array to the three paired-zone (head/torso/legs) means used + * by the spatial replay. Returns null unless the full 6 sensor channels are + * present — a Pod 3 scalar frame has no spatial resolution to replay. + */ +export function zoneTriple(values: number[]): [number, number, number] | null { + const zones = values.length >= 8 ? values.slice(0, 6) : values + // Exactly 6 sensor channels — matches reduceCap's peakZone gating so a + // persisted frame never carries zones without a corresponding peak vote. + if (zones.length !== 6) return null + return [ + mean([zones[0], zones[1]]), + mean([zones[2], zones[3]]), + mean([zones[4], zones[5]]), + ] +} diff --git a/src/automation/engine.ts b/src/automation/engine.ts new file mode 100644 index 00000000..22f34ba5 --- /dev/null +++ b/src/automation/engine.ts @@ -0,0 +1,493 @@ +/** + * AutomationEngine — the reactive rules engine that sits beside the scheduler's + * JobManager. It evaluates user-built WHEN/IF/THEN automations on a periodic + * tick and writes through the SAME hardware path the scheduler uses + * (getSharedHardwareClient + per-side mutex + broadcastMutationStatus + + * markSideMutated). It introduces no new hardware path. + * + * Safety properties (see docs/adr/0023-autopilot-reactive-automations.md "Safety stack"): + * - Two-layer temp clamp: per-action user band, then hardware 55–110°F. + * - Anti-thrash: a setpoint is only re-sent to hardware when it moves ≥0.5°F. + * - Runaway guard: a rule exceeding N hardware actions/hour auto-disables. + * - Manual-override hold: touching the dial suspends autopilot on that side. + * - Run-once gate: never writes a side with an active run-once session. + * - Dry-run: notify-only; logs would-fire events without touching hardware. + * + * Every evaluation (i.e. every tick where a rule's trigger is active) writes one + * row to automation_runs with outcome fired/skipped/clamped/dry_run/error — the + * audit log that powers the transparency wedge. + * + * The engine is fully dependency-injected so it unit-tests without hardware, + * the DB, or timers. `src/automation/instance.ts` wires the production deps. + */ + +import { MAX_TEMP, MIN_TEMP, fahrenheitToLevel } from '@/src/hardware/types' +import { evaluateCondition } from './evaluator' +import type { EvalContext } from './expressions' +import { evaluateExpr } from './expressions' +import { collectWindowSignals, type SignalReader } from './signals' +import { + AUTOMATION_ANTI_THRASH_F, + AUTOMATION_DEFAULT_USER_MAX, + AUTOMATION_DEFAULT_USER_MIN, + AUTOMATION_MANUAL_OVERRIDE_MS, + AUTOMATION_MAX_ACTIONS_PER_HOUR, + AUTOMATION_TICK_MS, + type Action, + type AutomationRule, + type DayOfWeek, + type Expr, + type RunOutcome, + type Side, +} from './types' +import { WindowStore } from './windows' + +/** How many minutes after a timeOfDay slot a late tick may still fire it. */ +const TIME_OF_DAY_GRACE_MIN = 10 + +/** Minimal hardware surface the engine writes through (shared client shape). */ +export interface HardwareWriter { + connect: () => Promise + setTemperature: (side: Side, temperature: number, duration?: number) => Promise + setPower: (side: Side, powered: boolean, temperature?: number) => Promise +} + +export interface AutomationEngineDeps { + signals: SignalReader + /** Epoch ms — injectable for deterministic tests. */ + now: () => number + /** Timezone-aware wall clock. dateKey (local yyyy-mm-dd) keys timeOfDay + * "already fired today"; when absent, dayOfWeek is used as a fallback. */ + clock: () => { nowMinutes: number, dayOfWeek: DayOfWeek, dateKey?: string } + getHardware: () => HardwareWriter + withSideLock: (side: Side, fn: () => Promise) => Promise + broadcast: (side: Side, overlay: Record) => void + markMutated: (side: Side) => void + loadRules: () => Promise + recordRun: (automationId: number, outcome: RunOutcome, detail: unknown) => Promise + /** Persist enabled=false when the runaway guard trips. */ + disableRule: (automationId: number) => Promise + hasActiveRunOnceSession: (side: Side) => Promise + /** Side-effect for notify actions (e.g. push/log); never touches hardware. */ + notify: (automationId: number, message: string) => void + log?: (msg: string) => void +} + +interface RuleRuntime { + lastFiredMs: number | null + /** Timestamps of real hardware actions in the trailing hour (runaway guard). */ + actionTimes: number[] + /** Last observed value of a signalChange trigger's signal. */ + lastTriggerValue?: number + lastTriggerSeen: boolean + /** Last tick a `tick` trigger evaluated. */ + lastTickEvalMs: number + /** Day+minute key the last time a `timeOfDay` trigger fired (dedupe). */ + lastTimeKey?: string +} + +export class AutomationEngine { + private deps: AutomationEngineDeps + private rules: AutomationRule[] = [] + private runtime = new Map() + private triggerFingerprints = new Map() + private windows = new WindowStore() + private windowSignals = new Set() + private timer: ReturnType | null = null + private ticking = false + + // Global kill-switch. When false, ticks short-circuit and no rule is + // evaluated or commanded — per-rule enabled/dryRun state is left untouched, so + // re-enabling resumes every rule exactly as it was. Persisted in + // deviceSettings.autopilotEnabled; the instance restores it at boot. + private globalEnabled = true + + // Per-side runtime state shared across rules. + private lastAsserted: Record = { left: undefined, right: undefined } + private manualOverrideUntil: Record = { left: 0, right: 0 } + + constructor(deps: AutomationEngineDeps) { + this.deps = deps + } + + /** Load rules and start the periodic tick. */ + async start(): Promise { + await this.reload() + if (!this.timer) { + this.timer = setInterval(() => { + void this.tick() + }, AUTOMATION_TICK_MS) + // Don't keep the process alive solely for the engine tick. + if (typeof this.timer.unref === 'function') this.timer.unref() + } + this.deps.log?.(`AutomationEngine started with ${this.rules.length} automations`) + } + + /** Reload automations from the source (call after CRUD mutations). */ + async reload(): Promise { + const nextRules = await this.deps.loadRules() + const nextIds = new Set() + for (const rule of nextRules) { + nextIds.add(rule.id) + const nextTrigger = JSON.stringify(rule.trigger) + if (this.triggerFingerprints.get(rule.id) !== nextTrigger) { + this.runtime.delete(rule.id) + } + this.triggerFingerprints.set(rule.id, nextTrigger) + } + + this.rules = nextRules + this.windowSignals = collectWindowSignals(this.rules) + // Drop runtime for rules that no longer exist. + for (const id of [...this.runtime.keys()]) { + if (!nextIds.has(id)) this.runtime.delete(id) + } + for (const id of [...this.triggerFingerprints.keys()]) { + if (!nextIds.has(id)) this.triggerFingerprints.delete(id) + } + } + + stop(): void { + if (this.timer) { + clearInterval(this.timer) + this.timer = null + } + } + + /** + * Suspend autopilot on a side for the manual-override hold window. A router or + * gesture handler calls this when the user changes the dial directly. + */ + registerManualOverride(side: Side): void { + this.manualOverrideUntil[side] = this.deps.now() + AUTOMATION_MANUAL_OVERRIDE_MS + } + + /** Flip the global kill-switch. `false` suspends all evaluation immediately. */ + setGlobalEnabled(on: boolean): void { + this.globalEnabled = on + this.deps.log?.(`AutomationEngine global kill-switch ${on ? 'ON (running)' : 'OFF (halted)'}`) + } + + /** Whether autopilot is globally enabled (kill-switch not engaged). */ + isGloballyEnabled(): boolean { + return this.globalEnabled + } + + private getRuntime(id: number): RuleRuntime { + let rt = this.runtime.get(id) + if (!rt) { + rt = { lastFiredMs: null, actionTimes: [], lastTriggerSeen: false, lastTickEvalMs: 0 } + this.runtime.set(id, rt) + } + return rt + } + + /** One evaluation pass over all enabled rules. */ + async tick(): Promise { + if (!this.globalEnabled) return // kill-switch engaged — evaluate nothing + if (this.ticking) return // never overlap ticks + this.ticking = true + try { + const now = this.deps.now() + const snapshot = this.deps.signals.read() + const { nowMinutes, dayOfWeek, dateKey } = this.deps.clock() + + // Feed windowed-aggregate buffers, then prune to the largest window asked. + for (const key of this.windowSignals) { + const v = snapshot[key] + if (typeof v === 'number') this.windows.record(key, v, now) + } + this.windows.prune(now, this.maxWindowMinutes()) + + const ctx: EvalContext = { + signal: key => snapshot[key], + windows: this.windows, + nowMs: now, + nowMinutes, + dayOfWeek, + dateKey, + } + + for (const rule of this.rules) { + if (!rule.enabled) continue + try { + await this.evaluateRule(rule, ctx, now) + } + catch (err) { + await this.deps.recordRun(rule.id, 'error', { + reason: 'eval-threw', + message: err instanceof Error ? err.message : String(err), + }) + } + } + } + finally { + this.ticking = false + } + } + + private maxWindowMinutes(): number { + let max = 10 + for (const rule of this.rules) { + if (!rule.enabled) continue + max = Math.max(max, windowMinsInCondition(rule.conditions)) + for (const a of rule.actions) { + if (a.kind === 'setTemperature') max = Math.max(max, windowMinsInExpr(a.temp)) + if (a.kind === 'setPower' && a.temp) max = Math.max(max, windowMinsInExpr(a.temp)) + } + } + return max + } + + /** Is this rule's trigger active on this tick? Mutates trigger runtime. */ + private triggerActive(rule: AutomationRule, ctx: EvalContext, now: number, rt: RuleRuntime): boolean { + const t = rule.trigger + switch (t.kind) { + case 'tick': { + const due = now - rt.lastTickEvalMs >= t.everyMin * 60_000 + if (due) rt.lastTickEvalMs = now + return due + } + case 'signalChange': { + const v = ctx.signal(t.signal) + if (v === undefined) return false + const changed = rt.lastTriggerSeen && v !== rt.lastTriggerValue + rt.lastTriggerValue = v + rt.lastTriggerSeen = true + return changed + } + case 'timeOfDay': { + const [h, m] = t.at.split(':').map(Number) + const atMin = h * 60 + m + if (t.days && !t.days.includes(ctx.dayOfWeek)) return false + // Fire on the first tick at-or-after the slot, within a grace window: + // exact-minute equality silently skipped the slot whenever a tick + // landed >60s late (engine stall, scheduler reload). The window stays + // bounded so an engine (re)started hours later doesn't fire a long- + // gone morning slot at 3pm. + if (ctx.nowMinutes < atMin || ctx.nowMinutes > atMin + TIME_OF_DAY_GRACE_MIN) { + return false + } + // Key by local calendar date so the same slot re-arms next week + // (weekday-only keys blocked every later occurrence for the life of + // the process) but fires at most once per day. + const key = `${ctx.dateKey ?? ctx.dayOfWeek}:${atMin}` + if (rt.lastTimeKey === key) return false // already fired today + rt.lastTimeKey = key + return true + } + } + } + + private async evaluateRule(rule: AutomationRule, ctx: EvalContext, now: number): Promise { + const rt = this.getRuntime(rule.id) + if (!this.triggerActive(rule, ctx, now, rt)) return // not an eval; no audit row + + // IF — three-valued. unknown/false both skip (never fire on missing data). + const cond = evaluateCondition(rule.conditions, ctx) + if (cond !== true) { + await this.deps.recordRun(rule.id, 'skipped', { + reason: cond === undefined ? 'condition-unknown' : 'condition-false', + }) + return + } + + // Cooldown gate. + if (rule.cooldownMin != null && rt.lastFiredMs != null + && now - rt.lastFiredMs < rule.cooldownMin * 60_000) { + await this.deps.recordRun(rule.id, 'skipped', { reason: 'cooldown' }) + return + } + + // Runaway guard — prune the trailing hour and trip if over budget. + rt.actionTimes = rt.actionTimes.filter(ts => now - ts < 3_600_000) + if (rt.actionTimes.length >= AUTOMATION_MAX_ACTIONS_PER_HOUR) { + await this.deps.disableRule(rule.id) + rule.enabled = false + await this.deps.recordRun(rule.id, 'error', { + reason: 'runaway-disabled', + actionsLastHour: rt.actionTimes.length, + }) + this.deps.log?.(`AutomationEngine disabled rule ${rule.id} (${rule.name}): runaway guard`) + return + } + + // THEN — run actions, tracking the aggregate outcome. + const results: ActionResult[] = [] + for (const action of rule.actions) { + results.push(...(await this.runAction(rule, action, ctx, now, rt))) + } + + const outcome = aggregateOutcome(rule.dryRun, results) + if (outcome === 'fired' || outcome === 'clamped' || outcome === 'dry_run') { + rt.lastFiredMs = now + } + await this.deps.recordRun(rule.id, outcome, { actions: results }) + } + + private async runAction( + rule: AutomationRule, + action: Action, + ctx: EvalContext, + now: number, + rt: RuleRuntime, + ): Promise { + if (action.kind === 'notify') { + this.deps.notify(rule.id, action.message) + return [{ kind: 'notify', notified: true }] + } + + // A null rule side (the builder's "both") fans a hardware action out to both + // sides. The temp expression's signal keys are already side-resolved at build + // time — a "both" rule reads the left side (builderModel.toAST) — so the + // resolved setpoint is shared; only the write target differs per side. + const sides: Side[] = action.side ? [action.side] : (rule.side ? [rule.side] : ['left', 'right']) + + // Resolve the target temperature (setPower may have none → hardware default). + let raw: number | undefined + if (action.kind === 'setTemperature') raw = evaluateExpr(action.temp, ctx) + else if (action.temp) raw = evaluateExpr(action.temp, ctx) + + if (action.kind === 'setTemperature' && raw === undefined) { + return sides.map(side => ({ kind: action.kind, side, skipped: 'temp-unknown' })) + } + if (action.kind === 'setPower' && action.on && action.temp && raw === undefined) { + return sides.map(side => ({ kind: action.kind, side, skipped: 'temp-unknown' })) + } + + // Two-layer clamp (only when a temperature is involved). + let temp: number | undefined + let clamped = false + if (raw !== undefined) { + const userMin = action.kind === 'setTemperature' ? action.clamp?.min ?? AUTOMATION_DEFAULT_USER_MIN : AUTOMATION_DEFAULT_USER_MIN + const userMax = action.kind === 'setTemperature' ? action.clamp?.max ?? AUTOMATION_DEFAULT_USER_MAX : AUTOMATION_DEFAULT_USER_MAX + const layer1 = Math.min(Math.max(raw, userMin), userMax) + const layer2 = Math.min(Math.max(layer1, MIN_TEMP), MAX_TEMP) + temp = layer2 + clamped = layer2 !== raw + } + + const out: ActionResult[] = [] + for (const side of sides) { + out.push(await this.writeSide(rule, action, side, now, rt, raw, temp, clamped)) + } + return out + } + + /** Apply one resolved hardware action to a single side (gates + write). */ + private async writeSide( + rule: AutomationRule, + action: Exclude, + side: Side, + now: number, + rt: RuleRuntime, + raw: number | undefined, + temp: number | undefined, + clamped: boolean, + ): Promise { + // Side gates apply only to real hardware writes. + if (this.manualOverrideUntil[side] > now) { + return { kind: action.kind, side, skipped: 'manual-override', raw, temp, clamped } + } + if (await this.deps.hasActiveRunOnceSession(side)) { + return { kind: action.kind, side, skipped: 'run-once', raw, temp, clamped } + } + + // Anti-thrash: skip a sub-threshold re-assertion of the same setpoint. + if (action.kind === 'setTemperature' && temp !== undefined) { + const last = this.lastAsserted[side] + if (last !== undefined && Math.abs(temp - last) < AUTOMATION_ANTI_THRASH_F) { + return { kind: action.kind, side, antiThrash: true, raw, temp, clamped } + } + } + + // Dry-run: log the would-be command but never touch hardware. + if (rule.dryRun) { + return { kind: action.kind, side, dryRun: true, raw, temp, clamped, on: action.kind === 'setPower' ? action.on : undefined } + } + + // Real write through the shared, serialized hardware path. + await this.deps.withSideLock(side, async () => { + const hw = this.deps.getHardware() + await hw.connect() + if (action.kind === 'setTemperature') { + if (temp === undefined) return // unreachable: setTemperature always resolves a temp + await hw.setTemperature(side, temp, action.durationSec) + this.deps.markMutated(side) + this.deps.broadcast(side, { targetTemperature: temp, targetLevel: fahrenheitToLevel(temp) }) + this.lastAsserted[side] = temp + } + else { + await hw.setPower(side, action.on, temp) + this.deps.markMutated(side) + this.deps.broadcast(side, action.on + ? { targetTemperature: temp ?? 75, targetLevel: fahrenheitToLevel(temp ?? 75) } + : { targetLevel: 0 }) + if (action.on && temp !== undefined) this.lastAsserted[side] = temp + else if (!action.on) this.lastAsserted[side] = undefined + } + }) + rt.actionTimes.push(now) + return { kind: action.kind, side, sent: true, raw, temp, clamped, on: action.kind === 'setPower' ? action.on : undefined } + } +} + +interface ActionResult { + kind: Action['kind'] + side?: Side + notified?: boolean + sent?: boolean + dryRun?: boolean + antiThrash?: boolean + clamped?: boolean + skipped?: string + error?: string + raw?: number + temp?: number + on?: boolean +} + +/** Largest window (minutes) referenced anywhere in an expression tree. */ +function windowMinsInExpr(expr: Expr): number { + switch (expr.kind) { + case 'window': + return expr.lastMin + case 'binary': + return Math.max(windowMinsInExpr(expr.left), windowMinsInExpr(expr.right)) + case 'clamp': + return Math.max(windowMinsInExpr(expr.value), windowMinsInExpr(expr.min), windowMinsInExpr(expr.max)) + default: + return 0 + } +} + +/** Largest window (minutes) referenced anywhere in a condition tree. */ +function windowMinsInCondition(cond: AutomationRule['conditions']): number { + switch (cond.kind) { + case 'and': + case 'or': + return cond.conditions.reduce((m, c) => Math.max(m, windowMinsInCondition(c)), 0) + case 'not': + return windowMinsInCondition(cond.condition) + case 'compare': + return Math.max(windowMinsInExpr(cond.left), windowMinsInExpr(cond.right)) + case 'between': + return Math.max( + windowMinsInExpr(cond.subject), + windowMinsInExpr(cond.min), + windowMinsInExpr(cond.max), + ) + default: + return 0 + } +} + +/** Fold per-action results into the single run outcome (worst-meaningful). */ +function aggregateOutcome(dryRun: boolean, results: ActionResult[]): RunOutcome { + if (results.some(r => r.error)) return 'error' + const acted = results.filter(r => r.notified || r.sent || r.dryRun || r.antiThrash) + if (acted.length === 0) return 'skipped' // every action gated out + if (dryRun && results.some(r => r.dryRun)) return 'dry_run' + if (results.some(r => r.clamped && (r.sent || r.antiThrash))) return 'clamped' + return 'fired' +} diff --git a/src/automation/evaluator.ts b/src/automation/evaluator.ts new file mode 100644 index 00000000..9c72f51c --- /dev/null +++ b/src/automation/evaluator.ts @@ -0,0 +1,94 @@ +/** + * Condition (IF) evaluation with three-valued logic: true / false / undefined. + * + * `undefined` means "unknown" — an operand referenced a signal with no current + * value. The engine treats unknown as a skip (never a fire), so a rule never + * acts on missing data. AND short-circuits on a definite false; OR on a + * definite true; an unknown child only matters when nothing else decides it. + */ + +import type { CompareOp, Condition } from './types' +import { evaluateExpr, type EvalContext } from './expressions' + +function compare(op: CompareOp, l: number, r: number): boolean { + switch (op) { + case '>': + return l > r + case '>=': + return l >= r + case '<': + return l < r + case '<=': + return l <= r + case '==': + return l === r + case '!=': + return l !== r + } +} + +/** Parse "HH:MM" to minutes since midnight. */ +function toMinutes(hhmm: string): number { + const [h, m] = hhmm.split(':').map(Number) + return h * 60 + m +} + +/** Inclusive-start, exclusive-end window that may wrap past midnight. */ +function inTimeWindow(nowMin: number, start: string, end: string): boolean { + const s = toMinutes(start) + const e = toMinutes(end) + if (s === e) return false + if (s < e) return nowMin >= s && nowMin < e + // wraps midnight, e.g. 23:00–06:00 + return nowMin >= s || nowMin < e +} + +export function evaluateCondition(cond: Condition, ctx: EvalContext): boolean | undefined { + switch (cond.kind) { + case 'and': { + let sawUnknown = false + for (const c of cond.conditions) { + const r = evaluateCondition(c, ctx) + if (r === false) return false + if (r === undefined) sawUnknown = true + } + return sawUnknown ? undefined : true + } + + case 'or': { + let sawUnknown = false + for (const c of cond.conditions) { + const r = evaluateCondition(c, ctx) + if (r === true) return true + if (r === undefined) sawUnknown = true + } + return sawUnknown ? undefined : false + } + + case 'not': { + const r = evaluateCondition(cond.condition, ctx) + return r === undefined ? undefined : !r + } + + case 'compare': { + const l = evaluateExpr(cond.left, ctx) + const r = evaluateExpr(cond.right, ctx) + if (l === undefined || r === undefined) return undefined + return compare(cond.op, l, r) + } + + case 'between': { + const v = evaluateExpr(cond.subject, ctx) + const lo = evaluateExpr(cond.min, ctx) + const hi = evaluateExpr(cond.max, ctx) + if (v === undefined || lo === undefined || hi === undefined) return undefined + return v >= lo && v <= hi + } + + case 'timeBetween': + return inTimeWindow(ctx.nowMinutes, cond.start, cond.end) + + case 'onDays': + return cond.days.includes(ctx.dayOfWeek) + } +} diff --git a/src/automation/expressions.ts b/src/automation/expressions.ts new file mode 100644 index 00000000..d4878f19 --- /dev/null +++ b/src/automation/expressions.ts @@ -0,0 +1,68 @@ +/** + * Expression evaluation. Every expression resolves to a number, or `undefined` + * when a referenced signal/window has no value — undefined propagates through + * arithmetic so a condition built on missing data evaluates to "unknown" and + * the rule skips rather than firing blind. + */ + +import type { DayOfWeek, Expr } from './types' +import type { WindowStore } from './windows' + +export interface EvalContext { + /** Resolve a scalar signal key (e.g. `left.currentTemperature`) to a number. */ + signal: (key: string) => number | undefined + /** Windowed-aggregate store, queried at `nowMs`. */ + windows: WindowStore + nowMs: number + /** Minutes since local midnight, timezone-aware. */ + nowMinutes: number + dayOfWeek: DayOfWeek + /** Local calendar date (yyyy-mm-dd) — keys once-per-day trigger state. */ + dateKey?: string +} + +/** Clamp `value` to `[min, max]`. Bounds may be undefined (then ignored). */ +export function clamp(value: number, min: number | undefined, max: number | undefined): number { + let out = value + if (min !== undefined && out < min) out = min + if (max !== undefined && out > max) out = max + return out +} + +export function evaluateExpr(expr: Expr, ctx: EvalContext): number | undefined { + switch (expr.kind) { + case 'literal': + return expr.value + + case 'signal': + return ctx.signal(expr.signal) + + case 'window': + return ctx.windows.aggregate(expr.fn, expr.signal, expr.lastMin, ctx.nowMs) + + case 'binary': { + const l = evaluateExpr(expr.left, ctx) + const r = evaluateExpr(expr.right, ctx) + if (l === undefined || r === undefined) return undefined + switch (expr.op) { + case '+': + return l + r + case '-': + return l - r + case '*': + return l * r + case '/': + return r === 0 ? undefined : l / r + } + return undefined + } + + case 'clamp': { + const v = evaluateExpr(expr.value, ctx) + if (v === undefined) return undefined + const lo = evaluateExpr(expr.min, ctx) + const hi = evaluateExpr(expr.max, ctx) + return clamp(v, lo, hi) + } + } +} diff --git a/src/automation/index.ts b/src/automation/index.ts new file mode 100644 index 00000000..ebe92031 --- /dev/null +++ b/src/automation/index.ts @@ -0,0 +1,30 @@ +/** + * Autopilot rules engine — a reactive WHEN/IF/THEN evaluator that sits beside + * the scheduler's JobManager and writes through the same shared hardware path. + * + * See docs/adr/0023-autopilot-reactive-automations.md for the model, signal catalog, and safety design. + */ + +export { AutomationEngine } from './engine' +export type { AutomationEngineDeps, HardwareWriter } from './engine' +export { + getAutomationEngine, + getAutomationEngineIfRunning, + shutdownAutomationEngine, +} from './instance' +export { clockInTimezone, collectWindowSignals, CompositeSignalReader, DeviceSignalReader } from './signals' +export type { SignalReader, SignalSnapshot } from './signals' +export { BiometricsSignalReader } from './signals.biometrics' +export { evaluateCondition } from './evaluator' +export { clamp, evaluateExpr } from './expressions' +export type { EvalContext } from './expressions' +export { WindowStore } from './windows' +export type { + Action, + AutomationRule, + Condition, + Expr, + RunOutcome, + Side, + Trigger, +} from './types' diff --git a/src/automation/instance.ts b/src/automation/instance.ts new file mode 100644 index 00000000..b385e5ce --- /dev/null +++ b/src/automation/instance.ts @@ -0,0 +1,174 @@ +/** + * Global AutomationEngine singleton — mirrors `src/scheduler/instance.ts`. + * + * Wires the production dependencies (live signal reader, shared hardware client, + * shared per-side lock, mutation broadcast, DB-backed rule load + audit log) and + * ensures a single engine runs across the application. Booted beside the + * JobManager from instrumentation.ts. + */ + +import { and, eq, gt } from 'drizzle-orm' +import { db } from '@/src/db' +import { automationRuns, automations, deviceSettings, runOnceSessions } from '@/src/db/schema' +import { getSharedHardwareClient } from '@/src/hardware/dacMonitor.instance' +import { markSideMutated } from '@/src/hardware/deviceStateSync' +import { withSideLock } from '@/src/hardware/sideLock' +import { broadcastMutationStatus } from '@/src/streaming/broadcastMutationStatus' +import { AutomationEngine } from './engine' +import { BiometricsSignalReader } from './signals.biometrics' +import { CompositeSignalReader, DeviceSignalReader, clockInTimezone } from './signals' +import type { + Action, + AutomationRule, + Condition, + RunOutcome, + Side, + Trigger, +} from './types' + +const DEFAULT_TIMEZONE = 'America/Los_Angeles' + +let engineInstance: AutomationEngine | null = null +let engineInitPromise: Promise | null = null +let cachedTimezone: string | null = null +// Read by the engine's clock closure on every tick, so a timezone change +// takes effect without rebuilding the engine. +let activeTimezone: string = DEFAULT_TIMEZONE + +async function loadTimezone(): Promise { + if (cachedTimezone) return cachedTimezone + try { + const [settings] = await db.select().from(deviceSettings).limit(1) + cachedTimezone = settings?.timezone || DEFAULT_TIMEZONE + return cachedTimezone + } + catch { + return DEFAULT_TIMEZONE + } +} + +async function loadRules(): Promise { + const rows = await db.select().from(automations) + // JSON columns come back parsed; the router validates them with zod on write. + return rows.map(r => ({ + id: r.id, + name: r.name, + enabled: r.enabled, + side: r.side, + priority: r.priority, + dryRun: r.dryRun, + cooldownMin: r.cooldownMin, + trigger: r.trigger as Trigger, + conditions: r.conditions as Condition, + actions: r.actions as Action[], + })) +} + +async function recordRun(automationId: number, outcome: RunOutcome, detail: unknown): Promise { + try { + await db.insert(automationRuns).values({ + automationId, + outcome, + detail: detail as object, + }) + } + catch (e) { + console.warn('[automation] failed to record run:', e instanceof Error ? e.message : e) + } +} + +async function disableRule(automationId: number): Promise { + await db + .update(automations) + .set({ enabled: false, updatedAt: new Date() }) + .where(eq(automations.id, automationId)) +} + +async function hasActiveRunOnceSession(side: Side): Promise { + const [session] = await db + .select({ id: runOnceSessions.id }) + .from(runOnceSessions) + .where(and( + eq(runOnceSessions.side, side), + eq(runOnceSessions.status, 'active'), + gt(runOnceSessions.expiresAt, new Date()), + )) + .limit(1) + return !!session +} + +export async function getAutomationEngine(): Promise { + if (engineInstance) return engineInstance + if (engineInitPromise) return engineInitPromise + + engineInitPromise = (async () => { + try { + const timezone = await loadTimezone() + activeTimezone = timezone + // DAC status first, biometrics merged on top; the DAC reader stays + // authoritative for any overlapping key (e.g. water.low). + const reader = new CompositeSignalReader([ + new BiometricsSignalReader(), + new DeviceSignalReader(), + ]) + const engine = new AutomationEngine({ + signals: reader, + now: () => Date.now(), + clock: () => clockInTimezone(activeTimezone, new Date()), + getHardware: () => getSharedHardwareClient(), + withSideLock, + broadcast: (side, overlay) => broadcastMutationStatus(side, overlay), + markMutated: markSideMutated, + loadRules, + recordRun, + disableRule, + hasActiveRunOnceSession, + notify: (id, message) => console.log(`[automation notify] rule ${id}: ${message}`), + log: msg => console.log(`[automation] ${msg}`), + }) + await engine.start() + // Restore the global kill-switch from persisted settings (default on). + try { + const [settings] = await db.select({ on: deviceSettings.autopilotEnabled }).from(deviceSettings).limit(1) + if (settings && settings.on === false) engine.setGlobalEnabled(false) + } + catch { + // Settings unreadable (e.g. fresh DB) — leave autopilot enabled. + } + engineInstance = engine + console.log('AutomationEngine initialized with timezone:', timezone) + return engine + } + finally { + engineInitPromise = null + } + })() + + return engineInitPromise +} + +export function getAutomationEngineIfRunning(): AutomationEngine | null { + return engineInstance +} + +/** + * Propagate a device-settings timezone change to the running engine. The + * clock closure reads activeTimezone on every tick, so timeOfDay triggers + * evaluate in the new timezone immediately — previously the closure kept the + * boot-time tz (and cachedTimezone was never invalidated) until restart. + */ +export function updateAutomationTimezone(timezone: string): void { + cachedTimezone = timezone + activeTimezone = timezone + console.log('[automation] timezone updated to', timezone) +} + +export async function shutdownAutomationEngine(): Promise { + if (engineInstance) { + engineInstance.stop() + engineInstance = null + engineInitPromise = null + cachedTimezone = null + console.log('AutomationEngine shut down') + } +} diff --git a/src/automation/signals.biometrics.ts b/src/automation/signals.biometrics.ts new file mode 100644 index 00000000..1ad964d8 --- /dev/null +++ b/src/automation/signals.biometrics.ts @@ -0,0 +1,117 @@ +/** + * Biometrics-backed signal reader for the automation engine. + * + * The live DAC monitor (DeviceSignalReader) only knows heat level / water level. + * The richer sensors — vitals, movement, ambient + bed-surface + water + * temperature, ambient light, and the capacitive sensor matrix — are produced by + * the streaming services and persisted to biometrics.db (scalars) or held as a + * live in-memory snapshot (the cap matrix). This reader surfaces them to the + * engine as numeric catalog signals, gated by a per-source freshness window so a + * stale or absent sample resolves to `undefined` — the engine then skips, the + * safe default DeviceSignalReader already relies on. + * + * The capacitive presence matrix (capSense2: `[A1,A2,B1,B2,C1,C2,ref1,ref2]` per + * side — body-contact load across head/torso/legs, NOT temperature) is exposed + * to the engine only as scalar reducers — max / mean / spread — keeping the + * engine scalar. This reader resolves them from the live in-memory snapshot; a + * downsampled copy is persisted separately for historical replay (see + * `streaming/capFramePersistence`). (`reduceCap` also derives a peakZone index, + * used by the UI zone visualization rather than the engine.) + * + * Automation temperature signals are Fahrenheit by contract because automation + * actions target hardware setpoints, which are also Fahrenheit. + */ + +import { desc, eq } from 'drizzle-orm' +import { biometricsDb } from '@/src/db' +import { ambientLight, bedTemp, freezerTemp, movement, vitals } from '@/src/db/biometrics-schema' +import { centiDegreesToF, centiPercentToPercent } from '@/src/lib/tempUtils' +import { getLatestCapSenseSnapshot } from '@/src/streaming/piezoStream' +import { mean, reduceCap } from './capReduce' +import type { SignalReader, SignalSnapshot } from './signals' + +// Freshness windows (ms). A latest row older than its window is treated as +// absent. Vitals/movement land ~once a minute while in bed; environment frames +// are slower; the cap matrix arrives ~2 Hz on the live stream (see piezoStream). +const VITALS_FRESH_MS = 5 * 60_000 +const MOVEMENT_FRESH_MS = 5 * 60_000 +const ENV_FRESH_MS = 15 * 60_000 +const CAP_FRESH_MS = 30_000 + +const SIDES = ['left', 'right'] as const + +export class BiometricsSignalReader implements SignalReader { + read(): SignalSnapshot { + const out: SignalSnapshot = {} + const now = Date.now() + const fresh = (ts: Date | number, maxAgeMs: number): boolean => + now - (ts instanceof Date ? ts.getTime() : ts) <= maxAgeMs + + try { + for (const side of SIDES) { + const [v] = biometricsDb.select().from(vitals) + .where(eq(vitals.side, side)).orderBy(desc(vitals.timestamp)).limit(1).all() + if (v && fresh(v.timestamp, VITALS_FRESH_MS)) { + if (v.heartRate != null) out[`${side}.heartRate`] = v.heartRate + if (v.hrv != null) out[`${side}.hrv`] = v.hrv + if (v.breathingRate != null) out[`${side}.breathingRate`] = v.breathingRate + } + + const [m] = biometricsDb.select().from(movement) + .where(eq(movement.side, side)).orderBy(desc(movement.timestamp)).limit(1).all() + if (m && fresh(m.timestamp, MOVEMENT_FRESH_MS)) { + out[`${side}.movement`] = m.totalMovement + } + } + + const [bt] = biometricsDb.select().from(bedTemp).orderBy(desc(bedTemp.timestamp)).limit(1).all() + if (bt && fresh(bt.timestamp, ENV_FRESH_MS)) { + if (bt.ambientTemp != null) out['ambient.temperature'] = centiDegreesToF(bt.ambientTemp) + if (bt.humidity != null) out['ambient.humidity'] = centiPercentToPercent(bt.humidity) + const zonesBySide = { + left: { outer: bt.leftOuterTemp, center: bt.leftCenterTemp, inner: bt.leftInnerTemp }, + right: { outer: bt.rightOuterTemp, center: bt.rightCenterTemp, inner: bt.rightInnerTemp }, + } + for (const side of SIDES) { + const z = zonesBySide[side] + const presentF = [z.outer, z.center, z.inner] + .filter((t): t is number => t != null) + .map(centiDegreesToF) + if (presentF.length > 0) out[`${side}.surfaceTemp`] = mean(presentF) + if (presentF.length >= 2) out[`${side}.surfaceTemp.spread`] = Math.max(...presentF) - Math.min(...presentF) + if (z.inner != null && z.outer != null) out[`${side}.surfaceTemp.gradient`] = centiDegreesToF(z.inner) - centiDegreesToF(z.outer) + } + } + + const [fz] = biometricsDb.select().from(freezerTemp).orderBy(desc(freezerTemp.timestamp)).limit(1).all() + if (fz && fresh(fz.timestamp, ENV_FRESH_MS)) { + if (fz.leftWaterTemp != null) out['left.waterTemp'] = centiDegreesToF(fz.leftWaterTemp) + if (fz.rightWaterTemp != null) out['right.waterTemp'] = centiDegreesToF(fz.rightWaterTemp) + } + + const [al] = biometricsDb.select().from(ambientLight).orderBy(desc(ambientLight.timestamp)).limit(1).all() + if (al && fresh(al.timestamp, ENV_FRESH_MS) && al.lux != null) { + out['ambient.light'] = al.lux + } + + const cap = getLatestCapSenseSnapshot() + if (cap && fresh(cap.receivedAtMs, CAP_FRESH_MS)) { + for (const side of SIDES) { + const raw = cap[side] + const r = reduceCap(Array.isArray(raw) ? raw : [raw]) + if (r) { + out[`${side}.cap.max`] = r.max + out[`${side}.cap.mean`] = r.mean + out[`${side}.cap.spread`] = r.spread + } + } + } + } + catch (err) { + // Surface rather than swallow; the partial/empty snapshot makes dependent + // rules skip, which is the safe default but shouldn't happen silently. + console.warn('[automation] BiometricsSignalReader.read failed:', err) + } + return out + } +} diff --git a/src/automation/signals.ts b/src/automation/signals.ts new file mode 100644 index 00000000..df950dca --- /dev/null +++ b/src/automation/signals.ts @@ -0,0 +1,169 @@ +/** + * Signal sources for the automation engine. + * + * A `SignalReader` returns a synchronous snapshot of currently-available + * numeric signals keyed by the catalog names in docs/adr/0023-autopilot-reactive-automations.md. Any + * signal not present resolves to `undefined`, which makes dependent conditions + * skip rather than fire on stale/missing data. + * + * P0 wires the device-status signals that are reliably available now (per-side + * temperature/level from the live DAC monitor). Ambient and biometric signals + * are part of the catalog but read `undefined` until their sources are wired in + * a later phase — the engine degrades to "skip", which is the safe default. + */ + +import { getDacMonitorIfRunning } from '@/src/hardware/dacMonitor.instance' +import type { AutomationRule, DayOfWeek, Expr } from './types' + +export type SignalSnapshot = Record + +export interface SignalReader { + /** Snapshot the numeric signals available this instant. */ + read: () => SignalSnapshot +} + +const DAYS: DayOfWeek[] = [ + 'sunday', + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', +] + +/** Timezone-aware wall clock: minutes since local midnight + day of week + + * local calendar date (used to key "already fired today" per actual day). */ +export function clockInTimezone( + timezone: string, + now: Date, +): { nowMinutes: number, dayOfWeek: DayOfWeek, dateKey: string } { + const parts = new Intl.DateTimeFormat('en-US', { + timeZone: timezone, + hourCycle: 'h23', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + weekday: 'short', + }).formatToParts(now) + const get = (type: string): string => { + const part = parts.find(p => p.type === type) + if (!part) throw new Error(`Invalid timezone: ${timezone}`) + return part.value + } + const hour = Number(get('hour')) + const minute = Number(get('minute')) + const weekday = get('weekday') // e.g. "Mon" + const dayIndex = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(weekday) + if (dayIndex < 0) throw new Error(`Unexpected weekday token: ${weekday}`) + return { + nowMinutes: hour * 60 + minute, + dayOfWeek: DAYS[dayIndex], + dateKey: `${get('year')}-${get('month')}-${get('day')}`, + } +} + +/** + * Production reader backed by the live DAC monitor's last status frame. Reads + * are lazy-imported so the engine module stays decoupled from hardware in tests. + */ +export class DeviceSignalReader implements SignalReader { + read(): SignalSnapshot { + const snapshot: SignalSnapshot = {} + try { + const status = getDacMonitorIfRunning()?.getLastStatus() + if (!status) return snapshot + for (const side of ['left', 'right'] as const) { + const s = side === 'left' ? status.leftSide : status.rightSide + if (!s) continue + // null = level 0 (off); leave the signal absent so conditions don't + // fire against a phantom neutral temperature. + snapshot[`${side}.currentTemperature`] = s.currentTemperature ?? undefined + snapshot[`${side}.targetTemperature`] = s.targetTemperature ?? undefined + snapshot[`${side}.currentLevel`] = s.currentLevel + } + if (status.waterLevel === 'low' || status.waterLevel === 'ok') { + snapshot['water.low'] = status.waterLevel === 'low' ? 1 : 0 + } + } + catch (err) { + // A genuine read failure (the "monitor not running" case returns early + // above). Surface it rather than swallowing; the empty snapshot makes + // dependent rules skip, which we don't want to do silently. + console.warn('[automation] DeviceSignalReader.read failed:', err) + } + return snapshot + } +} + +/** + * Merge several readers into one snapshot. Readers are applied in order, so a + * later reader overrides an earlier one on key collision — keep the + * authoritative source last (e.g. the live DAC reader's `water.low`). + */ +export class CompositeSignalReader implements SignalReader { + constructor(private readonly readers: SignalReader[]) {} + + read(): SignalSnapshot { + const out: SignalSnapshot = {} + for (const reader of this.readers) Object.assign(out, reader.read()) + return out + } +} + +/** Walk an expression tree collecting the signal keys referenced by windows. */ +function collectWindowKeysFromExpr(expr: Expr, out: Set): void { + switch (expr.kind) { + case 'window': + out.add(expr.signal) + break + case 'binary': + collectWindowKeysFromExpr(expr.left, out) + collectWindowKeysFromExpr(expr.right, out) + break + case 'clamp': + collectWindowKeysFromExpr(expr.value, out) + collectWindowKeysFromExpr(expr.min, out) + collectWindowKeysFromExpr(expr.max, out) + break + } +} + +/** + * The set of signal keys any enabled rule aggregates over a window. The engine + * samples exactly these each tick to feed the `WindowStore`. + */ +export function collectWindowSignals(rules: AutomationRule[]): Set { + const out = new Set() + const walkCond = (c: AutomationRule['conditions']): void => { + switch (c.kind) { + case 'and': + case 'or': + c.conditions.forEach(walkCond) + break + case 'not': + walkCond(c.condition) + break + case 'compare': + collectWindowKeysFromExpr(c.left, out) + collectWindowKeysFromExpr(c.right, out) + break + case 'between': + collectWindowKeysFromExpr(c.subject, out) + collectWindowKeysFromExpr(c.min, out) + collectWindowKeysFromExpr(c.max, out) + break + } + } + for (const rule of rules) { + if (!rule.enabled) continue + walkCond(rule.conditions) + for (const action of rule.actions) { + if (action.kind === 'setTemperature') collectWindowKeysFromExpr(action.temp, out) + if (action.kind === 'setPower' && action.temp) collectWindowKeysFromExpr(action.temp, out) + } + } + return out +} diff --git a/src/automation/tests/backtest.test.ts b/src/automation/tests/backtest.test.ts new file mode 100644 index 00000000..9a2151df --- /dev/null +++ b/src/automation/tests/backtest.test.ts @@ -0,0 +1,345 @@ +import { describe, expect, it } from 'vitest' +import { type BacktestRule, runBacktest, type Sample } from '../backtest' +import type { Action, Condition, Trigger } from '../types' + +const HOUR = 60 * 60_000 + +/** Per-minute movement samples: low, then a sustained burst, then low. */ +function movementSeries(): Sample[] { + const out: Sample[] = [] + for (let i = 0; i < 60; i++) { + const v = i >= 10 && i <= 40 ? 300 : 100 + out.push({ t: i * 60_000, v }) + } + return out +} + +function ambientSeries(value: number): Sample[] { + const out: Sample[] = [] + for (let i = 0; i < 60; i++) out.push({ t: i * 60_000, v: value }) + return out +} + +describe('runBacktest — edge-triggered (movement avg > 200 → lower 2°F)', () => { + const rule: BacktestRule = { + side: 'left', + cooldownMin: 30, + trigger: { kind: 'tick', everyMin: 1 } as Trigger, + conditions: { + kind: 'and', + conditions: [{ + kind: 'compare', + op: '>', + left: { kind: 'window', fn: 'avg', signal: 'left.movement', lastMin: 10 }, + right: { kind: 'literal', value: 200 }, + }], + } as Condition, + actions: [{ + kind: 'setTemperature', + temp: { kind: 'binary', op: '-', left: { kind: 'signal', signal: 'left.currentTemperature' }, right: { kind: 'literal', value: 2 } }, + clamp: { min: 60, max: 75 }, + durationSec: 1200, + }] as Action[], + } + + const r = runBacktest({ rule, timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'left.movement': movementSeries() } }) + + it('classifies as edge mode with the right threshold + primary trace', () => { + expect(r.mode).toBe('edge') + expect(r.threshold).toBe(200) + expect(r.primary?.key).toBe('left.movement') + expect(r.avg?.key).toBe('left.movement') + }) + it('fires once then suppresses repeats during the cooldown window', () => { + expect(r.fires.length).toBeGreaterThanOrEqual(1) + expect(r.fires.length).toBeLessThanOrEqual(2) + expect(r.suppressed.length).toBeGreaterThan(0) + }) + it('reports the net effect of the action', () => { + expect(r.summary.netEffect).toContain('-2°F') + }) +}) + +describe('runBacktest — continuous policy (ambient + 3, clamped)', () => { + const policyRule = (): BacktestRule => ({ + side: null, + cooldownMin: null, + trigger: { kind: 'tick', everyMin: 1 } as Trigger, + conditions: { kind: 'and', conditions: [] } as Condition, + actions: [{ + kind: 'setTemperature', + temp: { kind: 'binary', op: '+', left: { kind: 'signal', signal: 'ambient.temperature' }, right: { kind: 'literal', value: 3 } }, + clamp: { min: 60, max: 75 }, + }] as Action[], + }) + + it('tracks ambient + 3 and never fires discrete events', () => { + const r = runBacktest({ rule: policyRule(), timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'ambient.temperature': ambientSeries(70) } }) + expect(r.mode).toBe('policy') + expect(r.fires.length).toBe(0) + const sample = r.setpoint.find(v => v != null) + expect(sample).toBe(73) + expect(r.summary.clampHits).toBe(0) + }) + + it('clamps the setpoint and counts the clamp hits when the expr exceeds the band', () => { + const r = runBacktest({ rule: policyRule(), timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'ambient.temperature': ambientSeries(74) } }) + // 74 + 3 = 77, clamped to the 75 ceiling. + const sample = r.setpoint.find(v => v != null) + expect(sample).toBe(75) + expect(r.summary.clampHits).toBeGreaterThan(0) + }) +}) + +const lit = (value: number) => ({ kind: 'literal' as const, value }) +const sig = (signal: string) => ({ kind: 'signal' as const, signal }) +const tick: Trigger = { kind: 'tick', everyMin: 1 } +const vacuous: Condition = { kind: 'and', conditions: [] } + +describe('runBacktest — no setTemperature action', () => { + it('leaves the setpoint series null when the rule only notifies', () => { + const rule: BacktestRule = { + side: 'left', + cooldownMin: null, + trigger: tick, + conditions: { kind: 'and', conditions: [{ kind: 'compare', op: '>', left: sig('left.movement'), right: lit(50) }] } as Condition, + actions: [{ kind: 'notify', message: 'restless' }] as Action[], + } + const r = runBacktest({ rule, timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'left.movement': movementSeries() } }) + expect(r.mode).toBe('edge') + expect(r.setpoint.every(v => v === null)).toBe(true) + expect(r.fires.length).toBeGreaterThan(0) + }) +}) + +describe('runBacktest — trigger variants', () => { + it('fires on a signalChange trigger when the value moves', () => { + const rule: BacktestRule = { + side: 'left', + cooldownMin: null, + trigger: { kind: 'signalChange', signal: 'left.movement' }, + conditions: vacuous, + actions: [{ kind: 'notify', message: 'moved' }] as Action[], + } + const r = runBacktest({ rule, timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'left.movement': movementSeries() } }) + // movementSeries steps between 100 and 300 twice → at least two changes. + expect(r.fires.length).toBeGreaterThanOrEqual(2) + expect(r.primary).toBeNull() + }) + + it('fires once on a timeOfDay trigger at the matching minute', () => { + const rule: BacktestRule = { + side: 'left', + cooldownMin: null, + trigger: { kind: 'timeOfDay', at: '00:05', days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] }, + conditions: vacuous, + actions: [{ kind: 'notify', message: 'bedtime' }] as Action[], + } + // 1970-01-01T00:00Z is a Thursday; the window spans 00:00–01:00 UTC. + const r = runBacktest({ rule, timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: {} }) + expect(r.fires).toEqual([5]) + }) +}) + +describe('runBacktest — edge-mode setpoint deltas', () => { + const edgeRule = (temp: Action): BacktestRule => ({ + side: 'left', + cooldownMin: null, + trigger: tick, + conditions: vacuous, + actions: [temp] as Action[], + }) + + it('derives the delta from a literal setpoint relative to the nominal baseline', () => { + const rule = edgeRule({ kind: 'setTemperature', temp: lit(80), clamp: { min: 60, max: 75 } } as Action) + const r = runBacktest({ rule, timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: {} }) + expect(r.mode).toBe('edge') + // nominal = round((60+75)/2) = 68; literal 80 → +12 → clamped to 75. + expect(r.setpoint.some(v => v === 75)).toBe(true) + }) + + it('treats a bare currentTemperature setpoint as a zero delta', () => { + const rule = edgeRule({ kind: 'setTemperature', temp: sig('left.currentTemperature'), clamp: { min: 60, max: 75 } } as Action) + const r = runBacktest({ rule, timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: {} }) + expect(r.mode).toBe('edge') + expect(r.summary.netEffect).toContain('0°F') + }) + + it('reads the delta from a literal on the left of the binary', () => { + // current + 2 written as 2 + current → the literal operand is on the left. + const rule = edgeRule({ kind: 'setTemperature', temp: { kind: 'binary', op: '+', left: lit(2), right: sig('left.currentTemperature') }, clamp: { min: 60, max: 75 } } as Action) + const r = runBacktest({ rule, timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: {} }) + expect(r.summary.netEffect).toContain('+2°F') + }) +}) + +describe('runBacktest — policy time windows', () => { + const policyWindowRule = (start: string, end: string): BacktestRule => ({ + side: null, + cooldownMin: null, + trigger: tick, + conditions: { kind: 'and', conditions: [{ kind: 'timeBetween', start, end }] } as Condition, + actions: [{ kind: 'setTemperature', temp: { kind: 'binary', op: '+', left: sig('ambient.temperature'), right: lit(3) }, clamp: { min: 60, max: 75 } }] as Action[], + }) + + it('only emits a setpoint inside a same-day window', () => { + const r = runBacktest({ rule: policyWindowRule('00:10', '00:20'), timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'ambient.temperature': ambientSeries(68) } }) + expect(r.mode).toBe('policy') + expect(r.setpoint[0]).toBeNull() // 00:00 is outside the window + expect(r.setpoint[12]).toBe(71) // 00:12 is inside → 68 + 3 + expect(r.setpoint[30]).toBeNull() // 00:30 is outside again + }) + + it('handles a window that wraps past midnight', () => { + const r = runBacktest({ rule: policyWindowRule('00:50', '00:10'), timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'ambient.temperature': ambientSeries(68) } }) + expect(r.setpoint[5]).toBe(71) // 00:05 is inside the wrapped window + expect(r.setpoint[30]).toBeNull() // 00:30 is outside + }) + + it('treats a zero-width window (start === end) as always outside', () => { + const r = runBacktest({ rule: policyWindowRule('01:00', '01:00'), timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'ambient.temperature': ambientSeries(68) } }) + expect(r.setpoint.every(v => v === null)).toBe(true) + }) +}) + +describe('runBacktest — condition-tree introspection', () => { + it('finds a comparison nested inside a NOT and a time window inside a NOT', () => { + const rule: BacktestRule = { + side: 'left', + cooldownMin: null, + trigger: tick, + conditions: { + kind: 'and', + conditions: [ + { kind: 'not', condition: { kind: 'compare', op: '>', left: sig('left.movement'), right: lit(200) } }, + { kind: 'timeBetween', start: '23:00', end: '06:00' }, + ], + } as Condition, + actions: [{ kind: 'notify', message: 'x' }] as Action[], + } + const r = runBacktest({ rule, timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'left.movement': movementSeries() } }) + expect(r.primary?.key).toBe('left.movement') + expect(r.timeWindow).toEqual({ startMin: 23 * 60, endMin: 6 * 60 }) + }) + + it('skips literal-left comparisons and reads through a clamp to the driving signal', () => { + const rule: BacktestRule = { + side: 'left', + cooldownMin: null, + trigger: tick, + conditions: { + kind: 'and', + conditions: [ + { kind: 'compare', op: '>', left: lit(5), right: lit(1) }, // literal left → no signal + { kind: 'compare', op: '>', left: { kind: 'clamp', value: sig('left.movement'), min: lit(0), max: lit(1000) }, right: lit(200) }, + ], + } as Condition, + actions: [{ kind: 'notify', message: 'x' }] as Action[], + } + const r = runBacktest({ rule, timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'left.movement': movementSeries() } }) + expect(r.primary?.key).toBe('left.movement') + }) +}) + +describe('runBacktest — sampling & introspection corners', () => { + const notifyOn = (conditions: Condition): BacktestRule => ({ + side: 'left', cooldownMin: null, trigger: tick, conditions, actions: [{ kind: 'notify', message: 'x' }] as Action[], + }) + + it('defaults the step size to 1 minute when none is given', () => { + const r = runBacktest({ rule: notifyOn(vacuous), timezone: 'UTC', startMs: 0, endMs: HOUR, series: {} }) + expect(r.stepMin).toBe(1) + }) + + it('reports a null threshold for a compare whose right side is not a literal', () => { + const cond: Condition = { kind: 'and', conditions: [{ kind: 'compare', op: '>', left: sig('left.movement'), right: sig('left.heartRate') }] } + const r = runBacktest({ rule: notifyOn(cond), timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'left.movement': movementSeries() } }) + expect(r.threshold).toBeNull() + expect(r.primary?.key).toBe('left.movement') + }) + + it('labels a primary signal that has no friendly name with its raw key', () => { + const cond: Condition = { kind: 'and', conditions: [{ kind: 'compare', op: '>', left: sig('left.targetTemperature'), right: lit(70) }] } + const series = { 'left.targetTemperature': ambientSeries(72) } + const r = runBacktest({ rule: notifyOn(cond), timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series }) + expect(r.primary?.label).toBe('left.targetTemperature') + }) + + it('treats an empty sample buffer as no data', () => { + const cond: Condition = { kind: 'and', conditions: [{ kind: 'compare', op: '>', left: sig('left.movement'), right: lit(50) }] } + const r = runBacktest({ rule: notifyOn(cond), timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'left.movement': [] } }) + expect(r.primary?.values.every(v => v === null)).toBe(true) + expect(r.fires.length).toBe(0) + }) + + it('ignores samples that lie entirely in the future', () => { + const cond: Condition = { kind: 'and', conditions: [{ kind: 'compare', op: '>', left: sig('left.movement'), right: lit(50) }] } + // The only sample is 10h ahead of the whole replay window. + const r = runBacktest({ rule: notifyOn(cond), timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'left.movement': [{ t: 10 * HOUR, v: 300 }] } }) + expect(r.primary?.values.every(v => v === null)).toBe(true) + }) + + it('treats a sample older than the staleness bound as unavailable', () => { + const cond: Condition = { kind: 'and', conditions: [{ kind: 'compare', op: '>', left: sig('left.movement'), right: lit(50) }] } + // A single sample at t=0; by 00:20 it is >15 min stale. + const r = runBacktest({ rule: notifyOn(cond), timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'left.movement': [{ t: 0, v: 300 }] } }) + expect(r.primary?.values[0]).toBe(300) // fresh + expect(r.primary?.values[20]).toBeNull() // stale + }) + + it('produces null aggregate values for a windowed compare with no data', () => { + const cond: Condition = { + kind: 'and', + conditions: [{ kind: 'compare', op: '>', left: { kind: 'window', fn: 'avg', signal: 'left.movement', lastMin: 10 }, right: lit(200) }], + } + const r = runBacktest({ rule: notifyOn(cond), timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: {} }) + expect(r.avg?.values.every(v => v === null)).toBe(true) + expect(r.fires.length).toBe(0) + }) + + it('never fires a signalChange trigger when the signal is absent', () => { + const rule: BacktestRule = { + side: 'left', cooldownMin: null, trigger: { kind: 'signalChange', signal: 'left.movement' }, + conditions: vacuous, actions: [{ kind: 'notify', message: 'x' }] as Action[], + } + const r = runBacktest({ rule, timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: {} }) + expect(r.fires.length).toBe(0) + }) + + it('fires a timeOfDay trigger only once even when multiple steps share a minute', () => { + const rule: BacktestRule = { + side: 'left', cooldownMin: null, trigger: { kind: 'timeOfDay', at: '00:05' }, + conditions: vacuous, actions: [{ kind: 'notify', message: 'x' }] as Action[], + } + // Half-minute steps put two steps inside minute 5; the second must dedupe. + const r = runBacktest({ rule, timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 0.5, series: {} }) + expect(r.fires).toHaveLength(1) + }) + + it('reports a zero net delta for an edge action that subtracts two signals', () => { + const rule: BacktestRule = { + side: 'left', cooldownMin: null, trigger: tick, conditions: vacuous, + actions: [{ kind: 'setTemperature', temp: { kind: 'binary', op: '+', left: sig('left.currentTemperature'), right: sig('ambient.temperature') }, clamp: { min: 60, max: 75 } }] as Action[], + } + const r = runBacktest({ rule, timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: {} }) + expect(r.mode).toBe('edge') + expect(r.summary.netEffect).toContain('0°F') + }) + + it('reads through a clamp whose inner value is a literal to a bounding signal', () => { + const rule: BacktestRule = { + side: null, cooldownMin: null, trigger: tick, conditions: vacuous, + actions: [{ kind: 'setTemperature', temp: { kind: 'clamp', value: lit(72), min: sig('ambient.temperature'), max: lit(75) }, clamp: { min: 60, max: 75 } }] as Action[], + } + const r = runBacktest({ rule, timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series: { 'ambient.temperature': ambientSeries(65) } }) + // The clamp's bounding signal is ambient → policy mode tracking ambient. + expect(r.mode).toBe('policy') + }) + + it('rounds a non-positive primary axis maximum up to a whole number', () => { + const cond: Condition = { kind: 'and', conditions: [{ kind: 'compare', op: '>', left: sig('left.movement'), right: lit(0) }] } + const series = { 'left.movement': ambientSeries(0) } // every sample is 0 + const r = runBacktest({ rule: notifyOn(cond), timezone: 'UTC', startMs: 0, endMs: HOUR, stepMin: 1, series }) + expect(r.primaryAxis?.max).toBe(0) + }) +}) diff --git a/src/automation/tests/capReduce.test.ts b/src/automation/tests/capReduce.test.ts new file mode 100644 index 00000000..510fbf83 --- /dev/null +++ b/src/automation/tests/capReduce.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest' +import { zoneTriple } from '../capReduce' + +describe('zoneTriple', () => { + it('returns null for a scalar (Pod 3) frame', () => { + expect(zoneTriple([42])).toBeNull() + }) + + it('returns null below six sensor channels', () => { + expect(zoneTriple([10, 20, 30])).toBeNull() + }) + + it('pairs six channels into head/torso/legs means', () => { + expect(zoneTriple([10, 20, 30, 40, 50, 60])).toEqual([15, 35, 55]) + }) + + it('drops the two reference channels on a full capSense2 frame', () => { + expect(zoneTriple([10, 20, 30, 40, 50, 60, 999, 999])).toEqual([15, 35, 55]) + }) +}) diff --git a/src/automation/tests/engine.test.ts b/src/automation/tests/engine.test.ts new file mode 100644 index 00000000..3b058a58 --- /dev/null +++ b/src/automation/tests/engine.test.ts @@ -0,0 +1,730 @@ +import { describe, expect, it, vi } from 'vitest' +import { AutomationEngine, type AutomationEngineDeps } from '../engine' +import type { SignalSnapshot } from '../signals' +import { AUTOMATION_TICK_MS, type Action, type AutomationRule, type Condition, type DayOfWeek, type Expr, type RunOutcome, type Trigger, type Side } from '../types' + +interface HwCall { + op: 'temp' | 'power' + side: Side + temp?: number + duration?: number + on?: boolean +} + +interface RunDetail { + reason?: string + message?: string + actionsLastHour?: number + actions?: Array<{ + kind: string + skipped?: string + antiThrash?: boolean + clamped?: boolean + sent?: boolean + dryRun?: boolean + temp?: number + }> +} + +interface Harness { + engine: AutomationEngine + setNow: (ms: number) => void + advance: (ms: number) => void + setSignal: (key: string, value: number | undefined) => void + setClock: (nowMinutes: number, dayOfWeek?: DayOfWeek, dateKey?: string) => void + setRunOnce: (active: boolean) => void + runs: { id: number, outcome: RunOutcome, detail: RunDetail }[] + notifies: { id: number, message: string }[] + hwCalls: HwCall[] + disabled: number[] +} + +function makeHarness(rules: AutomationRule[]): Harness { + // Base clock is a realistic epoch so a `tick` trigger's first evaluation is + // due (production `now` is always >> any everyMin window from epoch 0). + let nowMs = 1_700_000_000_000 + let runOnce = false + let nowMinutes = 0 + let dayOfWeek: DayOfWeek = 'monday' + let dateKey = '2026-07-01' + const snapshot: SignalSnapshot = {} + const runs: Harness['runs'] = [] + const notifies: Harness['notifies'] = [] + const hwCalls: HwCall[] = [] + const disabled: number[] = [] + + const deps: AutomationEngineDeps = { + signals: { read: () => ({ ...snapshot }) }, + now: () => nowMs, + clock: () => ({ nowMinutes, dayOfWeek, dateKey }), + getHardware: () => ({ + connect: async () => {}, + setTemperature: async (side, temp, duration) => { hwCalls.push({ op: 'temp', side, temp, duration }) }, + setPower: async (side, on, temp) => { hwCalls.push({ op: 'power', side, on, temp }) }, + }), + withSideLock: async (_side, fn) => fn(), + broadcast: () => {}, + markMutated: () => {}, + loadRules: async () => rules, + recordRun: async (id, outcome, detail) => { runs.push({ id, outcome, detail: detail as RunDetail }) }, + disableRule: async (id) => { disabled.push(id) }, + hasActiveRunOnceSession: async () => runOnce, + notify: (id, message) => notifies.push({ id, message }), + } + + const engine = new AutomationEngine(deps) + return { + engine, + setNow: (ms) => { nowMs = ms }, + advance: (ms) => { nowMs += ms }, + setSignal: (key, value) => { snapshot[key] = value }, + setClock: (m, d, date) => { + nowMinutes = m + if (d) dayOfWeek = d + if (date) dateKey = date + }, + setRunOnce: (a) => { runOnce = a }, + runs, + notifies, + hwCalls, + disabled, + } +} + +const lit = (value: number): Expr => ({ kind: 'literal', value }) +const sig = (signal: string): Expr => ({ kind: 'signal', signal }) +const tickEvery = (everyMin: number): Trigger => ({ kind: 'tick', everyMin }) +const always: Condition = { kind: 'and', conditions: [] } // vacuously true + +function rule(overrides: Partial): AutomationRule { + return { + id: 1, + name: 'test', + enabled: true, + side: 'left', + priority: 0, + dryRun: false, + cooldownMin: null, + trigger: tickEvery(1), + conditions: always, + actions: [], + ...overrides, + } +} + +describe('AutomationEngine — outcomes & audit log', () => { + it('only evaluates a tick trigger when its interval has elapsed', async () => { + const h = makeHarness([rule({ trigger: tickEvery(5), actions: [{ kind: 'notify', message: 'hi' }] })]) + await h.engine.reload() + await h.engine.tick() // first eval is due → 1 row + h.advance(60_000) + await h.engine.tick() // 1 min later, not due → no new row + expect(h.runs).toHaveLength(1) + h.advance(5 * 60_000) + await h.engine.tick() // interval elapsed → new row + expect(h.runs).toHaveLength(2) + }) + + it('logs skipped/condition-unknown when a referenced signal is missing', async () => { + const cond: Condition = { kind: 'compare', op: '>', left: sig('absent'), right: lit(5) } + const h = makeHarness([rule({ conditions: cond, actions: [{ kind: 'notify', message: 'x' }] })]) + await h.engine.reload() + await h.engine.tick() + expect(h.runs[0].outcome).toBe('skipped') + expect(h.runs[0].detail.reason).toBe('condition-unknown') + expect(h.notifies).toHaveLength(0) + }) + + it('logs skipped/condition-false when conditions do not hold', async () => { + const cond: Condition = { kind: 'compare', op: '>', left: sig('x'), right: lit(5) } + const h = makeHarness([rule({ conditions: cond, actions: [{ kind: 'notify', message: 'x' }] })]) + h.setSignal('x', 1) + await h.engine.reload() + await h.engine.tick() + expect(h.runs[0].outcome).toBe('skipped') + expect(h.runs[0].detail.reason).toBe('condition-false') + }) + + it('fires a notify action and logs fired', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'notify', message: 'movement high' }] })]) + await h.engine.reload() + await h.engine.tick() + expect(h.notifies).toEqual([{ id: 1, message: 'movement high' }]) + expect(h.runs[0].outcome).toBe('fired') + }) +}) + +describe('AutomationEngine — dry-run', () => { + it('emits notify but never touches hardware, logging dry_run', async () => { + const actions: Action[] = [ + { kind: 'notify', message: 'would warm' }, + { kind: 'setTemperature', temp: lit(72) }, + ] + const h = makeHarness([rule({ dryRun: true, actions })]) + await h.engine.reload() + await h.engine.tick() + expect(h.hwCalls).toHaveLength(0) + expect(h.notifies).toHaveLength(1) + expect(h.runs[0].outcome).toBe('dry_run') + }) +}) + +describe('AutomationEngine — two-layer temp clamp', () => { + it('clamps to the per-action user band (layer 1), logs clamped', async () => { + const h = makeHarness([rule({ + actions: [{ kind: 'setTemperature', temp: lit(200), clamp: { min: 65, max: 75 } }], + })]) + await h.engine.reload() + await h.engine.tick() + expect(h.hwCalls[0]).toMatchObject({ op: 'temp', side: 'left', temp: 75 }) + expect(h.runs[0].outcome).toBe('clamped') + }) + + it('falls back to the default user band when no clamp is given', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'setTemperature', temp: lit(40) }] })]) + await h.engine.reload() + await h.engine.tick() + expect(h.hwCalls[0].temp).toBe(60) // AUTOMATION_DEFAULT_USER_MIN + }) + + it('applies the hardware bound (layer 2) even past the user band', async () => { + // User band intentionally wider than hardware (engine clamps defensively). + const h = makeHarness([rule({ + actions: [{ kind: 'setTemperature', temp: lit(200), clamp: { min: 50, max: 120 } }], + })]) + await h.engine.reload() + await h.engine.tick() + expect(h.hwCalls[0].temp).toBe(110) // MAX_TEMP + }) +}) + +describe('AutomationEngine — both-side fan-out', () => { + it('applies a null-side (both) hardware action to left and right', async () => { + const h = makeHarness([rule({ + side: null, + actions: [{ kind: 'setTemperature', temp: lit(72), clamp: { min: 60, max: 100 } }], + })]) + await h.engine.reload() + await h.engine.tick() + expect(h.hwCalls.map(c => c.side).sort()).toEqual(['left', 'right']) + expect(h.hwCalls.every(c => c.temp === 72)).toBe(true) + expect(h.runs[0].detail.actions).toHaveLength(2) + expect(h.runs[0].outcome).toBe('fired') + }) + + it('still honours an explicit per-action side over the null rule side', async () => { + const h = makeHarness([rule({ + side: null, + actions: [{ kind: 'setTemperature', side: 'right', temp: lit(72), clamp: { min: 60, max: 100 } }], + })]) + await h.engine.reload() + await h.engine.tick() + expect(h.hwCalls).toHaveLength(1) + expect(h.hwCalls[0].side).toBe('right') + }) +}) + +describe('AutomationEngine — anti-thrash', () => { + it('does not re-send a setpoint that moved less than 0.5°F', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'setTemperature', temp: sig('target') }] })]) + h.setSignal('target', 80) + await h.engine.reload() + await h.engine.tick() // writes 80 + h.advance(60_000) + h.setSignal('target', 80.3) // within 0.5 of 80 → no write + await h.engine.tick() + expect(h.hwCalls).toHaveLength(1) + expect(h.runs[1].outcome).toBe('fired') // setpoint maintained + expect(h.runs[1].detail.actions?.[0]?.antiThrash).toBe(true) + + h.advance(60_000) + h.setSignal('target', 81) // moved ≥0.5 → write + await h.engine.tick() + expect(h.hwCalls).toHaveLength(2) + expect(h.hwCalls[1].temp).toBe(81) + }) +}) + +describe('AutomationEngine — runaway guard', () => { + it('auto-disables a rule that exceeds the hourly action budget', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'setTemperature', temp: sig('target') }] })]) + await h.engine.reload() + // Alternate the setpoint far enough each tick that anti-thrash never blocks. + for (let i = 0; i < 13; i++) { + h.setSignal('target', i % 2 === 0 ? 70 : 90) + await h.engine.tick() + h.advance(60_000) + } + // 12 writes allowed, the 13th eval trips the guard. + expect(h.hwCalls).toHaveLength(12) + expect(h.disabled).toContain(1) + const errorRun = h.runs.find(r => r.outcome === 'error') + expect(errorRun?.detail.reason).toBe('runaway-disabled') + }) +}) + +describe('AutomationEngine — precedence gates', () => { + it('skips hardware while a manual-override hold is active on the side', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'setTemperature', temp: lit(72) }] })]) + await h.engine.reload() + h.engine.registerManualOverride('left') + await h.engine.tick() + expect(h.hwCalls).toHaveLength(0) + expect(h.runs[0].outcome).toBe('skipped') + expect(h.runs[0].detail.actions?.[0]?.skipped).toBe('manual-override') + }) + + it('skips hardware while a run-once session is active on the side', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'setTemperature', temp: lit(72) }] })]) + h.setRunOnce(true) + await h.engine.reload() + await h.engine.tick() + expect(h.hwCalls).toHaveLength(0) + expect(h.runs[0].detail.actions?.[0]?.skipped).toBe('run-once') + }) +}) + +describe('AutomationEngine — cooldown', () => { + it('skips re-firing within the cooldown window', async () => { + const h = makeHarness([rule({ cooldownMin: 30, actions: [{ kind: 'notify', message: 'x' }] })]) + await h.engine.reload() + await h.engine.tick() // fires + h.advance(5 * 60_000) // 5 min later, still within 30m cooldown + await h.engine.tick() + expect(h.runs[0].outcome).toBe('fired') + expect(h.runs[1].outcome).toBe('skipped') + expect(h.runs[1].detail.reason).toBe('cooldown') + }) +}) + +describe('AutomationEngine — windowed aggregate (example 2)', () => { + it('fires when avg(movement, 10m) crosses the threshold', async () => { + const cond: Condition = { + kind: 'compare', + op: '>', + left: { kind: 'window', fn: 'avg', signal: 'left.movement', lastMin: 10 }, + right: lit(200), + } + const h = makeHarness([rule({ conditions: cond, actions: [{ kind: 'notify', message: 'restless' }] })]) + h.setSignal('left.movement', 300) + await h.engine.reload() + // First tick records a sample then evaluates avg=300>200 → fires. + await h.engine.tick() + expect(h.runs[0].outcome).toBe('fired') + expect(h.notifies[0].message).toBe('restless') + }) +}) + +describe('AutomationEngine — triggers', () => { + it('signalChange fires only when the signal value changes', async () => { + const h = makeHarness([rule({ + trigger: { kind: 'signalChange', signal: 'ambient.temperature' }, + actions: [{ kind: 'notify', message: 'ambient moved' }], + })]) + h.setSignal('ambient.temperature', 68) + await h.engine.reload() + await h.engine.tick() // first observation: baseline, no fire + expect(h.runs).toHaveLength(0) + await h.engine.tick() // unchanged → no fire + expect(h.runs).toHaveLength(0) + h.setSignal('ambient.temperature', 70) + await h.engine.tick() // changed → fire + expect(h.runs).toHaveLength(1) + expect(h.runs[0].outcome).toBe('fired') + }) + + it('timeOfDay fires once at the matching minute', async () => { + const h = makeHarness([rule({ + trigger: { kind: 'timeOfDay', at: '23:00' }, + actions: [{ kind: 'notify', message: 'bedtime' }], + })]) + h.setClock(22 * 60 + 59) + await h.engine.reload() + await h.engine.tick() // 22:59 → no + expect(h.runs).toHaveLength(0) + h.setClock(23 * 60) + await h.engine.tick() // 23:00 → fire + await h.engine.tick() // same minute → no double-fire + expect(h.runs).toHaveLength(1) + }) + + it('timeOfDay fires on a late tick within the grace window', async () => { + // Regression: exact-minute equality skipped the slot entirely when a + // tick landed >60s late (engine stall, ~7s scheduler reloads, GC). + const h = makeHarness([rule({ + trigger: { kind: 'timeOfDay', at: '23:00' }, + actions: [{ kind: 'notify', message: 'bedtime' }], + })]) + await h.engine.reload() + h.setClock(22 * 60 + 59) + await h.engine.tick() // 22:59 → not yet + h.setClock(23 * 60 + 3) + await h.engine.tick() // 23:03 — tick skipped past 23:00 → still fires + expect(h.runs).toHaveLength(1) + await h.engine.tick() // later tick same day → no double-fire + expect(h.runs).toHaveLength(1) + }) + + it('timeOfDay does not resurrect a slot beyond the grace window', async () => { + // An engine (re)started hours later must not fire a long-gone slot. + const h = makeHarness([rule({ + trigger: { kind: 'timeOfDay', at: '07:00' }, + actions: [{ kind: 'notify', message: 'morning' }], + })]) + await h.engine.reload() + h.setClock(15 * 60) // 15:00 + await h.engine.tick() + expect(h.runs).toHaveLength(0) + }) + + it('timeOfDay re-arms on the next calendar day', async () => { + // Regression: weekday-keyed state blocked the same slot the following + // week for the life of the process. + const h = makeHarness([rule({ + trigger: { kind: 'timeOfDay', at: '23:00' }, + actions: [{ kind: 'notify', message: 'bedtime' }], + })]) + await h.engine.reload() + h.setClock(23 * 60, 'monday', '2026-07-06') + await h.engine.tick() + expect(h.runs).toHaveLength(1) + + // Same weekday, one week later + h.setClock(23 * 60, 'monday', '2026-07-13') + await h.engine.tick() + expect(h.runs).toHaveLength(2) + }) +}) + +describe('AutomationEngine — disabled rules', () => { + it('never evaluates a disabled rule', async () => { + const h = makeHarness([rule({ enabled: false, actions: [{ kind: 'notify', message: 'x' }] })]) + await h.engine.reload() + await h.engine.tick() + expect(h.runs).toHaveLength(0) + expect(h.notifies).toHaveLength(0) + }) +}) + +describe('AutomationEngine — lifecycle', () => { + it('start() installs the tick timer and stop() clears it', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'notify', message: 'x' }] })]) + await h.engine.start() // reloads + installs interval + h.engine.stop() // clears the interval + // A second stop() is a safe no-op. + h.engine.stop() + }) +}) + +describe('AutomationEngine — global kill-switch', () => { + it('halts all evaluation while disabled and resumes when re-enabled', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'notify', message: 'x' }] })]) + await h.engine.reload() + expect(h.engine.isGloballyEnabled()).toBe(true) + + h.engine.setGlobalEnabled(false) + expect(h.engine.isGloballyEnabled()).toBe(false) + await h.engine.tick() + expect(h.runs).toHaveLength(0) + + h.engine.setGlobalEnabled(true) + await h.engine.tick() + expect(h.runs).toHaveLength(1) + }) +}) + +describe('AutomationEngine — reload', () => { + it('drops runtime for rules that no longer exist', async () => { + const rules = [rule({ id: 1, actions: [{ kind: 'notify', message: 'a' }] })] + const h = makeHarness(rules) + await h.engine.reload() + await h.engine.tick() // creates runtime for rule 1 + // Replace the rule set; reload should prune runtime for the removed id 1. + rules.length = 0 + rules.push(rule({ id: 2, actions: [{ kind: 'notify', message: 'b' }] })) + await h.engine.reload() + await h.engine.tick() + expect(h.runs.some(r => r.id === 2)).toBe(true) + }) + + it('resets trigger memory when an existing rule changes trigger shape', async () => { + const rules = [rule({ + trigger: { kind: 'signalChange', signal: 'x' }, + actions: [{ kind: 'notify', message: 'changed' }], + })] + const h = makeHarness(rules) + h.setSignal('x', 1) + await h.engine.reload() + await h.engine.tick() // first observation only; no change yet + expect(h.runs).toHaveLength(0) + + rules[0] = rule({ + trigger: { kind: 'signalChange', signal: 'y' }, + actions: [{ kind: 'notify', message: 'changed' }], + }) + h.setSignal('y', 2) + await h.engine.reload() + await h.engine.tick() + expect(h.runs).toHaveLength(0) + + h.setSignal('y', 3) + await h.engine.tick() + expect(h.runs[0].outcome).toBe('fired') + }) +}) + +describe('AutomationEngine — setPower', () => { + it('writes power on with a resolved temperature and tracks the setpoint', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'setPower', on: true, temp: lit(72) }] })]) + await h.engine.reload() + await h.engine.tick() + expect(h.hwCalls[0]).toMatchObject({ op: 'power', side: 'left', on: true, temp: 72 }) + expect(h.runs[0].outcome).toBe('fired') + }) + + it('writes power off without a temperature', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'setPower', on: false }] })]) + await h.engine.reload() + await h.engine.tick() + expect(h.hwCalls[0]).toMatchObject({ op: 'power', side: 'left', on: false }) + expect(h.runs[0].outcome).toBe('fired') + }) +}) + +describe('AutomationEngine — unresolved temperature', () => { + it('skips a setTemperature whose expression resolves to undefined', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'setTemperature', temp: sig('absent') }] })]) + await h.engine.reload() + await h.engine.tick() + expect(h.hwCalls).toHaveLength(0) + expect(h.runs[0].outcome).toBe('skipped') + expect(h.runs[0].detail.actions?.[0]?.skipped).toBe('temp-unknown') + }) + + it('skips setPower when an explicit power-on temp expression is unknown', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'setPower', on: true, temp: sig('absent') }] })]) + await h.engine.reload() + await h.engine.tick() + expect(h.hwCalls).toHaveLength(0) + expect(h.runs[0].outcome).toBe('skipped') + expect(h.runs[0].detail.actions?.[0]?.skipped).toBe('temp-unknown') + }) +}) + +describe('AutomationEngine — window-bounded eval', () => { + it('handles windows nested in clamp/binary action temps and not/between/time conditions', async () => { + const win: Expr = { kind: 'window', fn: 'avg', signal: 'left.movement', lastMin: 30 } + const conditions: Condition = { + kind: 'and', + conditions: [ + { kind: 'not', condition: { kind: 'between', subject: win, min: lit(0), max: lit(1000) } }, + { kind: 'timeBetween', start: '23:00', end: '06:00' }, + ], + } + const action: Action = { + kind: 'setTemperature', + temp: { kind: 'clamp', value: { kind: 'binary', op: '+', left: win, right: lit(1) }, min: lit(60), max: lit(75) }, + } + const h = makeHarness([rule({ conditions, actions: [action] })]) + h.setSignal('left.movement', 500) + await h.engine.reload() + // Just needs to evaluate without throwing — exercises maxWindowMinutes' walk + // over the condition/expression trees. + await expect(h.engine.tick()).resolves.toBeUndefined() + expect(h.runs).toHaveLength(1) + }) +}) + +describe('AutomationEngine — action error', () => { + it('records an error outcome when a hardware write throws', async () => { + const runs: { id: number, outcome: RunOutcome, detail: RunDetail }[] = [] + const deps: AutomationEngineDeps = { + signals: { read: () => ({}) }, + now: () => 1_700_000_000_000, + clock: () => ({ nowMinutes: 0, dayOfWeek: 'monday' }), + getHardware: () => ({ + connect: async () => { throw new Error('hardware offline') }, + setTemperature: async () => {}, + setPower: async () => {}, + }), + withSideLock: async (_side, fn) => fn(), + broadcast: () => {}, + markMutated: () => {}, + loadRules: async () => [rule({ actions: [{ kind: 'setTemperature', temp: lit(72) }] })], + recordRun: async (id, outcome, detail) => { runs.push({ id, outcome, detail: detail as RunDetail }) }, + disableRule: async () => {}, + hasActiveRunOnceSession: async () => false, + notify: () => {}, + } + const engine = new AutomationEngine(deps) + await engine.reload() + await engine.tick() + expect(runs[0].outcome).toBe('error') + expect(runs[0].detail.reason).toBe('eval-threw') + }) + + it('stringifies a non-Error thrown during evaluation', async () => { + const runs: { id: number, outcome: RunOutcome, detail: RunDetail }[] = [] + const deps: AutomationEngineDeps = { + signals: { read: () => ({}) }, + now: () => 1_700_000_000_000, + clock: () => ({ nowMinutes: 0, dayOfWeek: 'monday' }), + getHardware: () => ({ connect: async () => {}, setTemperature: async () => {}, setPower: async () => {} }), + withSideLock: async (_side, fn) => fn(), + broadcast: () => {}, + markMutated: () => {}, + loadRules: async () => [rule({ actions: [{ kind: 'setTemperature', temp: lit(72) }] })], + recordRun: async (id, outcome, detail) => { runs.push({ id, outcome, detail: detail as RunDetail }) }, + disableRule: async () => {}, + // A non-Error rejection inside the per-rule try → caught and String()'d. + hasActiveRunOnceSession: async () => { throw 'gate exploded' }, + notify: () => {}, + } + const engine = new AutomationEngine(deps) + await engine.reload() + await engine.tick() + expect(runs[0].outcome).toBe('error') + expect(runs[0].detail.message).toBe('gate exploded') + }) +}) + +describe('AutomationEngine — branch coverage corners', () => { + it('start() is idempotent — a second call does not install a second timer', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'notify', message: 'x' }] })]) + await h.engine.start() + await h.engine.start() // timer already set → no-op on the interval + h.engine.stop() + }) + + it('the interval callback drives ticks', async () => { + vi.useFakeTimers() + try { + const h = makeHarness([rule({ actions: [{ kind: 'notify', message: 'x' }] })]) + await h.engine.start() + await vi.advanceTimersByTimeAsync(AUTOMATION_TICK_MS) + expect(h.runs.length).toBeGreaterThanOrEqual(1) + h.engine.stop() + } + finally { + vi.useRealTimers() + } + }) + + it('never overlaps two ticks', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'notify', message: 'x' }] })]) + await h.engine.reload() + // The first tick suspends at its first await with `ticking` set; the second + // sees it and bails immediately. + const p1 = h.engine.tick() + const p2 = h.engine.tick() + await Promise.all([p1, p2]) + expect(h.runs).toHaveLength(1) + }) + + it('keeps runtime for surviving rules while pruning removed ones', async () => { + const rules = [ + rule({ id: 1, actions: [{ kind: 'notify', message: 'a' }] }), + rule({ id: 2, actions: [{ kind: 'notify', message: 'b' }] }), + ] + const h = makeHarness(rules) + await h.engine.reload() + await h.engine.tick() // runtime for both 1 and 2 + rules.length = 0 + rules.push(rule({ id: 1, actions: [{ kind: 'notify', message: 'a' }] })) // keep 1, drop 2 + await h.engine.reload() + await h.engine.tick() + expect(h.runs.some(r => r.id === 1)).toBe(true) + }) + + it('logs through the optional log hook on lifecycle and kill-switch transitions', async () => { + const logs: string[] = [] + const deps: AutomationEngineDeps = { + signals: { read: () => ({}) }, + now: () => 1_700_000_000_000, + clock: () => ({ nowMinutes: 0, dayOfWeek: 'monday' }), + getHardware: () => ({ connect: async () => {}, setTemperature: async () => {}, setPower: async () => {} }), + withSideLock: async (_side, fn) => fn(), + broadcast: () => {}, + markMutated: () => {}, + loadRules: async () => [], + recordRun: async () => {}, + disableRule: async () => {}, + hasActiveRunOnceSession: async () => false, + notify: () => {}, + log: msg => logs.push(msg), + } + const engine = new AutomationEngine(deps) + await engine.start() + engine.setGlobalEnabled(false) + engine.setGlobalEnabled(true) + engine.stop() + expect(logs.some(m => m.includes('started'))).toBe(true) + expect(logs.some(m => m.includes('OFF'))).toBe(true) + expect(logs.some(m => m.includes('ON'))).toBe(true) + }) + + it('does not record a windowed sample when the signal is absent this tick', async () => { + const cond: Condition = { + kind: 'compare', + op: '>', + left: { kind: 'window', fn: 'avg', signal: 'left.movement', lastMin: 10 }, + right: lit(200), + } + const h = makeHarness([rule({ conditions: cond, actions: [{ kind: 'notify', message: 'x' }] })]) + await h.engine.reload() // left.movement never set → nothing to record → avg unknown + await h.engine.tick() + expect(h.runs[0].outcome).toBe('skipped') + expect(h.runs[0].detail.reason).toBe('condition-unknown') + }) + + it('signalChange stays inactive while its signal is unavailable', async () => { + const h = makeHarness([rule({ + trigger: { kind: 'signalChange', signal: 'absent' }, + actions: [{ kind: 'notify', message: 'x' }], + })]) + await h.engine.reload() + await h.engine.tick() + expect(h.runs).toHaveLength(0) + }) + + it('timeOfDay does not fire on a day outside its day filter', async () => { + const h = makeHarness([rule({ + trigger: { kind: 'timeOfDay', at: '23:00', days: ['saturday'] }, + actions: [{ kind: 'notify', message: 'x' }], + })]) + h.setClock(23 * 60, 'monday') // matching minute, wrong day + await h.engine.reload() + await h.engine.tick() + expect(h.runs).toHaveLength(0) + }) + + it('reports dry_run for a dry-run setPower without touching hardware', async () => { + const h = makeHarness([rule({ dryRun: true, actions: [{ kind: 'setPower', on: true, temp: lit(72) }] })]) + await h.engine.reload() + await h.engine.tick() + expect(h.hwCalls).toHaveLength(0) + expect(h.runs[0].outcome).toBe('dry_run') + }) + + it('writes power on with the hardware default temperature when none is resolved', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'setPower', on: true }] })]) // no temp + await h.engine.reload() + await h.engine.tick() + expect(h.hwCalls[0]).toMatchObject({ op: 'power', side: 'left', on: true }) + expect(h.hwCalls[0].temp).toBeUndefined() + expect(h.runs[0].outcome).toBe('fired') + }) + + it('reports clamped when an anti-thrash re-assertion is still out of band', async () => { + const h = makeHarness([rule({ actions: [{ kind: 'setTemperature', temp: sig('target'), clamp: { min: 60, max: 75 } }] })]) + h.setSignal('target', 200) // clamps to 75, sent + clamped + await h.engine.reload() + await h.engine.tick() + expect(h.runs[0].outcome).toBe('clamped') + h.advance(60_000) + h.setSignal('target', 201) // clamps to 75 again → anti-thrash, still clamped + await h.engine.tick() + expect(h.runs[1].detail.actions?.[0]?.antiThrash).toBe(true) + expect(h.runs[1].outcome).toBe('clamped') + }) +}) diff --git a/src/automation/tests/evaluator.test.ts b/src/automation/tests/evaluator.test.ts new file mode 100644 index 00000000..cd0b52ae --- /dev/null +++ b/src/automation/tests/evaluator.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it } from 'vitest' +import { evaluateCondition } from '../evaluator' +import type { EvalContext } from '../expressions' +import { WindowStore } from '../windows' +import type { CompareOp, Condition, DayOfWeek } from '../types' + +function ctx(opts: { + signals?: Record + nowMinutes?: number + dayOfWeek?: DayOfWeek +}): EvalContext { + return { + signal: k => (opts.signals ?? {})[k], + windows: new WindowStore(), + nowMs: 0, + nowMinutes: opts.nowMinutes ?? 0, + dayOfWeek: opts.dayOfWeek ?? 'monday', + } +} + +const cmp = (op: CompareOp, signal: string, value: number): Condition => ({ + kind: 'compare', + op, + left: { kind: 'signal', signal }, + right: { kind: 'literal', value }, +}) + +describe('evaluateCondition — comparisons', () => { + it('evaluates numeric comparisons', () => { + const c = ctx({ signals: { x: 10 } }) + expect(evaluateCondition(cmp('>', 'x', 5), c)).toBe(true) + expect(evaluateCondition(cmp('<', 'x', 5), c)).toBe(false) + expect(evaluateCondition(cmp('>=', 'x', 10), c)).toBe(true) + expect(evaluateCondition(cmp('<=', 'x', 10), c)).toBe(true) + expect(evaluateCondition(cmp('==', 'x', 10), c)).toBe(true) + expect(evaluateCondition(cmp('!=', 'x', 10), c)).toBe(false) + }) + + it('returns undefined when a signal is unavailable', () => { + const c = ctx({ signals: {} }) + expect(evaluateCondition(cmp('>', 'x', 5), c)).toBeUndefined() + }) +}) + +describe('evaluateCondition — three-valued AND/OR/NOT', () => { + it('AND short-circuits on a definite false, else unknown if any unknown', () => { + const known = ctx({ signals: { a: 1, b: 1 } }) + expect(evaluateCondition({ kind: 'and', conditions: [cmp('>', 'a', 0), cmp('>', 'b', 0)] }, known)).toBe(true) + + const oneFalse = ctx({ signals: { a: 1 } }) // b missing + // a>0 true, b>0 unknown → unknown + expect(evaluateCondition({ kind: 'and', conditions: [cmp('>', 'a', 0), cmp('>', 'b', 0)] }, oneFalse)).toBeUndefined() + // a<0 false dominates even with b unknown + expect(evaluateCondition({ kind: 'and', conditions: [cmp('<', 'a', 0), cmp('>', 'b', 0)] }, oneFalse)).toBe(false) + }) + + it('OR short-circuits on a definite true, else unknown if any unknown', () => { + const c = ctx({ signals: { a: 1 } }) // b missing + expect(evaluateCondition({ kind: 'or', conditions: [cmp('>', 'a', 0), cmp('>', 'b', 0)] }, c)).toBe(true) + expect(evaluateCondition({ kind: 'or', conditions: [cmp('<', 'a', 0), cmp('>', 'b', 0)] }, c)).toBeUndefined() + }) + + it('OR of all-definite-false members is false', () => { + const c = ctx({ signals: { a: 1 } }) + expect(evaluateCondition({ kind: 'or', conditions: [cmp('<', 'a', 0), cmp('>', 'a', 5)] }, c)).toBe(false) + }) + + it('NOT inverts, preserving unknown', () => { + const c = ctx({ signals: { a: 1 } }) + expect(evaluateCondition({ kind: 'not', condition: cmp('>', 'a', 0) }, c)).toBe(false) + expect(evaluateCondition({ kind: 'not', condition: cmp('>', 'missing', 0) }, c)).toBeUndefined() + }) +}) + +describe('evaluateCondition — between/time/days', () => { + it('evaluates between (inclusive)', () => { + const c = ctx({ signals: { x: 5 } }) + const between: Condition = { + kind: 'between', + subject: { kind: 'signal', signal: 'x' }, + min: { kind: 'literal', value: 0 }, + max: { kind: 'literal', value: 10 }, + } + expect(evaluateCondition(between, c)).toBe(true) + }) + + it('returns undefined for a between whose subject is unavailable', () => { + const between: Condition = { + kind: 'between', + subject: { kind: 'signal', signal: 'missing' }, + min: { kind: 'literal', value: 0 }, + max: { kind: 'literal', value: 10 }, + } + expect(evaluateCondition(between, ctx({ signals: {} }))).toBeUndefined() + }) + + it('handles a same-day time window (09:00–17:00)', () => { + const win: Condition = { kind: 'timeBetween', start: '09:00', end: '17:00' } + expect(evaluateCondition(win, ctx({ nowMinutes: 12 * 60 }))).toBe(true) // inside + expect(evaluateCondition(win, ctx({ nowMinutes: 8 * 60 }))).toBe(false) // before start + expect(evaluateCondition(win, ctx({ nowMinutes: 18 * 60 }))).toBe(false) // after end + }) + + it('treats a zero-width time window as always false', () => { + const win: Condition = { kind: 'timeBetween', start: '09:00', end: '09:00' } + expect(evaluateCondition(win, ctx({ nowMinutes: 9 * 60 }))).toBe(false) + }) + + it('handles a time window that wraps past midnight (23:00–06:00)', () => { + const win: Condition = { kind: 'timeBetween', start: '23:00', end: '06:00' } + expect(evaluateCondition(win, ctx({ nowMinutes: 23 * 60 + 30 }))).toBe(true) // 23:30 + expect(evaluateCondition(win, ctx({ nowMinutes: 2 * 60 }))).toBe(true) // 02:00 + expect(evaluateCondition(win, ctx({ nowMinutes: 12 * 60 }))).toBe(false) // 12:00 + }) + + it('matches onDays', () => { + const days: Condition = { kind: 'onDays', days: ['saturday', 'sunday'] } + expect(evaluateCondition(days, ctx({ dayOfWeek: 'saturday' }))).toBe(true) + expect(evaluateCondition(days, ctx({ dayOfWeek: 'monday' }))).toBe(false) + }) +}) diff --git a/src/automation/tests/expressions.test.ts b/src/automation/tests/expressions.test.ts new file mode 100644 index 00000000..6ed36267 --- /dev/null +++ b/src/automation/tests/expressions.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, it } from 'vitest' +import { clamp, evaluateExpr, type EvalContext } from '../expressions' +import { WindowStore } from '../windows' +import type { Expr } from '../types' + +function ctx(signals: Record, windows = new WindowStore()): EvalContext { + return { + signal: k => signals[k], + windows, + nowMs: 0, + nowMinutes: 0, + dayOfWeek: 'monday', + } +} + +describe('clamp', () => { + it('respects min and max, ignoring undefined bounds', () => { + expect(clamp(5, 0, 10)).toBe(5) + expect(clamp(-1, 0, 10)).toBe(0) + expect(clamp(11, 0, 10)).toBe(10) + expect(clamp(11, undefined, 10)).toBe(10) + expect(clamp(-5, 0, undefined)).toBe(0) + expect(clamp(42, undefined, undefined)).toBe(42) + }) +}) + +describe('evaluateExpr', () => { + it('resolves literals and signals', () => { + const c = ctx({ 'ambient.temperature': 68 }) + expect(evaluateExpr({ kind: 'literal', value: 3 }, c)).toBe(3) + expect(evaluateExpr({ kind: 'signal', signal: 'ambient.temperature' }, c)).toBe(68) + }) + + it('computes ambient + 3 (the continuous-policy expression)', () => { + const c = ctx({ 'ambient.temperature': 68 }) + const expr: Expr = { + kind: 'binary', + op: '+', + left: { kind: 'signal', signal: 'ambient.temperature' }, + right: { kind: 'literal', value: 3 }, + } + expect(evaluateExpr(expr, c)).toBe(71) + }) + + it('propagates undefined through arithmetic when a signal is missing', () => { + const c = ctx({}) + const expr: Expr = { + kind: 'binary', + op: '+', + left: { kind: 'signal', signal: 'ambient.temperature' }, + right: { kind: 'literal', value: 3 }, + } + expect(evaluateExpr(expr, c)).toBeUndefined() + }) + + it('computes subtraction and multiplication', () => { + const c = ctx({ 'left.currentTemperature': 80 }) + const sub: Expr = { kind: 'binary', op: '-', left: { kind: 'signal', signal: 'left.currentTemperature' }, right: { kind: 'literal', value: 2 } } + const mul: Expr = { kind: 'binary', op: '*', left: { kind: 'literal', value: 4 }, right: { kind: 'literal', value: 3 } } + expect(evaluateExpr(sub, c)).toBe(78) + expect(evaluateExpr(mul, c)).toBe(12) + }) + + it('treats divide-by-zero as undefined', () => { + const c = ctx({}) + const expr: Expr = { + kind: 'binary', + op: '/', + left: { kind: 'literal', value: 10 }, + right: { kind: 'literal', value: 0 }, + } + expect(evaluateExpr(expr, c)).toBeUndefined() + }) + + it('divides when the denominator is non-zero', () => { + const c = ctx({}) + const expr: Expr = { kind: 'binary', op: '/', left: { kind: 'literal', value: 10 }, right: { kind: 'literal', value: 4 } } + expect(evaluateExpr(expr, c)).toBe(2.5) + }) + + it('returns undefined for an unrecognized binary operator', () => { + const c = ctx({}) + const expr = { kind: 'binary', op: '%', left: { kind: 'literal', value: 10 }, right: { kind: 'literal', value: 4 } } as unknown as Expr + expect(evaluateExpr(expr, c)).toBeUndefined() + }) + + it('evaluates clamp() expressions', () => { + const c = ctx({}) + const expr: Expr = { + kind: 'clamp', + value: { kind: 'literal', value: 200 }, + min: { kind: 'literal', value: 60 }, + max: { kind: 'literal', value: 100 }, + } + expect(evaluateExpr(expr, c)).toBe(100) + }) + + it('propagates undefined out of a clamp whose value is unavailable', () => { + const c = ctx({}) + const expr: Expr = { + kind: 'clamp', + value: { kind: 'signal', signal: 'missing' }, + min: { kind: 'literal', value: 60 }, + max: { kind: 'literal', value: 100 }, + } + expect(evaluateExpr(expr, c)).toBeUndefined() + }) + + it('reads windowed aggregates from the store', () => { + const windows = new WindowStore() + windows.record('left.movement', 300, 0) + windows.record('left.movement', 100, 0) + const c = { ...ctx({}, windows), nowMs: 0 } + expect(evaluateExpr({ kind: 'window', fn: 'avg', signal: 'left.movement', lastMin: 10 }, c)).toBe(200) + }) +}) diff --git a/src/automation/tests/instance.test.ts b/src/automation/tests/instance.test.ts new file mode 100644 index 00000000..d5a972a6 --- /dev/null +++ b/src/automation/tests/instance.test.ts @@ -0,0 +1,308 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Tests for src/automation/instance.ts — the AutomationEngine singleton wrapper. + * + * Covers: lazy init + caching, timezone-from-db with fallbacks, kill-switch + * restore from persisted settings (on / off / unreadable), teardown, and the + * production dependency closures the wrapper injects (loadRules, recordRun, + * disableRule, hasActiveRunOnceSession, and the hardware/broadcast wiring). + * + * The AutomationEngine class and the hardware/signal modules are mocked at the + * import boundary so only the wrapper's wiring is exercised; engine behaviour + * is covered in engine.test.ts. + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const ctorMock = vi.fn() +const startMock = vi.fn(async () => {}) +const stopMock = vi.fn() +const setGlobalEnabledMock = vi.fn() +let capturedDeps: any = null + +vi.mock('../engine', () => { + class AutomationEngine { + constructor(deps: any) { + capturedDeps = deps + ctorMock(deps) + } + + start = startMock + stop = stopMock + setGlobalEnabled = setGlobalEnabledMock + } + return { AutomationEngine } +}) + +vi.mock('../signals', () => ({ + DeviceSignalReader: class { + read() { return {} } + }, + CompositeSignalReader: class { + constructor(readonly readers: Array<{ read: () => Record }>) {} + read() { return Object.assign({}, ...this.readers.map(r => r.read())) } + }, + clockInTimezone: vi.fn(() => ({ nowMinutes: 123, dayOfWeek: 'monday' })), +})) + +vi.mock('../signals.biometrics', () => ({ + BiometricsSignalReader: class { + read() { return {} } + }, +})) + +const hardwareClient = { connect: async () => {}, setTemperature: async () => {}, setPower: async () => {} } +const markSideMutatedMock = vi.fn() +const broadcastMock = vi.fn() + +vi.mock('@/src/hardware/dacMonitor.instance', () => ({ getSharedHardwareClient: () => hardwareClient })) +vi.mock('@/src/hardware/deviceStateSync', () => ({ markSideMutated: markSideMutatedMock })) +vi.mock('@/src/hardware/sideLock', () => ({ withSideLock: async (_side: any, fn: () => Promise) => fn() })) +vi.mock('@/src/streaming/broadcastMutationStatus', () => ({ broadcastMutationStatus: broadcastMock })) +vi.mock('drizzle-orm', () => ({ and: (...a: any[]) => ({ a }), eq: (...a: any[]) => ({ a }), gt: (...a: any[]) => ({ a }) })) +vi.mock('@/src/db/schema', () => ({ + automationRuns: {}, + automations: { id: 'id' }, + deviceSettings: { autopilotEnabled: 'autopilotEnabled', timezone: 'timezone' }, + runOnceSessions: { id: 'id', side: 'side', status: 'status', expiresAt: 'expiresAt' }, +})) + +// Reassignable behaviours the db mock delegates to. +let selectImpl: () => Promise = async () => [] +let insertImpl: (v: unknown) => Promise = async () => {} + +vi.mock('@/src/db', () => { + const thenable = (): any => { + const p: any = { + from() { return p }, + where() { return p }, + limit() { return p }, + then(res: (v: any) => void, rej?: (e: unknown) => void) { selectImpl().then(res, rej) }, + } + return p + } + return { + db: { + select: () => thenable(), + insert: () => ({ values: (v: unknown) => insertImpl(v) }), + update: () => ({ set: () => ({ where: async (w: unknown) => w }) }), + }, + } +}) + +import type * as InstanceModuleTypes from '../instance' +type InstanceModule = typeof InstanceModuleTypes + +async function freshModule(): Promise { + vi.resetModules() + return await import('../instance') +} + +beforeEach(() => { + ctorMock.mockClear() + startMock.mockClear() + stopMock.mockClear() + setGlobalEnabledMock.mockClear() + markSideMutatedMock.mockClear() + broadcastMock.mockClear() + capturedDeps = null + selectImpl = async () => [] + insertImpl = async () => {} +}) + +describe('automation/instance — init & caching', () => { + it('constructs the engine, starts it, and caches the instance', async () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}) + selectImpl = async () => [{ timezone: 'UTC', on: true }] + const mod = await freshModule() + + expect(mod.getAutomationEngineIfRunning()).toBeNull() + const a = await mod.getAutomationEngine() + const b = await mod.getAutomationEngine() + + expect(a).toBe(b) + expect(ctorMock).toHaveBeenCalledTimes(1) + expect(startMock).toHaveBeenCalledTimes(1) + expect(mod.getAutomationEngineIfRunning()).toBe(a) + expect(setGlobalEnabledMock).not.toHaveBeenCalled() // kill-switch on → no override + expect(log).toHaveBeenCalledWith('AutomationEngine initialized with timezone:', 'UTC') + log.mockRestore() + }) + + it('returns the same in-flight promise to concurrent callers', async () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}) + selectImpl = async () => [{ timezone: 'UTC', on: true }] + const mod = await freshModule() + // Both calls race before the first init resolves → second hits the cached promise. + const [a, b] = await Promise.all([mod.getAutomationEngine(), mod.getAutomationEngine()]) + expect(a).toBe(b) + expect(ctorMock).toHaveBeenCalledTimes(1) + log.mockRestore() + }) + + it('falls back to the default timezone when no settings row exists', async () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}) + selectImpl = async () => [] + const mod = await freshModule() + await mod.getAutomationEngine() + expect(log).toHaveBeenCalledWith('AutomationEngine initialized with timezone:', 'America/Los_Angeles') + log.mockRestore() + }) + + it('falls back to the default timezone when the db read throws', async () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}) + selectImpl = async () => { + throw new Error('db down') + } + const mod = await freshModule() + await mod.getAutomationEngine() + expect(log).toHaveBeenCalledWith('AutomationEngine initialized with timezone:', 'America/Los_Angeles') + log.mockRestore() + }) +}) + +describe('automation/instance — kill-switch restore', () => { + it('engages the kill-switch when the persisted setting is off', async () => { + selectImpl = async () => [{ timezone: 'UTC', on: false }] + const mod = await freshModule() + await mod.getAutomationEngine() + expect(setGlobalEnabledMock).toHaveBeenCalledWith(false) + }) + + it('leaves autopilot enabled when the kill-switch read throws', async () => { + // First select (timezone) succeeds; second (kill-switch) throws. + let calls = 0 + selectImpl = async () => { + calls++ + if (calls === 1) return [{ timezone: 'UTC' }] + throw new Error('settings unreadable') + } + const mod = await freshModule() + await mod.getAutomationEngine() + expect(setGlobalEnabledMock).not.toHaveBeenCalled() + }) +}) + +describe('automation/instance — updateAutomationTimezone', () => { + it('the clock closure follows a timezone change without engine rebuild', async () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}) + selectImpl = async () => [{ timezone: 'UTC', on: true }] + const mod = await freshModule() + await mod.getAutomationEngine() + + const { clockInTimezone } = await import('../signals') + const clockSpy = vi.mocked(clockInTimezone) + clockSpy.mockClear() + + capturedDeps.clock() + expect(clockSpy).toHaveBeenLastCalledWith('UTC', expect.any(Date)) + + // Settings change: the engine's clock must evaluate in the new tz on the + // very next tick — previously the closure kept the boot-time timezone. + mod.updateAutomationTimezone('Australia/Sydney') + capturedDeps.clock() + expect(clockSpy).toHaveBeenLastCalledWith('Australia/Sydney', expect.any(Date)) + log.mockRestore() + }) +}) + +describe('automation/instance — shutdown', () => { + it('stops the engine and clears state', async () => { + selectImpl = async () => [{ timezone: 'UTC', on: true }] + const mod = await freshModule() + await mod.getAutomationEngine() + + await mod.shutdownAutomationEngine() + expect(stopMock).toHaveBeenCalledTimes(1) + expect(mod.getAutomationEngineIfRunning()).toBeNull() + }) + + it('is a no-op when no engine is running', async () => { + const mod = await freshModule() + await mod.shutdownAutomationEngine() + expect(stopMock).not.toHaveBeenCalled() + }) +}) + +describe('automation/instance — injected dependency closures', () => { + it('wires loadRules / recordRun / disableRule / runOnce / hardware deps', async () => { + selectImpl = async () => [{ timezone: 'UTC', on: true }] + const mod = await freshModule() + await mod.getAutomationEngine() + expect(capturedDeps).toBeTruthy() + + // loadRules maps DB rows to the engine's AutomationRule shape. + selectImpl = async () => [{ + id: 9, + name: 'r', + enabled: true, + side: 'left', + priority: 0, + dryRun: false, + cooldownMin: 30, + trigger: { kind: 'tick', everyMin: 1 }, + conditions: { kind: 'and', conditions: [] }, + actions: [{ kind: 'notify', message: 'x' }], + }] + const rules = await capturedDeps.loadRules() + expect(rules).toEqual([{ + id: 9, + name: 'r', + enabled: true, + side: 'left', + priority: 0, + dryRun: false, + cooldownMin: 30, + trigger: { kind: 'tick', everyMin: 1 }, + conditions: { kind: 'and', conditions: [] }, + actions: [{ kind: 'notify', message: 'x' }], + }]) + + // recordRun inserts; the failure path is swallowed with a warning. + let inserted: any = null + insertImpl = async (v) => { + inserted = v + } + await capturedDeps.recordRun(9, 'fired', { actions: [] }) + expect(inserted).toMatchObject({ automationId: 9, outcome: 'fired' }) + + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + insertImpl = async () => { + throw new Error('insert failed') + } + await expect(capturedDeps.recordRun(9, 'error', {})).resolves.toBeUndefined() + expect(warn).toHaveBeenCalled() + // A non-Error rejection is logged verbatim (the e-is-not-Error branch). + insertImpl = async () => { + throw 'string failure' + } + await expect(capturedDeps.recordRun(9, 'error', {})).resolves.toBeUndefined() + expect(warn).toHaveBeenCalledWith('[automation] failed to record run:', 'string failure') + warn.mockRestore() + + // disableRule resolves through the mocked update chain. + await expect(capturedDeps.disableRule(9)).resolves.toBeUndefined() + + // hasActiveRunOnceSession reflects whether a row was found. + selectImpl = async () => [{ id: 1 }] + expect(await capturedDeps.hasActiveRunOnceSession('left')).toBe(true) + selectImpl = async () => [] + expect(await capturedDeps.hasActiveRunOnceSession('right')).toBe(false) + + // Remaining wiring closures. + expect(typeof capturedDeps.now()).toBe('number') + expect(capturedDeps.clock()).toEqual({ nowMinutes: 123, dayOfWeek: 'monday' }) + expect(capturedDeps.getHardware()).toBe(hardwareClient) + expect(await capturedDeps.withSideLock('left', async () => 42)).toBe(42) + + capturedDeps.broadcast('left', { targetLevel: 0 }) + expect(broadcastMock).toHaveBeenCalledWith('left', { targetLevel: 0 }) + capturedDeps.markMutated('left') + expect(markSideMutatedMock).toHaveBeenCalledWith('left') + + const log = vi.spyOn(console, 'log').mockImplementation(() => {}) + capturedDeps.notify(9, 'hello') + capturedDeps.log('a message') + expect(log).toHaveBeenCalled() + log.mockRestore() + }) +}) diff --git a/src/automation/tests/signals.biometrics.test.ts b/src/automation/tests/signals.biometrics.test.ts new file mode 100644 index 00000000..3dc83841 --- /dev/null +++ b/src/automation/tests/signals.biometrics.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest' +import { reduceCap } from '../capReduce' + +describe('reduceCap', () => { + it('returns null for an empty array', () => { + expect(reduceCap([])).toBeNull() + }) + + it('reduces a scalar (Pod 3) channel to degenerate stats', () => { + expect(reduceCap([42])).toEqual({ max: 42, mean: 42, spread: 0, peakZone: null }) + }) + + it('drops the two reference channels from a full capSense2 frame', () => { + // [A1,A2,B1,B2,C1,C2,ref1,ref2] — refs (999) must not affect the stats. + expect(reduceCap([10, 20, 30, 40, 50, 60, 999, 999])).toMatchObject({ max: 60, spread: 50, mean: 35 }) + }) + + it('picks the paired zone (A/B/C) with the highest mean as peakZone', () => { + // zone A mean=15, B mean=85, C mean=35 → B is index 1. + expect(reduceCap([10, 20, 80, 90, 30, 40, 0, 0])).toMatchObject({ peakZone: 1 }) + // zone C dominant → index 2. + expect(reduceCap([10, 10, 20, 20, 95, 95, 0, 0])).toMatchObject({ peakZone: 2 }) + }) + + it('leaves peakZone null when the frame is not the 6-channel shape', () => { + expect(reduceCap([10, 20, 30])).toMatchObject({ peakZone: null }) + }) +}) diff --git a/src/automation/tests/signals.test.ts b/src/automation/tests/signals.test.ts new file mode 100644 index 00000000..776efff0 --- /dev/null +++ b/src/automation/tests/signals.test.ts @@ -0,0 +1,177 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { AutomationRule, Condition, Expr } from '../types' + +// The DAC monitor is mocked at the import boundary so DeviceSignalReader can be +// exercised without real hardware. `getLastStatusMock` is swapped per test. +let getLastStatusMock: () => unknown = () => null +let monitorRunning = true + +vi.mock('@/src/hardware/dacMonitor.instance', () => ({ + getDacMonitorIfRunning: () => (monitorRunning ? { getLastStatus: getLastStatusMock } : null), +})) + +import { + DeviceSignalReader, + clockInTimezone, + collectWindowSignals, +} from '../signals' + +beforeEach(() => { + getLastStatusMock = () => null + monitorRunning = true +}) + +describe('clockInTimezone', () => { + it('returns minutes-since-midnight and the weekday for a known instant', () => { + // 2021-11-14T15:30:00Z is a Sunday; UTC keeps wall time == instant. + const { nowMinutes, dayOfWeek } = clockInTimezone('UTC', new Date('2021-11-14T15:30:00Z')) + expect(nowMinutes).toBe(15 * 60 + 30) + expect(dayOfWeek).toBe('sunday') + }) + + it('shifts the wall clock into the requested timezone', () => { + // 08:00Z in Los Angeles (UTC-8 in winter) is 00:00 local, Monday. + const { nowMinutes, dayOfWeek } = clockInTimezone('America/Los_Angeles', new Date('2021-11-15T08:00:00Z')) + expect(nowMinutes).toBe(0) + expect(dayOfWeek).toBe('monday') + }) + + it('throws on an invalid timezone', () => { + expect(() => clockInTimezone('Not/AZone', new Date())).toThrow() + }) +}) + +describe('DeviceSignalReader', () => { + it('returns an empty snapshot when the monitor is not running', () => { + monitorRunning = false + expect(new DeviceSignalReader().read()).toEqual({}) + }) + + it('returns an empty snapshot when there is no last status frame', () => { + getLastStatusMock = () => null + expect(new DeviceSignalReader().read()).toEqual({}) + }) + + it('maps both sides plus a low water flag', () => { + getLastStatusMock = () => ({ + leftSide: { currentTemperature: 75, targetTemperature: 80, currentLevel: 10 }, + rightSide: { currentTemperature: 70, targetTemperature: 68, currentLevel: -5 }, + waterLevel: 'low', + }) + expect(new DeviceSignalReader().read()).toEqual({ + 'left.currentTemperature': 75, + 'left.targetTemperature': 80, + 'left.currentLevel': 10, + 'right.currentTemperature': 70, + 'right.targetTemperature': 68, + 'right.currentLevel': -5, + 'water.low': 1, + }) + }) + + it('omits temperature signals for an off side reporting null level-0 temps', () => { + getLastStatusMock = () => ({ + leftSide: { currentTemperature: null, targetTemperature: null, currentLevel: 0 }, + rightSide: { currentTemperature: 70, targetTemperature: 68, currentLevel: -5 }, + waterLevel: 'ok', + }) + const snap = new DeviceSignalReader().read() + // Off side: temp signals stay absent so rules don't fire on a phantom neutral. + expect(snap['left.currentTemperature']).toBeUndefined() + expect(snap['left.targetTemperature']).toBeUndefined() + expect(snap['left.currentLevel']).toBe(0) + // Powered side still maps its temps through. + expect(snap['right.currentTemperature']).toBe(70) + expect(snap['right.targetTemperature']).toBe(68) + }) + + it('encodes an ok water level as 0 and skips an absent side', () => { + getLastStatusMock = () => ({ + leftSide: { currentTemperature: 72, targetTemperature: 72, currentLevel: 0 }, + rightSide: undefined, + waterLevel: 'ok', + }) + expect(new DeviceSignalReader().read()).toEqual({ + 'left.currentTemperature': 72, + 'left.targetTemperature': 72, + 'left.currentLevel': 0, + 'water.low': 0, + }) + }) + + it('omits the water flag for an unknown water level', () => { + getLastStatusMock = () => ({ + leftSide: { currentTemperature: 72, targetTemperature: 72, currentLevel: 0 }, + rightSide: { currentTemperature: 72, targetTemperature: 72, currentLevel: 0 }, + waterLevel: 'unknown', + }) + expect(new DeviceSignalReader().read()['water.low']).toBeUndefined() + }) + + it('warns and degrades to an empty snapshot when the read throws', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + getLastStatusMock = () => { + throw new Error('frame corrupt') + } + expect(new DeviceSignalReader().read()).toEqual({}) + expect(warn).toHaveBeenCalled() + warn.mockRestore() + }) +}) + +describe('collectWindowSignals', () => { + const win = (signal: string): Expr => ({ kind: 'window', fn: 'avg', signal, lastMin: 10 }) + const lit = (value: number): Expr => ({ kind: 'literal', value }) + + function rule(overrides: Partial): AutomationRule { + return { + id: 1, + name: 'r', + enabled: true, + side: 'left', + priority: 0, + dryRun: false, + cooldownMin: null, + trigger: { kind: 'tick', everyMin: 1 }, + conditions: { kind: 'and', conditions: [] }, + actions: [], + ...overrides, + } + } + + it('collects window keys from nested and/or/not/compare/between conditions', () => { + const cond: Condition = { + kind: 'and', + conditions: [ + { kind: 'or', conditions: [ + { kind: 'compare', op: '>', left: win('left.movement'), right: lit(200) }, + ] }, + { kind: 'not', condition: { kind: 'compare', op: '<', left: win('left.heartRate'), right: lit(50) } }, + { kind: 'between', subject: win('left.hrv'), min: lit(0), max: win('right.hrv') }, + ], + } + expect(collectWindowSignals([rule({ conditions: cond })])).toEqual( + new Set(['left.movement', 'left.heartRate', 'left.hrv', 'right.hrv']), + ) + }) + + it('collects window keys from setTemperature and setPower action temps', () => { + const rules = [rule({ + actions: [ + { kind: 'setTemperature', temp: { kind: 'clamp', value: win('ambient.temperature'), min: lit(60), max: lit(75) } }, + { kind: 'setPower', on: true, temp: win('left.breathingRate') }, + ], + })] + expect(collectWindowSignals(rules)).toEqual(new Set(['ambient.temperature', 'left.breathingRate'])) + }) + + it('ignores disabled rules', () => { + const cond: Condition = { kind: 'compare', op: '>', left: win('left.movement'), right: lit(1) } + expect(collectWindowSignals([rule({ enabled: false, conditions: cond })])).toEqual(new Set()) + }) + + it('returns an empty set when no windows are referenced', () => { + const cond: Condition = { kind: 'compare', op: '>', left: { kind: 'signal', signal: 'left.movement' }, right: lit(1) } + expect(collectWindowSignals([rule({ conditions: cond })])).toEqual(new Set()) + }) +}) diff --git a/src/automation/tests/windows.test.ts b/src/automation/tests/windows.test.ts new file mode 100644 index 00000000..1b2a3888 --- /dev/null +++ b/src/automation/tests/windows.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest' +import { WindowStore } from '../windows' + +const MIN = 60_000 + +describe('WindowStore', () => { + it('aggregates avg/min/max/sum/count over a trailing window', () => { + const w = new WindowStore() + const now = 100 * MIN + w.record('m', 100, now - 9 * MIN) + w.record('m', 200, now - 5 * MIN) + w.record('m', 300, now - 1 * MIN) + + expect(w.aggregate('avg', 'm', 10, now)).toBe(200) + expect(w.aggregate('min', 'm', 10, now)).toBe(100) + expect(w.aggregate('max', 'm', 10, now)).toBe(300) + expect(w.aggregate('sum', 'm', 10, now)).toBe(600) + expect(w.aggregate('count', 'm', 10, now)).toBe(3) + }) + + it('excludes samples older than the window', () => { + const w = new WindowStore() + const now = 100 * MIN + w.record('m', 100, now - 20 * MIN) // out of window + w.record('m', 400, now - 2 * MIN) + expect(w.aggregate('avg', 'm', 10, now)).toBe(400) + expect(w.aggregate('count', 'm', 10, now)).toBe(1) + }) + + it('returns undefined for unknown keys and empty windows (except count)', () => { + const w = new WindowStore() + const now = 100 * MIN + expect(w.aggregate('avg', 'missing', 10, now)).toBeUndefined() + w.record('m', 50, now - 30 * MIN) + expect(w.aggregate('avg', 'm', 10, now)).toBeUndefined() // exists but empty in-window + expect(w.aggregate('count', 'm', 10, now)).toBe(0) + }) + + it('prunes samples older than maxAgeMin', () => { + const w = new WindowStore() + const now = 100 * MIN + w.record('m', 1, now - 90 * MIN) + w.record('m', 2, now - 2 * MIN) + w.prune(now, 60) + expect(w.aggregate('count', 'm', 120, now)).toBe(1) + }) + + it('drops a buffer entirely when every sample ages out', () => { + const w = new WindowStore() + const now = 100 * MIN + w.record('m', 1, now - 90 * MIN) + w.record('m', 2, now - 80 * MIN) + w.prune(now, 60) + // Both samples pruned → key removed → unknown key, not an empty in-window. + expect(w.aggregate('count', 'm', 120, now)).toBeUndefined() + }) +}) diff --git a/src/automation/types.ts b/src/automation/types.ts new file mode 100644 index 00000000..87177a15 --- /dev/null +++ b/src/automation/types.ts @@ -0,0 +1,111 @@ +/** + * Autopilot rules-engine model — the WHEN / IF / THEN "language". + * + * These are hand-authored AST types and serve as the single source of truth: + * the zod schemas in `src/server/validation-schemas.ts` validate JSON against + * these shapes at the boundary, and the engine evaluates them directly. + */ + +export type Side = 'left' | 'right' + +export type DayOfWeek + = | 'sunday' + | 'monday' + | 'tuesday' + | 'wednesday' + | 'thursday' + | 'friday' + | 'saturday' + +export type CompareOp = '>' | '>=' | '<' | '<=' | '==' | '!=' +export type BinaryOp = '+' | '-' | '*' | '/' +export type WindowFn = 'avg' | 'min' | 'max' | 'sum' | 'count' + +/** + * Expression AST — used for both condition operands and action params. + * Every node evaluates to a number, or `undefined` when a referenced signal is + * unavailable (which propagates so a rule skips rather than firing on missing + * data). + */ +export type Expr + = | { kind: 'literal', value: number } + | { kind: 'signal', signal: string } + | { kind: 'window', fn: WindowFn, signal: string, lastMin: number } + | { kind: 'binary', op: BinaryOp, left: Expr, right: Expr } + | { kind: 'clamp', value: Expr, min: Expr, max: Expr } + +/** + * Condition AST — an AND/OR/NOT tree over comparisons. Evaluates to a boolean, + * or `undefined` ("unknown") when an underlying signal is unavailable. + */ +export type Condition + = | { kind: 'and', conditions: Condition[] } + | { kind: 'or', conditions: Condition[] } + | { kind: 'not', condition: Condition } + | { kind: 'compare', op: CompareOp, left: Expr, right: Expr } + | { kind: 'between', subject: Expr, min: Expr, max: Expr } + | { kind: 'timeBetween', start: string, end: string } // HH:MM, wraps past midnight + | { kind: 'onDays', days: DayOfWeek[] } + +/** + * Trigger (WHEN) — what wakes the rule up for an evaluation. + */ +export type Trigger + = | { kind: 'tick', everyMin: number } + | { kind: 'signalChange', signal: string } + | { kind: 'timeOfDay', at: string, days?: DayOfWeek[] } // HH:MM + +/** + * Action (THEN). `notify` is the no-hardware safe default. `setTemperature` + * carries an optional per-action clamp band (layer 1 of the two-layer clamp); + * `side` defaults to the rule's side. + */ +export type Action + = | { kind: 'notify', message: string } + | { + kind: 'setTemperature' + side?: Side + temp: Expr + clamp?: { min: number, max: number } + durationSec?: number + } + | { kind: 'setPower', side?: Side, on: boolean, temp?: Expr } + +/** + * A fully-resolved automation as the engine consumes it (JSON columns parsed). + */ +export interface AutomationRule { + id: number + name: string + enabled: boolean + side: Side | null + priority: number + dryRun: boolean + cooldownMin: number | null + trigger: Trigger + conditions: Condition + actions: Action[] +} + +export type RunOutcome = 'fired' | 'skipped' | 'clamped' | 'dry_run' | 'error' + +// --------------------------------------------------------------------------- +// Engine tunables (see docs/adr/0023-autopilot-reactive-automations.md "Resolved tunables") +// --------------------------------------------------------------------------- + +/** Global evaluator tick — 60s aligns to ambient/movement cadence. */ +export const AUTOMATION_TICK_MS = 60_000 + +/** Manual-override hold: touching the dial suspends autopilot on that side. */ +export const AUTOMATION_MANUAL_OVERRIDE_MS = 30 * 60_000 + +/** Layer-1 clamp defaults when an action omits its own `clamp` band. */ +export const AUTOMATION_DEFAULT_USER_MIN = 60 +export const AUTOMATION_DEFAULT_USER_MAX = 100 + +/** Anti-thrash: only re-assert a setpoint when it moves at least this much. */ +export const AUTOMATION_ANTI_THRASH_F = 0.5 + +/** Runaway guard: a rule firing more than this many times in a rolling hour + * is auto-disabled (enabled=false) and surfaced via an `error` run row. */ +export const AUTOMATION_MAX_ACTIONS_PER_HOUR = 12 diff --git a/src/automation/windows.ts b/src/automation/windows.ts new file mode 100644 index 00000000..81d57f60 --- /dev/null +++ b/src/automation/windows.ts @@ -0,0 +1,67 @@ +/** + * Windowed-aggregate buffers — backs `avg|min|max|sum|count(signal, last N min)`. + * + * The engine samples every windowed signal once per tick and records the value + * here; aggregate queries then read back over a trailing time window. Buffers + * are pruned to the largest window any rule asks for so memory stays bounded. + */ + +import type { WindowFn } from './types' + +interface Sample { + t: number // epoch ms + v: number +} + +export class WindowStore { + private buffers = new Map() + + /** Record a numeric sample for a signal key at time `atMs`. */ + record(key: string, value: number, atMs: number): void { + let buf = this.buffers.get(key) + if (!buf) { + buf = [] + this.buffers.set(key, buf) + } + buf.push({ t: atMs, v: value }) + } + + /** + * Aggregate samples for `key` over the trailing `lastMin` minutes ending at + * `nowMs`. Returns `undefined` when no samples fall in the window (so the + * expression propagates "unavailable" and the rule skips). `count` returns 0 + * only if the buffer exists but is empty in-window — callers treat that as a + * valid zero. + */ + aggregate(fn: WindowFn, key: string, lastMin: number, nowMs: number): number | undefined { + const buf = this.buffers.get(key) + if (!buf) return undefined + const cutoff = nowMs - lastMin * 60_000 + const values: number[] = [] + for (const s of buf) { + if (s.t >= cutoff && s.t <= nowMs) values.push(s.v) + } + if (fn === 'count') return values.length + if (values.length === 0) return undefined + switch (fn) { + case 'avg': + return values.reduce((a, b) => a + b, 0) / values.length + case 'sum': + return values.reduce((a, b) => a + b, 0) + case 'min': + return Math.min(...values) + case 'max': + return Math.max(...values) + } + } + + /** Drop samples older than `maxAgeMin` minutes before `nowMs`. */ + prune(nowMs: number, maxAgeMin: number): void { + const cutoff = nowMs - maxAgeMin * 60_000 + for (const [key, buf] of this.buffers) { + const kept = buf.filter(s => s.t >= cutoff) + if (kept.length === 0) this.buffers.delete(key) + else this.buffers.set(key, kept) + } + } +} diff --git a/src/components/Autopilot/AutomationsList.tsx b/src/components/Autopilot/AutomationsList.tsx new file mode 100644 index 00000000..082631cc --- /dev/null +++ b/src/components/Autopilot/AutomationsList.tsx @@ -0,0 +1,172 @@ +/** + * Automations list — the dense table-of-rules. Each row renders its rule as a + * plain-English sentence with the dynamic parts in the accent colour, plus an + * enabled toggle, status/side badges, last-fired and fires-today. + */ +'use client' + +import { Icon } from './icons' +import { Button, SideBadge, StatusBadge, Toggle } from './primitives' +import { type BuilderRule, buildSentence } from './builderModel' + +export interface ListItem { + id: number + name: string + enabled: boolean + mode: 'active' | 'dryrun' + side: 'left' | 'right' | 'both' + builder: BuilderRule + lastFired: string + firesToday: number +} + +function RuleSentence({ b }: { b: BuilderRule }) { + const chunks = buildSentence(b) + return ( + + {chunks.map((c, i) => ( + {c.text} + ))} + + ) +} + +function Row({ a, onToggle, onOpen }: { a: ListItem, onToggle: (id: number, enabled: boolean) => void, onOpen: (a: ListItem) => void }) { + return ( +
onOpen(a)} + onKeyDown={(e) => { + if (e.target !== e.currentTarget) return + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onOpen(a) + } + }} + className="group grid cursor-pointer grid-cols-[auto_1fr_auto] items-center gap-4 border-b border-zinc-800/60 px-5 py-4 transition-colors hover:bg-zinc-900/40 focus:outline-none focus:ring-2 focus:ring-zinc-500/60" + > +
e.stopPropagation()} className="pt-0.5"> + onToggle(a.id, !a.enabled)} /> +
+
+
+ {a.name} + + +
+ +
+
+
+
Last fired
+
{a.lastFired}
+
+
+
Today
+
+ {a.firesToday} + {' '} + fire + {a.firesToday === 1 ? '' : 's'} +
+
+ +
+
+ ) +} + +function EmptyState({ onNew }: { onNew: () => void }) { + return ( +
+
+
+ +
+

No automations yet

+

+ Autopilot reacts to live signals — movement, heart rate, ambient temperature — instead of just the clock. + Build a rule as + {' '} + When + {' '} + · + {' '} + If + {' '} + · + {' '} + Then + , then backtest it against past nights before it ever touches your bed. +

+
+ +
+
+ {[{ t: 'Hold ambient + 3°F overnight', s: 'continuous policy' }, { t: 'Cool down when restless', s: 'edge-triggered rule' }].map((x, i) => ( +
+
{x.t}
+
{x.s}
+
+ ))} +
+
+
+ ) +} + +export function AutomationsList({ items, loading, onToggle, onOpen, onNew }: { + items: ListItem[] + loading: boolean + onToggle: (id: number, enabled: boolean) => void + onOpen: (a: ListItem) => void + onNew: () => void +}) { + const activeCount = items.filter(a => a.enabled && a.mode === 'active').length + const dryCount = items.filter(a => a.enabled && a.mode === 'dryrun').length + const empty = !loading && items.length === 0 + return ( +
+
+
+

Automations

+

+ {empty + ? 'Reactive rules that respond to live signals' + : ( + <> + {activeCount} + {' '} + active · + {' '} + {dryCount} + {' '} + in dry-run · + {' '} + {items.length} + {' '} + total + + )} +

+
+ {!empty && ( + + )} +
+ + {loading + ?
Loading automations…
+ : empty + ? + :
{items.map(a => )}
} +
+ ) +} diff --git a/src/components/Autopilot/AutopilotConsole.tsx b/src/components/Autopilot/AutopilotConsole.tsx new file mode 100644 index 00000000..ed248bc8 --- /dev/null +++ b/src/components/Autopilot/AutopilotConsole.tsx @@ -0,0 +1,174 @@ +/** + * Autopilot console — the full-bleed desktop surface that hosts the Automations + * list, the Rule editor (modal), and the Diagnostics/status panel behind a + * left side-nav. Breaks out of the app's mobile `max-w-md` shell the same way + * the diagnostics console does. Owns all tRPC data + mutations. + */ +'use client' + +import { useMemo, useState } from 'react' +import { trpc } from '@/src/utils/trpc' +import { Icon, type IconName } from './icons' +import { AutomationsList, type ListItem } from './AutomationsList' +import { RuleEditor } from './RuleEditor' +import { StatusPanel } from './StatusPanel' +import { type BuilderRule, blankRule, fromAST, toAST } from './builderModel' + +const ACCENT = '#0c87c2' + +// Scoped styles ported from the design HTML: mono face, slim scrollbars, the +// modal fade, and the accent default — confined to `.ap-console`. +const SCOPED_CSS = ` +.ap-console { --accent: ${ACCENT}; } +.ap-console .mono { font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace; font-feature-settings: 'tnum'; } +.ap-console .tabular-nums { font-variant-numeric: tabular-nums; } +.ap-console *::-webkit-scrollbar { width: 10px; height: 10px; } +.ap-console *::-webkit-scrollbar-track { background: transparent; } +.ap-console *::-webkit-scrollbar-thumb { background: #27272a; border-radius: 999px; border: 2px solid transparent; background-clip: content-box; } +.ap-console input::placeholder { color: #52525b; } +@keyframes apFade { from { opacity: 0; transform: scale(0.99); } to { opacity: 1; transform: none; } } +` + +function NavItem({ icon, label, active, badge, onClick }: { icon: IconName, label: string, active: boolean, badge?: number, onClick: () => void }) { + const I = Icon[icon] + return ( + + ) +} + +export function AutopilotConsole() { + const utils = trpc.useUtils() + const [screen, setScreen] = useState<'list' | 'status'>('list') + const [editing, setEditing] = useState(null) + + const listQ = trpc.automations.list.useQuery({}) + const statusQ = trpc.automations.status.useQuery({}, { refetchInterval: 15000 }) + const runsQ = trpc.automations.runs.useQuery({ limit: 100 }, { refetchInterval: 15000 }) + + const invalidate = () => { + void utils.automations.list.invalidate() + void utils.automations.status.invalidate() + void utils.automations.runs.invalidate() + } + + const createM = trpc.automations.create.useMutation({ onSuccess: invalidate }) + const updateM = trpc.automations.update.useMutation({ onSuccess: invalidate }) + const setEnabledM = trpc.automations.setEnabled.useMutation({ onSuccess: invalidate }) + const setDryRunM = trpc.automations.setDryRun.useMutation({ onSuccess: invalidate }) + const killM = trpc.automations.setKillSwitch.useMutation({ onSuccess: () => { + void utils.automations.status.invalidate() + void utils.automations.getKillSwitch.invalidate() + } }) + + // Build list items from the rule rows + the status map (last-fired/today). + const items: ListItem[] = useMemo(() => { + const rows = listQ.data ?? [] + const statusById = new Map((statusQ.data?.rules ?? []).map(s => [s.id, s])) + return rows.map((row) => { + const builder = fromAST(row) + const s = statusById.get(row.id) + const lastFired = s?.lastFiredAt ? agoShort(s.lastFiredAt) : 'never' + return { + id: row.id, + name: row.name, + enabled: row.enabled, + mode: row.dryRun ? 'dryrun' : 'active', + side: (row.side ?? 'both') as ListItem['side'], + builder, + lastFired, + firesToday: s?.firesToday ?? 0, + } + }) + }, [listQ.data, statusQ.data]) + + const saving = createM.isPending || updateM.isPending + + const save = (rule: BuilderRule) => { + const ast = toAST(rule) + if (rule.id != null) updateM.mutate({ id: rule.id, ...ast }, { onSuccess: () => setEditing(null) }) + else createM.mutate(ast, { onSuccess: () => setEditing(null) }) + } + + const killed = statusQ.data ? !statusQ.data.globalEnabled : false + const activeCount = items.filter(i => i.enabled && i.mode === 'active').length + + return ( +
+