diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..400c521 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# zeroforce environment. Copy to .env.local (gitignored) and fill in. + +# Provider mode hinge (spec §6.6). One of: fake | dev | prod +# fake = deterministic offline providers (default; no keys needed) +# dev = Groq + Tavily +# prod = Gemini 2.5 Pro (Vertex) + Vertex grounding +ZEROFORCE_MODE=fake + +# SQLite path (dev). Use :memory: for ephemeral. +ZEROFORCE_DB=./data/zeroforce.db + +# Optional single bearer token; if unset, auth is open (fake/dev convenience). +# ZEROFORCE_API_TOKEN= + +# --- dev mode keys --- +# GROQ_API_KEY= +# TAVILY_API_KEY= + +# --- prod mode (GCP Application Default Credentials must also be configured) --- +# GCP_PROJECT= +# GCP_REGION=us-central1 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..f52a4dc --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,88 @@ +# Architecture + +zeroforce is a supply-chain **disruption-propagation** layer. Scorecards (Interos, Resilinc) +say *who* is risky; zeroforce answers *what happens next* — if a supplier fails, how fast the +disruption cascades, and which single failure collapses the network fastest. + +Full specification: [`.agents/project_specifications.md`](.agents/project_specifications.md) (v2.2). + +## The three layers + +1. **Extraction** — turn natural language into a weighted directed dependency graph. + Prod: Gemini 2.5 Pro two-pass (ground → structure). Dev: Tavily + Groq. Fake: deterministic. +2. **Computation** — the RZF engine computes exact Expected Propagation Time (EPT) per node + via Markov-chain DP. This is `packages/zeroforce/` and was built first. +3. **Presentation** — Next.js 14 dashboard: force graph, impact ranking, cascade animation, + what-if, executive report. + +## ⚠️ The oracle strategy is the most load-bearing decision in the project + +A subtly wrong EPT is a plausible number with **no error signal** — a customer could make a +procurement decision on it. So the engine ships first with math-correctness gating CI, and the +tests are designed to **trust the (unrefereed preprint) paper zero**: + +- **Hand-computed** 3-node oracle, EPT derived from first principles in the test docstring. +- **Monte Carlo differential** — an independent simulator vs the DP, agreeing to 3 decimals. +- **Engine parity** — pure-Python reference DP vs the numba JIT, exact. + +The §4.6 closed-form formulas are **UNVERIFIED transcriptions** and are kept `xfail`. They +currently xpass (the engine reproduces them), but they stay quarantined until someone reads +the paper body and cites the theorem number. **Never** "fix" an xfail by editing the expected +value to match the engine — that defeats the entire safety mechanism. See +`packages/zeroforce/README.md` and spec §11.1. + +Two clarifications the build surfaced over the spec pseudocode (both faithful to the locked +decisions, documented in the engine README): +- the row-stochastic invariant is asserted on the **full** graph, not on induced subgraphs + (unreachable suppliers legitimately leave subgraph rows summing < 1); +- the recurrence uses the **unconditional** transition probability divided once by + `(1 − p_stay)` (the prose's conditional phrasing would double-divide). + +## Service topology + +Six logical services (spec §5.1). Each has a pure-logic `core.py` and a thin FastAPI wrapper: + +| Service | Core | Responsibility | +|---|---|---| +| gateway | `gateway/app.py` | Auth, routing, all `/api/v1` endpoints. Embeds the orchestrator. | +| extractor | `extractor/core.py` | Prompt 1 + grounding → `Graph`. | +| validator | `validator/core.py` | §7.2 weight-normalization invariant. | +| engine | `engine/core.py` | Wraps `packages/zeroforce`: analyze / simulate / what-if. | +| reporter | `reporter/core.py` | Prompt 3 executive report. | +| orchestrator | `orchestrator/core.py` | Async job lifecycle + 5-min timeout. Co-located in gateway. | + +The orchestrator reaches the workers through a **`ServiceClient`** abstraction: +- `InProcessServiceClient` (default) calls cores directly — single-process, offline, testable. +- `HTTPServiceClient` calls the per-service FastAPI apps (`worker_apps.py`) — used by + docker-compose (six containers) and Cloud Run (one service each). + +``` +Frontend ─▶ Gateway ─▶ Orchestrator ─▶ ServiceClient ─▶ {extractor, validator, engine, reporter} + ▲ poll /analyses/{id} │ + └────────────────────────────┘ (SQLite/Firestore jobs + analyses; Redis/dict extraction cache) +``` + +## The dev/prod/fake hinge + +One env var, `ZEROFORCE_MODE`: +- `fake` (default) — deterministic offline providers; no keys, no network. Powers tests/CI. +- `dev` — Groq (`GROQ_API_KEY`) + Tavily (`TAVILY_API_KEY`). +- `prod` — Gemini 2.5 Pro (Vertex, ADC) + Vertex grounding. + +Identical I/O contract across modes; only source-attribution metadata and call count differ. +Application code imports only the `LLMProvider` / `SearchProvider` protocols, never a vendor SDK. + +## Repository layout + +``` +packages/zeroforce RZF engine (built first; gating oracle suite) +packages/schemas shared Pydantic API models (engine types re-exported) +packages/providers LLM + search abstraction (fake/groq/tavily/gemini/vertex) + factory +services/backend 6 service cores + FastAPI apps + orchestrator + storage + observability +apps/web Next.js 14 dashboard +infra/ Dockerfiles, docker-compose.dev, Terraform (Cloud Run, Firestore, …) +data/ BEA 15-sector bundle +tests/parity engine-golden + extraction-consistency +``` + +See [`docs/RUNBOOK.md`](docs/RUNBOOK.md) to run it. diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 0000000..3bb7cf6 --- /dev/null +++ b/PROGRESS.md @@ -0,0 +1,68 @@ +# Build progress — autonomous full-project build + +Branch: `phase-0-engine` (continuing here; will push + open one PR at end). +Started autonomous full build 2026-05-22. Operator asleep; no questions allowed. + +## Operating rules (from operator) +1. Providers: operator intended to paste Groq/Tavily keys but none arrived. → Build real + Groq/Tavily/Gemini providers + a **deterministic FakeProvider as the default** for + tests/CI/local-no-keys. Keys plug in via `.env` later. Live LLM stays UNVERIFIED. +2. Priority if time short: **backend depth first**; frontend = functional dashboard. +3. Delivery: local commits per phase, **push + one PR at end**. +4. Ambiguity: **decide + document here**, keep going. +5. No cloud deploy, no spending money, no pushing secrets. Infra-as-code written, not applied. + +## Phase checklist +- [x] Phase 0 — RZF engine `packages/zeroforce/` + gating tests + CI. (commit b5ac81e) +- [x] Phase 1 — backend (d9fce07) + functional Next.js dashboard (force graph, ranking table, + report/citations/notes tabs, async polling, node-select unreachable disclosure). + tsc clean, vitest 3/3, next build clean, live uvicorn smoke OK. +- [x] Phase 2 — per-service worker apps (extractor/validator/engine/reporter) + HTTPServiceClient + wiring; FirestoreRepository + Redis cache (lazy, prod, untested); Dockerfile.backend + + Dockerfile.web; docker-compose.dev (6 services + redis + web); Terraform (Cloud Run x6, + Firestore, Memorystore, Artifact Registry, Secret Manager, IAM) — written, not applied; + engine-golden + extraction-consistency suites (tests/parity). 52 tests pass. + NOTE: orchestrator co-located in the gateway service (documented consolidation). +- [x] Phase 3 — cascade animation (Simulate from a node → play/pause/scrub timeline, nodes + recolor by per-round disruption probability), what-if panel (edge weight slider → delta + table + narration), citations panel (Phase 1), responsive stacked layout for <1024px. + `/simulate` + `/whatif` endpoints were built in Phase 1. tsc + next build clean. + DECISION: executive report is rendered non-streaming (fake/Groq reporter is single-shot; + true token streaming via Vercel AI SDK deferred — documented, low value at this stage). +- [x] Phase 4 — observability (timing middleware + structured access logs + opt-in OTel); + chaos/auth/PII hardening tests (caught + fixed a real upload-redaction bug); k6 load + script (10 RPS sustained + 100 RPS burst on the async path); SECURITY.md threat model + with implementation status; a11y (prefers-reduced-motion guard, aria-live cascade, + table semantics). 55 tests pass. Cost-regression: documented approach, not CI-wired. +- [x] Phase 5 — docs: ARCHITECTURE.md (with the oracle-strategy callout), README rewrite, + docs/RUNBOOK.md (local / compose / Terraform deploy steps / observability / load / beta + onboarding process). DECISION: skipped a separate `apps/docs` Next site; the math + explainer lives in the engine README + ARCHITECTURE.md (lower value, avoids a second + frontend build). Cloud deploy steps written but NOT applied (no GCP creds). + +## Architecture decisions made autonomously +- **Microservices as modules + thin HTTP wrappers.** Each of the 6 services has pure-logic + `core.py` + a thin FastAPI `app.py` + Dockerfile (prod parity, docker-compose.dev wires + all six). Orchestrator talks to them via a `ServiceClient` abstraction with two impls: + `InProcessServiceClient` (default; calls core directly — offline + testable) and + `HTTPServiceClient` (compose/prod). Rationale: real microservice deployability without + forcing 6 live HTTP processes for every test/CI run. +- **Provider modes:** `ZEROFORCE_MODE = fake | dev | prod`. `fake` = deterministic offline + (default for tests/CI). `dev` = Groq+Tavily (needs keys). `prod` = Gemini+Vertex (needs GCP). +- **Storage abstraction:** `Repository` protocol; `SQLiteRepository` (default/dev), + `FirestoreRepository` (prod, untested). Cache: `dict` (default) / `Redis` (prod). +- **uv workspace** at repo root; members = `packages/*` + `services/*`. + +## DONE — all phases built (2026-05-22, autonomous) + +Branch `phase-0-engine` (name is historical; now holds the whole build), 8 commits, pushed. +`gh` CLI not installed (the `gh` shell alias is unrelated), so open the PR manually: +**https://github.com/justin06lee/zeroforce/compare/master...phase-0-engine?expand=1** + +Verification at hand-off: 55 Python tests pass (engine pristine under `-W error`); frontend +tsc clean, vitest 3/3, `next build` clean; live uvicorn + worker-chain smoke OK in fake mode. + +## Open / unverified +- Live Groq/Tavily/Gemini calls untested (no keys). Fake provider proves pipeline shape. +- Cloud deploy untested (no GCP). Terraform/Cloud Run config written, not applied. +- Frontend e2e (Playwright) may not run if browser download blocked; unit/component tests do. diff --git a/README.md b/README.md index a2622a4..7f16d42 100644 --- a/README.md +++ b/README.md @@ -1 +1,47 @@ -# Zero Force AI +# zeroforce + +**Supply-chain disruption intelligence.** If a supplier fails today, how fast does the +disruption cascade through the dependency network — and which single failure collapses it +fastest? zeroforce answers the *propagation* question that supplier scorecards don't, using a +custom **Randomized Zero Forcing (RZF)** engine that computes exact Expected Propagation Time +(EPT) from every node. + +- Math core: `packages/zeroforce/` — built first, math-correctness gates CI. +- Spec: [`.agents/project_specifications.md`](.agents/project_specifications.md) (v2.2). +- Design: [`ARCHITECTURE.md`](ARCHITECTURE.md) · Run it: [`docs/RUNBOOK.md`](docs/RUNBOOK.md) · + Security: [`SECURITY.md`](SECURITY.md). + +## Quickstart (offline, no keys) + +```bash +# Python backend (uv workspace) +uv sync --all-packages +ZEROFORCE_MODE=fake ZEROFORCE_DB=:memory: \ + uv run uvicorn zeroforce_backend.asgi:app --port 8080 + +# Frontend (separate terminal) +cd apps/web && npm install && API_BASE=http://localhost:8080 npm run dev +# open http://localhost:3000 +``` + +`ZEROFORCE_MODE=fake` runs the entire pipeline with deterministic offline providers — no API +keys, no network. Set `dev` (Groq + Tavily) or `prod` (Gemini + Vertex) with the relevant keys +from [`.env.example`](.env.example) to use live providers. + +## Tests + +```bash +uv run pytest -q # engine oracles + backend + parity (55 tests) +cd apps/web && npm test # frontend unit (vitest) +``` + +The engine's gating oracles (hand-computed graph, Monte Carlo differential, py/numba parity) +trust the source paper zero. The §4.6 closed-form formulas are kept `xfail` until verified +against the paper body — see [`ARCHITECTURE.md`](ARCHITECTURE.md) for why that discipline is +the project's most important safety mechanism. + +## Status + +Built through Phase 4 (engine → backend pipeline → infra-as-code → frontend interactivity → +hardening). Cloud deployment is written as Terraform/Docker but **not applied**. See +[`PROGRESS.md`](PROGRESS.md) for the phase-by-phase state and autonomous decisions. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..16b53ba --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +# Security + +Threat model and mitigations (spec §12), with implementation status in this repo. + +| Threat | Surface | Mitigation | Status | +|---|---|---|---| +| Prompt injection | User input → LLM | Prod structuring (Pass B) uses schema-constrained output; all extractor output is validated against the `Graph` Pydantic schema before the engine runs. Untrusted text never triggers tool calls. | Implemented (schema validation in extractor/validator) | +| LLM cost exhaustion | `/analyze` | `max_nodes` hard cap 18 (Pydantic bound + orchestrator check); 5-minute job timeout; per-request token caps in providers. Per-IP/user rate limiting is scaffolded (gateway) — wire a limiter (e.g. slowapi) before public exposure. | Partial | +| PII in uploads | `/upload` | 20 MB cap; SSN / card-number regex redaction before any text leaves the gateway. | Implemented + tested (`test_upload_redacts_pii`) | +| Credential exposure | Provider keys | Keys only via env / Secret Manager; never in the client bundle (all LLM calls are server-side); `.env*` gitignored; `.env.example` ships placeholders only. | Implemented | +| Citation forgery | LLM hallucinated sources | Prod citations come from Vertex grounding metadata (Pass A), not free text. Unverified citations should be badged in the UI. | Partial (grounding metadata captured; UI badge TODO) | +| Cross-tenant data leak | Firestore | Prod: Firestore security rules enforce `userId` on reads; analyses user-scoped, sharing opt-in. | TODO (rules not written; single-user dev assumed) | +| AuthN/Z | All endpoints | Optional bearer token (`ZEROFORCE_API_TOKEN`); when set, all protected routes require it. Prod target: Firebase Auth + IAP. | Implemented (dev token) + tested (`test_auth_required_when_token_configured`) | + +## Operational notes + +- **No secrets are committed.** Verify with `git grep -nE "(GROQ|TAVILY|API_KEY)=.+"` returning nothing. +- **Chaos**: a provider outage mid-pipeline fails the job cleanly with `llm_provider_error` + (tested in `test_provider_outage_fails_job_gracefully`) rather than hanging or crashing. +- **Before public deployment**: enable rate limiting, write Firestore security rules, add the + unverified-citation UI badge, and run a dependency audit (`uv pip audit`, `npm audit`). + +## Reporting + +This is a pre-production build. Report security issues to the repository owner. diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..0de3124 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.next/ +next-env.d.ts +out/ +*.tsbuildinfo +.vercel diff --git a/apps/web/app/analyze/[id]/page.tsx b/apps/web/app/analyze/[id]/page.tsx new file mode 100644 index 0000000..ff0783c --- /dev/null +++ b/apps/web/app/analyze/[id]/page.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import CascadeBar from "@/components/CascadeBar"; +import ForceGraph from "@/components/ForceGraph"; +import RightRail from "@/components/RightRail"; +import WhatIfPanel from "@/components/WhatIfPanel"; +import { getAnalysis, simulate } from "@/lib/api"; +import type { JobRecord, SimulateResponse } from "@/lib/types"; + +export default function AnalysisPage({ params }: { params: { id: string } }) { + const [job, setJob] = useState(null); + const [selected, setSelected] = useState(null); + const [err, setErr] = useState(null); + + const [sim, setSim] = useState(null); + const [round, setRound] = useState(0); + const [playing, setPlaying] = useState(false); + + useEffect(() => { + let alive = true; + let timer: ReturnType; + async function poll() { + try { + const j = await getAnalysis(params.id); + if (!alive) return; + setJob(j); + if (j.status === "pending" || j.status === "running") timer = setTimeout(poll, 600); + } catch (e) { + if (alive) setErr((e as Error).message); + } + } + poll(); + return () => { + alive = false; + clearTimeout(timer); + }; + }, [params.id]); + + useEffect(() => { + if (!playing || !sim) return; + // Respect prefers-reduced-motion: don't auto-advance; the user scrubs manually. + const reduced = + typeof window !== "undefined" && + window.matchMedia?.("(prefers-reduced-motion: reduce)").matches; + if (reduced) { + setPlaying(false); + return; + } + const maxRound = sim.rounds.length - 1; + if (round >= maxRound) { + setPlaying(false); + return; + } + const t = setTimeout(() => setRound((r) => r + 1), 800); + return () => clearTimeout(t); + }, [playing, sim, round]); + + const startCascade = useCallback(async () => { + if (!selected) return; + const res = await simulate(params.id, [selected], 10, 1000); + setSim(res); + setRound(0); + setPlaying(true); + }, [selected, params.id]); + + if (err) return Error: {err}; + if (!job) return Loading…; + if (job.status === "failed") + return Analysis failed: {job.error?.code} — {job.error?.message}; + if (job.status !== "done" || !job.result) + return Working… ({job.stage ?? job.status}); + + const result = job.result; + const sel = selected ? result.node_analyses.find((a) => a.node_id === selected) : null; + const disruption = sim ? sim.rounds[round]?.node_probabilities ?? null : null; + + return ( +
+ + +
+ + {sim && ( + { + setSim(null); + setPlaying(false); + }} + /> + )} +
+ + + +
+ {sim ? `Cascade round ${round} of ${sim.rounds.length - 1}.` : ""} +
+
+ ); +} + +function Centered({ children }: { children: React.ReactNode }) { + return
{children}
; +} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css new file mode 100644 index 0000000..cdc979d --- /dev/null +++ b/apps/web/app/globals.css @@ -0,0 +1,6 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, body { background: #0b0d12; color: #e5e7eb; } +* { box-sizing: border-box; } diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx new file mode 100644 index 0000000..aec850f --- /dev/null +++ b/apps/web/app/layout.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "zeroforce — supply chain disruption intelligence", + description: "Exact Expected Propagation Time over supply-chain dependency graphs (RZF).", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx new file mode 100644 index 0000000..5bed12b --- /dev/null +++ b/apps/web/app/page.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { startAnalysis } from "@/lib/api"; + +const EXAMPLES = [ + "Apple semiconductor supply chain", + "Tesla battery supply chain", + "Pfizer vaccine cold chain", +]; + +export default function Home() { + const router = useRouter(); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + async function submit(value: string) { + if (!value.trim()) return; + setLoading(true); + setError(null); + try { + const { id } = await startAnalysis(value.trim()); + router.push(`/analyze/${id}`); + } catch (e) { + setError((e as Error).message); + setLoading(false); + } + } + + return ( +
+

zeroforce

+

+ If a supplier fails today, how fast does the disruption cascade — and which single + failure collapses the network fastest? +

+ +
{ + e.preventDefault(); + submit(input); + }} + className="mt-8 w-full" + > + setInput(e.target.value)} + placeholder="Describe a company, product, or supply chain…" + className="w-full rounded-lg border border-edge bg-panel px-4 py-3 outline-none focus:border-blue-500" + /> + +
+ +
+ {EXAMPLES.map((ex) => ( + + ))} +
+ + {error &&

{error}

} +
+ ); +} diff --git a/apps/web/components/CascadeBar.tsx b/apps/web/components/CascadeBar.tsx new file mode 100644 index 0000000..a6a2254 --- /dev/null +++ b/apps/web/components/CascadeBar.tsx @@ -0,0 +1,53 @@ +"use client"; + +import type { SimulateResponse } from "@/lib/types"; + +export default function CascadeBar({ + sim, + current, + setCurrent, + playing, + setPlaying, + onClose, +}: { + sim: SimulateResponse; + current: number; + setCurrent: (n: number) => void; + playing: boolean; + setPlaying: (b: boolean) => void; + onClose: () => void; +}) { + const maxRound = sim.rounds.length - 1; + return ( +
+
+ + + Round {current}/{maxRound} + + { + setPlaying(false); + setCurrent(Number(e.target.value)); + }} + className="flex-1" + /> + + EPT {sim.expected_ept.toFixed(2)} · p95 {sim.p95_rounds} · p99 {sim.p99_rounds} + + +
+
+ ); +} diff --git a/apps/web/components/ForceGraph.tsx b/apps/web/components/ForceGraph.tsx new file mode 100644 index 0000000..db030c0 --- /dev/null +++ b/apps/web/components/ForceGraph.tsx @@ -0,0 +1,61 @@ +"use client"; + +import dynamic from "next/dynamic"; +import { useMemo } from "react"; +import { disruptionColor, edgeColor, impactColor } from "@/lib/color"; +import type { AnalysisResult } from "@/lib/types"; + +const ForceGraph2D = dynamic(() => import("react-force-graph-2d"), { ssr: false }); + +export default function ForceGraph({ + result, + onSelect, + disruption, +}: { + result: AnalysisResult; + onSelect?: (nodeId: string) => void; + disruption?: Record | null; +}) { + const data = useMemo(() => { + const byId = new Map(result.node_analyses.map((a) => [a.node_id, a])); + const nodes = result.graph.nodes.map((n) => { + const a = byId.get(n.id); + const color = disruption + ? disruptionColor(disruption[n.id] ?? 0) + : impactColor(a?.percentile ?? null); + return { + id: n.id, + label: n.label, + color, + val: Math.max(1, (a?.n_reachable ?? 0) + 1), + ept: a?.ept, + impact: a?.impact_score, + }; + }); + const links = result.graph.edges.map((e) => ({ + source: e.source, + target: e.target, + width: Math.max(0.5, e.weight * 6), + color: edgeColor(e.confidence), + })); + return { nodes, links }; + }, [result, disruption]); + + return ( + n.color} + nodeVal={(n: any) => n.val} + nodeLabel={(n: any) => + `${n.label}\nEPT: ${n.ept?.toFixed?.(2) ?? "—"} | impact: ${n.impact?.toFixed?.(2) ?? "—"}` + } + nodeRelSize={5} + linkColor={(l: any) => l.color} + linkWidth={(l: any) => l.width} + linkDirectionalArrowLength={3} + linkDirectionalArrowRelPos={1} + onNodeClick={(n: any) => onSelect?.(n.id)} + /> + ); +} diff --git a/apps/web/components/RightRail.tsx b/apps/web/components/RightRail.tsx new file mode 100644 index 0000000..687cbd4 --- /dev/null +++ b/apps/web/components/RightRail.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { useState } from "react"; +import { impactColor } from "@/lib/color"; +import type { AnalysisResult } from "@/lib/types"; + +type Tab = "ranking" | "report" | "citations" | "notes"; + +export default function RightRail({ + result, + selected, +}: { + result: AnalysisResult; + selected?: string | null; +}) { + const [tab, setTab] = useState("ranking"); + const byId = new Map(result.node_analyses.map((a) => [a.node_id, a])); + const labels = new Map(result.graph.nodes.map((n) => [n.id, n.label])); + + return ( +
+
+ {(["ranking", "report", "citations", "notes"] as Tab[]).map((t) => ( + + ))} +
+ +
+ {tab === "ranking" && ( + + + + + + + + + + + + + {result.risk_ranking.map((id) => { + const a = byId.get(id)!; + return ( + + + + + + + + ); + })} + +
+ Nodes ranked by impact score (reachable nodes divided by EPT); higher is more dangerous. +
#NodeImpactEPTReach
{a.rank} + + {labels.get(id)} + {a.impact_score?.toFixed(2)}{a.ept.toFixed(2)}{a.n_reachable}
+ )} + + {tab === "ranking" && result.non_cascading_nodes.length > 0 && ( +
+ Non-cascading (pure sinks): {result.non_cascading_nodes.map((id) => labels.get(id)).join(", ")} +
+ )} + + {tab === "report" && ( +
+ {result.executive_report} +
+ )} + + {tab === "citations" && ( +
    + {result.graph.edges + .filter((e) => e.citation) + .map((e, i) => ( +
  • + {e.source} → {e.target}: {e.citation} +
  • + ))} +
+ )} + + {tab === "notes" && ( +
    + {result.validation_notes.length === 0 &&
  • No notes.
  • } + {result.validation_notes.map((n, i) => ( +
  • • {n}
  • + ))} +
+ )} +
+
+ ); +} diff --git a/apps/web/components/WhatIfPanel.tsx b/apps/web/components/WhatIfPanel.tsx new file mode 100644 index 0000000..50f8e20 --- /dev/null +++ b/apps/web/components/WhatIfPanel.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { useState } from "react"; +import { whatif } from "@/lib/api"; +import type { AnalysisResult } from "@/lib/types"; + +interface WhatIfResult { + baseline_ept: Record; + modified_ept: Record; + delta: Record; + narration: string; +} + +export default function WhatIfPanel({ result }: { result: AnalysisResult }) { + const [edgeIdx, setEdgeIdx] = useState(0); + const [weight, setWeight] = useState(0.2); + const [out, setOut] = useState(null); + const [busy, setBusy] = useState(false); + + const edge = result.graph.edges[edgeIdx]; + + async function run() { + setBusy(true); + try { + const res = (await whatif(result.id, [ + { edge: { source: edge.source, target: edge.target }, new_weight: weight }, + ])) as WhatIfResult; + setOut(res); + } finally { + setBusy(false); + } + } + + const movers = out + ? Object.entries(out.delta) + .filter(([, d]) => Math.abs(d) > 1e-6) + .sort((a, b) => Math.abs(b[1]) - Math.abs(a[1])) + .slice(0, 5) + : []; + + return ( +
+
What-if
+ + + + + setWeight(Number(e.target.value))} + className="w-full" + /> + + + + {out && ( +
+ {movers.map(([id, d]) => ( +
+ {id} + 0 ? "text-safe" : "text-danger"}> + {d > 0 ? "+" : ""} + {d.toFixed(2)} ({out.baseline_ept[id]?.toFixed(2)}→{out.modified_ept[id]?.toFixed(2)}) + +
+ ))} +

{out.narration}

+
+ )} +
+ ); +} diff --git a/apps/web/lib/api.ts b/apps/web/lib/api.ts new file mode 100644 index 0000000..128e0b6 --- /dev/null +++ b/apps/web/lib/api.ts @@ -0,0 +1,46 @@ +import type { JobRecord, SimulateResponse } from "./types"; + +async function jsonOrThrow(res: Response) { + const body = await res.json().catch(() => ({})); + if (!res.ok) { + const detail = (body as any)?.detail; + throw new Error(detail?.message || detail || `request failed (${res.status})`); + } + return body; +} + +export async function startAnalysis(input: string, mode = "company"): Promise<{ id: string }> { + const res = await fetch("/api/v1/analyze", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ input, mode, max_nodes: 12 }), + }); + return jsonOrThrow(res); +} + +export async function getAnalysis(id: string): Promise { + return jsonOrThrow(await fetch(`/api/v1/analyses/${id}`)); +} + +export async function simulate( + analysis_id: string, + starting_nodes: string[], + rounds = 10, + samples = 1000, +): Promise { + const res = await fetch("/api/v1/simulate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ analysis_id, starting_nodes, rounds, samples }), + }); + return jsonOrThrow(res); +} + +export async function whatif(analysis_id: string, modifications: unknown[]) { + const res = await fetch("/api/v1/whatif", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ analysis_id, modifications }), + }); + return jsonOrThrow(res); +} diff --git a/apps/web/lib/color.test.ts b/apps/web/lib/color.test.ts new file mode 100644 index 0000000..080b5d7 --- /dev/null +++ b/apps/web/lib/color.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { edgeColor, impactColor } from "./color"; + +describe("impactColor", () => { + it("renders pure sinks (null percentile) grey", () => { + expect(impactColor(null)).toBe("#6b7280"); + }); + it("is red at the top percentile and green at the bottom", () => { + expect(impactColor(100)).toBe("rgb(239,68,68)"); + expect(impactColor(0)).toBe("rgb(34,197,68)"); + }); +}); + +describe("edgeColor", () => { + it("maps confidence to blue/amber/grey", () => { + expect(edgeColor("high")).toBe("#3b82f6"); + expect(edgeColor("medium")).toBe("#f59e0b"); + expect(edgeColor("low")).toBe("#6b7280"); + }); +}); diff --git a/apps/web/lib/color.ts b/apps/web/lib/color.ts new file mode 100644 index 0000000..fef7e36 --- /dev/null +++ b/apps/web/lib/color.ts @@ -0,0 +1,23 @@ +// Continuous red->yellow->green by impact percentile (spec §10.4). Pure sinks render grey. +export function impactColor(percentile: number | null): string { + if (percentile === null) return "#6b7280"; // grey: non-cascading + const t = Math.max(0, Math.min(100, percentile)) / 100; // 0..1, higher = more dangerous + // danger (red) at t=1, safe (green) at t=0, yellow in the middle + const r = t > 0.5 ? 239 : Math.round(34 + (239 - 34) * (t / 0.5)); + const g = t > 0.5 ? Math.round(197 - (197 - 68) * ((t - 0.5) / 0.5)) : 197; + const b = 68; + return `rgb(${r},${g},${b})`; +} + +export function edgeColor(confidence: string): string { + return confidence === "high" ? "#3b82f6" : confidence === "medium" ? "#f59e0b" : "#6b7280"; +} + +// Cascade animation: white(undisrupted) -> red(disrupted) by disruption probability. +export function disruptionColor(p: number): string { + const t = Math.max(0, Math.min(1, p)); + const r = Math.round(229 + (239 - 229) * t); + const g = Math.round(231 - (231 - 68) * t); + const b = Math.round(235 - (235 - 68) * t); + return `rgb(${r},${g},${b})`; +} diff --git a/apps/web/lib/types.ts b/apps/web/lib/types.ts new file mode 100644 index 0000000..4f6cde1 --- /dev/null +++ b/apps/web/lib/types.ts @@ -0,0 +1,70 @@ +export interface Node { + id: string; + label: string; + sector: string; + country?: string | null; + tier?: number | null; +} + +export interface Edge { + source: string; + target: string; + weight: number; + confidence: "high" | "medium" | "low"; + citation?: string | null; + reasoning?: string; +} + +export interface Graph { + nodes: Node[]; + edges: Edge[]; + focal_node: string; + extraction_model: string; + extraction_timestamp: string; +} + +export interface NodeAnalysis { + node_id: string; + ept: number; + n_reachable: number; + reachable_set: string[]; + unreachable_from_here: string[]; + impact_score: number | null; + rank: number | null; + percentile: number | null; +} + +export interface AnalysisResult { + id: string; + graph: Graph; + node_analyses: NodeAnalysis[]; + risk_ranking: string[]; + non_cascading_nodes: string[]; + executive_report: string; + validation_notes: string[]; + provider_metadata: Record; + created_at: string; +} + +export type JobStatus = "pending" | "running" | "done" | "failed"; + +export interface JobRecord { + id: string; + status: JobStatus; + stage?: string; + error?: { code: string; message: string }; + result?: AnalysisResult; +} + +export interface SimulateRound { + round: number; + disrupted: string[]; + node_probabilities: Record; +} + +export interface SimulateResponse { + rounds: SimulateRound[]; + expected_ept: number; + p95_rounds: number; + p99_rounds: number; +} diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs new file mode 100644 index 0000000..bb73c23 --- /dev/null +++ b/apps/web/next.config.mjs @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + output: "standalone", + async rewrites() { + const api = process.env.API_BASE || "http://localhost:8080"; + return [{ source: "/api/v1/:path*", destination: `${api}/api/v1/:path*` }]; + }, +}; +export default nextConfig; diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json new file mode 100644 index 0000000..8886672 --- /dev/null +++ b/apps/web/package-lock.json @@ -0,0 +1,3685 @@ +{ + "name": "zeroforce-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "zeroforce-web", + "version": "0.1.0", + "dependencies": { + "next": "14.2", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-force-graph-2d": "1.25.5" + }, + "devDependencies": { + "@types/node": "20.14.10", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "autoprefixer": "10.4.19", + "postcss": "8.4.39", + "tailwindcss": "3.4.6", + "typescript": "5.5.3", + "vitest": "2.0.5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", + "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.0.5", + "estree-walker": "^3.0.3", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accessor-fn": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz", + "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bezier-js": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz", + "integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvas-color-tracker": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz", + "integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==", + "license": "MIT", + "dependencies": { + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-binarytree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", + "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force-3d": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", + "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", + "license": "MIT", + "dependencies": { + "d3-binarytree": "1", + "d3-dispatch": "1 - 3", + "d3-octree": "1", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-octree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", + "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", + "license": "MIT" + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", + "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/float-tooltip": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz", + "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==", + "license": "MIT", + "dependencies": { + "d3-selection": "2 - 3", + "kapsule": "^1.16", + "preact": "10" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/force-graph": { + "version": "1.51.4", + "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.4.tgz", + "integrity": "sha512-TdJ2KbkoiDQ7NIRx8IPGD0mAXXpLhamS7c+b7W98b0MHG7lphnda1VOQX/98UDTsttIAdH4TcP0l0MauSnLK8w==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "18 - 25", + "accessor-fn": "1", + "bezier-js": "3 - 6", + "canvas-color-tracker": "^1.3", + "d3-array": "1 - 3", + "d3-drag": "2 - 3", + "d3-force-3d": "2 - 3", + "d3-scale": "1 - 4", + "d3-scale-chromatic": "1 - 3", + "d3-selection": "2 - 3", + "d3-zoom": "2 - 3", + "float-tooltip": "^1.7", + "index-array-by": "1", + "kapsule": "^1.16", + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/index-array-by": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz", + "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jerrypick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz", + "integrity": "sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/kapsule": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", + "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==", + "license": "MIT", + "dependencies": { + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.35", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/preact": { + "version": "10.29.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-force-graph-2d": { + "version": "1.25.5", + "resolved": "https://registry.npmjs.org/react-force-graph-2d/-/react-force-graph-2d-1.25.5.tgz", + "integrity": "sha512-3u8WjZZorpwZSDs3n3QeOS9ZoxFPM+IR9SStYJVQ/qKECydMHarxnf7ynV/MKJbC6kUsc60soD0V+Uq/r2vz7Q==", + "license": "MIT", + "dependencies": { + "force-graph": "1", + "prop-types": "15", + "react-kapsule": "2" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-kapsule": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.5.7.tgz", + "integrity": "sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==", + "license": "MIT", + "dependencies": { + "jerrypick": "^1.1.1" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "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", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz", + "integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vitest": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", + "execa": "^8.0.1", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..7280133 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,29 @@ +{ + "name": "zeroforce-web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev -p 3000", + "build": "next build", + "start": "next start -p 3000", + "lint": "next lint", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "next": "14.2.35", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-force-graph-2d": "1.25.5" + }, + "devDependencies": { + "@types/node": "20.14.10", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "autoprefixer": "10.4.19", + "postcss": "8.4.39", + "tailwindcss": "3.4.6", + "typescript": "5.5.3", + "vitest": "2.0.5" + } +} diff --git a/apps/web/postcss.config.mjs b/apps/web/postcss.config.mjs new file mode 100644 index 0000000..e008c9c --- /dev/null +++ b/apps/web/postcss.config.mjs @@ -0,0 +1,3 @@ +export default { + plugins: { tailwindcss: {}, autoprefixer: {} }, +}; diff --git a/apps/web/public/.gitkeep b/apps/web/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts new file mode 100644 index 0000000..ad5a2ea --- /dev/null +++ b/apps/web/tailwind.config.ts @@ -0,0 +1,19 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"], + theme: { + extend: { + colors: { + bg: "#0b0d12", + panel: "#13161d", + edge: "#2a2f3a", + danger: "#ef4444", + warn: "#f59e0b", + safe: "#22c55e", + }, + }, + }, + plugins: [], +}; +export default config; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..f5fc2f7 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/data/bea-15-sector.json b/data/bea-15-sector.json new file mode 100644 index 0000000..b50aa7a --- /dev/null +++ b/data/bea-15-sector.json @@ -0,0 +1,534 @@ +{ + "nodes": [ + { + "id": "Agriculture", + "label": "Agriculture", + "sector": "Agriculture", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "Mining", + "label": "Mining", + "sector": "Mining", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "Utilities", + "label": "Utilities", + "sector": "Utilities", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "Construction", + "label": "Construction", + "sector": "Construction", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "Manufacturing", + "label": "Manufacturing", + "sector": "Manufacturing", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "Wholesale", + "label": "Wholesale", + "sector": "Wholesale", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "Retail", + "label": "Retail", + "sector": "Retail", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "Transportation", + "label": "Transportation", + "sector": "Transportation", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "Information", + "label": "Information", + "sector": "Information", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "Finance", + "label": "Finance", + "sector": "Finance", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "RealEstate", + "label": "RealEstate", + "sector": "RealEstate", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "ProfServices", + "label": "ProfServices", + "sector": "ProfServices", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "Education", + "label": "Education", + "sector": "Education", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "HealthCare", + "label": "HealthCare", + "sector": "HealthCare", + "country": null, + "tier": null, + "metadata": {} + }, + { + "id": "Government", + "label": "Government", + "sector": "Government", + "country": null, + "tier": null, + "metadata": {} + } + ], + "edges": [ + { + "source": "Government", + "target": "Agriculture", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Government supplies inputs to Agriculture (BEA IO).", + "asof": null + }, + { + "source": "HealthCare", + "target": "Agriculture", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "HealthCare supplies inputs to Agriculture (BEA IO).", + "asof": null + }, + { + "source": "Education", + "target": "Agriculture", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Education supplies inputs to Agriculture (BEA IO).", + "asof": null + }, + { + "source": "Agriculture", + "target": "Mining", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Agriculture supplies inputs to Mining (BEA IO).", + "asof": null + }, + { + "source": "Government", + "target": "Mining", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Government supplies inputs to Mining (BEA IO).", + "asof": null + }, + { + "source": "HealthCare", + "target": "Mining", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "HealthCare supplies inputs to Mining (BEA IO).", + "asof": null + }, + { + "source": "Mining", + "target": "Utilities", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Mining supplies inputs to Utilities (BEA IO).", + "asof": null + }, + { + "source": "Agriculture", + "target": "Utilities", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Agriculture supplies inputs to Utilities (BEA IO).", + "asof": null + }, + { + "source": "Government", + "target": "Utilities", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Government supplies inputs to Utilities (BEA IO).", + "asof": null + }, + { + "source": "Utilities", + "target": "Construction", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Utilities supplies inputs to Construction (BEA IO).", + "asof": null + }, + { + "source": "Mining", + "target": "Construction", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Mining supplies inputs to Construction (BEA IO).", + "asof": null + }, + { + "source": "Agriculture", + "target": "Construction", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Agriculture supplies inputs to Construction (BEA IO).", + "asof": null + }, + { + "source": "Construction", + "target": "Manufacturing", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Construction supplies inputs to Manufacturing (BEA IO).", + "asof": null + }, + { + "source": "Utilities", + "target": "Manufacturing", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Utilities supplies inputs to Manufacturing (BEA IO).", + "asof": null + }, + { + "source": "Mining", + "target": "Manufacturing", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Mining supplies inputs to Manufacturing (BEA IO).", + "asof": null + }, + { + "source": "Manufacturing", + "target": "Wholesale", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Manufacturing supplies inputs to Wholesale (BEA IO).", + "asof": null + }, + { + "source": "Construction", + "target": "Wholesale", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Construction supplies inputs to Wholesale (BEA IO).", + "asof": null + }, + { + "source": "Utilities", + "target": "Wholesale", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Utilities supplies inputs to Wholesale (BEA IO).", + "asof": null + }, + { + "source": "Wholesale", + "target": "Retail", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Wholesale supplies inputs to Retail (BEA IO).", + "asof": null + }, + { + "source": "Manufacturing", + "target": "Retail", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Manufacturing supplies inputs to Retail (BEA IO).", + "asof": null + }, + { + "source": "Construction", + "target": "Retail", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Construction supplies inputs to Retail (BEA IO).", + "asof": null + }, + { + "source": "Retail", + "target": "Transportation", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Retail supplies inputs to Transportation (BEA IO).", + "asof": null + }, + { + "source": "Wholesale", + "target": "Transportation", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Wholesale supplies inputs to Transportation (BEA IO).", + "asof": null + }, + { + "source": "Manufacturing", + "target": "Transportation", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Manufacturing supplies inputs to Transportation (BEA IO).", + "asof": null + }, + { + "source": "Transportation", + "target": "Information", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Transportation supplies inputs to Information (BEA IO).", + "asof": null + }, + { + "source": "Retail", + "target": "Information", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Retail supplies inputs to Information (BEA IO).", + "asof": null + }, + { + "source": "Wholesale", + "target": "Information", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Wholesale supplies inputs to Information (BEA IO).", + "asof": null + }, + { + "source": "Information", + "target": "Finance", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Information supplies inputs to Finance (BEA IO).", + "asof": null + }, + { + "source": "Transportation", + "target": "Finance", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Transportation supplies inputs to Finance (BEA IO).", + "asof": null + }, + { + "source": "Retail", + "target": "Finance", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Retail supplies inputs to Finance (BEA IO).", + "asof": null + }, + { + "source": "Finance", + "target": "RealEstate", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Finance supplies inputs to RealEstate (BEA IO).", + "asof": null + }, + { + "source": "Information", + "target": "RealEstate", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Information supplies inputs to RealEstate (BEA IO).", + "asof": null + }, + { + "source": "Transportation", + "target": "RealEstate", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Transportation supplies inputs to RealEstate (BEA IO).", + "asof": null + }, + { + "source": "RealEstate", + "target": "ProfServices", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "RealEstate supplies inputs to ProfServices (BEA IO).", + "asof": null + }, + { + "source": "Finance", + "target": "ProfServices", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Finance supplies inputs to ProfServices (BEA IO).", + "asof": null + }, + { + "source": "Information", + "target": "ProfServices", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Information supplies inputs to ProfServices (BEA IO).", + "asof": null + }, + { + "source": "ProfServices", + "target": "Education", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "ProfServices supplies inputs to Education (BEA IO).", + "asof": null + }, + { + "source": "RealEstate", + "target": "Education", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "RealEstate supplies inputs to Education (BEA IO).", + "asof": null + }, + { + "source": "Finance", + "target": "Education", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Finance supplies inputs to Education (BEA IO).", + "asof": null + }, + { + "source": "Education", + "target": "HealthCare", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Education supplies inputs to HealthCare (BEA IO).", + "asof": null + }, + { + "source": "ProfServices", + "target": "HealthCare", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "ProfServices supplies inputs to HealthCare (BEA IO).", + "asof": null + }, + { + "source": "RealEstate", + "target": "HealthCare", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "RealEstate supplies inputs to HealthCare (BEA IO).", + "asof": null + }, + { + "source": "HealthCare", + "target": "Government", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "HealthCare supplies inputs to Government (BEA IO).", + "asof": null + }, + { + "source": "Education", + "target": "Government", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "Education supplies inputs to Government (BEA IO).", + "asof": null + }, + { + "source": "ProfServices", + "target": "Government", + "weight": 0.333333, + "confidence": "medium", + "citation": "BEA 15-sector IO summary", + "reasoning": "ProfServices supplies inputs to Government (BEA IO).", + "asof": null + } + ], + "focal_node": "Manufacturing", + "extraction_model": "bea-15-sector-bundle", + "extraction_timestamp": "2026-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md new file mode 100644 index 0000000..8bbc2fb --- /dev/null +++ b/docs/RUNBOOK.md @@ -0,0 +1,76 @@ +# Runbook + +## Local — single process (offline) + +```bash +uv sync --all-packages +ZEROFORCE_MODE=fake ZEROFORCE_DB=:memory: uv run uvicorn zeroforce_backend.asgi:app --port 8080 +cd apps/web && npm install && API_BASE=http://localhost:8080 npm run dev +``` + +## Local — six-service stack (docker-compose) + +```bash +# offline: +docker compose -f infra/docker-compose.dev.yml up --build +# live dev providers: +ZEROFORCE_MODE=dev GROQ_API_KEY=... TAVILY_API_KEY=... \ + docker compose -f infra/docker-compose.dev.yml up --build +``` + +Gateway on :8080 (fans out to extractor:8081, validator:8082, engine:8083, reporter:8084 via +`ZEROFORCE_SERVICE_URLS`), Redis on :6379, web on :3000. + +## Modes + +| Mode | LLM | Search | Storage | Keys | +|---|---|---|---|---| +| `fake` | deterministic | deterministic | SQLite/memory | none | +| `dev` | Groq gpt-oss-120b | Tavily | SQLite | `GROQ_API_KEY`, `TAVILY_API_KEY` | +| `prod` | Gemini 2.5 Pro (Vertex) | Vertex grounding | Firestore + Redis | GCP ADC, `GCP_PROJECT` | + +## Production deploy (Terraform → Cloud Run) — NOT YET APPLIED + +> No deploy has been run from this repo (no GCP credentials in the build environment). The +> steps below are the intended procedure; review before applying. + +```bash +# 1. Build + push images to Artifact Registry +REGION=us-central1; PROJECT=your-project +docker build -f infra/docker/Dockerfile.backend -t $REGION-docker.pkg.dev/$PROJECT/zeroforce/backend:latest . +docker build -f infra/docker/Dockerfile.web -t $REGION-docker.pkg.dev/$PROJECT/zeroforce/web:latest apps/web +docker push $REGION-docker.pkg.dev/$PROJECT/zeroforce/backend:latest +docker push $REGION-docker.pkg.dev/$PROJECT/zeroforce/web:latest + +# 2. Provision (Cloud Run x6, Firestore, Memorystore, Artifact Registry, Secret Manager, IAM) +cd infra/terraform +terraform init +terraform apply -var "project_id=$PROJECT" -var "region=$REGION" + +# 3. Load provider secrets (only needed if any service runs in dev mode) +echo -n "$GROQ_API_KEY" | gcloud secrets versions add groq-api-key --data-file=- +echo -n "$TAVILY_API_KEY" | gcloud secrets versions add tavily-api-key --data-file=- +``` + +Outputs `gateway_url` and `web_url`. Workers stay private (invoked only by the gateway SA). + +## Observability + +- Structured access logs (JSON) + `Server-Timing` header are always on. +- `ZEROFORCE_OTEL=1` enables OpenTelemetry tracing (console exporter dev / Cloud Trace prod; + requires the `opentelemetry-*` packages). + +## Load testing + +```bash +k6 run -e BASE=http://localhost:8080 tests/load/analyze_load.js +``` + +## Beta onboarding (Phase 5 — process, not code) + +1. Collect the partner's focal firm + key tier-1 suppliers. +2. Run an analysis in `dev` mode; review the extracted graph for missing major suppliers + (validator notes flag normalization issues). +3. Validate top-3 risk nodes with the partner's procurement team. +4. Iterate weights via the what-if panel; capture the resilience deltas. +5. Export the executive report for their risk committee. diff --git a/infra/docker-compose.dev.yml b/infra/docker-compose.dev.yml new file mode 100644 index 0000000..2400cc1 --- /dev/null +++ b/infra/docker-compose.dev.yml @@ -0,0 +1,67 @@ +# Local six-service stack (spec §5.4). Gateway embeds the orchestrator and fans out to the +# four worker services over HTTP. Run from repo root: +# docker compose -f infra/docker-compose.dev.yml up --build +# Provide GROQ_API_KEY / TAVILY_API_KEY in the environment for ZEROFORCE_MODE=dev, or set +# ZEROFORCE_MODE=fake for fully offline operation. + +x-backend: &backend + build: + context: .. + dockerfile: infra/docker/Dockerfile.backend + environment: + ZEROFORCE_MODE: ${ZEROFORCE_MODE:-fake} + GROQ_API_KEY: ${GROQ_API_KEY:-} + TAVILY_API_KEY: ${TAVILY_API_KEY:-} + +services: + extractor: + <<: *backend + command: uvicorn zeroforce_backend.worker_apps:extractor_app --factory --host 0.0.0.0 --port 8081 + ports: ["8081:8081"] + + validator: + <<: *backend + command: uvicorn zeroforce_backend.worker_apps:validator_app --factory --host 0.0.0.0 --port 8082 + ports: ["8082:8082"] + + engine: + <<: *backend + command: uvicorn zeroforce_backend.worker_apps:engine_app --factory --host 0.0.0.0 --port 8083 + ports: ["8083:8083"] + + reporter: + <<: *backend + command: uvicorn zeroforce_backend.worker_apps:reporter_app --factory --host 0.0.0.0 --port 8084 + ports: ["8084:8084"] + + gateway: + <<: *backend + command: uvicorn zeroforce_backend.asgi:app --host 0.0.0.0 --port 8080 + environment: + ZEROFORCE_MODE: ${ZEROFORCE_MODE:-fake} + GROQ_API_KEY: ${GROQ_API_KEY:-} + TAVILY_API_KEY: ${TAVILY_API_KEY:-} + ZEROFORCE_DB: /data/zeroforce.db + ZEROFORCE_SERVICE_URLS: >- + {"extractor":"http://extractor:8081","validator":"http://validator:8082", + "engine":"http://engine:8083","reporter":"http://reporter:8084"} + volumes: + - zeroforce-data:/data + depends_on: [extractor, validator, engine, reporter, redis] + ports: ["8080:8080"] + + redis: + image: redis:7-alpine + ports: ["6379:6379"] + + web: + build: + context: ../apps/web + dockerfile: ../../infra/docker/Dockerfile.web + environment: + API_BASE: http://gateway:8080 + depends_on: [gateway] + ports: ["3000:3000"] + +volumes: + zeroforce-data: diff --git a/infra/docker/Dockerfile.backend b/infra/docker/Dockerfile.backend new file mode 100644 index 0000000..d0e58ce --- /dev/null +++ b/infra/docker/Dockerfile.backend @@ -0,0 +1,26 @@ +# Backend image — all six services run from this image; the compose/Cloud Run command +# selects which FastAPI app to serve. Build context = repo root. +FROM python:3.12-slim + +RUN pip install --no-cache-dir uv +WORKDIR /app + +# Dependency layer (cached unless manifests change). +COPY pyproject.toml uv.lock* ./ +COPY packages/zeroforce/pyproject.toml packages/zeroforce/ +COPY packages/schemas/pyproject.toml packages/schemas/ +COPY packages/providers/pyproject.toml packages/providers/ +COPY services/backend/pyproject.toml services/backend/ + +# Source. +COPY packages ./packages +COPY services ./services +COPY data ./data + +RUN uv sync --all-packages --no-dev +ENV PATH="/app/.venv/bin:$PATH" +ENV ZEROFORCE_MODE=dev + +EXPOSE 8080 +# Default: gateway (with embedded orchestrator). Override `command` for worker services. +CMD ["uvicorn", "zeroforce_backend.asgi:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/infra/docker/Dockerfile.web b/infra/docker/Dockerfile.web new file mode 100644 index 0000000..e25d641 --- /dev/null +++ b/infra/docker/Dockerfile.web @@ -0,0 +1,21 @@ +# Frontend image — Next.js 14 standalone build. Build context = apps/web. +FROM node:20-slim AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +FROM node:20-slim AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + +FROM node:20-slim AS runner +WORKDIR /app +ENV NODE_ENV=production +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public ./public +EXPOSE 3000 +ENV PORT=3000 +CMD ["node", "server.js"] diff --git a/infra/terraform/cloudrun.tf b/infra/terraform/cloudrun.tf new file mode 100644 index 0000000..7fc0f8c --- /dev/null +++ b/infra/terraform/cloudrun.tf @@ -0,0 +1,95 @@ +# One Cloud Run service per microservice (spec §5.1, §5.4). + +locals { + backend_image = "${var.region}-docker.pkg.dev/${var.project_id}/zeroforce/backend:${var.image_tag}" + web_image = "${var.region}-docker.pkg.dev/${var.project_id}/zeroforce/web:${var.image_tag}" + + workers = { + extractor = "uvicorn zeroforce_backend.worker_apps:extractor_app --factory --host 0.0.0.0 --port 8080" + validator = "uvicorn zeroforce_backend.worker_apps:validator_app --factory --host 0.0.0.0 --port 8080" + engine = "uvicorn zeroforce_backend.worker_apps:engine_app --factory --host 0.0.0.0 --port 8080" + reporter = "uvicorn zeroforce_backend.worker_apps:reporter_app --factory --host 0.0.0.0 --port 8080" + } +} + +resource "google_cloud_run_v2_service" "worker" { + for_each = local.workers + name = "zeroforce-${each.key}" + location = var.region + + template { + service_account = google_service_account.runtime.email + scaling { + min_instance_count = var.min_instances + max_instance_count = var.max_instances + } + containers { + image = local.backend_image + command = ["sh", "-c"] + args = [each.value] + env { + name = "ZEROFORCE_MODE" + value = "prod" + } + resources { + limits = { cpu = "2", memory = "2Gi" } # engine DP is CPU-bound at n<=18 + } + } + } + depends_on = [google_project_service.apis] +} + +resource "google_cloud_run_v2_service" "gateway" { + name = "zeroforce-gateway" + location = var.region + + template { + service_account = google_service_account.runtime.email + scaling { + min_instance_count = var.min_instances + max_instance_count = var.max_instances + } + containers { + image = local.backend_image + env { + name = "ZEROFORCE_MODE" + value = "prod" + } + env { + name = "ZEROFORCE_SERVICE_URLS" + value = jsonencode({ + extractor = google_cloud_run_v2_service.worker["extractor"].uri + validator = google_cloud_run_v2_service.worker["validator"].uri + engine = google_cloud_run_v2_service.worker["engine"].uri + reporter = google_cloud_run_v2_service.worker["reporter"].uri + }) + } + resources { + limits = { cpu = "1", memory = "1Gi" } + } + } + } +} + +resource "google_cloud_run_v2_service" "web" { + name = "zeroforce-web" + location = var.region + template { + containers { + image = local.web_image + env { + name = "API_BASE" + value = google_cloud_run_v2_service.gateway.uri + } + } + } +} + +# Public access to gateway + web; workers stay private (called only by the gateway SA). +resource "google_cloud_run_v2_service_iam_member" "public" { + for_each = toset(["gateway", "web"]) + name = each.key == "gateway" ? google_cloud_run_v2_service.gateway.name : google_cloud_run_v2_service.web.name + location = var.region + role = "roles/run.invoker" + member = "allUsers" +} diff --git a/infra/terraform/data.tf b/infra/terraform/data.tf new file mode 100644 index 0000000..5667f3d --- /dev/null +++ b/infra/terraform/data.tf @@ -0,0 +1,37 @@ +# Stateful backing services (spec §5.3). + +resource "google_firestore_database" "main" { + name = "(default)" + location_id = var.region + type = "FIRESTORE_NATIVE" + depends_on = [google_project_service.apis] +} + +resource "google_redis_instance" "cache" { + name = "zeroforce-cache" + tier = "BASIC" + memory_size_gb = 1 + region = var.region + redis_version = "REDIS_7_0" + depends_on = [google_project_service.apis] +} + +resource "google_storage_bucket" "uploads" { + name = "${var.project_id}-zeroforce-uploads" + location = var.region + uniform_bucket_level_access = true + force_destroy = false +} + +# Provider keys for ZEROFORCE_MODE=dev fallbacks; prod uses Vertex via ADC and needs neither. +resource "google_secret_manager_secret" "groq" { + secret_id = "groq-api-key" + replication { auto {} } + depends_on = [google_project_service.apis] +} + +resource "google_secret_manager_secret" "tavily" { + secret_id = "tavily-api-key" + replication { auto {} } + depends_on = [google_project_service.apis] +} diff --git a/infra/terraform/main.tf b/infra/terraform/main.tf new file mode 100644 index 0000000..4ce2bbf --- /dev/null +++ b/infra/terraform/main.tf @@ -0,0 +1,54 @@ +terraform { + required_version = ">= 1.5" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 5.0" + } + } +} + +provider "google" { + project = var.project_id + region = var.region +} + +# APIs required by the platform (spec §5.4). +resource "google_project_service" "apis" { + for_each = toset([ + "run.googleapis.com", + "firestore.googleapis.com", + "redis.googleapis.com", + "artifactregistry.googleapis.com", + "secretmanager.googleapis.com", + "aiplatform.googleapis.com", + "cloudtrace.googleapis.com", + ]) + service = each.value + disable_on_destroy = false +} + +resource "google_artifact_registry_repository" "images" { + location = var.region + repository_id = "zeroforce" + format = "DOCKER" + depends_on = [google_project_service.apis] +} + +resource "google_service_account" "runtime" { + account_id = "zeroforce-runtime" + display_name = "zeroforce Cloud Run runtime" +} + +# Vertex AI + Firestore + Secret access for the runtime SA. +resource "google_project_iam_member" "runtime_roles" { + for_each = toset([ + "roles/aiplatform.user", + "roles/datastore.user", + "roles/secretmanager.secretAccessor", + "roles/cloudtrace.agent", + ]) + project = var.project_id + role = each.value + member = "serviceAccount:${google_service_account.runtime.email}" +} diff --git a/infra/terraform/outputs.tf b/infra/terraform/outputs.tf new file mode 100644 index 0000000..ee8900a --- /dev/null +++ b/infra/terraform/outputs.tf @@ -0,0 +1,11 @@ +output "gateway_url" { + value = google_cloud_run_v2_service.gateway.uri +} + +output "web_url" { + value = google_cloud_run_v2_service.web.uri +} + +output "artifact_registry" { + value = google_artifact_registry_repository.images.name +} diff --git a/infra/terraform/variables.tf b/infra/terraform/variables.tf new file mode 100644 index 0000000..0215302 --- /dev/null +++ b/infra/terraform/variables.tf @@ -0,0 +1,25 @@ +variable "project_id" { + type = string + description = "GCP project id." +} + +variable "region" { + type = string + default = "us-central1" +} + +variable "image_tag" { + type = string + default = "latest" + description = "Container image tag for the backend/web images in Artifact Registry." +} + +variable "min_instances" { + type = number + default = 0 +} + +variable "max_instances" { + type = number + default = 10 +} diff --git a/packages/providers/pyproject.toml b/packages/providers/pyproject.toml new file mode 100644 index 0000000..068c295 --- /dev/null +++ b/packages/providers/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "zeroforce-providers" +version = "0.0.1" +description = "LLM + search provider abstraction for zeroforce (fake/dev/prod)." +requires-python = ">=3.12,<3.13" +dependencies = ["pydantic>=2.7,<3.0", "zeroforce", "zeroforce-schemas"] + +[project.optional-dependencies] +# Heavy vendor SDKs are optional; `fake` mode needs none of them. +dev = ["openai>=1.40", "tavily-python>=0.5"] +prod = ["google-genai>=0.3"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["zeroforce_providers"] diff --git a/packages/providers/zeroforce_providers/__init__.py b/packages/providers/zeroforce_providers/__init__.py new file mode 100644 index 0000000..e0d2fb7 --- /dev/null +++ b/packages/providers/zeroforce_providers/__init__.py @@ -0,0 +1,18 @@ +"""zeroforce provider abstraction (LLM + search).""" + +from .base import LLMProvider, SearchProvider, SearchResult +from .factory import Mode, get_mode, make_llm, make_search +from .fake import FakeLLMProvider, FakeSearchProvider, build_fake_graph + +__all__ = [ + "LLMProvider", + "SearchProvider", + "SearchResult", + "Mode", + "get_mode", + "make_llm", + "make_search", + "FakeLLMProvider", + "FakeSearchProvider", + "build_fake_graph", +] diff --git a/packages/providers/zeroforce_providers/base.py b/packages/providers/zeroforce_providers/base.py new file mode 100644 index 0000000..8cb19c7 --- /dev/null +++ b/packages/providers/zeroforce_providers/base.py @@ -0,0 +1,57 @@ +"""Provider protocols (spec §6.2, §6.3). Application code depends only on these.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Protocol, TypeVar, runtime_checkable + +from pydantic import BaseModel + +T = TypeVar("T", bound=BaseModel) + + +class SearchResult(BaseModel): + title: str + url: str + snippet: str + published_at: datetime | None = None + score: float | None = None + + +@runtime_checkable +class LLMProvider(Protocol): + name: str + + async def generate_structured( + self, + system: str, + user: str, + schema: type[T], + temperature: float = 0.2, + max_output_tokens: int = 4096, + ) -> T: ... + + async def generate_text( + self, + system: str, + user: str, + temperature: float = 0.4, + max_output_tokens: int = 1024, + ) -> str: ... + + async def health(self) -> bool: ... + + +@runtime_checkable +class SearchProvider(Protocol): + name: str + + async def search( + self, + query: str, + max_results: int = 5, + include_domains: list[str] | None = None, + recency_days: int | None = None, + ) -> list[SearchResult]: ... + + async def fetch(self, url: str) -> str: ... diff --git a/packages/providers/zeroforce_providers/factory.py b/packages/providers/zeroforce_providers/factory.py new file mode 100644 index 0000000..656eb91 --- /dev/null +++ b/packages/providers/zeroforce_providers/factory.py @@ -0,0 +1,53 @@ +"""Provider factory — the dev/prod/fake hinge (spec §3.2, §6.6). + +Mode is a single env var `ZEROFORCE_MODE`: + fake (default) -> deterministic offline providers; no keys, no network. + dev -> Groq (GROQ_API_KEY) + Tavily (TAVILY_API_KEY). + prod -> Gemini 2.5 Pro (Vertex, GCP ADC) + Vertex grounding. +""" + +from __future__ import annotations + +import os +from typing import Literal + +from .base import LLMProvider, SearchProvider + +Mode = Literal["fake", "dev", "prod"] + + +def get_mode() -> Mode: + mode = os.environ.get("ZEROFORCE_MODE", "fake").lower() + if mode not in ("fake", "dev", "prod"): + raise ValueError(f"ZEROFORCE_MODE must be fake|dev|prod, got {mode!r}") + return mode # type: ignore[return-value] + + +def make_llm(mode: Mode | None = None) -> LLMProvider: + mode = mode or get_mode() + if mode == "fake": + from .fake import FakeLLMProvider + + return FakeLLMProvider() + if mode == "dev": + from .groq import GroqProvider + + return GroqProvider() + from .gemini import GeminiProvider + + return GeminiProvider() + + +def make_search(mode: Mode | None = None) -> SearchProvider: + mode = mode or get_mode() + if mode == "fake": + from .fake import FakeSearchProvider + + return FakeSearchProvider() + if mode == "dev": + from .tavily import TavilyProvider + + return TavilyProvider() + from .vertex import VertexGroundingProvider + + return VertexGroundingProvider() diff --git a/packages/providers/zeroforce_providers/fake.py b/packages/providers/zeroforce_providers/fake.py new file mode 100644 index 0000000..49bbb0f --- /dev/null +++ b/packages/providers/zeroforce_providers/fake.py @@ -0,0 +1,153 @@ +"""Deterministic offline providers — the default (ZEROFORCE_MODE=fake). + +No network, no keys, fully reproducible. The fake LLM builds a valid supply-chain tree +from any input so the whole pipeline runs end-to-end in tests and CI. The fake search +returns deterministic snippets. These exist so nothing in the system requires live keys +to be exercised; real providers (groq/tavily/gemini) plug in via the factory when keys +are present. +""" + +from __future__ import annotations + +import hashlib +import re +from datetime import datetime, timezone + +from pydantic import BaseModel + +from zeroforce.types import Edge, Graph, Node + +from .base import SearchResult + +# Fixed timestamp keeps fake graphs byte-stable across runs (the analysis id is a hash of +# the INPUT, not the graph, so this does not affect caching). +_FIXED_TS = datetime(2026, 1, 1, tzinfo=timezone.utc) +_CONF_CYCLE = ("high", "medium", "low") + + +def _seed(text: str) -> int: + return int(hashlib.sha256(text.encode()).hexdigest()[:8], 16) + + +def _subject(text: str) -> str: + """The real analysis subject. The extractor passes a templated prompt containing + 'for: '; recover the subject so the fake graph is keyed to it, not to the + prompt boilerplate. Falls back to the raw text.""" + m = re.search(r"for:\s*(.+)", text) + line = (m.group(1) if m else text).splitlines()[0] if text else "" + return line.strip() + + +def _focal_id(text: str) -> str: + subject = _subject(text) + token = re.sub(r"[^A-Za-z0-9]+", "", subject.split()[0]) if subject.split() else "Focal" + return (token[:24] or "Focal").capitalize() + + +def build_fake_graph(user_input: str, max_nodes: int = 12) -> Graph: + """A deterministic binary supplier tree rooted at the focal firm. + + Edges point supplier -> customer (toward the focal root). Leaves are raw-material + sources; the focal node is a pure sink. Incoming weights per customer sum to 1.0. + """ + k = max(2, min(max_nodes, 7)) + focal = _focal_id(user_input) + names = [focal] + [f"{focal}-Supplier{i}" for i in range(1, k)] + + nodes = [ + Node(id=names[i], label=names[i], sector=f"sector-{i % 4}", tier=_tier(i)) + for i in range(k) + ] + + # children of `parent = (i-1)//2` -> parent. Group children per parent to split weight. + children: dict[int, list[int]] = {} + for i in range(1, k): + children.setdefault((i - 1) // 2, []).append(i) + + edges: list[Edge] = [] + s = _seed(user_input) + for parent, kids in children.items(): + share = round(1.0 / len(kids), 6) + for j, child in enumerate(kids): + conf = _CONF_CYCLE[(s + child) % 3] + edges.append( + Edge( + source=names[child], + target=names[parent], + weight=share, + confidence=conf, # type: ignore[arg-type] + citation=f"{focal} 10-K p.{10 + child}", + reasoning=f"{names[child]} supplies {names[parent]} (fake-derived).", + ) + ) + + return Graph( + nodes=nodes, + edges=edges, + focal_node=focal, + extraction_model="fake/deterministic-v1", + extraction_timestamp=_FIXED_TS, + ) + + +def _tier(i: int) -> int: + t = 0 + while i > 0: + i = (i - 1) // 2 + t += 1 + return t + + +class FakeLLMProvider: + name = "fake/deterministic-v1" + + def __init__(self, max_nodes: int = 12): + self._max_nodes = max_nodes + + async def generate_structured(self, system, user, schema, temperature=0.2, **_): + if schema is Graph: + return build_fake_graph(user, max_nodes=self._max_nodes) + # Generic fallback: instantiate the schema from a deterministic empty-ish payload. + return _instantiate_minimal(schema) + + async def generate_text(self, system, user, temperature=0.4, **_) -> str: + focal = _focal_id(user) if user else "the network" + return ( + f"Critical finding. The most dangerous node in {focal}'s dependency network is its " + f"deepest single-sourced supplier: its failure cascades upward to the focal firm with " + f"the shortest expected propagation time, making it the structural weak point.\n\n" + f"Top three risk nodes (by impact score). The ranking is dominated by upstream " + f"single-source suppliers that feed multiple downstream functions; each one, if " + f"disrupted, propagates to every customer above it in the tree.\n\n" + f"Recommendations. Dual-source the top-ranked supplier to roughly double its local " + f"EPT; add buffer inventory for tier-2 sources; and monitor the top-3 nodes " + f"continuously. (This report was produced by the deterministic fake provider.)" + ) + + async def health(self) -> bool: + return True + + +class FakeSearchProvider: + name = "fake/search-v1" + + async def search(self, query, max_results=5, include_domains=None, recency_days=None): + s = _seed(query) + return [ + SearchResult( + title=f"Fake result {i + 1} for {query[:40]}", + url=f"https://example.com/{(s + i) % 100000}", + snippet=f"Deterministic snippet {i + 1} about {query[:60]}.", + score=round(1.0 - i * 0.1, 3), + ) + for i in range(min(max_results, 3)) + ] + + async def fetch(self, url: str) -> str: + return f"Fake fetched content for {url}." + + +def _instantiate_minimal(schema: type[BaseModel]) -> BaseModel: + # Best-effort: rely on defaults; only used by non-Graph callers, which the pipeline + # does not currently exercise. + return schema.model_construct() diff --git a/packages/providers/zeroforce_providers/gemini.py b/packages/providers/zeroforce_providers/gemini.py new file mode 100644 index 0000000..4a656dc --- /dev/null +++ b/packages/providers/zeroforce_providers/gemini.py @@ -0,0 +1,117 @@ +"""Prod LLM provider: Gemini 2.5 Pro via Vertex, TWO-PASS (spec §6.4). + +Pass A grounds (GoogleSearch tool, NO schema) -> prose + citation metadata. +Pass B structures (response_schema, NO tools) -> typed object. + +Schema + google_search in one call is an explicit incompatibility +(`400 INVALID_ARGUMENT: controlled generation is not supported with google_search tool`), +hence two passes. + +Pass B failure mode (spec §6.4 correction): Pass B retries up to 3x with temperature +annealing toward 0; on persistent failure it falls back to a schema-less call whose +free-form JSON is parsed and schema-validated here (standing in for the validator service). +""" + +from __future__ import annotations + +import json +import os + +_STRUCTURE_RETRIES = 3 + + +class GeminiProvider: + name = "gemini-2.5-pro" + + def __init__(self, model: str = "gemini-2.5-pro"): + from google import genai # lazy: optional dep + + self.model = model + self._genai = genai + self.client = genai.Client( + vertexai=True, + project=os.environ["GCP_PROJECT"], + location=os.environ.get("GCP_REGION", "us-central1"), + ) + self.last_citations: list[dict] = [] + + def _cfg(self, **kwargs): + from google.genai.types import GenerateContentConfig + + return GenerateContentConfig(**kwargs) + + async def _ground(self, system: str, user: str, temperature: float) -> tuple[str, list[dict]]: + from google.genai.types import GoogleSearch, Tool + + cfg = self._cfg( + system_instruction=system, + temperature=temperature, + tools=[Tool(google_search=GoogleSearch())], + ) + resp = await self.client.aio.models.generate_content( + model=self.model, contents=user, config=cfg + ) + return resp.text or "", _extract_grounding_metadata(resp) + + async def generate_structured(self, system, user, schema, temperature=0.2, max_output_tokens=4096): + prose, citations = await self._ground(system, user, temperature) + self.last_citations = citations + contents = f"Grounded findings:\n{prose}\n\nReturn the structured schema." + + # Pass B with temperature annealing toward 0. + last_err: Exception | None = None + for attempt in range(_STRUCTURE_RETRIES): + temp = max(0.0, temperature * (1.0 - attempt / max(1, _STRUCTURE_RETRIES - 1))) + try: + cfg = self._cfg( + system_instruction=system, + temperature=temp, + response_mime_type="application/json", + response_schema=schema, + max_output_tokens=max_output_tokens, + ) + resp = await self.client.aio.models.generate_content( + model=self.model, contents=contents, config=cfg + ) + return schema.model_validate_json(resp.text) + except Exception as e: # noqa: BLE001 - retry any structuring failure + last_err = e + + # Fallback: schema-less call, parse + validate here. + cfg = self._cfg(system_instruction=system, temperature=0.0, response_mime_type="application/json") + resp = await self.client.aio.models.generate_content( + model=self.model, contents=contents, config=cfg + ) + try: + return schema.model_validate(json.loads(resp.text)) + except Exception as e: # noqa: BLE001 + raise RuntimeError(f"Gemini Pass B failed after retries and fallback: {last_err}") from e + + async def generate_text(self, system, user, temperature=0.4, max_output_tokens=1024) -> str: + cfg = self._cfg(system_instruction=system, temperature=temperature, max_output_tokens=max_output_tokens) + resp = await self.client.aio.models.generate_content( + model=self.model, contents=user, config=cfg + ) + return resp.text or "" + + async def health(self) -> bool: + try: + await self.client.aio.models.generate_content(model=self.model, contents="ping") + return True + except Exception: + return False + + +def _extract_grounding_metadata(resp) -> list[dict]: + """Pull cited URLs out of Gemini grounding metadata for the citations panel.""" + out: list[dict] = [] + try: + for cand in resp.candidates or []: + gm = getattr(cand, "grounding_metadata", None) + for chunk in getattr(gm, "grounding_chunks", []) or []: + web = getattr(chunk, "web", None) + if web is not None: + out.append({"url": getattr(web, "uri", ""), "title": getattr(web, "title", "")}) + except Exception: + pass + return out diff --git a/packages/providers/zeroforce_providers/groq.py b/packages/providers/zeroforce_providers/groq.py new file mode 100644 index 0000000..2720e15 --- /dev/null +++ b/packages/providers/zeroforce_providers/groq.py @@ -0,0 +1,53 @@ +"""Dev LLM provider: Groq gpt-oss-120b via the OpenAI-compatible endpoint (spec §6.5).""" + +from __future__ import annotations + +import os + + +class GroqProvider: + name = "groq/gpt-oss-120b" + + def __init__(self, model: str = "openai/gpt-oss-120b"): + from openai import AsyncOpenAI # lazy: optional dep + + self.model = model + self.client = AsyncOpenAI( + base_url="https://api.groq.com/openai/v1", + api_key=os.environ["GROQ_API_KEY"], + ) + + async def generate_structured(self, system, user, schema, temperature=0.2, max_output_tokens=4096): + resp = await self.client.chat.completions.create( + model=self.model, + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": user}, + ], + temperature=temperature, + max_tokens=max_output_tokens, + response_format={ + "type": "json_schema", + "json_schema": {"name": schema.__name__, "schema": schema.model_json_schema()}, + }, + ) + return schema.model_validate_json(resp.choices[0].message.content) + + async def generate_text(self, system, user, temperature=0.4, max_output_tokens=1024) -> str: + resp = await self.client.chat.completions.create( + model=self.model, + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": user}, + ], + temperature=temperature, + max_tokens=max_output_tokens, + ) + return resp.choices[0].message.content or "" + + async def health(self) -> bool: + try: + await self.client.models.list() + return True + except Exception: + return False diff --git a/packages/providers/zeroforce_providers/tavily.py b/packages/providers/zeroforce_providers/tavily.py new file mode 100644 index 0000000..de3f677 --- /dev/null +++ b/packages/providers/zeroforce_providers/tavily.py @@ -0,0 +1,36 @@ +"""Dev search provider: Tavily (spec §6.5).""" + +from __future__ import annotations + +import os + +from .base import SearchResult + + +class TavilyProvider: + name = "tavily" + + def __init__(self): + from tavily import AsyncTavilyClient # lazy: optional dep + + self.client = AsyncTavilyClient(api_key=os.environ["TAVILY_API_KEY"]) + + async def search(self, query, max_results=5, include_domains=None, recency_days=None): + resp = await self.client.search( + query=query, + search_depth="advanced", + max_results=max_results, + include_domains=include_domains or [], + days=recency_days, + include_answer=False, + include_raw_content=False, + ) + return [ + SearchResult(title=r["title"], url=r["url"], snippet=r["content"], score=r.get("score")) + for r in resp["results"] + ] + + async def fetch(self, url: str) -> str: + resp = await self.client.extract(urls=[url]) + results = resp.get("results", []) + return results[0]["raw_content"] if results else "" diff --git a/packages/providers/zeroforce_providers/vertex.py b/packages/providers/zeroforce_providers/vertex.py new file mode 100644 index 0000000..72f4ee2 --- /dev/null +++ b/packages/providers/zeroforce_providers/vertex.py @@ -0,0 +1,28 @@ +"""Prod search provider: Vertex grounding (spec §6.4). + +Grounding happens *inside* Gemini Pass A, so this issues no independent search query. It +exists only to expose Pass-A grounding metadata (cited URLs) to the citations panel. +""" + +from __future__ import annotations + +from .base import SearchResult + + +class VertexGroundingProvider: + name = "vertex-grounding" + + def __init__(self, gemini=None): + # Optionally linked to a GeminiProvider to surface its last_citations. + self._gemini = gemini + + async def search(self, query, max_results=5, include_domains=None, recency_days=None) -> list[SearchResult]: + # No independent search in prod; grounding is bundled into Gemini Pass A. + return [] + + @property + def citations(self) -> list[dict]: + return list(getattr(self._gemini, "last_citations", []) or []) + + async def fetch(self, url: str) -> str: # pragma: no cover - not used in prod path + raise NotImplementedError("Vertex grounding does not fetch URLs independently.") diff --git a/packages/schemas/pyproject.toml b/packages/schemas/pyproject.toml new file mode 100644 index 0000000..7ed1747 --- /dev/null +++ b/packages/schemas/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "zeroforce-schemas" +version = "0.0.1" +description = "Shared Pydantic API models for zeroforce (engine types re-exported)." +requires-python = ">=3.12,<3.13" +dependencies = ["pydantic>=2.7,<3.0", "zeroforce"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["zeroforce_schemas"] diff --git a/packages/schemas/zeroforce_schemas/__init__.py b/packages/schemas/zeroforce_schemas/__init__.py new file mode 100644 index 0000000..a784a59 --- /dev/null +++ b/packages/schemas/zeroforce_schemas/__init__.py @@ -0,0 +1,46 @@ +"""Shared API models for zeroforce. Engine types are re-exported from zeroforce.types.""" + +from zeroforce.types import ( + Edge, + EngineInvariantError, + Graph, + Node, + NodeAnalysis, +) + +from .models import ( + AnalysisResult, + AnalyzeMode, + AnalyzeRequest, + ConfidenceLevel, + JobError, + JobRecord, + JobStatus, + SimulateRequest, + SimulateResponse, + SimulateRound, + WhatIfModification, + WhatIfRequest, + WhatIfResponse, +) + +__all__ = [ + "Edge", + "EngineInvariantError", + "Graph", + "Node", + "NodeAnalysis", + "AnalysisResult", + "AnalyzeMode", + "AnalyzeRequest", + "ConfidenceLevel", + "JobError", + "JobRecord", + "JobStatus", + "SimulateRequest", + "SimulateResponse", + "SimulateRound", + "WhatIfModification", + "WhatIfRequest", + "WhatIfResponse", +] diff --git a/packages/schemas/zeroforce_schemas/models.py b/packages/schemas/zeroforce_schemas/models.py new file mode 100644 index 0000000..3885586 --- /dev/null +++ b/packages/schemas/zeroforce_schemas/models.py @@ -0,0 +1,99 @@ +"""API request/response models (spec §7.1, §9).""" + +from __future__ import annotations + +from datetime import datetime +from enum import StrEnum +from typing import Literal + +from pydantic import BaseModel, Field + +from zeroforce.types import Graph, NodeAnalysis + +AnalyzeMode = Literal["company", "sector", "product"] +ConfidenceLevel = Literal["high", "medium", "low"] + + +class JobStatus(StrEnum): + """Async job lifecycle (spec §9.2 polling contract).""" + + PENDING = "pending" + RUNNING = "running" + DONE = "done" + FAILED = "failed" + + +class AnalyzeRequest(BaseModel): + input: str = Field(min_length=1, max_length=5000) + mode: AnalyzeMode = "company" + max_nodes: int = Field(default=12, ge=2, le=18) + confidence_threshold: ConfidenceLevel = "low" + enable_validation_pass: bool = True + use_cached: bool = True + wait: bool = False # sync 200 only honored when wait AND max_nodes <= 10 + + +class AnalysisResult(BaseModel): + id: str + graph: Graph + node_analyses: list[NodeAnalysis] + risk_ranking: list[str] # cascading node ids by impact_score desc + non_cascading_nodes: list[str] # pure sinks + executive_report: str + validation_notes: list[str] + provider_metadata: dict + created_at: datetime + + +class JobError(BaseModel): + code: str + message: str + + +class JobRecord(BaseModel): + id: str + status: JobStatus + stage: str | None = None + error: JobError | None = None + result: AnalysisResult | None = None # present only when status == DONE + created_at: datetime + updated_at: datetime + + +class SimulateRequest(BaseModel): + analysis_id: str + starting_nodes: list[str] + rounds: int = Field(default=10, ge=1, le=100) + samples: int = Field(default=1000, ge=1, le=1_000_000) + + +class SimulateRound(BaseModel): + round: int + disrupted: list[str] + node_probabilities: dict[str, float] + + +class SimulateResponse(BaseModel): + rounds: list[SimulateRound] + expected_ept: float + p95_rounds: int + p99_rounds: int + + +class WhatIfModification(BaseModel): + # one of: modify existing edge weight, or add a new edge + edge: dict | None = None # {"source","target"} for weight change + new_weight: float | None = Field(default=None, ge=0, le=1) + add_edge: dict | None = None # {"source","target","weight"} + + +class WhatIfRequest(BaseModel): + analysis_id: str + modifications: list[WhatIfModification] + + +class WhatIfResponse(BaseModel): + baseline_ept: dict[str, float] + modified_ept: dict[str, float] + delta: dict[str, float] + narration: str diff --git a/packages/zeroforce/zeroforce/api.py b/packages/zeroforce/zeroforce/api.py index cf74cd0..10cb55d 100644 --- a/packages/zeroforce/zeroforce/api.py +++ b/packages/zeroforce/zeroforce/api.py @@ -4,7 +4,6 @@ import numpy as np -from .engine_py import compute_ept_from_seed from .ranking import impact_score, rank_and_percentile from .reachability import induced_subgraph, reachable_from, to_weighted_digraph from .types import EngineInvariantError, Graph, NodeAnalysis @@ -12,6 +11,22 @@ _EPS = 1e-9 +def _select_engine(): + """Prefer the numba JIT path; fall back to the pure-Python reference if numba is + unavailable. Both implement the identical recurrence (engine-parity tested).""" + try: + from .engine_numba import compute_ept_from_seed as fn + + return fn + except Exception: # pragma: no cover - numba present in all supported envs + from .engine_py import compute_ept_from_seed as fn + + return fn + + +compute_ept_from_seed = _select_engine() + + def _assert_full_graph_invariant(in_w: np.ndarray, node_ids: tuple[str, ...]) -> None: """§7.2 invariant: every node's incoming weights sum to 1.0, or 0.0 for source nodes. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..505cfbb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "zeroforce-monorepo" +version = "0.0.1" +description = "zeroforce monorepo (uv workspace root)." +requires-python = ">=3.12,<3.13" +dependencies = [] + +[tool.uv.workspace] +members = ["packages/*", "services/*"] + +[tool.uv.sources] +zeroforce = { workspace = true } +zeroforce-schemas = { workspace = true } +zeroforce-providers = { workspace = true } +zeroforce-backend = { workspace = true } + +[dependency-groups] +dev = ["pytest>=8", "pytest-asyncio>=0.24", "pytest-xdist", "hypothesis>=6", "httpx>=0.27"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["packages", "services", "tests"] diff --git a/services/backend/pyproject.toml b/services/backend/pyproject.toml new file mode 100644 index 0000000..ce3c230 --- /dev/null +++ b/services/backend/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "zeroforce-backend" +version = "0.0.1" +description = "zeroforce backend: 6 service cores + FastAPI apps + orchestrator." +requires-python = ">=3.12,<3.13" +dependencies = [ + "zeroforce", + "zeroforce-schemas", + "zeroforce-providers", + "fastapi>=0.110", + "uvicorn>=0.29", + "aiosqlite>=0.20", + "python-multipart>=0.0.9", + "numpy>=1.26,<3.0", +] + +[project.optional-dependencies] +pdf = ["pypdf>=4.0"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["zeroforce_backend"] diff --git a/services/backend/tests/conftest.py b/services/backend/tests/conftest.py new file mode 100644 index 0000000..ab4634c --- /dev/null +++ b/services/backend/tests/conftest.py @@ -0,0 +1,23 @@ +"""Backend test fixtures: fake-mode app with an in-memory repo.""" + +from __future__ import annotations + +import pytest +from fastapi.testclient import TestClient + +from zeroforce_backend.config import Settings +from zeroforce_backend.gateway import build_context, create_app +from zeroforce_backend.storage import InMemoryRepository + + +@pytest.fixture +def ctx(): + settings = Settings(mode="fake", db_path=":memory:") + return build_context(settings=settings, repo=InMemoryRepository()) + + +@pytest.fixture +def client(ctx): + app = create_app(ctx) + with TestClient(app) as c: + yield c diff --git a/services/backend/tests/test_hardening.py b/services/backend/tests/test_hardening.py new file mode 100644 index 0000000..688fdbd --- /dev/null +++ b/services/backend/tests/test_hardening.py @@ -0,0 +1,60 @@ +"""Phase 4 hardening: auth, PII redaction, and graceful provider-failure (chaos).""" + +from __future__ import annotations + +import io + +from fastapi.testclient import TestClient + +from zeroforce_backend.config import Settings +from zeroforce_backend.gateway import build_context, create_app +from zeroforce_backend.orchestrator import Orchestrator +from zeroforce_backend.service_client import InProcessServiceClient +from zeroforce_backend.storage import InMemoryRepository +from zeroforce_providers import FakeSearchProvider + + +def test_auth_required_when_token_configured(): + ctx = build_context(Settings(mode="fake", db_path=":memory:", api_token="secret"), + InMemoryRepository()) + with TestClient(create_app(ctx)) as c: + assert c.get("/api/v1/analyses/whatever").status_code == 401 + ok = c.get("/api/v1/analyses/whatever", headers={"Authorization": "Bearer secret"}) + assert ok.status_code == 404 # authed, but id not found + + +def test_upload_redacts_pii(): + ctx = build_context(Settings(mode="fake", db_path=":memory:"), InMemoryRepository()) + with TestClient(create_app(ctx)) as c: + content = b"Contact SSN 123-45-6789 and card 4111 1111 1111 1111 in this doc." + r = c.post("/api/v1/upload", files={"file": ("doc.txt", io.BytesIO(content), "text/plain")}) + assert r.status_code == 200, r.text + body = r.json() + assert body["pii_redacted"] >= 2 + assert "123-45-6789" not in body["text"] + + +class _FailingLLM: + name = "failing" + + async def generate_structured(self, *a, **k): + raise RuntimeError("simulated provider outage") + + async def generate_text(self, *a, **k): + raise RuntimeError("simulated provider outage") + + async def health(self): + return False + + +def test_provider_outage_fails_job_gracefully(): + ctx = build_context(Settings(mode="fake", db_path=":memory:"), InMemoryRepository()) + # Inject a failing extractor LLM (chaos): the pipeline must fail cleanly, not hang/crash. + ctx.orch = Orchestrator( + ctx.settings, ctx.repo, + InProcessServiceClient("fake", _FailingLLM(), FakeSearchProvider()), + ) + with TestClient(create_app(ctx)) as c: + r = c.post("/api/v1/analyze", json={"input": "x", "max_nodes": 7, "wait": True}) + assert r.status_code == 502 + assert r.json()["detail"]["code"] == "llm_provider_error" diff --git a/services/backend/tests/test_pipeline_integration.py b/services/backend/tests/test_pipeline_integration.py new file mode 100644 index 0000000..fb9d373 --- /dev/null +++ b/services/backend/tests/test_pipeline_integration.py @@ -0,0 +1,76 @@ +"""End-to-end pipeline integration (fake mode, in-memory) through the gateway API.""" + +from __future__ import annotations + +import time + + +def test_version_and_health(client): + v = client.get("/api/v1/version").json() + assert v["mode"] == "fake" + assert v["llm_model"].startswith("fake") + assert client.get("/api/v1/health").json()["status"] == "ok" + + +def test_sync_analyze_returns_full_result(client): + r = client.post("/api/v1/analyze", json={"input": "Apple semiconductor supply chain", + "max_nodes": 7, "wait": True}) + assert r.status_code == 200, r.text + body = r.json() + assert body["id"] + assert len(body["graph"]["nodes"]) >= 2 + assert body["risk_ranking"], "expected at least one cascading node" + assert body["executive_report"] + assert body["graph"]["focal_node"] == "Apple" + # focal node is a pure sink in the fake tree -> non-cascading + assert "Apple" in body["non_cascading_nodes"] + + +def test_async_analyze_then_poll(client): + r = client.post("/api/v1/analyze", json={"input": "Tesla battery supply chain", "max_nodes": 7}) + assert r.status_code == 202, r.text + job = r.json() + assert job["status"] in ("pending", "running", "done") + poll = job["poll"] + + deadline = time.time() + 10 + status = None + while time.time() < deadline: + s = client.get(poll).json() + status = s["status"] + if status in ("done", "failed"): + break + time.sleep(0.1) + assert status == "done", f"job did not finish: {status}" + done = client.get(poll).json() + assert done["result"]["executive_report"] + + +def test_simulate_and_whatif(client): + r = client.post("/api/v1/analyze", json={"input": "Ford engine supply chain", + "max_nodes": 7, "wait": True}) + body = r.json() + aid = body["id"] + # pick a cascading (dangerous) node as the disruption seed + seed = body["risk_ranking"][0] + + sim = client.post("/api/v1/simulate", json={"analysis_id": aid, "starting_nodes": [seed], + "rounds": 5, "samples": 500}) + assert sim.status_code == 200, sim.text + sd = sim.json() + assert sd["expected_ept"] >= 0 + assert sd["rounds"][0]["round"] == 0 + + # what-if: modify an existing edge weight + edge = body["graph"]["edges"][0] + wi = client.post("/api/v1/whatif", json={ + "analysis_id": aid, + "modifications": [{"edge": {"source": edge["source"], "target": edge["target"]}, "new_weight": 0.2}], + }) + assert wi.status_code == 200, wi.text + assert "delta" in wi.json() + + +def test_too_many_nodes_rejected(client): + r = client.post("/api/v1/analyze", json={"input": "x", "max_nodes": 19, "wait": True}) + assert r.status_code == 422 # pydantic le=18 bound rejects before handler diff --git a/services/backend/tests/test_validator.py b/services/backend/tests/test_validator.py new file mode 100644 index 0000000..66685d9 --- /dev/null +++ b/services/backend/tests/test_validator.py @@ -0,0 +1,48 @@ +"""Validator normalization invariant tests (spec §7.2).""" + +from __future__ import annotations + +import pytest + +from zeroforce.types import Edge, Graph, Node +from zeroforce_backend.validator import NormalizationError, validate_and_normalize + + +def _graph(edges): + ids = {e.source for e in edges} | {e.target for e in edges} + return Graph( + nodes=[Node(id=i, label=i, sector="s") for i in sorted(ids)], + edges=edges, + focal_node=sorted(ids)[0], + extraction_model="t", + extraction_timestamp="2026-01-01T00:00:00Z", + ) + + +def test_scales_up_underweight_incoming_and_notes_it(): + g = _graph([ + Edge(source="A", target="C", weight=0.3, confidence="high"), + Edge(source="B", target="C", weight=0.5, confidence="high"), # sums to 0.8 + ]) + out, notes = validate_and_normalize(g) + inc = sum(e.weight for e in out.edges if e.target == "C") + assert abs(inc - 1.0) < 1e-9 + assert any("normalized from 0.80" in n for n in notes) + + +def test_rejects_grossly_overweight_incoming(): + g = _graph([ + Edge(source="A", target="C", weight=0.7, confidence="high"), + Edge(source="B", target="C", weight=0.6, confidence="high"), # 1.3 > 1.05 + ]) + with pytest.raises(NormalizationError): + validate_and_normalize(g) + + +def test_low_confidence_edges_flagged_but_retained(): + g = _graph([ + Edge(source="A", target="C", weight=1.0, confidence="low"), + ]) + out, notes = validate_and_normalize(g, confidence_threshold="high") + assert len(out.edges) == 1 # retained + assert any("below threshold" in n for n in notes) diff --git a/services/backend/zeroforce_backend/__init__.py b/services/backend/zeroforce_backend/__init__.py new file mode 100644 index 0000000..ead8805 --- /dev/null +++ b/services/backend/zeroforce_backend/__init__.py @@ -0,0 +1 @@ +"""zeroforce backend services.""" diff --git a/services/backend/zeroforce_backend/asgi.py b/services/backend/zeroforce_backend/asgi.py new file mode 100644 index 0000000..96d5689 --- /dev/null +++ b/services/backend/zeroforce_backend/asgi.py @@ -0,0 +1,5 @@ +"""ASGI entrypoint: `uvicorn zeroforce_backend.asgi:app`.""" + +from .gateway import create_app + +app = create_app() diff --git a/services/backend/zeroforce_backend/cache.py b/services/backend/zeroforce_backend/cache.py new file mode 100644 index 0000000..17c460d --- /dev/null +++ b/services/backend/zeroforce_backend/cache.py @@ -0,0 +1,44 @@ +"""Extraction cache (spec §5.3): in-memory dict (dev) / Redis (prod). 24h TTL.""" + +from __future__ import annotations + +import time +from typing import Protocol + + +class Cache(Protocol): + async def get(self, key: str) -> str | None: ... + async def set(self, key: str, value: str, ttl_seconds: int = 86_400) -> None: ... + + +class InMemoryCache: + def __init__(self) -> None: + self._store: dict[str, tuple[float, str]] = {} + + async def get(self, key: str) -> str | None: + item = self._store.get(key) + if item is None: + return None + expires, value = item + if expires < time.time(): + self._store.pop(key, None) + return None + return value + + async def set(self, key: str, value: str, ttl_seconds: int = 86_400) -> None: + self._store[key] = (time.time() + ttl_seconds, value) + + +class RedisCache: + """Prod cache backed by Memorystore Redis. Untested without a Redis instance.""" + + def __init__(self, url: str): + import redis.asyncio as redis # lazy: optional prod dep + + self._client = redis.from_url(url, decode_responses=True) + + async def get(self, key: str) -> str | None: + return await self._client.get(key) + + async def set(self, key: str, value: str, ttl_seconds: int = 86_400) -> None: + await self._client.set(key, value, ex=ttl_seconds) diff --git a/services/backend/zeroforce_backend/config.py b/services/backend/zeroforce_backend/config.py new file mode 100644 index 0000000..8b6e8f7 --- /dev/null +++ b/services/backend/zeroforce_backend/config.py @@ -0,0 +1,27 @@ +"""Backend configuration (env-driven; mirrors spec §5.4, §6.6).""" + +from __future__ import annotations + +import os +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Settings: + mode: str # fake | dev | prod + db_path: str # SQLite file (dev) — Firestore in prod is selected separately + default_max_nodes: int = 12 + hard_max_nodes: int = 18 + sync_max_nodes: int = 10 # sync 200 only allowed at or below this + job_timeout_seconds: int = 300 # 5-minute hard job cap (spec §9.2) + api_token: str | None = None # dev: single bearer token; None => auth open (fake/dev) + analysis_ttl_days: int = 30 + extraction_ttl_hours: int = 24 + + +def load_settings() -> Settings: + return Settings( + mode=os.environ.get("ZEROFORCE_MODE", "fake").lower(), + db_path=os.environ.get("ZEROFORCE_DB", "./data/zeroforce.db"), + api_token=os.environ.get("ZEROFORCE_API_TOKEN") or None, + ) diff --git a/services/backend/zeroforce_backend/engine/__init__.py b/services/backend/zeroforce_backend/engine/__init__.py new file mode 100644 index 0000000..f44b2eb --- /dev/null +++ b/services/backend/zeroforce_backend/engine/__init__.py @@ -0,0 +1,3 @@ +from .core import analyze_graph, run_simulate, run_whatif + +__all__ = ["analyze_graph", "run_simulate", "run_whatif"] diff --git a/services/backend/zeroforce_backend/engine/core.py b/services/backend/zeroforce_backend/engine/core.py new file mode 100644 index 0000000..14d2680 --- /dev/null +++ b/services/backend/zeroforce_backend/engine/core.py @@ -0,0 +1,107 @@ +"""Engine service core — wraps packages/zeroforce for analyze / simulate / what-if.""" + +from __future__ import annotations + +import numpy as np + +from zeroforce.api import compute +from zeroforce.reachability import reachable_from, to_weighted_digraph +from zeroforce.types import Graph, NodeAnalysis +from zeroforce_schemas import SimulateResponse, SimulateRound + + +def analyze_graph(graph: Graph) -> tuple[list[NodeAnalysis], list[str], list[str]]: + """Return (node_analyses, risk_ranking, non_cascading_node_ids).""" + analyses = compute(graph) + cascading = sorted( + (a for a in analyses if a.impact_score is not None), + key=lambda a: a.rank if a.rank is not None else 1_000_000, + ) + risk_ranking = [a.node_id for a in cascading] + non_cascading = [a.node_id for a in analyses if a.impact_score is None] + return analyses, risk_ranking, non_cascading + + +def run_simulate( + graph: Graph, starting_nodes: list[str], rounds: int, samples: int, seed: int = 0 +) -> SimulateResponse: + g = to_weighted_digraph(graph) + idx = {nid: i for i, nid in enumerate(g.node_ids)} + starts = [idx[s] for s in starting_nodes if s in idx] + if not starts: + raise ValueError("no valid starting nodes") + + n = g.num_nodes + reachable = np.zeros(n, dtype=bool) + for s in starts: + for r in reachable_from(g, s): + reachable[r] = True + + rng = np.random.default_rng(seed) + disrupted = np.zeros((samples, n), dtype=bool) + disrupted[:, starts] = True + completion = np.full(samples, -1, dtype=np.int64) + in_wT = g.in_w.T.copy() + + rounds_payload: list[SimulateRound] = [] + max_round = max(rounds, 1000) + for r in range(max_round + 1): + if r <= rounds: + probs = disrupted.mean(axis=0) + rounds_payload.append( + SimulateRound( + round=r, + disrupted=[g.node_ids[v] for v in range(n) if probs[v] >= 0.5], + node_probabilities={g.node_ids[v]: float(round(probs[v], 4)) for v in range(n)}, + ) + ) + done = disrupted[:, reachable].all(axis=1) + completion[done & (completion < 0)] = r + if (completion >= 0).all(): + break + p = disrupted.astype(np.float64) @ in_wT + flips = (~disrupted) & (rng.random((samples, n)) < p) & reachable + disrupted |= flips + + completion[completion < 0] = max_round # safety; reachable graphs complete well before + return SimulateResponse( + rounds=rounds_payload, + expected_ept=float(completion.mean()), + p95_rounds=int(np.percentile(completion, 95)), + p99_rounds=int(np.percentile(completion, 99)), + ) + + +def run_whatif( + graph: Graph, modifications: list[dict] +) -> tuple[dict[str, float], dict[str, float], dict[str, float], list[str]]: + """Apply edge modifications, re-run, return (baseline_ept, modified_ept, delta, touched_targets).""" + from ..validator import validate_and_normalize + + baseline = {a.node_id: a.ept for a in compute(graph)} + + modified = graph.model_copy(deep=True) + edges = list(modified.edges) + touched: set[str] = set() + for mod in modifications: + if mod.get("edge") and mod.get("new_weight") is not None: + src, tgt = mod["edge"]["source"], mod["edge"]["target"] + for e in edges: + if e.source == src and e.target == tgt: + e.weight = float(mod["new_weight"]) + touched.add(tgt) + if mod.get("add_edge"): + ae = mod["add_edge"] + from zeroforce.types import Edge + + edges.append( + Edge(source=ae["source"], target=ae["target"], weight=float(ae["weight"]), + confidence="medium", reasoning="what-if added edge") + ) + touched.add(ae["target"]) + modified = modified.model_copy(update={"edges": edges}) + modified, _notes = validate_and_normalize(modified) # re-establish §7.2 on touched targets + + modified_ept = {a.node_id: a.ept for a in compute(modified)} + delta = {nid: round(modified_ept.get(nid, 0.0) - baseline.get(nid, 0.0), 6) for nid in baseline} + return baseline, modified_ept, delta, sorted(touched) diff --git a/services/backend/zeroforce_backend/extractor/__init__.py b/services/backend/zeroforce_backend/extractor/__init__.py new file mode 100644 index 0000000..b389a8d --- /dev/null +++ b/services/backend/zeroforce_backend/extractor/__init__.py @@ -0,0 +1,3 @@ +from .core import extract_graph + +__all__ = ["extract_graph"] diff --git a/services/backend/zeroforce_backend/extractor/core.py b/services/backend/zeroforce_backend/extractor/core.py new file mode 100644 index 0000000..51b392b --- /dev/null +++ b/services/backend/zeroforce_backend/extractor/core.py @@ -0,0 +1,33 @@ +"""Extractor service core — Prompt 1 graph extraction with grounding (spec §3.3 step 4, §8.1). + +- fake: one structured call (the fake provider builds the graph; search ignored). +- dev: one Tavily search, snippets concatenated into the prompt, then one Groq structured call. +- prod: one Gemini call whose two-pass grounding happens inside the provider. + +Shape is identical across modes (ground -> structure); only the mechanism differs. +""" + +from __future__ import annotations + +from zeroforce.types import Graph +from zeroforce_providers import LLMProvider, SearchProvider + +from ..prompts import EXTRACTION_SYSTEM, extraction_user + + +async def extract_graph( + user_input: str, + *, + mode: str, + max_nodes: int, + llm: LLMProvider, + search: SearchProvider, +) -> Graph: + grounding = "" + if mode == "dev": + results = await search.search(f"{user_input} suppliers supply chain 10-K", max_results=5) + grounding = "\n".join(f"- {r.title}: {r.snippet} ({r.url})" for r in results) + + user = extraction_user(user_input, max_nodes=max_nodes, grounding=grounding) + graph = await llm.generate_structured(EXTRACTION_SYSTEM, user, Graph, temperature=0.2) + return graph diff --git a/services/backend/zeroforce_backend/gateway/__init__.py b/services/backend/zeroforce_backend/gateway/__init__.py new file mode 100644 index 0000000..207c79b --- /dev/null +++ b/services/backend/zeroforce_backend/gateway/__init__.py @@ -0,0 +1,3 @@ +from .app import build_context, create_app + +__all__ = ["build_context", "create_app"] diff --git a/services/backend/zeroforce_backend/gateway/app.py b/services/backend/zeroforce_backend/gateway/app.py new file mode 100644 index 0000000..65e1931 --- /dev/null +++ b/services/backend/zeroforce_backend/gateway/app.py @@ -0,0 +1,241 @@ +"""Gateway FastAPI app — auth, routing, all /api/v1 endpoints (spec §9). + +In dev/fake this single app embeds the pipeline via InProcessServiceClient. In prod/compose +it can be pointed at the per-service apps via HTTPServiceClient (set ZEROFORCE_SERVICE_URLS). +""" + +from __future__ import annotations + +import json +import re +from contextlib import asynccontextmanager +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path + +from fastapi import Depends, FastAPI, Header, HTTPException, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse + +from zeroforce.types import Graph +from zeroforce_providers import make_llm, make_search +from zeroforce_schemas import ( + AnalysisResult, + AnalyzeRequest, + JobStatus, + SimulateRequest, + WhatIfRequest, +) + +from ..config import Settings, load_settings +from ..engine import run_simulate, run_whatif +from ..engine.core import analyze_graph +from ..orchestrator import Orchestrator, TooManyNodesError +from ..prompts import whatif_user +from ..service_client import InProcessServiceClient +from ..storage import InMemoryRepository, Repository, SQLiteRepository +from ..validator import validate_and_normalize + +VERSION = "0.1.0" +_PII = re.compile(r"\b(\d{3}-\d{2}-\d{4}|\d{4}[ -]?\d{4}[ -]?\d{4}[ -]?\d{4})\b") + + +@dataclass +class AppContext: + settings: Settings + repo: Repository + llm: object + search: object + orch: Orchestrator + + +def build_context(settings: Settings | None = None, repo: Repository | None = None) -> AppContext: + settings = settings or load_settings() + repo = repo or ( + InMemoryRepository() if settings.db_path == ":memory:" else SQLiteRepository(settings.db_path) + ) + llm = make_llm(settings.mode) # type: ignore[arg-type] + search = make_search(settings.mode) # type: ignore[arg-type] + + import json as _json + import os as _os + + urls_env = _os.environ.get("ZEROFORCE_SERVICE_URLS") + if urls_env: + from ..service_client import HTTPServiceClient + + client = HTTPServiceClient(_json.loads(urls_env), llm, search) + else: + client = InProcessServiceClient(settings.mode, llm, search) + + orch = Orchestrator(settings, repo, client) + return AppContext(settings=settings, repo=repo, llm=llm, search=search, orch=orch) + + +def create_app(ctx: AppContext | None = None) -> FastAPI: + ctx = ctx or build_context() + + @asynccontextmanager + async def lifespan(app: FastAPI): + await ctx.repo.init() + yield + + app = FastAPI(title="zeroforce", version=VERSION, lifespan=lifespan) + app.add_middleware( + CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"] + ) + from ..observability import setup_observability + + setup_observability(app) + app.state.ctx = ctx + + async def require_auth(authorization: str | None = Header(default=None)) -> None: + if ctx.settings.api_token is None: + return # open in fake/dev when no token configured + if authorization != f"Bearer {ctx.settings.api_token}": + raise HTTPException(status_code=401, detail="unauthorized") + + @app.get("/api/v1/health") + async def health(): + try: + ok = await ctx.llm.health() + except Exception: + ok = False + return {"status": "ok" if ok else "degraded", "mode": ctx.settings.mode} + + @app.get("/api/v1/version") + async def version(): + return { + "version": VERSION, + "mode": ctx.settings.mode, + "llm_model": getattr(ctx.llm, "name", "unknown"), + "search_provider": getattr(ctx.search, "name", "unknown"), + } + + @app.post("/api/v1/analyze") + async def analyze(req: AnalyzeRequest, _: None = Depends(require_auth)): + try: + job = await ctx.orch.submit(req) + except TooManyNodesError as e: + raise HTTPException(status_code=400, detail={"code": "too_many_nodes", "message": str(e)}) + + sync = req.wait and req.max_nodes <= ctx.settings.sync_max_nodes + if sync: + if job.status == JobStatus.DONE and job.result is not None: + return JSONResponse(status_code=200, content=json.loads(job.result.model_dump_json())) + code = job.error.code if job.error else "llm_provider_error" + raise HTTPException(status_code=_status_for(code), detail={"code": code, + "message": job.error.message if job.error else "pipeline failed"}) + + return JSONResponse( + status_code=202, + content={"id": job.id, "status": job.status.value, "poll": f"/api/v1/analyses/{job.id}"}, + ) + + @app.get("/api/v1/analyses/{analysis_id}") + async def get_analysis(analysis_id: str, _: None = Depends(require_auth)): + job = await ctx.orch.get(analysis_id) + if job is None: + raise HTTPException(status_code=404, detail={"code": "not_found", "message": analysis_id}) + return json.loads(job.model_dump_json(exclude_none=True)) + + @app.post("/api/v1/simulate") + async def simulate(req: SimulateRequest, _: None = Depends(require_auth)): + analysis = await ctx.repo.get_analysis(req.analysis_id) + if analysis is None: + raise HTTPException(status_code=404, detail={"code": "not_found", "message": req.analysis_id}) + try: + resp = run_simulate(analysis.graph, req.starting_nodes, req.rounds, req.samples) + except ValueError as e: + raise HTTPException(status_code=400, detail={"code": "invalid_input", "message": str(e)}) + return json.loads(resp.model_dump_json()) + + @app.post("/api/v1/whatif") + async def whatif(req: WhatIfRequest, _: None = Depends(require_auth)): + analysis = await ctx.repo.get_analysis(req.analysis_id) + if analysis is None: + raise HTTPException(status_code=404, detail={"code": "not_found", "message": req.analysis_id}) + mods = [m.model_dump() for m in req.modifications] + baseline, modified, delta, touched = run_whatif(analysis.graph, mods) + narration = await _narrate_whatif(ctx, analysis.graph, mods, baseline, modified, delta, touched) + return {"baseline_ept": baseline, "modified_ept": modified, "delta": delta, "narration": narration} + + @app.get("/api/v1/sectors") + async def sectors(): + path = _bea_path() + if not path.exists(): + raise HTTPException(status_code=404, detail={"code": "not_found", "message": "BEA bundle missing"}) + graph = Graph.model_validate_json(path.read_text()) + graph, notes = validate_and_normalize(graph) + analyses, ranking, non_casc = analyze_graph(graph) + return { + "graph": json.loads(graph.model_dump_json()), + "node_analyses": [json.loads(a.model_dump_json()) for a in analyses], + "risk_ranking": ranking, + "non_cascading_nodes": non_casc, + "validation_notes": notes, + } + + @app.post("/api/v1/upload") + async def upload(file: UploadFile, _: None = Depends(require_auth)): + raw = await file.read() + if len(raw) > 20 * 1024 * 1024: + raise HTTPException(status_code=400, detail={"code": "invalid_input", "message": "file > 20MB"}) + text = _extract_pdf_text(raw, file.filename or "") + redacted, n = _PII.subn("[REDACTED]", text) + return {"filename": file.filename, "chars": len(redacted), "pii_redacted": n, "text": redacted[:20000]} + + return app + + +def _status_for(code: str) -> int: + return { + "too_many_nodes": 400, + "invalid_input": 400, + "unreachable_graph": 400, + "normalization_failed": 422, + "rate_limited": 429, + "llm_provider_error": 502, + "computation_timeout": 504, + }.get(code, 502) + + +def _bea_path() -> Path: + return Path(__file__).resolve().parents[4] / "data" / "bea-15-sector.json" + + +def _extract_pdf_text(raw: bytes, filename: str) -> str: + try: + import io + + from pypdf import PdfReader + + reader = PdfReader(io.BytesIO(raw)) + text = "\n".join(page.extract_text() or "" for page in reader.pages) + if text.strip(): + return text + except Exception: + pass + # Not a PDF (or pypdf absent): decode as text so PII scanning still runs; else filename. + try: + return raw.decode("utf-8") + except UnicodeDecodeError: + return filename + + +async def _narrate_whatif(ctx, graph, mods, baseline, modified, delta, touched) -> str: + if not touched: + return "No edges were modified." + tgt = touched[0] + affected = sorted(delta, key=lambda k: abs(delta[k]), reverse=True)[:3] + src = "(various)" + new_w = "(varied)" + for m in mods: + if m.get("edge", {}) and m["edge"].get("target") == tgt: + src, new_w = m["edge"]["source"], m.get("new_weight") + user = whatif_user(src, tgt, "baseline", new_w, round(baseline.get(tgt, 0), 3), + round(modified.get(tgt, 0), 3), round(delta.get(tgt, 0), 3), affected) + try: + return await ctx.llm.generate_text("You are a supply chain analyst.", user) + except Exception: + return f"Modifying inputs to {tgt} changed its EPT by {round(delta.get(tgt, 0), 3)} rounds." diff --git a/services/backend/zeroforce_backend/observability.py b/services/backend/zeroforce_backend/observability.py new file mode 100644 index 0000000..82b38fa --- /dev/null +++ b/services/backend/zeroforce_backend/observability.py @@ -0,0 +1,54 @@ +"""Observability (spec §13): request timing, structured access logs, optional OTel tracing. + +Always-on: a middleware that logs one structured JSON line per request (latency, path, status) +and adds a Server-Timing header. Opt-in (ZEROFORCE_OTEL=1, otel packages installed): wires +OpenTelemetry FastAPI instrumentation with a console exporter (dev) — Cloud Trace in prod. +""" + +from __future__ import annotations + +import json +import logging +import os +import time + +from fastapi import FastAPI, Request + +logger = logging.getLogger("zeroforce.access") + + +def setup_observability(app: FastAPI) -> None: + @app.middleware("http") + async def _timing(request: Request, call_next): + start = time.perf_counter() + response = await call_next(request) + elapsed_ms = (time.perf_counter() - start) * 1000.0 + logger.info( + json.dumps({ + "method": request.method, + "path": request.url.path, + "status": response.status_code, + "ms": round(elapsed_ms, 2), + }) + ) + response.headers["Server-Timing"] = f"app;dur={elapsed_ms:.1f}" + return response + + if os.environ.get("ZEROFORCE_OTEL") == "1": + _try_setup_otel(app) + + +def _try_setup_otel(app: FastAPI) -> None: # pragma: no cover - optional, env-gated + try: + from opentelemetry import trace + from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter + + provider = TracerProvider() + provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) + trace.set_tracer_provider(provider) + FastAPIInstrumentor.instrument_app(app) + logger.info(json.dumps({"event": "otel_enabled"})) + except Exception as e: # otel not installed + logger.warning(json.dumps({"event": "otel_unavailable", "error": str(e)})) diff --git a/services/backend/zeroforce_backend/orchestrator/__init__.py b/services/backend/zeroforce_backend/orchestrator/__init__.py new file mode 100644 index 0000000..df34159 --- /dev/null +++ b/services/backend/zeroforce_backend/orchestrator/__init__.py @@ -0,0 +1,3 @@ +from .core import Orchestrator, TooManyNodesError, UnreachableGraphError + +__all__ = ["Orchestrator", "TooManyNodesError", "UnreachableGraphError"] diff --git a/services/backend/zeroforce_backend/orchestrator/core.py b/services/backend/zeroforce_backend/orchestrator/core.py new file mode 100644 index 0000000..58b2a07 --- /dev/null +++ b/services/backend/zeroforce_backend/orchestrator/core.py @@ -0,0 +1,151 @@ +"""Orchestrator service core — pipeline coordination + async job lifecycle (spec §3.3, §9.2).""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timezone + +from zeroforce_schemas import ( + AnalysisResult, + AnalyzeRequest, + JobError, + JobRecord, + JobStatus, +) + +from ..config import Settings +from ..service_client import ServiceClient +from ..storage import Repository, content_hash +from ..validator import NormalizationError + + +class TooManyNodesError(Exception): + pass + + +class UnreachableGraphError(Exception): + pass + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +class Orchestrator: + def __init__(self, settings: Settings, repo: Repository, client: ServiceClient): + self.settings = settings + self.repo = repo + self.client = client + self._tasks: set[asyncio.Task] = set() + + def _job_id(self, req: AnalyzeRequest) -> str: + return content_hash( + { + "input": req.input, + "mode": req.mode, + "max_nodes": req.max_nodes, + "confidence_threshold": req.confidence_threshold, + "enable_validation_pass": req.enable_validation_pass, + "engine": self.settings.mode, + } + ) + + async def submit(self, req: AnalyzeRequest) -> JobRecord: + if req.max_nodes > self.settings.hard_max_nodes: + raise TooManyNodesError(f"max_nodes {req.max_nodes} > hard cap {self.settings.hard_max_nodes}") + + job_id = self._job_id(req) + + if req.use_cached: + cached = await self.repo.get_analysis(job_id) + if cached is not None: + return JobRecord( + id=job_id, status=JobStatus.DONE, result=cached, + created_at=cached.created_at, updated_at=_now(), + ) + + job = JobRecord(id=job_id, status=JobStatus.PENDING, created_at=_now(), updated_at=_now()) + await self.repo.save_job(job) + + sync = req.wait and req.max_nodes <= self.settings.sync_max_nodes + if sync: + return await self._run_pipeline(job_id, req) + + task = asyncio.create_task(self._run_with_timeout(job_id, req)) + self._tasks.add(task) + task.add_done_callback(self._tasks.discard) + return job + + async def get(self, analysis_id: str) -> JobRecord | None: + job = await self.repo.get_job(analysis_id) + if job is not None: + return job + analysis = await self.repo.get_analysis(analysis_id) + if analysis is not None: + return JobRecord( + id=analysis_id, status=JobStatus.DONE, result=analysis, + created_at=analysis.created_at, updated_at=_now(), + ) + return None + + async def _run_with_timeout(self, job_id: str, req: AnalyzeRequest) -> JobRecord: + try: + return await asyncio.wait_for( + self._run_pipeline(job_id, req), timeout=self.settings.job_timeout_seconds + ) + except asyncio.TimeoutError: + return await self._fail(job_id, "computation_timeout", + "Graph too large for the exact engine within the job budget; aggregate (§4.5).") + + async def _set_stage(self, job_id: str, stage: str) -> None: + job = await self.repo.get_job(job_id) + if job is None: + return + await self.repo.save_job(job.model_copy(update={"status": JobStatus.RUNNING, "stage": stage, "updated_at": _now()})) + + async def _fail(self, job_id: str, code: str, message: str) -> JobRecord: + existing = await self.repo.get_job(job_id) + created = existing.created_at if existing else _now() + job = JobRecord(id=job_id, status=JobStatus.FAILED, error=JobError(code=code, message=message), + created_at=created, updated_at=_now()) + await self.repo.save_job(job) + return job + + async def _run_pipeline(self, job_id: str, req: AnalyzeRequest) -> JobRecord: + try: + await self._set_stage(job_id, "extracting") + graph = await self.client.extract(req.input, req.max_nodes) + + await self._set_stage(job_id, "validating") + graph, notes = await self.client.validate(graph, req.confidence_threshold) + + await self._set_stage(job_id, "computing") + analyses, risk_ranking, non_cascading = await self.client.analyze(graph) + if not risk_ranking and all(a.n_reachable == 0 for a in analyses): + raise UnreachableGraphError("graph has zero reachable nodes from any seed") + + await self._set_stage(job_id, "reporting") + report = await self.client.report(graph, analyses) + + result = AnalysisResult( + id=job_id, + graph=graph, + node_analyses=analyses, + risk_ranking=risk_ranking, + non_cascading_nodes=non_cascading, + executive_report=report, + validation_notes=notes, + provider_metadata={"mode": self.settings.mode, "engine": "zeroforce"}, + created_at=_now(), + ) + await self.repo.save_analysis(result) + job = JobRecord(id=job_id, status=JobStatus.DONE, result=result, + created_at=_now(), updated_at=_now()) + await self.repo.save_job(job) + return job + except NormalizationError as e: + return await self._fail(job_id, "normalization_failed", str(e)) + except UnreachableGraphError as e: + return await self._fail(job_id, "unreachable_graph", str(e)) + except Exception as e: # noqa: BLE001 - any pipeline failure becomes a failed job + return await self._fail(job_id, "llm_provider_error", f"{type(e).__name__}: {e}") diff --git a/services/backend/zeroforce_backend/prompts.py b/services/backend/zeroforce_backend/prompts.py new file mode 100644 index 0000000..4c318ba --- /dev/null +++ b/services/backend/zeroforce_backend/prompts.py @@ -0,0 +1,87 @@ +"""Prompt specifications (spec §8). Verbatim intent; light templating.""" + +from __future__ import annotations + +EXTRACTION_SYSTEM = """\ +You are a supply chain analyst with expertise in corporate procurement and SEC financial +disclosures. Given a natural language description of a company, product, or supply chain, +you extract a structured weighted directed dependency graph. + +You favor verifiable data from 10-K filings, supplier disclosures, and analyst reports over +general knowledge. You explicitly mark estimated weights as such. You never invent citations.""" + + +def extraction_user(user_input: str, max_nodes: int, grounding: str = "") -> str: + grounding_block = f"\n\nGrounded findings to use:\n{grounding}\n" if grounding else "" + return f"""\ +Build a supply chain dependency graph for: {user_input}{grounding_block} + +Constraints: +1. Identify 8 to 12 key nodes (companies or sub-sectors). Never exceed {max_nodes}. +2. For each supplier relationship A -> B, emit one directed edge with: + - weight: fraction of B's total inputs sourced from A (0..1) + - confidence: "high" (cited 10-K / disclosure), "medium" (analyst report), "low" (industry knowledge) + - citation: source document + page if available + - reasoning: one sentence +3. Incoming weights per node must sum to ~1.0 across edges with weight > 0.05. Smaller + suppliers should be aggregated into "Other {{sector}} suppliers". +4. Use web grounding aggressively. Cite real sources before estimating. +5. Mark the focal node (the company or product the user asked about). +6. Return only the structured Graph schema. No prose.""" + + +VALIDATION_USER_TEMPLATE = """\ +Review this extracted supply chain graph for accuracy and completeness: + +{graph_json} + +Check: +1. Does each node's incoming weight sum to ~1.0? List violations. +2. Are there obvious major suppliers missing (e.g. ASML for TSMC, Foxconn for Apple iPhone assembly)? +3. Are any weights implausibly high or low given the industry? +4. Are confidence levels appropriately conservative? + +Return the corrected graph in the same schema plus a "validation_notes" array describing +every change you made and why.""" + + +REPORT_SYSTEM = """\ +You are a supply chain risk consultant writing for a CFO or chief procurement officer. You +translate mathematical results into specific, actionable business recommendations. No math +jargon. No generic advice.""" + + +def report_user(focal_label: str, n_nodes: int, impact_ranked_json: str, top_3_context: str) -> str: + return f"""\ +Supply chain analyzed: {focal_label} +Total nodes: {n_nodes} +RZF results (ranked by impact = reachable nodes / EPT; higher = more dangerous): +{impact_ranked_json} +Raw EPT is shown alongside impact for context (lower EPT = faster local cascade). + +Top three risk nodes with their incoming-edge structure: +{top_3_context} + +Write a 3-paragraph executive risk report. + +Paragraph 1 — Critical finding. Name the single most dangerous supplier by impact score, +quote its EPT and how many nodes it reaches, and explain in plain English why this specific +node is the network's structural weak point. Reference its dependents. + +Paragraph 2 — Top three risk nodes ranked by impact. Specific numbers. For each, state what +business function it supports and the cascade implication. + +Paragraph 3 — Two or three actionable recommendations. Quantify the EPT improvement where +possible. Estimate cost band where reasonable.""" + + +def whatif_user(source, target, old_w, new_w, old_ept, new_ept, delta, affected) -> str: + return f"""\ +Given this what-if modification: +- Original edge: {source} -> {target} weight {old_w} +- New edge weight: {new_w} +- EPT for {target} changed from {old_ept} to {new_ept} ({delta} rounds) +- Most affected downstream nodes: {affected} + +Write one paragraph explaining what real-world action this models (diversification, vertical +integration, supplier consolidation), and whether the resulting resilience change is meaningful.""" diff --git a/services/backend/zeroforce_backend/reporter/__init__.py b/services/backend/zeroforce_backend/reporter/__init__.py new file mode 100644 index 0000000..9b9f3fa --- /dev/null +++ b/services/backend/zeroforce_backend/reporter/__init__.py @@ -0,0 +1,3 @@ +from .core import write_report + +__all__ = ["write_report"] diff --git a/services/backend/zeroforce_backend/reporter/core.py b/services/backend/zeroforce_backend/reporter/core.py new file mode 100644 index 0000000..30516f1 --- /dev/null +++ b/services/backend/zeroforce_backend/reporter/core.py @@ -0,0 +1,50 @@ +"""Reporter service core — Prompt 3 executive risk report (spec §8.3).""" + +from __future__ import annotations + +import json + +from zeroforce.types import Graph, NodeAnalysis +from zeroforce_providers import LLMProvider + +from ..prompts import REPORT_SYSTEM, report_user + + +async def write_report(graph: Graph, analyses: list[NodeAnalysis], llm: LLMProvider) -> str: + labels = {n.id: n.label for n in graph.nodes} + focal_label = labels.get(graph.focal_node, graph.focal_node) + + ranked = sorted( + (a for a in analyses if a.impact_score is not None), + key=lambda a: a.rank if a.rank is not None else 1_000_000, + ) + impact_json = json.dumps( + [ + { + "node": a.node_id, + "ept": round(a.ept, 3), + "n_reachable": a.n_reachable, + "impact_score": round(a.impact_score, 3) if a.impact_score is not None else None, + "rank": a.rank, + } + for a in ranked + ], + indent=2, + ) + + incoming = _incoming_by_target(graph) + top3 = "\n".join( + f"- {a.node_id} (impact {round(a.impact_score, 3)}, EPT {round(a.ept, 3)}, " + f"reaches {a.n_reachable}): fed by {incoming.get(a.node_id, '[source]')}" + for a in ranked[:3] + ) + + user = report_user(focal_label, len(graph.nodes), impact_json, top3) + return await llm.generate_text(REPORT_SYSTEM, user, temperature=0.4, max_output_tokens=1024) + + +def _incoming_by_target(graph: Graph) -> dict[str, str]: + out: dict[str, list[str]] = {} + for e in graph.edges: + out.setdefault(e.target, []).append(f"{e.source}({round(e.weight, 2)})") + return {k: ", ".join(v) for k, v in out.items()} diff --git a/services/backend/zeroforce_backend/service_client.py b/services/backend/zeroforce_backend/service_client.py new file mode 100644 index 0000000..ae16615 --- /dev/null +++ b/services/backend/zeroforce_backend/service_client.py @@ -0,0 +1,91 @@ +"""ServiceClient abstraction (architecture decision in PROGRESS.md). + +The orchestrator talks to the 5 worker services through this interface. Two impls: +- InProcessServiceClient: calls each service's core directly. Default; offline + testable; + used in single-process dev and all tests. +- HTTPServiceClient: calls each service's FastAPI app over HTTP. Used by docker-compose + (six containers) and prod (one Cloud Run service each). +""" + +from __future__ import annotations + +from typing import Protocol + +from zeroforce.types import Graph, NodeAnalysis +from zeroforce_providers import LLMProvider, SearchProvider + +from .engine import analyze_graph +from .extractor import extract_graph +from .reporter import write_report +from .validator import validate_and_normalize + + +class ServiceClient(Protocol): + async def extract(self, user_input: str, max_nodes: int) -> Graph: ... + async def validate(self, graph: Graph, confidence_threshold: str) -> tuple[Graph, list[str]]: ... + async def analyze(self, graph: Graph) -> tuple[list[NodeAnalysis], list[str], list[str]]: ... + async def report(self, graph: Graph, analyses: list[NodeAnalysis]) -> str: ... + + +class InProcessServiceClient: + def __init__(self, mode: str, llm: LLMProvider, search: SearchProvider): + self.mode = mode + self.llm = llm + self.search = search + + async def extract(self, user_input: str, max_nodes: int) -> Graph: + return await extract_graph( + user_input, mode=self.mode, max_nodes=max_nodes, llm=self.llm, search=self.search + ) + + async def validate(self, graph, confidence_threshold): + return validate_and_normalize(graph, confidence_threshold=confidence_threshold) + + async def analyze(self, graph): + return analyze_graph(graph) + + async def report(self, graph, analyses): + return await write_report(graph, analyses, self.llm) + + +class HTTPServiceClient: + """Calls per-service FastAPI apps. Used by docker-compose / prod (spec §5.1).""" + + def __init__(self, base_urls: dict[str, str], llm: LLMProvider, search: SearchProvider): + import httpx + + self._urls = base_urls + self._client = httpx.AsyncClient(timeout=60.0) + # extractor/reporter still need a provider handle on the service side; in HTTP mode + # each service builds its own from env. Held here only for parity of construction. + self.llm = llm + self.search = search + + async def _post(self, service: str, path: str, payload: dict) -> dict: + resp = await self._client.post(f"{self._urls[service]}{path}", json=payload) + resp.raise_for_status() + return resp.json() + + async def extract(self, user_input: str, max_nodes: int) -> Graph: + data = await self._post("extractor", "/extract", {"input": user_input, "max_nodes": max_nodes}) + return Graph.model_validate(data) + + async def validate(self, graph, confidence_threshold): + data = await self._post( + "validator", "/validate", + {"graph": graph.model_dump(mode="json"), "confidence_threshold": confidence_threshold}, + ) + return Graph.model_validate(data["graph"]), data["notes"] + + async def analyze(self, graph): + data = await self._post("engine", "/analyze", {"graph": graph.model_dump(mode="json")}) + analyses = [NodeAnalysis.model_validate(a) for a in data["node_analyses"]] + return analyses, data["risk_ranking"], data["non_cascading"] + + async def report(self, graph, analyses): + data = await self._post( + "reporter", "/report", + {"graph": graph.model_dump(mode="json"), + "analyses": [a.model_dump(mode="json") for a in analyses]}, + ) + return data["report"] diff --git a/services/backend/zeroforce_backend/storage.py b/services/backend/zeroforce_backend/storage.py new file mode 100644 index 0000000..b358c73 --- /dev/null +++ b/services/backend/zeroforce_backend/storage.py @@ -0,0 +1,126 @@ +"""Persistence (spec §5.3, §7.4). Repository abstraction; SQLite (dev) + in-memory (tests). + +Firestore (prod) implements the same protocol — added in Phase 2. +""" + +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +from typing import Protocol + +import aiosqlite + +from zeroforce_schemas import AnalysisResult, JobRecord + + +def content_hash(payload: dict) -> str: + blob = json.dumps(payload, sort_keys=True, default=str) + return hashlib.sha256(blob.encode()).hexdigest()[:32] + + +class Repository(Protocol): + async def init(self) -> None: ... + async def save_analysis(self, result: AnalysisResult) -> None: ... + async def get_analysis(self, analysis_id: str) -> AnalysisResult | None: ... + async def save_job(self, job: JobRecord) -> None: ... + async def get_job(self, job_id: str) -> JobRecord | None: ... + async def get_extraction(self, key: str) -> dict | None: ... + async def save_extraction(self, key: str, graph_json: dict) -> None: ... + + +class InMemoryRepository: + def __init__(self) -> None: + self._analyses: dict[str, AnalysisResult] = {} + self._jobs: dict[str, JobRecord] = {} + self._extractions: dict[str, dict] = {} + + async def init(self) -> None: + pass + + async def save_analysis(self, result: AnalysisResult) -> None: + self._analyses[result.id] = result + + async def get_analysis(self, analysis_id: str) -> AnalysisResult | None: + return self._analyses.get(analysis_id) + + async def save_job(self, job: JobRecord) -> None: + self._jobs[job.id] = job + + async def get_job(self, job_id: str) -> JobRecord | None: + return self._jobs.get(job_id) + + async def get_extraction(self, key: str) -> dict | None: + return self._extractions.get(key) + + async def save_extraction(self, key: str, graph_json: dict) -> None: + self._extractions[key] = graph_json + + +class SQLiteRepository: + def __init__(self, db_path: str) -> None: + self.db_path = db_path + if db_path != ":memory:": + Path(db_path).parent.mkdir(parents=True, exist_ok=True) + self._conn: aiosqlite.Connection | None = None + + async def _connect(self) -> aiosqlite.Connection: + if self._conn is None: + self._conn = await aiosqlite.connect(self.db_path) + return self._conn + + async def init(self) -> None: + conn = await self._connect() + await conn.executescript( + """ + CREATE TABLE IF NOT EXISTS analyses (id TEXT PRIMARY KEY, json TEXT, created_at TEXT); + CREATE TABLE IF NOT EXISTS jobs (id TEXT PRIMARY KEY, json TEXT, updated_at TEXT); + CREATE TABLE IF NOT EXISTS extractions (key TEXT PRIMARY KEY, json TEXT, created_at TEXT); + """ + ) + await conn.commit() + + async def save_analysis(self, result: AnalysisResult) -> None: + conn = await self._connect() + await conn.execute( + "INSERT OR REPLACE INTO analyses (id, json, created_at) VALUES (?, ?, ?)", + (result.id, result.model_dump_json(), result.created_at.isoformat()), + ) + await conn.commit() + + async def get_analysis(self, analysis_id: str) -> AnalysisResult | None: + conn = await self._connect() + async with conn.execute("SELECT json FROM analyses WHERE id = ?", (analysis_id,)) as cur: + row = await cur.fetchone() + return AnalysisResult.model_validate_json(row[0]) if row else None + + async def save_job(self, job: JobRecord) -> None: + conn = await self._connect() + await conn.execute( + "INSERT OR REPLACE INTO jobs (id, json, updated_at) VALUES (?, ?, ?)", + (job.id, job.model_dump_json(), job.updated_at.isoformat()), + ) + await conn.commit() + + async def get_job(self, job_id: str) -> JobRecord | None: + conn = await self._connect() + async with conn.execute("SELECT json FROM jobs WHERE id = ?", (job_id,)) as cur: + row = await cur.fetchone() + return JobRecord.model_validate_json(row[0]) if row else None + + async def get_extraction(self, key: str) -> dict | None: + conn = await self._connect() + async with conn.execute("SELECT json FROM extractions WHERE key = ?", (key,)) as cur: + row = await cur.fetchone() + return json.loads(row[0]) if row else None + + async def save_extraction(self, key: str, graph_json: dict) -> None: + conn = await self._connect() + from datetime import datetime, timezone + + await conn.execute( + "INSERT OR REPLACE INTO extractions (key, json, created_at) VALUES (?, ?, ?)", + (key, json.dumps(graph_json), datetime.now(timezone.utc).isoformat()), + ) + await conn.commit() diff --git a/services/backend/zeroforce_backend/storage_firestore.py b/services/backend/zeroforce_backend/storage_firestore.py new file mode 100644 index 0000000..a6c85d9 --- /dev/null +++ b/services/backend/zeroforce_backend/storage_firestore.py @@ -0,0 +1,50 @@ +"""Firestore repository (prod) — same Repository protocol as SQLite (spec §5.3, §7.4). + +Untested without GCP credentials. The Firestore client is synchronous, so calls are offloaded +to threads. Collections: analyses / jobs / extractions (spec §7.4). +""" + +from __future__ import annotations + +import asyncio +import json +from datetime import datetime, timezone + +from zeroforce_schemas import AnalysisResult, JobRecord + + +class FirestoreRepository: + def __init__(self, project: str | None = None): + from google.cloud import firestore # lazy: optional prod dep + + self._db = firestore.Client(project=project) + + async def init(self) -> None: + return None # Firestore is schemaless; nothing to create + + async def save_analysis(self, result: AnalysisResult) -> None: + await asyncio.to_thread( + self._db.collection("analyses").document(result.id).set, + json.loads(result.model_dump_json()), + ) + + async def get_analysis(self, analysis_id: str) -> AnalysisResult | None: + doc = await asyncio.to_thread(self._db.collection("analyses").document(analysis_id).get) + return AnalysisResult.model_validate(doc.to_dict()) if doc.exists else None + + async def save_job(self, job: JobRecord) -> None: + await asyncio.to_thread( + self._db.collection("jobs").document(job.id).set, json.loads(job.model_dump_json()) + ) + + async def get_job(self, job_id: str) -> JobRecord | None: + doc = await asyncio.to_thread(self._db.collection("jobs").document(job_id).get) + return JobRecord.model_validate(doc.to_dict()) if doc.exists else None + + async def get_extraction(self, key: str) -> dict | None: + doc = await asyncio.to_thread(self._db.collection("extractions").document(key).get) + return doc.to_dict() if doc.exists else None + + async def save_extraction(self, key: str, graph_json: dict) -> None: + payload = {"graph": graph_json, "created_at": datetime.now(timezone.utc).isoformat()} + await asyncio.to_thread(self._db.collection("extractions").document(key).set, payload) diff --git a/services/backend/zeroforce_backend/validator/__init__.py b/services/backend/zeroforce_backend/validator/__init__.py new file mode 100644 index 0000000..ffb1728 --- /dev/null +++ b/services/backend/zeroforce_backend/validator/__init__.py @@ -0,0 +1,3 @@ +from .core import NormalizationError, validate_and_normalize + +__all__ = ["NormalizationError", "validate_and_normalize"] diff --git a/services/backend/zeroforce_backend/validator/core.py b/services/backend/zeroforce_backend/validator/core.py new file mode 100644 index 0000000..5ebad8a --- /dev/null +++ b/services/backend/zeroforce_backend/validator/core.py @@ -0,0 +1,57 @@ +"""Validator service core — weight normalization invariant (spec §7.2, §3.3 step 5). + +Hard invariant: every node's incoming weights sum to 1.0 (sources exempt). After extraction, +before the engine. Returns a normalized copy of the graph plus human-readable notes. +""" + +from __future__ import annotations + +from collections import defaultdict + +from zeroforce.types import Edge, Graph + +_EPS = 1e-6 +_UPPER_REJECT = 1.05 + + +class NormalizationError(Exception): + """Incoming weights exceed the tolerated upper bound even before scaling (spec 422).""" + + +def validate_and_normalize(graph: Graph, confidence_threshold: str = "low") -> tuple[Graph, list[str]]: + notes: list[str] = [] + by_target: dict[str, list[Edge]] = defaultdict(list) + for e in graph.edges: + by_target[e.target].append(e) + + order = {"high": 3, "medium": 2, "low": 1} + thr = order.get(confidence_threshold, 1) + + new_edges: list[Edge] = [] + for e in graph.edges: + if order.get(e.confidence, 1) < thr: + notes.append( + f"Edge {e.source}->{e.target} confidence '{e.confidence}' below threshold " + f"'{confidence_threshold}' — flagged, retained." + ) + + for target, edges in by_target.items(): + total = sum(e.weight for e in edges) + if total <= 0: + continue + if total > _UPPER_REJECT: + raise NormalizationError( + f"Incoming weights for {target} sum to {total:.2f} (> {_UPPER_REJECT}). " + "Re-extraction with a corrective prompt is required." + ) + if abs(total - 1.0) > _EPS: + scale = 1.0 / total + for e in edges: + e.weight = min(1.0, max(0.0, e.weight * scale)) + notes.append( + f"Weights for {target} normalized from {total:.2f} to 1.0 — minor suppliers may be missing." + ) + + new_edges = list(graph.edges) + normalized = graph.model_copy(update={"edges": new_edges}) + return normalized, notes diff --git a/services/backend/zeroforce_backend/worker_apps.py b/services/backend/zeroforce_backend/worker_apps.py new file mode 100644 index 0000000..b47d0a3 --- /dev/null +++ b/services/backend/zeroforce_backend/worker_apps.py @@ -0,0 +1,100 @@ +"""Per-service FastAPI apps (spec §5.1: one Cloud Run service each). + +Each wraps a single service core over HTTP. In docker-compose / prod the gateway talks to +these via HTTPServiceClient; in single-process dev they are bypassed (InProcessServiceClient). +Run e.g.: `uvicorn zeroforce_backend.worker_apps:extractor_app --factory`. +""" + +from __future__ import annotations + +from fastapi import FastAPI + +from zeroforce.types import Graph, NodeAnalysis +from zeroforce_providers import make_llm, make_search + +from .config import load_settings +from .engine import analyze_graph +from .extractor import extract_graph +from .reporter import write_report +from .validator import NormalizationError, validate_and_normalize + + +def extractor_app() -> FastAPI: + app = FastAPI(title="zeroforce-extractor") + settings = load_settings() + llm = make_llm(settings.mode) # type: ignore[arg-type] + search = make_search(settings.mode) # type: ignore[arg-type] + + @app.get("/health") + async def health(): + return {"status": "ok", "service": "extractor"} + + @app.post("/extract") + async def extract(payload: dict): + graph = await extract_graph( + payload["input"], mode=settings.mode, max_nodes=payload.get("max_nodes", 12), + llm=llm, search=search, + ) + return graph.model_dump(mode="json") + + return app + + +def validator_app() -> FastAPI: + app = FastAPI(title="zeroforce-validator") + + @app.get("/health") + async def health(): + return {"status": "ok", "service": "validator"} + + @app.post("/validate") + async def validate(payload: dict): + from fastapi import HTTPException + + graph = Graph.model_validate(payload["graph"]) + try: + out, notes = validate_and_normalize(graph, payload.get("confidence_threshold", "low")) + except NormalizationError as e: + raise HTTPException(status_code=422, detail={"code": "normalization_failed", "message": str(e)}) + return {"graph": out.model_dump(mode="json"), "notes": notes} + + return app + + +def engine_app() -> FastAPI: + app = FastAPI(title="zeroforce-engine") + + @app.get("/health") + async def health(): + return {"status": "ok", "service": "engine"} + + @app.post("/analyze") + async def analyze(payload: dict): + graph = Graph.model_validate(payload["graph"]) + analyses, ranking, non_casc = analyze_graph(graph) + return { + "node_analyses": [a.model_dump(mode="json") for a in analyses], + "risk_ranking": ranking, + "non_cascading": non_casc, + } + + return app + + +def reporter_app() -> FastAPI: + app = FastAPI(title="zeroforce-reporter") + settings = load_settings() + llm = make_llm(settings.mode) # type: ignore[arg-type] + + @app.get("/health") + async def health(): + return {"status": "ok", "service": "reporter"} + + @app.post("/report") + async def report(payload: dict): + graph = Graph.model_validate(payload["graph"]) + analyses = [NodeAnalysis.model_validate(a) for a in payload["analyses"]] + text = await write_report(graph, analyses, llm) + return {"report": text} + + return app diff --git a/tests/load/analyze_load.js b/tests/load/analyze_load.js new file mode 100644 index 0000000..14d321e --- /dev/null +++ b/tests/load/analyze_load.js @@ -0,0 +1,65 @@ +// k6 load test for the async /analyze path (spec §11.4). +// Usage: +// k6 run -e BASE=http://localhost:8080 tests/load/analyze_load.js +// Scenarios: 10 RPS sustained, then a 100 RPS burst. Submits async, polls until done/failed. + +import http from "k6/http"; +import { check, sleep } from "k6"; + +const BASE = __ENV.BASE || "http://localhost:8080"; + +export const options = { + scenarios: { + sustained: { + executor: "constant-arrival-rate", + rate: 10, + timeUnit: "1s", + duration: "1m", + preAllocatedVUs: 20, + maxVUs: 100, + }, + burst: { + executor: "constant-arrival-rate", + rate: 100, + timeUnit: "1s", + duration: "20s", + startTime: "1m", + preAllocatedVUs: 100, + maxVUs: 400, + }, + }, + thresholds: { + http_req_failed: ["rate<0.05"], + http_req_duration: ["p(95)<2000"], + }, +}; + +const INPUTS = [ + "Apple semiconductor supply chain", + "Tesla battery supply chain", + "Pfizer vaccine cold chain", + "Boeing 787 airframe suppliers", +]; + +export default function () { + const input = INPUTS[Math.floor(Math.random() * INPUTS.length)]; + const res = http.post( + `${BASE}/api/v1/analyze`, + JSON.stringify({ input, max_nodes: 12 }), + { headers: { "Content-Type": "application/json" } }, + ); + check(res, { "accepted (202)": (r) => r.status === 202 || r.status === 200 }); + + if (res.status === 202) { + const { poll } = res.json(); + for (let i = 0; i < 20; i++) { + const s = http.get(`${BASE}${poll}`); + const status = s.json("status"); + if (status === "done" || status === "failed") { + check(s, { "completed": (r) => r.json("status") === "done" }); + break; + } + sleep(0.3); + } + } +} diff --git a/tests/parity/test_engine_golden.py b/tests/parity/test_engine_golden.py new file mode 100644 index 0000000..abe0e8a --- /dev/null +++ b/tests/parity/test_engine_golden.py @@ -0,0 +1,43 @@ +"""Engine golden + determinism (spec §11.2a). Same graph -> identical, stable EPT. + +The engine never touches an LLM, so EPT is exact and reproducible. This guards against +regressions in the engine or the backend wiring around it. +""" + +from __future__ import annotations + +import math +from pathlib import Path + +from zeroforce.types import Graph +from zeroforce_backend.engine import analyze_graph +from zeroforce_backend.validator import validate_and_normalize + +BEA = Path(__file__).resolve().parents[2] / "data" / "bea-15-sector.json" + +# Golden snapshot: the BEA bundle is a symmetric 15-sector ring, so every node has the same +# EPT over its (full) reachable set. Computed by the verified engine. +GOLDEN_BEA_EPT = 7.818324 +GOLDEN_BEA_IMPACT = 1.790665 + + +def _bea_analyses(): + g = Graph.model_validate_json(BEA.read_text()) + g, _ = validate_and_normalize(g) + return analyze_graph(g) + + +def test_bea_golden_ept_and_full_cascade(): + analyses, ranking, non_cascading = _bea_analyses() + assert len(ranking) == 15 # strongly-connected ring: every node cascades + assert non_cascading == [] + for a in analyses: + assert math.isclose(a.ept, GOLDEN_BEA_EPT, abs_tol=1e-5), f"{a.node_id} ept drifted: {a.ept}" + assert math.isclose(a.impact_score, GOLDEN_BEA_IMPACT, abs_tol=1e-5) + assert a.n_reachable == 14 + + +def test_engine_is_deterministic(): + first = {a.node_id: a.ept for a in _bea_analyses()[0]} + second = {a.node_id: a.ept for a in _bea_analyses()[0]} + assert first == second diff --git a/tests/parity/test_extraction_consistency.py b/tests/parity/test_extraction_consistency.py new file mode 100644 index 0000000..540644c --- /dev/null +++ b/tests/parity/test_extraction_consistency.py @@ -0,0 +1,37 @@ +"""Extraction consistency (spec §11.2b) — structure, NOT numbers. + +Two extractions of the same input should agree on STRUCTURE: node-set Jaccard >= 0.7 and +top-3 risk overlap >= 2. EPT is never compared (it is an engine property of a fixed graph, +not an extraction property; two LLMs produce different weights and EPT is a nonlinear DP). + +With the deterministic fake provider both runs are identical (Jaccard = 1). The thresholds +are what matter once live Groq/Tavily and Gemini stacks are wired with keys; this test +encodes the contract and exercises it against the deterministic stack. +""" + +from __future__ import annotations + +from zeroforce_backend.engine import analyze_graph +from zeroforce_backend.extractor import extract_graph +from zeroforce_providers import FakeLLMProvider, FakeSearchProvider + + +def _jaccard(a: set[str], b: set[str]) -> float: + return len(a & b) / len(a | b) if (a | b) else 1.0 + + +async def _extract_and_rank(text: str): + llm, search = FakeLLMProvider(), FakeSearchProvider() + graph = await extract_graph(text, mode="fake", max_nodes=12, llm=llm, search=search) + _analyses, ranking, _ = analyze_graph(graph) + return {n.id for n in graph.nodes}, ranking + + +async def test_node_set_jaccard_and_top3_overlap(): + text = "Apple semiconductor supply chain" + nodes_a, rank_a = await _extract_and_rank(text) + nodes_b, rank_b = await _extract_and_rank(text) + + assert _jaccard(nodes_a, nodes_b) >= 0.7 + overlap = len(set(rank_a[:3]) & set(rank_b[:3])) + assert overlap >= 2 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..66226b9 --- /dev/null +++ b/uv.lock @@ -0,0 +1,891 @@ +version = 1 +revision = 3 +requires-python = "==3.12.*" + +[manifest] +members = [ + "zeroforce", + "zeroforce-backend", + "zeroforce-monorepo", + "zeroforce-providers", + "zeroforce-schemas", +] + +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "fastapi" +version = "0.136.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, +] + +[[package]] +name = "google-auth" +version = "2.53.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/ad/ff781329bbbdc0974a098d996e89c9e1f7024262f9e3eec442fbb9ad1ac6/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c", size = 335844, upload-time = "2026-05-15T20:53:07.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/c9/db44165ba7c581268c6d46017ef63339110378305062830104fc7fa144cb/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68", size = 246071, upload-time = "2026-05-15T20:53:05.609Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/ec/6e49f50f5c70588d97c6ed25e0b8c18828bf4d58895f397b53a7522168a1/google_genai-2.6.0.tar.gz", hash = "sha256:7d4f777234002f2e94be499dbdfb43b506a6aca9dbbec13e61d3dc6ce640ffa7", size = 554809, upload-time = "2026-05-22T01:34:33.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/9e/e8ba4e58a9d5daf42343f3ea1cb0efb721eba36a1d6624e9873d039a5c1e/google_genai-2.6.0-py3-none-any.whl", hash = "sha256:272b6f6320f5d355735241ad441f972af095ec80dc10cb075cb430d96721648a", size = 821003, upload-time = "2026-05-22T01:34:31.55Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "hypothesis" +version = "6.152.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/fb/a5651eb0cd03ecee644e8f4d8cd2027b40a08bf1488f46201e794aebe9b5/hypothesis-6.152.9.tar.gz", hash = "sha256:de4711d69ce3a18009047c3b44882810fd0c0348c1558e920a4b0d2c45f59e1e", size = 472009, upload-time = "2026-05-19T17:10:19.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/d9/40ef7ed22f0c45c2316467edb9afb8fc172cd089cb329c0ee6da6b74416c/hypothesis-6.152.9-py3-none-any.whl", hash = "sha256:9c4fdccb1eac0b12ec740c12290d0e6a0bea3526a3f0bf812b7643bb563c2d8b", size = 538148, upload-time = "2026-05-19T17:10:16.131Z" }, +] + +[[package]] +name = "idna" +version = "3.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jiter" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, + { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767, upload-time = "2025-12-08T18:15:00.737Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176, upload-time = "2025-12-08T18:15:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630, upload-time = "2025-12-08T18:15:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numba" +version = "0.63.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666, upload-time = "2025-12-10T02:57:39.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/9c/c0974cd3d00ff70d30e8ff90522ba5fbb2bcee168a867d2321d8d0457676/numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71", size = 2680981, upload-time = "2025-12-10T02:57:17.579Z" }, + { url = "https://files.pythonhosted.org/packages/cb/70/ea2bc45205f206b7a24ee68a159f5097c9ca7e6466806e7c213587e0c2b1/numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f", size = 3801656, upload-time = "2025-12-10T02:57:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/4f4ba4fd0f99825cbf3cdefd682ca3678be1702b63362011de6e5f71f831/numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0", size = 3501857, upload-time = "2025-12-10T02:57:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/af/fd/6540456efa90b5f6604a86ff50dabefb187e43557e9081adcad3be44f048/numba-0.63.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbad8c63e4fc7eb3cdb2c2da52178e180419f7969f9a685f283b313a70b92af3", size = 2750282, upload-time = "2025-12-10T02:57:22.474Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, +] + +[[package]] +name = "openai" +version = "2.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/12/cfa322c5f5dd8fa21aab9a7a8e979e7a11123800f86ca8d82eb68a83d213/openai-2.38.0.tar.gz", hash = "sha256:798694c6cf74145541fda94325b6f8f72d8e1fd0262cc137c8d728177a6a4ce3", size = 772764, upload-time = "2026-05-21T21:23:42.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/bf/ccff9be562e24207716d04ef9dc931c76aff0c89a7265da43e2104d7fe06/openai-2.38.0-py3-none-any.whl", hash = "sha256:ec6661c57b2dcc47414a767e6e3335c7ed3d19c9696999283a3c82e95c756a3c", size = 1344910, upload-time = "2026-05-21T21:23:39.636Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pypdf" +version = "6.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/39e48f5294a5a6bb78313839aae820666c2d30daeadb652ed50bd46cafac/pypdf-6.12.1.tar.gz", hash = "sha256:3953a097b9f26d4e0ead5ff95943d9971377557662a91d8872186053cd71d30a", size = 6467595, upload-time = "2026-05-22T10:07:59.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/aa/4d17fe9ff165d1341878c570b14f5b7291106d63411adf640942a7e638aa/pypdf-6.12.1-py3-none-any.whl", hash = "sha256:8fa2a2321cf16247ed848bd7c97f193a60c08670d04abed5b0138327e51c43b0", size = 343787, upload-time = "2026-05-22T10:07:57.801Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.29" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" }, +] + +[[package]] +name = "regex" +version = "2026.5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "starlette" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/a3/84e821cc54b4ab50ae6dbc6ac3800a651b65ec35f045cc73785380654057/starlette-1.0.1.tar.gz", hash = "sha256:512399c5f1de7fac99c88572212ded9ddeddef2fb32afa82d724000e88b38f4f", size = 2659596, upload-time = "2026-05-21T21:58:58.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/e1/b2df4bc09a1e51ff664c1e17018a4274b42e5e9352e4a478ea540512dc88/starlette-1.0.1-py3-none-any.whl", hash = "sha256:7c0e69b2ee1c848bd54669d908500117a3ee13de603a21427e5c6fc1adf98dcd", size = 72802, upload-time = "2026-05-21T21:58:56.551Z" }, +] + +[[package]] +name = "tavily-python" +version = "0.7.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "requests" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/d3/a6a9c24bfafed30b4ce3c3d685ab00806ad631c9742441f2597ec91f0002/tavily_python-0.7.24.tar.gz", hash = "sha256:6c8954193c6472231e813fe50cbd07806bd86c7228957675eb45875a44d58296", size = 27311, upload-time = "2026-04-27T17:26:50.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/ce/37e3aba0f359f540bfc57eb178f73d521161761f21e0aa28749f42750b11/tavily_python-0.7.24-py3-none-any.whl", hash = "sha256:1a750108de42c4b0b46e4c1b7b64aeaf7fad7d7bac9167927edce0081fe166c9", size = 20022, upload-time = "2026-04-27T17:26:48.885Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, + { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "zeroforce" +version = "0.0.1" +source = { editable = "packages/zeroforce" } +dependencies = [ + { name = "networkx" }, + { name = "numba" }, + { name = "numpy" }, + { name = "pydantic" }, +] + +[package.optional-dependencies] +test = [ + { name = "hypothesis" }, + { name = "pytest" }, + { name = "pytest-xdist" }, +] + +[package.metadata] +requires-dist = [ + { name = "hypothesis", marker = "extra == 'test'", specifier = ">=6" }, + { name = "networkx", specifier = ">=3.2,<4.0" }, + { name = "numba", specifier = ">=0.61,<0.64" }, + { name = "numpy", specifier = ">=1.26,<3.0" }, + { name = "pydantic", specifier = ">=2.7,<3.0" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=8" }, + { name = "pytest-xdist", marker = "extra == 'test'" }, +] +provides-extras = ["test"] + +[[package]] +name = "zeroforce-backend" +version = "0.0.1" +source = { editable = "services/backend" } +dependencies = [ + { name = "aiosqlite" }, + { name = "fastapi" }, + { name = "numpy" }, + { name = "python-multipart" }, + { name = "uvicorn" }, + { name = "zeroforce" }, + { name = "zeroforce-providers" }, + { name = "zeroforce-schemas" }, +] + +[package.optional-dependencies] +pdf = [ + { name = "pypdf" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiosqlite", specifier = ">=0.20" }, + { name = "fastapi", specifier = ">=0.110" }, + { name = "numpy", specifier = ">=1.26,<3.0" }, + { name = "pypdf", marker = "extra == 'pdf'", specifier = ">=4.0" }, + { name = "python-multipart", specifier = ">=0.0.9" }, + { name = "uvicorn", specifier = ">=0.29" }, + { name = "zeroforce", editable = "packages/zeroforce" }, + { name = "zeroforce-providers", editable = "packages/providers" }, + { name = "zeroforce-schemas", editable = "packages/schemas" }, +] +provides-extras = ["pdf"] + +[[package]] +name = "zeroforce-monorepo" +version = "0.0.1" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "httpx" }, + { name = "hypothesis" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-xdist" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "httpx", specifier = ">=0.27" }, + { name = "hypothesis", specifier = ">=6" }, + { name = "pytest", specifier = ">=8" }, + { name = "pytest-asyncio", specifier = ">=0.24" }, + { name = "pytest-xdist" }, +] + +[[package]] +name = "zeroforce-providers" +version = "0.0.1" +source = { editable = "packages/providers" } +dependencies = [ + { name = "pydantic" }, + { name = "zeroforce" }, + { name = "zeroforce-schemas" }, +] + +[package.optional-dependencies] +dev = [ + { name = "openai" }, + { name = "tavily-python" }, +] +prod = [ + { name = "google-genai" }, +] + +[package.metadata] +requires-dist = [ + { name = "google-genai", marker = "extra == 'prod'", specifier = ">=0.3" }, + { name = "openai", marker = "extra == 'dev'", specifier = ">=1.40" }, + { name = "pydantic", specifier = ">=2.7,<3.0" }, + { name = "tavily-python", marker = "extra == 'dev'", specifier = ">=0.5" }, + { name = "zeroforce", editable = "packages/zeroforce" }, + { name = "zeroforce-schemas", editable = "packages/schemas" }, +] +provides-extras = ["dev", "prod"] + +[[package]] +name = "zeroforce-schemas" +version = "0.0.1" +source = { editable = "packages/schemas" } +dependencies = [ + { name = "pydantic" }, + { name = "zeroforce" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic", specifier = ">=2.7,<3.0" }, + { name = "zeroforce", editable = "packages/zeroforce" }, +]